UNPKG

851 kBJavaScriptView Raw
1/**
2 * @license
3 * Video.js 8.21.0 <http://videojs.com/>
4 * Copyright Brightcove, Inc. <https://www.brightcove.com/>
5 * Available under Apache License Version 2.0
6 * <https://github.com/videojs/video.js/blob/main/LICENSE>
7 *
8 * Includes vtt.js <https://github.com/mozilla/vtt.js>
9 * Available under Apache License Version 2.0
10 * <https://github.com/mozilla/vtt.js/blob/main/LICENSE>
11 */
12
13'use strict';
14
15var window = require('global/window');
16var document$1 = require('global/document');
17var XHR = require('@videojs/xhr');
18var vtt = require('videojs-vtt.js');
19
20function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
21
22var window__default = /*#__PURE__*/_interopDefaultLegacy(window);
23var document__default = /*#__PURE__*/_interopDefaultLegacy(document$1);
24var XHR__default = /*#__PURE__*/_interopDefaultLegacy(XHR);
25var vtt__default = /*#__PURE__*/_interopDefaultLegacy(vtt);
26
27var version = "8.21.0";
28
29/**
30 * An Object that contains lifecycle hooks as keys which point to an array
31 * of functions that are run when a lifecycle is triggered
32 *
33 * @private
34 */
35const hooks_ = {};
36
37/**
38 * Get a list of hooks for a specific lifecycle
39 *
40 * @param {string} type
41 * the lifecycle to get hooks from
42 *
43 * @param {Function|Function[]} [fn]
44 * Optionally add a hook (or hooks) to the lifecycle that your are getting.
45 *
46 * @return {Array}
47 * an array of hooks, or an empty array if there are none.
48 */
49const hooks = function (type, fn) {
50 hooks_[type] = hooks_[type] || [];
51 if (fn) {
52 hooks_[type] = hooks_[type].concat(fn);
53 }
54 return hooks_[type];
55};
56
57/**
58 * Add a function hook to a specific videojs lifecycle.
59 *
60 * @param {string} type
61 * the lifecycle to hook the function to.
62 *
63 * @param {Function|Function[]}
64 * The function or array of functions to attach.
65 */
66const hook = function (type, fn) {
67 hooks(type, fn);
68};
69
70/**
71 * Remove a hook from a specific videojs lifecycle.
72 *
73 * @param {string} type
74 * the lifecycle that the function hooked to
75 *
76 * @param {Function} fn
77 * The hooked function to remove
78 *
79 * @return {boolean}
80 * The function that was removed or undef
81 */
82const removeHook = function (type, fn) {
83 const index = hooks(type).indexOf(fn);
84 if (index <= -1) {
85 return false;
86 }
87 hooks_[type] = hooks_[type].slice();
88 hooks_[type].splice(index, 1);
89 return true;
90};
91
92/**
93 * Add a function hook that will only run once to a specific videojs lifecycle.
94 *
95 * @param {string} type
96 * the lifecycle to hook the function to.
97 *
98 * @param {Function|Function[]}
99 * The function or array of functions to attach.
100 */
101const hookOnce = function (type, fn) {
102 hooks(type, [].concat(fn).map(original => {
103 const wrapper = (...args) => {
104 removeHook(type, wrapper);
105 return original(...args);
106 };
107 return wrapper;
108 }));
109};
110
111/**
112 * @file fullscreen-api.js
113 * @module fullscreen-api
114 */
115
116/**
117 * Store the browser-specific methods for the fullscreen API.
118 *
119 * @type {Object}
120 * @see [Specification]{@link https://fullscreen.spec.whatwg.org}
121 * @see [Map Approach From Screenfull.js]{@link https://github.com/sindresorhus/screenfull.js}
122 */
123const FullscreenApi = {
124 prefixed: true
125};
126
127// browser API methods
128const apiMap = [['requestFullscreen', 'exitFullscreen', 'fullscreenElement', 'fullscreenEnabled', 'fullscreenchange', 'fullscreenerror', 'fullscreen'],
129// WebKit
130['webkitRequestFullscreen', 'webkitExitFullscreen', 'webkitFullscreenElement', 'webkitFullscreenEnabled', 'webkitfullscreenchange', 'webkitfullscreenerror', '-webkit-full-screen']];
131const specApi = apiMap[0];
132let browserApi;
133
134// determine the supported set of functions
135for (let i = 0; i < apiMap.length; i++) {
136 // check for exitFullscreen function
137 if (apiMap[i][1] in document__default["default"]) {
138 browserApi = apiMap[i];
139 break;
140 }
141}
142
143// map the browser API names to the spec API names
144if (browserApi) {
145 for (let i = 0; i < browserApi.length; i++) {
146 FullscreenApi[specApi[i]] = browserApi[i];
147 }
148 FullscreenApi.prefixed = browserApi[0] !== specApi[0];
149}
150
151/**
152 * @file create-logger.js
153 * @module create-logger
154 */
155
156// This is the private tracking variable for the logging history.
157let history = [];
158
159/**
160 * Log messages to the console and history based on the type of message
161 *
162 * @private
163 * @param {string} name
164 * The name of the console method to use.
165 *
166 * @param {Object} log
167 * The arguments to be passed to the matching console method.
168 *
169 * @param {string} [styles]
170 * styles for name
171 */
172const LogByTypeFactory = (name, log, styles) => (type, level, args) => {
173 const lvl = log.levels[level];
174 const lvlRegExp = new RegExp(`^(${lvl})$`);
175 let resultName = name;
176 if (type !== 'log') {
177 // Add the type to the front of the message when it's not "log".
178 args.unshift(type.toUpperCase() + ':');
179 }
180 if (styles) {
181 resultName = `%c${name}`;
182 args.unshift(styles);
183 }
184
185 // Add console prefix after adding to history.
186 args.unshift(resultName + ':');
187
188 // Add a clone of the args at this point to history.
189 if (history) {
190 history.push([].concat(args));
191
192 // only store 1000 history entries
193 const splice = history.length - 1000;
194 history.splice(0, splice > 0 ? splice : 0);
195 }
196
197 // If there's no console then don't try to output messages, but they will
198 // still be stored in history.
199 if (!window__default["default"].console) {
200 return;
201 }
202
203 // Was setting these once outside of this function, but containing them
204 // in the function makes it easier to test cases where console doesn't exist
205 // when the module is executed.
206 let fn = window__default["default"].console[type];
207 if (!fn && type === 'debug') {
208 // Certain browsers don't have support for console.debug. For those, we
209 // should default to the closest comparable log.
210 fn = window__default["default"].console.info || window__default["default"].console.log;
211 }
212
213 // Bail out if there's no console or if this type is not allowed by the
214 // current logging level.
215 if (!fn || !lvl || !lvlRegExp.test(type)) {
216 return;
217 }
218 fn[Array.isArray(args) ? 'apply' : 'call'](window__default["default"].console, args);
219};
220function createLogger$1(name, delimiter = ':', styles = '') {
221 // This is the private tracking variable for logging level.
222 let level = 'info';
223
224 // the curried logByType bound to the specific log and history
225 let logByType;
226
227 /**
228 * Logs plain debug messages. Similar to `console.log`.
229 *
230 * Due to [limitations](https://github.com/jsdoc3/jsdoc/issues/955#issuecomment-313829149)
231 * of our JSDoc template, we cannot properly document this as both a function
232 * and a namespace, so its function signature is documented here.
233 *
234 * #### Arguments
235 * ##### *args
236 * *[]
237 *
238 * Any combination of values that could be passed to `console.log()`.
239 *
240 * #### Return Value
241 *
242 * `undefined`
243 *
244 * @namespace
245 * @param {...*} args
246 * One or more messages or objects that should be logged.
247 */
248 function log(...args) {
249 logByType('log', level, args);
250 }
251
252 // This is the logByType helper that the logging methods below use
253 logByType = LogByTypeFactory(name, log, styles);
254
255 /**
256 * Create a new subLogger which chains the old name to the new name.
257 *
258 * For example, doing `mylogger = videojs.log.createLogger('player')` and then using that logger will log the following:
259 * ```js
260 * mylogger('foo');
261 * // > VIDEOJS: player: foo
262 * ```
263 *
264 * @param {string} subName
265 * The name to add call the new logger
266 * @param {string} [subDelimiter]
267 * Optional delimiter
268 * @param {string} [subStyles]
269 * Optional styles
270 * @return {Object}
271 */
272 log.createLogger = (subName, subDelimiter, subStyles) => {
273 const resultDelimiter = subDelimiter !== undefined ? subDelimiter : delimiter;
274 const resultStyles = subStyles !== undefined ? subStyles : styles;
275 const resultName = `${name} ${resultDelimiter} ${subName}`;
276 return createLogger$1(resultName, resultDelimiter, resultStyles);
277 };
278
279 /**
280 * Create a new logger.
281 *
282 * @param {string} newName
283 * The name for the new logger
284 * @param {string} [newDelimiter]
285 * Optional delimiter
286 * @param {string} [newStyles]
287 * Optional styles
288 * @return {Object}
289 */
290 log.createNewLogger = (newName, newDelimiter, newStyles) => {
291 return createLogger$1(newName, newDelimiter, newStyles);
292 };
293
294 /**
295 * Enumeration of available logging levels, where the keys are the level names
296 * and the values are `|`-separated strings containing logging methods allowed
297 * in that logging level. These strings are used to create a regular expression
298 * matching the function name being called.
299 *
300 * Levels provided by Video.js are:
301 *
302 * - `off`: Matches no calls. Any value that can be cast to `false` will have
303 * this effect. The most restrictive.
304 * - `all`: Matches only Video.js-provided functions (`debug`, `log`,
305 * `log.warn`, and `log.error`).
306 * - `debug`: Matches `log.debug`, `log`, `log.warn`, and `log.error` calls.
307 * - `info` (default): Matches `log`, `log.warn`, and `log.error` calls.
308 * - `warn`: Matches `log.warn` and `log.error` calls.
309 * - `error`: Matches only `log.error` calls.
310 *
311 * @type {Object}
312 */
313 log.levels = {
314 all: 'debug|log|warn|error',
315 off: '',
316 debug: 'debug|log|warn|error',
317 info: 'log|warn|error',
318 warn: 'warn|error',
319 error: 'error',
320 DEFAULT: level
321 };
322
323 /**
324 * Get or set the current logging level.
325 *
326 * If a string matching a key from {@link module:log.levels} is provided, acts
327 * as a setter.
328 *
329 * @param {'all'|'debug'|'info'|'warn'|'error'|'off'} [lvl]
330 * Pass a valid level to set a new logging level.
331 *
332 * @return {string}
333 * The current logging level.
334 */
335 log.level = lvl => {
336 if (typeof lvl === 'string') {
337 if (!log.levels.hasOwnProperty(lvl)) {
338 throw new Error(`"${lvl}" in not a valid log level`);
339 }
340 level = lvl;
341 }
342 return level;
343 };
344
345 /**
346 * Returns an array containing everything that has been logged to the history.
347 *
348 * This array is a shallow clone of the internal history record. However, its
349 * contents are _not_ cloned; so, mutating objects inside this array will
350 * mutate them in history.
351 *
352 * @return {Array}
353 */
354 log.history = () => history ? [].concat(history) : [];
355
356 /**
357 * Allows you to filter the history by the given logger name
358 *
359 * @param {string} fname
360 * The name to filter by
361 *
362 * @return {Array}
363 * The filtered list to return
364 */
365 log.history.filter = fname => {
366 return (history || []).filter(historyItem => {
367 // if the first item in each historyItem includes `fname`, then it's a match
368 return new RegExp(`.*${fname}.*`).test(historyItem[0]);
369 });
370 };
371
372 /**
373 * Clears the internal history tracking, but does not prevent further history
374 * tracking.
375 */
376 log.history.clear = () => {
377 if (history) {
378 history.length = 0;
379 }
380 };
381
382 /**
383 * Disable history tracking if it is currently enabled.
384 */
385 log.history.disable = () => {
386 if (history !== null) {
387 history.length = 0;
388 history = null;
389 }
390 };
391
392 /**
393 * Enable history tracking if it is currently disabled.
394 */
395 log.history.enable = () => {
396 if (history === null) {
397 history = [];
398 }
399 };
400
401 /**
402 * Logs error messages. Similar to `console.error`.
403 *
404 * @param {...*} args
405 * One or more messages or objects that should be logged as an error
406 */
407 log.error = (...args) => logByType('error', level, args);
408
409 /**
410 * Logs warning messages. Similar to `console.warn`.
411 *
412 * @param {...*} args
413 * One or more messages or objects that should be logged as a warning.
414 */
415 log.warn = (...args) => logByType('warn', level, args);
416
417 /**
418 * Logs debug messages. Similar to `console.debug`, but may also act as a comparable
419 * log if `console.debug` is not available
420 *
421 * @param {...*} args
422 * One or more messages or objects that should be logged as debug.
423 */
424 log.debug = (...args) => logByType('debug', level, args);
425 return log;
426}
427
428/**
429 * @file log.js
430 * @module log
431 */
432const log = createLogger$1('VIDEOJS');
433const createLogger = log.createLogger;
434
435/**
436 * @file obj.js
437 * @module obj
438 */
439
440/**
441 * @callback obj:EachCallback
442 *
443 * @param {*} value
444 * The current key for the object that is being iterated over.
445 *
446 * @param {string} key
447 * The current key-value for object that is being iterated over
448 */
449
450/**
451 * @callback obj:ReduceCallback
452 *
453 * @param {*} accum
454 * The value that is accumulating over the reduce loop.
455 *
456 * @param {*} value
457 * The current key for the object that is being iterated over.
458 *
459 * @param {string} key
460 * The current key-value for object that is being iterated over
461 *
462 * @return {*}
463 * The new accumulated value.
464 */
465const toString = Object.prototype.toString;
466
467/**
468 * Get the keys of an Object
469 *
470 * @param {Object}
471 * The Object to get the keys from
472 *
473 * @return {string[]}
474 * An array of the keys from the object. Returns an empty array if the
475 * object passed in was invalid or had no keys.
476 *
477 * @private
478 */
479const keys = function (object) {
480 return isObject(object) ? Object.keys(object) : [];
481};
482
483/**
484 * Array-like iteration for objects.
485 *
486 * @param {Object} object
487 * The object to iterate over
488 *
489 * @param {obj:EachCallback} fn
490 * The callback function which is called for each key in the object.
491 */
492function each(object, fn) {
493 keys(object).forEach(key => fn(object[key], key));
494}
495
496/**
497 * Array-like reduce for objects.
498 *
499 * @param {Object} object
500 * The Object that you want to reduce.
501 *
502 * @param {Function} fn
503 * A callback function which is called for each key in the object. It
504 * receives the accumulated value and the per-iteration value and key
505 * as arguments.
506 *
507 * @param {*} [initial = 0]
508 * Starting value
509 *
510 * @return {*}
511 * The final accumulated value.
512 */
513function reduce(object, fn, initial = 0) {
514 return keys(object).reduce((accum, key) => fn(accum, object[key], key), initial);
515}
516
517/**
518 * Returns whether a value is an object of any kind - including DOM nodes,
519 * arrays, regular expressions, etc. Not functions, though.
520 *
521 * This avoids the gotcha where using `typeof` on a `null` value
522 * results in `'object'`.
523 *
524 * @param {Object} value
525 * @return {boolean}
526 */
527function isObject(value) {
528 return !!value && typeof value === 'object';
529}
530
531/**
532 * Returns whether an object appears to be a "plain" object - that is, a
533 * direct instance of `Object`.
534 *
535 * @param {Object} value
536 * @return {boolean}
537 */
538function isPlain(value) {
539 return isObject(value) && toString.call(value) === '[object Object]' && value.constructor === Object;
540}
541
542/**
543 * Merge two objects recursively.
544 *
545 * Performs a deep merge like
546 * {@link https://lodash.com/docs/4.17.10#merge|lodash.merge}, but only merges
547 * plain objects (not arrays, elements, or anything else).
548 *
549 * Non-plain object values will be copied directly from the right-most
550 * argument.
551 *
552 * @param {Object[]} sources
553 * One or more objects to merge into a new object.
554 *
555 * @return {Object}
556 * A new object that is the merged result of all sources.
557 */
558function merge(...sources) {
559 const result = {};
560 sources.forEach(source => {
561 if (!source) {
562 return;
563 }
564 each(source, (value, key) => {
565 if (!isPlain(value)) {
566 result[key] = value;
567 return;
568 }
569 if (!isPlain(result[key])) {
570 result[key] = {};
571 }
572 result[key] = merge(result[key], value);
573 });
574 });
575 return result;
576}
577
578/**
579 * Returns an array of values for a given object
580 *
581 * @param {Object} source - target object
582 * @return {Array<unknown>} - object values
583 */
584function values(source = {}) {
585 const result = [];
586 for (const key in source) {
587 if (source.hasOwnProperty(key)) {
588 const value = source[key];
589 result.push(value);
590 }
591 }
592 return result;
593}
594
595/**
596 * Object.defineProperty but "lazy", which means that the value is only set after
597 * it is retrieved the first time, rather than being set right away.
598 *
599 * @param {Object} obj the object to set the property on
600 * @param {string} key the key for the property to set
601 * @param {Function} getValue the function used to get the value when it is needed.
602 * @param {boolean} setter whether a setter should be allowed or not
603 */
604function defineLazyProperty(obj, key, getValue, setter = true) {
605 const set = value => Object.defineProperty(obj, key, {
606 value,
607 enumerable: true,
608 writable: true
609 });
610 const options = {
611 configurable: true,
612 enumerable: true,
613 get() {
614 const value = getValue();
615 set(value);
616 return value;
617 }
618 };
619 if (setter) {
620 options.set = set;
621 }
622 return Object.defineProperty(obj, key, options);
623}
624
625var Obj = /*#__PURE__*/Object.freeze({
626 __proto__: null,
627 each: each,
628 reduce: reduce,
629 isObject: isObject,
630 isPlain: isPlain,
631 merge: merge,
632 values: values,
633 defineLazyProperty: defineLazyProperty
634});
635
636/**
637 * @file browser.js
638 * @module browser
639 */
640
641/**
642 * Whether or not this device is an iPod.
643 *
644 * @static
645 * @type {Boolean}
646 */
647let IS_IPOD = false;
648
649/**
650 * The detected iOS version - or `null`.
651 *
652 * @static
653 * @type {string|null}
654 */
655let IOS_VERSION = null;
656
657/**
658 * Whether or not this is an Android device.
659 *
660 * @static
661 * @type {Boolean}
662 */
663let IS_ANDROID = false;
664
665/**
666 * The detected Android version - or `null` if not Android or indeterminable.
667 *
668 * @static
669 * @type {number|string|null}
670 */
671let ANDROID_VERSION;
672
673/**
674 * Whether or not this is Mozilla Firefox.
675 *
676 * @static
677 * @type {Boolean}
678 */
679let IS_FIREFOX = false;
680
681/**
682 * Whether or not this is Microsoft Edge.
683 *
684 * @static
685 * @type {Boolean}
686 */
687let IS_EDGE = false;
688
689/**
690 * Whether or not this is any Chromium Browser
691 *
692 * @static
693 * @type {Boolean}
694 */
695let IS_CHROMIUM = false;
696
697/**
698 * Whether or not this is any Chromium browser that is not Edge.
699 *
700 * This will also be `true` for Chrome on iOS, which will have different support
701 * as it is actually Safari under the hood.
702 *
703 * Deprecated, as the behaviour to not match Edge was to prevent Legacy Edge's UA matching.
704 * IS_CHROMIUM should be used instead.
705 * "Chromium but not Edge" could be explicitly tested with IS_CHROMIUM && !IS_EDGE
706 *
707 * @static
708 * @deprecated
709 * @type {Boolean}
710 */
711let IS_CHROME = false;
712
713/**
714 * The detected Chromium version - or `null`.
715 *
716 * @static
717 * @type {number|null}
718 */
719let CHROMIUM_VERSION = null;
720
721/**
722 * The detected Google Chrome version - or `null`.
723 * This has always been the _Chromium_ version, i.e. would return on Chromium Edge.
724 * Deprecated, use CHROMIUM_VERSION instead.
725 *
726 * @static
727 * @deprecated
728 * @type {number|null}
729 */
730let CHROME_VERSION = null;
731
732/**
733 * Whether or not this is a Chromecast receiver application.
734 *
735 * @static
736 * @type {Boolean}
737 */
738const IS_CHROMECAST_RECEIVER = Boolean(window__default["default"].cast && window__default["default"].cast.framework && window__default["default"].cast.framework.CastReceiverContext);
739
740/**
741 * The detected Internet Explorer version - or `null`.
742 *
743 * @static
744 * @deprecated
745 * @type {number|null}
746 */
747let IE_VERSION = null;
748
749/**
750 * Whether or not this is desktop Safari.
751 *
752 * @static
753 * @type {Boolean}
754 */
755let IS_SAFARI = false;
756
757/**
758 * Whether or not this is a Windows machine.
759 *
760 * @static
761 * @type {Boolean}
762 */
763let IS_WINDOWS = false;
764
765/**
766 * Whether or not this device is an iPad.
767 *
768 * @static
769 * @type {Boolean}
770 */
771let IS_IPAD = false;
772
773/**
774 * Whether or not this device is an iPhone.
775 *
776 * @static
777 * @type {Boolean}
778 */
779// The Facebook app's UIWebView identifies as both an iPhone and iPad, so
780// to identify iPhones, we need to exclude iPads.
781// http://artsy.github.io/blog/2012/10/18/the-perils-of-ios-user-agent-sniffing/
782let IS_IPHONE = false;
783
784/**
785 * Whether or not this is a Tizen device.
786 *
787 * @static
788 * @type {Boolean}
789 */
790let IS_TIZEN = false;
791
792/**
793 * Whether or not this is a WebOS device.
794 *
795 * @static
796 * @type {Boolean}
797 */
798let IS_WEBOS = false;
799
800/**
801 * Whether or not this is a Smart TV (Tizen or WebOS) device.
802 *
803 * @static
804 * @type {Boolean}
805 */
806let IS_SMART_TV = false;
807
808/**
809 * Whether or not this device is touch-enabled.
810 *
811 * @static
812 * @const
813 * @type {Boolean}
814 */
815const TOUCH_ENABLED = Boolean(isReal() && ('ontouchstart' in window__default["default"] || window__default["default"].navigator.maxTouchPoints || window__default["default"].DocumentTouch && window__default["default"].document instanceof window__default["default"].DocumentTouch));
816const UAD = window__default["default"].navigator && window__default["default"].navigator.userAgentData;
817if (UAD && UAD.platform && UAD.brands) {
818 // If userAgentData is present, use it instead of userAgent to avoid warnings
819 // Currently only implemented on Chromium
820 // userAgentData does not expose Android version, so ANDROID_VERSION remains `null`
821
822 IS_ANDROID = UAD.platform === 'Android';
823 IS_EDGE = Boolean(UAD.brands.find(b => b.brand === 'Microsoft Edge'));
824 IS_CHROMIUM = Boolean(UAD.brands.find(b => b.brand === 'Chromium'));
825 IS_CHROME = !IS_EDGE && IS_CHROMIUM;
826 CHROMIUM_VERSION = CHROME_VERSION = (UAD.brands.find(b => b.brand === 'Chromium') || {}).version || null;
827 IS_WINDOWS = UAD.platform === 'Windows';
828}
829
830// If the browser is not Chromium, either userAgentData is not present which could be an old Chromium browser,
831// or it's a browser that has added userAgentData since that we don't have tests for yet. In either case,
832// the checks need to be made agiainst the regular userAgent string.
833if (!IS_CHROMIUM) {
834 const USER_AGENT = window__default["default"].navigator && window__default["default"].navigator.userAgent || '';
835 IS_IPOD = /iPod/i.test(USER_AGENT);
836 IOS_VERSION = function () {
837 const match = USER_AGENT.match(/OS (\d+)_/i);
838 if (match && match[1]) {
839 return match[1];
840 }
841 return null;
842 }();
843 IS_ANDROID = /Android/i.test(USER_AGENT);
844 ANDROID_VERSION = function () {
845 // This matches Android Major.Minor.Patch versions
846 // ANDROID_VERSION is Major.Minor as a Number, if Minor isn't available, then only Major is returned
847 const match = USER_AGENT.match(/Android (\d+)(?:\.(\d+))?(?:\.(\d+))*/i);
848 if (!match) {
849 return null;
850 }
851 const major = match[1] && parseFloat(match[1]);
852 const minor = match[2] && parseFloat(match[2]);
853 if (major && minor) {
854 return parseFloat(match[1] + '.' + match[2]);
855 } else if (major) {
856 return major;
857 }
858 return null;
859 }();
860 IS_FIREFOX = /Firefox/i.test(USER_AGENT);
861 IS_EDGE = /Edg/i.test(USER_AGENT);
862 IS_CHROMIUM = /Chrome/i.test(USER_AGENT) || /CriOS/i.test(USER_AGENT);
863 IS_CHROME = !IS_EDGE && IS_CHROMIUM;
864 CHROMIUM_VERSION = CHROME_VERSION = function () {
865 const match = USER_AGENT.match(/(Chrome|CriOS)\/(\d+)/);
866 if (match && match[2]) {
867 return parseFloat(match[2]);
868 }
869 return null;
870 }();
871 IE_VERSION = function () {
872 const result = /MSIE\s(\d+)\.\d/.exec(USER_AGENT);
873 let version = result && parseFloat(result[1]);
874 if (!version && /Trident\/7.0/i.test(USER_AGENT) && /rv:11.0/.test(USER_AGENT)) {
875 // IE 11 has a different user agent string than other IE versions
876 version = 11.0;
877 }
878 return version;
879 }();
880 IS_TIZEN = /Tizen/i.test(USER_AGENT);
881 IS_WEBOS = /Web0S/i.test(USER_AGENT);
882 IS_SMART_TV = IS_TIZEN || IS_WEBOS;
883 IS_SAFARI = /Safari/i.test(USER_AGENT) && !IS_CHROME && !IS_ANDROID && !IS_EDGE && !IS_SMART_TV;
884 IS_WINDOWS = /Windows/i.test(USER_AGENT);
885 IS_IPAD = /iPad/i.test(USER_AGENT) || IS_SAFARI && TOUCH_ENABLED && !/iPhone/i.test(USER_AGENT);
886 IS_IPHONE = /iPhone/i.test(USER_AGENT) && !IS_IPAD;
887}
888
889/**
890 * Whether or not this is an iOS device.
891 *
892 * @static
893 * @const
894 * @type {Boolean}
895 */
896const IS_IOS = IS_IPHONE || IS_IPAD || IS_IPOD;
897
898/**
899 * Whether or not this is any flavor of Safari - including iOS.
900 *
901 * @static
902 * @const
903 * @type {Boolean}
904 */
905const IS_ANY_SAFARI = (IS_SAFARI || IS_IOS) && !IS_CHROME;
906
907var browser = /*#__PURE__*/Object.freeze({
908 __proto__: null,
909 get IS_IPOD () { return IS_IPOD; },
910 get IOS_VERSION () { return IOS_VERSION; },
911 get IS_ANDROID () { return IS_ANDROID; },
912 get ANDROID_VERSION () { return ANDROID_VERSION; },
913 get IS_FIREFOX () { return IS_FIREFOX; },
914 get IS_EDGE () { return IS_EDGE; },
915 get IS_CHROMIUM () { return IS_CHROMIUM; },
916 get IS_CHROME () { return IS_CHROME; },
917 get CHROMIUM_VERSION () { return CHROMIUM_VERSION; },
918 get CHROME_VERSION () { return CHROME_VERSION; },
919 IS_CHROMECAST_RECEIVER: IS_CHROMECAST_RECEIVER,
920 get IE_VERSION () { return IE_VERSION; },
921 get IS_SAFARI () { return IS_SAFARI; },
922 get IS_WINDOWS () { return IS_WINDOWS; },
923 get IS_IPAD () { return IS_IPAD; },
924 get IS_IPHONE () { return IS_IPHONE; },
925 get IS_TIZEN () { return IS_TIZEN; },
926 get IS_WEBOS () { return IS_WEBOS; },
927 get IS_SMART_TV () { return IS_SMART_TV; },
928 TOUCH_ENABLED: TOUCH_ENABLED,
929 IS_IOS: IS_IOS,
930 IS_ANY_SAFARI: IS_ANY_SAFARI
931});
932
933/**
934 * @file dom.js
935 * @module dom
936 */
937
938/**
939 * Detect if a value is a string with any non-whitespace characters.
940 *
941 * @private
942 * @param {string} str
943 * The string to check
944 *
945 * @return {boolean}
946 * Will be `true` if the string is non-blank, `false` otherwise.
947 *
948 */
949function isNonBlankString(str) {
950 // we use str.trim as it will trim any whitespace characters
951 // from the front or back of non-whitespace characters. aka
952 // Any string that contains non-whitespace characters will
953 // still contain them after `trim` but whitespace only strings
954 // will have a length of 0, failing this check.
955 return typeof str === 'string' && Boolean(str.trim());
956}
957
958/**
959 * Throws an error if the passed string has whitespace. This is used by
960 * class methods to be relatively consistent with the classList API.
961 *
962 * @private
963 * @param {string} str
964 * The string to check for whitespace.
965 *
966 * @throws {Error}
967 * Throws an error if there is whitespace in the string.
968 */
969function throwIfWhitespace(str) {
970 // str.indexOf instead of regex because str.indexOf is faster performance wise.
971 if (str.indexOf(' ') >= 0) {
972 throw new Error('class has illegal whitespace characters');
973 }
974}
975
976/**
977 * Whether the current DOM interface appears to be real (i.e. not simulated).
978 *
979 * @return {boolean}
980 * Will be `true` if the DOM appears to be real, `false` otherwise.
981 */
982function isReal() {
983 // Both document and window will never be undefined thanks to `global`.
984 return document__default["default"] === window__default["default"].document;
985}
986
987/**
988 * Determines, via duck typing, whether or not a value is a DOM element.
989 *
990 * @param {*} value
991 * The value to check.
992 *
993 * @return {boolean}
994 * Will be `true` if the value is a DOM element, `false` otherwise.
995 */
996function isEl(value) {
997 return isObject(value) && value.nodeType === 1;
998}
999
1000/**
1001 * Determines if the current DOM is embedded in an iframe.
1002 *
1003 * @return {boolean}
1004 * Will be `true` if the DOM is embedded in an iframe, `false`
1005 * otherwise.
1006 */
1007function isInFrame() {
1008 // We need a try/catch here because Safari will throw errors when attempting
1009 // to get either `parent` or `self`
1010 try {
1011 return window__default["default"].parent !== window__default["default"].self;
1012 } catch (x) {
1013 return true;
1014 }
1015}
1016
1017/**
1018 * Creates functions to query the DOM using a given method.
1019 *
1020 * @private
1021 * @param {string} method
1022 * The method to create the query with.
1023 *
1024 * @return {Function}
1025 * The query method
1026 */
1027function createQuerier(method) {
1028 return function (selector, context) {
1029 if (!isNonBlankString(selector)) {
1030 return document__default["default"][method](null);
1031 }
1032 if (isNonBlankString(context)) {
1033 context = document__default["default"].querySelector(context);
1034 }
1035 const ctx = isEl(context) ? context : document__default["default"];
1036 return ctx[method] && ctx[method](selector);
1037 };
1038}
1039
1040/**
1041 * Creates an element and applies properties, attributes, and inserts content.
1042 *
1043 * @param {string} [tagName='div']
1044 * Name of tag to be created.
1045 *
1046 * @param {Object} [properties={}]
1047 * Element properties to be applied.
1048 *
1049 * @param {Object} [attributes={}]
1050 * Element attributes to be applied.
1051 *
1052 * @param {ContentDescriptor} [content]
1053 * A content descriptor object.
1054 *
1055 * @return {Element}
1056 * The element that was created.
1057 */
1058function createEl(tagName = 'div', properties = {}, attributes = {}, content) {
1059 const el = document__default["default"].createElement(tagName);
1060 Object.getOwnPropertyNames(properties).forEach(function (propName) {
1061 const val = properties[propName];
1062
1063 // Handle textContent since it's not supported everywhere and we have a
1064 // method for it.
1065 if (propName === 'textContent') {
1066 textContent(el, val);
1067 } else if (el[propName] !== val || propName === 'tabIndex') {
1068 el[propName] = val;
1069 }
1070 });
1071 Object.getOwnPropertyNames(attributes).forEach(function (attrName) {
1072 el.setAttribute(attrName, attributes[attrName]);
1073 });
1074 if (content) {
1075 appendContent(el, content);
1076 }
1077 return el;
1078}
1079
1080/**
1081 * Injects text into an element, replacing any existing contents entirely.
1082 *
1083 * @param {HTMLElement} el
1084 * The element to add text content into
1085 *
1086 * @param {string} text
1087 * The text content to add.
1088 *
1089 * @return {Element}
1090 * The element with added text content.
1091 */
1092function textContent(el, text) {
1093 if (typeof el.textContent === 'undefined') {
1094 el.innerText = text;
1095 } else {
1096 el.textContent = text;
1097 }
1098 return el;
1099}
1100
1101/**
1102 * Insert an element as the first child node of another
1103 *
1104 * @param {Element} child
1105 * Element to insert
1106 *
1107 * @param {Element} parent
1108 * Element to insert child into
1109 */
1110function prependTo(child, parent) {
1111 if (parent.firstChild) {
1112 parent.insertBefore(child, parent.firstChild);
1113 } else {
1114 parent.appendChild(child);
1115 }
1116}
1117
1118/**
1119 * Check if an element has a class name.
1120 *
1121 * @param {Element} element
1122 * Element to check
1123 *
1124 * @param {string} classToCheck
1125 * Class name to check for
1126 *
1127 * @return {boolean}
1128 * Will be `true` if the element has a class, `false` otherwise.
1129 *
1130 * @throws {Error}
1131 * Throws an error if `classToCheck` has white space.
1132 */
1133function hasClass(element, classToCheck) {
1134 throwIfWhitespace(classToCheck);
1135 return element.classList.contains(classToCheck);
1136}
1137
1138/**
1139 * Add a class name to an element.
1140 *
1141 * @param {Element} element
1142 * Element to add class name to.
1143 *
1144 * @param {...string} classesToAdd
1145 * One or more class name to add.
1146 *
1147 * @return {Element}
1148 * The DOM element with the added class name.
1149 */
1150function addClass(element, ...classesToAdd) {
1151 element.classList.add(...classesToAdd.reduce((prev, current) => prev.concat(current.split(/\s+/)), []));
1152 return element;
1153}
1154
1155/**
1156 * Remove a class name from an element.
1157 *
1158 * @param {Element} element
1159 * Element to remove a class name from.
1160 *
1161 * @param {...string} classesToRemove
1162 * One or more class name to remove.
1163 *
1164 * @return {Element}
1165 * The DOM element with class name removed.
1166 */
1167function removeClass(element, ...classesToRemove) {
1168 // Protect in case the player gets disposed
1169 if (!element) {
1170 log.warn("removeClass was called with an element that doesn't exist");
1171 return null;
1172 }
1173 element.classList.remove(...classesToRemove.reduce((prev, current) => prev.concat(current.split(/\s+/)), []));
1174 return element;
1175}
1176
1177/**
1178 * The callback definition for toggleClass.
1179 *
1180 * @callback PredicateCallback
1181 * @param {Element} element
1182 * The DOM element of the Component.
1183 *
1184 * @param {string} classToToggle
1185 * The `className` that wants to be toggled
1186 *
1187 * @return {boolean|undefined}
1188 * If `true` is returned, the `classToToggle` will be added to the
1189 * `element`, but not removed. If `false`, the `classToToggle` will be removed from
1190 * the `element`, but not added. If `undefined`, the callback will be ignored.
1191 *
1192 */
1193
1194/**
1195 * Adds or removes a class name to/from an element depending on an optional
1196 * condition or the presence/absence of the class name.
1197 *
1198 * @param {Element} element
1199 * The element to toggle a class name on.
1200 *
1201 * @param {string} classToToggle
1202 * The class that should be toggled.
1203 *
1204 * @param {boolean|PredicateCallback} [predicate]
1205 * See the return value for {@link module:dom~PredicateCallback}
1206 *
1207 * @return {Element}
1208 * The element with a class that has been toggled.
1209 */
1210function toggleClass(element, classToToggle, predicate) {
1211 if (typeof predicate === 'function') {
1212 predicate = predicate(element, classToToggle);
1213 }
1214 if (typeof predicate !== 'boolean') {
1215 predicate = undefined;
1216 }
1217 classToToggle.split(/\s+/).forEach(className => element.classList.toggle(className, predicate));
1218 return element;
1219}
1220
1221/**
1222 * Apply attributes to an HTML element.
1223 *
1224 * @param {Element} el
1225 * Element to add attributes to.
1226 *
1227 * @param {Object} [attributes]
1228 * Attributes to be applied.
1229 */
1230function setAttributes(el, attributes) {
1231 Object.getOwnPropertyNames(attributes).forEach(function (attrName) {
1232 const attrValue = attributes[attrName];
1233 if (attrValue === null || typeof attrValue === 'undefined' || attrValue === false) {
1234 el.removeAttribute(attrName);
1235 } else {
1236 el.setAttribute(attrName, attrValue === true ? '' : attrValue);
1237 }
1238 });
1239}
1240
1241/**
1242 * Get an element's attribute values, as defined on the HTML tag.
1243 *
1244 * Attributes are not the same as properties. They're defined on the tag
1245 * or with setAttribute.
1246 *
1247 * @param {Element} tag
1248 * Element from which to get tag attributes.
1249 *
1250 * @return {Object}
1251 * All attributes of the element. Boolean attributes will be `true` or
1252 * `false`, others will be strings.
1253 */
1254function getAttributes(tag) {
1255 const obj = {};
1256
1257 // known boolean attributes
1258 // we can check for matching boolean properties, but not all browsers
1259 // and not all tags know about these attributes, so, we still want to check them manually
1260 const knownBooleans = ['autoplay', 'controls', 'playsinline', 'loop', 'muted', 'default', 'defaultMuted'];
1261 if (tag && tag.attributes && tag.attributes.length > 0) {
1262 const attrs = tag.attributes;
1263 for (let i = attrs.length - 1; i >= 0; i--) {
1264 const attrName = attrs[i].name;
1265 /** @type {boolean|string} */
1266 let attrVal = attrs[i].value;
1267
1268 // check for known booleans
1269 // the matching element property will return a value for typeof
1270 if (knownBooleans.includes(attrName)) {
1271 // the value of an included boolean attribute is typically an empty
1272 // string ('') which would equal false if we just check for a false value.
1273 // we also don't want support bad code like autoplay='false'
1274 attrVal = attrVal !== null ? true : false;
1275 }
1276 obj[attrName] = attrVal;
1277 }
1278 }
1279 return obj;
1280}
1281
1282/**
1283 * Get the value of an element's attribute.
1284 *
1285 * @param {Element} el
1286 * A DOM element.
1287 *
1288 * @param {string} attribute
1289 * Attribute to get the value of.
1290 *
1291 * @return {string}
1292 * The value of the attribute.
1293 */
1294function getAttribute(el, attribute) {
1295 return el.getAttribute(attribute);
1296}
1297
1298/**
1299 * Set the value of an element's attribute.
1300 *
1301 * @param {Element} el
1302 * A DOM element.
1303 *
1304 * @param {string} attribute
1305 * Attribute to set.
1306 *
1307 * @param {string} value
1308 * Value to set the attribute to.
1309 */
1310function setAttribute(el, attribute, value) {
1311 el.setAttribute(attribute, value);
1312}
1313
1314/**
1315 * Remove an element's attribute.
1316 *
1317 * @param {Element} el
1318 * A DOM element.
1319 *
1320 * @param {string} attribute
1321 * Attribute to remove.
1322 */
1323function removeAttribute(el, attribute) {
1324 el.removeAttribute(attribute);
1325}
1326
1327/**
1328 * Attempt to block the ability to select text.
1329 */
1330function blockTextSelection() {
1331 document__default["default"].body.focus();
1332 document__default["default"].onselectstart = function () {
1333 return false;
1334 };
1335}
1336
1337/**
1338 * Turn off text selection blocking.
1339 */
1340function unblockTextSelection() {
1341 document__default["default"].onselectstart = function () {
1342 return true;
1343 };
1344}
1345
1346/**
1347 * Identical to the native `getBoundingClientRect` function, but ensures that
1348 * the method is supported at all (it is in all browsers we claim to support)
1349 * and that the element is in the DOM before continuing.
1350 *
1351 * This wrapper function also shims properties which are not provided by some
1352 * older browsers (namely, IE8).
1353 *
1354 * Additionally, some browsers do not support adding properties to a
1355 * `ClientRect`/`DOMRect` object; so, we shallow-copy it with the standard
1356 * properties (except `x` and `y` which are not widely supported). This helps
1357 * avoid implementations where keys are non-enumerable.
1358 *
1359 * @param {Element} el
1360 * Element whose `ClientRect` we want to calculate.
1361 *
1362 * @return {Object|undefined}
1363 * Always returns a plain object - or `undefined` if it cannot.
1364 */
1365function getBoundingClientRect(el) {
1366 if (el && el.getBoundingClientRect && el.parentNode) {
1367 const rect = el.getBoundingClientRect();
1368 const result = {};
1369 ['bottom', 'height', 'left', 'right', 'top', 'width'].forEach(k => {
1370 if (rect[k] !== undefined) {
1371 result[k] = rect[k];
1372 }
1373 });
1374 if (!result.height) {
1375 result.height = parseFloat(computedStyle(el, 'height'));
1376 }
1377 if (!result.width) {
1378 result.width = parseFloat(computedStyle(el, 'width'));
1379 }
1380 return result;
1381 }
1382}
1383
1384/**
1385 * Represents the position of a DOM element on the page.
1386 *
1387 * @typedef {Object} module:dom~Position
1388 *
1389 * @property {number} left
1390 * Pixels to the left.
1391 *
1392 * @property {number} top
1393 * Pixels from the top.
1394 */
1395
1396/**
1397 * Get the position of an element in the DOM.
1398 *
1399 * Uses `getBoundingClientRect` technique from John Resig.
1400 *
1401 * @see http://ejohn.org/blog/getboundingclientrect-is-awesome/
1402 *
1403 * @param {Element} el
1404 * Element from which to get offset.
1405 *
1406 * @return {module:dom~Position}
1407 * The position of the element that was passed in.
1408 */
1409function findPosition(el) {
1410 if (!el || el && !el.offsetParent) {
1411 return {
1412 left: 0,
1413 top: 0,
1414 width: 0,
1415 height: 0
1416 };
1417 }
1418 const width = el.offsetWidth;
1419 const height = el.offsetHeight;
1420 let left = 0;
1421 let top = 0;
1422 while (el.offsetParent && el !== document__default["default"][FullscreenApi.fullscreenElement]) {
1423 left += el.offsetLeft;
1424 top += el.offsetTop;
1425 el = el.offsetParent;
1426 }
1427 return {
1428 left,
1429 top,
1430 width,
1431 height
1432 };
1433}
1434
1435/**
1436 * Represents x and y coordinates for a DOM element or mouse pointer.
1437 *
1438 * @typedef {Object} module:dom~Coordinates
1439 *
1440 * @property {number} x
1441 * x coordinate in pixels
1442 *
1443 * @property {number} y
1444 * y coordinate in pixels
1445 */
1446
1447/**
1448 * Get the pointer position within an element.
1449 *
1450 * The base on the coordinates are the bottom left of the element.
1451 *
1452 * @param {Element} el
1453 * Element on which to get the pointer position on.
1454 *
1455 * @param {Event} event
1456 * Event object.
1457 *
1458 * @return {module:dom~Coordinates}
1459 * A coordinates object corresponding to the mouse position.
1460 *
1461 */
1462function getPointerPosition(el, event) {
1463 const translated = {
1464 x: 0,
1465 y: 0
1466 };
1467 if (IS_IOS) {
1468 let item = el;
1469 while (item && item.nodeName.toLowerCase() !== 'html') {
1470 const transform = computedStyle(item, 'transform');
1471 if (/^matrix/.test(transform)) {
1472 const values = transform.slice(7, -1).split(/,\s/).map(Number);
1473 translated.x += values[4];
1474 translated.y += values[5];
1475 } else if (/^matrix3d/.test(transform)) {
1476 const values = transform.slice(9, -1).split(/,\s/).map(Number);
1477 translated.x += values[12];
1478 translated.y += values[13];
1479 }
1480 if (item.assignedSlot && item.assignedSlot.parentElement && window__default["default"].WebKitCSSMatrix) {
1481 const transformValue = window__default["default"].getComputedStyle(item.assignedSlot.parentElement).transform;
1482 const matrix = new window__default["default"].WebKitCSSMatrix(transformValue);
1483 translated.x += matrix.m41;
1484 translated.y += matrix.m42;
1485 }
1486 item = item.parentNode || item.host;
1487 }
1488 }
1489 const position = {};
1490 const boxTarget = findPosition(event.target);
1491 const box = findPosition(el);
1492 const boxW = box.width;
1493 const boxH = box.height;
1494 let offsetY = event.offsetY - (box.top - boxTarget.top);
1495 let offsetX = event.offsetX - (box.left - boxTarget.left);
1496 if (event.changedTouches) {
1497 offsetX = event.changedTouches[0].pageX - box.left;
1498 offsetY = event.changedTouches[0].pageY + box.top;
1499 if (IS_IOS) {
1500 offsetX -= translated.x;
1501 offsetY -= translated.y;
1502 }
1503 }
1504 position.y = 1 - Math.max(0, Math.min(1, offsetY / boxH));
1505 position.x = Math.max(0, Math.min(1, offsetX / boxW));
1506 return position;
1507}
1508
1509/**
1510 * Determines, via duck typing, whether or not a value is a text node.
1511 *
1512 * @param {*} value
1513 * Check if this value is a text node.
1514 *
1515 * @return {boolean}
1516 * Will be `true` if the value is a text node, `false` otherwise.
1517 */
1518function isTextNode(value) {
1519 return isObject(value) && value.nodeType === 3;
1520}
1521
1522/**
1523 * Empties the contents of an element.
1524 *
1525 * @param {Element} el
1526 * The element to empty children from
1527 *
1528 * @return {Element}
1529 * The element with no children
1530 */
1531function emptyEl(el) {
1532 while (el.firstChild) {
1533 el.removeChild(el.firstChild);
1534 }
1535 return el;
1536}
1537
1538/**
1539 * This is a mixed value that describes content to be injected into the DOM
1540 * via some method. It can be of the following types:
1541 *
1542 * Type | Description
1543 * -----------|-------------
1544 * `string` | The value will be normalized into a text node.
1545 * `Element` | The value will be accepted as-is.
1546 * `Text` | A TextNode. The value will be accepted as-is.
1547 * `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).
1548 * `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.
1549 *
1550 * @typedef {string|Element|Text|Array|Function} ContentDescriptor
1551 */
1552
1553/**
1554 * Normalizes content for eventual insertion into the DOM.
1555 *
1556 * This allows a wide range of content definition methods, but helps protect
1557 * from falling into the trap of simply writing to `innerHTML`, which could
1558 * be an XSS concern.
1559 *
1560 * The content for an element can be passed in multiple types and
1561 * combinations, whose behavior is as follows:
1562 *
1563 * @param {ContentDescriptor} content
1564 * A content descriptor value.
1565 *
1566 * @return {Array}
1567 * All of the content that was passed in, normalized to an array of
1568 * elements or text nodes.
1569 */
1570function normalizeContent(content) {
1571 // First, invoke content if it is a function. If it produces an array,
1572 // that needs to happen before normalization.
1573 if (typeof content === 'function') {
1574 content = content();
1575 }
1576
1577 // Next up, normalize to an array, so one or many items can be normalized,
1578 // filtered, and returned.
1579 return (Array.isArray(content) ? content : [content]).map(value => {
1580 // First, invoke value if it is a function to produce a new value,
1581 // which will be subsequently normalized to a Node of some kind.
1582 if (typeof value === 'function') {
1583 value = value();
1584 }
1585 if (isEl(value) || isTextNode(value)) {
1586 return value;
1587 }
1588 if (typeof value === 'string' && /\S/.test(value)) {
1589 return document__default["default"].createTextNode(value);
1590 }
1591 }).filter(value => value);
1592}
1593
1594/**
1595 * Normalizes and appends content to an element.
1596 *
1597 * @param {Element} el
1598 * Element to append normalized content to.
1599 *
1600 * @param {ContentDescriptor} content
1601 * A content descriptor value.
1602 *
1603 * @return {Element}
1604 * The element with appended normalized content.
1605 */
1606function appendContent(el, content) {
1607 normalizeContent(content).forEach(node => el.appendChild(node));
1608 return el;
1609}
1610
1611/**
1612 * Normalizes and inserts content into an element; this is identical to
1613 * `appendContent()`, except it empties the element first.
1614 *
1615 * @param {Element} el
1616 * Element to insert normalized content into.
1617 *
1618 * @param {ContentDescriptor} content
1619 * A content descriptor value.
1620 *
1621 * @return {Element}
1622 * The element with inserted normalized content.
1623 */
1624function insertContent(el, content) {
1625 return appendContent(emptyEl(el), content);
1626}
1627
1628/**
1629 * Check if an event was a single left click.
1630 *
1631 * @param {MouseEvent} event
1632 * Event object.
1633 *
1634 * @return {boolean}
1635 * Will be `true` if a single left click, `false` otherwise.
1636 */
1637function isSingleLeftClick(event) {
1638 // Note: if you create something draggable, be sure to
1639 // call it on both `mousedown` and `mousemove` event,
1640 // otherwise `mousedown` should be enough for a button
1641
1642 if (event.button === undefined && event.buttons === undefined) {
1643 // Why do we need `buttons` ?
1644 // Because, middle mouse sometimes have this:
1645 // e.button === 0 and e.buttons === 4
1646 // Furthermore, we want to prevent combination click, something like
1647 // HOLD middlemouse then left click, that would be
1648 // e.button === 0, e.buttons === 5
1649 // just `button` is not gonna work
1650
1651 // Alright, then what this block does ?
1652 // this is for chrome `simulate mobile devices`
1653 // I want to support this as well
1654
1655 return true;
1656 }
1657 if (event.button === 0 && event.buttons === undefined) {
1658 // Touch screen, sometimes on some specific device, `buttons`
1659 // doesn't have anything (safari on ios, blackberry...)
1660
1661 return true;
1662 }
1663
1664 // `mouseup` event on a single left click has
1665 // `button` and `buttons` equal to 0
1666 if (event.type === 'mouseup' && event.button === 0 && event.buttons === 0) {
1667 return true;
1668 }
1669
1670 // MacOS Sonoma trackpad when "tap to click enabled"
1671 if (event.type === 'mousedown' && event.button === 0 && event.buttons === 0) {
1672 return true;
1673 }
1674 if (event.button !== 0 || event.buttons !== 1) {
1675 // This is the reason we have those if else block above
1676 // if any special case we can catch and let it slide
1677 // we do it above, when get to here, this definitely
1678 // is-not-left-click
1679
1680 return false;
1681 }
1682 return true;
1683}
1684
1685/**
1686 * Finds a single DOM element matching `selector` within the optional
1687 * `context` of another DOM element (defaulting to `document`).
1688 *
1689 * @param {string} selector
1690 * A valid CSS selector, which will be passed to `querySelector`.
1691 *
1692 * @param {Element|String} [context=document]
1693 * A DOM element within which to query. Can also be a selector
1694 * string in which case the first matching element will be used
1695 * as context. If missing (or no element matches selector), falls
1696 * back to `document`.
1697 *
1698 * @return {Element|null}
1699 * The element that was found or null.
1700 */
1701const $ = createQuerier('querySelector');
1702
1703/**
1704 * Finds a all DOM elements matching `selector` within the optional
1705 * `context` of another DOM element (defaulting to `document`).
1706 *
1707 * @param {string} selector
1708 * A valid CSS selector, which will be passed to `querySelectorAll`.
1709 *
1710 * @param {Element|String} [context=document]
1711 * A DOM element within which to query. Can also be a selector
1712 * string in which case the first matching element will be used
1713 * as context. If missing (or no element matches selector), falls
1714 * back to `document`.
1715 *
1716 * @return {NodeList}
1717 * A element list of elements that were found. Will be empty if none
1718 * were found.
1719 *
1720 */
1721const $$ = createQuerier('querySelectorAll');
1722
1723/**
1724 * A safe getComputedStyle.
1725 *
1726 * This is needed because in Firefox, if the player is loaded in an iframe with
1727 * `display:none`, then `getComputedStyle` returns `null`, so, we do a
1728 * null-check to make sure that the player doesn't break in these cases.
1729 *
1730 * @param {Element} el
1731 * The element you want the computed style of
1732 *
1733 * @param {string} prop
1734 * The property name you want
1735 *
1736 * @see https://bugzilla.mozilla.org/show_bug.cgi?id=548397
1737 */
1738function computedStyle(el, prop) {
1739 if (!el || !prop) {
1740 return '';
1741 }
1742 if (typeof window__default["default"].getComputedStyle === 'function') {
1743 let computedStyleValue;
1744 try {
1745 computedStyleValue = window__default["default"].getComputedStyle(el);
1746 } catch (e) {
1747 return '';
1748 }
1749 return computedStyleValue ? computedStyleValue.getPropertyValue(prop) || computedStyleValue[prop] : '';
1750 }
1751 return '';
1752}
1753
1754/**
1755 * Copy document style sheets to another window.
1756 *
1757 * @param {Window} win
1758 * The window element you want to copy the document style sheets to.
1759 *
1760 */
1761function copyStyleSheetsToWindow(win) {
1762 [...document__default["default"].styleSheets].forEach(styleSheet => {
1763 try {
1764 const cssRules = [...styleSheet.cssRules].map(rule => rule.cssText).join('');
1765 const style = document__default["default"].createElement('style');
1766 style.textContent = cssRules;
1767 win.document.head.appendChild(style);
1768 } catch (e) {
1769 const link = document__default["default"].createElement('link');
1770 link.rel = 'stylesheet';
1771 link.type = styleSheet.type;
1772 // For older Safari this has to be the string; on other browsers setting the MediaList works
1773 link.media = styleSheet.media.mediaText;
1774 link.href = styleSheet.href;
1775 win.document.head.appendChild(link);
1776 }
1777 });
1778}
1779
1780var Dom = /*#__PURE__*/Object.freeze({
1781 __proto__: null,
1782 isReal: isReal,
1783 isEl: isEl,
1784 isInFrame: isInFrame,
1785 createEl: createEl,
1786 textContent: textContent,
1787 prependTo: prependTo,
1788 hasClass: hasClass,
1789 addClass: addClass,
1790 removeClass: removeClass,
1791 toggleClass: toggleClass,
1792 setAttributes: setAttributes,
1793 getAttributes: getAttributes,
1794 getAttribute: getAttribute,
1795 setAttribute: setAttribute,
1796 removeAttribute: removeAttribute,
1797 blockTextSelection: blockTextSelection,
1798 unblockTextSelection: unblockTextSelection,
1799 getBoundingClientRect: getBoundingClientRect,
1800 findPosition: findPosition,
1801 getPointerPosition: getPointerPosition,
1802 isTextNode: isTextNode,
1803 emptyEl: emptyEl,
1804 normalizeContent: normalizeContent,
1805 appendContent: appendContent,
1806 insertContent: insertContent,
1807 isSingleLeftClick: isSingleLeftClick,
1808 $: $,
1809 $$: $$,
1810 computedStyle: computedStyle,
1811 copyStyleSheetsToWindow: copyStyleSheetsToWindow
1812});
1813
1814/**
1815 * @file setup.js - Functions for setting up a player without
1816 * user interaction based on the data-setup `attribute` of the video tag.
1817 *
1818 * @module setup
1819 */
1820let _windowLoaded = false;
1821let videojs$1;
1822
1823/**
1824 * Set up any tags that have a data-setup `attribute` when the player is started.
1825 */
1826const autoSetup = function () {
1827 if (videojs$1.options.autoSetup === false) {
1828 return;
1829 }
1830 const vids = Array.prototype.slice.call(document__default["default"].getElementsByTagName('video'));
1831 const audios = Array.prototype.slice.call(document__default["default"].getElementsByTagName('audio'));
1832 const divs = Array.prototype.slice.call(document__default["default"].getElementsByTagName('video-js'));
1833 const mediaEls = vids.concat(audios, divs);
1834
1835 // Check if any media elements exist
1836 if (mediaEls && mediaEls.length > 0) {
1837 for (let i = 0, e = mediaEls.length; i < e; i++) {
1838 const mediaEl = mediaEls[i];
1839
1840 // Check if element exists, has getAttribute func.
1841 if (mediaEl && mediaEl.getAttribute) {
1842 // Make sure this player hasn't already been set up.
1843 if (mediaEl.player === undefined) {
1844 const options = mediaEl.getAttribute('data-setup');
1845
1846 // Check if data-setup attr exists.
1847 // We only auto-setup if they've added the data-setup attr.
1848 if (options !== null) {
1849 // Create new video.js instance.
1850 videojs$1(mediaEl);
1851 }
1852 }
1853
1854 // If getAttribute isn't defined, we need to wait for the DOM.
1855 } else {
1856 autoSetupTimeout(1);
1857 break;
1858 }
1859 }
1860
1861 // No videos were found, so keep looping unless page is finished loading.
1862 } else if (!_windowLoaded) {
1863 autoSetupTimeout(1);
1864 }
1865};
1866
1867/**
1868 * Wait until the page is loaded before running autoSetup. This will be called in
1869 * autoSetup if `hasLoaded` returns false.
1870 *
1871 * @param {number} wait
1872 * How long to wait in ms
1873 *
1874 * @param {module:videojs} [vjs]
1875 * The videojs library function
1876 */
1877function autoSetupTimeout(wait, vjs) {
1878 // Protect against breakage in non-browser environments
1879 if (!isReal()) {
1880 return;
1881 }
1882 if (vjs) {
1883 videojs$1 = vjs;
1884 }
1885 window__default["default"].setTimeout(autoSetup, wait);
1886}
1887
1888/**
1889 * Used to set the internal tracking of window loaded state to true.
1890 *
1891 * @private
1892 */
1893function setWindowLoaded() {
1894 _windowLoaded = true;
1895 window__default["default"].removeEventListener('load', setWindowLoaded);
1896}
1897if (isReal()) {
1898 if (document__default["default"].readyState === 'complete') {
1899 setWindowLoaded();
1900 } else {
1901 /**
1902 * Listen for the load event on window, and set _windowLoaded to true.
1903 *
1904 * We use a standard event listener here to avoid incrementing the GUID
1905 * before any players are created.
1906 *
1907 * @listens load
1908 */
1909 window__default["default"].addEventListener('load', setWindowLoaded);
1910 }
1911}
1912
1913/**
1914 * @file stylesheet.js
1915 * @module stylesheet
1916 */
1917
1918/**
1919 * Create a DOM style element given a className for it.
1920 *
1921 * @param {string} className
1922 * The className to add to the created style element.
1923 *
1924 * @return {Element}
1925 * The element that was created.
1926 */
1927const createStyleElement = function (className) {
1928 const style = document__default["default"].createElement('style');
1929 style.className = className;
1930 return style;
1931};
1932
1933/**
1934 * Add text to a DOM element.
1935 *
1936 * @param {Element} el
1937 * The Element to add text content to.
1938 *
1939 * @param {string} content
1940 * The text to add to the element.
1941 */
1942const setTextContent = function (el, content) {
1943 if (el.styleSheet) {
1944 el.styleSheet.cssText = content;
1945 } else {
1946 el.textContent = content;
1947 }
1948};
1949
1950/**
1951 * @file dom-data.js
1952 * @module dom-data
1953 */
1954
1955/**
1956 * Element Data Store.
1957 *
1958 * Allows for binding data to an element without putting it directly on the
1959 * element. Ex. Event listeners are stored here.
1960 * (also from jsninja.com, slightly modified and updated for closure compiler)
1961 *
1962 * @type {Object}
1963 * @private
1964 */
1965var DomData = new WeakMap();
1966
1967/**
1968 * @file guid.js
1969 * @module guid
1970 */
1971
1972// Default value for GUIDs. This allows us to reset the GUID counter in tests.
1973//
1974// The initial GUID is 3 because some users have come to rely on the first
1975// default player ID ending up as `vjs_video_3`.
1976//
1977// See: https://github.com/videojs/video.js/pull/6216
1978const _initialGuid = 3;
1979
1980/**
1981 * Unique ID for an element or function
1982 *
1983 * @type {Number}
1984 */
1985let _guid = _initialGuid;
1986
1987/**
1988 * Get a unique auto-incrementing ID by number that has not been returned before.
1989 *
1990 * @return {number}
1991 * A new unique ID.
1992 */
1993function newGUID() {
1994 return _guid++;
1995}
1996
1997/**
1998 * @file events.js. An Event System (John Resig - Secrets of a JS Ninja http://jsninja.com/)
1999 * (Original book version wasn't completely usable, so fixed some things and made Closure Compiler compatible)
2000 * This should work very similarly to jQuery's events, however it's based off the book version which isn't as
2001 * robust as jquery's, so there's probably some differences.
2002 *
2003 * @file events.js
2004 * @module events
2005 */
2006
2007/**
2008 * Clean up the listener cache and dispatchers
2009 *
2010 * @param {Element|Object} elem
2011 * Element to clean up
2012 *
2013 * @param {string} type
2014 * Type of event to clean up
2015 */
2016function _cleanUpEvents(elem, type) {
2017 if (!DomData.has(elem)) {
2018 return;
2019 }
2020 const data = DomData.get(elem);
2021
2022 // Remove the events of a particular type if there are none left
2023 if (data.handlers[type].length === 0) {
2024 delete data.handlers[type];
2025 // data.handlers[type] = null;
2026 // Setting to null was causing an error with data.handlers
2027
2028 // Remove the meta-handler from the element
2029 if (elem.removeEventListener) {
2030 elem.removeEventListener(type, data.dispatcher, false);
2031 } else if (elem.detachEvent) {
2032 elem.detachEvent('on' + type, data.dispatcher);
2033 }
2034 }
2035
2036 // Remove the events object if there are no types left
2037 if (Object.getOwnPropertyNames(data.handlers).length <= 0) {
2038 delete data.handlers;
2039 delete data.dispatcher;
2040 delete data.disabled;
2041 }
2042
2043 // Finally remove the element data if there is no data left
2044 if (Object.getOwnPropertyNames(data).length === 0) {
2045 DomData.delete(elem);
2046 }
2047}
2048
2049/**
2050 * Loops through an array of event types and calls the requested method for each type.
2051 *
2052 * @param {Function} fn
2053 * The event method we want to use.
2054 *
2055 * @param {Element|Object} elem
2056 * Element or object to bind listeners to
2057 *
2058 * @param {string[]} types
2059 * Type of event to bind to.
2060 *
2061 * @param {Function} callback
2062 * Event listener.
2063 */
2064function _handleMultipleEvents(fn, elem, types, callback) {
2065 types.forEach(function (type) {
2066 // Call the event method for each one of the types
2067 fn(elem, type, callback);
2068 });
2069}
2070
2071/**
2072 * Fix a native event to have standard property values
2073 *
2074 * @param {Object} event
2075 * Event object to fix.
2076 *
2077 * @return {Object}
2078 * Fixed event object.
2079 */
2080function fixEvent(event) {
2081 if (event.fixed_) {
2082 return event;
2083 }
2084 function returnTrue() {
2085 return true;
2086 }
2087 function returnFalse() {
2088 return false;
2089 }
2090
2091 // Test if fixing up is needed
2092 // Used to check if !event.stopPropagation instead of isPropagationStopped
2093 // But native events return true for stopPropagation, but don't have
2094 // other expected methods like isPropagationStopped. Seems to be a problem
2095 // with the Javascript Ninja code. So we're just overriding all events now.
2096 if (!event || !event.isPropagationStopped || !event.isImmediatePropagationStopped) {
2097 const old = event || window__default["default"].event;
2098 event = {};
2099 // Clone the old object so that we can modify the values event = {};
2100 // IE8 Doesn't like when you mess with native event properties
2101 // Firefox returns false for event.hasOwnProperty('type') and other props
2102 // which makes copying more difficult.
2103
2104 // TODO: Probably best to create an allowlist of event props
2105 const deprecatedProps = ['layerX', 'layerY', 'keyLocation', 'path', 'webkitMovementX', 'webkitMovementY', 'mozPressure', 'mozInputSource'];
2106 for (const key in old) {
2107 // Safari 6.0.3 warns you if you try to copy deprecated layerX/Y
2108 // Chrome warns you if you try to copy deprecated keyboardEvent.keyLocation
2109 // and webkitMovementX/Y
2110 // Lighthouse complains if Event.path is copied
2111 if (!deprecatedProps.includes(key)) {
2112 // Chrome 32+ warns if you try to copy deprecated returnValue, but
2113 // we still want to if preventDefault isn't supported (IE8).
2114 if (!(key === 'returnValue' && old.preventDefault)) {
2115 event[key] = old[key];
2116 }
2117 }
2118 }
2119
2120 // The event occurred on this element
2121 if (!event.target) {
2122 event.target = event.srcElement || document__default["default"];
2123 }
2124
2125 // Handle which other element the event is related to
2126 if (!event.relatedTarget) {
2127 event.relatedTarget = event.fromElement === event.target ? event.toElement : event.fromElement;
2128 }
2129
2130 // Stop the default browser action
2131 event.preventDefault = function () {
2132 if (old.preventDefault) {
2133 old.preventDefault();
2134 }
2135 event.returnValue = false;
2136 old.returnValue = false;
2137 event.defaultPrevented = true;
2138 };
2139 event.defaultPrevented = false;
2140
2141 // Stop the event from bubbling
2142 event.stopPropagation = function () {
2143 if (old.stopPropagation) {
2144 old.stopPropagation();
2145 }
2146 event.cancelBubble = true;
2147 old.cancelBubble = true;
2148 event.isPropagationStopped = returnTrue;
2149 };
2150 event.isPropagationStopped = returnFalse;
2151
2152 // Stop the event from bubbling and executing other handlers
2153 event.stopImmediatePropagation = function () {
2154 if (old.stopImmediatePropagation) {
2155 old.stopImmediatePropagation();
2156 }
2157 event.isImmediatePropagationStopped = returnTrue;
2158 event.stopPropagation();
2159 };
2160 event.isImmediatePropagationStopped = returnFalse;
2161
2162 // Handle mouse position
2163 if (event.clientX !== null && event.clientX !== undefined) {
2164 const doc = document__default["default"].documentElement;
2165 const body = document__default["default"].body;
2166 event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0);
2167 event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0);
2168 }
2169
2170 // Handle key presses
2171 event.which = event.charCode || event.keyCode;
2172
2173 // Fix button for mouse clicks:
2174 // 0 == left; 1 == middle; 2 == right
2175 if (event.button !== null && event.button !== undefined) {
2176 // The following is disabled because it does not pass videojs-standard
2177 // and... yikes.
2178 /* eslint-disable */
2179 event.button = event.button & 1 ? 0 : event.button & 4 ? 1 : event.button & 2 ? 2 : 0;
2180 /* eslint-enable */
2181 }
2182 }
2183 event.fixed_ = true;
2184 // Returns fixed-up instance
2185 return event;
2186}
2187
2188/**
2189 * Whether passive event listeners are supported
2190 */
2191let _supportsPassive;
2192const supportsPassive = function () {
2193 if (typeof _supportsPassive !== 'boolean') {
2194 _supportsPassive = false;
2195 try {
2196 const opts = Object.defineProperty({}, 'passive', {
2197 get() {
2198 _supportsPassive = true;
2199 }
2200 });
2201 window__default["default"].addEventListener('test', null, opts);
2202 window__default["default"].removeEventListener('test', null, opts);
2203 } catch (e) {
2204 // disregard
2205 }
2206 }
2207 return _supportsPassive;
2208};
2209
2210/**
2211 * Touch events Chrome expects to be passive
2212 */
2213const passiveEvents = ['touchstart', 'touchmove'];
2214
2215/**
2216 * Add an event listener to element
2217 * It stores the handler function in a separate cache object
2218 * and adds a generic handler to the element's event,
2219 * along with a unique id (guid) to the element.
2220 *
2221 * @param {Element|Object} elem
2222 * Element or object to bind listeners to
2223 *
2224 * @param {string|string[]} type
2225 * Type of event to bind to.
2226 *
2227 * @param {Function} fn
2228 * Event listener.
2229 */
2230function on(elem, type, fn) {
2231 if (Array.isArray(type)) {
2232 return _handleMultipleEvents(on, elem, type, fn);
2233 }
2234 if (!DomData.has(elem)) {
2235 DomData.set(elem, {});
2236 }
2237 const data = DomData.get(elem);
2238
2239 // We need a place to store all our handler data
2240 if (!data.handlers) {
2241 data.handlers = {};
2242 }
2243 if (!data.handlers[type]) {
2244 data.handlers[type] = [];
2245 }
2246 if (!fn.guid) {
2247 fn.guid = newGUID();
2248 }
2249 data.handlers[type].push(fn);
2250 if (!data.dispatcher) {
2251 data.disabled = false;
2252 data.dispatcher = function (event, hash) {
2253 if (data.disabled) {
2254 return;
2255 }
2256 event = fixEvent(event);
2257 const handlers = data.handlers[event.type];
2258 if (handlers) {
2259 // Copy handlers so if handlers are added/removed during the process it doesn't throw everything off.
2260 const handlersCopy = handlers.slice(0);
2261 for (let m = 0, n = handlersCopy.length; m < n; m++) {
2262 if (event.isImmediatePropagationStopped()) {
2263 break;
2264 } else {
2265 try {
2266 handlersCopy[m].call(elem, event, hash);
2267 } catch (e) {
2268 log.error(e);
2269 }
2270 }
2271 }
2272 }
2273 };
2274 }
2275 if (data.handlers[type].length === 1) {
2276 if (elem.addEventListener) {
2277 let options = false;
2278 if (supportsPassive() && passiveEvents.indexOf(type) > -1) {
2279 options = {
2280 passive: true
2281 };
2282 }
2283 elem.addEventListener(type, data.dispatcher, options);
2284 } else if (elem.attachEvent) {
2285 elem.attachEvent('on' + type, data.dispatcher);
2286 }
2287 }
2288}
2289
2290/**
2291 * Removes event listeners from an element
2292 *
2293 * @param {Element|Object} elem
2294 * Object to remove listeners from.
2295 *
2296 * @param {string|string[]} [type]
2297 * Type of listener to remove. Don't include to remove all events from element.
2298 *
2299 * @param {Function} [fn]
2300 * Specific listener to remove. Don't include to remove listeners for an event
2301 * type.
2302 */
2303function off(elem, type, fn) {
2304 // Don't want to add a cache object through getElData if not needed
2305 if (!DomData.has(elem)) {
2306 return;
2307 }
2308 const data = DomData.get(elem);
2309
2310 // If no events exist, nothing to unbind
2311 if (!data.handlers) {
2312 return;
2313 }
2314 if (Array.isArray(type)) {
2315 return _handleMultipleEvents(off, elem, type, fn);
2316 }
2317
2318 // Utility function
2319 const removeType = function (el, t) {
2320 data.handlers[t] = [];
2321 _cleanUpEvents(el, t);
2322 };
2323
2324 // Are we removing all bound events?
2325 if (type === undefined) {
2326 for (const t in data.handlers) {
2327 if (Object.prototype.hasOwnProperty.call(data.handlers || {}, t)) {
2328 removeType(elem, t);
2329 }
2330 }
2331 return;
2332 }
2333 const handlers = data.handlers[type];
2334
2335 // If no handlers exist, nothing to unbind
2336 if (!handlers) {
2337 return;
2338 }
2339
2340 // If no listener was provided, remove all listeners for type
2341 if (!fn) {
2342 removeType(elem, type);
2343 return;
2344 }
2345
2346 // We're only removing a single handler
2347 if (fn.guid) {
2348 for (let n = 0; n < handlers.length; n++) {
2349 if (handlers[n].guid === fn.guid) {
2350 handlers.splice(n--, 1);
2351 }
2352 }
2353 }
2354 _cleanUpEvents(elem, type);
2355}
2356
2357/**
2358 * Trigger an event for an element
2359 *
2360 * @param {Element|Object} elem
2361 * Element to trigger an event on
2362 *
2363 * @param {EventTarget~Event|string} event
2364 * A string (the type) or an event object with a type attribute
2365 *
2366 * @param {Object} [hash]
2367 * data hash to pass along with the event
2368 *
2369 * @return {boolean|undefined}
2370 * Returns the opposite of `defaultPrevented` if default was
2371 * prevented. Otherwise, returns `undefined`
2372 */
2373function trigger(elem, event, hash) {
2374 // Fetches element data and a reference to the parent (for bubbling).
2375 // Don't want to add a data object to cache for every parent,
2376 // so checking hasElData first.
2377 const elemData = DomData.has(elem) ? DomData.get(elem) : {};
2378 const parent = elem.parentNode || elem.ownerDocument;
2379 // type = event.type || event,
2380 // handler;
2381
2382 // If an event name was passed as a string, creates an event out of it
2383 if (typeof event === 'string') {
2384 event = {
2385 type: event,
2386 target: elem
2387 };
2388 } else if (!event.target) {
2389 event.target = elem;
2390 }
2391
2392 // Normalizes the event properties.
2393 event = fixEvent(event);
2394
2395 // If the passed element has a dispatcher, executes the established handlers.
2396 if (elemData.dispatcher) {
2397 elemData.dispatcher.call(elem, event, hash);
2398 }
2399
2400 // Unless explicitly stopped or the event does not bubble (e.g. media events)
2401 // recursively calls this function to bubble the event up the DOM.
2402 if (parent && !event.isPropagationStopped() && event.bubbles === true) {
2403 trigger.call(null, parent, event, hash);
2404
2405 // If at the top of the DOM, triggers the default action unless disabled.
2406 } else if (!parent && !event.defaultPrevented && event.target && event.target[event.type]) {
2407 if (!DomData.has(event.target)) {
2408 DomData.set(event.target, {});
2409 }
2410 const targetData = DomData.get(event.target);
2411
2412 // Checks if the target has a default action for this event.
2413 if (event.target[event.type]) {
2414 // Temporarily disables event dispatching on the target as we have already executed the handler.
2415 targetData.disabled = true;
2416 // Executes the default action.
2417 if (typeof event.target[event.type] === 'function') {
2418 event.target[event.type]();
2419 }
2420 // Re-enables event dispatching.
2421 targetData.disabled = false;
2422 }
2423 }
2424
2425 // Inform the triggerer if the default was prevented by returning false
2426 return !event.defaultPrevented;
2427}
2428
2429/**
2430 * Trigger a listener only once for an event.
2431 *
2432 * @param {Element|Object} elem
2433 * Element or object to bind to.
2434 *
2435 * @param {string|string[]} type
2436 * Name/type of event
2437 *
2438 * @param {Event~EventListener} fn
2439 * Event listener function
2440 */
2441function one(elem, type, fn) {
2442 if (Array.isArray(type)) {
2443 return _handleMultipleEvents(one, elem, type, fn);
2444 }
2445 const func = function () {
2446 off(elem, type, func);
2447 fn.apply(this, arguments);
2448 };
2449
2450 // copy the guid to the new function so it can removed using the original function's ID
2451 func.guid = fn.guid = fn.guid || newGUID();
2452 on(elem, type, func);
2453}
2454
2455/**
2456 * Trigger a listener only once and then turn if off for all
2457 * configured events
2458 *
2459 * @param {Element|Object} elem
2460 * Element or object to bind to.
2461 *
2462 * @param {string|string[]} type
2463 * Name/type of event
2464 *
2465 * @param {Event~EventListener} fn
2466 * Event listener function
2467 */
2468function any(elem, type, fn) {
2469 const func = function () {
2470 off(elem, type, func);
2471 fn.apply(this, arguments);
2472 };
2473
2474 // copy the guid to the new function so it can removed using the original function's ID
2475 func.guid = fn.guid = fn.guid || newGUID();
2476
2477 // multiple ons, but one off for everything
2478 on(elem, type, func);
2479}
2480
2481var Events = /*#__PURE__*/Object.freeze({
2482 __proto__: null,
2483 fixEvent: fixEvent,
2484 on: on,
2485 off: off,
2486 trigger: trigger,
2487 one: one,
2488 any: any
2489});
2490
2491/**
2492 * @file fn.js
2493 * @module fn
2494 */
2495const UPDATE_REFRESH_INTERVAL = 30;
2496
2497/**
2498 * A private, internal-only function for changing the context of a function.
2499 *
2500 * It also stores a unique id on the function so it can be easily removed from
2501 * events.
2502 *
2503 * @private
2504 * @function
2505 * @param {*} context
2506 * The object to bind as scope.
2507 *
2508 * @param {Function} fn
2509 * The function to be bound to a scope.
2510 *
2511 * @param {number} [uid]
2512 * An optional unique ID for the function to be set
2513 *
2514 * @return {Function}
2515 * The new function that will be bound into the context given
2516 */
2517const bind_ = function (context, fn, uid) {
2518 // Make sure the function has a unique ID
2519 if (!fn.guid) {
2520 fn.guid = newGUID();
2521 }
2522
2523 // Create the new function that changes the context
2524 const bound = fn.bind(context);
2525
2526 // Allow for the ability to individualize this function
2527 // Needed in the case where multiple objects might share the same prototype
2528 // IF both items add an event listener with the same function, then you try to remove just one
2529 // it will remove both because they both have the same guid.
2530 // when using this, you need to use the bind method when you remove the listener as well.
2531 // currently used in text tracks
2532 bound.guid = uid ? uid + '_' + fn.guid : fn.guid;
2533 return bound;
2534};
2535
2536/**
2537 * Wraps the given function, `fn`, with a new function that only invokes `fn`
2538 * at most once per every `wait` milliseconds.
2539 *
2540 * @function
2541 * @param {Function} fn
2542 * The function to be throttled.
2543 *
2544 * @param {number} wait
2545 * The number of milliseconds by which to throttle.
2546 *
2547 * @return {Function}
2548 */
2549const throttle = function (fn, wait) {
2550 let last = window__default["default"].performance.now();
2551 const throttled = function (...args) {
2552 const now = window__default["default"].performance.now();
2553 if (now - last >= wait) {
2554 fn(...args);
2555 last = now;
2556 }
2557 };
2558 return throttled;
2559};
2560
2561/**
2562 * Creates a debounced function that delays invoking `func` until after `wait`
2563 * milliseconds have elapsed since the last time the debounced function was
2564 * invoked.
2565 *
2566 * Inspired by lodash and underscore implementations.
2567 *
2568 * @function
2569 * @param {Function} func
2570 * The function to wrap with debounce behavior.
2571 *
2572 * @param {number} wait
2573 * The number of milliseconds to wait after the last invocation.
2574 *
2575 * @param {boolean} [immediate]
2576 * Whether or not to invoke the function immediately upon creation.
2577 *
2578 * @param {Object} [context=window]
2579 * The "context" in which the debounced function should debounce. For
2580 * example, if this function should be tied to a Video.js player,
2581 * the player can be passed here. Alternatively, defaults to the
2582 * global `window` object.
2583 *
2584 * @return {Function}
2585 * A debounced function.
2586 */
2587const debounce = function (func, wait, immediate, context = window__default["default"]) {
2588 let timeout;
2589 const cancel = () => {
2590 context.clearTimeout(timeout);
2591 timeout = null;
2592 };
2593
2594 /* eslint-disable consistent-this */
2595 const debounced = function () {
2596 const self = this;
2597 const args = arguments;
2598 let later = function () {
2599 timeout = null;
2600 later = null;
2601 if (!immediate) {
2602 func.apply(self, args);
2603 }
2604 };
2605 if (!timeout && immediate) {
2606 func.apply(self, args);
2607 }
2608 context.clearTimeout(timeout);
2609 timeout = context.setTimeout(later, wait);
2610 };
2611 /* eslint-enable consistent-this */
2612
2613 debounced.cancel = cancel;
2614 return debounced;
2615};
2616
2617var Fn = /*#__PURE__*/Object.freeze({
2618 __proto__: null,
2619 UPDATE_REFRESH_INTERVAL: UPDATE_REFRESH_INTERVAL,
2620 bind_: bind_,
2621 throttle: throttle,
2622 debounce: debounce
2623});
2624
2625/**
2626 * @file src/js/event-target.js
2627 */
2628let EVENT_MAP;
2629
2630/**
2631 * `EventTarget` is a class that can have the same API as the DOM `EventTarget`. It
2632 * adds shorthand functions that wrap around lengthy functions. For example:
2633 * the `on` function is a wrapper around `addEventListener`.
2634 *
2635 * @see [EventTarget Spec]{@link https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventTarget}
2636 * @class EventTarget
2637 */
2638class EventTarget {
2639 /**
2640 * Adds an `event listener` to an instance of an `EventTarget`. An `event listener` is a
2641 * function that will get called when an event with a certain name gets triggered.
2642 *
2643 * @param {string|string[]} type
2644 * An event name or an array of event names.
2645 *
2646 * @param {Function} fn
2647 * The function to call with `EventTarget`s
2648 */
2649 on(type, fn) {
2650 // Remove the addEventListener alias before calling Events.on
2651 // so we don't get into an infinite type loop
2652 const ael = this.addEventListener;
2653 this.addEventListener = () => {};
2654 on(this, type, fn);
2655 this.addEventListener = ael;
2656 }
2657 /**
2658 * Removes an `event listener` for a specific event from an instance of `EventTarget`.
2659 * This makes it so that the `event listener` will no longer get called when the
2660 * named event happens.
2661 *
2662 * @param {string|string[]} type
2663 * An event name or an array of event names.
2664 *
2665 * @param {Function} fn
2666 * The function to remove.
2667 */
2668 off(type, fn) {
2669 off(this, type, fn);
2670 }
2671 /**
2672 * This function will add an `event listener` that gets triggered only once. After the
2673 * first trigger it will get removed. This is like adding an `event listener`
2674 * with {@link EventTarget#on} that calls {@link EventTarget#off} on itself.
2675 *
2676 * @param {string|string[]} type
2677 * An event name or an array of event names.
2678 *
2679 * @param {Function} fn
2680 * The function to be called once for each event name.
2681 */
2682 one(type, fn) {
2683 // Remove the addEventListener aliasing Events.on
2684 // so we don't get into an infinite type loop
2685 const ael = this.addEventListener;
2686 this.addEventListener = () => {};
2687 one(this, type, fn);
2688 this.addEventListener = ael;
2689 }
2690 /**
2691 * This function will add an `event listener` that gets triggered only once and is
2692 * removed from all events. This is like adding an array of `event listener`s
2693 * with {@link EventTarget#on} that calls {@link EventTarget#off} on all events the
2694 * first time it is triggered.
2695 *
2696 * @param {string|string[]} type
2697 * An event name or an array of event names.
2698 *
2699 * @param {Function} fn
2700 * The function to be called once for each event name.
2701 */
2702 any(type, fn) {
2703 // Remove the addEventListener aliasing Events.on
2704 // so we don't get into an infinite type loop
2705 const ael = this.addEventListener;
2706 this.addEventListener = () => {};
2707 any(this, type, fn);
2708 this.addEventListener = ael;
2709 }
2710 /**
2711 * This function causes an event to happen. This will then cause any `event listeners`
2712 * that are waiting for that event, to get called. If there are no `event listeners`
2713 * for an event then nothing will happen.
2714 *
2715 * If the name of the `Event` that is being triggered is in `EventTarget.allowedEvents_`.
2716 * Trigger will also call the `on` + `uppercaseEventName` function.
2717 *
2718 * Example:
2719 * 'click' is in `EventTarget.allowedEvents_`, so, trigger will attempt to call
2720 * `onClick` if it exists.
2721 *
2722 * @param {string|EventTarget~Event|Object} event
2723 * The name of the event, an `Event`, or an object with a key of type set to
2724 * an event name.
2725 */
2726 trigger(event) {
2727 const type = event.type || event;
2728
2729 // deprecation
2730 // In a future version we should default target to `this`
2731 // similar to how we default the target to `elem` in
2732 // `Events.trigger`. Right now the default `target` will be
2733 // `document` due to the `Event.fixEvent` call.
2734 if (typeof event === 'string') {
2735 event = {
2736 type
2737 };
2738 }
2739 event = fixEvent(event);
2740 if (this.allowedEvents_[type] && this['on' + type]) {
2741 this['on' + type](event);
2742 }
2743 trigger(this, event);
2744 }
2745 queueTrigger(event) {
2746 // only set up EVENT_MAP if it'll be used
2747 if (!EVENT_MAP) {
2748 EVENT_MAP = new Map();
2749 }
2750 const type = event.type || event;
2751 let map = EVENT_MAP.get(this);
2752 if (!map) {
2753 map = new Map();
2754 EVENT_MAP.set(this, map);
2755 }
2756 const oldTimeout = map.get(type);
2757 map.delete(type);
2758 window__default["default"].clearTimeout(oldTimeout);
2759 const timeout = window__default["default"].setTimeout(() => {
2760 map.delete(type);
2761 // if we cleared out all timeouts for the current target, delete its map
2762 if (map.size === 0) {
2763 map = null;
2764 EVENT_MAP.delete(this);
2765 }
2766 this.trigger(event);
2767 }, 0);
2768 map.set(type, timeout);
2769 }
2770}
2771
2772/**
2773 * A Custom DOM event.
2774 *
2775 * @typedef {CustomEvent} Event
2776 * @see [Properties]{@link https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent}
2777 */
2778
2779/**
2780 * All event listeners should follow the following format.
2781 *
2782 * @callback EventListener
2783 * @this {EventTarget}
2784 *
2785 * @param {Event} event
2786 * the event that triggered this function
2787 *
2788 * @param {Object} [hash]
2789 * hash of data sent during the event
2790 */
2791
2792/**
2793 * An object containing event names as keys and booleans as values.
2794 *
2795 * > NOTE: If an event name is set to a true value here {@link EventTarget#trigger}
2796 * will have extra functionality. See that function for more information.
2797 *
2798 * @property EventTarget.prototype.allowedEvents_
2799 * @protected
2800 */
2801EventTarget.prototype.allowedEvents_ = {};
2802
2803/**
2804 * An alias of {@link EventTarget#on}. Allows `EventTarget` to mimic
2805 * the standard DOM API.
2806 *
2807 * @function
2808 * @see {@link EventTarget#on}
2809 */
2810EventTarget.prototype.addEventListener = EventTarget.prototype.on;
2811
2812/**
2813 * An alias of {@link EventTarget#off}. Allows `EventTarget` to mimic
2814 * the standard DOM API.
2815 *
2816 * @function
2817 * @see {@link EventTarget#off}
2818 */
2819EventTarget.prototype.removeEventListener = EventTarget.prototype.off;
2820
2821/**
2822 * An alias of {@link EventTarget#trigger}. Allows `EventTarget` to mimic
2823 * the standard DOM API.
2824 *
2825 * @function
2826 * @see {@link EventTarget#trigger}
2827 */
2828EventTarget.prototype.dispatchEvent = EventTarget.prototype.trigger;
2829
2830/**
2831 * @file mixins/evented.js
2832 * @module evented
2833 */
2834const objName = obj => {
2835 if (typeof obj.name === 'function') {
2836 return obj.name();
2837 }
2838 if (typeof obj.name === 'string') {
2839 return obj.name;
2840 }
2841 if (obj.name_) {
2842 return obj.name_;
2843 }
2844 if (obj.constructor && obj.constructor.name) {
2845 return obj.constructor.name;
2846 }
2847 return typeof obj;
2848};
2849
2850/**
2851 * Returns whether or not an object has had the evented mixin applied.
2852 *
2853 * @param {Object} object
2854 * An object to test.
2855 *
2856 * @return {boolean}
2857 * Whether or not the object appears to be evented.
2858 */
2859const isEvented = object => object instanceof EventTarget || !!object.eventBusEl_ && ['on', 'one', 'off', 'trigger'].every(k => typeof object[k] === 'function');
2860
2861/**
2862 * Adds a callback to run after the evented mixin applied.
2863 *
2864 * @param {Object} target
2865 * An object to Add
2866 * @param {Function} callback
2867 * The callback to run.
2868 */
2869const addEventedCallback = (target, callback) => {
2870 if (isEvented(target)) {
2871 callback();
2872 } else {
2873 if (!target.eventedCallbacks) {
2874 target.eventedCallbacks = [];
2875 }
2876 target.eventedCallbacks.push(callback);
2877 }
2878};
2879
2880/**
2881 * Whether a value is a valid event type - non-empty string or array.
2882 *
2883 * @private
2884 * @param {string|Array} type
2885 * The type value to test.
2886 *
2887 * @return {boolean}
2888 * Whether or not the type is a valid event type.
2889 */
2890const isValidEventType = type =>
2891// The regex here verifies that the `type` contains at least one non-
2892// whitespace character.
2893typeof type === 'string' && /\S/.test(type) || Array.isArray(type) && !!type.length;
2894
2895/**
2896 * Validates a value to determine if it is a valid event target. Throws if not.
2897 *
2898 * @private
2899 * @throws {Error}
2900 * If the target does not appear to be a valid event target.
2901 *
2902 * @param {Object} target
2903 * The object to test.
2904 *
2905 * @param {Object} obj
2906 * The evented object we are validating for
2907 *
2908 * @param {string} fnName
2909 * The name of the evented mixin function that called this.
2910 */
2911const validateTarget = (target, obj, fnName) => {
2912 if (!target || !target.nodeName && !isEvented(target)) {
2913 throw new Error(`Invalid target for ${objName(obj)}#${fnName}; must be a DOM node or evented object.`);
2914 }
2915};
2916
2917/**
2918 * Validates a value to determine if it is a valid event target. Throws if not.
2919 *
2920 * @private
2921 * @throws {Error}
2922 * If the type does not appear to be a valid event type.
2923 *
2924 * @param {string|Array} type
2925 * The type to test.
2926 *
2927 * @param {Object} obj
2928* The evented object we are validating for
2929 *
2930 * @param {string} fnName
2931 * The name of the evented mixin function that called this.
2932 */
2933const validateEventType = (type, obj, fnName) => {
2934 if (!isValidEventType(type)) {
2935 throw new Error(`Invalid event type for ${objName(obj)}#${fnName}; must be a non-empty string or array.`);
2936 }
2937};
2938
2939/**
2940 * Validates a value to determine if it is a valid listener. Throws if not.
2941 *
2942 * @private
2943 * @throws {Error}
2944 * If the listener is not a function.
2945 *
2946 * @param {Function} listener
2947 * The listener to test.
2948 *
2949 * @param {Object} obj
2950 * The evented object we are validating for
2951 *
2952 * @param {string} fnName
2953 * The name of the evented mixin function that called this.
2954 */
2955const validateListener = (listener, obj, fnName) => {
2956 if (typeof listener !== 'function') {
2957 throw new Error(`Invalid listener for ${objName(obj)}#${fnName}; must be a function.`);
2958 }
2959};
2960
2961/**
2962 * Takes an array of arguments given to `on()` or `one()`, validates them, and
2963 * normalizes them into an object.
2964 *
2965 * @private
2966 * @param {Object} self
2967 * The evented object on which `on()` or `one()` was called. This
2968 * object will be bound as the `this` value for the listener.
2969 *
2970 * @param {Array} args
2971 * An array of arguments passed to `on()` or `one()`.
2972 *
2973 * @param {string} fnName
2974 * The name of the evented mixin function that called this.
2975 *
2976 * @return {Object}
2977 * An object containing useful values for `on()` or `one()` calls.
2978 */
2979const normalizeListenArgs = (self, args, fnName) => {
2980 // If the number of arguments is less than 3, the target is always the
2981 // evented object itself.
2982 const isTargetingSelf = args.length < 3 || args[0] === self || args[0] === self.eventBusEl_;
2983 let target;
2984 let type;
2985 let listener;
2986 if (isTargetingSelf) {
2987 target = self.eventBusEl_;
2988
2989 // Deal with cases where we got 3 arguments, but we are still listening to
2990 // the evented object itself.
2991 if (args.length >= 3) {
2992 args.shift();
2993 }
2994 [type, listener] = args;
2995 } else {
2996 // This was `[target, type, listener] = args;` but this block needs more than
2997 // one statement to produce minified output compatible with Chrome 53.
2998 // See https://github.com/videojs/video.js/pull/8810
2999 target = args[0];
3000 type = args[1];
3001 listener = args[2];
3002 }
3003 validateTarget(target, self, fnName);
3004 validateEventType(type, self, fnName);
3005 validateListener(listener, self, fnName);
3006 listener = bind_(self, listener);
3007 return {
3008 isTargetingSelf,
3009 target,
3010 type,
3011 listener
3012 };
3013};
3014
3015/**
3016 * Adds the listener to the event type(s) on the target, normalizing for
3017 * the type of target.
3018 *
3019 * @private
3020 * @param {Element|Object} target
3021 * A DOM node or evented object.
3022 *
3023 * @param {string} method
3024 * The event binding method to use ("on" or "one").
3025 *
3026 * @param {string|Array} type
3027 * One or more event type(s).
3028 *
3029 * @param {Function} listener
3030 * A listener function.
3031 */
3032const listen = (target, method, type, listener) => {
3033 validateTarget(target, target, method);
3034 if (target.nodeName) {
3035 Events[method](target, type, listener);
3036 } else {
3037 target[method](type, listener);
3038 }
3039};
3040
3041/**
3042 * Contains methods that provide event capabilities to an object which is passed
3043 * to {@link module:evented|evented}.
3044 *
3045 * @mixin EventedMixin
3046 */
3047const EventedMixin = {
3048 /**
3049 * Add a listener to an event (or events) on this object or another evented
3050 * object.
3051 *
3052 * @param {string|Array|Element|Object} targetOrType
3053 * If this is a string or array, it represents the event type(s)
3054 * that will trigger the listener.
3055 *
3056 * Another evented object can be passed here instead, which will
3057 * cause the listener to listen for events on _that_ object.
3058 *
3059 * In either case, the listener's `this` value will be bound to
3060 * this object.
3061 *
3062 * @param {string|Array|Function} typeOrListener
3063 * If the first argument was a string or array, this should be the
3064 * listener function. Otherwise, this is a string or array of event
3065 * type(s).
3066 *
3067 * @param {Function} [listener]
3068 * If the first argument was another evented object, this will be
3069 * the listener function.
3070 */
3071 on(...args) {
3072 const {
3073 isTargetingSelf,
3074 target,
3075 type,
3076 listener
3077 } = normalizeListenArgs(this, args, 'on');
3078 listen(target, 'on', type, listener);
3079
3080 // If this object is listening to another evented object.
3081 if (!isTargetingSelf) {
3082 // If this object is disposed, remove the listener.
3083 const removeListenerOnDispose = () => this.off(target, type, listener);
3084
3085 // Use the same function ID as the listener so we can remove it later it
3086 // using the ID of the original listener.
3087 removeListenerOnDispose.guid = listener.guid;
3088
3089 // Add a listener to the target's dispose event as well. This ensures
3090 // that if the target is disposed BEFORE this object, we remove the
3091 // removal listener that was just added. Otherwise, we create a memory leak.
3092 const removeRemoverOnTargetDispose = () => this.off('dispose', removeListenerOnDispose);
3093
3094 // Use the same function ID as the listener so we can remove it later
3095 // it using the ID of the original listener.
3096 removeRemoverOnTargetDispose.guid = listener.guid;
3097 listen(this, 'on', 'dispose', removeListenerOnDispose);
3098 listen(target, 'on', 'dispose', removeRemoverOnTargetDispose);
3099 }
3100 },
3101 /**
3102 * Add a listener to an event (or events) on this object or another evented
3103 * object. The listener will be called once per event and then removed.
3104 *
3105 * @param {string|Array|Element|Object} targetOrType
3106 * If this is a string or array, it represents the event type(s)
3107 * that will trigger the listener.
3108 *
3109 * Another evented object can be passed here instead, which will
3110 * cause the listener to listen for events on _that_ object.
3111 *
3112 * In either case, the listener's `this` value will be bound to
3113 * this object.
3114 *
3115 * @param {string|Array|Function} typeOrListener
3116 * If the first argument was a string or array, this should be the
3117 * listener function. Otherwise, this is a string or array of event
3118 * type(s).
3119 *
3120 * @param {Function} [listener]
3121 * If the first argument was another evented object, this will be
3122 * the listener function.
3123 */
3124 one(...args) {
3125 const {
3126 isTargetingSelf,
3127 target,
3128 type,
3129 listener
3130 } = normalizeListenArgs(this, args, 'one');
3131
3132 // Targeting this evented object.
3133 if (isTargetingSelf) {
3134 listen(target, 'one', type, listener);
3135
3136 // Targeting another evented object.
3137 } else {
3138 // TODO: This wrapper is incorrect! It should only
3139 // remove the wrapper for the event type that called it.
3140 // Instead all listeners are removed on the first trigger!
3141 // see https://github.com/videojs/video.js/issues/5962
3142 const wrapper = (...largs) => {
3143 this.off(target, type, wrapper);
3144 listener.apply(null, largs);
3145 };
3146
3147 // Use the same function ID as the listener so we can remove it later
3148 // it using the ID of the original listener.
3149 wrapper.guid = listener.guid;
3150 listen(target, 'one', type, wrapper);
3151 }
3152 },
3153 /**
3154 * Add a listener to an event (or events) on this object or another evented
3155 * object. The listener will only be called once for the first event that is triggered
3156 * then removed.
3157 *
3158 * @param {string|Array|Element|Object} targetOrType
3159 * If this is a string or array, it represents the event type(s)
3160 * that will trigger the listener.
3161 *
3162 * Another evented object can be passed here instead, which will
3163 * cause the listener to listen for events on _that_ object.
3164 *
3165 * In either case, the listener's `this` value will be bound to
3166 * this object.
3167 *
3168 * @param {string|Array|Function} typeOrListener
3169 * If the first argument was a string or array, this should be the
3170 * listener function. Otherwise, this is a string or array of event
3171 * type(s).
3172 *
3173 * @param {Function} [listener]
3174 * If the first argument was another evented object, this will be
3175 * the listener function.
3176 */
3177 any(...args) {
3178 const {
3179 isTargetingSelf,
3180 target,
3181 type,
3182 listener
3183 } = normalizeListenArgs(this, args, 'any');
3184
3185 // Targeting this evented object.
3186 if (isTargetingSelf) {
3187 listen(target, 'any', type, listener);
3188
3189 // Targeting another evented object.
3190 } else {
3191 const wrapper = (...largs) => {
3192 this.off(target, type, wrapper);
3193 listener.apply(null, largs);
3194 };
3195
3196 // Use the same function ID as the listener so we can remove it later
3197 // it using the ID of the original listener.
3198 wrapper.guid = listener.guid;
3199 listen(target, 'any', type, wrapper);
3200 }
3201 },
3202 /**
3203 * Removes listener(s) from event(s) on an evented object.
3204 *
3205 * @param {string|Array|Element|Object} [targetOrType]
3206 * If this is a string or array, it represents the event type(s).
3207 *
3208 * Another evented object can be passed here instead, in which case
3209 * ALL 3 arguments are _required_.
3210 *
3211 * @param {string|Array|Function} [typeOrListener]
3212 * If the first argument was a string or array, this may be the
3213 * listener function. Otherwise, this is a string or array of event
3214 * type(s).
3215 *
3216 * @param {Function} [listener]
3217 * If the first argument was another evented object, this will be
3218 * the listener function; otherwise, _all_ listeners bound to the
3219 * event type(s) will be removed.
3220 */
3221 off(targetOrType, typeOrListener, listener) {
3222 // Targeting this evented object.
3223 if (!targetOrType || isValidEventType(targetOrType)) {
3224 off(this.eventBusEl_, targetOrType, typeOrListener);
3225
3226 // Targeting another evented object.
3227 } else {
3228 const target = targetOrType;
3229 const type = typeOrListener;
3230
3231 // Fail fast and in a meaningful way!
3232 validateTarget(target, this, 'off');
3233 validateEventType(type, this, 'off');
3234 validateListener(listener, this, 'off');
3235
3236 // Ensure there's at least a guid, even if the function hasn't been used
3237 listener = bind_(this, listener);
3238
3239 // Remove the dispose listener on this evented object, which was given
3240 // the same guid as the event listener in on().
3241 this.off('dispose', listener);
3242 if (target.nodeName) {
3243 off(target, type, listener);
3244 off(target, 'dispose', listener);
3245 } else if (isEvented(target)) {
3246 target.off(type, listener);
3247 target.off('dispose', listener);
3248 }
3249 }
3250 },
3251 /**
3252 * Fire an event on this evented object, causing its listeners to be called.
3253 *
3254 * @param {string|Object} event
3255 * An event type or an object with a type property.
3256 *
3257 * @param {Object} [hash]
3258 * An additional object to pass along to listeners.
3259 *
3260 * @return {boolean}
3261 * Whether or not the default behavior was prevented.
3262 */
3263 trigger(event, hash) {
3264 validateTarget(this.eventBusEl_, this, 'trigger');
3265 const type = event && typeof event !== 'string' ? event.type : event;
3266 if (!isValidEventType(type)) {
3267 throw new Error(`Invalid event type for ${objName(this)}#trigger; ` + 'must be a non-empty string or object with a type key that has a non-empty value.');
3268 }
3269 return trigger(this.eventBusEl_, event, hash);
3270 }
3271};
3272
3273/**
3274 * Applies {@link module:evented~EventedMixin|EventedMixin} to a target object.
3275 *
3276 * @param {Object} target
3277 * The object to which to add event methods.
3278 *
3279 * @param {Object} [options={}]
3280 * Options for customizing the mixin behavior.
3281 *
3282 * @param {string} [options.eventBusKey]
3283 * By default, adds a `eventBusEl_` DOM element to the target object,
3284 * which is used as an event bus. If the target object already has a
3285 * DOM element that should be used, pass its key here.
3286 *
3287 * @return {Object}
3288 * The target object.
3289 */
3290function evented(target, options = {}) {
3291 const {
3292 eventBusKey
3293 } = options;
3294
3295 // Set or create the eventBusEl_.
3296 if (eventBusKey) {
3297 if (!target[eventBusKey].nodeName) {
3298 throw new Error(`The eventBusKey "${eventBusKey}" does not refer to an element.`);
3299 }
3300 target.eventBusEl_ = target[eventBusKey];
3301 } else {
3302 target.eventBusEl_ = createEl('span', {
3303 className: 'vjs-event-bus'
3304 });
3305 }
3306 Object.assign(target, EventedMixin);
3307 if (target.eventedCallbacks) {
3308 target.eventedCallbacks.forEach(callback => {
3309 callback();
3310 });
3311 }
3312
3313 // When any evented object is disposed, it removes all its listeners.
3314 target.on('dispose', () => {
3315 target.off();
3316 [target, target.el_, target.eventBusEl_].forEach(function (val) {
3317 if (val && DomData.has(val)) {
3318 DomData.delete(val);
3319 }
3320 });
3321 window__default["default"].setTimeout(() => {
3322 target.eventBusEl_ = null;
3323 }, 0);
3324 });
3325 return target;
3326}
3327
3328/**
3329 * @file mixins/stateful.js
3330 * @module stateful
3331 */
3332
3333/**
3334 * Contains methods that provide statefulness to an object which is passed
3335 * to {@link module:stateful}.
3336 *
3337 * @mixin StatefulMixin
3338 */
3339const StatefulMixin = {
3340 /**
3341 * A hash containing arbitrary keys and values representing the state of
3342 * the object.
3343 *
3344 * @type {Object}
3345 */
3346 state: {},
3347 /**
3348 * Set the state of an object by mutating its
3349 * {@link module:stateful~StatefulMixin.state|state} object in place.
3350 *
3351 * @fires module:stateful~StatefulMixin#statechanged
3352 * @param {Object|Function} stateUpdates
3353 * A new set of properties to shallow-merge into the plugin state.
3354 * Can be a plain object or a function returning a plain object.
3355 *
3356 * @return {Object|undefined}
3357 * An object containing changes that occurred. If no changes
3358 * occurred, returns `undefined`.
3359 */
3360 setState(stateUpdates) {
3361 // Support providing the `stateUpdates` state as a function.
3362 if (typeof stateUpdates === 'function') {
3363 stateUpdates = stateUpdates();
3364 }
3365 let changes;
3366 each(stateUpdates, (value, key) => {
3367 // Record the change if the value is different from what's in the
3368 // current state.
3369 if (this.state[key] !== value) {
3370 changes = changes || {};
3371 changes[key] = {
3372 from: this.state[key],
3373 to: value
3374 };
3375 }
3376 this.state[key] = value;
3377 });
3378
3379 // Only trigger "statechange" if there were changes AND we have a trigger
3380 // function. This allows us to not require that the target object be an
3381 // evented object.
3382 if (changes && isEvented(this)) {
3383 /**
3384 * An event triggered on an object that is both
3385 * {@link module:stateful|stateful} and {@link module:evented|evented}
3386 * indicating that its state has changed.
3387 *
3388 * @event module:stateful~StatefulMixin#statechanged
3389 * @type {Object}
3390 * @property {Object} changes
3391 * A hash containing the properties that were changed and
3392 * the values they were changed `from` and `to`.
3393 */
3394 this.trigger({
3395 changes,
3396 type: 'statechanged'
3397 });
3398 }
3399 return changes;
3400 }
3401};
3402
3403/**
3404 * Applies {@link module:stateful~StatefulMixin|StatefulMixin} to a target
3405 * object.
3406 *
3407 * If the target object is {@link module:evented|evented} and has a
3408 * `handleStateChanged` method, that method will be automatically bound to the
3409 * `statechanged` event on itself.
3410 *
3411 * @param {Object} target
3412 * The object to be made stateful.
3413 *
3414 * @param {Object} [defaultState]
3415 * A default set of properties to populate the newly-stateful object's
3416 * `state` property.
3417 *
3418 * @return {Object}
3419 * Returns the `target`.
3420 */
3421function stateful(target, defaultState) {
3422 Object.assign(target, StatefulMixin);
3423
3424 // This happens after the mixing-in because we need to replace the `state`
3425 // added in that step.
3426 target.state = Object.assign({}, target.state, defaultState);
3427
3428 // Auto-bind the `handleStateChanged` method of the target object if it exists.
3429 if (typeof target.handleStateChanged === 'function' && isEvented(target)) {
3430 target.on('statechanged', target.handleStateChanged);
3431 }
3432 return target;
3433}
3434
3435/**
3436 * @file str.js
3437 * @module to-lower-case
3438 */
3439
3440/**
3441 * Lowercase the first letter of a string.
3442 *
3443 * @param {string} string
3444 * String to be lowercased
3445 *
3446 * @return {string}
3447 * The string with a lowercased first letter
3448 */
3449const toLowerCase = function (string) {
3450 if (typeof string !== 'string') {
3451 return string;
3452 }
3453 return string.replace(/./, w => w.toLowerCase());
3454};
3455
3456/**
3457 * Uppercase the first letter of a string.
3458 *
3459 * @param {string} string
3460 * String to be uppercased
3461 *
3462 * @return {string}
3463 * The string with an uppercased first letter
3464 */
3465const toTitleCase = function (string) {
3466 if (typeof string !== 'string') {
3467 return string;
3468 }
3469 return string.replace(/./, w => w.toUpperCase());
3470};
3471
3472/**
3473 * Compares the TitleCase versions of the two strings for equality.
3474 *
3475 * @param {string} str1
3476 * The first string to compare
3477 *
3478 * @param {string} str2
3479 * The second string to compare
3480 *
3481 * @return {boolean}
3482 * Whether the TitleCase versions of the strings are equal
3483 */
3484const titleCaseEquals = function (str1, str2) {
3485 return toTitleCase(str1) === toTitleCase(str2);
3486};
3487
3488var Str = /*#__PURE__*/Object.freeze({
3489 __proto__: null,
3490 toLowerCase: toLowerCase,
3491 toTitleCase: toTitleCase,
3492 titleCaseEquals: titleCaseEquals
3493});
3494
3495/**
3496 * Player Component - Base class for all UI objects
3497 *
3498 * @file component.js
3499 */
3500
3501/** @import Player from './player' */
3502
3503/**
3504 * A callback to be called if and when the component is ready.
3505 * `this` will be the Component instance.
3506 *
3507 * @callback ReadyCallback
3508 * @returns {void}
3509 */
3510
3511/**
3512 * Base class for all UI Components.
3513 * Components are UI objects which represent both a javascript object and an element
3514 * in the DOM. They can be children of other components, and can have
3515 * children themselves.
3516 *
3517 * Components can also use methods from {@link EventTarget}
3518 */
3519class Component {
3520 /**
3521 * Creates an instance of this class.
3522 *
3523 * @param {Player} player
3524 * The `Player` that this class should be attached to.
3525 *
3526 * @param {Object} [options]
3527 * The key/value store of component options.
3528 *
3529 * @param {Object[]} [options.children]
3530 * An array of children objects to initialize this component with. Children objects have
3531 * a name property that will be used if more than one component of the same type needs to be
3532 * added.
3533 *
3534 * @param {string} [options.className]
3535 * A class or space separated list of classes to add the component
3536 *
3537 * @param {ReadyCallback} [ready]
3538 * Function that gets called when the `Component` is ready.
3539 */
3540 constructor(player, options, ready) {
3541 // The component might be the player itself and we can't pass `this` to super
3542 if (!player && this.play) {
3543 this.player_ = player = this; // eslint-disable-line
3544 } else {
3545 this.player_ = player;
3546 }
3547 this.isDisposed_ = false;
3548
3549 // Hold the reference to the parent component via `addChild` method
3550 this.parentComponent_ = null;
3551
3552 // Make a copy of prototype.options_ to protect against overriding defaults
3553 this.options_ = merge({}, this.options_);
3554
3555 // Updated options with supplied options
3556 options = this.options_ = merge(this.options_, options);
3557
3558 // Get ID from options or options element if one is supplied
3559 this.id_ = options.id || options.el && options.el.id;
3560
3561 // If there was no ID from the options, generate one
3562 if (!this.id_) {
3563 // Don't require the player ID function in the case of mock players
3564 const id = player && player.id && player.id() || 'no_player';
3565 this.id_ = `${id}_component_${newGUID()}`;
3566 }
3567 this.name_ = options.name || null;
3568
3569 // Create element if one wasn't provided in options
3570 if (options.el) {
3571 this.el_ = options.el;
3572 } else if (options.createEl !== false) {
3573 this.el_ = this.createEl();
3574 }
3575 if (options.className && this.el_) {
3576 options.className.split(' ').forEach(c => this.addClass(c));
3577 }
3578
3579 // Remove the placeholder event methods. If the component is evented, the
3580 // real methods are added next
3581 ['on', 'off', 'one', 'any', 'trigger'].forEach(fn => {
3582 this[fn] = undefined;
3583 });
3584
3585 // if evented is anything except false, we want to mixin in evented
3586 if (options.evented !== false) {
3587 // Make this an evented object and use `el_`, if available, as its event bus
3588 evented(this, {
3589 eventBusKey: this.el_ ? 'el_' : null
3590 });
3591 this.handleLanguagechange = this.handleLanguagechange.bind(this);
3592 this.on(this.player_, 'languagechange', this.handleLanguagechange);
3593 }
3594 stateful(this, this.constructor.defaultState);
3595 this.children_ = [];
3596 this.childIndex_ = {};
3597 this.childNameIndex_ = {};
3598 this.setTimeoutIds_ = new Set();
3599 this.setIntervalIds_ = new Set();
3600 this.rafIds_ = new Set();
3601 this.namedRafs_ = new Map();
3602 this.clearingTimersOnDispose_ = false;
3603
3604 // Add any child components in options
3605 if (options.initChildren !== false) {
3606 this.initChildren();
3607 }
3608
3609 // Don't want to trigger ready here or it will go before init is actually
3610 // finished for all children that run this constructor
3611 this.ready(ready);
3612 if (options.reportTouchActivity !== false) {
3613 this.enableTouchActivity();
3614 }
3615 }
3616
3617 // `on`, `off`, `one`, `any` and `trigger` are here so tsc includes them in definitions.
3618 // They are replaced or removed in the constructor
3619
3620 /**
3621 * Adds an `event listener` to an instance of an `EventTarget`. An `event listener` is a
3622 * function that will get called when an event with a certain name gets triggered.
3623 *
3624 * @param {string|string[]} type
3625 * An event name or an array of event names.
3626 *
3627 * @param {Function} fn
3628 * The function to call with `EventTarget`s
3629 */
3630
3631 /**
3632 * Removes an `event listener` for a specific event from an instance of `EventTarget`.
3633 * This makes it so that the `event listener` will no longer get called when the
3634 * named event happens.
3635 *
3636 * @param {string|string[]} type
3637 * An event name or an array of event names.
3638 *
3639 * @param {Function} [fn]
3640 * The function to remove. If not specified, all listeners managed by Video.js will be removed.
3641 */
3642
3643 /**
3644 * This function will add an `event listener` that gets triggered only once. After the
3645 * first trigger it will get removed. This is like adding an `event listener`
3646 * with {@link EventTarget#on} that calls {@link EventTarget#off} on itself.
3647 *
3648 * @param {string|string[]} type
3649 * An event name or an array of event names.
3650 *
3651 * @param {Function} fn
3652 * The function to be called once for each event name.
3653 */
3654
3655 /**
3656 * This function will add an `event listener` that gets triggered only once and is
3657 * removed from all events. This is like adding an array of `event listener`s
3658 * with {@link EventTarget#on} that calls {@link EventTarget#off} on all events the
3659 * first time it is triggered.
3660 *
3661 * @param {string|string[]} type
3662 * An event name or an array of event names.
3663 *
3664 * @param {Function} fn
3665 * The function to be called once for each event name.
3666 */
3667
3668 /**
3669 * This function causes an event to happen. This will then cause any `event listeners`
3670 * that are waiting for that event, to get called. If there are no `event listeners`
3671 * for an event then nothing will happen.
3672 *
3673 * If the name of the `Event` that is being triggered is in `EventTarget.allowedEvents_`.
3674 * Trigger will also call the `on` + `uppercaseEventName` function.
3675 *
3676 * Example:
3677 * 'click' is in `EventTarget.allowedEvents_`, so, trigger will attempt to call
3678 * `onClick` if it exists.
3679 *
3680 * @param {string|Event|Object} event
3681 * The name of the event, an `Event`, or an object with a key of type set to
3682 * an event name.
3683 *
3684 * @param {Object} [hash]
3685 * Optionally extra argument to pass through to an event listener
3686 */
3687
3688 /**
3689 * Dispose of the `Component` and all child components.
3690 *
3691 * @fires Component#dispose
3692 *
3693 * @param {Object} options
3694 * @param {Element} options.originalEl element with which to replace player element
3695 */
3696 dispose(options = {}) {
3697 // Bail out if the component has already been disposed.
3698 if (this.isDisposed_) {
3699 return;
3700 }
3701 if (this.readyQueue_) {
3702 this.readyQueue_.length = 0;
3703 }
3704
3705 /**
3706 * Triggered when a `Component` is disposed.
3707 *
3708 * @event Component#dispose
3709 * @type {Event}
3710 *
3711 * @property {boolean} [bubbles=false]
3712 * set to false so that the dispose event does not
3713 * bubble up
3714 */
3715 this.trigger({
3716 type: 'dispose',
3717 bubbles: false
3718 });
3719 this.isDisposed_ = true;
3720
3721 // Dispose all children.
3722 if (this.children_) {
3723 for (let i = this.children_.length - 1; i >= 0; i--) {
3724 if (this.children_[i].dispose) {
3725 this.children_[i].dispose();
3726 }
3727 }
3728 }
3729
3730 // Delete child references
3731 this.children_ = null;
3732 this.childIndex_ = null;
3733 this.childNameIndex_ = null;
3734 this.parentComponent_ = null;
3735 if (this.el_) {
3736 // Remove element from DOM
3737 if (this.el_.parentNode) {
3738 if (options.restoreEl) {
3739 this.el_.parentNode.replaceChild(options.restoreEl, this.el_);
3740 } else {
3741 this.el_.parentNode.removeChild(this.el_);
3742 }
3743 }
3744 this.el_ = null;
3745 }
3746
3747 // remove reference to the player after disposing of the element
3748 this.player_ = null;
3749 }
3750
3751 /**
3752 * Determine whether or not this component has been disposed.
3753 *
3754 * @return {boolean}
3755 * If the component has been disposed, will be `true`. Otherwise, `false`.
3756 */
3757 isDisposed() {
3758 return Boolean(this.isDisposed_);
3759 }
3760
3761 /**
3762 * Return the {@link Player} that the `Component` has attached to.
3763 *
3764 * @return {Player}
3765 * The player that this `Component` has attached to.
3766 */
3767 player() {
3768 return this.player_;
3769 }
3770
3771 /**
3772 * Deep merge of options objects with new options.
3773 * > Note: When both `obj` and `options` contain properties whose values are objects.
3774 * The two properties get merged using {@link module:obj.merge}
3775 *
3776 * @param {Object} obj
3777 * The object that contains new options.
3778 *
3779 * @return {Object}
3780 * A new object of `this.options_` and `obj` merged together.
3781 */
3782 options(obj) {
3783 if (!obj) {
3784 return this.options_;
3785 }
3786 this.options_ = merge(this.options_, obj);
3787 return this.options_;
3788 }
3789
3790 /**
3791 * Get the `Component`s DOM element
3792 *
3793 * @return {Element}
3794 * The DOM element for this `Component`.
3795 */
3796 el() {
3797 return this.el_;
3798 }
3799
3800 /**
3801 * Create the `Component`s DOM element.
3802 *
3803 * @param {string} [tagName]
3804 * Element's DOM node type. e.g. 'div'
3805 *
3806 * @param {Object} [properties]
3807 * An object of properties that should be set.
3808 *
3809 * @param {Object} [attributes]
3810 * An object of attributes that should be set.
3811 *
3812 * @return {Element}
3813 * The element that gets created.
3814 */
3815 createEl(tagName, properties, attributes) {
3816 return createEl(tagName, properties, attributes);
3817 }
3818
3819 /**
3820 * Localize a string given the string in english.
3821 *
3822 * If tokens are provided, it'll try and run a simple token replacement on the provided string.
3823 * The tokens it looks for look like `{1}` with the index being 1-indexed into the tokens array.
3824 *
3825 * If a `defaultValue` is provided, it'll use that over `string`,
3826 * if a value isn't found in provided language files.
3827 * This is useful if you want to have a descriptive key for token replacement
3828 * but have a succinct localized string and not require `en.json` to be included.
3829 *
3830 * Currently, it is used for the progress bar timing.
3831 * ```js
3832 * {
3833 * "progress bar timing: currentTime={1} duration={2}": "{1} of {2}"
3834 * }
3835 * ```
3836 * It is then used like so:
3837 * ```js
3838 * this.localize('progress bar timing: currentTime={1} duration{2}',
3839 * [this.player_.currentTime(), this.player_.duration()],
3840 * '{1} of {2}');
3841 * ```
3842 *
3843 * Which outputs something like: `01:23 of 24:56`.
3844 *
3845 *
3846 * @param {string} string
3847 * The string to localize and the key to lookup in the language files.
3848 * @param {string[]} [tokens]
3849 * If the current item has token replacements, provide the tokens here.
3850 * @param {string} [defaultValue]
3851 * Defaults to `string`. Can be a default value to use for token replacement
3852 * if the lookup key is needed to be separate.
3853 *
3854 * @return {string}
3855 * The localized string or if no localization exists the english string.
3856 */
3857 localize(string, tokens, defaultValue = string) {
3858 const code = this.player_.language && this.player_.language();
3859 const languages = this.player_.languages && this.player_.languages();
3860 const language = languages && languages[code];
3861 const primaryCode = code && code.split('-')[0];
3862 const primaryLang = languages && languages[primaryCode];
3863 let localizedString = defaultValue;
3864 if (language && language[string]) {
3865 localizedString = language[string];
3866 } else if (primaryLang && primaryLang[string]) {
3867 localizedString = primaryLang[string];
3868 }
3869 if (tokens) {
3870 localizedString = localizedString.replace(/\{(\d+)\}/g, function (match, index) {
3871 const value = tokens[index - 1];
3872 let ret = value;
3873 if (typeof value === 'undefined') {
3874 ret = match;
3875 }
3876 return ret;
3877 });
3878 }
3879 return localizedString;
3880 }
3881
3882 /**
3883 * Handles language change for the player in components. Should be overridden by sub-components.
3884 *
3885 * @abstract
3886 */
3887 handleLanguagechange() {}
3888
3889 /**
3890 * Return the `Component`s DOM element. This is where children get inserted.
3891 * This will usually be the the same as the element returned in {@link Component#el}.
3892 *
3893 * @return {Element}
3894 * The content element for this `Component`.
3895 */
3896 contentEl() {
3897 return this.contentEl_ || this.el_;
3898 }
3899
3900 /**
3901 * Get this `Component`s ID
3902 *
3903 * @return {string}
3904 * The id of this `Component`
3905 */
3906 id() {
3907 return this.id_;
3908 }
3909
3910 /**
3911 * Get the `Component`s name. The name gets used to reference the `Component`
3912 * and is set during registration.
3913 *
3914 * @return {string}
3915 * The name of this `Component`.
3916 */
3917 name() {
3918 return this.name_;
3919 }
3920
3921 /**
3922 * Get an array of all child components
3923 *
3924 * @return {Array}
3925 * The children
3926 */
3927 children() {
3928 return this.children_;
3929 }
3930
3931 /**
3932 * Returns the child `Component` with the given `id`.
3933 *
3934 * @param {string} id
3935 * The id of the child `Component` to get.
3936 *
3937 * @return {Component|undefined}
3938 * The child `Component` with the given `id` or undefined.
3939 */
3940 getChildById(id) {
3941 return this.childIndex_[id];
3942 }
3943
3944 /**
3945 * Returns the child `Component` with the given `name`.
3946 *
3947 * @param {string} name
3948 * The name of the child `Component` to get.
3949 *
3950 * @return {Component|undefined}
3951 * The child `Component` with the given `name` or undefined.
3952 */
3953 getChild(name) {
3954 if (!name) {
3955 return;
3956 }
3957 return this.childNameIndex_[name];
3958 }
3959
3960 /**
3961 * Returns the descendant `Component` following the givent
3962 * descendant `names`. For instance ['foo', 'bar', 'baz'] would
3963 * try to get 'foo' on the current component, 'bar' on the 'foo'
3964 * component and 'baz' on the 'bar' component and return undefined
3965 * if any of those don't exist.
3966 *
3967 * @param {...string[]|...string} names
3968 * The name of the child `Component` to get.
3969 *
3970 * @return {Component|undefined}
3971 * The descendant `Component` following the given descendant
3972 * `names` or undefined.
3973 */
3974 getDescendant(...names) {
3975 // flatten array argument into the main array
3976 names = names.reduce((acc, n) => acc.concat(n), []);
3977 let currentChild = this;
3978 for (let i = 0; i < names.length; i++) {
3979 currentChild = currentChild.getChild(names[i]);
3980 if (!currentChild || !currentChild.getChild) {
3981 return;
3982 }
3983 }
3984 return currentChild;
3985 }
3986
3987 /**
3988 * Adds an SVG icon element to another element or component.
3989 *
3990 * @param {string} iconName
3991 * The name of icon. A list of all the icon names can be found at 'sandbox/svg-icons.html'
3992 *
3993 * @param {Element} [el=this.el()]
3994 * Element to set the title on. Defaults to the current Component's element.
3995 *
3996 * @return {Element}
3997 * The newly created icon element.
3998 */
3999 setIcon(iconName, el = this.el()) {
4000 // TODO: In v9 of video.js, we will want to remove font icons entirely.
4001 // This means this check, as well as the others throughout the code, and
4002 // the unecessary CSS for font icons, will need to be removed.
4003 // See https://github.com/videojs/video.js/pull/8260 as to which components
4004 // need updating.
4005 if (!this.player_.options_.experimentalSvgIcons) {
4006 return;
4007 }
4008 const xmlnsURL = 'http://www.w3.org/2000/svg';
4009
4010 // The below creates an element in the format of:
4011 // <span><svg><use>....</use></svg></span>
4012 const iconContainer = createEl('span', {
4013 className: 'vjs-icon-placeholder vjs-svg-icon'
4014 }, {
4015 'aria-hidden': 'true'
4016 });
4017 const svgEl = document__default["default"].createElementNS(xmlnsURL, 'svg');
4018 svgEl.setAttributeNS(null, 'viewBox', '0 0 512 512');
4019 const useEl = document__default["default"].createElementNS(xmlnsURL, 'use');
4020 svgEl.appendChild(useEl);
4021 useEl.setAttributeNS(null, 'href', `#vjs-icon-${iconName}`);
4022 iconContainer.appendChild(svgEl);
4023
4024 // Replace a pre-existing icon if one exists.
4025 if (this.iconIsSet_) {
4026 el.replaceChild(iconContainer, el.querySelector('.vjs-icon-placeholder'));
4027 } else {
4028 el.appendChild(iconContainer);
4029 }
4030 this.iconIsSet_ = true;
4031 return iconContainer;
4032 }
4033
4034 /**
4035 * Add a child `Component` inside the current `Component`.
4036 *
4037 * @param {string|Component} child
4038 * The name or instance of a child to add.
4039 *
4040 * @param {Object} [options={}]
4041 * The key/value store of options that will get passed to children of
4042 * the child.
4043 *
4044 * @param {number} [index=this.children_.length]
4045 * The index to attempt to add a child into.
4046 *
4047 *
4048 * @return {Component}
4049 * The `Component` that gets added as a child. When using a string the
4050 * `Component` will get created by this process.
4051 */
4052 addChild(child, options = {}, index = this.children_.length) {
4053 let component;
4054 let componentName;
4055
4056 // If child is a string, create component with options
4057 if (typeof child === 'string') {
4058 componentName = toTitleCase(child);
4059 const componentClassName = options.componentClass || componentName;
4060
4061 // Set name through options
4062 options.name = componentName;
4063
4064 // Create a new object & element for this controls set
4065 // If there's no .player_, this is a player
4066 const ComponentClass = Component.getComponent(componentClassName);
4067 if (!ComponentClass) {
4068 throw new Error(`Component ${componentClassName} does not exist`);
4069 }
4070
4071 // data stored directly on the videojs object may be
4072 // misidentified as a component to retain
4073 // backwards-compatibility with 4.x. check to make sure the
4074 // component class can be instantiated.
4075 if (typeof ComponentClass !== 'function') {
4076 return null;
4077 }
4078 component = new ComponentClass(this.player_ || this, options);
4079
4080 // child is a component instance
4081 } else {
4082 component = child;
4083 }
4084 if (component.parentComponent_) {
4085 component.parentComponent_.removeChild(component);
4086 }
4087 this.children_.splice(index, 0, component);
4088 component.parentComponent_ = this;
4089 if (typeof component.id === 'function') {
4090 this.childIndex_[component.id()] = component;
4091 }
4092
4093 // If a name wasn't used to create the component, check if we can use the
4094 // name function of the component
4095 componentName = componentName || component.name && toTitleCase(component.name());
4096 if (componentName) {
4097 this.childNameIndex_[componentName] = component;
4098 this.childNameIndex_[toLowerCase(componentName)] = component;
4099 }
4100
4101 // Add the UI object's element to the container div (box)
4102 // Having an element is not required
4103 if (typeof component.el === 'function' && component.el()) {
4104 // If inserting before a component, insert before that component's element
4105 let refNode = null;
4106 if (this.children_[index + 1]) {
4107 // Most children are components, but the video tech is an HTML element
4108 if (this.children_[index + 1].el_) {
4109 refNode = this.children_[index + 1].el_;
4110 } else if (isEl(this.children_[index + 1])) {
4111 refNode = this.children_[index + 1];
4112 }
4113 }
4114 this.contentEl().insertBefore(component.el(), refNode);
4115 }
4116
4117 // Return so it can stored on parent object if desired.
4118 return component;
4119 }
4120
4121 /**
4122 * Remove a child `Component` from this `Component`s list of children. Also removes
4123 * the child `Component`s element from this `Component`s element.
4124 *
4125 * @param {Component} component
4126 * The child `Component` to remove.
4127 */
4128 removeChild(component) {
4129 if (typeof component === 'string') {
4130 component = this.getChild(component);
4131 }
4132 if (!component || !this.children_) {
4133 return;
4134 }
4135 let childFound = false;
4136 for (let i = this.children_.length - 1; i >= 0; i--) {
4137 if (this.children_[i] === component) {
4138 childFound = true;
4139 this.children_.splice(i, 1);
4140 break;
4141 }
4142 }
4143 if (!childFound) {
4144 return;
4145 }
4146 component.parentComponent_ = null;
4147 this.childIndex_[component.id()] = null;
4148 this.childNameIndex_[toTitleCase(component.name())] = null;
4149 this.childNameIndex_[toLowerCase(component.name())] = null;
4150 const compEl = component.el();
4151 if (compEl && compEl.parentNode === this.contentEl()) {
4152 this.contentEl().removeChild(component.el());
4153 }
4154 }
4155
4156 /**
4157 * Add and initialize default child `Component`s based upon options.
4158 */
4159 initChildren() {
4160 const children = this.options_.children;
4161 if (children) {
4162 // `this` is `parent`
4163 const parentOptions = this.options_;
4164 const handleAdd = child => {
4165 const name = child.name;
4166 let opts = child.opts;
4167
4168 // Allow options for children to be set at the parent options
4169 // e.g. videojs(id, { controlBar: false });
4170 // instead of videojs(id, { children: { controlBar: false });
4171 if (parentOptions[name] !== undefined) {
4172 opts = parentOptions[name];
4173 }
4174
4175 // Allow for disabling default components
4176 // e.g. options['children']['posterImage'] = false
4177 if (opts === false) {
4178 return;
4179 }
4180
4181 // Allow options to be passed as a simple boolean if no configuration
4182 // is necessary.
4183 if (opts === true) {
4184 opts = {};
4185 }
4186
4187 // We also want to pass the original player options
4188 // to each component as well so they don't need to
4189 // reach back into the player for options later.
4190 opts.playerOptions = this.options_.playerOptions;
4191
4192 // Create and add the child component.
4193 // Add a direct reference to the child by name on the parent instance.
4194 // If two of the same component are used, different names should be supplied
4195 // for each
4196 const newChild = this.addChild(name, opts);
4197 if (newChild) {
4198 this[name] = newChild;
4199 }
4200 };
4201
4202 // Allow for an array of children details to passed in the options
4203 let workingChildren;
4204 const Tech = Component.getComponent('Tech');
4205 if (Array.isArray(children)) {
4206 workingChildren = children;
4207 } else {
4208 workingChildren = Object.keys(children);
4209 }
4210 workingChildren
4211 // children that are in this.options_ but also in workingChildren would
4212 // give us extra children we do not want. So, we want to filter them out.
4213 .concat(Object.keys(this.options_).filter(function (child) {
4214 return !workingChildren.some(function (wchild) {
4215 if (typeof wchild === 'string') {
4216 return child === wchild;
4217 }
4218 return child === wchild.name;
4219 });
4220 })).map(child => {
4221 let name;
4222 let opts;
4223 if (typeof child === 'string') {
4224 name = child;
4225 opts = children[name] || this.options_[name] || {};
4226 } else {
4227 name = child.name;
4228 opts = child;
4229 }
4230 return {
4231 name,
4232 opts
4233 };
4234 }).filter(child => {
4235 // we have to make sure that child.name isn't in the techOrder since
4236 // techs are registered as Components but can't aren't compatible
4237 // See https://github.com/videojs/video.js/issues/2772
4238 const c = Component.getComponent(child.opts.componentClass || toTitleCase(child.name));
4239 return c && !Tech.isTech(c);
4240 }).forEach(handleAdd);
4241 }
4242 }
4243
4244 /**
4245 * Builds the default DOM class name. Should be overridden by sub-components.
4246 *
4247 * @return {string}
4248 * The DOM class name for this object.
4249 *
4250 * @abstract
4251 */
4252 buildCSSClass() {
4253 // Child classes can include a function that does:
4254 // return 'CLASS NAME' + this._super();
4255 return '';
4256 }
4257
4258 /**
4259 * Bind a listener to the component's ready state.
4260 * Different from event listeners in that if the ready event has already happened
4261 * it will trigger the function immediately.
4262 *
4263 * @param {ReadyCallback} fn
4264 * Function that gets called when the `Component` is ready.
4265 */
4266 ready(fn, sync = false) {
4267 if (!fn) {
4268 return;
4269 }
4270 if (!this.isReady_) {
4271 this.readyQueue_ = this.readyQueue_ || [];
4272 this.readyQueue_.push(fn);
4273 return;
4274 }
4275 if (sync) {
4276 fn.call(this);
4277 } else {
4278 // Call the function asynchronously by default for consistency
4279 this.setTimeout(fn, 1);
4280 }
4281 }
4282
4283 /**
4284 * Trigger all the ready listeners for this `Component`.
4285 *
4286 * @fires Component#ready
4287 */
4288 triggerReady() {
4289 this.isReady_ = true;
4290
4291 // Ensure ready is triggered asynchronously
4292 this.setTimeout(function () {
4293 const readyQueue = this.readyQueue_;
4294
4295 // Reset Ready Queue
4296 this.readyQueue_ = [];
4297 if (readyQueue && readyQueue.length > 0) {
4298 readyQueue.forEach(function (fn) {
4299 fn.call(this);
4300 }, this);
4301 }
4302
4303 // Allow for using event listeners also
4304 /**
4305 * Triggered when a `Component` is ready.
4306 *
4307 * @event Component#ready
4308 * @type {Event}
4309 */
4310 this.trigger('ready');
4311 }, 1);
4312 }
4313
4314 /**
4315 * Find a single DOM element matching a `selector`. This can be within the `Component`s
4316 * `contentEl()` or another custom context.
4317 *
4318 * @param {string} selector
4319 * A valid CSS selector, which will be passed to `querySelector`.
4320 *
4321 * @param {Element|string} [context=this.contentEl()]
4322 * A DOM element within which to query. Can also be a selector string in
4323 * which case the first matching element will get used as context. If
4324 * missing `this.contentEl()` gets used. If `this.contentEl()` returns
4325 * nothing it falls back to `document`.
4326 *
4327 * @return {Element|null}
4328 * the dom element that was found, or null
4329 *
4330 * @see [Information on CSS Selectors](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors)
4331 */
4332 $(selector, context) {
4333 return $(selector, context || this.contentEl());
4334 }
4335
4336 /**
4337 * Finds all DOM element matching a `selector`. This can be within the `Component`s
4338 * `contentEl()` or another custom context.
4339 *
4340 * @param {string} selector
4341 * A valid CSS selector, which will be passed to `querySelectorAll`.
4342 *
4343 * @param {Element|string} [context=this.contentEl()]
4344 * A DOM element within which to query. Can also be a selector string in
4345 * which case the first matching element will get used as context. If
4346 * missing `this.contentEl()` gets used. If `this.contentEl()` returns
4347 * nothing it falls back to `document`.
4348 *
4349 * @return {NodeList}
4350 * a list of dom elements that were found
4351 *
4352 * @see [Information on CSS Selectors](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors)
4353 */
4354 $$(selector, context) {
4355 return $$(selector, context || this.contentEl());
4356 }
4357
4358 /**
4359 * Check if a component's element has a CSS class name.
4360 *
4361 * @param {string} classToCheck
4362 * CSS class name to check.
4363 *
4364 * @return {boolean}
4365 * - True if the `Component` has the class.
4366 * - False if the `Component` does not have the class`
4367 */
4368 hasClass(classToCheck) {
4369 return hasClass(this.el_, classToCheck);
4370 }
4371
4372 /**
4373 * Add a CSS class name to the `Component`s element.
4374 *
4375 * @param {...string} classesToAdd
4376 * One or more CSS class name to add.
4377 */
4378 addClass(...classesToAdd) {
4379 addClass(this.el_, ...classesToAdd);
4380 }
4381
4382 /**
4383 * Remove a CSS class name from the `Component`s element.
4384 *
4385 * @param {...string} classesToRemove
4386 * One or more CSS class name to remove.
4387 */
4388 removeClass(...classesToRemove) {
4389 removeClass(this.el_, ...classesToRemove);
4390 }
4391
4392 /**
4393 * Add or remove a CSS class name from the component's element.
4394 * - `classToToggle` gets added when {@link Component#hasClass} would return false.
4395 * - `classToToggle` gets removed when {@link Component#hasClass} would return true.
4396 *
4397 * @param {string} classToToggle
4398 * The class to add or remove. Passed to DOMTokenList's toggle()
4399 *
4400 * @param {boolean|Dom.PredicateCallback} [predicate]
4401 * A boolean or function that returns a boolean. Passed to DOMTokenList's toggle().
4402 */
4403 toggleClass(classToToggle, predicate) {
4404 toggleClass(this.el_, classToToggle, predicate);
4405 }
4406
4407 /**
4408 * Show the `Component`s element if it is hidden by removing the
4409 * 'vjs-hidden' class name from it.
4410 */
4411 show() {
4412 this.removeClass('vjs-hidden');
4413 }
4414
4415 /**
4416 * Hide the `Component`s element if it is currently showing by adding the
4417 * 'vjs-hidden` class name to it.
4418 */
4419 hide() {
4420 this.addClass('vjs-hidden');
4421 }
4422
4423 /**
4424 * Lock a `Component`s element in its visible state by adding the 'vjs-lock-showing'
4425 * class name to it. Used during fadeIn/fadeOut.
4426 *
4427 * @private
4428 */
4429 lockShowing() {
4430 this.addClass('vjs-lock-showing');
4431 }
4432
4433 /**
4434 * Unlock a `Component`s element from its visible state by removing the 'vjs-lock-showing'
4435 * class name from it. Used during fadeIn/fadeOut.
4436 *
4437 * @private
4438 */
4439 unlockShowing() {
4440 this.removeClass('vjs-lock-showing');
4441 }
4442
4443 /**
4444 * Get the value of an attribute on the `Component`s element.
4445 *
4446 * @param {string} attribute
4447 * Name of the attribute to get the value from.
4448 *
4449 * @return {string|null}
4450 * - The value of the attribute that was asked for.
4451 * - Can be an empty string on some browsers if the attribute does not exist
4452 * or has no value
4453 * - Most browsers will return null if the attribute does not exist or has
4454 * no value.
4455 *
4456 * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute}
4457 */
4458 getAttribute(attribute) {
4459 return getAttribute(this.el_, attribute);
4460 }
4461
4462 /**
4463 * Set the value of an attribute on the `Component`'s element
4464 *
4465 * @param {string} attribute
4466 * Name of the attribute to set.
4467 *
4468 * @param {string} value
4469 * Value to set the attribute to.
4470 *
4471 * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute}
4472 */
4473 setAttribute(attribute, value) {
4474 setAttribute(this.el_, attribute, value);
4475 }
4476
4477 /**
4478 * Remove an attribute from the `Component`s element.
4479 *
4480 * @param {string} attribute
4481 * Name of the attribute to remove.
4482 *
4483 * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/removeAttribute}
4484 */
4485 removeAttribute(attribute) {
4486 removeAttribute(this.el_, attribute);
4487 }
4488
4489 /**
4490 * Get or set the width of the component based upon the CSS styles.
4491 * See {@link Component#dimension} for more detailed information.
4492 *
4493 * @param {number|string} [num]
4494 * The width that you want to set postfixed with '%', 'px' or nothing.
4495 *
4496 * @param {boolean} [skipListeners]
4497 * Skip the componentresize event trigger
4498 *
4499 * @return {number|undefined}
4500 * The width when getting, zero if there is no width
4501 */
4502 width(num, skipListeners) {
4503 return this.dimension('width', num, skipListeners);
4504 }
4505
4506 /**
4507 * Get or set the height of the component based upon the CSS styles.
4508 * See {@link Component#dimension} for more detailed information.
4509 *
4510 * @param {number|string} [num]
4511 * The height that you want to set postfixed with '%', 'px' or nothing.
4512 *
4513 * @param {boolean} [skipListeners]
4514 * Skip the componentresize event trigger
4515 *
4516 * @return {number|undefined}
4517 * The height when getting, zero if there is no height
4518 */
4519 height(num, skipListeners) {
4520 return this.dimension('height', num, skipListeners);
4521 }
4522
4523 /**
4524 * Set both the width and height of the `Component` element at the same time.
4525 *
4526 * @param {number|string} width
4527 * Width to set the `Component`s element to.
4528 *
4529 * @param {number|string} height
4530 * Height to set the `Component`s element to.
4531 */
4532 dimensions(width, height) {
4533 // Skip componentresize listeners on width for optimization
4534 this.width(width, true);
4535 this.height(height);
4536 }
4537
4538 /**
4539 * Get or set width or height of the `Component` element. This is the shared code
4540 * for the {@link Component#width} and {@link Component#height}.
4541 *
4542 * Things to know:
4543 * - If the width or height in an number this will return the number postfixed with 'px'.
4544 * - If the width/height is a percent this will return the percent postfixed with '%'
4545 * - Hidden elements have a width of 0 with `window.getComputedStyle`. This function
4546 * defaults to the `Component`s `style.width` and falls back to `window.getComputedStyle`.
4547 * See [this]{@link http://www.foliotek.com/devblog/getting-the-width-of-a-hidden-element-with-jquery-using-width/}
4548 * for more information
4549 * - If you want the computed style of the component, use {@link Component#currentWidth}
4550 * and {@link {Component#currentHeight}
4551 *
4552 * @fires Component#componentresize
4553 *
4554 * @param {string} widthOrHeight
4555 8 'width' or 'height'
4556 *
4557 * @param {number|string} [num]
4558 8 New dimension
4559 *
4560 * @param {boolean} [skipListeners]
4561 * Skip componentresize event trigger
4562 *
4563 * @return {number|undefined}
4564 * The dimension when getting or 0 if unset
4565 */
4566 dimension(widthOrHeight, num, skipListeners) {
4567 if (num !== undefined) {
4568 // Set to zero if null or literally NaN (NaN !== NaN)
4569 if (num === null || num !== num) {
4570 num = 0;
4571 }
4572
4573 // Check if using css width/height (% or px) and adjust
4574 if (('' + num).indexOf('%') !== -1 || ('' + num).indexOf('px') !== -1) {
4575 this.el_.style[widthOrHeight] = num;
4576 } else if (num === 'auto') {
4577 this.el_.style[widthOrHeight] = '';
4578 } else {
4579 this.el_.style[widthOrHeight] = num + 'px';
4580 }
4581
4582 // skipListeners allows us to avoid triggering the resize event when setting both width and height
4583 if (!skipListeners) {
4584 /**
4585 * Triggered when a component is resized.
4586 *
4587 * @event Component#componentresize
4588 * @type {Event}
4589 */
4590 this.trigger('componentresize');
4591 }
4592 return;
4593 }
4594
4595 // Not setting a value, so getting it
4596 // Make sure element exists
4597 if (!this.el_) {
4598 return 0;
4599 }
4600
4601 // Get dimension value from style
4602 const val = this.el_.style[widthOrHeight];
4603 const pxIndex = val.indexOf('px');
4604 if (pxIndex !== -1) {
4605 // Return the pixel value with no 'px'
4606 return parseInt(val.slice(0, pxIndex), 10);
4607 }
4608
4609 // No px so using % or no style was set, so falling back to offsetWidth/height
4610 // If component has display:none, offset will return 0
4611 // TODO: handle display:none and no dimension style using px
4612 return parseInt(this.el_['offset' + toTitleCase(widthOrHeight)], 10);
4613 }
4614
4615 /**
4616 * Get the computed width or the height of the component's element.
4617 *
4618 * Uses `window.getComputedStyle`.
4619 *
4620 * @param {string} widthOrHeight
4621 * A string containing 'width' or 'height'. Whichever one you want to get.
4622 *
4623 * @return {number}
4624 * The dimension that gets asked for or 0 if nothing was set
4625 * for that dimension.
4626 */
4627 currentDimension(widthOrHeight) {
4628 let computedWidthOrHeight = 0;
4629 if (widthOrHeight !== 'width' && widthOrHeight !== 'height') {
4630 throw new Error('currentDimension only accepts width or height value');
4631 }
4632 computedWidthOrHeight = computedStyle(this.el_, widthOrHeight);
4633
4634 // remove 'px' from variable and parse as integer
4635 computedWidthOrHeight = parseFloat(computedWidthOrHeight);
4636
4637 // if the computed value is still 0, it's possible that the browser is lying
4638 // and we want to check the offset values.
4639 // This code also runs wherever getComputedStyle doesn't exist.
4640 if (computedWidthOrHeight === 0 || isNaN(computedWidthOrHeight)) {
4641 const rule = `offset${toTitleCase(widthOrHeight)}`;
4642 computedWidthOrHeight = this.el_[rule];
4643 }
4644 return computedWidthOrHeight;
4645 }
4646
4647 /**
4648 * An object that contains width and height values of the `Component`s
4649 * computed style. Uses `window.getComputedStyle`.
4650 *
4651 * @typedef {Object} Component~DimensionObject
4652 *
4653 * @property {number} width
4654 * The width of the `Component`s computed style.
4655 *
4656 * @property {number} height
4657 * The height of the `Component`s computed style.
4658 */
4659
4660 /**
4661 * Get an object that contains computed width and height values of the
4662 * component's element.
4663 *
4664 * Uses `window.getComputedStyle`.
4665 *
4666 * @return {Component~DimensionObject}
4667 * The computed dimensions of the component's element.
4668 */
4669 currentDimensions() {
4670 return {
4671 width: this.currentDimension('width'),
4672 height: this.currentDimension('height')
4673 };
4674 }
4675
4676 /**
4677 * Get the computed width of the component's element.
4678 *
4679 * Uses `window.getComputedStyle`.
4680 *
4681 * @return {number}
4682 * The computed width of the component's element.
4683 */
4684 currentWidth() {
4685 return this.currentDimension('width');
4686 }
4687
4688 /**
4689 * Get the computed height of the component's element.
4690 *
4691 * Uses `window.getComputedStyle`.
4692 *
4693 * @return {number}
4694 * The computed height of the component's element.
4695 */
4696 currentHeight() {
4697 return this.currentDimension('height');
4698 }
4699
4700 /**
4701 * Retrieves the position and size information of the component's element.
4702 *
4703 * @return {Object} An object with `boundingClientRect` and `center` properties.
4704 * - `boundingClientRect`: An object with properties `x`, `y`, `width`,
4705 * `height`, `top`, `right`, `bottom`, and `left`, representing
4706 * the bounding rectangle of the element.
4707 * - `center`: An object with properties `x` and `y`, representing
4708 * the center point of the element. `width` and `height` are set to 0.
4709 */
4710 getPositions() {
4711 const rect = this.el_.getBoundingClientRect();
4712
4713 // Creating objects that mirror DOMRectReadOnly for boundingClientRect and center
4714 const boundingClientRect = {
4715 x: rect.x,
4716 y: rect.y,
4717 width: rect.width,
4718 height: rect.height,
4719 top: rect.top,
4720 right: rect.right,
4721 bottom: rect.bottom,
4722 left: rect.left
4723 };
4724
4725 // Calculating the center position
4726 const center = {
4727 x: rect.left + rect.width / 2,
4728 y: rect.top + rect.height / 2,
4729 width: 0,
4730 height: 0,
4731 top: rect.top + rect.height / 2,
4732 right: rect.left + rect.width / 2,
4733 bottom: rect.top + rect.height / 2,
4734 left: rect.left + rect.width / 2
4735 };
4736 return {
4737 boundingClientRect,
4738 center
4739 };
4740 }
4741
4742 /**
4743 * Set the focus to this component
4744 */
4745 focus() {
4746 this.el_.focus();
4747 }
4748
4749 /**
4750 * Remove the focus from this component
4751 */
4752 blur() {
4753 this.el_.blur();
4754 }
4755
4756 /**
4757 * When this Component receives a `keydown` event which it does not process,
4758 * it passes the event to the Player for handling.
4759 *
4760 * @param {KeyboardEvent} event
4761 * The `keydown` event that caused this function to be called.
4762 */
4763 handleKeyDown(event) {
4764 if (this.player_) {
4765 // We only stop propagation here because we want unhandled events to fall
4766 // back to the browser. Exclude Tab for focus trapping, exclude also when spatialNavigation is enabled.
4767 if (event.key !== 'Tab' && !(this.player_.options_.playerOptions.spatialNavigation && this.player_.options_.playerOptions.spatialNavigation.enabled)) {
4768 event.stopPropagation();
4769 }
4770 this.player_.handleKeyDown(event);
4771 }
4772 }
4773
4774 /**
4775 * Many components used to have a `handleKeyPress` method, which was poorly
4776 * named because it listened to a `keydown` event. This method name now
4777 * delegates to `handleKeyDown`. This means anyone calling `handleKeyPress`
4778 * will not see their method calls stop working.
4779 *
4780 * @param {KeyboardEvent} event
4781 * The event that caused this function to be called.
4782 */
4783 handleKeyPress(event) {
4784 this.handleKeyDown(event);
4785 }
4786
4787 /**
4788 * Emit a 'tap' events when touch event support gets detected. This gets used to
4789 * support toggling the controls through a tap on the video. They get enabled
4790 * because every sub-component would have extra overhead otherwise.
4791 *
4792 * @protected
4793 * @fires Component#tap
4794 * @listens Component#touchstart
4795 * @listens Component#touchmove
4796 * @listens Component#touchleave
4797 * @listens Component#touchcancel
4798 * @listens Component#touchend
4799 */
4800 emitTapEvents() {
4801 // Track the start time so we can determine how long the touch lasted
4802 let touchStart = 0;
4803 let firstTouch = null;
4804
4805 // Maximum movement allowed during a touch event to still be considered a tap
4806 // Other popular libs use anywhere from 2 (hammer.js) to 15,
4807 // so 10 seems like a nice, round number.
4808 const tapMovementThreshold = 10;
4809
4810 // The maximum length a touch can be while still being considered a tap
4811 const touchTimeThreshold = 200;
4812 let couldBeTap;
4813 this.on('touchstart', function (event) {
4814 // If more than one finger, don't consider treating this as a click
4815 if (event.touches.length === 1) {
4816 // Copy pageX/pageY from the object
4817 firstTouch = {
4818 pageX: event.touches[0].pageX,
4819 pageY: event.touches[0].pageY
4820 };
4821 // Record start time so we can detect a tap vs. "touch and hold"
4822 touchStart = window__default["default"].performance.now();
4823 // Reset couldBeTap tracking
4824 couldBeTap = true;
4825 }
4826 });
4827 this.on('touchmove', function (event) {
4828 // If more than one finger, don't consider treating this as a click
4829 if (event.touches.length > 1) {
4830 couldBeTap = false;
4831 } else if (firstTouch) {
4832 // Some devices will throw touchmoves for all but the slightest of taps.
4833 // So, if we moved only a small distance, this could still be a tap
4834 const xdiff = event.touches[0].pageX - firstTouch.pageX;
4835 const ydiff = event.touches[0].pageY - firstTouch.pageY;
4836 const touchDistance = Math.sqrt(xdiff * xdiff + ydiff * ydiff);
4837 if (touchDistance > tapMovementThreshold) {
4838 couldBeTap = false;
4839 }
4840 }
4841 });
4842 const noTap = function () {
4843 couldBeTap = false;
4844 };
4845
4846 // TODO: Listen to the original target. http://youtu.be/DujfpXOKUp8?t=13m8s
4847 this.on('touchleave', noTap);
4848 this.on('touchcancel', noTap);
4849
4850 // When the touch ends, measure how long it took and trigger the appropriate
4851 // event
4852 this.on('touchend', function (event) {
4853 firstTouch = null;
4854 // Proceed only if the touchmove/leave/cancel event didn't happen
4855 if (couldBeTap === true) {
4856 // Measure how long the touch lasted
4857 const touchTime = window__default["default"].performance.now() - touchStart;
4858
4859 // Make sure the touch was less than the threshold to be considered a tap
4860 if (touchTime < touchTimeThreshold) {
4861 // Don't let browser turn this into a click
4862 event.preventDefault();
4863 /**
4864 * Triggered when a `Component` is tapped.
4865 *
4866 * @event Component#tap
4867 * @type {MouseEvent}
4868 */
4869 this.trigger('tap');
4870 // It may be good to copy the touchend event object and change the
4871 // type to tap, if the other event properties aren't exact after
4872 // Events.fixEvent runs (e.g. event.target)
4873 }
4874 }
4875 });
4876 }
4877
4878 /**
4879 * This function reports user activity whenever touch events happen. This can get
4880 * turned off by any sub-components that wants touch events to act another way.
4881 *
4882 * Report user touch activity when touch events occur. User activity gets used to
4883 * determine when controls should show/hide. It is simple when it comes to mouse
4884 * events, because any mouse event should show the controls. So we capture mouse
4885 * events that bubble up to the player and report activity when that happens.
4886 * With touch events it isn't as easy as `touchstart` and `touchend` toggle player
4887 * controls. So touch events can't help us at the player level either.
4888 *
4889 * User activity gets checked asynchronously. So what could happen is a tap event
4890 * on the video turns the controls off. Then the `touchend` event bubbles up to
4891 * the player. Which, if it reported user activity, would turn the controls right
4892 * back on. We also don't want to completely block touch events from bubbling up.
4893 * Furthermore a `touchmove` event and anything other than a tap, should not turn
4894 * controls back on.
4895 *
4896 * @listens Component#touchstart
4897 * @listens Component#touchmove
4898 * @listens Component#touchend
4899 * @listens Component#touchcancel
4900 */
4901 enableTouchActivity() {
4902 // Don't continue if the root player doesn't support reporting user activity
4903 if (!this.player() || !this.player().reportUserActivity) {
4904 return;
4905 }
4906
4907 // listener for reporting that the user is active
4908 const report = bind_(this.player(), this.player().reportUserActivity);
4909 let touchHolding;
4910 this.on('touchstart', function () {
4911 report();
4912 // For as long as the they are touching the device or have their mouse down,
4913 // we consider them active even if they're not moving their finger or mouse.
4914 // So we want to continue to update that they are active
4915 this.clearInterval(touchHolding);
4916 // report at the same interval as activityCheck
4917 touchHolding = this.setInterval(report, 250);
4918 });
4919 const touchEnd = function (event) {
4920 report();
4921 // stop the interval that maintains activity if the touch is holding
4922 this.clearInterval(touchHolding);
4923 };
4924 this.on('touchmove', report);
4925 this.on('touchend', touchEnd);
4926 this.on('touchcancel', touchEnd);
4927 }
4928
4929 /**
4930 * A callback that has no parameters and is bound into `Component`s context.
4931 *
4932 * @callback Component~GenericCallback
4933 * @this Component
4934 */
4935
4936 /**
4937 * Creates a function that runs after an `x` millisecond timeout. This function is a
4938 * wrapper around `window.setTimeout`. There are a few reasons to use this one
4939 * instead though:
4940 * 1. It gets cleared via {@link Component#clearTimeout} when
4941 * {@link Component#dispose} gets called.
4942 * 2. The function callback will gets turned into a {@link Component~GenericCallback}
4943 *
4944 * > Note: You can't use `window.clearTimeout` on the id returned by this function. This
4945 * will cause its dispose listener not to get cleaned up! Please use
4946 * {@link Component#clearTimeout} or {@link Component#dispose} instead.
4947 *
4948 * @param {Component~GenericCallback} fn
4949 * The function that will be run after `timeout`.
4950 *
4951 * @param {number} timeout
4952 * Timeout in milliseconds to delay before executing the specified function.
4953 *
4954 * @return {number}
4955 * Returns a timeout ID that gets used to identify the timeout. It can also
4956 * get used in {@link Component#clearTimeout} to clear the timeout that
4957 * was set.
4958 *
4959 * @listens Component#dispose
4960 * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setTimeout}
4961 */
4962 setTimeout(fn, timeout) {
4963 // declare as variables so they are properly available in timeout function
4964 // eslint-disable-next-line
4965 var timeoutId;
4966 fn = bind_(this, fn);
4967 this.clearTimersOnDispose_();
4968 timeoutId = window__default["default"].setTimeout(() => {
4969 if (this.setTimeoutIds_.has(timeoutId)) {
4970 this.setTimeoutIds_.delete(timeoutId);
4971 }
4972 fn();
4973 }, timeout);
4974 this.setTimeoutIds_.add(timeoutId);
4975 return timeoutId;
4976 }
4977
4978 /**
4979 * Clears a timeout that gets created via `window.setTimeout` or
4980 * {@link Component#setTimeout}. If you set a timeout via {@link Component#setTimeout}
4981 * use this function instead of `window.clearTimout`. If you don't your dispose
4982 * listener will not get cleaned up until {@link Component#dispose}!
4983 *
4984 * @param {number} timeoutId
4985 * The id of the timeout to clear. The return value of
4986 * {@link Component#setTimeout} or `window.setTimeout`.
4987 *
4988 * @return {number}
4989 * Returns the timeout id that was cleared.
4990 *
4991 * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearTimeout}
4992 */
4993 clearTimeout(timeoutId) {
4994 if (this.setTimeoutIds_.has(timeoutId)) {
4995 this.setTimeoutIds_.delete(timeoutId);
4996 window__default["default"].clearTimeout(timeoutId);
4997 }
4998 return timeoutId;
4999 }
5000
5001 /**
5002 * Creates a function that gets run every `x` milliseconds. This function is a wrapper
5003 * around `window.setInterval`. There are a few reasons to use this one instead though.
5004 * 1. It gets cleared via {@link Component#clearInterval} when
5005 * {@link Component#dispose} gets called.
5006 * 2. The function callback will be a {@link Component~GenericCallback}
5007 *
5008 * @param {Component~GenericCallback} fn
5009 * The function to run every `x` seconds.
5010 *
5011 * @param {number} interval
5012 * Execute the specified function every `x` milliseconds.
5013 *
5014 * @return {number}
5015 * Returns an id that can be used to identify the interval. It can also be be used in
5016 * {@link Component#clearInterval} to clear the interval.
5017 *
5018 * @listens Component#dispose
5019 * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setInterval}
5020 */
5021 setInterval(fn, interval) {
5022 fn = bind_(this, fn);
5023 this.clearTimersOnDispose_();
5024 const intervalId = window__default["default"].setInterval(fn, interval);
5025 this.setIntervalIds_.add(intervalId);
5026 return intervalId;
5027 }
5028
5029 /**
5030 * Clears an interval that gets created via `window.setInterval` or
5031 * {@link Component#setInterval}. If you set an interval via {@link Component#setInterval}
5032 * use this function instead of `window.clearInterval`. If you don't your dispose
5033 * listener will not get cleaned up until {@link Component#dispose}!
5034 *
5035 * @param {number} intervalId
5036 * The id of the interval to clear. The return value of
5037 * {@link Component#setInterval} or `window.setInterval`.
5038 *
5039 * @return {number}
5040 * Returns the interval id that was cleared.
5041 *
5042 * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearInterval}
5043 */
5044 clearInterval(intervalId) {
5045 if (this.setIntervalIds_.has(intervalId)) {
5046 this.setIntervalIds_.delete(intervalId);
5047 window__default["default"].clearInterval(intervalId);
5048 }
5049 return intervalId;
5050 }
5051
5052 /**
5053 * Queues up a callback to be passed to requestAnimationFrame (rAF), but
5054 * with a few extra bonuses:
5055 *
5056 * - Supports browsers that do not support rAF by falling back to
5057 * {@link Component#setTimeout}.
5058 *
5059 * - The callback is turned into a {@link Component~GenericCallback} (i.e.
5060 * bound to the component).
5061 *
5062 * - Automatic cancellation of the rAF callback is handled if the component
5063 * is disposed before it is called.
5064 *
5065 * @param {Component~GenericCallback} fn
5066 * A function that will be bound to this component and executed just
5067 * before the browser's next repaint.
5068 *
5069 * @return {number}
5070 * Returns an rAF ID that gets used to identify the timeout. It can
5071 * also be used in {@link Component#cancelAnimationFrame} to cancel
5072 * the animation frame callback.
5073 *
5074 * @listens Component#dispose
5075 * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame}
5076 */
5077 requestAnimationFrame(fn) {
5078 this.clearTimersOnDispose_();
5079
5080 // declare as variables so they are properly available in rAF function
5081 // eslint-disable-next-line
5082 var id;
5083 fn = bind_(this, fn);
5084 id = window__default["default"].requestAnimationFrame(() => {
5085 if (this.rafIds_.has(id)) {
5086 this.rafIds_.delete(id);
5087 }
5088 fn();
5089 });
5090 this.rafIds_.add(id);
5091 return id;
5092 }
5093
5094 /**
5095 * Request an animation frame, but only one named animation
5096 * frame will be queued. Another will never be added until
5097 * the previous one finishes.
5098 *
5099 * @param {string} name
5100 * The name to give this requestAnimationFrame
5101 *
5102 * @param {Component~GenericCallback} fn
5103 * A function that will be bound to this component and executed just
5104 * before the browser's next repaint.
5105 */
5106 requestNamedAnimationFrame(name, fn) {
5107 if (this.namedRafs_.has(name)) {
5108 this.cancelNamedAnimationFrame(name);
5109 }
5110 this.clearTimersOnDispose_();
5111 fn = bind_(this, fn);
5112 const id = this.requestAnimationFrame(() => {
5113 fn();
5114 if (this.namedRafs_.has(name)) {
5115 this.namedRafs_.delete(name);
5116 }
5117 });
5118 this.namedRafs_.set(name, id);
5119 return name;
5120 }
5121
5122 /**
5123 * Cancels a current named animation frame if it exists.
5124 *
5125 * @param {string} name
5126 * The name of the requestAnimationFrame to cancel.
5127 */
5128 cancelNamedAnimationFrame(name) {
5129 if (!this.namedRafs_.has(name)) {
5130 return;
5131 }
5132 this.cancelAnimationFrame(this.namedRafs_.get(name));
5133 this.namedRafs_.delete(name);
5134 }
5135
5136 /**
5137 * Cancels a queued callback passed to {@link Component#requestAnimationFrame}
5138 * (rAF).
5139 *
5140 * If you queue an rAF callback via {@link Component#requestAnimationFrame},
5141 * use this function instead of `window.cancelAnimationFrame`. If you don't,
5142 * your dispose listener will not get cleaned up until {@link Component#dispose}!
5143 *
5144 * @param {number} id
5145 * The rAF ID to clear. The return value of {@link Component#requestAnimationFrame}.
5146 *
5147 * @return {number}
5148 * Returns the rAF ID that was cleared.
5149 *
5150 * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/cancelAnimationFrame}
5151 */
5152 cancelAnimationFrame(id) {
5153 if (this.rafIds_.has(id)) {
5154 this.rafIds_.delete(id);
5155 window__default["default"].cancelAnimationFrame(id);
5156 }
5157 return id;
5158 }
5159
5160 /**
5161 * A function to setup `requestAnimationFrame`, `setTimeout`,
5162 * and `setInterval`, clearing on dispose.
5163 *
5164 * > Previously each timer added and removed dispose listeners on it's own.
5165 * For better performance it was decided to batch them all, and use `Set`s
5166 * to track outstanding timer ids.
5167 *
5168 * @private
5169 */
5170 clearTimersOnDispose_() {
5171 if (this.clearingTimersOnDispose_) {
5172 return;
5173 }
5174 this.clearingTimersOnDispose_ = true;
5175 this.one('dispose', () => {
5176 [['namedRafs_', 'cancelNamedAnimationFrame'], ['rafIds_', 'cancelAnimationFrame'], ['setTimeoutIds_', 'clearTimeout'], ['setIntervalIds_', 'clearInterval']].forEach(([idName, cancelName]) => {
5177 // for a `Set` key will actually be the value again
5178 // so forEach((val, val) =>` but for maps we want to use
5179 // the key.
5180 this[idName].forEach((val, key) => this[cancelName](key));
5181 });
5182 this.clearingTimersOnDispose_ = false;
5183 });
5184 }
5185
5186 /**
5187 * Decide whether an element is actually disabled or not.
5188 *
5189 * @function isActuallyDisabled
5190 * @param element {Node}
5191 * @return {boolean}
5192 *
5193 * @see {@link https://html.spec.whatwg.org/multipage/semantics-other.html#concept-element-disabled}
5194 */
5195 getIsDisabled() {
5196 return Boolean(this.el_.disabled);
5197 }
5198
5199 /**
5200 * Decide whether the element is expressly inert or not.
5201 *
5202 * @see {@link https://html.spec.whatwg.org/multipage/interaction.html#expressly-inert}
5203 * @function isExpresslyInert
5204 * @param element {Node}
5205 * @return {boolean}
5206 */
5207 getIsExpresslyInert() {
5208 return this.el_.inert && !this.el_.ownerDocument.documentElement.inert;
5209 }
5210
5211 /**
5212 * Determine whether or not this component can be considered as focusable component.
5213 *
5214 * @param {HTMLElement} el - The HTML element representing the component.
5215 * @return {boolean}
5216 * If the component can be focused, will be `true`. Otherwise, `false`.
5217 */
5218 getIsFocusable(el) {
5219 const element = el || this.el_;
5220 return element.tabIndex >= 0 && !(this.getIsDisabled() || this.getIsExpresslyInert());
5221 }
5222
5223 /**
5224 * Determine whether or not this component is currently visible/enabled/etc...
5225 *
5226 * @param {HTMLElement} el - The HTML element representing the component.
5227 * @return {boolean}
5228 * If the component can is currently visible & enabled, will be `true`. Otherwise, `false`.
5229 */
5230 getIsAvailableToBeFocused(el) {
5231 /**
5232 * Decide the style property of this element is specified whether it's visible or not.
5233 *
5234 * @function isVisibleStyleProperty
5235 * @param element {CSSStyleDeclaration}
5236 * @return {boolean}
5237 */
5238 function isVisibleStyleProperty(element) {
5239 const elementStyle = window__default["default"].getComputedStyle(element, null);
5240 const thisVisibility = elementStyle.getPropertyValue('visibility');
5241 const thisDisplay = elementStyle.getPropertyValue('display');
5242 const invisibleStyle = ['hidden', 'collapse'];
5243 return thisDisplay !== 'none' && !invisibleStyle.includes(thisVisibility);
5244 }
5245
5246 /**
5247 * Decide whether the element is being rendered or not.
5248 * 1. If an element has the style as "visibility: hidden | collapse" or "display: none", it is not being rendered.
5249 * 2. If an element has the style as "opacity: 0", it is not being rendered.(that is, invisible).
5250 * 3. If width and height of an element are explicitly set to 0, it is not being rendered.
5251 * 4. If a parent element is hidden, an element itself is not being rendered.
5252 * (CSS visibility property and display property are inherited.)
5253 *
5254 * @see {@link https://html.spec.whatwg.org/multipage/rendering.html#being-rendered}
5255 * @function isBeingRendered
5256 * @param element {Node}
5257 * @return {boolean}
5258 */
5259 function isBeingRendered(element) {
5260 if (!isVisibleStyleProperty(element.parentElement)) {
5261 return false;
5262 }
5263 if (!isVisibleStyleProperty(element) || element.style.opacity === '0' || window__default["default"].getComputedStyle(element).height === '0px' || window__default["default"].getComputedStyle(element).width === '0px') {
5264 return false;
5265 }
5266 return true;
5267 }
5268
5269 /**
5270 * Determine if the element is visible for the user or not.
5271 * 1. If an element sum of its offsetWidth, offsetHeight, height and width is less than 1 is not visible.
5272 * 2. If elementCenter.x is less than is not visible.
5273 * 3. If elementCenter.x is more than the document's width is not visible.
5274 * 4. If elementCenter.y is less than 0 is not visible.
5275 * 5. If elementCenter.y is the document's height is not visible.
5276 *
5277 * @function isVisible
5278 * @param element {Node}
5279 * @return {boolean}
5280 */
5281 function isVisible(element) {
5282 if (element.offsetWidth + element.offsetHeight + element.getBoundingClientRect().height + element.getBoundingClientRect().width === 0) {
5283 return false;
5284 }
5285
5286 // Define elementCenter object with props of x and y
5287 // x: Left position relative to the viewport plus element's width (no margin) divided between 2.
5288 // y: Top position relative to the viewport plus element's height (no margin) divided between 2.
5289 const elementCenter = {
5290 x: element.getBoundingClientRect().left + element.offsetWidth / 2,
5291 y: element.getBoundingClientRect().top + element.offsetHeight / 2
5292 };
5293 if (elementCenter.x < 0) {
5294 return false;
5295 }
5296 if (elementCenter.x > (document__default["default"].documentElement.clientWidth || window__default["default"].innerWidth)) {
5297 return false;
5298 }
5299 if (elementCenter.y < 0) {
5300 return false;
5301 }
5302 if (elementCenter.y > (document__default["default"].documentElement.clientHeight || window__default["default"].innerHeight)) {
5303 return false;
5304 }
5305 let pointContainer = document__default["default"].elementFromPoint(elementCenter.x, elementCenter.y);
5306 while (pointContainer) {
5307 if (pointContainer === element) {
5308 return true;
5309 }
5310 if (pointContainer.parentNode) {
5311 pointContainer = pointContainer.parentNode;
5312 } else {
5313 return false;
5314 }
5315 }
5316 }
5317
5318 // If no DOM element was passed as argument use this component's element.
5319 if (!el) {
5320 el = this.el();
5321 }
5322
5323 // If element is visible, is being rendered & either does not have a parent element or its tabIndex is not negative.
5324 if (isVisible(el) && isBeingRendered(el) && (!el.parentElement || el.tabIndex >= 0)) {
5325 return true;
5326 }
5327 return false;
5328 }
5329
5330 /**
5331 * Register a `Component` with `videojs` given the name and the component.
5332 *
5333 * > NOTE: {@link Tech}s should not be registered as a `Component`. {@link Tech}s
5334 * should be registered using {@link Tech.registerTech} or
5335 * {@link videojs:videojs.registerTech}.
5336 *
5337 * > NOTE: This function can also be seen on videojs as
5338 * {@link videojs:videojs.registerComponent}.
5339 *
5340 * @param {string} name
5341 * The name of the `Component` to register.
5342 *
5343 * @param {Component} ComponentToRegister
5344 * The `Component` class to register.
5345 *
5346 * @return {Component}
5347 * The `Component` that was registered.
5348 */
5349 static registerComponent(name, ComponentToRegister) {
5350 if (typeof name !== 'string' || !name) {
5351 throw new Error(`Illegal component name, "${name}"; must be a non-empty string.`);
5352 }
5353 const Tech = Component.getComponent('Tech');
5354
5355 // We need to make sure this check is only done if Tech has been registered.
5356 const isTech = Tech && Tech.isTech(ComponentToRegister);
5357 const isComp = Component === ComponentToRegister || Component.prototype.isPrototypeOf(ComponentToRegister.prototype);
5358 if (isTech || !isComp) {
5359 let reason;
5360 if (isTech) {
5361 reason = 'techs must be registered using Tech.registerTech()';
5362 } else {
5363 reason = 'must be a Component subclass';
5364 }
5365 throw new Error(`Illegal component, "${name}"; ${reason}.`);
5366 }
5367 name = toTitleCase(name);
5368 if (!Component.components_) {
5369 Component.components_ = {};
5370 }
5371 const Player = Component.getComponent('Player');
5372 if (name === 'Player' && Player && Player.players) {
5373 const players = Player.players;
5374 const playerNames = Object.keys(players);
5375
5376 // If we have players that were disposed, then their name will still be
5377 // in Players.players. So, we must loop through and verify that the value
5378 // for each item is not null. This allows registration of the Player component
5379 // after all players have been disposed or before any were created.
5380 if (players && playerNames.length > 0 && playerNames.map(pname => players[pname]).every(Boolean)) {
5381 throw new Error('Can not register Player component after player has been created.');
5382 }
5383 }
5384 Component.components_[name] = ComponentToRegister;
5385 Component.components_[toLowerCase(name)] = ComponentToRegister;
5386 return ComponentToRegister;
5387 }
5388
5389 /**
5390 * Get a `Component` based on the name it was registered with.
5391 *
5392 * @param {string} name
5393 * The Name of the component to get.
5394 *
5395 * @return {typeof Component}
5396 * The `Component` that got registered under the given name.
5397 */
5398 static getComponent(name) {
5399 if (!name || !Component.components_) {
5400 return;
5401 }
5402 return Component.components_[name];
5403 }
5404}
5405Component.registerComponent('Component', Component);
5406
5407/**
5408 * @file time.js
5409 * @module time
5410 */
5411
5412/**
5413 * Returns the time for the specified index at the start or end
5414 * of a TimeRange object.
5415 *
5416 * @typedef {Function} TimeRangeIndex
5417 *
5418 * @param {number} [index=0]
5419 * The range number to return the time for.
5420 *
5421 * @return {number}
5422 * The time offset at the specified index.
5423 *
5424 * @deprecated The index argument must be provided.
5425 * In the future, leaving it out will throw an error.
5426 */
5427
5428/**
5429 * An object that contains ranges of time, which mimics {@link TimeRanges}.
5430 *
5431 * @typedef {Object} TimeRange
5432 *
5433 * @property {number} length
5434 * The number of time ranges represented by this object.
5435 *
5436 * @property {module:time~TimeRangeIndex} start
5437 * Returns the time offset at which a specified time range begins.
5438 *
5439 * @property {module:time~TimeRangeIndex} end
5440 * Returns the time offset at which a specified time range ends.
5441 *
5442 * @see https://developer.mozilla.org/en-US/docs/Web/API/TimeRanges
5443 */
5444
5445/**
5446 * Check if any of the time ranges are over the maximum index.
5447 *
5448 * @private
5449 * @param {string} fnName
5450 * The function name to use for logging
5451 *
5452 * @param {number} index
5453 * The index to check
5454 *
5455 * @param {number} maxIndex
5456 * The maximum possible index
5457 *
5458 * @throws {Error} if the timeRanges provided are over the maxIndex
5459 */
5460function rangeCheck(fnName, index, maxIndex) {
5461 if (typeof index !== 'number' || index < 0 || index > maxIndex) {
5462 throw new Error(`Failed to execute '${fnName}' on 'TimeRanges': The index provided (${index}) is non-numeric or out of bounds (0-${maxIndex}).`);
5463 }
5464}
5465
5466/**
5467 * Get the time for the specified index at the start or end
5468 * of a TimeRange object.
5469 *
5470 * @private
5471 * @param {string} fnName
5472 * The function name to use for logging
5473 *
5474 * @param {string} valueIndex
5475 * The property that should be used to get the time. should be
5476 * 'start' or 'end'
5477 *
5478 * @param {Array} ranges
5479 * An array of time ranges
5480 *
5481 * @param {Array} [rangeIndex=0]
5482 * The index to start the search at
5483 *
5484 * @return {number}
5485 * The time that offset at the specified index.
5486 *
5487 * @deprecated rangeIndex must be set to a value, in the future this will throw an error.
5488 * @throws {Error} if rangeIndex is more than the length of ranges
5489 */
5490function getRange(fnName, valueIndex, ranges, rangeIndex) {
5491 rangeCheck(fnName, rangeIndex, ranges.length - 1);
5492 return ranges[rangeIndex][valueIndex];
5493}
5494
5495/**
5496 * Create a time range object given ranges of time.
5497 *
5498 * @private
5499 * @param {Array} [ranges]
5500 * An array of time ranges.
5501 *
5502 * @return {TimeRange}
5503 */
5504function createTimeRangesObj(ranges) {
5505 let timeRangesObj;
5506 if (ranges === undefined || ranges.length === 0) {
5507 timeRangesObj = {
5508 length: 0,
5509 start() {
5510 throw new Error('This TimeRanges object is empty');
5511 },
5512 end() {
5513 throw new Error('This TimeRanges object is empty');
5514 }
5515 };
5516 } else {
5517 timeRangesObj = {
5518 length: ranges.length,
5519 start: getRange.bind(null, 'start', 0, ranges),
5520 end: getRange.bind(null, 'end', 1, ranges)
5521 };
5522 }
5523 if (window__default["default"].Symbol && window__default["default"].Symbol.iterator) {
5524 timeRangesObj[window__default["default"].Symbol.iterator] = () => (ranges || []).values();
5525 }
5526 return timeRangesObj;
5527}
5528
5529/**
5530 * Create a `TimeRange` object which mimics an
5531 * {@link https://developer.mozilla.org/en-US/docs/Web/API/TimeRanges|HTML5 TimeRanges instance}.
5532 *
5533 * @param {number|Array[]} start
5534 * The start of a single range (a number) or an array of ranges (an
5535 * array of arrays of two numbers each).
5536 *
5537 * @param {number} end
5538 * The end of a single range. Cannot be used with the array form of
5539 * the `start` argument.
5540 *
5541 * @return {TimeRange}
5542 */
5543function createTimeRanges(start, end) {
5544 if (Array.isArray(start)) {
5545 return createTimeRangesObj(start);
5546 } else if (start === undefined || end === undefined) {
5547 return createTimeRangesObj();
5548 }
5549 return createTimeRangesObj([[start, end]]);
5550}
5551
5552/**
5553 * Format seconds as a time string, H:MM:SS or M:SS. Supplying a guide (in
5554 * seconds) will force a number of leading zeros to cover the length of the
5555 * guide.
5556 *
5557 * @private
5558 * @param {number} seconds
5559 * Number of seconds to be turned into a string
5560 *
5561 * @param {number} guide
5562 * Number (in seconds) to model the string after
5563 *
5564 * @return {string}
5565 * Time formatted as H:MM:SS or M:SS
5566 */
5567const defaultImplementation = function (seconds, guide) {
5568 seconds = seconds < 0 ? 0 : seconds;
5569 let s = Math.floor(seconds % 60);
5570 let m = Math.floor(seconds / 60 % 60);
5571 let h = Math.floor(seconds / 3600);
5572 const gm = Math.floor(guide / 60 % 60);
5573 const gh = Math.floor(guide / 3600);
5574
5575 // handle invalid times
5576 if (isNaN(seconds) || seconds === Infinity) {
5577 // '-' is false for all relational operators (e.g. <, >=) so this setting
5578 // will add the minimum number of fields specified by the guide
5579 h = m = s = '-';
5580 }
5581
5582 // Check if we need to show hours
5583 h = h > 0 || gh > 0 ? h + ':' : '';
5584
5585 // If hours are showing, we may need to add a leading zero.
5586 // Always show at least one digit of minutes.
5587 m = ((h || gm >= 10) && m < 10 ? '0' + m : m) + ':';
5588
5589 // Check if leading zero is need for seconds
5590 s = s < 10 ? '0' + s : s;
5591 return h + m + s;
5592};
5593
5594// Internal pointer to the current implementation.
5595let implementation = defaultImplementation;
5596
5597/**
5598 * Replaces the default formatTime implementation with a custom implementation.
5599 *
5600 * @param {Function} customImplementation
5601 * A function which will be used in place of the default formatTime
5602 * implementation. Will receive the current time in seconds and the
5603 * guide (in seconds) as arguments.
5604 */
5605function setFormatTime(customImplementation) {
5606 implementation = customImplementation;
5607}
5608
5609/**
5610 * Resets formatTime to the default implementation.
5611 */
5612function resetFormatTime() {
5613 implementation = defaultImplementation;
5614}
5615
5616/**
5617 * Delegates to either the default time formatting function or a custom
5618 * function supplied via `setFormatTime`.
5619 *
5620 * Formats seconds as a time string (H:MM:SS or M:SS). Supplying a
5621 * guide (in seconds) will force a number of leading zeros to cover the
5622 * length of the guide.
5623 *
5624 * @example formatTime(125, 600) === "02:05"
5625 * @param {number} seconds
5626 * Number of seconds to be turned into a string
5627 *
5628 * @param {number} guide
5629 * Number (in seconds) to model the string after
5630 *
5631 * @return {string}
5632 * Time formatted as H:MM:SS or M:SS
5633 */
5634function formatTime(seconds, guide = seconds) {
5635 return implementation(seconds, guide);
5636}
5637
5638var Time = /*#__PURE__*/Object.freeze({
5639 __proto__: null,
5640 createTimeRanges: createTimeRanges,
5641 createTimeRange: createTimeRanges,
5642 setFormatTime: setFormatTime,
5643 resetFormatTime: resetFormatTime,
5644 formatTime: formatTime
5645});
5646
5647/**
5648 * @file buffer.js
5649 * @module buffer
5650 */
5651
5652/** @import { TimeRange } from './time' */
5653
5654/**
5655 * Compute the percentage of the media that has been buffered.
5656 *
5657 * @param {TimeRange} buffered
5658 * The current `TimeRanges` object representing buffered time ranges
5659 *
5660 * @param {number} duration
5661 * Total duration of the media
5662 *
5663 * @return {number}
5664 * Percent buffered of the total duration in decimal form.
5665 */
5666function bufferedPercent(buffered, duration) {
5667 let bufferedDuration = 0;
5668 let start;
5669 let end;
5670 if (!duration) {
5671 return 0;
5672 }
5673 if (!buffered || !buffered.length) {
5674 buffered = createTimeRanges(0, 0);
5675 }
5676 for (let i = 0; i < buffered.length; i++) {
5677 start = buffered.start(i);
5678 end = buffered.end(i);
5679
5680 // buffered end can be bigger than duration by a very small fraction
5681 if (end > duration) {
5682 end = duration;
5683 }
5684 bufferedDuration += end - start;
5685 }
5686 return bufferedDuration / duration;
5687}
5688
5689/**
5690 * @file media-error.js
5691 */
5692
5693/**
5694 * A Custom `MediaError` class which mimics the standard HTML5 `MediaError` class.
5695 *
5696 * @param {number|string|Object|MediaError} value
5697 * This can be of multiple types:
5698 * - number: should be a standard error code
5699 * - string: an error message (the code will be 0)
5700 * - Object: arbitrary properties
5701 * - `MediaError` (native): used to populate a video.js `MediaError` object
5702 * - `MediaError` (video.js): will return itself if it's already a
5703 * video.js `MediaError` object.
5704 *
5705 * @see [MediaError Spec]{@link https://dev.w3.org/html5/spec-author-view/video.html#mediaerror}
5706 * @see [Encrypted MediaError Spec]{@link https://www.w3.org/TR/2013/WD-encrypted-media-20130510/#error-codes}
5707 *
5708 * @class MediaError
5709 */
5710function MediaError(value) {
5711 // Allow redundant calls to this constructor to avoid having `instanceof`
5712 // checks peppered around the code.
5713 if (value instanceof MediaError) {
5714 return value;
5715 }
5716 if (typeof value === 'number') {
5717 this.code = value;
5718 } else if (typeof value === 'string') {
5719 // default code is zero, so this is a custom error
5720 this.message = value;
5721 } else if (isObject(value)) {
5722 // We assign the `code` property manually because native `MediaError` objects
5723 // do not expose it as an own/enumerable property of the object.
5724 if (typeof value.code === 'number') {
5725 this.code = value.code;
5726 }
5727 Object.assign(this, value);
5728 }
5729 if (!this.message) {
5730 this.message = MediaError.defaultMessages[this.code] || '';
5731 }
5732}
5733
5734/**
5735 * The error code that refers two one of the defined `MediaError` types
5736 *
5737 * @type {Number}
5738 */
5739MediaError.prototype.code = 0;
5740
5741/**
5742 * An optional message that to show with the error. Message is not part of the HTML5
5743 * video spec but allows for more informative custom errors.
5744 *
5745 * @type {String}
5746 */
5747MediaError.prototype.message = '';
5748
5749/**
5750 * An optional status code that can be set by plugins to allow even more detail about
5751 * the error. For example a plugin might provide a specific HTTP status code and an
5752 * error message for that code. Then when the plugin gets that error this class will
5753 * know how to display an error message for it. This allows a custom message to show
5754 * up on the `Player` error overlay.
5755 *
5756 * @type {Array}
5757 */
5758MediaError.prototype.status = null;
5759
5760/**
5761 * An object containing an error type, as well as other information regarding the error.
5762 *
5763 * @typedef {{errorType: string, [key: string]: any}} ErrorMetadata
5764 */
5765
5766/**
5767 * An optional object to give more detail about the error. This can be used to give
5768 * a higher level of specificity to an error versus the more generic MediaError codes.
5769 * `metadata` expects an `errorType` string that should align with the values from videojs.Error.
5770 *
5771 * @type {ErrorMetadata}
5772 */
5773MediaError.prototype.metadata = null;
5774
5775/**
5776 * Errors indexed by the W3C standard. The order **CANNOT CHANGE**! See the
5777 * specification listed under {@link MediaError} for more information.
5778 *
5779 * @enum {array}
5780 * @readonly
5781 * @property {string} 0 - MEDIA_ERR_CUSTOM
5782 * @property {string} 1 - MEDIA_ERR_ABORTED
5783 * @property {string} 2 - MEDIA_ERR_NETWORK
5784 * @property {string} 3 - MEDIA_ERR_DECODE
5785 * @property {string} 4 - MEDIA_ERR_SRC_NOT_SUPPORTED
5786 * @property {string} 5 - MEDIA_ERR_ENCRYPTED
5787 */
5788MediaError.errorTypes = ['MEDIA_ERR_CUSTOM', 'MEDIA_ERR_ABORTED', 'MEDIA_ERR_NETWORK', 'MEDIA_ERR_DECODE', 'MEDIA_ERR_SRC_NOT_SUPPORTED', 'MEDIA_ERR_ENCRYPTED'];
5789
5790/**
5791 * The default `MediaError` messages based on the {@link MediaError.errorTypes}.
5792 *
5793 * @type {Array}
5794 * @constant
5795 */
5796MediaError.defaultMessages = {
5797 1: 'You aborted the media playback',
5798 2: 'A network error caused the media download to fail part-way.',
5799 3: 'The media playback was aborted due to a corruption problem or because the media used features your browser did not support.',
5800 4: 'The media could not be loaded, either because the server or network failed or because the format is not supported.',
5801 5: 'The media is encrypted and we do not have the keys to decrypt it.'
5802};
5803
5804/**
5805 * W3C error code for any custom error.
5806 *
5807 * @member MediaError#MEDIA_ERR_CUSTOM
5808 * @constant {number}
5809 * @default 0
5810 */
5811MediaError.MEDIA_ERR_CUSTOM = 0;
5812
5813/**
5814 * W3C error code for any custom error.
5815 *
5816 * @member MediaError.MEDIA_ERR_CUSTOM
5817 * @constant {number}
5818 * @default 0
5819 */
5820MediaError.prototype.MEDIA_ERR_CUSTOM = 0;
5821
5822/**
5823 * W3C error code for media error aborted.
5824 *
5825 * @member MediaError#MEDIA_ERR_ABORTED
5826 * @constant {number}
5827 * @default 1
5828 */
5829MediaError.MEDIA_ERR_ABORTED = 1;
5830
5831/**
5832 * W3C error code for media error aborted.
5833 *
5834 * @member MediaError.MEDIA_ERR_ABORTED
5835 * @constant {number}
5836 * @default 1
5837 */
5838MediaError.prototype.MEDIA_ERR_ABORTED = 1;
5839
5840/**
5841 * W3C error code for any network error.
5842 *
5843 * @member MediaError#MEDIA_ERR_NETWORK
5844 * @constant {number}
5845 * @default 2
5846 */
5847MediaError.MEDIA_ERR_NETWORK = 2;
5848
5849/**
5850 * W3C error code for any network error.
5851 *
5852 * @member MediaError.MEDIA_ERR_NETWORK
5853 * @constant {number}
5854 * @default 2
5855 */
5856MediaError.prototype.MEDIA_ERR_NETWORK = 2;
5857
5858/**
5859 * W3C error code for any decoding error.
5860 *
5861 * @member MediaError#MEDIA_ERR_DECODE
5862 * @constant {number}
5863 * @default 3
5864 */
5865MediaError.MEDIA_ERR_DECODE = 3;
5866
5867/**
5868 * W3C error code for any decoding error.
5869 *
5870 * @member MediaError.MEDIA_ERR_DECODE
5871 * @constant {number}
5872 * @default 3
5873 */
5874MediaError.prototype.MEDIA_ERR_DECODE = 3;
5875
5876/**
5877 * W3C error code for any time that a source is not supported.
5878 *
5879 * @member MediaError#MEDIA_ERR_SRC_NOT_SUPPORTED
5880 * @constant {number}
5881 * @default 4
5882 */
5883MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED = 4;
5884
5885/**
5886 * W3C error code for any time that a source is not supported.
5887 *
5888 * @member MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED
5889 * @constant {number}
5890 * @default 4
5891 */
5892MediaError.prototype.MEDIA_ERR_SRC_NOT_SUPPORTED = 4;
5893
5894/**
5895 * W3C error code for any time that a source is encrypted.
5896 *
5897 * @member MediaError#MEDIA_ERR_ENCRYPTED
5898 * @constant {number}
5899 * @default 5
5900 */
5901MediaError.MEDIA_ERR_ENCRYPTED = 5;
5902
5903/**
5904 * W3C error code for any time that a source is encrypted.
5905 *
5906 * @member MediaError.MEDIA_ERR_ENCRYPTED
5907 * @constant {number}
5908 * @default 5
5909 */
5910MediaError.prototype.MEDIA_ERR_ENCRYPTED = 5;
5911
5912/**
5913 * Returns whether an object is `Promise`-like (i.e. has a `then` method).
5914 *
5915 * @param {Object} value
5916 * An object that may or may not be `Promise`-like.
5917 *
5918 * @return {boolean}
5919 * Whether or not the object is `Promise`-like.
5920 */
5921function isPromise(value) {
5922 return value !== undefined && value !== null && typeof value.then === 'function';
5923}
5924
5925/**
5926 * Silence a Promise-like object.
5927 *
5928 * This is useful for avoiding non-harmful, but potentially confusing "uncaught
5929 * play promise" rejection error messages.
5930 *
5931 * @param {Object} value
5932 * An object that may or may not be `Promise`-like.
5933 */
5934function silencePromise(value) {
5935 if (isPromise(value)) {
5936 value.then(null, e => {});
5937 }
5938}
5939
5940/**
5941 * @file text-track-list-converter.js Utilities for capturing text track state and
5942 * re-creating tracks based on a capture.
5943 *
5944 * @module text-track-list-converter
5945 */
5946
5947/** @import Tech from '../tech/tech' */
5948
5949/**
5950 * Examine a single {@link TextTrack} and return a JSON-compatible javascript object that
5951 * represents the {@link TextTrack}'s state.
5952 *
5953 * @param {TextTrack} track
5954 * The text track to query.
5955 *
5956 * @return {Object}
5957 * A serializable javascript representation of the TextTrack.
5958 * @private
5959 */
5960const trackToJson_ = function (track) {
5961 const ret = ['kind', 'label', 'language', 'id', 'inBandMetadataTrackDispatchType', 'mode', 'src'].reduce((acc, prop, i) => {
5962 if (track[prop]) {
5963 acc[prop] = track[prop];
5964 }
5965 return acc;
5966 }, {
5967 cues: track.cues && Array.prototype.map.call(track.cues, function (cue) {
5968 return {
5969 startTime: cue.startTime,
5970 endTime: cue.endTime,
5971 text: cue.text,
5972 id: cue.id
5973 };
5974 })
5975 });
5976 return ret;
5977};
5978
5979/**
5980 * Examine a {@link Tech} and return a JSON-compatible javascript array that represents the
5981 * state of all {@link TextTrack}s currently configured. The return array is compatible with
5982 * {@link text-track-list-converter:jsonToTextTracks}.
5983 *
5984 * @param {Tech} tech
5985 * The tech object to query
5986 *
5987 * @return {Array}
5988 * A serializable javascript representation of the {@link Tech}s
5989 * {@link TextTrackList}.
5990 */
5991const textTracksToJson = function (tech) {
5992 const trackEls = tech.$$('track');
5993 const trackObjs = Array.prototype.map.call(trackEls, t => t.track);
5994 const tracks = Array.prototype.map.call(trackEls, function (trackEl) {
5995 const json = trackToJson_(trackEl.track);
5996 if (trackEl.src) {
5997 json.src = trackEl.src;
5998 }
5999 return json;
6000 });
6001 return tracks.concat(Array.prototype.filter.call(tech.textTracks(), function (track) {
6002 return trackObjs.indexOf(track) === -1;
6003 }).map(trackToJson_));
6004};
6005
6006/**
6007 * Create a set of remote {@link TextTrack}s on a {@link Tech} based on an array of javascript
6008 * object {@link TextTrack} representations.
6009 *
6010 * @param {Array} json
6011 * An array of `TextTrack` representation objects, like those that would be
6012 * produced by `textTracksToJson`.
6013 *
6014 * @param {Tech} tech
6015 * The `Tech` to create the `TextTrack`s on.
6016 */
6017const jsonToTextTracks = function (json, tech) {
6018 json.forEach(function (track) {
6019 const addedTrack = tech.addRemoteTextTrack(track).track;
6020 if (!track.src && track.cues) {
6021 track.cues.forEach(cue => addedTrack.addCue(cue));
6022 }
6023 });
6024 return tech.textTracks();
6025};
6026var textTrackConverter = {
6027 textTracksToJson,
6028 jsonToTextTracks,
6029 trackToJson_
6030};
6031
6032/**
6033 * @file modal-dialog.js
6034 */
6035
6036/** @import Player from './player' */
6037/** @import { ContentDescriptor } from './utils/dom' */
6038
6039const MODAL_CLASS_NAME = 'vjs-modal-dialog';
6040
6041/**
6042 * The `ModalDialog` displays over the video and its controls, which blocks
6043 * interaction with the player until it is closed.
6044 *
6045 * Modal dialogs include a "Close" button and will close when that button
6046 * is activated - or when ESC is pressed anywhere.
6047 *
6048 * @extends Component
6049 */
6050class ModalDialog extends Component {
6051 /**
6052 * Creates an instance of this class.
6053 *
6054 * @param {Player} player
6055 * The `Player` that this class should be attached to.
6056 *
6057 * @param {Object} [options]
6058 * The key/value store of player options.
6059 *
6060 * @param {ContentDescriptor} [options.content=undefined]
6061 * Provide customized content for this modal.
6062 *
6063 * @param {string} [options.description]
6064 * A text description for the modal, primarily for accessibility.
6065 *
6066 * @param {boolean} [options.fillAlways=false]
6067 * Normally, modals are automatically filled only the first time
6068 * they open. This tells the modal to refresh its content
6069 * every time it opens.
6070 *
6071 * @param {string} [options.label]
6072 * A text label for the modal, primarily for accessibility.
6073 *
6074 * @param {boolean} [options.pauseOnOpen=true]
6075 * If `true`, playback will will be paused if playing when
6076 * the modal opens, and resumed when it closes.
6077 *
6078 * @param {boolean} [options.temporary=true]
6079 * If `true`, the modal can only be opened once; it will be
6080 * disposed as soon as it's closed.
6081 *
6082 * @param {boolean} [options.uncloseable=false]
6083 * If `true`, the user will not be able to close the modal
6084 * through the UI in the normal ways. Programmatic closing is
6085 * still possible.
6086 */
6087 constructor(player, options) {
6088 super(player, options);
6089 this.handleKeyDown_ = e => this.handleKeyDown(e);
6090 this.close_ = e => this.close(e);
6091 this.opened_ = this.hasBeenOpened_ = this.hasBeenFilled_ = false;
6092 this.closeable(!this.options_.uncloseable);
6093 this.content(this.options_.content);
6094
6095 // Make sure the contentEl is defined AFTER any children are initialized
6096 // because we only want the contents of the modal in the contentEl
6097 // (not the UI elements like the close button).
6098 this.contentEl_ = createEl('div', {
6099 className: `${MODAL_CLASS_NAME}-content`
6100 }, {
6101 role: 'document'
6102 });
6103 this.descEl_ = createEl('p', {
6104 className: `${MODAL_CLASS_NAME}-description vjs-control-text`,
6105 id: this.el().getAttribute('aria-describedby')
6106 });
6107 textContent(this.descEl_, this.description());
6108 this.el_.appendChild(this.descEl_);
6109 this.el_.appendChild(this.contentEl_);
6110 }
6111
6112 /**
6113 * Create the `ModalDialog`'s DOM element
6114 *
6115 * @return {Element}
6116 * The DOM element that gets created.
6117 */
6118 createEl() {
6119 return super.createEl('div', {
6120 className: this.buildCSSClass(),
6121 tabIndex: -1
6122 }, {
6123 'aria-describedby': `${this.id()}_description`,
6124 'aria-hidden': 'true',
6125 'aria-label': this.label(),
6126 'role': 'dialog',
6127 'aria-live': 'polite'
6128 });
6129 }
6130 dispose() {
6131 this.contentEl_ = null;
6132 this.descEl_ = null;
6133 this.previouslyActiveEl_ = null;
6134 super.dispose();
6135 }
6136
6137 /**
6138 * Builds the default DOM `className`.
6139 *
6140 * @return {string}
6141 * The DOM `className` for this object.
6142 */
6143 buildCSSClass() {
6144 return `${MODAL_CLASS_NAME} vjs-hidden ${super.buildCSSClass()}`;
6145 }
6146
6147 /**
6148 * Returns the label string for this modal. Primarily used for accessibility.
6149 *
6150 * @return {string}
6151 * the localized or raw label of this modal.
6152 */
6153 label() {
6154 return this.localize(this.options_.label || 'Modal Window');
6155 }
6156
6157 /**
6158 * Returns the description string for this modal. Primarily used for
6159 * accessibility.
6160 *
6161 * @return {string}
6162 * The localized or raw description of this modal.
6163 */
6164 description() {
6165 let desc = this.options_.description || this.localize('This is a modal window.');
6166
6167 // Append a universal closeability message if the modal is closeable.
6168 if (this.closeable()) {
6169 desc += ' ' + this.localize('This modal can be closed by pressing the Escape key or activating the close button.');
6170 }
6171 return desc;
6172 }
6173
6174 /**
6175 * Opens the modal.
6176 *
6177 * @fires ModalDialog#beforemodalopen
6178 * @fires ModalDialog#modalopen
6179 */
6180 open() {
6181 if (this.opened_) {
6182 if (this.options_.fillAlways) {
6183 this.fill();
6184 }
6185 return;
6186 }
6187 const player = this.player();
6188
6189 /**
6190 * Fired just before a `ModalDialog` is opened.
6191 *
6192 * @event ModalDialog#beforemodalopen
6193 * @type {Event}
6194 */
6195 this.trigger('beforemodalopen');
6196 this.opened_ = true;
6197
6198 // Fill content if the modal has never opened before and
6199 // never been filled.
6200 if (this.options_.fillAlways || !this.hasBeenOpened_ && !this.hasBeenFilled_) {
6201 this.fill();
6202 }
6203
6204 // If the player was playing, pause it and take note of its previously
6205 // playing state.
6206 this.wasPlaying_ = !player.paused();
6207 if (this.options_.pauseOnOpen && this.wasPlaying_) {
6208 player.pause();
6209 }
6210 this.on('keydown', this.handleKeyDown_);
6211
6212 // Hide controls and note if they were enabled.
6213 this.hadControls_ = player.controls();
6214 player.controls(false);
6215 this.show();
6216 this.conditionalFocus_();
6217 this.el().setAttribute('aria-hidden', 'false');
6218
6219 /**
6220 * Fired just after a `ModalDialog` is opened.
6221 *
6222 * @event ModalDialog#modalopen
6223 * @type {Event}
6224 */
6225 this.trigger('modalopen');
6226 this.hasBeenOpened_ = true;
6227 }
6228
6229 /**
6230 * If the `ModalDialog` is currently open or closed.
6231 *
6232 * @param {boolean} [value]
6233 * If given, it will open (`true`) or close (`false`) the modal.
6234 *
6235 * @return {boolean}
6236 * the current open state of the modaldialog
6237 */
6238 opened(value) {
6239 if (typeof value === 'boolean') {
6240 this[value ? 'open' : 'close']();
6241 }
6242 return this.opened_;
6243 }
6244
6245 /**
6246 * Closes the modal, does nothing if the `ModalDialog` is
6247 * not open.
6248 *
6249 * @fires ModalDialog#beforemodalclose
6250 * @fires ModalDialog#modalclose
6251 */
6252 close() {
6253 if (!this.opened_) {
6254 return;
6255 }
6256 const player = this.player();
6257
6258 /**
6259 * Fired just before a `ModalDialog` is closed.
6260 *
6261 * @event ModalDialog#beforemodalclose
6262 * @type {Event}
6263 */
6264 this.trigger('beforemodalclose');
6265 this.opened_ = false;
6266 if (this.wasPlaying_ && this.options_.pauseOnOpen) {
6267 player.play();
6268 }
6269 this.off('keydown', this.handleKeyDown_);
6270 if (this.hadControls_) {
6271 player.controls(true);
6272 }
6273 this.hide();
6274 this.el().setAttribute('aria-hidden', 'true');
6275
6276 /**
6277 * Fired just after a `ModalDialog` is closed.
6278 *
6279 * @event ModalDialog#modalclose
6280 * @type {Event}
6281 *
6282 * @property {boolean} [bubbles=true]
6283 */
6284 this.trigger({
6285 type: 'modalclose',
6286 bubbles: true
6287 });
6288 this.conditionalBlur_();
6289 if (this.options_.temporary) {
6290 this.dispose();
6291 }
6292 }
6293
6294 /**
6295 * Check to see if the `ModalDialog` is closeable via the UI.
6296 *
6297 * @param {boolean} [value]
6298 * If given as a boolean, it will set the `closeable` option.
6299 *
6300 * @return {boolean}
6301 * Returns the final value of the closable option.
6302 */
6303 closeable(value) {
6304 if (typeof value === 'boolean') {
6305 const closeable = this.closeable_ = !!value;
6306 let close = this.getChild('closeButton');
6307
6308 // If this is being made closeable and has no close button, add one.
6309 if (closeable && !close) {
6310 // The close button should be a child of the modal - not its
6311 // content element, so temporarily change the content element.
6312 const temp = this.contentEl_;
6313 this.contentEl_ = this.el_;
6314 close = this.addChild('closeButton', {
6315 controlText: 'Close Modal Dialog'
6316 });
6317 this.contentEl_ = temp;
6318 this.on(close, 'close', this.close_);
6319 }
6320
6321 // If this is being made uncloseable and has a close button, remove it.
6322 if (!closeable && close) {
6323 this.off(close, 'close', this.close_);
6324 this.removeChild(close);
6325 close.dispose();
6326 }
6327 }
6328 return this.closeable_;
6329 }
6330
6331 /**
6332 * Fill the modal's content element with the modal's "content" option.
6333 * The content element will be emptied before this change takes place.
6334 */
6335 fill() {
6336 this.fillWith(this.content());
6337 }
6338
6339 /**
6340 * Fill the modal's content element with arbitrary content.
6341 * The content element will be emptied before this change takes place.
6342 *
6343 * @fires ModalDialog#beforemodalfill
6344 * @fires ModalDialog#modalfill
6345 *
6346 * @param {ContentDescriptor} [content]
6347 * The same rules apply to this as apply to the `content` option.
6348 */
6349 fillWith(content) {
6350 const contentEl = this.contentEl();
6351 const parentEl = contentEl.parentNode;
6352 const nextSiblingEl = contentEl.nextSibling;
6353
6354 /**
6355 * Fired just before a `ModalDialog` is filled with content.
6356 *
6357 * @event ModalDialog#beforemodalfill
6358 * @type {Event}
6359 */
6360 this.trigger('beforemodalfill');
6361 this.hasBeenFilled_ = true;
6362
6363 // Detach the content element from the DOM before performing
6364 // manipulation to avoid modifying the live DOM multiple times.
6365 parentEl.removeChild(contentEl);
6366 this.empty();
6367 insertContent(contentEl, content);
6368 /**
6369 * Fired just after a `ModalDialog` is filled with content.
6370 *
6371 * @event ModalDialog#modalfill
6372 * @type {Event}
6373 */
6374 this.trigger('modalfill');
6375
6376 // Re-inject the re-filled content element.
6377 if (nextSiblingEl) {
6378 parentEl.insertBefore(contentEl, nextSiblingEl);
6379 } else {
6380 parentEl.appendChild(contentEl);
6381 }
6382
6383 // make sure that the close button is last in the dialog DOM
6384 const closeButton = this.getChild('closeButton');
6385 if (closeButton) {
6386 parentEl.appendChild(closeButton.el_);
6387 }
6388
6389 /**
6390 * Fired after `ModalDialog` is re-filled with content & close button is appended.
6391 *
6392 * @event ModalDialog#aftermodalfill
6393 * @type {Event}
6394 */
6395 this.trigger('aftermodalfill');
6396 }
6397
6398 /**
6399 * Empties the content element. This happens anytime the modal is filled.
6400 *
6401 * @fires ModalDialog#beforemodalempty
6402 * @fires ModalDialog#modalempty
6403 */
6404 empty() {
6405 /**
6406 * Fired just before a `ModalDialog` is emptied.
6407 *
6408 * @event ModalDialog#beforemodalempty
6409 * @type {Event}
6410 */
6411 this.trigger('beforemodalempty');
6412 emptyEl(this.contentEl());
6413
6414 /**
6415 * Fired just after a `ModalDialog` is emptied.
6416 *
6417 * @event ModalDialog#modalempty
6418 * @type {Event}
6419 */
6420 this.trigger('modalempty');
6421 }
6422
6423 /**
6424 * Gets or sets the modal content, which gets normalized before being
6425 * rendered into the DOM.
6426 *
6427 * This does not update the DOM or fill the modal, but it is called during
6428 * that process.
6429 *
6430 * @param {ContentDescriptor} [value]
6431 * If defined, sets the internal content value to be used on the
6432 * next call(s) to `fill`. This value is normalized before being
6433 * inserted. To "clear" the internal content value, pass `null`.
6434 *
6435 * @return {ContentDescriptor}
6436 * The current content of the modal dialog
6437 */
6438 content(value) {
6439 if (typeof value !== 'undefined') {
6440 this.content_ = value;
6441 }
6442 return this.content_;
6443 }
6444
6445 /**
6446 * conditionally focus the modal dialog if focus was previously on the player.
6447 *
6448 * @private
6449 */
6450 conditionalFocus_() {
6451 const activeEl = document__default["default"].activeElement;
6452 const playerEl = this.player_.el_;
6453 this.previouslyActiveEl_ = null;
6454 if (playerEl.contains(activeEl) || playerEl === activeEl) {
6455 this.previouslyActiveEl_ = activeEl;
6456 this.focus();
6457 }
6458 }
6459
6460 /**
6461 * conditionally blur the element and refocus the last focused element
6462 *
6463 * @private
6464 */
6465 conditionalBlur_() {
6466 if (this.previouslyActiveEl_) {
6467 this.previouslyActiveEl_.focus();
6468 this.previouslyActiveEl_ = null;
6469 }
6470 }
6471
6472 /**
6473 * Keydown handler. Attached when modal is focused.
6474 *
6475 * @listens keydown
6476 */
6477 handleKeyDown(event) {
6478 /**
6479 * Fired a custom keyDown event that bubbles.
6480 *
6481 * @event ModalDialog#modalKeydown
6482 * @type {Event}
6483 */
6484 this.trigger({
6485 type: 'modalKeydown',
6486 originalEvent: event,
6487 target: this,
6488 bubbles: true
6489 });
6490 // Do not allow keydowns to reach out of the modal dialog.
6491 event.stopPropagation();
6492 if (event.key === 'Escape' && this.closeable()) {
6493 event.preventDefault();
6494 this.close();
6495 return;
6496 }
6497
6498 // exit early if it isn't a tab key
6499 if (event.key !== 'Tab') {
6500 return;
6501 }
6502 const focusableEls = this.focusableEls_();
6503 const activeEl = this.el_.querySelector(':focus');
6504 let focusIndex;
6505 for (let i = 0; i < focusableEls.length; i++) {
6506 if (activeEl === focusableEls[i]) {
6507 focusIndex = i;
6508 break;
6509 }
6510 }
6511 if (document__default["default"].activeElement === this.el_) {
6512 focusIndex = 0;
6513 }
6514 if (event.shiftKey && focusIndex === 0) {
6515 focusableEls[focusableEls.length - 1].focus();
6516 event.preventDefault();
6517 } else if (!event.shiftKey && focusIndex === focusableEls.length - 1) {
6518 focusableEls[0].focus();
6519 event.preventDefault();
6520 }
6521 }
6522
6523 /**
6524 * get all focusable elements
6525 *
6526 * @private
6527 */
6528 focusableEls_() {
6529 const allChildren = this.el_.querySelectorAll('*');
6530 return Array.prototype.filter.call(allChildren, child => {
6531 return (child instanceof window__default["default"].HTMLAnchorElement || child instanceof window__default["default"].HTMLAreaElement) && child.hasAttribute('href') || (child instanceof window__default["default"].HTMLInputElement || child instanceof window__default["default"].HTMLSelectElement || child instanceof window__default["default"].HTMLTextAreaElement || child instanceof window__default["default"].HTMLButtonElement) && !child.hasAttribute('disabled') || child instanceof window__default["default"].HTMLIFrameElement || child instanceof window__default["default"].HTMLObjectElement || child instanceof window__default["default"].HTMLEmbedElement || child.hasAttribute('tabindex') && child.getAttribute('tabindex') !== -1 || child.hasAttribute('contenteditable');
6532 });
6533 }
6534}
6535
6536/**
6537 * Default options for `ModalDialog` default options.
6538 *
6539 * @type {Object}
6540 * @private
6541 */
6542ModalDialog.prototype.options_ = {
6543 pauseOnOpen: true,
6544 temporary: true
6545};
6546Component.registerComponent('ModalDialog', ModalDialog);
6547
6548/**
6549 * @file track-list.js
6550 */
6551
6552/** @import Track from './track' */
6553
6554/**
6555 * Common functionaliy between {@link TextTrackList}, {@link AudioTrackList}, and
6556 * {@link VideoTrackList}
6557 *
6558 * @extends EventTarget
6559 */
6560class TrackList extends EventTarget {
6561 /**
6562 * Create an instance of this class
6563 *
6564 * @param { Track[] } tracks
6565 * A list of tracks to initialize the list with.
6566 *
6567 * @abstract
6568 */
6569 constructor(tracks = []) {
6570 super();
6571 this.tracks_ = [];
6572
6573 /**
6574 * @memberof TrackList
6575 * @member {number} length
6576 * The current number of `Track`s in the this Trackist.
6577 * @instance
6578 */
6579 Object.defineProperty(this, 'length', {
6580 get() {
6581 return this.tracks_.length;
6582 }
6583 });
6584 for (let i = 0; i < tracks.length; i++) {
6585 this.addTrack(tracks[i]);
6586 }
6587 }
6588
6589 /**
6590 * Add a {@link Track} to the `TrackList`
6591 *
6592 * @param {Track} track
6593 * The audio, video, or text track to add to the list.
6594 *
6595 * @fires TrackList#addtrack
6596 */
6597 addTrack(track) {
6598 const index = this.tracks_.length;
6599 if (!('' + index in this)) {
6600 Object.defineProperty(this, index, {
6601 get() {
6602 return this.tracks_[index];
6603 }
6604 });
6605 }
6606
6607 // Do not add duplicate tracks
6608 if (this.tracks_.indexOf(track) === -1) {
6609 this.tracks_.push(track);
6610 /**
6611 * Triggered when a track is added to a track list.
6612 *
6613 * @event TrackList#addtrack
6614 * @type {Event}
6615 * @property {Track} track
6616 * A reference to track that was added.
6617 */
6618 this.trigger({
6619 track,
6620 type: 'addtrack',
6621 target: this
6622 });
6623 }
6624
6625 /**
6626 * Triggered when a track label is changed.
6627 *
6628 * @event TrackList#addtrack
6629 * @type {Event}
6630 * @property {Track} track
6631 * A reference to track that was added.
6632 */
6633 track.labelchange_ = () => {
6634 this.trigger({
6635 track,
6636 type: 'labelchange',
6637 target: this
6638 });
6639 };
6640 if (isEvented(track)) {
6641 track.addEventListener('labelchange', track.labelchange_);
6642 }
6643 }
6644
6645 /**
6646 * Remove a {@link Track} from the `TrackList`
6647 *
6648 * @param {Track} rtrack
6649 * The audio, video, or text track to remove from the list.
6650 *
6651 * @fires TrackList#removetrack
6652 */
6653 removeTrack(rtrack) {
6654 let track;
6655 for (let i = 0, l = this.length; i < l; i++) {
6656 if (this[i] === rtrack) {
6657 track = this[i];
6658 if (track.off) {
6659 track.off();
6660 }
6661 this.tracks_.splice(i, 1);
6662 break;
6663 }
6664 }
6665 if (!track) {
6666 return;
6667 }
6668
6669 /**
6670 * Triggered when a track is removed from track list.
6671 *
6672 * @event TrackList#removetrack
6673 * @type {Event}
6674 * @property {Track} track
6675 * A reference to track that was removed.
6676 */
6677 this.trigger({
6678 track,
6679 type: 'removetrack',
6680 target: this
6681 });
6682 }
6683
6684 /**
6685 * Get a Track from the TrackList by a tracks id
6686 *
6687 * @param {string} id - the id of the track to get
6688 * @method getTrackById
6689 * @return {Track}
6690 * @private
6691 */
6692 getTrackById(id) {
6693 let result = null;
6694 for (let i = 0, l = this.length; i < l; i++) {
6695 const track = this[i];
6696 if (track.id === id) {
6697 result = track;
6698 break;
6699 }
6700 }
6701 return result;
6702 }
6703}
6704
6705/**
6706 * Triggered when a different track is selected/enabled.
6707 *
6708 * @event TrackList#change
6709 * @type {Event}
6710 */
6711
6712/**
6713 * Events that can be called with on + eventName. See {@link EventHandler}.
6714 *
6715 * @property {Object} TrackList#allowedEvents_
6716 * @protected
6717 */
6718TrackList.prototype.allowedEvents_ = {
6719 change: 'change',
6720 addtrack: 'addtrack',
6721 removetrack: 'removetrack',
6722 labelchange: 'labelchange'
6723};
6724
6725// emulate attribute EventHandler support to allow for feature detection
6726for (const event in TrackList.prototype.allowedEvents_) {
6727 TrackList.prototype['on' + event] = null;
6728}
6729
6730/**
6731 * @file audio-track-list.js
6732 */
6733
6734/** @import AudioTrack from './audio-track' */
6735
6736/**
6737 * Anywhere we call this function we diverge from the spec
6738 * as we only support one enabled audiotrack at a time
6739 *
6740 * @param {AudioTrackList} list
6741 * list to work on
6742 *
6743 * @param {AudioTrack} track
6744 * The track to skip
6745 *
6746 * @private
6747 */
6748const disableOthers$1 = function (list, track) {
6749 for (let i = 0; i < list.length; i++) {
6750 if (!Object.keys(list[i]).length || track.id === list[i].id) {
6751 continue;
6752 }
6753 // another audio track is enabled, disable it
6754 list[i].enabled = false;
6755 }
6756};
6757
6758/**
6759 * The current list of {@link AudioTrack} for a media file.
6760 *
6761 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotracklist}
6762 * @extends TrackList
6763 */
6764class AudioTrackList extends TrackList {
6765 /**
6766 * Create an instance of this class.
6767 *
6768 * @param {AudioTrack[]} [tracks=[]]
6769 * A list of `AudioTrack` to instantiate the list with.
6770 */
6771 constructor(tracks = []) {
6772 // make sure only 1 track is enabled
6773 // sorted from last index to first index
6774 for (let i = tracks.length - 1; i >= 0; i--) {
6775 if (tracks[i].enabled) {
6776 disableOthers$1(tracks, tracks[i]);
6777 break;
6778 }
6779 }
6780 super(tracks);
6781 this.changing_ = false;
6782 }
6783
6784 /**
6785 * Add an {@link AudioTrack} to the `AudioTrackList`.
6786 *
6787 * @param {AudioTrack} track
6788 * The AudioTrack to add to the list
6789 *
6790 * @fires TrackList#addtrack
6791 */
6792 addTrack(track) {
6793 if (track.enabled) {
6794 disableOthers$1(this, track);
6795 }
6796 super.addTrack(track);
6797 // native tracks don't have this
6798 if (!track.addEventListener) {
6799 return;
6800 }
6801 track.enabledChange_ = () => {
6802 // when we are disabling other tracks (since we don't support
6803 // more than one track at a time) we will set changing_
6804 // to true so that we don't trigger additional change events
6805 if (this.changing_) {
6806 return;
6807 }
6808 this.changing_ = true;
6809 disableOthers$1(this, track);
6810 this.changing_ = false;
6811 this.trigger('change');
6812 };
6813
6814 /**
6815 * @listens AudioTrack#enabledchange
6816 * @fires TrackList#change
6817 */
6818 track.addEventListener('enabledchange', track.enabledChange_);
6819 }
6820 removeTrack(rtrack) {
6821 super.removeTrack(rtrack);
6822 if (rtrack.removeEventListener && rtrack.enabledChange_) {
6823 rtrack.removeEventListener('enabledchange', rtrack.enabledChange_);
6824 rtrack.enabledChange_ = null;
6825 }
6826 }
6827}
6828
6829/**
6830 * @file video-track-list.js
6831 */
6832
6833/** @import VideoTrack from './video-track' */
6834
6835/**
6836 * Un-select all other {@link VideoTrack}s that are selected.
6837 *
6838 * @param {VideoTrackList} list
6839 * list to work on
6840 *
6841 * @param {VideoTrack} track
6842 * The track to skip
6843 *
6844 * @private
6845 */
6846const disableOthers = function (list, track) {
6847 for (let i = 0; i < list.length; i++) {
6848 if (!Object.keys(list[i]).length || track.id === list[i].id) {
6849 continue;
6850 }
6851 // another video track is enabled, disable it
6852 list[i].selected = false;
6853 }
6854};
6855
6856/**
6857 * The current list of {@link VideoTrack} for a video.
6858 *
6859 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#videotracklist}
6860 * @extends TrackList
6861 */
6862class VideoTrackList extends TrackList {
6863 /**
6864 * Create an instance of this class.
6865 *
6866 * @param {VideoTrack[]} [tracks=[]]
6867 * A list of `VideoTrack` to instantiate the list with.
6868 */
6869 constructor(tracks = []) {
6870 // make sure only 1 track is enabled
6871 // sorted from last index to first index
6872 for (let i = tracks.length - 1; i >= 0; i--) {
6873 if (tracks[i].selected) {
6874 disableOthers(tracks, tracks[i]);
6875 break;
6876 }
6877 }
6878 super(tracks);
6879 this.changing_ = false;
6880
6881 /**
6882 * @member {number} VideoTrackList#selectedIndex
6883 * The current index of the selected {@link VideoTrack`}.
6884 */
6885 Object.defineProperty(this, 'selectedIndex', {
6886 get() {
6887 for (let i = 0; i < this.length; i++) {
6888 if (this[i].selected) {
6889 return i;
6890 }
6891 }
6892 return -1;
6893 },
6894 set() {}
6895 });
6896 }
6897
6898 /**
6899 * Add a {@link VideoTrack} to the `VideoTrackList`.
6900 *
6901 * @param {VideoTrack} track
6902 * The VideoTrack to add to the list
6903 *
6904 * @fires TrackList#addtrack
6905 */
6906 addTrack(track) {
6907 if (track.selected) {
6908 disableOthers(this, track);
6909 }
6910 super.addTrack(track);
6911 // native tracks don't have this
6912 if (!track.addEventListener) {
6913 return;
6914 }
6915 track.selectedChange_ = () => {
6916 if (this.changing_) {
6917 return;
6918 }
6919 this.changing_ = true;
6920 disableOthers(this, track);
6921 this.changing_ = false;
6922 this.trigger('change');
6923 };
6924
6925 /**
6926 * @listens VideoTrack#selectedchange
6927 * @fires TrackList#change
6928 */
6929 track.addEventListener('selectedchange', track.selectedChange_);
6930 }
6931 removeTrack(rtrack) {
6932 super.removeTrack(rtrack);
6933 if (rtrack.removeEventListener && rtrack.selectedChange_) {
6934 rtrack.removeEventListener('selectedchange', rtrack.selectedChange_);
6935 rtrack.selectedChange_ = null;
6936 }
6937 }
6938}
6939
6940/**
6941 * @file text-track-list.js
6942 */
6943
6944/** @import TextTrack from './text-track' */
6945
6946/**
6947 * The current list of {@link TextTrack} for a media file.
6948 *
6949 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttracklist}
6950 * @extends TrackList
6951 */
6952class TextTrackList extends TrackList {
6953 /**
6954 * Add a {@link TextTrack} to the `TextTrackList`
6955 *
6956 * @param {TextTrack} track
6957 * The text track to add to the list.
6958 *
6959 * @fires TrackList#addtrack
6960 */
6961 addTrack(track) {
6962 super.addTrack(track);
6963 if (!this.queueChange_) {
6964 this.queueChange_ = () => this.queueTrigger('change');
6965 }
6966 if (!this.triggerSelectedlanguagechange) {
6967 this.triggerSelectedlanguagechange_ = () => this.trigger('selectedlanguagechange');
6968 }
6969
6970 /**
6971 * @listens TextTrack#modechange
6972 * @fires TrackList#change
6973 */
6974 track.addEventListener('modechange', this.queueChange_);
6975 const nonLanguageTextTrackKind = ['metadata', 'chapters'];
6976 if (nonLanguageTextTrackKind.indexOf(track.kind) === -1) {
6977 track.addEventListener('modechange', this.triggerSelectedlanguagechange_);
6978 }
6979 }
6980 removeTrack(rtrack) {
6981 super.removeTrack(rtrack);
6982
6983 // manually remove the event handlers we added
6984 if (rtrack.removeEventListener) {
6985 if (this.queueChange_) {
6986 rtrack.removeEventListener('modechange', this.queueChange_);
6987 }
6988 if (this.selectedlanguagechange_) {
6989 rtrack.removeEventListener('modechange', this.triggerSelectedlanguagechange_);
6990 }
6991 }
6992 }
6993}
6994
6995/**
6996 * @file html-track-element-list.js
6997 */
6998
6999/**
7000 * The current list of {@link HtmlTrackElement}s.
7001 */
7002class HtmlTrackElementList {
7003 /**
7004 * Create an instance of this class.
7005 *
7006 * @param {HtmlTrackElement[]} [tracks=[]]
7007 * A list of `HtmlTrackElement` to instantiate the list with.
7008 */
7009 constructor(trackElements = []) {
7010 this.trackElements_ = [];
7011
7012 /**
7013 * @memberof HtmlTrackElementList
7014 * @member {number} length
7015 * The current number of `Track`s in the this Trackist.
7016 * @instance
7017 */
7018 Object.defineProperty(this, 'length', {
7019 get() {
7020 return this.trackElements_.length;
7021 }
7022 });
7023 for (let i = 0, length = trackElements.length; i < length; i++) {
7024 this.addTrackElement_(trackElements[i]);
7025 }
7026 }
7027
7028 /**
7029 * Add an {@link HtmlTrackElement} to the `HtmlTrackElementList`
7030 *
7031 * @param {HtmlTrackElement} trackElement
7032 * The track element to add to the list.
7033 *
7034 * @private
7035 */
7036 addTrackElement_(trackElement) {
7037 const index = this.trackElements_.length;
7038 if (!('' + index in this)) {
7039 Object.defineProperty(this, index, {
7040 get() {
7041 return this.trackElements_[index];
7042 }
7043 });
7044 }
7045
7046 // Do not add duplicate elements
7047 if (this.trackElements_.indexOf(trackElement) === -1) {
7048 this.trackElements_.push(trackElement);
7049 }
7050 }
7051
7052 /**
7053 * Get an {@link HtmlTrackElement} from the `HtmlTrackElementList` given an
7054 * {@link TextTrack}.
7055 *
7056 * @param {TextTrack} track
7057 * The track associated with a track element.
7058 *
7059 * @return {HtmlTrackElement|undefined}
7060 * The track element that was found or undefined.
7061 *
7062 * @private
7063 */
7064 getTrackElementByTrack_(track) {
7065 let trackElement_;
7066 for (let i = 0, length = this.trackElements_.length; i < length; i++) {
7067 if (track === this.trackElements_[i].track) {
7068 trackElement_ = this.trackElements_[i];
7069 break;
7070 }
7071 }
7072 return trackElement_;
7073 }
7074
7075 /**
7076 * Remove a {@link HtmlTrackElement} from the `HtmlTrackElementList`
7077 *
7078 * @param {HtmlTrackElement} trackElement
7079 * The track element to remove from the list.
7080 *
7081 * @private
7082 */
7083 removeTrackElement_(trackElement) {
7084 for (let i = 0, length = this.trackElements_.length; i < length; i++) {
7085 if (trackElement === this.trackElements_[i]) {
7086 if (this.trackElements_[i].track && typeof this.trackElements_[i].track.off === 'function') {
7087 this.trackElements_[i].track.off();
7088 }
7089 if (typeof this.trackElements_[i].off === 'function') {
7090 this.trackElements_[i].off();
7091 }
7092 this.trackElements_.splice(i, 1);
7093 break;
7094 }
7095 }
7096 }
7097}
7098
7099/**
7100 * @file text-track-cue-list.js
7101 */
7102
7103/**
7104 * @typedef {Object} TextTrackCueList~TextTrackCue
7105 *
7106 * @property {string} id
7107 * The unique id for this text track cue
7108 *
7109 * @property {number} startTime
7110 * The start time for this text track cue
7111 *
7112 * @property {number} endTime
7113 * The end time for this text track cue
7114 *
7115 * @property {boolean} pauseOnExit
7116 * Pause when the end time is reached if true.
7117 *
7118 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackcue}
7119 */
7120
7121/**
7122 * A List of TextTrackCues.
7123 *
7124 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackcuelist}
7125 */
7126class TextTrackCueList {
7127 /**
7128 * Create an instance of this class..
7129 *
7130 * @param {Array} cues
7131 * A list of cues to be initialized with
7132 */
7133 constructor(cues) {
7134 TextTrackCueList.prototype.setCues_.call(this, cues);
7135
7136 /**
7137 * @memberof TextTrackCueList
7138 * @member {number} length
7139 * The current number of `TextTrackCue`s in the TextTrackCueList.
7140 * @instance
7141 */
7142 Object.defineProperty(this, 'length', {
7143 get() {
7144 return this.length_;
7145 }
7146 });
7147 }
7148
7149 /**
7150 * A setter for cues in this list. Creates getters
7151 * an an index for the cues.
7152 *
7153 * @param {Array} cues
7154 * An array of cues to set
7155 *
7156 * @private
7157 */
7158 setCues_(cues) {
7159 const oldLength = this.length || 0;
7160 let i = 0;
7161 const l = cues.length;
7162 this.cues_ = cues;
7163 this.length_ = cues.length;
7164 const defineProp = function (index) {
7165 if (!('' + index in this)) {
7166 Object.defineProperty(this, '' + index, {
7167 get() {
7168 return this.cues_[index];
7169 }
7170 });
7171 }
7172 };
7173 if (oldLength < l) {
7174 i = oldLength;
7175 for (; i < l; i++) {
7176 defineProp.call(this, i);
7177 }
7178 }
7179 }
7180
7181 /**
7182 * Get a `TextTrackCue` that is currently in the `TextTrackCueList` by id.
7183 *
7184 * @param {string} id
7185 * The id of the cue that should be searched for.
7186 *
7187 * @return {TextTrackCueList~TextTrackCue|null}
7188 * A single cue or null if none was found.
7189 */
7190 getCueById(id) {
7191 let result = null;
7192 for (let i = 0, l = this.length; i < l; i++) {
7193 const cue = this[i];
7194 if (cue.id === id) {
7195 result = cue;
7196 break;
7197 }
7198 }
7199 return result;
7200 }
7201}
7202
7203/**
7204 * @file track-kinds.js
7205 */
7206
7207/**
7208 * All possible `VideoTrackKind`s
7209 *
7210 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-videotrack-kind
7211 * @typedef VideoTrack~Kind
7212 * @enum
7213 */
7214const VideoTrackKind = {
7215 alternative: 'alternative',
7216 captions: 'captions',
7217 main: 'main',
7218 sign: 'sign',
7219 subtitles: 'subtitles',
7220 commentary: 'commentary'
7221};
7222
7223/**
7224 * All possible `AudioTrackKind`s
7225 *
7226 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-audiotrack-kind
7227 * @typedef AudioTrack~Kind
7228 * @enum
7229 */
7230const AudioTrackKind = {
7231 'alternative': 'alternative',
7232 'descriptions': 'descriptions',
7233 'main': 'main',
7234 'main-desc': 'main-desc',
7235 'translation': 'translation',
7236 'commentary': 'commentary'
7237};
7238
7239/**
7240 * All possible `TextTrackKind`s
7241 *
7242 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-texttrack-kind
7243 * @typedef TextTrack~Kind
7244 * @enum
7245 */
7246const TextTrackKind = {
7247 subtitles: 'subtitles',
7248 captions: 'captions',
7249 descriptions: 'descriptions',
7250 chapters: 'chapters',
7251 metadata: 'metadata'
7252};
7253
7254/**
7255 * All possible `TextTrackMode`s
7256 *
7257 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackmode
7258 * @typedef TextTrack~Mode
7259 * @enum
7260 */
7261const TextTrackMode = {
7262 disabled: 'disabled',
7263 hidden: 'hidden',
7264 showing: 'showing'
7265};
7266
7267/**
7268 * @file track.js
7269 */
7270
7271/**
7272 * A Track class that contains all of the common functionality for {@link AudioTrack},
7273 * {@link VideoTrack}, and {@link TextTrack}.
7274 *
7275 * > Note: This class should not be used directly
7276 *
7277 * @see {@link https://html.spec.whatwg.org/multipage/embedded-content.html}
7278 * @extends EventTarget
7279 * @abstract
7280 */
7281class Track extends EventTarget {
7282 /**
7283 * Create an instance of this class.
7284 *
7285 * @param {Object} [options={}]
7286 * Object of option names and values
7287 *
7288 * @param {string} [options.kind='']
7289 * A valid kind for the track type you are creating.
7290 *
7291 * @param {string} [options.id='vjs_track_' + Guid.newGUID()]
7292 * A unique id for this AudioTrack.
7293 *
7294 * @param {string} [options.label='']
7295 * The menu label for this track.
7296 *
7297 * @param {string} [options.language='']
7298 * A valid two character language code.
7299 *
7300 * @abstract
7301 */
7302 constructor(options = {}) {
7303 super();
7304 const trackProps = {
7305 id: options.id || 'vjs_track_' + newGUID(),
7306 kind: options.kind || '',
7307 language: options.language || ''
7308 };
7309 let label = options.label || '';
7310
7311 /**
7312 * @memberof Track
7313 * @member {string} id
7314 * The id of this track. Cannot be changed after creation.
7315 * @instance
7316 *
7317 * @readonly
7318 */
7319
7320 /**
7321 * @memberof Track
7322 * @member {string} kind
7323 * The kind of track that this is. Cannot be changed after creation.
7324 * @instance
7325 *
7326 * @readonly
7327 */
7328
7329 /**
7330 * @memberof Track
7331 * @member {string} language
7332 * The two letter language code for this track. Cannot be changed after
7333 * creation.
7334 * @instance
7335 *
7336 * @readonly
7337 */
7338
7339 for (const key in trackProps) {
7340 Object.defineProperty(this, key, {
7341 get() {
7342 return trackProps[key];
7343 },
7344 set() {}
7345 });
7346 }
7347
7348 /**
7349 * @memberof Track
7350 * @member {string} label
7351 * The label of this track. Cannot be changed after creation.
7352 * @instance
7353 *
7354 * @fires Track#labelchange
7355 */
7356 Object.defineProperty(this, 'label', {
7357 get() {
7358 return label;
7359 },
7360 set(newLabel) {
7361 if (newLabel !== label) {
7362 label = newLabel;
7363
7364 /**
7365 * An event that fires when label changes on this track.
7366 *
7367 * > Note: This is not part of the spec!
7368 *
7369 * @event Track#labelchange
7370 * @type {Event}
7371 */
7372 this.trigger('labelchange');
7373 }
7374 }
7375 });
7376 }
7377}
7378
7379/**
7380 * @file url.js
7381 * @module url
7382 */
7383
7384/**
7385 * Resolve and parse the elements of a URL.
7386 *
7387 * @function
7388 * @param {string} url
7389 * The url to parse
7390 *
7391 * @return {URL}
7392 * An object of url details
7393 */
7394const parseUrl = function (url) {
7395 return new URL(url, document__default["default"].baseURI);
7396};
7397
7398/**
7399 * Get absolute version of relative URL.
7400 *
7401 * @function
7402 * @param {string} url
7403 * URL to make absolute
7404 *
7405 * @return {string}
7406 * Absolute URL
7407 */
7408const getAbsoluteURL = function (url) {
7409 return new URL(url, document__default["default"].baseURI).href;
7410};
7411
7412/**
7413 * Returns the extension of the passed file name. It will return an empty string
7414 * if passed an invalid path.
7415 *
7416 * @function
7417 * @param {string} path
7418 * The fileName path like '/path/to/file.mp4'
7419 *
7420 * @return {string}
7421 * The extension in lower case or an empty string if no
7422 * extension could be found.
7423 */
7424const getFileExtension = function (path) {
7425 if (typeof path === 'string') {
7426 const splitPathRe = /^(\/?)([\s\S]*?)((?:\.{1,2}|[^\/]+?)(\.([^\.\/\?]+)))(?:[\/]*|[\?].*)$/;
7427 const pathParts = splitPathRe.exec(path);
7428 if (pathParts) {
7429 return pathParts.pop().toLowerCase();
7430 }
7431 }
7432 return '';
7433};
7434
7435/**
7436 * Returns whether the url passed is a cross domain request or not.
7437 *
7438 * @function
7439 * @param {string} url
7440 * The url to check.
7441 *
7442 * @param {URL} [winLoc]
7443 * the domain to check the url against, defaults to window.location
7444 *
7445 * @return {boolean}
7446 * Whether it is a cross domain request or not.
7447 */
7448const isCrossOrigin = function (url, winLoc = window__default["default"].location) {
7449 return parseUrl(url).origin !== winLoc.origin;
7450};
7451
7452var Url = /*#__PURE__*/Object.freeze({
7453 __proto__: null,
7454 parseUrl: parseUrl,
7455 getAbsoluteURL: getAbsoluteURL,
7456 getFileExtension: getFileExtension,
7457 isCrossOrigin: isCrossOrigin
7458});
7459
7460/**
7461 * @file text-track.js
7462 */
7463
7464/** @import Tech from '../tech/tech' */
7465
7466/**
7467 * Takes a webvtt file contents and parses it into cues
7468 *
7469 * @param {string} srcContent
7470 * webVTT file contents
7471 *
7472 * @param {TextTrack} track
7473 * TextTrack to add cues to. Cues come from the srcContent.
7474 *
7475 * @private
7476 */
7477const parseCues = function (srcContent, track) {
7478 const parser = new window__default["default"].WebVTT.Parser(window__default["default"], window__default["default"].vttjs, window__default["default"].WebVTT.StringDecoder());
7479 const errors = [];
7480 parser.oncue = function (cue) {
7481 track.addCue(cue);
7482 };
7483 parser.onparsingerror = function (error) {
7484 errors.push(error);
7485 };
7486 parser.onflush = function () {
7487 track.trigger({
7488 type: 'loadeddata',
7489 target: track
7490 });
7491 };
7492 parser.parse(srcContent);
7493 if (errors.length > 0) {
7494 if (window__default["default"].console && window__default["default"].console.groupCollapsed) {
7495 window__default["default"].console.groupCollapsed(`Text Track parsing errors for ${track.src}`);
7496 }
7497 errors.forEach(error => log.error(error));
7498 if (window__default["default"].console && window__default["default"].console.groupEnd) {
7499 window__default["default"].console.groupEnd();
7500 }
7501 }
7502 parser.flush();
7503};
7504
7505/**
7506 * Load a `TextTrack` from a specified url.
7507 *
7508 * @param {string} src
7509 * Url to load track from.
7510 *
7511 * @param {TextTrack} track
7512 * Track to add cues to. Comes from the content at the end of `url`.
7513 *
7514 * @private
7515 */
7516const loadTrack = function (src, track) {
7517 const opts = {
7518 uri: src
7519 };
7520 const crossOrigin = isCrossOrigin(src);
7521 if (crossOrigin) {
7522 opts.cors = crossOrigin;
7523 }
7524 const withCredentials = track.tech_.crossOrigin() === 'use-credentials';
7525 if (withCredentials) {
7526 opts.withCredentials = withCredentials;
7527 }
7528 XHR__default["default"](opts, bind_(this, function (err, response, responseBody) {
7529 if (err) {
7530 return log.error(err, response);
7531 }
7532 track.loaded_ = true;
7533
7534 // Make sure that vttjs has loaded, otherwise, wait till it finished loading
7535 // NOTE: this is only used for the alt/video.novtt.js build
7536 if (typeof window__default["default"].WebVTT !== 'function') {
7537 if (track.tech_) {
7538 // to prevent use before define eslint error, we define loadHandler
7539 // as a let here
7540 track.tech_.any(['vttjsloaded', 'vttjserror'], event => {
7541 if (event.type === 'vttjserror') {
7542 log.error(`vttjs failed to load, stopping trying to process ${track.src}`);
7543 return;
7544 }
7545 return parseCues(responseBody, track);
7546 });
7547 }
7548 } else {
7549 parseCues(responseBody, track);
7550 }
7551 }));
7552};
7553
7554/**
7555 * A representation of a single `TextTrack`.
7556 *
7557 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrack}
7558 * @extends Track
7559 */
7560class TextTrack extends Track {
7561 /**
7562 * Create an instance of this class.
7563 *
7564 * @param {Object} options={}
7565 * Object of option names and values
7566 *
7567 * @param {Tech} options.tech
7568 * A reference to the tech that owns this TextTrack.
7569 *
7570 * @param {TextTrack~Kind} [options.kind='subtitles']
7571 * A valid text track kind.
7572 *
7573 * @param {TextTrack~Mode} [options.mode='disabled']
7574 * A valid text track mode.
7575 *
7576 * @param {string} [options.id='vjs_track_' + Guid.newGUID()]
7577 * A unique id for this TextTrack.
7578 *
7579 * @param {string} [options.label='']
7580 * The menu label for this track.
7581 *
7582 * @param {string} [options.language='']
7583 * A valid two character language code.
7584 *
7585 * @param {string} [options.srclang='']
7586 * A valid two character language code. An alternative, but deprioritized
7587 * version of `options.language`
7588 *
7589 * @param {string} [options.src]
7590 * A url to TextTrack cues.
7591 *
7592 * @param {boolean} [options.default]
7593 * If this track should default to on or off.
7594 */
7595 constructor(options = {}) {
7596 if (!options.tech) {
7597 throw new Error('A tech was not provided.');
7598 }
7599 const settings = merge(options, {
7600 kind: TextTrackKind[options.kind] || 'subtitles',
7601 language: options.language || options.srclang || ''
7602 });
7603 let mode = TextTrackMode[settings.mode] || 'disabled';
7604 const default_ = settings.default;
7605 if (settings.kind === 'metadata' || settings.kind === 'chapters') {
7606 mode = 'hidden';
7607 }
7608 super(settings);
7609 this.tech_ = settings.tech;
7610 this.cues_ = [];
7611 this.activeCues_ = [];
7612 this.preload_ = this.tech_.preloadTextTracks !== false;
7613 const cues = new TextTrackCueList(this.cues_);
7614 const activeCues = new TextTrackCueList(this.activeCues_);
7615 let changed = false;
7616 this.timeupdateHandler = bind_(this, function (event = {}) {
7617 if (this.tech_.isDisposed()) {
7618 return;
7619 }
7620 if (!this.tech_.isReady_) {
7621 if (event.type !== 'timeupdate') {
7622 this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler);
7623 }
7624 return;
7625 }
7626
7627 // Accessing this.activeCues for the side-effects of updating itself
7628 // due to its nature as a getter function. Do not remove or cues will
7629 // stop updating!
7630 // Use the setter to prevent deletion from uglify (pure_getters rule)
7631 this.activeCues = this.activeCues;
7632 if (changed) {
7633 this.trigger('cuechange');
7634 changed = false;
7635 }
7636 if (event.type !== 'timeupdate') {
7637 this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler);
7638 }
7639 });
7640 const disposeHandler = () => {
7641 this.stopTracking();
7642 };
7643 this.tech_.one('dispose', disposeHandler);
7644 if (mode !== 'disabled') {
7645 this.startTracking();
7646 }
7647 Object.defineProperties(this, {
7648 /**
7649 * @memberof TextTrack
7650 * @member {boolean} default
7651 * If this track was set to be on or off by default. Cannot be changed after
7652 * creation.
7653 * @instance
7654 *
7655 * @readonly
7656 */
7657 default: {
7658 get() {
7659 return default_;
7660 },
7661 set() {}
7662 },
7663 /**
7664 * @memberof TextTrack
7665 * @member {string} mode
7666 * Set the mode of this TextTrack to a valid {@link TextTrack~Mode}. Will
7667 * not be set if setting to an invalid mode.
7668 * @instance
7669 *
7670 * @fires TextTrack#modechange
7671 */
7672 mode: {
7673 get() {
7674 return mode;
7675 },
7676 set(newMode) {
7677 if (!TextTrackMode[newMode]) {
7678 return;
7679 }
7680 if (mode === newMode) {
7681 return;
7682 }
7683 mode = newMode;
7684 if (!this.preload_ && mode !== 'disabled' && this.cues.length === 0) {
7685 // On-demand load.
7686 loadTrack(this.src, this);
7687 }
7688 this.stopTracking();
7689 if (mode !== 'disabled') {
7690 this.startTracking();
7691 }
7692 /**
7693 * An event that fires when mode changes on this track. This allows
7694 * the TextTrackList that holds this track to act accordingly.
7695 *
7696 * > Note: This is not part of the spec!
7697 *
7698 * @event TextTrack#modechange
7699 * @type {Event}
7700 */
7701 this.trigger('modechange');
7702 }
7703 },
7704 /**
7705 * @memberof TextTrack
7706 * @member {TextTrackCueList} cues
7707 * The text track cue list for this TextTrack.
7708 * @instance
7709 */
7710 cues: {
7711 get() {
7712 if (!this.loaded_) {
7713 return null;
7714 }
7715 return cues;
7716 },
7717 set() {}
7718 },
7719 /**
7720 * @memberof TextTrack
7721 * @member {TextTrackCueList} activeCues
7722 * The list text track cues that are currently active for this TextTrack.
7723 * @instance
7724 */
7725 activeCues: {
7726 get() {
7727 if (!this.loaded_) {
7728 return null;
7729 }
7730
7731 // nothing to do
7732 if (this.cues.length === 0) {
7733 return activeCues;
7734 }
7735 const ct = this.tech_.currentTime();
7736 const active = [];
7737 for (let i = 0, l = this.cues.length; i < l; i++) {
7738 const cue = this.cues[i];
7739 if (cue.startTime <= ct && cue.endTime >= ct) {
7740 active.push(cue);
7741 }
7742 }
7743 changed = false;
7744 if (active.length !== this.activeCues_.length) {
7745 changed = true;
7746 } else {
7747 for (let i = 0; i < active.length; i++) {
7748 if (this.activeCues_.indexOf(active[i]) === -1) {
7749 changed = true;
7750 }
7751 }
7752 }
7753 this.activeCues_ = active;
7754 activeCues.setCues_(this.activeCues_);
7755 return activeCues;
7756 },
7757 // /!\ Keep this setter empty (see the timeupdate handler above)
7758 set() {}
7759 }
7760 });
7761 if (settings.src) {
7762 this.src = settings.src;
7763 if (!this.preload_) {
7764 // Tracks will load on-demand.
7765 // Act like we're loaded for other purposes.
7766 this.loaded_ = true;
7767 }
7768 if (this.preload_ || settings.kind !== 'subtitles' && settings.kind !== 'captions') {
7769 loadTrack(this.src, this);
7770 }
7771 } else {
7772 this.loaded_ = true;
7773 }
7774 }
7775 startTracking() {
7776 // More precise cues based on requestVideoFrameCallback with a requestAnimationFram fallback
7777 this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler);
7778 // Also listen to timeupdate in case rVFC/rAF stops (window in background, audio in video el)
7779 this.tech_.on('timeupdate', this.timeupdateHandler);
7780 }
7781 stopTracking() {
7782 if (this.rvf_) {
7783 this.tech_.cancelVideoFrameCallback(this.rvf_);
7784 this.rvf_ = undefined;
7785 }
7786 this.tech_.off('timeupdate', this.timeupdateHandler);
7787 }
7788
7789 /**
7790 * Add a cue to the internal list of cues.
7791 *
7792 * @param {TextTrack~Cue} cue
7793 * The cue to add to our internal list
7794 */
7795 addCue(originalCue) {
7796 let cue = originalCue;
7797
7798 // Testing if the cue is a VTTCue in a way that survives minification
7799 if (!('getCueAsHTML' in cue)) {
7800 cue = new window__default["default"].vttjs.VTTCue(originalCue.startTime, originalCue.endTime, originalCue.text);
7801 for (const prop in originalCue) {
7802 if (!(prop in cue)) {
7803 cue[prop] = originalCue[prop];
7804 }
7805 }
7806
7807 // make sure that `id` is copied over
7808 cue.id = originalCue.id;
7809 cue.originalCue_ = originalCue;
7810 }
7811 const tracks = this.tech_.textTracks();
7812 for (let i = 0; i < tracks.length; i++) {
7813 if (tracks[i] !== this) {
7814 tracks[i].removeCue(cue);
7815 }
7816 }
7817 this.cues_.push(cue);
7818 this.cues.setCues_(this.cues_);
7819 }
7820
7821 /**
7822 * Remove a cue from our internal list
7823 *
7824 * @param {TextTrack~Cue} removeCue
7825 * The cue to remove from our internal list
7826 */
7827 removeCue(removeCue) {
7828 let i = this.cues_.length;
7829 while (i--) {
7830 const cue = this.cues_[i];
7831 if (cue === removeCue || cue.originalCue_ && cue.originalCue_ === removeCue) {
7832 this.cues_.splice(i, 1);
7833 this.cues.setCues_(this.cues_);
7834 break;
7835 }
7836 }
7837 }
7838}
7839
7840/**
7841 * cuechange - One or more cues in the track have become active or stopped being active.
7842 *
7843 * @protected
7844 */
7845TextTrack.prototype.allowedEvents_ = {
7846 cuechange: 'cuechange'
7847};
7848
7849/**
7850 * A representation of a single `AudioTrack`. If it is part of an {@link AudioTrackList}
7851 * only one `AudioTrack` in the list will be enabled at a time.
7852 *
7853 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotrack}
7854 * @extends Track
7855 */
7856class AudioTrack extends Track {
7857 /**
7858 * Create an instance of this class.
7859 *
7860 * @param {Object} [options={}]
7861 * Object of option names and values
7862 *
7863 * @param {AudioTrack~Kind} [options.kind='']
7864 * A valid audio track kind
7865 *
7866 * @param {string} [options.id='vjs_track_' + Guid.newGUID()]
7867 * A unique id for this AudioTrack.
7868 *
7869 * @param {string} [options.label='']
7870 * The menu label for this track.
7871 *
7872 * @param {string} [options.language='']
7873 * A valid two character language code.
7874 *
7875 * @param {boolean} [options.enabled]
7876 * If this track is the one that is currently playing. If this track is part of
7877 * an {@link AudioTrackList}, only one {@link AudioTrack} will be enabled.
7878 */
7879 constructor(options = {}) {
7880 const settings = merge(options, {
7881 kind: AudioTrackKind[options.kind] || ''
7882 });
7883 super(settings);
7884 let enabled = false;
7885
7886 /**
7887 * @memberof AudioTrack
7888 * @member {boolean} enabled
7889 * If this `AudioTrack` is enabled or not. When setting this will
7890 * fire {@link AudioTrack#enabledchange} if the state of enabled is changed.
7891 * @instance
7892 *
7893 * @fires VideoTrack#selectedchange
7894 */
7895 Object.defineProperty(this, 'enabled', {
7896 get() {
7897 return enabled;
7898 },
7899 set(newEnabled) {
7900 // an invalid or unchanged value
7901 if (typeof newEnabled !== 'boolean' || newEnabled === enabled) {
7902 return;
7903 }
7904 enabled = newEnabled;
7905
7906 /**
7907 * An event that fires when enabled changes on this track. This allows
7908 * the AudioTrackList that holds this track to act accordingly.
7909 *
7910 * > Note: This is not part of the spec! Native tracks will do
7911 * this internally without an event.
7912 *
7913 * @event AudioTrack#enabledchange
7914 * @type {Event}
7915 */
7916 this.trigger('enabledchange');
7917 }
7918 });
7919
7920 // if the user sets this track to selected then
7921 // set selected to that true value otherwise
7922 // we keep it false
7923 if (settings.enabled) {
7924 this.enabled = settings.enabled;
7925 }
7926 this.loaded_ = true;
7927 }
7928}
7929
7930/**
7931 * A representation of a single `VideoTrack`.
7932 *
7933 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#videotrack}
7934 * @extends Track
7935 */
7936class VideoTrack extends Track {
7937 /**
7938 * Create an instance of this class.
7939 *
7940 * @param {Object} [options={}]
7941 * Object of option names and values
7942 *
7943 * @param {string} [options.kind='']
7944 * A valid {@link VideoTrack~Kind}
7945 *
7946 * @param {string} [options.id='vjs_track_' + Guid.newGUID()]
7947 * A unique id for this AudioTrack.
7948 *
7949 * @param {string} [options.label='']
7950 * The menu label for this track.
7951 *
7952 * @param {string} [options.language='']
7953 * A valid two character language code.
7954 *
7955 * @param {boolean} [options.selected]
7956 * If this track is the one that is currently playing.
7957 */
7958 constructor(options = {}) {
7959 const settings = merge(options, {
7960 kind: VideoTrackKind[options.kind] || ''
7961 });
7962 super(settings);
7963 let selected = false;
7964
7965 /**
7966 * @memberof VideoTrack
7967 * @member {boolean} selected
7968 * If this `VideoTrack` is selected or not. When setting this will
7969 * fire {@link VideoTrack#selectedchange} if the state of selected changed.
7970 * @instance
7971 *
7972 * @fires VideoTrack#selectedchange
7973 */
7974 Object.defineProperty(this, 'selected', {
7975 get() {
7976 return selected;
7977 },
7978 set(newSelected) {
7979 // an invalid or unchanged value
7980 if (typeof newSelected !== 'boolean' || newSelected === selected) {
7981 return;
7982 }
7983 selected = newSelected;
7984
7985 /**
7986 * An event that fires when selected changes on this track. This allows
7987 * the VideoTrackList that holds this track to act accordingly.
7988 *
7989 * > Note: This is not part of the spec! Native tracks will do
7990 * this internally without an event.
7991 *
7992 * @event VideoTrack#selectedchange
7993 * @type {Event}
7994 */
7995 this.trigger('selectedchange');
7996 }
7997 });
7998
7999 // if the user sets this track to selected then
8000 // set selected to that true value otherwise
8001 // we keep it false
8002 if (settings.selected) {
8003 this.selected = settings.selected;
8004 }
8005 }
8006}
8007
8008/**
8009 * @file html-track-element.js
8010 */
8011
8012/** @import Tech from '../tech/tech' */
8013
8014/**
8015 * A single track represented in the DOM.
8016 *
8017 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#htmltrackelement}
8018 * @extends EventTarget
8019 */
8020class HTMLTrackElement extends EventTarget {
8021 /**
8022 * Create an instance of this class.
8023 *
8024 * @param {Object} options={}
8025 * Object of option names and values
8026 *
8027 * @param {Tech} options.tech
8028 * A reference to the tech that owns this HTMLTrackElement.
8029 *
8030 * @param {TextTrack~Kind} [options.kind='subtitles']
8031 * A valid text track kind.
8032 *
8033 * @param {TextTrack~Mode} [options.mode='disabled']
8034 * A valid text track mode.
8035 *
8036 * @param {string} [options.id='vjs_track_' + Guid.newGUID()]
8037 * A unique id for this TextTrack.
8038 *
8039 * @param {string} [options.label='']
8040 * The menu label for this track.
8041 *
8042 * @param {string} [options.language='']
8043 * A valid two character language code.
8044 *
8045 * @param {string} [options.srclang='']
8046 * A valid two character language code. An alternative, but deprioritized
8047 * version of `options.language`
8048 *
8049 * @param {string} [options.src]
8050 * A url to TextTrack cues.
8051 *
8052 * @param {boolean} [options.default]
8053 * If this track should default to on or off.
8054 */
8055 constructor(options = {}) {
8056 super();
8057 let readyState;
8058 const track = new TextTrack(options);
8059 this.kind = track.kind;
8060 this.src = track.src;
8061 this.srclang = track.language;
8062 this.label = track.label;
8063 this.default = track.default;
8064 Object.defineProperties(this, {
8065 /**
8066 * @memberof HTMLTrackElement
8067 * @member {HTMLTrackElement~ReadyState} readyState
8068 * The current ready state of the track element.
8069 * @instance
8070 */
8071 readyState: {
8072 get() {
8073 return readyState;
8074 }
8075 },
8076 /**
8077 * @memberof HTMLTrackElement
8078 * @member {TextTrack} track
8079 * The underlying TextTrack object.
8080 * @instance
8081 *
8082 */
8083 track: {
8084 get() {
8085 return track;
8086 }
8087 }
8088 });
8089 readyState = HTMLTrackElement.NONE;
8090
8091 /**
8092 * @listens TextTrack#loadeddata
8093 * @fires HTMLTrackElement#load
8094 */
8095 track.addEventListener('loadeddata', () => {
8096 readyState = HTMLTrackElement.LOADED;
8097 this.trigger({
8098 type: 'load',
8099 target: this
8100 });
8101 });
8102 }
8103}
8104
8105/**
8106 * @protected
8107 */
8108HTMLTrackElement.prototype.allowedEvents_ = {
8109 load: 'load'
8110};
8111
8112/**
8113 * The text track not loaded state.
8114 *
8115 * @type {number}
8116 * @static
8117 */
8118HTMLTrackElement.NONE = 0;
8119
8120/**
8121 * The text track loading state.
8122 *
8123 * @type {number}
8124 * @static
8125 */
8126HTMLTrackElement.LOADING = 1;
8127
8128/**
8129 * The text track loaded state.
8130 *
8131 * @type {number}
8132 * @static
8133 */
8134HTMLTrackElement.LOADED = 2;
8135
8136/**
8137 * The text track failed to load state.
8138 *
8139 * @type {number}
8140 * @static
8141 */
8142HTMLTrackElement.ERROR = 3;
8143
8144/*
8145 * This file contains all track properties that are used in
8146 * player.js, tech.js, html5.js and possibly other techs in the future.
8147 */
8148
8149const NORMAL = {
8150 audio: {
8151 ListClass: AudioTrackList,
8152 TrackClass: AudioTrack,
8153 capitalName: 'Audio'
8154 },
8155 video: {
8156 ListClass: VideoTrackList,
8157 TrackClass: VideoTrack,
8158 capitalName: 'Video'
8159 },
8160 text: {
8161 ListClass: TextTrackList,
8162 TrackClass: TextTrack,
8163 capitalName: 'Text'
8164 }
8165};
8166Object.keys(NORMAL).forEach(function (type) {
8167 NORMAL[type].getterName = `${type}Tracks`;
8168 NORMAL[type].privateName = `${type}Tracks_`;
8169});
8170const REMOTE = {
8171 remoteText: {
8172 ListClass: TextTrackList,
8173 TrackClass: TextTrack,
8174 capitalName: 'RemoteText',
8175 getterName: 'remoteTextTracks',
8176 privateName: 'remoteTextTracks_'
8177 },
8178 remoteTextEl: {
8179 ListClass: HtmlTrackElementList,
8180 TrackClass: HTMLTrackElement,
8181 capitalName: 'RemoteTextTrackEls',
8182 getterName: 'remoteTextTrackEls',
8183 privateName: 'remoteTextTrackEls_'
8184 }
8185};
8186const ALL = Object.assign({}, NORMAL, REMOTE);
8187REMOTE.names = Object.keys(REMOTE);
8188NORMAL.names = Object.keys(NORMAL);
8189ALL.names = [].concat(REMOTE.names).concat(NORMAL.names);
8190
8191/**
8192 * @file tech.js
8193 */
8194
8195/** @import { TimeRange } from '../utils/time' */
8196
8197/**
8198 * An Object containing a structure like: `{src: 'url', type: 'mimetype'}` or string
8199 * that just contains the src url alone.
8200 * * `var SourceObject = {src: 'http://ex.com/video.mp4', type: 'video/mp4'};`
8201 * `var SourceString = 'http://example.com/some-video.mp4';`
8202 *
8203 * @typedef {Object|string} SourceObject
8204 *
8205 * @property {string} src
8206 * The url to the source
8207 *
8208 * @property {string} type
8209 * The mime type of the source
8210 */
8211
8212/**
8213 * A function used by {@link Tech} to create a new {@link TextTrack}.
8214 *
8215 * @private
8216 *
8217 * @param {Tech} self
8218 * An instance of the Tech class.
8219 *
8220 * @param {string} kind
8221 * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata)
8222 *
8223 * @param {string} [label]
8224 * Label to identify the text track
8225 *
8226 * @param {string} [language]
8227 * Two letter language abbreviation
8228 *
8229 * @param {Object} [options={}]
8230 * An object with additional text track options
8231 *
8232 * @return {TextTrack}
8233 * The text track that was created.
8234 */
8235function createTrackHelper(self, kind, label, language, options = {}) {
8236 const tracks = self.textTracks();
8237 options.kind = kind;
8238 if (label) {
8239 options.label = label;
8240 }
8241 if (language) {
8242 options.language = language;
8243 }
8244 options.tech = self;
8245 const track = new ALL.text.TrackClass(options);
8246 tracks.addTrack(track);
8247 return track;
8248}
8249
8250/**
8251 * This is the base class for media playback technology controllers, such as
8252 * {@link HTML5}
8253 *
8254 * @extends Component
8255 */
8256class Tech extends Component {
8257 /**
8258 * Create an instance of this Tech.
8259 *
8260 * @param {Object} [options]
8261 * The key/value store of player options.
8262 *
8263 * @param {Function} [ready]
8264 * Callback function to call when the `HTML5` Tech is ready.
8265 */
8266 constructor(options = {}, ready = function () {}) {
8267 // we don't want the tech to report user activity automatically.
8268 // This is done manually in addControlsListeners
8269 options.reportTouchActivity = false;
8270 super(null, options, ready);
8271 this.onDurationChange_ = e => this.onDurationChange(e);
8272 this.trackProgress_ = e => this.trackProgress(e);
8273 this.trackCurrentTime_ = e => this.trackCurrentTime(e);
8274 this.stopTrackingCurrentTime_ = e => this.stopTrackingCurrentTime(e);
8275 this.disposeSourceHandler_ = e => this.disposeSourceHandler(e);
8276 this.queuedHanders_ = new Set();
8277
8278 // keep track of whether the current source has played at all to
8279 // implement a very limited played()
8280 this.hasStarted_ = false;
8281 this.on('playing', function () {
8282 this.hasStarted_ = true;
8283 });
8284 this.on('loadstart', function () {
8285 this.hasStarted_ = false;
8286 });
8287 ALL.names.forEach(name => {
8288 const props = ALL[name];
8289 if (options && options[props.getterName]) {
8290 this[props.privateName] = options[props.getterName];
8291 }
8292 });
8293
8294 // Manually track progress in cases where the browser/tech doesn't report it.
8295 if (!this.featuresProgressEvents) {
8296 this.manualProgressOn();
8297 }
8298
8299 // Manually track timeupdates in cases where the browser/tech doesn't report it.
8300 if (!this.featuresTimeupdateEvents) {
8301 this.manualTimeUpdatesOn();
8302 }
8303 ['Text', 'Audio', 'Video'].forEach(track => {
8304 if (options[`native${track}Tracks`] === false) {
8305 this[`featuresNative${track}Tracks`] = false;
8306 }
8307 });
8308 if (options.nativeCaptions === false || options.nativeTextTracks === false) {
8309 this.featuresNativeTextTracks = false;
8310 } else if (options.nativeCaptions === true || options.nativeTextTracks === true) {
8311 this.featuresNativeTextTracks = true;
8312 }
8313 if (!this.featuresNativeTextTracks) {
8314 this.emulateTextTracks();
8315 }
8316 this.preloadTextTracks = options.preloadTextTracks !== false;
8317 this.autoRemoteTextTracks_ = new ALL.text.ListClass();
8318 this.initTrackListeners();
8319
8320 // Turn on component tap events only if not using native controls
8321 if (!options.nativeControlsForTouch) {
8322 this.emitTapEvents();
8323 }
8324 if (this.constructor) {
8325 this.name_ = this.constructor.name || 'Unknown Tech';
8326 }
8327 }
8328
8329 /**
8330 * A special function to trigger source set in a way that will allow player
8331 * to re-trigger if the player or tech are not ready yet.
8332 *
8333 * @fires Tech#sourceset
8334 * @param {string} src The source string at the time of the source changing.
8335 */
8336 triggerSourceset(src) {
8337 if (!this.isReady_) {
8338 // on initial ready we have to trigger source set
8339 // 1ms after ready so that player can watch for it.
8340 this.one('ready', () => this.setTimeout(() => this.triggerSourceset(src), 1));
8341 }
8342
8343 /**
8344 * Fired when the source is set on the tech causing the media element
8345 * to reload.
8346 *
8347 * @see {@link Player#event:sourceset}
8348 * @event Tech#sourceset
8349 * @type {Event}
8350 */
8351 this.trigger({
8352 src,
8353 type: 'sourceset'
8354 });
8355 }
8356
8357 /* Fallbacks for unsupported event types
8358 ================================================================================ */
8359
8360 /**
8361 * Polyfill the `progress` event for browsers that don't support it natively.
8362 *
8363 * @see {@link Tech#trackProgress}
8364 */
8365 manualProgressOn() {
8366 this.on('durationchange', this.onDurationChange_);
8367 this.manualProgress = true;
8368
8369 // Trigger progress watching when a source begins loading
8370 this.one('ready', this.trackProgress_);
8371 }
8372
8373 /**
8374 * Turn off the polyfill for `progress` events that was created in
8375 * {@link Tech#manualProgressOn}
8376 */
8377 manualProgressOff() {
8378 this.manualProgress = false;
8379 this.stopTrackingProgress();
8380 this.off('durationchange', this.onDurationChange_);
8381 }
8382
8383 /**
8384 * This is used to trigger a `progress` event when the buffered percent changes. It
8385 * sets an interval function that will be called every 500 milliseconds to check if the
8386 * buffer end percent has changed.
8387 *
8388 * > This function is called by {@link Tech#manualProgressOn}
8389 *
8390 * @param {Event} event
8391 * The `ready` event that caused this to run.
8392 *
8393 * @listens Tech#ready
8394 * @fires Tech#progress
8395 */
8396 trackProgress(event) {
8397 this.stopTrackingProgress();
8398 this.progressInterval = this.setInterval(bind_(this, function () {
8399 // Don't trigger unless buffered amount is greater than last time
8400
8401 const numBufferedPercent = this.bufferedPercent();
8402 if (this.bufferedPercent_ !== numBufferedPercent) {
8403 /**
8404 * See {@link Player#progress}
8405 *
8406 * @event Tech#progress
8407 * @type {Event}
8408 */
8409 this.trigger('progress');
8410 }
8411 this.bufferedPercent_ = numBufferedPercent;
8412 if (numBufferedPercent === 1) {
8413 this.stopTrackingProgress();
8414 }
8415 }), 500);
8416 }
8417
8418 /**
8419 * Update our internal duration on a `durationchange` event by calling
8420 * {@link Tech#duration}.
8421 *
8422 * @param {Event} event
8423 * The `durationchange` event that caused this to run.
8424 *
8425 * @listens Tech#durationchange
8426 */
8427 onDurationChange(event) {
8428 this.duration_ = this.duration();
8429 }
8430
8431 /**
8432 * Get and create a `TimeRange` object for buffering.
8433 *
8434 * @return {TimeRange}
8435 * The time range object that was created.
8436 */
8437 buffered() {
8438 return createTimeRanges(0, 0);
8439 }
8440
8441 /**
8442 * Get the percentage of the current video that is currently buffered.
8443 *
8444 * @return {number}
8445 * A number from 0 to 1 that represents the decimal percentage of the
8446 * video that is buffered.
8447 *
8448 */
8449 bufferedPercent() {
8450 return bufferedPercent(this.buffered(), this.duration_);
8451 }
8452
8453 /**
8454 * Turn off the polyfill for `progress` events that was created in
8455 * {@link Tech#manualProgressOn}
8456 * Stop manually tracking progress events by clearing the interval that was set in
8457 * {@link Tech#trackProgress}.
8458 */
8459 stopTrackingProgress() {
8460 this.clearInterval(this.progressInterval);
8461 }
8462
8463 /**
8464 * Polyfill the `timeupdate` event for browsers that don't support it.
8465 *
8466 * @see {@link Tech#trackCurrentTime}
8467 */
8468 manualTimeUpdatesOn() {
8469 this.manualTimeUpdates = true;
8470 this.on('play', this.trackCurrentTime_);
8471 this.on('pause', this.stopTrackingCurrentTime_);
8472 }
8473
8474 /**
8475 * Turn off the polyfill for `timeupdate` events that was created in
8476 * {@link Tech#manualTimeUpdatesOn}
8477 */
8478 manualTimeUpdatesOff() {
8479 this.manualTimeUpdates = false;
8480 this.stopTrackingCurrentTime();
8481 this.off('play', this.trackCurrentTime_);
8482 this.off('pause', this.stopTrackingCurrentTime_);
8483 }
8484
8485 /**
8486 * Sets up an interval function to track current time and trigger `timeupdate` every
8487 * 250 milliseconds.
8488 *
8489 * @listens Tech#play
8490 * @triggers Tech#timeupdate
8491 */
8492 trackCurrentTime() {
8493 if (this.currentTimeInterval) {
8494 this.stopTrackingCurrentTime();
8495 }
8496 this.currentTimeInterval = this.setInterval(function () {
8497 /**
8498 * Triggered at an interval of 250ms to indicated that time is passing in the video.
8499 *
8500 * @event Tech#timeupdate
8501 * @type {Event}
8502 */
8503 this.trigger({
8504 type: 'timeupdate',
8505 target: this,
8506 manuallyTriggered: true
8507 });
8508
8509 // 42 = 24 fps // 250 is what Webkit uses // FF uses 15
8510 }, 250);
8511 }
8512
8513 /**
8514 * Stop the interval function created in {@link Tech#trackCurrentTime} so that the
8515 * `timeupdate` event is no longer triggered.
8516 *
8517 * @listens {Tech#pause}
8518 */
8519 stopTrackingCurrentTime() {
8520 this.clearInterval(this.currentTimeInterval);
8521
8522 // #1002 - if the video ends right before the next timeupdate would happen,
8523 // the progress bar won't make it all the way to the end
8524 this.trigger({
8525 type: 'timeupdate',
8526 target: this,
8527 manuallyTriggered: true
8528 });
8529 }
8530
8531 /**
8532 * Turn off all event polyfills, clear the `Tech`s {@link AudioTrackList},
8533 * {@link VideoTrackList}, and {@link TextTrackList}, and dispose of this Tech.
8534 *
8535 * @fires Component#dispose
8536 */
8537 dispose() {
8538 // clear out all tracks because we can't reuse them between techs
8539 this.clearTracks(NORMAL.names);
8540
8541 // Turn off any manual progress or timeupdate tracking
8542 if (this.manualProgress) {
8543 this.manualProgressOff();
8544 }
8545 if (this.manualTimeUpdates) {
8546 this.manualTimeUpdatesOff();
8547 }
8548 super.dispose();
8549 }
8550
8551 /**
8552 * Clear out a single `TrackList` or an array of `TrackLists` given their names.
8553 *
8554 * > Note: Techs without source handlers should call this between sources for `video`
8555 * & `audio` tracks. You don't want to use them between tracks!
8556 *
8557 * @param {string[]|string} types
8558 * TrackList names to clear, valid names are `video`, `audio`, and
8559 * `text`.
8560 */
8561 clearTracks(types) {
8562 types = [].concat(types);
8563 // clear out all tracks because we can't reuse them between techs
8564 types.forEach(type => {
8565 const list = this[`${type}Tracks`]() || [];
8566 let i = list.length;
8567 while (i--) {
8568 const track = list[i];
8569 if (type === 'text') {
8570 this.removeRemoteTextTrack(track);
8571 }
8572 list.removeTrack(track);
8573 }
8574 });
8575 }
8576
8577 /**
8578 * Remove any TextTracks added via addRemoteTextTrack that are
8579 * flagged for automatic garbage collection
8580 */
8581 cleanupAutoTextTracks() {
8582 const list = this.autoRemoteTextTracks_ || [];
8583 let i = list.length;
8584 while (i--) {
8585 const track = list[i];
8586 this.removeRemoteTextTrack(track);
8587 }
8588 }
8589
8590 /**
8591 * Reset the tech, which will removes all sources and reset the internal readyState.
8592 *
8593 * @abstract
8594 */
8595 reset() {}
8596
8597 /**
8598 * Get the value of `crossOrigin` from the tech.
8599 *
8600 * @abstract
8601 *
8602 * @see {Html5#crossOrigin}
8603 */
8604 crossOrigin() {}
8605
8606 /**
8607 * Set the value of `crossOrigin` on the tech.
8608 *
8609 * @abstract
8610 *
8611 * @param {string} crossOrigin the crossOrigin value
8612 * @see {Html5#setCrossOrigin}
8613 */
8614 setCrossOrigin() {}
8615
8616 /**
8617 * Get or set an error on the Tech.
8618 *
8619 * @param {MediaError} [err]
8620 * Error to set on the Tech
8621 *
8622 * @return {MediaError|null}
8623 * The current error object on the tech, or null if there isn't one.
8624 */
8625 error(err) {
8626 if (err !== undefined) {
8627 this.error_ = new MediaError(err);
8628 this.trigger('error');
8629 }
8630 return this.error_;
8631 }
8632
8633 /**
8634 * Returns the `TimeRange`s that have been played through for the current source.
8635 *
8636 * > NOTE: This implementation is incomplete. It does not track the played `TimeRange`.
8637 * It only checks whether the source has played at all or not.
8638 *
8639 * @return {TimeRange}
8640 * - A single time range if this video has played
8641 * - An empty set of ranges if not.
8642 */
8643 played() {
8644 if (this.hasStarted_) {
8645 return createTimeRanges(0, 0);
8646 }
8647 return createTimeRanges();
8648 }
8649
8650 /**
8651 * Start playback
8652 *
8653 * @abstract
8654 *
8655 * @see {Html5#play}
8656 */
8657 play() {}
8658
8659 /**
8660 * Set whether we are scrubbing or not
8661 *
8662 * @abstract
8663 * @param {boolean} _isScrubbing
8664 * - true for we are currently scrubbing
8665 * - false for we are no longer scrubbing
8666 *
8667 * @see {Html5#setScrubbing}
8668 */
8669 setScrubbing(_isScrubbing) {}
8670
8671 /**
8672 * Get whether we are scrubbing or not
8673 *
8674 * @abstract
8675 *
8676 * @see {Html5#scrubbing}
8677 */
8678 scrubbing() {}
8679
8680 /**
8681 * Causes a manual time update to occur if {@link Tech#manualTimeUpdatesOn} was
8682 * previously called.
8683 *
8684 * @param {number} _seconds
8685 * Set the current time of the media to this.
8686 * @fires Tech#timeupdate
8687 */
8688 setCurrentTime(_seconds) {
8689 // improve the accuracy of manual timeupdates
8690 if (this.manualTimeUpdates) {
8691 /**
8692 * A manual `timeupdate` event.
8693 *
8694 * @event Tech#timeupdate
8695 * @type {Event}
8696 */
8697 this.trigger({
8698 type: 'timeupdate',
8699 target: this,
8700 manuallyTriggered: true
8701 });
8702 }
8703 }
8704
8705 /**
8706 * Turn on listeners for {@link VideoTrackList}, {@link {AudioTrackList}, and
8707 * {@link TextTrackList} events.
8708 *
8709 * This adds {@link EventTarget~EventListeners} for `addtrack`, and `removetrack`.
8710 *
8711 * @fires Tech#audiotrackchange
8712 * @fires Tech#videotrackchange
8713 * @fires Tech#texttrackchange
8714 */
8715 initTrackListeners() {
8716 /**
8717 * Triggered when tracks are added or removed on the Tech {@link AudioTrackList}
8718 *
8719 * @event Tech#audiotrackchange
8720 * @type {Event}
8721 */
8722
8723 /**
8724 * Triggered when tracks are added or removed on the Tech {@link VideoTrackList}
8725 *
8726 * @event Tech#videotrackchange
8727 * @type {Event}
8728 */
8729
8730 /**
8731 * Triggered when tracks are added or removed on the Tech {@link TextTrackList}
8732 *
8733 * @event Tech#texttrackchange
8734 * @type {Event}
8735 */
8736 NORMAL.names.forEach(name => {
8737 const props = NORMAL[name];
8738 const trackListChanges = () => {
8739 this.trigger(`${name}trackchange`);
8740 };
8741 const tracks = this[props.getterName]();
8742 tracks.addEventListener('removetrack', trackListChanges);
8743 tracks.addEventListener('addtrack', trackListChanges);
8744 this.on('dispose', () => {
8745 tracks.removeEventListener('removetrack', trackListChanges);
8746 tracks.removeEventListener('addtrack', trackListChanges);
8747 });
8748 });
8749 }
8750
8751 /**
8752 * Emulate TextTracks using vtt.js if necessary
8753 *
8754 * @fires Tech#vttjsloaded
8755 * @fires Tech#vttjserror
8756 */
8757 addWebVttScript_() {
8758 if (window__default["default"].WebVTT) {
8759 return;
8760 }
8761
8762 // Initially, Tech.el_ is a child of a dummy-div wait until the Component system
8763 // signals that the Tech is ready at which point Tech.el_ is part of the DOM
8764 // before inserting the WebVTT script
8765 if (document__default["default"].body.contains(this.el())) {
8766 // load via require if available and vtt.js script location was not passed in
8767 // as an option. novtt builds will turn the above require call into an empty object
8768 // which will cause this if check to always fail.
8769 if (!this.options_['vtt.js'] && isPlain(vtt__default["default"]) && Object.keys(vtt__default["default"]).length > 0) {
8770 this.trigger('vttjsloaded');
8771 return;
8772 }
8773
8774 // load vtt.js via the script location option or the cdn of no location was
8775 // passed in
8776 const script = document__default["default"].createElement('script');
8777 script.src = this.options_['vtt.js'] || 'https://vjs.zencdn.net/vttjs/0.14.1/vtt.min.js';
8778 script.onload = () => {
8779 /**
8780 * Fired when vtt.js is loaded.
8781 *
8782 * @event Tech#vttjsloaded
8783 * @type {Event}
8784 */
8785 this.trigger('vttjsloaded');
8786 };
8787 script.onerror = () => {
8788 /**
8789 * Fired when vtt.js was not loaded due to an error
8790 *
8791 * @event Tech#vttjsloaded
8792 * @type {Event}
8793 */
8794 this.trigger('vttjserror');
8795 };
8796 this.on('dispose', () => {
8797 script.onload = null;
8798 script.onerror = null;
8799 });
8800 // but have not loaded yet and we set it to true before the inject so that
8801 // we don't overwrite the injected window.WebVTT if it loads right away
8802 window__default["default"].WebVTT = true;
8803 this.el().parentNode.appendChild(script);
8804 } else {
8805 this.ready(this.addWebVttScript_);
8806 }
8807 }
8808
8809 /**
8810 * Emulate texttracks
8811 *
8812 */
8813 emulateTextTracks() {
8814 const tracks = this.textTracks();
8815 const remoteTracks = this.remoteTextTracks();
8816 const handleAddTrack = e => tracks.addTrack(e.track);
8817 const handleRemoveTrack = e => tracks.removeTrack(e.track);
8818 remoteTracks.on('addtrack', handleAddTrack);
8819 remoteTracks.on('removetrack', handleRemoveTrack);
8820 this.addWebVttScript_();
8821 const updateDisplay = () => this.trigger('texttrackchange');
8822 const textTracksChanges = () => {
8823 updateDisplay();
8824 for (let i = 0; i < tracks.length; i++) {
8825 const track = tracks[i];
8826 track.removeEventListener('cuechange', updateDisplay);
8827 if (track.mode === 'showing') {
8828 track.addEventListener('cuechange', updateDisplay);
8829 }
8830 }
8831 };
8832 textTracksChanges();
8833 tracks.addEventListener('change', textTracksChanges);
8834 tracks.addEventListener('addtrack', textTracksChanges);
8835 tracks.addEventListener('removetrack', textTracksChanges);
8836 this.on('dispose', function () {
8837 remoteTracks.off('addtrack', handleAddTrack);
8838 remoteTracks.off('removetrack', handleRemoveTrack);
8839 tracks.removeEventListener('change', textTracksChanges);
8840 tracks.removeEventListener('addtrack', textTracksChanges);
8841 tracks.removeEventListener('removetrack', textTracksChanges);
8842 for (let i = 0; i < tracks.length; i++) {
8843 const track = tracks[i];
8844 track.removeEventListener('cuechange', updateDisplay);
8845 }
8846 });
8847 }
8848
8849 /**
8850 * Create and returns a remote {@link TextTrack} object.
8851 *
8852 * @param {string} kind
8853 * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata)
8854 *
8855 * @param {string} [label]
8856 * Label to identify the text track
8857 *
8858 * @param {string} [language]
8859 * Two letter language abbreviation
8860 *
8861 * @return {TextTrack}
8862 * The TextTrack that gets created.
8863 */
8864 addTextTrack(kind, label, language) {
8865 if (!kind) {
8866 throw new Error('TextTrack kind is required but was not provided');
8867 }
8868 return createTrackHelper(this, kind, label, language);
8869 }
8870
8871 /**
8872 * Create an emulated TextTrack for use by addRemoteTextTrack
8873 *
8874 * This is intended to be overridden by classes that inherit from
8875 * Tech in order to create native or custom TextTracks.
8876 *
8877 * @param {Object} options
8878 * The object should contain the options to initialize the TextTrack with.
8879 *
8880 * @param {string} [options.kind]
8881 * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata).
8882 *
8883 * @param {string} [options.label].
8884 * Label to identify the text track
8885 *
8886 * @param {string} [options.language]
8887 * Two letter language abbreviation.
8888 *
8889 * @return {HTMLTrackElement}
8890 * The track element that gets created.
8891 */
8892 createRemoteTextTrack(options) {
8893 const track = merge(options, {
8894 tech: this
8895 });
8896 return new REMOTE.remoteTextEl.TrackClass(track);
8897 }
8898
8899 /**
8900 * Creates a remote text track object and returns an html track element.
8901 *
8902 * > Note: This can be an emulated {@link HTMLTrackElement} or a native one.
8903 *
8904 * @param {Object} options
8905 * See {@link Tech#createRemoteTextTrack} for more detailed properties.
8906 *
8907 * @param {boolean} [manualCleanup=false]
8908 * - When false: the TextTrack will be automatically removed from the video
8909 * element whenever the source changes
8910 * - When True: The TextTrack will have to be cleaned up manually
8911 *
8912 * @return {HTMLTrackElement}
8913 * An Html Track Element.
8914 *
8915 */
8916 addRemoteTextTrack(options = {}, manualCleanup) {
8917 const htmlTrackElement = this.createRemoteTextTrack(options);
8918 if (typeof manualCleanup !== 'boolean') {
8919 manualCleanup = false;
8920 }
8921
8922 // store HTMLTrackElement and TextTrack to remote list
8923 this.remoteTextTrackEls().addTrackElement_(htmlTrackElement);
8924 this.remoteTextTracks().addTrack(htmlTrackElement.track);
8925 if (manualCleanup === false) {
8926 // create the TextTrackList if it doesn't exist
8927 this.ready(() => this.autoRemoteTextTracks_.addTrack(htmlTrackElement.track));
8928 }
8929 return htmlTrackElement;
8930 }
8931
8932 /**
8933 * Remove a remote text track from the remote `TextTrackList`.
8934 *
8935 * @param {TextTrack} track
8936 * `TextTrack` to remove from the `TextTrackList`
8937 */
8938 removeRemoteTextTrack(track) {
8939 const trackElement = this.remoteTextTrackEls().getTrackElementByTrack_(track);
8940
8941 // remove HTMLTrackElement and TextTrack from remote list
8942 this.remoteTextTrackEls().removeTrackElement_(trackElement);
8943 this.remoteTextTracks().removeTrack(track);
8944 this.autoRemoteTextTracks_.removeTrack(track);
8945 }
8946
8947 /**
8948 * Gets available media playback quality metrics as specified by the W3C's Media
8949 * Playback Quality API.
8950 *
8951 * @see [Spec]{@link https://wicg.github.io/media-playback-quality}
8952 *
8953 * @return {Object}
8954 * An object with supported media playback quality metrics
8955 *
8956 * @abstract
8957 */
8958 getVideoPlaybackQuality() {
8959 return {};
8960 }
8961
8962 /**
8963 * Attempt to create a floating video window always on top of other windows
8964 * so that users may continue consuming media while they interact with other
8965 * content sites, or applications on their device.
8966 *
8967 * @see [Spec]{@link https://wicg.github.io/picture-in-picture}
8968 *
8969 * @return {Promise|undefined}
8970 * A promise with a Picture-in-Picture window if the browser supports
8971 * Promises (or one was passed in as an option). It returns undefined
8972 * otherwise.
8973 *
8974 * @abstract
8975 */
8976 requestPictureInPicture() {
8977 return Promise.reject();
8978 }
8979
8980 /**
8981 * A method to check for the value of the 'disablePictureInPicture' <video> property.
8982 * Defaults to true, as it should be considered disabled if the tech does not support pip
8983 *
8984 * @abstract
8985 */
8986 disablePictureInPicture() {
8987 return true;
8988 }
8989
8990 /**
8991 * A method to set or unset the 'disablePictureInPicture' <video> property.
8992 *
8993 * @abstract
8994 */
8995 setDisablePictureInPicture() {}
8996
8997 /**
8998 * A fallback implementation of requestVideoFrameCallback using requestAnimationFrame
8999 *
9000 * @param {function} cb
9001 * @return {number} request id
9002 */
9003 requestVideoFrameCallback(cb) {
9004 const id = newGUID();
9005 if (!this.isReady_ || this.paused()) {
9006 this.queuedHanders_.add(id);
9007 this.one('playing', () => {
9008 if (this.queuedHanders_.has(id)) {
9009 this.queuedHanders_.delete(id);
9010 cb();
9011 }
9012 });
9013 } else {
9014 this.requestNamedAnimationFrame(id, cb);
9015 }
9016 return id;
9017 }
9018
9019 /**
9020 * A fallback implementation of cancelVideoFrameCallback
9021 *
9022 * @param {number} id id of callback to be cancelled
9023 */
9024 cancelVideoFrameCallback(id) {
9025 if (this.queuedHanders_.has(id)) {
9026 this.queuedHanders_.delete(id);
9027 } else {
9028 this.cancelNamedAnimationFrame(id);
9029 }
9030 }
9031
9032 /**
9033 * A method to set a poster from a `Tech`.
9034 *
9035 * @abstract
9036 */
9037 setPoster() {}
9038
9039 /**
9040 * A method to check for the presence of the 'playsinline' <video> attribute.
9041 *
9042 * @abstract
9043 */
9044 playsinline() {}
9045
9046 /**
9047 * A method to set or unset the 'playsinline' <video> attribute.
9048 *
9049 * @abstract
9050 */
9051 setPlaysinline() {}
9052
9053 /**
9054 * Attempt to force override of native audio tracks.
9055 *
9056 * @param {boolean} override - If set to true native audio will be overridden,
9057 * otherwise native audio will potentially be used.
9058 *
9059 * @abstract
9060 */
9061 overrideNativeAudioTracks(override) {}
9062
9063 /**
9064 * Attempt to force override of native video tracks.
9065 *
9066 * @param {boolean} override - If set to true native video will be overridden,
9067 * otherwise native video will potentially be used.
9068 *
9069 * @abstract
9070 */
9071 overrideNativeVideoTracks(override) {}
9072
9073 /**
9074 * Check if the tech can support the given mime-type.
9075 *
9076 * The base tech does not support any type, but source handlers might
9077 * overwrite this.
9078 *
9079 * @param {string} _type
9080 * The mimetype to check for support
9081 *
9082 * @return {string}
9083 * 'probably', 'maybe', or empty string
9084 *
9085 * @see [Spec]{@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/canPlayType}
9086 *
9087 * @abstract
9088 */
9089 canPlayType(_type) {
9090 return '';
9091 }
9092
9093 /**
9094 * Check if the type is supported by this tech.
9095 *
9096 * The base tech does not support any type, but source handlers might
9097 * overwrite this.
9098 *
9099 * @param {string} _type
9100 * The media type to check
9101 * @return {string} Returns the native video element's response
9102 */
9103 static canPlayType(_type) {
9104 return '';
9105 }
9106
9107 /**
9108 * Check if the tech can support the given source
9109 *
9110 * @param {Object} srcObj
9111 * The source object
9112 * @param {Object} options
9113 * The options passed to the tech
9114 * @return {string} 'probably', 'maybe', or '' (empty string)
9115 */
9116 static canPlaySource(srcObj, options) {
9117 return Tech.canPlayType(srcObj.type);
9118 }
9119
9120 /*
9121 * Return whether the argument is a Tech or not.
9122 * Can be passed either a Class like `Html5` or a instance like `player.tech_`
9123 *
9124 * @param {Object} component
9125 * The item to check
9126 *
9127 * @return {boolean}
9128 * Whether it is a tech or not
9129 * - True if it is a tech
9130 * - False if it is not
9131 */
9132 static isTech(component) {
9133 return component.prototype instanceof Tech || component instanceof Tech || component === Tech;
9134 }
9135
9136 /**
9137 * Registers a `Tech` into a shared list for videojs.
9138 *
9139 * @param {string} name
9140 * Name of the `Tech` to register.
9141 *
9142 * @param {Object} tech
9143 * The `Tech` class to register.
9144 */
9145 static registerTech(name, tech) {
9146 if (!Tech.techs_) {
9147 Tech.techs_ = {};
9148 }
9149 if (!Tech.isTech(tech)) {
9150 throw new Error(`Tech ${name} must be a Tech`);
9151 }
9152 if (!Tech.canPlayType) {
9153 throw new Error('Techs must have a static canPlayType method on them');
9154 }
9155 if (!Tech.canPlaySource) {
9156 throw new Error('Techs must have a static canPlaySource method on them');
9157 }
9158 name = toTitleCase(name);
9159 Tech.techs_[name] = tech;
9160 Tech.techs_[toLowerCase(name)] = tech;
9161 if (name !== 'Tech') {
9162 // camel case the techName for use in techOrder
9163 Tech.defaultTechOrder_.push(name);
9164 }
9165 return tech;
9166 }
9167
9168 /**
9169 * Get a `Tech` from the shared list by name.
9170 *
9171 * @param {string} name
9172 * `camelCase` or `TitleCase` name of the Tech to get
9173 *
9174 * @return {Tech|undefined}
9175 * The `Tech` or undefined if there was no tech with the name requested.
9176 */
9177 static getTech(name) {
9178 if (!name) {
9179 return;
9180 }
9181 if (Tech.techs_ && Tech.techs_[name]) {
9182 return Tech.techs_[name];
9183 }
9184 name = toTitleCase(name);
9185 if (window__default["default"] && window__default["default"].videojs && window__default["default"].videojs[name]) {
9186 log.warn(`The ${name} tech was added to the videojs object when it should be registered using videojs.registerTech(name, tech)`);
9187 return window__default["default"].videojs[name];
9188 }
9189 }
9190}
9191
9192/**
9193 * Get the {@link VideoTrackList}
9194 *
9195 * @returns {VideoTrackList}
9196 * @method Tech.prototype.videoTracks
9197 */
9198
9199/**
9200 * Get the {@link AudioTrackList}
9201 *
9202 * @returns {AudioTrackList}
9203 * @method Tech.prototype.audioTracks
9204 */
9205
9206/**
9207 * Get the {@link TextTrackList}
9208 *
9209 * @returns {TextTrackList}
9210 * @method Tech.prototype.textTracks
9211 */
9212
9213/**
9214 * Get the remote element {@link TextTrackList}
9215 *
9216 * @returns {TextTrackList}
9217 * @method Tech.prototype.remoteTextTracks
9218 */
9219
9220/**
9221 * Get the remote element {@link HtmlTrackElementList}
9222 *
9223 * @returns {HtmlTrackElementList}
9224 * @method Tech.prototype.remoteTextTrackEls
9225 */
9226
9227ALL.names.forEach(function (name) {
9228 const props = ALL[name];
9229 Tech.prototype[props.getterName] = function () {
9230 this[props.privateName] = this[props.privateName] || new props.ListClass();
9231 return this[props.privateName];
9232 };
9233});
9234
9235/**
9236 * List of associated text tracks
9237 *
9238 * @type {TextTrackList}
9239 * @private
9240 * @property Tech#textTracks_
9241 */
9242
9243/**
9244 * List of associated audio tracks.
9245 *
9246 * @type {AudioTrackList}
9247 * @private
9248 * @property Tech#audioTracks_
9249 */
9250
9251/**
9252 * List of associated video tracks.
9253 *
9254 * @type {VideoTrackList}
9255 * @private
9256 * @property Tech#videoTracks_
9257 */
9258
9259/**
9260 * Boolean indicating whether the `Tech` supports volume control.
9261 *
9262 * @type {boolean}
9263 * @default
9264 */
9265Tech.prototype.featuresVolumeControl = true;
9266
9267/**
9268 * Boolean indicating whether the `Tech` supports muting volume.
9269 *
9270 * @type {boolean}
9271 * @default
9272 */
9273Tech.prototype.featuresMuteControl = true;
9274
9275/**
9276 * Boolean indicating whether the `Tech` supports fullscreen resize control.
9277 * Resizing plugins using request fullscreen reloads the plugin
9278 *
9279 * @type {boolean}
9280 * @default
9281 */
9282Tech.prototype.featuresFullscreenResize = false;
9283
9284/**
9285 * Boolean indicating whether the `Tech` supports changing the speed at which the video
9286 * plays. Examples:
9287 * - Set player to play 2x (twice) as fast
9288 * - Set player to play 0.5x (half) as fast
9289 *
9290 * @type {boolean}
9291 * @default
9292 */
9293Tech.prototype.featuresPlaybackRate = false;
9294
9295/**
9296 * Boolean indicating whether the `Tech` supports the `progress` event.
9297 * This will be used to determine if {@link Tech#manualProgressOn} should be called.
9298 *
9299 * @type {boolean}
9300 * @default
9301 */
9302Tech.prototype.featuresProgressEvents = false;
9303
9304/**
9305 * Boolean indicating whether the `Tech` supports the `sourceset` event.
9306 *
9307 * A tech should set this to `true` and then use {@link Tech#triggerSourceset}
9308 * to trigger a {@link Tech#event:sourceset} at the earliest time after getting
9309 * a new source.
9310 *
9311 * @type {boolean}
9312 * @default
9313 */
9314Tech.prototype.featuresSourceset = false;
9315
9316/**
9317 * Boolean indicating whether the `Tech` supports the `timeupdate` event.
9318 * This will be used to determine if {@link Tech#manualTimeUpdates} should be called.
9319 *
9320 * @type {boolean}
9321 * @default
9322 */
9323Tech.prototype.featuresTimeupdateEvents = false;
9324
9325/**
9326 * Boolean indicating whether the `Tech` supports the native `TextTrack`s.
9327 * This will help us integrate with native `TextTrack`s if the browser supports them.
9328 *
9329 * @type {boolean}
9330 * @default
9331 */
9332Tech.prototype.featuresNativeTextTracks = false;
9333
9334/**
9335 * Boolean indicating whether the `Tech` supports `requestVideoFrameCallback`.
9336 *
9337 * @type {boolean}
9338 * @default
9339 */
9340Tech.prototype.featuresVideoFrameCallback = false;
9341
9342/**
9343 * A functional mixin for techs that want to use the Source Handler pattern.
9344 * Source handlers are scripts for handling specific formats.
9345 * The source handler pattern is used for adaptive formats (HLS, DASH) that
9346 * manually load video data and feed it into a Source Buffer (Media Source Extensions)
9347 * Example: `Tech.withSourceHandlers.call(MyTech);`
9348 *
9349 * @param {Tech} _Tech
9350 * The tech to add source handler functions to.
9351 *
9352 * @mixes Tech~SourceHandlerAdditions
9353 */
9354Tech.withSourceHandlers = function (_Tech) {
9355 /**
9356 * Register a source handler
9357 *
9358 * @param {Function} handler
9359 * The source handler class
9360 *
9361 * @param {number} [index]
9362 * Register it at the following index
9363 */
9364 _Tech.registerSourceHandler = function (handler, index) {
9365 let handlers = _Tech.sourceHandlers;
9366 if (!handlers) {
9367 handlers = _Tech.sourceHandlers = [];
9368 }
9369 if (index === undefined) {
9370 // add to the end of the list
9371 index = handlers.length;
9372 }
9373 handlers.splice(index, 0, handler);
9374 };
9375
9376 /**
9377 * Check if the tech can support the given type. Also checks the
9378 * Techs sourceHandlers.
9379 *
9380 * @param {string} type
9381 * The mimetype to check.
9382 *
9383 * @return {string}
9384 * 'probably', 'maybe', or '' (empty string)
9385 */
9386 _Tech.canPlayType = function (type) {
9387 const handlers = _Tech.sourceHandlers || [];
9388 let can;
9389 for (let i = 0; i < handlers.length; i++) {
9390 can = handlers[i].canPlayType(type);
9391 if (can) {
9392 return can;
9393 }
9394 }
9395 return '';
9396 };
9397
9398 /**
9399 * Returns the first source handler that supports the source.
9400 *
9401 * TODO: Answer question: should 'probably' be prioritized over 'maybe'
9402 *
9403 * @param {SourceObject} source
9404 * The source object
9405 *
9406 * @param {Object} options
9407 * The options passed to the tech
9408 *
9409 * @return {SourceHandler|null}
9410 * The first source handler that supports the source or null if
9411 * no SourceHandler supports the source
9412 */
9413 _Tech.selectSourceHandler = function (source, options) {
9414 const handlers = _Tech.sourceHandlers || [];
9415 let can;
9416 for (let i = 0; i < handlers.length; i++) {
9417 can = handlers[i].canHandleSource(source, options);
9418 if (can) {
9419 return handlers[i];
9420 }
9421 }
9422 return null;
9423 };
9424
9425 /**
9426 * Check if the tech can support the given source.
9427 *
9428 * @param {SourceObject} srcObj
9429 * The source object
9430 *
9431 * @param {Object} options
9432 * The options passed to the tech
9433 *
9434 * @return {string}
9435 * 'probably', 'maybe', or '' (empty string)
9436 */
9437 _Tech.canPlaySource = function (srcObj, options) {
9438 const sh = _Tech.selectSourceHandler(srcObj, options);
9439 if (sh) {
9440 return sh.canHandleSource(srcObj, options);
9441 }
9442 return '';
9443 };
9444
9445 /**
9446 * When using a source handler, prefer its implementation of
9447 * any function normally provided by the tech.
9448 */
9449 const deferrable = ['seekable', 'seeking', 'duration'];
9450
9451 /**
9452 * A wrapper around {@link Tech#seekable} that will call a `SourceHandler`s seekable
9453 * function if it exists, with a fallback to the Techs seekable function.
9454 *
9455 * @method _Tech.seekable
9456 */
9457
9458 /**
9459 * A wrapper around {@link Tech#duration} that will call a `SourceHandler`s duration
9460 * function if it exists, otherwise it will fallback to the techs duration function.
9461 *
9462 * @method _Tech.duration
9463 */
9464
9465 deferrable.forEach(function (fnName) {
9466 const originalFn = this[fnName];
9467 if (typeof originalFn !== 'function') {
9468 return;
9469 }
9470 this[fnName] = function () {
9471 if (this.sourceHandler_ && this.sourceHandler_[fnName]) {
9472 return this.sourceHandler_[fnName].apply(this.sourceHandler_, arguments);
9473 }
9474 return originalFn.apply(this, arguments);
9475 };
9476 }, _Tech.prototype);
9477
9478 /**
9479 * Create a function for setting the source using a source object
9480 * and source handlers.
9481 * Should never be called unless a source handler was found.
9482 *
9483 * @param {SourceObject} source
9484 * A source object with src and type keys
9485 */
9486 _Tech.prototype.setSource = function (source) {
9487 let sh = _Tech.selectSourceHandler(source, this.options_);
9488 if (!sh) {
9489 // Fall back to a native source handler when unsupported sources are
9490 // deliberately set
9491 if (_Tech.nativeSourceHandler) {
9492 sh = _Tech.nativeSourceHandler;
9493 } else {
9494 log.error('No source handler found for the current source.');
9495 }
9496 }
9497
9498 // Dispose any existing source handler
9499 this.disposeSourceHandler();
9500 this.off('dispose', this.disposeSourceHandler_);
9501 if (sh !== _Tech.nativeSourceHandler) {
9502 this.currentSource_ = source;
9503 }
9504 this.sourceHandler_ = sh.handleSource(source, this, this.options_);
9505 this.one('dispose', this.disposeSourceHandler_);
9506 };
9507
9508 /**
9509 * Clean up any existing SourceHandlers and listeners when the Tech is disposed.
9510 *
9511 * @listens Tech#dispose
9512 */
9513 _Tech.prototype.disposeSourceHandler = function () {
9514 // if we have a source and get another one
9515 // then we are loading something new
9516 // than clear all of our current tracks
9517 if (this.currentSource_) {
9518 this.clearTracks(['audio', 'video']);
9519 this.currentSource_ = null;
9520 }
9521
9522 // always clean up auto-text tracks
9523 this.cleanupAutoTextTracks();
9524 if (this.sourceHandler_) {
9525 if (this.sourceHandler_.dispose) {
9526 this.sourceHandler_.dispose();
9527 }
9528 this.sourceHandler_ = null;
9529 }
9530 };
9531};
9532
9533// The base Tech class needs to be registered as a Component. It is the only
9534// Tech that can be registered as a Component.
9535Component.registerComponent('Tech', Tech);
9536Tech.registerTech('Tech', Tech);
9537
9538/**
9539 * A list of techs that should be added to techOrder on Players
9540 *
9541 * @private
9542 */
9543Tech.defaultTechOrder_ = [];
9544
9545/**
9546 * @file middleware.js
9547 * @module middleware
9548 */
9549
9550/** @import Player from '../player' */
9551/** @import Tech from '../tech/tech' */
9552
9553const middlewares = {};
9554const middlewareInstances = {};
9555const TERMINATOR = {};
9556
9557/**
9558 * A middleware object is a plain JavaScript object that has methods that
9559 * match the {@link Tech} methods found in the lists of allowed
9560 * {@link module:middleware.allowedGetters|getters},
9561 * {@link module:middleware.allowedSetters|setters}, and
9562 * {@link module:middleware.allowedMediators|mediators}.
9563 *
9564 * @typedef {Object} MiddlewareObject
9565 */
9566
9567/**
9568 * A middleware factory function that should return a
9569 * {@link module:middleware~MiddlewareObject|MiddlewareObject}.
9570 *
9571 * This factory will be called for each player when needed, with the player
9572 * passed in as an argument.
9573 *
9574 * @callback MiddlewareFactory
9575 * @param {Player} player
9576 * A Video.js player.
9577 */
9578
9579/**
9580 * Define a middleware that the player should use by way of a factory function
9581 * that returns a middleware object.
9582 *
9583 * @param {string} type
9584 * The MIME type to match or `"*"` for all MIME types.
9585 *
9586 * @param {MiddlewareFactory} middleware
9587 * A middleware factory function that will be executed for
9588 * matching types.
9589 */
9590function use(type, middleware) {
9591 middlewares[type] = middlewares[type] || [];
9592 middlewares[type].push(middleware);
9593}
9594
9595/**
9596 * Asynchronously sets a source using middleware by recursing through any
9597 * matching middlewares and calling `setSource` on each, passing along the
9598 * previous returned value each time.
9599 *
9600 * @param {Player} player
9601 * A {@link Player} instance.
9602 *
9603 * @param {Tech~SourceObject} src
9604 * A source object.
9605 *
9606 * @param {Function}
9607 * The next middleware to run.
9608 */
9609function setSource(player, src, next) {
9610 player.setTimeout(() => setSourceHelper(src, middlewares[src.type], next, player), 1);
9611}
9612
9613/**
9614 * When the tech is set, passes the tech to each middleware's `setTech` method.
9615 *
9616 * @param {Object[]} middleware
9617 * An array of middleware instances.
9618 *
9619 * @param {Tech} tech
9620 * A Video.js tech.
9621 */
9622function setTech(middleware, tech) {
9623 middleware.forEach(mw => mw.setTech && mw.setTech(tech));
9624}
9625
9626/**
9627 * Calls a getter on the tech first, through each middleware
9628 * from right to left to the player.
9629 *
9630 * @param {Object[]} middleware
9631 * An array of middleware instances.
9632 *
9633 * @param {Tech} tech
9634 * The current tech.
9635 *
9636 * @param {string} method
9637 * A method name.
9638 *
9639 * @return {*}
9640 * The final value from the tech after middleware has intercepted it.
9641 */
9642function get(middleware, tech, method) {
9643 return middleware.reduceRight(middlewareIterator(method), tech[method]());
9644}
9645
9646/**
9647 * Takes the argument given to the player and calls the setter method on each
9648 * middleware from left to right to the tech.
9649 *
9650 * @param {Object[]} middleware
9651 * An array of middleware instances.
9652 *
9653 * @param {Tech} tech
9654 * The current tech.
9655 *
9656 * @param {string} method
9657 * A method name.
9658 *
9659 * @param {*} arg
9660 * The value to set on the tech.
9661 *
9662 * @return {*}
9663 * The return value of the `method` of the `tech`.
9664 */
9665function set(middleware, tech, method, arg) {
9666 return tech[method](middleware.reduce(middlewareIterator(method), arg));
9667}
9668
9669/**
9670 * Takes the argument given to the player and calls the `call` version of the
9671 * method on each middleware from left to right.
9672 *
9673 * Then, call the passed in method on the tech and return the result unchanged
9674 * back to the player, through middleware, this time from right to left.
9675 *
9676 * @param {Object[]} middleware
9677 * An array of middleware instances.
9678 *
9679 * @param {Tech} tech
9680 * The current tech.
9681 *
9682 * @param {string} method
9683 * A method name.
9684 *
9685 * @param {*} arg
9686 * The value to set on the tech.
9687 *
9688 * @return {*}
9689 * The return value of the `method` of the `tech`, regardless of the
9690 * return values of middlewares.
9691 */
9692function mediate(middleware, tech, method, arg = null) {
9693 const callMethod = 'call' + toTitleCase(method);
9694 const middlewareValue = middleware.reduce(middlewareIterator(callMethod), arg);
9695 const terminated = middlewareValue === TERMINATOR;
9696 // deprecated. The `null` return value should instead return TERMINATOR to
9697 // prevent confusion if a techs method actually returns null.
9698 const returnValue = terminated ? null : tech[method](middlewareValue);
9699 executeRight(middleware, method, returnValue, terminated);
9700 return returnValue;
9701}
9702
9703/**
9704 * Enumeration of allowed getters where the keys are method names.
9705 *
9706 * @type {Object}
9707 */
9708const allowedGetters = {
9709 buffered: 1,
9710 currentTime: 1,
9711 duration: 1,
9712 muted: 1,
9713 played: 1,
9714 paused: 1,
9715 seekable: 1,
9716 volume: 1,
9717 ended: 1
9718};
9719
9720/**
9721 * Enumeration of allowed setters where the keys are method names.
9722 *
9723 * @type {Object}
9724 */
9725const allowedSetters = {
9726 setCurrentTime: 1,
9727 setMuted: 1,
9728 setVolume: 1
9729};
9730
9731/**
9732 * Enumeration of allowed mediators where the keys are method names.
9733 *
9734 * @type {Object}
9735 */
9736const allowedMediators = {
9737 play: 1,
9738 pause: 1
9739};
9740function middlewareIterator(method) {
9741 return (value, mw) => {
9742 // if the previous middleware terminated, pass along the termination
9743 if (value === TERMINATOR) {
9744 return TERMINATOR;
9745 }
9746 if (mw[method]) {
9747 return mw[method](value);
9748 }
9749 return value;
9750 };
9751}
9752function executeRight(mws, method, value, terminated) {
9753 for (let i = mws.length - 1; i >= 0; i--) {
9754 const mw = mws[i];
9755 if (mw[method]) {
9756 mw[method](terminated, value);
9757 }
9758 }
9759}
9760
9761/**
9762 * Clear the middleware cache for a player.
9763 *
9764 * @param {Player} player
9765 * A {@link Player} instance.
9766 */
9767function clearCacheForPlayer(player) {
9768 if (middlewareInstances.hasOwnProperty(player.id())) {
9769 delete middlewareInstances[player.id()];
9770 }
9771}
9772
9773/**
9774 * {
9775 * [playerId]: [[mwFactory, mwInstance], ...]
9776 * }
9777 *
9778 * @private
9779 */
9780function getOrCreateFactory(player, mwFactory) {
9781 const mws = middlewareInstances[player.id()];
9782 let mw = null;
9783 if (mws === undefined || mws === null) {
9784 mw = mwFactory(player);
9785 middlewareInstances[player.id()] = [[mwFactory, mw]];
9786 return mw;
9787 }
9788 for (let i = 0; i < mws.length; i++) {
9789 const [mwf, mwi] = mws[i];
9790 if (mwf !== mwFactory) {
9791 continue;
9792 }
9793 mw = mwi;
9794 }
9795 if (mw === null) {
9796 mw = mwFactory(player);
9797 mws.push([mwFactory, mw]);
9798 }
9799 return mw;
9800}
9801function setSourceHelper(src = {}, middleware = [], next, player, acc = [], lastRun = false) {
9802 const [mwFactory, ...mwrest] = middleware;
9803
9804 // if mwFactory is a string, then we're at a fork in the road
9805 if (typeof mwFactory === 'string') {
9806 setSourceHelper(src, middlewares[mwFactory], next, player, acc, lastRun);
9807
9808 // if we have an mwFactory, call it with the player to get the mw,
9809 // then call the mw's setSource method
9810 } else if (mwFactory) {
9811 const mw = getOrCreateFactory(player, mwFactory);
9812
9813 // if setSource isn't present, implicitly select this middleware
9814 if (!mw.setSource) {
9815 acc.push(mw);
9816 return setSourceHelper(src, mwrest, next, player, acc, lastRun);
9817 }
9818 mw.setSource(Object.assign({}, src), function (err, _src) {
9819 // something happened, try the next middleware on the current level
9820 // make sure to use the old src
9821 if (err) {
9822 return setSourceHelper(src, mwrest, next, player, acc, lastRun);
9823 }
9824
9825 // we've succeeded, now we need to go deeper
9826 acc.push(mw);
9827
9828 // if it's the same type, continue down the current chain
9829 // otherwise, we want to go down the new chain
9830 setSourceHelper(_src, src.type === _src.type ? mwrest : middlewares[_src.type], next, player, acc, lastRun);
9831 });
9832 } else if (mwrest.length) {
9833 setSourceHelper(src, mwrest, next, player, acc, lastRun);
9834 } else if (lastRun) {
9835 next(src, acc);
9836 } else {
9837 setSourceHelper(src, middlewares['*'], next, player, acc, true);
9838 }
9839}
9840
9841/** @import Player from '../player' */
9842
9843/**
9844 * Mimetypes
9845 *
9846 * @see https://www.iana.org/assignments/media-types/media-types.xhtml
9847 * @typedef Mimetypes~Kind
9848 * @enum
9849 */
9850const MimetypesKind = {
9851 opus: 'video/ogg',
9852 ogv: 'video/ogg',
9853 mp4: 'video/mp4',
9854 mov: 'video/mp4',
9855 m4v: 'video/mp4',
9856 mkv: 'video/x-matroska',
9857 m4a: 'audio/mp4',
9858 mp3: 'audio/mpeg',
9859 aac: 'audio/aac',
9860 caf: 'audio/x-caf',
9861 flac: 'audio/flac',
9862 oga: 'audio/ogg',
9863 wav: 'audio/wav',
9864 m3u8: 'application/x-mpegURL',
9865 mpd: 'application/dash+xml',
9866 jpg: 'image/jpeg',
9867 jpeg: 'image/jpeg',
9868 gif: 'image/gif',
9869 png: 'image/png',
9870 svg: 'image/svg+xml',
9871 webp: 'image/webp'
9872};
9873
9874/**
9875 * Get the mimetype of a given src url if possible
9876 *
9877 * @param {string} src
9878 * The url to the src
9879 *
9880 * @return {string}
9881 * return the mimetype if it was known or empty string otherwise
9882 */
9883const getMimetype = function (src = '') {
9884 const ext = getFileExtension(src);
9885 const mimetype = MimetypesKind[ext.toLowerCase()];
9886 return mimetype || '';
9887};
9888
9889/**
9890 * Find the mime type of a given source string if possible. Uses the player
9891 * source cache.
9892 *
9893 * @param {Player} player
9894 * The player object
9895 *
9896 * @param {string} src
9897 * The source string
9898 *
9899 * @return {string}
9900 * The type that was found
9901 */
9902const findMimetype = (player, src) => {
9903 if (!src) {
9904 return '';
9905 }
9906
9907 // 1. check for the type in the `source` cache
9908 if (player.cache_.source.src === src && player.cache_.source.type) {
9909 return player.cache_.source.type;
9910 }
9911
9912 // 2. see if we have this source in our `currentSources` cache
9913 const matchingSources = player.cache_.sources.filter(s => s.src === src);
9914 if (matchingSources.length) {
9915 return matchingSources[0].type;
9916 }
9917
9918 // 3. look for the src url in source elements and use the type there
9919 const sources = player.$$('source');
9920 for (let i = 0; i < sources.length; i++) {
9921 const s = sources[i];
9922 if (s.type && s.src && s.src === src) {
9923 return s.type;
9924 }
9925 }
9926
9927 // 4. finally fallback to our list of mime types based on src url extension
9928 return getMimetype(src);
9929};
9930
9931/**
9932 * @module filter-source
9933 */
9934
9935/**
9936 * Filter out single bad source objects or multiple source objects in an
9937 * array. Also flattens nested source object arrays into a 1 dimensional
9938 * array of source objects.
9939 *
9940 * @param {Tech~SourceObject|Tech~SourceObject[]} src
9941 * The src object to filter
9942 *
9943 * @return {Tech~SourceObject[]}
9944 * An array of sourceobjects containing only valid sources
9945 *
9946 * @private
9947 */
9948const filterSource = function (src) {
9949 // traverse array
9950 if (Array.isArray(src)) {
9951 let newsrc = [];
9952 src.forEach(function (srcobj) {
9953 srcobj = filterSource(srcobj);
9954 if (Array.isArray(srcobj)) {
9955 newsrc = newsrc.concat(srcobj);
9956 } else if (isObject(srcobj)) {
9957 newsrc.push(srcobj);
9958 }
9959 });
9960 src = newsrc;
9961 } else if (typeof src === 'string' && src.trim()) {
9962 // convert string into object
9963 src = [fixSource({
9964 src
9965 })];
9966 } else if (isObject(src) && typeof src.src === 'string' && src.src && src.src.trim()) {
9967 // src is already valid
9968 src = [fixSource(src)];
9969 } else {
9970 // invalid source, turn it into an empty array
9971 src = [];
9972 }
9973 return src;
9974};
9975
9976/**
9977 * Checks src mimetype, adding it when possible
9978 *
9979 * @param {Tech~SourceObject} src
9980 * The src object to check
9981 * @return {Tech~SourceObject}
9982 * src Object with known type
9983 */
9984function fixSource(src) {
9985 if (!src.type) {
9986 const mimetype = getMimetype(src.src);
9987 if (mimetype) {
9988 src.type = mimetype;
9989 }
9990 }
9991 return src;
9992}
9993
9994var icons = "<svg xmlns=\"http://www.w3.org/2000/svg\">\n <defs>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-play\">\n <path d=\"M16 10v28l22-14z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-pause\">\n <path d=\"M12 38h8V10h-8v28zm16-28v28h8V10h-8z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-audio\">\n <path d=\"M24 2C14.06 2 6 10.06 6 20v14c0 3.31 2.69 6 6 6h6V24h-8v-4c0-7.73 6.27-14 14-14s14 6.27 14 14v4h-8v16h6c3.31 0 6-2.69 6-6V20c0-9.94-8.06-18-18-18z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-captions\">\n <path d=\"M38 8H10c-2.21 0-4 1.79-4 4v24c0 2.21 1.79 4 4 4h28c2.21 0 4-1.79 4-4V12c0-2.21-1.79-4-4-4zM22 22h-3v-1h-4v6h4v-1h3v2a2 2 0 0 1-2 2h-6a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v2zm14 0h-3v-1h-4v6h4v-1h3v2a2 2 0 0 1-2 2h-6a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v2z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-subtitles\">\n <path d=\"M40 8H8c-2.21 0-4 1.79-4 4v24c0 2.21 1.79 4 4 4h32c2.21 0 4-1.79 4-4V12c0-2.21-1.79-4-4-4zM8 24h8v4H8v-4zm20 12H8v-4h20v4zm12 0h-8v-4h8v4zm0-8H20v-4h20v4z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-fullscreen-enter\">\n <path d=\"M14 28h-4v10h10v-4h-6v-6zm-4-8h4v-6h6v-4H10v10zm24 14h-6v4h10V28h-4v6zm-6-24v4h6v6h4V10H28z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-fullscreen-exit\">\n <path d=\"M10 32h6v6h4V28H10v4zm6-16h-6v4h10V10h-4v6zm12 22h4v-6h6v-4H28v10zm4-22v-6h-4v10h10v-4h-6z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-play-circle\">\n <path d=\"M20 33l12-9-12-9v18zm4-29C12.95 4 4 12.95 4 24s8.95 20 20 20 20-8.95 20-20S35.05 4 24 4zm0 36c-8.82 0-16-7.18-16-16S15.18 8 24 8s16 7.18 16 16-7.18 16-16 16z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-volume-mute\">\n <path d=\"M33 24c0-3.53-2.04-6.58-5-8.05v4.42l4.91 4.91c.06-.42.09-.85.09-1.28zm5 0c0 1.88-.41 3.65-1.08 5.28l3.03 3.03C41.25 29.82 42 27 42 24c0-8.56-5.99-15.72-14-17.54v4.13c5.78 1.72 10 7.07 10 13.41zM8.55 6L6 8.55 15.45 18H6v12h8l10 10V26.55l8.51 8.51c-1.34 1.03-2.85 1.86-4.51 2.36v4.13a17.94 17.94 0 0 0 7.37-3.62L39.45 42 42 39.45l-18-18L8.55 6zM24 8l-4.18 4.18L24 16.36V8z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-volume-low\">\n <path d=\"M14 18v12h8l10 10V8L22 18h-8z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-volume-medium\">\n <path d=\"M37 24c0-3.53-2.04-6.58-5-8.05v16.11c2.96-1.48 5-4.53 5-8.06zm-27-6v12h8l10 10V8L18 18h-8z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-volume-high\">\n <path d=\"M6 18v12h8l10 10V8L14 18H6zm27 6c0-3.53-2.04-6.58-5-8.05v16.11c2.96-1.48 5-4.53 5-8.06zM28 6.46v4.13c5.78 1.72 10 7.07 10 13.41s-4.22 11.69-10 13.41v4.13c8.01-1.82 14-8.97 14-17.54S36.01 8.28 28 6.46z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-spinner\">\n <path d=\"M18.8 21l9.53-16.51C26.94 4.18 25.49 4 24 4c-4.8 0-9.19 1.69-12.64 4.51l7.33 12.69.11-.2zm24.28-3c-1.84-5.85-6.3-10.52-11.99-12.68L23.77 18h19.31zm.52 2H28.62l.58 1 9.53 16.5C41.99 33.94 44 29.21 44 24c0-1.37-.14-2.71-.4-4zm-26.53 4l-7.8-13.5C6.01 14.06 4 18.79 4 24c0 1.37.14 2.71.4 4h14.98l-2.31-4zM4.92 30c1.84 5.85 6.3 10.52 11.99 12.68L24.23 30H4.92zm22.54 0l-7.8 13.51c1.4.31 2.85.49 4.34.49 4.8 0 9.19-1.69 12.64-4.51L29.31 26.8 27.46 30z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 24 24\" id=\"vjs-icon-hd\">\n <path d=\"M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-8 12H9.5v-2h-2v2H6V9h1.5v2.5h2V9H11v6zm2-6h4c.55 0 1 .45 1 1v4c0 .55-.45 1-1 1h-4V9zm1.5 4.5h2v-3h-2v3z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-chapters\">\n <path d=\"M6 26h4v-4H6v4zm0 8h4v-4H6v4zm0-16h4v-4H6v4zm8 8h28v-4H14v4zm0 8h28v-4H14v4zm0-20v4h28v-4H14z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 40 40\" id=\"vjs-icon-downloading\">\n <path d=\"M18.208 36.875q-3.208-.292-5.979-1.729-2.771-1.438-4.812-3.729-2.042-2.292-3.188-5.229-1.146-2.938-1.146-6.23 0-6.583 4.334-11.416 4.333-4.834 10.833-5.5v3.166q-5.167.75-8.583 4.646Q6.25 14.75 6.25 19.958q0 5.209 3.396 9.104 3.396 3.896 8.562 4.646zM20 28.417L11.542 20l2.083-2.083 4.917 4.916v-11.25h2.916v11.25l4.875-4.916L28.417 20zm1.792 8.458v-3.167q1.833-.25 3.541-.958 1.709-.708 3.167-1.875l2.333 2.292q-1.958 1.583-4.25 2.541-2.291.959-4.791 1.167zm6.791-27.792q-1.541-1.125-3.25-1.854-1.708-.729-3.541-1.021V3.042q2.5.25 4.77 1.208 2.271.958 4.271 2.5zm4.584 21.584l-2.25-2.25q1.166-1.5 1.854-3.209.687-1.708.937-3.541h3.209q-.292 2.5-1.229 4.791-.938 2.292-2.521 4.209zm.541-12.417q-.291-1.833-.958-3.562-.667-1.73-1.833-3.188l2.375-2.208q1.541 1.916 2.458 4.208.917 2.292 1.167 4.75z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-file-download\">\n <path d=\"M10.8 40.55q-1.35 0-2.375-1T7.4 37.15v-7.7h3.4v7.7h26.35v-7.7h3.4v7.7q0 1.4-1 2.4t-2.4 1zM24 32.1L13.9 22.05l2.45-2.45 5.95 5.95V7.15h3.4v18.4l5.95-5.95 2.45 2.45z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-file-download-done\">\n <path d=\"M9.8 40.5v-3.45h28.4v3.45zm9.2-9.05L7.4 19.85l2.45-2.35L19 26.65l19.2-19.2 2.4 2.4z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-file-download-off\">\n <path d=\"M4.9 4.75L43.25 43.1 41 45.3l-4.75-4.75q-.05.05-.075.025-.025-.025-.075-.025H10.8q-1.35 0-2.375-1T7.4 37.15v-7.7h3.4v7.7h22.05l-7-7-1.85 1.8L13.9 21.9l1.85-1.85L2.7 7zm26.75 14.7l2.45 2.45-3.75 3.8-2.45-2.5zM25.7 7.15V21.1l-3.4-3.45V7.15z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-share\">\n <path d=\"M36 32.17c-1.52 0-2.89.59-3.93 1.54L17.82 25.4c.11-.45.18-.92.18-1.4s-.07-.95-.18-1.4l14.1-8.23c1.07 1 2.5 1.62 4.08 1.62 3.31 0 6-2.69 6-6s-2.69-6-6-6-6 2.69-6 6c0 .48.07.95.18 1.4l-14.1 8.23c-1.07-1-2.5-1.62-4.08-1.62-3.31 0-6 2.69-6 6s2.69 6 6 6c1.58 0 3.01-.62 4.08-1.62l14.25 8.31c-.1.42-.16.86-.16 1.31A5.83 5.83 0 1 0 36 32.17z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-cog\">\n <path d=\"M38.86 25.95c.08-.64.14-1.29.14-1.95s-.06-1.31-.14-1.95l4.23-3.31c.38-.3.49-.84.24-1.28l-4-6.93c-.25-.43-.77-.61-1.22-.43l-4.98 2.01c-1.03-.79-2.16-1.46-3.38-1.97L29 4.84c-.09-.47-.5-.84-1-.84h-8c-.5 0-.91.37-.99.84l-.75 5.3a14.8 14.8 0 0 0-3.38 1.97L9.9 10.1a1 1 0 0 0-1.22.43l-4 6.93c-.25.43-.14.97.24 1.28l4.22 3.31C9.06 22.69 9 23.34 9 24s.06 1.31.14 1.95l-4.22 3.31c-.38.3-.49.84-.24 1.28l4 6.93c.25.43.77.61 1.22.43l4.98-2.01c1.03.79 2.16 1.46 3.38 1.97l.75 5.3c.08.47.49.84.99.84h8c.5 0 .91-.37.99-.84l.75-5.3a14.8 14.8 0 0 0 3.38-1.97l4.98 2.01a1 1 0 0 0 1.22-.43l4-6.93c.25-.43.14-.97-.24-1.28l-4.22-3.31zM24 31c-3.87 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-square\">\n <path d=\"M36 8H12c-2.21 0-4 1.79-4 4v24c0 2.21 1.79 4 4 4h24c2.21 0 4-1.79 4-4V12c0-2.21-1.79-4-4-4zm0 28H12V12h24v24z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-circle\">\n <circle cx=\"24\" cy=\"24\" r=\"20\"></circle>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-circle-outline\">\n <path d=\"M24 4C12.95 4 4 12.95 4 24s8.95 20 20 20 20-8.95 20-20S35.05 4 24 4zm0 36c-8.82 0-16-7.18-16-16S15.18 8 24 8s16 7.18 16 16-7.18 16-16 16z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-circle-inner-circle\">\n <path d=\"M24 4C12.97 4 4 12.97 4 24s8.97 20 20 20 20-8.97 20-20S35.03 4 24 4zm0 36c-8.82 0-16-7.18-16-16S15.18 8 24 8s16 7.18 16 16-7.18 16-16 16zm6-16c0 3.31-2.69 6-6 6s-6-2.69-6-6 2.69-6 6-6 6 2.69 6 6z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-cancel\">\n <path d=\"M24 4C12.95 4 4 12.95 4 24s8.95 20 20 20 20-8.95 20-20S35.05 4 24 4zm10 27.17L31.17 34 24 26.83 16.83 34 14 31.17 21.17 24 14 16.83 16.83 14 24 21.17 31.17 14 34 16.83 26.83 24 34 31.17z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-replay\">\n <path d=\"M24 10V2L14 12l10 10v-8c6.63 0 12 5.37 12 12s-5.37 12-12 12-12-5.37-12-12H8c0 8.84 7.16 16 16 16s16-7.16 16-16-7.16-16-16-16z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-repeat\">\n <path d=\"M14 14h20v6l8-8-8-8v6H10v12h4v-8zm20 20H14v-6l-8 8 8 8v-6h24V26h-4v8z\"></path>\n </symbol>\n <symbol viewBox=\"0 96 48 48\" id=\"vjs-icon-replay-5\">\n <path d=\"M17.689 98l-8.697 8.696 8.697 8.697 2.486-2.485-4.32-4.319h1.302c4.93 0 9.071 1.722 12.424 5.165 3.352 3.443 5.029 7.638 5.029 12.584h3.55c0-2.958-.553-5.73-1.658-8.313-1.104-2.583-2.622-4.841-4.555-6.774-1.932-1.932-4.19-3.45-6.773-4.555-2.584-1.104-5.355-1.657-8.313-1.657H15.5l4.615-4.615zm-8.08 21.659v13.861h11.357v5.008H9.609V143h12.7c.834 0 1.55-.298 2.146-.894.596-.597.895-1.31.895-2.145v-7.781c0-.835-.299-1.55-.895-2.147a2.929 2.929 0 0 0-2.147-.894h-8.227v-5.096H25.35v-4.384z\"></path>\n </symbol>\n <symbol viewBox=\"0 96 48 48\" id=\"vjs-icon-replay-10\">\n <path d=\"M42.315 125.63c0-4.997-1.694-9.235-5.08-12.713-3.388-3.479-7.571-5.218-12.552-5.218h-1.315l4.363 4.363-2.51 2.51-8.787-8.786L25.221 97l2.45 2.45-4.662 4.663h1.375c2.988 0 5.788.557 8.397 1.673 2.61 1.116 4.892 2.65 6.844 4.602 1.953 1.953 3.487 4.234 4.602 6.844 1.116 2.61 1.674 5.41 1.674 8.398zM8.183 142v-19.657H3.176V117.8h9.643V142zm13.63 0c-1.156 0-2.127-.393-2.912-1.178-.778-.778-1.168-1.746-1.168-2.902v-16.04c0-1.156.393-2.127 1.178-2.912.779-.779 1.746-1.168 2.902-1.168h7.696c1.156 0 2.126.392 2.911 1.177.779.78 1.168 1.747 1.168 2.903v16.04c0 1.156-.392 2.127-1.177 2.912-.779.779-1.746 1.168-2.902 1.168zm.556-4.636h6.583v-15.02H22.37z\"></path>\n </symbol>\n <symbol viewBox=\"0 96 48 48\" id=\"vjs-icon-replay-30\">\n <path d=\"M26.047 97l-8.733 8.732 8.733 8.733 2.496-2.494-4.336-4.338h1.307c4.95 0 9.108 1.73 12.474 5.187 3.367 3.458 5.051 7.668 5.051 12.635h3.565c0-2.97-.556-5.751-1.665-8.346-1.109-2.594-2.633-4.862-4.574-6.802-1.94-1.941-4.208-3.466-6.803-4.575-2.594-1.109-5.375-1.664-8.345-1.664H23.85l4.634-4.634zM2.555 117.531v4.688h10.297v5.25H5.873v4.687h6.979v5.156H2.555V142H13.36c1.061 0 1.95-.395 2.668-1.186.718-.79 1.076-1.772 1.076-2.94v-16.218c0-1.168-.358-2.149-1.076-2.94-.717-.79-1.607-1.185-2.668-1.185zm22.482.14c-1.149 0-2.11.39-2.885 1.165-.78.78-1.172 1.744-1.172 2.893v15.943c0 1.149.388 2.11 1.163 2.885.78.78 1.745 1.172 2.894 1.172h7.649c1.148 0 2.11-.388 2.884-1.163.78-.78 1.17-1.745 1.17-2.894v-15.943c0-1.15-.386-2.111-1.16-2.885-.78-.78-1.746-1.172-2.894-1.172zm.553 4.518h6.545v14.93H25.59z\"></path>\n </symbol>\n <symbol viewBox=\"0 96 48 48\" id=\"vjs-icon-forward-5\">\n <path d=\"M29.508 97l-2.431 2.43 4.625 4.625h-1.364c-2.965 0-5.742.554-8.332 1.66-2.589 1.107-4.851 2.629-6.788 4.566-1.937 1.937-3.458 4.2-4.565 6.788-1.107 2.59-1.66 5.367-1.66 8.331h3.557c0-4.957 1.68-9.16 5.04-12.611 3.36-3.45 7.51-5.177 12.451-5.177h1.304l-4.326 4.33 2.49 2.49 8.715-8.716zm-9.783 21.61v13.89h11.382v5.018H19.725V142h12.727a2.93 2.93 0 0 0 2.15-.896 2.93 2.93 0 0 0 .896-2.15v-7.798c0-.837-.299-1.554-.896-2.152a2.93 2.93 0 0 0-2.15-.896h-8.245V123h11.29v-4.392z\"></path>\n </symbol>\n <symbol viewBox=\"0 96 48 48\" id=\"vjs-icon-forward-10\">\n <path d=\"M23.119 97l-2.386 2.383 4.538 4.538h-1.339c-2.908 0-5.633.543-8.173 1.63-2.54 1.085-4.76 2.577-6.66 4.478-1.9 1.9-3.392 4.12-4.478 6.66-1.085 2.54-1.629 5.264-1.629 8.172h3.49c0-4.863 1.648-8.986 4.944-12.372 3.297-3.385 7.368-5.078 12.216-5.078h1.279l-4.245 4.247 2.443 2.442 8.55-8.55zm-9.52 21.45v4.42h4.871V142h4.513v-23.55zm18.136 0c-1.125 0-2.066.377-2.824 1.135-.764.764-1.148 1.709-1.148 2.834v15.612c0 1.124.38 2.066 1.139 2.824.764.764 1.708 1.145 2.833 1.145h7.489c1.125 0 2.066-.378 2.824-1.136.764-.764 1.145-1.709 1.145-2.833v-15.612c0-1.125-.378-2.067-1.136-2.825-.764-.764-1.708-1.145-2.833-1.145zm.54 4.42h6.408v14.617h-6.407z\"></path>\n </symbol>\n <symbol viewBox=\"0 96 48 48\" id=\"vjs-icon-forward-30\">\n <path d=\"M25.549 97l-2.437 2.434 4.634 4.635H26.38c-2.97 0-5.753.555-8.347 1.664-2.594 1.109-4.861 2.633-6.802 4.574-1.94 1.94-3.465 4.207-4.574 6.802-1.109 2.594-1.664 5.377-1.664 8.347h3.565c0-4.967 1.683-9.178 5.05-12.636 3.366-3.458 7.525-5.187 12.475-5.187h1.307l-4.335 4.338 2.495 2.494 8.732-8.732zm-11.553 20.53v4.689h10.297v5.249h-6.978v4.688h6.978v5.156H13.996V142h10.808c1.06 0 1.948-.395 2.666-1.186.718-.79 1.077-1.771 1.077-2.94v-16.217c0-1.169-.36-2.15-1.077-2.94-.718-.79-1.605-1.186-2.666-1.186zm21.174.168c-1.149 0-2.11.389-2.884 1.163-.78.78-1.172 1.745-1.172 2.894v15.942c0 1.15.388 2.11 1.162 2.885.78.78 1.745 1.17 2.894 1.17h7.649c1.149 0 2.11-.386 2.885-1.16.78-.78 1.17-1.746 1.17-2.895v-15.942c0-1.15-.387-2.11-1.161-2.885-.78-.78-1.745-1.172-2.894-1.172zm.552 4.516h6.542v14.931h-6.542z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 512 512\" id=\"vjs-icon-audio-description\">\n <g fill-rule=\"evenodd\"><path d=\"M227.29 381.351V162.993c50.38-1.017 89.108-3.028 117.631 17.126 27.374 19.342 48.734 56.965 44.89 105.325-4.067 51.155-41.335 94.139-89.776 98.475-24.085 2.155-71.972 0-71.972 0s-.84-1.352-.773-2.568m48.755-54.804c31.43 1.26 53.208-16.633 56.495-45.386 4.403-38.51-21.188-63.552-58.041-60.796v103.612c-.036 1.466.575 2.22 1.546 2.57\"></path><path d=\"M383.78 381.328c13.336 3.71 17.387-11.06 23.215-21.408 12.722-22.571 22.294-51.594 22.445-84.774.221-47.594-18.343-82.517-35.6-106.182h-8.51c-.587 3.874 2.226 7.315 3.865 10.276 13.166 23.762 25.367 56.553 25.54 94.194.2 43.176-14.162 79.278-30.955 107.894\"></path><path d=\"M425.154 381.328c13.336 3.71 17.384-11.061 23.215-21.408 12.721-22.571 22.291-51.594 22.445-84.774.221-47.594-18.343-82.517-35.6-106.182h-8.511c-.586 3.874 2.226 7.315 3.866 10.276 13.166 23.762 25.367 56.553 25.54 94.194.2 43.176-14.162 79.278-30.955 107.894\"></path><path d=\"M466.26 381.328c13.337 3.71 17.385-11.061 23.216-21.408 12.722-22.571 22.292-51.594 22.445-84.774.221-47.594-18.343-82.517-35.6-106.182h-8.51c-.587 3.874 2.225 7.315 3.865 10.276 13.166 23.762 25.367 56.553 25.54 94.194.2 43.176-14.162 79.278-30.955 107.894M4.477 383.005H72.58l18.573-28.484 64.169-.135s.065 19.413.065 28.62h48.756V160.307h-58.816c-5.653 9.537-140.85 222.697-140.85 222.697zm152.667-145.282v71.158l-40.453-.27 40.453-70.888z\"></path></g>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-next-item\">\n <path d=\"M12 36l17-12-17-12v24zm20-24v24h4V12h-4z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-previous-item\">\n <path d=\"M12 12h4v24h-4zm7 12l17 12V12z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-shuffle\">\n <path d=\"M21.17 18.34L10.83 8 8 10.83l10.34 10.34 2.83-2.83zM29 8l4.09 4.09L8 37.17 10.83 40l25.09-25.09L40 19V8H29zm.66 18.83l-2.83 2.83 6.26 6.26L29 40h11V29l-4.09 4.09-6.25-6.26z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-cast\">\n <path d=\"M42 6H6c-2.21 0-4 1.79-4 4v6h4v-6h36v28H28v4h14c2.21 0 4-1.79 4-4V10c0-2.21-1.79-4-4-4zM2 36v6h6c0-3.31-2.69-6-6-6zm0-8v4c5.52 0 10 4.48 10 10h4c0-7.73-6.27-14-14-14zm0-8v4c9.94 0 18 8.06 18 18h4c0-12.15-9.85-22-22-22z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-picture-in-picture-enter\">\n <path d=\"M38 22H22v11.99h16V22zm8 16V9.96C46 7.76 44.2 6 42 6H6C3.8 6 2 7.76 2 9.96V38c0 2.2 1.8 4 4 4h36c2.2 0 4-1.8 4-4zm-4 .04H6V9.94h36v28.1z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 22 18\" id=\"vjs-icon-picture-in-picture-exit\">\n <path d=\"M18 4H4v10h14V4zm4 12V1.98C22 .88 21.1 0 20 0H2C.9 0 0 .88 0 1.98V16c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2zm-2 .02H2V1.97h18v14.05z\"></path>\n <path fill=\"none\" d=\"M-1-3h24v24H-1z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 1792 1792\" id=\"vjs-icon-facebook\">\n <path d=\"M1343 12v264h-157q-86 0-116 36t-30 108v189h293l-39 296h-254v759H734V905H479V609h255V391q0-186 104-288.5T1115 0q147 0 228 12z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 1792 1792\" id=\"vjs-icon-linkedin\">\n <path d=\"M477 625v991H147V625h330zm21-306q1 73-50.5 122T312 490h-2q-82 0-132-49t-50-122q0-74 51.5-122.5T314 148t133 48.5T498 319zm1166 729v568h-329v-530q0-105-40.5-164.5T1168 862q-63 0-105.5 34.5T999 982q-11 30-11 81v553H659q2-399 2-647t-1-296l-1-48h329v144h-2q20-32 41-56t56.5-52 87-43.5T1285 602q171 0 275 113.5t104 332.5z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 1200 1227\" id=\"vjs-icon-twitter\">\n <path d=\"M714.163 519.284L1160.89 0H1055.03L667.137 450.887L357.328 0H0L468.492 681.821L0 1226.37H105.866L515.491 750.218L842.672 1226.37H1200L714.137 519.284H714.163ZM569.165 687.828L521.697 619.934L144.011 79.6944H306.615L611.412 515.685L658.88 583.579L1055.08 1150.3H892.476L569.165 687.854V687.828Z\"/>\n </symbol>\n <symbol viewBox=\"0 0 1792 1792\" id=\"vjs-icon-tumblr\">\n <path d=\"M1328 1329l80 237q-23 35-111 66t-177 32q-104 2-190.5-26T787 1564t-95-106-55.5-120-16.5-118V676H452V461q72-26 129-69.5t91-90 58-102 34-99T779 12q1-5 4.5-8.5T791 0h244v424h333v252h-334v518q0 30 6.5 56t22.5 52.5 49.5 41.5 81.5 14q78-2 134-29z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 1792 1792\" id=\"vjs-icon-pinterest\">\n <path d=\"M1664 896q0 209-103 385.5T1281.5 1561 896 1664q-111 0-218-32 59-93 78-164 9-34 54-211 20 39 73 67.5t114 28.5q121 0 216-68.5t147-188.5 52-270q0-114-59.5-214T1180 449t-255-63q-105 0-196 29t-154.5 77-109 110.5-67 129.5T377 866q0 104 40 183t117 111q30 12 38-20 2-7 8-31t8-30q6-23-11-43-51-61-51-151 0-151 104.5-259.5T904 517q151 0 235.5 82t84.5 213q0 170-68.5 289T980 1220q-61 0-98-43.5T859 1072q8-35 26.5-93.5t30-103T927 800q0-50-27-83t-77-33q-62 0-105 57t-43 142q0 73 25 122l-99 418q-17 70-13 177-206-91-333-281T128 896q0-209 103-385.5T510.5 231 896 128t385.5 103T1561 510.5 1664 896z\"></path>\n </symbol>\n </defs>\n</svg>";
9995
9996// /**
9997
9998// Determine the keycode for the 'back' key based on the platform
9999const backKeyCode = IS_TIZEN ? 10009 : IS_WEBOS ? 461 : 8;
10000const SpatialNavKeyCodes = {
10001 codes: {
10002 play: 415,
10003 pause: 19,
10004 ff: 417,
10005 rw: 412,
10006 back: backKeyCode
10007 },
10008 names: {
10009 415: 'play',
10010 19: 'pause',
10011 417: 'ff',
10012 412: 'rw',
10013 [backKeyCode]: 'back'
10014 },
10015 isEventKey(event, keyName) {
10016 keyName = keyName.toLowerCase();
10017 if (this.names[event.keyCode] && this.names[event.keyCode] === keyName) {
10018 return true;
10019 }
10020 return false;
10021 },
10022 getEventName(event) {
10023 if (this.names[event.keyCode]) {
10024 return this.names[event.keyCode];
10025 } else if (this.codes[event.code]) {
10026 const code = this.codes[event.code];
10027 return this.names[code];
10028 }
10029 return null;
10030 }
10031};
10032
10033/**
10034 * @file spatial-navigation.js
10035 */
10036
10037/** @import Component from './component' */
10038/** @import Player from './player' */
10039
10040// The number of seconds the `step*` functions move the timeline.
10041const STEP_SECONDS$1 = 5;
10042
10043/**
10044 * Spatial Navigation in Video.js enhances user experience and accessibility on smartTV devices,
10045 * enabling seamless navigation through interactive elements within the player using remote control arrow keys.
10046 * This functionality allows users to effortlessly navigate through focusable components.
10047 *
10048 * @extends EventTarget
10049 */
10050class SpatialNavigation extends EventTarget {
10051 /**
10052 * Constructs a SpatialNavigation instance with initial settings.
10053 * Sets up the player instance, and prepares the spatial navigation system.
10054 *
10055 * @class
10056 * @param {Player} player - The Video.js player instance to which the spatial navigation is attached.
10057 */
10058 constructor(player) {
10059 super();
10060 this.player_ = player;
10061 this.focusableComponents = [];
10062 this.isListening_ = false;
10063 this.isPaused_ = false;
10064 this.onKeyDown_ = this.onKeyDown_.bind(this);
10065 this.lastFocusedComponent_ = null;
10066 }
10067
10068 /**
10069 * Starts the spatial navigation by adding a keydown event listener to the video container.
10070 * This method ensures that the event listener is added only once.
10071 */
10072 start() {
10073 // If the listener is already active, exit early.
10074 if (this.isListening_) {
10075 return;
10076 }
10077
10078 // Add the event listener since the listener is not yet active.
10079 this.player_.on('keydown', this.onKeyDown_);
10080 this.player_.on('modalKeydown', this.onKeyDown_);
10081 // Listen for source change events
10082 this.player_.on('loadedmetadata', () => {
10083 this.focus(this.updateFocusableComponents()[0]);
10084 });
10085 this.player_.on('modalclose', () => {
10086 this.refocusComponent();
10087 });
10088 this.player_.on('focusin', this.handlePlayerFocus_.bind(this));
10089 this.player_.on('focusout', this.handlePlayerBlur_.bind(this));
10090 this.isListening_ = true;
10091 if (this.player_.errorDisplay) {
10092 this.player_.errorDisplay.on('aftermodalfill', () => {
10093 this.updateFocusableComponents();
10094 if (this.focusableComponents.length) {
10095 // The modal has focusable components:
10096
10097 if (this.focusableComponents.length > 1) {
10098 // The modal has close button + some additional buttons.
10099 // Focusing first additional button:
10100
10101 this.focusableComponents[1].focus();
10102 } else {
10103 // The modal has only close button,
10104 // Focusing it:
10105
10106 this.focusableComponents[0].focus();
10107 }
10108 }
10109 });
10110 }
10111 }
10112
10113 /**
10114 * Stops the spatial navigation by removing the keydown event listener from the video container.
10115 * Also sets the `isListening_` flag to false.
10116 */
10117 stop() {
10118 this.player_.off('keydown', this.onKeyDown_);
10119 this.isListening_ = false;
10120 }
10121
10122 /**
10123 * Responds to keydown events for spatial navigation and media control.
10124 *
10125 * Determines if spatial navigation or media control is active and handles key inputs accordingly.
10126 *
10127 * @param {KeyboardEvent} event - The keydown event to be handled.
10128 */
10129 onKeyDown_(event) {
10130 // Determine if the event is a custom modalKeydown event
10131 const actualEvent = event.originalEvent ? event.originalEvent : event;
10132 if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(actualEvent.key)) {
10133 // Handle directional navigation
10134 if (this.isPaused_) {
10135 return;
10136 }
10137 actualEvent.preventDefault();
10138
10139 // "ArrowLeft" => "left" etc
10140 const direction = actualEvent.key.substring(5).toLowerCase();
10141 this.move(direction);
10142 } else if (SpatialNavKeyCodes.isEventKey(actualEvent, 'play') || SpatialNavKeyCodes.isEventKey(actualEvent, 'pause') || SpatialNavKeyCodes.isEventKey(actualEvent, 'ff') || SpatialNavKeyCodes.isEventKey(actualEvent, 'rw')) {
10143 // Handle media actions
10144 actualEvent.preventDefault();
10145 const action = SpatialNavKeyCodes.getEventName(actualEvent);
10146 this.performMediaAction_(action);
10147 } else if (SpatialNavKeyCodes.isEventKey(actualEvent, 'Back') && event.target && typeof event.target.closeable === 'function' && event.target.closeable()) {
10148 actualEvent.preventDefault();
10149 event.target.close();
10150 }
10151 }
10152
10153 /**
10154 * Performs media control actions based on the given key input.
10155 *
10156 * Controls the playback and seeking functionalities of the media player.
10157 *
10158 * @param {string} key - The key representing the media action to be performed.
10159 * Accepted keys: 'play', 'pause', 'ff' (fast-forward), 'rw' (rewind).
10160 */
10161 performMediaAction_(key) {
10162 if (this.player_) {
10163 switch (key) {
10164 case 'play':
10165 if (this.player_.paused()) {
10166 this.player_.play();
10167 }
10168 break;
10169 case 'pause':
10170 if (!this.player_.paused()) {
10171 this.player_.pause();
10172 }
10173 break;
10174 case 'ff':
10175 this.userSeek_(this.player_.currentTime() + STEP_SECONDS$1);
10176 break;
10177 case 'rw':
10178 this.userSeek_(this.player_.currentTime() - STEP_SECONDS$1);
10179 break;
10180 }
10181 }
10182 }
10183
10184 /**
10185 * Prevent liveThreshold from causing seeks to seem like they
10186 * are not happening from a user perspective.
10187 *
10188 * @param {number} ct
10189 * current time to seek to
10190 */
10191 userSeek_(ct) {
10192 if (this.player_.liveTracker && this.player_.liveTracker.isLive()) {
10193 this.player_.liveTracker.nextSeekedFromUser();
10194 }
10195 this.player_.currentTime(ct);
10196 }
10197
10198 /**
10199 * Pauses the spatial navigation functionality.
10200 * This method sets a flag that can be used to temporarily disable the navigation logic.
10201 */
10202 pause() {
10203 this.isPaused_ = true;
10204 }
10205
10206 /**
10207 * Resumes the spatial navigation functionality if it has been paused.
10208 * This method resets the pause flag, re-enabling the navigation logic.
10209 */
10210 resume() {
10211 this.isPaused_ = false;
10212 }
10213
10214 /**
10215 * Handles Player Blur.
10216 *
10217 * @param {string|Event|Object} event
10218 * The name of the event, an `Event`, or an object with a key of type set to
10219 * an event name.
10220 *
10221 * Calls for handling of the Player Blur if:
10222 * *The next focused element is not a child of current focused element &
10223 * The next focused element is not a child of the Player.
10224 * *There is no next focused element
10225 */
10226 handlePlayerBlur_(event) {
10227 const nextFocusedElement = event.relatedTarget;
10228 let isChildrenOfPlayer = null;
10229 const currentComponent = this.getCurrentComponent(event.target);
10230 if (nextFocusedElement) {
10231 isChildrenOfPlayer = Boolean(nextFocusedElement.closest('.video-js'));
10232
10233 // If nextFocusedElement is the 'TextTrackSettings' component
10234 if (nextFocusedElement.classList.contains('vjs-text-track-settings') && !this.isPaused_) {
10235 this.searchForTrackSelect_();
10236 }
10237 }
10238 if (!event.currentTarget.contains(event.relatedTarget) && !isChildrenOfPlayer || !nextFocusedElement) {
10239 if (currentComponent && currentComponent.name() === 'CloseButton') {
10240 this.refocusComponent();
10241 } else {
10242 this.pause();
10243 if (currentComponent && currentComponent.el()) {
10244 // Store last focused component
10245 this.lastFocusedComponent_ = currentComponent;
10246 }
10247 }
10248 }
10249 }
10250
10251 /**
10252 * Handles the Player focus event.
10253 *
10254 * Calls for handling of the Player Focus if current element is focusable.
10255 */
10256 handlePlayerFocus_() {
10257 if (this.getCurrentComponent() && this.getCurrentComponent().getIsFocusable()) {
10258 this.resume();
10259 }
10260 }
10261
10262 /**
10263 * Gets a set of focusable components.
10264 *
10265 * @return {Array}
10266 * Returns an array of focusable components.
10267 */
10268 updateFocusableComponents() {
10269 const player = this.player_;
10270 const focusableComponents = [];
10271
10272 /**
10273 * Searches for children candidates.
10274 *
10275 * Pushes Components to array of 'focusableComponents'.
10276 * Calls itself if there is children elements inside iterated component.
10277 *
10278 * @param {Array} componentsArray - The array of components to search for focusable children.
10279 */
10280 function searchForChildrenCandidates(componentsArray) {
10281 for (const i of componentsArray) {
10282 if (i.hasOwnProperty('el_') && i.getIsFocusable() && i.getIsAvailableToBeFocused(i.el())) {
10283 focusableComponents.push(i);
10284 }
10285 if (i.hasOwnProperty('children_') && i.children_.length > 0) {
10286 searchForChildrenCandidates(i.children_);
10287 }
10288 }
10289 }
10290
10291 // Iterate inside all children components of the player.
10292 player.children_.forEach(value => {
10293 if (value.hasOwnProperty('el_')) {
10294 // If component has required functions 'getIsFocusable' & 'getIsAvailableToBeFocused', is focusable & available to be focused.
10295 if (value.getIsFocusable && value.getIsAvailableToBeFocused && value.getIsFocusable() && value.getIsAvailableToBeFocused(value.el())) {
10296 focusableComponents.push(value);
10297 return;
10298 // If component has posible children components as candidates.
10299 } else if (value.hasOwnProperty('children_') && value.children_.length > 0) {
10300 searchForChildrenCandidates(value.children_);
10301 // If component has posible item components as candidates.
10302 } else if (value.hasOwnProperty('items') && value.items.length > 0) {
10303 searchForChildrenCandidates(value.items);
10304 // If there is a suitable child element within the component's DOM element.
10305 } else if (this.findSuitableDOMChild(value)) {
10306 focusableComponents.push(value);
10307 }
10308 }
10309
10310 // TODO - Refactor the following logic after refactor of videojs-errors elements to be components is done.
10311 if (value.name_ === 'ErrorDisplay' && value.opened_) {
10312 const buttonContainer = value.el_.querySelector('.vjs-errors-ok-button-container');
10313 if (buttonContainer) {
10314 const modalButtons = buttonContainer.querySelectorAll('button');
10315 modalButtons.forEach((element, index) => {
10316 // Add elements as objects to be handled by the spatial navigation
10317 focusableComponents.push({
10318 name: () => {
10319 return 'ModalButton' + (index + 1);
10320 },
10321 el: () => element,
10322 getPositions: () => {
10323 const rect = element.getBoundingClientRect();
10324
10325 // Creating objects that mirror DOMRectReadOnly for boundingClientRect and center
10326 const boundingClientRect = {
10327 x: rect.x,
10328 y: rect.y,
10329 width: rect.width,
10330 height: rect.height,
10331 top: rect.top,
10332 right: rect.right,
10333 bottom: rect.bottom,
10334 left: rect.left
10335 };
10336
10337 // Calculating the center position
10338 const center = {
10339 x: rect.left + rect.width / 2,
10340 y: rect.top + rect.height / 2,
10341 width: 0,
10342 height: 0,
10343 top: rect.top + rect.height / 2,
10344 right: rect.left + rect.width / 2,
10345 bottom: rect.top + rect.height / 2,
10346 left: rect.left + rect.width / 2
10347 };
10348 return {
10349 boundingClientRect,
10350 center
10351 };
10352 },
10353 // Asume that the following are always focusable
10354 getIsAvailableToBeFocused: () => true,
10355 getIsFocusable: el => true,
10356 focus: () => element.focus()
10357 });
10358 });
10359 }
10360 }
10361 });
10362 this.focusableComponents = focusableComponents;
10363 return this.focusableComponents;
10364 }
10365
10366 /**
10367 * Finds a suitable child element within the provided component's DOM element.
10368 *
10369 * @param {Object} component - The component containing the DOM element to search within.
10370 * @return {HTMLElement|null} Returns the suitable child element if found, or null if not found.
10371 */
10372 findSuitableDOMChild(component) {
10373 /**
10374 * Recursively searches for a suitable child node that can be focused within a given component.
10375 * It first checks if the provided node itself can be focused according to the component's
10376 * `getIsFocusable` and `getIsAvailableToBeFocused` methods. If not, it recursively searches
10377 * through the node's children to find a suitable child node that meets the focusability criteria.
10378 *
10379 * @param {HTMLElement} node - The DOM node to start the search from.
10380 * @return {HTMLElement|null} The first child node that is focusable and available to be focused,
10381 * or `null` if no suitable child is found.
10382 */
10383 function searchForSuitableChild(node) {
10384 if (component.getIsFocusable(node) && component.getIsAvailableToBeFocused(node)) {
10385 return node;
10386 }
10387 for (let i = 0; i < node.children.length; i++) {
10388 const child = node.children[i];
10389 const suitableChild = searchForSuitableChild(child);
10390 if (suitableChild) {
10391 return suitableChild;
10392 }
10393 }
10394 return null;
10395 }
10396 if (component.el()) {
10397 return searchForSuitableChild(component.el());
10398 }
10399 return null;
10400 }
10401
10402 /**
10403 * Gets the currently focused component from the list of focusable components.
10404 * If a target element is provided, it uses that element to find the corresponding
10405 * component. If no target is provided, it defaults to using the document's currently
10406 * active element.
10407 *
10408 * @param {HTMLElement} [target] - The DOM element to check against the focusable components.
10409 * If not provided, `document.activeElement` is used.
10410 * @return {Component|null} - Returns the focused component if found among the focusable components,
10411 * otherwise returns null if no matching component is found.
10412 */
10413 getCurrentComponent(target) {
10414 this.updateFocusableComponents();
10415 // eslint-disable-next-line
10416 const curComp = target || document.activeElement;
10417 if (this.focusableComponents.length) {
10418 for (const i of this.focusableComponents) {
10419 // If component Node is equal to the current active element.
10420 if (i.el() === curComp) {
10421 return i;
10422 }
10423 }
10424 }
10425 }
10426
10427 /**
10428 * Adds a component to the array of focusable components.
10429 *
10430 * @param {Component} component
10431 * The `Component` to be added.
10432 */
10433 add(component) {
10434 const focusableComponents = [...this.focusableComponents];
10435 if (component.hasOwnProperty('el_') && component.getIsFocusable() && component.getIsAvailableToBeFocused(component.el())) {
10436 focusableComponents.push(component);
10437 }
10438 this.focusableComponents = focusableComponents;
10439 // Trigger the notification manually
10440 this.trigger({
10441 type: 'focusableComponentsChanged',
10442 focusableComponents: this.focusableComponents
10443 });
10444 }
10445
10446 /**
10447 * Removes component from the array of focusable components.
10448 *
10449 * @param {Component} component - The component to be removed from the focusable components array.
10450 */
10451 remove(component) {
10452 for (let i = 0; i < this.focusableComponents.length; i++) {
10453 if (this.focusableComponents[i].name() === component.name()) {
10454 this.focusableComponents.splice(i, 1);
10455 // Trigger the notification manually
10456 this.trigger({
10457 type: 'focusableComponentsChanged',
10458 focusableComponents: this.focusableComponents
10459 });
10460 return;
10461 }
10462 }
10463 }
10464
10465 /**
10466 * Clears array of focusable components.
10467 */
10468 clear() {
10469 // Check if the array is already empty to avoid unnecessary event triggering
10470 if (this.focusableComponents.length > 0) {
10471 // Clear the array
10472 this.focusableComponents = [];
10473
10474 // Trigger the notification manually
10475 this.trigger({
10476 type: 'focusableComponentsChanged',
10477 focusableComponents: this.focusableComponents
10478 });
10479 }
10480 }
10481
10482 /**
10483 * Navigates to the next focusable component based on the specified direction.
10484 *
10485 * @param {string} direction 'up', 'down', 'left', 'right'
10486 */
10487 move(direction) {
10488 const currentFocusedComponent = this.getCurrentComponent();
10489 if (!currentFocusedComponent) {
10490 return;
10491 }
10492 const currentPositions = currentFocusedComponent.getPositions();
10493 const candidates = this.focusableComponents.filter(component => component !== currentFocusedComponent && this.isInDirection_(currentPositions.boundingClientRect, component.getPositions().boundingClientRect, direction));
10494 const bestCandidate = this.findBestCandidate_(currentPositions.center, candidates, direction);
10495 if (bestCandidate) {
10496 this.focus(bestCandidate);
10497 } else {
10498 this.trigger({
10499 type: 'endOfFocusableComponents',
10500 direction,
10501 focusedComponent: currentFocusedComponent
10502 });
10503 }
10504 }
10505
10506 /**
10507 * Finds the best candidate on the current center position,
10508 * the list of candidates, and the specified navigation direction.
10509 *
10510 * @param {Object} currentCenter The center position of the current focused component element.
10511 * @param {Array} candidates An array of candidate components to receive focus.
10512 * @param {string} direction The direction of navigation ('up', 'down', 'left', 'right').
10513 * @return {Object|null} The component that is the best candidate for receiving focus.
10514 */
10515 findBestCandidate_(currentCenter, candidates, direction) {
10516 let minDistance = Infinity;
10517 let bestCandidate = null;
10518 for (const candidate of candidates) {
10519 const candidateCenter = candidate.getPositions().center;
10520 const distance = this.calculateDistance_(currentCenter, candidateCenter, direction);
10521 if (distance < minDistance) {
10522 minDistance = distance;
10523 bestCandidate = candidate;
10524 }
10525 }
10526 return bestCandidate;
10527 }
10528
10529 /**
10530 * Determines if a target rectangle is in the specified navigation direction
10531 * relative to a source rectangle.
10532 *
10533 * @param {Object} srcRect The bounding rectangle of the source element.
10534 * @param {Object} targetRect The bounding rectangle of the target element.
10535 * @param {string} direction The navigation direction ('up', 'down', 'left', 'right').
10536 * @return {boolean} True if the target is in the specified direction relative to the source.
10537 */
10538 isInDirection_(srcRect, targetRect, direction) {
10539 switch (direction) {
10540 case 'right':
10541 return targetRect.left >= srcRect.right;
10542 case 'left':
10543 return targetRect.right <= srcRect.left;
10544 case 'down':
10545 return targetRect.top >= srcRect.bottom;
10546 case 'up':
10547 return targetRect.bottom <= srcRect.top;
10548 default:
10549 return false;
10550 }
10551 }
10552
10553 /**
10554 * Focus the last focused component saved before blur on player.
10555 */
10556 refocusComponent() {
10557 if (this.lastFocusedComponent_) {
10558 // If user is not active, set it to active.
10559 if (!this.player_.userActive()) {
10560 this.player_.userActive(true);
10561 }
10562 this.updateFocusableComponents();
10563
10564 // Search inside array of 'focusableComponents' for a match of name of
10565 // the last focused component.
10566 for (let i = 0; i < this.focusableComponents.length; i++) {
10567 if (this.focusableComponents[i].name() === this.lastFocusedComponent_.name()) {
10568 this.focus(this.focusableComponents[i]);
10569 return;
10570 }
10571 }
10572 } else {
10573 this.focus(this.updateFocusableComponents()[0]);
10574 }
10575 }
10576
10577 /**
10578 * Focuses on a given component.
10579 * If the component is available to be focused, it focuses on the component.
10580 * If not, it attempts to find a suitable DOM child within the component and focuses on it.
10581 *
10582 * @param {Component} component - The component to be focused.
10583 */
10584 focus(component) {
10585 if (typeof component !== 'object') {
10586 return;
10587 }
10588 if (component.getIsAvailableToBeFocused(component.el())) {
10589 component.focus();
10590 } else if (this.findSuitableDOMChild(component)) {
10591 this.findSuitableDOMChild(component).focus();
10592 }
10593 }
10594
10595 /**
10596 * Calculates the distance between two points, adjusting the calculation based on
10597 * the specified navigation direction.
10598 *
10599 * @param {Object} center1 The center point of the first element.
10600 * @param {Object} center2 The center point of the second element.
10601 * @param {string} direction The direction of navigation ('up', 'down', 'left', 'right').
10602 * @return {number} The calculated distance between the two centers.
10603 */
10604 calculateDistance_(center1, center2, direction) {
10605 const dx = Math.abs(center1.x - center2.x);
10606 const dy = Math.abs(center1.y - center2.y);
10607 let distance;
10608 switch (direction) {
10609 case 'right':
10610 case 'left':
10611 // Higher weight for vertical distance in horizontal navigation.
10612 distance = dx + dy * 100;
10613 break;
10614 case 'up':
10615 // Strongly prioritize vertical proximity for UP navigation.
10616 // Adjust the weight to ensure that elements directly above are favored.
10617 distance = dy * 2 + dx * 0.5;
10618 break;
10619 case 'down':
10620 // More balanced weight for vertical and horizontal distances.
10621 // Adjust the weights here to find the best balance.
10622 distance = dy * 5 + dx;
10623 break;
10624 default:
10625 distance = dx + dy;
10626 }
10627 return distance;
10628 }
10629
10630 /**
10631 * This gets called by 'handlePlayerBlur_' if 'spatialNavigation' is enabled.
10632 * Searches for the first 'TextTrackSelect' inside of modal to focus.
10633 *
10634 * @private
10635 */
10636 searchForTrackSelect_() {
10637 const spatialNavigation = this;
10638 for (const component of spatialNavigation.updateFocusableComponents()) {
10639 if (component.constructor.name === 'TextTrackSelect') {
10640 spatialNavigation.focus(component);
10641 break;
10642 }
10643 }
10644 }
10645}
10646
10647/**
10648 * @file loader.js
10649 */
10650
10651/** @import Player from '../player' */
10652
10653/**
10654 * The `MediaLoader` is the `Component` that decides which playback technology to load
10655 * when a player is initialized.
10656 *
10657 * @extends Component
10658 */
10659class MediaLoader extends Component {
10660 /**
10661 * Create an instance of this class.
10662 *
10663 * @param {Player} player
10664 * The `Player` that this class should attach to.
10665 *
10666 * @param {Object} [options]
10667 * The key/value store of player options.
10668 *
10669 * @param {Function} [ready]
10670 * The function that is run when this component is ready.
10671 */
10672 constructor(player, options, ready) {
10673 // MediaLoader has no element
10674 const options_ = merge({
10675 createEl: false
10676 }, options);
10677 super(player, options_, ready);
10678
10679 // If there are no sources when the player is initialized,
10680 // load the first supported playback technology.
10681
10682 if (!options.playerOptions.sources || options.playerOptions.sources.length === 0) {
10683 for (let i = 0, j = options.playerOptions.techOrder; i < j.length; i++) {
10684 const techName = toTitleCase(j[i]);
10685 let tech = Tech.getTech(techName);
10686
10687 // Support old behavior of techs being registered as components.
10688 // Remove once that deprecated behavior is removed.
10689 if (!techName) {
10690 tech = Component.getComponent(techName);
10691 }
10692
10693 // Check if the browser supports this technology
10694 if (tech && tech.isSupported()) {
10695 player.loadTech_(techName);
10696 break;
10697 }
10698 }
10699 } else {
10700 // Loop through playback technologies (e.g. HTML5) and check for support.
10701 // Then load the best source.
10702 // A few assumptions here:
10703 // All playback technologies respect preload false.
10704 player.src(options.playerOptions.sources);
10705 }
10706 }
10707}
10708Component.registerComponent('MediaLoader', MediaLoader);
10709
10710/**
10711 * @file clickable-component.js
10712 */
10713
10714/** @import Player from './player' */
10715
10716/**
10717 * Component which is clickable or keyboard actionable, but is not a
10718 * native HTML button.
10719 *
10720 * @extends Component
10721 */
10722class ClickableComponent extends Component {
10723 /**
10724 * Creates an instance of this class.
10725 *
10726 * @param {Player} player
10727 * The `Player` that this class should be attached to.
10728 *
10729 * @param {Object} [options]
10730 * The key/value store of component options.
10731 *
10732 * @param {function} [options.clickHandler]
10733 * The function to call when the button is clicked / activated
10734 *
10735 * @param {string} [options.controlText]
10736 * The text to set on the button
10737 *
10738 * @param {string} [options.className]
10739 * A class or space separated list of classes to add the component
10740 *
10741 */
10742 constructor(player, options) {
10743 super(player, options);
10744 if (this.options_.controlText) {
10745 this.controlText(this.options_.controlText);
10746 }
10747 this.handleMouseOver_ = e => this.handleMouseOver(e);
10748 this.handleMouseOut_ = e => this.handleMouseOut(e);
10749 this.handleClick_ = e => this.handleClick(e);
10750 this.handleKeyDown_ = e => this.handleKeyDown(e);
10751 this.emitTapEvents();
10752 this.enable();
10753 }
10754
10755 /**
10756 * Create the `ClickableComponent`s DOM element.
10757 *
10758 * @param {string} [tag=div]
10759 * The element's node type.
10760 *
10761 * @param {Object} [props={}]
10762 * An object of properties that should be set on the element.
10763 *
10764 * @param {Object} [attributes={}]
10765 * An object of attributes that should be set on the element.
10766 *
10767 * @return {Element}
10768 * The element that gets created.
10769 */
10770 createEl(tag = 'div', props = {}, attributes = {}) {
10771 props = Object.assign({
10772 className: this.buildCSSClass(),
10773 tabIndex: 0
10774 }, props);
10775 if (tag === 'button') {
10776 log.error(`Creating a ClickableComponent with an HTML element of ${tag} is not supported; use a Button instead.`);
10777 }
10778
10779 // Add ARIA attributes for clickable element which is not a native HTML button
10780 attributes = Object.assign({
10781 role: 'button'
10782 }, attributes);
10783 this.tabIndex_ = props.tabIndex;
10784 const el = createEl(tag, props, attributes);
10785 if (!this.player_.options_.experimentalSvgIcons) {
10786 el.appendChild(createEl('span', {
10787 className: 'vjs-icon-placeholder'
10788 }, {
10789 'aria-hidden': true
10790 }));
10791 }
10792 this.createControlTextEl(el);
10793 return el;
10794 }
10795 dispose() {
10796 // remove controlTextEl_ on dispose
10797 this.controlTextEl_ = null;
10798 super.dispose();
10799 }
10800
10801 /**
10802 * Create a control text element on this `ClickableComponent`
10803 *
10804 * @param {Element} [el]
10805 * Parent element for the control text.
10806 *
10807 * @return {Element}
10808 * The control text element that gets created.
10809 */
10810 createControlTextEl(el) {
10811 this.controlTextEl_ = createEl('span', {
10812 className: 'vjs-control-text'
10813 }, {
10814 // let the screen reader user know that the text of the element may change
10815 'aria-live': 'polite'
10816 });
10817 if (el) {
10818 el.appendChild(this.controlTextEl_);
10819 }
10820 this.controlText(this.controlText_, el);
10821 return this.controlTextEl_;
10822 }
10823
10824 /**
10825 * Get or set the localize text to use for the controls on the `ClickableComponent`.
10826 *
10827 * @param {string} [text]
10828 * Control text for element.
10829 *
10830 * @param {Element} [el=this.el()]
10831 * Element to set the title on.
10832 *
10833 * @return {string}
10834 * - The control text when getting
10835 */
10836 controlText(text, el = this.el()) {
10837 if (text === undefined) {
10838 return this.controlText_ || 'Need Text';
10839 }
10840 const localizedText = this.localize(text);
10841
10842 /** @protected */
10843 this.controlText_ = text;
10844 textContent(this.controlTextEl_, localizedText);
10845 if (!this.nonIconControl && !this.player_.options_.noUITitleAttributes) {
10846 // Set title attribute if only an icon is shown
10847 el.setAttribute('title', localizedText);
10848 }
10849 }
10850
10851 /**
10852 * Builds the default DOM `className`.
10853 *
10854 * @return {string}
10855 * The DOM `className` for this object.
10856 */
10857 buildCSSClass() {
10858 return `vjs-control vjs-button ${super.buildCSSClass()}`;
10859 }
10860
10861 /**
10862 * Enable this `ClickableComponent`
10863 */
10864 enable() {
10865 if (!this.enabled_) {
10866 this.enabled_ = true;
10867 this.removeClass('vjs-disabled');
10868 this.el_.setAttribute('aria-disabled', 'false');
10869 if (typeof this.tabIndex_ !== 'undefined') {
10870 this.el_.setAttribute('tabIndex', this.tabIndex_);
10871 }
10872 this.on(['tap', 'click'], this.handleClick_);
10873 this.on('keydown', this.handleKeyDown_);
10874 }
10875 }
10876
10877 /**
10878 * Disable this `ClickableComponent`
10879 */
10880 disable() {
10881 this.enabled_ = false;
10882 this.addClass('vjs-disabled');
10883 this.el_.setAttribute('aria-disabled', 'true');
10884 if (typeof this.tabIndex_ !== 'undefined') {
10885 this.el_.removeAttribute('tabIndex');
10886 }
10887 this.off('mouseover', this.handleMouseOver_);
10888 this.off('mouseout', this.handleMouseOut_);
10889 this.off(['tap', 'click'], this.handleClick_);
10890 this.off('keydown', this.handleKeyDown_);
10891 }
10892
10893 /**
10894 * Handles language change in ClickableComponent for the player in components
10895 *
10896 *
10897 */
10898 handleLanguagechange() {
10899 this.controlText(this.controlText_);
10900 }
10901
10902 /**
10903 * Event handler that is called when a `ClickableComponent` receives a
10904 * `click` or `tap` event.
10905 *
10906 * @param {Event} event
10907 * The `tap` or `click` event that caused this function to be called.
10908 *
10909 * @listens tap
10910 * @listens click
10911 * @abstract
10912 */
10913 handleClick(event) {
10914 if (this.options_.clickHandler) {
10915 this.options_.clickHandler.call(this, arguments);
10916 }
10917 }
10918
10919 /**
10920 * Event handler that is called when a `ClickableComponent` receives a
10921 * `keydown` event.
10922 *
10923 * By default, if the key is Space or Enter, it will trigger a `click` event.
10924 *
10925 * @param {KeyboardEvent} event
10926 * The `keydown` event that caused this function to be called.
10927 *
10928 * @listens keydown
10929 */
10930 handleKeyDown(event) {
10931 // Support Space or Enter key operation to fire a click event. Also,
10932 // prevent the event from propagating through the DOM and triggering
10933 // Player hotkeys.
10934 if (event.key === ' ' || event.key === 'Enter') {
10935 event.preventDefault();
10936 event.stopPropagation();
10937 this.trigger('click');
10938 } else {
10939 // Pass keypress handling up for unsupported keys
10940 super.handleKeyDown(event);
10941 }
10942 }
10943}
10944Component.registerComponent('ClickableComponent', ClickableComponent);
10945
10946/**
10947 * @file poster-image.js
10948 */
10949
10950/** @import Player from './player' */
10951
10952/**
10953 * A `ClickableComponent` that handles showing the poster image for the player.
10954 *
10955 * @extends ClickableComponent
10956 */
10957class PosterImage extends ClickableComponent {
10958 /**
10959 * Create an instance of this class.
10960 *
10961 * @param {Player} player
10962 * The `Player` that this class should attach to.
10963 *
10964 * @param {Object} [options]
10965 * The key/value store of player options.
10966 */
10967 constructor(player, options) {
10968 super(player, options);
10969 this.update();
10970 this.update_ = e => this.update(e);
10971 player.on('posterchange', this.update_);
10972 }
10973
10974 /**
10975 * Clean up and dispose of the `PosterImage`.
10976 */
10977 dispose() {
10978 this.player().off('posterchange', this.update_);
10979 super.dispose();
10980 }
10981
10982 /**
10983 * Create the `PosterImage`s DOM element.
10984 *
10985 * @return {Element}
10986 * The element that gets created.
10987 */
10988 createEl() {
10989 // The el is an empty div to keep position in the DOM
10990 // A picture and img el will be inserted when a source is set
10991 return createEl('div', {
10992 className: 'vjs-poster'
10993 });
10994 }
10995
10996 /**
10997 * Get or set the `PosterImage`'s crossOrigin option.
10998 *
10999 * @param {string|null} [value]
11000 * The value to set the crossOrigin to. If an argument is
11001 * given, must be one of `'anonymous'` or `'use-credentials'`, or 'null'.
11002 *
11003 * @return {string|null}
11004 * - The current crossOrigin value of the `Player` when getting.
11005 * - undefined when setting
11006 */
11007 crossOrigin(value) {
11008 // `null` can be set to unset a value
11009 if (typeof value === 'undefined') {
11010 if (this.$('img')) {
11011 // If the poster's element exists, give its value
11012 return this.$('img').crossOrigin;
11013 } else if (this.player_.tech_ && this.player_.tech_.isReady_) {
11014 // If not but the tech is ready, query the tech
11015 return this.player_.crossOrigin();
11016 }
11017 // Otherwise check options as the poster is usually set before the state of crossorigin
11018 // can be retrieved by the getter
11019 return this.player_.options_.crossOrigin || this.player_.options_.crossorigin || null;
11020 }
11021 if (value !== null && value !== 'anonymous' && value !== 'use-credentials') {
11022 this.player_.log.warn(`crossOrigin must be null, "anonymous" or "use-credentials", given "${value}"`);
11023 return;
11024 }
11025 if (this.$('img')) {
11026 this.$('img').crossOrigin = value;
11027 }
11028 return;
11029 }
11030
11031 /**
11032 * An {@link EventTarget~EventListener} for {@link Player#posterchange} events.
11033 *
11034 * @listens Player#posterchange
11035 *
11036 * @param {Event} [event]
11037 * The `Player#posterchange` event that triggered this function.
11038 */
11039 update(event) {
11040 const url = this.player().poster();
11041 this.setSrc(url);
11042
11043 // If there's no poster source we should display:none on this component
11044 // so it's not still clickable or right-clickable
11045 if (url) {
11046 this.show();
11047 } else {
11048 this.hide();
11049 }
11050 }
11051
11052 /**
11053 * Set the source of the `PosterImage` depending on the display method. (Re)creates
11054 * the inner picture and img elementss when needed.
11055 *
11056 * @param {string} [url]
11057 * The URL to the source for the `PosterImage`. If not specified or falsy,
11058 * any source and ant inner picture/img are removed.
11059 */
11060 setSrc(url) {
11061 if (!url) {
11062 this.el_.textContent = '';
11063 return;
11064 }
11065 if (!this.$('img')) {
11066 this.el_.appendChild(createEl('picture', {
11067 className: 'vjs-poster',
11068 // Don't want poster to be tabbable.
11069 tabIndex: -1
11070 }, {}, createEl('img', {
11071 loading: 'lazy',
11072 crossOrigin: this.crossOrigin()
11073 }, {
11074 alt: ''
11075 })));
11076 }
11077 this.$('img').src = url;
11078 }
11079
11080 /**
11081 * An {@link EventTarget~EventListener} for clicks on the `PosterImage`. See
11082 * {@link ClickableComponent#handleClick} for instances where this will be triggered.
11083 *
11084 * @listens tap
11085 * @listens click
11086 * @listens keydown
11087 *
11088 * @param {Event} event
11089 + The `click`, `tap` or `keydown` event that caused this function to be called.
11090 */
11091 handleClick(event) {
11092 // We don't want a click to trigger playback when controls are disabled
11093 if (!this.player_.controls()) {
11094 return;
11095 }
11096 if (this.player_.tech(true)) {
11097 this.player_.tech(true).focus();
11098 }
11099 if (this.player_.paused()) {
11100 silencePromise(this.player_.play());
11101 } else {
11102 this.player_.pause();
11103 }
11104 }
11105}
11106
11107/**
11108 * Get or set the `PosterImage`'s crossorigin option. For the HTML5 player, this
11109 * sets the `crossOrigin` property on the `<img>` tag to control the CORS
11110 * behavior.
11111 *
11112 * @param {string|null} [value]
11113 * The value to set the `PosterImages`'s crossorigin to. If an argument is
11114 * given, must be one of `anonymous` or `use-credentials`.
11115 *
11116 * @return {string|null|undefined}
11117 * - The current crossorigin value of the `Player` when getting.
11118 * - undefined when setting
11119 */
11120PosterImage.prototype.crossorigin = PosterImage.prototype.crossOrigin;
11121Component.registerComponent('PosterImage', PosterImage);
11122
11123/**
11124 * @file text-track-display.js
11125 */
11126
11127/** @import Player from '../player' */
11128
11129const darkGray = '#222';
11130const lightGray = '#ccc';
11131const fontMap = {
11132 monospace: 'monospace',
11133 sansSerif: 'sans-serif',
11134 serif: 'serif',
11135 monospaceSansSerif: '"Andale Mono", "Lucida Console", monospace',
11136 monospaceSerif: '"Courier New", monospace',
11137 proportionalSansSerif: 'sans-serif',
11138 proportionalSerif: 'serif',
11139 casual: '"Comic Sans MS", Impact, fantasy',
11140 script: '"Monotype Corsiva", cursive',
11141 smallcaps: '"Andale Mono", "Lucida Console", monospace, sans-serif'
11142};
11143
11144/**
11145 * Construct an rgba color from a given hex color code.
11146 *
11147 * @param {number} color
11148 * Hex number for color, like #f0e or #f604e2.
11149 *
11150 * @param {number} opacity
11151 * Value for opacity, 0.0 - 1.0.
11152 *
11153 * @return {string}
11154 * The rgba color that was created, like 'rgba(255, 0, 0, 0.3)'.
11155 */
11156function constructColor(color, opacity) {
11157 let hex;
11158 if (color.length === 4) {
11159 // color looks like "#f0e"
11160 hex = color[1] + color[1] + color[2] + color[2] + color[3] + color[3];
11161 } else if (color.length === 7) {
11162 // color looks like "#f604e2"
11163 hex = color.slice(1);
11164 } else {
11165 throw new Error('Invalid color code provided, ' + color + '; must be formatted as e.g. #f0e or #f604e2.');
11166 }
11167 return 'rgba(' + parseInt(hex.slice(0, 2), 16) + ',' + parseInt(hex.slice(2, 4), 16) + ',' + parseInt(hex.slice(4, 6), 16) + ',' + opacity + ')';
11168}
11169
11170/**
11171 * Try to update the style of a DOM element. Some style changes will throw an error,
11172 * particularly in IE8. Those should be noops.
11173 *
11174 * @param {Element} el
11175 * The DOM element to be styled.
11176 *
11177 * @param {string} style
11178 * The CSS property on the element that should be styled.
11179 *
11180 * @param {string} rule
11181 * The style rule that should be applied to the property.
11182 *
11183 * @private
11184 */
11185function tryUpdateStyle(el, style, rule) {
11186 try {
11187 el.style[style] = rule;
11188 } catch (e) {
11189 // Satisfies linter.
11190 return;
11191 }
11192}
11193
11194/**
11195 * Converts the CSS top/right/bottom/left property numeric value to string in pixels.
11196 *
11197 * @param {number} position
11198 * The CSS top/right/bottom/left property value.
11199 *
11200 * @return {string}
11201 * The CSS property value that was created, like '10px'.
11202 *
11203 * @private
11204 */
11205function getCSSPositionValue(position) {
11206 return position ? `${position}px` : '';
11207}
11208
11209/**
11210 * The component for displaying text track cues.
11211 *
11212 * @extends Component
11213 */
11214class TextTrackDisplay extends Component {
11215 /**
11216 * Creates an instance of this class.
11217 *
11218 * @param {Player} player
11219 * The `Player` that this class should be attached to.
11220 *
11221 * @param {Object} [options]
11222 * The key/value store of player options.
11223 *
11224 * @param {Function} [ready]
11225 * The function to call when `TextTrackDisplay` is ready.
11226 */
11227 constructor(player, options, ready) {
11228 super(player, options, ready);
11229 const updateDisplayTextHandler = e => this.updateDisplay(e);
11230 const updateDisplayHandler = e => {
11231 this.updateDisplayOverlay();
11232 this.updateDisplay(e);
11233 };
11234 player.on('loadstart', e => this.toggleDisplay(e));
11235 player.on('texttrackchange', updateDisplayTextHandler);
11236 player.on('loadedmetadata', e => {
11237 this.updateDisplayOverlay();
11238 this.preselectTrack(e);
11239 });
11240
11241 // This used to be called during player init, but was causing an error
11242 // if a track should show by default and the display hadn't loaded yet.
11243 // Should probably be moved to an external track loader when we support
11244 // tracks that don't need a display.
11245 player.ready(bind_(this, function () {
11246 if (player.tech_ && player.tech_.featuresNativeTextTracks) {
11247 this.hide();
11248 return;
11249 }
11250 player.on('fullscreenchange', updateDisplayHandler);
11251 player.on('playerresize', updateDisplayHandler);
11252 const screenOrientation = window__default["default"].screen.orientation || window__default["default"];
11253 const changeOrientationEvent = window__default["default"].screen.orientation ? 'change' : 'orientationchange';
11254 screenOrientation.addEventListener(changeOrientationEvent, updateDisplayHandler);
11255 player.on('dispose', () => screenOrientation.removeEventListener(changeOrientationEvent, updateDisplayHandler));
11256 const tracks = this.options_.playerOptions.tracks || [];
11257 for (let i = 0; i < tracks.length; i++) {
11258 this.player_.addRemoteTextTrack(tracks[i], true);
11259 }
11260 this.preselectTrack();
11261 }));
11262 }
11263
11264 /**
11265 * Preselect a track following this precedence:
11266 * - matches the previously selected {@link TextTrack}'s language and kind
11267 * - matches the previously selected {@link TextTrack}'s language only
11268 * - is the first default captions track
11269 * - is the first default descriptions track
11270 *
11271 * @listens Player#loadstart
11272 */
11273 preselectTrack() {
11274 const modes = {
11275 captions: 1,
11276 subtitles: 1
11277 };
11278 const trackList = this.player_.textTracks();
11279 const userPref = this.player_.cache_.selectedLanguage;
11280 let firstDesc;
11281 let firstCaptions;
11282 let preferredTrack;
11283 for (let i = 0; i < trackList.length; i++) {
11284 const track = trackList[i];
11285 if (userPref && userPref.enabled && userPref.language && userPref.language === track.language && track.kind in modes) {
11286 // Always choose the track that matches both language and kind
11287 if (track.kind === userPref.kind) {
11288 preferredTrack = track;
11289 // or choose the first track that matches language
11290 } else if (!preferredTrack) {
11291 preferredTrack = track;
11292 }
11293
11294 // clear everything if offTextTrackMenuItem was clicked
11295 } else if (userPref && !userPref.enabled) {
11296 preferredTrack = null;
11297 firstDesc = null;
11298 firstCaptions = null;
11299 } else if (track.default) {
11300 if (track.kind === 'descriptions' && !firstDesc) {
11301 firstDesc = track;
11302 } else if (track.kind in modes && !firstCaptions) {
11303 firstCaptions = track;
11304 }
11305 }
11306 }
11307
11308 // The preferredTrack matches the user preference and takes
11309 // precedence over all the other tracks.
11310 // So, display the preferredTrack before the first default track
11311 // and the subtitles/captions track before the descriptions track
11312 if (preferredTrack) {
11313 preferredTrack.mode = 'showing';
11314 } else if (firstCaptions) {
11315 firstCaptions.mode = 'showing';
11316 } else if (firstDesc) {
11317 firstDesc.mode = 'showing';
11318 }
11319 }
11320
11321 /**
11322 * Turn display of {@link TextTrack}'s from the current state into the other state.
11323 * There are only two states:
11324 * - 'shown'
11325 * - 'hidden'
11326 *
11327 * @listens Player#loadstart
11328 */
11329 toggleDisplay() {
11330 if (this.player_.tech_ && this.player_.tech_.featuresNativeTextTracks) {
11331 this.hide();
11332 } else {
11333 this.show();
11334 }
11335 }
11336
11337 /**
11338 * Create the {@link Component}'s DOM element.
11339 *
11340 * @return {Element}
11341 * The element that was created.
11342 */
11343 createEl() {
11344 return super.createEl('div', {
11345 className: 'vjs-text-track-display'
11346 }, {
11347 'translate': 'yes',
11348 'aria-live': 'off',
11349 'aria-atomic': 'true'
11350 });
11351 }
11352
11353 /**
11354 * Clear all displayed {@link TextTrack}s.
11355 */
11356 clearDisplay() {
11357 if (typeof window__default["default"].WebVTT === 'function') {
11358 window__default["default"].WebVTT.processCues(window__default["default"], [], this.el_);
11359 }
11360 }
11361
11362 /**
11363 * Update the displayed TextTrack when a either a {@link Player#texttrackchange} or
11364 * a {@link Player#fullscreenchange} is fired.
11365 *
11366 * @listens Player#texttrackchange
11367 * @listens Player#fullscreenchange
11368 */
11369 updateDisplay() {
11370 const tracks = this.player_.textTracks();
11371 const allowMultipleShowingTracks = this.options_.allowMultipleShowingTracks;
11372 this.clearDisplay();
11373 if (allowMultipleShowingTracks) {
11374 const showingTracks = [];
11375 for (let i = 0; i < tracks.length; ++i) {
11376 const track = tracks[i];
11377 if (track.mode !== 'showing') {
11378 continue;
11379 }
11380 showingTracks.push(track);
11381 }
11382 this.updateForTrack(showingTracks);
11383 return;
11384 }
11385
11386 // Track display prioritization model: if multiple tracks are 'showing',
11387 // display the first 'subtitles' or 'captions' track which is 'showing',
11388 // otherwise display the first 'descriptions' track which is 'showing'
11389
11390 let descriptionsTrack = null;
11391 let captionsSubtitlesTrack = null;
11392 let i = tracks.length;
11393 while (i--) {
11394 const track = tracks[i];
11395 if (track.mode === 'showing') {
11396 if (track.kind === 'descriptions') {
11397 descriptionsTrack = track;
11398 } else {
11399 captionsSubtitlesTrack = track;
11400 }
11401 }
11402 }
11403 if (captionsSubtitlesTrack) {
11404 if (this.getAttribute('aria-live') !== 'off') {
11405 this.setAttribute('aria-live', 'off');
11406 }
11407 this.updateForTrack(captionsSubtitlesTrack);
11408 } else if (descriptionsTrack) {
11409 if (this.getAttribute('aria-live') !== 'assertive') {
11410 this.setAttribute('aria-live', 'assertive');
11411 }
11412 this.updateForTrack(descriptionsTrack);
11413 }
11414 if (!window__default["default"].CSS.supports('inset', '10px')) {
11415 const textTrackDisplay = this.el_;
11416 const vjsTextTrackCues = textTrackDisplay.querySelectorAll('.vjs-text-track-cue');
11417 const controlBarHeight = this.player_.controlBar.el_.getBoundingClientRect().height;
11418 const playerHeight = this.player_.el_.getBoundingClientRect().height;
11419
11420 // Clear inline style before getting actual height of textTrackDisplay
11421 textTrackDisplay.style = '';
11422
11423 // textrack style updates, this styles are required to be inline
11424 tryUpdateStyle(textTrackDisplay, 'position', 'relative');
11425 tryUpdateStyle(textTrackDisplay, 'height', playerHeight - controlBarHeight + 'px');
11426 tryUpdateStyle(textTrackDisplay, 'top', 'unset');
11427 if (IS_SMART_TV) {
11428 tryUpdateStyle(textTrackDisplay, 'bottom', playerHeight + 'px');
11429 } else {
11430 tryUpdateStyle(textTrackDisplay, 'bottom', '0px');
11431 }
11432
11433 // vjsTextTrackCue style updates
11434 if (vjsTextTrackCues.length > 0) {
11435 vjsTextTrackCues.forEach(vjsTextTrackCue => {
11436 // verify if inset styles are inline
11437 if (vjsTextTrackCue.style.inset) {
11438 const insetStyles = vjsTextTrackCue.style.inset.split(' ');
11439
11440 // expected value is always 3
11441 if (insetStyles.length === 3) {
11442 Object.assign(vjsTextTrackCue.style, {
11443 top: insetStyles[0],
11444 right: insetStyles[1],
11445 bottom: insetStyles[2],
11446 left: 'unset'
11447 });
11448 }
11449 }
11450 });
11451 }
11452 }
11453 }
11454
11455 /**
11456 * Updates the displayed TextTrack to be sure it overlays the video when a either
11457 * a {@link Player#texttrackchange} or a {@link Player#fullscreenchange} is fired.
11458 */
11459 updateDisplayOverlay() {
11460 // inset-inline and inset-block are not supprted on old chrome, but these are
11461 // only likely to be used on TV devices
11462 if (!this.player_.videoHeight() || !window__default["default"].CSS.supports('inset-inline: 10px')) {
11463 return;
11464 }
11465 const playerWidth = this.player_.currentWidth();
11466 const playerHeight = this.player_.currentHeight();
11467 const playerAspectRatio = playerWidth / playerHeight;
11468 const videoAspectRatio = this.player_.videoWidth() / this.player_.videoHeight();
11469 let insetInlineMatch = 0;
11470 let insetBlockMatch = 0;
11471 if (Math.abs(playerAspectRatio - videoAspectRatio) > 0.1) {
11472 if (playerAspectRatio > videoAspectRatio) {
11473 insetInlineMatch = Math.round((playerWidth - playerHeight * videoAspectRatio) / 2);
11474 } else {
11475 insetBlockMatch = Math.round((playerHeight - playerWidth / videoAspectRatio) / 2);
11476 }
11477 }
11478 tryUpdateStyle(this.el_, 'insetInline', getCSSPositionValue(insetInlineMatch));
11479 tryUpdateStyle(this.el_, 'insetBlock', getCSSPositionValue(insetBlockMatch));
11480 }
11481
11482 /**
11483 * Style {@Link TextTrack} activeCues according to {@Link TextTrackSettings}.
11484 *
11485 * @param {TextTrack} track
11486 * Text track object containing active cues to style.
11487 */
11488 updateDisplayState(track) {
11489 const overrides = this.player_.textTrackSettings.getValues();
11490 const cues = track.activeCues;
11491 let i = cues.length;
11492 while (i--) {
11493 const cue = cues[i];
11494 if (!cue) {
11495 continue;
11496 }
11497 const cueDiv = cue.displayState;
11498 if (overrides.color) {
11499 cueDiv.firstChild.style.color = overrides.color;
11500 }
11501 if (overrides.textOpacity) {
11502 tryUpdateStyle(cueDiv.firstChild, 'color', constructColor(overrides.color || '#fff', overrides.textOpacity));
11503 }
11504 if (overrides.backgroundColor) {
11505 cueDiv.firstChild.style.backgroundColor = overrides.backgroundColor;
11506 }
11507 if (overrides.backgroundOpacity) {
11508 tryUpdateStyle(cueDiv.firstChild, 'backgroundColor', constructColor(overrides.backgroundColor || '#000', overrides.backgroundOpacity));
11509 }
11510 if (overrides.windowColor) {
11511 if (overrides.windowOpacity) {
11512 tryUpdateStyle(cueDiv, 'backgroundColor', constructColor(overrides.windowColor, overrides.windowOpacity));
11513 } else {
11514 cueDiv.style.backgroundColor = overrides.windowColor;
11515 }
11516 }
11517 if (overrides.edgeStyle) {
11518 if (overrides.edgeStyle === 'dropshadow') {
11519 cueDiv.firstChild.style.textShadow = `2px 2px 3px ${darkGray}, 2px 2px 4px ${darkGray}, 2px 2px 5px ${darkGray}`;
11520 } else if (overrides.edgeStyle === 'raised') {
11521 cueDiv.firstChild.style.textShadow = `1px 1px ${darkGray}, 2px 2px ${darkGray}, 3px 3px ${darkGray}`;
11522 } else if (overrides.edgeStyle === 'depressed') {
11523 cueDiv.firstChild.style.textShadow = `1px 1px ${lightGray}, 0 1px ${lightGray}, -1px -1px ${darkGray}, 0 -1px ${darkGray}`;
11524 } else if (overrides.edgeStyle === 'uniform') {
11525 cueDiv.firstChild.style.textShadow = `0 0 4px ${darkGray}, 0 0 4px ${darkGray}, 0 0 4px ${darkGray}, 0 0 4px ${darkGray}`;
11526 }
11527 }
11528 if (overrides.fontPercent && overrides.fontPercent !== 1) {
11529 const fontSize = window__default["default"].parseFloat(cueDiv.style.fontSize);
11530 cueDiv.style.fontSize = fontSize * overrides.fontPercent + 'px';
11531 cueDiv.style.height = 'auto';
11532 cueDiv.style.top = 'auto';
11533 }
11534 if (overrides.fontFamily && overrides.fontFamily !== 'default') {
11535 if (overrides.fontFamily === 'small-caps') {
11536 cueDiv.firstChild.style.fontVariant = 'small-caps';
11537 } else {
11538 cueDiv.firstChild.style.fontFamily = fontMap[overrides.fontFamily];
11539 }
11540 }
11541 }
11542 }
11543
11544 /**
11545 * Add an {@link TextTrack} to to the {@link Tech}s {@link TextTrackList}.
11546 *
11547 * @param {TextTrack|TextTrack[]} tracks
11548 * Text track object or text track array to be added to the list.
11549 */
11550 updateForTrack(tracks) {
11551 if (!Array.isArray(tracks)) {
11552 tracks = [tracks];
11553 }
11554 if (typeof window__default["default"].WebVTT !== 'function' || tracks.every(track => {
11555 return !track.activeCues;
11556 })) {
11557 return;
11558 }
11559 const cues = [];
11560
11561 // push all active track cues
11562 for (let i = 0; i < tracks.length; ++i) {
11563 const track = tracks[i];
11564 for (let j = 0; j < track.activeCues.length; ++j) {
11565 cues.push(track.activeCues[j]);
11566 }
11567 }
11568
11569 // removes all cues before it processes new ones
11570 window__default["default"].WebVTT.processCues(window__default["default"], cues, this.el_);
11571
11572 // add unique class to each language text track & add settings styling if necessary
11573 for (let i = 0; i < tracks.length; ++i) {
11574 const track = tracks[i];
11575 for (let j = 0; j < track.activeCues.length; ++j) {
11576 const cueEl = track.activeCues[j].displayState;
11577 addClass(cueEl, 'vjs-text-track-cue', 'vjs-text-track-cue-' + (track.language ? track.language : i));
11578 if (track.language) {
11579 setAttribute(cueEl, 'lang', track.language);
11580 }
11581 }
11582 if (this.player_.textTrackSettings) {
11583 this.updateDisplayState(track);
11584 }
11585 }
11586 }
11587}
11588Component.registerComponent('TextTrackDisplay', TextTrackDisplay);
11589
11590/**
11591 * @file loading-spinner.js
11592 */
11593
11594/**
11595 * A loading spinner for use during waiting/loading events.
11596 *
11597 * @extends Component
11598 */
11599class LoadingSpinner extends Component {
11600 /**
11601 * Create the `LoadingSpinner`s DOM element.
11602 *
11603 * @return {Element}
11604 * The dom element that gets created.
11605 */
11606 createEl() {
11607 const isAudio = this.player_.isAudio();
11608 const playerType = this.localize(isAudio ? 'Audio Player' : 'Video Player');
11609 const controlText = createEl('span', {
11610 className: 'vjs-control-text',
11611 textContent: this.localize('{1} is loading.', [playerType])
11612 });
11613 const el = super.createEl('div', {
11614 className: 'vjs-loading-spinner',
11615 dir: 'ltr'
11616 });
11617 el.appendChild(controlText);
11618 return el;
11619 }
11620
11621 /**
11622 * Update control text on languagechange
11623 */
11624 handleLanguagechange() {
11625 this.$('.vjs-control-text').textContent = this.localize('{1} is loading.', [this.player_.isAudio() ? 'Audio Player' : 'Video Player']);
11626 }
11627}
11628Component.registerComponent('LoadingSpinner', LoadingSpinner);
11629
11630/**
11631 * @file button.js
11632 */
11633
11634/**
11635 * Base class for all buttons.
11636 *
11637 * @extends ClickableComponent
11638 */
11639class Button extends ClickableComponent {
11640 /**
11641 * Create the `Button`s DOM element.
11642 *
11643 * @param {string} [tag="button"]
11644 * The element's node type. This argument is IGNORED: no matter what
11645 * is passed, it will always create a `button` element.
11646 *
11647 * @param {Object} [props={}]
11648 * An object of properties that should be set on the element.
11649 *
11650 * @param {Object} [attributes={}]
11651 * An object of attributes that should be set on the element.
11652 *
11653 * @return {Element}
11654 * The element that gets created.
11655 */
11656 createEl(tag, props = {}, attributes = {}) {
11657 tag = 'button';
11658 props = Object.assign({
11659 className: this.buildCSSClass()
11660 }, props);
11661
11662 // Add attributes for button element
11663 attributes = Object.assign({
11664 // Necessary since the default button type is "submit"
11665 type: 'button'
11666 }, attributes);
11667 const el = createEl(tag, props, attributes);
11668 if (!this.player_.options_.experimentalSvgIcons) {
11669 el.appendChild(createEl('span', {
11670 className: 'vjs-icon-placeholder'
11671 }, {
11672 'aria-hidden': true
11673 }));
11674 }
11675 this.createControlTextEl(el);
11676 return el;
11677 }
11678
11679 /**
11680 * Add a child `Component` inside of this `Button`.
11681 *
11682 * @param {string|Component} child
11683 * The name or instance of a child to add.
11684 *
11685 * @param {Object} [options={}]
11686 * The key/value store of options that will get passed to children of
11687 * the child.
11688 *
11689 * @return {Component}
11690 * The `Component` that gets added as a child. When using a string the
11691 * `Component` will get created by this process.
11692 *
11693 * @deprecated since version 5
11694 */
11695 addChild(child, options = {}) {
11696 const className = this.constructor.name;
11697 log.warn(`Adding an actionable (user controllable) child to a Button (${className}) is not supported; use a ClickableComponent instead.`);
11698
11699 // Avoid the error message generated by ClickableComponent's addChild method
11700 return Component.prototype.addChild.call(this, child, options);
11701 }
11702
11703 /**
11704 * Enable the `Button` element so that it can be activated or clicked. Use this with
11705 * {@link Button#disable}.
11706 */
11707 enable() {
11708 super.enable();
11709 this.el_.removeAttribute('disabled');
11710 }
11711
11712 /**
11713 * Disable the `Button` element so that it cannot be activated or clicked. Use this with
11714 * {@link Button#enable}.
11715 */
11716 disable() {
11717 super.disable();
11718 this.el_.setAttribute('disabled', 'disabled');
11719 }
11720
11721 /**
11722 * This gets called when a `Button` has focus and `keydown` is triggered via a key
11723 * press.
11724 *
11725 * @param {KeyboardEvent} event
11726 * The event that caused this function to get called.
11727 *
11728 * @listens keydown
11729 */
11730 handleKeyDown(event) {
11731 // Ignore Space or Enter key operation, which is handled by the browser for
11732 // a button - though not for its super class, ClickableComponent. Also,
11733 // prevent the event from propagating through the DOM and triggering Player
11734 // hotkeys. We do not preventDefault here because we _want_ the browser to
11735 // handle it.
11736 if (event.key === ' ' || event.key === 'Enter') {
11737 event.stopPropagation();
11738 return;
11739 }
11740
11741 // Pass keypress handling up for unsupported keys
11742 super.handleKeyDown(event);
11743 }
11744}
11745Component.registerComponent('Button', Button);
11746
11747/**
11748 * @file big-play-button.js
11749 */
11750
11751/**
11752 * The initial play button that shows before the video has played. The hiding of the
11753 * `BigPlayButton` get done via CSS and `Player` states.
11754 *
11755 * @extends Button
11756 */
11757class BigPlayButton extends Button {
11758 constructor(player, options) {
11759 super(player, options);
11760 this.mouseused_ = false;
11761 this.setIcon('play');
11762 this.on('mousedown', e => this.handleMouseDown(e));
11763 }
11764
11765 /**
11766 * Builds the default DOM `className`.
11767 *
11768 * @return {string}
11769 * The DOM `className` for this object. Always returns 'vjs-big-play-button'.
11770 */
11771 buildCSSClass() {
11772 return 'vjs-big-play-button';
11773 }
11774
11775 /**
11776 * This gets called when a `BigPlayButton` "clicked". See {@link ClickableComponent}
11777 * for more detailed information on what a click can be.
11778 *
11779 * @param {KeyboardEvent|MouseEvent|TouchEvent} event
11780 * The `keydown`, `tap`, or `click` event that caused this function to be
11781 * called.
11782 *
11783 * @listens tap
11784 * @listens click
11785 */
11786 handleClick(event) {
11787 const playPromise = this.player_.play();
11788
11789 // exit early if clicked via the mouse
11790 if (this.mouseused_ && 'clientX' in event && 'clientY' in event) {
11791 silencePromise(playPromise);
11792 if (this.player_.tech(true)) {
11793 this.player_.tech(true).focus();
11794 }
11795 return;
11796 }
11797 const cb = this.player_.getChild('controlBar');
11798 const playToggle = cb && cb.getChild('playToggle');
11799 if (!playToggle) {
11800 this.player_.tech(true).focus();
11801 return;
11802 }
11803 const playFocus = () => playToggle.focus();
11804 if (isPromise(playPromise)) {
11805 playPromise.then(playFocus, () => {});
11806 } else {
11807 this.setTimeout(playFocus, 1);
11808 }
11809 }
11810
11811 /**
11812 * Event handler that is called when a `BigPlayButton` receives a
11813 * `keydown` event.
11814 *
11815 * @param {KeyboardEvent} event
11816 * The `keydown` event that caused this function to be called.
11817 *
11818 * @listens keydown
11819 */
11820 handleKeyDown(event) {
11821 this.mouseused_ = false;
11822 super.handleKeyDown(event);
11823 }
11824
11825 /**
11826 * Handle `mousedown` events on the `BigPlayButton`.
11827 *
11828 * @param {MouseEvent} event
11829 * `mousedown` or `touchstart` event that triggered this function
11830 *
11831 * @listens mousedown
11832 */
11833 handleMouseDown(event) {
11834 this.mouseused_ = true;
11835 }
11836}
11837
11838/**
11839 * The text that should display over the `BigPlayButton`s controls. Added to for localization.
11840 *
11841 * @type {string}
11842 * @protected
11843 */
11844BigPlayButton.prototype.controlText_ = 'Play Video';
11845Component.registerComponent('BigPlayButton', BigPlayButton);
11846
11847/**
11848 * @file close-button.js
11849 */
11850
11851/** @import Player from './player' */
11852
11853/**
11854 * The `CloseButton` is a `{@link Button}` that fires a `close` event when
11855 * it gets clicked.
11856 *
11857 * @extends Button
11858 */
11859class CloseButton extends Button {
11860 /**
11861 * Creates an instance of the this class.
11862 *
11863 * @param {Player} player
11864 * The `Player` that this class should be attached to.
11865 *
11866 * @param {Object} [options]
11867 * The key/value store of player options.
11868 */
11869 constructor(player, options) {
11870 super(player, options);
11871 this.setIcon('cancel');
11872 this.controlText(options && options.controlText || this.localize('Close'));
11873 }
11874
11875 /**
11876 * Builds the default DOM `className`.
11877 *
11878 * @return {string}
11879 * The DOM `className` for this object.
11880 */
11881 buildCSSClass() {
11882 return `vjs-close-button ${super.buildCSSClass()}`;
11883 }
11884
11885 /**
11886 * This gets called when a `CloseButton` gets clicked. See
11887 * {@link ClickableComponent#handleClick} for more information on when
11888 * this will be triggered
11889 *
11890 * @param {Event} event
11891 * The `keydown`, `tap`, or `click` event that caused this function to be
11892 * called.
11893 *
11894 * @listens tap
11895 * @listens click
11896 * @fires CloseButton#close
11897 */
11898 handleClick(event) {
11899 /**
11900 * Triggered when the a `CloseButton` is clicked.
11901 *
11902 * @event CloseButton#close
11903 * @type {Event}
11904 *
11905 * @property {boolean} [bubbles=false]
11906 * set to false so that the close event does not
11907 * bubble up to parents if there is no listener
11908 */
11909 this.trigger({
11910 type: 'close',
11911 bubbles: false
11912 });
11913 }
11914 /**
11915 * Event handler that is called when a `CloseButton` receives a
11916 * `keydown` event.
11917 *
11918 * By default, if the key is Esc, it will trigger a `click` event.
11919 *
11920 * @param {KeyboardEvent} event
11921 * The `keydown` event that caused this function to be called.
11922 *
11923 * @listens keydown
11924 */
11925 handleKeyDown(event) {
11926 // Esc button will trigger `click` event
11927 if (event.key === 'Escape') {
11928 event.preventDefault();
11929 event.stopPropagation();
11930 this.trigger('click');
11931 } else {
11932 // Pass keypress handling up for unsupported keys
11933 super.handleKeyDown(event);
11934 }
11935 }
11936}
11937Component.registerComponent('CloseButton', CloseButton);
11938
11939/**
11940 * @file play-toggle.js
11941 */
11942
11943/** @import Player from './player' */
11944
11945/**
11946 * Button to toggle between play and pause.
11947 *
11948 * @extends Button
11949 */
11950class PlayToggle extends Button {
11951 /**
11952 * Creates an instance of this class.
11953 *
11954 * @param {Player} player
11955 * The `Player` that this class should be attached to.
11956 *
11957 * @param {Object} [options={}]
11958 * The key/value store of player options.
11959 */
11960 constructor(player, options = {}) {
11961 super(player, options);
11962
11963 // show or hide replay icon
11964 options.replay = options.replay === undefined || options.replay;
11965 this.setIcon('play');
11966 this.on(player, 'play', e => this.handlePlay(e));
11967 this.on(player, 'pause', e => this.handlePause(e));
11968 if (options.replay) {
11969 this.on(player, 'ended', e => this.handleEnded(e));
11970 }
11971 }
11972
11973 /**
11974 * Builds the default DOM `className`.
11975 *
11976 * @return {string}
11977 * The DOM `className` for this object.
11978 */
11979 buildCSSClass() {
11980 return `vjs-play-control ${super.buildCSSClass()}`;
11981 }
11982
11983 /**
11984 * This gets called when an `PlayToggle` is "clicked". See
11985 * {@link ClickableComponent} for more detailed information on what a click can be.
11986 *
11987 * @param {Event} [event]
11988 * The `keydown`, `tap`, or `click` event that caused this function to be
11989 * called.
11990 *
11991 * @listens tap
11992 * @listens click
11993 */
11994 handleClick(event) {
11995 if (this.player_.paused()) {
11996 silencePromise(this.player_.play());
11997 } else {
11998 this.player_.pause();
11999 }
12000 }
12001
12002 /**
12003 * This gets called once after the video has ended and the user seeks so that
12004 * we can change the replay button back to a play button.
12005 *
12006 * @param {Event} [event]
12007 * The event that caused this function to run.
12008 *
12009 * @listens Player#seeked
12010 */
12011 handleSeeked(event) {
12012 this.removeClass('vjs-ended');
12013 if (this.player_.paused()) {
12014 this.handlePause(event);
12015 } else {
12016 this.handlePlay(event);
12017 }
12018 }
12019
12020 /**
12021 * Add the vjs-playing class to the element so it can change appearance.
12022 *
12023 * @param {Event} [event]
12024 * The event that caused this function to run.
12025 *
12026 * @listens Player#play
12027 */
12028 handlePlay(event) {
12029 this.removeClass('vjs-ended', 'vjs-paused');
12030 this.addClass('vjs-playing');
12031 // change the button text to "Pause"
12032 this.setIcon('pause');
12033 this.controlText('Pause');
12034 }
12035
12036 /**
12037 * Add the vjs-paused class to the element so it can change appearance.
12038 *
12039 * @param {Event} [event]
12040 * The event that caused this function to run.
12041 *
12042 * @listens Player#pause
12043 */
12044 handlePause(event) {
12045 this.removeClass('vjs-playing');
12046 this.addClass('vjs-paused');
12047 // change the button text to "Play"
12048 this.setIcon('play');
12049 this.controlText('Play');
12050 }
12051
12052 /**
12053 * Add the vjs-ended class to the element so it can change appearance
12054 *
12055 * @param {Event} [event]
12056 * The event that caused this function to run.
12057 *
12058 * @listens Player#ended
12059 */
12060 handleEnded(event) {
12061 this.removeClass('vjs-playing');
12062 this.addClass('vjs-ended');
12063 // change the button text to "Replay"
12064 this.setIcon('replay');
12065 this.controlText('Replay');
12066
12067 // on the next seek remove the replay button
12068 this.one(this.player_, 'seeked', e => this.handleSeeked(e));
12069 }
12070}
12071
12072/**
12073 * The text that should display over the `PlayToggle`s controls. Added for localization.
12074 *
12075 * @type {string}
12076 * @protected
12077 */
12078PlayToggle.prototype.controlText_ = 'Play';
12079Component.registerComponent('PlayToggle', PlayToggle);
12080
12081/**
12082 * @file time-display.js
12083 */
12084
12085/** @import Player from '../../player' */
12086
12087/**
12088 * Displays time information about the video
12089 *
12090 * @extends Component
12091 */
12092class TimeDisplay extends Component {
12093 /**
12094 * Creates an instance of this class.
12095 *
12096 * @param {Player} player
12097 * The `Player` that this class should be attached to.
12098 *
12099 * @param {Object} [options]
12100 * The key/value store of player options.
12101 */
12102 constructor(player, options) {
12103 super(player, options);
12104 this.on(player, ['timeupdate', 'ended', 'seeking'], e => this.update(e));
12105 this.updateTextNode_();
12106 }
12107
12108 /**
12109 * Create the `Component`'s DOM element
12110 *
12111 * @return {Element}
12112 * The element that was created.
12113 */
12114 createEl() {
12115 const className = this.buildCSSClass();
12116 const el = super.createEl('div', {
12117 className: `${className} vjs-time-control vjs-control`
12118 });
12119 const span = createEl('span', {
12120 className: 'vjs-control-text',
12121 textContent: `${this.localize(this.labelText_)}\u00a0`
12122 }, {
12123 role: 'presentation'
12124 });
12125 el.appendChild(span);
12126 this.contentEl_ = createEl('span', {
12127 className: `${className}-display`
12128 }, {
12129 // span elements have no implicit role, but some screen readers (notably VoiceOver)
12130 // treat them as a break between items in the DOM when using arrow keys
12131 // (or left-to-right swipes on iOS) to read contents of a page. Using
12132 // role='presentation' causes VoiceOver to NOT treat this span as a break.
12133 role: 'presentation'
12134 });
12135 el.appendChild(this.contentEl_);
12136 return el;
12137 }
12138 dispose() {
12139 this.contentEl_ = null;
12140 this.textNode_ = null;
12141 super.dispose();
12142 }
12143
12144 /**
12145 * Updates the displayed time according to the `updateContent` function which is defined in the child class.
12146 *
12147 * @param {Event} [event]
12148 * The `timeupdate`, `ended` or `seeking` (if enableSmoothSeeking is true) event that caused this function to be called.
12149 */
12150 update(event) {
12151 if (!this.player_.options_.enableSmoothSeeking && event.type === 'seeking') {
12152 return;
12153 }
12154 this.updateContent(event);
12155 }
12156
12157 /**
12158 * Updates the time display text node with a new time
12159 *
12160 * @param {number} [time=0] the time to update to
12161 *
12162 * @private
12163 */
12164 updateTextNode_(time = 0) {
12165 time = formatTime(time);
12166 if (this.formattedTime_ === time) {
12167 return;
12168 }
12169 this.formattedTime_ = time;
12170 this.requestNamedAnimationFrame('TimeDisplay#updateTextNode_', () => {
12171 if (!this.contentEl_) {
12172 return;
12173 }
12174 let oldNode = this.textNode_;
12175 if (oldNode && this.contentEl_.firstChild !== oldNode) {
12176 oldNode = null;
12177 log.warn('TimeDisplay#updateTextnode_: Prevented replacement of text node element since it was no longer a child of this node. Appending a new node instead.');
12178 }
12179 this.textNode_ = document__default["default"].createTextNode(this.formattedTime_);
12180 if (!this.textNode_) {
12181 return;
12182 }
12183 if (oldNode) {
12184 this.contentEl_.replaceChild(this.textNode_, oldNode);
12185 } else {
12186 this.contentEl_.appendChild(this.textNode_);
12187 }
12188 });
12189 }
12190
12191 /**
12192 * To be filled out in the child class, should update the displayed time
12193 * in accordance with the fact that the current time has changed.
12194 *
12195 * @param {Event} [event]
12196 * The `timeupdate` event that caused this to run.
12197 *
12198 * @listens Player#timeupdate
12199 */
12200 updateContent(event) {}
12201}
12202
12203/**
12204 * The text that is added to the `TimeDisplay` for screen reader users.
12205 *
12206 * @type {string}
12207 * @private
12208 */
12209TimeDisplay.prototype.labelText_ = 'Time';
12210
12211/**
12212 * The text that should display over the `TimeDisplay`s controls. Added to for localization.
12213 *
12214 * @type {string}
12215 * @protected
12216 *
12217 * @deprecated in v7; controlText_ is not used in non-active display Components
12218 */
12219TimeDisplay.prototype.controlText_ = 'Time';
12220Component.registerComponent('TimeDisplay', TimeDisplay);
12221
12222/**
12223 * @file current-time-display.js
12224 */
12225
12226/**
12227 * Displays the current time
12228 *
12229 * @extends Component
12230 */
12231class CurrentTimeDisplay extends TimeDisplay {
12232 /**
12233 * Builds the default DOM `className`.
12234 *
12235 * @return {string}
12236 * The DOM `className` for this object.
12237 */
12238 buildCSSClass() {
12239 return 'vjs-current-time';
12240 }
12241
12242 /**
12243 * Update current time display
12244 *
12245 * @param {Event} [event]
12246 * The `timeupdate` event that caused this function to run.
12247 *
12248 * @listens Player#timeupdate
12249 */
12250 updateContent(event) {
12251 // Allows for smooth scrubbing, when player can't keep up.
12252 let time;
12253 if (this.player_.ended()) {
12254 time = this.player_.duration();
12255 } else {
12256 time = this.player_.scrubbing() ? this.player_.getCache().currentTime : this.player_.currentTime();
12257 }
12258 this.updateTextNode_(time);
12259 }
12260}
12261
12262/**
12263 * The text that is added to the `CurrentTimeDisplay` for screen reader users.
12264 *
12265 * @type {string}
12266 * @private
12267 */
12268CurrentTimeDisplay.prototype.labelText_ = 'Current Time';
12269
12270/**
12271 * The text that should display over the `CurrentTimeDisplay`s controls. Added to for localization.
12272 *
12273 * @type {string}
12274 * @protected
12275 *
12276 * @deprecated in v7; controlText_ is not used in non-active display Components
12277 */
12278CurrentTimeDisplay.prototype.controlText_ = 'Current Time';
12279Component.registerComponent('CurrentTimeDisplay', CurrentTimeDisplay);
12280
12281/**
12282 * @file duration-display.js
12283 */
12284
12285/** @import Player from '../../player' */
12286
12287/**
12288 * Displays the duration
12289 *
12290 * @extends Component
12291 */
12292class DurationDisplay extends TimeDisplay {
12293 /**
12294 * Creates an instance of this class.
12295 *
12296 * @param {Player} player
12297 * The `Player` that this class should be attached to.
12298 *
12299 * @param {Object} [options]
12300 * The key/value store of player options.
12301 */
12302 constructor(player, options) {
12303 super(player, options);
12304 const updateContent = e => this.updateContent(e);
12305
12306 // we do not want to/need to throttle duration changes,
12307 // as they should always display the changed duration as
12308 // it has changed
12309 this.on(player, 'durationchange', updateContent);
12310
12311 // Listen to loadstart because the player duration is reset when a new media element is loaded,
12312 // but the durationchange on the user agent will not fire.
12313 // @see [Spec]{@link https://www.w3.org/TR/2011/WD-html5-20110113/video.html#media-element-load-algorithm}
12314 this.on(player, 'loadstart', updateContent);
12315
12316 // Also listen for timeupdate (in the parent) and loadedmetadata because removing those
12317 // listeners could have broken dependent applications/libraries. These
12318 // can likely be removed for 7.0.
12319 this.on(player, 'loadedmetadata', updateContent);
12320 }
12321
12322 /**
12323 * Builds the default DOM `className`.
12324 *
12325 * @return {string}
12326 * The DOM `className` for this object.
12327 */
12328 buildCSSClass() {
12329 return 'vjs-duration';
12330 }
12331
12332 /**
12333 * Update duration time display.
12334 *
12335 * @param {Event} [event]
12336 * The `durationchange`, `timeupdate`, or `loadedmetadata` event that caused
12337 * this function to be called.
12338 *
12339 * @listens Player#durationchange
12340 * @listens Player#timeupdate
12341 * @listens Player#loadedmetadata
12342 */
12343 updateContent(event) {
12344 const duration = this.player_.duration();
12345 this.updateTextNode_(duration);
12346 }
12347}
12348
12349/**
12350 * The text that is added to the `DurationDisplay` for screen reader users.
12351 *
12352 * @type {string}
12353 * @private
12354 */
12355DurationDisplay.prototype.labelText_ = 'Duration';
12356
12357/**
12358 * The text that should display over the `DurationDisplay`s controls. Added to for localization.
12359 *
12360 * @type {string}
12361 * @protected
12362 *
12363 * @deprecated in v7; controlText_ is not used in non-active display Components
12364 */
12365DurationDisplay.prototype.controlText_ = 'Duration';
12366Component.registerComponent('DurationDisplay', DurationDisplay);
12367
12368/**
12369 * @file time-divider.js
12370 */
12371
12372/**
12373 * The separator between the current time and duration.
12374 * Can be hidden if it's not needed in the design.
12375 *
12376 * @extends Component
12377 */
12378class TimeDivider extends Component {
12379 /**
12380 * Create the component's DOM element
12381 *
12382 * @return {Element}
12383 * The element that was created.
12384 */
12385 createEl() {
12386 const el = super.createEl('div', {
12387 className: 'vjs-time-control vjs-time-divider'
12388 }, {
12389 // this element and its contents can be hidden from assistive techs since
12390 // it is made extraneous by the announcement of the control text
12391 // for the current time and duration displays
12392 'aria-hidden': true
12393 });
12394 const div = super.createEl('div');
12395 const span = super.createEl('span', {
12396 textContent: '/'
12397 });
12398 div.appendChild(span);
12399 el.appendChild(div);
12400 return el;
12401 }
12402}
12403Component.registerComponent('TimeDivider', TimeDivider);
12404
12405/**
12406 * @file remaining-time-display.js
12407 */
12408
12409/** @import Player from '../../player' */
12410
12411/**
12412 * Displays the time left in the video
12413 *
12414 * @extends Component
12415 */
12416class RemainingTimeDisplay extends TimeDisplay {
12417 /**
12418 * Creates an instance of this class.
12419 *
12420 * @param {Player} player
12421 * The `Player` that this class should be attached to.
12422 *
12423 * @param {Object} [options]
12424 * The key/value store of player options.
12425 */
12426 constructor(player, options) {
12427 super(player, options);
12428 this.on(player, 'durationchange', e => this.updateContent(e));
12429 }
12430
12431 /**
12432 * Builds the default DOM `className`.
12433 *
12434 * @return {string}
12435 * The DOM `className` for this object.
12436 */
12437 buildCSSClass() {
12438 return 'vjs-remaining-time';
12439 }
12440
12441 /**
12442 * Create the `Component`'s DOM element with the "minus" character prepend to the time
12443 *
12444 * @return {Element}
12445 * The element that was created.
12446 */
12447 createEl() {
12448 const el = super.createEl();
12449 if (this.options_.displayNegative !== false) {
12450 el.insertBefore(createEl('span', {}, {
12451 'aria-hidden': true
12452 }, '-'), this.contentEl_);
12453 }
12454 return el;
12455 }
12456
12457 /**
12458 * Update remaining time display.
12459 *
12460 * @param {Event} [event]
12461 * The `timeupdate` or `durationchange` event that caused this to run.
12462 *
12463 * @listens Player#timeupdate
12464 * @listens Player#durationchange
12465 */
12466 updateContent(event) {
12467 if (typeof this.player_.duration() !== 'number') {
12468 return;
12469 }
12470 let time;
12471
12472 // @deprecated We should only use remainingTimeDisplay
12473 // as of video.js 7
12474 if (this.player_.ended()) {
12475 time = 0;
12476 } else if (this.player_.remainingTimeDisplay) {
12477 time = this.player_.remainingTimeDisplay();
12478 } else {
12479 time = this.player_.remainingTime();
12480 }
12481 this.updateTextNode_(time);
12482 }
12483}
12484
12485/**
12486 * The text that is added to the `RemainingTimeDisplay` for screen reader users.
12487 *
12488 * @type {string}
12489 * @private
12490 */
12491RemainingTimeDisplay.prototype.labelText_ = 'Remaining Time';
12492
12493/**
12494 * The text that should display over the `RemainingTimeDisplay`s controls. Added to for localization.
12495 *
12496 * @type {string}
12497 * @protected
12498 *
12499 * @deprecated in v7; controlText_ is not used in non-active display Components
12500 */
12501RemainingTimeDisplay.prototype.controlText_ = 'Remaining Time';
12502Component.registerComponent('RemainingTimeDisplay', RemainingTimeDisplay);
12503
12504/**
12505 * @file live-display.js
12506 */
12507
12508/** @import Player from './player' */
12509
12510// TODO - Future make it click to snap to live
12511
12512/**
12513 * Displays the live indicator when duration is Infinity.
12514 *
12515 * @extends Component
12516 */
12517class LiveDisplay extends Component {
12518 /**
12519 * Creates an instance of this class.
12520 *
12521 * @param {Player} player
12522 * The `Player` that this class should be attached to.
12523 *
12524 * @param {Object} [options]
12525 * The key/value store of player options.
12526 */
12527 constructor(player, options) {
12528 super(player, options);
12529 this.updateShowing();
12530 this.on(this.player(), 'durationchange', e => this.updateShowing(e));
12531 }
12532
12533 /**
12534 * Create the `Component`'s DOM element
12535 *
12536 * @return {Element}
12537 * The element that was created.
12538 */
12539 createEl() {
12540 const el = super.createEl('div', {
12541 className: 'vjs-live-control vjs-control'
12542 });
12543 this.contentEl_ = createEl('div', {
12544 className: 'vjs-live-display'
12545 }, {
12546 'aria-live': 'off'
12547 });
12548 this.contentEl_.appendChild(createEl('span', {
12549 className: 'vjs-control-text',
12550 textContent: `${this.localize('Stream Type')}\u00a0`
12551 }));
12552 this.contentEl_.appendChild(document__default["default"].createTextNode(this.localize('LIVE')));
12553 el.appendChild(this.contentEl_);
12554 return el;
12555 }
12556 dispose() {
12557 this.contentEl_ = null;
12558 super.dispose();
12559 }
12560
12561 /**
12562 * Check the duration to see if the LiveDisplay should be showing or not. Then show/hide
12563 * it accordingly
12564 *
12565 * @param {Event} [event]
12566 * The {@link Player#durationchange} event that caused this function to run.
12567 *
12568 * @listens Player#durationchange
12569 */
12570 updateShowing(event) {
12571 if (this.player().duration() === Infinity) {
12572 this.show();
12573 } else {
12574 this.hide();
12575 }
12576 }
12577}
12578Component.registerComponent('LiveDisplay', LiveDisplay);
12579
12580/**
12581 * @file seek-to-live.js
12582 */
12583
12584/** @import Player from './player' */
12585
12586/**
12587 * Displays the live indicator when duration is Infinity.
12588 *
12589 * @extends Component
12590 */
12591class SeekToLive extends Button {
12592 /**
12593 * Creates an instance of this class.
12594 *
12595 * @param {Player} player
12596 * The `Player` that this class should be attached to.
12597 *
12598 * @param {Object} [options]
12599 * The key/value store of player options.
12600 */
12601 constructor(player, options) {
12602 super(player, options);
12603 this.updateLiveEdgeStatus();
12604 if (this.player_.liveTracker) {
12605 this.updateLiveEdgeStatusHandler_ = e => this.updateLiveEdgeStatus(e);
12606 this.on(this.player_.liveTracker, 'liveedgechange', this.updateLiveEdgeStatusHandler_);
12607 }
12608 }
12609
12610 /**
12611 * Create the `Component`'s DOM element
12612 *
12613 * @return {Element}
12614 * The element that was created.
12615 */
12616 createEl() {
12617 const el = super.createEl('button', {
12618 className: 'vjs-seek-to-live-control vjs-control'
12619 });
12620 this.setIcon('circle', el);
12621 this.textEl_ = createEl('span', {
12622 className: 'vjs-seek-to-live-text',
12623 textContent: this.localize('LIVE')
12624 }, {
12625 'aria-hidden': 'true'
12626 });
12627 el.appendChild(this.textEl_);
12628 return el;
12629 }
12630
12631 /**
12632 * Update the state of this button if we are at the live edge
12633 * or not
12634 */
12635 updateLiveEdgeStatus() {
12636 // default to live edge
12637 if (!this.player_.liveTracker || this.player_.liveTracker.atLiveEdge()) {
12638 this.setAttribute('aria-disabled', true);
12639 this.addClass('vjs-at-live-edge');
12640 this.controlText('Seek to live, currently playing live');
12641 } else {
12642 this.setAttribute('aria-disabled', false);
12643 this.removeClass('vjs-at-live-edge');
12644 this.controlText('Seek to live, currently behind live');
12645 }
12646 }
12647
12648 /**
12649 * On click bring us as near to the live point as possible.
12650 * This requires that we wait for the next `live-seekable-change`
12651 * event which will happen every segment length seconds.
12652 */
12653 handleClick() {
12654 this.player_.liveTracker.seekToLiveEdge();
12655 }
12656
12657 /**
12658 * Dispose of the element and stop tracking
12659 */
12660 dispose() {
12661 if (this.player_.liveTracker) {
12662 this.off(this.player_.liveTracker, 'liveedgechange', this.updateLiveEdgeStatusHandler_);
12663 }
12664 this.textEl_ = null;
12665 super.dispose();
12666 }
12667}
12668/**
12669 * The text that should display over the `SeekToLive`s control. Added for localization.
12670 *
12671 * @type {string}
12672 * @protected
12673 */
12674SeekToLive.prototype.controlText_ = 'Seek to live, currently playing live';
12675Component.registerComponent('SeekToLive', SeekToLive);
12676
12677/**
12678 * @file num.js
12679 * @module num
12680 */
12681
12682/**
12683 * Keep a number between a min and a max value
12684 *
12685 * @param {number} number
12686 * The number to clamp
12687 *
12688 * @param {number} min
12689 * The minimum value
12690 * @param {number} max
12691 * The maximum value
12692 *
12693 * @return {number}
12694 * the clamped number
12695 */
12696function clamp(number, min, max) {
12697 number = Number(number);
12698 return Math.min(max, Math.max(min, isNaN(number) ? min : number));
12699}
12700
12701var Num = /*#__PURE__*/Object.freeze({
12702 __proto__: null,
12703 clamp: clamp
12704});
12705
12706/**
12707 * @file slider.js
12708 */
12709
12710/** @import Player from '../player' */
12711
12712/**
12713 * The base functionality for a slider. Can be vertical or horizontal.
12714 * For instance the volume bar or the seek bar on a video is a slider.
12715 *
12716 * @extends Component
12717 */
12718class Slider extends Component {
12719 /**
12720 * Create an instance of this class
12721 *
12722 * @param {Player} player
12723 * The `Player` that this class should be attached to.
12724 *
12725 * @param {Object} [options]
12726 * The key/value store of player options.
12727 */
12728 constructor(player, options) {
12729 super(player, options);
12730 this.handleMouseDown_ = e => this.handleMouseDown(e);
12731 this.handleMouseUp_ = e => this.handleMouseUp(e);
12732 this.handleKeyDown_ = e => this.handleKeyDown(e);
12733 this.handleClick_ = e => this.handleClick(e);
12734 this.handleMouseMove_ = e => this.handleMouseMove(e);
12735 this.update_ = e => this.update(e);
12736
12737 // Set property names to bar to match with the child Slider class is looking for
12738 this.bar = this.getChild(this.options_.barName);
12739
12740 // Set a horizontal or vertical class on the slider depending on the slider type
12741 this.vertical(!!this.options_.vertical);
12742 this.enable();
12743 }
12744
12745 /**
12746 * Are controls are currently enabled for this slider or not.
12747 *
12748 * @return {boolean}
12749 * true if controls are enabled, false otherwise
12750 */
12751 enabled() {
12752 return this.enabled_;
12753 }
12754
12755 /**
12756 * Enable controls for this slider if they are disabled
12757 */
12758 enable() {
12759 if (this.enabled()) {
12760 return;
12761 }
12762 this.on('mousedown', this.handleMouseDown_);
12763 this.on('touchstart', this.handleMouseDown_);
12764 this.on('keydown', this.handleKeyDown_);
12765 this.on('click', this.handleClick_);
12766
12767 // TODO: deprecated, controlsvisible does not seem to be fired
12768 this.on(this.player_, 'controlsvisible', this.update);
12769 if (this.playerEvent) {
12770 this.on(this.player_, this.playerEvent, this.update);
12771 }
12772 this.removeClass('disabled');
12773 this.setAttribute('tabindex', 0);
12774 this.enabled_ = true;
12775 }
12776
12777 /**
12778 * Disable controls for this slider if they are enabled
12779 */
12780 disable() {
12781 if (!this.enabled()) {
12782 return;
12783 }
12784 const doc = this.bar.el_.ownerDocument;
12785 this.off('mousedown', this.handleMouseDown_);
12786 this.off('touchstart', this.handleMouseDown_);
12787 this.off('keydown', this.handleKeyDown_);
12788 this.off('click', this.handleClick_);
12789 this.off(this.player_, 'controlsvisible', this.update_);
12790 this.off(doc, 'mousemove', this.handleMouseMove_);
12791 this.off(doc, 'mouseup', this.handleMouseUp_);
12792 this.off(doc, 'touchmove', this.handleMouseMove_);
12793 this.off(doc, 'touchend', this.handleMouseUp_);
12794 this.removeAttribute('tabindex');
12795 this.addClass('disabled');
12796 if (this.playerEvent) {
12797 this.off(this.player_, this.playerEvent, this.update);
12798 }
12799 this.enabled_ = false;
12800 }
12801
12802 /**
12803 * Create the `Slider`s DOM element.
12804 *
12805 * @param {string} type
12806 * Type of element to create.
12807 *
12808 * @param {Object} [props={}]
12809 * List of properties in Object form.
12810 *
12811 * @param {Object} [attributes={}]
12812 * list of attributes in Object form.
12813 *
12814 * @return {Element}
12815 * The element that gets created.
12816 */
12817 createEl(type, props = {}, attributes = {}) {
12818 // Add the slider element class to all sub classes
12819 props.className = props.className + ' vjs-slider';
12820 props = Object.assign({
12821 tabIndex: 0
12822 }, props);
12823 attributes = Object.assign({
12824 'role': 'slider',
12825 'aria-valuenow': 0,
12826 'aria-valuemin': 0,
12827 'aria-valuemax': 100
12828 }, attributes);
12829 return super.createEl(type, props, attributes);
12830 }
12831
12832 /**
12833 * Handle `mousedown` or `touchstart` events on the `Slider`.
12834 *
12835 * @param {MouseEvent} event
12836 * `mousedown` or `touchstart` event that triggered this function
12837 *
12838 * @listens mousedown
12839 * @listens touchstart
12840 * @fires Slider#slideractive
12841 */
12842 handleMouseDown(event) {
12843 const doc = this.bar.el_.ownerDocument;
12844 if (event.type === 'mousedown') {
12845 event.preventDefault();
12846 }
12847 // Do not call preventDefault() on touchstart in Chrome
12848 // to avoid console warnings. Use a 'touch-action: none' style
12849 // instead to prevent unintended scrolling.
12850 // https://developers.google.com/web/updates/2017/01/scrolling-intervention
12851 if (event.type === 'touchstart' && !IS_CHROME) {
12852 event.preventDefault();
12853 }
12854 blockTextSelection();
12855 this.addClass('vjs-sliding');
12856 /**
12857 * Triggered when the slider is in an active state
12858 *
12859 * @event Slider#slideractive
12860 * @type {MouseEvent}
12861 */
12862 this.trigger('slideractive');
12863 this.on(doc, 'mousemove', this.handleMouseMove_);
12864 this.on(doc, 'mouseup', this.handleMouseUp_);
12865 this.on(doc, 'touchmove', this.handleMouseMove_);
12866 this.on(doc, 'touchend', this.handleMouseUp_);
12867 this.handleMouseMove(event, true);
12868 }
12869
12870 /**
12871 * Handle the `mousemove`, `touchmove`, and `mousedown` events on this `Slider`.
12872 * The `mousemove` and `touchmove` events will only only trigger this function during
12873 * `mousedown` and `touchstart`. This is due to {@link Slider#handleMouseDown} and
12874 * {@link Slider#handleMouseUp}.
12875 *
12876 * @param {MouseEvent} event
12877 * `mousedown`, `mousemove`, `touchstart`, or `touchmove` event that triggered
12878 * this function
12879 * @param {boolean} mouseDown this is a flag that should be set to true if `handleMouseMove` is called directly. It allows us to skip things that should not happen if coming from mouse down but should happen on regular mouse move handler. Defaults to false.
12880 *
12881 * @listens mousemove
12882 * @listens touchmove
12883 */
12884 handleMouseMove(event) {}
12885
12886 /**
12887 * Handle `mouseup` or `touchend` events on the `Slider`.
12888 *
12889 * @param {MouseEvent} event
12890 * `mouseup` or `touchend` event that triggered this function.
12891 *
12892 * @listens touchend
12893 * @listens mouseup
12894 * @fires Slider#sliderinactive
12895 */
12896 handleMouseUp(event) {
12897 const doc = this.bar.el_.ownerDocument;
12898 unblockTextSelection();
12899 this.removeClass('vjs-sliding');
12900 /**
12901 * Triggered when the slider is no longer in an active state.
12902 *
12903 * @event Slider#sliderinactive
12904 * @type {Event}
12905 */
12906 this.trigger('sliderinactive');
12907 this.off(doc, 'mousemove', this.handleMouseMove_);
12908 this.off(doc, 'mouseup', this.handleMouseUp_);
12909 this.off(doc, 'touchmove', this.handleMouseMove_);
12910 this.off(doc, 'touchend', this.handleMouseUp_);
12911 this.update();
12912 }
12913
12914 /**
12915 * Update the progress bar of the `Slider`.
12916 *
12917 * @return {number}
12918 * The percentage of progress the progress bar represents as a
12919 * number from 0 to 1.
12920 */
12921 update() {
12922 // In VolumeBar init we have a setTimeout for update that pops and update
12923 // to the end of the execution stack. The player is destroyed before then
12924 // update will cause an error
12925 // If there's no bar...
12926 if (!this.el_ || !this.bar) {
12927 return;
12928 }
12929
12930 // clamp progress between 0 and 1
12931 // and only round to four decimal places, as we round to two below
12932 const progress = this.getProgress();
12933 if (progress === this.progress_) {
12934 return progress;
12935 }
12936 this.progress_ = progress;
12937 this.requestNamedAnimationFrame('Slider#update', () => {
12938 // Set the new bar width or height
12939 const sizeKey = this.vertical() ? 'height' : 'width';
12940
12941 // Convert to a percentage for css value
12942 this.bar.el().style[sizeKey] = (progress * 100).toFixed(2) + '%';
12943 });
12944 return progress;
12945 }
12946
12947 /**
12948 * Get the percentage of the bar that should be filled
12949 * but clamped and rounded.
12950 *
12951 * @return {number}
12952 * percentage filled that the slider is
12953 */
12954 getProgress() {
12955 return Number(clamp(this.getPercent(), 0, 1).toFixed(4));
12956 }
12957
12958 /**
12959 * Calculate distance for slider
12960 *
12961 * @param {Event} event
12962 * The event that caused this function to run.
12963 *
12964 * @return {number}
12965 * The current position of the Slider.
12966 * - position.x for vertical `Slider`s
12967 * - position.y for horizontal `Slider`s
12968 */
12969 calculateDistance(event) {
12970 const position = getPointerPosition(this.el_, event);
12971 if (this.vertical()) {
12972 return position.y;
12973 }
12974 return position.x;
12975 }
12976
12977 /**
12978 * Handle a `keydown` event on the `Slider`. Watches for left, right, up, and down
12979 * arrow keys. This function will only be called when the slider has focus. See
12980 * {@link Slider#handleFocus} and {@link Slider#handleBlur}.
12981 *
12982 * @param {KeyboardEvent} event
12983 * the `keydown` event that caused this function to run.
12984 *
12985 * @listens keydown
12986 */
12987 handleKeyDown(event) {
12988 const spatialNavOptions = this.options_.playerOptions.spatialNavigation;
12989 const spatialNavEnabled = spatialNavOptions && spatialNavOptions.enabled;
12990 const horizontalSeek = spatialNavOptions && spatialNavOptions.horizontalSeek;
12991 if (spatialNavEnabled) {
12992 if (horizontalSeek && event.key === 'ArrowLeft' || !horizontalSeek && event.key === 'ArrowDown') {
12993 event.preventDefault();
12994 event.stopPropagation();
12995 this.stepBack();
12996 } else if (horizontalSeek && event.key === 'ArrowRight' || !horizontalSeek && event.key === 'ArrowUp') {
12997 event.preventDefault();
12998 event.stopPropagation();
12999 this.stepForward();
13000 } else {
13001 super.handleKeyDown(event);
13002 }
13003
13004 // Left and Down Arrows
13005 } else if (event.key === 'ArrowLeft' || event.key === 'ArrowDown') {
13006 event.preventDefault();
13007 event.stopPropagation();
13008 this.stepBack();
13009
13010 // Up and Right Arrows
13011 } else if (event.key === 'ArrowUp' || event.key === 'ArrowRight') {
13012 event.preventDefault();
13013 event.stopPropagation();
13014 this.stepForward();
13015 } else {
13016 // Pass keydown handling up for unsupported keys
13017 super.handleKeyDown(event);
13018 }
13019 }
13020
13021 /**
13022 * Listener for click events on slider, used to prevent clicks
13023 * from bubbling up to parent elements like button menus.
13024 *
13025 * @param {Object} event
13026 * Event that caused this object to run
13027 */
13028 handleClick(event) {
13029 event.stopPropagation();
13030 event.preventDefault();
13031 }
13032
13033 /**
13034 * Get/set if slider is horizontal for vertical
13035 *
13036 * @param {boolean} [bool]
13037 * - true if slider is vertical,
13038 * - false is horizontal
13039 *
13040 * @return {boolean}
13041 * - true if slider is vertical, and getting
13042 * - false if the slider is horizontal, and getting
13043 */
13044 vertical(bool) {
13045 if (bool === undefined) {
13046 return this.vertical_ || false;
13047 }
13048 this.vertical_ = !!bool;
13049 if (this.vertical_) {
13050 this.addClass('vjs-slider-vertical');
13051 } else {
13052 this.addClass('vjs-slider-horizontal');
13053 }
13054 }
13055}
13056Component.registerComponent('Slider', Slider);
13057
13058/**
13059 * @file load-progress-bar.js
13060 */
13061
13062/** @import Player from '../../player' */
13063
13064// get the percent width of a time compared to the total end
13065const percentify = (time, end) => clamp(time / end * 100, 0, 100).toFixed(2) + '%';
13066
13067/**
13068 * Shows loading progress
13069 *
13070 * @extends Component
13071 */
13072class LoadProgressBar extends Component {
13073 /**
13074 * Creates an instance of this class.
13075 *
13076 * @param {Player} player
13077 * The `Player` that this class should be attached to.
13078 *
13079 * @param {Object} [options]
13080 * The key/value store of player options.
13081 */
13082 constructor(player, options) {
13083 super(player, options);
13084 this.partEls_ = [];
13085 this.on(player, 'progress', e => this.update(e));
13086 }
13087
13088 /**
13089 * Create the `Component`'s DOM element
13090 *
13091 * @return {Element}
13092 * The element that was created.
13093 */
13094 createEl() {
13095 const el = super.createEl('div', {
13096 className: 'vjs-load-progress'
13097 });
13098 const wrapper = createEl('span', {
13099 className: 'vjs-control-text'
13100 });
13101 const loadedText = createEl('span', {
13102 textContent: this.localize('Loaded')
13103 });
13104 const separator = document__default["default"].createTextNode(': ');
13105 this.percentageEl_ = createEl('span', {
13106 className: 'vjs-control-text-loaded-percentage',
13107 textContent: '0%'
13108 });
13109 el.appendChild(wrapper);
13110 wrapper.appendChild(loadedText);
13111 wrapper.appendChild(separator);
13112 wrapper.appendChild(this.percentageEl_);
13113 return el;
13114 }
13115 dispose() {
13116 this.partEls_ = null;
13117 this.percentageEl_ = null;
13118 super.dispose();
13119 }
13120
13121 /**
13122 * Update progress bar
13123 *
13124 * @param {Event} [event]
13125 * The `progress` event that caused this function to run.
13126 *
13127 * @listens Player#progress
13128 */
13129 update(event) {
13130 this.requestNamedAnimationFrame('LoadProgressBar#update', () => {
13131 const liveTracker = this.player_.liveTracker;
13132 const buffered = this.player_.buffered();
13133 const duration = liveTracker && liveTracker.isLive() ? liveTracker.seekableEnd() : this.player_.duration();
13134 const bufferedEnd = this.player_.bufferedEnd();
13135 const children = this.partEls_;
13136 const percent = percentify(bufferedEnd, duration);
13137 if (this.percent_ !== percent) {
13138 // update the width of the progress bar
13139 this.el_.style.width = percent;
13140 // update the control-text
13141 textContent(this.percentageEl_, percent);
13142 this.percent_ = percent;
13143 }
13144
13145 // add child elements to represent the individual buffered time ranges
13146 for (let i = 0; i < buffered.length; i++) {
13147 const start = buffered.start(i);
13148 const end = buffered.end(i);
13149 let part = children[i];
13150 if (!part) {
13151 part = this.el_.appendChild(createEl());
13152 children[i] = part;
13153 }
13154
13155 // only update if changed
13156 if (part.dataset.start === start && part.dataset.end === end) {
13157 continue;
13158 }
13159 part.dataset.start = start;
13160 part.dataset.end = end;
13161
13162 // set the percent based on the width of the progress bar (bufferedEnd)
13163 part.style.left = percentify(start, bufferedEnd);
13164 part.style.width = percentify(end - start, bufferedEnd);
13165 }
13166
13167 // remove unused buffered range elements
13168 for (let i = children.length; i > buffered.length; i--) {
13169 this.el_.removeChild(children[i - 1]);
13170 }
13171 children.length = buffered.length;
13172 });
13173 }
13174}
13175Component.registerComponent('LoadProgressBar', LoadProgressBar);
13176
13177/**
13178 * @file time-tooltip.js
13179 */
13180
13181/** @import Player from '../../player' */
13182
13183/**
13184 * Time tooltips display a time above the progress bar.
13185 *
13186 * @extends Component
13187 */
13188class TimeTooltip extends Component {
13189 /**
13190 * Creates an instance of this class.
13191 *
13192 * @param {Player} player
13193 * The {@link Player} that this class should be attached to.
13194 *
13195 * @param {Object} [options]
13196 * The key/value store of player options.
13197 */
13198 constructor(player, options) {
13199 super(player, options);
13200 this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL);
13201 }
13202
13203 /**
13204 * Create the time tooltip DOM element
13205 *
13206 * @return {Element}
13207 * The element that was created.
13208 */
13209 createEl() {
13210 return super.createEl('div', {
13211 className: 'vjs-time-tooltip'
13212 }, {
13213 'aria-hidden': 'true'
13214 });
13215 }
13216
13217 /**
13218 * Updates the position of the time tooltip relative to the `SeekBar`.
13219 *
13220 * @param {Object} seekBarRect
13221 * The `ClientRect` for the {@link SeekBar} element.
13222 *
13223 * @param {number} seekBarPoint
13224 * A number from 0 to 1, representing a horizontal reference point
13225 * from the left edge of the {@link SeekBar}
13226 */
13227 update(seekBarRect, seekBarPoint, content) {
13228 const tooltipRect = findPosition(this.el_);
13229 const playerRect = getBoundingClientRect(this.player_.el());
13230 const seekBarPointPx = seekBarRect.width * seekBarPoint;
13231
13232 // do nothing if either rect isn't available
13233 // for example, if the player isn't in the DOM for testing
13234 if (!playerRect || !tooltipRect) {
13235 return;
13236 }
13237
13238 // This is the space left of the `seekBarPoint` available within the bounds
13239 // of the player. We calculate any gap between the left edge of the player
13240 // and the left edge of the `SeekBar` and add the number of pixels in the
13241 // `SeekBar` before hitting the `seekBarPoint`
13242 let spaceLeftOfPoint = seekBarRect.left - playerRect.left + seekBarPointPx;
13243
13244 // This is the space right of the `seekBarPoint` available within the bounds
13245 // of the player. We calculate the number of pixels from the `seekBarPoint`
13246 // to the right edge of the `SeekBar` and add to that any gap between the
13247 // right edge of the `SeekBar` and the player.
13248 let spaceRightOfPoint = seekBarRect.width - seekBarPointPx + (playerRect.right - seekBarRect.right);
13249
13250 // spaceRightOfPoint is always NaN for mouse time display
13251 // because the seekbarRect does not have a right property. This causes
13252 // the mouse tool tip to be truncated when it's close to the right edge of the player.
13253 // In such cases, we ignore the `playerRect.right - seekBarRect.right` value when calculating.
13254 // For the sake of consistency, we ignore seekBarRect.left - playerRect.left for the left edge.
13255 if (!spaceRightOfPoint) {
13256 spaceRightOfPoint = seekBarRect.width - seekBarPointPx;
13257 spaceLeftOfPoint = seekBarPointPx;
13258 }
13259 // This is the number of pixels by which the tooltip will need to be pulled
13260 // further to the right to center it over the `seekBarPoint`.
13261 let pullTooltipBy = tooltipRect.width / 2;
13262
13263 // Adjust the `pullTooltipBy` distance to the left or right depending on
13264 // the results of the space calculations above.
13265 if (spaceLeftOfPoint < pullTooltipBy) {
13266 pullTooltipBy += pullTooltipBy - spaceLeftOfPoint;
13267 } else if (spaceRightOfPoint < pullTooltipBy) {
13268 pullTooltipBy = spaceRightOfPoint;
13269 }
13270
13271 // Due to the imprecision of decimal/ratio based calculations and varying
13272 // rounding behaviors, there are cases where the spacing adjustment is off
13273 // by a pixel or two. This adds insurance to these calculations.
13274 if (pullTooltipBy < 0) {
13275 pullTooltipBy = 0;
13276 } else if (pullTooltipBy > tooltipRect.width) {
13277 pullTooltipBy = tooltipRect.width;
13278 }
13279
13280 // prevent small width fluctuations within 0.4px from
13281 // changing the value below.
13282 // This really helps for live to prevent the play
13283 // progress time tooltip from jittering
13284 pullTooltipBy = Math.round(pullTooltipBy);
13285 this.el_.style.right = `-${pullTooltipBy}px`;
13286 this.write(content);
13287 }
13288
13289 /**
13290 * Write the time to the tooltip DOM element.
13291 *
13292 * @param {string} content
13293 * The formatted time for the tooltip.
13294 */
13295 write(content) {
13296 textContent(this.el_, content);
13297 }
13298
13299 /**
13300 * Updates the position of the time tooltip relative to the `SeekBar`.
13301 *
13302 * @param {Object} seekBarRect
13303 * The `ClientRect` for the {@link SeekBar} element.
13304 *
13305 * @param {number} seekBarPoint
13306 * A number from 0 to 1, representing a horizontal reference point
13307 * from the left edge of the {@link SeekBar}
13308 *
13309 * @param {number} time
13310 * The time to update the tooltip to, not used during live playback
13311 *
13312 * @param {Function} cb
13313 * A function that will be called during the request animation frame
13314 * for tooltips that need to do additional animations from the default
13315 */
13316 updateTime(seekBarRect, seekBarPoint, time, cb) {
13317 this.requestNamedAnimationFrame('TimeTooltip#updateTime', () => {
13318 let content;
13319 const duration = this.player_.duration();
13320 if (this.player_.liveTracker && this.player_.liveTracker.isLive()) {
13321 const liveWindow = this.player_.liveTracker.liveWindow();
13322 const secondsBehind = liveWindow - seekBarPoint * liveWindow;
13323 content = (secondsBehind < 1 ? '' : '-') + formatTime(secondsBehind, liveWindow);
13324 } else {
13325 content = formatTime(time, duration);
13326 }
13327 this.update(seekBarRect, seekBarPoint, content);
13328 if (cb) {
13329 cb();
13330 }
13331 });
13332 }
13333}
13334Component.registerComponent('TimeTooltip', TimeTooltip);
13335
13336/**
13337 * @file play-progress-bar.js
13338 */
13339
13340/**
13341 * Used by {@link SeekBar} to display media playback progress as part of the
13342 * {@link ProgressControl}.
13343 *
13344 * @extends Component
13345 */
13346class PlayProgressBar extends Component {
13347 /**
13348 * Creates an instance of this class.
13349 *
13350 * @param {Player} player
13351 * The {@link Player} that this class should be attached to.
13352 *
13353 * @param {Object} [options]
13354 * The key/value store of player options.
13355 */
13356 constructor(player, options) {
13357 super(player, options);
13358 this.setIcon('circle');
13359 this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL);
13360 }
13361
13362 /**
13363 * Create the the DOM element for this class.
13364 *
13365 * @return {Element}
13366 * The element that was created.
13367 */
13368 createEl() {
13369 return super.createEl('div', {
13370 className: 'vjs-play-progress vjs-slider-bar'
13371 }, {
13372 'aria-hidden': 'true'
13373 });
13374 }
13375
13376 /**
13377 * Enqueues updates to its own DOM as well as the DOM of its
13378 * {@link TimeTooltip} child.
13379 *
13380 * @param {Object} seekBarRect
13381 * The `ClientRect` for the {@link SeekBar} element.
13382 *
13383 * @param {number} seekBarPoint
13384 * A number from 0 to 1, representing a horizontal reference point
13385 * from the left edge of the {@link SeekBar}
13386 */
13387 update(seekBarRect, seekBarPoint) {
13388 const timeTooltip = this.getChild('timeTooltip');
13389 if (!timeTooltip) {
13390 return;
13391 }
13392 const time = this.player_.scrubbing() ? this.player_.getCache().currentTime : this.player_.currentTime();
13393 timeTooltip.updateTime(seekBarRect, seekBarPoint, time);
13394 }
13395}
13396
13397/**
13398 * Default options for {@link PlayProgressBar}.
13399 *
13400 * @type {Object}
13401 * @private
13402 */
13403PlayProgressBar.prototype.options_ = {
13404 children: []
13405};
13406
13407// Time tooltips should not be added to a player on mobile devices
13408if (!IS_IOS && !IS_ANDROID) {
13409 PlayProgressBar.prototype.options_.children.push('timeTooltip');
13410}
13411Component.registerComponent('PlayProgressBar', PlayProgressBar);
13412
13413/**
13414 * @file mouse-time-display.js
13415 */
13416
13417/**
13418 * The {@link MouseTimeDisplay} component tracks mouse movement over the
13419 * {@link ProgressControl}. It displays an indicator and a {@link TimeTooltip}
13420 * indicating the time which is represented by a given point in the
13421 * {@link ProgressControl}.
13422 *
13423 * @extends Component
13424 */
13425class MouseTimeDisplay extends Component {
13426 /**
13427 * Creates an instance of this class.
13428 *
13429 * @param {Player} player
13430 * The {@link Player} that this class should be attached to.
13431 *
13432 * @param {Object} [options]
13433 * The key/value store of player options.
13434 */
13435 constructor(player, options) {
13436 super(player, options);
13437 this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL);
13438 }
13439
13440 /**
13441 * Create the DOM element for this class.
13442 *
13443 * @return {Element}
13444 * The element that was created.
13445 */
13446 createEl() {
13447 return super.createEl('div', {
13448 className: 'vjs-mouse-display'
13449 });
13450 }
13451
13452 /**
13453 * Enqueues updates to its own DOM as well as the DOM of its
13454 * {@link TimeTooltip} child.
13455 *
13456 * @param {Object} seekBarRect
13457 * The `ClientRect` for the {@link SeekBar} element.
13458 *
13459 * @param {number} seekBarPoint
13460 * A number from 0 to 1, representing a horizontal reference point
13461 * from the left edge of the {@link SeekBar}
13462 */
13463 update(seekBarRect, seekBarPoint) {
13464 const time = seekBarPoint * this.player_.duration();
13465 this.getChild('timeTooltip').updateTime(seekBarRect, seekBarPoint, time, () => {
13466 this.el_.style.left = `${seekBarRect.width * seekBarPoint}px`;
13467 });
13468 }
13469}
13470
13471/**
13472 * Default options for `MouseTimeDisplay`
13473 *
13474 * @type {Object}
13475 * @private
13476 */
13477MouseTimeDisplay.prototype.options_ = {
13478 children: ['timeTooltip']
13479};
13480Component.registerComponent('MouseTimeDisplay', MouseTimeDisplay);
13481
13482/**
13483 * @file seek-bar.js
13484 */
13485
13486// The number of seconds the `step*` functions move the timeline.
13487const STEP_SECONDS = 5;
13488
13489// The multiplier of STEP_SECONDS that PgUp/PgDown move the timeline.
13490const PAGE_KEY_MULTIPLIER = 12;
13491
13492/**
13493 * Seek bar and container for the progress bars. Uses {@link PlayProgressBar}
13494 * as its `bar`.
13495 *
13496 * @extends Slider
13497 */
13498class SeekBar extends Slider {
13499 /**
13500 * Creates an instance of this class.
13501 *
13502 * @param {Player} player
13503 * The `Player` that this class should be attached to.
13504 *
13505 * @param {Object} [options]
13506 * The key/value store of player options.
13507 */
13508 constructor(player, options) {
13509 options = merge(SeekBar.prototype.options_, options);
13510
13511 // Avoid mutating the prototype's `children` array by creating a copy
13512 options.children = [...options.children];
13513 const shouldDisableSeekWhileScrubbingOnMobile = player.options_.disableSeekWhileScrubbingOnMobile && (IS_IOS || IS_ANDROID);
13514
13515 // Add the TimeTooltip as a child if we are on desktop, or on mobile with `disableSeekWhileScrubbingOnMobile: true`
13516 if (!IS_IOS && !IS_ANDROID || shouldDisableSeekWhileScrubbingOnMobile) {
13517 options.children.splice(1, 0, 'mouseTimeDisplay');
13518 }
13519 super(player, options);
13520 this.shouldDisableSeekWhileScrubbingOnMobile_ = shouldDisableSeekWhileScrubbingOnMobile;
13521 this.pendingSeekTime_ = null;
13522 this.setEventHandlers_();
13523 }
13524
13525 /**
13526 * Sets the event handlers
13527 *
13528 * @private
13529 */
13530 setEventHandlers_() {
13531 this.update_ = bind_(this, this.update);
13532 this.update = throttle(this.update_, UPDATE_REFRESH_INTERVAL);
13533 this.on(this.player_, ['durationchange', 'timeupdate'], this.update);
13534 this.on(this.player_, ['ended'], this.update_);
13535 if (this.player_.liveTracker) {
13536 this.on(this.player_.liveTracker, 'liveedgechange', this.update);
13537 }
13538
13539 // when playing, let's ensure we smoothly update the play progress bar
13540 // via an interval
13541 this.updateInterval = null;
13542 this.enableIntervalHandler_ = e => this.enableInterval_(e);
13543 this.disableIntervalHandler_ = e => this.disableInterval_(e);
13544 this.on(this.player_, ['playing'], this.enableIntervalHandler_);
13545 this.on(this.player_, ['ended', 'pause', 'waiting'], this.disableIntervalHandler_);
13546
13547 // we don't need to update the play progress if the document is hidden,
13548 // also, this causes the CPU to spike and eventually crash the page on IE11.
13549 if ('hidden' in document__default["default"] && 'visibilityState' in document__default["default"]) {
13550 this.on(document__default["default"], 'visibilitychange', this.toggleVisibility_);
13551 }
13552 }
13553 toggleVisibility_(e) {
13554 if (document__default["default"].visibilityState === 'hidden') {
13555 this.cancelNamedAnimationFrame('SeekBar#update');
13556 this.cancelNamedAnimationFrame('Slider#update');
13557 this.disableInterval_(e);
13558 } else {
13559 if (!this.player_.ended() && !this.player_.paused()) {
13560 this.enableInterval_();
13561 }
13562
13563 // we just switched back to the page and someone may be looking, so, update ASAP
13564 this.update();
13565 }
13566 }
13567 enableInterval_() {
13568 if (this.updateInterval) {
13569 return;
13570 }
13571 this.updateInterval = this.setInterval(this.update, UPDATE_REFRESH_INTERVAL);
13572 }
13573 disableInterval_(e) {
13574 if (this.player_.liveTracker && this.player_.liveTracker.isLive() && e && e.type !== 'ended') {
13575 return;
13576 }
13577 if (!this.updateInterval) {
13578 return;
13579 }
13580 this.clearInterval(this.updateInterval);
13581 this.updateInterval = null;
13582 }
13583
13584 /**
13585 * Create the `Component`'s DOM element
13586 *
13587 * @return {Element}
13588 * The element that was created.
13589 */
13590 createEl() {
13591 return super.createEl('div', {
13592 className: 'vjs-progress-holder'
13593 }, {
13594 'aria-label': this.localize('Progress Bar')
13595 });
13596 }
13597
13598 /**
13599 * This function updates the play progress bar and accessibility
13600 * attributes to whatever is passed in.
13601 *
13602 * @param {Event} [event]
13603 * The `timeupdate` or `ended` event that caused this to run.
13604 *
13605 * @listens Player#timeupdate
13606 *
13607 * @return {number}
13608 * The current percent at a number from 0-1
13609 */
13610 update(event) {
13611 // ignore updates while the tab is hidden
13612 if (document__default["default"].visibilityState === 'hidden') {
13613 return;
13614 }
13615 const percent = super.update();
13616 this.requestNamedAnimationFrame('SeekBar#update', () => {
13617 const currentTime = this.player_.ended() ? this.player_.duration() : this.getCurrentTime_();
13618 const liveTracker = this.player_.liveTracker;
13619 let duration = this.player_.duration();
13620 if (liveTracker && liveTracker.isLive()) {
13621 duration = this.player_.liveTracker.liveCurrentTime();
13622 }
13623 if (this.percent_ !== percent) {
13624 // machine readable value of progress bar (percentage complete)
13625 this.el_.setAttribute('aria-valuenow', (percent * 100).toFixed(2));
13626 this.percent_ = percent;
13627 }
13628 if (this.currentTime_ !== currentTime || this.duration_ !== duration) {
13629 // human readable value of progress bar (time complete)
13630 this.el_.setAttribute('aria-valuetext', this.localize('progress bar timing: currentTime={1} duration={2}', [formatTime(currentTime, duration), formatTime(duration, duration)], '{1} of {2}'));
13631 this.currentTime_ = currentTime;
13632 this.duration_ = duration;
13633 }
13634
13635 // update the progress bar time tooltip with the current time
13636 if (this.bar) {
13637 this.bar.update(getBoundingClientRect(this.el()), this.getProgress());
13638 }
13639 });
13640 return percent;
13641 }
13642
13643 /**
13644 * Prevent liveThreshold from causing seeks to seem like they
13645 * are not happening from a user perspective.
13646 *
13647 * @param {number} ct
13648 * current time to seek to
13649 */
13650 userSeek_(ct) {
13651 if (this.player_.liveTracker && this.player_.liveTracker.isLive()) {
13652 this.player_.liveTracker.nextSeekedFromUser();
13653 }
13654 this.player_.currentTime(ct);
13655 }
13656
13657 /**
13658 * Get the value of current time but allows for smooth scrubbing,
13659 * when player can't keep up.
13660 *
13661 * @return {number}
13662 * The current time value to display
13663 *
13664 * @private
13665 */
13666 getCurrentTime_() {
13667 return this.player_.scrubbing() ? this.player_.getCache().currentTime : this.player_.currentTime();
13668 }
13669
13670 /**
13671 * Get the percentage of media played so far.
13672 *
13673 * @return {number}
13674 * The percentage of media played so far (0 to 1).
13675 */
13676 getPercent() {
13677 // If we have a pending seek time, we are scrubbing on mobile and should set the slider percent
13678 // to reflect the current scrub location.
13679 if (this.pendingSeekTime_) {
13680 return this.pendingSeekTime_ / this.player_.duration();
13681 }
13682 const currentTime = this.getCurrentTime_();
13683 let percent;
13684 const liveTracker = this.player_.liveTracker;
13685 if (liveTracker && liveTracker.isLive()) {
13686 percent = (currentTime - liveTracker.seekableStart()) / liveTracker.liveWindow();
13687
13688 // prevent the percent from changing at the live edge
13689 if (liveTracker.atLiveEdge()) {
13690 percent = 1;
13691 }
13692 } else {
13693 percent = currentTime / this.player_.duration();
13694 }
13695 return percent;
13696 }
13697
13698 /**
13699 * Handle mouse down on seek bar
13700 *
13701 * @param {MouseEvent} event
13702 * The `mousedown` event that caused this to run.
13703 *
13704 * @listens mousedown
13705 */
13706 handleMouseDown(event) {
13707 if (!isSingleLeftClick(event)) {
13708 return;
13709 }
13710
13711 // Stop event propagation to prevent double fire in progress-control.js
13712 event.stopPropagation();
13713 this.videoWasPlaying = !this.player_.paused();
13714
13715 // Don't pause if we are on mobile and `disableSeekWhileScrubbingOnMobile: true`.
13716 // In that case, playback should continue while the player scrubs to a new location.
13717 if (!this.shouldDisableSeekWhileScrubbingOnMobile_) {
13718 this.player_.pause();
13719 }
13720 super.handleMouseDown(event);
13721 }
13722
13723 /**
13724 * Handle mouse move on seek bar
13725 *
13726 * @param {MouseEvent} event
13727 * The `mousemove` event that caused this to run.
13728 * @param {boolean} mouseDown this is a flag that should be set to true if `handleMouseMove` is called directly. It allows us to skip things that should not happen if coming from mouse down but should happen on regular mouse move handler. Defaults to false
13729 *
13730 * @listens mousemove
13731 */
13732 handleMouseMove(event, mouseDown = false) {
13733 if (!isSingleLeftClick(event) || isNaN(this.player_.duration())) {
13734 return;
13735 }
13736 if (!mouseDown && !this.player_.scrubbing()) {
13737 this.player_.scrubbing(true);
13738 }
13739 let newTime;
13740 const distance = this.calculateDistance(event);
13741 const liveTracker = this.player_.liveTracker;
13742 if (!liveTracker || !liveTracker.isLive()) {
13743 newTime = distance * this.player_.duration();
13744
13745 // Don't let video end while scrubbing.
13746 if (newTime === this.player_.duration()) {
13747 newTime = newTime - 0.1;
13748 }
13749 } else {
13750 if (distance >= 0.99) {
13751 liveTracker.seekToLiveEdge();
13752 return;
13753 }
13754 const seekableStart = liveTracker.seekableStart();
13755 const seekableEnd = liveTracker.liveCurrentTime();
13756 newTime = seekableStart + distance * liveTracker.liveWindow();
13757
13758 // Don't let video end while scrubbing.
13759 if (newTime >= seekableEnd) {
13760 newTime = seekableEnd;
13761 }
13762
13763 // Compensate for precision differences so that currentTime is not less
13764 // than seekable start
13765 if (newTime <= seekableStart) {
13766 newTime = seekableStart + 0.1;
13767 }
13768
13769 // On android seekableEnd can be Infinity sometimes,
13770 // this will cause newTime to be Infinity, which is
13771 // not a valid currentTime.
13772 if (newTime === Infinity) {
13773 return;
13774 }
13775 }
13776
13777 // if on mobile and `disableSeekWhileScrubbingOnMobile: true`, keep track of the desired seek point but we won't initiate the seek until 'touchend'
13778 if (this.shouldDisableSeekWhileScrubbingOnMobile_) {
13779 this.pendingSeekTime_ = newTime;
13780 } else {
13781 this.userSeek_(newTime);
13782 }
13783 if (this.player_.options_.enableSmoothSeeking) {
13784 this.update();
13785 }
13786 }
13787 enable() {
13788 super.enable();
13789 const mouseTimeDisplay = this.getChild('mouseTimeDisplay');
13790 if (!mouseTimeDisplay) {
13791 return;
13792 }
13793 mouseTimeDisplay.show();
13794 }
13795 disable() {
13796 super.disable();
13797 const mouseTimeDisplay = this.getChild('mouseTimeDisplay');
13798 if (!mouseTimeDisplay) {
13799 return;
13800 }
13801 mouseTimeDisplay.hide();
13802 }
13803
13804 /**
13805 * Handle mouse up on seek bar
13806 *
13807 * @param {MouseEvent} event
13808 * The `mouseup` event that caused this to run.
13809 *
13810 * @listens mouseup
13811 */
13812 handleMouseUp(event) {
13813 super.handleMouseUp(event);
13814
13815 // Stop event propagation to prevent double fire in progress-control.js
13816 if (event) {
13817 event.stopPropagation();
13818 }
13819 this.player_.scrubbing(false);
13820
13821 // If we have a pending seek time, then we have finished scrubbing on mobile and should initiate a seek.
13822 if (this.pendingSeekTime_) {
13823 this.userSeek_(this.pendingSeekTime_);
13824 this.pendingSeekTime_ = null;
13825 }
13826
13827 /**
13828 * Trigger timeupdate because we're done seeking and the time has changed.
13829 * This is particularly useful for if the player is paused to time the time displays.
13830 *
13831 * @event Tech#timeupdate
13832 * @type {Event}
13833 */
13834 this.player_.trigger({
13835 type: 'timeupdate',
13836 target: this,
13837 manuallyTriggered: true
13838 });
13839 if (this.videoWasPlaying) {
13840 silencePromise(this.player_.play());
13841 } else {
13842 // We're done seeking and the time has changed.
13843 // If the player is paused, make sure we display the correct time on the seek bar.
13844 this.update_();
13845 }
13846 }
13847
13848 /**
13849 * Move more quickly fast forward for keyboard-only users
13850 */
13851 stepForward() {
13852 this.userSeek_(this.player_.currentTime() + STEP_SECONDS);
13853 }
13854
13855 /**
13856 * Move more quickly rewind for keyboard-only users
13857 */
13858 stepBack() {
13859 this.userSeek_(this.player_.currentTime() - STEP_SECONDS);
13860 }
13861
13862 /**
13863 * Toggles the playback state of the player
13864 * This gets called when enter or space is used on the seekbar
13865 *
13866 * @param {KeyboardEvent} event
13867 * The `keydown` event that caused this function to be called
13868 *
13869 */
13870 handleAction(event) {
13871 if (this.player_.paused()) {
13872 this.player_.play();
13873 } else {
13874 this.player_.pause();
13875 }
13876 }
13877
13878 /**
13879 * Called when this SeekBar has focus and a key gets pressed down.
13880 * Supports the following keys:
13881 *
13882 * Space or Enter key fire a click event
13883 * Home key moves to start of the timeline
13884 * End key moves to end of the timeline
13885 * Digit "0" through "9" keys move to 0%, 10% ... 80%, 90% of the timeline
13886 * PageDown key moves back a larger step than ArrowDown
13887 * PageUp key moves forward a large step
13888 *
13889 * @param {KeyboardEvent} event
13890 * The `keydown` event that caused this function to be called.
13891 *
13892 * @listens keydown
13893 */
13894 handleKeyDown(event) {
13895 const liveTracker = this.player_.liveTracker;
13896 if (event.key === ' ' || event.key === 'Enter') {
13897 event.preventDefault();
13898 event.stopPropagation();
13899 this.handleAction(event);
13900 } else if (event.key === 'Home') {
13901 event.preventDefault();
13902 event.stopPropagation();
13903 this.userSeek_(0);
13904 } else if (event.key === 'End') {
13905 event.preventDefault();
13906 event.stopPropagation();
13907 if (liveTracker && liveTracker.isLive()) {
13908 this.userSeek_(liveTracker.liveCurrentTime());
13909 } else {
13910 this.userSeek_(this.player_.duration());
13911 }
13912 } else if (/^[0-9]$/.test(event.key)) {
13913 event.preventDefault();
13914 event.stopPropagation();
13915 const gotoFraction = parseInt(event.key, 10) * 0.1;
13916 if (liveTracker && liveTracker.isLive()) {
13917 this.userSeek_(liveTracker.seekableStart() + liveTracker.liveWindow() * gotoFraction);
13918 } else {
13919 this.userSeek_(this.player_.duration() * gotoFraction);
13920 }
13921 } else if (event.key === 'PageDown') {
13922 event.preventDefault();
13923 event.stopPropagation();
13924 this.userSeek_(this.player_.currentTime() - STEP_SECONDS * PAGE_KEY_MULTIPLIER);
13925 } else if (event.key === 'PageUp') {
13926 event.preventDefault();
13927 event.stopPropagation();
13928 this.userSeek_(this.player_.currentTime() + STEP_SECONDS * PAGE_KEY_MULTIPLIER);
13929 } else {
13930 // Pass keydown handling up for unsupported keys
13931 super.handleKeyDown(event);
13932 }
13933 }
13934 dispose() {
13935 this.disableInterval_();
13936 this.off(this.player_, ['durationchange', 'timeupdate'], this.update);
13937 this.off(this.player_, ['ended'], this.update_);
13938 if (this.player_.liveTracker) {
13939 this.off(this.player_.liveTracker, 'liveedgechange', this.update);
13940 }
13941 this.off(this.player_, ['playing'], this.enableIntervalHandler_);
13942 this.off(this.player_, ['ended', 'pause', 'waiting'], this.disableIntervalHandler_);
13943
13944 // we don't need to update the play progress if the document is hidden,
13945 // also, this causes the CPU to spike and eventually crash the page on IE11.
13946 if ('hidden' in document__default["default"] && 'visibilityState' in document__default["default"]) {
13947 this.off(document__default["default"], 'visibilitychange', this.toggleVisibility_);
13948 }
13949 super.dispose();
13950 }
13951}
13952
13953/**
13954 * Default options for the `SeekBar`
13955 *
13956 * @type {Object}
13957 * @private
13958 */
13959SeekBar.prototype.options_ = {
13960 children: ['loadProgressBar', 'playProgressBar'],
13961 barName: 'playProgressBar'
13962};
13963Component.registerComponent('SeekBar', SeekBar);
13964
13965/**
13966 * @file progress-control.js
13967 */
13968
13969/**
13970 * The Progress Control component contains the seek bar, load progress,
13971 * and play progress.
13972 *
13973 * @extends Component
13974 */
13975class ProgressControl extends Component {
13976 /**
13977 * Creates an instance of this class.
13978 *
13979 * @param {Player} player
13980 * The `Player` that this class should be attached to.
13981 *
13982 * @param {Object} [options]
13983 * The key/value store of player options.
13984 */
13985 constructor(player, options) {
13986 super(player, options);
13987 this.handleMouseMove = throttle(bind_(this, this.handleMouseMove), UPDATE_REFRESH_INTERVAL);
13988 this.throttledHandleMouseSeek = throttle(bind_(this, this.handleMouseSeek), UPDATE_REFRESH_INTERVAL);
13989 this.handleMouseUpHandler_ = e => this.handleMouseUp(e);
13990 this.handleMouseDownHandler_ = e => this.handleMouseDown(e);
13991 this.enable();
13992 }
13993
13994 /**
13995 * Create the `Component`'s DOM element
13996 *
13997 * @return {Element}
13998 * The element that was created.
13999 */
14000 createEl() {
14001 return super.createEl('div', {
14002 className: 'vjs-progress-control vjs-control'
14003 });
14004 }
14005
14006 /**
14007 * When the mouse moves over the `ProgressControl`, the pointer position
14008 * gets passed down to the `MouseTimeDisplay` component.
14009 *
14010 * @param {Event} event
14011 * The `mousemove` event that caused this function to run.
14012 *
14013 * @listen mousemove
14014 */
14015 handleMouseMove(event) {
14016 const seekBar = this.getChild('seekBar');
14017 if (!seekBar) {
14018 return;
14019 }
14020 const playProgressBar = seekBar.getChild('playProgressBar');
14021 const mouseTimeDisplay = seekBar.getChild('mouseTimeDisplay');
14022 if (!playProgressBar && !mouseTimeDisplay) {
14023 return;
14024 }
14025 const seekBarEl = seekBar.el();
14026 const seekBarRect = findPosition(seekBarEl);
14027 let seekBarPoint = getPointerPosition(seekBarEl, event).x;
14028
14029 // The default skin has a gap on either side of the `SeekBar`. This means
14030 // that it's possible to trigger this behavior outside the boundaries of
14031 // the `SeekBar`. This ensures we stay within it at all times.
14032 seekBarPoint = clamp(seekBarPoint, 0, 1);
14033 if (mouseTimeDisplay) {
14034 mouseTimeDisplay.update(seekBarRect, seekBarPoint);
14035 }
14036 if (playProgressBar) {
14037 playProgressBar.update(seekBarRect, seekBar.getProgress());
14038 }
14039 }
14040
14041 /**
14042 * A throttled version of the {@link ProgressControl#handleMouseSeek} listener.
14043 *
14044 * @method ProgressControl#throttledHandleMouseSeek
14045 * @param {Event} event
14046 * The `mousemove` event that caused this function to run.
14047 *
14048 * @listen mousemove
14049 * @listen touchmove
14050 */
14051
14052 /**
14053 * Handle `mousemove` or `touchmove` events on the `ProgressControl`.
14054 *
14055 * @param {Event} event
14056 * `mousedown` or `touchstart` event that triggered this function
14057 *
14058 * @listens mousemove
14059 * @listens touchmove
14060 */
14061 handleMouseSeek(event) {
14062 const seekBar = this.getChild('seekBar');
14063 if (seekBar) {
14064 seekBar.handleMouseMove(event);
14065 }
14066 }
14067
14068 /**
14069 * Are controls are currently enabled for this progress control.
14070 *
14071 * @return {boolean}
14072 * true if controls are enabled, false otherwise
14073 */
14074 enabled() {
14075 return this.enabled_;
14076 }
14077
14078 /**
14079 * Disable all controls on the progress control and its children
14080 */
14081 disable() {
14082 this.children().forEach(child => child.disable && child.disable());
14083 if (!this.enabled()) {
14084 return;
14085 }
14086 this.off(['mousedown', 'touchstart'], this.handleMouseDownHandler_);
14087 this.off(this.el_, ['mousemove', 'touchmove'], this.handleMouseMove);
14088 this.removeListenersAddedOnMousedownAndTouchstart();
14089 this.addClass('disabled');
14090 this.enabled_ = false;
14091
14092 // Restore normal playback state if controls are disabled while scrubbing
14093 if (this.player_.scrubbing()) {
14094 const seekBar = this.getChild('seekBar');
14095 this.player_.scrubbing(false);
14096 if (seekBar.videoWasPlaying) {
14097 silencePromise(this.player_.play());
14098 }
14099 }
14100 }
14101
14102 /**
14103 * Enable all controls on the progress control and its children
14104 */
14105 enable() {
14106 this.children().forEach(child => child.enable && child.enable());
14107 if (this.enabled()) {
14108 return;
14109 }
14110 this.on(['mousedown', 'touchstart'], this.handleMouseDownHandler_);
14111 this.on(this.el_, ['mousemove', 'touchmove'], this.handleMouseMove);
14112 this.removeClass('disabled');
14113 this.enabled_ = true;
14114 }
14115
14116 /**
14117 * Cleanup listeners after the user finishes interacting with the progress controls
14118 */
14119 removeListenersAddedOnMousedownAndTouchstart() {
14120 const doc = this.el_.ownerDocument;
14121 this.off(doc, 'mousemove', this.throttledHandleMouseSeek);
14122 this.off(doc, 'touchmove', this.throttledHandleMouseSeek);
14123 this.off(doc, 'mouseup', this.handleMouseUpHandler_);
14124 this.off(doc, 'touchend', this.handleMouseUpHandler_);
14125 }
14126
14127 /**
14128 * Handle `mousedown` or `touchstart` events on the `ProgressControl`.
14129 *
14130 * @param {Event} event
14131 * `mousedown` or `touchstart` event that triggered this function
14132 *
14133 * @listens mousedown
14134 * @listens touchstart
14135 */
14136 handleMouseDown(event) {
14137 const doc = this.el_.ownerDocument;
14138 const seekBar = this.getChild('seekBar');
14139 if (seekBar) {
14140 seekBar.handleMouseDown(event);
14141 }
14142 this.on(doc, 'mousemove', this.throttledHandleMouseSeek);
14143 this.on(doc, 'touchmove', this.throttledHandleMouseSeek);
14144 this.on(doc, 'mouseup', this.handleMouseUpHandler_);
14145 this.on(doc, 'touchend', this.handleMouseUpHandler_);
14146 }
14147
14148 /**
14149 * Handle `mouseup` or `touchend` events on the `ProgressControl`.
14150 *
14151 * @param {Event} event
14152 * `mouseup` or `touchend` event that triggered this function.
14153 *
14154 * @listens touchend
14155 * @listens mouseup
14156 */
14157 handleMouseUp(event) {
14158 const seekBar = this.getChild('seekBar');
14159 if (seekBar) {
14160 seekBar.handleMouseUp(event);
14161 }
14162 this.removeListenersAddedOnMousedownAndTouchstart();
14163 }
14164}
14165
14166/**
14167 * Default options for `ProgressControl`
14168 *
14169 * @type {Object}
14170 * @private
14171 */
14172ProgressControl.prototype.options_ = {
14173 children: ['seekBar']
14174};
14175Component.registerComponent('ProgressControl', ProgressControl);
14176
14177/**
14178 * @file picture-in-picture-toggle.js
14179 */
14180
14181/** @import Player from './player' */
14182
14183/**
14184 * Toggle Picture-in-Picture mode
14185 *
14186 * @extends Button
14187 */
14188class PictureInPictureToggle extends Button {
14189 /**
14190 * Creates an instance of this class.
14191 *
14192 * @param {Player} player
14193 * The `Player` that this class should be attached to.
14194 *
14195 * @param {Object} [options]
14196 * The key/value store of player options.
14197 *
14198 * @listens Player#enterpictureinpicture
14199 * @listens Player#leavepictureinpicture
14200 */
14201 constructor(player, options) {
14202 super(player, options);
14203 this.setIcon('picture-in-picture-enter');
14204 this.on(player, ['enterpictureinpicture', 'leavepictureinpicture'], e => this.handlePictureInPictureChange(e));
14205 this.on(player, ['disablepictureinpicturechanged', 'loadedmetadata'], e => this.handlePictureInPictureEnabledChange(e));
14206 this.on(player, ['loadedmetadata', 'audioonlymodechange', 'audiopostermodechange'], () => this.handlePictureInPictureAudioModeChange());
14207
14208 // TODO: Deactivate button on player emptied event.
14209 this.disable();
14210 }
14211
14212 /**
14213 * Builds the default DOM `className`.
14214 *
14215 * @return {string}
14216 * The DOM `className` for this object.
14217 */
14218 buildCSSClass() {
14219 return `vjs-picture-in-picture-control vjs-hidden ${super.buildCSSClass()}`;
14220 }
14221
14222 /**
14223 * Displays or hides the button depending on the audio mode detection.
14224 * Exits picture-in-picture if it is enabled when switching to audio mode.
14225 */
14226 handlePictureInPictureAudioModeChange() {
14227 // This audio detection will not detect HLS or DASH audio-only streams because there was no reliable way to detect them at the time
14228 const isSourceAudio = this.player_.currentType().substring(0, 5) === 'audio';
14229 const isAudioMode = isSourceAudio || this.player_.audioPosterMode() || this.player_.audioOnlyMode();
14230 if (!isAudioMode) {
14231 this.show();
14232 return;
14233 }
14234 if (this.player_.isInPictureInPicture()) {
14235 this.player_.exitPictureInPicture();
14236 }
14237 this.hide();
14238 }
14239
14240 /**
14241 * Enables or disables button based on availability of a Picture-In-Picture mode.
14242 *
14243 * Enabled if
14244 * - `player.options().enableDocumentPictureInPicture` is true and
14245 * window.documentPictureInPicture is available; or
14246 * - `player.disablePictureInPicture()` is false and
14247 * element.requestPictureInPicture is available
14248 */
14249 handlePictureInPictureEnabledChange() {
14250 if (document__default["default"].pictureInPictureEnabled && this.player_.disablePictureInPicture() === false || this.player_.options_.enableDocumentPictureInPicture && 'documentPictureInPicture' in window__default["default"]) {
14251 this.enable();
14252 } else {
14253 this.disable();
14254 }
14255 }
14256
14257 /**
14258 * Handles enterpictureinpicture and leavepictureinpicture on the player and change control text accordingly.
14259 *
14260 * @param {Event} [event]
14261 * The {@link Player#enterpictureinpicture} or {@link Player#leavepictureinpicture} event that caused this function to be
14262 * called.
14263 *
14264 * @listens Player#enterpictureinpicture
14265 * @listens Player#leavepictureinpicture
14266 */
14267 handlePictureInPictureChange(event) {
14268 if (this.player_.isInPictureInPicture()) {
14269 this.setIcon('picture-in-picture-exit');
14270 this.controlText('Exit Picture-in-Picture');
14271 } else {
14272 this.setIcon('picture-in-picture-enter');
14273 this.controlText('Picture-in-Picture');
14274 }
14275 this.handlePictureInPictureEnabledChange();
14276 }
14277
14278 /**
14279 * This gets called when an `PictureInPictureToggle` is "clicked". See
14280 * {@link ClickableComponent} for more detailed information on what a click can be.
14281 *
14282 * @param {Event} [event]
14283 * The `keydown`, `tap`, or `click` event that caused this function to be
14284 * called.
14285 *
14286 * @listens tap
14287 * @listens click
14288 */
14289 handleClick(event) {
14290 if (!this.player_.isInPictureInPicture()) {
14291 this.player_.requestPictureInPicture();
14292 } else {
14293 this.player_.exitPictureInPicture();
14294 }
14295 }
14296
14297 /**
14298 * Show the `Component`s element if it is hidden by removing the
14299 * 'vjs-hidden' class name from it only in browsers that support the Picture-in-Picture API.
14300 */
14301 show() {
14302 // Does not allow to display the pictureInPictureToggle in browsers that do not support the Picture-in-Picture API, e.g. Firefox.
14303 if (typeof document__default["default"].exitPictureInPicture !== 'function') {
14304 return;
14305 }
14306 super.show();
14307 }
14308}
14309
14310/**
14311 * The text that should display over the `PictureInPictureToggle`s controls. Added for localization.
14312 *
14313 * @type {string}
14314 * @protected
14315 */
14316PictureInPictureToggle.prototype.controlText_ = 'Picture-in-Picture';
14317Component.registerComponent('PictureInPictureToggle', PictureInPictureToggle);
14318
14319/**
14320 * @file fullscreen-toggle.js
14321 */
14322
14323/** @import Player from './player' */
14324
14325/**
14326 * Toggle fullscreen video
14327 *
14328 * @extends Button
14329 */
14330class FullscreenToggle extends Button {
14331 /**
14332 * Creates an instance of this class.
14333 *
14334 * @param {Player} player
14335 * The `Player` that this class should be attached to.
14336 *
14337 * @param {Object} [options]
14338 * The key/value store of player options.
14339 */
14340 constructor(player, options) {
14341 super(player, options);
14342 this.setIcon('fullscreen-enter');
14343 this.on(player, 'fullscreenchange', e => this.handleFullscreenChange(e));
14344 if (document__default["default"][player.fsApi_.fullscreenEnabled] === false) {
14345 this.disable();
14346 }
14347 }
14348
14349 /**
14350 * Builds the default DOM `className`.
14351 *
14352 * @return {string}
14353 * The DOM `className` for this object.
14354 */
14355 buildCSSClass() {
14356 return `vjs-fullscreen-control ${super.buildCSSClass()}`;
14357 }
14358
14359 /**
14360 * Handles fullscreenchange on the player and change control text accordingly.
14361 *
14362 * @param {Event} [event]
14363 * The {@link Player#fullscreenchange} event that caused this function to be
14364 * called.
14365 *
14366 * @listens Player#fullscreenchange
14367 */
14368 handleFullscreenChange(event) {
14369 if (this.player_.isFullscreen()) {
14370 this.controlText('Exit Fullscreen');
14371 this.setIcon('fullscreen-exit');
14372 } else {
14373 this.controlText('Fullscreen');
14374 this.setIcon('fullscreen-enter');
14375 }
14376 }
14377
14378 /**
14379 * This gets called when an `FullscreenToggle` is "clicked". See
14380 * {@link ClickableComponent} for more detailed information on what a click can be.
14381 *
14382 * @param {Event} [event]
14383 * The `keydown`, `tap`, or `click` event that caused this function to be
14384 * called.
14385 *
14386 * @listens tap
14387 * @listens click
14388 */
14389 handleClick(event) {
14390 if (!this.player_.isFullscreen()) {
14391 this.player_.requestFullscreen();
14392 } else {
14393 this.player_.exitFullscreen();
14394 }
14395 }
14396}
14397
14398/**
14399 * The text that should display over the `FullscreenToggle`s controls. Added for localization.
14400 *
14401 * @type {string}
14402 * @protected
14403 */
14404FullscreenToggle.prototype.controlText_ = 'Fullscreen';
14405Component.registerComponent('FullscreenToggle', FullscreenToggle);
14406
14407/** @import Component from '../../component' */
14408/** @import Player from '../../player' */
14409
14410/**
14411 * Check if volume control is supported and if it isn't hide the
14412 * `Component` that was passed using the `vjs-hidden` class.
14413 *
14414 * @param {Component} self
14415 * The component that should be hidden if volume is unsupported
14416 *
14417 * @param {Player} player
14418 * A reference to the player
14419 *
14420 * @private
14421 */
14422const checkVolumeSupport = function (self, player) {
14423 // hide volume controls when they're not supported by the current tech
14424 if (player.tech_ && !player.tech_.featuresVolumeControl) {
14425 self.addClass('vjs-hidden');
14426 }
14427 self.on(player, 'loadstart', function () {
14428 if (!player.tech_.featuresVolumeControl) {
14429 self.addClass('vjs-hidden');
14430 } else {
14431 self.removeClass('vjs-hidden');
14432 }
14433 });
14434};
14435
14436/**
14437 * @file volume-level.js
14438 */
14439
14440/**
14441 * Shows volume level
14442 *
14443 * @extends Component
14444 */
14445class VolumeLevel extends Component {
14446 /**
14447 * Create the `Component`'s DOM element
14448 *
14449 * @return {Element}
14450 * The element that was created.
14451 */
14452 createEl() {
14453 const el = super.createEl('div', {
14454 className: 'vjs-volume-level'
14455 });
14456 this.setIcon('circle', el);
14457 el.appendChild(super.createEl('span', {
14458 className: 'vjs-control-text'
14459 }));
14460 return el;
14461 }
14462}
14463Component.registerComponent('VolumeLevel', VolumeLevel);
14464
14465/**
14466 * @file volume-level-tooltip.js
14467 */
14468
14469/** @import Player from '../../player' */
14470
14471/**
14472 * Volume level tooltips display a volume above or side by side the volume bar.
14473 *
14474 * @extends Component
14475 */
14476class VolumeLevelTooltip extends Component {
14477 /**
14478 * Creates an instance of this class.
14479 *
14480 * @param {Player} player
14481 * The {@link Player} that this class should be attached to.
14482 *
14483 * @param {Object} [options]
14484 * The key/value store of player options.
14485 */
14486 constructor(player, options) {
14487 super(player, options);
14488 this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL);
14489 }
14490
14491 /**
14492 * Create the volume tooltip DOM element
14493 *
14494 * @return {Element}
14495 * The element that was created.
14496 */
14497 createEl() {
14498 return super.createEl('div', {
14499 className: 'vjs-volume-tooltip'
14500 }, {
14501 'aria-hidden': 'true'
14502 });
14503 }
14504
14505 /**
14506 * Updates the position of the tooltip relative to the `VolumeBar` and
14507 * its content text.
14508 *
14509 * @param {Object} rangeBarRect
14510 * The `ClientRect` for the {@link VolumeBar} element.
14511 *
14512 * @param {number} rangeBarPoint
14513 * A number from 0 to 1, representing a horizontal/vertical reference point
14514 * from the left edge of the {@link VolumeBar}
14515 *
14516 * @param {boolean} vertical
14517 * Referees to the Volume control position
14518 * in the control bar{@link VolumeControl}
14519 *
14520 */
14521 update(rangeBarRect, rangeBarPoint, vertical, content) {
14522 if (!vertical) {
14523 const tooltipRect = getBoundingClientRect(this.el_);
14524 const playerRect = getBoundingClientRect(this.player_.el());
14525 const volumeBarPointPx = rangeBarRect.width * rangeBarPoint;
14526 if (!playerRect || !tooltipRect) {
14527 return;
14528 }
14529 const spaceLeftOfPoint = rangeBarRect.left - playerRect.left + volumeBarPointPx;
14530 const spaceRightOfPoint = rangeBarRect.width - volumeBarPointPx + (playerRect.right - rangeBarRect.right);
14531 let pullTooltipBy = tooltipRect.width / 2;
14532 if (spaceLeftOfPoint < pullTooltipBy) {
14533 pullTooltipBy += pullTooltipBy - spaceLeftOfPoint;
14534 } else if (spaceRightOfPoint < pullTooltipBy) {
14535 pullTooltipBy = spaceRightOfPoint;
14536 }
14537 if (pullTooltipBy < 0) {
14538 pullTooltipBy = 0;
14539 } else if (pullTooltipBy > tooltipRect.width) {
14540 pullTooltipBy = tooltipRect.width;
14541 }
14542 this.el_.style.right = `-${pullTooltipBy}px`;
14543 }
14544 this.write(`${content}%`);
14545 }
14546
14547 /**
14548 * Write the volume to the tooltip DOM element.
14549 *
14550 * @param {string} content
14551 * The formatted volume for the tooltip.
14552 */
14553 write(content) {
14554 textContent(this.el_, content);
14555 }
14556
14557 /**
14558 * Updates the position of the volume tooltip relative to the `VolumeBar`.
14559 *
14560 * @param {Object} rangeBarRect
14561 * The `ClientRect` for the {@link VolumeBar} element.
14562 *
14563 * @param {number} rangeBarPoint
14564 * A number from 0 to 1, representing a horizontal/vertical reference point
14565 * from the left edge of the {@link VolumeBar}
14566 *
14567 * @param {boolean} vertical
14568 * Referees to the Volume control position
14569 * in the control bar{@link VolumeControl}
14570 *
14571 * @param {number} volume
14572 * The volume level to update the tooltip to
14573 *
14574 * @param {Function} cb
14575 * A function that will be called during the request animation frame
14576 * for tooltips that need to do additional animations from the default
14577 */
14578 updateVolume(rangeBarRect, rangeBarPoint, vertical, volume, cb) {
14579 this.requestNamedAnimationFrame('VolumeLevelTooltip#updateVolume', () => {
14580 this.update(rangeBarRect, rangeBarPoint, vertical, volume.toFixed(0));
14581 if (cb) {
14582 cb();
14583 }
14584 });
14585 }
14586}
14587Component.registerComponent('VolumeLevelTooltip', VolumeLevelTooltip);
14588
14589/**
14590 * @file mouse-volume-level-display.js
14591 */
14592
14593/**
14594 * The {@link MouseVolumeLevelDisplay} component tracks mouse movement over the
14595 * {@link VolumeControl}. It displays an indicator and a {@link VolumeLevelTooltip}
14596 * indicating the volume level which is represented by a given point in the
14597 * {@link VolumeBar}.
14598 *
14599 * @extends Component
14600 */
14601class MouseVolumeLevelDisplay extends Component {
14602 /**
14603 * Creates an instance of this class.
14604 *
14605 * @param {Player} player
14606 * The {@link Player} that this class should be attached to.
14607 *
14608 * @param {Object} [options]
14609 * The key/value store of player options.
14610 */
14611 constructor(player, options) {
14612 super(player, options);
14613 this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL);
14614 }
14615
14616 /**
14617 * Create the DOM element for this class.
14618 *
14619 * @return {Element}
14620 * The element that was created.
14621 */
14622 createEl() {
14623 return super.createEl('div', {
14624 className: 'vjs-mouse-display'
14625 });
14626 }
14627
14628 /**
14629 * Enquires updates to its own DOM as well as the DOM of its
14630 * {@link VolumeLevelTooltip} child.
14631 *
14632 * @param {Object} rangeBarRect
14633 * The `ClientRect` for the {@link VolumeBar} element.
14634 *
14635 * @param {number} rangeBarPoint
14636 * A number from 0 to 1, representing a horizontal/vertical reference point
14637 * from the left edge of the {@link VolumeBar}
14638 *
14639 * @param {boolean} vertical
14640 * Referees to the Volume control position
14641 * in the control bar{@link VolumeControl}
14642 *
14643 */
14644 update(rangeBarRect, rangeBarPoint, vertical) {
14645 const volume = 100 * rangeBarPoint;
14646 this.getChild('volumeLevelTooltip').updateVolume(rangeBarRect, rangeBarPoint, vertical, volume, () => {
14647 if (vertical) {
14648 this.el_.style.bottom = `${rangeBarRect.height * rangeBarPoint}px`;
14649 } else {
14650 this.el_.style.left = `${rangeBarRect.width * rangeBarPoint}px`;
14651 }
14652 });
14653 }
14654}
14655
14656/**
14657 * Default options for `MouseVolumeLevelDisplay`
14658 *
14659 * @type {Object}
14660 * @private
14661 */
14662MouseVolumeLevelDisplay.prototype.options_ = {
14663 children: ['volumeLevelTooltip']
14664};
14665Component.registerComponent('MouseVolumeLevelDisplay', MouseVolumeLevelDisplay);
14666
14667/**
14668 * @file volume-bar.js
14669 */
14670
14671/**
14672 * The bar that contains the volume level and can be clicked on to adjust the level
14673 *
14674 * @extends Slider
14675 */
14676class VolumeBar extends Slider {
14677 /**
14678 * Creates an instance of this class.
14679 *
14680 * @param {Player} player
14681 * The `Player` that this class should be attached to.
14682 *
14683 * @param {Object} [options]
14684 * The key/value store of player options.
14685 */
14686 constructor(player, options) {
14687 super(player, options);
14688 this.on('slideractive', e => this.updateLastVolume_(e));
14689 this.on(player, 'volumechange', e => this.updateARIAAttributes(e));
14690 player.ready(() => this.updateARIAAttributes());
14691 }
14692
14693 /**
14694 * Create the `Component`'s DOM element
14695 *
14696 * @return {Element}
14697 * The element that was created.
14698 */
14699 createEl() {
14700 return super.createEl('div', {
14701 className: 'vjs-volume-bar vjs-slider-bar'
14702 }, {
14703 'aria-label': this.localize('Volume Level'),
14704 'aria-live': 'polite'
14705 });
14706 }
14707
14708 /**
14709 * Handle mouse down on volume bar
14710 *
14711 * @param {Event} event
14712 * The `mousedown` event that caused this to run.
14713 *
14714 * @listens mousedown
14715 */
14716 handleMouseDown(event) {
14717 if (!isSingleLeftClick(event)) {
14718 return;
14719 }
14720 super.handleMouseDown(event);
14721 }
14722
14723 /**
14724 * Handle movement events on the {@link VolumeMenuButton}.
14725 *
14726 * @param {Event} event
14727 * The event that caused this function to run.
14728 *
14729 * @listens mousemove
14730 */
14731 handleMouseMove(event) {
14732 const mouseVolumeLevelDisplay = this.getChild('mouseVolumeLevelDisplay');
14733 if (mouseVolumeLevelDisplay) {
14734 const volumeBarEl = this.el();
14735 const volumeBarRect = getBoundingClientRect(volumeBarEl);
14736 const vertical = this.vertical();
14737 let volumeBarPoint = getPointerPosition(volumeBarEl, event);
14738 volumeBarPoint = vertical ? volumeBarPoint.y : volumeBarPoint.x;
14739 // The default skin has a gap on either side of the `VolumeBar`. This means
14740 // that it's possible to trigger this behavior outside the boundaries of
14741 // the `VolumeBar`. This ensures we stay within it at all times.
14742 volumeBarPoint = clamp(volumeBarPoint, 0, 1);
14743 mouseVolumeLevelDisplay.update(volumeBarRect, volumeBarPoint, vertical);
14744 }
14745 if (!isSingleLeftClick(event)) {
14746 return;
14747 }
14748 this.checkMuted();
14749 this.player_.volume(this.calculateDistance(event));
14750 }
14751
14752 /**
14753 * If the player is muted unmute it.
14754 */
14755 checkMuted() {
14756 if (this.player_.muted()) {
14757 this.player_.muted(false);
14758 }
14759 }
14760
14761 /**
14762 * Get percent of volume level
14763 *
14764 * @return {number}
14765 * Volume level percent as a decimal number.
14766 */
14767 getPercent() {
14768 if (this.player_.muted()) {
14769 return 0;
14770 }
14771 return this.player_.volume();
14772 }
14773
14774 /**
14775 * Increase volume level for keyboard users
14776 */
14777 stepForward() {
14778 this.checkMuted();
14779 this.player_.volume(this.player_.volume() + 0.1);
14780 }
14781
14782 /**
14783 * Decrease volume level for keyboard users
14784 */
14785 stepBack() {
14786 this.checkMuted();
14787 this.player_.volume(this.player_.volume() - 0.1);
14788 }
14789
14790 /**
14791 * Update ARIA accessibility attributes
14792 *
14793 * @param {Event} [event]
14794 * The `volumechange` event that caused this function to run.
14795 *
14796 * @listens Player#volumechange
14797 */
14798 updateARIAAttributes(event) {
14799 const ariaValue = this.player_.muted() ? 0 : this.volumeAsPercentage_();
14800 this.el_.setAttribute('aria-valuenow', ariaValue);
14801 this.el_.setAttribute('aria-valuetext', ariaValue + '%');
14802 }
14803
14804 /**
14805 * Returns the current value of the player volume as a percentage
14806 *
14807 * @private
14808 */
14809 volumeAsPercentage_() {
14810 return Math.round(this.player_.volume() * 100);
14811 }
14812
14813 /**
14814 * When user starts dragging the VolumeBar, store the volume and listen for
14815 * the end of the drag. When the drag ends, if the volume was set to zero,
14816 * set lastVolume to the stored volume.
14817 *
14818 * @listens slideractive
14819 * @private
14820 */
14821 updateLastVolume_() {
14822 const volumeBeforeDrag = this.player_.volume();
14823 this.one('sliderinactive', () => {
14824 if (this.player_.volume() === 0) {
14825 this.player_.lastVolume_(volumeBeforeDrag);
14826 }
14827 });
14828 }
14829}
14830
14831/**
14832 * Default options for the `VolumeBar`
14833 *
14834 * @type {Object}
14835 * @private
14836 */
14837VolumeBar.prototype.options_ = {
14838 children: ['volumeLevel'],
14839 barName: 'volumeLevel'
14840};
14841
14842// MouseVolumeLevelDisplay tooltip should not be added to a player on mobile devices
14843if (!IS_IOS && !IS_ANDROID) {
14844 VolumeBar.prototype.options_.children.splice(0, 0, 'mouseVolumeLevelDisplay');
14845}
14846
14847/**
14848 * Call the update event for this Slider when this event happens on the player.
14849 *
14850 * @type {string}
14851 */
14852VolumeBar.prototype.playerEvent = 'volumechange';
14853Component.registerComponent('VolumeBar', VolumeBar);
14854
14855/**
14856 * @file volume-control.js
14857 */
14858
14859/**
14860 * The component for controlling the volume level
14861 *
14862 * @extends Component
14863 */
14864class VolumeControl extends Component {
14865 /**
14866 * Creates an instance of this class.
14867 *
14868 * @param {Player} player
14869 * The `Player` that this class should be attached to.
14870 *
14871 * @param {Object} [options={}]
14872 * The key/value store of player options.
14873 */
14874 constructor(player, options = {}) {
14875 options.vertical = options.vertical || false;
14876
14877 // Pass the vertical option down to the VolumeBar if
14878 // the VolumeBar is turned on.
14879 if (typeof options.volumeBar === 'undefined' || isPlain(options.volumeBar)) {
14880 options.volumeBar = options.volumeBar || {};
14881 options.volumeBar.vertical = options.vertical;
14882 }
14883 super(player, options);
14884
14885 // hide this control if volume support is missing
14886 checkVolumeSupport(this, player);
14887 this.throttledHandleMouseMove = throttle(bind_(this, this.handleMouseMove), UPDATE_REFRESH_INTERVAL);
14888 this.handleMouseUpHandler_ = e => this.handleMouseUp(e);
14889 this.on('mousedown', e => this.handleMouseDown(e));
14890 this.on('touchstart', e => this.handleMouseDown(e));
14891 this.on('mousemove', e => this.handleMouseMove(e));
14892
14893 // while the slider is active (the mouse has been pressed down and
14894 // is dragging) or in focus we do not want to hide the VolumeBar
14895 this.on(this.volumeBar, ['focus', 'slideractive'], () => {
14896 this.volumeBar.addClass('vjs-slider-active');
14897 this.addClass('vjs-slider-active');
14898 this.trigger('slideractive');
14899 });
14900 this.on(this.volumeBar, ['blur', 'sliderinactive'], () => {
14901 this.volumeBar.removeClass('vjs-slider-active');
14902 this.removeClass('vjs-slider-active');
14903 this.trigger('sliderinactive');
14904 });
14905 }
14906
14907 /**
14908 * Create the `Component`'s DOM element
14909 *
14910 * @return {Element}
14911 * The element that was created.
14912 */
14913 createEl() {
14914 let orientationClass = 'vjs-volume-horizontal';
14915 if (this.options_.vertical) {
14916 orientationClass = 'vjs-volume-vertical';
14917 }
14918 return super.createEl('div', {
14919 className: `vjs-volume-control vjs-control ${orientationClass}`
14920 });
14921 }
14922
14923 /**
14924 * Handle `mousedown` or `touchstart` events on the `VolumeControl`.
14925 *
14926 * @param {Event} event
14927 * `mousedown` or `touchstart` event that triggered this function
14928 *
14929 * @listens mousedown
14930 * @listens touchstart
14931 */
14932 handleMouseDown(event) {
14933 const doc = this.el_.ownerDocument;
14934 this.on(doc, 'mousemove', this.throttledHandleMouseMove);
14935 this.on(doc, 'touchmove', this.throttledHandleMouseMove);
14936 this.on(doc, 'mouseup', this.handleMouseUpHandler_);
14937 this.on(doc, 'touchend', this.handleMouseUpHandler_);
14938 }
14939
14940 /**
14941 * Handle `mouseup` or `touchend` events on the `VolumeControl`.
14942 *
14943 * @param {Event} event
14944 * `mouseup` or `touchend` event that triggered this function.
14945 *
14946 * @listens touchend
14947 * @listens mouseup
14948 */
14949 handleMouseUp(event) {
14950 const doc = this.el_.ownerDocument;
14951 this.off(doc, 'mousemove', this.throttledHandleMouseMove);
14952 this.off(doc, 'touchmove', this.throttledHandleMouseMove);
14953 this.off(doc, 'mouseup', this.handleMouseUpHandler_);
14954 this.off(doc, 'touchend', this.handleMouseUpHandler_);
14955 }
14956
14957 /**
14958 * Handle `mousedown` or `touchstart` events on the `VolumeControl`.
14959 *
14960 * @param {Event} event
14961 * `mousedown` or `touchstart` event that triggered this function
14962 *
14963 * @listens mousedown
14964 * @listens touchstart
14965 */
14966 handleMouseMove(event) {
14967 this.volumeBar.handleMouseMove(event);
14968 }
14969}
14970
14971/**
14972 * Default options for the `VolumeControl`
14973 *
14974 * @type {Object}
14975 * @private
14976 */
14977VolumeControl.prototype.options_ = {
14978 children: ['volumeBar']
14979};
14980Component.registerComponent('VolumeControl', VolumeControl);
14981
14982/** @import Component from '../../component' */
14983/** @import Player from '../../player' */
14984
14985/**
14986 * Check if muting volume is supported and if it isn't hide the mute toggle
14987 * button.
14988 *
14989 * @param {Component} self
14990 * A reference to the mute toggle button
14991 *
14992 * @param {Player} player
14993 * A reference to the player
14994 *
14995 * @private
14996 */
14997const checkMuteSupport = function (self, player) {
14998 // hide mute toggle button if it's not supported by the current tech
14999 if (player.tech_ && !player.tech_.featuresMuteControl) {
15000 self.addClass('vjs-hidden');
15001 }
15002 self.on(player, 'loadstart', function () {
15003 if (!player.tech_.featuresMuteControl) {
15004 self.addClass('vjs-hidden');
15005 } else {
15006 self.removeClass('vjs-hidden');
15007 }
15008 });
15009};
15010
15011/**
15012 * @file mute-toggle.js
15013 */
15014
15015/** @import Player from './player' */
15016
15017/**
15018 * A button component for muting the audio.
15019 *
15020 * @extends Button
15021 */
15022class MuteToggle extends Button {
15023 /**
15024 * Creates an instance of this class.
15025 *
15026 * @param {Player} player
15027 * The `Player` that this class should be attached to.
15028 *
15029 * @param {Object} [options]
15030 * The key/value store of player options.
15031 */
15032 constructor(player, options) {
15033 super(player, options);
15034
15035 // hide this control if volume support is missing
15036 checkMuteSupport(this, player);
15037 this.on(player, ['loadstart', 'volumechange'], e => this.update(e));
15038 }
15039
15040 /**
15041 * Builds the default DOM `className`.
15042 *
15043 * @return {string}
15044 * The DOM `className` for this object.
15045 */
15046 buildCSSClass() {
15047 return `vjs-mute-control ${super.buildCSSClass()}`;
15048 }
15049
15050 /**
15051 * This gets called when an `MuteToggle` is "clicked". See
15052 * {@link ClickableComponent} for more detailed information on what a click can be.
15053 *
15054 * @param {Event} [event]
15055 * The `keydown`, `tap`, or `click` event that caused this function to be
15056 * called.
15057 *
15058 * @listens tap
15059 * @listens click
15060 */
15061 handleClick(event) {
15062 const vol = this.player_.volume();
15063 const lastVolume = this.player_.lastVolume_();
15064 if (vol === 0) {
15065 const volumeToSet = lastVolume < 0.1 ? 0.1 : lastVolume;
15066 this.player_.volume(volumeToSet);
15067 this.player_.muted(false);
15068 } else {
15069 this.player_.muted(this.player_.muted() ? false : true);
15070 }
15071 }
15072
15073 /**
15074 * Update the `MuteToggle` button based on the state of `volume` and `muted`
15075 * on the player.
15076 *
15077 * @param {Event} [event]
15078 * The {@link Player#loadstart} event if this function was called
15079 * through an event.
15080 *
15081 * @listens Player#loadstart
15082 * @listens Player#volumechange
15083 */
15084 update(event) {
15085 this.updateIcon_();
15086 this.updateControlText_();
15087 }
15088
15089 /**
15090 * Update the appearance of the `MuteToggle` icon.
15091 *
15092 * Possible states (given `level` variable below):
15093 * - 0: crossed out
15094 * - 1: zero bars of volume
15095 * - 2: one bar of volume
15096 * - 3: two bars of volume
15097 *
15098 * @private
15099 */
15100 updateIcon_() {
15101 const vol = this.player_.volume();
15102 let level = 3;
15103 this.setIcon('volume-high');
15104
15105 // in iOS when a player is loaded with muted attribute
15106 // and volume is changed with a native mute button
15107 // we want to make sure muted state is updated
15108 if (IS_IOS && this.player_.tech_ && this.player_.tech_.el_) {
15109 this.player_.muted(this.player_.tech_.el_.muted);
15110 }
15111 if (vol === 0 || this.player_.muted()) {
15112 this.setIcon('volume-mute');
15113 level = 0;
15114 } else if (vol < 0.33) {
15115 this.setIcon('volume-low');
15116 level = 1;
15117 } else if (vol < 0.67) {
15118 this.setIcon('volume-medium');
15119 level = 2;
15120 }
15121 removeClass(this.el_, [0, 1, 2, 3].reduce((str, i) => str + `${i ? ' ' : ''}vjs-vol-${i}`, ''));
15122 addClass(this.el_, `vjs-vol-${level}`);
15123 }
15124
15125 /**
15126 * If `muted` has changed on the player, update the control text
15127 * (`title` attribute on `vjs-mute-control` element and content of
15128 * `vjs-control-text` element).
15129 *
15130 * @private
15131 */
15132 updateControlText_() {
15133 const soundOff = this.player_.muted() || this.player_.volume() === 0;
15134 const text = soundOff ? 'Unmute' : 'Mute';
15135 if (this.controlText() !== text) {
15136 this.controlText(text);
15137 }
15138 }
15139}
15140
15141/**
15142 * The text that should display over the `MuteToggle`s controls. Added for localization.
15143 *
15144 * @type {string}
15145 * @protected
15146 */
15147MuteToggle.prototype.controlText_ = 'Mute';
15148Component.registerComponent('MuteToggle', MuteToggle);
15149
15150/**
15151 * @file volume-control.js
15152 */
15153
15154/**
15155 * A Component to contain the MuteToggle and VolumeControl so that
15156 * they can work together.
15157 *
15158 * @extends Component
15159 */
15160class VolumePanel extends Component {
15161 /**
15162 * Creates an instance of this class.
15163 *
15164 * @param {Player} player
15165 * The `Player` that this class should be attached to.
15166 *
15167 * @param {Object} [options={}]
15168 * The key/value store of player options.
15169 */
15170 constructor(player, options = {}) {
15171 if (typeof options.inline !== 'undefined') {
15172 options.inline = options.inline;
15173 } else {
15174 options.inline = true;
15175 }
15176
15177 // pass the inline option down to the VolumeControl as vertical if
15178 // the VolumeControl is on.
15179 if (typeof options.volumeControl === 'undefined' || isPlain(options.volumeControl)) {
15180 options.volumeControl = options.volumeControl || {};
15181 options.volumeControl.vertical = !options.inline;
15182 }
15183 super(player, options);
15184
15185 // this handler is used by mouse handler methods below
15186 this.handleKeyPressHandler_ = e => this.handleKeyPress(e);
15187 this.on(player, ['loadstart'], e => this.volumePanelState_(e));
15188 this.on(this.muteToggle, 'keyup', e => this.handleKeyPress(e));
15189 this.on(this.volumeControl, 'keyup', e => this.handleVolumeControlKeyUp(e));
15190 this.on('keydown', e => this.handleKeyPress(e));
15191 this.on('mouseover', e => this.handleMouseOver(e));
15192 this.on('mouseout', e => this.handleMouseOut(e));
15193
15194 // while the slider is active (the mouse has been pressed down and
15195 // is dragging) we do not want to hide the VolumeBar
15196 this.on(this.volumeControl, ['slideractive'], this.sliderActive_);
15197 this.on(this.volumeControl, ['sliderinactive'], this.sliderInactive_);
15198 }
15199
15200 /**
15201 * Add vjs-slider-active class to the VolumePanel
15202 *
15203 * @listens VolumeControl#slideractive
15204 * @private
15205 */
15206 sliderActive_() {
15207 this.addClass('vjs-slider-active');
15208 }
15209
15210 /**
15211 * Removes vjs-slider-active class to the VolumePanel
15212 *
15213 * @listens VolumeControl#sliderinactive
15214 * @private
15215 */
15216 sliderInactive_() {
15217 this.removeClass('vjs-slider-active');
15218 }
15219
15220 /**
15221 * Adds vjs-hidden or vjs-mute-toggle-only to the VolumePanel
15222 * depending on MuteToggle and VolumeControl state
15223 *
15224 * @listens Player#loadstart
15225 * @private
15226 */
15227 volumePanelState_() {
15228 // hide volume panel if neither volume control or mute toggle
15229 // are displayed
15230 if (this.volumeControl.hasClass('vjs-hidden') && this.muteToggle.hasClass('vjs-hidden')) {
15231 this.addClass('vjs-hidden');
15232 }
15233
15234 // if only mute toggle is visible we don't want
15235 // volume panel expanding when hovered or active
15236 if (this.volumeControl.hasClass('vjs-hidden') && !this.muteToggle.hasClass('vjs-hidden')) {
15237 this.addClass('vjs-mute-toggle-only');
15238 }
15239 }
15240
15241 /**
15242 * Create the `Component`'s DOM element
15243 *
15244 * @return {Element}
15245 * The element that was created.
15246 */
15247 createEl() {
15248 let orientationClass = 'vjs-volume-panel-horizontal';
15249 if (!this.options_.inline) {
15250 orientationClass = 'vjs-volume-panel-vertical';
15251 }
15252 return super.createEl('div', {
15253 className: `vjs-volume-panel vjs-control ${orientationClass}`
15254 });
15255 }
15256
15257 /**
15258 * Dispose of the `volume-panel` and all child components.
15259 */
15260 dispose() {
15261 this.handleMouseOut();
15262 super.dispose();
15263 }
15264
15265 /**
15266 * Handles `keyup` events on the `VolumeControl`, looking for ESC, which closes
15267 * the volume panel and sets focus on `MuteToggle`.
15268 *
15269 * @param {Event} event
15270 * The `keyup` event that caused this function to be called.
15271 *
15272 * @listens keyup
15273 */
15274 handleVolumeControlKeyUp(event) {
15275 if (event.key === 'Escape') {
15276 this.muteToggle.focus();
15277 }
15278 }
15279
15280 /**
15281 * This gets called when a `VolumePanel` gains hover via a `mouseover` event.
15282 * Turns on listening for `mouseover` event. When they happen it
15283 * calls `this.handleMouseOver`.
15284 *
15285 * @param {Event} event
15286 * The `mouseover` event that caused this function to be called.
15287 *
15288 * @listens mouseover
15289 */
15290 handleMouseOver(event) {
15291 this.addClass('vjs-hover');
15292 on(document__default["default"], 'keyup', this.handleKeyPressHandler_);
15293 }
15294
15295 /**
15296 * This gets called when a `VolumePanel` gains hover via a `mouseout` event.
15297 * Turns on listening for `mouseout` event. When they happen it
15298 * calls `this.handleMouseOut`.
15299 *
15300 * @param {Event} event
15301 * The `mouseout` event that caused this function to be called.
15302 *
15303 * @listens mouseout
15304 */
15305 handleMouseOut(event) {
15306 this.removeClass('vjs-hover');
15307 off(document__default["default"], 'keyup', this.handleKeyPressHandler_);
15308 }
15309
15310 /**
15311 * Handles `keyup` event on the document or `keydown` event on the `VolumePanel`,
15312 * looking for ESC, which hides the `VolumeControl`.
15313 *
15314 * @param {Event} event
15315 * The keypress that triggered this event.
15316 *
15317 * @listens keydown | keyup
15318 */
15319 handleKeyPress(event) {
15320 if (event.key === 'Escape') {
15321 this.handleMouseOut();
15322 }
15323 }
15324}
15325
15326/**
15327 * Default options for the `VolumeControl`
15328 *
15329 * @type {Object}
15330 * @private
15331 */
15332VolumePanel.prototype.options_ = {
15333 children: ['muteToggle', 'volumeControl']
15334};
15335Component.registerComponent('VolumePanel', VolumePanel);
15336
15337/**
15338 * Button to skip forward a configurable amount of time
15339 * through a video. Renders in the control bar.
15340 *
15341 * e.g. options: {controlBar: {skipButtons: forward: 5}}
15342 *
15343 * @extends Button
15344 */
15345class SkipForward extends Button {
15346 constructor(player, options) {
15347 super(player, options);
15348 this.validOptions = [5, 10, 30];
15349 this.skipTime = this.getSkipForwardTime();
15350 if (this.skipTime && this.validOptions.includes(this.skipTime)) {
15351 this.setIcon(`forward-${this.skipTime}`);
15352 this.controlText(this.localize('Skip forward {1} seconds', [this.skipTime.toLocaleString(player.language())]));
15353 this.show();
15354 } else {
15355 this.hide();
15356 }
15357 }
15358 getSkipForwardTime() {
15359 const playerOptions = this.options_.playerOptions;
15360 return playerOptions.controlBar && playerOptions.controlBar.skipButtons && playerOptions.controlBar.skipButtons.forward;
15361 }
15362 buildCSSClass() {
15363 return `vjs-skip-forward-${this.getSkipForwardTime()} ${super.buildCSSClass()}`;
15364 }
15365
15366 /**
15367 * On click, skips forward in the duration/seekable range by a configurable amount of seconds.
15368 * If the time left in the duration/seekable range is less than the configured 'skip forward' time,
15369 * skips to end of duration/seekable range.
15370 *
15371 * Handle a click on a `SkipForward` button
15372 *
15373 * @param {EventTarget~Event} event
15374 * The `click` event that caused this function
15375 * to be called
15376 */
15377 handleClick(event) {
15378 if (isNaN(this.player_.duration())) {
15379 return;
15380 }
15381 const currentVideoTime = this.player_.currentTime();
15382 const liveTracker = this.player_.liveTracker;
15383 const duration = liveTracker && liveTracker.isLive() ? liveTracker.seekableEnd() : this.player_.duration();
15384 let newTime;
15385 if (currentVideoTime + this.skipTime <= duration) {
15386 newTime = currentVideoTime + this.skipTime;
15387 } else {
15388 newTime = duration;
15389 }
15390 this.player_.currentTime(newTime);
15391 }
15392
15393 /**
15394 * Update control text on languagechange
15395 */
15396 handleLanguagechange() {
15397 this.controlText(this.localize('Skip forward {1} seconds', [this.skipTime]));
15398 }
15399}
15400SkipForward.prototype.controlText_ = 'Skip Forward';
15401Component.registerComponent('SkipForward', SkipForward);
15402
15403/**
15404 * Button to skip backward a configurable amount of time
15405 * through a video. Renders in the control bar.
15406 *
15407 * * e.g. options: {controlBar: {skipButtons: backward: 5}}
15408 *
15409 * @extends Button
15410 */
15411class SkipBackward extends Button {
15412 constructor(player, options) {
15413 super(player, options);
15414 this.validOptions = [5, 10, 30];
15415 this.skipTime = this.getSkipBackwardTime();
15416 if (this.skipTime && this.validOptions.includes(this.skipTime)) {
15417 this.setIcon(`replay-${this.skipTime}`);
15418 this.controlText(this.localize('Skip backward {1} seconds', [this.skipTime.toLocaleString(player.language())]));
15419 this.show();
15420 } else {
15421 this.hide();
15422 }
15423 }
15424 getSkipBackwardTime() {
15425 const playerOptions = this.options_.playerOptions;
15426 return playerOptions.controlBar && playerOptions.controlBar.skipButtons && playerOptions.controlBar.skipButtons.backward;
15427 }
15428 buildCSSClass() {
15429 return `vjs-skip-backward-${this.getSkipBackwardTime()} ${super.buildCSSClass()}`;
15430 }
15431
15432 /**
15433 * On click, skips backward in the video by a configurable amount of seconds.
15434 * If the current time in the video is less than the configured 'skip backward' time,
15435 * skips to beginning of video or seekable range.
15436 *
15437 * Handle a click on a `SkipBackward` button
15438 *
15439 * @param {EventTarget~Event} event
15440 * The `click` event that caused this function
15441 * to be called
15442 */
15443 handleClick(event) {
15444 const currentVideoTime = this.player_.currentTime();
15445 const liveTracker = this.player_.liveTracker;
15446 const seekableStart = liveTracker && liveTracker.isLive() && liveTracker.seekableStart();
15447 let newTime;
15448 if (seekableStart && currentVideoTime - this.skipTime <= seekableStart) {
15449 newTime = seekableStart;
15450 } else if (currentVideoTime >= this.skipTime) {
15451 newTime = currentVideoTime - this.skipTime;
15452 } else {
15453 newTime = 0;
15454 }
15455 this.player_.currentTime(newTime);
15456 }
15457
15458 /**
15459 * Update control text on languagechange
15460 */
15461 handleLanguagechange() {
15462 this.controlText(this.localize('Skip backward {1} seconds', [this.skipTime]));
15463 }
15464}
15465SkipBackward.prototype.controlText_ = 'Skip Backward';
15466Component.registerComponent('SkipBackward', SkipBackward);
15467
15468/**
15469 * @file menu.js
15470 */
15471
15472/** @import Player from '../player' */
15473
15474/**
15475 * The Menu component is used to build popup menus, including subtitle and
15476 * captions selection menus.
15477 *
15478 * @extends Component
15479 */
15480class Menu extends Component {
15481 /**
15482 * Create an instance of this class.
15483 *
15484 * @param {Player} player
15485 * the player that this component should attach to
15486 *
15487 * @param {Object} [options]
15488 * Object of option names and values
15489 *
15490 */
15491 constructor(player, options) {
15492 super(player, options);
15493 if (options) {
15494 this.menuButton_ = options.menuButton;
15495 }
15496 this.focusedChild_ = -1;
15497 this.on('keydown', e => this.handleKeyDown(e));
15498
15499 // All the menu item instances share the same blur handler provided by the menu container.
15500 this.boundHandleBlur_ = e => this.handleBlur(e);
15501 this.boundHandleTapClick_ = e => this.handleTapClick(e);
15502 }
15503
15504 /**
15505 * Add event listeners to the {@link MenuItem}.
15506 *
15507 * @param {Object} component
15508 * The instance of the `MenuItem` to add listeners to.
15509 *
15510 */
15511 addEventListenerForItem(component) {
15512 if (!(component instanceof Component)) {
15513 return;
15514 }
15515 this.on(component, 'blur', this.boundHandleBlur_);
15516 this.on(component, ['tap', 'click'], this.boundHandleTapClick_);
15517 }
15518
15519 /**
15520 * Remove event listeners from the {@link MenuItem}.
15521 *
15522 * @param {Object} component
15523 * The instance of the `MenuItem` to remove listeners.
15524 *
15525 */
15526 removeEventListenerForItem(component) {
15527 if (!(component instanceof Component)) {
15528 return;
15529 }
15530 this.off(component, 'blur', this.boundHandleBlur_);
15531 this.off(component, ['tap', 'click'], this.boundHandleTapClick_);
15532 }
15533
15534 /**
15535 * This method will be called indirectly when the component has been added
15536 * before the component adds to the new menu instance by `addItem`.
15537 * In this case, the original menu instance will remove the component
15538 * by calling `removeChild`.
15539 *
15540 * @param {Object} component
15541 * The instance of the `MenuItem`
15542 */
15543 removeChild(component) {
15544 if (typeof component === 'string') {
15545 component = this.getChild(component);
15546 }
15547 this.removeEventListenerForItem(component);
15548 super.removeChild(component);
15549 }
15550
15551 /**
15552 * Add a {@link MenuItem} to the menu.
15553 *
15554 * @param {Object|string} component
15555 * The name or instance of the `MenuItem` to add.
15556 *
15557 */
15558 addItem(component) {
15559 const childComponent = this.addChild(component);
15560 if (childComponent) {
15561 this.addEventListenerForItem(childComponent);
15562 }
15563 }
15564
15565 /**
15566 * Create the `Menu`s DOM element.
15567 *
15568 * @return {Element}
15569 * the element that was created
15570 */
15571 createEl() {
15572 const contentElType = this.options_.contentElType || 'ul';
15573 this.contentEl_ = createEl(contentElType, {
15574 className: 'vjs-menu-content'
15575 });
15576 this.contentEl_.setAttribute('role', 'menu');
15577 const el = super.createEl('div', {
15578 append: this.contentEl_,
15579 className: 'vjs-menu'
15580 });
15581 el.appendChild(this.contentEl_);
15582
15583 // Prevent clicks from bubbling up. Needed for Menu Buttons,
15584 // where a click on the parent is significant
15585 on(el, 'click', function (event) {
15586 event.preventDefault();
15587 event.stopImmediatePropagation();
15588 });
15589 return el;
15590 }
15591 dispose() {
15592 this.contentEl_ = null;
15593 this.boundHandleBlur_ = null;
15594 this.boundHandleTapClick_ = null;
15595 super.dispose();
15596 }
15597
15598 /**
15599 * Called when a `MenuItem` loses focus.
15600 *
15601 * @param {Event} event
15602 * The `blur` event that caused this function to be called.
15603 *
15604 * @listens blur
15605 */
15606 handleBlur(event) {
15607 const relatedTarget = event.relatedTarget || document__default["default"].activeElement;
15608
15609 // Close menu popup when a user clicks outside the menu
15610 if (!this.children().some(element => {
15611 return element.el() === relatedTarget;
15612 })) {
15613 const btn = this.menuButton_;
15614 if (btn && btn.buttonPressed_ && relatedTarget !== btn.el().firstChild) {
15615 btn.unpressButton();
15616 }
15617 }
15618 }
15619
15620 /**
15621 * Called when a `MenuItem` gets clicked or tapped.
15622 *
15623 * @param {Event} event
15624 * The `click` or `tap` event that caused this function to be called.
15625 *
15626 * @listens click,tap
15627 */
15628 handleTapClick(event) {
15629 // Unpress the associated MenuButton, and move focus back to it
15630 if (this.menuButton_) {
15631 this.menuButton_.unpressButton();
15632 const childComponents = this.children();
15633 if (!Array.isArray(childComponents)) {
15634 return;
15635 }
15636 const foundComponent = childComponents.filter(component => component.el() === event.target)[0];
15637 if (!foundComponent) {
15638 return;
15639 }
15640
15641 // don't focus menu button if item is a caption settings item
15642 // because focus will move elsewhere
15643 if (foundComponent.name() !== 'CaptionSettingsMenuItem') {
15644 this.menuButton_.focus();
15645 }
15646 }
15647 }
15648
15649 /**
15650 * Handle a `keydown` event on this menu. This listener is added in the constructor.
15651 *
15652 * @param {KeyboardEvent} event
15653 * A `keydown` event that happened on the menu.
15654 *
15655 * @listens keydown
15656 */
15657 handleKeyDown(event) {
15658 // Left and Down Arrows
15659 if (event.key === 'ArrowLeft' || event.key === 'ArrowDown') {
15660 event.preventDefault();
15661 event.stopPropagation();
15662 this.stepForward();
15663
15664 // Up and Right Arrows
15665 } else if (event.key === 'ArrowRight' || event.key === 'ArrowUp') {
15666 event.preventDefault();
15667 event.stopPropagation();
15668 this.stepBack();
15669 }
15670 }
15671
15672 /**
15673 * Move to next (lower) menu item for keyboard users.
15674 */
15675 stepForward() {
15676 let stepChild = 0;
15677 if (this.focusedChild_ !== undefined) {
15678 stepChild = this.focusedChild_ + 1;
15679 }
15680 this.focus(stepChild);
15681 }
15682
15683 /**
15684 * Move to previous (higher) menu item for keyboard users.
15685 */
15686 stepBack() {
15687 let stepChild = 0;
15688 if (this.focusedChild_ !== undefined) {
15689 stepChild = this.focusedChild_ - 1;
15690 }
15691 this.focus(stepChild);
15692 }
15693
15694 /**
15695 * Set focus on a {@link MenuItem} in the `Menu`.
15696 *
15697 * @param {Object|string} [item=0]
15698 * Index of child item set focus on.
15699 */
15700 focus(item = 0) {
15701 const children = this.children().slice();
15702 const haveTitle = children.length && children[0].hasClass('vjs-menu-title');
15703 if (haveTitle) {
15704 children.shift();
15705 }
15706 if (children.length > 0) {
15707 if (item < 0) {
15708 item = 0;
15709 } else if (item >= children.length) {
15710 item = children.length - 1;
15711 }
15712 this.focusedChild_ = item;
15713 children[item].el_.focus();
15714 }
15715 }
15716}
15717Component.registerComponent('Menu', Menu);
15718
15719/**
15720 * @file menu-button.js
15721 */
15722
15723/** @import Player from '../player' */
15724
15725/**
15726 * A `MenuButton` class for any popup {@link Menu}.
15727 *
15728 * @extends Component
15729 */
15730class MenuButton extends Component {
15731 /**
15732 * Creates an instance of this class.
15733 *
15734 * @param {Player} player
15735 * The `Player` that this class should be attached to.
15736 *
15737 * @param {Object} [options={}]
15738 * The key/value store of player options.
15739 */
15740 constructor(player, options = {}) {
15741 super(player, options);
15742 this.menuButton_ = new Button(player, options);
15743 this.menuButton_.controlText(this.controlText_);
15744 this.menuButton_.el_.setAttribute('aria-haspopup', 'true');
15745
15746 // Add buildCSSClass values to the button, not the wrapper
15747 const buttonClass = Button.prototype.buildCSSClass();
15748 this.menuButton_.el_.className = this.buildCSSClass() + ' ' + buttonClass;
15749 this.menuButton_.removeClass('vjs-control');
15750 this.addChild(this.menuButton_);
15751 this.update();
15752 this.enabled_ = true;
15753 const handleClick = e => this.handleClick(e);
15754 this.handleMenuKeyUp_ = e => this.handleMenuKeyUp(e);
15755 this.on(this.menuButton_, 'tap', handleClick);
15756 this.on(this.menuButton_, 'click', handleClick);
15757 this.on(this.menuButton_, 'keydown', e => this.handleKeyDown(e));
15758 this.on(this.menuButton_, 'mouseenter', () => {
15759 this.addClass('vjs-hover');
15760 this.menu.show();
15761 on(document__default["default"], 'keyup', this.handleMenuKeyUp_);
15762 });
15763 this.on('mouseleave', e => this.handleMouseLeave(e));
15764 this.on('keydown', e => this.handleSubmenuKeyDown(e));
15765 }
15766
15767 /**
15768 * Update the menu based on the current state of its items.
15769 */
15770 update() {
15771 const menu = this.createMenu();
15772 if (this.menu) {
15773 this.menu.dispose();
15774 this.removeChild(this.menu);
15775 }
15776 this.menu = menu;
15777 this.addChild(menu);
15778
15779 /**
15780 * Track the state of the menu button
15781 *
15782 * @type {Boolean}
15783 * @private
15784 */
15785 this.buttonPressed_ = false;
15786 this.menuButton_.el_.setAttribute('aria-expanded', 'false');
15787 if (this.items && this.items.length <= this.hideThreshold_) {
15788 this.hide();
15789 this.menu.contentEl_.removeAttribute('role');
15790 } else {
15791 this.show();
15792 this.menu.contentEl_.setAttribute('role', 'menu');
15793 }
15794 }
15795
15796 /**
15797 * Create the menu and add all items to it.
15798 *
15799 * @return {Menu}
15800 * The constructed menu
15801 */
15802 createMenu() {
15803 const menu = new Menu(this.player_, {
15804 menuButton: this
15805 });
15806
15807 /**
15808 * Hide the menu if the number of items is less than or equal to this threshold. This defaults
15809 * to 0 and whenever we add items which can be hidden to the menu we'll increment it. We list
15810 * it here because every time we run `createMenu` we need to reset the value.
15811 *
15812 * @protected
15813 * @type {Number}
15814 */
15815 this.hideThreshold_ = 0;
15816
15817 // Add a title list item to the top
15818 if (this.options_.title) {
15819 const titleEl = createEl('li', {
15820 className: 'vjs-menu-title',
15821 textContent: toTitleCase(this.options_.title),
15822 tabIndex: -1
15823 });
15824 const titleComponent = new Component(this.player_, {
15825 el: titleEl
15826 });
15827 menu.addItem(titleComponent);
15828 }
15829 this.items = this.createItems();
15830 if (this.items) {
15831 // Add menu items to the menu
15832 for (let i = 0; i < this.items.length; i++) {
15833 menu.addItem(this.items[i]);
15834 }
15835 }
15836 return menu;
15837 }
15838
15839 /**
15840 * Create the list of menu items. Specific to each subclass.
15841 *
15842 * @abstract
15843 */
15844 createItems() {}
15845
15846 /**
15847 * Create the `MenuButtons`s DOM element.
15848 *
15849 * @return {Element}
15850 * The element that gets created.
15851 */
15852 createEl() {
15853 return super.createEl('div', {
15854 className: this.buildWrapperCSSClass()
15855 }, {});
15856 }
15857
15858 /**
15859 * Overwrites the `setIcon` method from `Component`.
15860 * In this case, we want the icon to be appended to the menuButton.
15861 *
15862 * @param {string} name
15863 * The icon name to be added.
15864 */
15865 setIcon(name) {
15866 super.setIcon(name, this.menuButton_.el_);
15867 }
15868
15869 /**
15870 * Allow sub components to stack CSS class names for the wrapper element
15871 *
15872 * @return {string}
15873 * The constructed wrapper DOM `className`
15874 */
15875 buildWrapperCSSClass() {
15876 let menuButtonClass = 'vjs-menu-button';
15877
15878 // If the inline option is passed, we want to use different styles altogether.
15879 if (this.options_.inline === true) {
15880 menuButtonClass += '-inline';
15881 } else {
15882 menuButtonClass += '-popup';
15883 }
15884
15885 // TODO: Fix the CSS so that this isn't necessary
15886 const buttonClass = Button.prototype.buildCSSClass();
15887 return `vjs-menu-button ${menuButtonClass} ${buttonClass} ${super.buildCSSClass()}`;
15888 }
15889
15890 /**
15891 * Builds the default DOM `className`.
15892 *
15893 * @return {string}
15894 * The DOM `className` for this object.
15895 */
15896 buildCSSClass() {
15897 let menuButtonClass = 'vjs-menu-button';
15898
15899 // If the inline option is passed, we want to use different styles altogether.
15900 if (this.options_.inline === true) {
15901 menuButtonClass += '-inline';
15902 } else {
15903 menuButtonClass += '-popup';
15904 }
15905 return `vjs-menu-button ${menuButtonClass} ${super.buildCSSClass()}`;
15906 }
15907
15908 /**
15909 * Get or set the localized control text that will be used for accessibility.
15910 *
15911 * > NOTE: This will come from the internal `menuButton_` element.
15912 *
15913 * @param {string} [text]
15914 * Control text for element.
15915 *
15916 * @param {Element} [el=this.menuButton_.el()]
15917 * Element to set the title on.
15918 *
15919 * @return {string}
15920 * - The control text when getting
15921 */
15922 controlText(text, el = this.menuButton_.el()) {
15923 return this.menuButton_.controlText(text, el);
15924 }
15925
15926 /**
15927 * Dispose of the `menu-button` and all child components.
15928 */
15929 dispose() {
15930 this.handleMouseLeave();
15931 super.dispose();
15932 }
15933
15934 /**
15935 * Handle a click on a `MenuButton`.
15936 * See {@link ClickableComponent#handleClick} for instances where this is called.
15937 *
15938 * @param {Event} event
15939 * The `keydown`, `tap`, or `click` event that caused this function to be
15940 * called.
15941 *
15942 * @listens tap
15943 * @listens click
15944 */
15945 handleClick(event) {
15946 if (this.buttonPressed_) {
15947 this.unpressButton();
15948 } else {
15949 this.pressButton();
15950 }
15951 }
15952
15953 /**
15954 * Handle `mouseleave` for `MenuButton`.
15955 *
15956 * @param {Event} event
15957 * The `mouseleave` event that caused this function to be called.
15958 *
15959 * @listens mouseleave
15960 */
15961 handleMouseLeave(event) {
15962 this.removeClass('vjs-hover');
15963 off(document__default["default"], 'keyup', this.handleMenuKeyUp_);
15964 }
15965
15966 /**
15967 * Set the focus to the actual button, not to this element
15968 */
15969 focus() {
15970 this.menuButton_.focus();
15971 }
15972
15973 /**
15974 * Remove the focus from the actual button, not this element
15975 */
15976 blur() {
15977 this.menuButton_.blur();
15978 }
15979
15980 /**
15981 * Handle tab, escape, down arrow, and up arrow keys for `MenuButton`. See
15982 * {@link ClickableComponent#handleKeyDown} for instances where this is called.
15983 *
15984 * @param {Event} event
15985 * The `keydown` event that caused this function to be called.
15986 *
15987 * @listens keydown
15988 */
15989 handleKeyDown(event) {
15990 // Escape or Tab unpress the 'button'
15991 if (event.key === 'Escape' || event.key === 'Tab') {
15992 if (this.buttonPressed_) {
15993 this.unpressButton();
15994 }
15995
15996 // Don't preventDefault for Tab key - we still want to lose focus
15997 if (!event.key === 'Tab') {
15998 event.preventDefault();
15999 // Set focus back to the menu button's button
16000 this.menuButton_.focus();
16001 }
16002 // Up Arrow or Down Arrow also 'press' the button to open the menu
16003 } else if (event.key === 'Up' || event.key === 'Down' && !(this.player_.options_.playerOptions.spatialNavigation && this.player_.options_.playerOptions.spatialNavigation.enabled)) {
16004 if (!this.buttonPressed_) {
16005 event.preventDefault();
16006 this.pressButton();
16007 }
16008 }
16009 }
16010
16011 /**
16012 * Handle a `keyup` event on a `MenuButton`. The listener for this is added in
16013 * the constructor.
16014 *
16015 * @param {Event} event
16016 * Key press event
16017 *
16018 * @listens keyup
16019 */
16020 handleMenuKeyUp(event) {
16021 // Escape hides popup menu
16022 if (event.key === 'Escape' || event.key === 'Tab') {
16023 this.removeClass('vjs-hover');
16024 }
16025 }
16026
16027 /**
16028 * This method name now delegates to `handleSubmenuKeyDown`. This means
16029 * anyone calling `handleSubmenuKeyPress` will not see their method calls
16030 * stop working.
16031 *
16032 * @param {Event} event
16033 * The event that caused this function to be called.
16034 */
16035 handleSubmenuKeyPress(event) {
16036 this.handleSubmenuKeyDown(event);
16037 }
16038
16039 /**
16040 * Handle a `keydown` event on a sub-menu. The listener for this is added in
16041 * the constructor.
16042 *
16043 * @param {Event} event
16044 * Key press event
16045 *
16046 * @listens keydown
16047 */
16048 handleSubmenuKeyDown(event) {
16049 // Escape or Tab unpress the 'button'
16050 if (event.key === 'Escape' || event.key === 'Tab') {
16051 if (this.buttonPressed_) {
16052 this.unpressButton();
16053 }
16054 // Don't preventDefault for Tab key - we still want to lose focus
16055 if (!event.key === 'Tab') {
16056 event.preventDefault();
16057 // Set focus back to the menu button's button
16058 this.menuButton_.focus();
16059 }
16060 }
16061 }
16062
16063 /**
16064 * Put the current `MenuButton` into a pressed state.
16065 */
16066 pressButton() {
16067 if (this.enabled_) {
16068 this.buttonPressed_ = true;
16069 this.menu.show();
16070 this.menu.lockShowing();
16071 this.menuButton_.el_.setAttribute('aria-expanded', 'true');
16072
16073 // set the focus into the submenu, except on iOS where it is resulting in
16074 // undesired scrolling behavior when the player is in an iframe
16075 if (IS_IOS && isInFrame()) {
16076 // Return early so that the menu isn't focused
16077 return;
16078 }
16079 this.menu.focus();
16080 }
16081 }
16082
16083 /**
16084 * Take the current `MenuButton` out of a pressed state.
16085 */
16086 unpressButton() {
16087 if (this.enabled_) {
16088 this.buttonPressed_ = false;
16089 this.menu.unlockShowing();
16090 this.menu.hide();
16091 this.menuButton_.el_.setAttribute('aria-expanded', 'false');
16092 }
16093 }
16094
16095 /**
16096 * Disable the `MenuButton`. Don't allow it to be clicked.
16097 */
16098 disable() {
16099 this.unpressButton();
16100 this.enabled_ = false;
16101 this.addClass('vjs-disabled');
16102 this.menuButton_.disable();
16103 }
16104
16105 /**
16106 * Enable the `MenuButton`. Allow it to be clicked.
16107 */
16108 enable() {
16109 this.enabled_ = true;
16110 this.removeClass('vjs-disabled');
16111 this.menuButton_.enable();
16112 }
16113}
16114Component.registerComponent('MenuButton', MenuButton);
16115
16116/**
16117 * @file track-button.js
16118 */
16119
16120/** @import Player from './player' */
16121
16122/**
16123 * The base class for buttons that toggle specific track types (e.g. subtitles).
16124 *
16125 * @extends MenuButton
16126 */
16127class TrackButton extends MenuButton {
16128 /**
16129 * Creates an instance of this class.
16130 *
16131 * @param {Player} player
16132 * The `Player` that this class should be attached to.
16133 *
16134 * @param {Object} [options]
16135 * The key/value store of player options.
16136 */
16137 constructor(player, options) {
16138 const tracks = options.tracks;
16139 super(player, options);
16140 if (this.items.length <= 1) {
16141 this.hide();
16142 }
16143 if (!tracks) {
16144 return;
16145 }
16146 const updateHandler = bind_(this, this.update);
16147 tracks.addEventListener('removetrack', updateHandler);
16148 tracks.addEventListener('addtrack', updateHandler);
16149 tracks.addEventListener('labelchange', updateHandler);
16150 this.player_.on('ready', updateHandler);
16151 this.player_.on('dispose', function () {
16152 tracks.removeEventListener('removetrack', updateHandler);
16153 tracks.removeEventListener('addtrack', updateHandler);
16154 tracks.removeEventListener('labelchange', updateHandler);
16155 });
16156 }
16157}
16158Component.registerComponent('TrackButton', TrackButton);
16159
16160/**
16161 * @file menu-item.js
16162 */
16163
16164/** @import Player from '../player' */
16165
16166/**
16167 * The component for a menu item. `<li>`
16168 *
16169 * @extends ClickableComponent
16170 */
16171class MenuItem extends ClickableComponent {
16172 /**
16173 * Creates an instance of the this class.
16174 *
16175 * @param {Player} player
16176 * The `Player` that this class should be attached to.
16177 *
16178 * @param {Object} [options={}]
16179 * The key/value store of player options.
16180 *
16181 */
16182 constructor(player, options) {
16183 super(player, options);
16184 this.selectable = options.selectable;
16185 this.isSelected_ = options.selected || false;
16186 this.multiSelectable = options.multiSelectable;
16187 this.selected(this.isSelected_);
16188 if (this.selectable) {
16189 if (this.multiSelectable) {
16190 this.el_.setAttribute('role', 'menuitemcheckbox');
16191 } else {
16192 this.el_.setAttribute('role', 'menuitemradio');
16193 }
16194 } else {
16195 this.el_.setAttribute('role', 'menuitem');
16196 }
16197 }
16198
16199 /**
16200 * Create the `MenuItem's DOM element
16201 *
16202 * @param {string} [type=li]
16203 * Element's node type, not actually used, always set to `li`.
16204 *
16205 * @param {Object} [props={}]
16206 * An object of properties that should be set on the element
16207 *
16208 * @param {Object} [attrs={}]
16209 * An object of attributes that should be set on the element
16210 *
16211 * @return {Element}
16212 * The element that gets created.
16213 */
16214 createEl(type, props, attrs) {
16215 // The control is textual, not just an icon
16216 this.nonIconControl = true;
16217 const el = super.createEl('li', Object.assign({
16218 className: 'vjs-menu-item',
16219 tabIndex: -1
16220 }, props), attrs);
16221
16222 // swap icon with menu item text.
16223 const menuItemEl = createEl('span', {
16224 className: 'vjs-menu-item-text',
16225 textContent: this.localize(this.options_.label)
16226 });
16227
16228 // If using SVG icons, the element with vjs-icon-placeholder will be added separately.
16229 if (this.player_.options_.experimentalSvgIcons) {
16230 el.appendChild(menuItemEl);
16231 } else {
16232 el.replaceChild(menuItemEl, el.querySelector('.vjs-icon-placeholder'));
16233 }
16234 return el;
16235 }
16236
16237 /**
16238 * Ignore keys which are used by the menu, but pass any other ones up. See
16239 * {@link ClickableComponent#handleKeyDown} for instances where this is called.
16240 *
16241 * @param {KeyboardEvent} event
16242 * The `keydown` event that caused this function to be called.
16243 *
16244 * @listens keydown
16245 */
16246 handleKeyDown(event) {
16247 if (!['Tab', 'Escape', 'ArrowUp', 'ArrowLeft', 'ArrowRight', 'ArrowDown'].includes(event.key)) {
16248 // Pass keydown handling up for unused keys
16249 super.handleKeyDown(event);
16250 }
16251 }
16252
16253 /**
16254 * Any click on a `MenuItem` puts it into the selected state.
16255 * See {@link ClickableComponent#handleClick} for instances where this is called.
16256 *
16257 * @param {Event} event
16258 * The `keydown`, `tap`, or `click` event that caused this function to be
16259 * called.
16260 *
16261 * @listens tap
16262 * @listens click
16263 */
16264 handleClick(event) {
16265 this.selected(true);
16266 }
16267
16268 /**
16269 * Set the state for this menu item as selected or not.
16270 *
16271 * @param {boolean} selected
16272 * if the menu item is selected or not
16273 */
16274 selected(selected) {
16275 if (this.selectable) {
16276 if (selected) {
16277 this.addClass('vjs-selected');
16278 this.el_.setAttribute('aria-checked', 'true');
16279 // aria-checked isn't fully supported by browsers/screen readers,
16280 // so indicate selected state to screen reader in the control text.
16281 this.controlText(', selected');
16282 this.isSelected_ = true;
16283 } else {
16284 this.removeClass('vjs-selected');
16285 this.el_.setAttribute('aria-checked', 'false');
16286 // Indicate un-selected state to screen reader
16287 this.controlText('');
16288 this.isSelected_ = false;
16289 }
16290 }
16291 }
16292}
16293Component.registerComponent('MenuItem', MenuItem);
16294
16295/**
16296 * @file text-track-menu-item.js
16297 */
16298
16299/** @import Player from '../../player' */
16300
16301/**
16302 * The specific menu item type for selecting a language within a text track kind
16303 *
16304 * @extends MenuItem
16305 */
16306class TextTrackMenuItem extends MenuItem {
16307 /**
16308 * Creates an instance of this class.
16309 *
16310 * @param {Player} player
16311 * The `Player` that this class should be attached to.
16312 *
16313 * @param {Object} [options]
16314 * The key/value store of player options.
16315 */
16316 constructor(player, options) {
16317 const track = options.track;
16318 const tracks = player.textTracks();
16319
16320 // Modify options for parent MenuItem class's init.
16321 options.label = track.label || track.language || 'Unknown';
16322 options.selected = track.mode === 'showing';
16323 super(player, options);
16324 this.track = track;
16325 // Determine the relevant kind(s) of tracks for this component and filter
16326 // out empty kinds.
16327 this.kinds = (options.kinds || [options.kind || this.track.kind]).filter(Boolean);
16328 const changeHandler = (...args) => {
16329 this.handleTracksChange.apply(this, args);
16330 };
16331 const selectedLanguageChangeHandler = (...args) => {
16332 this.handleSelectedLanguageChange.apply(this, args);
16333 };
16334 player.on(['loadstart', 'texttrackchange'], changeHandler);
16335 tracks.addEventListener('change', changeHandler);
16336 tracks.addEventListener('selectedlanguagechange', selectedLanguageChangeHandler);
16337 this.on('dispose', function () {
16338 player.off(['loadstart', 'texttrackchange'], changeHandler);
16339 tracks.removeEventListener('change', changeHandler);
16340 tracks.removeEventListener('selectedlanguagechange', selectedLanguageChangeHandler);
16341 });
16342
16343 // iOS7 doesn't dispatch change events to TextTrackLists when an
16344 // associated track's mode changes. Without something like
16345 // Object.observe() (also not present on iOS7), it's not
16346 // possible to detect changes to the mode attribute and polyfill
16347 // the change event. As a poor substitute, we manually dispatch
16348 // change events whenever the controls modify the mode.
16349 if (tracks.onchange === undefined) {
16350 let event;
16351 this.on(['tap', 'click'], function () {
16352 if (typeof window__default["default"].Event !== 'object') {
16353 // Android 2.3 throws an Illegal Constructor error for window.Event
16354 try {
16355 event = new window__default["default"].Event('change');
16356 } catch (err) {
16357 // continue regardless of error
16358 }
16359 }
16360 if (!event) {
16361 event = document__default["default"].createEvent('Event');
16362 event.initEvent('change', true, true);
16363 }
16364 tracks.dispatchEvent(event);
16365 });
16366 }
16367
16368 // set the default state based on current tracks
16369 this.handleTracksChange();
16370 }
16371
16372 /**
16373 * This gets called when an `TextTrackMenuItem` is "clicked". See
16374 * {@link ClickableComponent} for more detailed information on what a click can be.
16375 *
16376 * @param {Event} event
16377 * The `keydown`, `tap`, or `click` event that caused this function to be
16378 * called.
16379 *
16380 * @listens tap
16381 * @listens click
16382 */
16383 handleClick(event) {
16384 const referenceTrack = this.track;
16385 const tracks = this.player_.textTracks();
16386 super.handleClick(event);
16387 if (!tracks) {
16388 return;
16389 }
16390 for (let i = 0; i < tracks.length; i++) {
16391 const track = tracks[i];
16392
16393 // If the track from the text tracks list is not of the right kind,
16394 // skip it. We do not want to affect tracks of incompatible kind(s).
16395 if (this.kinds.indexOf(track.kind) === -1) {
16396 continue;
16397 }
16398
16399 // If this text track is the component's track and it is not showing,
16400 // set it to showing.
16401 if (track === referenceTrack) {
16402 if (track.mode !== 'showing') {
16403 track.mode = 'showing';
16404 }
16405
16406 // If this text track is not the component's track and it is not
16407 // disabled, set it to disabled.
16408 } else if (track.mode !== 'disabled') {
16409 track.mode = 'disabled';
16410 }
16411 }
16412 }
16413
16414 /**
16415 * Handle text track list change
16416 *
16417 * @param {Event} event
16418 * The `change` event that caused this function to be called.
16419 *
16420 * @listens TextTrackList#change
16421 */
16422 handleTracksChange(event) {
16423 const shouldBeSelected = this.track.mode === 'showing';
16424
16425 // Prevent redundant selected() calls because they may cause
16426 // screen readers to read the appended control text unnecessarily
16427 if (shouldBeSelected !== this.isSelected_) {
16428 this.selected(shouldBeSelected);
16429 }
16430 }
16431 handleSelectedLanguageChange(event) {
16432 if (this.track.mode === 'showing') {
16433 const selectedLanguage = this.player_.cache_.selectedLanguage;
16434
16435 // Don't replace the kind of track across the same language
16436 if (selectedLanguage && selectedLanguage.enabled && selectedLanguage.language === this.track.language && selectedLanguage.kind !== this.track.kind) {
16437 return;
16438 }
16439 this.player_.cache_.selectedLanguage = {
16440 enabled: true,
16441 language: this.track.language,
16442 kind: this.track.kind
16443 };
16444 }
16445 }
16446 dispose() {
16447 // remove reference to track object on dispose
16448 this.track = null;
16449 super.dispose();
16450 }
16451}
16452Component.registerComponent('TextTrackMenuItem', TextTrackMenuItem);
16453
16454/**
16455 * @file off-text-track-menu-item.js
16456 */
16457
16458/** @import Player from '../../player' */
16459
16460/**
16461 * A special menu item for turning off a specific type of text track
16462 *
16463 * @extends TextTrackMenuItem
16464 */
16465class OffTextTrackMenuItem extends TextTrackMenuItem {
16466 /**
16467 * Creates an instance of this class.
16468 *
16469 * @param {Player} player
16470 * The `Player` that this class should be attached to.
16471 *
16472 * @param {Object} [options]
16473 * The key/value store of player options.
16474 */
16475 constructor(player, options) {
16476 // Create pseudo track info
16477 // Requires options['kind']
16478 options.track = {
16479 player,
16480 // it is no longer necessary to store `kind` or `kinds` on the track itself
16481 // since they are now stored in the `kinds` property of all instances of
16482 // TextTrackMenuItem, but this will remain for backwards compatibility
16483 kind: options.kind,
16484 kinds: options.kinds,
16485 default: false,
16486 mode: 'disabled'
16487 };
16488 if (!options.kinds) {
16489 options.kinds = [options.kind];
16490 }
16491 if (options.label) {
16492 options.track.label = options.label;
16493 } else {
16494 options.track.label = options.kinds.join(' and ') + ' off';
16495 }
16496
16497 // MenuItem is selectable
16498 options.selectable = true;
16499 // MenuItem is NOT multiSelectable (i.e. only one can be marked "selected" at a time)
16500 options.multiSelectable = false;
16501 super(player, options);
16502 }
16503
16504 /**
16505 * Handle text track change
16506 *
16507 * @param {Event} event
16508 * The event that caused this function to run
16509 */
16510 handleTracksChange(event) {
16511 const tracks = this.player().textTracks();
16512 let shouldBeSelected = true;
16513 for (let i = 0, l = tracks.length; i < l; i++) {
16514 const track = tracks[i];
16515 if (this.options_.kinds.indexOf(track.kind) > -1 && track.mode === 'showing') {
16516 shouldBeSelected = false;
16517 break;
16518 }
16519 }
16520
16521 // Prevent redundant selected() calls because they may cause
16522 // screen readers to read the appended control text unnecessarily
16523 if (shouldBeSelected !== this.isSelected_) {
16524 this.selected(shouldBeSelected);
16525 }
16526 }
16527 handleSelectedLanguageChange(event) {
16528 const tracks = this.player().textTracks();
16529 let allHidden = true;
16530 for (let i = 0, l = tracks.length; i < l; i++) {
16531 const track = tracks[i];
16532 if (['captions', 'descriptions', 'subtitles'].indexOf(track.kind) > -1 && track.mode === 'showing') {
16533 allHidden = false;
16534 break;
16535 }
16536 }
16537 if (allHidden) {
16538 this.player_.cache_.selectedLanguage = {
16539 enabled: false
16540 };
16541 }
16542 }
16543
16544 /**
16545 * Update control text and label on languagechange
16546 */
16547 handleLanguagechange() {
16548 this.$('.vjs-menu-item-text').textContent = this.player_.localize(this.options_.label);
16549 super.handleLanguagechange();
16550 }
16551}
16552Component.registerComponent('OffTextTrackMenuItem', OffTextTrackMenuItem);
16553
16554/**
16555 * @file text-track-button.js
16556 */
16557
16558/** @import Player from '../../player' */
16559
16560/**
16561 * The base class for buttons that toggle specific text track types (e.g. subtitles)
16562 *
16563 * @extends MenuButton
16564 */
16565class TextTrackButton extends TrackButton {
16566 /**
16567 * Creates an instance of this class.
16568 *
16569 * @param {Player} player
16570 * The `Player` that this class should be attached to.
16571 *
16572 * @param {Object} [options={}]
16573 * The key/value store of player options.
16574 */
16575 constructor(player, options = {}) {
16576 options.tracks = player.textTracks();
16577 super(player, options);
16578 }
16579
16580 /**
16581 * Create a menu item for each text track
16582 *
16583 * @param {TextTrackMenuItem[]} [items=[]]
16584 * Existing array of items to use during creation
16585 *
16586 * @return {TextTrackMenuItem[]}
16587 * Array of menu items that were created
16588 */
16589 createItems(items = [], TrackMenuItem = TextTrackMenuItem) {
16590 // Label is an override for the [track] off label
16591 // USed to localise captions/subtitles
16592 let label;
16593 if (this.label_) {
16594 label = `${this.label_} off`;
16595 }
16596 // Add an OFF menu item to turn all tracks off
16597 items.push(new OffTextTrackMenuItem(this.player_, {
16598 kinds: this.kinds_,
16599 kind: this.kind_,
16600 label
16601 }));
16602 this.hideThreshold_ += 1;
16603 const tracks = this.player_.textTracks();
16604 if (!Array.isArray(this.kinds_)) {
16605 this.kinds_ = [this.kind_];
16606 }
16607 for (let i = 0; i < tracks.length; i++) {
16608 const track = tracks[i];
16609
16610 // only add tracks that are of an appropriate kind and have a label
16611 if (this.kinds_.indexOf(track.kind) > -1) {
16612 const item = new TrackMenuItem(this.player_, {
16613 track,
16614 kinds: this.kinds_,
16615 kind: this.kind_,
16616 // MenuItem is selectable
16617 selectable: true,
16618 // MenuItem is NOT multiSelectable (i.e. only one can be marked "selected" at a time)
16619 multiSelectable: false
16620 });
16621 item.addClass(`vjs-${track.kind}-menu-item`);
16622 items.push(item);
16623 }
16624 }
16625 return items;
16626 }
16627}
16628Component.registerComponent('TextTrackButton', TextTrackButton);
16629
16630/**
16631 * @file chapters-track-menu-item.js
16632 */
16633
16634/** @import Player from '../../player' */
16635
16636/**
16637 * The chapter track menu item
16638 *
16639 * @extends MenuItem
16640 */
16641class ChaptersTrackMenuItem extends MenuItem {
16642 /**
16643 * Creates an instance of this class.
16644 *
16645 * @param {Player} player
16646 * The `Player` that this class should be attached to.
16647 *
16648 * @param {Object} [options]
16649 * The key/value store of player options.
16650 */
16651 constructor(player, options) {
16652 const track = options.track;
16653 const cue = options.cue;
16654 const currentTime = player.currentTime();
16655
16656 // Modify options for parent MenuItem class's init.
16657 options.selectable = true;
16658 options.multiSelectable = false;
16659 options.label = cue.text;
16660 options.selected = cue.startTime <= currentTime && currentTime < cue.endTime;
16661 super(player, options);
16662 this.track = track;
16663 this.cue = cue;
16664 }
16665
16666 /**
16667 * This gets called when an `ChaptersTrackMenuItem` is "clicked". See
16668 * {@link ClickableComponent} for more detailed information on what a click can be.
16669 *
16670 * @param {Event} [event]
16671 * The `keydown`, `tap`, or `click` event that caused this function to be
16672 * called.
16673 *
16674 * @listens tap
16675 * @listens click
16676 */
16677 handleClick(event) {
16678 super.handleClick();
16679 this.player_.currentTime(this.cue.startTime);
16680 }
16681}
16682Component.registerComponent('ChaptersTrackMenuItem', ChaptersTrackMenuItem);
16683
16684/**
16685 * @file chapters-button.js
16686 */
16687
16688/** @import Player from '../../player' */
16689/** @import Menu from '../../menu/menu' */
16690/** @import TextTrack from '../../tracks/text-track' */
16691/** @import TextTrackMenuItem from '../text-track-controls/text-track-menu-item' */
16692
16693/**
16694 * The button component for toggling and selecting chapters
16695 * Chapters act much differently than other text tracks
16696 * Cues are navigation vs. other tracks of alternative languages
16697 *
16698 * @extends TextTrackButton
16699 */
16700class ChaptersButton extends TextTrackButton {
16701 /**
16702 * Creates an instance of this class.
16703 *
16704 * @param {Player} player
16705 * The `Player` that this class should be attached to.
16706 *
16707 * @param {Object} [options]
16708 * The key/value store of player options.
16709 *
16710 * @param {Function} [ready]
16711 * The function to call when this function is ready.
16712 */
16713 constructor(player, options, ready) {
16714 super(player, options, ready);
16715 this.setIcon('chapters');
16716 this.selectCurrentItem_ = () => {
16717 this.items.forEach(item => {
16718 item.selected(this.track_.activeCues[0] === item.cue);
16719 });
16720 };
16721 }
16722
16723 /**
16724 * Builds the default DOM `className`.
16725 *
16726 * @return {string}
16727 * The DOM `className` for this object.
16728 */
16729 buildCSSClass() {
16730 return `vjs-chapters-button ${super.buildCSSClass()}`;
16731 }
16732 buildWrapperCSSClass() {
16733 return `vjs-chapters-button ${super.buildWrapperCSSClass()}`;
16734 }
16735
16736 /**
16737 * Update the menu based on the current state of its items.
16738 *
16739 * @param {Event} [event]
16740 * An event that triggered this function to run.
16741 *
16742 * @listens TextTrackList#addtrack
16743 * @listens TextTrackList#removetrack
16744 * @listens TextTrackList#change
16745 */
16746 update(event) {
16747 if (event && event.track && event.track.kind !== 'chapters') {
16748 return;
16749 }
16750 const track = this.findChaptersTrack();
16751 if (track !== this.track_) {
16752 this.setTrack(track);
16753 super.update();
16754 } else if (!this.items || track && track.cues && track.cues.length !== this.items.length) {
16755 // Update the menu initially or if the number of cues has changed since set
16756 super.update();
16757 }
16758 }
16759
16760 /**
16761 * Set the currently selected track for the chapters button.
16762 *
16763 * @param {TextTrack} track
16764 * The new track to select. Nothing will change if this is the currently selected
16765 * track.
16766 */
16767 setTrack(track) {
16768 if (this.track_ === track) {
16769 return;
16770 }
16771 if (!this.updateHandler_) {
16772 this.updateHandler_ = this.update.bind(this);
16773 }
16774
16775 // here this.track_ refers to the old track instance
16776 if (this.track_) {
16777 const remoteTextTrackEl = this.player_.remoteTextTrackEls().getTrackElementByTrack_(this.track_);
16778 if (remoteTextTrackEl) {
16779 remoteTextTrackEl.removeEventListener('load', this.updateHandler_);
16780 }
16781 this.track_.removeEventListener('cuechange', this.selectCurrentItem_);
16782 this.track_ = null;
16783 }
16784 this.track_ = track;
16785
16786 // here this.track_ refers to the new track instance
16787 if (this.track_) {
16788 this.track_.mode = 'hidden';
16789 const remoteTextTrackEl = this.player_.remoteTextTrackEls().getTrackElementByTrack_(this.track_);
16790 if (remoteTextTrackEl) {
16791 remoteTextTrackEl.addEventListener('load', this.updateHandler_);
16792 }
16793 this.track_.addEventListener('cuechange', this.selectCurrentItem_);
16794 }
16795 }
16796
16797 /**
16798 * Find the track object that is currently in use by this ChaptersButton
16799 *
16800 * @return {TextTrack|undefined}
16801 * The current track or undefined if none was found.
16802 */
16803 findChaptersTrack() {
16804 const tracks = this.player_.textTracks() || [];
16805 for (let i = tracks.length - 1; i >= 0; i--) {
16806 // We will always choose the last track as our chaptersTrack
16807 const track = tracks[i];
16808 if (track.kind === this.kind_) {
16809 return track;
16810 }
16811 }
16812 }
16813
16814 /**
16815 * Get the caption for the ChaptersButton based on the track label. This will also
16816 * use the current tracks localized kind as a fallback if a label does not exist.
16817 *
16818 * @return {string}
16819 * The tracks current label or the localized track kind.
16820 */
16821 getMenuCaption() {
16822 if (this.track_ && this.track_.label) {
16823 return this.track_.label;
16824 }
16825 return this.localize(toTitleCase(this.kind_));
16826 }
16827
16828 /**
16829 * Create menu from chapter track
16830 *
16831 * @return {Menu}
16832 * New menu for the chapter buttons
16833 */
16834 createMenu() {
16835 this.options_.title = this.getMenuCaption();
16836 return super.createMenu();
16837 }
16838
16839 /**
16840 * Create a menu item for each text track
16841 *
16842 * @return {TextTrackMenuItem[]}
16843 * Array of menu items
16844 */
16845 createItems() {
16846 const items = [];
16847 if (!this.track_) {
16848 return items;
16849 }
16850 const cues = this.track_.cues;
16851 if (!cues) {
16852 return items;
16853 }
16854 for (let i = 0, l = cues.length; i < l; i++) {
16855 const cue = cues[i];
16856 const mi = new ChaptersTrackMenuItem(this.player_, {
16857 track: this.track_,
16858 cue
16859 });
16860 items.push(mi);
16861 }
16862 return items;
16863 }
16864}
16865
16866/**
16867 * `kind` of TextTrack to look for to associate it with this menu.
16868 *
16869 * @type {string}
16870 * @private
16871 */
16872ChaptersButton.prototype.kind_ = 'chapters';
16873
16874/**
16875 * The text that should display over the `ChaptersButton`s controls. Added for localization.
16876 *
16877 * @type {string}
16878 * @protected
16879 */
16880ChaptersButton.prototype.controlText_ = 'Chapters';
16881Component.registerComponent('ChaptersButton', ChaptersButton);
16882
16883/**
16884 * @file descriptions-button.js
16885 */
16886
16887/** @import Player from '../../player' */
16888
16889/**
16890 * The button component for toggling and selecting descriptions
16891 *
16892 * @extends TextTrackButton
16893 */
16894class DescriptionsButton extends TextTrackButton {
16895 /**
16896 * Creates an instance of this class.
16897 *
16898 * @param {Player} player
16899 * The `Player` that this class should be attached to.
16900 *
16901 * @param {Object} [options]
16902 * The key/value store of player options.
16903 *
16904 * @param {Function} [ready]
16905 * The function to call when this component is ready.
16906 */
16907 constructor(player, options, ready) {
16908 super(player, options, ready);
16909 this.setIcon('audio-description');
16910 const tracks = player.textTracks();
16911 const changeHandler = bind_(this, this.handleTracksChange);
16912 tracks.addEventListener('change', changeHandler);
16913 this.on('dispose', function () {
16914 tracks.removeEventListener('change', changeHandler);
16915 });
16916 }
16917
16918 /**
16919 * Handle text track change
16920 *
16921 * @param {Event} event
16922 * The event that caused this function to run
16923 *
16924 * @listens TextTrackList#change
16925 */
16926 handleTracksChange(event) {
16927 const tracks = this.player().textTracks();
16928 let disabled = false;
16929
16930 // Check whether a track of a different kind is showing
16931 for (let i = 0, l = tracks.length; i < l; i++) {
16932 const track = tracks[i];
16933 if (track.kind !== this.kind_ && track.mode === 'showing') {
16934 disabled = true;
16935 break;
16936 }
16937 }
16938
16939 // If another track is showing, disable this menu button
16940 if (disabled) {
16941 this.disable();
16942 } else {
16943 this.enable();
16944 }
16945 }
16946
16947 /**
16948 * Builds the default DOM `className`.
16949 *
16950 * @return {string}
16951 * The DOM `className` for this object.
16952 */
16953 buildCSSClass() {
16954 return `vjs-descriptions-button ${super.buildCSSClass()}`;
16955 }
16956 buildWrapperCSSClass() {
16957 return `vjs-descriptions-button ${super.buildWrapperCSSClass()}`;
16958 }
16959}
16960
16961/**
16962 * `kind` of TextTrack to look for to associate it with this menu.
16963 *
16964 * @type {string}
16965 * @private
16966 */
16967DescriptionsButton.prototype.kind_ = 'descriptions';
16968
16969/**
16970 * The text that should display over the `DescriptionsButton`s controls. Added for localization.
16971 *
16972 * @type {string}
16973 * @protected
16974 */
16975DescriptionsButton.prototype.controlText_ = 'Descriptions';
16976Component.registerComponent('DescriptionsButton', DescriptionsButton);
16977
16978/**
16979 * @file subtitles-button.js
16980 */
16981
16982/** @import Player from '../../player' */
16983
16984/**
16985 * The button component for toggling and selecting subtitles
16986 *
16987 * @extends TextTrackButton
16988 */
16989class SubtitlesButton extends TextTrackButton {
16990 /**
16991 * Creates an instance of this class.
16992 *
16993 * @param {Player} player
16994 * The `Player` that this class should be attached to.
16995 *
16996 * @param {Object} [options]
16997 * The key/value store of player options.
16998 *
16999 * @param {Function} [ready]
17000 * The function to call when this component is ready.
17001 */
17002 constructor(player, options, ready) {
17003 super(player, options, ready);
17004 this.setIcon('subtitles');
17005 }
17006
17007 /**
17008 * Builds the default DOM `className`.
17009 *
17010 * @return {string}
17011 * The DOM `className` for this object.
17012 */
17013 buildCSSClass() {
17014 return `vjs-subtitles-button ${super.buildCSSClass()}`;
17015 }
17016 buildWrapperCSSClass() {
17017 return `vjs-subtitles-button ${super.buildWrapperCSSClass()}`;
17018 }
17019}
17020
17021/**
17022 * `kind` of TextTrack to look for to associate it with this menu.
17023 *
17024 * @type {string}
17025 * @private
17026 */
17027SubtitlesButton.prototype.kind_ = 'subtitles';
17028
17029/**
17030 * The text that should display over the `SubtitlesButton`s controls. Added for localization.
17031 *
17032 * @type {string}
17033 * @protected
17034 */
17035SubtitlesButton.prototype.controlText_ = 'Subtitles';
17036Component.registerComponent('SubtitlesButton', SubtitlesButton);
17037
17038/**
17039 * @file caption-settings-menu-item.js
17040 */
17041
17042/** @import Player from '../../player' */
17043
17044/**
17045 * The menu item for caption track settings menu
17046 *
17047 * @extends TextTrackMenuItem
17048 */
17049class CaptionSettingsMenuItem extends TextTrackMenuItem {
17050 /**
17051 * Creates an instance of this class.
17052 *
17053 * @param {Player} player
17054 * The `Player` that this class should be attached to.
17055 *
17056 * @param {Object} [options]
17057 * The key/value store of player options.
17058 */
17059 constructor(player, options) {
17060 options.track = {
17061 player,
17062 kind: options.kind,
17063 label: options.kind + ' settings',
17064 selectable: false,
17065 default: false,
17066 mode: 'disabled'
17067 };
17068
17069 // CaptionSettingsMenuItem has no concept of 'selected'
17070 options.selectable = false;
17071 options.name = 'CaptionSettingsMenuItem';
17072 super(player, options);
17073 this.addClass('vjs-texttrack-settings');
17074 this.controlText(', opens ' + options.kind + ' settings dialog');
17075 }
17076
17077 /**
17078 * This gets called when an `CaptionSettingsMenuItem` is "clicked". See
17079 * {@link ClickableComponent} for more detailed information on what a click can be.
17080 *
17081 * @param {Event} [event]
17082 * The `keydown`, `tap`, or `click` event that caused this function to be
17083 * called.
17084 *
17085 * @listens tap
17086 * @listens click
17087 */
17088 handleClick(event) {
17089 this.player().getChild('textTrackSettings').open();
17090 }
17091
17092 /**
17093 * Update control text and label on languagechange
17094 */
17095 handleLanguagechange() {
17096 this.$('.vjs-menu-item-text').textContent = this.player_.localize(this.options_.kind + ' settings');
17097 super.handleLanguagechange();
17098 }
17099}
17100Component.registerComponent('CaptionSettingsMenuItem', CaptionSettingsMenuItem);
17101
17102/**
17103 * @file captions-button.js
17104 */
17105
17106/** @import Player from '../../player' */
17107
17108/**
17109 * The button component for toggling and selecting captions
17110 *
17111 * @extends TextTrackButton
17112 */
17113class CaptionsButton extends TextTrackButton {
17114 /**
17115 * Creates an instance of this class.
17116 *
17117 * @param {Player} player
17118 * The `Player` that this class should be attached to.
17119 *
17120 * @param {Object} [options]
17121 * The key/value store of player options.
17122 *
17123 * @param {Function} [ready]
17124 * The function to call when this component is ready.
17125 */
17126 constructor(player, options, ready) {
17127 super(player, options, ready);
17128 this.setIcon('captions');
17129 }
17130
17131 /**
17132 * Builds the default DOM `className`.
17133 *
17134 * @return {string}
17135 * The DOM `className` for this object.
17136 */
17137 buildCSSClass() {
17138 return `vjs-captions-button ${super.buildCSSClass()}`;
17139 }
17140 buildWrapperCSSClass() {
17141 return `vjs-captions-button ${super.buildWrapperCSSClass()}`;
17142 }
17143
17144 /**
17145 * Create caption menu items
17146 *
17147 * @return {CaptionSettingsMenuItem[]}
17148 * The array of current menu items.
17149 */
17150 createItems() {
17151 const items = [];
17152 if (!(this.player().tech_ && this.player().tech_.featuresNativeTextTracks) && this.player().getChild('textTrackSettings')) {
17153 items.push(new CaptionSettingsMenuItem(this.player_, {
17154 kind: this.kind_
17155 }));
17156 this.hideThreshold_ += 1;
17157 }
17158 return super.createItems(items);
17159 }
17160}
17161
17162/**
17163 * `kind` of TextTrack to look for to associate it with this menu.
17164 *
17165 * @type {string}
17166 * @private
17167 */
17168CaptionsButton.prototype.kind_ = 'captions';
17169
17170/**
17171 * The text that should display over the `CaptionsButton`s controls. Added for localization.
17172 *
17173 * @type {string}
17174 * @protected
17175 */
17176CaptionsButton.prototype.controlText_ = 'Captions';
17177Component.registerComponent('CaptionsButton', CaptionsButton);
17178
17179/**
17180 * @file subs-caps-menu-item.js
17181 */
17182
17183/**
17184 * SubsCapsMenuItem has an [cc] icon to distinguish captions from subtitles
17185 * in the SubsCapsMenu.
17186 *
17187 * @extends TextTrackMenuItem
17188 */
17189class SubsCapsMenuItem extends TextTrackMenuItem {
17190 createEl(type, props, attrs) {
17191 const el = super.createEl(type, props, attrs);
17192 const parentSpan = el.querySelector('.vjs-menu-item-text');
17193 if (this.options_.track.kind === 'captions') {
17194 if (this.player_.options_.experimentalSvgIcons) {
17195 this.setIcon('captions', el);
17196 } else {
17197 parentSpan.appendChild(createEl('span', {
17198 className: 'vjs-icon-placeholder'
17199 }, {
17200 'aria-hidden': true
17201 }));
17202 }
17203 parentSpan.appendChild(createEl('span', {
17204 className: 'vjs-control-text',
17205 // space added as the text will visually flow with the
17206 // label
17207 textContent: ` ${this.localize('Captions')}`
17208 }));
17209 }
17210 return el;
17211 }
17212}
17213Component.registerComponent('SubsCapsMenuItem', SubsCapsMenuItem);
17214
17215/**
17216 * @file sub-caps-button.js
17217 */
17218
17219/** @import Player from '../../player' */
17220
17221/**
17222 * The button component for toggling and selecting captions and/or subtitles
17223 *
17224 * @extends TextTrackButton
17225 */
17226class SubsCapsButton extends TextTrackButton {
17227 /**
17228 * Creates an instance of this class.
17229 *
17230 * @param {Player} player
17231 * The `Player` that this class should be attached to.
17232 *
17233 * @param {Object} [options]
17234 * The key/value store of player options.
17235 *
17236 * @param {Function} [ready]
17237 * The function to call when this component is ready.
17238 */
17239 constructor(player, options = {}) {
17240 super(player, options);
17241
17242 // Although North America uses "captions" in most cases for
17243 // "captions and subtitles" other locales use "subtitles"
17244 this.label_ = 'subtitles';
17245 this.setIcon('subtitles');
17246 if (['en', 'en-us', 'en-ca', 'fr-ca'].indexOf(this.player_.language_) > -1) {
17247 this.label_ = 'captions';
17248 this.setIcon('captions');
17249 }
17250 this.menuButton_.controlText(toTitleCase(this.label_));
17251 }
17252
17253 /**
17254 * Builds the default DOM `className`.
17255 *
17256 * @return {string}
17257 * The DOM `className` for this object.
17258 */
17259 buildCSSClass() {
17260 return `vjs-subs-caps-button ${super.buildCSSClass()}`;
17261 }
17262 buildWrapperCSSClass() {
17263 return `vjs-subs-caps-button ${super.buildWrapperCSSClass()}`;
17264 }
17265
17266 /**
17267 * Create caption/subtitles menu items
17268 *
17269 * @return {CaptionSettingsMenuItem[]}
17270 * The array of current menu items.
17271 */
17272 createItems() {
17273 let items = [];
17274 if (!(this.player().tech_ && this.player().tech_.featuresNativeTextTracks) && this.player().getChild('textTrackSettings')) {
17275 items.push(new CaptionSettingsMenuItem(this.player_, {
17276 kind: this.label_
17277 }));
17278 this.hideThreshold_ += 1;
17279 }
17280 items = super.createItems(items, SubsCapsMenuItem);
17281 return items;
17282 }
17283}
17284
17285/**
17286 * `kind`s of TextTrack to look for to associate it with this menu.
17287 *
17288 * @type {array}
17289 * @private
17290 */
17291SubsCapsButton.prototype.kinds_ = ['captions', 'subtitles'];
17292
17293/**
17294 * The text that should display over the `SubsCapsButton`s controls.
17295 *
17296 *
17297 * @type {string}
17298 * @protected
17299 */
17300SubsCapsButton.prototype.controlText_ = 'Subtitles';
17301Component.registerComponent('SubsCapsButton', SubsCapsButton);
17302
17303/**
17304 * @file audio-track-menu-item.js
17305 */
17306
17307/** @import Player from '../../player' */
17308
17309/**
17310 * An {@link AudioTrack} {@link MenuItem}
17311 *
17312 * @extends MenuItem
17313 */
17314class AudioTrackMenuItem extends MenuItem {
17315 /**
17316 * Creates an instance of this class.
17317 *
17318 * @param {Player} player
17319 * The `Player` that this class should be attached to.
17320 *
17321 * @param {Object} [options]
17322 * The key/value store of player options.
17323 */
17324 constructor(player, options) {
17325 const track = options.track;
17326 const tracks = player.audioTracks();
17327
17328 // Modify options for parent MenuItem class's init.
17329 options.label = track.label || track.language || 'Unknown';
17330 options.selected = track.enabled;
17331 super(player, options);
17332 this.track = track;
17333 this.addClass(`vjs-${track.kind}-menu-item`);
17334 const changeHandler = (...args) => {
17335 this.handleTracksChange.apply(this, args);
17336 };
17337 tracks.addEventListener('change', changeHandler);
17338 this.on('dispose', () => {
17339 tracks.removeEventListener('change', changeHandler);
17340 });
17341 }
17342 createEl(type, props, attrs) {
17343 const el = super.createEl(type, props, attrs);
17344 const parentSpan = el.querySelector('.vjs-menu-item-text');
17345 if (['main-desc', 'descriptions'].indexOf(this.options_.track.kind) >= 0) {
17346 parentSpan.appendChild(createEl('span', {
17347 className: 'vjs-icon-placeholder'
17348 }, {
17349 'aria-hidden': true
17350 }));
17351 parentSpan.appendChild(createEl('span', {
17352 className: 'vjs-control-text',
17353 textContent: ' ' + this.localize('Descriptions')
17354 }));
17355 }
17356 return el;
17357 }
17358
17359 /**
17360 * This gets called when an `AudioTrackMenuItem is "clicked". See {@link ClickableComponent}
17361 * for more detailed information on what a click can be.
17362 *
17363 * @param {Event} [event]
17364 * The `keydown`, `tap`, or `click` event that caused this function to be
17365 * called.
17366 *
17367 * @listens tap
17368 * @listens click
17369 */
17370 handleClick(event) {
17371 super.handleClick(event);
17372
17373 // the audio track list will automatically toggle other tracks
17374 // off for us.
17375 this.track.enabled = true;
17376
17377 // when native audio tracks are used, we want to make sure that other tracks are turned off
17378 if (this.player_.tech_.featuresNativeAudioTracks) {
17379 const tracks = this.player_.audioTracks();
17380 for (let i = 0; i < tracks.length; i++) {
17381 const track = tracks[i];
17382
17383 // skip the current track since we enabled it above
17384 if (track === this.track) {
17385 continue;
17386 }
17387 track.enabled = track === this.track;
17388 }
17389 }
17390 }
17391
17392 /**
17393 * Handle any {@link AudioTrack} change.
17394 *
17395 * @param {Event} [event]
17396 * The {@link AudioTrackList#change} event that caused this to run.
17397 *
17398 * @listens AudioTrackList#change
17399 */
17400 handleTracksChange(event) {
17401 this.selected(this.track.enabled);
17402 }
17403}
17404Component.registerComponent('AudioTrackMenuItem', AudioTrackMenuItem);
17405
17406/**
17407 * @file audio-track-button.js
17408 */
17409
17410/**
17411 * The base class for buttons that toggle specific {@link AudioTrack} types.
17412 *
17413 * @extends TrackButton
17414 */
17415class AudioTrackButton extends TrackButton {
17416 /**
17417 * Creates an instance of this class.
17418 *
17419 * @param {Player} player
17420 * The `Player` that this class should be attached to.
17421 *
17422 * @param {Object} [options={}]
17423 * The key/value store of player options.
17424 */
17425 constructor(player, options = {}) {
17426 options.tracks = player.audioTracks();
17427 super(player, options);
17428 this.setIcon('audio');
17429 }
17430
17431 /**
17432 * Builds the default DOM `className`.
17433 *
17434 * @return {string}
17435 * The DOM `className` for this object.
17436 */
17437 buildCSSClass() {
17438 return `vjs-audio-button ${super.buildCSSClass()}`;
17439 }
17440 buildWrapperCSSClass() {
17441 return `vjs-audio-button ${super.buildWrapperCSSClass()}`;
17442 }
17443
17444 /**
17445 * Create a menu item for each audio track
17446 *
17447 * @param {AudioTrackMenuItem[]} [items=[]]
17448 * An array of existing menu items to use.
17449 *
17450 * @return {AudioTrackMenuItem[]}
17451 * An array of menu items
17452 */
17453 createItems(items = []) {
17454 // if there's only one audio track, there no point in showing it
17455 this.hideThreshold_ = 1;
17456 const tracks = this.player_.audioTracks();
17457 for (let i = 0; i < tracks.length; i++) {
17458 const track = tracks[i];
17459 items.push(new AudioTrackMenuItem(this.player_, {
17460 track,
17461 // MenuItem is selectable
17462 selectable: true,
17463 // MenuItem is NOT multiSelectable (i.e. only one can be marked "selected" at a time)
17464 multiSelectable: false
17465 }));
17466 }
17467 return items;
17468 }
17469}
17470
17471/**
17472 * The text that should display over the `AudioTrackButton`s controls. Added for localization.
17473 *
17474 * @type {string}
17475 * @protected
17476 */
17477AudioTrackButton.prototype.controlText_ = 'Audio Track';
17478Component.registerComponent('AudioTrackButton', AudioTrackButton);
17479
17480/**
17481 * @file playback-rate-menu-item.js
17482 */
17483
17484/** @import Player from '../../player' */
17485
17486/**
17487 * The specific menu item type for selecting a playback rate.
17488 *
17489 * @extends MenuItem
17490 */
17491class PlaybackRateMenuItem extends MenuItem {
17492 /**
17493 * Creates an instance of this class.
17494 *
17495 * @param {Player} player
17496 * The `Player` that this class should be attached to.
17497 *
17498 * @param {Object} [options]
17499 * The key/value store of player options.
17500 */
17501 constructor(player, options) {
17502 const label = options.rate;
17503 const rate = parseFloat(label, 10);
17504
17505 // Modify options for parent MenuItem class's init.
17506 options.label = label;
17507 options.selected = rate === player.playbackRate();
17508 options.selectable = true;
17509 options.multiSelectable = false;
17510 super(player, options);
17511 this.label = label;
17512 this.rate = rate;
17513 this.on(player, 'ratechange', e => this.update(e));
17514 }
17515
17516 /**
17517 * This gets called when an `PlaybackRateMenuItem` is "clicked". See
17518 * {@link ClickableComponent} for more detailed information on what a click can be.
17519 *
17520 * @param {Event} [event]
17521 * The `keydown`, `tap`, or `click` event that caused this function to be
17522 * called.
17523 *
17524 * @listens tap
17525 * @listens click
17526 */
17527 handleClick(event) {
17528 super.handleClick();
17529 this.player().playbackRate(this.rate);
17530 }
17531
17532 /**
17533 * Update the PlaybackRateMenuItem when the playbackrate changes.
17534 *
17535 * @param {Event} [event]
17536 * The `ratechange` event that caused this function to run.
17537 *
17538 * @listens Player#ratechange
17539 */
17540 update(event) {
17541 this.selected(this.player().playbackRate() === this.rate);
17542 }
17543}
17544
17545/**
17546 * The text that should display over the `PlaybackRateMenuItem`s controls. Added for localization.
17547 *
17548 * @type {string}
17549 * @private
17550 */
17551PlaybackRateMenuItem.prototype.contentElType = 'button';
17552Component.registerComponent('PlaybackRateMenuItem', PlaybackRateMenuItem);
17553
17554/**
17555 * @file playback-rate-menu-button.js
17556 */
17557
17558/** @import Player from '../../player' */
17559
17560/**
17561 * The component for controlling the playback rate.
17562 *
17563 * @extends MenuButton
17564 */
17565class PlaybackRateMenuButton extends MenuButton {
17566 /**
17567 * Creates an instance of this class.
17568 *
17569 * @param {Player} player
17570 * The `Player` that this class should be attached to.
17571 *
17572 * @param {Object} [options]
17573 * The key/value store of player options.
17574 */
17575 constructor(player, options) {
17576 super(player, options);
17577 this.menuButton_.el_.setAttribute('aria-describedby', this.labelElId_);
17578 this.updateVisibility();
17579 this.updateLabel();
17580 this.on(player, 'loadstart', e => this.updateVisibility(e));
17581 this.on(player, 'ratechange', e => this.updateLabel(e));
17582 this.on(player, 'playbackrateschange', e => this.handlePlaybackRateschange(e));
17583 }
17584
17585 /**
17586 * Create the `Component`'s DOM element
17587 *
17588 * @return {Element}
17589 * The element that was created.
17590 */
17591 createEl() {
17592 const el = super.createEl();
17593 this.labelElId_ = 'vjs-playback-rate-value-label-' + this.id_;
17594 this.labelEl_ = createEl('div', {
17595 className: 'vjs-playback-rate-value',
17596 id: this.labelElId_,
17597 textContent: '1x'
17598 });
17599 el.appendChild(this.labelEl_);
17600 return el;
17601 }
17602 dispose() {
17603 this.labelEl_ = null;
17604 super.dispose();
17605 }
17606
17607 /**
17608 * Builds the default DOM `className`.
17609 *
17610 * @return {string}
17611 * The DOM `className` for this object.
17612 */
17613 buildCSSClass() {
17614 return `vjs-playback-rate ${super.buildCSSClass()}`;
17615 }
17616 buildWrapperCSSClass() {
17617 return `vjs-playback-rate ${super.buildWrapperCSSClass()}`;
17618 }
17619
17620 /**
17621 * Create the list of menu items. Specific to each subclass.
17622 *
17623 */
17624 createItems() {
17625 const rates = this.playbackRates();
17626 const items = [];
17627 for (let i = rates.length - 1; i >= 0; i--) {
17628 items.push(new PlaybackRateMenuItem(this.player(), {
17629 rate: rates[i] + 'x'
17630 }));
17631 }
17632 return items;
17633 }
17634
17635 /**
17636 * On playbackrateschange, update the menu to account for the new items.
17637 *
17638 * @listens Player#playbackrateschange
17639 */
17640 handlePlaybackRateschange(event) {
17641 this.update();
17642 }
17643
17644 /**
17645 * Get possible playback rates
17646 *
17647 * @return {Array}
17648 * All possible playback rates
17649 */
17650 playbackRates() {
17651 const player = this.player();
17652 return player.playbackRates && player.playbackRates() || [];
17653 }
17654
17655 /**
17656 * Get whether playback rates is supported by the tech
17657 * and an array of playback rates exists
17658 *
17659 * @return {boolean}
17660 * Whether changing playback rate is supported
17661 */
17662 playbackRateSupported() {
17663 return this.player().tech_ && this.player().tech_.featuresPlaybackRate && this.playbackRates() && this.playbackRates().length > 0;
17664 }
17665
17666 /**
17667 * Hide playback rate controls when they're no playback rate options to select
17668 *
17669 * @param {Event} [event]
17670 * The event that caused this function to run.
17671 *
17672 * @listens Player#loadstart
17673 */
17674 updateVisibility(event) {
17675 if (this.playbackRateSupported()) {
17676 this.removeClass('vjs-hidden');
17677 } else {
17678 this.addClass('vjs-hidden');
17679 }
17680 }
17681
17682 /**
17683 * Update button label when rate changed
17684 *
17685 * @param {Event} [event]
17686 * The event that caused this function to run.
17687 *
17688 * @listens Player#ratechange
17689 */
17690 updateLabel(event) {
17691 if (this.playbackRateSupported()) {
17692 this.labelEl_.textContent = this.player().playbackRate() + 'x';
17693 }
17694 }
17695}
17696
17697/**
17698 * The text that should display over the `PlaybackRateMenuButton`s controls.
17699 *
17700 * Added for localization.
17701 *
17702 * @type {string}
17703 * @protected
17704 */
17705PlaybackRateMenuButton.prototype.controlText_ = 'Playback Rate';
17706Component.registerComponent('PlaybackRateMenuButton', PlaybackRateMenuButton);
17707
17708/**
17709 * @file spacer.js
17710 */
17711
17712/**
17713 * Just an empty spacer element that can be used as an append point for plugins, etc.
17714 * Also can be used to create space between elements when necessary.
17715 *
17716 * @extends Component
17717 */
17718class Spacer extends Component {
17719 /**
17720 * Builds the default DOM `className`.
17721 *
17722 * @return {string}
17723 * The DOM `className` for this object.
17724 */
17725 buildCSSClass() {
17726 return `vjs-spacer ${super.buildCSSClass()}`;
17727 }
17728
17729 /**
17730 * Create the `Component`'s DOM element
17731 *
17732 * @return {Element}
17733 * The element that was created.
17734 */
17735 createEl(tag = 'div', props = {}, attributes = {}) {
17736 if (!props.className) {
17737 props.className = this.buildCSSClass();
17738 }
17739 return super.createEl(tag, props, attributes);
17740 }
17741}
17742Component.registerComponent('Spacer', Spacer);
17743
17744/**
17745 * @file custom-control-spacer.js
17746 */
17747
17748/**
17749 * Spacer specifically meant to be used as an insertion point for new plugins, etc.
17750 *
17751 * @extends Spacer
17752 */
17753class CustomControlSpacer extends Spacer {
17754 /**
17755 * Builds the default DOM `className`.
17756 *
17757 * @return {string}
17758 * The DOM `className` for this object.
17759 */
17760 buildCSSClass() {
17761 return `vjs-custom-control-spacer ${super.buildCSSClass()}`;
17762 }
17763
17764 /**
17765 * Create the `Component`'s DOM element
17766 *
17767 * @return {Element}
17768 * The element that was created.
17769 */
17770 createEl() {
17771 return super.createEl('div', {
17772 className: this.buildCSSClass(),
17773 // No-flex/table-cell mode requires there be some content
17774 // in the cell to fill the remaining space of the table.
17775 textContent: '\u00a0'
17776 });
17777 }
17778}
17779Component.registerComponent('CustomControlSpacer', CustomControlSpacer);
17780
17781/**
17782 * @file control-bar.js
17783 */
17784
17785/**
17786 * Container of main controls.
17787 *
17788 * @extends Component
17789 */
17790class ControlBar extends Component {
17791 /**
17792 * Create the `Component`'s DOM element
17793 *
17794 * @return {Element}
17795 * The element that was created.
17796 */
17797 createEl() {
17798 return super.createEl('div', {
17799 className: 'vjs-control-bar',
17800 dir: 'ltr'
17801 });
17802 }
17803}
17804
17805/**
17806 * Default options for `ControlBar`
17807 *
17808 * @type {Object}
17809 * @private
17810 */
17811ControlBar.prototype.options_ = {
17812 children: ['playToggle', 'skipBackward', 'skipForward', 'volumePanel', 'currentTimeDisplay', 'timeDivider', 'durationDisplay', 'progressControl', 'liveDisplay', 'seekToLive', 'remainingTimeDisplay', 'customControlSpacer', 'playbackRateMenuButton', 'chaptersButton', 'descriptionsButton', 'subsCapsButton', 'audioTrackButton', 'pictureInPictureToggle', 'fullscreenToggle']
17813};
17814Component.registerComponent('ControlBar', ControlBar);
17815
17816/**
17817 * @file error-display.js
17818 */
17819
17820/** @import Player from './player' */
17821
17822/**
17823 * A display that indicates an error has occurred. This means that the video
17824 * is unplayable.
17825 *
17826 * @extends ModalDialog
17827 */
17828class ErrorDisplay extends ModalDialog {
17829 /**
17830 * Creates an instance of this class.
17831 *
17832 * @param {Player} player
17833 * The `Player` that this class should be attached to.
17834 *
17835 * @param {Object} [options]
17836 * The key/value store of player options.
17837 */
17838 constructor(player, options) {
17839 super(player, options);
17840 this.on(player, 'error', e => {
17841 this.open(e);
17842 });
17843 }
17844
17845 /**
17846 * Builds the default DOM `className`.
17847 *
17848 * @return {string}
17849 * The DOM `className` for this object.
17850 *
17851 * @deprecated Since version 5.
17852 */
17853 buildCSSClass() {
17854 return `vjs-error-display ${super.buildCSSClass()}`;
17855 }
17856
17857 /**
17858 * Gets the localized error message based on the `Player`s error.
17859 *
17860 * @return {string}
17861 * The `Player`s error message localized or an empty string.
17862 */
17863 content() {
17864 const error = this.player().error();
17865 return error ? this.localize(error.message) : '';
17866 }
17867}
17868
17869/**
17870 * The default options for an `ErrorDisplay`.
17871 *
17872 * @private
17873 */
17874ErrorDisplay.prototype.options_ = Object.assign({}, ModalDialog.prototype.options_, {
17875 pauseOnOpen: false,
17876 fillAlways: true,
17877 temporary: false,
17878 uncloseable: true
17879});
17880Component.registerComponent('ErrorDisplay', ErrorDisplay);
17881
17882/** @import Player from './player' */
17883/** @import { ContentDescriptor } from '../utils/dom' */
17884
17885/**
17886 * Creates DOM element of 'select' & its options.
17887 *
17888 * @extends Component
17889 */
17890class TextTrackSelect extends Component {
17891 /**
17892 * Creates an instance of this class.
17893 *
17894 * @param {Player} player
17895 * The `Player` that this class should be attached to.
17896 *
17897 * @param {Object} [options]
17898 * The key/value store of player options.
17899 *
17900 * @param {ContentDescriptor} [options.content=undefined]
17901 * Provide customized content for this modal.
17902 *
17903 * @param {string} [options.legendId]
17904 * A text with part of an string to create atribute of aria-labelledby.
17905 *
17906 * @param {string} [options.id]
17907 * A text with part of an string to create atribute of aria-labelledby.
17908 *
17909 * @param {Array} [options.SelectOptions]
17910 * Array that contains the value & textContent of for each of the
17911 * options elements.
17912 */
17913 constructor(player, options = {}) {
17914 super(player, options);
17915 this.el_.setAttribute('aria-labelledby', this.selectLabelledbyIds);
17916 }
17917
17918 /**
17919 * Create the `TextTrackSelect`'s DOM element
17920 *
17921 * @return {Element}
17922 * The DOM element that gets created.
17923 */
17924 createEl() {
17925 this.selectLabelledbyIds = [this.options_.legendId, this.options_.labelId].join(' ').trim();
17926
17927 // Create select & inner options
17928 const selectoptions = createEl('select', {
17929 id: this.options_.id
17930 }, {}, this.options_.SelectOptions.map(optionText => {
17931 // Constructs an id for the <option>.
17932 // For the colour settings that have two <selects> with a <label> each, generates an id based off the label value
17933 // For font size/family and edge style with one <select> and no <label>, generates an id with a guid
17934 const optionId = (this.options_.labelId ? this.options_.labelId : `vjs-track-option-${newGUID()}`) + '-' + optionText[1].replace(/\W+/g, '');
17935 const option = createEl('option', {
17936 id: optionId,
17937 value: this.localize(optionText[0]),
17938 textContent: this.localize(optionText[1])
17939 });
17940 option.setAttribute('aria-labelledby', `${this.selectLabelledbyIds} ${optionId}`);
17941 return option;
17942 }));
17943 return selectoptions;
17944 }
17945}
17946Component.registerComponent('TextTrackSelect', TextTrackSelect);
17947
17948/** @import Player from './player' */
17949/** @import { ContentDescriptor } from '../utils/dom' */
17950
17951/**
17952 * Creates fieldset section of 'TextTrackSettings'.
17953 * Manganes two versions of fieldsets, one for type of 'colors'
17954 * & the other for 'font', Component adds diferent DOM elements
17955 * to that fieldset depending on the type.
17956 *
17957 * @extends Component
17958 */
17959class TextTrackFieldset extends Component {
17960 /**
17961 * Creates an instance of this class.
17962 *
17963 * @param {Player} player
17964 * The `Player` that this class should be attached to.
17965 *
17966 * @param {Object} [options]
17967 * The key/value store of player options.
17968 *
17969 * @param {ContentDescriptor} [options.content=undefined]
17970 * Provide customized content for this modal.
17971 *
17972 * @param {string} [options.legendId]
17973 * A text with part of an string to create atribute of aria-labelledby.
17974 * It passes to 'TextTrackSelect'.
17975 *
17976 * @param {string} [options.id]
17977 * A text with part of an string to create atribute of aria-labelledby.
17978 * It passes to 'TextTrackSelect'.
17979 *
17980 * @param {string} [options.legendText]
17981 * A text to use as the text content of the legend element.
17982 *
17983 * @param {Array} [options.selects]
17984 * Array that contains the selects that are use to create 'selects'
17985 * components.
17986 *
17987 * @param {Array} [options.SelectOptions]
17988 * Array that contains the value & textContent of for each of the
17989 * options elements, it passes to 'TextTrackSelect'.
17990 *
17991 * @param {string} [options.type]
17992 * Conditions if some DOM elements will be added to the fieldset
17993 * component.
17994 *
17995 * @param {Object} [options.selectConfigs]
17996 * Object with the following properties that are the selects configurations:
17997 * backgroundColor, backgroundOpacity, color, edgeStyle, fontFamily,
17998 * fontPercent, textOpacity, windowColor, windowOpacity.
17999 * These properties are use to configure the 'TextTrackSelect' Component.
18000 */
18001 constructor(player, options = {}) {
18002 super(player, options);
18003
18004 // Add Components & DOM Elements
18005 const legendElement = createEl('legend', {
18006 textContent: this.localize(this.options_.legendText),
18007 id: this.options_.legendId
18008 });
18009 this.el().appendChild(legendElement);
18010 const selects = this.options_.selects;
18011
18012 // Iterate array of selects to create 'selects' components
18013 for (const i of selects) {
18014 const selectConfig = this.options_.selectConfigs[i];
18015 const selectClassName = selectConfig.className;
18016 const id = selectConfig.id.replace('%s', this.options_.id_);
18017 let span = null;
18018 const guid = `vjs_select_${newGUID()}`;
18019
18020 // Conditionally create span to add on the component
18021 if (this.options_.type === 'colors') {
18022 span = createEl('span', {
18023 className: selectClassName
18024 });
18025 const label = createEl('label', {
18026 id,
18027 className: 'vjs-label',
18028 textContent: this.localize(selectConfig.label)
18029 });
18030 label.setAttribute('for', guid);
18031 span.appendChild(label);
18032 }
18033 const textTrackSelect = new TextTrackSelect(player, {
18034 SelectOptions: selectConfig.options,
18035 legendId: this.options_.legendId,
18036 id: guid,
18037 labelId: id
18038 });
18039 this.addChild(textTrackSelect);
18040
18041 // Conditionally append to 'select' component to conditionally created span
18042 if (this.options_.type === 'colors') {
18043 span.appendChild(textTrackSelect.el());
18044 this.el().appendChild(span);
18045 }
18046 }
18047 }
18048
18049 /**
18050 * Create the `TextTrackFieldset`'s DOM element
18051 *
18052 * @return {Element}
18053 * The DOM element that gets created.
18054 */
18055 createEl() {
18056 const el = createEl('fieldset', {
18057 // Prefixing classes of elements within a player with "vjs-"
18058 // is a convention used in Video.js.
18059 className: this.options_.className
18060 });
18061 return el;
18062 }
18063}
18064Component.registerComponent('TextTrackFieldset', TextTrackFieldset);
18065
18066/** @import Player from './player' */
18067/** @import { ContentDescriptor } from '../utils/dom' */
18068
18069/**
18070 * The component 'TextTrackSettingsColors' displays a set of 'fieldsets'
18071 * using the component 'TextTrackFieldset'.
18072 *
18073 * @extends Component
18074 */
18075class TextTrackSettingsColors extends Component {
18076 /**
18077 * Creates an instance of this class.
18078 *
18079 * @param {Player} player
18080 * The `Player` that this class should be attached to.
18081 *
18082 * @param {Object} [options]
18083 * The key/value store of player options.
18084 *
18085 * @param {ContentDescriptor} [options.content=undefined]
18086 * Provide customized content for this modal.
18087 *
18088 * @param {Array} [options.fieldSets]
18089 * Array that contains the configurations for the selects.
18090 *
18091 * @param {Object} [options.selectConfigs]
18092 * Object with the following properties that are the select confugations:
18093 * backgroundColor, backgroundOpacity, color, edgeStyle, fontFamily,
18094 * fontPercent, textOpacity, windowColor, windowOpacity.
18095 * it passes to 'TextTrackFieldset'.
18096 */
18097 constructor(player, options = {}) {
18098 super(player, options);
18099 const id_ = this.options_.textTrackComponentid;
18100
18101 // createElFgColor_
18102 const ElFgColorFieldset = new TextTrackFieldset(player, {
18103 id_,
18104 legendId: `captions-text-legend-${id_}`,
18105 legendText: this.localize('Text'),
18106 className: 'vjs-fg vjs-track-setting',
18107 selects: this.options_.fieldSets[0],
18108 selectConfigs: this.options_.selectConfigs,
18109 type: 'colors'
18110 });
18111 this.addChild(ElFgColorFieldset);
18112
18113 // createElBgColor_
18114 const ElBgColorFieldset = new TextTrackFieldset(player, {
18115 id_,
18116 legendId: `captions-background-${id_}`,
18117 legendText: this.localize('Text Background'),
18118 className: 'vjs-bg vjs-track-setting',
18119 selects: this.options_.fieldSets[1],
18120 selectConfigs: this.options_.selectConfigs,
18121 type: 'colors'
18122 });
18123 this.addChild(ElBgColorFieldset);
18124
18125 // createElWinColor_
18126 const ElWinColorFieldset = new TextTrackFieldset(player, {
18127 id_,
18128 legendId: `captions-window-${id_}`,
18129 legendText: this.localize('Caption Area Background'),
18130 className: 'vjs-window vjs-track-setting',
18131 selects: this.options_.fieldSets[2],
18132 selectConfigs: this.options_.selectConfigs,
18133 type: 'colors'
18134 });
18135 this.addChild(ElWinColorFieldset);
18136 }
18137
18138 /**
18139 * Create the `TextTrackSettingsColors`'s DOM element
18140 *
18141 * @return {Element}
18142 * The DOM element that gets created.
18143 */
18144 createEl() {
18145 const el = createEl('div', {
18146 className: 'vjs-track-settings-colors'
18147 });
18148 return el;
18149 }
18150}
18151Component.registerComponent('TextTrackSettingsColors', TextTrackSettingsColors);
18152
18153/** @import Player from './player' */
18154/** @import { ContentDescriptor } from '../utils/dom' */
18155
18156/**
18157 * The component 'TextTrackSettingsFont' displays a set of 'fieldsets'
18158 * using the component 'TextTrackFieldset'.
18159 *
18160 * @extends Component
18161 */
18162class TextTrackSettingsFont extends Component {
18163 /**
18164 * Creates an instance of this class.
18165 *
18166 * @param {Player} player
18167 * The `Player` that this class should be attached to.
18168 *
18169 * @param {Object} [options]
18170 * The key/value store of player options.
18171 *
18172 * @param {ContentDescriptor} [options.content=undefined]
18173 * Provide customized content for this modal.
18174 *
18175 * @param {Array} [options.fieldSets]
18176 * Array that contains the configurations for the selects.
18177 *
18178 * @param {Object} [options.selectConfigs]
18179 * Object with the following properties that are the select confugations:
18180 * backgroundColor, backgroundOpacity, color, edgeStyle, fontFamily,
18181 * fontPercent, textOpacity, windowColor, windowOpacity.
18182 * it passes to 'TextTrackFieldset'.
18183 */
18184 constructor(player, options = {}) {
18185 super(player, options);
18186 const id_ = this.options_.textTrackComponentid;
18187 const ElFgColorFieldset = new TextTrackFieldset(player, {
18188 id_,
18189 legendId: `captions-font-size-${id_}`,
18190 legendText: 'Font Size',
18191 className: 'vjs-font-percent vjs-track-setting',
18192 selects: this.options_.fieldSets[0],
18193 selectConfigs: this.options_.selectConfigs,
18194 type: 'font'
18195 });
18196 this.addChild(ElFgColorFieldset);
18197 const ElBgColorFieldset = new TextTrackFieldset(player, {
18198 id_,
18199 legendId: `captions-edge-style-${id_}`,
18200 legendText: this.localize('Text Edge Style'),
18201 className: 'vjs-edge-style vjs-track-setting',
18202 selects: this.options_.fieldSets[1],
18203 selectConfigs: this.options_.selectConfigs,
18204 type: 'font'
18205 });
18206 this.addChild(ElBgColorFieldset);
18207 const ElWinColorFieldset = new TextTrackFieldset(player, {
18208 id_,
18209 legendId: `captions-font-family-${id_}`,
18210 legendText: this.localize('Font Family'),
18211 className: 'vjs-font-family vjs-track-setting',
18212 selects: this.options_.fieldSets[2],
18213 selectConfigs: this.options_.selectConfigs,
18214 type: 'font'
18215 });
18216 this.addChild(ElWinColorFieldset);
18217 }
18218
18219 /**
18220 * Create the `TextTrackSettingsFont`'s DOM element
18221 *
18222 * @return {Element}
18223 * The DOM element that gets created.
18224 */
18225 createEl() {
18226 const el = createEl('div', {
18227 className: 'vjs-track-settings-font'
18228 });
18229 return el;
18230 }
18231}
18232Component.registerComponent('TextTrackSettingsFont', TextTrackSettingsFont);
18233
18234/**
18235 * Buttons of reset & done that modal 'TextTrackSettings'
18236 * uses as part of its content.
18237 *
18238 * 'Reset': Resets all settings on 'TextTrackSettings'.
18239 * 'Done': Closes 'TextTrackSettings' modal.
18240 *
18241 * @extends Component
18242 */
18243class TrackSettingsControls extends Component {
18244 constructor(player, options = {}) {
18245 super(player, options);
18246
18247 // Create DOM elements
18248 const defaultsDescription = this.localize('restore all settings to the default values');
18249 const resetButton = new Button(player, {
18250 controlText: defaultsDescription,
18251 className: 'vjs-default-button'
18252 });
18253 resetButton.el().classList.remove('vjs-control', 'vjs-button');
18254 resetButton.el().textContent = this.localize('Reset');
18255 this.addChild(resetButton);
18256 const doneButton = new Button(player, {
18257 controlText: defaultsDescription,
18258 className: 'vjs-done-button'
18259 });
18260
18261 // Remove unrequired style classes
18262 doneButton.el().classList.remove('vjs-control', 'vjs-button');
18263 doneButton.el().textContent = this.localize('Done');
18264 this.addChild(doneButton);
18265 }
18266
18267 /**
18268 * Create the `TrackSettingsControls`'s DOM element
18269 *
18270 * @return {Element}
18271 * The DOM element that gets created.
18272 */
18273 createEl() {
18274 const el = createEl('div', {
18275 className: 'vjs-track-settings-controls'
18276 });
18277 return el;
18278 }
18279}
18280Component.registerComponent('TrackSettingsControls', TrackSettingsControls);
18281
18282/**
18283 * @file text-track-settings.js
18284 */
18285
18286/** @import Player from '../player' */
18287
18288const LOCAL_STORAGE_KEY = 'vjs-text-track-settings';
18289const COLOR_BLACK = ['#000', 'Black'];
18290const COLOR_BLUE = ['#00F', 'Blue'];
18291const COLOR_CYAN = ['#0FF', 'Cyan'];
18292const COLOR_GREEN = ['#0F0', 'Green'];
18293const COLOR_MAGENTA = ['#F0F', 'Magenta'];
18294const COLOR_RED = ['#F00', 'Red'];
18295const COLOR_WHITE = ['#FFF', 'White'];
18296const COLOR_YELLOW = ['#FF0', 'Yellow'];
18297const OPACITY_OPAQUE = ['1', 'Opaque'];
18298const OPACITY_SEMI = ['0.5', 'Semi-Transparent'];
18299const OPACITY_TRANS = ['0', 'Transparent'];
18300
18301// Configuration for the various <select> elements in the DOM of this component.
18302//
18303// Possible keys include:
18304//
18305// `default`:
18306// The default option index. Only needs to be provided if not zero.
18307// `parser`:
18308// A function which is used to parse the value from the selected option in
18309// a customized way.
18310// `selector`:
18311// The selector used to find the associated <select> element.
18312const selectConfigs = {
18313 backgroundColor: {
18314 selector: '.vjs-bg-color > select',
18315 id: 'captions-background-color-%s',
18316 label: 'Color',
18317 options: [COLOR_BLACK, COLOR_WHITE, COLOR_RED, COLOR_GREEN, COLOR_BLUE, COLOR_YELLOW, COLOR_MAGENTA, COLOR_CYAN],
18318 className: 'vjs-bg-color'
18319 },
18320 backgroundOpacity: {
18321 selector: '.vjs-bg-opacity > select',
18322 id: 'captions-background-opacity-%s',
18323 label: 'Opacity',
18324 options: [OPACITY_OPAQUE, OPACITY_SEMI, OPACITY_TRANS],
18325 className: 'vjs-bg-opacity vjs-opacity'
18326 },
18327 color: {
18328 selector: '.vjs-text-color > select',
18329 id: 'captions-foreground-color-%s',
18330 label: 'Color',
18331 options: [COLOR_WHITE, COLOR_BLACK, COLOR_RED, COLOR_GREEN, COLOR_BLUE, COLOR_YELLOW, COLOR_MAGENTA, COLOR_CYAN],
18332 className: 'vjs-text-color'
18333 },
18334 edgeStyle: {
18335 selector: '.vjs-edge-style > select',
18336 id: '',
18337 label: 'Text Edge Style',
18338 options: [['none', 'None'], ['raised', 'Raised'], ['depressed', 'Depressed'], ['uniform', 'Uniform'], ['dropshadow', 'Drop shadow']]
18339 },
18340 fontFamily: {
18341 selector: '.vjs-font-family > select',
18342 id: '',
18343 label: 'Font Family',
18344 options: [['proportionalSansSerif', 'Proportional Sans-Serif'], ['monospaceSansSerif', 'Monospace Sans-Serif'], ['proportionalSerif', 'Proportional Serif'], ['monospaceSerif', 'Monospace Serif'], ['casual', 'Casual'], ['script', 'Script'], ['small-caps', 'Small Caps']]
18345 },
18346 fontPercent: {
18347 selector: '.vjs-font-percent > select',
18348 id: '',
18349 label: 'Font Size',
18350 options: [['0.50', '50%'], ['0.75', '75%'], ['1.00', '100%'], ['1.25', '125%'], ['1.50', '150%'], ['1.75', '175%'], ['2.00', '200%'], ['3.00', '300%'], ['4.00', '400%']],
18351 default: 2,
18352 parser: v => v === '1.00' ? null : Number(v)
18353 },
18354 textOpacity: {
18355 selector: '.vjs-text-opacity > select',
18356 id: 'captions-foreground-opacity-%s',
18357 label: 'Opacity',
18358 options: [OPACITY_OPAQUE, OPACITY_SEMI],
18359 className: 'vjs-text-opacity vjs-opacity'
18360 },
18361 // Options for this object are defined below.
18362 windowColor: {
18363 selector: '.vjs-window-color > select',
18364 id: 'captions-window-color-%s',
18365 label: 'Color',
18366 className: 'vjs-window-color'
18367 },
18368 // Options for this object are defined below.
18369 windowOpacity: {
18370 selector: '.vjs-window-opacity > select',
18371 id: 'captions-window-opacity-%s',
18372 label: 'Opacity',
18373 options: [OPACITY_TRANS, OPACITY_SEMI, OPACITY_OPAQUE],
18374 className: 'vjs-window-opacity vjs-opacity'
18375 }
18376};
18377selectConfigs.windowColor.options = selectConfigs.backgroundColor.options;
18378
18379/**
18380 * Get the actual value of an option.
18381 *
18382 * @param {string} value
18383 * The value to get
18384 *
18385 * @param {Function} [parser]
18386 * Optional function to adjust the value.
18387 *
18388 * @return {*}
18389 * - Will be `undefined` if no value exists
18390 * - Will be `undefined` if the given value is "none".
18391 * - Will be the actual value otherwise.
18392 *
18393 * @private
18394 */
18395function parseOptionValue(value, parser) {
18396 if (parser) {
18397 value = parser(value);
18398 }
18399 if (value && value !== 'none') {
18400 return value;
18401 }
18402}
18403
18404/**
18405 * Gets the value of the selected <option> element within a <select> element.
18406 *
18407 * @param {Element} el
18408 * the element to look in
18409 *
18410 * @param {Function} [parser]
18411 * Optional function to adjust the value.
18412 *
18413 * @return {*}
18414 * - Will be `undefined` if no value exists
18415 * - Will be `undefined` if the given value is "none".
18416 * - Will be the actual value otherwise.
18417 *
18418 * @private
18419 */
18420function getSelectedOptionValue(el, parser) {
18421 const value = el.options[el.options.selectedIndex].value;
18422 return parseOptionValue(value, parser);
18423}
18424
18425/**
18426 * Sets the selected <option> element within a <select> element based on a
18427 * given value.
18428 *
18429 * @param {Element} el
18430 * The element to look in.
18431 *
18432 * @param {string} value
18433 * the property to look on.
18434 *
18435 * @param {Function} [parser]
18436 * Optional function to adjust the value before comparing.
18437 *
18438 * @private
18439 */
18440function setSelectedOption(el, value, parser) {
18441 if (!value) {
18442 return;
18443 }
18444 for (let i = 0; i < el.options.length; i++) {
18445 if (parseOptionValue(el.options[i].value, parser) === value) {
18446 el.selectedIndex = i;
18447 break;
18448 }
18449 }
18450}
18451
18452/**
18453 * Manipulate Text Tracks settings.
18454 *
18455 * @extends ModalDialog
18456 */
18457class TextTrackSettings extends ModalDialog {
18458 /**
18459 * Creates an instance of this class.
18460 *
18461 * @param {Player} player
18462 * The `Player` that this class should be attached to.
18463 *
18464 * @param {Object} [options]
18465 * The key/value store of player options.
18466 */
18467 constructor(player, options) {
18468 options.temporary = false;
18469 super(player, options);
18470 this.updateDisplay = this.updateDisplay.bind(this);
18471
18472 // fill the modal and pretend we have opened it
18473 this.fill();
18474 this.hasBeenOpened_ = this.hasBeenFilled_ = true;
18475 this.renderModalComponents(player);
18476 this.endDialog = createEl('p', {
18477 className: 'vjs-control-text',
18478 textContent: this.localize('End of dialog window.')
18479 });
18480 this.el().appendChild(this.endDialog);
18481 this.setDefaults();
18482
18483 // Grab `persistTextTrackSettings` from the player options if not passed in child options
18484 if (options.persistTextTrackSettings === undefined) {
18485 this.options_.persistTextTrackSettings = this.options_.playerOptions.persistTextTrackSettings;
18486 }
18487 this.bindFunctionsToSelectsAndButtons();
18488 if (this.options_.persistTextTrackSettings) {
18489 this.restoreSettings();
18490 }
18491 }
18492 renderModalComponents(player) {
18493 const textTrackSettingsColors = new TextTrackSettingsColors(player, {
18494 textTrackComponentid: this.id_,
18495 selectConfigs,
18496 fieldSets: [['color', 'textOpacity'], ['backgroundColor', 'backgroundOpacity'], ['windowColor', 'windowOpacity']]
18497 });
18498 this.addChild(textTrackSettingsColors);
18499 const textTrackSettingsFont = new TextTrackSettingsFont(player, {
18500 textTrackComponentid: this.id_,
18501 selectConfigs,
18502 fieldSets: [['fontPercent'], ['edgeStyle'], ['fontFamily']]
18503 });
18504 this.addChild(textTrackSettingsFont);
18505 const trackSettingsControls = new TrackSettingsControls(player);
18506 this.addChild(trackSettingsControls);
18507 }
18508 bindFunctionsToSelectsAndButtons() {
18509 this.on(this.$('.vjs-done-button'), ['click', 'tap'], () => {
18510 this.saveSettings();
18511 this.close();
18512 });
18513 this.on(this.$('.vjs-default-button'), ['click', 'tap'], () => {
18514 this.setDefaults();
18515 this.updateDisplay();
18516 });
18517 each(selectConfigs, config => {
18518 this.on(this.$(config.selector), 'change', this.updateDisplay);
18519 });
18520 }
18521 dispose() {
18522 this.endDialog = null;
18523 super.dispose();
18524 }
18525 label() {
18526 return this.localize('Caption Settings Dialog');
18527 }
18528 description() {
18529 return this.localize('Beginning of dialog window. Escape will cancel and close the window.');
18530 }
18531 buildCSSClass() {
18532 return super.buildCSSClass() + ' vjs-text-track-settings';
18533 }
18534
18535 /**
18536 * Gets an object of text track settings (or null).
18537 *
18538 * @return {Object}
18539 * An object with config values parsed from the DOM or localStorage.
18540 */
18541 getValues() {
18542 return reduce(selectConfigs, (accum, config, key) => {
18543 const value = getSelectedOptionValue(this.$(config.selector), config.parser);
18544 if (value !== undefined) {
18545 accum[key] = value;
18546 }
18547 return accum;
18548 }, {});
18549 }
18550
18551 /**
18552 * Sets text track settings from an object of values.
18553 *
18554 * @param {Object} values
18555 * An object with config values parsed from the DOM or localStorage.
18556 */
18557 setValues(values) {
18558 each(selectConfigs, (config, key) => {
18559 setSelectedOption(this.$(config.selector), values[key], config.parser);
18560 });
18561 }
18562
18563 /**
18564 * Sets all `<select>` elements to their default values.
18565 */
18566 setDefaults() {
18567 each(selectConfigs, config => {
18568 const index = config.hasOwnProperty('default') ? config.default : 0;
18569 this.$(config.selector).selectedIndex = index;
18570 });
18571 }
18572
18573 /**
18574 * Restore texttrack settings from localStorage
18575 */
18576 restoreSettings() {
18577 let values;
18578 try {
18579 values = JSON.parse(window__default["default"].localStorage.getItem(LOCAL_STORAGE_KEY));
18580 } catch (err) {
18581 log.warn(err);
18582 }
18583 if (values) {
18584 this.setValues(values);
18585 }
18586 }
18587
18588 /**
18589 * Save text track settings to localStorage
18590 */
18591 saveSettings() {
18592 if (!this.options_.persistTextTrackSettings) {
18593 return;
18594 }
18595 const values = this.getValues();
18596 try {
18597 if (Object.keys(values).length) {
18598 window__default["default"].localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(values));
18599 } else {
18600 window__default["default"].localStorage.removeItem(LOCAL_STORAGE_KEY);
18601 }
18602 } catch (err) {
18603 log.warn(err);
18604 }
18605 }
18606
18607 /**
18608 * Update display of text track settings
18609 */
18610 updateDisplay() {
18611 const ttDisplay = this.player_.getChild('textTrackDisplay');
18612 if (ttDisplay) {
18613 ttDisplay.updateDisplay();
18614 }
18615 }
18616
18617 /**
18618 * Repopulate dialog with new localizations on languagechange
18619 */
18620 handleLanguagechange() {
18621 this.fill();
18622 this.renderModalComponents(this.player_);
18623 this.bindFunctionsToSelectsAndButtons();
18624 }
18625}
18626Component.registerComponent('TextTrackSettings', TextTrackSettings);
18627
18628/**
18629 * @file resize-manager.js
18630 */
18631
18632/**
18633 * A Resize Manager. It is in charge of triggering `playerresize` on the player in the right conditions.
18634 *
18635 * It'll either create an iframe and use a debounced resize handler on it or use the new {@link https://wicg.github.io/ResizeObserver/|ResizeObserver}.
18636 *
18637 * If the ResizeObserver is available natively, it will be used. A polyfill can be passed in as an option.
18638 * If a `playerresize` event is not needed, the ResizeManager component can be removed from the player, see the example below.
18639 *
18640 * @example <caption>How to disable the resize manager</caption>
18641 * const player = videojs('#vid', {
18642 * resizeManager: false
18643 * });
18644 *
18645 * @see {@link https://wicg.github.io/ResizeObserver/|ResizeObserver specification}
18646 *
18647 * @extends Component
18648 */
18649class ResizeManager extends Component {
18650 /**
18651 * Create the ResizeManager.
18652 *
18653 * @param {Object} player
18654 * The `Player` that this class should be attached to.
18655 *
18656 * @param {Object} [options]
18657 * The key/value store of ResizeManager options.
18658 *
18659 * @param {Object} [options.ResizeObserver]
18660 * A polyfill for ResizeObserver can be passed in here.
18661 * If this is set to null it will ignore the native ResizeObserver and fall back to the iframe fallback.
18662 */
18663 constructor(player, options) {
18664 let RESIZE_OBSERVER_AVAILABLE = options.ResizeObserver || window__default["default"].ResizeObserver;
18665
18666 // if `null` was passed, we want to disable the ResizeObserver
18667 if (options.ResizeObserver === null) {
18668 RESIZE_OBSERVER_AVAILABLE = false;
18669 }
18670
18671 // Only create an element when ResizeObserver isn't available
18672 const options_ = merge({
18673 createEl: !RESIZE_OBSERVER_AVAILABLE,
18674 reportTouchActivity: false
18675 }, options);
18676 super(player, options_);
18677 this.ResizeObserver = options.ResizeObserver || window__default["default"].ResizeObserver;
18678 this.loadListener_ = null;
18679 this.resizeObserver_ = null;
18680 this.debouncedHandler_ = debounce(() => {
18681 this.resizeHandler();
18682 }, 100, false, this);
18683 if (RESIZE_OBSERVER_AVAILABLE) {
18684 this.resizeObserver_ = new this.ResizeObserver(this.debouncedHandler_);
18685 this.resizeObserver_.observe(player.el());
18686 } else {
18687 this.loadListener_ = () => {
18688 if (!this.el_ || !this.el_.contentWindow) {
18689 return;
18690 }
18691 const debouncedHandler_ = this.debouncedHandler_;
18692 let unloadListener_ = this.unloadListener_ = function () {
18693 off(this, 'resize', debouncedHandler_);
18694 off(this, 'unload', unloadListener_);
18695 unloadListener_ = null;
18696 };
18697
18698 // safari and edge can unload the iframe before resizemanager dispose
18699 // we have to dispose of event handlers correctly before that happens
18700 on(this.el_.contentWindow, 'unload', unloadListener_);
18701 on(this.el_.contentWindow, 'resize', debouncedHandler_);
18702 };
18703 this.one('load', this.loadListener_);
18704 }
18705 }
18706 createEl() {
18707 return super.createEl('iframe', {
18708 className: 'vjs-resize-manager',
18709 tabIndex: -1,
18710 title: this.localize('No content')
18711 }, {
18712 'aria-hidden': 'true'
18713 });
18714 }
18715
18716 /**
18717 * Called when a resize is triggered on the iframe or a resize is observed via the ResizeObserver
18718 *
18719 * @fires Player#playerresize
18720 */
18721 resizeHandler() {
18722 /**
18723 * Called when the player size has changed
18724 *
18725 * @event Player#playerresize
18726 * @type {Event}
18727 */
18728 // make sure player is still around to trigger
18729 // prevents this from causing an error after dispose
18730 if (!this.player_ || !this.player_.trigger) {
18731 return;
18732 }
18733 this.player_.trigger('playerresize');
18734 }
18735 dispose() {
18736 if (this.debouncedHandler_) {
18737 this.debouncedHandler_.cancel();
18738 }
18739 if (this.resizeObserver_) {
18740 if (this.player_.el()) {
18741 this.resizeObserver_.unobserve(this.player_.el());
18742 }
18743 this.resizeObserver_.disconnect();
18744 }
18745 if (this.loadListener_) {
18746 this.off('load', this.loadListener_);
18747 }
18748 if (this.el_ && this.el_.contentWindow && this.unloadListener_) {
18749 this.unloadListener_.call(this.el_.contentWindow);
18750 }
18751 this.ResizeObserver = null;
18752 this.resizeObserver = null;
18753 this.debouncedHandler_ = null;
18754 this.loadListener_ = null;
18755 super.dispose();
18756 }
18757}
18758Component.registerComponent('ResizeManager', ResizeManager);
18759
18760/** @import Player from './player' */
18761
18762const defaults$1 = {
18763 trackingThreshold: 20,
18764 liveTolerance: 15
18765};
18766
18767/*
18768 track when we are at the live edge, and other helpers for live playback */
18769
18770/**
18771 * A class for checking live current time and determining when the player
18772 * is at or behind the live edge.
18773 */
18774class LiveTracker extends Component {
18775 /**
18776 * Creates an instance of this class.
18777 *
18778 * @param {Player} player
18779 * The `Player` that this class should be attached to.
18780 *
18781 * @param {Object} [options]
18782 * The key/value store of player options.
18783 *
18784 * @param {number} [options.trackingThreshold=20]
18785 * Number of seconds of live window (seekableEnd - seekableStart) that
18786 * media needs to have before the liveui will be shown.
18787 *
18788 * @param {number} [options.liveTolerance=15]
18789 * Number of seconds behind live that we have to be
18790 * before we will be considered non-live. Note that this will only
18791 * be used when playing at the live edge. This allows large seekable end
18792 * changes to not effect whether we are live or not.
18793 */
18794 constructor(player, options) {
18795 // LiveTracker does not need an element
18796 const options_ = merge(defaults$1, options, {
18797 createEl: false
18798 });
18799 super(player, options_);
18800 this.trackLiveHandler_ = () => this.trackLive_();
18801 this.handlePlay_ = e => this.handlePlay(e);
18802 this.handleFirstTimeupdate_ = e => this.handleFirstTimeupdate(e);
18803 this.handleSeeked_ = e => this.handleSeeked(e);
18804 this.seekToLiveEdge_ = e => this.seekToLiveEdge(e);
18805 this.reset_();
18806 this.on(this.player_, 'durationchange', e => this.handleDurationchange(e));
18807 // we should try to toggle tracking on canplay as native playback engines, like Safari
18808 // may not have the proper values for things like seekableEnd until then
18809 this.on(this.player_, 'canplay', () => this.toggleTracking());
18810 }
18811
18812 /**
18813 * all the functionality for tracking when seek end changes
18814 * and for tracking how far past seek end we should be
18815 */
18816 trackLive_() {
18817 const seekable = this.player_.seekable();
18818
18819 // skip undefined seekable
18820 if (!seekable || !seekable.length) {
18821 return;
18822 }
18823 const newTime = Number(window__default["default"].performance.now().toFixed(4));
18824 const deltaTime = this.lastTime_ === -1 ? 0 : (newTime - this.lastTime_) / 1000;
18825 this.lastTime_ = newTime;
18826 this.pastSeekEnd_ = this.pastSeekEnd() + deltaTime;
18827 const liveCurrentTime = this.liveCurrentTime();
18828 const currentTime = this.player_.currentTime();
18829
18830 // we are behind live if any are true
18831 // 1. the player is paused
18832 // 2. the user seeked to a location 2 seconds away from live
18833 // 3. the difference between live and current time is greater
18834 // liveTolerance which defaults to 15s
18835 let isBehind = this.player_.paused() || this.seekedBehindLive_ || Math.abs(liveCurrentTime - currentTime) > this.options_.liveTolerance;
18836
18837 // we cannot be behind if
18838 // 1. until we have not seen a timeupdate yet
18839 // 2. liveCurrentTime is Infinity, which happens on Android and Native Safari
18840 if (!this.timeupdateSeen_ || liveCurrentTime === Infinity) {
18841 isBehind = false;
18842 }
18843 if (isBehind !== this.behindLiveEdge_) {
18844 this.behindLiveEdge_ = isBehind;
18845 this.trigger('liveedgechange');
18846 }
18847 }
18848
18849 /**
18850 * handle a durationchange event on the player
18851 * and start/stop tracking accordingly.
18852 */
18853 handleDurationchange() {
18854 this.toggleTracking();
18855 }
18856
18857 /**
18858 * start/stop tracking
18859 */
18860 toggleTracking() {
18861 if (this.player_.duration() === Infinity && this.liveWindow() >= this.options_.trackingThreshold) {
18862 if (this.player_.options_.liveui) {
18863 this.player_.addClass('vjs-liveui');
18864 }
18865 this.startTracking();
18866 } else {
18867 this.player_.removeClass('vjs-liveui');
18868 this.stopTracking();
18869 }
18870 }
18871
18872 /**
18873 * start tracking live playback
18874 */
18875 startTracking() {
18876 if (this.isTracking()) {
18877 return;
18878 }
18879
18880 // If we haven't seen a timeupdate, we need to check whether playback
18881 // began before this component started tracking. This can happen commonly
18882 // when using autoplay.
18883 if (!this.timeupdateSeen_) {
18884 this.timeupdateSeen_ = this.player_.hasStarted();
18885 }
18886 this.trackingInterval_ = this.setInterval(this.trackLiveHandler_, UPDATE_REFRESH_INTERVAL);
18887 this.trackLive_();
18888 this.on(this.player_, ['play', 'pause'], this.trackLiveHandler_);
18889 if (!this.timeupdateSeen_) {
18890 this.one(this.player_, 'play', this.handlePlay_);
18891 this.one(this.player_, 'timeupdate', this.handleFirstTimeupdate_);
18892 } else {
18893 this.on(this.player_, 'seeked', this.handleSeeked_);
18894 }
18895 }
18896
18897 /**
18898 * handle the first timeupdate on the player if it wasn't already playing
18899 * when live tracker started tracking.
18900 */
18901 handleFirstTimeupdate() {
18902 this.timeupdateSeen_ = true;
18903 this.on(this.player_, 'seeked', this.handleSeeked_);
18904 }
18905
18906 /**
18907 * Keep track of what time a seek starts, and listen for seeked
18908 * to find where a seek ends.
18909 */
18910 handleSeeked() {
18911 const timeDiff = Math.abs(this.liveCurrentTime() - this.player_.currentTime());
18912 this.seekedBehindLive_ = this.nextSeekedFromUser_ && timeDiff > 2;
18913 this.nextSeekedFromUser_ = false;
18914 this.trackLive_();
18915 }
18916
18917 /**
18918 * handle the first play on the player, and make sure that we seek
18919 * right to the live edge.
18920 */
18921 handlePlay() {
18922 this.one(this.player_, 'timeupdate', this.seekToLiveEdge_);
18923 }
18924
18925 /**
18926 * Stop tracking, and set all internal variables to
18927 * their initial value.
18928 */
18929 reset_() {
18930 this.lastTime_ = -1;
18931 this.pastSeekEnd_ = 0;
18932 this.lastSeekEnd_ = -1;
18933 this.behindLiveEdge_ = true;
18934 this.timeupdateSeen_ = false;
18935 this.seekedBehindLive_ = false;
18936 this.nextSeekedFromUser_ = false;
18937 this.clearInterval(this.trackingInterval_);
18938 this.trackingInterval_ = null;
18939 this.off(this.player_, ['play', 'pause'], this.trackLiveHandler_);
18940 this.off(this.player_, 'seeked', this.handleSeeked_);
18941 this.off(this.player_, 'play', this.handlePlay_);
18942 this.off(this.player_, 'timeupdate', this.handleFirstTimeupdate_);
18943 this.off(this.player_, 'timeupdate', this.seekToLiveEdge_);
18944 }
18945
18946 /**
18947 * The next seeked event is from the user. Meaning that any seek
18948 * > 2s behind live will be considered behind live for real and
18949 * liveTolerance will be ignored.
18950 */
18951 nextSeekedFromUser() {
18952 this.nextSeekedFromUser_ = true;
18953 }
18954
18955 /**
18956 * stop tracking live playback
18957 */
18958 stopTracking() {
18959 if (!this.isTracking()) {
18960 return;
18961 }
18962 this.reset_();
18963 this.trigger('liveedgechange');
18964 }
18965
18966 /**
18967 * A helper to get the player seekable end
18968 * so that we don't have to null check everywhere
18969 *
18970 * @return {number}
18971 * The furthest seekable end or Infinity.
18972 */
18973 seekableEnd() {
18974 const seekable = this.player_.seekable();
18975 const seekableEnds = [];
18976 let i = seekable ? seekable.length : 0;
18977 while (i--) {
18978 seekableEnds.push(seekable.end(i));
18979 }
18980
18981 // grab the furthest seekable end after sorting, or if there are none
18982 // default to Infinity
18983 return seekableEnds.length ? seekableEnds.sort()[seekableEnds.length - 1] : Infinity;
18984 }
18985
18986 /**
18987 * A helper to get the player seekable start
18988 * so that we don't have to null check everywhere
18989 *
18990 * @return {number}
18991 * The earliest seekable start or 0.
18992 */
18993 seekableStart() {
18994 const seekable = this.player_.seekable();
18995 const seekableStarts = [];
18996 let i = seekable ? seekable.length : 0;
18997 while (i--) {
18998 seekableStarts.push(seekable.start(i));
18999 }
19000
19001 // grab the first seekable start after sorting, or if there are none
19002 // default to 0
19003 return seekableStarts.length ? seekableStarts.sort()[0] : 0;
19004 }
19005
19006 /**
19007 * Get the live time window aka
19008 * the amount of time between seekable start and
19009 * live current time.
19010 *
19011 * @return {number}
19012 * The amount of seconds that are seekable in
19013 * the live video.
19014 */
19015 liveWindow() {
19016 const liveCurrentTime = this.liveCurrentTime();
19017
19018 // if liveCurrenTime is Infinity then we don't have a liveWindow at all
19019 if (liveCurrentTime === Infinity) {
19020 return 0;
19021 }
19022 return liveCurrentTime - this.seekableStart();
19023 }
19024
19025 /**
19026 * Determines if the player is live, only checks if this component
19027 * is tracking live playback or not
19028 *
19029 * @return {boolean}
19030 * Whether liveTracker is tracking
19031 */
19032 isLive() {
19033 return this.isTracking();
19034 }
19035
19036 /**
19037 * Determines if currentTime is at the live edge and won't fall behind
19038 * on each seekableendchange
19039 *
19040 * @return {boolean}
19041 * Whether playback is at the live edge
19042 */
19043 atLiveEdge() {
19044 return !this.behindLiveEdge();
19045 }
19046
19047 /**
19048 * get what we expect the live current time to be
19049 *
19050 * @return {number}
19051 * The expected live current time
19052 */
19053 liveCurrentTime() {
19054 return this.pastSeekEnd() + this.seekableEnd();
19055 }
19056
19057 /**
19058 * The number of seconds that have occurred after seekable end
19059 * changed. This will be reset to 0 once seekable end changes.
19060 *
19061 * @return {number}
19062 * Seconds past the current seekable end
19063 */
19064 pastSeekEnd() {
19065 const seekableEnd = this.seekableEnd();
19066 if (this.lastSeekEnd_ !== -1 && seekableEnd !== this.lastSeekEnd_) {
19067 this.pastSeekEnd_ = 0;
19068 }
19069 this.lastSeekEnd_ = seekableEnd;
19070 return this.pastSeekEnd_;
19071 }
19072
19073 /**
19074 * If we are currently behind the live edge, aka currentTime will be
19075 * behind on a seekableendchange
19076 *
19077 * @return {boolean}
19078 * If we are behind the live edge
19079 */
19080 behindLiveEdge() {
19081 return this.behindLiveEdge_;
19082 }
19083
19084 /**
19085 * Whether live tracker is currently tracking or not.
19086 */
19087 isTracking() {
19088 return typeof this.trackingInterval_ === 'number';
19089 }
19090
19091 /**
19092 * Seek to the live edge if we are behind the live edge
19093 */
19094 seekToLiveEdge() {
19095 this.seekedBehindLive_ = false;
19096 if (this.atLiveEdge()) {
19097 return;
19098 }
19099 this.nextSeekedFromUser_ = false;
19100 this.player_.currentTime(this.liveCurrentTime());
19101 }
19102
19103 /**
19104 * Dispose of liveTracker
19105 */
19106 dispose() {
19107 this.stopTracking();
19108 super.dispose();
19109 }
19110}
19111Component.registerComponent('LiveTracker', LiveTracker);
19112
19113/**
19114 * Displays an element over the player which contains an optional title and
19115 * description for the current content.
19116 *
19117 * Much of the code for this component originated in the now obsolete
19118 * videojs-dock plugin: https://github.com/brightcove/videojs-dock/
19119 *
19120 * @extends Component
19121 */
19122class TitleBar extends Component {
19123 constructor(player, options) {
19124 super(player, options);
19125 this.on('statechanged', e => this.updateDom_());
19126 this.updateDom_();
19127 }
19128
19129 /**
19130 * Create the `TitleBar`'s DOM element
19131 *
19132 * @return {Element}
19133 * The element that was created.
19134 */
19135 createEl() {
19136 this.els = {
19137 title: createEl('div', {
19138 className: 'vjs-title-bar-title',
19139 id: `vjs-title-bar-title-${newGUID()}`
19140 }),
19141 description: createEl('div', {
19142 className: 'vjs-title-bar-description',
19143 id: `vjs-title-bar-description-${newGUID()}`
19144 })
19145 };
19146 return createEl('div', {
19147 className: 'vjs-title-bar'
19148 }, {}, values(this.els));
19149 }
19150
19151 /**
19152 * Updates the DOM based on the component's state object.
19153 */
19154 updateDom_() {
19155 const tech = this.player_.tech_;
19156 const techEl = tech && tech.el_;
19157 const techAriaAttrs = {
19158 title: 'aria-labelledby',
19159 description: 'aria-describedby'
19160 };
19161 ['title', 'description'].forEach(k => {
19162 const value = this.state[k];
19163 const el = this.els[k];
19164 const techAriaAttr = techAriaAttrs[k];
19165 emptyEl(el);
19166 if (value) {
19167 textContent(el, value);
19168 }
19169
19170 // If there is a tech element available, update its ARIA attributes
19171 // according to whether a title and/or description have been provided.
19172 if (techEl) {
19173 techEl.removeAttribute(techAriaAttr);
19174 if (value) {
19175 techEl.setAttribute(techAriaAttr, el.id);
19176 }
19177 }
19178 });
19179 if (this.state.title || this.state.description) {
19180 this.show();
19181 } else {
19182 this.hide();
19183 }
19184 }
19185
19186 /**
19187 * Update the contents of the title bar component with new title and
19188 * description text.
19189 *
19190 * If both title and description are missing, the title bar will be hidden.
19191 *
19192 * If either title or description are present, the title bar will be visible.
19193 *
19194 * NOTE: Any previously set value will be preserved. To unset a previously
19195 * set value, you must pass an empty string or null.
19196 *
19197 * For example:
19198 *
19199 * ```
19200 * update({title: 'foo', description: 'bar'}) // title: 'foo', description: 'bar'
19201 * update({description: 'bar2'}) // title: 'foo', description: 'bar2'
19202 * update({title: ''}) // title: '', description: 'bar2'
19203 * update({title: 'foo', description: null}) // title: 'foo', description: null
19204 * ```
19205 *
19206 * @param {Object} [options={}]
19207 * An options object. When empty, the title bar will be hidden.
19208 *
19209 * @param {string} [options.title]
19210 * A title to display in the title bar.
19211 *
19212 * @param {string} [options.description]
19213 * A description to display in the title bar.
19214 */
19215 update(options) {
19216 this.setState(options);
19217 }
19218
19219 /**
19220 * Dispose the component.
19221 */
19222 dispose() {
19223 const tech = this.player_.tech_;
19224 const techEl = tech && tech.el_;
19225 if (techEl) {
19226 techEl.removeAttribute('aria-labelledby');
19227 techEl.removeAttribute('aria-describedby');
19228 }
19229 super.dispose();
19230 this.els = null;
19231 }
19232}
19233Component.registerComponent('TitleBar', TitleBar);
19234
19235/** @import Player from './player' */
19236
19237/**
19238 * @typedef {object} TransientButtonOptions
19239 * @property {string} [controlText] Control text, usually visible for these buttons
19240 * @property {number} [initialDisplay=4000] Time in ms that button should initially remain visible
19241 * @property {Array<'top'|'neartop'|'bottom'|'left'|'right'>} [position] Array of position strings to add basic styles for positioning
19242 * @property {string} [className] Class(es) to add
19243 * @property {boolean} [takeFocus=false] Whether element sohuld take focus when shown
19244 * @property {Function} [clickHandler] Function called on button activation
19245 */
19246
19247/** @type {TransientButtonOptions} */
19248const defaults = {
19249 initialDisplay: 4000,
19250 position: [],
19251 takeFocus: false
19252};
19253
19254/**
19255 * A floating transient button.
19256 * It's recommended to insert these buttons _before_ the control bar with the this argument to `addChild`
19257 * for a logical tab order.
19258 *
19259 * @example
19260 * ```
19261 * player.addChild(
19262 * 'TransientButton',
19263 * options,
19264 * player.children().indexOf(player.getChild("ControlBar"))
19265 * )
19266 * ```
19267 *
19268 * @extends Button
19269 */
19270class TransientButton extends Button {
19271 /**
19272 * TransientButton constructor
19273 *
19274 * @param {Player} player The button's player
19275 * @param {TransientButtonOptions} options Options for the transient button
19276 */
19277 constructor(player, options) {
19278 options = merge(defaults, options);
19279 super(player, options);
19280 this.controlText(options.controlText);
19281 this.hide();
19282
19283 // When shown, the float button will be visible even if the user is inactive.
19284 // Clear this if there is any interaction.
19285 this.on(this.player_, ['useractive', 'userinactive'], e => {
19286 this.removeClass('force-display');
19287 });
19288 }
19289
19290 /**
19291 * Return CSS class including position classes
19292 *
19293 * @return {string} CSS class list
19294 */
19295 buildCSSClass() {
19296 return `vjs-transient-button focus-visible ${this.options_.position.map(c => `vjs-${c}`).join(' ')}`;
19297 }
19298
19299 /**
19300 * Create the button element
19301 *
19302 * @return {HTMLButtonElement} The button element
19303 */
19304 createEl() {
19305 /** @type HTMLButtonElement */
19306 const el = createEl('button', {}, {
19307 type: 'button',
19308 class: this.buildCSSClass()
19309 }, createEl('span'));
19310 this.controlTextEl_ = el.querySelector('span');
19311 return el;
19312 }
19313
19314 /**
19315 * Show the button. The button will remain visible for the `initialDisplay` time, default 4s,
19316 * and when there is user activity.
19317 */
19318 show() {
19319 super.show();
19320 this.addClass('force-display');
19321 if (this.options_.takeFocus) {
19322 this.el().focus({
19323 preventScroll: true
19324 });
19325 }
19326 this.forceDisplayTimeout = this.player_.setTimeout(() => {
19327 this.removeClass('force-display');
19328 }, this.options_.initialDisplay);
19329 }
19330
19331 /**
19332 * Hide the display, even if during the `initialDisplay` time.
19333 */
19334 hide() {
19335 this.removeClass('force-display');
19336 super.hide();
19337 }
19338
19339 /**
19340 * Dispose the component
19341 */
19342 dispose() {
19343 this.player_.clearTimeout(this.forceDisplayTimeout);
19344 super.dispose();
19345 }
19346}
19347Component.registerComponent('TransientButton', TransientButton);
19348
19349/** @import Html5 from './html5' */
19350
19351/**
19352 * This function is used to fire a sourceset when there is something
19353 * similar to `mediaEl.load()` being called. It will try to find the source via
19354 * the `src` attribute and then the `<source>` elements. It will then fire `sourceset`
19355 * with the source that was found or empty string if we cannot know. If it cannot
19356 * find a source then `sourceset` will not be fired.
19357 *
19358 * @param {Html5} tech
19359 * The tech object that sourceset was setup on
19360 *
19361 * @return {boolean}
19362 * returns false if the sourceset was not fired and true otherwise.
19363 */
19364const sourcesetLoad = tech => {
19365 const el = tech.el();
19366
19367 // if `el.src` is set, that source will be loaded.
19368 if (el.hasAttribute('src')) {
19369 tech.triggerSourceset(el.src);
19370 return true;
19371 }
19372
19373 /**
19374 * Since there isn't a src property on the media element, source elements will be used for
19375 * implementing the source selection algorithm. This happens asynchronously and
19376 * for most cases were there is more than one source we cannot tell what source will
19377 * be loaded, without re-implementing the source selection algorithm. At this time we are not
19378 * going to do that. There are three special cases that we do handle here though:
19379 *
19380 * 1. If there are no sources, do not fire `sourceset`.
19381 * 2. If there is only one `<source>` with a `src` property/attribute that is our `src`
19382 * 3. If there is more than one `<source>` but all of them have the same `src` url.
19383 * That will be our src.
19384 */
19385 const sources = tech.$$('source');
19386 const srcUrls = [];
19387 let src = '';
19388
19389 // if there are no sources, do not fire sourceset
19390 if (!sources.length) {
19391 return false;
19392 }
19393
19394 // only count valid/non-duplicate source elements
19395 for (let i = 0; i < sources.length; i++) {
19396 const url = sources[i].src;
19397 if (url && srcUrls.indexOf(url) === -1) {
19398 srcUrls.push(url);
19399 }
19400 }
19401
19402 // there were no valid sources
19403 if (!srcUrls.length) {
19404 return false;
19405 }
19406
19407 // there is only one valid source element url
19408 // use that
19409 if (srcUrls.length === 1) {
19410 src = srcUrls[0];
19411 }
19412 tech.triggerSourceset(src);
19413 return true;
19414};
19415
19416/**
19417 * our implementation of an `innerHTML` descriptor for browsers
19418 * that do not have one.
19419 */
19420const innerHTMLDescriptorPolyfill = Object.defineProperty({}, 'innerHTML', {
19421 get() {
19422 return this.cloneNode(true).innerHTML;
19423 },
19424 set(v) {
19425 // make a dummy node to use innerHTML on
19426 const dummy = document__default["default"].createElement(this.nodeName.toLowerCase());
19427
19428 // set innerHTML to the value provided
19429 dummy.innerHTML = v;
19430
19431 // make a document fragment to hold the nodes from dummy
19432 const docFrag = document__default["default"].createDocumentFragment();
19433
19434 // copy all of the nodes created by the innerHTML on dummy
19435 // to the document fragment
19436 while (dummy.childNodes.length) {
19437 docFrag.appendChild(dummy.childNodes[0]);
19438 }
19439
19440 // remove content
19441 this.innerText = '';
19442
19443 // now we add all of that html in one by appending the
19444 // document fragment. This is how innerHTML does it.
19445 window__default["default"].Element.prototype.appendChild.call(this, docFrag);
19446
19447 // then return the result that innerHTML's setter would
19448 return this.innerHTML;
19449 }
19450});
19451
19452/**
19453 * Get a property descriptor given a list of priorities and the
19454 * property to get.
19455 */
19456const getDescriptor = (priority, prop) => {
19457 let descriptor = {};
19458 for (let i = 0; i < priority.length; i++) {
19459 descriptor = Object.getOwnPropertyDescriptor(priority[i], prop);
19460 if (descriptor && descriptor.set && descriptor.get) {
19461 break;
19462 }
19463 }
19464 descriptor.enumerable = true;
19465 descriptor.configurable = true;
19466 return descriptor;
19467};
19468const getInnerHTMLDescriptor = tech => getDescriptor([tech.el(), window__default["default"].HTMLMediaElement.prototype, window__default["default"].Element.prototype, innerHTMLDescriptorPolyfill], 'innerHTML');
19469
19470/**
19471 * Patches browser internal functions so that we can tell synchronously
19472 * if a `<source>` was appended to the media element. For some reason this
19473 * causes a `sourceset` if the the media element is ready and has no source.
19474 * This happens when:
19475 * - The page has just loaded and the media element does not have a source.
19476 * - The media element was emptied of all sources, then `load()` was called.
19477 *
19478 * It does this by patching the following functions/properties when they are supported:
19479 *
19480 * - `append()` - can be used to add a `<source>` element to the media element
19481 * - `appendChild()` - can be used to add a `<source>` element to the media element
19482 * - `insertAdjacentHTML()` - can be used to add a `<source>` element to the media element
19483 * - `innerHTML` - can be used to add a `<source>` element to the media element
19484 *
19485 * @param {Html5} tech
19486 * The tech object that sourceset is being setup on.
19487 */
19488const firstSourceWatch = function (tech) {
19489 const el = tech.el();
19490
19491 // make sure firstSourceWatch isn't setup twice.
19492 if (el.resetSourceWatch_) {
19493 return;
19494 }
19495 const old = {};
19496 const innerDescriptor = getInnerHTMLDescriptor(tech);
19497 const appendWrapper = appendFn => (...args) => {
19498 const retval = appendFn.apply(el, args);
19499 sourcesetLoad(tech);
19500 return retval;
19501 };
19502 ['append', 'appendChild', 'insertAdjacentHTML'].forEach(k => {
19503 if (!el[k]) {
19504 return;
19505 }
19506
19507 // store the old function
19508 old[k] = el[k];
19509
19510 // call the old function with a sourceset if a source
19511 // was loaded
19512 el[k] = appendWrapper(old[k]);
19513 });
19514 Object.defineProperty(el, 'innerHTML', merge(innerDescriptor, {
19515 set: appendWrapper(innerDescriptor.set)
19516 }));
19517 el.resetSourceWatch_ = () => {
19518 el.resetSourceWatch_ = null;
19519 Object.keys(old).forEach(k => {
19520 el[k] = old[k];
19521 });
19522 Object.defineProperty(el, 'innerHTML', innerDescriptor);
19523 };
19524
19525 // on the first sourceset, we need to revert our changes
19526 tech.one('sourceset', el.resetSourceWatch_);
19527};
19528
19529/**
19530 * our implementation of a `src` descriptor for browsers
19531 * that do not have one
19532 */
19533const srcDescriptorPolyfill = Object.defineProperty({}, 'src', {
19534 get() {
19535 if (this.hasAttribute('src')) {
19536 return getAbsoluteURL(window__default["default"].Element.prototype.getAttribute.call(this, 'src'));
19537 }
19538 return '';
19539 },
19540 set(v) {
19541 window__default["default"].Element.prototype.setAttribute.call(this, 'src', v);
19542 return v;
19543 }
19544});
19545const getSrcDescriptor = tech => getDescriptor([tech.el(), window__default["default"].HTMLMediaElement.prototype, srcDescriptorPolyfill], 'src');
19546
19547/**
19548 * setup `sourceset` handling on the `Html5` tech. This function
19549 * patches the following element properties/functions:
19550 *
19551 * - `src` - to determine when `src` is set
19552 * - `setAttribute()` - to determine when `src` is set
19553 * - `load()` - this re-triggers the source selection algorithm, and can
19554 * cause a sourceset.
19555 *
19556 * If there is no source when we are adding `sourceset` support or during a `load()`
19557 * we also patch the functions listed in `firstSourceWatch`.
19558 *
19559 * @param {Html5} tech
19560 * The tech to patch
19561 */
19562const setupSourceset = function (tech) {
19563 if (!tech.featuresSourceset) {
19564 return;
19565 }
19566 const el = tech.el();
19567
19568 // make sure sourceset isn't setup twice.
19569 if (el.resetSourceset_) {
19570 return;
19571 }
19572 const srcDescriptor = getSrcDescriptor(tech);
19573 const oldSetAttribute = el.setAttribute;
19574 const oldLoad = el.load;
19575 Object.defineProperty(el, 'src', merge(srcDescriptor, {
19576 set: v => {
19577 const retval = srcDescriptor.set.call(el, v);
19578
19579 // we use the getter here to get the actual value set on src
19580 tech.triggerSourceset(el.src);
19581 return retval;
19582 }
19583 }));
19584 el.setAttribute = (n, v) => {
19585 const retval = oldSetAttribute.call(el, n, v);
19586 if (/src/i.test(n)) {
19587 tech.triggerSourceset(el.src);
19588 }
19589 return retval;
19590 };
19591 el.load = () => {
19592 const retval = oldLoad.call(el);
19593
19594 // if load was called, but there was no source to fire
19595 // sourceset on. We have to watch for a source append
19596 // as that can trigger a `sourceset` when the media element
19597 // has no source
19598 if (!sourcesetLoad(tech)) {
19599 tech.triggerSourceset('');
19600 firstSourceWatch(tech);
19601 }
19602 return retval;
19603 };
19604 if (el.currentSrc) {
19605 tech.triggerSourceset(el.currentSrc);
19606 } else if (!sourcesetLoad(tech)) {
19607 firstSourceWatch(tech);
19608 }
19609 el.resetSourceset_ = () => {
19610 el.resetSourceset_ = null;
19611 el.load = oldLoad;
19612 el.setAttribute = oldSetAttribute;
19613 Object.defineProperty(el, 'src', srcDescriptor);
19614 if (el.resetSourceWatch_) {
19615 el.resetSourceWatch_();
19616 }
19617 };
19618};
19619
19620/**
19621 * @file html5.js
19622 */
19623
19624/**
19625 * HTML5 Media Controller - Wrapper for HTML5 Media API
19626 *
19627 * @mixes Tech~SourceHandlerAdditions
19628 * @extends Tech
19629 */
19630class Html5 extends Tech {
19631 /**
19632 * Create an instance of this Tech.
19633 *
19634 * @param {Object} [options]
19635 * The key/value store of player options.
19636 *
19637 * @param {Function} [ready]
19638 * Callback function to call when the `HTML5` Tech is ready.
19639 */
19640 constructor(options, ready) {
19641 super(options, ready);
19642 const source = options.source;
19643 let crossoriginTracks = false;
19644 this.featuresVideoFrameCallback = this.featuresVideoFrameCallback && this.el_.tagName === 'VIDEO';
19645
19646 // Set the source if one is provided
19647 // 1) Check if the source is new (if not, we want to keep the original so playback isn't interrupted)
19648 // 2) Check to see if the network state of the tag was failed at init, and if so, reset the source
19649 // anyway so the error gets fired.
19650 if (source && (this.el_.currentSrc !== source.src || options.tag && options.tag.initNetworkState_ === 3)) {
19651 this.setSource(source);
19652 } else {
19653 this.handleLateInit_(this.el_);
19654 }
19655
19656 // setup sourceset after late sourceset/init
19657 if (options.enableSourceset) {
19658 this.setupSourcesetHandling_();
19659 }
19660 this.isScrubbing_ = false;
19661 if (this.el_.hasChildNodes()) {
19662 const nodes = this.el_.childNodes;
19663 let nodesLength = nodes.length;
19664 const removeNodes = [];
19665 while (nodesLength--) {
19666 const node = nodes[nodesLength];
19667 const nodeName = node.nodeName.toLowerCase();
19668 if (nodeName === 'track') {
19669 if (!this.featuresNativeTextTracks) {
19670 // Empty video tag tracks so the built-in player doesn't use them also.
19671 // This may not be fast enough to stop HTML5 browsers from reading the tags
19672 // so we'll need to turn off any default tracks if we're manually doing
19673 // captions and subtitles. videoElement.textTracks
19674 removeNodes.push(node);
19675 } else {
19676 // store HTMLTrackElement and TextTrack to remote list
19677 this.remoteTextTrackEls().addTrackElement_(node);
19678 this.remoteTextTracks().addTrack(node.track);
19679 this.textTracks().addTrack(node.track);
19680 if (!crossoriginTracks && !this.el_.hasAttribute('crossorigin') && isCrossOrigin(node.src)) {
19681 crossoriginTracks = true;
19682 }
19683 }
19684 }
19685 }
19686 for (let i = 0; i < removeNodes.length; i++) {
19687 this.el_.removeChild(removeNodes[i]);
19688 }
19689 }
19690 this.proxyNativeTracks_();
19691 if (this.featuresNativeTextTracks && crossoriginTracks) {
19692 log.warn('Text Tracks are being loaded from another origin but the crossorigin attribute isn\'t used.\n' + 'This may prevent text tracks from loading.');
19693 }
19694
19695 // prevent iOS Safari from disabling metadata text tracks during native playback
19696 this.restoreMetadataTracksInIOSNativePlayer_();
19697
19698 // Determine if native controls should be used
19699 // Our goal should be to get the custom controls on mobile solid everywhere
19700 // so we can remove this all together. Right now this will block custom
19701 // controls on touch enabled laptops like the Chrome Pixel
19702 if ((TOUCH_ENABLED || IS_IPHONE) && options.nativeControlsForTouch === true) {
19703 this.setControls(true);
19704 }
19705
19706 // on iOS, we want to proxy `webkitbeginfullscreen` and `webkitendfullscreen`
19707 // into a `fullscreenchange` event
19708 this.proxyWebkitFullscreen_();
19709 this.triggerReady();
19710 }
19711
19712 /**
19713 * Dispose of `HTML5` media element and remove all tracks.
19714 */
19715 dispose() {
19716 if (this.el_ && this.el_.resetSourceset_) {
19717 this.el_.resetSourceset_();
19718 }
19719 Html5.disposeMediaElement(this.el_);
19720 this.options_ = null;
19721
19722 // tech will handle clearing of the emulated track list
19723 super.dispose();
19724 }
19725
19726 /**
19727 * Modify the media element so that we can detect when
19728 * the source is changed. Fires `sourceset` just after the source has changed
19729 */
19730 setupSourcesetHandling_() {
19731 setupSourceset(this);
19732 }
19733
19734 /**
19735 * When a captions track is enabled in the iOS Safari native player, all other
19736 * tracks are disabled (including metadata tracks), which nulls all of their
19737 * associated cue points. This will restore metadata tracks to their pre-fullscreen
19738 * state in those cases so that cue points are not needlessly lost.
19739 *
19740 * @private
19741 */
19742 restoreMetadataTracksInIOSNativePlayer_() {
19743 const textTracks = this.textTracks();
19744 let metadataTracksPreFullscreenState;
19745
19746 // captures a snapshot of every metadata track's current state
19747 const takeMetadataTrackSnapshot = () => {
19748 metadataTracksPreFullscreenState = [];
19749 for (let i = 0; i < textTracks.length; i++) {
19750 const track = textTracks[i];
19751 if (track.kind === 'metadata') {
19752 metadataTracksPreFullscreenState.push({
19753 track,
19754 storedMode: track.mode
19755 });
19756 }
19757 }
19758 };
19759
19760 // snapshot each metadata track's initial state, and update the snapshot
19761 // each time there is a track 'change' event
19762 takeMetadataTrackSnapshot();
19763 textTracks.addEventListener('change', takeMetadataTrackSnapshot);
19764 this.on('dispose', () => textTracks.removeEventListener('change', takeMetadataTrackSnapshot));
19765 const restoreTrackMode = () => {
19766 for (let i = 0; i < metadataTracksPreFullscreenState.length; i++) {
19767 const storedTrack = metadataTracksPreFullscreenState[i];
19768 if (storedTrack.track.mode === 'disabled' && storedTrack.track.mode !== storedTrack.storedMode) {
19769 storedTrack.track.mode = storedTrack.storedMode;
19770 }
19771 }
19772 // we only want this handler to be executed on the first 'change' event
19773 textTracks.removeEventListener('change', restoreTrackMode);
19774 };
19775
19776 // when we enter fullscreen playback, stop updating the snapshot and
19777 // restore all track modes to their pre-fullscreen state
19778 this.on('webkitbeginfullscreen', () => {
19779 textTracks.removeEventListener('change', takeMetadataTrackSnapshot);
19780
19781 // remove the listener before adding it just in case it wasn't previously removed
19782 textTracks.removeEventListener('change', restoreTrackMode);
19783 textTracks.addEventListener('change', restoreTrackMode);
19784 });
19785
19786 // start updating the snapshot again after leaving fullscreen
19787 this.on('webkitendfullscreen', () => {
19788 // remove the listener before adding it just in case it wasn't previously removed
19789 textTracks.removeEventListener('change', takeMetadataTrackSnapshot);
19790 textTracks.addEventListener('change', takeMetadataTrackSnapshot);
19791
19792 // remove the restoreTrackMode handler in case it wasn't triggered during fullscreen playback
19793 textTracks.removeEventListener('change', restoreTrackMode);
19794 });
19795 }
19796
19797 /**
19798 * Attempt to force override of tracks for the given type
19799 *
19800 * @param {string} type - Track type to override, possible values include 'Audio',
19801 * 'Video', and 'Text'.
19802 * @param {boolean} override - If set to true native audio/video will be overridden,
19803 * otherwise native audio/video will potentially be used.
19804 * @private
19805 */
19806 overrideNative_(type, override) {
19807 // If there is no behavioral change don't add/remove listeners
19808 if (override !== this[`featuresNative${type}Tracks`]) {
19809 return;
19810 }
19811 const lowerCaseType = type.toLowerCase();
19812 if (this[`${lowerCaseType}TracksListeners_`]) {
19813 Object.keys(this[`${lowerCaseType}TracksListeners_`]).forEach(eventName => {
19814 const elTracks = this.el()[`${lowerCaseType}Tracks`];
19815 elTracks.removeEventListener(eventName, this[`${lowerCaseType}TracksListeners_`][eventName]);
19816 });
19817 }
19818 this[`featuresNative${type}Tracks`] = !override;
19819 this[`${lowerCaseType}TracksListeners_`] = null;
19820 this.proxyNativeTracksForType_(lowerCaseType);
19821 }
19822
19823 /**
19824 * Attempt to force override of native audio tracks.
19825 *
19826 * @param {boolean} override - If set to true native audio will be overridden,
19827 * otherwise native audio will potentially be used.
19828 */
19829 overrideNativeAudioTracks(override) {
19830 this.overrideNative_('Audio', override);
19831 }
19832
19833 /**
19834 * Attempt to force override of native video tracks.
19835 *
19836 * @param {boolean} override - If set to true native video will be overridden,
19837 * otherwise native video will potentially be used.
19838 */
19839 overrideNativeVideoTracks(override) {
19840 this.overrideNative_('Video', override);
19841 }
19842
19843 /**
19844 * Proxy native track list events for the given type to our track
19845 * lists if the browser we are playing in supports that type of track list.
19846 *
19847 * @param {string} name - Track type; values include 'audio', 'video', and 'text'
19848 * @private
19849 */
19850 proxyNativeTracksForType_(name) {
19851 const props = NORMAL[name];
19852 const elTracks = this.el()[props.getterName];
19853 const techTracks = this[props.getterName]();
19854 if (!this[`featuresNative${props.capitalName}Tracks`] || !elTracks || !elTracks.addEventListener) {
19855 return;
19856 }
19857 const listeners = {
19858 change: e => {
19859 const event = {
19860 type: 'change',
19861 target: techTracks,
19862 currentTarget: techTracks,
19863 srcElement: techTracks
19864 };
19865 techTracks.trigger(event);
19866
19867 // if we are a text track change event, we should also notify the
19868 // remote text track list. This can potentially cause a false positive
19869 // if we were to get a change event on a non-remote track and
19870 // we triggered the event on the remote text track list which doesn't
19871 // contain that track. However, best practices mean looping through the
19872 // list of tracks and searching for the appropriate mode value, so,
19873 // this shouldn't pose an issue
19874 if (name === 'text') {
19875 this[REMOTE.remoteText.getterName]().trigger(event);
19876 }
19877 },
19878 addtrack(e) {
19879 techTracks.addTrack(e.track);
19880 },
19881 removetrack(e) {
19882 techTracks.removeTrack(e.track);
19883 }
19884 };
19885 const removeOldTracks = function () {
19886 const removeTracks = [];
19887 for (let i = 0; i < techTracks.length; i++) {
19888 let found = false;
19889 for (let j = 0; j < elTracks.length; j++) {
19890 if (elTracks[j] === techTracks[i]) {
19891 found = true;
19892 break;
19893 }
19894 }
19895 if (!found) {
19896 removeTracks.push(techTracks[i]);
19897 }
19898 }
19899 while (removeTracks.length) {
19900 techTracks.removeTrack(removeTracks.shift());
19901 }
19902 };
19903 this[props.getterName + 'Listeners_'] = listeners;
19904 Object.keys(listeners).forEach(eventName => {
19905 const listener = listeners[eventName];
19906 elTracks.addEventListener(eventName, listener);
19907 this.on('dispose', e => elTracks.removeEventListener(eventName, listener));
19908 });
19909
19910 // Remove (native) tracks that are not used anymore
19911 this.on('loadstart', removeOldTracks);
19912 this.on('dispose', e => this.off('loadstart', removeOldTracks));
19913 }
19914
19915 /**
19916 * Proxy all native track list events to our track lists if the browser we are playing
19917 * in supports that type of track list.
19918 *
19919 * @private
19920 */
19921 proxyNativeTracks_() {
19922 NORMAL.names.forEach(name => {
19923 this.proxyNativeTracksForType_(name);
19924 });
19925 }
19926
19927 /**
19928 * Create the `Html5` Tech's DOM element.
19929 *
19930 * @return {Element}
19931 * The element that gets created.
19932 */
19933 createEl() {
19934 let el = this.options_.tag;
19935
19936 // Check if this browser supports moving the element into the box.
19937 // On the iPhone video will break if you move the element,
19938 // So we have to create a brand new element.
19939 // If we ingested the player div, we do not need to move the media element.
19940 if (!el || !(this.options_.playerElIngest || this.movingMediaElementInDOM)) {
19941 // If the original tag is still there, clone and remove it.
19942 if (el) {
19943 const clone = el.cloneNode(true);
19944 if (el.parentNode) {
19945 el.parentNode.insertBefore(clone, el);
19946 }
19947 Html5.disposeMediaElement(el);
19948 el = clone;
19949 } else {
19950 el = document__default["default"].createElement('video');
19951
19952 // determine if native controls should be used
19953 const tagAttributes = this.options_.tag && getAttributes(this.options_.tag);
19954 const attributes = merge({}, tagAttributes);
19955 if (!TOUCH_ENABLED || this.options_.nativeControlsForTouch !== true) {
19956 delete attributes.controls;
19957 }
19958 setAttributes(el, Object.assign(attributes, {
19959 id: this.options_.techId,
19960 class: 'vjs-tech'
19961 }));
19962 }
19963 el.playerId = this.options_.playerId;
19964 }
19965 if (typeof this.options_.preload !== 'undefined') {
19966 setAttribute(el, 'preload', this.options_.preload);
19967 }
19968 if (this.options_.disablePictureInPicture !== undefined) {
19969 el.disablePictureInPicture = this.options_.disablePictureInPicture;
19970 }
19971
19972 // Update specific tag settings, in case they were overridden
19973 // `autoplay` has to be *last* so that `muted` and `playsinline` are present
19974 // when iOS/Safari or other browsers attempt to autoplay.
19975 const settingsAttrs = ['loop', 'muted', 'playsinline', 'autoplay'];
19976 for (let i = 0; i < settingsAttrs.length; i++) {
19977 const attr = settingsAttrs[i];
19978 const value = this.options_[attr];
19979 if (typeof value !== 'undefined') {
19980 if (value) {
19981 setAttribute(el, attr, attr);
19982 } else {
19983 removeAttribute(el, attr);
19984 }
19985 el[attr] = value;
19986 }
19987 }
19988 return el;
19989 }
19990
19991 /**
19992 * This will be triggered if the loadstart event has already fired, before videojs was
19993 * ready. Two known examples of when this can happen are:
19994 * 1. If we're loading the playback object after it has started loading
19995 * 2. The media is already playing the (often with autoplay on) then
19996 *
19997 * This function will fire another loadstart so that videojs can catchup.
19998 *
19999 * @fires Tech#loadstart
20000 *
20001 * @return {undefined}
20002 * returns nothing.
20003 */
20004 handleLateInit_(el) {
20005 if (el.networkState === 0 || el.networkState === 3) {
20006 // The video element hasn't started loading the source yet
20007 // or didn't find a source
20008 return;
20009 }
20010 if (el.readyState === 0) {
20011 // NetworkState is set synchronously BUT loadstart is fired at the
20012 // end of the current stack, usually before setInterval(fn, 0).
20013 // So at this point we know loadstart may have already fired or is
20014 // about to fire, and either way the player hasn't seen it yet.
20015 // We don't want to fire loadstart prematurely here and cause a
20016 // double loadstart so we'll wait and see if it happens between now
20017 // and the next loop, and fire it if not.
20018 // HOWEVER, we also want to make sure it fires before loadedmetadata
20019 // which could also happen between now and the next loop, so we'll
20020 // watch for that also.
20021 let loadstartFired = false;
20022 const setLoadstartFired = function () {
20023 loadstartFired = true;
20024 };
20025 this.on('loadstart', setLoadstartFired);
20026 const triggerLoadstart = function () {
20027 // We did miss the original loadstart. Make sure the player
20028 // sees loadstart before loadedmetadata
20029 if (!loadstartFired) {
20030 this.trigger('loadstart');
20031 }
20032 };
20033 this.on('loadedmetadata', triggerLoadstart);
20034 this.ready(function () {
20035 this.off('loadstart', setLoadstartFired);
20036 this.off('loadedmetadata', triggerLoadstart);
20037 if (!loadstartFired) {
20038 // We did miss the original native loadstart. Fire it now.
20039 this.trigger('loadstart');
20040 }
20041 });
20042 return;
20043 }
20044
20045 // From here on we know that loadstart already fired and we missed it.
20046 // The other readyState events aren't as much of a problem if we double
20047 // them, so not going to go to as much trouble as loadstart to prevent
20048 // that unless we find reason to.
20049 const eventsToTrigger = ['loadstart'];
20050
20051 // loadedmetadata: newly equal to HAVE_METADATA (1) or greater
20052 eventsToTrigger.push('loadedmetadata');
20053
20054 // loadeddata: newly increased to HAVE_CURRENT_DATA (2) or greater
20055 if (el.readyState >= 2) {
20056 eventsToTrigger.push('loadeddata');
20057 }
20058
20059 // canplay: newly increased to HAVE_FUTURE_DATA (3) or greater
20060 if (el.readyState >= 3) {
20061 eventsToTrigger.push('canplay');
20062 }
20063
20064 // canplaythrough: newly equal to HAVE_ENOUGH_DATA (4)
20065 if (el.readyState >= 4) {
20066 eventsToTrigger.push('canplaythrough');
20067 }
20068
20069 // We still need to give the player time to add event listeners
20070 this.ready(function () {
20071 eventsToTrigger.forEach(function (type) {
20072 this.trigger(type);
20073 }, this);
20074 });
20075 }
20076
20077 /**
20078 * Set whether we are scrubbing or not.
20079 * This is used to decide whether we should use `fastSeek` or not.
20080 * `fastSeek` is used to provide trick play on Safari browsers.
20081 *
20082 * @param {boolean} isScrubbing
20083 * - true for we are currently scrubbing
20084 * - false for we are no longer scrubbing
20085 */
20086 setScrubbing(isScrubbing) {
20087 this.isScrubbing_ = isScrubbing;
20088 }
20089
20090 /**
20091 * Get whether we are scrubbing or not.
20092 *
20093 * @return {boolean} isScrubbing
20094 * - true for we are currently scrubbing
20095 * - false for we are no longer scrubbing
20096 */
20097 scrubbing() {
20098 return this.isScrubbing_;
20099 }
20100
20101 /**
20102 * Set current time for the `HTML5` tech.
20103 *
20104 * @param {number} seconds
20105 * Set the current time of the media to this.
20106 */
20107 setCurrentTime(seconds) {
20108 try {
20109 if (this.isScrubbing_ && this.el_.fastSeek && IS_ANY_SAFARI) {
20110 this.el_.fastSeek(seconds);
20111 } else {
20112 this.el_.currentTime = seconds;
20113 }
20114 } catch (e) {
20115 log(e, 'Video is not ready. (Video.js)');
20116 // this.warning(VideoJS.warnings.videoNotReady);
20117 }
20118 }
20119
20120 /**
20121 * Get the current duration of the HTML5 media element.
20122 *
20123 * @return {number}
20124 * The duration of the media or 0 if there is no duration.
20125 */
20126 duration() {
20127 // Android Chrome will report duration as Infinity for VOD HLS until after
20128 // playback has started, which triggers the live display erroneously.
20129 // Return NaN if playback has not started and trigger a durationupdate once
20130 // the duration can be reliably known.
20131 if (this.el_.duration === Infinity && IS_ANDROID && IS_CHROME && this.el_.currentTime === 0) {
20132 // Wait for the first `timeupdate` with currentTime > 0 - there may be
20133 // several with 0
20134 const checkProgress = () => {
20135 if (this.el_.currentTime > 0) {
20136 // Trigger durationchange for genuinely live video
20137 if (this.el_.duration === Infinity) {
20138 this.trigger('durationchange');
20139 }
20140 this.off('timeupdate', checkProgress);
20141 }
20142 };
20143 this.on('timeupdate', checkProgress);
20144 return NaN;
20145 }
20146 return this.el_.duration || NaN;
20147 }
20148
20149 /**
20150 * Get the current width of the HTML5 media element.
20151 *
20152 * @return {number}
20153 * The width of the HTML5 media element.
20154 */
20155 width() {
20156 return this.el_.offsetWidth;
20157 }
20158
20159 /**
20160 * Get the current height of the HTML5 media element.
20161 *
20162 * @return {number}
20163 * The height of the HTML5 media element.
20164 */
20165 height() {
20166 return this.el_.offsetHeight;
20167 }
20168
20169 /**
20170 * Proxy iOS `webkitbeginfullscreen` and `webkitendfullscreen` into
20171 * `fullscreenchange` event.
20172 *
20173 * @private
20174 * @fires fullscreenchange
20175 * @listens webkitendfullscreen
20176 * @listens webkitbeginfullscreen
20177 * @listens webkitbeginfullscreen
20178 */
20179 proxyWebkitFullscreen_() {
20180 if (!('webkitDisplayingFullscreen' in this.el_)) {
20181 return;
20182 }
20183 const endFn = function () {
20184 this.trigger('fullscreenchange', {
20185 isFullscreen: false
20186 });
20187 // Safari will sometimes set controls on the videoelement when existing fullscreen.
20188 if (this.el_.controls && !this.options_.nativeControlsForTouch && this.controls()) {
20189 this.el_.controls = false;
20190 }
20191 };
20192 const beginFn = function () {
20193 if ('webkitPresentationMode' in this.el_ && this.el_.webkitPresentationMode !== 'picture-in-picture') {
20194 this.one('webkitendfullscreen', endFn);
20195 this.trigger('fullscreenchange', {
20196 isFullscreen: true,
20197 // set a flag in case another tech triggers fullscreenchange
20198 nativeIOSFullscreen: true
20199 });
20200 }
20201 };
20202 this.on('webkitbeginfullscreen', beginFn);
20203 this.on('dispose', () => {
20204 this.off('webkitbeginfullscreen', beginFn);
20205 this.off('webkitendfullscreen', endFn);
20206 });
20207 }
20208
20209 /**
20210 * Check if fullscreen is supported on the video el.
20211 *
20212 * @return {boolean}
20213 * - True if fullscreen is supported.
20214 * - False if fullscreen is not supported.
20215 */
20216 supportsFullScreen() {
20217 return typeof this.el_.webkitEnterFullScreen === 'function';
20218 }
20219
20220 /**
20221 * Request that the `HTML5` Tech enter fullscreen.
20222 */
20223 enterFullScreen() {
20224 const video = this.el_;
20225 if (video.paused && video.networkState <= video.HAVE_METADATA) {
20226 // attempt to prime the video element for programmatic access
20227 // this isn't necessary on the desktop but shouldn't hurt
20228 silencePromise(this.el_.play());
20229
20230 // playing and pausing synchronously during the transition to fullscreen
20231 // can get iOS ~6.1 devices into a play/pause loop
20232 this.setTimeout(function () {
20233 video.pause();
20234 try {
20235 video.webkitEnterFullScreen();
20236 } catch (e) {
20237 this.trigger('fullscreenerror', e);
20238 }
20239 }, 0);
20240 } else {
20241 try {
20242 video.webkitEnterFullScreen();
20243 } catch (e) {
20244 this.trigger('fullscreenerror', e);
20245 }
20246 }
20247 }
20248
20249 /**
20250 * Request that the `HTML5` Tech exit fullscreen.
20251 */
20252 exitFullScreen() {
20253 if (!this.el_.webkitDisplayingFullscreen) {
20254 this.trigger('fullscreenerror', new Error('The video is not fullscreen'));
20255 return;
20256 }
20257 this.el_.webkitExitFullScreen();
20258 }
20259
20260 /**
20261 * Create a floating video window always on top of other windows so that users may
20262 * continue consuming media while they interact with other content sites, or
20263 * applications on their device.
20264 *
20265 * @see [Spec]{@link https://wicg.github.io/picture-in-picture}
20266 *
20267 * @return {Promise}
20268 * A promise with a Picture-in-Picture window.
20269 */
20270 requestPictureInPicture() {
20271 return this.el_.requestPictureInPicture();
20272 }
20273
20274 /**
20275 * Native requestVideoFrameCallback if supported by browser/tech, or fallback
20276 * Don't use rVCF on Safari when DRM is playing, as it doesn't fire
20277 * Needs to be checked later than the constructor
20278 * This will be a false positive for clear sources loaded after a Fairplay source
20279 *
20280 * @param {function} cb function to call
20281 * @return {number} id of request
20282 */
20283 requestVideoFrameCallback(cb) {
20284 if (this.featuresVideoFrameCallback && !this.el_.webkitKeys) {
20285 return this.el_.requestVideoFrameCallback(cb);
20286 }
20287 return super.requestVideoFrameCallback(cb);
20288 }
20289
20290 /**
20291 * Native or fallback requestVideoFrameCallback
20292 *
20293 * @param {number} id request id to cancel
20294 */
20295 cancelVideoFrameCallback(id) {
20296 if (this.featuresVideoFrameCallback && !this.el_.webkitKeys) {
20297 this.el_.cancelVideoFrameCallback(id);
20298 } else {
20299 super.cancelVideoFrameCallback(id);
20300 }
20301 }
20302
20303 /**
20304 * A getter/setter for the `Html5` Tech's source object.
20305 * > Note: Please use {@link Html5#setSource}
20306 *
20307 * @param {Tech~SourceObject} [src]
20308 * The source object you want to set on the `HTML5` techs element.
20309 *
20310 * @return {Tech~SourceObject|undefined}
20311 * - The current source object when a source is not passed in.
20312 * - undefined when setting
20313 *
20314 * @deprecated Since version 5.
20315 */
20316 src(src) {
20317 if (src === undefined) {
20318 return this.el_.src;
20319 }
20320
20321 // Setting src through `src` instead of `setSrc` will be deprecated
20322 this.setSrc(src);
20323 }
20324
20325 /**
20326 * Add a <source> element to the <video> element.
20327 *
20328 * @param {string} srcUrl
20329 * The URL of the video source.
20330 *
20331 * @param {string} [mimeType]
20332 * The MIME type of the video source. Optional but recommended.
20333 *
20334 * @return {boolean}
20335 * Returns true if the source element was successfully added, false otherwise.
20336 */
20337 addSourceElement(srcUrl, mimeType) {
20338 if (!srcUrl) {
20339 log.error('Invalid source URL.');
20340 return false;
20341 }
20342 const sourceAttributes = {
20343 src: srcUrl
20344 };
20345 if (mimeType) {
20346 sourceAttributes.type = mimeType;
20347 }
20348 const sourceElement = createEl('source', {}, sourceAttributes);
20349 this.el_.appendChild(sourceElement);
20350 return true;
20351 }
20352
20353 /**
20354 * Remove a <source> element from the <video> element by its URL.
20355 *
20356 * @param {string} srcUrl
20357 * The URL of the source to remove.
20358 *
20359 * @return {boolean}
20360 * Returns true if the source element was successfully removed, false otherwise.
20361 */
20362 removeSourceElement(srcUrl) {
20363 if (!srcUrl) {
20364 log.error('Source URL is required to remove the source element.');
20365 return false;
20366 }
20367 const sourceElements = this.el_.querySelectorAll('source');
20368 for (const sourceElement of sourceElements) {
20369 if (sourceElement.src === srcUrl) {
20370 this.el_.removeChild(sourceElement);
20371 return true;
20372 }
20373 }
20374 log.warn(`No matching source element found with src: ${srcUrl}`);
20375 return false;
20376 }
20377
20378 /**
20379 * Reset the tech by removing all sources and then calling
20380 * {@link Html5.resetMediaElement}.
20381 */
20382 reset() {
20383 Html5.resetMediaElement(this.el_);
20384 }
20385
20386 /**
20387 * Get the current source on the `HTML5` Tech. Falls back to returning the source from
20388 * the HTML5 media element.
20389 *
20390 * @return {Tech~SourceObject}
20391 * The current source object from the HTML5 tech. With a fallback to the
20392 * elements source.
20393 */
20394 currentSrc() {
20395 if (this.currentSource_) {
20396 return this.currentSource_.src;
20397 }
20398 return this.el_.currentSrc;
20399 }
20400
20401 /**
20402 * Set controls attribute for the HTML5 media Element.
20403 *
20404 * @param {string} val
20405 * Value to set the controls attribute to
20406 */
20407 setControls(val) {
20408 this.el_.controls = !!val;
20409 }
20410
20411 /**
20412 * Create and returns a remote {@link TextTrack} object.
20413 *
20414 * @param {string} kind
20415 * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata)
20416 *
20417 * @param {string} [label]
20418 * Label to identify the text track
20419 *
20420 * @param {string} [language]
20421 * Two letter language abbreviation
20422 *
20423 * @return {TextTrack}
20424 * The TextTrack that gets created.
20425 */
20426 addTextTrack(kind, label, language) {
20427 if (!this.featuresNativeTextTracks) {
20428 return super.addTextTrack(kind, label, language);
20429 }
20430 return this.el_.addTextTrack(kind, label, language);
20431 }
20432
20433 /**
20434 * Creates either native TextTrack or an emulated TextTrack depending
20435 * on the value of `featuresNativeTextTracks`
20436 *
20437 * @param {Object} options
20438 * The object should contain the options to initialize the TextTrack with.
20439 *
20440 * @param {string} [options.kind]
20441 * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata).
20442 *
20443 * @param {string} [options.label]
20444 * Label to identify the text track
20445 *
20446 * @param {string} [options.language]
20447 * Two letter language abbreviation.
20448 *
20449 * @param {boolean} [options.default]
20450 * Default this track to on.
20451 *
20452 * @param {string} [options.id]
20453 * The internal id to assign this track.
20454 *
20455 * @param {string} [options.src]
20456 * A source url for the track.
20457 *
20458 * @return {HTMLTrackElement}
20459 * The track element that gets created.
20460 */
20461 createRemoteTextTrack(options) {
20462 if (!this.featuresNativeTextTracks) {
20463 return super.createRemoteTextTrack(options);
20464 }
20465 const htmlTrackElement = document__default["default"].createElement('track');
20466 if (options.kind) {
20467 htmlTrackElement.kind = options.kind;
20468 }
20469 if (options.label) {
20470 htmlTrackElement.label = options.label;
20471 }
20472 if (options.language || options.srclang) {
20473 htmlTrackElement.srclang = options.language || options.srclang;
20474 }
20475 if (options.default) {
20476 htmlTrackElement.default = options.default;
20477 }
20478 if (options.id) {
20479 htmlTrackElement.id = options.id;
20480 }
20481 if (options.src) {
20482 htmlTrackElement.src = options.src;
20483 }
20484 return htmlTrackElement;
20485 }
20486
20487 /**
20488 * Creates a remote text track object and returns an html track element.
20489 *
20490 * @param {Object} options The object should contain values for
20491 * kind, language, label, and src (location of the WebVTT file)
20492 * @param {boolean} [manualCleanup=false] if set to true, the TextTrack
20493 * will not be removed from the TextTrackList and HtmlTrackElementList
20494 * after a source change
20495 * @return {HTMLTrackElement} An Html Track Element.
20496 * This can be an emulated {@link HTMLTrackElement} or a native one.
20497 *
20498 */
20499 addRemoteTextTrack(options, manualCleanup) {
20500 const htmlTrackElement = super.addRemoteTextTrack(options, manualCleanup);
20501 if (this.featuresNativeTextTracks) {
20502 this.el().appendChild(htmlTrackElement);
20503 }
20504 return htmlTrackElement;
20505 }
20506
20507 /**
20508 * Remove remote `TextTrack` from `TextTrackList` object
20509 *
20510 * @param {TextTrack} track
20511 * `TextTrack` object to remove
20512 */
20513 removeRemoteTextTrack(track) {
20514 super.removeRemoteTextTrack(track);
20515 if (this.featuresNativeTextTracks) {
20516 const tracks = this.$$('track');
20517 let i = tracks.length;
20518 while (i--) {
20519 if (track === tracks[i] || track === tracks[i].track) {
20520 this.el().removeChild(tracks[i]);
20521 }
20522 }
20523 }
20524 }
20525
20526 /**
20527 * Gets available media playback quality metrics as specified by the W3C's Media
20528 * Playback Quality API.
20529 *
20530 * @see [Spec]{@link https://wicg.github.io/media-playback-quality}
20531 *
20532 * @return {Object}
20533 * An object with supported media playback quality metrics
20534 */
20535 getVideoPlaybackQuality() {
20536 if (typeof this.el().getVideoPlaybackQuality === 'function') {
20537 return this.el().getVideoPlaybackQuality();
20538 }
20539 const videoPlaybackQuality = {};
20540 if (typeof this.el().webkitDroppedFrameCount !== 'undefined' && typeof this.el().webkitDecodedFrameCount !== 'undefined') {
20541 videoPlaybackQuality.droppedVideoFrames = this.el().webkitDroppedFrameCount;
20542 videoPlaybackQuality.totalVideoFrames = this.el().webkitDecodedFrameCount;
20543 }
20544 if (window__default["default"].performance) {
20545 videoPlaybackQuality.creationTime = window__default["default"].performance.now();
20546 }
20547 return videoPlaybackQuality;
20548 }
20549}
20550
20551/* HTML5 Support Testing ---------------------------------------------------- */
20552
20553/**
20554 * Element for testing browser HTML5 media capabilities
20555 *
20556 * @type {Element}
20557 * @constant
20558 * @private
20559 */
20560defineLazyProperty(Html5, 'TEST_VID', function () {
20561 if (!isReal()) {
20562 return;
20563 }
20564 const video = document__default["default"].createElement('video');
20565 const track = document__default["default"].createElement('track');
20566 track.kind = 'captions';
20567 track.srclang = 'en';
20568 track.label = 'English';
20569 video.appendChild(track);
20570 return video;
20571});
20572
20573/**
20574 * Check if HTML5 media is supported by this browser/device.
20575 *
20576 * @return {boolean}
20577 * - True if HTML5 media is supported.
20578 * - False if HTML5 media is not supported.
20579 */
20580Html5.isSupported = function () {
20581 // IE with no Media Player is a LIAR! (#984)
20582 try {
20583 Html5.TEST_VID.volume = 0.5;
20584 } catch (e) {
20585 return false;
20586 }
20587 return !!(Html5.TEST_VID && Html5.TEST_VID.canPlayType);
20588};
20589
20590/**
20591 * Check if the tech can support the given type
20592 *
20593 * @param {string} type
20594 * The mimetype to check
20595 * @return {string} 'probably', 'maybe', or '' (empty string)
20596 */
20597Html5.canPlayType = function (type) {
20598 return Html5.TEST_VID.canPlayType(type);
20599};
20600
20601/**
20602 * Check if the tech can support the given source
20603 *
20604 * @param {Object} srcObj
20605 * The source object
20606 * @param {Object} options
20607 * The options passed to the tech
20608 * @return {string} 'probably', 'maybe', or '' (empty string)
20609 */
20610Html5.canPlaySource = function (srcObj, options) {
20611 return Html5.canPlayType(srcObj.type);
20612};
20613
20614/**
20615 * Check if the volume can be changed in this browser/device.
20616 * Volume cannot be changed in a lot of mobile devices.
20617 * Specifically, it can't be changed from 1 on iOS.
20618 *
20619 * @return {boolean}
20620 * - True if volume can be controlled
20621 * - False otherwise
20622 */
20623Html5.canControlVolume = function () {
20624 // IE will error if Windows Media Player not installed #3315
20625 try {
20626 const volume = Html5.TEST_VID.volume;
20627 Html5.TEST_VID.volume = volume / 2 + 0.1;
20628 const canControl = volume !== Html5.TEST_VID.volume;
20629
20630 // With the introduction of iOS 15, there are cases where the volume is read as
20631 // changed but reverts back to its original state at the start of the next tick.
20632 // To determine whether volume can be controlled on iOS,
20633 // a timeout is set and the volume is checked asynchronously.
20634 // Since `features` doesn't currently work asynchronously, the value is manually set.
20635 if (canControl && IS_IOS) {
20636 window__default["default"].setTimeout(() => {
20637 if (Html5 && Html5.prototype) {
20638 Html5.prototype.featuresVolumeControl = volume !== Html5.TEST_VID.volume;
20639 }
20640 });
20641
20642 // default iOS to false, which will be updated in the timeout above.
20643 return false;
20644 }
20645 return canControl;
20646 } catch (e) {
20647 return false;
20648 }
20649};
20650
20651/**
20652 * Check if the volume can be muted in this browser/device.
20653 * Some devices, e.g. iOS, don't allow changing volume
20654 * but permits muting/unmuting.
20655 *
20656 * @return {boolean}
20657 * - True if volume can be muted
20658 * - False otherwise
20659 */
20660Html5.canMuteVolume = function () {
20661 try {
20662 const muted = Html5.TEST_VID.muted;
20663
20664 // in some versions of iOS muted property doesn't always
20665 // work, so we want to set both property and attribute
20666 Html5.TEST_VID.muted = !muted;
20667 if (Html5.TEST_VID.muted) {
20668 setAttribute(Html5.TEST_VID, 'muted', 'muted');
20669 } else {
20670 removeAttribute(Html5.TEST_VID, 'muted', 'muted');
20671 }
20672 return muted !== Html5.TEST_VID.muted;
20673 } catch (e) {
20674 return false;
20675 }
20676};
20677
20678/**
20679 * Check if the playback rate can be changed in this browser/device.
20680 *
20681 * @return {boolean}
20682 * - True if playback rate can be controlled
20683 * - False otherwise
20684 */
20685Html5.canControlPlaybackRate = function () {
20686 // Playback rate API is implemented in Android Chrome, but doesn't do anything
20687 // https://github.com/videojs/video.js/issues/3180
20688 if (IS_ANDROID && IS_CHROME && CHROME_VERSION < 58) {
20689 return false;
20690 }
20691 // IE will error if Windows Media Player not installed #3315
20692 try {
20693 const playbackRate = Html5.TEST_VID.playbackRate;
20694 Html5.TEST_VID.playbackRate = playbackRate / 2 + 0.1;
20695 return playbackRate !== Html5.TEST_VID.playbackRate;
20696 } catch (e) {
20697 return false;
20698 }
20699};
20700
20701/**
20702 * Check if we can override a video/audio elements attributes, with
20703 * Object.defineProperty.
20704 *
20705 * @return {boolean}
20706 * - True if builtin attributes can be overridden
20707 * - False otherwise
20708 */
20709Html5.canOverrideAttributes = function () {
20710 // if we cannot overwrite the src/innerHTML property, there is no support
20711 // iOS 7 safari for instance cannot do this.
20712 try {
20713 const noop = () => {};
20714 Object.defineProperty(document__default["default"].createElement('video'), 'src', {
20715 get: noop,
20716 set: noop
20717 });
20718 Object.defineProperty(document__default["default"].createElement('audio'), 'src', {
20719 get: noop,
20720 set: noop
20721 });
20722 Object.defineProperty(document__default["default"].createElement('video'), 'innerHTML', {
20723 get: noop,
20724 set: noop
20725 });
20726 Object.defineProperty(document__default["default"].createElement('audio'), 'innerHTML', {
20727 get: noop,
20728 set: noop
20729 });
20730 } catch (e) {
20731 return false;
20732 }
20733 return true;
20734};
20735
20736/**
20737 * Check to see if native `TextTrack`s are supported by this browser/device.
20738 *
20739 * @return {boolean}
20740 * - True if native `TextTrack`s are supported.
20741 * - False otherwise
20742 */
20743Html5.supportsNativeTextTracks = function () {
20744 return IS_ANY_SAFARI || IS_IOS && IS_CHROME;
20745};
20746
20747/**
20748 * Check to see if native `VideoTrack`s are supported by this browser/device
20749 *
20750 * @return {boolean}
20751 * - True if native `VideoTrack`s are supported.
20752 * - False otherwise
20753 */
20754Html5.supportsNativeVideoTracks = function () {
20755 return !!(Html5.TEST_VID && Html5.TEST_VID.videoTracks);
20756};
20757
20758/**
20759 * Check to see if native `AudioTrack`s are supported by this browser/device
20760 *
20761 * @return {boolean}
20762 * - True if native `AudioTrack`s are supported.
20763 * - False otherwise
20764 */
20765Html5.supportsNativeAudioTracks = function () {
20766 return !!(Html5.TEST_VID && Html5.TEST_VID.audioTracks);
20767};
20768
20769/**
20770 * An array of events available on the Html5 tech.
20771 *
20772 * @private
20773 * @type {Array}
20774 */
20775Html5.Events = ['loadstart', 'suspend', 'abort', 'error', 'emptied', 'stalled', 'loadedmetadata', 'loadeddata', 'canplay', 'canplaythrough', 'playing', 'waiting', 'seeking', 'seeked', 'ended', 'durationchange', 'timeupdate', 'progress', 'play', 'pause', 'ratechange', 'resize', 'volumechange'];
20776
20777/**
20778 * Boolean indicating whether the `Tech` supports volume control.
20779 *
20780 * @type {boolean}
20781 * @default {@link Html5.canControlVolume}
20782 */
20783/**
20784 * Boolean indicating whether the `Tech` supports muting volume.
20785 *
20786 * @type {boolean}
20787 * @default {@link Html5.canMuteVolume}
20788 */
20789
20790/**
20791 * Boolean indicating whether the `Tech` supports changing the speed at which the media
20792 * plays. Examples:
20793 * - Set player to play 2x (twice) as fast
20794 * - Set player to play 0.5x (half) as fast
20795 *
20796 * @type {boolean}
20797 * @default {@link Html5.canControlPlaybackRate}
20798 */
20799
20800/**
20801 * Boolean indicating whether the `Tech` supports the `sourceset` event.
20802 *
20803 * @type {boolean}
20804 * @default
20805 */
20806/**
20807 * Boolean indicating whether the `HTML5` tech currently supports native `TextTrack`s.
20808 *
20809 * @type {boolean}
20810 * @default {@link Html5.supportsNativeTextTracks}
20811 */
20812/**
20813 * Boolean indicating whether the `HTML5` tech currently supports native `VideoTrack`s.
20814 *
20815 * @type {boolean}
20816 * @default {@link Html5.supportsNativeVideoTracks}
20817 */
20818/**
20819 * Boolean indicating whether the `HTML5` tech currently supports native `AudioTrack`s.
20820 *
20821 * @type {boolean}
20822 * @default {@link Html5.supportsNativeAudioTracks}
20823 */
20824[['featuresMuteControl', 'canMuteVolume'], ['featuresPlaybackRate', 'canControlPlaybackRate'], ['featuresSourceset', 'canOverrideAttributes'], ['featuresNativeTextTracks', 'supportsNativeTextTracks'], ['featuresNativeVideoTracks', 'supportsNativeVideoTracks'], ['featuresNativeAudioTracks', 'supportsNativeAudioTracks']].forEach(function ([key, fn]) {
20825 defineLazyProperty(Html5.prototype, key, () => Html5[fn](), true);
20826});
20827Html5.prototype.featuresVolumeControl = Html5.canControlVolume();
20828
20829/**
20830 * Boolean indicating whether the `HTML5` tech currently supports the media element
20831 * moving in the DOM. iOS breaks if you move the media element, so this is set this to
20832 * false there. Everywhere else this should be true.
20833 *
20834 * @type {boolean}
20835 * @default
20836 */
20837Html5.prototype.movingMediaElementInDOM = !IS_IOS;
20838
20839// TODO: Previous comment: No longer appears to be used. Can probably be removed.
20840// Is this true?
20841/**
20842 * Boolean indicating whether the `HTML5` tech currently supports automatic media resize
20843 * when going into fullscreen.
20844 *
20845 * @type {boolean}
20846 * @default
20847 */
20848Html5.prototype.featuresFullscreenResize = true;
20849
20850/**
20851 * Boolean indicating whether the `HTML5` tech currently supports the progress event.
20852 * If this is false, manual `progress` events will be triggered instead.
20853 *
20854 * @type {boolean}
20855 * @default
20856 */
20857Html5.prototype.featuresProgressEvents = true;
20858
20859/**
20860 * Boolean indicating whether the `HTML5` tech currently supports the timeupdate event.
20861 * If this is false, manual `timeupdate` events will be triggered instead.
20862 *
20863 * @default
20864 */
20865Html5.prototype.featuresTimeupdateEvents = true;
20866
20867/**
20868 * Whether the HTML5 el supports `requestVideoFrameCallback`
20869 *
20870 * @type {boolean}
20871 */
20872Html5.prototype.featuresVideoFrameCallback = !!(Html5.TEST_VID && Html5.TEST_VID.requestVideoFrameCallback);
20873Html5.disposeMediaElement = function (el) {
20874 if (!el) {
20875 return;
20876 }
20877 if (el.parentNode) {
20878 el.parentNode.removeChild(el);
20879 }
20880
20881 // remove any child track or source nodes to prevent their loading
20882 while (el.hasChildNodes()) {
20883 el.removeChild(el.firstChild);
20884 }
20885
20886 // remove any src reference. not setting `src=''` because that causes a warning
20887 // in firefox
20888 el.removeAttribute('src');
20889
20890 // force the media element to update its loading state by calling load()
20891 // however IE on Windows 7N has a bug that throws an error so need a try/catch (#793)
20892 if (typeof el.load === 'function') {
20893 // wrapping in an iife so it's not deoptimized (#1060#discussion_r10324473)
20894 (function () {
20895 try {
20896 el.load();
20897 } catch (e) {
20898 // not supported
20899 }
20900 })();
20901 }
20902};
20903Html5.resetMediaElement = function (el) {
20904 if (!el) {
20905 return;
20906 }
20907 const sources = el.querySelectorAll('source');
20908 let i = sources.length;
20909 while (i--) {
20910 el.removeChild(sources[i]);
20911 }
20912
20913 // remove any src reference.
20914 // not setting `src=''` because that throws an error
20915 el.removeAttribute('src');
20916 if (typeof el.load === 'function') {
20917 // wrapping in an iife so it's not deoptimized (#1060#discussion_r10324473)
20918 (function () {
20919 try {
20920 el.load();
20921 } catch (e) {
20922 // satisfy linter
20923 }
20924 })();
20925 }
20926};
20927
20928/* Native HTML5 element property wrapping ----------------------------------- */
20929// Wrap native boolean attributes with getters that check both property and attribute
20930// The list is as followed:
20931// muted, defaultMuted, autoplay, controls, loop, playsinline
20932[
20933/**
20934 * Get the value of `muted` from the media element. `muted` indicates
20935 * that the volume for the media should be set to silent. This does not actually change
20936 * the `volume` attribute.
20937 *
20938 * @method Html5#muted
20939 * @return {boolean}
20940 * - True if the value of `volume` should be ignored and the audio set to silent.
20941 * - False if the value of `volume` should be used.
20942 *
20943 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-muted}
20944 */
20945'muted',
20946/**
20947 * Get the value of `defaultMuted` from the media element. `defaultMuted` indicates
20948 * whether the media should start muted or not. Only changes the default state of the
20949 * media. `muted` and `defaultMuted` can have different values. {@link Html5#muted} indicates the
20950 * current state.
20951 *
20952 * @method Html5#defaultMuted
20953 * @return {boolean}
20954 * - The value of `defaultMuted` from the media element.
20955 * - True indicates that the media should start muted.
20956 * - False indicates that the media should not start muted
20957 *
20958 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultmuted}
20959 */
20960'defaultMuted',
20961/**
20962 * Get the value of `autoplay` from the media element. `autoplay` indicates
20963 * that the media should start to play as soon as the page is ready.
20964 *
20965 * @method Html5#autoplay
20966 * @return {boolean}
20967 * - The value of `autoplay` from the media element.
20968 * - True indicates that the media should start as soon as the page loads.
20969 * - False indicates that the media should not start as soon as the page loads.
20970 *
20971 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-autoplay}
20972 */
20973'autoplay',
20974/**
20975 * Get the value of `controls` from the media element. `controls` indicates
20976 * whether the native media controls should be shown or hidden.
20977 *
20978 * @method Html5#controls
20979 * @return {boolean}
20980 * - The value of `controls` from the media element.
20981 * - True indicates that native controls should be showing.
20982 * - False indicates that native controls should be hidden.
20983 *
20984 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-controls}
20985 */
20986'controls',
20987/**
20988 * Get the value of `loop` from the media element. `loop` indicates
20989 * that the media should return to the start of the media and continue playing once
20990 * it reaches the end.
20991 *
20992 * @method Html5#loop
20993 * @return {boolean}
20994 * - The value of `loop` from the media element.
20995 * - True indicates that playback should seek back to start once
20996 * the end of a media is reached.
20997 * - False indicates that playback should not loop back to the start when the
20998 * end of the media is reached.
20999 *
21000 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-loop}
21001 */
21002'loop',
21003/**
21004 * Get the value of `playsinline` from the media element. `playsinline` indicates
21005 * to the browser that non-fullscreen playback is preferred when fullscreen
21006 * playback is the native default, such as in iOS Safari.
21007 *
21008 * @method Html5#playsinline
21009 * @return {boolean}
21010 * - The value of `playsinline` from the media element.
21011 * - True indicates that the media should play inline.
21012 * - False indicates that the media should not play inline.
21013 *
21014 * @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline}
21015 */
21016'playsinline'].forEach(function (prop) {
21017 Html5.prototype[prop] = function () {
21018 return this.el_[prop] || this.el_.hasAttribute(prop);
21019 };
21020});
21021
21022// Wrap native boolean attributes with setters that set both property and attribute
21023// The list is as followed:
21024// setMuted, setDefaultMuted, setAutoplay, setLoop, setPlaysinline
21025// setControls is special-cased above
21026[
21027/**
21028 * Set the value of `muted` on the media element. `muted` indicates that the current
21029 * audio level should be silent.
21030 *
21031 * @method Html5#setMuted
21032 * @param {boolean} muted
21033 * - True if the audio should be set to silent
21034 * - False otherwise
21035 *
21036 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-muted}
21037 */
21038'muted',
21039/**
21040 * Set the value of `defaultMuted` on the media element. `defaultMuted` indicates that the current
21041 * audio level should be silent, but will only effect the muted level on initial playback..
21042 *
21043 * @method Html5.prototype.setDefaultMuted
21044 * @param {boolean} defaultMuted
21045 * - True if the audio should be set to silent
21046 * - False otherwise
21047 *
21048 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultmuted}
21049 */
21050'defaultMuted',
21051/**
21052 * Set the value of `autoplay` on the media element. `autoplay` indicates
21053 * that the media should start to play as soon as the page is ready.
21054 *
21055 * @method Html5#setAutoplay
21056 * @param {boolean} autoplay
21057 * - True indicates that the media should start as soon as the page loads.
21058 * - False indicates that the media should not start as soon as the page loads.
21059 *
21060 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-autoplay}
21061 */
21062'autoplay',
21063/**
21064 * Set the value of `loop` on the media element. `loop` indicates
21065 * that the media should return to the start of the media and continue playing once
21066 * it reaches the end.
21067 *
21068 * @method Html5#setLoop
21069 * @param {boolean} loop
21070 * - True indicates that playback should seek back to start once
21071 * the end of a media is reached.
21072 * - False indicates that playback should not loop back to the start when the
21073 * end of the media is reached.
21074 *
21075 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-loop}
21076 */
21077'loop',
21078/**
21079 * Set the value of `playsinline` from the media element. `playsinline` indicates
21080 * to the browser that non-fullscreen playback is preferred when fullscreen
21081 * playback is the native default, such as in iOS Safari.
21082 *
21083 * @method Html5#setPlaysinline
21084 * @param {boolean} playsinline
21085 * - True indicates that the media should play inline.
21086 * - False indicates that the media should not play inline.
21087 *
21088 * @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline}
21089 */
21090'playsinline'].forEach(function (prop) {
21091 Html5.prototype['set' + toTitleCase(prop)] = function (v) {
21092 this.el_[prop] = v;
21093 if (v) {
21094 this.el_.setAttribute(prop, prop);
21095 } else {
21096 this.el_.removeAttribute(prop);
21097 }
21098 };
21099});
21100
21101// Wrap native properties with a getter
21102// The list is as followed
21103// paused, currentTime, buffered, volume, poster, preload, error, seeking
21104// seekable, ended, playbackRate, defaultPlaybackRate, disablePictureInPicture
21105// played, networkState, readyState, videoWidth, videoHeight, crossOrigin
21106[
21107/**
21108 * Get the value of `paused` from the media element. `paused` indicates whether the media element
21109 * is currently paused or not.
21110 *
21111 * @method Html5#paused
21112 * @return {boolean}
21113 * The value of `paused` from the media element.
21114 *
21115 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-paused}
21116 */
21117'paused',
21118/**
21119 * Get the value of `currentTime` from the media element. `currentTime` indicates
21120 * the current second that the media is at in playback.
21121 *
21122 * @method Html5#currentTime
21123 * @return {number}
21124 * The value of `currentTime` from the media element.
21125 *
21126 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-currenttime}
21127 */
21128'currentTime',
21129/**
21130 * Get the value of `buffered` from the media element. `buffered` is a `TimeRange`
21131 * object that represents the parts of the media that are already downloaded and
21132 * available for playback.
21133 *
21134 * @method Html5#buffered
21135 * @return {TimeRange}
21136 * The value of `buffered` from the media element.
21137 *
21138 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-buffered}
21139 */
21140'buffered',
21141/**
21142 * Get the value of `volume` from the media element. `volume` indicates
21143 * the current playback volume of audio for a media. `volume` will be a value from 0
21144 * (silent) to 1 (loudest and default).
21145 *
21146 * @method Html5#volume
21147 * @return {number}
21148 * The value of `volume` from the media element. Value will be between 0-1.
21149 *
21150 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-a-volume}
21151 */
21152'volume',
21153/**
21154 * Get the value of `poster` from the media element. `poster` indicates
21155 * that the url of an image file that can/will be shown when no media data is available.
21156 *
21157 * @method Html5#poster
21158 * @return {string}
21159 * The value of `poster` from the media element. Value will be a url to an
21160 * image.
21161 *
21162 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-video-poster}
21163 */
21164'poster',
21165/**
21166 * Get the value of `preload` from the media element. `preload` indicates
21167 * what should download before the media is interacted with. It can have the following
21168 * values:
21169 * - none: nothing should be downloaded
21170 * - metadata: poster and the first few frames of the media may be downloaded to get
21171 * media dimensions and other metadata
21172 * - auto: allow the media and metadata for the media to be downloaded before
21173 * interaction
21174 *
21175 * @method Html5#preload
21176 * @return {string}
21177 * The value of `preload` from the media element. Will be 'none', 'metadata',
21178 * or 'auto'.
21179 *
21180 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-preload}
21181 */
21182'preload',
21183/**
21184 * Get the value of the `error` from the media element. `error` indicates any
21185 * MediaError that may have occurred during playback. If error returns null there is no
21186 * current error.
21187 *
21188 * @method Html5#error
21189 * @return {MediaError|null}
21190 * The value of `error` from the media element. Will be `MediaError` if there
21191 * is a current error and null otherwise.
21192 *
21193 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-error}
21194 */
21195'error',
21196/**
21197 * Get the value of `seeking` from the media element. `seeking` indicates whether the
21198 * media is currently seeking to a new position or not.
21199 *
21200 * @method Html5#seeking
21201 * @return {boolean}
21202 * - The value of `seeking` from the media element.
21203 * - True indicates that the media is currently seeking to a new position.
21204 * - False indicates that the media is not seeking to a new position at this time.
21205 *
21206 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-seeking}
21207 */
21208'seeking',
21209/**
21210 * Get the value of `seekable` from the media element. `seekable` returns a
21211 * `TimeRange` object indicating ranges of time that can currently be `seeked` to.
21212 *
21213 * @method Html5#seekable
21214 * @return {TimeRange}
21215 * The value of `seekable` from the media element. A `TimeRange` object
21216 * indicating the current ranges of time that can be seeked to.
21217 *
21218 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-seekable}
21219 */
21220'seekable',
21221/**
21222 * Get the value of `ended` from the media element. `ended` indicates whether
21223 * the media has reached the end or not.
21224 *
21225 * @method Html5#ended
21226 * @return {boolean}
21227 * - The value of `ended` from the media element.
21228 * - True indicates that the media has ended.
21229 * - False indicates that the media has not ended.
21230 *
21231 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-ended}
21232 */
21233'ended',
21234/**
21235 * Get the value of `playbackRate` from the media element. `playbackRate` indicates
21236 * the rate at which the media is currently playing back. Examples:
21237 * - if playbackRate is set to 2, media will play twice as fast.
21238 * - if playbackRate is set to 0.5, media will play half as fast.
21239 *
21240 * @method Html5#playbackRate
21241 * @return {number}
21242 * The value of `playbackRate` from the media element. A number indicating
21243 * the current playback speed of the media, where 1 is normal speed.
21244 *
21245 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate}
21246 */
21247'playbackRate',
21248/**
21249 * Get the value of `defaultPlaybackRate` from the media element. `defaultPlaybackRate` indicates
21250 * the rate at which the media is currently playing back. This value will not indicate the current
21251 * `playbackRate` after playback has started, use {@link Html5#playbackRate} for that.
21252 *
21253 * Examples:
21254 * - if defaultPlaybackRate is set to 2, media will play twice as fast.
21255 * - if defaultPlaybackRate is set to 0.5, media will play half as fast.
21256 *
21257 * @method Html5.prototype.defaultPlaybackRate
21258 * @return {number}
21259 * The value of `defaultPlaybackRate` from the media element. A number indicating
21260 * the current playback speed of the media, where 1 is normal speed.
21261 *
21262 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate}
21263 */
21264'defaultPlaybackRate',
21265/**
21266 * Get the value of 'disablePictureInPicture' from the video element.
21267 *
21268 * @method Html5#disablePictureInPicture
21269 * @return {boolean} value
21270 * - The value of `disablePictureInPicture` from the video element.
21271 * - True indicates that the video can't be played in Picture-In-Picture mode
21272 * - False indicates that the video can be played in Picture-In-Picture mode
21273 *
21274 * @see [Spec]{@link https://w3c.github.io/picture-in-picture/#disable-pip}
21275 */
21276'disablePictureInPicture',
21277/**
21278 * Get the value of `played` from the media element. `played` returns a `TimeRange`
21279 * object representing points in the media timeline that have been played.
21280 *
21281 * @method Html5#played
21282 * @return {TimeRange}
21283 * The value of `played` from the media element. A `TimeRange` object indicating
21284 * the ranges of time that have been played.
21285 *
21286 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-played}
21287 */
21288'played',
21289/**
21290 * Get the value of `networkState` from the media element. `networkState` indicates
21291 * the current network state. It returns an enumeration from the following list:
21292 * - 0: NETWORK_EMPTY
21293 * - 1: NETWORK_IDLE
21294 * - 2: NETWORK_LOADING
21295 * - 3: NETWORK_NO_SOURCE
21296 *
21297 * @method Html5#networkState
21298 * @return {number}
21299 * The value of `networkState` from the media element. This will be a number
21300 * from the list in the description.
21301 *
21302 * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-networkstate}
21303 */
21304'networkState',
21305/**
21306 * Get the value of `readyState` from the media element. `readyState` indicates
21307 * the current state of the media element. It returns an enumeration from the
21308 * following list:
21309 * - 0: HAVE_NOTHING
21310 * - 1: HAVE_METADATA
21311 * - 2: HAVE_CURRENT_DATA
21312 * - 3: HAVE_FUTURE_DATA
21313 * - 4: HAVE_ENOUGH_DATA
21314 *
21315 * @method Html5#readyState
21316 * @return {number}
21317 * The value of `readyState` from the media element. This will be a number
21318 * from the list in the description.
21319 *
21320 * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#ready-states}
21321 */
21322'readyState',
21323/**
21324 * Get the value of `videoWidth` from the video element. `videoWidth` indicates
21325 * the current width of the video in css pixels.
21326 *
21327 * @method Html5#videoWidth
21328 * @return {number}
21329 * The value of `videoWidth` from the video element. This will be a number
21330 * in css pixels.
21331 *
21332 * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-video-videowidth}
21333 */
21334'videoWidth',
21335/**
21336 * Get the value of `videoHeight` from the video element. `videoHeight` indicates
21337 * the current height of the video in css pixels.
21338 *
21339 * @method Html5#videoHeight
21340 * @return {number}
21341 * The value of `videoHeight` from the video element. This will be a number
21342 * in css pixels.
21343 *
21344 * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-video-videowidth}
21345 */
21346'videoHeight',
21347/**
21348 * Get the value of `crossOrigin` from the media element. `crossOrigin` indicates
21349 * to the browser that should sent the cookies along with the requests for the
21350 * different assets/playlists
21351 *
21352 * @method Html5#crossOrigin
21353 * @return {string}
21354 * - anonymous indicates that the media should not sent cookies.
21355 * - use-credentials indicates that the media should sent cookies along the requests.
21356 *
21357 * @see [Spec]{@link https://html.spec.whatwg.org/#attr-media-crossorigin}
21358 */
21359'crossOrigin'].forEach(function (prop) {
21360 Html5.prototype[prop] = function () {
21361 return this.el_[prop];
21362 };
21363});
21364
21365// Wrap native properties with a setter in this format:
21366// set + toTitleCase(name)
21367// The list is as follows:
21368// setVolume, setSrc, setPoster, setPreload, setPlaybackRate, setDefaultPlaybackRate,
21369// setDisablePictureInPicture, setCrossOrigin
21370[
21371/**
21372 * Set the value of `volume` on the media element. `volume` indicates the current
21373 * audio level as a percentage in decimal form. This means that 1 is 100%, 0.5 is 50%, and
21374 * so on.
21375 *
21376 * @method Html5#setVolume
21377 * @param {number} percentAsDecimal
21378 * The volume percent as a decimal. Valid range is from 0-1.
21379 *
21380 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-a-volume}
21381 */
21382'volume',
21383/**
21384 * Set the value of `src` on the media element. `src` indicates the current
21385 * {@link Tech~SourceObject} for the media.
21386 *
21387 * @method Html5#setSrc
21388 * @param {Tech~SourceObject} src
21389 * The source object to set as the current source.
21390 *
21391 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-src}
21392 */
21393'src',
21394/**
21395 * Set the value of `poster` on the media element. `poster` is the url to
21396 * an image file that can/will be shown when no media data is available.
21397 *
21398 * @method Html5#setPoster
21399 * @param {string} poster
21400 * The url to an image that should be used as the `poster` for the media
21401 * element.
21402 *
21403 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-poster}
21404 */
21405'poster',
21406/**
21407 * Set the value of `preload` on the media element. `preload` indicates
21408 * what should download before the media is interacted with. It can have the following
21409 * values:
21410 * - none: nothing should be downloaded
21411 * - metadata: poster and the first few frames of the media may be downloaded to get
21412 * media dimensions and other metadata
21413 * - auto: allow the media and metadata for the media to be downloaded before
21414 * interaction
21415 *
21416 * @method Html5#setPreload
21417 * @param {string} preload
21418 * The value of `preload` to set on the media element. Must be 'none', 'metadata',
21419 * or 'auto'.
21420 *
21421 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-preload}
21422 */
21423'preload',
21424/**
21425 * Set the value of `playbackRate` on the media element. `playbackRate` indicates
21426 * the rate at which the media should play back. Examples:
21427 * - if playbackRate is set to 2, media will play twice as fast.
21428 * - if playbackRate is set to 0.5, media will play half as fast.
21429 *
21430 * @method Html5#setPlaybackRate
21431 * @return {number}
21432 * The value of `playbackRate` from the media element. A number indicating
21433 * the current playback speed of the media, where 1 is normal speed.
21434 *
21435 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate}
21436 */
21437'playbackRate',
21438/**
21439 * Set the value of `defaultPlaybackRate` on the media element. `defaultPlaybackRate` indicates
21440 * the rate at which the media should play back upon initial startup. Changing this value
21441 * after a video has started will do nothing. Instead you should used {@link Html5#setPlaybackRate}.
21442 *
21443 * Example Values:
21444 * - if playbackRate is set to 2, media will play twice as fast.
21445 * - if playbackRate is set to 0.5, media will play half as fast.
21446 *
21447 * @method Html5.prototype.setDefaultPlaybackRate
21448 * @return {number}
21449 * The value of `defaultPlaybackRate` from the media element. A number indicating
21450 * the current playback speed of the media, where 1 is normal speed.
21451 *
21452 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultplaybackrate}
21453 */
21454'defaultPlaybackRate',
21455/**
21456 * Prevents the browser from suggesting a Picture-in-Picture context menu
21457 * or to request Picture-in-Picture automatically in some cases.
21458 *
21459 * @method Html5#setDisablePictureInPicture
21460 * @param {boolean} value
21461 * The true value will disable Picture-in-Picture mode.
21462 *
21463 * @see [Spec]{@link https://w3c.github.io/picture-in-picture/#disable-pip}
21464 */
21465'disablePictureInPicture',
21466/**
21467 * Set the value of `crossOrigin` from the media element. `crossOrigin` indicates
21468 * to the browser that should sent the cookies along with the requests for the
21469 * different assets/playlists
21470 *
21471 * @method Html5#setCrossOrigin
21472 * @param {string} crossOrigin
21473 * - anonymous indicates that the media should not sent cookies.
21474 * - use-credentials indicates that the media should sent cookies along the requests.
21475 *
21476 * @see [Spec]{@link https://html.spec.whatwg.org/#attr-media-crossorigin}
21477 */
21478'crossOrigin'].forEach(function (prop) {
21479 Html5.prototype['set' + toTitleCase(prop)] = function (v) {
21480 this.el_[prop] = v;
21481 };
21482});
21483
21484// wrap native functions with a function
21485// The list is as follows:
21486// pause, load, play
21487[
21488/**
21489 * A wrapper around the media elements `pause` function. This will call the `HTML5`
21490 * media elements `pause` function.
21491 *
21492 * @method Html5#pause
21493 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-pause}
21494 */
21495'pause',
21496/**
21497 * A wrapper around the media elements `load` function. This will call the `HTML5`s
21498 * media element `load` function.
21499 *
21500 * @method Html5#load
21501 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-load}
21502 */
21503'load',
21504/**
21505 * A wrapper around the media elements `play` function. This will call the `HTML5`s
21506 * media element `play` function.
21507 *
21508 * @method Html5#play
21509 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-play}
21510 */
21511'play'].forEach(function (prop) {
21512 Html5.prototype[prop] = function () {
21513 return this.el_[prop]();
21514 };
21515});
21516Tech.withSourceHandlers(Html5);
21517
21518/**
21519 * Native source handler for Html5, simply passes the source to the media element.
21520 *
21521 * @property {Tech~SourceObject} source
21522 * The source object
21523 *
21524 * @property {Html5} tech
21525 * The instance of the HTML5 tech.
21526 */
21527Html5.nativeSourceHandler = {};
21528
21529/**
21530 * Check if the media element can play the given mime type.
21531 *
21532 * @param {string} type
21533 * The mimetype to check
21534 *
21535 * @return {string}
21536 * 'probably', 'maybe', or '' (empty string)
21537 */
21538Html5.nativeSourceHandler.canPlayType = function (type) {
21539 // IE without MediaPlayer throws an error (#519)
21540 try {
21541 return Html5.TEST_VID.canPlayType(type);
21542 } catch (e) {
21543 return '';
21544 }
21545};
21546
21547/**
21548 * Check if the media element can handle a source natively.
21549 *
21550 * @param {Tech~SourceObject} source
21551 * The source object
21552 *
21553 * @param {Object} [options]
21554 * Options to be passed to the tech.
21555 *
21556 * @return {string}
21557 * 'probably', 'maybe', or '' (empty string).
21558 */
21559Html5.nativeSourceHandler.canHandleSource = function (source, options) {
21560 // If a type was provided we should rely on that
21561 if (source.type) {
21562 return Html5.nativeSourceHandler.canPlayType(source.type);
21563
21564 // If no type, fall back to checking 'video/[EXTENSION]'
21565 } else if (source.src) {
21566 const ext = getFileExtension(source.src);
21567 return Html5.nativeSourceHandler.canPlayType(`video/${ext}`);
21568 }
21569 return '';
21570};
21571
21572/**
21573 * Pass the source to the native media element.
21574 *
21575 * @param {Tech~SourceObject} source
21576 * The source object
21577 *
21578 * @param {Html5} tech
21579 * The instance of the Html5 tech
21580 *
21581 * @param {Object} [options]
21582 * The options to pass to the source
21583 */
21584Html5.nativeSourceHandler.handleSource = function (source, tech, options) {
21585 tech.setSrc(source.src);
21586};
21587
21588/**
21589 * A noop for the native dispose function, as cleanup is not needed.
21590 */
21591Html5.nativeSourceHandler.dispose = function () {};
21592
21593// Register the native source handler
21594Html5.registerSourceHandler(Html5.nativeSourceHandler);
21595Tech.registerTech('Html5', Html5);
21596
21597/**
21598 * @file player.js
21599 */
21600
21601/** @import { TimeRange } from './utils/time' */
21602/** @import HtmlTrackElement from './tracks/html-track-element' */
21603
21604/**
21605 * @callback PlayerReadyCallback
21606 * @this {Player}
21607 * @returns {void}
21608 */
21609
21610// The following tech events are simply re-triggered
21611// on the player when they happen
21612const TECH_EVENTS_RETRIGGER = [
21613/**
21614 * Fired while the user agent is downloading media data.
21615 *
21616 * @event Player#progress
21617 * @type {Event}
21618 */
21619/**
21620 * Retrigger the `progress` event that was triggered by the {@link Tech}.
21621 *
21622 * @private
21623 * @method Player#handleTechProgress_
21624 * @fires Player#progress
21625 * @listens Tech#progress
21626 */
21627'progress',
21628/**
21629 * Fires when the loading of an audio/video is aborted.
21630 *
21631 * @event Player#abort
21632 * @type {Event}
21633 */
21634/**
21635 * Retrigger the `abort` event that was triggered by the {@link Tech}.
21636 *
21637 * @private
21638 * @method Player#handleTechAbort_
21639 * @fires Player#abort
21640 * @listens Tech#abort
21641 */
21642'abort',
21643/**
21644 * Fires when the browser is intentionally not getting media data.
21645 *
21646 * @event Player#suspend
21647 * @type {Event}
21648 */
21649/**
21650 * Retrigger the `suspend` event that was triggered by the {@link Tech}.
21651 *
21652 * @private
21653 * @method Player#handleTechSuspend_
21654 * @fires Player#suspend
21655 * @listens Tech#suspend
21656 */
21657'suspend',
21658/**
21659 * Fires when the current playlist is empty.
21660 *
21661 * @event Player#emptied
21662 * @type {Event}
21663 */
21664/**
21665 * Retrigger the `emptied` event that was triggered by the {@link Tech}.
21666 *
21667 * @private
21668 * @method Player#handleTechEmptied_
21669 * @fires Player#emptied
21670 * @listens Tech#emptied
21671 */
21672'emptied',
21673/**
21674 * Fires when the browser is trying to get media data, but data is not available.
21675 *
21676 * @event Player#stalled
21677 * @type {Event}
21678 */
21679/**
21680 * Retrigger the `stalled` event that was triggered by the {@link Tech}.
21681 *
21682 * @private
21683 * @method Player#handleTechStalled_
21684 * @fires Player#stalled
21685 * @listens Tech#stalled
21686 */
21687'stalled',
21688/**
21689 * Fires when the browser has loaded meta data for the audio/video.
21690 *
21691 * @event Player#loadedmetadata
21692 * @type {Event}
21693 */
21694/**
21695 * Retrigger the `loadedmetadata` event that was triggered by the {@link Tech}.
21696 *
21697 * @private
21698 * @method Player#handleTechLoadedmetadata_
21699 * @fires Player#loadedmetadata
21700 * @listens Tech#loadedmetadata
21701 */
21702'loadedmetadata',
21703/**
21704 * Fires when the browser has loaded the current frame of the audio/video.
21705 *
21706 * @event Player#loadeddata
21707 * @type {event}
21708 */
21709/**
21710 * Retrigger the `loadeddata` event that was triggered by the {@link Tech}.
21711 *
21712 * @private
21713 * @method Player#handleTechLoaddeddata_
21714 * @fires Player#loadeddata
21715 * @listens Tech#loadeddata
21716 */
21717'loadeddata',
21718/**
21719 * Fires when the current playback position has changed.
21720 *
21721 * @event Player#timeupdate
21722 * @type {event}
21723 */
21724/**
21725 * Retrigger the `timeupdate` event that was triggered by the {@link Tech}.
21726 *
21727 * @private
21728 * @method Player#handleTechTimeUpdate_
21729 * @fires Player#timeupdate
21730 * @listens Tech#timeupdate
21731 */
21732'timeupdate',
21733/**
21734 * Fires when the video's intrinsic dimensions change
21735 *
21736 * @event Player#resize
21737 * @type {event}
21738 */
21739/**
21740 * Retrigger the `resize` event that was triggered by the {@link Tech}.
21741 *
21742 * @private
21743 * @method Player#handleTechResize_
21744 * @fires Player#resize
21745 * @listens Tech#resize
21746 */
21747'resize',
21748/**
21749 * Fires when the volume has been changed
21750 *
21751 * @event Player#volumechange
21752 * @type {event}
21753 */
21754/**
21755 * Retrigger the `volumechange` event that was triggered by the {@link Tech}.
21756 *
21757 * @private
21758 * @method Player#handleTechVolumechange_
21759 * @fires Player#volumechange
21760 * @listens Tech#volumechange
21761 */
21762'volumechange',
21763/**
21764 * Fires when the text track has been changed
21765 *
21766 * @event Player#texttrackchange
21767 * @type {event}
21768 */
21769/**
21770 * Retrigger the `texttrackchange` event that was triggered by the {@link Tech}.
21771 *
21772 * @private
21773 * @method Player#handleTechTexttrackchange_
21774 * @fires Player#texttrackchange
21775 * @listens Tech#texttrackchange
21776 */
21777'texttrackchange'];
21778
21779// events to queue when playback rate is zero
21780// this is a hash for the sole purpose of mapping non-camel-cased event names
21781// to camel-cased function names
21782const TECH_EVENTS_QUEUE = {
21783 canplay: 'CanPlay',
21784 canplaythrough: 'CanPlayThrough',
21785 playing: 'Playing',
21786 seeked: 'Seeked'
21787};
21788const BREAKPOINT_ORDER = ['tiny', 'xsmall', 'small', 'medium', 'large', 'xlarge', 'huge'];
21789const BREAKPOINT_CLASSES = {};
21790
21791// grep: vjs-layout-tiny
21792// grep: vjs-layout-x-small
21793// grep: vjs-layout-small
21794// grep: vjs-layout-medium
21795// grep: vjs-layout-large
21796// grep: vjs-layout-x-large
21797// grep: vjs-layout-huge
21798BREAKPOINT_ORDER.forEach(k => {
21799 const v = k.charAt(0) === 'x' ? `x-${k.substring(1)}` : k;
21800 BREAKPOINT_CLASSES[k] = `vjs-layout-${v}`;
21801});
21802const DEFAULT_BREAKPOINTS = {
21803 tiny: 210,
21804 xsmall: 320,
21805 small: 425,
21806 medium: 768,
21807 large: 1440,
21808 xlarge: 2560,
21809 huge: Infinity
21810};
21811
21812/**
21813 * An instance of the `Player` class is created when any of the Video.js setup methods
21814 * are used to initialize a video.
21815 *
21816 * After an instance has been created it can be accessed globally in three ways:
21817 * 1. By calling `videojs.getPlayer('example_video_1');`
21818 * 2. By calling `videojs('example_video_1');` (not recommended)
21819 * 2. By using it directly via `videojs.players.example_video_1;`
21820 *
21821 * @extends Component
21822 * @global
21823 */
21824class Player extends Component {
21825 /**
21826 * Create an instance of this class.
21827 *
21828 * @param {Element} tag
21829 * The original video DOM element used for configuring options.
21830 *
21831 * @param {Object} [options]
21832 * Object of option names and values.
21833 *
21834 * @param {PlayerReadyCallback} [ready]
21835 * Ready callback function.
21836 */
21837 constructor(tag, options, ready) {
21838 // Make sure tag ID exists
21839 // also here.. probably better
21840 tag.id = tag.id || options.id || `vjs_video_${newGUID()}`;
21841
21842 // Set Options
21843 // The options argument overrides options set in the video tag
21844 // which overrides globally set options.
21845 // This latter part coincides with the load order
21846 // (tag must exist before Player)
21847 options = Object.assign(Player.getTagSettings(tag), options);
21848
21849 // Delay the initialization of children because we need to set up
21850 // player properties first, and can't use `this` before `super()`
21851 options.initChildren = false;
21852
21853 // Same with creating the element
21854 options.createEl = false;
21855
21856 // don't auto mixin the evented mixin
21857 options.evented = false;
21858
21859 // we don't want the player to report touch activity on itself
21860 // see enableTouchActivity in Component
21861 options.reportTouchActivity = false;
21862
21863 // If language is not set, get the closest lang attribute
21864 if (!options.language) {
21865 const closest = tag.closest('[lang]');
21866 if (closest) {
21867 options.language = closest.getAttribute('lang');
21868 }
21869 }
21870
21871 // Run base component initializing with new options
21872 super(null, options, ready);
21873
21874 // Create bound methods for document listeners.
21875 this.boundDocumentFullscreenChange_ = e => this.documentFullscreenChange_(e);
21876 this.boundFullWindowOnEscKey_ = e => this.fullWindowOnEscKey(e);
21877 this.boundUpdateStyleEl_ = e => this.updateStyleEl_(e);
21878 this.boundApplyInitTime_ = e => this.applyInitTime_(e);
21879 this.boundUpdateCurrentBreakpoint_ = e => this.updateCurrentBreakpoint_(e);
21880 this.boundHandleTechClick_ = e => this.handleTechClick_(e);
21881 this.boundHandleTechDoubleClick_ = e => this.handleTechDoubleClick_(e);
21882 this.boundHandleTechTouchStart_ = e => this.handleTechTouchStart_(e);
21883 this.boundHandleTechTouchMove_ = e => this.handleTechTouchMove_(e);
21884 this.boundHandleTechTouchEnd_ = e => this.handleTechTouchEnd_(e);
21885 this.boundHandleTechTap_ = e => this.handleTechTap_(e);
21886 this.boundUpdatePlayerHeightOnAudioOnlyMode_ = e => this.updatePlayerHeightOnAudioOnlyMode_(e);
21887
21888 // default isFullscreen_ to false
21889 this.isFullscreen_ = false;
21890
21891 // create logger
21892 this.log = createLogger(this.id_);
21893
21894 // Hold our own reference to fullscreen api so it can be mocked in tests
21895 this.fsApi_ = FullscreenApi;
21896
21897 // Tracks when a tech changes the poster
21898 this.isPosterFromTech_ = false;
21899
21900 // Holds callback info that gets queued when playback rate is zero
21901 // and a seek is happening
21902 this.queuedCallbacks_ = [];
21903
21904 // Turn off API access because we're loading a new tech that might load asynchronously
21905 this.isReady_ = false;
21906
21907 // Init state hasStarted_
21908 this.hasStarted_ = false;
21909
21910 // Init state userActive_
21911 this.userActive_ = false;
21912
21913 // Init debugEnabled_
21914 this.debugEnabled_ = false;
21915
21916 // Init state audioOnlyMode_
21917 this.audioOnlyMode_ = false;
21918
21919 // Init state audioPosterMode_
21920 this.audioPosterMode_ = false;
21921
21922 // Init state audioOnlyCache_
21923 this.audioOnlyCache_ = {
21924 controlBarHeight: null,
21925 playerHeight: null,
21926 hiddenChildren: []
21927 };
21928
21929 // if the global option object was accidentally blown away by
21930 // someone, bail early with an informative error
21931 if (!this.options_ || !this.options_.techOrder || !this.options_.techOrder.length) {
21932 throw new Error('No techOrder specified. Did you overwrite ' + 'videojs.options instead of just changing the ' + 'properties you want to override?');
21933 }
21934
21935 // Store the original tag used to set options
21936 this.tag = tag;
21937
21938 // Store the tag attributes used to restore html5 element
21939 this.tagAttributes = tag && getAttributes(tag);
21940
21941 // Update current language
21942 this.language(this.options_.language);
21943
21944 // Update Supported Languages
21945 if (options.languages) {
21946 // Normalise player option languages to lowercase
21947 const languagesToLower = {};
21948 Object.getOwnPropertyNames(options.languages).forEach(function (name) {
21949 languagesToLower[name.toLowerCase()] = options.languages[name];
21950 });
21951 this.languages_ = languagesToLower;
21952 } else {
21953 this.languages_ = Player.prototype.options_.languages;
21954 }
21955 this.resetCache_();
21956
21957 // Set poster
21958 /** @type string */
21959 this.poster_ = options.poster || '';
21960
21961 // Set controls
21962 /** @type {boolean} */
21963 this.controls_ = !!options.controls;
21964
21965 // Original tag settings stored in options
21966 // now remove immediately so native controls don't flash.
21967 // May be turned back on by HTML5 tech if nativeControlsForTouch is true
21968 tag.controls = false;
21969 tag.removeAttribute('controls');
21970 this.changingSrc_ = false;
21971 this.playCallbacks_ = [];
21972 this.playTerminatedQueue_ = [];
21973
21974 // the attribute overrides the option
21975 if (tag.hasAttribute('autoplay')) {
21976 this.autoplay(true);
21977 } else {
21978 // otherwise use the setter to validate and
21979 // set the correct value.
21980 this.autoplay(this.options_.autoplay);
21981 }
21982
21983 // check plugins
21984 if (options.plugins) {
21985 Object.keys(options.plugins).forEach(name => {
21986 if (typeof this[name] !== 'function') {
21987 throw new Error(`plugin "${name}" does not exist`);
21988 }
21989 });
21990 }
21991
21992 /*
21993 * Store the internal state of scrubbing
21994 *
21995 * @private
21996 * @return {Boolean} True if the user is scrubbing
21997 */
21998 this.scrubbing_ = false;
21999 this.el_ = this.createEl();
22000
22001 // Make this an evented object and use `el_` as its event bus.
22002 evented(this, {
22003 eventBusKey: 'el_'
22004 });
22005
22006 // listen to document and player fullscreenchange handlers so we receive those events
22007 // before a user can receive them so we can update isFullscreen appropriately.
22008 // make sure that we listen to fullscreenchange events before everything else to make sure that
22009 // our isFullscreen method is updated properly for internal components as well as external.
22010 if (this.fsApi_.requestFullscreen) {
22011 on(document__default["default"], this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_);
22012 this.on(this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_);
22013 }
22014 if (this.fluid_) {
22015 this.on(['playerreset', 'resize'], this.boundUpdateStyleEl_);
22016 }
22017 // We also want to pass the original player options to each component and plugin
22018 // as well so they don't need to reach back into the player for options later.
22019 // We also need to do another copy of this.options_ so we don't end up with
22020 // an infinite loop.
22021 const playerOptionsCopy = merge(this.options_);
22022
22023 // Load plugins
22024 if (options.plugins) {
22025 Object.keys(options.plugins).forEach(name => {
22026 this[name](options.plugins[name]);
22027 });
22028 }
22029
22030 // Enable debug mode to fire debugon event for all plugins.
22031 if (options.debug) {
22032 this.debug(true);
22033 }
22034 this.options_.playerOptions = playerOptionsCopy;
22035 this.middleware_ = [];
22036 this.playbackRates(options.playbackRates);
22037 if (options.experimentalSvgIcons) {
22038 // Add SVG Sprite to the DOM
22039 const parser = new window__default["default"].DOMParser();
22040 const parsedSVG = parser.parseFromString(icons, 'image/svg+xml');
22041 const errorNode = parsedSVG.querySelector('parsererror');
22042 if (errorNode) {
22043 log.warn('Failed to load SVG Icons. Falling back to Font Icons.');
22044 this.options_.experimentalSvgIcons = null;
22045 } else {
22046 const sprite = parsedSVG.documentElement;
22047 sprite.style.display = 'none';
22048 this.el_.appendChild(sprite);
22049 this.addClass('vjs-svg-icons-enabled');
22050 }
22051 }
22052 this.initChildren();
22053
22054 // Set isAudio based on whether or not an audio tag was used
22055 this.isAudio(tag.nodeName.toLowerCase() === 'audio');
22056
22057 // Update controls className. Can't do this when the controls are initially
22058 // set because the element doesn't exist yet.
22059 if (this.controls()) {
22060 this.addClass('vjs-controls-enabled');
22061 } else {
22062 this.addClass('vjs-controls-disabled');
22063 }
22064
22065 // Set ARIA label and region role depending on player type
22066 this.el_.setAttribute('role', 'region');
22067 if (this.isAudio()) {
22068 this.el_.setAttribute('aria-label', this.localize('Audio Player'));
22069 } else {
22070 this.el_.setAttribute('aria-label', this.localize('Video Player'));
22071 }
22072 if (this.isAudio()) {
22073 this.addClass('vjs-audio');
22074 }
22075
22076 // Check if spatial navigation is enabled in the options.
22077 // If enabled, instantiate the SpatialNavigation class.
22078 if (options.spatialNavigation && options.spatialNavigation.enabled) {
22079 this.spatialNavigation = new SpatialNavigation(this);
22080 this.addClass('vjs-spatial-navigation-enabled');
22081 }
22082
22083 // TODO: Make this smarter. Toggle user state between touching/mousing
22084 // using events, since devices can have both touch and mouse events.
22085 // TODO: Make this check be performed again when the window switches between monitors
22086 // (See https://github.com/videojs/video.js/issues/5683)
22087 if (TOUCH_ENABLED) {
22088 this.addClass('vjs-touch-enabled');
22089 }
22090
22091 // iOS Safari has broken hover handling
22092 if (!IS_IOS) {
22093 this.addClass('vjs-workinghover');
22094 }
22095
22096 // Make player easily findable by ID
22097 Player.players[this.id_] = this;
22098
22099 // Add a major version class to aid css in plugins
22100 const majorVersion = version.split('.')[0];
22101 this.addClass(`vjs-v${majorVersion}`);
22102
22103 // When the player is first initialized, trigger activity so components
22104 // like the control bar show themselves if needed
22105 this.userActive(true);
22106 this.reportUserActivity();
22107 this.one('play', e => this.listenForUserActivity_(e));
22108 this.on('keydown', e => this.handleKeyDown(e));
22109 this.on('languagechange', e => this.handleLanguagechange(e));
22110 this.breakpoints(this.options_.breakpoints);
22111 this.responsive(this.options_.responsive);
22112
22113 // Calling both the audio mode methods after the player is fully
22114 // setup to be able to listen to the events triggered by them
22115 this.on('ready', () => {
22116 // Calling the audioPosterMode method first so that
22117 // the audioOnlyMode can take precedence when both options are set to true
22118 this.audioPosterMode(this.options_.audioPosterMode);
22119 this.audioOnlyMode(this.options_.audioOnlyMode);
22120 });
22121 }
22122
22123 /**
22124 * Destroys the video player and does any necessary cleanup.
22125 *
22126 * This is especially helpful if you are dynamically adding and removing videos
22127 * to/from the DOM.
22128 *
22129 * @fires Player#dispose
22130 */
22131 dispose() {
22132 /**
22133 * Called when the player is being disposed of.
22134 *
22135 * @event Player#dispose
22136 * @type {Event}
22137 */
22138 this.trigger('dispose');
22139 // prevent dispose from being called twice
22140 this.off('dispose');
22141
22142 // Make sure all player-specific document listeners are unbound. This is
22143 off(document__default["default"], this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_);
22144 off(document__default["default"], 'keydown', this.boundFullWindowOnEscKey_);
22145 if (this.styleEl_ && this.styleEl_.parentNode) {
22146 this.styleEl_.parentNode.removeChild(this.styleEl_);
22147 this.styleEl_ = null;
22148 }
22149
22150 // Kill reference to this player
22151 Player.players[this.id_] = null;
22152 if (this.tag && this.tag.player) {
22153 this.tag.player = null;
22154 }
22155 if (this.el_ && this.el_.player) {
22156 this.el_.player = null;
22157 }
22158 if (this.tech_) {
22159 this.tech_.dispose();
22160 this.isPosterFromTech_ = false;
22161 this.poster_ = '';
22162 }
22163 if (this.playerElIngest_) {
22164 this.playerElIngest_ = null;
22165 }
22166 if (this.tag) {
22167 this.tag = null;
22168 }
22169 clearCacheForPlayer(this);
22170
22171 // remove all event handlers for track lists
22172 // all tracks and track listeners are removed on
22173 // tech dispose
22174 ALL.names.forEach(name => {
22175 const props = ALL[name];
22176 const list = this[props.getterName]();
22177
22178 // if it is not a native list
22179 // we have to manually remove event listeners
22180 if (list && list.off) {
22181 list.off();
22182 }
22183 });
22184
22185 // the actual .el_ is removed here, or replaced if
22186 super.dispose({
22187 restoreEl: this.options_.restoreEl
22188 });
22189 }
22190
22191 /**
22192 * Create the `Player`'s DOM element.
22193 *
22194 * @return {Element}
22195 * The DOM element that gets created.
22196 */
22197 createEl() {
22198 let tag = this.tag;
22199 let el;
22200 let playerElIngest = this.playerElIngest_ = tag.parentNode && tag.parentNode.hasAttribute && tag.parentNode.hasAttribute('data-vjs-player');
22201 const divEmbed = this.tag.tagName.toLowerCase() === 'video-js';
22202 if (playerElIngest) {
22203 el = this.el_ = tag.parentNode;
22204 } else if (!divEmbed) {
22205 el = this.el_ = super.createEl('div');
22206 }
22207
22208 // Copy over all the attributes from the tag, including ID and class
22209 // ID will now reference player box, not the video tag
22210 const attrs = getAttributes(tag);
22211 if (divEmbed) {
22212 el = this.el_ = tag;
22213 tag = this.tag = document__default["default"].createElement('video');
22214 while (el.children.length) {
22215 tag.appendChild(el.firstChild);
22216 }
22217 if (!hasClass(el, 'video-js')) {
22218 addClass(el, 'video-js');
22219 }
22220 el.appendChild(tag);
22221 playerElIngest = this.playerElIngest_ = el;
22222 // move properties over from our custom `video-js` element
22223 // to our new `video` element. This will move things like
22224 // `src` or `controls` that were set via js before the player
22225 // was initialized.
22226 Object.keys(el).forEach(k => {
22227 try {
22228 tag[k] = el[k];
22229 } catch (e) {
22230 // we got a a property like outerHTML which we can't actually copy, ignore it
22231 }
22232 });
22233 }
22234
22235 // set tabindex to -1 to remove the video element from the focus order
22236 tag.setAttribute('tabindex', '-1');
22237 attrs.tabindex = '-1';
22238
22239 // Workaround for #4583 on Chrome (on Windows) with JAWS.
22240 // See https://github.com/FreedomScientific/VFO-standards-support/issues/78
22241 // Note that we can't detect if JAWS is being used, but this ARIA attribute
22242 // doesn't change behavior of Chrome if JAWS is not being used
22243 if (IS_CHROME && IS_WINDOWS) {
22244 tag.setAttribute('role', 'application');
22245 attrs.role = 'application';
22246 }
22247
22248 // Remove width/height attrs from tag so CSS can make it 100% width/height
22249 tag.removeAttribute('width');
22250 tag.removeAttribute('height');
22251 if ('width' in attrs) {
22252 delete attrs.width;
22253 }
22254 if ('height' in attrs) {
22255 delete attrs.height;
22256 }
22257 Object.getOwnPropertyNames(attrs).forEach(function (attr) {
22258 // don't copy over the class attribute to the player element when we're in a div embed
22259 // the class is already set up properly in the divEmbed case
22260 // and we want to make sure that the `video-js` class doesn't get lost
22261 if (!(divEmbed && attr === 'class')) {
22262 el.setAttribute(attr, attrs[attr]);
22263 }
22264 if (divEmbed) {
22265 tag.setAttribute(attr, attrs[attr]);
22266 }
22267 });
22268
22269 // Update tag id/class for use as HTML5 playback tech
22270 // Might think we should do this after embedding in container so .vjs-tech class
22271 // doesn't flash 100% width/height, but class only applies with .video-js parent
22272 tag.playerId = tag.id;
22273 tag.id += '_html5_api';
22274 tag.className = 'vjs-tech';
22275
22276 // Make player findable on elements
22277 tag.player = el.player = this;
22278 // Default state of video is paused
22279 this.addClass('vjs-paused');
22280 const deviceClassNames = ['IS_SMART_TV', 'IS_TIZEN', 'IS_WEBOS', 'IS_ANDROID', 'IS_IPAD', 'IS_IPHONE', 'IS_CHROMECAST_RECEIVER'].filter(key => browser[key]).map(key => {
22281 return 'vjs-device-' + key.substring(3).toLowerCase().replace(/\_/g, '-');
22282 });
22283 this.addClass(...deviceClassNames);
22284
22285 // Add a style element in the player that we'll use to set the width/height
22286 // of the player in a way that's still overridable by CSS, just like the
22287 // video element
22288 if (window__default["default"].VIDEOJS_NO_DYNAMIC_STYLE !== true) {
22289 this.styleEl_ = createStyleElement('vjs-styles-dimensions');
22290 const defaultsStyleEl = $('.vjs-styles-defaults');
22291 const head = $('head');
22292 head.insertBefore(this.styleEl_, defaultsStyleEl ? defaultsStyleEl.nextSibling : head.firstChild);
22293 }
22294 this.fill_ = false;
22295 this.fluid_ = false;
22296
22297 // Pass in the width/height/aspectRatio options which will update the style el
22298 this.width(this.options_.width);
22299 this.height(this.options_.height);
22300 this.fill(this.options_.fill);
22301 this.fluid(this.options_.fluid);
22302 this.aspectRatio(this.options_.aspectRatio);
22303 // support both crossOrigin and crossorigin to reduce confusion and issues around the name
22304 this.crossOrigin(this.options_.crossOrigin || this.options_.crossorigin);
22305
22306 // Hide any links within the video/audio tag,
22307 // because IE doesn't hide them completely from screen readers.
22308 const links = tag.getElementsByTagName('a');
22309 for (let i = 0; i < links.length; i++) {
22310 const linkEl = links.item(i);
22311 addClass(linkEl, 'vjs-hidden');
22312 linkEl.setAttribute('hidden', 'hidden');
22313 }
22314
22315 // insertElFirst seems to cause the networkState to flicker from 3 to 2, so
22316 // keep track of the original for later so we can know if the source originally failed
22317 tag.initNetworkState_ = tag.networkState;
22318
22319 // Wrap video tag in div (el/box) container
22320 if (tag.parentNode && !playerElIngest) {
22321 tag.parentNode.insertBefore(el, tag);
22322 }
22323
22324 // insert the tag as the first child of the player element
22325 // then manually add it to the children array so that this.addChild
22326 // will work properly for other components
22327 //
22328 // Breaks iPhone, fixed in HTML5 setup.
22329 prependTo(tag, el);
22330 this.children_.unshift(tag);
22331
22332 // Set lang attr on player to ensure CSS :lang() in consistent with player
22333 // if it's been set to something different to the doc
22334 this.el_.setAttribute('lang', this.language_);
22335 this.el_.setAttribute('translate', 'no');
22336 this.el_ = el;
22337 return el;
22338 }
22339
22340 /**
22341 * Get or set the `Player`'s crossOrigin option. For the HTML5 player, this
22342 * sets the `crossOrigin` property on the `<video>` tag to control the CORS
22343 * behavior.
22344 *
22345 * @see [Video Element Attributes]{@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#attr-crossorigin}
22346 *
22347 * @param {string|null} [value]
22348 * The value to set the `Player`'s crossOrigin to. If an argument is
22349 * given, must be one of `'anonymous'` or `'use-credentials'`, or 'null'.
22350 *
22351 * @return {string|null|undefined}
22352 * - The current crossOrigin value of the `Player` when getting.
22353 * - undefined when setting
22354 */
22355 crossOrigin(value) {
22356 // `null` can be set to unset a value
22357 if (typeof value === 'undefined') {
22358 return this.techGet_('crossOrigin');
22359 }
22360 if (value !== null && value !== 'anonymous' && value !== 'use-credentials') {
22361 log.warn(`crossOrigin must be null, "anonymous" or "use-credentials", given "${value}"`);
22362 return;
22363 }
22364 this.techCall_('setCrossOrigin', value);
22365 if (this.posterImage) {
22366 this.posterImage.crossOrigin(value);
22367 }
22368 return;
22369 }
22370
22371 /**
22372 * A getter/setter for the `Player`'s width. Returns the player's configured value.
22373 * To get the current width use `currentWidth()`.
22374 *
22375 * @param {number|string} [value]
22376 * CSS value to set the `Player`'s width to.
22377 *
22378 * @return {number|undefined}
22379 * - The current width of the `Player` when getting.
22380 * - Nothing when setting
22381 */
22382 width(value) {
22383 return this.dimension('width', value);
22384 }
22385
22386 /**
22387 * A getter/setter for the `Player`'s height. Returns the player's configured value.
22388 * To get the current height use `currentheight()`.
22389 *
22390 * @param {number|string} [value]
22391 * CSS value to set the `Player`'s height to.
22392 *
22393 * @return {number|undefined}
22394 * - The current height of the `Player` when getting.
22395 * - Nothing when setting
22396 */
22397 height(value) {
22398 return this.dimension('height', value);
22399 }
22400
22401 /**
22402 * A getter/setter for the `Player`'s width & height.
22403 *
22404 * @param {string} dimension
22405 * This string can be:
22406 * - 'width'
22407 * - 'height'
22408 *
22409 * @param {number|string} [value]
22410 * Value for dimension specified in the first argument.
22411 *
22412 * @return {number}
22413 * The dimension arguments value when getting (width/height).
22414 */
22415 dimension(dimension, value) {
22416 const privDimension = dimension + '_';
22417 if (value === undefined) {
22418 return this[privDimension] || 0;
22419 }
22420 if (value === '' || value === 'auto') {
22421 // If an empty string is given, reset the dimension to be automatic
22422 this[privDimension] = undefined;
22423 this.updateStyleEl_();
22424 return;
22425 }
22426 const parsedVal = parseFloat(value);
22427 if (isNaN(parsedVal)) {
22428 log.error(`Improper value "${value}" supplied for for ${dimension}`);
22429 return;
22430 }
22431 this[privDimension] = parsedVal;
22432 this.updateStyleEl_();
22433 }
22434
22435 /**
22436 * A getter/setter/toggler for the vjs-fluid `className` on the `Player`.
22437 *
22438 * Turning this on will turn off fill mode.
22439 *
22440 * @param {boolean} [bool]
22441 * - A value of true adds the class.
22442 * - A value of false removes the class.
22443 * - No value will be a getter.
22444 *
22445 * @return {boolean|undefined}
22446 * - The value of fluid when getting.
22447 * - `undefined` when setting.
22448 */
22449 fluid(bool) {
22450 if (bool === undefined) {
22451 return !!this.fluid_;
22452 }
22453 this.fluid_ = !!bool;
22454 if (isEvented(this)) {
22455 this.off(['playerreset', 'resize'], this.boundUpdateStyleEl_);
22456 }
22457 if (bool) {
22458 this.addClass('vjs-fluid');
22459 this.fill(false);
22460 addEventedCallback(this, () => {
22461 this.on(['playerreset', 'resize'], this.boundUpdateStyleEl_);
22462 });
22463 } else {
22464 this.removeClass('vjs-fluid');
22465 }
22466 this.updateStyleEl_();
22467 }
22468
22469 /**
22470 * A getter/setter/toggler for the vjs-fill `className` on the `Player`.
22471 *
22472 * Turning this on will turn off fluid mode.
22473 *
22474 * @param {boolean} [bool]
22475 * - A value of true adds the class.
22476 * - A value of false removes the class.
22477 * - No value will be a getter.
22478 *
22479 * @return {boolean|undefined}
22480 * - The value of fluid when getting.
22481 * - `undefined` when setting.
22482 */
22483 fill(bool) {
22484 if (bool === undefined) {
22485 return !!this.fill_;
22486 }
22487 this.fill_ = !!bool;
22488 if (bool) {
22489 this.addClass('vjs-fill');
22490 this.fluid(false);
22491 } else {
22492 this.removeClass('vjs-fill');
22493 }
22494 }
22495
22496 /**
22497 * Get/Set the aspect ratio
22498 *
22499 * @param {string} [ratio]
22500 * Aspect ratio for player
22501 *
22502 * @return {string|undefined}
22503 * returns the current aspect ratio when getting
22504 */
22505
22506 /**
22507 * A getter/setter for the `Player`'s aspect ratio.
22508 *
22509 * @param {string} [ratio]
22510 * The value to set the `Player`'s aspect ratio to.
22511 *
22512 * @return {string|undefined}
22513 * - The current aspect ratio of the `Player` when getting.
22514 * - undefined when setting
22515 */
22516 aspectRatio(ratio) {
22517 if (ratio === undefined) {
22518 return this.aspectRatio_;
22519 }
22520
22521 // Check for width:height format
22522 if (!/^\d+\:\d+$/.test(ratio)) {
22523 throw new Error('Improper value supplied for aspect ratio. The format should be width:height, for example 16:9.');
22524 }
22525 this.aspectRatio_ = ratio;
22526
22527 // We're assuming if you set an aspect ratio you want fluid mode,
22528 // because in fixed mode you could calculate width and height yourself.
22529 this.fluid(true);
22530 this.updateStyleEl_();
22531 }
22532
22533 /**
22534 * Update styles of the `Player` element (height, width and aspect ratio).
22535 *
22536 * @private
22537 * @listens Tech#loadedmetadata
22538 */
22539 updateStyleEl_() {
22540 if (window__default["default"].VIDEOJS_NO_DYNAMIC_STYLE === true) {
22541 const width = typeof this.width_ === 'number' ? this.width_ : this.options_.width;
22542 const height = typeof this.height_ === 'number' ? this.height_ : this.options_.height;
22543 const techEl = this.tech_ && this.tech_.el();
22544 if (techEl) {
22545 if (width >= 0) {
22546 techEl.width = width;
22547 }
22548 if (height >= 0) {
22549 techEl.height = height;
22550 }
22551 }
22552 return;
22553 }
22554 let width;
22555 let height;
22556 let aspectRatio;
22557 let idClass;
22558
22559 // The aspect ratio is either used directly or to calculate width and height.
22560 if (this.aspectRatio_ !== undefined && this.aspectRatio_ !== 'auto') {
22561 // Use any aspectRatio that's been specifically set
22562 aspectRatio = this.aspectRatio_;
22563 } else if (this.videoWidth() > 0) {
22564 // Otherwise try to get the aspect ratio from the video metadata
22565 aspectRatio = this.videoWidth() + ':' + this.videoHeight();
22566 } else {
22567 // Or use a default. The video element's is 2:1, but 16:9 is more common.
22568 aspectRatio = '16:9';
22569 }
22570
22571 // Get the ratio as a decimal we can use to calculate dimensions
22572 const ratioParts = aspectRatio.split(':');
22573 const ratioMultiplier = ratioParts[1] / ratioParts[0];
22574 if (this.width_ !== undefined) {
22575 // Use any width that's been specifically set
22576 width = this.width_;
22577 } else if (this.height_ !== undefined) {
22578 // Or calculate the width from the aspect ratio if a height has been set
22579 width = this.height_ / ratioMultiplier;
22580 } else {
22581 // Or use the video's metadata, or use the video el's default of 300
22582 width = this.videoWidth() || 300;
22583 }
22584 if (this.height_ !== undefined) {
22585 // Use any height that's been specifically set
22586 height = this.height_;
22587 } else {
22588 // Otherwise calculate the height from the ratio and the width
22589 height = width * ratioMultiplier;
22590 }
22591
22592 // Ensure the CSS class is valid by starting with an alpha character
22593 if (/^[^a-zA-Z]/.test(this.id())) {
22594 idClass = 'dimensions-' + this.id();
22595 } else {
22596 idClass = this.id() + '-dimensions';
22597 }
22598
22599 // Ensure the right class is still on the player for the style element
22600 this.addClass(idClass);
22601 setTextContent(this.styleEl_, `
22602 .${idClass} {
22603 width: ${width}px;
22604 height: ${height}px;
22605 }
22606
22607 .${idClass}.vjs-fluid:not(.vjs-audio-only-mode) {
22608 padding-top: ${ratioMultiplier * 100}%;
22609 }
22610 `);
22611 }
22612
22613 /**
22614 * Load/Create an instance of playback {@link Tech} including element
22615 * and API methods. Then append the `Tech` element in `Player` as a child.
22616 *
22617 * @param {string} techName
22618 * name of the playback technology
22619 *
22620 * @param {string} source
22621 * video source
22622 *
22623 * @private
22624 */
22625 loadTech_(techName, source) {
22626 // Pause and remove current playback technology
22627 if (this.tech_) {
22628 this.unloadTech_();
22629 }
22630 const titleTechName = toTitleCase(techName);
22631 const camelTechName = techName.charAt(0).toLowerCase() + techName.slice(1);
22632
22633 // get rid of the HTML5 video tag as soon as we are using another tech
22634 if (titleTechName !== 'Html5' && this.tag) {
22635 Tech.getTech('Html5').disposeMediaElement(this.tag);
22636 this.tag.player = null;
22637 this.tag = null;
22638 }
22639 this.techName_ = titleTechName;
22640
22641 // Turn off API access because we're loading a new tech that might load asynchronously
22642 this.isReady_ = false;
22643 let autoplay = this.autoplay();
22644
22645 // if autoplay is a string (or `true` with normalizeAutoplay: true) we pass false to the tech
22646 // because the player is going to handle autoplay on `loadstart`
22647 if (typeof this.autoplay() === 'string' || this.autoplay() === true && this.options_.normalizeAutoplay) {
22648 autoplay = false;
22649 }
22650
22651 // Grab tech-specific options from player options and add source and parent element to use.
22652 const techOptions = {
22653 source,
22654 autoplay,
22655 'nativeControlsForTouch': this.options_.nativeControlsForTouch,
22656 'playerId': this.id(),
22657 'techId': `${this.id()}_${camelTechName}_api`,
22658 'playsinline': this.options_.playsinline,
22659 'preload': this.options_.preload,
22660 'loop': this.options_.loop,
22661 'disablePictureInPicture': this.options_.disablePictureInPicture,
22662 'muted': this.options_.muted,
22663 'poster': this.poster(),
22664 'language': this.language(),
22665 'playerElIngest': this.playerElIngest_ || false,
22666 'vtt.js': this.options_['vtt.js'],
22667 'canOverridePoster': !!this.options_.techCanOverridePoster,
22668 'enableSourceset': this.options_.enableSourceset
22669 };
22670 ALL.names.forEach(name => {
22671 const props = ALL[name];
22672 techOptions[props.getterName] = this[props.privateName];
22673 });
22674 Object.assign(techOptions, this.options_[titleTechName]);
22675 Object.assign(techOptions, this.options_[camelTechName]);
22676 Object.assign(techOptions, this.options_[techName.toLowerCase()]);
22677 if (this.tag) {
22678 techOptions.tag = this.tag;
22679 }
22680 if (source && source.src === this.cache_.src && this.cache_.currentTime > 0) {
22681 techOptions.startTime = this.cache_.currentTime;
22682 }
22683
22684 // Initialize tech instance
22685 const TechClass = Tech.getTech(techName);
22686 if (!TechClass) {
22687 throw new Error(`No Tech named '${titleTechName}' exists! '${titleTechName}' should be registered using videojs.registerTech()'`);
22688 }
22689 this.tech_ = new TechClass(techOptions);
22690
22691 // player.triggerReady is always async, so don't need this to be async
22692 this.tech_.ready(bind_(this, this.handleTechReady_), true);
22693 textTrackConverter.jsonToTextTracks(this.textTracksJson_ || [], this.tech_);
22694
22695 // Listen to all HTML5-defined events and trigger them on the player
22696 TECH_EVENTS_RETRIGGER.forEach(event => {
22697 this.on(this.tech_, event, e => this[`handleTech${toTitleCase(event)}_`](e));
22698 });
22699 Object.keys(TECH_EVENTS_QUEUE).forEach(event => {
22700 this.on(this.tech_, event, eventObj => {
22701 if (this.tech_.playbackRate() === 0 && this.tech_.seeking()) {
22702 this.queuedCallbacks_.push({
22703 callback: this[`handleTech${TECH_EVENTS_QUEUE[event]}_`].bind(this),
22704 event: eventObj
22705 });
22706 return;
22707 }
22708 this[`handleTech${TECH_EVENTS_QUEUE[event]}_`](eventObj);
22709 });
22710 });
22711 this.on(this.tech_, 'loadstart', e => this.handleTechLoadStart_(e));
22712 this.on(this.tech_, 'sourceset', e => this.handleTechSourceset_(e));
22713 this.on(this.tech_, 'waiting', e => this.handleTechWaiting_(e));
22714 this.on(this.tech_, 'ended', e => this.handleTechEnded_(e));
22715 this.on(this.tech_, 'seeking', e => this.handleTechSeeking_(e));
22716 this.on(this.tech_, 'play', e => this.handleTechPlay_(e));
22717 this.on(this.tech_, 'pause', e => this.handleTechPause_(e));
22718 this.on(this.tech_, 'durationchange', e => this.handleTechDurationChange_(e));
22719 this.on(this.tech_, 'fullscreenchange', (e, data) => this.handleTechFullscreenChange_(e, data));
22720 this.on(this.tech_, 'fullscreenerror', (e, err) => this.handleTechFullscreenError_(e, err));
22721 this.on(this.tech_, 'enterpictureinpicture', e => this.handleTechEnterPictureInPicture_(e));
22722 this.on(this.tech_, 'leavepictureinpicture', e => this.handleTechLeavePictureInPicture_(e));
22723 this.on(this.tech_, 'error', e => this.handleTechError_(e));
22724 this.on(this.tech_, 'posterchange', e => this.handleTechPosterChange_(e));
22725 this.on(this.tech_, 'textdata', e => this.handleTechTextData_(e));
22726 this.on(this.tech_, 'ratechange', e => this.handleTechRateChange_(e));
22727 this.on(this.tech_, 'loadedmetadata', this.boundUpdateStyleEl_);
22728 this.usingNativeControls(this.techGet_('controls'));
22729 if (this.controls() && !this.usingNativeControls()) {
22730 this.addTechControlsListeners_();
22731 }
22732
22733 // Add the tech element in the DOM if it was not already there
22734 // Make sure to not insert the original video element if using Html5
22735 if (this.tech_.el().parentNode !== this.el() && (titleTechName !== 'Html5' || !this.tag)) {
22736 prependTo(this.tech_.el(), this.el());
22737 }
22738
22739 // Get rid of the original video tag reference after the first tech is loaded
22740 if (this.tag) {
22741 this.tag.player = null;
22742 this.tag = null;
22743 }
22744 }
22745
22746 /**
22747 * Unload and dispose of the current playback {@link Tech}.
22748 *
22749 * @private
22750 */
22751 unloadTech_() {
22752 // Save the current text tracks so that we can reuse the same text tracks with the next tech
22753 ALL.names.forEach(name => {
22754 const props = ALL[name];
22755 this[props.privateName] = this[props.getterName]();
22756 });
22757 this.textTracksJson_ = textTrackConverter.textTracksToJson(this.tech_);
22758 this.isReady_ = false;
22759 this.tech_.dispose();
22760 this.tech_ = false;
22761 if (this.isPosterFromTech_) {
22762 this.poster_ = '';
22763 this.trigger('posterchange');
22764 }
22765 this.isPosterFromTech_ = false;
22766 }
22767
22768 /**
22769 * Return a reference to the current {@link Tech}.
22770 * It will print a warning by default about the danger of using the tech directly
22771 * but any argument that is passed in will silence the warning.
22772 *
22773 * @param {*} [safety]
22774 * Anything passed in to silence the warning
22775 *
22776 * @return {Tech}
22777 * The Tech
22778 */
22779 tech(safety) {
22780 if (safety === undefined) {
22781 log.warn('Using the tech directly can be dangerous. I hope you know what you\'re doing.\n' + 'See https://github.com/videojs/video.js/issues/2617 for more info.\n');
22782 }
22783 return this.tech_;
22784 }
22785
22786 /**
22787 * An object that contains Video.js version.
22788 *
22789 * @typedef {Object} PlayerVersion
22790 *
22791 * @property {string} 'video.js' - Video.js version
22792 */
22793
22794 /**
22795 * Returns an object with Video.js version.
22796 *
22797 * @return {PlayerVersion}
22798 * An object with Video.js version.
22799 */
22800 version() {
22801 return {
22802 'video.js': version
22803 };
22804 }
22805
22806 /**
22807 * Set up click and touch listeners for the playback element
22808 *
22809 * - On desktops: a click on the video itself will toggle playback
22810 * - On mobile devices: a click on the video toggles controls
22811 * which is done by toggling the user state between active and
22812 * inactive
22813 * - A tap can signal that a user has become active or has become inactive
22814 * e.g. a quick tap on an iPhone movie should reveal the controls. Another
22815 * quick tap should hide them again (signaling the user is in an inactive
22816 * viewing state)
22817 * - In addition to this, we still want the user to be considered inactive after
22818 * a few seconds of inactivity.
22819 *
22820 * > Note: the only part of iOS interaction we can't mimic with this setup
22821 * is a touch and hold on the video element counting as activity in order to
22822 * keep the controls showing, but that shouldn't be an issue. A touch and hold
22823 * on any controls will still keep the user active
22824 *
22825 * @private
22826 */
22827 addTechControlsListeners_() {
22828 // Make sure to remove all the previous listeners in case we are called multiple times.
22829 this.removeTechControlsListeners_();
22830 this.on(this.tech_, 'click', this.boundHandleTechClick_);
22831 this.on(this.tech_, 'dblclick', this.boundHandleTechDoubleClick_);
22832
22833 // If the controls were hidden we don't want that to change without a tap event
22834 // so we'll check if the controls were already showing before reporting user
22835 // activity
22836 this.on(this.tech_, 'touchstart', this.boundHandleTechTouchStart_);
22837 this.on(this.tech_, 'touchmove', this.boundHandleTechTouchMove_);
22838 this.on(this.tech_, 'touchend', this.boundHandleTechTouchEnd_);
22839
22840 // The tap listener needs to come after the touchend listener because the tap
22841 // listener cancels out any reportedUserActivity when setting userActive(false)
22842 this.on(this.tech_, 'tap', this.boundHandleTechTap_);
22843 }
22844
22845 /**
22846 * Remove the listeners used for click and tap controls. This is needed for
22847 * toggling to controls disabled, where a tap/touch should do nothing.
22848 *
22849 * @private
22850 */
22851 removeTechControlsListeners_() {
22852 // We don't want to just use `this.off()` because there might be other needed
22853 // listeners added by techs that extend this.
22854 this.off(this.tech_, 'tap', this.boundHandleTechTap_);
22855 this.off(this.tech_, 'touchstart', this.boundHandleTechTouchStart_);
22856 this.off(this.tech_, 'touchmove', this.boundHandleTechTouchMove_);
22857 this.off(this.tech_, 'touchend', this.boundHandleTechTouchEnd_);
22858 this.off(this.tech_, 'click', this.boundHandleTechClick_);
22859 this.off(this.tech_, 'dblclick', this.boundHandleTechDoubleClick_);
22860 }
22861
22862 /**
22863 * Player waits for the tech to be ready
22864 *
22865 * @private
22866 */
22867 handleTechReady_() {
22868 this.triggerReady();
22869
22870 // Keep the same volume as before
22871 if (this.cache_.volume) {
22872 this.techCall_('setVolume', this.cache_.volume);
22873 }
22874
22875 // Look if the tech found a higher resolution poster while loading
22876 this.handleTechPosterChange_();
22877
22878 // Update the duration if available
22879 this.handleTechDurationChange_();
22880 }
22881
22882 /**
22883 * Retrigger the `loadstart` event that was triggered by the {@link Tech}.
22884 *
22885 * @fires Player#loadstart
22886 * @listens Tech#loadstart
22887 * @private
22888 */
22889 handleTechLoadStart_() {
22890 // TODO: Update to use `emptied` event instead. See #1277.
22891
22892 this.removeClass('vjs-ended', 'vjs-seeking');
22893
22894 // reset the error state
22895 this.error(null);
22896
22897 // Update the duration
22898 this.handleTechDurationChange_();
22899 if (!this.paused()) {
22900 /**
22901 * Fired when the user agent begins looking for media data
22902 *
22903 * @event Player#loadstart
22904 * @type {Event}
22905 */
22906 this.trigger('loadstart');
22907 } else {
22908 // reset the hasStarted state
22909 this.hasStarted(false);
22910 this.trigger('loadstart');
22911 }
22912
22913 // autoplay happens after loadstart for the browser,
22914 // so we mimic that behavior
22915 this.manualAutoplay_(this.autoplay() === true && this.options_.normalizeAutoplay ? 'play' : this.autoplay());
22916 }
22917
22918 /**
22919 * Handle autoplay string values, rather than the typical boolean
22920 * values that should be handled by the tech. Note that this is not
22921 * part of any specification. Valid values and what they do can be
22922 * found on the autoplay getter at Player#autoplay()
22923 */
22924 manualAutoplay_(type) {
22925 if (!this.tech_ || typeof type !== 'string') {
22926 return;
22927 }
22928
22929 // Save original muted() value, set muted to true, and attempt to play().
22930 // On promise rejection, restore muted from saved value
22931 const resolveMuted = () => {
22932 const previouslyMuted = this.muted();
22933 this.muted(true);
22934 const restoreMuted = () => {
22935 this.muted(previouslyMuted);
22936 };
22937
22938 // restore muted on play terminatation
22939 this.playTerminatedQueue_.push(restoreMuted);
22940 const mutedPromise = this.play();
22941 if (!isPromise(mutedPromise)) {
22942 return;
22943 }
22944 return mutedPromise.catch(err => {
22945 restoreMuted();
22946 throw new Error(`Rejection at manualAutoplay. Restoring muted value. ${err ? err : ''}`);
22947 });
22948 };
22949 let promise;
22950
22951 // if muted defaults to true
22952 // the only thing we can do is call play
22953 if (type === 'any' && !this.muted()) {
22954 promise = this.play();
22955 if (isPromise(promise)) {
22956 promise = promise.catch(resolveMuted);
22957 }
22958 } else if (type === 'muted' && !this.muted()) {
22959 promise = resolveMuted();
22960 } else {
22961 promise = this.play();
22962 }
22963 if (!isPromise(promise)) {
22964 return;
22965 }
22966 return promise.then(() => {
22967 this.trigger({
22968 type: 'autoplay-success',
22969 autoplay: type
22970 });
22971 }).catch(() => {
22972 this.trigger({
22973 type: 'autoplay-failure',
22974 autoplay: type
22975 });
22976 });
22977 }
22978
22979 /**
22980 * Update the internal source caches so that we return the correct source from
22981 * `src()`, `currentSource()`, and `currentSources()`.
22982 *
22983 * > Note: `currentSources` will not be updated if the source that is passed in exists
22984 * in the current `currentSources` cache.
22985 *
22986 *
22987 * @param {Tech~SourceObject} srcObj
22988 * A string or object source to update our caches to.
22989 */
22990 updateSourceCaches_(srcObj = '') {
22991 let src = srcObj;
22992 let type = '';
22993 if (typeof src !== 'string') {
22994 src = srcObj.src;
22995 type = srcObj.type;
22996 }
22997
22998 // make sure all the caches are set to default values
22999 // to prevent null checking
23000 this.cache_.source = this.cache_.source || {};
23001 this.cache_.sources = this.cache_.sources || [];
23002
23003 // try to get the type of the src that was passed in
23004 if (src && !type) {
23005 type = findMimetype(this, src);
23006 }
23007
23008 // update `currentSource` cache always
23009 this.cache_.source = merge({}, srcObj, {
23010 src,
23011 type
23012 });
23013 const matchingSources = this.cache_.sources.filter(s => s.src && s.src === src);
23014 const sourceElSources = [];
23015 const sourceEls = this.$$('source');
23016 const matchingSourceEls = [];
23017 for (let i = 0; i < sourceEls.length; i++) {
23018 const sourceObj = getAttributes(sourceEls[i]);
23019 sourceElSources.push(sourceObj);
23020 if (sourceObj.src && sourceObj.src === src) {
23021 matchingSourceEls.push(sourceObj.src);
23022 }
23023 }
23024
23025 // if we have matching source els but not matching sources
23026 // the current source cache is not up to date
23027 if (matchingSourceEls.length && !matchingSources.length) {
23028 this.cache_.sources = sourceElSources;
23029 // if we don't have matching source or source els set the
23030 // sources cache to the `currentSource` cache
23031 } else if (!matchingSources.length) {
23032 this.cache_.sources = [this.cache_.source];
23033 }
23034
23035 // update the tech `src` cache
23036 this.cache_.src = src;
23037 }
23038
23039 /**
23040 * *EXPERIMENTAL* Fired when the source is set or changed on the {@link Tech}
23041 * causing the media element to reload.
23042 *
23043 * It will fire for the initial source and each subsequent source.
23044 * This event is a custom event from Video.js and is triggered by the {@link Tech}.
23045 *
23046 * The event object for this event contains a `src` property that will contain the source
23047 * that was available when the event was triggered. This is generally only necessary if Video.js
23048 * is switching techs while the source was being changed.
23049 *
23050 * It is also fired when `load` is called on the player (or media element)
23051 * because the {@link https://html.spec.whatwg.org/multipage/media.html#dom-media-load|specification for `load`}
23052 * says that the resource selection algorithm needs to be aborted and restarted.
23053 * In this case, it is very likely that the `src` property will be set to the
23054 * empty string `""` to indicate we do not know what the source will be but
23055 * that it is changing.
23056 *
23057 * *This event is currently still experimental and may change in minor releases.*
23058 * __To use this, pass `enableSourceset` option to the player.__
23059 *
23060 * @event Player#sourceset
23061 * @type {Event}
23062 * @prop {string} src
23063 * The source url available when the `sourceset` was triggered.
23064 * It will be an empty string if we cannot know what the source is
23065 * but know that the source will change.
23066 */
23067 /**
23068 * Retrigger the `sourceset` event that was triggered by the {@link Tech}.
23069 *
23070 * @fires Player#sourceset
23071 * @listens Tech#sourceset
23072 * @private
23073 */
23074 handleTechSourceset_(event) {
23075 // only update the source cache when the source
23076 // was not updated using the player api
23077 if (!this.changingSrc_) {
23078 let updateSourceCaches = src => this.updateSourceCaches_(src);
23079 const playerSrc = this.currentSource().src;
23080 const eventSrc = event.src;
23081
23082 // if we have a playerSrc that is not a blob, and a tech src that is a blob
23083 if (playerSrc && !/^blob:/.test(playerSrc) && /^blob:/.test(eventSrc)) {
23084 // if both the tech source and the player source were updated we assume
23085 // something like @videojs/http-streaming did the sourceset and skip updating the source cache.
23086 if (!this.lastSource_ || this.lastSource_.tech !== eventSrc && this.lastSource_.player !== playerSrc) {
23087 updateSourceCaches = () => {};
23088 }
23089 }
23090
23091 // update the source to the initial source right away
23092 // in some cases this will be empty string
23093 updateSourceCaches(eventSrc);
23094
23095 // if the `sourceset` `src` was an empty string
23096 // wait for a `loadstart` to update the cache to `currentSrc`.
23097 // If a sourceset happens before a `loadstart`, we reset the state
23098 if (!event.src) {
23099 this.tech_.any(['sourceset', 'loadstart'], e => {
23100 // if a sourceset happens before a `loadstart` there
23101 // is nothing to do as this `handleTechSourceset_`
23102 // will be called again and this will be handled there.
23103 if (e.type === 'sourceset') {
23104 return;
23105 }
23106 const techSrc = this.techGet_('currentSrc');
23107 this.lastSource_.tech = techSrc;
23108 this.updateSourceCaches_(techSrc);
23109 });
23110 }
23111 }
23112 this.lastSource_ = {
23113 player: this.currentSource().src,
23114 tech: event.src
23115 };
23116 this.trigger({
23117 src: event.src,
23118 type: 'sourceset'
23119 });
23120 }
23121
23122 /**
23123 * Add/remove the vjs-has-started class
23124 *
23125 *
23126 * @param {boolean} request
23127 * - true: adds the class
23128 * - false: remove the class
23129 *
23130 * @return {boolean}
23131 * the boolean value of hasStarted_
23132 */
23133 hasStarted(request) {
23134 if (request === undefined) {
23135 // act as getter, if we have no request to change
23136 return this.hasStarted_;
23137 }
23138 if (request === this.hasStarted_) {
23139 return;
23140 }
23141 this.hasStarted_ = request;
23142 if (this.hasStarted_) {
23143 this.addClass('vjs-has-started');
23144 } else {
23145 this.removeClass('vjs-has-started');
23146 }
23147 }
23148
23149 /**
23150 * Fired whenever the media begins or resumes playback
23151 *
23152 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-play}
23153 * @fires Player#play
23154 * @listens Tech#play
23155 * @private
23156 */
23157 handleTechPlay_() {
23158 this.removeClass('vjs-ended', 'vjs-paused');
23159 this.addClass('vjs-playing');
23160
23161 // hide the poster when the user hits play
23162 this.hasStarted(true);
23163 /**
23164 * Triggered whenever an {@link Tech#play} event happens. Indicates that
23165 * playback has started or resumed.
23166 *
23167 * @event Player#play
23168 * @type {Event}
23169 */
23170 this.trigger('play');
23171 }
23172
23173 /**
23174 * Retrigger the `ratechange` event that was triggered by the {@link Tech}.
23175 *
23176 * If there were any events queued while the playback rate was zero, fire
23177 * those events now.
23178 *
23179 * @private
23180 * @method Player#handleTechRateChange_
23181 * @fires Player#ratechange
23182 * @listens Tech#ratechange
23183 */
23184 handleTechRateChange_() {
23185 if (this.tech_.playbackRate() > 0 && this.cache_.lastPlaybackRate === 0) {
23186 this.queuedCallbacks_.forEach(queued => queued.callback(queued.event));
23187 this.queuedCallbacks_ = [];
23188 }
23189 this.cache_.lastPlaybackRate = this.tech_.playbackRate();
23190 /**
23191 * Fires when the playing speed of the audio/video is changed
23192 *
23193 * @event Player#ratechange
23194 * @type {event}
23195 */
23196 this.trigger('ratechange');
23197 }
23198
23199 /**
23200 * Retrigger the `waiting` event that was triggered by the {@link Tech}.
23201 *
23202 * @fires Player#waiting
23203 * @listens Tech#waiting
23204 * @private
23205 */
23206 handleTechWaiting_() {
23207 this.addClass('vjs-waiting');
23208 /**
23209 * A readyState change on the DOM element has caused playback to stop.
23210 *
23211 * @event Player#waiting
23212 * @type {Event}
23213 */
23214 this.trigger('waiting');
23215
23216 // Browsers may emit a timeupdate event after a waiting event. In order to prevent
23217 // premature removal of the waiting class, wait for the time to change.
23218 const timeWhenWaiting = this.currentTime();
23219 const timeUpdateListener = () => {
23220 if (timeWhenWaiting !== this.currentTime()) {
23221 this.removeClass('vjs-waiting');
23222 this.off('timeupdate', timeUpdateListener);
23223 }
23224 };
23225 this.on('timeupdate', timeUpdateListener);
23226 }
23227
23228 /**
23229 * Retrigger the `canplay` event that was triggered by the {@link Tech}.
23230 * > Note: This is not consistent between browsers. See #1351
23231 *
23232 * @fires Player#canplay
23233 * @listens Tech#canplay
23234 * @private
23235 */
23236 handleTechCanPlay_() {
23237 this.removeClass('vjs-waiting');
23238 /**
23239 * The media has a readyState of HAVE_FUTURE_DATA or greater.
23240 *
23241 * @event Player#canplay
23242 * @type {Event}
23243 */
23244 this.trigger('canplay');
23245 }
23246
23247 /**
23248 * Retrigger the `canplaythrough` event that was triggered by the {@link Tech}.
23249 *
23250 * @fires Player#canplaythrough
23251 * @listens Tech#canplaythrough
23252 * @private
23253 */
23254 handleTechCanPlayThrough_() {
23255 this.removeClass('vjs-waiting');
23256 /**
23257 * The media has a readyState of HAVE_ENOUGH_DATA or greater. This means that the
23258 * entire media file can be played without buffering.
23259 *
23260 * @event Player#canplaythrough
23261 * @type {Event}
23262 */
23263 this.trigger('canplaythrough');
23264 }
23265
23266 /**
23267 * Retrigger the `playing` event that was triggered by the {@link Tech}.
23268 *
23269 * @fires Player#playing
23270 * @listens Tech#playing
23271 * @private
23272 */
23273 handleTechPlaying_() {
23274 this.removeClass('vjs-waiting');
23275 /**
23276 * The media is no longer blocked from playback, and has started playing.
23277 *
23278 * @event Player#playing
23279 * @type {Event}
23280 */
23281 this.trigger('playing');
23282 }
23283
23284 /**
23285 * Retrigger the `seeking` event that was triggered by the {@link Tech}.
23286 *
23287 * @fires Player#seeking
23288 * @listens Tech#seeking
23289 * @private
23290 */
23291 handleTechSeeking_() {
23292 this.addClass('vjs-seeking');
23293 /**
23294 * Fired whenever the player is jumping to a new time
23295 *
23296 * @event Player#seeking
23297 * @type {Event}
23298 */
23299 this.trigger('seeking');
23300 }
23301
23302 /**
23303 * Retrigger the `seeked` event that was triggered by the {@link Tech}.
23304 *
23305 * @fires Player#seeked
23306 * @listens Tech#seeked
23307 * @private
23308 */
23309 handleTechSeeked_() {
23310 this.removeClass('vjs-seeking', 'vjs-ended');
23311 /**
23312 * Fired when the player has finished jumping to a new time
23313 *
23314 * @event Player#seeked
23315 * @type {Event}
23316 */
23317 this.trigger('seeked');
23318 }
23319
23320 /**
23321 * Retrigger the `pause` event that was triggered by the {@link Tech}.
23322 *
23323 * @fires Player#pause
23324 * @listens Tech#pause
23325 * @private
23326 */
23327 handleTechPause_() {
23328 this.removeClass('vjs-playing');
23329 this.addClass('vjs-paused');
23330 /**
23331 * Fired whenever the media has been paused
23332 *
23333 * @event Player#pause
23334 * @type {Event}
23335 */
23336 this.trigger('pause');
23337 }
23338
23339 /**
23340 * Retrigger the `ended` event that was triggered by the {@link Tech}.
23341 *
23342 * @fires Player#ended
23343 * @listens Tech#ended
23344 * @private
23345 */
23346 handleTechEnded_() {
23347 this.addClass('vjs-ended');
23348 this.removeClass('vjs-waiting');
23349 if (this.options_.loop) {
23350 this.currentTime(0);
23351 this.play();
23352 } else if (!this.paused()) {
23353 this.pause();
23354 }
23355
23356 /**
23357 * Fired when the end of the media resource is reached (currentTime == duration)
23358 *
23359 * @event Player#ended
23360 * @type {Event}
23361 */
23362 this.trigger('ended');
23363 }
23364
23365 /**
23366 * Fired when the duration of the media resource is first known or changed
23367 *
23368 * @listens Tech#durationchange
23369 * @private
23370 */
23371 handleTechDurationChange_() {
23372 this.duration(this.techGet_('duration'));
23373 }
23374
23375 /**
23376 * Handle a click on the media element to play/pause
23377 *
23378 * @param {Event} event
23379 * the event that caused this function to trigger
23380 *
23381 * @listens Tech#click
23382 * @private
23383 */
23384 handleTechClick_(event) {
23385 // When controls are disabled a click should not toggle playback because
23386 // the click is considered a control
23387 if (!this.controls_) {
23388 return;
23389 }
23390 if (this.options_ === undefined || this.options_.userActions === undefined || this.options_.userActions.click === undefined || this.options_.userActions.click !== false) {
23391 if (this.options_ !== undefined && this.options_.userActions !== undefined && typeof this.options_.userActions.click === 'function') {
23392 this.options_.userActions.click.call(this, event);
23393 } else if (this.paused()) {
23394 silencePromise(this.play());
23395 } else {
23396 this.pause();
23397 }
23398 }
23399 }
23400
23401 /**
23402 * Handle a double-click on the media element to enter/exit fullscreen,
23403 * or exit documentPictureInPicture mode
23404 *
23405 * @param {Event} event
23406 * the event that caused this function to trigger
23407 *
23408 * @listens Tech#dblclick
23409 * @private
23410 */
23411 handleTechDoubleClick_(event) {
23412 if (!this.controls_) {
23413 return;
23414 }
23415
23416 // we do not want to toggle fullscreen state
23417 // when double-clicking inside a control bar or a modal
23418 const inAllowedEls = Array.prototype.some.call(this.$$('.vjs-control-bar, .vjs-modal-dialog'), el => el.contains(event.target));
23419 if (!inAllowedEls) {
23420 /*
23421 * options.userActions.doubleClick
23422 *
23423 * If `undefined` or `true`, double-click toggles fullscreen if controls are present
23424 * Set to `false` to disable double-click handling
23425 * Set to a function to substitute an external double-click handler
23426 */
23427 if (this.options_ === undefined || this.options_.userActions === undefined || this.options_.userActions.doubleClick === undefined || this.options_.userActions.doubleClick !== false) {
23428 if (this.options_ !== undefined && this.options_.userActions !== undefined && typeof this.options_.userActions.doubleClick === 'function') {
23429 this.options_.userActions.doubleClick.call(this, event);
23430 } else if (this.isInPictureInPicture() && !document__default["default"].pictureInPictureElement) {
23431 // Checking the presence of `window.documentPictureInPicture.window` complicates
23432 // tests, checking `document.pictureInPictureElement` also works. It wouldn't
23433 // be null in regular picture in picture.
23434 // Exit picture in picture mode. This gesture can't trigger pip on the main window.
23435 this.exitPictureInPicture();
23436 } else if (this.isFullscreen()) {
23437 this.exitFullscreen();
23438 } else {
23439 this.requestFullscreen();
23440 }
23441 }
23442 }
23443 }
23444
23445 /**
23446 * Handle a tap on the media element. It will toggle the user
23447 * activity state, which hides and shows the controls.
23448 *
23449 * @listens Tech#tap
23450 * @private
23451 */
23452 handleTechTap_() {
23453 this.userActive(!this.userActive());
23454 }
23455
23456 /**
23457 * Handle touch to start
23458 *
23459 * @listens Tech#touchstart
23460 * @private
23461 */
23462 handleTechTouchStart_() {
23463 this.userWasActive = this.userActive();
23464 }
23465
23466 /**
23467 * Handle touch to move
23468 *
23469 * @listens Tech#touchmove
23470 * @private
23471 */
23472 handleTechTouchMove_() {
23473 if (this.userWasActive) {
23474 this.reportUserActivity();
23475 }
23476 }
23477
23478 /**
23479 * Handle touch to end
23480 *
23481 * @param {Event} event
23482 * the touchend event that triggered
23483 * this function
23484 *
23485 * @listens Tech#touchend
23486 * @private
23487 */
23488 handleTechTouchEnd_(event) {
23489 // Stop the mouse events from also happening
23490 if (event.cancelable) {
23491 event.preventDefault();
23492 }
23493 }
23494
23495 /**
23496 * @private
23497 */
23498 toggleFullscreenClass_() {
23499 if (this.isFullscreen()) {
23500 this.addClass('vjs-fullscreen');
23501 } else {
23502 this.removeClass('vjs-fullscreen');
23503 }
23504 }
23505
23506 /**
23507 * when the document fschange event triggers it calls this
23508 */
23509 documentFullscreenChange_(e) {
23510 const targetPlayer = e.target.player;
23511
23512 // if another player was fullscreen
23513 // do a null check for targetPlayer because older firefox's would put document as e.target
23514 if (targetPlayer && targetPlayer !== this) {
23515 return;
23516 }
23517 const el = this.el();
23518 let isFs = document__default["default"][this.fsApi_.fullscreenElement] === el;
23519 if (!isFs && el.matches) {
23520 isFs = el.matches(':' + this.fsApi_.fullscreen);
23521 }
23522 this.isFullscreen(isFs);
23523 }
23524
23525 /**
23526 * Handle Tech Fullscreen Change
23527 *
23528 * @param {Event} event
23529 * the fullscreenchange event that triggered this function
23530 *
23531 * @param {Object} data
23532 * the data that was sent with the event
23533 *
23534 * @private
23535 * @listens Tech#fullscreenchange
23536 * @fires Player#fullscreenchange
23537 */
23538 handleTechFullscreenChange_(event, data) {
23539 if (data) {
23540 if (data.nativeIOSFullscreen) {
23541 this.addClass('vjs-ios-native-fs');
23542 this.tech_.one('webkitendfullscreen', () => {
23543 this.removeClass('vjs-ios-native-fs');
23544 });
23545 }
23546 this.isFullscreen(data.isFullscreen);
23547 }
23548 }
23549 handleTechFullscreenError_(event, err) {
23550 this.trigger('fullscreenerror', err);
23551 }
23552
23553 /**
23554 * @private
23555 */
23556 togglePictureInPictureClass_() {
23557 if (this.isInPictureInPicture()) {
23558 this.addClass('vjs-picture-in-picture');
23559 } else {
23560 this.removeClass('vjs-picture-in-picture');
23561 }
23562 }
23563
23564 /**
23565 * Handle Tech Enter Picture-in-Picture.
23566 *
23567 * @param {Event} event
23568 * the enterpictureinpicture event that triggered this function
23569 *
23570 * @private
23571 * @listens Tech#enterpictureinpicture
23572 */
23573 handleTechEnterPictureInPicture_(event) {
23574 this.isInPictureInPicture(true);
23575 }
23576
23577 /**
23578 * Handle Tech Leave Picture-in-Picture.
23579 *
23580 * @param {Event} event
23581 * the leavepictureinpicture event that triggered this function
23582 *
23583 * @private
23584 * @listens Tech#leavepictureinpicture
23585 */
23586 handleTechLeavePictureInPicture_(event) {
23587 this.isInPictureInPicture(false);
23588 }
23589
23590 /**
23591 * Fires when an error occurred during the loading of an audio/video.
23592 *
23593 * @private
23594 * @listens Tech#error
23595 */
23596 handleTechError_() {
23597 const error = this.tech_.error();
23598 if (error) {
23599 this.error(error);
23600 }
23601 }
23602
23603 /**
23604 * Retrigger the `textdata` event that was triggered by the {@link Tech}.
23605 *
23606 * @fires Player#textdata
23607 * @listens Tech#textdata
23608 * @private
23609 */
23610 handleTechTextData_() {
23611 let data = null;
23612 if (arguments.length > 1) {
23613 data = arguments[1];
23614 }
23615
23616 /**
23617 * Fires when we get a textdata event from tech
23618 *
23619 * @event Player#textdata
23620 * @type {Event}
23621 */
23622 this.trigger('textdata', data);
23623 }
23624
23625 /**
23626 * Get object for cached values.
23627 *
23628 * @return {Object}
23629 * get the current object cache
23630 */
23631 getCache() {
23632 return this.cache_;
23633 }
23634
23635 /**
23636 * Resets the internal cache object.
23637 *
23638 * Using this function outside the player constructor or reset method may
23639 * have unintended side-effects.
23640 *
23641 * @private
23642 */
23643 resetCache_() {
23644 this.cache_ = {
23645 // Right now, the currentTime is not _really_ cached because it is always
23646 // retrieved from the tech (see: currentTime). However, for completeness,
23647 // we set it to zero here to ensure that if we do start actually caching
23648 // it, we reset it along with everything else.
23649 currentTime: 0,
23650 initTime: 0,
23651 inactivityTimeout: this.options_.inactivityTimeout,
23652 duration: NaN,
23653 lastVolume: 1,
23654 lastPlaybackRate: this.defaultPlaybackRate(),
23655 media: null,
23656 src: '',
23657 source: {},
23658 sources: [],
23659 playbackRates: [],
23660 volume: 1
23661 };
23662 }
23663
23664 /**
23665 * Pass values to the playback tech
23666 *
23667 * @param {string} [method]
23668 * the method to call
23669 *
23670 * @param {Object} [arg]
23671 * the argument to pass
23672 *
23673 * @private
23674 */
23675 techCall_(method, arg) {
23676 // If it's not ready yet, call method when it is
23677
23678 this.ready(function () {
23679 if (method in allowedSetters) {
23680 return set(this.middleware_, this.tech_, method, arg);
23681 } else if (method in allowedMediators) {
23682 return mediate(this.middleware_, this.tech_, method, arg);
23683 }
23684 try {
23685 if (this.tech_) {
23686 this.tech_[method](arg);
23687 }
23688 } catch (e) {
23689 log(e);
23690 throw e;
23691 }
23692 }, true);
23693 }
23694
23695 /**
23696 * Mediate attempt to call playback tech method
23697 * and return the value of the method called.
23698 *
23699 * @param {string} method
23700 * Tech method
23701 *
23702 * @return {*}
23703 * Value returned by the tech method called, undefined if tech
23704 * is not ready or tech method is not present
23705 *
23706 * @private
23707 */
23708 techGet_(method) {
23709 if (!this.tech_ || !this.tech_.isReady_) {
23710 return;
23711 }
23712 if (method in allowedGetters) {
23713 return get(this.middleware_, this.tech_, method);
23714 } else if (method in allowedMediators) {
23715 return mediate(this.middleware_, this.tech_, method);
23716 }
23717
23718 // Log error when playback tech object is present but method
23719 // is undefined or unavailable
23720 try {
23721 return this.tech_[method]();
23722 } catch (e) {
23723 // When building additional tech libs, an expected method may not be defined yet
23724 if (this.tech_[method] === undefined) {
23725 log(`Video.js: ${method} method not defined for ${this.techName_} playback technology.`, e);
23726 throw e;
23727 }
23728
23729 // When a method isn't available on the object it throws a TypeError
23730 if (e.name === 'TypeError') {
23731 log(`Video.js: ${method} unavailable on ${this.techName_} playback technology element.`, e);
23732 this.tech_.isReady_ = false;
23733 throw e;
23734 }
23735
23736 // If error unknown, just log and throw
23737 log(e);
23738 throw e;
23739 }
23740 }
23741
23742 /**
23743 * Attempt to begin playback at the first opportunity.
23744 *
23745 * @return {Promise|undefined}
23746 * Returns a promise if the browser supports Promises (or one
23747 * was passed in as an option). This promise will be resolved on
23748 * the return value of play. If this is undefined it will fulfill the
23749 * promise chain otherwise the promise chain will be fulfilled when
23750 * the promise from play is fulfilled.
23751 */
23752 play() {
23753 return new Promise(resolve => {
23754 this.play_(resolve);
23755 });
23756 }
23757
23758 /**
23759 * The actual logic for play, takes a callback that will be resolved on the
23760 * return value of play. This allows us to resolve to the play promise if there
23761 * is one on modern browsers.
23762 *
23763 * @private
23764 * @param {Function} [callback]
23765 * The callback that should be called when the techs play is actually called
23766 */
23767 play_(callback = silencePromise) {
23768 this.playCallbacks_.push(callback);
23769 const isSrcReady = Boolean(!this.changingSrc_ && (this.src() || this.currentSrc()));
23770 const isSafariOrIOS = Boolean(IS_ANY_SAFARI || IS_IOS);
23771
23772 // treat calls to play_ somewhat like the `one` event function
23773 if (this.waitToPlay_) {
23774 this.off(['ready', 'loadstart'], this.waitToPlay_);
23775 this.waitToPlay_ = null;
23776 }
23777
23778 // if the player/tech is not ready or the src itself is not ready
23779 // queue up a call to play on `ready` or `loadstart`
23780 if (!this.isReady_ || !isSrcReady) {
23781 this.waitToPlay_ = e => {
23782 this.play_();
23783 };
23784 this.one(['ready', 'loadstart'], this.waitToPlay_);
23785
23786 // if we are in Safari, there is a high chance that loadstart will trigger after the gesture timeperiod
23787 // in that case, we need to prime the video element by calling load so it'll be ready in time
23788 if (!isSrcReady && isSafariOrIOS) {
23789 this.load();
23790 }
23791 return;
23792 }
23793
23794 // If the player/tech is ready and we have a source, we can attempt playback.
23795 const val = this.techGet_('play');
23796
23797 // For native playback, reset the progress bar if we get a play call from a replay.
23798 const isNativeReplay = isSafariOrIOS && this.hasClass('vjs-ended');
23799 if (isNativeReplay) {
23800 this.resetProgressBar_();
23801 }
23802 // play was terminated if the returned value is null
23803 if (val === null) {
23804 this.runPlayTerminatedQueue_();
23805 } else {
23806 this.runPlayCallbacks_(val);
23807 }
23808 }
23809
23810 /**
23811 * These functions will be run when if play is terminated. If play
23812 * runPlayCallbacks_ is run these function will not be run. This allows us
23813 * to differentiate between a terminated play and an actual call to play.
23814 */
23815 runPlayTerminatedQueue_() {
23816 const queue = this.playTerminatedQueue_.slice(0);
23817 this.playTerminatedQueue_ = [];
23818 queue.forEach(function (q) {
23819 q();
23820 });
23821 }
23822
23823 /**
23824 * When a callback to play is delayed we have to run these
23825 * callbacks when play is actually called on the tech. This function
23826 * runs the callbacks that were delayed and accepts the return value
23827 * from the tech.
23828 *
23829 * @param {undefined|Promise} val
23830 * The return value from the tech.
23831 */
23832 runPlayCallbacks_(val) {
23833 const callbacks = this.playCallbacks_.slice(0);
23834 this.playCallbacks_ = [];
23835 // clear play terminatedQueue since we finished a real play
23836 this.playTerminatedQueue_ = [];
23837 callbacks.forEach(function (cb) {
23838 cb(val);
23839 });
23840 }
23841
23842 /**
23843 * Pause the video playback
23844 */
23845 pause() {
23846 this.techCall_('pause');
23847 }
23848
23849 /**
23850 * Check if the player is paused or has yet to play
23851 *
23852 * @return {boolean}
23853 * - false: if the media is currently playing
23854 * - true: if media is not currently playing
23855 */
23856 paused() {
23857 // The initial state of paused should be true (in Safari it's actually false)
23858 return this.techGet_('paused') === false ? false : true;
23859 }
23860
23861 /**
23862 * Get a TimeRange object representing the current ranges of time that the user
23863 * has played.
23864 *
23865 * @return {TimeRange}
23866 * A time range object that represents all the increments of time that have
23867 * been played.
23868 */
23869 played() {
23870 return this.techGet_('played') || createTimeRanges(0, 0);
23871 }
23872
23873 /**
23874 * Sets or returns whether or not the user is "scrubbing". Scrubbing is
23875 * when the user has clicked the progress bar handle and is
23876 * dragging it along the progress bar.
23877 *
23878 * @param {boolean} [isScrubbing]
23879 * whether the user is or is not scrubbing
23880 *
23881 * @return {boolean|undefined}
23882 * - The value of scrubbing when getting
23883 * - Nothing when setting
23884 */
23885 scrubbing(isScrubbing) {
23886 if (typeof isScrubbing === 'undefined') {
23887 return this.scrubbing_;
23888 }
23889 this.scrubbing_ = !!isScrubbing;
23890 this.techCall_('setScrubbing', this.scrubbing_);
23891 if (isScrubbing) {
23892 this.addClass('vjs-scrubbing');
23893 } else {
23894 this.removeClass('vjs-scrubbing');
23895 }
23896 }
23897
23898 /**
23899 * Get or set the current time (in seconds)
23900 *
23901 * @param {number|string} [seconds]
23902 * The time to seek to in seconds
23903 *
23904 * @return {number|undefined}
23905 * - the current time in seconds when getting
23906 * - Nothing when setting
23907 */
23908 currentTime(seconds) {
23909 if (seconds === undefined) {
23910 // cache last currentTime and return. default to 0 seconds
23911 //
23912 // Caching the currentTime is meant to prevent a massive amount of reads on the tech's
23913 // currentTime when scrubbing, but may not provide much performance benefit after all.
23914 // Should be tested. Also something has to read the actual current time or the cache will
23915 // never get updated.
23916 this.cache_.currentTime = this.techGet_('currentTime') || 0;
23917 return this.cache_.currentTime;
23918 }
23919 if (seconds < 0) {
23920 seconds = 0;
23921 }
23922 if (!this.isReady_ || this.changingSrc_ || !this.tech_ || !this.tech_.isReady_) {
23923 this.cache_.initTime = seconds;
23924 this.off('canplay', this.boundApplyInitTime_);
23925 this.one('canplay', this.boundApplyInitTime_);
23926 return;
23927 }
23928 this.techCall_('setCurrentTime', seconds);
23929 this.cache_.initTime = 0;
23930 if (isFinite(seconds)) {
23931 this.cache_.currentTime = Number(seconds);
23932 }
23933 }
23934
23935 /**
23936 * Apply the value of initTime stored in cache as currentTime.
23937 *
23938 * @private
23939 */
23940 applyInitTime_() {
23941 this.currentTime(this.cache_.initTime);
23942 }
23943
23944 /**
23945 * Normally gets the length in time of the video in seconds;
23946 * in all but the rarest use cases an argument will NOT be passed to the method
23947 *
23948 * > **NOTE**: The video must have started loading before the duration can be
23949 * known, and depending on preload behaviour may not be known until the video starts
23950 * playing.
23951 *
23952 * @fires Player#durationchange
23953 *
23954 * @param {number} [seconds]
23955 * The duration of the video to set in seconds
23956 *
23957 * @return {number|undefined}
23958 * - The duration of the video in seconds when getting
23959 * - Nothing when setting
23960 */
23961 duration(seconds) {
23962 if (seconds === undefined) {
23963 // return NaN if the duration is not known
23964 return this.cache_.duration !== undefined ? this.cache_.duration : NaN;
23965 }
23966 seconds = parseFloat(seconds);
23967
23968 // Standardize on Infinity for signaling video is live
23969 if (seconds < 0) {
23970 seconds = Infinity;
23971 }
23972 if (seconds !== this.cache_.duration) {
23973 // Cache the last set value for optimized scrubbing
23974 this.cache_.duration = seconds;
23975 if (seconds === Infinity) {
23976 this.addClass('vjs-live');
23977 } else {
23978 this.removeClass('vjs-live');
23979 }
23980 if (!isNaN(seconds)) {
23981 // Do not fire durationchange unless the duration value is known.
23982 // @see [Spec]{@link https://www.w3.org/TR/2011/WD-html5-20110113/video.html#media-element-load-algorithm}
23983
23984 /**
23985 * @event Player#durationchange
23986 * @type {Event}
23987 */
23988 this.trigger('durationchange');
23989 }
23990 }
23991 }
23992
23993 /**
23994 * Calculates how much time is left in the video. Not part
23995 * of the native video API.
23996 *
23997 * @return {number}
23998 * The time remaining in seconds
23999 */
24000 remainingTime() {
24001 return this.duration() - this.currentTime();
24002 }
24003
24004 /**
24005 * A remaining time function that is intended to be used when
24006 * the time is to be displayed directly to the user.
24007 *
24008 * @return {number}
24009 * The rounded time remaining in seconds
24010 */
24011 remainingTimeDisplay() {
24012 return Math.floor(this.duration()) - Math.floor(this.currentTime());
24013 }
24014
24015 //
24016 // Kind of like an array of portions of the video that have been downloaded.
24017
24018 /**
24019 * Get a TimeRange object with an array of the times of the video
24020 * that have been downloaded. If you just want the percent of the
24021 * video that's been downloaded, use bufferedPercent.
24022 *
24023 * @see [Buffered Spec]{@link http://dev.w3.org/html5/spec/video.html#dom-media-buffered}
24024 *
24025 * @return {TimeRange}
24026 * A mock {@link TimeRanges} object (following HTML spec)
24027 */
24028 buffered() {
24029 let buffered = this.techGet_('buffered');
24030 if (!buffered || !buffered.length) {
24031 buffered = createTimeRanges(0, 0);
24032 }
24033 return buffered;
24034 }
24035
24036 /**
24037 * Get the TimeRanges of the media that are currently available
24038 * for seeking to.
24039 *
24040 * @see [Seekable Spec]{@link https://html.spec.whatwg.org/multipage/media.html#dom-media-seekable}
24041 *
24042 * @return {TimeRange}
24043 * A mock {@link TimeRanges} object (following HTML spec)
24044 */
24045 seekable() {
24046 let seekable = this.techGet_('seekable');
24047 if (!seekable || !seekable.length) {
24048 seekable = createTimeRanges(0, 0);
24049 }
24050 return seekable;
24051 }
24052
24053 /**
24054 * Returns whether the player is in the "seeking" state.
24055 *
24056 * @return {boolean} True if the player is in the seeking state, false if not.
24057 */
24058 seeking() {
24059 return this.techGet_('seeking');
24060 }
24061
24062 /**
24063 * Returns whether the player is in the "ended" state.
24064 *
24065 * @return {boolean} True if the player is in the ended state, false if not.
24066 */
24067 ended() {
24068 return this.techGet_('ended');
24069 }
24070
24071 /**
24072 * Returns the current state of network activity for the element, from
24073 * the codes in the list below.
24074 * - NETWORK_EMPTY (numeric value 0)
24075 * The element has not yet been initialised. All attributes are in
24076 * their initial states.
24077 * - NETWORK_IDLE (numeric value 1)
24078 * The element's resource selection algorithm is active and has
24079 * selected a resource, but it is not actually using the network at
24080 * this time.
24081 * - NETWORK_LOADING (numeric value 2)
24082 * The user agent is actively trying to download data.
24083 * - NETWORK_NO_SOURCE (numeric value 3)
24084 * The element's resource selection algorithm is active, but it has
24085 * not yet found a resource to use.
24086 *
24087 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#network-states
24088 * @return {number} the current network activity state
24089 */
24090 networkState() {
24091 return this.techGet_('networkState');
24092 }
24093
24094 /**
24095 * Returns a value that expresses the current state of the element
24096 * with respect to rendering the current playback position, from the
24097 * codes in the list below.
24098 * - HAVE_NOTHING (numeric value 0)
24099 * No information regarding the media resource is available.
24100 * - HAVE_METADATA (numeric value 1)
24101 * Enough of the resource has been obtained that the duration of the
24102 * resource is available.
24103 * - HAVE_CURRENT_DATA (numeric value 2)
24104 * Data for the immediate current playback position is available.
24105 * - HAVE_FUTURE_DATA (numeric value 3)
24106 * Data for the immediate current playback position is available, as
24107 * well as enough data for the user agent to advance the current
24108 * playback position in the direction of playback.
24109 * - HAVE_ENOUGH_DATA (numeric value 4)
24110 * The user agent estimates that enough data is available for
24111 * playback to proceed uninterrupted.
24112 *
24113 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-readystate
24114 * @return {number} the current playback rendering state
24115 */
24116 readyState() {
24117 return this.techGet_('readyState');
24118 }
24119
24120 /**
24121 * Get the percent (as a decimal) of the video that's been downloaded.
24122 * This method is not a part of the native HTML video API.
24123 *
24124 * @return {number}
24125 * A decimal between 0 and 1 representing the percent
24126 * that is buffered 0 being 0% and 1 being 100%
24127 */
24128 bufferedPercent() {
24129 return bufferedPercent(this.buffered(), this.duration());
24130 }
24131
24132 /**
24133 * Get the ending time of the last buffered time range
24134 * This is used in the progress bar to encapsulate all time ranges.
24135 *
24136 * @return {number}
24137 * The end of the last buffered time range
24138 */
24139 bufferedEnd() {
24140 const buffered = this.buffered();
24141 const duration = this.duration();
24142 let end = buffered.end(buffered.length - 1);
24143 if (end > duration) {
24144 end = duration;
24145 }
24146 return end;
24147 }
24148
24149 /**
24150 * Get or set the current volume of the media
24151 *
24152 * @param {number} [percentAsDecimal]
24153 * The new volume as a decimal percent:
24154 * - 0 is muted/0%/off
24155 * - 1.0 is 100%/full
24156 * - 0.5 is half volume or 50%
24157 *
24158 * @return {number|undefined}
24159 * The current volume as a percent when getting
24160 */
24161 volume(percentAsDecimal) {
24162 let vol;
24163 if (percentAsDecimal !== undefined) {
24164 // Force value to between 0 and 1
24165 vol = Math.max(0, Math.min(1, percentAsDecimal));
24166 this.cache_.volume = vol;
24167 this.techCall_('setVolume', vol);
24168 if (vol > 0) {
24169 this.lastVolume_(vol);
24170 }
24171 return;
24172 }
24173
24174 // Default to 1 when returning current volume.
24175 vol = parseFloat(this.techGet_('volume'));
24176 return isNaN(vol) ? 1 : vol;
24177 }
24178
24179 /**
24180 * Get the current muted state, or turn mute on or off
24181 *
24182 * @param {boolean} [muted]
24183 * - true to mute
24184 * - false to unmute
24185 *
24186 * @return {boolean|undefined}
24187 * - true if mute is on and getting
24188 * - false if mute is off and getting
24189 * - nothing if setting
24190 */
24191 muted(muted) {
24192 if (muted !== undefined) {
24193 this.techCall_('setMuted', muted);
24194 return;
24195 }
24196 return this.techGet_('muted') || false;
24197 }
24198
24199 /**
24200 * Get the current defaultMuted state, or turn defaultMuted on or off. defaultMuted
24201 * indicates the state of muted on initial playback.
24202 *
24203 * ```js
24204 * var myPlayer = videojs('some-player-id');
24205 *
24206 * myPlayer.src("http://www.example.com/path/to/video.mp4");
24207 *
24208 * // get, should be false
24209 * console.log(myPlayer.defaultMuted());
24210 * // set to true
24211 * myPlayer.defaultMuted(true);
24212 * // get should be true
24213 * console.log(myPlayer.defaultMuted());
24214 * ```
24215 *
24216 * @param {boolean} [defaultMuted]
24217 * - true to mute
24218 * - false to unmute
24219 *
24220 * @return {boolean|undefined}
24221 * - true if defaultMuted is on and getting
24222 * - false if defaultMuted is off and getting
24223 * - Nothing when setting
24224 */
24225 defaultMuted(defaultMuted) {
24226 if (defaultMuted !== undefined) {
24227 this.techCall_('setDefaultMuted', defaultMuted);
24228 }
24229 return this.techGet_('defaultMuted') || false;
24230 }
24231
24232 /**
24233 * Get the last volume, or set it
24234 *
24235 * @param {number} [percentAsDecimal]
24236 * The new last volume as a decimal percent:
24237 * - 0 is muted/0%/off
24238 * - 1.0 is 100%/full
24239 * - 0.5 is half volume or 50%
24240 *
24241 * @return {number|undefined}
24242 * - The current value of lastVolume as a percent when getting
24243 * - Nothing when setting
24244 *
24245 * @private
24246 */
24247 lastVolume_(percentAsDecimal) {
24248 if (percentAsDecimal !== undefined && percentAsDecimal !== 0) {
24249 this.cache_.lastVolume = percentAsDecimal;
24250 return;
24251 }
24252 return this.cache_.lastVolume;
24253 }
24254
24255 /**
24256 * Check if current tech can support native fullscreen
24257 * (e.g. with built in controls like iOS)
24258 *
24259 * @return {boolean}
24260 * if native fullscreen is supported
24261 */
24262 supportsFullScreen() {
24263 return this.techGet_('supportsFullScreen') || false;
24264 }
24265
24266 /**
24267 * Check if the player is in fullscreen mode or tell the player that it
24268 * is or is not in fullscreen mode.
24269 *
24270 * > NOTE: As of the latest HTML5 spec, isFullscreen is no longer an official
24271 * property and instead document.fullscreenElement is used. But isFullscreen is
24272 * still a valuable property for internal player workings.
24273 *
24274 * @param {boolean} [isFS]
24275 * Set the players current fullscreen state
24276 *
24277 * @return {boolean|undefined}
24278 * - true if fullscreen is on and getting
24279 * - false if fullscreen is off and getting
24280 * - Nothing when setting
24281 */
24282 isFullscreen(isFS) {
24283 if (isFS !== undefined) {
24284 const oldValue = this.isFullscreen_;
24285 this.isFullscreen_ = Boolean(isFS);
24286
24287 // if we changed fullscreen state and we're in prefixed mode, trigger fullscreenchange
24288 // this is the only place where we trigger fullscreenchange events for older browsers
24289 // fullWindow mode is treated as a prefixed event and will get a fullscreenchange event as well
24290 if (this.isFullscreen_ !== oldValue && this.fsApi_.prefixed) {
24291 /**
24292 * @event Player#fullscreenchange
24293 * @type {Event}
24294 */
24295 this.trigger('fullscreenchange');
24296 }
24297 this.toggleFullscreenClass_();
24298 return;
24299 }
24300 return this.isFullscreen_;
24301 }
24302
24303 /**
24304 * Increase the size of the video to full screen
24305 * In some browsers, full screen is not supported natively, so it enters
24306 * "full window mode", where the video fills the browser window.
24307 * In browsers and devices that support native full screen, sometimes the
24308 * browser's default controls will be shown, and not the Video.js custom skin.
24309 * This includes most mobile devices (iOS, Android) and older versions of
24310 * Safari.
24311 *
24312 * @param {Object} [fullscreenOptions]
24313 * Override the player fullscreen options
24314 *
24315 * @fires Player#fullscreenchange
24316 */
24317 requestFullscreen(fullscreenOptions) {
24318 if (this.isInPictureInPicture()) {
24319 this.exitPictureInPicture();
24320 }
24321 const self = this;
24322 return new Promise((resolve, reject) => {
24323 function offHandler() {
24324 self.off('fullscreenerror', errorHandler);
24325 self.off('fullscreenchange', changeHandler);
24326 }
24327 function changeHandler() {
24328 offHandler();
24329 resolve();
24330 }
24331 function errorHandler(e, err) {
24332 offHandler();
24333 reject(err);
24334 }
24335 self.one('fullscreenchange', changeHandler);
24336 self.one('fullscreenerror', errorHandler);
24337 const promise = self.requestFullscreenHelper_(fullscreenOptions);
24338 if (promise) {
24339 promise.then(offHandler, offHandler);
24340 promise.then(resolve, reject);
24341 }
24342 });
24343 }
24344 requestFullscreenHelper_(fullscreenOptions) {
24345 let fsOptions;
24346
24347 // Only pass fullscreen options to requestFullscreen in spec-compliant browsers.
24348 // Use defaults or player configured option unless passed directly to this method.
24349 if (!this.fsApi_.prefixed) {
24350 fsOptions = this.options_.fullscreen && this.options_.fullscreen.options || {};
24351 if (fullscreenOptions !== undefined) {
24352 fsOptions = fullscreenOptions;
24353 }
24354 }
24355
24356 // This method works as follows:
24357 // 1. if a fullscreen api is available, use it
24358 // 1. call requestFullscreen with potential options
24359 // 2. if we got a promise from above, use it to update isFullscreen()
24360 // 2. otherwise, if the tech supports fullscreen, call `enterFullScreen` on it.
24361 // This is particularly used for iPhone, older iPads, and non-safari browser on iOS.
24362 // 3. otherwise, use "fullWindow" mode
24363 if (this.fsApi_.requestFullscreen) {
24364 const promise = this.el_[this.fsApi_.requestFullscreen](fsOptions);
24365
24366 // Even on browsers with promise support this may not return a promise
24367 if (promise) {
24368 promise.then(() => this.isFullscreen(true), () => this.isFullscreen(false));
24369 }
24370 return promise;
24371 } else if (this.tech_.supportsFullScreen() && !this.options_.preferFullWindow === true) {
24372 // we can't take the video.js controls fullscreen but we can go fullscreen
24373 // with native controls
24374 this.techCall_('enterFullScreen');
24375 } else {
24376 // fullscreen isn't supported so we'll just stretch the video element to
24377 // fill the viewport
24378 this.enterFullWindow();
24379 }
24380 }
24381
24382 /**
24383 * Return the video to its normal size after having been in full screen mode
24384 *
24385 * @fires Player#fullscreenchange
24386 */
24387 exitFullscreen() {
24388 const self = this;
24389 return new Promise((resolve, reject) => {
24390 function offHandler() {
24391 self.off('fullscreenerror', errorHandler);
24392 self.off('fullscreenchange', changeHandler);
24393 }
24394 function changeHandler() {
24395 offHandler();
24396 resolve();
24397 }
24398 function errorHandler(e, err) {
24399 offHandler();
24400 reject(err);
24401 }
24402 self.one('fullscreenchange', changeHandler);
24403 self.one('fullscreenerror', errorHandler);
24404 const promise = self.exitFullscreenHelper_();
24405 if (promise) {
24406 promise.then(offHandler, offHandler);
24407 // map the promise to our resolve/reject methods
24408 promise.then(resolve, reject);
24409 }
24410 });
24411 }
24412 exitFullscreenHelper_() {
24413 if (this.fsApi_.requestFullscreen) {
24414 const promise = document__default["default"][this.fsApi_.exitFullscreen]();
24415
24416 // Even on browsers with promise support this may not return a promise
24417 if (promise) {
24418 // we're splitting the promise here, so, we want to catch the
24419 // potential error so that this chain doesn't have unhandled errors
24420 silencePromise(promise.then(() => this.isFullscreen(false)));
24421 }
24422 return promise;
24423 } else if (this.tech_.supportsFullScreen() && !this.options_.preferFullWindow === true) {
24424 this.techCall_('exitFullScreen');
24425 } else {
24426 this.exitFullWindow();
24427 }
24428 }
24429
24430 /**
24431 * When fullscreen isn't supported we can stretch the
24432 * video container to as wide as the browser will let us.
24433 *
24434 * @fires Player#enterFullWindow
24435 */
24436 enterFullWindow() {
24437 this.isFullscreen(true);
24438 this.isFullWindow = true;
24439
24440 // Storing original doc overflow value to return to when fullscreen is off
24441 this.docOrigOverflow = document__default["default"].documentElement.style.overflow;
24442
24443 // Add listener for esc key to exit fullscreen
24444 on(document__default["default"], 'keydown', this.boundFullWindowOnEscKey_);
24445
24446 // Hide any scroll bars
24447 document__default["default"].documentElement.style.overflow = 'hidden';
24448
24449 // Apply fullscreen styles
24450 addClass(document__default["default"].body, 'vjs-full-window');
24451
24452 /**
24453 * @event Player#enterFullWindow
24454 * @type {Event}
24455 */
24456 this.trigger('enterFullWindow');
24457 }
24458
24459 /**
24460 * Check for call to either exit full window or
24461 * full screen on ESC key
24462 *
24463 * @param {string} event
24464 * Event to check for key press
24465 */
24466 fullWindowOnEscKey(event) {
24467 if (event.key === 'Escape') {
24468 if (this.isFullscreen() === true) {
24469 if (!this.isFullWindow) {
24470 this.exitFullscreen();
24471 } else {
24472 this.exitFullWindow();
24473 }
24474 }
24475 }
24476 }
24477
24478 /**
24479 * Exit full window
24480 *
24481 * @fires Player#exitFullWindow
24482 */
24483 exitFullWindow() {
24484 this.isFullscreen(false);
24485 this.isFullWindow = false;
24486 off(document__default["default"], 'keydown', this.boundFullWindowOnEscKey_);
24487
24488 // Unhide scroll bars.
24489 document__default["default"].documentElement.style.overflow = this.docOrigOverflow;
24490
24491 // Remove fullscreen styles
24492 removeClass(document__default["default"].body, 'vjs-full-window');
24493
24494 // Resize the box, controller, and poster to original sizes
24495 // this.positionAll();
24496 /**
24497 * @event Player#exitFullWindow
24498 * @type {Event}
24499 */
24500 this.trigger('exitFullWindow');
24501 }
24502
24503 /**
24504 * Get or set disable Picture-in-Picture mode.
24505 *
24506 * @param {boolean} [value]
24507 * - true will disable Picture-in-Picture mode
24508 * - false will enable Picture-in-Picture mode
24509 */
24510 disablePictureInPicture(value) {
24511 if (value === undefined) {
24512 return this.techGet_('disablePictureInPicture');
24513 }
24514 this.techCall_('setDisablePictureInPicture', value);
24515 this.options_.disablePictureInPicture = value;
24516 this.trigger('disablepictureinpicturechanged');
24517 }
24518
24519 /**
24520 * Check if the player is in Picture-in-Picture mode or tell the player that it
24521 * is or is not in Picture-in-Picture mode.
24522 *
24523 * @param {boolean} [isPiP]
24524 * Set the players current Picture-in-Picture state
24525 *
24526 * @return {boolean|undefined}
24527 * - true if Picture-in-Picture is on and getting
24528 * - false if Picture-in-Picture is off and getting
24529 * - nothing if setting
24530 */
24531 isInPictureInPicture(isPiP) {
24532 if (isPiP !== undefined) {
24533 this.isInPictureInPicture_ = !!isPiP;
24534 this.togglePictureInPictureClass_();
24535 return;
24536 }
24537 return !!this.isInPictureInPicture_;
24538 }
24539
24540 /**
24541 * Create a floating video window always on top of other windows so that users may
24542 * continue consuming media while they interact with other content sites, or
24543 * applications on their device.
24544 *
24545 * This can use document picture-in-picture or element picture in picture
24546 *
24547 * Set `enableDocumentPictureInPicture` to `true` to use docPiP on a supported browser
24548 * Else set `disablePictureInPicture` to `false` to disable elPiP on a supported browser
24549 *
24550 *
24551 * @see [Spec]{@link https://w3c.github.io/picture-in-picture/}
24552 * @see [Spec]{@link https://wicg.github.io/document-picture-in-picture/}
24553 *
24554 * @fires Player#enterpictureinpicture
24555 *
24556 * @return {Promise}
24557 * A promise with a Picture-in-Picture window.
24558 */
24559 requestPictureInPicture() {
24560 if (this.options_.enableDocumentPictureInPicture && window__default["default"].documentPictureInPicture) {
24561 const pipContainer = document__default["default"].createElement(this.el().tagName);
24562 pipContainer.classList = this.el().classList;
24563 pipContainer.classList.add('vjs-pip-container');
24564 if (this.posterImage) {
24565 pipContainer.appendChild(this.posterImage.el().cloneNode(true));
24566 }
24567 if (this.titleBar) {
24568 pipContainer.appendChild(this.titleBar.el().cloneNode(true));
24569 }
24570 pipContainer.appendChild(createEl('p', {
24571 className: 'vjs-pip-text'
24572 }, {}, this.localize('Playing in picture-in-picture')));
24573 return window__default["default"].documentPictureInPicture.requestWindow({
24574 // The aspect ratio won't be correct, Chrome bug https://crbug.com/1407629
24575 width: this.videoWidth(),
24576 height: this.videoHeight()
24577 }).then(pipWindow => {
24578 copyStyleSheetsToWindow(pipWindow);
24579 this.el_.parentNode.insertBefore(pipContainer, this.el_);
24580 pipWindow.document.body.appendChild(this.el_);
24581 pipWindow.document.body.classList.add('vjs-pip-window');
24582 this.player_.isInPictureInPicture(true);
24583 this.player_.trigger({
24584 type: 'enterpictureinpicture',
24585 pipWindow
24586 });
24587
24588 // Listen for the PiP closing event to move the video back.
24589 pipWindow.addEventListener('pagehide', event => {
24590 const pipVideo = event.target.querySelector('.video-js');
24591 pipContainer.parentNode.replaceChild(pipVideo, pipContainer);
24592 this.player_.isInPictureInPicture(false);
24593 this.player_.trigger('leavepictureinpicture');
24594 });
24595 return pipWindow;
24596 });
24597 }
24598 if ('pictureInPictureEnabled' in document__default["default"] && this.disablePictureInPicture() === false) {
24599 /**
24600 * This event fires when the player enters picture in picture mode
24601 *
24602 * @event Player#enterpictureinpicture
24603 * @type {Event}
24604 */
24605 return this.techGet_('requestPictureInPicture');
24606 }
24607 return Promise.reject('No PiP mode is available');
24608 }
24609
24610 /**
24611 * Exit Picture-in-Picture mode.
24612 *
24613 * @see [Spec]{@link https://wicg.github.io/picture-in-picture}
24614 *
24615 * @fires Player#leavepictureinpicture
24616 *
24617 * @return {Promise}
24618 * A promise.
24619 */
24620 exitPictureInPicture() {
24621 if (window__default["default"].documentPictureInPicture && window__default["default"].documentPictureInPicture.window) {
24622 // With documentPictureInPicture, Player#leavepictureinpicture is fired in the pagehide handler
24623 window__default["default"].documentPictureInPicture.window.close();
24624 return Promise.resolve();
24625 }
24626 if ('pictureInPictureEnabled' in document__default["default"]) {
24627 /**
24628 * This event fires when the player leaves picture in picture mode
24629 *
24630 * @event Player#leavepictureinpicture
24631 * @type {Event}
24632 */
24633 return document__default["default"].exitPictureInPicture();
24634 }
24635 }
24636
24637 /**
24638 * Called when this Player has focus and a key gets pressed down, or when
24639 * any Component of this player receives a key press that it doesn't handle.
24640 * This allows player-wide hotkeys (either as defined below, or optionally
24641 * by an external function).
24642 *
24643 * @param {KeyboardEvent} event
24644 * The `keydown` event that caused this function to be called.
24645 *
24646 * @listens keydown
24647 */
24648 handleKeyDown(event) {
24649 const {
24650 userActions
24651 } = this.options_;
24652
24653 // Bail out if hotkeys are not configured.
24654 if (!userActions || !userActions.hotkeys) {
24655 return;
24656 }
24657
24658 // Function that determines whether or not to exclude an element from
24659 // hotkeys handling.
24660 const excludeElement = el => {
24661 const tagName = el.tagName.toLowerCase();
24662
24663 // The first and easiest test is for `contenteditable` elements.
24664 if (el.isContentEditable) {
24665 return true;
24666 }
24667
24668 // Inputs matching these types will still trigger hotkey handling as
24669 // they are not text inputs.
24670 const allowedInputTypes = ['button', 'checkbox', 'hidden', 'radio', 'reset', 'submit'];
24671 if (tagName === 'input') {
24672 return allowedInputTypes.indexOf(el.type) === -1;
24673 }
24674
24675 // The final test is by tag name. These tags will be excluded entirely.
24676 const excludedTags = ['textarea'];
24677 return excludedTags.indexOf(tagName) !== -1;
24678 };
24679
24680 // Bail out if the user is focused on an interactive form element.
24681 if (excludeElement(this.el_.ownerDocument.activeElement)) {
24682 return;
24683 }
24684 if (typeof userActions.hotkeys === 'function') {
24685 userActions.hotkeys.call(this, event);
24686 } else {
24687 this.handleHotkeys(event);
24688 }
24689 }
24690
24691 /**
24692 * Called when this Player receives a hotkey keydown event.
24693 * Supported player-wide hotkeys are:
24694 *
24695 * f - toggle fullscreen
24696 * m - toggle mute
24697 * k or Space - toggle play/pause
24698 *
24699 * @param {Event} event
24700 * The `keydown` event that caused this function to be called.
24701 */
24702 handleHotkeys(event) {
24703 const hotkeys = this.options_.userActions ? this.options_.userActions.hotkeys : {};
24704
24705 // set fullscreenKey, muteKey, playPauseKey from `hotkeys`, use defaults if not set
24706 const {
24707 fullscreenKey = keydownEvent => event.key.toLowerCase() === 'f',
24708 muteKey = keydownEvent => event.key.toLowerCase() === 'm',
24709 playPauseKey = keydownEvent => event.key.toLowerCase() === 'k' || event.key.toLowerCase() === ' '
24710 } = hotkeys;
24711 if (fullscreenKey.call(this, event)) {
24712 event.preventDefault();
24713 event.stopPropagation();
24714 const FSToggle = Component.getComponent('FullscreenToggle');
24715 if (document__default["default"][this.fsApi_.fullscreenEnabled] !== false) {
24716 FSToggle.prototype.handleClick.call(this, event);
24717 }
24718 } else if (muteKey.call(this, event)) {
24719 event.preventDefault();
24720 event.stopPropagation();
24721 const MuteToggle = Component.getComponent('MuteToggle');
24722 MuteToggle.prototype.handleClick.call(this, event);
24723 } else if (playPauseKey.call(this, event)) {
24724 event.preventDefault();
24725 event.stopPropagation();
24726 const PlayToggle = Component.getComponent('PlayToggle');
24727 PlayToggle.prototype.handleClick.call(this, event);
24728 }
24729 }
24730
24731 /**
24732 * Check whether the player can play a given mimetype
24733 *
24734 * @see https://www.w3.org/TR/2011/WD-html5-20110113/video.html#dom-navigator-canplaytype
24735 *
24736 * @param {string} type
24737 * The mimetype to check
24738 *
24739 * @return {string}
24740 * 'probably', 'maybe', or '' (empty string)
24741 */
24742 canPlayType(type) {
24743 let can;
24744
24745 // Loop through each playback technology in the options order
24746 for (let i = 0, j = this.options_.techOrder; i < j.length; i++) {
24747 const techName = j[i];
24748 let tech = Tech.getTech(techName);
24749
24750 // Support old behavior of techs being registered as components.
24751 // Remove once that deprecated behavior is removed.
24752 if (!tech) {
24753 tech = Component.getComponent(techName);
24754 }
24755
24756 // Check if the current tech is defined before continuing
24757 if (!tech) {
24758 log.error(`The "${techName}" tech is undefined. Skipped browser support check for that tech.`);
24759 continue;
24760 }
24761
24762 // Check if the browser supports this technology
24763 if (tech.isSupported()) {
24764 can = tech.canPlayType(type);
24765 if (can) {
24766 return can;
24767 }
24768 }
24769 }
24770 return '';
24771 }
24772
24773 /**
24774 * Select source based on tech-order or source-order
24775 * Uses source-order selection if `options.sourceOrder` is truthy. Otherwise,
24776 * defaults to tech-order selection
24777 *
24778 * @param {Array} sources
24779 * The sources for a media asset
24780 *
24781 * @return {Object|boolean}
24782 * Object of source and tech order or false
24783 */
24784 selectSource(sources) {
24785 // Get only the techs specified in `techOrder` that exist and are supported by the
24786 // current platform
24787 const techs = this.options_.techOrder.map(techName => {
24788 return [techName, Tech.getTech(techName)];
24789 }).filter(([techName, tech]) => {
24790 // Check if the current tech is defined before continuing
24791 if (tech) {
24792 // Check if the browser supports this technology
24793 return tech.isSupported();
24794 }
24795 log.error(`The "${techName}" tech is undefined. Skipped browser support check for that tech.`);
24796 return false;
24797 });
24798
24799 // Iterate over each `innerArray` element once per `outerArray` element and execute
24800 // `tester` with both. If `tester` returns a non-falsy value, exit early and return
24801 // that value.
24802 const findFirstPassingTechSourcePair = function (outerArray, innerArray, tester) {
24803 let found;
24804 outerArray.some(outerChoice => {
24805 return innerArray.some(innerChoice => {
24806 found = tester(outerChoice, innerChoice);
24807 if (found) {
24808 return true;
24809 }
24810 });
24811 });
24812 return found;
24813 };
24814 let foundSourceAndTech;
24815 const flip = fn => (a, b) => fn(b, a);
24816 const finder = ([techName, tech], source) => {
24817 if (tech.canPlaySource(source, this.options_[techName.toLowerCase()])) {
24818 return {
24819 source,
24820 tech: techName
24821 };
24822 }
24823 };
24824
24825 // Depending on the truthiness of `options.sourceOrder`, we swap the order of techs and sources
24826 // to select from them based on their priority.
24827 if (this.options_.sourceOrder) {
24828 // Source-first ordering
24829 foundSourceAndTech = findFirstPassingTechSourcePair(sources, techs, flip(finder));
24830 } else {
24831 // Tech-first ordering
24832 foundSourceAndTech = findFirstPassingTechSourcePair(techs, sources, finder);
24833 }
24834 return foundSourceAndTech || false;
24835 }
24836
24837 /**
24838 * Executes source setting and getting logic
24839 *
24840 * @param {Tech~SourceObject|Tech~SourceObject[]|string} [source]
24841 * A SourceObject, an array of SourceObjects, or a string referencing
24842 * a URL to a media source. It is _highly recommended_ that an object
24843 * or array of objects is used here, so that source selection
24844 * algorithms can take the `type` into account.
24845 *
24846 * If not provided, this method acts as a getter.
24847 * @param {boolean} [isRetry]
24848 * Indicates whether this is being called internally as a result of a retry
24849 *
24850 * @return {string|undefined}
24851 * If the `source` argument is missing, returns the current source
24852 * URL. Otherwise, returns nothing/undefined.
24853 */
24854 handleSrc_(source, isRetry) {
24855 // getter usage
24856 if (typeof source === 'undefined') {
24857 return this.cache_.src || '';
24858 }
24859
24860 // Reset retry behavior for new source
24861 if (this.resetRetryOnError_) {
24862 this.resetRetryOnError_();
24863 }
24864
24865 // filter out invalid sources and turn our source into
24866 // an array of source objects
24867 const sources = filterSource(source);
24868
24869 // if a source was passed in then it is invalid because
24870 // it was filtered to a zero length Array. So we have to
24871 // show an error
24872 if (!sources.length) {
24873 this.setTimeout(function () {
24874 this.error({
24875 code: 4,
24876 message: this.options_.notSupportedMessage
24877 });
24878 }, 0);
24879 return;
24880 }
24881
24882 // initial sources
24883 this.changingSrc_ = true;
24884
24885 // Only update the cached source list if we are not retrying a new source after error,
24886 // since in that case we want to include the failed source(s) in the cache
24887 if (!isRetry) {
24888 this.cache_.sources = sources;
24889 }
24890 this.updateSourceCaches_(sources[0]);
24891
24892 // middlewareSource is the source after it has been changed by middleware
24893 setSource(this, sources[0], (middlewareSource, mws) => {
24894 this.middleware_ = mws;
24895
24896 // since sourceSet is async we have to update the cache again after we select a source since
24897 // the source that is selected could be out of order from the cache update above this callback.
24898 if (!isRetry) {
24899 this.cache_.sources = sources;
24900 }
24901 this.updateSourceCaches_(middlewareSource);
24902 const err = this.src_(middlewareSource);
24903 if (err) {
24904 if (sources.length > 1) {
24905 return this.handleSrc_(sources.slice(1));
24906 }
24907 this.changingSrc_ = false;
24908
24909 // We need to wrap this in a timeout to give folks a chance to add error event handlers
24910 this.setTimeout(function () {
24911 this.error({
24912 code: 4,
24913 message: this.options_.notSupportedMessage
24914 });
24915 }, 0);
24916
24917 // we could not find an appropriate tech, but let's still notify the delegate that this is it
24918 // this needs a better comment about why this is needed
24919 this.triggerReady();
24920 return;
24921 }
24922 setTech(mws, this.tech_);
24923 });
24924
24925 // Try another available source if this one fails before playback.
24926 if (sources.length > 1) {
24927 const retry = () => {
24928 // Remove the error modal
24929 this.error(null);
24930 this.handleSrc_(sources.slice(1), true);
24931 };
24932 const stopListeningForErrors = () => {
24933 this.off('error', retry);
24934 };
24935 this.one('error', retry);
24936 this.one('playing', stopListeningForErrors);
24937 this.resetRetryOnError_ = () => {
24938 this.off('error', retry);
24939 this.off('playing', stopListeningForErrors);
24940 };
24941 }
24942 }
24943
24944 /**
24945 * Get or set the video source.
24946 *
24947 * @param {Tech~SourceObject|Tech~SourceObject[]|string} [source]
24948 * A SourceObject, an array of SourceObjects, or a string referencing
24949 * a URL to a media source. It is _highly recommended_ that an object
24950 * or array of objects is used here, so that source selection
24951 * algorithms can take the `type` into account.
24952 *
24953 * If not provided, this method acts as a getter.
24954 *
24955 * @return {string|undefined}
24956 * If the `source` argument is missing, returns the current source
24957 * URL. Otherwise, returns nothing/undefined.
24958 */
24959 src(source) {
24960 return this.handleSrc_(source, false);
24961 }
24962
24963 /**
24964 * Set the source object on the tech, returns a boolean that indicates whether
24965 * there is a tech that can play the source or not
24966 *
24967 * @param {Tech~SourceObject} source
24968 * The source object to set on the Tech
24969 *
24970 * @return {boolean}
24971 * - True if there is no Tech to playback this source
24972 * - False otherwise
24973 *
24974 * @private
24975 */
24976 src_(source) {
24977 const sourceTech = this.selectSource([source]);
24978 if (!sourceTech) {
24979 return true;
24980 }
24981 if (!titleCaseEquals(sourceTech.tech, this.techName_)) {
24982 this.changingSrc_ = true;
24983 // load this technology with the chosen source
24984 this.loadTech_(sourceTech.tech, sourceTech.source);
24985 this.tech_.ready(() => {
24986 this.changingSrc_ = false;
24987 });
24988 return false;
24989 }
24990
24991 // wait until the tech is ready to set the source
24992 // and set it synchronously if possible (#2326)
24993 this.ready(function () {
24994 // The setSource tech method was added with source handlers
24995 // so older techs won't support it
24996 // We need to check the direct prototype for the case where subclasses
24997 // of the tech do not support source handlers
24998 if (this.tech_.constructor.prototype.hasOwnProperty('setSource')) {
24999 this.techCall_('setSource', source);
25000 } else {
25001 this.techCall_('src', source.src);
25002 }
25003 this.changingSrc_ = false;
25004 }, true);
25005 return false;
25006 }
25007
25008 /**
25009 * Add a <source> element to the <video> element.
25010 *
25011 * @param {string} srcUrl
25012 * The URL of the video source.
25013 *
25014 * @param {string} [mimeType]
25015 * The MIME type of the video source. Optional but recommended.
25016 *
25017 * @return {boolean}
25018 * Returns true if the source element was successfully added, false otherwise.
25019 */
25020 addSourceElement(srcUrl, mimeType) {
25021 if (!this.tech_) {
25022 return false;
25023 }
25024 return this.tech_.addSourceElement(srcUrl, mimeType);
25025 }
25026
25027 /**
25028 * Remove a <source> element from the <video> element by its URL.
25029 *
25030 * @param {string} srcUrl
25031 * The URL of the source to remove.
25032 *
25033 * @return {boolean}
25034 * Returns true if the source element was successfully removed, false otherwise.
25035 */
25036 removeSourceElement(srcUrl) {
25037 if (!this.tech_) {
25038 return false;
25039 }
25040 return this.tech_.removeSourceElement(srcUrl);
25041 }
25042
25043 /**
25044 * Begin loading the src data.
25045 */
25046 load() {
25047 // Workaround to use the load method with the VHS.
25048 // Does not cover the case when the load method is called directly from the mediaElement.
25049 if (this.tech_ && this.tech_.vhs) {
25050 this.src(this.currentSource());
25051 return;
25052 }
25053 this.techCall_('load');
25054 }
25055
25056 /**
25057 * Reset the player. Loads the first tech in the techOrder,
25058 * removes all the text tracks in the existing `tech`,
25059 * and calls `reset` on the `tech`.
25060 */
25061 reset() {
25062 if (this.paused()) {
25063 this.doReset_();
25064 } else {
25065 const playPromise = this.play();
25066 silencePromise(playPromise.then(() => this.doReset_()));
25067 }
25068 }
25069 doReset_() {
25070 if (this.tech_) {
25071 this.tech_.clearTracks('text');
25072 }
25073 this.removeClass('vjs-playing');
25074 this.addClass('vjs-paused');
25075 this.resetCache_();
25076 this.poster('');
25077 this.loadTech_(this.options_.techOrder[0], null);
25078 this.techCall_('reset');
25079 this.resetControlBarUI_();
25080 this.error(null);
25081 if (this.titleBar) {
25082 this.titleBar.update({
25083 title: undefined,
25084 description: undefined
25085 });
25086 }
25087 if (isEvented(this)) {
25088 this.trigger('playerreset');
25089 }
25090 }
25091
25092 /**
25093 * Reset Control Bar's UI by calling sub-methods that reset
25094 * all of Control Bar's components
25095 */
25096 resetControlBarUI_() {
25097 this.resetProgressBar_();
25098 this.resetPlaybackRate_();
25099 this.resetVolumeBar_();
25100 }
25101
25102 /**
25103 * Reset tech's progress so progress bar is reset in the UI
25104 */
25105 resetProgressBar_() {
25106 this.currentTime(0);
25107 const {
25108 currentTimeDisplay,
25109 durationDisplay,
25110 progressControl,
25111 remainingTimeDisplay
25112 } = this.controlBar || {};
25113 const {
25114 seekBar
25115 } = progressControl || {};
25116 if (currentTimeDisplay) {
25117 currentTimeDisplay.updateContent();
25118 }
25119 if (durationDisplay) {
25120 durationDisplay.updateContent();
25121 }
25122 if (remainingTimeDisplay) {
25123 remainingTimeDisplay.updateContent();
25124 }
25125 if (seekBar) {
25126 seekBar.update();
25127 if (seekBar.loadProgressBar) {
25128 seekBar.loadProgressBar.update();
25129 }
25130 }
25131 }
25132
25133 /**
25134 * Reset Playback ratio
25135 */
25136 resetPlaybackRate_() {
25137 this.playbackRate(this.defaultPlaybackRate());
25138 this.handleTechRateChange_();
25139 }
25140
25141 /**
25142 * Reset Volume bar
25143 */
25144 resetVolumeBar_() {
25145 this.volume(1.0);
25146 this.trigger('volumechange');
25147 }
25148
25149 /**
25150 * Returns all of the current source objects.
25151 *
25152 * @return {Tech~SourceObject[]}
25153 * The current source objects
25154 */
25155 currentSources() {
25156 const source = this.currentSource();
25157 const sources = [];
25158
25159 // assume `{}` or `{ src }`
25160 if (Object.keys(source).length !== 0) {
25161 sources.push(source);
25162 }
25163 return this.cache_.sources || sources;
25164 }
25165
25166 /**
25167 * Returns the current source object.
25168 *
25169 * @return {Tech~SourceObject}
25170 * The current source object
25171 */
25172 currentSource() {
25173 return this.cache_.source || {};
25174 }
25175
25176 /**
25177 * Returns the fully qualified URL of the current source value e.g. http://mysite.com/video.mp4
25178 * Can be used in conjunction with `currentType` to assist in rebuilding the current source object.
25179 *
25180 * @return {string}
25181 * The current source
25182 */
25183 currentSrc() {
25184 return this.currentSource() && this.currentSource().src || '';
25185 }
25186
25187 /**
25188 * Get the current source type e.g. video/mp4
25189 * This can allow you rebuild the current source object so that you could load the same
25190 * source and tech later
25191 *
25192 * @return {string}
25193 * The source MIME type
25194 */
25195 currentType() {
25196 return this.currentSource() && this.currentSource().type || '';
25197 }
25198
25199 /**
25200 * Get or set the preload attribute
25201 *
25202 * @param {'none'|'auto'|'metadata'} [value]
25203 * Preload mode to pass to tech
25204 *
25205 * @return {string|undefined}
25206 * - The preload attribute value when getting
25207 * - Nothing when setting
25208 */
25209 preload(value) {
25210 if (value !== undefined) {
25211 this.techCall_('setPreload', value);
25212 this.options_.preload = value;
25213 return;
25214 }
25215 return this.techGet_('preload');
25216 }
25217
25218 /**
25219 * Get or set the autoplay option. When this is a boolean it will
25220 * modify the attribute on the tech. When this is a string the attribute on
25221 * the tech will be removed and `Player` will handle autoplay on loadstarts.
25222 *
25223 * @param {boolean|'play'|'muted'|'any'} [value]
25224 * - true: autoplay using the browser behavior
25225 * - false: do not autoplay
25226 * - 'play': call play() on every loadstart
25227 * - 'muted': call muted() then play() on every loadstart
25228 * - 'any': call play() on every loadstart. if that fails call muted() then play().
25229 * - *: values other than those listed here will be set `autoplay` to true
25230 *
25231 * @return {boolean|string|undefined}
25232 * - The current value of autoplay when getting
25233 * - Nothing when setting
25234 */
25235 autoplay(value) {
25236 // getter usage
25237 if (value === undefined) {
25238 return this.options_.autoplay || false;
25239 }
25240 let techAutoplay;
25241
25242 // if the value is a valid string set it to that, or normalize `true` to 'play', if need be
25243 if (typeof value === 'string' && /(any|play|muted)/.test(value) || value === true && this.options_.normalizeAutoplay) {
25244 this.options_.autoplay = value;
25245 this.manualAutoplay_(typeof value === 'string' ? value : 'play');
25246 techAutoplay = false;
25247
25248 // any falsy value sets autoplay to false in the browser,
25249 // lets do the same
25250 } else if (!value) {
25251 this.options_.autoplay = false;
25252
25253 // any other value (ie truthy) sets autoplay to true
25254 } else {
25255 this.options_.autoplay = true;
25256 }
25257 techAutoplay = typeof techAutoplay === 'undefined' ? this.options_.autoplay : techAutoplay;
25258
25259 // if we don't have a tech then we do not queue up
25260 // a setAutoplay call on tech ready. We do this because the
25261 // autoplay option will be passed in the constructor and we
25262 // do not need to set it twice
25263 if (this.tech_) {
25264 this.techCall_('setAutoplay', techAutoplay);
25265 }
25266 }
25267
25268 /**
25269 * Set or unset the playsinline attribute.
25270 * Playsinline tells the browser that non-fullscreen playback is preferred.
25271 *
25272 * @param {boolean} [value]
25273 * - true means that we should try to play inline by default
25274 * - false means that we should use the browser's default playback mode,
25275 * which in most cases is inline. iOS Safari is a notable exception
25276 * and plays fullscreen by default.
25277 *
25278 * @return {string|undefined}
25279 * - the current value of playsinline
25280 * - Nothing when setting
25281 *
25282 * @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline}
25283 */
25284 playsinline(value) {
25285 if (value !== undefined) {
25286 this.techCall_('setPlaysinline', value);
25287 this.options_.playsinline = value;
25288 }
25289 return this.techGet_('playsinline');
25290 }
25291
25292 /**
25293 * Get or set the loop attribute on the video element.
25294 *
25295 * @param {boolean} [value]
25296 * - true means that we should loop the video
25297 * - false means that we should not loop the video
25298 *
25299 * @return {boolean|undefined}
25300 * - The current value of loop when getting
25301 * - Nothing when setting
25302 */
25303 loop(value) {
25304 if (value !== undefined) {
25305 this.techCall_('setLoop', value);
25306 this.options_.loop = value;
25307 return;
25308 }
25309 return this.techGet_('loop');
25310 }
25311
25312 /**
25313 * Get or set the poster image source url
25314 *
25315 * @fires Player#posterchange
25316 *
25317 * @param {string} [src]
25318 * Poster image source URL
25319 *
25320 * @return {string|undefined}
25321 * - The current value of poster when getting
25322 * - Nothing when setting
25323 */
25324 poster(src) {
25325 if (src === undefined) {
25326 return this.poster_;
25327 }
25328
25329 // The correct way to remove a poster is to set as an empty string
25330 // other falsey values will throw errors
25331 if (!src) {
25332 src = '';
25333 }
25334 if (src === this.poster_) {
25335 return;
25336 }
25337
25338 // update the internal poster variable
25339 this.poster_ = src;
25340
25341 // update the tech's poster
25342 this.techCall_('setPoster', src);
25343 this.isPosterFromTech_ = false;
25344
25345 // alert components that the poster has been set
25346 /**
25347 * This event fires when the poster image is changed on the player.
25348 *
25349 * @event Player#posterchange
25350 * @type {Event}
25351 */
25352 this.trigger('posterchange');
25353 }
25354
25355 /**
25356 * Some techs (e.g. YouTube) can provide a poster source in an
25357 * asynchronous way. We want the poster component to use this
25358 * poster source so that it covers up the tech's controls.
25359 * (YouTube's play button). However we only want to use this
25360 * source if the player user hasn't set a poster through
25361 * the normal APIs.
25362 *
25363 * @fires Player#posterchange
25364 * @listens Tech#posterchange
25365 * @private
25366 */
25367 handleTechPosterChange_() {
25368 if ((!this.poster_ || this.options_.techCanOverridePoster) && this.tech_ && this.tech_.poster) {
25369 const newPoster = this.tech_.poster() || '';
25370 if (newPoster !== this.poster_) {
25371 this.poster_ = newPoster;
25372 this.isPosterFromTech_ = true;
25373
25374 // Let components know the poster has changed
25375 this.trigger('posterchange');
25376 }
25377 }
25378 }
25379
25380 /**
25381 * Get or set whether or not the controls are showing.
25382 *
25383 * @fires Player#controlsenabled
25384 *
25385 * @param {boolean} [bool]
25386 * - true to turn controls on
25387 * - false to turn controls off
25388 *
25389 * @return {boolean|undefined}
25390 * - The current value of controls when getting
25391 * - Nothing when setting
25392 */
25393 controls(bool) {
25394 if (bool === undefined) {
25395 return !!this.controls_;
25396 }
25397 bool = !!bool;
25398
25399 // Don't trigger a change event unless it actually changed
25400 if (this.controls_ === bool) {
25401 return;
25402 }
25403 this.controls_ = bool;
25404 if (this.usingNativeControls()) {
25405 this.techCall_('setControls', bool);
25406 }
25407 if (this.controls_) {
25408 this.removeClass('vjs-controls-disabled');
25409 this.addClass('vjs-controls-enabled');
25410 /**
25411 * @event Player#controlsenabled
25412 * @type {Event}
25413 */
25414 this.trigger('controlsenabled');
25415 if (!this.usingNativeControls()) {
25416 this.addTechControlsListeners_();
25417 }
25418 } else {
25419 this.removeClass('vjs-controls-enabled');
25420 this.addClass('vjs-controls-disabled');
25421 /**
25422 * @event Player#controlsdisabled
25423 * @type {Event}
25424 */
25425 this.trigger('controlsdisabled');
25426 if (!this.usingNativeControls()) {
25427 this.removeTechControlsListeners_();
25428 }
25429 }
25430 }
25431
25432 /**
25433 * Toggle native controls on/off. Native controls are the controls built into
25434 * devices (e.g. default iPhone controls) or other techs
25435 * (e.g. Vimeo Controls)
25436 * **This should only be set by the current tech, because only the tech knows
25437 * if it can support native controls**
25438 *
25439 * @fires Player#usingnativecontrols
25440 * @fires Player#usingcustomcontrols
25441 *
25442 * @param {boolean} [bool]
25443 * - true to turn native controls on
25444 * - false to turn native controls off
25445 *
25446 * @return {boolean|undefined}
25447 * - The current value of native controls when getting
25448 * - Nothing when setting
25449 */
25450 usingNativeControls(bool) {
25451 if (bool === undefined) {
25452 return !!this.usingNativeControls_;
25453 }
25454 bool = !!bool;
25455
25456 // Don't trigger a change event unless it actually changed
25457 if (this.usingNativeControls_ === bool) {
25458 return;
25459 }
25460 this.usingNativeControls_ = bool;
25461 if (this.usingNativeControls_) {
25462 this.addClass('vjs-using-native-controls');
25463
25464 /**
25465 * player is using the native device controls
25466 *
25467 * @event Player#usingnativecontrols
25468 * @type {Event}
25469 */
25470 this.trigger('usingnativecontrols');
25471 } else {
25472 this.removeClass('vjs-using-native-controls');
25473
25474 /**
25475 * player is using the custom HTML controls
25476 *
25477 * @event Player#usingcustomcontrols
25478 * @type {Event}
25479 */
25480 this.trigger('usingcustomcontrols');
25481 }
25482 }
25483
25484 /**
25485 * Set or get the current MediaError
25486 *
25487 * @fires Player#error
25488 *
25489 * @param {MediaError|string|number} [err]
25490 * A MediaError or a string/number to be turned
25491 * into a MediaError
25492 *
25493 * @return {MediaError|null|undefined}
25494 * - The current MediaError when getting (or null)
25495 * - Nothing when setting
25496 */
25497 error(err) {
25498 if (err === undefined) {
25499 return this.error_ || null;
25500 }
25501
25502 // allow hooks to modify error object
25503 hooks('beforeerror').forEach(hookFunction => {
25504 const newErr = hookFunction(this, err);
25505 if (!(isObject(newErr) && !Array.isArray(newErr) || typeof newErr === 'string' || typeof newErr === 'number' || newErr === null)) {
25506 this.log.error('please return a value that MediaError expects in beforeerror hooks');
25507 return;
25508 }
25509 err = newErr;
25510 });
25511
25512 // Suppress the first error message for no compatible source until
25513 // user interaction
25514 if (this.options_.suppressNotSupportedError && err && err.code === 4) {
25515 const triggerSuppressedError = function () {
25516 this.error(err);
25517 };
25518 this.options_.suppressNotSupportedError = false;
25519 this.any(['click', 'touchstart'], triggerSuppressedError);
25520 this.one('loadstart', function () {
25521 this.off(['click', 'touchstart'], triggerSuppressedError);
25522 });
25523 return;
25524 }
25525
25526 // restoring to default
25527 if (err === null) {
25528 this.error_ = null;
25529 this.removeClass('vjs-error');
25530 if (this.errorDisplay) {
25531 this.errorDisplay.close();
25532 }
25533 return;
25534 }
25535 this.error_ = new MediaError(err);
25536
25537 // add the vjs-error classname to the player
25538 this.addClass('vjs-error');
25539
25540 // log the name of the error type and any message
25541 // IE11 logs "[object object]" and required you to expand message to see error object
25542 log.error(`(CODE:${this.error_.code} ${MediaError.errorTypes[this.error_.code]})`, this.error_.message, this.error_);
25543
25544 /**
25545 * @event Player#error
25546 * @type {Event}
25547 */
25548 this.trigger('error');
25549
25550 // notify hooks of the per player error
25551 hooks('error').forEach(hookFunction => hookFunction(this, this.error_));
25552 return;
25553 }
25554
25555 /**
25556 * Report user activity
25557 *
25558 * @param {Object} event
25559 * Event object
25560 */
25561 reportUserActivity(event) {
25562 this.userActivity_ = true;
25563 }
25564
25565 /**
25566 * Get/set if user is active
25567 *
25568 * @fires Player#useractive
25569 * @fires Player#userinactive
25570 *
25571 * @param {boolean} [bool]
25572 * - true if the user is active
25573 * - false if the user is inactive
25574 *
25575 * @return {boolean|undefined}
25576 * - The current value of userActive when getting
25577 * - Nothing when setting
25578 */
25579 userActive(bool) {
25580 if (bool === undefined) {
25581 return this.userActive_;
25582 }
25583 bool = !!bool;
25584 if (bool === this.userActive_) {
25585 return;
25586 }
25587 this.userActive_ = bool;
25588 if (this.userActive_) {
25589 this.userActivity_ = true;
25590 this.removeClass('vjs-user-inactive');
25591 this.addClass('vjs-user-active');
25592 /**
25593 * @event Player#useractive
25594 * @type {Event}
25595 */
25596 this.trigger('useractive');
25597 return;
25598 }
25599
25600 // Chrome/Safari/IE have bugs where when you change the cursor it can
25601 // trigger a mousemove event. This causes an issue when you're hiding
25602 // the cursor when the user is inactive, and a mousemove signals user
25603 // activity. Making it impossible to go into inactive mode. Specifically
25604 // this happens in fullscreen when we really need to hide the cursor.
25605 //
25606 // When this gets resolved in ALL browsers it can be removed
25607 // https://code.google.com/p/chromium/issues/detail?id=103041
25608 if (this.tech_) {
25609 this.tech_.one('mousemove', function (e) {
25610 e.stopPropagation();
25611 e.preventDefault();
25612 });
25613 }
25614 this.userActivity_ = false;
25615 this.removeClass('vjs-user-active');
25616 this.addClass('vjs-user-inactive');
25617 /**
25618 * @event Player#userinactive
25619 * @type {Event}
25620 */
25621 this.trigger('userinactive');
25622 }
25623
25624 /**
25625 * Listen for user activity based on timeout value
25626 *
25627 * @private
25628 */
25629 listenForUserActivity_() {
25630 let mouseInProgress;
25631 let lastMoveX;
25632 let lastMoveY;
25633 const handleActivity = bind_(this, this.reportUserActivity);
25634 const handleMouseMove = function (e) {
25635 // #1068 - Prevent mousemove spamming
25636 // Chrome Bug: https://code.google.com/p/chromium/issues/detail?id=366970
25637 if (e.screenX !== lastMoveX || e.screenY !== lastMoveY) {
25638 lastMoveX = e.screenX;
25639 lastMoveY = e.screenY;
25640 handleActivity();
25641 }
25642 };
25643 const handleMouseDown = function () {
25644 handleActivity();
25645 // For as long as the they are touching the device or have their mouse down,
25646 // we consider them active even if they're not moving their finger or mouse.
25647 // So we want to continue to update that they are active
25648 this.clearInterval(mouseInProgress);
25649 // Setting userActivity=true now and setting the interval to the same time
25650 // as the activityCheck interval (250) should ensure we never miss the
25651 // next activityCheck
25652 mouseInProgress = this.setInterval(handleActivity, 250);
25653 };
25654 const handleMouseUpAndMouseLeave = function (event) {
25655 handleActivity();
25656 // Stop the interval that maintains activity if the mouse/touch is down
25657 this.clearInterval(mouseInProgress);
25658 };
25659
25660 // Any mouse movement will be considered user activity
25661 this.on('mousedown', handleMouseDown);
25662 this.on('mousemove', handleMouseMove);
25663 this.on('mouseup', handleMouseUpAndMouseLeave);
25664 this.on('mouseleave', handleMouseUpAndMouseLeave);
25665 const controlBar = this.getChild('controlBar');
25666
25667 // Fixes bug on Android & iOS where when tapping progressBar (when control bar is displayed)
25668 // controlBar would no longer be hidden by default timeout.
25669 if (controlBar && !IS_IOS && !IS_ANDROID) {
25670 controlBar.on('mouseenter', function (event) {
25671 if (this.player().options_.inactivityTimeout !== 0) {
25672 this.player().cache_.inactivityTimeout = this.player().options_.inactivityTimeout;
25673 }
25674 this.player().options_.inactivityTimeout = 0;
25675 });
25676 controlBar.on('mouseleave', function (event) {
25677 this.player().options_.inactivityTimeout = this.player().cache_.inactivityTimeout;
25678 });
25679 }
25680
25681 // Listen for keyboard navigation
25682 // Shouldn't need to use inProgress interval because of key repeat
25683 this.on('keydown', handleActivity);
25684 this.on('keyup', handleActivity);
25685
25686 // Run an interval every 250 milliseconds instead of stuffing everything into
25687 // the mousemove/touchmove function itself, to prevent performance degradation.
25688 // `this.reportUserActivity` simply sets this.userActivity_ to true, which
25689 // then gets picked up by this loop
25690 // http://ejohn.org/blog/learning-from-twitter/
25691 let inactivityTimeout;
25692
25693 /** @this Player */
25694 const activityCheck = function () {
25695 // Check to see if mouse/touch activity has happened
25696 if (!this.userActivity_) {
25697 return;
25698 }
25699
25700 // Reset the activity tracker
25701 this.userActivity_ = false;
25702
25703 // If the user state was inactive, set the state to active
25704 this.userActive(true);
25705
25706 // Clear any existing inactivity timeout to start the timer over
25707 this.clearTimeout(inactivityTimeout);
25708 const timeout = this.options_.inactivityTimeout;
25709 if (timeout <= 0) {
25710 return;
25711 }
25712
25713 // In <timeout> milliseconds, if no more activity has occurred the
25714 // user will be considered inactive
25715 inactivityTimeout = this.setTimeout(function () {
25716 // Protect against the case where the inactivityTimeout can trigger just
25717 // before the next user activity is picked up by the activity check loop
25718 // causing a flicker
25719 if (!this.userActivity_) {
25720 this.userActive(false);
25721 }
25722 }, timeout);
25723 };
25724 this.setInterval(activityCheck, 250);
25725 }
25726
25727 /**
25728 * Gets or sets the current playback rate. A playback rate of
25729 * 1.0 represents normal speed and 0.5 would indicate half-speed
25730 * playback, for instance.
25731 *
25732 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-playbackrate
25733 *
25734 * @param {number} [rate]
25735 * New playback rate to set.
25736 *
25737 * @return {number|undefined}
25738 * - The current playback rate when getting or 1.0
25739 * - Nothing when setting
25740 */
25741 playbackRate(rate) {
25742 if (rate !== undefined) {
25743 // NOTE: this.cache_.lastPlaybackRate is set from the tech handler
25744 // that is registered above
25745 this.techCall_('setPlaybackRate', rate);
25746 return;
25747 }
25748 if (this.tech_ && this.tech_.featuresPlaybackRate) {
25749 return this.cache_.lastPlaybackRate || this.techGet_('playbackRate');
25750 }
25751 return 1.0;
25752 }
25753
25754 /**
25755 * Gets or sets the current default playback rate. A default playback rate of
25756 * 1.0 represents normal speed and 0.5 would indicate half-speed playback, for instance.
25757 * defaultPlaybackRate will only represent what the initial playbackRate of a video was, not
25758 * not the current playbackRate.
25759 *
25760 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-defaultplaybackrate
25761 *
25762 * @param {number} [rate]
25763 * New default playback rate to set.
25764 *
25765 * @return {number|undefined}
25766 * - The default playback rate when getting or 1.0
25767 * - Nothing when setting
25768 */
25769 defaultPlaybackRate(rate) {
25770 if (rate !== undefined) {
25771 return this.techCall_('setDefaultPlaybackRate', rate);
25772 }
25773 if (this.tech_ && this.tech_.featuresPlaybackRate) {
25774 return this.techGet_('defaultPlaybackRate');
25775 }
25776 return 1.0;
25777 }
25778
25779 /**
25780 * Gets or sets the audio flag
25781 *
25782 * @param {boolean} [bool]
25783 * - true signals that this is an audio player
25784 * - false signals that this is not an audio player
25785 *
25786 * @return {boolean|undefined}
25787 * - The current value of isAudio when getting
25788 * - Nothing when setting
25789 */
25790 isAudio(bool) {
25791 if (bool !== undefined) {
25792 this.isAudio_ = !!bool;
25793 return;
25794 }
25795 return !!this.isAudio_;
25796 }
25797 updatePlayerHeightOnAudioOnlyMode_() {
25798 const controlBar = this.getChild('ControlBar');
25799 if (!controlBar || this.audioOnlyCache_.controlBarHeight === controlBar.currentHeight()) {
25800 return;
25801 }
25802 this.audioOnlyCache_.controlBarHeight = controlBar.currentHeight();
25803 this.height(this.audioOnlyCache_.controlBarHeight);
25804 }
25805 enableAudioOnlyUI_() {
25806 // Update styling immediately to show the control bar so we can get its height
25807 this.addClass('vjs-audio-only-mode');
25808 const playerChildren = this.children();
25809 const controlBar = this.getChild('ControlBar');
25810 const controlBarHeight = controlBar && controlBar.currentHeight();
25811
25812 // Hide all player components except the control bar. Control bar components
25813 // needed only for video are hidden with CSS
25814 playerChildren.forEach(child => {
25815 if (child === controlBar) {
25816 return;
25817 }
25818 if (child.el_ && !child.hasClass('vjs-hidden')) {
25819 child.hide();
25820 this.audioOnlyCache_.hiddenChildren.push(child);
25821 }
25822 });
25823 this.audioOnlyCache_.playerHeight = this.currentHeight();
25824 this.audioOnlyCache_.controlBarHeight = controlBarHeight;
25825 this.on('playerresize', this.boundUpdatePlayerHeightOnAudioOnlyMode_);
25826
25827 // Set the player height the same as the control bar
25828 this.height(controlBarHeight);
25829 this.trigger('audioonlymodechange');
25830 }
25831 disableAudioOnlyUI_() {
25832 this.removeClass('vjs-audio-only-mode');
25833 this.off('playerresize', this.boundUpdatePlayerHeightOnAudioOnlyMode_);
25834
25835 // Show player components that were previously hidden
25836 this.audioOnlyCache_.hiddenChildren.forEach(child => child.show());
25837
25838 // Reset player height
25839 this.height(this.audioOnlyCache_.playerHeight);
25840 this.trigger('audioonlymodechange');
25841 }
25842
25843 /**
25844 * Get the current audioOnlyMode state or set audioOnlyMode to true or false.
25845 *
25846 * Setting this to `true` will hide all player components except the control bar,
25847 * as well as control bar components needed only for video.
25848 *
25849 * @param {boolean} [value]
25850 * The value to set audioOnlyMode to.
25851 *
25852 * @return {Promise|boolean}
25853 * A Promise is returned when setting the state, and a boolean when getting
25854 * the present state
25855 */
25856 audioOnlyMode(value) {
25857 if (typeof value !== 'boolean' || value === this.audioOnlyMode_) {
25858 return this.audioOnlyMode_;
25859 }
25860 this.audioOnlyMode_ = value;
25861
25862 // Enable Audio Only Mode
25863 if (value) {
25864 const exitPromises = [];
25865
25866 // Fullscreen and PiP are not supported in audioOnlyMode, so exit if we need to.
25867 if (this.isInPictureInPicture()) {
25868 exitPromises.push(this.exitPictureInPicture());
25869 }
25870 if (this.isFullscreen()) {
25871 exitPromises.push(this.exitFullscreen());
25872 }
25873 if (this.audioPosterMode()) {
25874 exitPromises.push(this.audioPosterMode(false));
25875 }
25876 return Promise.all(exitPromises).then(() => this.enableAudioOnlyUI_());
25877 }
25878
25879 // Disable Audio Only Mode
25880 return Promise.resolve().then(() => this.disableAudioOnlyUI_());
25881 }
25882 enablePosterModeUI_() {
25883 // Hide the video element and show the poster image to enable posterModeUI
25884 const tech = this.tech_ && this.tech_;
25885 tech.hide();
25886 this.addClass('vjs-audio-poster-mode');
25887 this.trigger('audiopostermodechange');
25888 }
25889 disablePosterModeUI_() {
25890 // Show the video element and hide the poster image to disable posterModeUI
25891 const tech = this.tech_ && this.tech_;
25892 tech.show();
25893 this.removeClass('vjs-audio-poster-mode');
25894 this.trigger('audiopostermodechange');
25895 }
25896
25897 /**
25898 * Get the current audioPosterMode state or set audioPosterMode to true or false
25899 *
25900 * @param {boolean} [value]
25901 * The value to set audioPosterMode to.
25902 *
25903 * @return {Promise|boolean}
25904 * A Promise is returned when setting the state, and a boolean when getting
25905 * the present state
25906 */
25907 audioPosterMode(value) {
25908 if (typeof value !== 'boolean' || value === this.audioPosterMode_) {
25909 return this.audioPosterMode_;
25910 }
25911 this.audioPosterMode_ = value;
25912 if (value) {
25913 if (this.audioOnlyMode()) {
25914 const audioOnlyModePromise = this.audioOnlyMode(false);
25915 return audioOnlyModePromise.then(() => {
25916 // enable audio poster mode after audio only mode is disabled
25917 this.enablePosterModeUI_();
25918 });
25919 }
25920 return Promise.resolve().then(() => {
25921 // enable audio poster mode
25922 this.enablePosterModeUI_();
25923 });
25924 }
25925 return Promise.resolve().then(() => {
25926 // disable audio poster mode
25927 this.disablePosterModeUI_();
25928 });
25929 }
25930
25931 /**
25932 * A helper method for adding a {@link TextTrack} to our
25933 * {@link TextTrackList}.
25934 *
25935 * In addition to the W3C settings we allow adding additional info through options.
25936 *
25937 * @see http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-addtexttrack
25938 *
25939 * @param {string} [kind]
25940 * the kind of TextTrack you are adding
25941 *
25942 * @param {string} [label]
25943 * the label to give the TextTrack label
25944 *
25945 * @param {string} [language]
25946 * the language to set on the TextTrack
25947 *
25948 * @return {TextTrack|undefined}
25949 * the TextTrack that was added or undefined
25950 * if there is no tech
25951 */
25952 addTextTrack(kind, label, language) {
25953 if (this.tech_) {
25954 return this.tech_.addTextTrack(kind, label, language);
25955 }
25956 }
25957
25958 /**
25959 * Create a remote {@link TextTrack} and an {@link HTMLTrackElement}.
25960 *
25961 * @param {Object} options
25962 * Options to pass to {@link HTMLTrackElement} during creation. See
25963 * {@link HTMLTrackElement} for object properties that you should use.
25964 *
25965 * @param {boolean} [manualCleanup=false] if set to true, the TextTrack will not be removed
25966 * from the TextTrackList and HtmlTrackElementList
25967 * after a source change
25968 *
25969 * @return {HtmlTrackElement}
25970 * the HTMLTrackElement that was created and added
25971 * to the HtmlTrackElementList and the remote
25972 * TextTrackList
25973 *
25974 */
25975 addRemoteTextTrack(options, manualCleanup) {
25976 if (this.tech_) {
25977 return this.tech_.addRemoteTextTrack(options, manualCleanup);
25978 }
25979 }
25980
25981 /**
25982 * Remove a remote {@link TextTrack} from the respective
25983 * {@link TextTrackList} and {@link HtmlTrackElementList}.
25984 *
25985 * @param {Object} track
25986 * Remote {@link TextTrack} to remove
25987 *
25988 * @return {undefined}
25989 * does not return anything
25990 */
25991 removeRemoteTextTrack(obj = {}) {
25992 let {
25993 track
25994 } = obj;
25995 if (!track) {
25996 track = obj;
25997 }
25998
25999 // destructure the input into an object with a track argument, defaulting to arguments[0]
26000 // default the whole argument to an empty object if nothing was passed in
26001
26002 if (this.tech_) {
26003 return this.tech_.removeRemoteTextTrack(track);
26004 }
26005 }
26006
26007 /**
26008 * Gets available media playback quality metrics as specified by the W3C's Media
26009 * Playback Quality API.
26010 *
26011 * @see [Spec]{@link https://wicg.github.io/media-playback-quality}
26012 *
26013 * @return {Object|undefined}
26014 * An object with supported media playback quality metrics or undefined if there
26015 * is no tech or the tech does not support it.
26016 */
26017 getVideoPlaybackQuality() {
26018 return this.techGet_('getVideoPlaybackQuality');
26019 }
26020
26021 /**
26022 * Get video width
26023 *
26024 * @return {number}
26025 * current video width
26026 */
26027 videoWidth() {
26028 return this.tech_ && this.tech_.videoWidth && this.tech_.videoWidth() || 0;
26029 }
26030
26031 /**
26032 * Get video height
26033 *
26034 * @return {number}
26035 * current video height
26036 */
26037 videoHeight() {
26038 return this.tech_ && this.tech_.videoHeight && this.tech_.videoHeight() || 0;
26039 }
26040
26041 /**
26042 * Set or get the player's language code.
26043 *
26044 * Changing the language will trigger
26045 * [languagechange]{@link Player#event:languagechange}
26046 * which Components can use to update control text.
26047 * ClickableComponent will update its control text by default on
26048 * [languagechange]{@link Player#event:languagechange}.
26049 *
26050 * @fires Player#languagechange
26051 *
26052 * @param {string} [code]
26053 * the language code to set the player to
26054 *
26055 * @return {string|undefined}
26056 * - The current language code when getting
26057 * - Nothing when setting
26058 */
26059 language(code) {
26060 if (code === undefined) {
26061 return this.language_;
26062 }
26063 if (this.language_ !== String(code).toLowerCase()) {
26064 this.language_ = String(code).toLowerCase();
26065
26066 // during first init, it's possible some things won't be evented
26067 if (isEvented(this)) {
26068 /**
26069 * fires when the player language change
26070 *
26071 * @event Player#languagechange
26072 * @type {Event}
26073 */
26074 this.trigger('languagechange');
26075 }
26076 }
26077 }
26078
26079 /**
26080 * Get the player's language dictionary
26081 * Merge every time, because a newly added plugin might call videojs.addLanguage() at any time
26082 * Languages specified directly in the player options have precedence
26083 *
26084 * @return {Array}
26085 * An array of of supported languages
26086 */
26087 languages() {
26088 return merge(Player.prototype.options_.languages, this.languages_);
26089 }
26090
26091 /**
26092 * returns a JavaScript object representing the current track
26093 * information. **DOES not return it as JSON**
26094 *
26095 * @return {Object}
26096 * Object representing the current of track info
26097 */
26098 toJSON() {
26099 const options = merge(this.options_);
26100 const tracks = options.tracks;
26101 options.tracks = [];
26102 for (let i = 0; i < tracks.length; i++) {
26103 let track = tracks[i];
26104
26105 // deep merge tracks and null out player so no circular references
26106 track = merge(track);
26107 track.player = undefined;
26108 options.tracks[i] = track;
26109 }
26110 return options;
26111 }
26112
26113 /**
26114 * Creates a simple modal dialog (an instance of the {@link ModalDialog}
26115 * component) that immediately overlays the player with arbitrary
26116 * content and removes itself when closed.
26117 *
26118 * @param {string|Function|Element|Array|null} content
26119 * Same as {@link ModalDialog#content}'s param of the same name.
26120 * The most straight-forward usage is to provide a string or DOM
26121 * element.
26122 *
26123 * @param {Object} [options]
26124 * Extra options which will be passed on to the {@link ModalDialog}.
26125 *
26126 * @return {ModalDialog}
26127 * the {@link ModalDialog} that was created
26128 */
26129 createModal(content, options) {
26130 options = options || {};
26131 options.content = content || '';
26132 const modal = new ModalDialog(this, options);
26133 this.addChild(modal);
26134 modal.on('dispose', () => {
26135 this.removeChild(modal);
26136 });
26137 modal.open();
26138 return modal;
26139 }
26140
26141 /**
26142 * Change breakpoint classes when the player resizes.
26143 *
26144 * @private
26145 */
26146 updateCurrentBreakpoint_() {
26147 if (!this.responsive()) {
26148 return;
26149 }
26150 const currentBreakpoint = this.currentBreakpoint();
26151 const currentWidth = this.currentWidth();
26152 for (let i = 0; i < BREAKPOINT_ORDER.length; i++) {
26153 const candidateBreakpoint = BREAKPOINT_ORDER[i];
26154 const maxWidth = this.breakpoints_[candidateBreakpoint];
26155 if (currentWidth <= maxWidth) {
26156 // The current breakpoint did not change, nothing to do.
26157 if (currentBreakpoint === candidateBreakpoint) {
26158 return;
26159 }
26160
26161 // Only remove a class if there is a current breakpoint.
26162 if (currentBreakpoint) {
26163 this.removeClass(BREAKPOINT_CLASSES[currentBreakpoint]);
26164 }
26165 this.addClass(BREAKPOINT_CLASSES[candidateBreakpoint]);
26166 this.breakpoint_ = candidateBreakpoint;
26167 break;
26168 }
26169 }
26170 }
26171
26172 /**
26173 * Removes the current breakpoint.
26174 *
26175 * @private
26176 */
26177 removeCurrentBreakpoint_() {
26178 const className = this.currentBreakpointClass();
26179 this.breakpoint_ = '';
26180 if (className) {
26181 this.removeClass(className);
26182 }
26183 }
26184
26185 /**
26186 * Get or set breakpoints on the player.
26187 *
26188 * Calling this method with an object or `true` will remove any previous
26189 * custom breakpoints and start from the defaults again.
26190 *
26191 * @param {Object|boolean} [breakpoints]
26192 * If an object is given, it can be used to provide custom
26193 * breakpoints. If `true` is given, will set default breakpoints.
26194 * If this argument is not given, will simply return the current
26195 * breakpoints.
26196 *
26197 * @param {number} [breakpoints.tiny]
26198 * The maximum width for the "vjs-layout-tiny" class.
26199 *
26200 * @param {number} [breakpoints.xsmall]
26201 * The maximum width for the "vjs-layout-x-small" class.
26202 *
26203 * @param {number} [breakpoints.small]
26204 * The maximum width for the "vjs-layout-small" class.
26205 *
26206 * @param {number} [breakpoints.medium]
26207 * The maximum width for the "vjs-layout-medium" class.
26208 *
26209 * @param {number} [breakpoints.large]
26210 * The maximum width for the "vjs-layout-large" class.
26211 *
26212 * @param {number} [breakpoints.xlarge]
26213 * The maximum width for the "vjs-layout-x-large" class.
26214 *
26215 * @param {number} [breakpoints.huge]
26216 * The maximum width for the "vjs-layout-huge" class.
26217 *
26218 * @return {Object}
26219 * An object mapping breakpoint names to maximum width values.
26220 */
26221 breakpoints(breakpoints) {
26222 // Used as a getter.
26223 if (breakpoints === undefined) {
26224 return Object.assign(this.breakpoints_);
26225 }
26226 this.breakpoint_ = '';
26227 this.breakpoints_ = Object.assign({}, DEFAULT_BREAKPOINTS, breakpoints);
26228
26229 // When breakpoint definitions change, we need to update the currently
26230 // selected breakpoint.
26231 this.updateCurrentBreakpoint_();
26232
26233 // Clone the breakpoints before returning.
26234 return Object.assign(this.breakpoints_);
26235 }
26236
26237 /**
26238 * Get or set a flag indicating whether or not this player should adjust
26239 * its UI based on its dimensions.
26240 *
26241 * @param {boolean} [value]
26242 * Should be `true` if the player should adjust its UI based on its
26243 * dimensions; otherwise, should be `false`.
26244 *
26245 * @return {boolean|undefined}
26246 * Will be `true` if this player should adjust its UI based on its
26247 * dimensions; otherwise, will be `false`.
26248 * Nothing if setting
26249 */
26250 responsive(value) {
26251 // Used as a getter.
26252 if (value === undefined) {
26253 return this.responsive_;
26254 }
26255 value = Boolean(value);
26256 const current = this.responsive_;
26257
26258 // Nothing changed.
26259 if (value === current) {
26260 return;
26261 }
26262
26263 // The value actually changed, set it.
26264 this.responsive_ = value;
26265
26266 // Start listening for breakpoints and set the initial breakpoint if the
26267 // player is now responsive.
26268 if (value) {
26269 this.on('playerresize', this.boundUpdateCurrentBreakpoint_);
26270 this.updateCurrentBreakpoint_();
26271
26272 // Stop listening for breakpoints if the player is no longer responsive.
26273 } else {
26274 this.off('playerresize', this.boundUpdateCurrentBreakpoint_);
26275 this.removeCurrentBreakpoint_();
26276 }
26277 return value;
26278 }
26279
26280 /**
26281 * Get current breakpoint name, if any.
26282 *
26283 * @return {string}
26284 * If there is currently a breakpoint set, returns a the key from the
26285 * breakpoints object matching it. Otherwise, returns an empty string.
26286 */
26287 currentBreakpoint() {
26288 return this.breakpoint_;
26289 }
26290
26291 /**
26292 * Get the current breakpoint class name.
26293 *
26294 * @return {string}
26295 * The matching class name (e.g. `"vjs-layout-tiny"` or
26296 * `"vjs-layout-large"`) for the current breakpoint. Empty string if
26297 * there is no current breakpoint.
26298 */
26299 currentBreakpointClass() {
26300 return BREAKPOINT_CLASSES[this.breakpoint_] || '';
26301 }
26302
26303 /**
26304 * An object that describes a single piece of media.
26305 *
26306 * Properties that are not part of this type description will be retained; so,
26307 * this can be viewed as a generic metadata storage mechanism as well.
26308 *
26309 * @see {@link https://wicg.github.io/mediasession/#the-mediametadata-interface}
26310 * @typedef {Object} Player~MediaObject
26311 *
26312 * @property {string} [album]
26313 * Unused, except if this object is passed to the `MediaSession`
26314 * API.
26315 *
26316 * @property {string} [artist]
26317 * Unused, except if this object is passed to the `MediaSession`
26318 * API.
26319 *
26320 * @property {Object[]} [artwork]
26321 * Unused, except if this object is passed to the `MediaSession`
26322 * API. If not specified, will be populated via the `poster`, if
26323 * available.
26324 *
26325 * @property {string} [poster]
26326 * URL to an image that will display before playback.
26327 *
26328 * @property {Tech~SourceObject|Tech~SourceObject[]|string} [src]
26329 * A single source object, an array of source objects, or a string
26330 * referencing a URL to a media source. It is _highly recommended_
26331 * that an object or array of objects is used here, so that source
26332 * selection algorithms can take the `type` into account.
26333 *
26334 * @property {string} [title]
26335 * Unused, except if this object is passed to the `MediaSession`
26336 * API.
26337 *
26338 * @property {Object[]} [textTracks]
26339 * An array of objects to be used to create text tracks, following
26340 * the {@link https://www.w3.org/TR/html50/embedded-content-0.html#the-track-element|native track element format}.
26341 * For ease of removal, these will be created as "remote" text
26342 * tracks and set to automatically clean up on source changes.
26343 *
26344 * These objects may have properties like `src`, `kind`, `label`,
26345 * and `language`, see {@link Tech#createRemoteTextTrack}.
26346 */
26347
26348 /**
26349 * Populate the player using a {@link Player~MediaObject|MediaObject}.
26350 *
26351 * @param {Player~MediaObject} media
26352 * A media object.
26353 *
26354 * @param {Function} ready
26355 * A callback to be called when the player is ready.
26356 */
26357 loadMedia(media, ready) {
26358 if (!media || typeof media !== 'object') {
26359 return;
26360 }
26361 const crossOrigin = this.crossOrigin();
26362 this.reset();
26363
26364 // Clone the media object so it cannot be mutated from outside.
26365 this.cache_.media = merge(media);
26366 const {
26367 artist,
26368 artwork,
26369 description,
26370 poster,
26371 src,
26372 textTracks,
26373 title
26374 } = this.cache_.media;
26375
26376 // If `artwork` is not given, create it using `poster`.
26377 if (!artwork && poster) {
26378 this.cache_.media.artwork = [{
26379 src: poster,
26380 type: getMimetype(poster)
26381 }];
26382 }
26383 if (crossOrigin) {
26384 this.crossOrigin(crossOrigin);
26385 }
26386 if (src) {
26387 this.src(src);
26388 }
26389 if (poster) {
26390 this.poster(poster);
26391 }
26392 if (Array.isArray(textTracks)) {
26393 textTracks.forEach(tt => this.addRemoteTextTrack(tt, false));
26394 }
26395 if (this.titleBar) {
26396 this.titleBar.update({
26397 title,
26398 description: description || artist || ''
26399 });
26400 }
26401 this.ready(ready);
26402 }
26403
26404 /**
26405 * Get a clone of the current {@link Player~MediaObject} for this player.
26406 *
26407 * If the `loadMedia` method has not been used, will attempt to return a
26408 * {@link Player~MediaObject} based on the current state of the player.
26409 *
26410 * @return {Player~MediaObject}
26411 */
26412 getMedia() {
26413 if (!this.cache_.media) {
26414 const poster = this.poster();
26415 const src = this.currentSources();
26416 const textTracks = Array.prototype.map.call(this.remoteTextTracks(), tt => ({
26417 kind: tt.kind,
26418 label: tt.label,
26419 language: tt.language,
26420 src: tt.src
26421 }));
26422 const media = {
26423 src,
26424 textTracks
26425 };
26426 if (poster) {
26427 media.poster = poster;
26428 media.artwork = [{
26429 src: media.poster,
26430 type: getMimetype(media.poster)
26431 }];
26432 }
26433 return media;
26434 }
26435 return merge(this.cache_.media);
26436 }
26437
26438 /**
26439 * Gets tag settings
26440 *
26441 * @param {Element} tag
26442 * The player tag
26443 *
26444 * @return {Object}
26445 * An object containing all of the settings
26446 * for a player tag
26447 */
26448 static getTagSettings(tag) {
26449 const baseOptions = {
26450 sources: [],
26451 tracks: []
26452 };
26453 const tagOptions = getAttributes(tag);
26454 const dataSetup = tagOptions['data-setup'];
26455 if (hasClass(tag, 'vjs-fill')) {
26456 tagOptions.fill = true;
26457 }
26458 if (hasClass(tag, 'vjs-fluid')) {
26459 tagOptions.fluid = true;
26460 }
26461
26462 // Check if data-setup attr exists.
26463 if (dataSetup !== null) {
26464 // Parse options JSON
26465 try {
26466 // If empty string, make it a parsable json object.
26467 Object.assign(tagOptions, JSON.parse(dataSetup || '{}'));
26468 } catch (e) {
26469 log.error('data-setup', e);
26470 }
26471 }
26472 Object.assign(baseOptions, tagOptions);
26473
26474 // Get tag children settings
26475 if (tag.hasChildNodes()) {
26476 const children = tag.childNodes;
26477 for (let i = 0, j = children.length; i < j; i++) {
26478 const child = children[i];
26479 // Change case needed: http://ejohn.org/blog/nodename-case-sensitivity/
26480 const childName = child.nodeName.toLowerCase();
26481 if (childName === 'source') {
26482 baseOptions.sources.push(getAttributes(child));
26483 } else if (childName === 'track') {
26484 baseOptions.tracks.push(getAttributes(child));
26485 }
26486 }
26487 }
26488 return baseOptions;
26489 }
26490
26491 /**
26492 * Set debug mode to enable/disable logs at info level.
26493 *
26494 * @param {boolean} enabled
26495 * @fires Player#debugon
26496 * @fires Player#debugoff
26497 * @return {boolean|undefined}
26498 */
26499 debug(enabled) {
26500 if (enabled === undefined) {
26501 return this.debugEnabled_;
26502 }
26503 if (enabled) {
26504 this.trigger('debugon');
26505 this.previousLogLevel_ = this.log.level;
26506 this.log.level('debug');
26507 this.debugEnabled_ = true;
26508 } else {
26509 this.trigger('debugoff');
26510 this.log.level(this.previousLogLevel_);
26511 this.previousLogLevel_ = undefined;
26512 this.debugEnabled_ = false;
26513 }
26514 }
26515
26516 /**
26517 * Set or get current playback rates.
26518 * Takes an array and updates the playback rates menu with the new items.
26519 * Pass in an empty array to hide the menu.
26520 * Values other than arrays are ignored.
26521 *
26522 * @fires Player#playbackrateschange
26523 * @param {number[]} [newRates]
26524 * The new rates that the playback rates menu should update to.
26525 * An empty array will hide the menu
26526 * @return {number[]} When used as a getter will return the current playback rates
26527 */
26528 playbackRates(newRates) {
26529 if (newRates === undefined) {
26530 return this.cache_.playbackRates;
26531 }
26532
26533 // ignore any value that isn't an array
26534 if (!Array.isArray(newRates)) {
26535 return;
26536 }
26537
26538 // ignore any arrays that don't only contain numbers
26539 if (!newRates.every(rate => typeof rate === 'number')) {
26540 return;
26541 }
26542 this.cache_.playbackRates = newRates;
26543
26544 /**
26545 * fires when the playback rates in a player are changed
26546 *
26547 * @event Player#playbackrateschange
26548 * @type {Event}
26549 */
26550 this.trigger('playbackrateschange');
26551 }
26552
26553 /**
26554 * Reports whether or not a player has a plugin available.
26555 *
26556 * This does not report whether or not the plugin has ever been initialized
26557 * on this player. For that, [usingPlugin]{@link Player#usingPlugin}.
26558 *
26559 * @method hasPlugin
26560 * @param {string} name
26561 * The name of a plugin.
26562 *
26563 * @return {boolean}
26564 * Whether or not this player has the requested plugin available.
26565 */
26566
26567 /**
26568 * Reports whether or not a player is using a plugin by name.
26569 *
26570 * For basic plugins, this only reports whether the plugin has _ever_ been
26571 * initialized on this player.
26572 *
26573 * @method Player#usingPlugin
26574 * @param {string} name
26575 * The name of a plugin.
26576 *
26577 * @return {boolean}
26578 * Whether or not this player is using the requested plugin.
26579 */
26580}
26581
26582/**
26583 * Get the {@link VideoTrackList}
26584 *
26585 * @link https://html.spec.whatwg.org/multipage/embedded-content.html#videotracklist
26586 *
26587 * @return {VideoTrackList}
26588 * the current video track list
26589 *
26590 * @method Player.prototype.videoTracks
26591 */
26592
26593/**
26594 * Get the {@link AudioTrackList}
26595 *
26596 * @link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotracklist
26597 *
26598 * @return {AudioTrackList}
26599 * the current audio track list
26600 *
26601 * @method Player.prototype.audioTracks
26602 */
26603
26604/**
26605 * Get the {@link TextTrackList}
26606 *
26607 * @link http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-texttracks
26608 *
26609 * @return {TextTrackList}
26610 * the current text track list
26611 *
26612 * @method Player.prototype.textTracks
26613 */
26614
26615/**
26616 * Get the remote {@link TextTrackList}
26617 *
26618 * @return {TextTrackList}
26619 * The current remote text track list
26620 *
26621 * @method Player.prototype.remoteTextTracks
26622 */
26623
26624/**
26625 * Get the remote {@link HtmlTrackElementList} tracks.
26626 *
26627 * @return {HtmlTrackElementList}
26628 * The current remote text track element list
26629 *
26630 * @method Player.prototype.remoteTextTrackEls
26631 */
26632
26633ALL.names.forEach(function (name) {
26634 const props = ALL[name];
26635 Player.prototype[props.getterName] = function () {
26636 if (this.tech_) {
26637 return this.tech_[props.getterName]();
26638 }
26639
26640 // if we have not yet loadTech_, we create {video,audio,text}Tracks_
26641 // these will be passed to the tech during loading
26642 this[props.privateName] = this[props.privateName] || new props.ListClass();
26643 return this[props.privateName];
26644 };
26645});
26646
26647/**
26648 * Get or set the `Player`'s crossorigin option. For the HTML5 player, this
26649 * sets the `crossOrigin` property on the `<video>` tag to control the CORS
26650 * behavior.
26651 *
26652 * @see [Video Element Attributes]{@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#attr-crossorigin}
26653 *
26654 * @param {string} [value]
26655 * The value to set the `Player`'s crossorigin to. If an argument is
26656 * given, must be one of `anonymous` or `use-credentials`.
26657 *
26658 * @return {string|undefined}
26659 * - The current crossorigin value of the `Player` when getting.
26660 * - undefined when setting
26661 */
26662Player.prototype.crossorigin = Player.prototype.crossOrigin;
26663
26664/**
26665 * Global enumeration of players.
26666 *
26667 * The keys are the player IDs and the values are either the {@link Player}
26668 * instance or `null` for disposed players.
26669 *
26670 * @type {Object}
26671 */
26672Player.players = {};
26673const navigator = window__default["default"].navigator;
26674
26675/*
26676 * Player instance options, surfaced using options
26677 * options = Player.prototype.options_
26678 * Make changes in options, not here.
26679 *
26680 * @type {Object}
26681 * @private
26682 */
26683Player.prototype.options_ = {
26684 // Default order of fallback technology
26685 techOrder: Tech.defaultTechOrder_,
26686 html5: {},
26687 // enable sourceset by default
26688 enableSourceset: true,
26689 // default inactivity timeout
26690 inactivityTimeout: 2000,
26691 // default playback rates
26692 playbackRates: [],
26693 // Add playback rate selection by adding rates
26694 // 'playbackRates': [0.5, 1, 1.5, 2],
26695 liveui: false,
26696 // Included control sets
26697 children: ['mediaLoader', 'posterImage', 'titleBar', 'textTrackDisplay', 'loadingSpinner', 'bigPlayButton', 'liveTracker', 'controlBar', 'errorDisplay', 'textTrackSettings', 'resizeManager'],
26698 language: navigator && (navigator.languages && navigator.languages[0] || navigator.userLanguage || navigator.language) || 'en',
26699 // locales and their language translations
26700 languages: {},
26701 // Default message to show when a video cannot be played.
26702 notSupportedMessage: 'No compatible source was found for this media.',
26703 normalizeAutoplay: false,
26704 fullscreen: {
26705 options: {
26706 navigationUI: 'hide'
26707 }
26708 },
26709 breakpoints: {},
26710 responsive: false,
26711 audioOnlyMode: false,
26712 audioPosterMode: false,
26713 spatialNavigation: {
26714 enabled: false,
26715 horizontalSeek: false
26716 },
26717 // Default smooth seeking to false
26718 enableSmoothSeeking: false,
26719 disableSeekWhileScrubbingOnMobile: false
26720};
26721TECH_EVENTS_RETRIGGER.forEach(function (event) {
26722 Player.prototype[`handleTech${toTitleCase(event)}_`] = function () {
26723 return this.trigger(event);
26724 };
26725});
26726
26727/**
26728 * Fired when the player has initial duration and dimension information
26729 *
26730 * @event Player#loadedmetadata
26731 * @type {Event}
26732 */
26733
26734/**
26735 * Fired when the player has downloaded data at the current playback position
26736 *
26737 * @event Player#loadeddata
26738 * @type {Event}
26739 */
26740
26741/**
26742 * Fired when the current playback position has changed *
26743 * During playback this is fired every 15-250 milliseconds, depending on the
26744 * playback technology in use.
26745 *
26746 * @event Player#timeupdate
26747 * @type {Event}
26748 */
26749
26750/**
26751 * Fired when the volume changes
26752 *
26753 * @event Player#volumechange
26754 * @type {Event}
26755 */
26756
26757Component.registerComponent('Player', Player);
26758
26759/**
26760 * @file plugin.js
26761 */
26762
26763/**
26764 * The base plugin name.
26765 *
26766 * @private
26767 * @constant
26768 * @type {string}
26769 */
26770const BASE_PLUGIN_NAME = 'plugin';
26771
26772/**
26773 * The key on which a player's active plugins cache is stored.
26774 *
26775 * @private
26776 * @constant
26777 * @type {string}
26778 */
26779const PLUGIN_CACHE_KEY = 'activePlugins_';
26780
26781/**
26782 * Stores registered plugins in a private space.
26783 *
26784 * @private
26785 * @type {Object}
26786 */
26787const pluginStorage = {};
26788
26789/**
26790 * Reports whether or not a plugin has been registered.
26791 *
26792 * @private
26793 * @param {string} name
26794 * The name of a plugin.
26795 *
26796 * @return {boolean}
26797 * Whether or not the plugin has been registered.
26798 */
26799const pluginExists = name => pluginStorage.hasOwnProperty(name);
26800
26801/**
26802 * Get a single registered plugin by name.
26803 *
26804 * @private
26805 * @param {string} name
26806 * The name of a plugin.
26807 *
26808 * @return {typeof Plugin|Function|undefined}
26809 * The plugin (or undefined).
26810 */
26811const getPlugin = name => pluginExists(name) ? pluginStorage[name] : undefined;
26812
26813/**
26814 * Marks a plugin as "active" on a player.
26815 *
26816 * Also, ensures that the player has an object for tracking active plugins.
26817 *
26818 * @private
26819 * @param {Player} player
26820 * A Video.js player instance.
26821 *
26822 * @param {string} name
26823 * The name of a plugin.
26824 */
26825const markPluginAsActive = (player, name) => {
26826 player[PLUGIN_CACHE_KEY] = player[PLUGIN_CACHE_KEY] || {};
26827 player[PLUGIN_CACHE_KEY][name] = true;
26828};
26829
26830/**
26831 * Triggers a pair of plugin setup events.
26832 *
26833 * @private
26834 * @param {Player} player
26835 * A Video.js player instance.
26836 *
26837 * @param {PluginEventHash} hash
26838 * A plugin event hash.
26839 *
26840 * @param {boolean} [before]
26841 * If true, prefixes the event name with "before". In other words,
26842 * use this to trigger "beforepluginsetup" instead of "pluginsetup".
26843 */
26844const triggerSetupEvent = (player, hash, before) => {
26845 const eventName = (before ? 'before' : '') + 'pluginsetup';
26846 player.trigger(eventName, hash);
26847 player.trigger(eventName + ':' + hash.name, hash);
26848};
26849
26850/**
26851 * Takes a basic plugin function and returns a wrapper function which marks
26852 * on the player that the plugin has been activated.
26853 *
26854 * @private
26855 * @param {string} name
26856 * The name of the plugin.
26857 *
26858 * @param {Function} plugin
26859 * The basic plugin.
26860 *
26861 * @return {Function}
26862 * A wrapper function for the given plugin.
26863 */
26864const createBasicPlugin = function (name, plugin) {
26865 const basicPluginWrapper = function () {
26866 // We trigger the "beforepluginsetup" and "pluginsetup" events on the player
26867 // regardless, but we want the hash to be consistent with the hash provided
26868 // for advanced plugins.
26869 //
26870 // The only potentially counter-intuitive thing here is the `instance` in
26871 // the "pluginsetup" event is the value returned by the `plugin` function.
26872 triggerSetupEvent(this, {
26873 name,
26874 plugin,
26875 instance: null
26876 }, true);
26877 const instance = plugin.apply(this, arguments);
26878 markPluginAsActive(this, name);
26879 triggerSetupEvent(this, {
26880 name,
26881 plugin,
26882 instance
26883 });
26884 return instance;
26885 };
26886 Object.keys(plugin).forEach(function (prop) {
26887 basicPluginWrapper[prop] = plugin[prop];
26888 });
26889 return basicPluginWrapper;
26890};
26891
26892/**
26893 * Takes a plugin sub-class and returns a factory function for generating
26894 * instances of it.
26895 *
26896 * This factory function will replace itself with an instance of the requested
26897 * sub-class of Plugin.
26898 *
26899 * @private
26900 * @param {string} name
26901 * The name of the plugin.
26902 *
26903 * @param {Plugin} PluginSubClass
26904 * The advanced plugin.
26905 *
26906 * @return {Function}
26907 */
26908const createPluginFactory = (name, PluginSubClass) => {
26909 // Add a `name` property to the plugin prototype so that each plugin can
26910 // refer to itself by name.
26911 PluginSubClass.prototype.name = name;
26912 return function (...args) {
26913 triggerSetupEvent(this, {
26914 name,
26915 plugin: PluginSubClass,
26916 instance: null
26917 }, true);
26918 const instance = new PluginSubClass(...[this, ...args]);
26919
26920 // The plugin is replaced by a function that returns the current instance.
26921 this[name] = () => instance;
26922 triggerSetupEvent(this, instance.getEventHash());
26923 return instance;
26924 };
26925};
26926
26927/**
26928 * Parent class for all advanced plugins.
26929 *
26930 * @mixes module:evented~EventedMixin
26931 * @mixes module:stateful~StatefulMixin
26932 * @fires Player#beforepluginsetup
26933 * @fires Player#beforepluginsetup:$name
26934 * @fires Player#pluginsetup
26935 * @fires Player#pluginsetup:$name
26936 * @listens Player#dispose
26937 * @throws {Error}
26938 * If attempting to instantiate the base {@link Plugin} class
26939 * directly instead of via a sub-class.
26940 */
26941class Plugin {
26942 /**
26943 * Creates an instance of this class.
26944 *
26945 * Sub-classes should call `super` to ensure plugins are properly initialized.
26946 *
26947 * @param {Player} player
26948 * A Video.js player instance.
26949 */
26950 constructor(player) {
26951 if (this.constructor === Plugin) {
26952 throw new Error('Plugin must be sub-classed; not directly instantiated.');
26953 }
26954 this.player = player;
26955 if (!this.log) {
26956 this.log = this.player.log.createLogger(this.name);
26957 }
26958
26959 // Make this object evented, but remove the added `trigger` method so we
26960 // use the prototype version instead.
26961 evented(this);
26962 delete this.trigger;
26963 stateful(this, this.constructor.defaultState);
26964 markPluginAsActive(player, this.name);
26965
26966 // Auto-bind the dispose method so we can use it as a listener and unbind
26967 // it later easily.
26968 this.dispose = this.dispose.bind(this);
26969
26970 // If the player is disposed, dispose the plugin.
26971 player.on('dispose', this.dispose);
26972 }
26973
26974 /**
26975 * Get the version of the plugin that was set on <pluginName>.VERSION
26976 */
26977 version() {
26978 return this.constructor.VERSION;
26979 }
26980
26981 /**
26982 * Each event triggered by plugins includes a hash of additional data with
26983 * conventional properties.
26984 *
26985 * This returns that object or mutates an existing hash.
26986 *
26987 * @param {Object} [hash={}]
26988 * An object to be used as event an event hash.
26989 *
26990 * @return {PluginEventHash}
26991 * An event hash object with provided properties mixed-in.
26992 */
26993 getEventHash(hash = {}) {
26994 hash.name = this.name;
26995 hash.plugin = this.constructor;
26996 hash.instance = this;
26997 return hash;
26998 }
26999
27000 /**
27001 * Triggers an event on the plugin object and overrides
27002 * {@link module:evented~EventedMixin.trigger|EventedMixin.trigger}.
27003 *
27004 * @param {string|Object} event
27005 * An event type or an object with a type property.
27006 *
27007 * @param {Object} [hash={}]
27008 * Additional data hash to merge with a
27009 * {@link PluginEventHash|PluginEventHash}.
27010 *
27011 * @return {boolean}
27012 * Whether or not default was prevented.
27013 */
27014 trigger(event, hash = {}) {
27015 return trigger(this.eventBusEl_, event, this.getEventHash(hash));
27016 }
27017
27018 /**
27019 * Handles "statechanged" events on the plugin. No-op by default, override by
27020 * subclassing.
27021 *
27022 * @abstract
27023 * @param {Event} e
27024 * An event object provided by a "statechanged" event.
27025 *
27026 * @param {Object} e.changes
27027 * An object describing changes that occurred with the "statechanged"
27028 * event.
27029 */
27030 handleStateChanged(e) {}
27031
27032 /**
27033 * Disposes a plugin.
27034 *
27035 * Subclasses can override this if they want, but for the sake of safety,
27036 * it's probably best to subscribe the "dispose" event.
27037 *
27038 * @fires Plugin#dispose
27039 */
27040 dispose() {
27041 const {
27042 name,
27043 player
27044 } = this;
27045
27046 /**
27047 * Signals that a advanced plugin is about to be disposed.
27048 *
27049 * @event Plugin#dispose
27050 * @type {Event}
27051 */
27052 this.trigger('dispose');
27053 this.off();
27054 player.off('dispose', this.dispose);
27055
27056 // Eliminate any possible sources of leaking memory by clearing up
27057 // references between the player and the plugin instance and nulling out
27058 // the plugin's state and replacing methods with a function that throws.
27059 player[PLUGIN_CACHE_KEY][name] = false;
27060 this.player = this.state = null;
27061
27062 // Finally, replace the plugin name on the player with a new factory
27063 // function, so that the plugin is ready to be set up again.
27064 player[name] = createPluginFactory(name, pluginStorage[name]);
27065 }
27066
27067 /**
27068 * Determines if a plugin is a basic plugin (i.e. not a sub-class of `Plugin`).
27069 *
27070 * @param {string|Function} plugin
27071 * If a string, matches the name of a plugin. If a function, will be
27072 * tested directly.
27073 *
27074 * @return {boolean}
27075 * Whether or not a plugin is a basic plugin.
27076 */
27077 static isBasic(plugin) {
27078 const p = typeof plugin === 'string' ? getPlugin(plugin) : plugin;
27079 return typeof p === 'function' && !Plugin.prototype.isPrototypeOf(p.prototype);
27080 }
27081
27082 /**
27083 * Register a Video.js plugin.
27084 *
27085 * @param {string} name
27086 * The name of the plugin to be registered. Must be a string and
27087 * must not match an existing plugin or a method on the `Player`
27088 * prototype.
27089 *
27090 * @param {typeof Plugin|Function} plugin
27091 * A sub-class of `Plugin` or a function for basic plugins.
27092 *
27093 * @return {typeof Plugin|Function}
27094 * For advanced plugins, a factory function for that plugin. For
27095 * basic plugins, a wrapper function that initializes the plugin.
27096 */
27097 static registerPlugin(name, plugin) {
27098 if (typeof name !== 'string') {
27099 throw new Error(`Illegal plugin name, "${name}", must be a string, was ${typeof name}.`);
27100 }
27101 if (pluginExists(name)) {
27102 log.warn(`A plugin named "${name}" already exists. You may want to avoid re-registering plugins!`);
27103 } else if (Player.prototype.hasOwnProperty(name)) {
27104 throw new Error(`Illegal plugin name, "${name}", cannot share a name with an existing player method!`);
27105 }
27106 if (typeof plugin !== 'function') {
27107 throw new Error(`Illegal plugin for "${name}", must be a function, was ${typeof plugin}.`);
27108 }
27109 pluginStorage[name] = plugin;
27110
27111 // Add a player prototype method for all sub-classed plugins (but not for
27112 // the base Plugin class).
27113 if (name !== BASE_PLUGIN_NAME) {
27114 if (Plugin.isBasic(plugin)) {
27115 Player.prototype[name] = createBasicPlugin(name, plugin);
27116 } else {
27117 Player.prototype[name] = createPluginFactory(name, plugin);
27118 }
27119 }
27120 return plugin;
27121 }
27122
27123 /**
27124 * De-register a Video.js plugin.
27125 *
27126 * @param {string} name
27127 * The name of the plugin to be de-registered. Must be a string that
27128 * matches an existing plugin.
27129 *
27130 * @throws {Error}
27131 * If an attempt is made to de-register the base plugin.
27132 */
27133 static deregisterPlugin(name) {
27134 if (name === BASE_PLUGIN_NAME) {
27135 throw new Error('Cannot de-register base plugin.');
27136 }
27137 if (pluginExists(name)) {
27138 delete pluginStorage[name];
27139 delete Player.prototype[name];
27140 }
27141 }
27142
27143 /**
27144 * Gets an object containing multiple Video.js plugins.
27145 *
27146 * @param {Array} [names]
27147 * If provided, should be an array of plugin names. Defaults to _all_
27148 * plugin names.
27149 *
27150 * @return {Object|undefined}
27151 * An object containing plugin(s) associated with their name(s) or
27152 * `undefined` if no matching plugins exist).
27153 */
27154 static getPlugins(names = Object.keys(pluginStorage)) {
27155 let result;
27156 names.forEach(name => {
27157 const plugin = getPlugin(name);
27158 if (plugin) {
27159 result = result || {};
27160 result[name] = plugin;
27161 }
27162 });
27163 return result;
27164 }
27165
27166 /**
27167 * Gets a plugin's version, if available
27168 *
27169 * @param {string} name
27170 * The name of a plugin.
27171 *
27172 * @return {string}
27173 * The plugin's version or an empty string.
27174 */
27175 static getPluginVersion(name) {
27176 const plugin = getPlugin(name);
27177 return plugin && plugin.VERSION || '';
27178 }
27179}
27180
27181/**
27182 * Gets a plugin by name if it exists.
27183 *
27184 * @static
27185 * @method getPlugin
27186 * @memberOf Plugin
27187 * @param {string} name
27188 * The name of a plugin.
27189 *
27190 * @returns {typeof Plugin|Function|undefined}
27191 * The plugin (or `undefined`).
27192 */
27193Plugin.getPlugin = getPlugin;
27194
27195/**
27196 * The name of the base plugin class as it is registered.
27197 *
27198 * @type {string}
27199 */
27200Plugin.BASE_PLUGIN_NAME = BASE_PLUGIN_NAME;
27201Plugin.registerPlugin(BASE_PLUGIN_NAME, Plugin);
27202
27203/**
27204 * Documented in player.js
27205 *
27206 * @ignore
27207 */
27208Player.prototype.usingPlugin = function (name) {
27209 return !!this[PLUGIN_CACHE_KEY] && this[PLUGIN_CACHE_KEY][name] === true;
27210};
27211
27212/**
27213 * Documented in player.js
27214 *
27215 * @ignore
27216 */
27217Player.prototype.hasPlugin = function (name) {
27218 return !!pluginExists(name);
27219};
27220
27221/**
27222 * Signals that a plugin is about to be set up on a player.
27223 *
27224 * @event Player#beforepluginsetup
27225 * @type {PluginEventHash}
27226 */
27227
27228/**
27229 * Signals that a plugin is about to be set up on a player - by name. The name
27230 * is the name of the plugin.
27231 *
27232 * @event Player#beforepluginsetup:$name
27233 * @type {PluginEventHash}
27234 */
27235
27236/**
27237 * Signals that a plugin has just been set up on a player.
27238 *
27239 * @event Player#pluginsetup
27240 * @type {PluginEventHash}
27241 */
27242
27243/**
27244 * Signals that a plugin has just been set up on a player - by name. The name
27245 * is the name of the plugin.
27246 *
27247 * @event Player#pluginsetup:$name
27248 * @type {PluginEventHash}
27249 */
27250
27251/**
27252 * @typedef {Object} PluginEventHash
27253 *
27254 * @property {string} instance
27255 * For basic plugins, the return value of the plugin function. For
27256 * advanced plugins, the plugin instance on which the event is fired.
27257 *
27258 * @property {string} name
27259 * The name of the plugin.
27260 *
27261 * @property {string} plugin
27262 * For basic plugins, the plugin function. For advanced plugins, the
27263 * plugin class/constructor.
27264 */
27265
27266/**
27267 * @file deprecate.js
27268 * @module deprecate
27269 */
27270
27271/**
27272 * Decorate a function with a deprecation message the first time it is called.
27273 *
27274 * @param {string} message
27275 * A deprecation message to log the first time the returned function
27276 * is called.
27277 *
27278 * @param {Function} fn
27279 * The function to be deprecated.
27280 *
27281 * @return {Function}
27282 * A wrapper function that will log a deprecation warning the first
27283 * time it is called. The return value will be the return value of
27284 * the wrapped function.
27285 */
27286function deprecate(message, fn) {
27287 let warned = false;
27288 return function (...args) {
27289 if (!warned) {
27290 log.warn(message);
27291 }
27292 warned = true;
27293 return fn.apply(this, args);
27294 };
27295}
27296
27297/**
27298 * Internal function used to mark a function as deprecated in the next major
27299 * version with consistent messaging.
27300 *
27301 * @param {number} major The major version where it will be removed
27302 * @param {string} oldName The old function name
27303 * @param {string} newName The new function name
27304 * @param {Function} fn The function to deprecate
27305 * @return {Function} The decorated function
27306 */
27307function deprecateForMajor(major, oldName, newName, fn) {
27308 return deprecate(`${oldName} is deprecated and will be removed in ${major}.0; please use ${newName} instead.`, fn);
27309}
27310
27311var VjsErrors = {
27312 NetworkBadStatus: 'networkbadstatus',
27313 NetworkRequestFailed: 'networkrequestfailed',
27314 NetworkRequestAborted: 'networkrequestaborted',
27315 NetworkRequestTimeout: 'networkrequesttimeout',
27316 NetworkBodyParserFailed: 'networkbodyparserfailed',
27317 StreamingHlsPlaylistParserError: 'streaminghlsplaylistparsererror',
27318 StreamingDashManifestParserError: 'streamingdashmanifestparsererror',
27319 StreamingContentSteeringParserError: 'streamingcontentsteeringparsererror',
27320 StreamingVttParserError: 'streamingvttparsererror',
27321 StreamingFailedToSelectNextSegment: 'streamingfailedtoselectnextsegment',
27322 StreamingFailedToDecryptSegment: 'streamingfailedtodecryptsegment',
27323 StreamingFailedToTransmuxSegment: 'streamingfailedtotransmuxsegment',
27324 StreamingFailedToAppendSegment: 'streamingfailedtoappendsegment',
27325 StreamingCodecsChangeError: 'streamingcodecschangeerror'
27326};
27327
27328/**
27329 * @file video.js
27330 * @module videojs
27331 */
27332
27333/** @import { PlayerReadyCallback } from './player' */
27334
27335/**
27336 * Normalize an `id` value by trimming off a leading `#`
27337 *
27338 * @private
27339 * @param {string} id
27340 * A string, maybe with a leading `#`.
27341 *
27342 * @return {string}
27343 * The string, without any leading `#`.
27344 */
27345const normalizeId = id => id.indexOf('#') === 0 ? id.slice(1) : id;
27346
27347/**
27348 * The `videojs()` function doubles as the main function for users to create a
27349 * {@link Player} instance as well as the main library namespace.
27350 *
27351 * It can also be used as a getter for a pre-existing {@link Player} instance.
27352 * However, we _strongly_ recommend using `videojs.getPlayer()` for this
27353 * purpose because it avoids any potential for unintended initialization.
27354 *
27355 * Due to [limitations](https://github.com/jsdoc3/jsdoc/issues/955#issuecomment-313829149)
27356 * of our JSDoc template, we cannot properly document this as both a function
27357 * and a namespace, so its function signature is documented here.
27358 *
27359 * #### Arguments
27360 * ##### id
27361 * string|Element, **required**
27362 *
27363 * Video element or video element ID.
27364 *
27365 * ##### options
27366 * Object, optional
27367 *
27368 * Options object for providing settings.
27369 * See: [Options Guide](https://docs.videojs.com/tutorial-options.html).
27370 *
27371 * ##### ready
27372 * {@link Component~ReadyCallback}, optional
27373 *
27374 * A function to be called when the {@link Player} and {@link Tech} are ready.
27375 *
27376 * #### Return Value
27377 *
27378 * The `videojs()` function returns a {@link Player} instance.
27379 *
27380 * @namespace
27381 *
27382 * @borrows AudioTrack as AudioTrack
27383 * @borrows Component.getComponent as getComponent
27384 * @borrows module:events.on as on
27385 * @borrows module:events.one as one
27386 * @borrows module:events.off as off
27387 * @borrows module:events.trigger as trigger
27388 * @borrows EventTarget as EventTarget
27389 * @borrows module:middleware.use as use
27390 * @borrows Player.players as players
27391 * @borrows Plugin.registerPlugin as registerPlugin
27392 * @borrows Plugin.deregisterPlugin as deregisterPlugin
27393 * @borrows Plugin.getPlugins as getPlugins
27394 * @borrows Plugin.getPlugin as getPlugin
27395 * @borrows Plugin.getPluginVersion as getPluginVersion
27396 * @borrows Tech.getTech as getTech
27397 * @borrows Tech.registerTech as registerTech
27398 * @borrows TextTrack as TextTrack
27399 * @borrows VideoTrack as VideoTrack
27400 *
27401 * @param {string|Element} id
27402 * Video element or video element ID.
27403 *
27404 * @param {Object} [options]
27405 * Options object for providing settings.
27406 * See: [Options Guide](https://docs.videojs.com/tutorial-options.html).
27407 *
27408 * @param {PlayerReadyCallback} [ready]
27409 * A function to be called when the {@link Player} and {@link Tech} are
27410 * ready.
27411 *
27412 * @return {Player}
27413 * The `videojs()` function returns a {@link Player|Player} instance.
27414 */
27415function videojs(id, options, ready) {
27416 let player = videojs.getPlayer(id);
27417 if (player) {
27418 if (options) {
27419 log.warn(`Player "${id}" is already initialised. Options will not be applied.`);
27420 }
27421 if (ready) {
27422 player.ready(ready);
27423 }
27424 return player;
27425 }
27426 const el = typeof id === 'string' ? $('#' + normalizeId(id)) : id;
27427 if (!isEl(el)) {
27428 throw new TypeError('The element or ID supplied is not valid. (videojs)');
27429 }
27430
27431 // document.body.contains(el) will only check if el is contained within that one document.
27432 // This causes problems for elements in iframes.
27433 // Instead, use the element's ownerDocument instead of the global document.
27434 // This will make sure that the element is indeed in the dom of that document.
27435 // Additionally, check that the document in question has a default view.
27436 // If the document is no longer attached to the dom, the defaultView of the document will be null.
27437 // If element is inside Shadow DOM (e.g. is part of a Custom element), ownerDocument.body
27438 // always returns false. Instead, use the Shadow DOM root.
27439 const inShadowDom = 'getRootNode' in el ? el.getRootNode() instanceof window__default["default"].ShadowRoot : false;
27440 const rootNode = inShadowDom ? el.getRootNode() : el.ownerDocument.body;
27441 if (!el.ownerDocument.defaultView || !rootNode.contains(el)) {
27442 log.warn('The element supplied is not included in the DOM');
27443 }
27444 options = options || {};
27445
27446 // Store a copy of the el before modification, if it is to be restored in destroy()
27447 // If div ingest, store the parent div
27448 if (options.restoreEl === true) {
27449 options.restoreEl = (el.parentNode && el.parentNode.hasAttribute && el.parentNode.hasAttribute('data-vjs-player') ? el.parentNode : el).cloneNode(true);
27450 }
27451 hooks('beforesetup').forEach(hookFunction => {
27452 const opts = hookFunction(el, merge(options));
27453 if (!isObject(opts) || Array.isArray(opts)) {
27454 log.error('please return an object in beforesetup hooks');
27455 return;
27456 }
27457 options = merge(options, opts);
27458 });
27459
27460 // We get the current "Player" component here in case an integration has
27461 // replaced it with a custom player.
27462 const PlayerComponent = Component.getComponent('Player');
27463 player = new PlayerComponent(el, options, ready);
27464 hooks('setup').forEach(hookFunction => hookFunction(player));
27465 return player;
27466}
27467videojs.hooks_ = hooks_;
27468videojs.hooks = hooks;
27469videojs.hook = hook;
27470videojs.hookOnce = hookOnce;
27471videojs.removeHook = removeHook;
27472
27473// Add default styles
27474if (window__default["default"].VIDEOJS_NO_DYNAMIC_STYLE !== true && isReal()) {
27475 let style = $('.vjs-styles-defaults');
27476 if (!style) {
27477 style = createStyleElement('vjs-styles-defaults');
27478 const head = $('head');
27479 if (head) {
27480 head.insertBefore(style, head.firstChild);
27481 }
27482 setTextContent(style, `
27483 .video-js {
27484 width: 300px;
27485 height: 150px;
27486 }
27487
27488 .vjs-fluid:not(.vjs-audio-only-mode) {
27489 padding-top: 56.25%
27490 }
27491 `);
27492 }
27493}
27494
27495// Run Auto-load players
27496// You have to wait at least once in case this script is loaded after your
27497// video in the DOM (weird behavior only with minified version)
27498autoSetupTimeout(1, videojs);
27499
27500/**
27501 * Current Video.js version. Follows [semantic versioning](https://semver.org/).
27502 *
27503 * @type {string}
27504 */
27505videojs.VERSION = version;
27506
27507/**
27508 * The global options object. These are the settings that take effect
27509 * if no overrides are specified when the player is created.
27510 *
27511 * @type {Object}
27512 */
27513videojs.options = Player.prototype.options_;
27514
27515/**
27516 * Get an object with the currently created players, keyed by player ID
27517 *
27518 * @return {Object}
27519 * The created players
27520 */
27521videojs.getPlayers = () => Player.players;
27522
27523/**
27524 * Get a single player based on an ID or DOM element.
27525 *
27526 * This is useful if you want to check if an element or ID has an associated
27527 * Video.js player, but not create one if it doesn't.
27528 *
27529 * @param {string|Element} id
27530 * An HTML element - `<video>`, `<audio>`, or `<video-js>` -
27531 * or a string matching the `id` of such an element.
27532 *
27533 * @return {Player|undefined}
27534 * A player instance or `undefined` if there is no player instance
27535 * matching the argument.
27536 */
27537videojs.getPlayer = id => {
27538 const players = Player.players;
27539 let tag;
27540 if (typeof id === 'string') {
27541 const nId = normalizeId(id);
27542 const player = players[nId];
27543 if (player) {
27544 return player;
27545 }
27546 tag = $('#' + nId);
27547 } else {
27548 tag = id;
27549 }
27550 if (isEl(tag)) {
27551 const {
27552 player,
27553 playerId
27554 } = tag;
27555
27556 // Element may have a `player` property referring to an already created
27557 // player instance. If so, return that.
27558 if (player || players[playerId]) {
27559 return player || players[playerId];
27560 }
27561 }
27562};
27563
27564/**
27565 * Returns an array of all current players.
27566 *
27567 * @return {Array}
27568 * An array of all players. The array will be in the order that
27569 * `Object.keys` provides, which could potentially vary between
27570 * JavaScript engines.
27571 *
27572 */
27573videojs.getAllPlayers = () =>
27574// Disposed players leave a key with a `null` value, so we need to make sure
27575// we filter those out.
27576Object.keys(Player.players).map(k => Player.players[k]).filter(Boolean);
27577videojs.players = Player.players;
27578videojs.getComponent = Component.getComponent;
27579
27580/**
27581 * Register a component so it can referred to by name. Used when adding to other
27582 * components, either through addChild `component.addChild('myComponent')` or through
27583 * default children options `{ children: ['myComponent'] }`.
27584 *
27585 * > NOTE: You could also just initialize the component before adding.
27586 * `component.addChild(new MyComponent());`
27587 *
27588 * @param {string} name
27589 * The class name of the component
27590 *
27591 * @param {typeof Component} comp
27592 * The component class
27593 *
27594 * @return {typeof Component}
27595 * The newly registered component
27596 */
27597videojs.registerComponent = (name, comp) => {
27598 if (Tech.isTech(comp)) {
27599 log.warn(`The ${name} tech was registered as a component. It should instead be registered using videojs.registerTech(name, tech)`);
27600 }
27601 return Component.registerComponent.call(Component, name, comp);
27602};
27603videojs.getTech = Tech.getTech;
27604videojs.registerTech = Tech.registerTech;
27605videojs.use = use;
27606
27607/**
27608 * An object that can be returned by a middleware to signify
27609 * that the middleware is being terminated.
27610 *
27611 * @type {object}
27612 * @property {object} middleware.TERMINATOR
27613 */
27614Object.defineProperty(videojs, 'middleware', {
27615 value: {},
27616 writeable: false,
27617 enumerable: true
27618});
27619Object.defineProperty(videojs.middleware, 'TERMINATOR', {
27620 value: TERMINATOR,
27621 writeable: false,
27622 enumerable: true
27623});
27624
27625/**
27626 * A reference to the {@link module:browser|browser utility module} as an object.
27627 *
27628 * @type {Object}
27629 * @see {@link module:browser|browser}
27630 */
27631videojs.browser = browser;
27632
27633/**
27634 * A reference to the {@link module:obj|obj utility module} as an object.
27635 *
27636 * @type {Object}
27637 * @see {@link module:obj|obj}
27638 */
27639videojs.obj = Obj;
27640
27641/**
27642 * Deprecated reference to the {@link module:obj.merge|merge function}
27643 *
27644 * @type {Function}
27645 * @see {@link module:obj.merge|merge}
27646 * @deprecated Deprecated and will be removed in 9.0. Please use videojs.obj.merge instead.
27647 */
27648videojs.mergeOptions = deprecateForMajor(9, 'videojs.mergeOptions', 'videojs.obj.merge', merge);
27649
27650/**
27651 * Deprecated reference to the {@link module:obj.defineLazyProperty|defineLazyProperty function}
27652 *
27653 * @type {Function}
27654 * @see {@link module:obj.defineLazyProperty|defineLazyProperty}
27655 * @deprecated Deprecated and will be removed in 9.0. Please use videojs.obj.defineLazyProperty instead.
27656 */
27657videojs.defineLazyProperty = deprecateForMajor(9, 'videojs.defineLazyProperty', 'videojs.obj.defineLazyProperty', defineLazyProperty);
27658
27659/**
27660 * Deprecated reference to the {@link module:fn.bind_|fn.bind_ function}
27661 *
27662 * @type {Function}
27663 * @see {@link module:fn.bind_|fn.bind_}
27664 * @deprecated Deprecated and will be removed in 9.0. Please use native Function.prototype.bind instead.
27665 */
27666videojs.bind = deprecateForMajor(9, 'videojs.bind', 'native Function.prototype.bind', bind_);
27667videojs.registerPlugin = Plugin.registerPlugin;
27668videojs.deregisterPlugin = Plugin.deregisterPlugin;
27669
27670/**
27671 * Deprecated method to register a plugin with Video.js
27672 *
27673 * @deprecated Deprecated and will be removed in 9.0. Use videojs.registerPlugin() instead.
27674 *
27675 * @param {string} name
27676 * The plugin name
27677*
27678 * @param {typeof Plugin|Function} plugin
27679 * The plugin sub-class or function
27680 *
27681 * @return {typeof Plugin|Function}
27682 */
27683videojs.plugin = (name, plugin) => {
27684 log.warn('videojs.plugin() is deprecated; use videojs.registerPlugin() instead');
27685 return Plugin.registerPlugin(name, plugin);
27686};
27687videojs.getPlugins = Plugin.getPlugins;
27688videojs.getPlugin = Plugin.getPlugin;
27689videojs.getPluginVersion = Plugin.getPluginVersion;
27690
27691/**
27692 * Adding languages so that they're available to all players.
27693 * Example: `videojs.addLanguage('es', { 'Hello': 'Hola' });`
27694 *
27695 * @param {string} code
27696 * The language code or dictionary property
27697 *
27698 * @param {Object} data
27699 * The data values to be translated
27700 *
27701 * @return {Object}
27702 * The resulting language dictionary object
27703 */
27704videojs.addLanguage = function (code, data) {
27705 code = ('' + code).toLowerCase();
27706 videojs.options.languages = merge(videojs.options.languages, {
27707 [code]: data
27708 });
27709 return videojs.options.languages[code];
27710};
27711
27712/**
27713 * A reference to the {@link module:log|log utility module} as an object.
27714 *
27715 * @type {Function}
27716 * @see {@link module:log|log}
27717 */
27718videojs.log = log;
27719videojs.createLogger = createLogger;
27720
27721/**
27722 * A reference to the {@link module:time|time utility module} as an object.
27723 *
27724 * @type {Object}
27725 * @see {@link module:time|time}
27726 */
27727videojs.time = Time;
27728
27729/**
27730 * Deprecated reference to the {@link module:time.createTimeRanges|createTimeRanges function}
27731 *
27732 * @type {Function}
27733 * @see {@link module:time.createTimeRanges|createTimeRanges}
27734 * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.createTimeRanges instead.
27735 */
27736videojs.createTimeRange = deprecateForMajor(9, 'videojs.createTimeRange', 'videojs.time.createTimeRanges', createTimeRanges);
27737
27738/**
27739 * Deprecated reference to the {@link module:time.createTimeRanges|createTimeRanges function}
27740 *
27741 * @type {Function}
27742 * @see {@link module:time.createTimeRanges|createTimeRanges}
27743 * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.createTimeRanges instead.
27744 */
27745videojs.createTimeRanges = deprecateForMajor(9, 'videojs.createTimeRanges', 'videojs.time.createTimeRanges', createTimeRanges);
27746
27747/**
27748 * Deprecated reference to the {@link module:time.formatTime|formatTime function}
27749 *
27750 * @type {Function}
27751 * @see {@link module:time.formatTime|formatTime}
27752 * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.format instead.
27753 */
27754videojs.formatTime = deprecateForMajor(9, 'videojs.formatTime', 'videojs.time.formatTime', formatTime);
27755
27756/**
27757 * Deprecated reference to the {@link module:time.setFormatTime|setFormatTime function}
27758 *
27759 * @type {Function}
27760 * @see {@link module:time.setFormatTime|setFormatTime}
27761 * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.setFormat instead.
27762 */
27763videojs.setFormatTime = deprecateForMajor(9, 'videojs.setFormatTime', 'videojs.time.setFormatTime', setFormatTime);
27764
27765/**
27766 * Deprecated reference to the {@link module:time.resetFormatTime|resetFormatTime function}
27767 *
27768 * @type {Function}
27769 * @see {@link module:time.resetFormatTime|resetFormatTime}
27770 * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.resetFormat instead.
27771 */
27772videojs.resetFormatTime = deprecateForMajor(9, 'videojs.resetFormatTime', 'videojs.time.resetFormatTime', resetFormatTime);
27773
27774/**
27775 * Deprecated reference to the {@link module:url.parseUrl|Url.parseUrl function}
27776 *
27777 * @type {Function}
27778 * @see {@link module:url.parseUrl|parseUrl}
27779 * @deprecated Deprecated and will be removed in 9.0. Please use videojs.url.parseUrl instead.
27780 */
27781videojs.parseUrl = deprecateForMajor(9, 'videojs.parseUrl', 'videojs.url.parseUrl', parseUrl);
27782
27783/**
27784 * Deprecated reference to the {@link module:url.isCrossOrigin|Url.isCrossOrigin function}
27785 *
27786 * @type {Function}
27787 * @see {@link module:url.isCrossOrigin|isCrossOrigin}
27788 * @deprecated Deprecated and will be removed in 9.0. Please use videojs.url.isCrossOrigin instead.
27789 */
27790videojs.isCrossOrigin = deprecateForMajor(9, 'videojs.isCrossOrigin', 'videojs.url.isCrossOrigin', isCrossOrigin);
27791videojs.EventTarget = EventTarget;
27792videojs.any = any;
27793videojs.on = on;
27794videojs.one = one;
27795videojs.off = off;
27796videojs.trigger = trigger;
27797
27798/**
27799 * A cross-browser XMLHttpRequest wrapper.
27800 *
27801 * @function
27802 * @param {Object} options
27803 * Settings for the request.
27804 *
27805 * @return {XMLHttpRequest|XDomainRequest}
27806 * The request object.
27807 *
27808 * @see https://github.com/Raynos/xhr
27809 */
27810videojs.xhr = XHR__default["default"];
27811videojs.TextTrack = TextTrack;
27812videojs.AudioTrack = AudioTrack;
27813videojs.VideoTrack = VideoTrack;
27814['isEl', 'isTextNode', 'createEl', 'hasClass', 'addClass', 'removeClass', 'toggleClass', 'setAttributes', 'getAttributes', 'emptyEl', 'appendContent', 'insertContent'].forEach(k => {
27815 videojs[k] = function () {
27816 log.warn(`videojs.${k}() is deprecated; use videojs.dom.${k}() instead`);
27817 return Dom[k].apply(null, arguments);
27818 };
27819});
27820videojs.computedStyle = deprecateForMajor(9, 'videojs.computedStyle', 'videojs.dom.computedStyle', computedStyle);
27821
27822/**
27823 * A reference to the {@link module:dom|DOM utility module} as an object.
27824 *
27825 * @type {Object}
27826 * @see {@link module:dom|dom}
27827 */
27828videojs.dom = Dom;
27829
27830/**
27831 * A reference to the {@link module:fn|fn utility module} as an object.
27832 *
27833 * @type {Object}
27834 * @see {@link module:fn|fn}
27835 */
27836videojs.fn = Fn;
27837
27838/**
27839 * A reference to the {@link module:num|num utility module} as an object.
27840 *
27841 * @type {Object}
27842 * @see {@link module:num|num}
27843 */
27844videojs.num = Num;
27845
27846/**
27847 * A reference to the {@link module:str|str utility module} as an object.
27848 *
27849 * @type {Object}
27850 * @see {@link module:str|str}
27851 */
27852videojs.str = Str;
27853
27854/**
27855 * A reference to the {@link module:url|URL utility module} as an object.
27856 *
27857 * @type {Object}
27858 * @see {@link module:url|url}
27859 */
27860videojs.url = Url;
27861
27862// The list of possible error types to occur in video.js
27863videojs.Error = VjsErrors;
27864
27865module.exports = videojs;