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 | ;
|
14 |
|
15 | var window = require('global/window');
|
16 | var document$1 = require('global/document');
|
17 | var XHR = require('@videojs/xhr');
|
18 | var vtt = require('videojs-vtt.js');
|
19 |
|
20 | function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
21 |
|
22 | var window__default = /*#__PURE__*/_interopDefaultLegacy(window);
|
23 | var document__default = /*#__PURE__*/_interopDefaultLegacy(document$1);
|
24 | var XHR__default = /*#__PURE__*/_interopDefaultLegacy(XHR);
|
25 | var vtt__default = /*#__PURE__*/_interopDefaultLegacy(vtt);
|
26 |
|
27 | var 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 | */
|
35 | const 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 | */
|
49 | const 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 | */
|
66 | const 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 | */
|
82 | const 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 | */
|
101 | const 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 | */
|
123 | const FullscreenApi = {
|
124 | prefixed: true
|
125 | };
|
126 |
|
127 | // browser API methods
|
128 | const apiMap = [['requestFullscreen', 'exitFullscreen', 'fullscreenElement', 'fullscreenEnabled', 'fullscreenchange', 'fullscreenerror', 'fullscreen'],
|
129 | // WebKit
|
130 | ['webkitRequestFullscreen', 'webkitExitFullscreen', 'webkitFullscreenElement', 'webkitFullscreenEnabled', 'webkitfullscreenchange', 'webkitfullscreenerror', '-webkit-full-screen']];
|
131 | const specApi = apiMap[0];
|
132 | let browserApi;
|
133 |
|
134 | // determine the supported set of functions
|
135 | for (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
|
144 | if (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.
|
157 | let 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 | */
|
172 | const 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 | };
|
220 | function 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 | */
|
432 | const log = createLogger$1('VIDEOJS');
|
433 | const 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 | */
|
465 | const 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 | */
|
479 | const 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 | */
|
492 | function 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 | */
|
513 | function 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 | */
|
527 | function 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 | */
|
538 | function 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 | */
|
558 | function 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 | */
|
584 | function 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 | */
|
604 | function 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 |
|
625 | var 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 | */
|
647 | let IS_IPOD = false;
|
648 |
|
649 | /**
|
650 | * The detected iOS version - or `null`.
|
651 | *
|
652 | * @static
|
653 | * @type {string|null}
|
654 | */
|
655 | let IOS_VERSION = null;
|
656 |
|
657 | /**
|
658 | * Whether or not this is an Android device.
|
659 | *
|
660 | * @static
|
661 | * @type {Boolean}
|
662 | */
|
663 | let 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 | */
|
671 | let ANDROID_VERSION;
|
672 |
|
673 | /**
|
674 | * Whether or not this is Mozilla Firefox.
|
675 | *
|
676 | * @static
|
677 | * @type {Boolean}
|
678 | */
|
679 | let IS_FIREFOX = false;
|
680 |
|
681 | /**
|
682 | * Whether or not this is Microsoft Edge.
|
683 | *
|
684 | * @static
|
685 | * @type {Boolean}
|
686 | */
|
687 | let IS_EDGE = false;
|
688 |
|
689 | /**
|
690 | * Whether or not this is any Chromium Browser
|
691 | *
|
692 | * @static
|
693 | * @type {Boolean}
|
694 | */
|
695 | let 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 | */
|
711 | let IS_CHROME = false;
|
712 |
|
713 | /**
|
714 | * The detected Chromium version - or `null`.
|
715 | *
|
716 | * @static
|
717 | * @type {number|null}
|
718 | */
|
719 | let 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 | */
|
730 | let CHROME_VERSION = null;
|
731 |
|
732 | /**
|
733 | * Whether or not this is a Chromecast receiver application.
|
734 | *
|
735 | * @static
|
736 | * @type {Boolean}
|
737 | */
|
738 | const 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 | */
|
747 | let IE_VERSION = null;
|
748 |
|
749 | /**
|
750 | * Whether or not this is desktop Safari.
|
751 | *
|
752 | * @static
|
753 | * @type {Boolean}
|
754 | */
|
755 | let IS_SAFARI = false;
|
756 |
|
757 | /**
|
758 | * Whether or not this is a Windows machine.
|
759 | *
|
760 | * @static
|
761 | * @type {Boolean}
|
762 | */
|
763 | let IS_WINDOWS = false;
|
764 |
|
765 | /**
|
766 | * Whether or not this device is an iPad.
|
767 | *
|
768 | * @static
|
769 | * @type {Boolean}
|
770 | */
|
771 | let 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/
|
782 | let IS_IPHONE = false;
|
783 |
|
784 | /**
|
785 | * Whether or not this is a Tizen device.
|
786 | *
|
787 | * @static
|
788 | * @type {Boolean}
|
789 | */
|
790 | let IS_TIZEN = false;
|
791 |
|
792 | /**
|
793 | * Whether or not this is a WebOS device.
|
794 | *
|
795 | * @static
|
796 | * @type {Boolean}
|
797 | */
|
798 | let 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 | */
|
806 | let IS_SMART_TV = false;
|
807 |
|
808 | /**
|
809 | * Whether or not this device is touch-enabled.
|
810 | *
|
811 | * @static
|
812 | * @const
|
813 | * @type {Boolean}
|
814 | */
|
815 | const 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));
|
816 | const UAD = window__default["default"].navigator && window__default["default"].navigator.userAgentData;
|
817 | if (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.
|
833 | if (!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 | */
|
896 | const 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 | */
|
905 | const IS_ANY_SAFARI = (IS_SAFARI || IS_IOS) && !IS_CHROME;
|
906 |
|
907 | var 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 | */
|
949 | function 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 | */
|
969 | function 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 | */
|
982 | function 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 | */
|
996 | function 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 | */
|
1007 | function 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 | */
|
1027 | function 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 | */
|
1058 | function 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 | */
|
1092 | function 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 | */
|
1110 | function 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 | */
|
1133 | function 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 | */
|
1150 | function 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 | */
|
1167 | function 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 | */
|
1210 | function 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 | */
|
1230 | function 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 | */
|
1254 | function 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 | */
|
1294 | function 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 | */
|
1310 | function 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 | */
|
1323 | function removeAttribute(el, attribute) {
|
1324 | el.removeAttribute(attribute);
|
1325 | }
|
1326 |
|
1327 | /**
|
1328 | * Attempt to block the ability to select text.
|
1329 | */
|
1330 | function 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 | */
|
1340 | function 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 | */
|
1365 | function 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 | */
|
1409 | function 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 | */
|
1462 | function 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 | */
|
1518 | function 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 | */
|
1531 | function 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 | */
|
1570 | function 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 | */
|
1606 | function 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 | */
|
1624 | function 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 | */
|
1637 | function 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 | */
|
1701 | const $ = 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 | */
|
1721 | const $$ = 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 | */
|
1738 | function 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 | */
|
1761 | function 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 |
|
1780 | var 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 | */
|
1820 | let _windowLoaded = false;
|
1821 | let videojs$1;
|
1822 |
|
1823 | /**
|
1824 | * Set up any tags that have a data-setup `attribute` when the player is started.
|
1825 | */
|
1826 | const 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 | */
|
1877 | function 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 | */
|
1893 | function setWindowLoaded() {
|
1894 | _windowLoaded = true;
|
1895 | window__default["default"].removeEventListener('load', setWindowLoaded);
|
1896 | }
|
1897 | if (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 | */
|
1927 | const 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 | */
|
1942 | const 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 | */
|
1965 | var 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
|
1978 | const _initialGuid = 3;
|
1979 |
|
1980 | /**
|
1981 | * Unique ID for an element or function
|
1982 | *
|
1983 | * @type {Number}
|
1984 | */
|
1985 | let _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 | */
|
1993 | function 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 | */
|
2016 | function _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 | */
|
2064 | function _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 | */
|
2080 | function 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 | */
|
2191 | let _supportsPassive;
|
2192 | const 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 | */
|
2213 | const 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 | */
|
2230 | function 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 | */
|
2303 | function 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 | */
|
2373 | function 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 | */
|
2441 | function 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 | */
|
2468 | function 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 |
|
2481 | var 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 | */
|
2495 | const 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 | */
|
2517 | const 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 | */
|
2549 | const 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 | */
|
2587 | const 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 |
|
2617 | var 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 | */
|
2628 | let 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 | */
|
2638 | class 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 | */
|
2801 | EventTarget.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 | */
|
2810 | EventTarget.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 | */
|
2819 | EventTarget.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 | */
|
2828 | EventTarget.prototype.dispatchEvent = EventTarget.prototype.trigger;
|
2829 |
|
2830 | /**
|
2831 | * @file mixins/evented.js
|
2832 | * @module evented
|
2833 | */
|
2834 | const 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 | */
|
2859 | const 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 | */
|
2869 | const 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 | */
|
2890 | const isValidEventType = type =>
|
2891 | // The regex here verifies that the `type` contains at least one non-
|
2892 | // whitespace character.
|
2893 | typeof 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 | */
|
2911 | const 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 | */
|
2933 | const 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 | */
|
2955 | const 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 | */
|
2979 | const 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 | */
|
3032 | const 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 | */
|
3047 | const 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 | */
|
3290 | function 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 | */
|
3339 | const 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 | */
|
3421 | function 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 | */
|
3449 | const 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 | */
|
3465 | const 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 | */
|
3484 | const titleCaseEquals = function (str1, str2) {
|
3485 | return toTitleCase(str1) === toTitleCase(str2);
|
3486 | };
|
3487 |
|
3488 | var 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 | */
|
3519 | class 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 | }
|
5405 | Component.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 | */
|
5460 | function 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 | */
|
5490 | function 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 | */
|
5504 | function 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 | */
|
5543 | function 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 | */
|
5567 | const 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.
|
5595 | let 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 | */
|
5605 | function setFormatTime(customImplementation) {
|
5606 | implementation = customImplementation;
|
5607 | }
|
5608 |
|
5609 | /**
|
5610 | * Resets formatTime to the default implementation.
|
5611 | */
|
5612 | function 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 | */
|
5634 | function formatTime(seconds, guide = seconds) {
|
5635 | return implementation(seconds, guide);
|
5636 | }
|
5637 |
|
5638 | var 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 | */
|
5666 | function 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 | */
|
5710 | function 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 | */
|
5739 | MediaError.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 | */
|
5747 | MediaError.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 | */
|
5758 | MediaError.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 | */
|
5773 | MediaError.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 | */
|
5788 | MediaError.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 | */
|
5796 | MediaError.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 | */
|
5811 | MediaError.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 | */
|
5820 | MediaError.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 | */
|
5829 | MediaError.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 | */
|
5838 | MediaError.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 | */
|
5847 | MediaError.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 | */
|
5856 | MediaError.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 | */
|
5865 | MediaError.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 | */
|
5874 | MediaError.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 | */
|
5883 | MediaError.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 | */
|
5892 | MediaError.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 | */
|
5901 | MediaError.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 | */
|
5910 | MediaError.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 | */
|
5921 | function 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 | */
|
5934 | function 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 | */
|
5960 | const 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 | */
|
5991 | const 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 | */
|
6017 | const 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 | };
|
6026 | var 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 |
|
6039 | const 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 | */
|
6050 | class 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 | */
|
6542 | ModalDialog.prototype.options_ = {
|
6543 | pauseOnOpen: true,
|
6544 | temporary: true
|
6545 | };
|
6546 | Component.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 | */
|
6560 | class 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 | */
|
6718 | TrackList.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
|
6726 | for (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 | */
|
6748 | const 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 | */
|
6764 | class 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 | */
|
6846 | const 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 | */
|
6862 | class 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 | */
|
6952 | class 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 | */
|
7002 | class 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 | */
|
7126 | class 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 | */
|
7214 | const 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 | */
|
7230 | const 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 | */
|
7246 | const 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 | */
|
7261 | const 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 | */
|
7281 | class 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 | */
|
7394 | const 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 | */
|
7408 | const 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 | */
|
7424 | const 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 | */
|
7448 | const isCrossOrigin = function (url, winLoc = window__default["default"].location) {
|
7449 | return parseUrl(url).origin !== winLoc.origin;
|
7450 | };
|
7451 |
|
7452 | var 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 | */
|
7477 | const 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 | */
|
7516 | const 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 | */
|
7560 | class 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 | */
|
7845 | TextTrack.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 | */
|
7856 | class 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 | */
|
7936 | class 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 | */
|
8020 | class 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 | */
|
8108 | HTMLTrackElement.prototype.allowedEvents_ = {
|
8109 | load: 'load'
|
8110 | };
|
8111 |
|
8112 | /**
|
8113 | * The text track not loaded state.
|
8114 | *
|
8115 | * @type {number}
|
8116 | * @static
|
8117 | */
|
8118 | HTMLTrackElement.NONE = 0;
|
8119 |
|
8120 | /**
|
8121 | * The text track loading state.
|
8122 | *
|
8123 | * @type {number}
|
8124 | * @static
|
8125 | */
|
8126 | HTMLTrackElement.LOADING = 1;
|
8127 |
|
8128 | /**
|
8129 | * The text track loaded state.
|
8130 | *
|
8131 | * @type {number}
|
8132 | * @static
|
8133 | */
|
8134 | HTMLTrackElement.LOADED = 2;
|
8135 |
|
8136 | /**
|
8137 | * The text track failed to load state.
|
8138 | *
|
8139 | * @type {number}
|
8140 | * @static
|
8141 | */
|
8142 | HTMLTrackElement.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 |
|
8149 | const 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 | };
|
8166 | Object.keys(NORMAL).forEach(function (type) {
|
8167 | NORMAL[type].getterName = `${type}Tracks`;
|
8168 | NORMAL[type].privateName = `${type}Tracks_`;
|
8169 | });
|
8170 | const 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 | };
|
8186 | const ALL = Object.assign({}, NORMAL, REMOTE);
|
8187 | REMOTE.names = Object.keys(REMOTE);
|
8188 | NORMAL.names = Object.keys(NORMAL);
|
8189 | ALL.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 | */
|
8235 | function 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 | */
|
8256 | class 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 |
|
9227 | ALL.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 | */
|
9265 | Tech.prototype.featuresVolumeControl = true;
|
9266 |
|
9267 | /**
|
9268 | * Boolean indicating whether the `Tech` supports muting volume.
|
9269 | *
|
9270 | * @type {boolean}
|
9271 | * @default
|
9272 | */
|
9273 | Tech.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 | */
|
9282 | Tech.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 | */
|
9293 | Tech.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 | */
|
9302 | Tech.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 | */
|
9314 | Tech.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 | */
|
9323 | Tech.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 | */
|
9332 | Tech.prototype.featuresNativeTextTracks = false;
|
9333 |
|
9334 | /**
|
9335 | * Boolean indicating whether the `Tech` supports `requestVideoFrameCallback`.
|
9336 | *
|
9337 | * @type {boolean}
|
9338 | * @default
|
9339 | */
|
9340 | Tech.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 | */
|
9354 | Tech.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.
|
9535 | Component.registerComponent('Tech', Tech);
|
9536 | Tech.registerTech('Tech', Tech);
|
9537 |
|
9538 | /**
|
9539 | * A list of techs that should be added to techOrder on Players
|
9540 | *
|
9541 | * @private
|
9542 | */
|
9543 | Tech.defaultTechOrder_ = [];
|
9544 |
|
9545 | /**
|
9546 | * @file middleware.js
|
9547 | * @module middleware
|
9548 | */
|
9549 |
|
9550 | /** @import Player from '../player' */
|
9551 | /** @import Tech from '../tech/tech' */
|
9552 |
|
9553 | const middlewares = {};
|
9554 | const middlewareInstances = {};
|
9555 | const 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 | */
|
9590 | function 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 | */
|
9609 | function 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 | */
|
9622 | function 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 | */
|
9642 | function 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 | */
|
9665 | function 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 | */
|
9692 | function 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 | */
|
9708 | const 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 | */
|
9725 | const 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 | */
|
9736 | const allowedMediators = {
|
9737 | play: 1,
|
9738 | pause: 1
|
9739 | };
|
9740 | function 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 | }
|
9752 | function 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 | */
|
9767 | function 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 | */
|
9780 | function 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 | }
|
9801 | function 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 | */
|
9850 | const 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 | */
|
9883 | const 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 | */
|
9902 | const 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 | */
|
9948 | const 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 | */
|
9984 | function 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 |
|
9994 | var 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
|
9999 | const backKeyCode = IS_TIZEN ? 10009 : IS_WEBOS ? 461 : 8;
|
10000 | const 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.
|
10041 | const 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 | */
|
10050 | class 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 | */
|
10659 | class 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 | }
|
10708 | Component.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 | */
|
10722 | class 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 | }
|
10944 | Component.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 | */
|
10957 | class 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 | */
|
11120 | PosterImage.prototype.crossorigin = PosterImage.prototype.crossOrigin;
|
11121 | Component.registerComponent('PosterImage', PosterImage);
|
11122 |
|
11123 | /**
|
11124 | * @file text-track-display.js
|
11125 | */
|
11126 |
|
11127 | /** @import Player from '../player' */
|
11128 |
|
11129 | const darkGray = '#222';
|
11130 | const lightGray = '#ccc';
|
11131 | const 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 | */
|
11156 | function 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 | */
|
11185 | function 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 | */
|
11205 | function getCSSPositionValue(position) {
|
11206 | return position ? `${position}px` : '';
|
11207 | }
|
11208 |
|
11209 | /**
|
11210 | * The component for displaying text track cues.
|
11211 | *
|
11212 | * @extends Component
|
11213 | */
|
11214 | class 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 | }
|
11588 | Component.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 | */
|
11599 | class 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 | }
|
11628 | Component.registerComponent('LoadingSpinner', LoadingSpinner);
|
11629 |
|
11630 | /**
|
11631 | * @file button.js
|
11632 | */
|
11633 |
|
11634 | /**
|
11635 | * Base class for all buttons.
|
11636 | *
|
11637 | * @extends ClickableComponent
|
11638 | */
|
11639 | class 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 | }
|
11745 | Component.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 | */
|
11757 | class 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 | */
|
11844 | BigPlayButton.prototype.controlText_ = 'Play Video';
|
11845 | Component.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 | */
|
11859 | class 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 | }
|
11937 | Component.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 | */
|
11950 | class 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 | */
|
12078 | PlayToggle.prototype.controlText_ = 'Play';
|
12079 | Component.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 | */
|
12092 | class 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 | */
|
12209 | TimeDisplay.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 | */
|
12219 | TimeDisplay.prototype.controlText_ = 'Time';
|
12220 | Component.registerComponent('TimeDisplay', TimeDisplay);
|
12221 |
|
12222 | /**
|
12223 | * @file current-time-display.js
|
12224 | */
|
12225 |
|
12226 | /**
|
12227 | * Displays the current time
|
12228 | *
|
12229 | * @extends Component
|
12230 | */
|
12231 | class 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 | */
|
12268 | CurrentTimeDisplay.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 | */
|
12278 | CurrentTimeDisplay.prototype.controlText_ = 'Current Time';
|
12279 | Component.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 | */
|
12292 | class 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 | */
|
12355 | DurationDisplay.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 | */
|
12365 | DurationDisplay.prototype.controlText_ = 'Duration';
|
12366 | Component.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 | */
|
12378 | class 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 | }
|
12403 | Component.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 | */
|
12416 | class 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 | */
|
12491 | RemainingTimeDisplay.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 | */
|
12501 | RemainingTimeDisplay.prototype.controlText_ = 'Remaining Time';
|
12502 | Component.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 | */
|
12517 | class 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 | }
|
12578 | Component.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 | */
|
12591 | class 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 | */
|
12674 | SeekToLive.prototype.controlText_ = 'Seek to live, currently playing live';
|
12675 | Component.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 | */
|
12696 | function clamp(number, min, max) {
|
12697 | number = Number(number);
|
12698 | return Math.min(max, Math.max(min, isNaN(number) ? min : number));
|
12699 | }
|
12700 |
|
12701 | var 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 | */
|
12718 | class 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 | }
|
13056 | Component.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
|
13065 | const percentify = (time, end) => clamp(time / end * 100, 0, 100).toFixed(2) + '%';
|
13066 |
|
13067 | /**
|
13068 | * Shows loading progress
|
13069 | *
|
13070 | * @extends Component
|
13071 | */
|
13072 | class 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 | }
|
13175 | Component.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 | */
|
13188 | class 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 | }
|
13334 | Component.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 | */
|
13346 | class 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 | */
|
13403 | PlayProgressBar.prototype.options_ = {
|
13404 | children: []
|
13405 | };
|
13406 |
|
13407 | // Time tooltips should not be added to a player on mobile devices
|
13408 | if (!IS_IOS && !IS_ANDROID) {
|
13409 | PlayProgressBar.prototype.options_.children.push('timeTooltip');
|
13410 | }
|
13411 | Component.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 | */
|
13425 | class 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 | */
|
13477 | MouseTimeDisplay.prototype.options_ = {
|
13478 | children: ['timeTooltip']
|
13479 | };
|
13480 | Component.registerComponent('MouseTimeDisplay', MouseTimeDisplay);
|
13481 |
|
13482 | /**
|
13483 | * @file seek-bar.js
|
13484 | */
|
13485 |
|
13486 | // The number of seconds the `step*` functions move the timeline.
|
13487 | const STEP_SECONDS = 5;
|
13488 |
|
13489 | // The multiplier of STEP_SECONDS that PgUp/PgDown move the timeline.
|
13490 | const 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 | */
|
13498 | class 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 | */
|
13959 | SeekBar.prototype.options_ = {
|
13960 | children: ['loadProgressBar', 'playProgressBar'],
|
13961 | barName: 'playProgressBar'
|
13962 | };
|
13963 | Component.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 | */
|
13975 | class 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 | */
|
14172 | ProgressControl.prototype.options_ = {
|
14173 | children: ['seekBar']
|
14174 | };
|
14175 | Component.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 | */
|
14188 | class 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 | */
|
14316 | PictureInPictureToggle.prototype.controlText_ = 'Picture-in-Picture';
|
14317 | Component.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 | */
|
14330 | class 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 | */
|
14404 | FullscreenToggle.prototype.controlText_ = 'Fullscreen';
|
14405 | Component.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 | */
|
14422 | const 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 | */
|
14445 | class 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 | }
|
14463 | Component.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 | */
|
14476 | class 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 | }
|
14587 | Component.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 | */
|
14601 | class 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 | */
|
14662 | MouseVolumeLevelDisplay.prototype.options_ = {
|
14663 | children: ['volumeLevelTooltip']
|
14664 | };
|
14665 | Component.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 | */
|
14676 | class 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 | */
|
14837 | VolumeBar.prototype.options_ = {
|
14838 | children: ['volumeLevel'],
|
14839 | barName: 'volumeLevel'
|
14840 | };
|
14841 |
|
14842 | // MouseVolumeLevelDisplay tooltip should not be added to a player on mobile devices
|
14843 | if (!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 | */
|
14852 | VolumeBar.prototype.playerEvent = 'volumechange';
|
14853 | Component.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 | */
|
14864 | class 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 | */
|
14977 | VolumeControl.prototype.options_ = {
|
14978 | children: ['volumeBar']
|
14979 | };
|
14980 | Component.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 | */
|
14997 | const 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 | */
|
15022 | class 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 | */
|
15147 | MuteToggle.prototype.controlText_ = 'Mute';
|
15148 | Component.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 | */
|
15160 | class 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 | */
|
15332 | VolumePanel.prototype.options_ = {
|
15333 | children: ['muteToggle', 'volumeControl']
|
15334 | };
|
15335 | Component.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 | */
|
15345 | class 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 | }
|
15400 | SkipForward.prototype.controlText_ = 'Skip Forward';
|
15401 | Component.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 | */
|
15411 | class 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 | }
|
15465 | SkipBackward.prototype.controlText_ = 'Skip Backward';
|
15466 | Component.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 | */
|
15480 | class 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 | }
|
15717 | Component.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 | */
|
15730 | class 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 | }
|
16114 | Component.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 | */
|
16127 | class 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 | }
|
16158 | Component.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 | */
|
16171 | class 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 | }
|
16293 | Component.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 | */
|
16306 | class 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 | }
|
16452 | Component.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 | */
|
16465 | class 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 | }
|
16552 | Component.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 | */
|
16565 | class 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 | }
|
16628 | Component.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 | */
|
16641 | class 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 | }
|
16682 | Component.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 | */
|
16700 | class 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 | */
|
16872 | ChaptersButton.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 | */
|
16880 | ChaptersButton.prototype.controlText_ = 'Chapters';
|
16881 | Component.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 | */
|
16894 | class 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 | */
|
16967 | DescriptionsButton.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 | */
|
16975 | DescriptionsButton.prototype.controlText_ = 'Descriptions';
|
16976 | Component.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 | */
|
16989 | class 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 | */
|
17027 | SubtitlesButton.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 | */
|
17035 | SubtitlesButton.prototype.controlText_ = 'Subtitles';
|
17036 | Component.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 | */
|
17049 | class 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 | }
|
17100 | Component.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 | */
|
17113 | class 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 | */
|
17168 | CaptionsButton.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 | */
|
17176 | CaptionsButton.prototype.controlText_ = 'Captions';
|
17177 | Component.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 | */
|
17189 | class 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 | }
|
17213 | Component.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 | */
|
17226 | class 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 | */
|
17291 | SubsCapsButton.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 | */
|
17300 | SubsCapsButton.prototype.controlText_ = 'Subtitles';
|
17301 | Component.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 | */
|
17314 | class 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 | }
|
17404 | Component.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 | */
|
17415 | class 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 | */
|
17477 | AudioTrackButton.prototype.controlText_ = 'Audio Track';
|
17478 | Component.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 | */
|
17491 | class 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 | */
|
17551 | PlaybackRateMenuItem.prototype.contentElType = 'button';
|
17552 | Component.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 | */
|
17565 | class 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 | */
|
17705 | PlaybackRateMenuButton.prototype.controlText_ = 'Playback Rate';
|
17706 | Component.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 | */
|
17718 | class 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 | }
|
17742 | Component.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 | */
|
17753 | class 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 | }
|
17779 | Component.registerComponent('CustomControlSpacer', CustomControlSpacer);
|
17780 |
|
17781 | /**
|
17782 | * @file control-bar.js
|
17783 | */
|
17784 |
|
17785 | /**
|
17786 | * Container of main controls.
|
17787 | *
|
17788 | * @extends Component
|
17789 | */
|
17790 | class 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 | */
|
17811 | ControlBar.prototype.options_ = {
|
17812 | children: ['playToggle', 'skipBackward', 'skipForward', 'volumePanel', 'currentTimeDisplay', 'timeDivider', 'durationDisplay', 'progressControl', 'liveDisplay', 'seekToLive', 'remainingTimeDisplay', 'customControlSpacer', 'playbackRateMenuButton', 'chaptersButton', 'descriptionsButton', 'subsCapsButton', 'audioTrackButton', 'pictureInPictureToggle', 'fullscreenToggle']
|
17813 | };
|
17814 | Component.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 | */
|
17828 | class 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 | */
|
17874 | ErrorDisplay.prototype.options_ = Object.assign({}, ModalDialog.prototype.options_, {
|
17875 | pauseOnOpen: false,
|
17876 | fillAlways: true,
|
17877 | temporary: false,
|
17878 | uncloseable: true
|
17879 | });
|
17880 | Component.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 | */
|
17890 | class 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 | }
|
17946 | Component.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 | */
|
17959 | class 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 | }
|
18064 | Component.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 | */
|
18075 | class 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 | }
|
18151 | Component.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 | */
|
18162 | class 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 | }
|
18232 | Component.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 | */
|
18243 | class 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 | }
|
18280 | Component.registerComponent('TrackSettingsControls', TrackSettingsControls);
|
18281 |
|
18282 | /**
|
18283 | * @file text-track-settings.js
|
18284 | */
|
18285 |
|
18286 | /** @import Player from '../player' */
|
18287 |
|
18288 | const LOCAL_STORAGE_KEY = 'vjs-text-track-settings';
|
18289 | const COLOR_BLACK = ['#000', 'Black'];
|
18290 | const COLOR_BLUE = ['#00F', 'Blue'];
|
18291 | const COLOR_CYAN = ['#0FF', 'Cyan'];
|
18292 | const COLOR_GREEN = ['#0F0', 'Green'];
|
18293 | const COLOR_MAGENTA = ['#F0F', 'Magenta'];
|
18294 | const COLOR_RED = ['#F00', 'Red'];
|
18295 | const COLOR_WHITE = ['#FFF', 'White'];
|
18296 | const COLOR_YELLOW = ['#FF0', 'Yellow'];
|
18297 | const OPACITY_OPAQUE = ['1', 'Opaque'];
|
18298 | const OPACITY_SEMI = ['0.5', 'Semi-Transparent'];
|
18299 | const 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.
|
18312 | const 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 | };
|
18377 | selectConfigs.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 | */
|
18395 | function 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 | */
|
18420 | function 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 | */
|
18440 | function 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 | */
|
18457 | class 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 | }
|
18626 | Component.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 | */
|
18649 | class 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 | }
|
18758 | Component.registerComponent('ResizeManager', ResizeManager);
|
18759 |
|
18760 | /** @import Player from './player' */
|
18761 |
|
18762 | const 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 | */
|
18774 | class 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 | }
|
19111 | Component.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 | */
|
19122 | class 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 | }
|
19233 | Component.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} */
|
19248 | const 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 | */
|
19270 | class 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 | }
|
19347 | Component.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 | */
|
19364 | const 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 | */
|
19420 | const 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 | */
|
19456 | const 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 | };
|
19468 | const 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 | */
|
19488 | const 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 | */
|
19533 | const 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 | });
|
19545 | const 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 | */
|
19562 | const 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 | */
|
19630 | class 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 | */
|
20560 | defineLazyProperty(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 | */
|
20580 | Html5.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 | */
|
20597 | Html5.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 | */
|
20610 | Html5.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 | */
|
20623 | Html5.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 | */
|
20660 | Html5.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 | */
|
20685 | Html5.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 | */
|
20709 | Html5.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 | */
|
20743 | Html5.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 | */
|
20754 | Html5.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 | */
|
20765 | Html5.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 | */
|
20775 | Html5.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 | });
|
20827 | Html5.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 | */
|
20837 | Html5.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 | */
|
20848 | Html5.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 | */
|
20857 | Html5.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 | */
|
20865 | Html5.prototype.featuresTimeupdateEvents = true;
|
20866 |
|
20867 | /**
|
20868 | * Whether the HTML5 el supports `requestVideoFrameCallback`
|
20869 | *
|
20870 | * @type {boolean}
|
20871 | */
|
20872 | Html5.prototype.featuresVideoFrameCallback = !!(Html5.TEST_VID && Html5.TEST_VID.requestVideoFrameCallback);
|
20873 | Html5.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 | };
|
20903 | Html5.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 | });
|
21516 | Tech.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 | */
|
21527 | Html5.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 | */
|
21538 | Html5.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 | */
|
21559 | Html5.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 | */
|
21584 | Html5.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 | */
|
21591 | Html5.nativeSourceHandler.dispose = function () {};
|
21592 |
|
21593 | // Register the native source handler
|
21594 | Html5.registerSourceHandler(Html5.nativeSourceHandler);
|
21595 | Tech.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
|
21612 | const 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
|
21782 | const TECH_EVENTS_QUEUE = {
|
21783 | canplay: 'CanPlay',
|
21784 | canplaythrough: 'CanPlayThrough',
|
21785 | playing: 'Playing',
|
21786 | seeked: 'Seeked'
|
21787 | };
|
21788 | const BREAKPOINT_ORDER = ['tiny', 'xsmall', 'small', 'medium', 'large', 'xlarge', 'huge'];
|
21789 | const 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
|
21798 | BREAKPOINT_ORDER.forEach(k => {
|
21799 | const v = k.charAt(0) === 'x' ? `x-${k.substring(1)}` : k;
|
21800 | BREAKPOINT_CLASSES[k] = `vjs-layout-${v}`;
|
21801 | });
|
21802 | const 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 | */
|
21824 | class 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 |
|
26633 | ALL.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 | */
|
26662 | Player.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 | */
|
26672 | Player.players = {};
|
26673 | const 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 | */
|
26683 | Player.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 | };
|
26721 | TECH_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 |
|
26757 | Component.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 | */
|
26770 | const 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 | */
|
26779 | const PLUGIN_CACHE_KEY = 'activePlugins_';
|
26780 |
|
26781 | /**
|
26782 | * Stores registered plugins in a private space.
|
26783 | *
|
26784 | * @private
|
26785 | * @type {Object}
|
26786 | */
|
26787 | const 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 | */
|
26799 | const 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 | */
|
26811 | const 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 | */
|
26825 | const 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 | */
|
26844 | const 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 | */
|
26864 | const 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 | */
|
26908 | const 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 | */
|
26941 | class 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 | */
|
27193 | Plugin.getPlugin = getPlugin;
|
27194 |
|
27195 | /**
|
27196 | * The name of the base plugin class as it is registered.
|
27197 | *
|
27198 | * @type {string}
|
27199 | */
|
27200 | Plugin.BASE_PLUGIN_NAME = BASE_PLUGIN_NAME;
|
27201 | Plugin.registerPlugin(BASE_PLUGIN_NAME, Plugin);
|
27202 |
|
27203 | /**
|
27204 | * Documented in player.js
|
27205 | *
|
27206 | * @ignore
|
27207 | */
|
27208 | Player.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 | */
|
27217 | Player.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 | */
|
27286 | function 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 | */
|
27307 | function 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 |
|
27311 | var 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 | */
|
27345 | const 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 | */
|
27415 | function 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 | }
|
27467 | videojs.hooks_ = hooks_;
|
27468 | videojs.hooks = hooks;
|
27469 | videojs.hook = hook;
|
27470 | videojs.hookOnce = hookOnce;
|
27471 | videojs.removeHook = removeHook;
|
27472 |
|
27473 | // Add default styles
|
27474 | if (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)
|
27498 | autoSetupTimeout(1, videojs);
|
27499 |
|
27500 | /**
|
27501 | * Current Video.js version. Follows [semantic versioning](https://semver.org/).
|
27502 | *
|
27503 | * @type {string}
|
27504 | */
|
27505 | videojs.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 | */
|
27513 | videojs.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 | */
|
27521 | videojs.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 | */
|
27537 | videojs.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 | */
|
27573 | videojs.getAllPlayers = () =>
|
27574 | // Disposed players leave a key with a `null` value, so we need to make sure
|
27575 | // we filter those out.
|
27576 | Object.keys(Player.players).map(k => Player.players[k]).filter(Boolean);
|
27577 | videojs.players = Player.players;
|
27578 | videojs.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 | */
|
27597 | videojs.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 | };
|
27603 | videojs.getTech = Tech.getTech;
|
27604 | videojs.registerTech = Tech.registerTech;
|
27605 | videojs.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 | */
|
27614 | Object.defineProperty(videojs, 'middleware', {
|
27615 | value: {},
|
27616 | writeable: false,
|
27617 | enumerable: true
|
27618 | });
|
27619 | Object.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 | */
|
27631 | videojs.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 | */
|
27639 | videojs.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 | */
|
27648 | videojs.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 | */
|
27657 | videojs.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 | */
|
27666 | videojs.bind = deprecateForMajor(9, 'videojs.bind', 'native Function.prototype.bind', bind_);
|
27667 | videojs.registerPlugin = Plugin.registerPlugin;
|
27668 | videojs.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 | */
|
27683 | videojs.plugin = (name, plugin) => {
|
27684 | log.warn('videojs.plugin() is deprecated; use videojs.registerPlugin() instead');
|
27685 | return Plugin.registerPlugin(name, plugin);
|
27686 | };
|
27687 | videojs.getPlugins = Plugin.getPlugins;
|
27688 | videojs.getPlugin = Plugin.getPlugin;
|
27689 | videojs.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 | */
|
27704 | videojs.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 | */
|
27718 | videojs.log = log;
|
27719 | videojs.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 | */
|
27727 | videojs.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 | */
|
27736 | videojs.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 | */
|
27745 | videojs.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 | */
|
27754 | videojs.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 | */
|
27763 | videojs.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 | */
|
27772 | videojs.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 | */
|
27781 | videojs.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 | */
|
27790 | videojs.isCrossOrigin = deprecateForMajor(9, 'videojs.isCrossOrigin', 'videojs.url.isCrossOrigin', isCrossOrigin);
|
27791 | videojs.EventTarget = EventTarget;
|
27792 | videojs.any = any;
|
27793 | videojs.on = on;
|
27794 | videojs.one = one;
|
27795 | videojs.off = off;
|
27796 | videojs.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 | */
|
27810 | videojs.xhr = XHR__default["default"];
|
27811 | videojs.TextTrack = TextTrack;
|
27812 | videojs.AudioTrack = AudioTrack;
|
27813 | videojs.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 | });
|
27820 | videojs.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 | */
|
27828 | videojs.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 | */
|
27836 | videojs.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 | */
|
27844 | videojs.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 | */
|
27852 | videojs.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 | */
|
27860 | videojs.url = Url;
|
27861 |
|
27862 | // The list of possible error types to occur in video.js
|
27863 | videojs.Error = VjsErrors;
|
27864 |
|
27865 | module.exports = videojs;
|