vendor: 12,711 bytes, lines 1-444
1/** 2 * @license 3 * Video.js 8.20.0 <http://videojs.com/> 4 * Copyright Brightcove, Inc. <https://www.brightcove.com/> 5 * Available under Apache License Version 2.0 6 * <https://github.com/videojs/video.js/blob/main/LICENSE> 7 * 8 * Includes vtt.js <https://github.com/mozilla/vtt.js> 9 * Available under Apache License Version 2.0 10 * <https://github.com/mozilla/vtt.js/blob/main/LICENSE> 11 */ 12 13'use strict'; 14 15var window$1 = require('global/window'); 16var document$1 = require('global/document'); 17var XHR = require('@videojs/xhr'); 18var vtt = require('videojs-vtt.js'); 19var _extends = require('@babel/runtime/helpers/extends'); 20var _resolveUrl = require('@videojs/vhs-utils/cjs/resolve-url.js'); 21var m3u8Parser = require('m3u8-parser'); 22var codecs_js = require('@videojs/vhs-utils/cjs/codecs.js'); 23var mediaTypes_js = require('@videojs/vhs-utils/cjs/media-types.js'); 24var byteHelpers = require('@videojs/vhs-utils/cjs/byte-helpers'); 25var mpdParser = require('mpd-parser'); 26var parseSidx = require('mux.js/lib/tools/parse-sidx'); 27var id3Helpers = require('@videojs/vhs-utils/cjs/id3-helpers'); 28var containers = require('@videojs/vhs-utils/cjs/containers'); 29var clock = require('mux.js/lib/utils/clock'); 30 31function _interopDefaultLegacy(e) { 32 return e && typeof e === 'object' && 'default' in e ? e : { default: e }; 33} 34 35var window__default = /*#__PURE__*/ _interopDefaultLegacy(window$1); 36var document__default = /*#__PURE__*/ _interopDefaultLegacy(document$1); 37var XHR__default = /*#__PURE__*/ _interopDefaultLegacy(XHR); 38var vtt__default = /*#__PURE__*/ _interopDefaultLegacy(vtt); 39var _extends__default = /*#__PURE__*/ _interopDefaultLegacy(_extends); 40var _resolveUrl__default = /*#__PURE__*/ _interopDefaultLegacy(_resolveUrl); 41var parseSidx__default = /*#__PURE__*/ _interopDefaultLegacy(parseSidx); 42 43var version$6 = '8.20.0'; 44 45/** 46 * An Object that contains lifecycle hooks as keys which point to an array 47 * of functions that are run when a lifecycle is triggered 48 * 49 * @private 50 */ 51const hooks_ = {}; 52 53/** 54 * Get a list of hooks for a specific lifecycle 55 * 56 * @param {string} type 57 * the lifecycle to get hooks from 58 * 59 * @param {Function|Function[]} [fn] 60 * Optionally add a hook (or hooks) to the lifecycle that your are getting. 61 * 62 * @return {Array} 63 * an array of hooks, or an empty array if there are none. 64 */ 65const hooks = function (type, fn) { 66 hooks_[type] = hooks_[type] || []; 67 if (fn) { 68 hooks_[type] = hooks_[type].concat(fn); 69 } 70 return hooks_[type]; 71}; 72 73/** 74 * Add a function hook to a specific videojs lifecycle. 75 * 76 * @param {string} type 77 * the lifecycle to hook the function to. 78 * 79 * @param {Function|Function[]} 80 * The function or array of functions to attach. 81 */ 82const hook = function (type, fn) { 83 hooks(type, fn); 84}; 85 86/** 87 * Remove a hook from a specific videojs lifecycle. 88 * 89 * @param {string} type 90 * the lifecycle that the function hooked to 91 * 92 * @param {Function} fn 93 * The hooked function to remove 94 * 95 * @return {boolean} 96 * The function that was removed or undef 97 */ 98const removeHook = function (type, fn) { 99 const index = hooks(type).indexOf(fn); 100 if (index <= -1) { 101 return false; 102 } 103 hooks_[type] = hooks_[type].slice(); 104 hooks_[type].splice(index, 1); 105 return true; 106}; 107 108/** 109 * Add a function hook that will only run once to a specific videojs lifecycle. 110 * 111 * @param {string} type 112 * the lifecycle to hook the function to. 113 * 114 * @param {Function|Function[]} 115 * The function or array of functions to attach. 116 */ 117const hookOnce = function (type, fn) { 118 hooks( 119 type, 120 [].concat(fn).map((original) => { 121 const wrapper = (...args) => { 122 removeHook(type, wrapper); 123 return original(...args); 124 }; 125 return wrapper; 126 }) 127 ); 128}; 129 130/** 131 * @file fullscreen-api.js 132 * @module fullscreen-api 133 */ 134 135/** 136 * Store the browser-specific methods for the fullscreen API. 137 * 138 * @type {Object} 139 * @see [Specification]{@link https://fullscreen.spec.whatwg.org} 140 * @see [Map Approach From Screenfull.js]{@link https://github.com/sindresorhus/screenfull.js} 141 */ 142const FullscreenApi = { 143 prefixed: true, 144}; 145 146// browser API methods 147const apiMap = [ 148 [ 149 'requestFullscreen', 150 'exitFullscreen', 151 'fullscreenElement', 152 'fullscreenEnabled', 153 'fullscreenchange', 154 'fullscreenerror', 155 'fullscreen', 156 ], 157 // WebKit 158 [ 159 'webkitRequestFullscreen', 160 'webkitExitFullscreen', 161 'webkitFullscreenElement', 162 'webkitFullscreenEnabled', 163 'webkitfullscreenchange', 164 'webkitfullscreenerror', 165 '-webkit-full-screen', 166 ], 167]; 168const specApi = apiMap[0]; 169let browserApi; 170 171// determine the supported set of functions 172for (let i = 0; i < apiMap.length; i++) { 173 // check for exitFullscreen function 174 if (apiMap[i][1] in document__default['default']) { 175 browserApi = apiMap[i]; 176 break; 177 } 178} 179 180// map the browser API names to the spec API names 181if (browserApi) { 182 for (let i = 0; i < browserApi.length; i++) { 183 FullscreenApi[specApi[i]] = browserApi[i]; 184 } 185 FullscreenApi.prefixed = browserApi[0] !== specApi[0]; 186} 187 188/** 189 * @file create-logger.js 190 * @module create-logger 191 */ 192 193// This is the private tracking variable for the logging history. 194let history = []; 195 196/** 197 * Log messages to the console and history based on the type of message 198 * 199 * @private 200 * @param {string} name 201 * The name of the console method to use. 202 * 203 * @param {Object} log 204 * The arguments to be passed to the matching console method. 205 * 206 * @param {string} [styles] 207 * styles for name 208 */ 209const LogByTypeFactory = (name, log, styles) => (type, level, args) => { 210 const lvl = log.levels[level]; 211 const lvlRegExp = new RegExp(`^(${lvl})$`); 212 let resultName = name; 213 if (type !== 'log') { 214 // Add the type to the front of the message when it's not "log". 215 args.unshift(type.toUpperCase() + ':'); 216 } 217 if (styles) { 218 resultName = `%c${name}`; 219 args.unshift(styles); 220 } 221 222 // Add console prefix after adding to history. 223 args.unshift(resultName + ':'); 224 225 // Add a clone of the args at this point to history. 226 if (history) { 227 history.push([].concat(args)); 228 229 // only store 1000 history entries 230 const splice = history.length - 1000; 231 history.splice(0, splice > 0 ? splice : 0); 232 } 233 234 // If there's no console then don't try to output messages, but they will 235 // still be stored in history. 236 if (!window__default['default'].console) { 237 return; 238 } 239 240 // Was setting these once outside of this function, but containing them 241 // in the function makes it easier to test cases where console doesn't exist 242 // when the module is executed. 243 let fn = window__default['default'].console[type]; 244 if (!fn && type === 'debug') { 245 // Certain browsers don't have support for console.debug. For those, we 246 // should default to the closest comparable log. 247 fn = window__default['default'].console.info || window__default['default'].console.log; 248 } 249 250 // Bail out if there's no console or if this type is not allowed by the 251 // current logging level. 252 if (!fn || !lvl || !lvlRegExp.test(type)) { 253 return; 254 } 255 fn[Array.isArray(args) ? 'apply' : 'call'](window__default['default'].console, args); 256}; 257function createLogger$1(name, delimiter = ':', styles = '') { 258 // This is the private tracking variable for logging level. 259 let level = 'info'; 260 261 // the curried logByType bound to the specific log and history 262 let logByType; 263 264 /** 265 * Logs plain debug messages. Similar to `console.log`. 266 * 267 * Due to [limitations](https://github.com/jsdoc3/jsdoc/issues/955#issuecomment-313829149) 268 * of our JSDoc template, we cannot properly document this as both a function 269 * and a namespace, so its function signature is documented here. 270 * 271 * #### Arguments 272 * ##### *args 273 * *[] 274 * 275 * Any combination of values that could be passed to `console.log()`. 276 * 277 * #### Return Value 278 * 279 * `undefined` 280 * 281 * @namespace 282 * @param {...*} args 283 * One or more messages or objects that should be logged. 284 */ 285 function log(...args) { 286 logByType('log', level, args); 287 } 288 289 // This is the logByType helper that the logging methods below use 290 logByType = LogByTypeFactory(name, log, styles); 291 292 /** 293 * Create a new subLogger which chains the old name to the new name. 294 * 295 * For example, doing `mylogger = videojs.log.createLogger('player')` and then using that logger will log the following: 296 * ```js 297 * mylogger('foo'); 298 * // > VIDEOJS: player: foo 299 * ``` 300 * 301 * @param {string} subName 302 * The name to add call the new logger 303 * @param {string} [subDelimiter] 304 * Optional delimiter 305 * @param {string} [subStyles] 306 * Optional styles 307 * @return {Object} 308 */ 309 log.createLogger = (subName, subDelimiter, subStyles) => { 310 const resultDelimiter = subDelimiter !== undefined ? subDelimiter : delimiter; 311 const resultStyles = subStyles !== undefined ? subStyles : styles; 312 const resultName = `${name} ${resultDelimiter} ${subName}`; 313 return createLogger$1(resultName, resultDelimiter, resultStyles); 314 }; 315 316 /** 317 * Create a new logger. 318 * 319 * @param {string} newName 320 * The name for the new logger 321 * @param {string} [newDelimiter] 322 * Optional delimiter 323 * @param {string} [newStyles] 324 * Optional styles 325 * @return {Object} 326 */ 327 log.createNewLogger = (newName, newDelimiter, newStyles) => { 328 return createLogger$1(newName, newDelimiter, newStyles); 329 }; 330 331 /** 332 * Enumeration of available logging levels, where the keys are the level names 333 * and the values are `|`-separated strings containing logging methods allowed 334 * in that logging level. These strings are used to create a regular expression 335 * matching the function name being called. 336 * 337 * Levels provided by Video.js are: 338 * 339 * - `off`: Matches no calls. Any value that can be cast to `false` will have 340 * this effect. The most restrictive. 341 * - `all`: Matches only Video.js-provided functions (`debug`, `log`, 342 * `log.warn`, and `log.error`). 343 * - `debug`: Matches `log.debug`, `log`, `log.warn`, and `log.error` calls. 344 * - `info` (default): Matches `log`, `log.warn`, and `log.error` calls. 345 * - `warn`: Matches `log.warn` and `log.error` calls. 346 * - `error`: Matches only `log.error` calls. 347 * 348 * @type {Object} 349 */ 350 log.levels = { 351 all: 'debug|log|warn|error', 352 off: '', 353 debug: 'debug|log|warn|error', 354 info: 'log|warn|error', 355 warn: 'warn|error', 356 error: 'error', 357 DEFAULT: level, 358 }; 359 360 /** 361 * Get or set the current logging level. 362 * 363 * If a string matching a key from {@link module:log.levels} is provided, acts 364 * as a setter. 365 * 366 * @param {'all'|'debug'|'info'|'warn'|'error'|'off'} [lvl] 367 * Pass a valid level to set a new logging level. 368 * 369 * @return {string} 370 * The current logging level. 371 */ 372 log.level = (lvl) => { 373 if (typeof lvl === 'string') { 374 if (!log.levels.hasOwnProperty(lvl)) { 375 throw new Error(`"${lvl}" in not a valid log level`); 376 } 377 level = lvl; 378 } 379 return level; 380 }; 381 382 /** 383 * Returns an array containing everything that has been logged to the history. 384 * 385 * This array is a shallow clone of the internal history record. However, its 386 * contents are _not_ cloned; so, mutating objects inside this array will 387 * mutate them in history. 388 * 389 * @return {Array} 390 */ 391 log.history = () => (history ? [].concat(history) : []); 392 393 /** 394 * Allows you to filter the history by the given logger name 395 * 396 * @param {string} fname 397 * The name to filter by 398 * 399 * @return {Array} 400 * The filtered list to return 401 */ 402 log.history.filter = (fname) => { 403 return (history || []).filter((historyItem) => { 404 // if the first item in each historyItem includes `fname`, then it's a match 405 return new RegExp(`.*${fname}.*`).test(historyItem[0]); 406 }); 407 }; 408 409 /** 410 * Clears the internal history tracking, but does not prevent further history 411 * tracking. 412 */ 413 log.history.clear = () => { 414 if (history) { 415 history.length = 0; 416 } 417 }; 418 419 /** 420 * Disable history tracking if it is currently enabled. 421 */ 422 log.history.disable = () => { 423 if (history !== null) { 424 history.length = 0; 425 history = null; 426 } 427 }; 428 429 /** 430 * Enable history tracking if it is currently disabled. 431 */ 432 log.history.enable = () => { 433 if (history === null) { 434 history = []; 435 } 436 }; 437 438 /** 439 * Logs error messages. Similar to `console.error`. 440 * 441 * @param {...*} args 442 * One or more messages or objects that should be logged as an error 443 */ 444 log.error = (...args) =>
444 logByType('error', level, args); 445 446 /** 447 * Logs warning messages. Similar to `console.warn`. 448 * 449 * @param {...*} args 450 * One or more messages or objects that should be logged as a warning. 451 */ 452 log.warn = (...args) => logByType('warn', level, args); 453 454 /** 455 * Logs debug messages. Similar to `console.debug`, but may also act as a comparable 456 * log if `console.debug` is not available 457 * 458 * @param {...*} args 459 * One or more messages or objects that should be logged as debug. 460 */ 461 log.debug = (...args) => logByType('debug', level, args); 462 return log; 463} 464 465/** 466 * @file log.js 467 * @module log 468 */ 469const log$1 = createLogger$1('VIDEOJS'); 470const createLogger = log$1.createLogger; 471 472/** 473 * @file obj.js 474 * @module obj 475 */ 476 477/** 478 * @callback obj:EachCallback 479 * 480 * @param {*} value 481 * The current key for the object that is being iterated over. 482 * 483 * @param {string} key 484 * The current key-value for object that is being iterated over 485 */ 486 487/** 488 * @callback obj:ReduceCallback 489 * 490 * @param {*} accum 491 * The value that is accumulating over the reduce loop. 492 * 493 * @param {*} value 494 * The current key for the object that is being iterated over. 495 * 496 * @param {string} key 497 * The current key-value for object that is being iterated over 498 * 499 * @return {*} 500 * The new accumulated value. 501 */ 502const toString = Object.prototype.toString; 503 504/** 505 * Get the keys of an Object 506 * 507 * @param {Object} 508 * The Object to get the keys from 509 * 510 * @return {string[]} 511 * An array of the keys from the object. Returns an empty array if the 512 * object passed in was invalid or had no keys. 513 * 514 * @private 515 */ 516const keys = function (object) { 517 return isObject(object) ? Object.keys(object) : []; 518}; 519 520/** 521 * Array-like iteration for objects. 522 * 523 * @param {Object} object 524 * The object to iterate over 525 * 526 * @param {obj:EachCallback} fn 527 * The callback function which is called for each key in the object. 528 */ 529function each(object, fn) { 530 keys(object).forEach((key) => fn(object[key], key)); 531} 532 533/** 534 * Array-like reduce for objects. 535 * 536 * @param {Object} object 537 * The Object that you want to reduce. 538 * 539 * @param {Function} fn 540 * A callback function which is called for each key in the object. It 541 * receives the accumulated value and the per-iteration value and key 542 * as arguments. 543 * 544 * @param {*} [initial = 0] 545 * Starting value 546 * 547 * @return {*} 548 * The final accumulated value. 549 */ 550function reduce(object, fn, initial = 0) { 551 return keys(object).reduce((accum, key) => fn(accum, object[key], key), initial); 552} 553 554/** 555 * Returns whether a value is an object of any kind - including DOM nodes, 556 * arrays, regular expressions, etc. Not functions, though. 557 * 558 * This avoids the gotcha where using `typeof` on a `null` value 559 * results in `'object'`. 560 * 561 * @param {Object} value 562 * @return {boolean} 563 */ 564function isObject(value) { 565 return !!value && typeof value === 'object'; 566} 567 568/** 569 * Returns whether an object appears to be a "plain" object - that is, a 570 * direct instance of `Object`. 571 * 572 * @param {Object} value 573 * @return {boolean} 574 */ 575function isPlain(value) { 576 return ( 577 isObject(value) && toString.call(value) === '[object Object]' && value.constructor === Object 578 ); 579} 580 581/** 582 * Merge two objects recursively. 583 * 584 * Performs a deep merge like 585 * {@link https://lodash.com/docs/4.17.10#merge|lodash.merge}, but only merges 586 * plain objects (not arrays, elements, or anything else). 587 * 588 * Non-plain object values will be copied directly from the right-most 589 * argument. 590 * 591 * @param {Object[]} sources 592 * One or more objects to merge into a new object. 593 * 594 * @return {Object} 595 * A new object that is the merged result of all sources. 596 */ 597function merge$1(...sources) { 598 const result = {}; 599 sources.forEach((source) => { 600 if (!source) { 601 return; 602 } 603 each(source, (value, key) => { 604 if (!isPlain(value)) { 605 result[key] = value; 606 return; 607 } 608 if (!isPlain(result[key])) { 609 result[key] = {}; 610 } 611 result[key] = merge$1(result[key], value); 612 }); 613 }); 614 return result; 615} 616 617/** 618 * Returns an array of values for a given object 619 * 620 * @param {Object} source - target object 621 * @return {Array<unknown>} - object values 622 */ 623function values(source = {}) { 624 const result = []; 625 for (const key in source) { 626 if (source.hasOwnProperty(key)) { 627 const value = source[key]; 628 result.push(value); 629 } 630 } 631 return result; 632} 633 634/** 635 * Object.defineProperty but "lazy", which means that the value is only set after 636 * it is retrieved the first time, rather than being set right away. 637 * 638 * @param {Object} obj the object to set the property on 639 * @param {string} key the key for the property to set 640 * @param {Function} getValue the function used to get the value when it is needed. 641 * @param {boolean} setter whether a setter should be allowed or not 642 */ 643function defineLazyProperty(obj, key, getValue, setter = true) { 644 const set = (value) => 645 Object.defineProperty(obj, key, { 646 value, 647 enumerable: true, 648 writable: true, 649 }); 650 const options = { 651 configurable: true, 652 enumerable: true, 653 get() { 654 const value = getValue(); 655 set(value); 656 return value; 657 }, 658 }; 659 if (setter) { 660 options.set = set; 661 } 662 return Object.defineProperty(obj, key, options); 663} 664 665var Obj = /*#__PURE__*/ Object.freeze({ 666 __proto__: null, 667 each: each, 668 reduce: reduce, 669 isObject: isObject, 670 isPlain: isPlain, 671 merge: merge$1, 672 values: values, 673 defineLazyProperty: defineLazyProperty, 674}); 675 676/** 677 * @file browser.js 678 * @module browser 679 */ 680 681/** 682 * Whether or not this device is an iPod. 683 * 684 * @static 685 * @type {Boolean} 686 */ 687let IS_IPOD = false;
688 689/** 690 * The detected iOS version - or `null`. 691 * 692 * @static 693 * @type {string|null} 694 */ 695let IOS_VERSION = null; 696 697/** 698 * Whether or not this is an Android device. 699 * 700 * @static 701 * @type {Boolean} 702 */ 703let IS_ANDROID = false; 704 705/** 706 * The detected Android version - or `null` if not Android or indeterminable. 707 * 708 * @static 709 * @type {number|string|null} 710 */ 711let ANDROID_VERSION; 712 713/** 714 * Whether or not this is Mozilla Firefox. 715 * 716 * @static 717 * @type {Boolean} 718 */ 719let IS_FIREFOX = false; 720 721/** 722 * Whether or not this is Microsoft Edge. 723 * 724 * @static 725 * @type {Boolean} 726 */ 727let IS_EDGE = false; 728 729/** 730 * Whether or not this is any Chromium Browser 731 * 732 * @static 733 * @type {Boolean} 734 */ 735let IS_CHROMIUM = false; 736 737/** 738 * Whether or not this is any Chromium browser that is not Edge. 739 * 740 * This will also be `true` for Chrome on iOS, which will have different support 741 * as it is actually Safari under the hood. 742 * 743 * Deprecated, as the behaviour to not match Edge was to prevent Legacy Edge's UA matching. 744 * IS_CHROMIUM should be used instead. 745 * "Chromium but not Edge" could be explicitly tested with IS_CHROMIUM && !IS_EDGE 746 * 747 * @static 748 * @deprecated 749 * @type {Boolean} 750 */ 751let IS_CHROME = false; 752 753/** 754 * The detected Chromium version - or `null`. 755 * 756 * @static 757 * @type {number|null} 758 */ 759let CHROMIUM_VERSION = null; 760 761/** 762 * The detected Google Chrome version - or `null`. 763 * This has always been the _Chromium_ version, i.e. would return on Chromium Edge. 764 * Deprecated, use CHROMIUM_VERSION instead. 765 * 766 * @static 767 * @deprecated 768 * @type {number|null} 769 */ 770let CHROME_VERSION = null; 771 772/** 773 * Whether or not this is a Chromecast receiver application. 774 * 775 * @static 776 * @type {Boolean} 777 */ 778const IS_CHROMECAST_RECEIVER = Boolean( 779 window__default['default'].cast && 780 window__default['default'].cast.framework && 781 window__default['default'].cast.framework.CastReceiverContext 782); 783 784/** 785 * The detected Internet Explorer version - or `null`. 786 * 787 * @static 788 * @deprecated 789 * @type {number|null} 790 */ 791let IE_VERSION = null; 792 793/** 794 * Whether or not this is desktop Safari. 795 * 796 * @static 797 * @type {Boolean} 798 */ 799let IS_SAFARI = false; 800 801/** 802 * Whether or not this is a Windows machine. 803 * 804 * @static 805 * @type {Boolean} 806 */ 807let IS_WINDOWS = false; 808 809/** 810 * Whether or not this device is an iPad. 811 * 812 * @static 813 * @type {Boolean} 814 */ 815let IS_IPAD = false; 816 817/** 818 * Whether or not this device is an iPhone. 819 * 820 * @static 821 * @type {Boolean} 822 */ 823// The Facebook app's UIWebView identifies as both an iPhone and iPad, so 824// to identify iPhones, we need to exclude iPads. 825// http://artsy.github.io/blog/2012/10/18/the-perils-of-ios-user-agent-sniffing/ 826let IS_IPHONE = false; 827 828/** 829 * Whether or not this is a Tizen device. 830 * 831 * @static 832 * @type {Boolean} 833 */ 834let IS_TIZEN = false; 835 836/** 837 * Whether or not this is a WebOS device. 838 * 839 * @static 840 * @type {Boolean} 841 */ 842let IS_WEBOS = false; 843 844/** 845 * Whether or not this is a Smart TV (Tizen or WebOS) device. 846 * 847 * @static 848 * @type {Boolean} 849 */ 850let IS_SMART_TV = false; 851 852/** 853 * Whether or not this device is touch-enabled. 854 * 855 * @static 856 * @const 857 * @type {Boolean} 858 */ 859const TOUCH_ENABLED = Boolean( 860 isReal() && 861 ('ontouchstart' in window__default['default'] || 862 window__default['default'].navigator.maxTouchPoints || 863 (window__default['default'].DocumentTouch && 864 window__default['default'].document instanceof window__default['default'].DocumentTouch)) 865); 866const UAD = 867 window__default['default'].navigator && window__default['default'].navigator.userAgentData; 868if (UAD && UAD.platform && UAD.brands) { 869 // If userAgentData is present, use it instead of userAgent to avoid warnings 870 // Currently only implemented on Chromium 871 // userAgentData does not expose Android version, so ANDROID_VERSION remains `null` 872 873 IS_ANDROID = UAD.platform === 'Android'; 874 IS_EDGE = Boolean(UAD.brands.find((b) => b.brand === 'Microsoft Edge')); 875 IS_CHROMIUM = Boolean(UAD.brands.find((b) => b.brand === 'Chromium')); 876 IS_CHROME = !IS_EDGE && IS_CHROMIUM; 877 CHROMIUM_VERSION = CHROME_VERSION = 878 (UAD.brands.find((b) => b.brand === 'Chromium') || {}).version || null; 879 IS_WINDOWS = UAD.platform === 'Windows'; 880} 881 882// If the browser is not Chromium, either userAgentData is not present which could be an old Chromium browser, 883// or it's a browser that has added userAgentData since that we don't have tests for yet. In either case, 884// the checks need to be made agiainst the regular userAgent string. 885if (!IS_CHROMIUM) { 886 const USER_AGENT = 887 (window__default['default'].navigator && window__default['default'].navigator.userAgent) || ''; 888 IS_IPOD = /iPod/i.test(USER_AGENT); 889 IOS_VERSION = (function () { 890 const match = USER_AGENT.match(/OS (\d+)_/i); 891 if (match && match[1]) { 892 return match[1]; 893 } 894 return null; 895 })(); 896 IS_ANDROID = /Android/i.test(USER_AGENT); 897 ANDROID_VERSION = (function () { 898 // This matches Android Major.Minor.Patch versions
vendor: 9,002 bytes, lines 899-1245
899 // ANDROID_VERSION is Major.Minor as a Number, if Minor isn't available, then only Major is returned 900 const match = USER_AGENT.match(/Android (\d+)(?:\.(\d+))?(?:\.(\d+))*/i); 901 if (!match) { 902 return null; 903 } 904 const major = match[1] && parseFloat(match[1]); 905 const minor = match[2] && parseFloat(match[2]); 906 if (major && minor) { 907 return parseFloat(match[1] + '.' + match[2]); 908 } else if (major) { 909 return major; 910 } 911 return null; 912 })(); 913 IS_FIREFOX = /Firefox/i.test(USER_AGENT); 914 IS_EDGE = /Edg/i.test(USER_AGENT); 915 IS_CHROMIUM = /Chrome/i.test(USER_AGENT) || /CriOS/i.test(USER_AGENT); 916 IS_CHROME = !IS_EDGE && IS_CHROMIUM; 917 CHROMIUM_VERSION = CHROME_VERSION = (function () { 918 const match = USER_AGENT.match(/(Chrome|CriOS)\/(\d+)/); 919 if (match && match[2]) { 920 return parseFloat(match[2]); 921 } 922 return null; 923 })(); 924 IE_VERSION = (function () { 925 const result = /MSIE\s(\d+)\.\d/.exec(USER_AGENT); 926 let version = result && parseFloat(result[1]); 927 if (!version && /Trident\/7.0/i.test(USER_AGENT) && /rv:11.0/.test(USER_AGENT)) { 928 // IE 11 has a different user agent string than other IE versions 929 version = 11.0; 930 } 931 return version; 932 })(); 933 IS_TIZEN = /Tizen/i.test(USER_AGENT); 934 IS_WEBOS = /Web0S/i.test(USER_AGENT); 935 IS_SMART_TV = IS_TIZEN || IS_WEBOS; 936 IS_SAFARI = /Safari/i.test(USER_AGENT) && !IS_CHROME && !IS_ANDROID && !IS_EDGE && !IS_SMART_TV; 937 IS_WINDOWS = /Windows/i.test(USER_AGENT); 938 IS_IPAD = /iPad/i.test(USER_AGENT) || (IS_SAFARI && TOUCH_ENABLED && !/iPhone/i.test(USER_AGENT)); 939 IS_IPHONE = /iPhone/i.test(USER_AGENT) && !IS_IPAD; 940} 941 942/** 943 * Whether or not this is an iOS device. 944 * 945 * @static 946 * @const 947 * @type {Boolean} 948 */ 949const IS_IOS = IS_IPHONE || IS_IPAD || IS_IPOD; 950 951/** 952 * Whether or not this is any flavor of Safari - including iOS. 953 * 954 * @static 955 * @const 956 * @type {Boolean} 957 */ 958const IS_ANY_SAFARI = (IS_SAFARI || IS_IOS) && !IS_CHROME; 959 960var browser = /*#__PURE__*/ Object.freeze({ 961 __proto__: null, 962 get IS_IPOD() { 963 return IS_IPOD; 964 }, 965 get IOS_VERSION() { 966 return IOS_VERSION; 967 }, 968 get IS_ANDROID() { 969 return IS_ANDROID; 970 }, 971 get ANDROID_VERSION() { 972 return ANDROID_VERSION; 973 }, 974 get IS_FIREFOX() { 975 return IS_FIREFOX; 976 }, 977 get IS_EDGE() { 978 return IS_EDGE; 979 }, 980 get IS_CHROMIUM() { 981 return IS_CHROMIUM; 982 }, 983 get IS_CHROME() { 984 return IS_CHROME; 985 }, 986 get CHROMIUM_VERSION() { 987 return CHROMIUM_VERSION; 988 }, 989 get CHROME_VERSION() { 990 return CHROME_VERSION; 991 }, 992 IS_CHROMECAST_RECEIVER: IS_CHROMECAST_RECEIVER, 993 get IE_VERSION() { 994 return IE_VERSION; 995 }, 996 get IS_SAFARI() { 997 return IS_SAFARI; 998 }, 999 get IS_WINDOWS() { 1000 return IS_WINDOWS; 1001 }, 1002 get IS_IPAD() { 1003 return IS_IPAD; 1004 }, 1005 get IS_IPHONE() { 1006 return IS_IPHONE; 1007 }, 1008 get IS_TIZEN() { 1009 return IS_TIZEN; 1010 }, 1011 get IS_WEBOS() { 1012 return IS_WEBOS; 1013 }, 1014 get IS_SMART_TV() { 1015 return IS_SMART_TV; 1016 }, 1017 TOUCH_ENABLED: TOUCH_ENABLED, 1018 IS_IOS: IS_IOS, 1019 IS_ANY_SAFARI: IS_ANY_SAFARI, 1020}); 1021 1022/** 1023 * @file dom.js 1024 * @module dom 1025 */ 1026 1027/** 1028 * Detect if a value is a string with any non-whitespace characters. 1029 * 1030 * @private 1031 * @param {string} str 1032 * The string to check 1033 * 1034 * @return {boolean} 1035 * Will be `true` if the string is non-blank, `false` otherwise. 1036 * 1037 */ 1038function isNonBlankString(str) { 1039 // we use str.trim as it will trim any whitespace characters 1040 // from the front or back of non-whitespace characters. aka 1041 // Any string that contains non-whitespace characters will 1042 // still contain them after `trim` but whitespace only strings 1043 // will have a length of 0, failing this check. 1044 return typeof str === 'string' && Boolean(str.trim()); 1045} 1046 1047/** 1048 * Throws an error if the passed string has whitespace. This is used by 1049 * class methods to be relatively consistent with the classList API. 1050 * 1051 * @private 1052 * @param {string} str 1053 * The string to check for whitespace. 1054 * 1055 * @throws {Error} 1056 * Throws an error if there is whitespace in the string. 1057 */ 1058function throwIfWhitespace(str) { 1059 // str.indexOf instead of regex because str.indexOf is faster performance wise. 1060 if (str.indexOf(' ') >= 0) { 1061 throw new Error('class has illegal whitespace characters'); 1062 } 1063} 1064 1065/** 1066 * Whether the current DOM interface appears to be real (i.e. not simulated). 1067 * 1068 * @return {boolean} 1069 * Will be `true` if the DOM appears to be real, `false` otherwise. 1070 */ 1071function isReal() { 1072 // Both document and window will never be undefined thanks to `global`. 1073 return document__default['default'] === window__default['default'].document; 1074} 1075 1076/** 1077 * Determines, via duck typing, whether or not a value is a DOM element. 1078 * 1079 * @param {*} value 1080 * The value to check. 1081 * 1082 * @return {boolean} 1083 * Will be `true` if the value is a DOM element, `false` otherwise. 1084 */ 1085function isEl(value) { 1086 return isObject(value) && value.nodeType === 1; 1087} 1088 1089/** 1090 * Determines if the current DOM is embedded in an iframe. 1091 * 1092 * @return {boolean} 1093 * Will be `true` if the DOM is embedded in an iframe, `false` 1094 * otherwise. 1095 */ 1096function isInFrame() { 1097 // We need a try/catch here because Safari will throw errors when attempting 1098 // to get either `parent` or `self` 1099 try { 1100 return window__default['default'].parent !== window__default['default'].self; 1101 } catch (x) { 1102 return true; 1103 } 1104} 1105 1106/** 1107 * Creates functions to query the DOM using a given method. 1108 * 1109 * @private 1110 * @param {string} method 1111 * The method to create the query with. 1112 * 1113 * @return {Function} 1114 * The query method 1115 */ 1116function createQuerier(method) { 1117 return function (selector, context) { 1118 if (!isNonBlankString(selector)) { 1119 return document__default['default'][method](null); 1120 } 1121 if (isNonBlankString(context)) { 1122 context = document__default['default'].querySelector(context); 1123 } 1124 const ctx = isEl(context) ? context : document__default['default']; 1125 return ctx[method] && ctx[method](selector); 1126 }; 1127} 1128 1129/** 1130 * Creates an element and applies properties, attributes, and inserts content. 1131 * 1132 * @param {string} [tagName='div'] 1133 * Name of tag to be created. 1134 * 1135 * @param {Object} [properties={}] 1136 * Element properties to be applied. 1137 * 1138 * @param {Object} [attributes={}] 1139 * Element attributes to be applied. 1140 * 1141 * @param {ContentDescriptor} [content] 1142 * A content descriptor object. 1143 * 1144 * @return {Element} 1145 * The element that was created. 1146 */ 1147function createEl(tagName = 'div', properties = {}, attributes = {}, content) { 1148 const el = document__default['default'].createElement(tagName); 1149 Object.getOwnPropertyNames(properties).forEach(function (propName) { 1150 const val = properties[propName]; 1151 1152 // Handle textContent since it's not supported everywhere and we have a 1153 // method for it. 1154 if (propName === 'textContent') { 1155 textContent(el, val); 1156 } else if (el[propName] !== val || propName === 'tabIndex') { 1157 el[propName] = val; 1158 } 1159 }); 1160 Object.getOwnPropertyNames(attributes).forEach(function (attrName) { 1161 el.setAttribute(attrName, attributes[attrName]); 1162 }); 1163 if (content) { 1164 appendContent(el, content); 1165 } 1166 return el; 1167} 1168 1169/** 1170 * Injects text into an element, replacing any existing contents entirely. 1171 * 1172 * @param {HTMLElement} el 1173 * The element to add text content into 1174 * 1175 * @param {string} text 1176 * The text content to add. 1177 * 1178 * @return {Element} 1179 * The element with added text content. 1180 */ 1181function textContent(el, text) { 1182 if (typeof el.textContent === 'undefined') { 1183 el.innerText = text; 1184 } else { 1185 el.textContent = text; 1186 } 1187 return el; 1188} 1189 1190/** 1191 * Insert an element as the first child node of another 1192 * 1193 * @param {Element} child 1194 * Element to insert 1195 * 1196 * @param {Element} parent 1197 * Element to insert child into 1198 */ 1199function prependTo(child, parent) { 1200 if (parent.firstChild) { 1201 parent.insertBefore(child, parent.firstChild); 1202 } else { 1203 parent.appendChild(child); 1204 } 1205} 1206 1207/** 1208 * Check if an element has a class name. 1209 * 1210 * @param {Element} element 1211 * Element to check 1212 * 1213 * @param {string} classToCheck 1214 * Class name to check for 1215 * 1216 * @return {boolean} 1217 * Will be `true` if the element has a class, `false` otherwise. 1218 * 1219 * @throws {Error} 1220 * Throws an error if `classToCheck` has white space. 1221 */ 1222function hasClass(element, classToCheck) { 1223 throwIfWhitespace(classToCheck); 1224 return element.classList.contains(classToCheck); 1225} 1226 1227/** 1228 * Add a class name to an element. 1229 * 1230 * @param {Element} element 1231 * Element to add class name to. 1232 * 1233 * @param {...string} classesToAdd 1234 * One or more class name to add. 1235 * 1236 * @return {Element} 1237 * The DOM element with the added class name. 1238 */ 1239function addClass(element, ...classesToAdd) { 1240 element.classList.add( 1241 ...classesToAdd.reduce((prev, current) => prev.concat(current.split(/\s+/)), []) 1242 ); 1243 return element; 1244} 1245
vendor: 4,100 bytes, lines 1246-1375
1246/** 1247 * Remove a class name from an element. 1248 * 1249 * @param {Element} element 1250 * Element to remove a class name from. 1251 * 1252 * @param {...string} classesToRemove 1253 * One or more class name to remove. 1254 * 1255 * @return {Element} 1256 * The DOM element with class name removed. 1257 */ 1258function removeClass(element, ...classesToRemove) { 1259 // Protect in case the player gets disposed 1260 if (!element) { 1261 log$1.warn("removeClass was called with an element that doesn't exist"); 1262 return null; 1263 } 1264 element.classList.remove( 1265 ...classesToRemove.reduce((prev, current) => prev.concat(current.split(/\s+/)), []) 1266 ); 1267 return element; 1268} 1269 1270/** 1271 * The callback definition for toggleClass. 1272 * 1273 * @callback PredicateCallback 1274 * @param {Element} element 1275 * The DOM element of the Component. 1276 * 1277 * @param {string} classToToggle 1278 * The `className` that wants to be toggled 1279 * 1280 * @return {boolean|undefined} 1281 * If `true` is returned, the `classToToggle` will be added to the 1282 * `element`, but not removed. If `false`, the `classToToggle` will be removed from 1283 * the `element`, but not added. If `undefined`, the callback will be ignored. 1284 * 1285 */ 1286 1287/** 1288 * Adds or removes a class name to/from an element depending on an optional 1289 * condition or the presence/absence of the class name. 1290 * 1291 * @param {Element} element 1292 * The element to toggle a class name on. 1293 * 1294 * @param {string} classToToggle 1295 * The class that should be toggled. 1296 * 1297 * @param {boolean|PredicateCallback} [predicate] 1298 * See the return value for {@link module:dom~PredicateCallback} 1299 * 1300 * @return {Element} 1301 * The element with a class that has been toggled. 1302 */ 1303function toggleClass(element, classToToggle, predicate) { 1304 if (typeof predicate === 'function') { 1305 predicate = predicate(element, classToToggle); 1306 } 1307 if (typeof predicate !== 'boolean') { 1308 predicate = undefined; 1309 } 1310 classToToggle.split(/\s+/).forEach((className) => element.classList.toggle(className, predicate)); 1311 return element; 1312} 1313 1314/** 1315 * Apply attributes to an HTML element. 1316 * 1317 * @param {Element} el 1318 * Element to add attributes to. 1319 * 1320 * @param {Object} [attributes] 1321 * Attributes to be applied. 1322 */ 1323function setAttributes(el, attributes) { 1324 Object.getOwnPropertyNames(attributes).forEach(function (attrName) { 1325 const attrValue = attributes[attrName]; 1326 if (attrValue === null || typeof attrValue === 'undefined' || attrValue === false) { 1327 el.removeAttribute(attrName); 1328 } else { 1329 el.setAttribute(attrName, attrValue === true ? '' : attrValue); 1330 } 1331 }); 1332} 1333 1334/** 1335 * Get an element's attribute values, as defined on the HTML tag. 1336 * 1337 * Attributes are not the same as properties. They're defined on the tag 1338 * or with setAttribute. 1339 * 1340 * @param {Element} tag 1341 * Element from which to get tag attributes. 1342 * 1343 * @return {Object} 1344 * All attributes of the element. Boolean attributes will be `true` or 1345 * `false`, others will be strings. 1346 */ 1347function getAttributes(tag) { 1348 const obj = {}; 1349 1350 // known boolean attributes 1351 // we can check for matching boolean properties, but not all browsers 1352 // and not all tags know about these attributes, so, we still want to check them manually 1353 const knownBooleans = [ 1354 'autoplay', 1355 'controls', 1356 'playsinline', 1357 'loop', 1358 'muted', 1359 'default', 1360 'defaultMuted', 1361 ]; 1362 if (tag && tag.attributes && tag.attributes.length > 0) { 1363 const attrs = tag.attributes; 1364 for (let i = attrs.length - 1; i >= 0; i--) { 1365 const attrName = attrs[i].name; 1366 /** @type {boolean|string} */ 1367 let attrVal = attrs[i].value; 1368 1369 // check for known booleans 1370 // the matching element property will return a value for typeof 1371 if (knownBooleans.includes(attrName)) { 1372 // the value of an included boolean attribute is typically an empty 1373 // string ('') which would equal false if we just check for a false value. 1374 // we also don't want support bad code like autoplay='false' 1375 attrVal = attrVal !== null ? true : false;
vendor: 8,609 bytes, lines 1376-1684
1376 } 1377 obj[attrName] = attrVal; 1378 } 1379 } 1380 return obj; 1381} 1382 1383/** 1384 * Get the value of an element's attribute. 1385 * 1386 * @param {Element} el 1387 * A DOM element. 1388 * 1389 * @param {string} attribute 1390 * Attribute to get the value of. 1391 * 1392 * @return {string} 1393 * The value of the attribute. 1394 */ 1395function getAttribute(el, attribute) { 1396 return el.getAttribute(attribute); 1397} 1398 1399/** 1400 * Set the value of an element's attribute. 1401 * 1402 * @param {Element} el 1403 * A DOM element. 1404 * 1405 * @param {string} attribute 1406 * Attribute to set. 1407 * 1408 * @param {string} value 1409 * Value to set the attribute to. 1410 */ 1411function setAttribute(el, attribute, value) { 1412 el.setAttribute(attribute, value); 1413} 1414 1415/** 1416 * Remove an element's attribute. 1417 * 1418 * @param {Element} el 1419 * A DOM element. 1420 * 1421 * @param {string} attribute 1422 * Attribute to remove. 1423 */ 1424function removeAttribute(el, attribute) { 1425 el.removeAttribute(attribute); 1426} 1427 1428/** 1429 * Attempt to block the ability to select text. 1430 */ 1431function blockTextSelection() { 1432 document__default['default'].body.focus(); 1433 document__default['default'].onselectstart = function () { 1434 return false; 1435 }; 1436} 1437 1438/** 1439 * Turn off text selection blocking. 1440 */ 1441function unblockTextSelection() { 1442 document__default['default'].onselectstart = function () { 1443 return true; 1444 }; 1445} 1446 1447/** 1448 * Identical to the native `getBoundingClientRect` function, but ensures that 1449 * the method is supported at all (it is in all browsers we claim to support) 1450 * and that the element is in the DOM before continuing. 1451 * 1452 * This wrapper function also shims properties which are not provided by some 1453 * older browsers (namely, IE8). 1454 * 1455 * Additionally, some browsers do not support adding properties to a 1456 * `ClientRect`/`DOMRect` object; so, we shallow-copy it with the standard 1457 * properties (except `x` and `y` which are not widely supported). This helps 1458 * avoid implementations where keys are non-enumerable. 1459 * 1460 * @param {Element} el 1461 * Element whose `ClientRect` we want to calculate. 1462 * 1463 * @return {Object|undefined} 1464 * Always returns a plain object - or `undefined` if it cannot. 1465 */ 1466function getBoundingClientRect(el) { 1467 if (el && el.getBoundingClientRect && el.parentNode) { 1468 const rect = el.getBoundingClientRect(); 1469 const result = {}; 1470 ['bottom', 'height', 'left', 'right', 'top', 'width'].forEach((k) => { 1471 if (rect[k] !== undefined) { 1472 result[k] = rect[k]; 1473 } 1474 }); 1475 if (!result.height) { 1476 result.height = parseFloat(computedStyle(el, 'height')); 1477 } 1478 if (!result.width) { 1479 result.width = parseFloat(computedStyle(el, 'width')); 1480 } 1481 return result; 1482 } 1483} 1484 1485/** 1486 * Represents the position of a DOM element on the page. 1487 * 1488 * @typedef {Object} module:dom~Position 1489 * 1490 * @property {number} left 1491 * Pixels to the left. 1492 * 1493 * @property {number} top 1494 * Pixels from the top. 1495 */ 1496 1497/** 1498 * Get the position of an element in the DOM. 1499 * 1500 * Uses `getBoundingClientRect` technique from John Resig. 1501 * 1502 * @see http://ejohn.org/blog/getboundingclientrect-is-awesome/ 1503 * 1504 * @param {Element} el 1505 * Element from which to get offset. 1506 * 1507 * @return {module:dom~Position} 1508 * The position of the element that was passed in. 1509 */ 1510function findPosition(el) { 1511 if (!el || (el && !el.offsetParent)) { 1512 return { 1513 left: 0, 1514 top: 0, 1515 width: 0, 1516 height: 0, 1517 }; 1518 } 1519 const width = el.offsetWidth; 1520 const height = el.offsetHeight; 1521 let left = 0; 1522 let top = 0; 1523 while (el.offsetParent && el !== document__default['default'][FullscreenApi.fullscreenElement]) { 1524 left += el.offsetLeft; 1525 top += el.offsetTop; 1526 el = el.offsetParent; 1527 } 1528 return { 1529 left, 1530 top, 1531 width, 1532 height, 1533 }; 1534} 1535 1536/** 1537 * Represents x and y coordinates for a DOM element or mouse pointer. 1538 * 1539 * @typedef {Object} module:dom~Coordinates 1540 * 1541 * @property {number} x 1542 * x coordinate in pixels 1543 * 1544 * @property {number} y 1545 * y coordinate in pixels 1546 */ 1547 1548/** 1549 * Get the pointer position within an element. 1550 * 1551 * The base on the coordinates are the bottom left of the element. 1552 * 1553 * @param {Element} el 1554 * Element on which to get the pointer position on. 1555 * 1556 * @param {Event} event 1557 * Event object. 1558 * 1559 * @return {module:dom~Coordinates} 1560 * A coordinates object corresponding to the mouse position. 1561 * 1562 */ 1563function getPointerPosition(el, event) { 1564 const translated = { 1565 x: 0, 1566 y: 0, 1567 }; 1568 if (IS_IOS) { 1569 let item = el; 1570 while (item && item.nodeName.toLowerCase() !== 'html') { 1571 const transform = computedStyle(item, 'transform'); 1572 if (/^matrix/.test(transform)) { 1573 const values = transform.slice(7, -1).split(/,\s/).map(Number); 1574 translated.x += values[4]; 1575 translated.y += values[5]; 1576 } else if (/^matrix3d/.test(transform)) { 1577 const values = transform.slice(9, -1).split(/,\s/).map(Number); 1578 translated.x += values[12]; 1579 translated.y += values[13]; 1580 } 1581 if ( 1582 item.assignedSlot && 1583 item.assignedSlot.parentElement && 1584 window__default['default'].WebKitCSSMatrix 1585 ) { 1586 const transformValue = window__default['default'].getComputedStyle( 1587 item.assignedSlot.parentElement 1588 ).transform; 1589 const matrix = new window__default['default'].WebKitCSSMatrix(transformValue); 1590 translated.x += matrix.m41; 1591 translated.y += matrix.m42; 1592 } 1593 item = item.parentNode || item.host; 1594 } 1595 } 1596 const position = {}; 1597 const boxTarget = findPosition(event.target); 1598 const box = findPosition(el); 1599 const boxW = box.width; 1600 const boxH = box.height; 1601 let offsetY = event.offsetY - (box.top - boxTarget.top); 1602 let offsetX = event.offsetX - (box.left - boxTarget.left); 1603 if (event.changedTouches) { 1604 offsetX = event.changedTouches[0].pageX - box.left; 1605 offsetY = event.changedTouches[0].pageY + box.top; 1606 if (IS_IOS) { 1607 offsetX -= translated.x; 1608 offsetY -= translated.y; 1609 } 1610 } 1611 position.y = 1 - Math.max(0, Math.min(1, offsetY / boxH)); 1612 position.x = Math.max(0, Math.min(1, offsetX / boxW)); 1613 return position; 1614} 1615 1616/** 1617 * Determines, via duck typing, whether or not a value is a text node. 1618 * 1619 * @param {*} value 1620 * Check if this value is a text node. 1621 * 1622 * @return {boolean} 1623 * Will be `true` if the value is a text node, `false` otherwise. 1624 */ 1625function isTextNode(value) { 1626 return isObject(value) && value.nodeType === 3; 1627} 1628 1629/** 1630 * Empties the contents of an element. 1631 * 1632 * @param {Element} el 1633 * The element to empty children from 1634 * 1635 * @return {Element} 1636 * The element with no children 1637 */ 1638function emptyEl(el) { 1639 while (el.firstChild) { 1640 el.removeChild(el.firstChild); 1641 } 1642 return el; 1643} 1644 1645/** 1646 * This is a mixed value that describes content to be injected into the DOM 1647 * via some method. It can be of the following types: 1648 * 1649 * Type | Description 1650 * -----------|------------- 1651 * `string` | The value will be normalized into a text node. 1652 * `Element` | The value will be accepted as-is. 1653 * `Text` | A TextNode. The value will be accepted as-is. 1654 * `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). 1655 * `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. 1656 * 1657 * @typedef {string|Element|Text|Array|Function} ContentDescriptor 1658 */ 1659 1660/** 1661 * Normalizes content for eventual insertion into the DOM. 1662 * 1663 * This allows a wide range of content definition methods, but helps protect 1664 * from falling into the trap of simply writing to `innerHTML`, which could 1665 * be an XSS concern. 1666 * 1667 * The content for an element can be passed in multiple types and 1668 * combinations, whose behavior is as follows: 1669 * 1670 * @param {ContentDescriptor} content 1671 * A content descriptor value. 1672 * 1673 * @return {Array} 1674 * All of the content that was passed in, normalized to an array of 1675 * elements or text nodes. 1676 */ 1677function normalizeContent(content) { 1678 // First, invoke content if it is a function. If it produces an array, 1679 // that needs to happen before normalization. 1680 if (typeof content === 'function') { 1681 content = content(); 1682 } 1683 1684 // Next up, normalize to an array, so one or many items can be normalized,
1685 // filtered, and returned. 1686 return (Array.isArray(content) ? content : [content]) 1687 .map((value) => { 1688 // First, invoke value if it is a function to produce a new value, 1689 // which will be subsequently normalized to a Node of some kind. 1690 if (typeof value === 'function') { 1691 value = value(); 1692 } 1693 if (isEl(value) || isTextNode(value)) { 1694 return value; 1695 } 1696 if (typeof value === 'string' && /\S/.test(value)) { 1697 return document__default['default'].createTextNode(value); 1698 } 1699 }) 1700 .filter((value) => value); 1701} 1702 1703/** 1704 * Normalizes and appends content to an element. 1705 * 1706 * @param {Element} el 1707 * Element to append normalized content to. 1708 * 1709 * @param {ContentDescriptor} content 1710 * A content descriptor value. 1711 * 1712 * @return {Element} 1713 * The element with appended normalized content. 1714 */ 1715function appendContent(el, content) { 1716 normalizeContent(content).forEach((node) => el.appendChild(node)); 1717 return el; 1718} 1719 1720/** 1721 * Normalizes and inserts content into an element; this is identical to 1722 * `appendContent()`, except it empties the element first. 1723 * 1724 * @param {Element} el 1725 * Element to insert normalized content into. 1726 * 1727 * @param {ContentDescriptor} content 1728 * A content descriptor value. 1729 * 1730 * @return {Element} 1731 * The element with inserted normalized content. 1732 */ 1733function insertContent(el, content) { 1734 return appendContent(emptyEl(el), content); 1735} 1736 1737/** 1738 * Check if an event was a single left click. 1739 * 1740 * @param {MouseEvent} event 1741 * Event object. 1742 * 1743 * @return {boolean} 1744 * Will be `true` if a single left click, `false` otherwise. 1745 */ 1746function isSingleLeftClick(event) { 1747 // Note: if you create something draggable, be sure to 1748 // call it on both `mousedown` and `mousemove` event, 1749 // otherwise `mousedown` should be enough for a button 1750 1751 if (event.button === undefined && event.buttons === undefined) { 1752 // Why do we need `buttons` ? 1753 // Because, middle mouse sometimes have this: 1754 // e.button === 0 and e.buttons === 4 1755 // Furthermore, we want to prevent combination click, something like 1756 // HOLD middlemouse then left click, that would be 1757 // e.button === 0, e.buttons === 5 1758 // just `button` is not gonna work 1759 1760 // Alright, then what this block does ? 1761 // this is for chrome `simulate mobile devices` 1762 // I want to support this as well 1763 1764 return true; 1765 } 1766 if (event.button === 0 && event.buttons === undefined) { 1767 // Touch screen, sometimes on some specific device, `buttons` 1768 // doesn't have anything (safari on ios, blackberry...) 1769 1770 return true; 1771 } 1772 1773 // `mouseup` event on a single left click has 1774 // `button` and `buttons` equal to 0 1775 if (event.type === 'mouseup' && event.button === 0 && event.buttons === 0) { 1776 return true; 1777 } 1778 1779 // MacOS Sonoma trackpad when "tap to click enabled" 1780 if (event.type === 'mousedown' && event.button === 0 && event.buttons === 0) { 1781 return true; 1782 } 1783 if (event.button !== 0 || event.buttons !== 1) { 1784 // This is the reason we have those if else block above 1785 // if any special case we can catch and let it slide 1786 // we do it above, when get to here, this definitely 1787 // is-not-left-click 1788 1789 return false; 1790 } 1791 return true; 1792} 1793 1794/** 1795 * Finds a single DOM element matching `selector` within the optional 1796 * `context` of another DOM element (defaulting to `document`). 1797 * 1798 * @param {string} selector 1799 * A valid CSS selector, which will be passed to `querySelector`. 1800 * 1801 * @param {Element|String} [context=document] 1802 * A DOM element within which to query. Can also be a selector 1803 * string in which case the first matching element will be used 1804 * as context. If missing (or no element matches selector), falls 1805 * back to `document`. 1806 * 1807 * @return {Element|null} 1808 * The element that was found or null. 1809 */ 1810const $ = createQuerier('querySelector'); 1811 1812/** 1813 * Finds a all DOM elements matching `selector` within the optional 1814 * `context` of another DOM element (defaulting to `document`). 1815 * 1816 * @param {string} selector 1817 * A valid CSS selector, which will be passed to `querySelectorAll`. 1818 * 1819 * @param {Element|String} [context=document] 1820 * A DOM element within which to query. Can also be a selector
vendor: 16,730 bytes, lines 1821-2410
1821 * string in which case the first matching element will be used 1822 * as context. If missing (or no element matches selector), falls 1823 * back to `document`. 1824 * 1825 * @return {NodeList} 1826 * A element list of elements that were found. Will be empty if none 1827 * were found. 1828 * 1829 */ 1830const $$ = createQuerier('querySelectorAll'); 1831 1832/** 1833 * A safe getComputedStyle. 1834 * 1835 * This is needed because in Firefox, if the player is loaded in an iframe with 1836 * `display:none`, then `getComputedStyle` returns `null`, so, we do a 1837 * null-check to make sure that the player doesn't break in these cases. 1838 * 1839 * @param {Element} el 1840 * The element you want the computed style of 1841 * 1842 * @param {string} prop 1843 * The property name you want 1844 * 1845 * @see https://bugzilla.mozilla.org/show_bug.cgi?id=548397 1846 */ 1847function computedStyle(el, prop) { 1848 if (!el || !prop) { 1849 return ''; 1850 } 1851 if (typeof window__default['default'].getComputedStyle === 'function') { 1852 let computedStyleValue; 1853 try { 1854 computedStyleValue = window__default['default'].getComputedStyle(el); 1855 } catch (e) { 1856 return ''; 1857 } 1858 return computedStyleValue 1859 ? computedStyleValue.getPropertyValue(prop) || computedStyleValue[prop] 1860 : ''; 1861 } 1862 return ''; 1863} 1864 1865/** 1866 * Copy document style sheets to another window. 1867 * 1868 * @param {Window} win 1869 * The window element you want to copy the document style sheets to. 1870 * 1871 */ 1872function copyStyleSheetsToWindow(win) { 1873 [...document__default['default'].styleSheets].forEach((styleSheet) => { 1874 try { 1875 const cssRules = [...styleSheet.cssRules].map((rule) => rule.cssText).join(''); 1876 const style = document__default['default'].createElement('style'); 1877 style.textContent = cssRules; 1878 win.document.head.appendChild(style); 1879 } catch (e) { 1880 const link = document__default['default'].createElement('link'); 1881 link.rel = 'stylesheet'; 1882 link.type = styleSheet.type; 1883 // For older Safari this has to be the string; on other browsers setting the MediaList works 1884 link.media = styleSheet.media.mediaText; 1885 link.href = styleSheet.href; 1886 win.document.head.appendChild(link); 1887 } 1888 }); 1889} 1890 1891var Dom = /*#__PURE__*/ Object.freeze({ 1892 __proto__: null, 1893 isReal: isReal, 1894 isEl: isEl, 1895 isInFrame: isInFrame, 1896 createEl: createEl, 1897 textContent: textContent, 1898 prependTo: prependTo, 1899 hasClass: hasClass, 1900 addClass: addClass, 1901 removeClass: removeClass, 1902 toggleClass: toggleClass, 1903 setAttributes: setAttributes, 1904 getAttributes: getAttributes, 1905 getAttribute: getAttribute, 1906 setAttribute: setAttribute, 1907 removeAttribute: removeAttribute, 1908 blockTextSelection: blockTextSelection, 1909 unblockTextSelection: unblockTextSelection, 1910 getBoundingClientRect: getBoundingClientRect, 1911 findPosition: findPosition, 1912 getPointerPosition: getPointerPosition, 1913 isTextNode: isTextNode, 1914 emptyEl: emptyEl, 1915 normalizeContent: normalizeContent, 1916 appendContent: appendContent, 1917 insertContent: insertContent, 1918 isSingleLeftClick: isSingleLeftClick, 1919 $: $, 1920 $$: $$, 1921 computedStyle: computedStyle, 1922 copyStyleSheetsToWindow: copyStyleSheetsToWindow, 1923}); 1924 1925/** 1926 * @file setup.js - Functions for setting up a player without 1927 * user interaction based on the data-setup `attribute` of the video tag. 1928 * 1929 * @module setup 1930 */ 1931let _windowLoaded = false; 1932let videojs$1; 1933 1934/** 1935 * Set up any tags that have a data-setup `attribute` when the player is started. 1936 */ 1937const autoSetup = function () { 1938 if (videojs$1.options.autoSetup === false) { 1939 return; 1940 } 1941 const vids = Array.prototype.slice.call( 1942 document__default['default'].getElementsByTagName('video') 1943 ); 1944 const audios = Array.prototype.slice.call( 1945 document__default['default'].getElementsByTagName('audio') 1946 ); 1947 const divs = Array.prototype.slice.call( 1948 document__default['default'].getElementsByTagName('video-js') 1949 ); 1950 const mediaEls = vids.concat(audios, divs); 1951 1952 // Check if any media elements exist 1953 if (mediaEls && mediaEls.length > 0) { 1954 for (let i = 0, e = mediaEls.length; i < e; i++) { 1955 const mediaEl = mediaEls[i]; 1956 1957 // Check if element exists, has getAttribute func. 1958 if (mediaEl && mediaEl.getAttribute) { 1959 // Make sure this player hasn't already been set up. 1960 if (mediaEl.player === undefined) { 1961 const options = mediaEl.getAttribute('data-setup'); 1962 1963 // Check if data-setup attr exists. 1964 // We only auto-setup if they've added the data-setup attr. 1965 if (options !== null) { 1966 // Create new video.js instance. 1967 videojs$1(mediaEl); 1968 } 1969 } 1970 1971 // If getAttribute isn't defined, we need to wait for the DOM. 1972 } else { 1973 autoSetupTimeout(1); 1974 break; 1975 } 1976 } 1977 1978 // No videos were found, so keep looping unless page is finished loading. 1979 } else if (!_windowLoaded) { 1980 autoSetupTimeout(1); 1981 } 1982}; 1983 1984/** 1985 * Wait until the page is loaded before running autoSetup. This will be called in 1986 * autoSetup if `hasLoaded` returns false. 1987 * 1988 * @param {number} wait 1989 * How long to wait in ms 1990 * 1991 * @param {module:videojs} [vjs] 1992 * The videojs library function 1993 */ 1994function autoSetupTimeout(wait, vjs) { 1995 // Protect against breakage in non-browser environments 1996 if (!isReal()) { 1997 return; 1998 } 1999 if (vjs) { 2000 videojs$1 = vjs; 2001 } 2002 window__default['default'].setTimeout(autoSetup, wait); 2003} 2004 2005/** 2006 * Used to set the internal tracking of window loaded state to true. 2007 * 2008 * @private 2009 */ 2010function setWindowLoaded() { 2011 _windowLoaded = true; 2012 window__default['default'].removeEventListener('load', setWindowLoaded); 2013} 2014if (isReal()) { 2015 if (document__default['default'].readyState === 'complete') { 2016 setWindowLoaded(); 2017 } else { 2018 /** 2019 * Listen for the load event on window, and set _windowLoaded to true. 2020 * 2021 * We use a standard event listener here to avoid incrementing the GUID 2022 * before any players are created. 2023 * 2024 * @listens load 2025 */ 2026 window__default['default'].addEventListener('load', setWindowLoaded); 2027 } 2028} 2029 2030/** 2031 * @file stylesheet.js 2032 * @module stylesheet 2033 */ 2034 2035/** 2036 * Create a DOM style element given a className for it. 2037 * 2038 * @param {string} className 2039 * The className to add to the created style element. 2040 * 2041 * @return {Element} 2042 * The element that was created. 2043 */ 2044const createStyleElement = function (className) { 2045 const style = document__default['default'].createElement('style'); 2046 style.className = className; 2047 return style; 2048}; 2049 2050/** 2051 * Add text to a DOM element. 2052 * 2053 * @param {Element} el 2054 * The Element to add text content to. 2055 * 2056 * @param {string} content 2057 * The text to add to the element. 2058 */ 2059const setTextContent = function (el, content) { 2060 if (el.styleSheet) { 2061 el.styleSheet.cssText = content; 2062 } else { 2063 el.textContent = content; 2064 } 2065}; 2066 2067/** 2068 * @file dom-data.js 2069 * @module dom-data 2070 */ 2071 2072/** 2073 * Element Data Store. 2074 * 2075 * Allows for binding data to an element without putting it directly on the 2076 * element. Ex. Event listeners are stored here. 2077 * (also from jsninja.com, slightly modified and updated for closure compiler) 2078 * 2079 * @type {Object} 2080 * @private 2081 */ 2082var DomData = new WeakMap(); 2083 2084/** 2085 * @file guid.js 2086 * @module guid 2087 */ 2088 2089// Default value for GUIDs. This allows us to reset the GUID counter in tests. 2090// 2091// The initial GUID is 3 because some users have come to rely on the first 2092// default player ID ending up as `vjs_video_3`. 2093// 2094// See: https://github.com/videojs/video.js/pull/6216 2095const _initialGuid = 3; 2096 2097/** 2098 * Unique ID for an element or function 2099 * 2100 * @type {Number} 2101 */ 2102let _guid = _initialGuid; 2103 2104/** 2105 * Get a unique auto-incrementing ID by number that has not been returned before. 2106 * 2107 * @return {number} 2108 * A new unique ID. 2109 */ 2110function newGUID() { 2111 return _guid++; 2112} 2113 2114/** 2115 * @file events.js. An Event System (John Resig - Secrets of a JS Ninja http://jsninja.com/) 2116 * (Original book version wasn't completely usable, so fixed some things and made Closure Compiler compatible) 2117 * This should work very similarly to jQuery's events, however it's based off the book version which isn't as 2118 * robust as jquery's, so there's probably some differences. 2119 * 2120 * @file events.js 2121 * @module events 2122 */ 2123 2124/** 2125 * Clean up the listener cache and dispatchers 2126 * 2127 * @param {Element|Object} elem 2128 * Element to clean up 2129 * 2130 * @param {string} type 2131 * Type of event to clean up 2132 */ 2133function _cleanUpEvents(elem, type) { 2134 if (!DomData.has(elem)) { 2135 return; 2136 } 2137 const data = DomData.get(elem); 2138 2139 // Remove the events of a particular type if there are none left 2140 if (data.handlers[type].length === 0) { 2141 delete data.handlers[type]; 2142 // data.handlers[type] = null; 2143 // Setting to null was causing an error with data.handlers 2144 2145 // Remove the meta-handler from the element 2146 if (elem.removeEventListener) { 2147 elem.removeEventListener(type, data.dispatcher, false); 2148 } else if (elem.detachEvent) { 2149 elem.detachEvent('on' + type, data.dispatcher); 2150 } 2151 } 2152 2153 // Remove the events object if there are no types left 2154 if (Object.getOwnPropertyNames(data.handlers).length <= 0) { 2155 delete data.handlers; 2156 delete data.dispatcher; 2157 delete data.disabled; 2158 } 2159 2160 // Finally remove the element data if there is no data left 2161 if (Object.getOwnPropertyNames(data).length === 0) { 2162 DomData.delete(elem); 2163 } 2164} 2165 2166/** 2167 * Loops through an array of event types and calls the requested method for each type. 2168 * 2169 * @param {Function} fn 2170 * The event method we want to use. 2171 * 2172 * @param {Element|Object} elem 2173 * Element or object to bind listeners to 2174 * 2175 * @param {string[]} types 2176 * Type of event to bind to. 2177 * 2178 * @param {Function} callback 2179 * Event listener. 2180 */ 2181function _handleMultipleEvents(fn, elem, types, callback) { 2182 types.forEach(function (type) { 2183 // Call the event method for each one of the types 2184 fn(elem, type, callback); 2185 }); 2186} 2187 2188/** 2189 * Fix a native event to have standard property values 2190 * 2191 * @param {Object} event 2192 * Event object to fix. 2193 * 2194 * @return {Object} 2195 * Fixed event object. 2196 */ 2197function fixEvent(event) { 2198 if (event.fixed_) { 2199 return event; 2200 } 2201 function returnTrue() { 2202 return true; 2203 } 2204 function returnFalse() { 2205 return false; 2206 } 2207 2208 // Test if fixing up is needed 2209 // Used to check if !event.stopPropagation instead of isPropagationStopped 2210 // But native events return true for stopPropagation, but don't have 2211 // other expected methods like isPropagationStopped. Seems to be a problem 2212 // with the Javascript Ninja code. So we're just overriding all events now. 2213 if (!event || !event.isPropagationStopped || !event.isImmediatePropagationStopped) { 2214 const old = event || window__default['default'].event; 2215 event = {}; 2216 // Clone the old object so that we can modify the values event = {}; 2217 // IE8 Doesn't like when you mess with native event properties 2218 // Firefox returns false for event.hasOwnProperty('type') and other props 2219 // which makes copying more difficult. 2220 2221 // TODO: Probably best to create an allowlist of event props 2222 const deprecatedProps = [ 2223 'layerX', 2224 'layerY', 2225 'keyLocation', 2226 'path', 2227 'webkitMovementX', 2228 'webkitMovementY', 2229 'mozPressure', 2230 'mozInputSource', 2231 ]; 2232 for (const key in old) { 2233 // Safari 6.0.3 warns you if you try to copy deprecated layerX/Y 2234 // Chrome warns you if you try to copy deprecated keyboardEvent.keyLocation 2235 // and webkitMovementX/Y 2236 // Lighthouse complains if Event.path is copied 2237 if (!deprecatedProps.includes(key)) { 2238 // Chrome 32+ warns if you try to copy deprecated returnValue, but 2239 // we still want to if preventDefault isn't supported (IE8). 2240 if (!(key === 'returnValue' && old.preventDefault)) { 2241 event[key] = old[key]; 2242 } 2243 } 2244 } 2245 2246 // The event occurred on this element 2247 if (!event.target) { 2248 event.target = event.srcElement || document__default['default']; 2249 } 2250 2251 // Handle which other element the event is related to 2252 if (!event.relatedTarget) { 2253 event.relatedTarget = 2254 event.fromElement === event.target ? event.toElement : event.fromElement; 2255 } 2256 2257 // Stop the default browser action 2258 event.preventDefault = function () { 2259 if (old.preventDefault) { 2260 old.preventDefault(); 2261 } 2262 event.returnValue = false; 2263 old.returnValue = false; 2264 event.defaultPrevented = true; 2265 }; 2266 event.defaultPrevented = false; 2267 2268 // Stop the event from bubbling 2269 event.stopPropagation = function () { 2270 if (old.stopPropagation) { 2271 old.stopPropagation(); 2272 } 2273 event.cancelBubble = true; 2274 old.cancelBubble = true; 2275 event.isPropagationStopped = returnTrue; 2276 }; 2277 event.isPropagationStopped = returnFalse; 2278 2279 // Stop the event from bubbling and executing other handlers 2280 event.stopImmediatePropagation = function () { 2281 if (old.stopImmediatePropagation) { 2282 old.stopImmediatePropagation(); 2283 } 2284 event.isImmediatePropagationStopped = returnTrue; 2285 event.stopPropagation(); 2286 }; 2287 event.isImmediatePropagationStopped = returnFalse; 2288 2289 // Handle mouse position 2290 if (event.clientX !== null && event.clientX !== undefined) { 2291 const doc = document__default['default'].documentElement; 2292 const body = document__default['default'].body; 2293 event.pageX = 2294 event.clientX + 2295 ((doc && doc.scrollLeft) || (body && body.scrollLeft) || 0) - 2296 ((doc && doc.clientLeft) || (body && body.clientLeft) || 0); 2297 event.pageY = 2298 event.clientY + 2299 ((doc && doc.scrollTop) || (body && body.scrollTop) || 0) - 2300 ((doc && doc.clientTop) || (body && body.clientTop) || 0); 2301 } 2302 2303 // Handle key presses 2304 event.which = event.charCode || event.keyCode; 2305 2306 // Fix button for mouse clicks: 2307 // 0 == left; 1 == middle; 2 == right 2308 if (event.button !== null && event.button !== undefined) { 2309 // The following is disabled because it does not pass videojs-standard 2310 // and... yikes. 2311 /* eslint-disable */ 2312 event.button = event.button & 1 ? 0 : event.button & 4 ? 1 : event.button & 2 ? 2 : 0; 2313 /* eslint-enable */ 2314 } 2315 } 2316 event.fixed_ = true; 2317 // Returns fixed-up instance 2318 return event; 2319} 2320 2321/** 2322 * Whether passive event listeners are supported 2323 */ 2324let _supportsPassive; 2325const supportsPassive = function () { 2326 if (typeof _supportsPassive !== 'boolean') { 2327 _supportsPassive = false; 2328 try { 2329 const opts = Object.defineProperty({}, 'passive', { 2330 get() { 2331 _supportsPassive = true; 2332 }, 2333 }); 2334 window__default['default'].addEventListener('test', null, opts); 2335 window__default['default'].removeEventListener('test', null, opts); 2336 } catch (e) { 2337 // disregard 2338 } 2339 } 2340 return _supportsPassive; 2341}; 2342 2343/** 2344 * Touch events Chrome expects to be passive 2345 */ 2346const passiveEvents = ['touchstart', 'touchmove']; 2347 2348/** 2349 * Add an event listener to element 2350 * It stores the handler function in a separate cache object 2351 * and adds a generic handler to the element's event, 2352 * along with a unique id (guid) to the element. 2353 * 2354 * @param {Element|Object} elem 2355 * Element or object to bind listeners to 2356 * 2357 * @param {string|string[]} type 2358 * Type of event to bind to. 2359 * 2360 * @param {Function} fn 2361 * Event listener. 2362 */ 2363function on(elem, type, fn) { 2364 if (Array.isArray(type)) { 2365 return _handleMultipleEvents(on, elem, type, fn); 2366 } 2367 if (!DomData.has(elem)) { 2368 DomData.set(elem, {}); 2369 } 2370 const data = DomData.get(elem); 2371 2372 // We need a place to store all our handler data 2373 if (!data.handlers) { 2374 data.handlers = {}; 2375 } 2376 if (!data.handlers[type]) { 2377 data.handlers[type] = []; 2378 } 2379 if (!fn.guid) { 2380 fn.guid = newGUID(); 2381 } 2382 data.handlers[type].push(fn); 2383 if (!data.dispatcher) { 2384 data.disabled = false; 2385 data.dispatcher = function (event, hash) { 2386 if (data.disabled) { 2387 return; 2388 } 2389 event = fixEvent(event); 2390 const handlers = data.handlers[event.type]; 2391 if (handlers) { 2392 // Copy handlers so if handlers are added/removed during the process it doesn't throw everything off. 2393 const handlersCopy = handlers.slice(0); 2394 for (let m = 0, n = handlersCopy.length; m < n; m++) { 2395 if (event.isImmediatePropagationStopped()) { 2396 break; 2397 } else { 2398 try { 2399 handlersCopy[m].call(elem, event, hash); 2400 } catch (e) { 2401 log$1.error(e); 2402 } 2403 } 2404 } 2405 } 2406 }; 2407 } 2408 if (data.handlers[type].length === 1) { 2409 if (elem.addEventListener) { 2410 let options = false;
vendor: 4,198 bytes, lines 2411-2554
2411 if (supportsPassive() && passiveEvents.indexOf(type) > -1) { 2412 options = { 2413 passive: true, 2414 }; 2415 } 2416 elem.addEventListener(type, data.dispatcher, options); 2417 } else if (elem.attachEvent) { 2418 elem.attachEvent('on' + type, data.dispatcher); 2419 } 2420 } 2421} 2422 2423/** 2424 * Removes event listeners from an element 2425 * 2426 * @param {Element|Object} elem 2427 * Object to remove listeners from. 2428 * 2429 * @param {string|string[]} [type] 2430 * Type of listener to remove. Don't include to remove all events from element. 2431 * 2432 * @param {Function} [fn] 2433 * Specific listener to remove. Don't include to remove listeners for an event 2434 * type. 2435 */ 2436function off(elem, type, fn) { 2437 // Don't want to add a cache object through getElData if not needed 2438 if (!DomData.has(elem)) { 2439 return; 2440 } 2441 const data = DomData.get(elem); 2442 2443 // If no events exist, nothing to unbind 2444 if (!data.handlers) { 2445 return; 2446 } 2447 if (Array.isArray(type)) { 2448 return _handleMultipleEvents(off, elem, type, fn); 2449 } 2450 2451 // Utility function 2452 const removeType = function (el, t) { 2453 data.handlers[t] = []; 2454 _cleanUpEvents(el, t); 2455 }; 2456 2457 // Are we removing all bound events? 2458 if (type === undefined) { 2459 for (const t in data.handlers) { 2460 if (Object.prototype.hasOwnProperty.call(data.handlers || {}, t)) { 2461 removeType(elem, t); 2462 } 2463 } 2464 return; 2465 } 2466 const handlers = data.handlers[type]; 2467 2468 // If no handlers exist, nothing to unbind 2469 if (!handlers) { 2470 return; 2471 } 2472 2473 // If no listener was provided, remove all listeners for type 2474 if (!fn) { 2475 removeType(elem, type); 2476 return; 2477 } 2478 2479 // We're only removing a single handler 2480 if (fn.guid) { 2481 for (let n = 0; n < handlers.length; n++) { 2482 if (handlers[n].guid === fn.guid) { 2483 handlers.splice(n--, 1); 2484 } 2485 } 2486 } 2487 _cleanUpEvents(elem, type); 2488} 2489 2490/** 2491 * Trigger an event for an element 2492 * 2493 * @param {Element|Object} elem 2494 * Element to trigger an event on 2495 * 2496 * @param {EventTarget~Event|string} event 2497 * A string (the type) or an event object with a type attribute 2498 * 2499 * @param {Object} [hash] 2500 * data hash to pass along with the event 2501 * 2502 * @return {boolean|undefined} 2503 * Returns the opposite of `defaultPrevented` if default was 2504 * prevented. Otherwise, returns `undefined` 2505 */ 2506function trigger(elem, event, hash) { 2507 // Fetches element data and a reference to the parent (for bubbling). 2508 // Don't want to add a data object to cache for every parent, 2509 // so checking hasElData first. 2510 const elemData = DomData.has(elem) ? DomData.get(elem) : {}; 2511 const parent = elem.parentNode || elem.ownerDocument; 2512 // type = event.type || event, 2513 // handler; 2514 2515 // If an event name was passed as a string, creates an event out of it 2516 if (typeof event === 'string') { 2517 event = { 2518 type: event, 2519 target: elem, 2520 }; 2521 } else if (!event.target) { 2522 event.target = elem; 2523 } 2524 2525 // Normalizes the event properties. 2526 event = fixEvent(event); 2527 2528 // If the passed element has a dispatcher, executes the established handlers. 2529 if (elemData.dispatcher) { 2530 elemData.dispatcher.call(elem, event, hash); 2531 } 2532 2533 // Unless explicitly stopped or the event does not bubble (e.g. media events) 2534 // recursively calls this function to bubble the event up the DOM. 2535 if (parent && !event.isPropagationStopped() && event.bubbles === true) { 2536 trigger.call(null, parent, event, hash); 2537 2538 // If at the top of the DOM, triggers the default action unless disabled. 2539 } else if (!parent && !event.defaultPrevented && event.target && event.target[event.type]) { 2540 if (!DomData.has(event.target)) { 2541 DomData.set(event.target, {}); 2542 } 2543 const targetData = DomData.get(event.target); 2544 2545 // Checks if the target has a default action for this event. 2546 if (event.target[event.type]) { 2547 // Temporarily disables event dispatching on the target as we have already executed the handler. 2548 targetData.disabled = true; 2549 // Executes the default action. 2550 if (typeof event.target[event.type] === 'function') { 2551 event.target[event.type](); 2552 } 2553 // Re-enables event dispatching. 2554 targetData.disabled = false;
vendor: 5,945 bytes, lines 2555-2774
2555 } 2556 } 2557 2558 // Inform the triggerer if the default was prevented by returning false 2559 return !event.defaultPrevented; 2560} 2561 2562/** 2563 * Trigger a listener only once for an event. 2564 * 2565 * @param {Element|Object} elem 2566 * Element or object to bind to. 2567 * 2568 * @param {string|string[]} type 2569 * Name/type of event 2570 * 2571 * @param {Event~EventListener} fn 2572 * Event listener function 2573 */ 2574function one(elem, type, fn) { 2575 if (Array.isArray(type)) { 2576 return _handleMultipleEvents(one, elem, type, fn); 2577 } 2578 const func = function () { 2579 off(elem, type, func); 2580 fn.apply(this, arguments); 2581 }; 2582 2583 // copy the guid to the new function so it can removed using the original function's ID 2584 func.guid = fn.guid = fn.guid || newGUID(); 2585 on(elem, type, func); 2586} 2587 2588/** 2589 * Trigger a listener only once and then turn if off for all 2590 * configured events 2591 * 2592 * @param {Element|Object} elem 2593 * Element or object to bind to. 2594 * 2595 * @param {string|string[]} type 2596 * Name/type of event 2597 * 2598 * @param {Event~EventListener} fn 2599 * Event listener function 2600 */ 2601function any(elem, type, fn) { 2602 const func = function () { 2603 off(elem, type, func); 2604 fn.apply(this, arguments); 2605 }; 2606 2607 // copy the guid to the new function so it can removed using the original function's ID 2608 func.guid = fn.guid = fn.guid || newGUID(); 2609 2610 // multiple ons, but one off for everything 2611 on(elem, type, func); 2612} 2613 2614var Events = /*#__PURE__*/ Object.freeze({ 2615 __proto__: null, 2616 fixEvent: fixEvent, 2617 on: on, 2618 off: off, 2619 trigger: trigger, 2620 one: one, 2621 any: any, 2622}); 2623 2624/** 2625 * @file fn.js 2626 * @module fn 2627 */ 2628const UPDATE_REFRESH_INTERVAL = 30; 2629 2630/** 2631 * A private, internal-only function for changing the context of a function. 2632 * 2633 * It also stores a unique id on the function so it can be easily removed from 2634 * events. 2635 * 2636 * @private 2637 * @function 2638 * @param {*} context 2639 * The object to bind as scope. 2640 * 2641 * @param {Function} fn 2642 * The function to be bound to a scope. 2643 * 2644 * @param {number} [uid] 2645 * An optional unique ID for the function to be set 2646 * 2647 * @return {Function} 2648 * The new function that will be bound into the context given 2649 */ 2650const bind_ = function (context, fn, uid) { 2651 // Make sure the function has a unique ID 2652 if (!fn.guid) { 2653 fn.guid = newGUID(); 2654 } 2655 2656 // Create the new function that changes the context 2657 const bound = fn.bind(context); 2658 2659 // Allow for the ability to individualize this function 2660 // Needed in the case where multiple objects might share the same prototype 2661 // IF both items add an event listener with the same function, then you try to remove just one 2662 // it will remove both because they both have the same guid. 2663 // when using this, you need to use the bind method when you remove the listener as well. 2664 // currently used in text tracks 2665 bound.guid = uid ? uid + '_' + fn.guid : fn.guid; 2666 return bound; 2667}; 2668 2669/** 2670 * Wraps the given function, `fn`, with a new function that only invokes `fn` 2671 * at most once per every `wait` milliseconds. 2672 * 2673 * @function 2674 * @param {Function} fn 2675 * The function to be throttled. 2676 * 2677 * @param {number} wait 2678 * The number of milliseconds by which to throttle. 2679 * 2680 * @return {Function} 2681 */ 2682const throttle = function (fn, wait) { 2683 let last = window__default['default'].performance.now(); 2684 const throttled = function (...args) { 2685 const now = window__default['default'].performance.now(); 2686 if (now - last >= wait) { 2687 fn(...args); 2688 last = now; 2689 } 2690 }; 2691 return throttled; 2692}; 2693 2694/** 2695 * Creates a debounced function that delays invoking `func` until after `wait` 2696 * milliseconds have elapsed since the last time the debounced function was 2697 * invoked. 2698 * 2699 * Inspired by lodash and underscore implementations. 2700 * 2701 * @function 2702 * @param {Function} func 2703 * The function to wrap with debounce behavior. 2704 * 2705 * @param {number} wait 2706 * The number of milliseconds to wait after the last invocation. 2707 * 2708 * @param {boolean} [immediate] 2709 * Whether or not to invoke the function immediately upon creation. 2710 * 2711 * @param {Object} [context=window] 2712 * The "context" in which the debounced function should debounce. For 2713 * example, if this function should be tied to a Video.js player, 2714 * the player can be passed here. Alternatively, defaults to the 2715 * global `window` object. 2716 * 2717 * @return {Function} 2718 * A debounced function. 2719 */ 2720const debounce = function (func, wait, immediate, context = window__default['default']) { 2721 let timeout; 2722 const cancel = () => { 2723 context.clearTimeout(timeout); 2724 timeout = null; 2725 }; 2726 2727 /* eslint-disable consistent-this */ 2728 const debounced = function () { 2729 const self = this; 2730 const args = arguments; 2731 let later = function () { 2732 timeout = null; 2733 later = null; 2734 if (!immediate) { 2735 func.apply(self, args); 2736 } 2737 }; 2738 if (!timeout && immediate) { 2739 func.apply(self, args); 2740 } 2741 context.clearTimeout(timeout); 2742 timeout = context.setTimeout(later, wait); 2743 }; 2744 /* eslint-enable consistent-this */ 2745 2746 debounced.cancel = cancel; 2747 return debounced; 2748}; 2749 2750var Fn = /*#__PURE__*/ Object.freeze({ 2751 __proto__: null, 2752 UPDATE_REFRESH_INTERVAL: UPDATE_REFRESH_INTERVAL, 2753 bind_: bind_, 2754 throttle: throttle, 2755 debounce: debounce, 2756}); 2757 2758/** 2759 * @file src/js/event-target.js 2760 */ 2761let EVENT_MAP; 2762 2763/** 2764 * `EventTarget` is a class that can have the same API as the DOM `EventTarget`. It 2765 * adds shorthand functions that wrap around lengthy functions. For example: 2766 * the `on` function is a wrapper around `addEventListener`. 2767 * 2768 * @see [EventTarget Spec]{@link https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventTarget} 2769 * @class EventTarget 2770 */ 2771class EventTarget$2 { 2772 /** 2773 * Adds an `event listener` to an instance of an `EventTarget`. An `event listener` is a 2774 * function that will get called when an event with a certain
2774name gets triggered. 2775 * 2776 * @param {string|string[]} type 2777 * An event name or an array of event names. 2778 * 2779 * @param {Function} fn 2780 * The function to call with `EventTarget`s 2781 */ 2782 on(type, fn) { 2783 // Remove the addEventListener alias before calling Events.on 2784 // so we don't get into an infinite type loop 2785 const ael = this.addEventListener; 2786 this.addEventListener = () => {}; 2787 on(this, type, fn); 2788 this.addEventListener = ael; 2789 } 2790 /** 2791 * Removes an `event listener` for a specific event from an instance of `EventTarget`. 2792 * This makes it so that the `event listener` will no longer get called when the 2793 * named event happens. 2794 * 2795 * @param {string|string[]} type 2796 * An event name or an array of event names. 2797 * 2798 * @param {Function} fn 2799 * The function to remove. 2800 */ 2801 off(type, fn) { 2802 off(this, type, fn); 2803 } 2804 /** 2805 * This function will add an `event listener` that gets triggered only once. After the 2806 * first trigger it will get removed. This is like adding an `event listener` 2807 * with {@link EventTarget#on} that calls {@link EventTarget#off} on itself. 2808 * 2809 * @param {string|string[]} type 2810 * An event name or an array of event names. 2811 * 2812 * @param {Function} fn 2813 * The function to be called once for each event name. 2814 */ 2815 one(type, fn) { 2816 // Remove the addEventListener aliasing Events.on 2817 // so we don't get into an infinite type loop 2818 const ael = this.addEventListener; 2819 this.addEventListener = () => {}; 2820 one(this, type, fn); 2821 this.addEventListener = ael; 2822 } 2823 /** 2824 * This function will add an `event listener` that gets triggered only once and is 2825 * removed from all events. This is like adding an array of `event listener`s 2826 * with {@link EventTarget#on} that calls {@link EventTarget#off} on all events the 2827 * first time it is triggered. 2828 * 2829 * @param {string|string[]} type 2830 * An event name or an array of event names. 2831 * 2832 * @param {Function} fn 2833 * The function to be called once for each event name. 2834 */ 2835 any(type, fn) { 2836 // Remove the addEventListener aliasing Events.on 2837 // so we don't get into an infinite type loop 2838 const ael = this.addEventListener; 2839 this.addEventListener = () => {}; 2840 any(this, type, fn); 2841 this.addEventListener = ael; 2842 } 2843 /** 2844 * This function causes an event to happen. This will then cause any `event listeners` 2845 * that are waiting for that event, to get called. If there are no `event listeners` 2846 * for an event then nothing will happen. 2847 * 2848 * If the name of the `Event` that is being triggered is in `EventTarget.allowedEvents_`. 2849 * Trigger will also call the `on` + `uppercaseEventName` function. 2850 * 2851 * Example: 2852 * 'click' is in `EventTarget.allowedEvents_`, so, trigger will attempt to call 2853 * `onClick` if it exists. 2854 * 2855 * @param {string|EventTarget~Event|Object} event 2856 * The name of the event, an `Event`, or an object with a key of type set to 2857 * an event name. 2858 */ 2859 trigger(event) { 2860 const type = event.type || event; 2861 2862 // deprecation 2863 // In a future version we should default target to `this` 2864 // similar to how we default the target to `elem` in 2865 // `Events.trigger`. Right now the default `target` will be 2866 // `document` due to the `Event.fixEvent` call. 2867 if (typeof event === 'string') { 2868 event = { 2869 type, 2870 }; 2871 } 2872 event = fixEvent(event); 2873 if (this.allowedEvents_[type] && this['on' + type]) { 2874 this['on' + type](event); 2875 } 2876 trigger(this, event); 2877 } 2878 queueTrigger(event) { 2879 // only set up EVENT_MAP if it'll be used 2880 if (!EVENT_MAP) { 2881 EVENT_MAP = new Map(); 2882 } 2883 const type = event.type || event; 2884 let map = EVENT_MAP.get(this); 2885 if (!map) { 2886 map = new Map(); 2887 EVENT_MAP.set(this, map); 2888 } 2889 const oldTimeout = map.get(type); 2890 map.delete(type); 2891 window__default['default'].clearTimeout(oldTimeout); 2892 const timeout = window__default['default'].setTimeout(() => { 2893 map.delete(type); 2894 // if we cleared out all timeouts for the current target, delete its map 2895 if (map.size === 0) { 2896 map = null; 2897 EVENT_MAP.delete(this); 2898 } 2899 this.trigger(event); 2900 }, 0); 2901 map.set(type, timeout); 2902 } 2903} 2904 2905/** 2906 * A Custom DOM event. 2907 * 2908 * @typedef {CustomEvent} Event 2909 * @see [Properties]{@link https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent} 2910 */ 2911 2912/** 2913 * All event listeners should follow the following format. 2914 * 2915 * @callback EventListener 2916 * @this {EventTarget} 2917 * 2918 * @param {Event} event 2919 * the event that triggered this function 2920 * 2921 * @param {Object} [hash] 2922 * hash of data sent during the event 2923 */ 2924 2925/**
2926 * An object containing event names as keys and booleans as values. 2927 * 2928 * > NOTE: If an event name is set to a true value here {@link EventTarget#trigger} 2929 * will have extra functionality. See that function for more information. 2930 * 2931 * @property EventTarget.prototype.allowedEvents_ 2932 * @protected 2933 */ 2934EventTarget$2.prototype.allowedEvents_ = {}; 2935 2936/** 2937 * An alias of {@link EventTarget#on}. Allows `EventTarget` to mimic 2938 * the standard DOM API. 2939 * 2940 * @function 2941 * @see {@link EventTarget#on} 2942 */ 2943EventTarget$2.prototype.addEventListener = EventTarget$2.prototype.on; 2944 2945/** 2946 * An alias of {@link EventTarget#off}. Allows `EventTarget` to mimic 2947 * the standard DOM API. 2948 * 2949 * @function 2950 * @see {@link EventTarget#off} 2951 */ 2952EventTarget$2.prototype.removeEventListener = EventTarget$2.prototype.off; 2953 2954/** 2955 * An alias of {@link EventTarget#trigger}. Allows `EventTarget` to mimic 2956 * the standard DOM API. 2957 * 2958 * @function 2959 * @see {@link EventTarget#trigger} 2960 */ 2961EventTarget$2.prototype.dispatchEvent = EventTarget$2.prototype.trigger; 2962 2963/** 2964 * @file mixins/evented.js 2965 * @module evented 2966 */ 2967const objName = (obj) => { 2968 if (typeof obj.name === 'function') { 2969 return obj.name(); 2970 } 2971 if (typeof obj.name === 'string') { 2972 return obj.name; 2973 } 2974 if (obj.name_) { 2975 return obj.name_; 2976 } 2977 if (obj.constructor && obj.constructor.name) { 2978 return obj.constructor.name; 2979 } 2980 return typeof obj; 2981}; 2982 2983/** 2984 * Returns whether or not an object has had the evented mixin applied. 2985 * 2986 * @param {Object} object 2987 * An object to test. 2988 * 2989 * @return {boolean} 2990 * Whether or not the object appears to be evented. 2991 */ 2992const isEvented = (object) => 2993 object instanceof EventTarget$2 || 2994 (!!object.eventBusEl_ && 2995 ['on', 'one', 'off', 'trigger'].every((k) => typeof object[k] === 'function')); 2996 2997/** 2998 * Adds a callback to run after the evented mixin applied. 2999 * 3000 * @param {Object} target 3001 * An object to Add 3002 * @param {Function} callback 3003 * The callback to run. 3004 */ 3005const addEventedCallback = (target, callback) => { 3006 if (isEvented(target)) { 3007 callback(); 3008 } else { 3009 if (!target.eventedCallbacks) { 3010 target.eventedCallbacks = []; 3011 } 3012 target.eventedCallbacks.push(callback); 3013 } 3014}; 3015 3016/** 3017 * Whether a value is a valid event type - non-empty string or array. 3018 * 3019 * @private 3020 * @param {string|Array} type 3021 * The type value to test. 3022 * 3023 * @return {boolean} 3024 * Whether or not the type is a valid event type. 3025 */ 3026const isValidEventType = (type) => 3027 // The regex here verifies that the `type` contains at least one non- 3028 // whitespace character. 3029 (typeof type === 'string' && /\S/.test(type)) || (Array.isArray(type) && !!type.length); 3030 3031/** 3032 * Validates a value to determine if it is a valid event target. Throws if not. 3033 * 3034 * @private 3035 * @throws {Error} 3036 * If the target does not appear to be a valid event target. 3037 * 3038 * @param {Object} target 3039 * The object to test. 3040 * 3041 * @param {Object} obj 3042 * The evented object we are validating for 3043 * 3044 * @param {string} fnName 3045 * The name of the evented mixin function that called this. 3046 */ 3047const validateTarget = (target, obj, fnName) => { 3048 if (!target || (!target.nodeName && !isEvented(target))) { 3049 throw new Error( 3050 `Invalid target for ${objName(obj)}#${fnName}; must be a DOM node or evented object.` 3051 ); 3052 } 3053}; 3054 3055/** 3056 * Validates a value to determine if it is a valid event target. Throws if not. 3057 * 3058 * @private 3059 * @throws {Error} 3060 * If the type does not appear to be a valid event type. 3061 * 3062 * @param {string|Array} type 3063 * The type to test. 3064 * 3065 * @param {Object} obj 3066 * The evented object we are validating for 3067 * 3068 * @param {string} fnName 3069 * The name of the evented mixin function that called this. 3070 */ 3071const validateEventType = (type, obj, fnName) => { 3072 if (!isValidEventType(type)) { 3073 throw new Error( 3074 `Invalid event type for ${objName(obj)}#${fnName}; must be a non-empty string or array.` 3075 ); 3076 } 3077}; 3078 3079/** 3080 * Validates a value to determine if it is a valid listener. Throws if not. 3081 * 3082 * @private 3083 * @throws {Error} 3084 * If the listener is not a function. 3085 * 3086 * @param {Function} listener 3087 * The listener to test. 3088 * 3089 * @param {Object} obj 3090 * The evented object we are validating for 3091 * 3092 * @param {string} fnName 3093 * The name of the evented mixin function that called this. 3094 */ 3095const validateListener = (listener, obj, fnName) => { 3096 if (typeof listener !== 'function') { 3097 throw new Error(`Invalid listener for ${objName(obj)}#${fnName}; must be a function.`); 3098 } 3099}; 3100 3101/** 3102 * Takes an array of arguments given to `on()` or `one()`, validates them, and 3103 * normalizes them into an object. 3104 * 3105 * @private 3106 * @param {Object} self 3107 * The evented object on which `on()` or `one()` was called. This 3108 * object will be bound as the `this` value for the listener. 3109 * 3110 * @param {Array} args 3111 * An array of arguments passed to `on()` or `one()`. 3112 * 3113 * @param {string} fnName 3114 * The name of the evented mixin function that called this. 3115 * 3116 * @return {Object}
vendor: 4,280 bytes, lines 3117-3239
3117 * An object containing useful values for `on()` or `one()` calls. 3118 */ 3119const normalizeListenArgs = (self, args, fnName) => { 3120 // If the number of arguments is less than 3, the target is always the 3121 // evented object itself. 3122 const isTargetingSelf = args.length < 3 || args[0] === self || args[0] === self.eventBusEl_; 3123 let target; 3124 let type; 3125 let listener; 3126 if (isTargetingSelf) { 3127 target = self.eventBusEl_; 3128 3129 // Deal with cases where we got 3 arguments, but we are still listening to 3130 // the evented object itself. 3131 if (args.length >= 3) { 3132 args.shift(); 3133 } 3134 [type, listener] = args; 3135 } else { 3136 // This was `[target, type, listener] = args;` but this block needs more than 3137 // one statement to produce minified output compatible with Chrome 53. 3138 // See https://github.com/videojs/video.js/pull/8810 3139 target = args[0]; 3140 type = args[1]; 3141 listener = args[2]; 3142 } 3143 validateTarget(target, self, fnName); 3144 validateEventType(type, self, fnName); 3145 validateListener(listener, self, fnName); 3146 listener = bind_(self, listener); 3147 return { 3148 isTargetingSelf, 3149 target, 3150 type, 3151 listener, 3152 }; 3153}; 3154 3155/** 3156 * Adds the listener to the event type(s) on the target, normalizing for 3157 * the type of target. 3158 * 3159 * @private 3160 * @param {Element|Object} target 3161 * A DOM node or evented object. 3162 * 3163 * @param {string} method 3164 * The event binding method to use ("on" or "one"). 3165 * 3166 * @param {string|Array} type 3167 * One or more event type(s). 3168 * 3169 * @param {Function} listener 3170 * A listener function. 3171 */ 3172const listen = (target, method, type, listener) => { 3173 validateTarget(target, target, method); 3174 if (target.nodeName) { 3175 Events[method](target, type, listener); 3176 } else { 3177 target[method](type, listener); 3178 } 3179}; 3180 3181/** 3182 * Contains methods that provide event capabilities to an object which is passed 3183 * to {@link module:evented|evented}. 3184 * 3185 * @mixin EventedMixin 3186 */ 3187const EventedMixin = { 3188 /** 3189 * Add a listener to an event (or events) on this object or another evented 3190 * object. 3191 * 3192 * @param {string|Array|Element|Object} targetOrType 3193 * If this is a string or array, it represents the event type(s) 3194 * that will trigger the listener. 3195 * 3196 * Another evented object can be passed here instead, which will 3197 * cause the listener to listen for events on _that_ object. 3198 * 3199 * In either case, the listener's `this` value will be bound to 3200 * this object. 3201 * 3202 * @param {string|Array|Function} typeOrListener 3203 * If the first argument was a string or array, this should be the 3204 * listener function. Otherwise, this is a string or array of event 3205 * type(s). 3206 * 3207 * @param {Function} [listener] 3208 * If the first argument was another evented object, this will be 3209 * the listener function. 3210 */ 3211 on(...args) { 3212 const { isTargetingSelf, target, type, listener } = normalizeListenArgs(this, args, 'on'); 3213 listen(target, 'on', type, listener); 3214 3215 // If this object is listening to another evented object. 3216 if (!isTargetingSelf) { 3217 // If this object is disposed, remove the listener. 3218 const removeListenerOnDispose = () => this.off(target, type, listener); 3219 3220 // Use the same function ID as the listener so we can remove it later it 3221 // using the ID of the original listener. 3222 removeListenerOnDispose.guid = listener.guid; 3223 3224 // Add a listener to the target's dispose event as well. This ensures 3225 // that if the target is disposed BEFORE this object, we remove the 3226 // removal listener that was just added. Otherwise, we create a memory leak. 3227 const removeRemoverOnTargetDispose = () => this.off('dispose', removeListenerOnDispose); 3228 3229 // Use the same function ID as the listener so we can remove it later 3230 // it using the ID of the original listener. 3231 removeRemoverOnTargetDispose.guid = listener.guid; 3232 listen(this, 'on', 'dispose', removeListenerOnDispose); 3233 listen(target, 'on', 'dispose', removeRemoverOnTargetDispose); 3234 } 3235 }, 3236 /** 3237 * Add a listener to an event (or events) on this object or another evented 3238 * object. The listener will be called once per event and then removed. 3239 *
3240 * @param {string|Array|Element|Object} targetOrType 3241 * If this is a string or array, it represents the event type(s) 3242 * that will trigger the listener. 3243 * 3244 * Another evented object can be passed here instead, which will 3245 * cause the listener to listen for events on _that_ object. 3246 * 3247 * In either case, the listener's `this` value will be bound to 3248 * this object. 3249 * 3250 * @param {string|Array|Function} typeOrListener 3251 * If the first argument was a string or array, this should be the 3252 * listener function. Otherwise, this is a string or array of event 3253 * type(s). 3254 * 3255 * @param {Function} [listener] 3256 * If the first argument was another evented object, this will be 3257 * the listener function. 3258 */ 3259 one(...args) { 3260 const { isTargetingSelf, target, type, listener } = normalizeListenArgs(this, args, 'one'); 3261 3262 // Targeting this evented object. 3263 if (isTargetingSelf) { 3264 listen(target, 'one', type, listener); 3265 3266 // Targeting another evented object. 3267 } else { 3268 // TODO: This wrapper is incorrect! It should only 3269 // remove the wrapper for the event type that called it. 3270 // Instead all listeners are removed on the first trigger! 3271 // see https://github.com/videojs/video.js/issues/5962 3272 const wrapper = (...largs) => { 3273 this.off(target, type, wrapper); 3274 listener.apply(null, largs); 3275 }; 3276 3277 // Use the same function ID as the listener so we can remove it later 3278 // it using the ID of the original listener. 3279 wrapper.guid = listener.guid; 3280 listen(target, 'one', type, wrapper); 3281 } 3282 }, 3283 /** 3284 * Add a listener to an event (or events) on this object or another evented 3285 * object. The listener will only be called once for the first event that is triggered 3286 * then removed. 3287 * 3288 * @param {string|Array|Element|Object} targetOrType 3289 * If this is a string or array, it represents the event type(s) 3290 * that will trigger the listener. 3291 * 3292 * Another evented object can be passed here instead, which will 3293 * cause the listener to listen for events on _that_ object. 3294 * 3295 * In either case, the listener's `this` value will be bound to 3296 * this object. 3297 * 3298 * @param {string|Array|Function} typeOrListener 3299 * If the first argument was a string or array, this should be the 3300 * listener function. Otherwise, this is a string or array of event 3301 * type(s). 3302 * 3303 * @param {Function} [listener] 3304 * If the first argument was another evented object, this will be 3305 * the listener function. 3306 */ 3307 any(...args) { 3308 const { isTargetingSelf, target, type, listener } = normalizeListenArgs(this, args, 'any'); 3309 3310 // Targeting this evented object. 3311 if (isTargetingSelf) { 3312 listen(target, 'any', type, listener); 3313 3314 // Targeting another evented object. 3315 } else { 3316 const wrapper = (...largs) => { 3317 this.off(target, type, wrapper); 3318 listener.apply(null, largs); 3319 }; 3320 3321 // Use the same function ID as the listener so we can remove it later 3322 // it using the ID of the original listener. 3323 wrapper.guid = listener.guid; 3324 listen(target, 'any', type, wrapper); 3325 } 3326 }, 3327 /** 3328 * Removes listener(s) from event(s) on an evented object. 3329 * 3330 * @param {string|Array|Element|Object} [targetOrType] 3331 * If this is a string or array, it represents the event type(s). 3332 * 3333 * Another evented object can be passed here instead, in which case 3334 * ALL 3 arguments are _required_. 3335 * 3336 * @param {string|Array|Function} [typeOrListener] 3337 * If the first argument was a string or array, this may be the 3338 * listener function. Otherwise, this is a string or array of event 3339 * type(s). 3340 * 3341 * @param {Function} [listener] 3342 * If the first argument was another evented object, this will be 3343 * the listener function; otherwise, _all_ listeners bound to the 3344 * event type(s) will be removed. 3345 */ 3346 off(targetOrType, typeOrListener, listener) { 3347 // Targeting this evented object. 3348 if (!targetOrType || isValidEventType(targetOrType)) { 3349 off(this.eventBusEl_, targetOrType, typeOrListener); 3350 3351 // Targeting another evented object. 3352 } else { 3353 const target = targetOrType; 3354 const type = typeOrListener; 3355 3356 // Fail fast and in a meaningful way! 3357 validateTarget(target, this, 'off'); 3358 validateEventType(type, this, 'off'); 3359 validateListener(listener, this, 'off'); 3360 3361 // Ensure there's at least a guid, even if the function hasn't been used 3362 listener = bind_(this, listener); 3363 3364 // Remove the dispose listener on this evented object, which was given 3365 // the same guid as the event listener in on(). 3366 this.off('dispose', listener); 3367 if (target.nodeName) { 3368 off(target, type, listener); 3369 off(target, 'dispose', listener); 3370 } else if (isEvented(target)) { 3371 target.off(type, listener); 3372 target.off('dispose', listener); 3373 } 3374 } 3375 }, 3376 /** 3377 * Fire an event on this evented object, causing its listeners to be called. 3378 * 3379 * @param {string|Object} event 3380 * An event type or an object with a type property. 3381 * 3382 * @param {Object} [hash] 3383 * An additional object to pass along to listeners. 3384 * 3385 * @return {boolean} 3386 * Whether or not the default behavior was prevented. 3387 */ 3388 trigger(event, hash) { 3389 validateTarget(this.eventBusEl_, this, 'trigger'); 3390 const type = event && typeof event !== 'string' ? event.type : event; 3391 if (!isValidEventType(type)) { 3392 throw new Error( 3393 `Invalid event type for ${objName(this)}#trigger; ` + 3394 'must be a non-empty string or object with a type key that has a non-empty value.' 3395 ); 3396 } 3397 return trigger(this.eventBusEl_, event, hash); 3398 }, 3399}; 3400 3401/** 3402 * Applies {@link module:evented~EventedMixin|EventedMixin} to a target object. 3403 * 3404 * @param {Object} target 3405 * The object to which to add event methods. 3406 * 3407 * @param {Object} [options={}] 3408 * Options for customizing the mixin behavior. 3409 * 3410 * @param {string} [options.eventBusKey] 3411 * By default, adds a `eventBusEl_` DOM element to the target object, 3412 * which is used as an event bus. If the target object already has a 3413 * DOM element that should be used, pass its key here. 3414 * 3415 * @return {Object} 3416 * The target object. 3417 */ 3418function evented(target, options = {}) { 3419 const { eventBusKey } = options; 3420 3421 // Set or create the eventBusEl_.
vendor: 5,759 bytes, lines 3422-3636
3422 if (eventBusKey) { 3423 if (!target[eventBusKey].nodeName) { 3424 throw new Error(`The eventBusKey "${eventBusKey}" does not refer to an element.`); 3425 } 3426 target.eventBusEl_ = target[eventBusKey]; 3427 } else { 3428 target.eventBusEl_ = createEl('span', { 3429 className: 'vjs-event-bus', 3430 }); 3431 } 3432 Object.assign(target, EventedMixin); 3433 if (target.eventedCallbacks) { 3434 target.eventedCallbacks.forEach((callback) => { 3435 callback(); 3436 }); 3437 } 3438 3439 // When any evented object is disposed, it removes all its listeners. 3440 target.on('dispose', () => { 3441 target.off(); 3442 [target, target.el_, target.eventBusEl_].forEach(function (val) { 3443 if (val && DomData.has(val)) { 3444 DomData.delete(val); 3445 } 3446 }); 3447 window__default['default'].setTimeout(() => { 3448 target.eventBusEl_ = null; 3449 }, 0); 3450 }); 3451 return target; 3452} 3453 3454/** 3455 * @file mixins/stateful.js 3456 * @module stateful 3457 */ 3458 3459/** 3460 * Contains methods that provide statefulness to an object which is passed 3461 * to {@link module:stateful}. 3462 * 3463 * @mixin StatefulMixin 3464 */ 3465const StatefulMixin = { 3466 /** 3467 * A hash containing arbitrary keys and values representing the state of 3468 * the object. 3469 * 3470 * @type {Object} 3471 */ 3472 state: {}, 3473 /** 3474 * Set the state of an object by mutating its 3475 * {@link module:stateful~StatefulMixin.state|state} object in place. 3476 * 3477 * @fires module:stateful~StatefulMixin#statechanged 3478 * @param {Object|Function} stateUpdates 3479 * A new set of properties to shallow-merge into the plugin state. 3480 * Can be a plain object or a function returning a plain object. 3481 * 3482 * @return {Object|undefined} 3483 * An object containing changes that occurred. If no changes 3484 * occurred, returns `undefined`. 3485 */ 3486 setState(stateUpdates) { 3487 // Support providing the `stateUpdates` state as a function. 3488 if (typeof stateUpdates === 'function') { 3489 stateUpdates = stateUpdates(); 3490 } 3491 let changes; 3492 each(stateUpdates, (value, key) => { 3493 // Record the change if the value is different from what's in the 3494 // current state. 3495 if (this.state[key] !== value) { 3496 changes = changes || {}; 3497 changes[key] = { 3498 from: this.state[key], 3499 to: value, 3500 }; 3501 } 3502 this.state[key] = value; 3503 }); 3504 3505 // Only trigger "statechange" if there were changes AND we have a trigger 3506 // function. This allows us to not require that the target object be an 3507 // evented object. 3508 if (changes && isEvented(this)) { 3509 /** 3510 * An event triggered on an object that is both 3511 * {@link module:stateful|stateful} and {@link module:evented|evented} 3512 * indicating that its state has changed. 3513 * 3514 * @event module:stateful~StatefulMixin#statechanged 3515 * @type {Object} 3516 * @property {Object} changes 3517 * A hash containing the properties that were changed and 3518 * the values they were changed `from` and `to`. 3519 */ 3520 this.trigger({ 3521 changes, 3522 type: 'statechanged', 3523 }); 3524 } 3525 return changes; 3526 }, 3527}; 3528 3529/** 3530 * Applies {@link module:stateful~StatefulMixin|StatefulMixin} to a target 3531 * object. 3532 * 3533 * If the target object is {@link module:evented|evented} and has a 3534 * `handleStateChanged` method, that method will be automatically bound to the 3535 * `statechanged` event on itself. 3536 * 3537 * @param {Object} target 3538 * The object to be made stateful. 3539 * 3540 * @param {Object} [defaultState] 3541 * A default set of properties to populate the newly-stateful object's 3542 * `state` property. 3543 * 3544 * @return {Object} 3545 * Returns the `target`. 3546 */ 3547function stateful(target, defaultState) { 3548 Object.assign(target, StatefulMixin); 3549 3550 // This happens after the mixing-in because we need to replace the `state` 3551 // added in that step. 3552 target.state = Object.assign({}, target.state, defaultState); 3553 3554 // Auto-bind the `handleStateChanged` method of the target object if it exists. 3555 if (typeof target.handleStateChanged === 'function' && isEvented(target)) { 3556 target.on('statechanged', target.handleStateChanged); 3557 } 3558 return target; 3559} 3560 3561/** 3562 * @file str.js 3563 * @module to-lower-case 3564 */ 3565 3566/** 3567 * Lowercase the first letter of a string. 3568 * 3569 * @param {string} string 3570 * String to be lowercased 3571 * 3572 * @return {string} 3573 * The string with a lowercased first letter 3574 */ 3575const toLowerCase = function (string) { 3576 if (typeof string !== 'string') { 3577 return string; 3578 } 3579 return string.replace(/./, (w) => w.toLowerCase()); 3580}; 3581 3582/** 3583 * Uppercase the first letter of a string. 3584 * 3585 * @param {string} string 3586 * String to be uppercased 3587 * 3588 * @return {string} 3589 * The string with an uppercased first letter 3590 */ 3591const toTitleCase$1 = function (string) { 3592 if (typeof string !== 'string') { 3593 return string; 3594 } 3595 return string.replace(/./, (w) => w.toUpperCase()); 3596}; 3597 3598/** 3599 * Compares the TitleCase versions of the two strings for equality. 3600 * 3601 * @param {string} str1 3602 * The first string to compare 3603 * 3604 * @param {string} str2 3605 * The second string to compare 3606 * 3607 * @return {boolean} 3608 * Whether the TitleCase versions of the strings are equal 3609 */ 3610const titleCaseEquals = function (str1, str2) { 3611 return toTitleCase$1(str1) === toTitleCase$1(str2); 3612}; 3613 3614var Str = /*#__PURE__*/ Object.freeze({ 3615 __proto__: null, 3616 toLowerCase: toLowerCase, 3617 toTitleCase: toTitleCase$1, 3618 titleCaseEquals: titleCaseEquals, 3619}); 3620 3621/** 3622 * Player Component - Base class for all UI objects 3623 * 3624 * @file component.js 3625 */ 3626 3627/** @import Player from './player' */ 3628 3629/** 3630 * A callback to be called if and when the component is ready. 3631 * `this` will be the Component instance. 3632 * 3633 * @callback ReadyCallback 3634 * @returns {void} 3635 */ 3636
3637/** 3638 * Base class for all UI Components. 3639 * Components are UI objects which represent both a javascript object and an element 3640 * in the DOM. They can be children of other components, and can have 3641 * children themselves. 3642 * 3643 * Components can also use methods from {@link EventTarget} 3644 */ 3645class Component$1 { 3646 /** 3647 * Creates an instance of this class. 3648 * 3649 * @param {Player} player 3650 * The `Player` that this class should be attached to. 3651 * 3652 * @param {Object} [options] 3653 * The key/value store of component options. 3654 * 3655 * @param {Object[]} [options.children] 3656 * An array of children objects to initialize this component with. Children objects have 3657 * a name property that will be used if more than one component of the same type needs to be 3658 * added. 3659 * 3660 * @param {string} [options.className] 3661 * A class or space separated list of classes to add the component 3662 * 3663 * @param {ReadyCallback} [ready] 3664 * Function that gets called when the `Component` is ready. 3665 */ 3666 constructor(player, options, ready) { 3667 // The component might be the player itself and we can't pass `this` to super 3668 if (!player && this.play) { 3669 this.player_ = player = this; // eslint-disable-line 3670 } else { 3671 this.player_ = player; 3672 } 3673 this.isDisposed_ = false; 3674 3675 // Hold the reference to the parent component via `addChild` method 3676 this.parentComponent_ = null; 3677 3678 // Make a copy of prototype.options_ to protect against overriding defaults 3679 this.options_ = merge$1({}, this.options_); 3680 3681 // Updated options with supplied options 3682 options = this.options_ = merge$1(this.options_, options); 3683 3684 // Get ID from options or options element if one is supplied 3685 this.id_ = options.id || (options.el && options.el.id); 3686 3687 // If there was no ID from the options, generate one 3688 if (!this.id_) { 3689 // Don't require the player ID function in the case of mock players 3690 const id = (player && player.id && player.id()) || 'no_player'; 3691 this.id_ = `${id}_component_${newGUID()}`; 3692 } 3693 this.name_ = options.name || null; 3694 3695 // Create element if one wasn't provided in options 3696 if (options.el) { 3697 this.el_ = options.el; 3698 } else if (options.createEl !== false) { 3699 this.el_ = this.createEl(); 3700 } 3701 if (options.className && this.el_) { 3702 options.className.split(' ').forEach((c) => this.addClass(c)); 3703 } 3704 3705 // Remove the placeholder event methods. If the component is evented, the 3706 // real methods are added next 3707 ['on', 'off', 'one', 'any', 'trigger'].forEach((fn) => { 3708 this[fn] = undefined; 3709 }); 3710 3711 // if evented is anything except false, we want to mixin in evented 3712 if (options.evented !== false) { 3713 // Make this an evented object and use `el_`, if available, as its event bus 3714 evented(this, { 3715 eventBusKey: this.el_ ? 'el_' : null, 3716 }); 3717 this.handleLanguagechange = this.handleLanguagechange.bind(this); 3718 this.on(this.player_, 'languagechange', this.handleLanguagechange); 3719 } 3720 stateful(this, this.constructor.defaultState); 3721 this.children_ = []; 3722 this.childIndex_ = {}; 3723 this.childNameIndex_ = {}; 3724 this.setTimeoutIds_ = new Set(); 3725 this.setIntervalIds_ = new Set(); 3726 this.rafIds_ = new Set(); 3727 this.namedRafs_ = new Map(); 3728 this.clearingTimersOnDispose_ = false; 3729 3730 // Add any child components in options 3731 if (options.initChildren !== false) { 3732 this.initChildren(); 3733 } 3734 3735 // Don't want to trigger ready here or it will go before init is actually 3736 // finished for all children that run this constructor 3737 this.ready(ready); 3738 if (options.reportTouchActivity !== false) { 3739 this.enableTouchActivity(); 3740 } 3741 } 3742 3743 // `on`, `off`, `one`, `any` and `trigger` are here so tsc includes them in definitions. 3744 // They are replaced or removed in the constructor 3745 3746 /** 3747 * Adds an `event listener` to an instance of an `EventTarget`. An `event listener` is a 3748 * function that will get called when an event with a certain name gets triggered. 3749 * 3750 * @param {string|string[]} type 3751 * An event name or an array of event names. 3752 * 3753 * @param {Function} fn 3754 * The function to call with `EventTarget`s 3755 */ 3756 3757 /** 3758 * Removes an `event listener` for a specific event from an instance of `EventTarget`. 3759 * This makes it so that the `event listener` will no longer get called when the 3760 * named event happens. 3761 * 3762 * @param {string|string[]} type 3763 * An event name or an array of event names. 3764 * 3765 * @param {Function}
3765 [fn] 3766 * The function to remove. If not specified, all listeners managed by Video.js will be removed. 3767 */ 3768 3769 /** 3770 * This function will add an `event listener` that gets triggered only once. After the 3771 * first trigger it will get removed. This is like adding an `event listener` 3772 * with {@link EventTarget#on} that calls {@link EventTarget#off} on itself. 3773 * 3774 * @param {string|string[]} type 3775 * An event name or an array of event names. 3776 * 3777 * @param {Function} fn 3778 * The function to be called once for each event name. 3779 */ 3780 3781 /** 3782 * This function will add an `event listener` that gets triggered only once and is 3783 * removed from all events. This is like adding an array of `event listener`s 3784 * with {@link EventTarget#on} that calls {@link EventTarget#off} on all events the 3785 * first time it is triggered. 3786 * 3787 * @param {string|string[]} type 3788 * An event name or an array of event names. 3789 * 3790 * @param {Function} fn 3791 * The function to be called once for each event name. 3792 */ 3793 3794 /** 3795 * This function causes an event to happen. This will then cause any `event listeners` 3796 * that are waiting for that event, to get called. If there are no `event listeners` 3797 * for an event then nothing will happen. 3798 * 3799 * If the name of the `Event` that is being triggered is in `EventTarget.allowedEvents_`. 3800 * Trigger will also call the `on` + `uppercaseEventName` function. 3801 * 3802 * Example: 3803 * 'click' is in `EventTarget.allowedEvents_`, so, trigger will attempt to call 3804 * `onClick` if it exists. 3805 * 3806 * @param {string|Event|Object} event 3807 * The name of the event, an `Event`, or an object with a key of type set to 3808 * an event name. 3809 * 3810 * @param {Object} [hash] 3811 * Optionally extra argument to pass through to an event listener 3812 */ 3813 3814 /** 3815 * Dispose of the `Component` and all child components. 3816 * 3817 * @fires Component#dispose 3818 * 3819 * @param {Object} options 3820 * @param {Element} options.originalEl element with which to replace player element 3821 */ 3822 dispose(options = {}) { 3823 // Bail out if the component has already been disposed. 3824 if (this.isDisposed_) { 3825 return; 3826 } 3827 if (this.readyQueue_) { 3828 this.readyQueue_.length = 0; 3829 } 3830 3831 /** 3832 * Triggered when a `Component` is disposed. 3833 * 3834 * @event Component#dispose 3835 * @type {Event} 3836 * 3837 * @property {boolean} [bubbles=false] 3838 * set to false so that the dispose event does not 3839 * bubble up 3840 */ 3841 this.trigger({ 3842 type: 'dispose', 3843 bubbles: false,
vendor: 8,608 bytes, lines 3843-4138
3843 3844 }); 3845 this.isDisposed_ = true; 3846 3847 // Dispose all children. 3848 if (this.children_) { 3849 for (let i = this.children_.length - 1; i >= 0; i--) { 3850 if (this.children_[i].dispose) { 3851 this.children_[i].dispose(); 3852 } 3853 } 3854 } 3855 3856 // Delete child references 3857 this.children_ = null; 3858 this.childIndex_ = null; 3859 this.childNameIndex_ = null; 3860 this.parentComponent_ = null; 3861 if (this.el_) { 3862 // Remove element from DOM 3863 if (this.el_.parentNode) { 3864 if (options.restoreEl) { 3865 this.el_.parentNode.replaceChild(options.restoreEl, this.el_); 3866 } else { 3867 this.el_.parentNode.removeChild(this.el_); 3868 } 3869 } 3870 this.el_ = null; 3871 } 3872 3873 // remove reference to the player after disposing of the element 3874 this.player_ = null; 3875 } 3876 3877 /** 3878 * Determine whether or not this component has been disposed. 3879 * 3880 * @return {boolean} 3881 * If the component has been disposed, will be `true`. Otherwise, `false`. 3882 */ 3883 isDisposed() { 3884 return Boolean(this.isDisposed_); 3885 } 3886 3887 /** 3888 * Return the {@link Player} that the `Component` has attached to. 3889 * 3890 * @return {Player} 3891 * The player that this `Component` has attached to. 3892 */ 3893 player() { 3894 return this.player_; 3895 } 3896 3897 /** 3898 * Deep merge of options objects with new options. 3899 * > Note: When both `obj` and `options` contain properties whose values are objects. 3900 * The two properties get merged using {@link module:obj.merge} 3901 * 3902 * @param {Object} obj 3903 * The object that contains new options. 3904 * 3905 * @return {Object} 3906 * A new object of `this.options_` and `obj` merged together. 3907 */ 3908 options(obj) { 3909 if (!obj) { 3910 return this.options_; 3911 } 3912 this.options_ = merge$1(this.options_, obj); 3913 return this.options_; 3914 } 3915 3916 /** 3917 * Get the `Component`s DOM element 3918 * 3919 * @return {Element} 3920 * The DOM element for this `Component`. 3921 */ 3922 el() { 3923 return this.el_; 3924 } 3925 3926 /** 3927 * Create the `Component`s DOM element. 3928 * 3929 * @param {string} [tagName] 3930 * Element's DOM node type. e.g. 'div' 3931 * 3932 * @param {Object} [properties] 3933 * An object of properties that should be set. 3934 * 3935 * @param {Object} [attributes] 3936 * An object of attributes that should be set. 3937 * 3938 * @return {Element} 3939 * The element that gets created. 3940 */ 3941 createEl(tagName, properties, attributes) { 3942 return createEl(tagName, properties, attributes); 3943 } 3944 3945 /** 3946 * Localize a string given the string in english. 3947 * 3948 * If tokens are provided, it'll try and run a simple token replacement on the provided string. 3949 * The tokens it looks for look like `{1}` with the index being 1-indexed into the tokens array. 3950 * 3951 * If a `defaultValue` is provided, it'll use that over `string`, 3952 * if a value isn't found in provided language files. 3953 * This is useful if you want to have a descriptive key for token replacement 3954 * but have a succinct localized string and not require `en.json` to be included. 3955 * 3956 * Currently, it is used for the progress bar timing. 3957 * ```js 3958 * { 3959 * "progress bar timing: currentTime={1} duration={2}": "{1} of {2}" 3960 * } 3961 * ``` 3962 * It is then used like so: 3963 * ```js 3964 * this.localize('progress bar timing: currentTime={1} duration{2}', 3965 * [this.player_.currentTime(), this.player_.duration()], 3966 * '{1} of {2}'); 3967 * ``` 3968 * 3969 * Which outputs something like: `01:23 of 24:56`. 3970 * 3971 * 3972 * @param {string} string 3973 * The string to localize and the key to lookup in the language files. 3974 * @param {string[]} [tokens] 3975 * If the current item has token replacements, provide the tokens here. 3976 * @param {string} [defaultValue] 3977 * Defaults to `string`. Can be a default value to use for token replacement 3978 * if the lookup key is needed to be separate. 3979 * 3980 * @return {string} 3981 * The localized string or if no localization exists the english string. 3982 */ 3983 localize(string, tokens, defaultValue = string) { 3984 const code = this.player_.language && this.player_.language(); 3985 const languages = this.player_.languages && this.player_.languages(); 3986 const language = languages && languages[code]; 3987 const primaryCode = code && code.split('-')[0]; 3988 const primaryLang = languages && languages[primaryCode]; 3989 let localizedString = defaultValue; 3990 if (language && language[string]) { 3991 localizedString = language[string]; 3992 } else if (primaryLang && primaryLang[string]) { 3993 localizedString = primaryLang[string]; 3994 } 3995 if (tokens) { 3996 localizedString = localizedString.replace(/\{(\d+)\}/g, function (match, index) { 3997 const value = tokens[index - 1]; 3998 let ret = value; 3999 if (typeof value === 'undefined') { 4000 ret = match; 4001 } 4002 return ret; 4003 }); 4004 } 4005 return localizedString; 4006 } 4007 4008 /** 4009 * Handles language change for the player in components. Should be overridden by sub-components. 4010 * 4011 * @abstract 4012 */ 4013 handleLanguagechange() {} 4014 4015 /** 4016 * Return the `Component`s DOM element. This is where children get inserted. 4017 * This will usually be the the same as the element returned in {@link Component#el}. 4018 * 4019 * @return {Element} 4020 * The content element for this `Component`. 4021 */ 4022 contentEl() { 4023 return this.contentEl_ || this.el_; 4024 } 4025 4026 /** 4027 * Get this `Component`s ID 4028 * 4029 * @return {string} 4030 * The id of this `Component` 4031 */ 4032 id() { 4033 return this.id_; 4034 } 4035 4036 /** 4037 * Get the `Component`s name. The name gets used to reference the `Component` 4038 * and is set during registration. 4039 * 4040 * @return {string} 4041 * The name of this `Component`. 4042 */ 4043 name() { 4044 return this.name_; 4045 } 4046 4047 /** 4048 * Get an array of all child components 4049 * 4050 * @return {Array} 4051 * The children 4052 */ 4053 children() { 4054 return this.children_; 4055 } 4056 4057 /** 4058 * Returns the child `Component` with the given `id`. 4059 * 4060 * @param {string} id 4061 * The id of the child `Component` to get. 4062 * 4063 * @return {Component|undefined} 4064 * The child `Component` with the given `id` or undefined. 4065 */ 4066 getChildById(id) { 4067 return this.childIndex_[id]; 4068 } 4069 4070 /** 4071 * Returns the child `Component` with the given `name`. 4072 * 4073 * @param {string} name 4074 * The name of the child `Component` to get. 4075 * 4076 * @return {Component|undefined} 4077 * The child `Component` with the given `name` or undefined. 4078 */ 4079 getChild(name) { 4080 if (!name) { 4081 return; 4082 } 4083 return this.childNameIndex_[name]; 4084 } 4085 4086 /** 4087 * Returns the descendant `Component` following the givent 4088 * descendant `names`. For instance ['foo', 'bar', 'baz'] would 4089 * try to get 'foo' on the current component, 'bar' on the 'foo' 4090 * component and 'baz' on the 'bar' component and return undefined 4091 * if any of those don't exist. 4092 * 4093 * @param {...string[]|...string} names 4094 * The name of the child `Component` to get. 4095 * 4096 * @return {Component|undefined} 4097 * The descendant `Component` following the given descendant 4098 * `names` or undefined. 4099 */ 4100 getDescendant(...names) { 4101 // flatten array argument into the main array 4102 names = names.reduce((acc, n) => acc.concat(n), []); 4103 let currentChild = this; 4104 for (let i = 0; i < names.length; i++) { 4105 currentChild = currentChild.getChild(names[i]); 4106 if (!currentChild || !currentChild.getChild) { 4107 return; 4108 } 4109 } 4110 return currentChild; 4111 } 4112 4113 /** 4114 * Adds an SVG icon element to another element or component. 4115 * 4116 * @param {string} iconName 4117 * The name of icon. A list of all the icon names can be found at 'sandbox/svg-icons.html' 4118 * 4119 * @param {Element} [el=this.el()] 4120 * Element to set the title on. Defaults to the current Component's element. 4121 * 4122 * @return {Element} 4123 * The newly created icon element. 4124 */ 4125 setIcon(iconName, el = this.el()) { 4126 // TODO: In v9 of video.js, we will want to remove font icons entirely. 4127 // This means this check, as well as the others throughout the code, and 4128 // the unecessary CSS for font icons, will need to be removed. 4129 // See https://github.com/videojs/video.js/pull/8260 as to which components 4130 // need updating. 4131 if (!this.player_.options_.experimentalSvgIcons) { 4132 return; 4133 } 4134 const xmlnsURL = 'http://www.w3.org/2000/svg'; 4135 4136 // The below creates an element in the format of: 4137 // <span><svg><use>....</use></svg></span> 4138 const iconContainer = createEl(
vendor: 1,427 bytes, lines 4138-4182
4138 4139 'span', 4140 { 4141 className: 'vjs-icon-placeholder vjs-svg-icon', 4142 }, 4143 { 4144 'aria-hidden': 'true', 4145 } 4146 ); 4147 const svgEl = document__default['default'].createElementNS(xmlnsURL, 'svg'); 4148 svgEl.setAttributeNS(null, 'viewBox', '0 0 512 512'); 4149 const useEl = document__default['default'].createElementNS(xmlnsURL, 'use'); 4150 svgEl.appendChild(useEl); 4151 useEl.setAttributeNS(null, 'href', `#vjs-icon-${iconName}`); 4152 iconContainer.appendChild(svgEl); 4153 4154 // Replace a pre-existing icon if one exists. 4155 if (this.iconIsSet_) { 4156 el.replaceChild(iconContainer, el.querySelector('.vjs-icon-placeholder')); 4157 } else { 4158 el.appendChild(iconContainer); 4159 } 4160 this.iconIsSet_ = true; 4161 return iconContainer; 4162 } 4163 4164 /** 4165 * Add a child `Component` inside the current `Component`. 4166 * 4167 * @param {string|Component} child 4168 * The name or instance of a child to add. 4169 * 4170 * @param {Object} [options={}] 4171 * The key/value store of options that will get passed to children of 4172 * the child. 4173 * 4174 * @param {number} [index=this.children_.length] 4175 * The index to attempt to add a child into. 4176 * 4177 * 4178 * @return {Component} 4179 * The `Component` that gets added as a child. When using a string the 4180 * `Component` will get created by this process. 4181 */ 4182 addChild(child, options = {}, index = this.children_.length) {
vendor: 10,421 bytes, lines 4183-4501
4183 let component; 4184 let componentName; 4185 4186 // If child is a string, create component with options 4187 if (typeof child === 'string') { 4188 componentName = toTitleCase$1(child); 4189 const componentClassName = options.componentClass || componentName; 4190 4191 // Set name through options 4192 options.name = componentName; 4193 4194 // Create a new object & element for this controls set 4195 // If there's no .player_, this is a player 4196 const ComponentClass = Component$1.getComponent(componentClassName); 4197 if (!ComponentClass) { 4198 throw new Error(`Component ${componentClassName} does not exist`); 4199 } 4200 4201 // data stored directly on the videojs object may be 4202 // misidentified as a component to retain 4203 // backwards-compatibility with 4.x. check to make sure the 4204 // component class can be instantiated. 4205 if (typeof ComponentClass !== 'function') { 4206 return null; 4207 } 4208 component = new ComponentClass(this.player_ || this, options); 4209 4210 // child is a component instance 4211 } else { 4212 component = child; 4213 } 4214 if (component.parentComponent_) { 4215 component.parentComponent_.removeChild(component); 4216 } 4217 this.children_.splice(index, 0, component); 4218 component.parentComponent_ = this; 4219 if (typeof component.id === 'function') { 4220 this.childIndex_[component.id()] = component; 4221 } 4222 4223 // If a name wasn't used to create the component, check if we can use the 4224 // name function of the component 4225 componentName = componentName || (component.name && toTitleCase$1(component.name())); 4226 if (componentName) { 4227 this.childNameIndex_[componentName] = component; 4228 this.childNameIndex_[toLowerCase(componentName)] = component; 4229 } 4230 4231 // Add the UI object's element to the container div (box) 4232 // Having an element is not required 4233 if (typeof component.el === 'function' && component.el()) { 4234 // If inserting before a component, insert before that component's element 4235 let refNode = null; 4236 if (this.children_[index + 1]) { 4237 // Most children are components, but the video tech is an HTML element 4238 if (this.children_[index + 1].el_) { 4239 refNode = this.children_[index + 1].el_; 4240 } else if (isEl(this.children_[index + 1])) { 4241 refNode = this.children_[index + 1]; 4242 } 4243 } 4244 this.contentEl().insertBefore(component.el(), refNode); 4245 } 4246 4247 // Return so it can stored on parent object if desired. 4248 return component; 4249 } 4250 4251 /** 4252 * Remove a child `Component` from this `Component`s list of children. Also removes 4253 * the child `Component`s element from this `Component`s element. 4254 * 4255 * @param {Component} component 4256 * The child `Component` to remove. 4257 */ 4258 removeChild(component) { 4259 if (typeof component === 'string') { 4260 component = this.getChild(component); 4261 } 4262 if (!component || !this.children_) { 4263 return; 4264 } 4265 let childFound = false; 4266 for (let i = this.children_.length - 1; i >= 0; i--) { 4267 if (this.children_[i] === component) { 4268 childFound = true; 4269 this.children_.splice(i, 1); 4270 break; 4271 } 4272 } 4273 if (!childFound) { 4274 return; 4275 } 4276 component.parentComponent_ = null; 4277 this.childIndex_[component.id()] = null; 4278 this.childNameIndex_[toTitleCase$1(component.name())] = null; 4279 this.childNameIndex_[toLowerCase(component.name())] = null; 4280 const compEl = component.el(); 4281 if (compEl && compEl.parentNode === this.contentEl()) { 4282 this.contentEl().removeChild(component.el()); 4283 } 4284 } 4285 4286 /** 4287 * Add and initialize default child `Component`s based upon options. 4288 */ 4289 initChildren() { 4290 const children = this.options_.children; 4291 if (children) { 4292 // `this` is `parent` 4293 const parentOptions = this.options_; 4294 const handleAdd = (child) => { 4295 const name = child.name; 4296 let opts = child.opts; 4297 4298 // Allow options for children to be set at the parent options 4299 // e.g. videojs(id, { controlBar: false }); 4300 // instead of videojs(id, { children: { controlBar: false }); 4301 if (parentOptions[name] !== undefined) { 4302 opts = parentOptions[name]; 4303 } 4304 4305 // Allow for disabling default components 4306 // e.g. options['children']['posterImage'] = false 4307 if (opts === false) { 4308 return; 4309 } 4310 4311 // Allow options to be passed as a simple boolean if no configuration 4312 // is necessary. 4313 if (opts === true) { 4314 opts = {}; 4315 } 4316 4317 // We also want to pass the original player options 4318 // to each component as well so they don't need to 4319 // reach back into the player for options later. 4320 opts.playerOptions = this.options_.playerOptions; 4321 4322 // Create and add the child component. 4323 // Add a direct reference to the child by name on the parent instance. 4324 // If two of the same component are used, different names should be supplied 4325 // for each 4326 const newChild = this.addChild(name, opts); 4327 if (newChild) { 4328 this[name] = newChild; 4329 } 4330 }; 4331 4332 // Allow for an array of children details to passed in the options 4333 let workingChildren; 4334 const Tech = Component$1.getComponent('Tech'); 4335 if (Array.isArray(children)) { 4336 workingChildren = children; 4337 } else { 4338 workingChildren = Object.keys(children); 4339 } 4340 workingChildren 4341 // children that are in this.options_ but also in workingChildren would 4342 // give us extra children we do not want. So, we want to filter them out. 4343 .concat( 4344 Object.keys(this.options_).filter(function (child) { 4345 return !workingChildren.some(function (wchild) { 4346 if (typeof wchild === 'string') { 4347 return child === wchild; 4348 } 4349 return child === wchild.name; 4350 }); 4351 }) 4352 ) 4353 .map((child) => { 4354 let name; 4355 let opts; 4356 if (typeof child === 'string') { 4357 name = child; 4358 opts = children[name] || this.options_[name] || {}; 4359 } else { 4360 name = child.name; 4361 opts = child; 4362 } 4363 return { 4364 name, 4365 opts, 4366 }; 4367 }) 4368 .filter((child) => { 4369 // we have to make sure that child.name isn't in the techOrder since 4370 // techs are registered as Components but can't aren't compatible 4371 // See https://github.com/videojs/video.js/issues/2772 4372 const c = Component$1.getComponent( 4373 child.opts.componentClass || toTitleCase$1(child.name) 4374 ); 4375 return c && !Tech.isTech(c); 4376 }) 4377 .forEach(handleAdd); 4378 } 4379 } 4380 4381 /** 4382 * Builds the default DOM class name. Should be overridden by sub-components. 4383 * 4384 * @return {string} 4385 * The DOM class name for this object. 4386 * 4387 * @abstract 4388 */ 4389 buildCSSClass() { 4390 // Child classes can include a function that does: 4391 // return 'CLASS NAME' + this._super(); 4392 return ''; 4393 } 4394 4395 /** 4396 * Bind a listener to the component's ready state. 4397 * Different from event listeners in that if the ready event has already happened 4398 * it will trigger the function immediately. 4399 * 4400 * @param {ReadyCallback} fn 4401 * Function that gets called when the `Component` is ready. 4402 */ 4403 ready(fn, sync = false) { 4404 if (!fn) { 4405 return; 4406 } 4407 if (!this.isReady_) { 4408 this.readyQueue_ = this.readyQueue_ || []; 4409 this.readyQueue_.push(fn); 4410 return; 4411 } 4412 if (sync) { 4413 fn.call(this); 4414 } else { 4415 // Call the function asynchronously by default for consistency 4416 this.setTimeout(fn, 1); 4417 } 4418 } 4419 4420 /** 4421 * Trigger all the ready listeners for this `Component`. 4422 * 4423 * @fires Component#ready 4424 */ 4425 triggerReady() { 4426 this.isReady_ = true; 4427 4428 // Ensure ready is triggered asynchronously 4429 this.setTimeout(function () { 4430 const readyQueue = this.readyQueue_; 4431 4432 // Reset Ready Queue 4433 this.readyQueue_ = []; 4434 if (readyQueue && readyQueue.length > 0) { 4435 readyQueue.forEach(function (fn) { 4436 fn.call(this); 4437 }, this); 4438 } 4439 4440 // Allow for using event listeners also 4441 /** 4442 * Triggered when a `Component` is ready. 4443 * 4444 * @event Component#ready 4445 * @type {Event} 4446 */ 4447 this.trigger('ready'); 4448 }, 1); 4449 } 4450 4451 /** 4452 * Find a single DOM element matching a `selector`. This can be within the `Component`s 4453 * `contentEl()` or another custom context. 4454 * 4455 * @param {string} selector 4456 * A valid CSS selector, which will be passed to `querySelector`. 4457 * 4458 * @param {Element|string} [context=this.contentEl()] 4459 * A DOM element within which to query. Can also be a selector string in 4460 * which case the first matching element will get used as context. If 4461 * missing `this.contentEl()` gets used. If `this.contentEl()` returns 4462 * nothing it falls back to `document`. 4463 * 4464 * @return {Element|null} 4465 * the dom element that was found, or null 4466 * 4467 * @see [Information on CSS Selectors](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors) 4468 */ 4469 $(selector, context) { 4470 return $(selector, context || this.contentEl()); 4471 } 4472 4473 /** 4474 * Finds all DOM element matching a `selector`. This can be within the `Component`s 4475 * `contentEl()` or another custom context. 4476 * 4477 * @param {string} selector 4478 * A valid CSS selector, which will be passed to `querySelectorAll`. 4479 * 4480 * @param {Element|string} [context=this.contentEl()] 4481 * A DOM element within which to query. Can also be a selector string in 4482 * which case the first matching element will get used as context. If 4483 * missing `this.contentEl()` gets used. If `this.contentEl()` returns 4484 * nothing it falls back to `document`. 4485 * 4486 * @return {NodeList} 4487 * a list of dom elements that were found 4488 * 4489 * @see [Information on CSS Selectors](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors) 4490 */ 4491 $$(selector, context) { 4492 return $$(selector, context || this.contentEl()); 4493 } 4494 4495 /** 4496 * Check if a component's element has a CSS class name. 4497 * 4498 * @param {string} classToCheck 4499 * CSS class name to check. 4500 * 4501 * @return {boolean}
4502 * - True if the `Component` has the class. 4503 * - False if the `Component` does not have the class` 4504 */ 4505 hasClass(classToCheck) { 4506 return hasClass(this.el_, classToCheck); 4507 } 4508 4509 /** 4510 * Add a CSS class name to the `Component`s element. 4511 * 4512 * @param {...string} classesToAdd 4513 * One or more CSS class name to add. 4514 */ 4515 addClass(...classesToAdd) { 4516 addClass(this.el_, ...classesToAdd); 4517 } 4518 4519 /** 4520 * Remove a CSS class name from the `Component`s element. 4521 * 4522 * @param {...string} classesToRemove 4523 * One or more CSS class name to remove. 4524 */ 4525 removeClass(...classesToRemove) { 4526 removeClass(this.el_, ...classesToRemove); 4527 } 4528 4529 /** 4530 * Add or remove a CSS class name from the component's element. 4531 * - `classToToggle` gets added when {@link Component#hasClass} would return false. 4532 * - `classToToggle` gets removed when {@link Component#hasClass} would return true. 4533 * 4534 * @param {string} classToToggle 4535 * The class to add or remove. Passed to DOMTokenList's toggle() 4536 * 4537 * @param {boolean|Dom.PredicateCallback} [predicate] 4538 * A boolean or function that returns a boolean. Passed to DOMTokenList's toggle(). 4539 */ 4540 toggleClass(classToToggle, predicate) { 4541 toggleClass(this.el_, classToToggle, predicate); 4542 } 4543 4544 /** 4545 * Show the `Component`s element if it is hidden by removing the 4546 * 'vjs-hidden' class name from it. 4547 */ 4548 show() { 4549 this.removeClass('vjs-hidden'); 4550 } 4551 4552 /** 4553 * Hide the `Component`s element if it is currently showing by adding the 4554 * 'vjs-hidden` class name to it. 4555 */ 4556 hide() { 4557 this.addClass('vjs-hidden'); 4558 } 4559 4560 /** 4561 * Lock a `Component`s element in its visible state by adding the 'vjs-lock-showing' 4562 * class name to it. Used during fadeIn/fadeOut. 4563 * 4564 * @private 4565 */ 4566 lockShowing() { 4567 this.addClass('vjs-lock-showing'); 4568 } 4569 4570 /** 4571 * Unlock a `Component`s element from its visible state by removing the 'vjs-lock-showing' 4572 * class name from it. Used during fadeIn/fadeOut. 4573 * 4574 * @private 4575 */ 4576 unlockShowing() { 4577 this.removeClass('vjs-lock-showing'); 4578 } 4579 4580 /** 4581 * Get the value of an attribute on the `Component`s element. 4582 * 4583 * @param {string} attribute 4584 * Name of the attribute to get the value from. 4585 * 4586 * @return {string|null} 4587 * - The value of the attribute that was asked for. 4588 * - Can be an empty string on some browsers if the attribute does not exist 4589 * or has no value 4590 * - Most browsers will return null if the attribute does not exist or has 4591 * no value. 4592 * 4593 * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute} 4594 */ 4595 getAttribute(attribute) { 4596 return getAttribute(this.el_, attribute); 4597 } 4598 4599 /** 4600 * Set the value of an attribute on the `Component`'s element 4601 * 4602 * @param {string} attribute 4603 * Name of the attribute to set. 4604 * 4605 * @param {string} value 4606 * Value to set the attribute to. 4607 * 4608 * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute} 4609 */ 4610 setAttribute(attribute, value) { 4611 setAttribute(this.el_, attribute, value); 4612 } 4613 4614 /** 4615 * Remove an attribute from the `Component`s element. 4616 * 4617 * @param {string} attribute 4618 * Name of the attribute to remove. 4619 * 4620 * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/removeAttribute} 4621 */ 4622 removeAttribute(attribute) { 4623 removeAttribute(this.el_, attribute); 4624 } 4625 4626 /** 4627 * Get or set the width of the component based upon the CSS styles. 4628 * See {@link Component#dimension} for more detailed information. 4629 * 4630 * @param {number|string} [num] 4631 * The width that you want to set postfixed with '%', 'px' or nothing. 4632 * 4633 * @param {boolean} [skipListeners] 4634 * Skip the componentresize event trigger 4635 * 4636 * @return {number|undefined} 4637 * The width when getting, zero if there is no width 4638 */ 4639 width(num, skipListeners) { 4640 return this.dimension('width', num, skipListeners); 4641 } 4642 4643 /** 4644 * Get or set the height of the component based upon the CSS styles. 4645 * See {@link Component#dimension} for more detailed information. 4646 * 4647 * @param {number|string} [num] 4648 * The height that you want to set postfixed with '%', 'px' or nothing. 4649 * 4650 * @param {boolean} [skipListeners] 4651 * Skip the componentresize event trigger 4652 * 4653 * @return {number|undefined} 4654 * The height when getting, zero if there is no height 4655 */ 4656 height(num, skipListeners) { 4657 return this.dimension('height', num, skipListeners); 4658 } 4659 4660 /** 4661 * Set both the width and height of the `Component` element at the same time. 4662 * 4663 * @param {number|string} width 4664 * Width to set the `Component`s element to. 4665 * 4666 * @param {number|string} height 4667 * Height to set the `Component`s element to. 4668 */ 4669 dimensions(width, height) { 4670 // Skip componentresize listeners on width for optimization 4671 this.width(width, true); 4672 this.height(height); 4673 } 4674 4675 /** 4676 * Get or set width or height of the `Component` element. This is the shared code 4677 * for the {@link Component#width} and {@link Component#height}. 4678 * 4679 * Things to know: 4680 * - If the width or height in an number this will return the number postfixed with 'px'. 4681 * - If the width/height is a percent this will return the percent postfixed with '%' 4682 * - Hidden elements have a width of 0 with `window.getComputedStyle`. This function 4683 * defaults to the `Component`s `style.width` and falls back to `window.getComputedStyle`. 4684 * See [this]{@link http://www.foliotek.com/devblog/getting-the-width-of-a-hidden-element-with-jquery-u
4684sing-width/} 4685 * for more information 4686 * - If you want the computed style of the component, use {@link Component#currentWidth} 4687 * and {@link {Component#currentHeight} 4688 * 4689 * @fires Component#componentresize 4690 * 4691 * @param {string} widthOrHeight 4692 8 'width' or 'height' 4693 * 4694 * @param {number|string} [num] 4695 8 New dimension 4696 * 4697 * @param {boolean} [skipListeners] 4698 * Skip componentresize event trigger 4699 * 4700 * @return {number|undefined} 4701 * The dimension when getting or 0 if unset 4702 */ 4703 dimension(widthOrHeight, num, skipListeners) { 4704 if (num !== undefined) { 4705 // Set to zero if null or literally NaN (NaN !== NaN) 4706 if (num === null || num !== num) { 4707 num = 0; 4708 } 4709 4710 // Check if using css width/height (% or px) and adjust 4711 if (('' + num).indexOf('%') !== -1 || ('' + num).indexOf('px') !== -1) { 4712 this.el_.style[widthOrHeight] = num; 4713 } else if (num === 'auto') { 4714 this.el_.style[widthOrHeight] = ''; 4715 } else { 4716 this.el_.style[widthOrHeight] = num + 'px'; 4717 } 4718 4719 // skipListeners allows us to avoid triggering the resize event when setting both width and height 4720 if (!skipListeners) { 4721 /** 4722 * Triggered when a component is resized. 4723 * 4724 * @event Component#componentresize 4725 * @type {Event} 4726 */ 4727 this.trigger('componentresize'); 4728 } 4729 return; 4730 } 4731 4732 // Not setting a value, so getting it 4733 // Make sure element exists 4734 if (!this.el_) { 4735 return 0; 4736 } 4737 4738 // Get dimension value from style 4739 const val = this.el_.style[widthOrHeight]; 4740 const pxIndex = val.indexOf('px'); 4741 if (pxIndex !== -1) { 4742 // Return the pixel value with no 'px' 4743 return parseInt(val.slice(0, pxIndex), 10); 4744 } 4745 4746 // No px so using % or no style was set, so falling back to offsetWidth/height 4747 // If component has display:none, offset will return 0 4748 // TODO: handle display:none and no dimension style using px 4749 return parseInt(this.el_['offset' + toTitleCase$1(widthOrHeight)], 10); 4750 } 4751 4752 /** 4753 * Get the computed width or the height of the component's element. 4754 * 4755 * Uses `window.getComputedStyle`. 4756 * 4757 * @param {string} widthOrHeight 4758 * A string containing 'width' or 'height'. Whichever one you want to get. 4759 * 4760 * @return {number} 4761 * The dimension that gets asked for or 0 if nothing was set 4762 * for that dimension. 4763 */ 4764 currentDimension(widthOrHeight) { 4765 let computedWidthOrHeight = 0; 4766 if (widthOrHeight !== 'width' && widthOrHeight !== 'height') { 4767 throw new Error('currentDimension only accepts width or height value'); 4768 } 4769 computedWidthOrHeight = computedStyle(this.el_, widthOrHeight); 4770 4771 // remove 'px' from variable and parse as integer 4772 computedWidthOrHeight = parseFloat(computedWidthOrHeight); 4773 4774 // if the computed value is still 0, it's possible that the browser is lying 4775 // and we want to check the offset values. 4776 // This code also runs wherever getComputedStyle doesn't exist. 4777 if (computedWidthOrHeight === 0 || isNaN(computedWidthOrHeight)) { 4778 const rule = `offset${toTitleCase$1(widthOrHeight)}`; 4779 computedWidthOrHeight = this.el_[rule]; 4780 } 4781 return computedWidthOrHeight; 4782 } 4783 4784 /** 4785 * An object that contains width and height values of the `Component`s 4786 * computed style. Uses `window.getComputedStyle`. 4787 * 4788 * @typedef {Object} Component~DimensionObject 4789 * 4790 * @property {number} width 4791 * The width of the `Component`s computed style. 4792 * 4793 * @property {number} height 4794 * The height of the `Component`s computed style. 4795 */ 4796 4797 /** 4798 * Get an object that contains computed width and height values of the 4799 * component's element. 4800 * 4801 * Uses `window.getComputedStyle`. 4802 * 4803 * @return {Component~DimensionObject} 4804 * The computed dimensions of the component's element. 4805 */ 4806 currentDimensions() { 4807 return { 4808 width: this.currentDimension('width'), 4809 height: this.currentDimension('height'), 4810 }; 4811 } 4812 4813 /** 4814 * Get the computed width of the component's element. 4815 * 4816 * Uses `window.getComputedStyle`. 4817 * 4818 * @return {number} 4819 * The computed width of the component's element. 4820 */ 4821 currentWidth() { 4822 return this.currentDimension('width'); 4823 } 4824 4825 /** 4826 * Get the computed height of the component's element. 4827 * 4828 * Uses `window.getComputedStyle`. 4829 * 4830 * @return {number} 4831 * The computed height of the component's element. 4832 */ 4833 currentHeight() { 4834 return this.currentDimension('height'); 4835 } 4836 4837 /** 4838 * Retrieves the position and size information of the component's element. 4839 * 4840 * @return {Object} An object with `boundingClientRect` and `center` properties. 4841 * - `boundingClientRect`: An object with properties `x`, `y`, `width`, 4842 * `height`, `top`, `right`, `bottom`, and `left`, representing 4843 * the bounding rectangle of the element. 4844 * - `center`: An object with properties `x` and `y`, representing 4845 * the center point of the element. `width` and `height` are set to 0. 4846 */ 4847 getPositions() { 4848 const rect = this.el_.getBoundingClientRect(); 4849 4850 // Creating objects that mirror DOMRectReadOnly for boundingClientRect and center 4851 const boundingClientRect = { 4852 x: rect.x, 4853 y: rect.y, 4854 width: rect.width, 4855 height: rect.height, 4856 top: rect.top, 4857 right: rect.right, 4858 bottom: rect.bottom, 4859 left: rect.left, 4860 }; 4861 4862 // Calculating the center position 4863 const center = { 4864 x: rect.left + rect.width / 2, 4865 y: rect.top + rect.height / 2, 4866 width: 0, 4867 height: 0, 4868 top: rect.top + rect.height / 2, 4869 right: rect.left + rect.width / 2, 4870 bottom: rect.top + rect.height / 2, 4871 left: rect.left + rect.width / 2, 4872 }; 4873 return { 4874 boundingClientRect, 4875 center, 4876 }; 4877 } 4878 4879 /** 4880 * Set the focus to this component 4881 */ 4882 focus() { 4883 this.el_.focus(); 4884 } 4885 4886 /** 4887 * Remove the focus from this component 4888 */ 4889 blur() { 4890 this.el_.blur(); 4891 } 4892 4893 /** 4894 * When this Component receives a `keydown` event which it does not process, 4895 * it passes the event to the Player for handling. 4896 * 4897 * @param {KeyboardEvent} event
vendor: 30,222 bytes, lines 4898-5762
4898 * The `keydown` event that caused this function to be called. 4899 */ 4900 handleKeyDown(event) { 4901 if (this.player_) { 4902 // We only stop propagation here because we want unhandled events to fall 4903 // back to the browser. Exclude Tab for focus trapping, exclude also when spatialNavigation is enabled. 4904 if ( 4905 event.key !== 'Tab' && 4906 !( 4907 this.player_.options_.playerOptions.spatialNavigation && 4908 this.player_.options_.playerOptions.spatialNavigation.enabled 4909 ) 4910 ) { 4911 event.stopPropagation(); 4912 } 4913 this.player_.handleKeyDown(event); 4914 } 4915 } 4916 4917 /** 4918 * Many components used to have a `handleKeyPress` method, which was poorly 4919 * named because it listened to a `keydown` event. This method name now 4920 * delegates to `handleKeyDown`. This means anyone calling `handleKeyPress` 4921 * will not see their method calls stop working. 4922 * 4923 * @param {KeyboardEvent} event 4924 * The event that caused this function to be called. 4925 */ 4926 handleKeyPress(event) { 4927 this.handleKeyDown(event); 4928 } 4929 4930 /** 4931 * Emit a 'tap' events when touch event support gets detected. This gets used to 4932 * support toggling the controls through a tap on the video. They get enabled 4933 * because every sub-component would have extra overhead otherwise. 4934 * 4935 * @protected 4936 * @fires Component#tap 4937 * @listens Component#touchstart 4938 * @listens Component#touchmove 4939 * @listens Component#touchleave 4940 * @listens Component#touchcancel 4941 * @listens Component#touchend 4942 */ 4943 emitTapEvents() { 4944 // Track the start time so we can determine how long the touch lasted 4945 let touchStart = 0; 4946 let firstTouch = null; 4947 4948 // Maximum movement allowed during a touch event to still be considered a tap 4949 // Other popular libs use anywhere from 2 (hammer.js) to 15, 4950 // so 10 seems like a nice, round number. 4951 const tapMovementThreshold = 10; 4952 4953 // The maximum length a touch can be while still being considered a tap 4954 const touchTimeThreshold = 200; 4955 let couldBeTap; 4956 this.on('touchstart', function (event) { 4957 // If more than one finger, don't consider treating this as a click 4958 if (event.touches.length === 1) { 4959 // Copy pageX/pageY from the object 4960 firstTouch = { 4961 pageX: event.touches[0].pageX, 4962 pageY: event.touches[0].pageY, 4963 }; 4964 // Record start time so we can detect a tap vs. "touch and hold" 4965 touchStart = window__default['default'].performance.now(); 4966 // Reset couldBeTap tracking 4967 couldBeTap = true; 4968 } 4969 }); 4970 this.on('touchmove', function (event) { 4971 // If more than one finger, don't consider treating this as a click 4972 if (event.touches.length > 1) { 4973 couldBeTap = false; 4974 } else if (firstTouch) { 4975 // Some devices will throw touchmoves for all but the slightest of taps. 4976 // So, if we moved only a small distance, this could still be a tap 4977 const xdiff = event.touches[0].pageX - firstTouch.pageX; 4978 const ydiff = event.touches[0].pageY - firstTouch.pageY; 4979 const touchDistance = Math.sqrt(xdiff * xdiff + ydiff * ydiff); 4980 if (touchDistance > tapMovementThreshold) { 4981 couldBeTap = false; 4982 } 4983 } 4984 }); 4985 const noTap = function () { 4986 couldBeTap = false; 4987 }; 4988 4989 // TODO: Listen to the original target. http://youtu.be/DujfpXOKUp8?t=13m8s 4990 this.on('touchleave', noTap); 4991 this.on('touchcancel', noTap); 4992 4993 // When the touch ends, measure how long it took and trigger the appropriate 4994 // event 4995 this.on('touchend', function (event) { 4996 firstTouch = null; 4997 // Proceed only if the touchmove/leave/cancel event didn't happen 4998 if (couldBeTap === true) { 4999 // Measure how long the touch lasted 5000 const touchTime = window__default['default'].performance.now() - touchStart; 5001 5002 // Make sure the touch was less than the threshold to be considered a tap 5003 if (touchTime < touchTimeThreshold) { 5004 // Don't let browser turn this into a click 5005 event.preventDefault(); 5006 /** 5007 * Triggered when a `Component` is tapped. 5008 * 5009 * @event Component#tap 5010 * @type {MouseEvent} 5011 */ 5012 this.trigger('tap'); 5013 // It may be good to copy the touchend event object and change the 5014 // type to tap, if the other event properties aren't exact after 5015 // Events.fixEvent runs (e.g. event.target) 5016 } 5017 } 5018 }); 5019 } 5020 5021 /** 5022 * This function reports user activity whenever touch events happen. This can get 5023 * turned off by any sub-components that wants touch events to act another way. 5024 * 5025 * Report user touch activity when touch events occur. User activity gets used to 5026 * determine when controls should show/hide. It is simple when it comes to mouse 5027 * events, because any mouse event should show the controls. So we capture mouse 5028 * events that bubble up to the player and report activity when that happens. 5029 * With touch events it isn't as easy as `touchstart` and `touchend` toggle player 5030 * controls. So touch events can't help us at the player level either. 5031 * 5032 * User activity gets checked asynchronously. So what could happen is a tap event 5033 * on the video turns the controls off. Then the `touchend` event bubbles up to 5034 * the player. Which, if it reported user activity, would turn the controls right 5035 * back on. We also don't want to completely block touch events from bubbling up. 5036 * Furthermore a `touchmove` event and anything other than a tap, should not turn 5037 * controls back on. 5038 * 5039 * @listens Component#touchstart 5040 * @listens Component#touchmove 5041 * @listens Component#touchend 5042 * @listens Component#touchcancel 5043 */ 5044 enableTouchActivity() { 5045 // Don't continue if the root player doesn't support reporting user activity 5046 if (!this.player() || !this.player().reportUserActivity) { 5047 return; 5048 } 5049 5050 // listener for reporting that the user is active 5051 const report = bind_(this.player(), this.player().reportUserActivity); 5052 let touchHolding; 5053 this.on('touchstart', function () { 5054 report(); 5055 // For as long as the they are touching the device or have their mouse down, 5056 // we consider them active even if they're not moving their finger or mouse. 5057 // So we want to continue to update that they are active 5058 this.clearInterval(touchHolding); 5059 // report at the same interval as activityCheck 5060 touchHolding = this.setInterval(report, 250); 5061 }); 5062 const touchEnd = function (event) { 5063 report(); 5064 // stop the interval that maintains activity if the touch is holding 5065 this.clearInterval(touchHolding); 5066 }; 5067 this.on('touchmove', report); 5068 this.on('touchend', touchEnd); 5069 this.on('touchcancel', touchEnd); 5070 } 5071 5072 /** 5073 * A callback that has no parameters and is bound into `Component`s context. 5074 * 5075 * @callback Component~GenericCallback 5076 * @this Component 5077 */ 5078 5079 /** 5080 * Creates a function that runs after an `x` millisecond timeout. This function is a 5081 * wrapper around `window.setTimeout`. There are a few reasons to use this one 5082 * instead though: 5083 * 1. It gets cleared via {@link Component#clearTimeout} when 5084 * {@link Component#dispose} gets called. 5085 * 2. The function callback will gets turned into a {@link Component~GenericCallback} 5086 * 5087 * > Note: You can't use `window.clearTimeout` on the id returned by this function. This 5088 * will cause its dispose listener not to get cleaned up! Please use 5089 * {@link Component#clearTimeout} or {@link Component#dispose} instead. 5090 * 5091 * @param {Component~GenericCallback} fn 5092 * The function that will be run after `timeout`. 5093 * 5094 * @param {number} timeout 5095 * Timeout in milliseconds to delay before executing the specified function. 5096 * 5097 * @return {number} 5098 * Returns a timeout ID that gets used to identify the timeout. It can also 5099 * get used in {@link Component#clearTimeout} to clear the timeout that 5100 * was set. 5101 * 5102 * @listens Component#dispose 5103 * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setTimeout} 5104 */ 5105 setTimeout(fn, timeout) { 5106 // declare as variables so they are properly available in timeout function 5107 // eslint-disable-next-line 5108 var timeoutId; 5109 fn = bind_(this, fn); 5110 this.clearTimersOnDispose_(); 5111 timeoutId = window__default['default'].setTimeout(() => { 5112 if (this.setTimeoutIds_.has(timeoutId)) { 5113 this.setTimeoutIds_.delete(timeoutId); 5114 } 5115 fn(); 5116 }, timeout); 5117 this.setTimeoutIds_.add(timeoutId); 5118 return timeoutId; 5119 } 5120 5121 /** 5122 * Clears a timeout that gets created via `window.setTimeout` or 5123 * {@link Component#setTimeout}. If you set a timeout via {@link Component#setTimeout} 5124 * use this function instead of `window.clearTimout`. If you don't your dispose 5125 * listener will not get cleaned up until {@link Component#dispose}! 5126 * 5127 * @param {number} timeoutId 5128 * The id of the timeout to clear. The return value of 5129 * {@link Component#setTimeout} or `window.setTimeout`. 5130 * 5131 * @return {number} 5132 * Returns the timeout id that was cleared. 5133 * 5134 * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearTimeout} 5135 */ 5136 clearTimeout(timeoutId) { 5137 if (this.setTimeoutIds_.has(timeoutId)) { 5138 this.setTimeoutIds_.delete(timeoutId); 5139 window__default['default'].clearTimeout(timeoutId); 5140 } 5141 return timeoutId; 5142 } 5143 5144 /** 5145 * Creates a function that gets run every `x` milliseconds. This function is a wrapper 5146 * around `window.setInterval`. There are a few reasons to use this one instead though. 5147 * 1. It gets cleared via {@link Component#clearInterval} when 5148 * {@link Component#dispose} gets called. 5149 * 2. The function callback will be a {@link Component~GenericCallback} 5150 * 5151 * @param {Component~GenericCallback} fn 5152 * The function to run every `x` seconds. 5153 * 5154 * @param {number} interval 5155 * Execute the specified function every `x` milliseconds. 5156 * 5157 * @return {number} 5158 * Returns an id that can be used to identify the interval. It can also be be used in 5159 * {@link Component#clearInterval} to clear the interval. 5160 * 5161 * @listens Component#dispose 5162 * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setInterval} 5163 */ 5164 setInterval(fn, interval) { 5165 fn = bind_(this, fn); 5166 this.clearTimersOnDispose_(); 5167 const intervalId = window__default['default'].setInterval(fn, interval); 5168 this.setIntervalIds_.add(intervalId); 5169 return intervalId; 5170 } 5171 5172 /** 5173 * Clears an interval that gets created via `window.setInterval` or 5174 * {@link Component#setInterval}. If you set an interval via {@link Component#setInterval} 5175 * use this function instead of `window.clearInterval`. If you don't your dispose 5176 * listener will not get cleaned up until {@link Component#dispose}! 5177 * 5178 * @param {number} intervalId 5179 * The id of the interval to clear. The return value of 5180 * {@link Component#setInterval} or `window.setInterval`. 5181 * 5182 * @return {number} 5183 * Returns the interval id that was cleared. 5184 * 5185 * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearInterval} 5186 */ 5187 clearInterval(intervalId) { 5188 if (this.setIntervalIds_.has(intervalId)) { 5189 this.setIntervalIds_.delete(intervalId); 5190 window__default['default'].clearInterval(intervalId); 5191 } 5192 return intervalId; 5193 } 5194 5195 /** 5196 * Queues up a callback to be passed to requestAnimationFrame (rAF), but 5197 * with a few extra bonuses: 5198 * 5199 * - Supports browsers that do not support rAF by falling back to 5200 * {@link Component#setTimeout}. 5201 * 5202 * - The callback is turned into a {@link Component~GenericCallback} (i.e. 5203 * bound to the component). 5204 * 5205 * - Automatic cancellation of the rAF callback is handled if the component 5206 * is disposed before it is called. 5207 * 5208 * @param {Component~GenericCallback} fn 5209 * A function that will be bound to this component and executed just 5210 * before the browser's next repaint. 5211 * 5212 * @return {number} 5213 * Returns an rAF ID that gets used to identify the timeout. It can 5214 * also be used in {@link Component#cancelAnimationFrame} to cancel 5215 * the animation frame callback. 5216 * 5217 * @listens Component#dispose 5218 * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame} 5219 */ 5220 requestAnimationFrame(fn) { 5221 this.clearTimersOnDispose_(); 5222 5223 // declare as variables so they are properly available in rAF function 5224 // eslint-disable-next-line 5225 var id; 5226 fn = bind_(this, fn); 5227 id = window__default['default'].requestAnimationFrame(() => { 5228 if (this.rafIds_.has(id)) { 5229 this.rafIds_.delete(id); 5230 } 5231 fn(); 5232 }); 5233 this.rafIds_.add(id); 5234 return id; 5235 } 5236 5237 /** 5238 * Request an animation frame, but only one named animation 5239 * frame will be queued. Another will never be added until 5240 * the previous one finishes. 5241 * 5242 * @param {string} name 5243 * The name to give this requestAnimationFrame 5244 * 5245 * @param {Component~GenericCallback} fn 5246 * A function that will be bound to this component and executed just 5247 * before the browser's next repaint. 5248 */ 5249 requestNamedAnimationFrame(name, fn) { 5250 if (this.namedRafs_.has(name)) { 5251 this.cancelNamedAnimationFrame(name); 5252 } 5253 this.clearTimersOnDispose_(); 5254 fn = bind_(this, fn); 5255 const id = this.requestAnimationFrame(() => { 5256 fn(); 5257 if (this.namedRafs_.has(name)) { 5258 this.namedRafs_.delete(name); 5259 } 5260 }); 5261 this.namedRafs_.set(name, id); 5262 return name; 5263 } 5264 5265 /** 5266 * Cancels a current named animation frame if it exists. 5267 * 5268 * @param {string} name 5269 * The name of the requestAnimationFrame to cancel. 5270 */ 5271 cancelNamedAnimationFrame(name) { 5272 if (!this.namedRafs_.has(name)) { 5273 return; 5274 } 5275 this.cancelAnimationFrame(this.namedRafs_.get(name)); 5276 this.namedRafs_.delete(name); 5277 } 5278 5279 /** 5280 * Cancels a queued callback passed to {@link Component#requestAnimationFrame} 5281 * (rAF). 5282 * 5283 * If you queue an rAF callback via {@link Component#requestAnimationFrame}, 5284 * use this function instead of `window.cancelAnimationFrame`. If you don't, 5285 * your dispose listener will not get cleaned up until {@link Component#dispose}! 5286 * 5287 * @param {number} id 5288 * The rAF ID to clear. The return value of {@link Component#requestAnimationFrame}. 5289 * 5290 * @return {number} 5291 * Returns the rAF ID that was cleared. 5292 * 5293 * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/cancelAnimationFrame} 5294 */ 5295 cancelAnimationFrame(id) { 5296 if (this.rafIds_.has(id)) { 5297 this.rafIds_.delete(id); 5298 window__default['default'].cancelAnimationFrame(id); 5299 } 5300 return id; 5301 } 5302 5303 /** 5304 * A function to setup `requestAnimationFrame`, `setTimeout`, 5305 * and `setInterval`, clearing on dispose. 5306 * 5307 * > Previously each timer added and removed dispose listeners on it's own. 5308 * For better performance it was decided to batch them all, and use `Set`s 5309 * to track outstanding timer ids. 5310 * 5311 * @private 5312 */ 5313 clearTimersOnDispose_() { 5314 if (this.clearingTimersOnDispose_) { 5315 return; 5316 } 5317 this.clearingTimersOnDispose_ = true; 5318 this.one('dispose', () => { 5319 [ 5320 ['namedRafs_', 'cancelNamedAnimationFrame'], 5321 ['rafIds_', 'cancelAnimationFrame'], 5322 ['setTimeoutIds_', 'clearTimeout'], 5323 ['setIntervalIds_', 'clearInterval'], 5324 ].forEach(([idName, cancelName]) => { 5325 // for a `Set` key will actually be the value again 5326 // so forEach((val, val) =>` but for maps we want to use 5327 // the key. 5328 this[idName].forEach((val, key) => this[cancelName](key)); 5329 }); 5330 this.clearingTimersOnDispose_ = false; 5331 }); 5332 } 5333 5334 /** 5335 * Decide whether an element is actually disabled or not. 5336 * 5337 * @function isActuallyDisabled 5338 * @param element {Node} 5339 * @return {boolean} 5340 * 5341 * @see {@link https://html.spec.whatwg.org/multipage/semantics-other.html#concept-element-disabled} 5342 */ 5343 getIsDisabled() { 5344 return Boolean(this.el_.disabled); 5345 } 5346 5347 /** 5348 * Decide whether the element is expressly inert or not. 5349 * 5350 * @see {@link https://html.spec.whatwg.org/multipage/interaction.html#expressly-inert} 5351 * @function isExpresslyInert 5352 * @param element {Node} 5353 * @return {boolean} 5354 */ 5355 getIsExpresslyInert() { 5356 return this.el_.inert && !this.el_.ownerDocument.documentElement.inert; 5357 } 5358 5359 /** 5360 * Determine whether or not this component can be considered as focusable component. 5361 * 5362 * @param {HTMLElement} el - The HTML element representing the component. 5363 * @return {boolean} 5364 * If the component can be focused, will be `true`. Otherwise, `false`. 5365 */ 5366 getIsFocusable(el) { 5367 const element = el || this.el_; 5368 return element.tabIndex >= 0 && !(this.getIsDisabled() || this.getIsExpresslyInert()); 5369 } 5370 5371 /** 5372 * Determine whether or not this component is currently visible/enabled/etc... 5373 * 5374 * @param {HTMLElement} el - The HTML element representing the component. 5375 * @return {boolean} 5376 * If the component can is currently visible & enabled, will be `true`. Otherwise, `false`. 5377 */ 5378 getIsAvailableToBeFocused(el) { 5379 /** 5380 * Decide the style property of this element is specified whether it's visible or not. 5381 * 5382 * @function isVisibleStyleProperty 5383 * @param element {CSSStyleDeclaration} 5384 * @return {boolean} 5385 */ 5386 function isVisibleStyleProperty(element) { 5387 const elementStyle = window__default['default'].getComputedStyle(element, null); 5388 const thisVisibility = elementStyle.getPropertyValue('visibility'); 5389 const thisDisplay = elementStyle.getPropertyValue('display'); 5390 const invisibleStyle = ['hidden', 'collapse']; 5391 return thisDisplay !== 'none' && !invisibleStyle.includes(thisVisibility); 5392 } 5393 5394 /** 5395 * Decide whether the element is being rendered or not. 5396 * 1. If an element has the style as "visibility: hidden | collapse" or "display: none", it is not being rendered. 5397 * 2. If an element has the style as "opacity: 0", it is not being rendered.(that is, invisible). 5398 * 3. If width and height of an element are explicitly set to 0, it is not being rendered. 5399 * 4. If a parent element is hidden, an element itself is not being rendered. 5400 * (CSS visibility property and display property are inherited.) 5401 * 5402 * @see {@link https://html.spec.whatwg.org/multipage/rendering.html#being-rendered} 5403 * @function isBeingRendered 5404 * @param element {Node} 5405 * @return {boolean} 5406 */ 5407 function isBeingRendered(element) { 5408 if (!isVisibleStyleProperty(element.parentElement)) { 5409 return false; 5410 } 5411 if ( 5412 !isVisibleStyleProperty(element) || 5413 element.style.opacity === '0' || 5414 window__default['default'].getComputedStyle(element).height === '0px' || 5415 window__default['default'].getComputedStyle(element).width === '0px' 5416 ) { 5417 return false; 5418 } 5419 return true; 5420 } 5421 5422 /** 5423 * Determine if the element is visible for the user or not. 5424 * 1. If an element sum of its offsetWidth, offsetHeight, height and width is less than 1 is not visible. 5425 * 2. If elementCenter.x is less than is not visible. 5426 * 3. If elementCenter.x is more than the document's width is not visible. 5427 * 4. If elementCenter.y is less than 0 is not visible. 5428 * 5. If elementCenter.y is the document's height is not visible. 5429 * 5430 * @function isVisible 5431 * @param element {Node} 5432 * @return {boolean} 5433 */ 5434 function isVisible(element) { 5435 if ( 5436 element.offsetWidth + 5437 element.offsetHeight + 5438 element.getBoundingClientRect().height + 5439 element.getBoundingClientRect().width === 5440 0 5441 ) { 5442 return false; 5443 } 5444 5445 // Define elementCenter object with props of x and y 5446 // x: Left position relative to the viewport plus element's width (no margin) divided between 2. 5447 // y: Top position relative to the viewport plus element's height (no margin) divided between 2. 5448 const elementCenter = { 5449 x: element.getBoundingClientRect().left + element.offsetWidth / 2, 5450 y: element.getBoundingClientRect().top + element.offsetHeight / 2, 5451 }; 5452 if (elementCenter.x < 0) { 5453 return false; 5454 } 5455 if ( 5456 elementCenter.x > 5457 (document__default['default'].documentElement.clientWidth || 5458 window__default['default'].innerWidth) 5459 ) { 5460 return false; 5461 } 5462 if (elementCenter.y < 0) { 5463 return false; 5464 } 5465 if ( 5466 elementCenter.y > 5467 (document__default['default'].documentElement.clientHeight || 5468 window__default['default'].innerHeight) 5469 ) { 5470 return false; 5471 } 5472 let pointContainer = document__default['default'].elementFromPoint( 5473 elementCenter.x, 5474 elementCenter.y 5475 ); 5476 while (pointContainer) { 5477 if (pointContainer === element) { 5478 return true; 5479 } 5480 if (pointContainer.parentNode) { 5481 pointContainer = pointContainer.parentNode; 5482 } else { 5483 return false; 5484 } 5485 } 5486 } 5487 5488 // If no DOM element was passed as argument use this component's element. 5489 if (!el) { 5490 el = this.el(); 5491 } 5492 5493 // If element is visible, is being rendered & either does not have a parent element or its tabIndex is not negative. 5494 if (isVisible(el) && isBeingRendered(el) && (!el.parentElement || el.tabIndex >= 0)) { 5495 return true; 5496 } 5497 return false; 5498 } 5499 5500 /** 5501 * Register a `Component` with `videojs` given the name and the component. 5502 * 5503 * > NOTE: {@link Tech}s should not be registered as a `Component`. {@link Tech}s 5504 * should be registered using {@link Tech.registerTech} or 5505 * {@link videojs:videojs.registerTech}. 5506 * 5507 * > NOTE: This function can also be seen on videojs as 5508 * {@link videojs:videojs.registerComponent}. 5509 * 5510 * @param {string} name 5511 * The name of the `Component` to register. 5512 * 5513 * @param {Component} ComponentToRegister 5514 * The `Component` class to register. 5515 * 5516 * @return {Component} 5517 * The `Component` that was registered. 5518 */ 5519 static registerComponent(name, ComponentToRegister) { 5520 if (typeof name !== 'string' || !name) { 5521 throw new Error(`Illegal component name, "${name}"; must be a non-empty string.`); 5522 } 5523 const Tech = Component$1.getComponent('Tech'); 5524 5525 // We need to make sure this check is only done if Tech has been registered. 5526 const isTech = Tech && Tech.isTech(ComponentToRegister); 5527 const isComp = 5528 Component$1 === ComponentToRegister || 5529 Component$1.prototype.isPrototypeOf(ComponentToRegister.prototype); 5530 if (isTech || !isComp) { 5531 let reason; 5532 if (isTech) { 5533 reason = 'techs must be registered using Tech.registerTech()'; 5534 } else { 5535 reason = 'must be a Component subclass'; 5536 } 5537 throw new Error(`Illegal component, "${name}"; ${reason}.`); 5538 } 5539 name = toTitleCase$1(name); 5540 if (!Component$1.components_) { 5541 Component$1.components_ = {}; 5542 } 5543 const Player = Component$1.getComponent('Player'); 5544 if (name === 'Player' && Player && Player.players) { 5545 const players = Player.players; 5546 const playerNames = Object.keys(players); 5547 5548 // If we have players that were disposed, then their name will still be 5549 // in Players.players. So, we must loop through and verify that the value 5550 // for each item is not null. This allows registration of the Player component 5551 // after all players have been disposed or before any were created. 5552 if ( 5553 players && 5554 playerNames.length > 0 && 5555 playerNames.map((pname) => players[pname]).every(Boolean) 5556 ) { 5557 throw new Error('Can not register Player component after player has been created.'); 5558 } 5559 } 5560 Component$1.components_[name] = ComponentToRegister; 5561 Component$1.components_[toLowerCase(name)] = ComponentToRegister; 5562 return ComponentToRegister; 5563 } 5564 5565 /** 5566 * Get a `Component` based on the name it was registered with. 5567 * 5568 * @param {string} name 5569 * The Name of the component to get. 5570 * 5571 * @return {typeof Component} 5572 * The `Component` that got registered under the given name. 5573 */ 5574 static getComponent(name) { 5575 if (!name || !Component$1.components_) { 5576 return; 5577 } 5578 return Component$1.components_[name]; 5579 } 5580} 5581Component$1.registerComponent('Component', Component$1); 5582 5583/** 5584 * @file time.js 5585 * @module time 5586 */ 5587 5588/** 5589 * Returns the time for the specified index at the start or end 5590 * of a TimeRange object. 5591 * 5592 * @typedef {Function} TimeRangeIndex 5593 * 5594 * @param {number} [index=0] 5595 * The range number to return the time for. 5596 * 5597 * @return {number} 5598 * The time offset at the specified index. 5599 * 5600 * @deprecated The index argument must be provided. 5601 * In the future, leaving it out will throw an error. 5602 */ 5603 5604/** 5605 * An object that contains ranges of time, which mimics {@link TimeRanges}. 5606 * 5607 * @typedef {Object} TimeRange 5608 * 5609 * @property {number} length 5610 * The number of time ranges represented by this object. 5611 * 5612 * @property {module:time~TimeRangeIndex} start 5613 * Returns the time offset at which a specified time range begins. 5614 * 5615 * @property {module:time~TimeRangeIndex} end 5616 * Returns the time offset at which a specified time range ends. 5617 * 5618 * @see https://developer.mozilla.org/en-US/docs/Web/API/TimeRanges 5619 */ 5620 5621/** 5622 * Check if any of the time ranges are over the maximum index. 5623 * 5624 * @private 5625 * @param {string} fnName 5626 * The function name to use for logging 5627 * 5628 * @param {number} index 5629 * The index to check 5630 * 5631 * @param {number} maxIndex 5632 * The maximum possible index 5633 * 5634 * @throws {Error} if the timeRanges provided are over the maxIndex 5635 */ 5636function rangeCheck(fnName, index, maxIndex) { 5637 if (typeof index !== 'number' || index < 0 || index > maxIndex) { 5638 throw new Error( 5639 `Failed to execute '${fnName}' on 'TimeRanges': The index provided (${index}) is non-numeric or out of bounds (0-${maxIndex}).` 5640 ); 5641 } 5642} 5643 5644/** 5645 * Get the time for the specified index at the start or end 5646 * of a TimeRange object. 5647 * 5648 * @private 5649 * @param {string} fnName 5650 * The function name to use for logging 5651 * 5652 * @param {string} valueIndex 5653 * The property that should be used to get the time. should be 5654 * 'start' or 'end' 5655 * 5656 * @param {Array} ranges 5657 * An array of time ranges 5658 * 5659 * @param {Array} [rangeIndex=0] 5660 * The index to start the search at 5661 * 5662 * @return {number} 5663 * The time that offset at the specified index. 5664 * 5665 * @deprecated rangeIndex must be set to a value, in the future this will throw an error. 5666 * @throws {Error} if rangeIndex is more than the length of ranges 5667 */ 5668function getRange(fnName, valueIndex, ranges, rangeIndex) { 5669 rangeCheck(fnName, rangeIndex, ranges.length - 1); 5670 return ranges[rangeIndex][valueIndex]; 5671} 5672 5673/** 5674 * Create a time range object given ranges of time. 5675 * 5676 * @private 5677 * @param {Array} [ranges] 5678 * An array of time ranges. 5679 * 5680 * @return {TimeRange} 5681 */ 5682function createTimeRangesObj(ranges) { 5683 let timeRangesObj; 5684 if (ranges === undefined || ranges.length === 0) { 5685 timeRangesObj = { 5686 length: 0, 5687 start() { 5688 throw new Error('This TimeRanges object is empty'); 5689 }, 5690 end() { 5691 throw new Error('This TimeRanges object is empty'); 5692 }, 5693 }; 5694 } else { 5695 timeRangesObj = { 5696 length: ranges.length, 5697 start: getRange.bind(null, 'start', 0, ranges), 5698 end: getRange.bind(null, 'end', 1, ranges), 5699 }; 5700 } 5701 if (window__default['default'].Symbol && window__default['default'].Symbol.iterator) { 5702 timeRangesObj[window__default['default'].Symbol.iterator] = () => (ranges || []).values(); 5703 } 5704 return timeRangesObj; 5705} 5706 5707/** 5708 * Create a `TimeRange` object which mimics an 5709 * {@link https://developer.mozilla.org/en-US/docs/Web/API/TimeRanges|HTML5 TimeRanges instance}. 5710 * 5711 * @param {number|Array[]} start 5712 * The start of a single range (a number) or an array of ranges (an 5713 * array of arrays of two numbers each). 5714 * 5715 * @param {number} end 5716 * The end of a single range. Cannot be used with the array form of 5717 * the `start` argument. 5718 * 5719 * @return {TimeRange} 5720 */ 5721function createTimeRanges$1(start, end) { 5722 if (Array.isArray(start)) { 5723 return createTimeRangesObj(start); 5724 } else if (start === undefined || end === undefined) { 5725 return createTimeRangesObj(); 5726 } 5727 return createTimeRangesObj([[start, end]]); 5728} 5729 5730/** 5731 * Format seconds as a time string, H:MM:SS or M:SS. Supplying a guide (in 5732 * seconds) will force a number of leading zeros to cover the length of the 5733 * guide. 5734 * 5735 * @private 5736 * @param {number} seconds 5737 * Number of seconds to be turned into a string 5738 * 5739 * @param {number} guide 5740 * Number (in seconds) to model the string after 5741 * 5742 * @return {string} 5743 * Time formatted as H:MM:SS or M:SS 5744 */ 5745const defaultImplementation = function (seconds, guide) { 5746 seconds = seconds < 0 ? 0 : seconds; 5747 let s = Math.floor(seconds % 60); 5748 let m = Math.floor((seconds / 60) % 60); 5749 let h = Math.floor(seconds / 3600); 5750 const gm = Math.floor((guide / 60) % 60); 5751 const gh = Math.floor(guide / 3600); 5752 5753 // handle invalid times 5754 if (isNaN(seconds) || seconds === Infinity) { 5755 // '-' is false for all relational operators (e.g. <, >=) so this setting 5756 // will add the minimum number of fields specified by the guide 5757 h = m = s = '-'; 5758 } 5759 5760 // Check if we need to show hours 5761 h = h > 0 || gh > 0 ? h + ':' : ''; 5762
vendor: 9,700 bytes, lines 5763-6119
5763 // If hours are showing, we may need to add a leading zero. 5764 // Always show at least one digit of minutes. 5765 m = ((h || gm >= 10) && m < 10 ? '0' + m : m) + ':'; 5766 5767 // Check if leading zero is need for seconds 5768 s = s < 10 ? '0' + s : s; 5769 return h + m + s; 5770}; 5771 5772// Internal pointer to the current implementation. 5773let implementation = defaultImplementation; 5774 5775/** 5776 * Replaces the default formatTime implementation with a custom implementation. 5777 * 5778 * @param {Function} customImplementation 5779 * A function which will be used in place of the default formatTime 5780 * implementation. Will receive the current time in seconds and the 5781 * guide (in seconds) as arguments. 5782 */ 5783function setFormatTime(customImplementation) { 5784 implementation = customImplementation; 5785} 5786 5787/** 5788 * Resets formatTime to the default implementation. 5789 */ 5790function resetFormatTime() { 5791 implementation = defaultImplementation; 5792} 5793 5794/** 5795 * Delegates to either the default time formatting function or a custom 5796 * function supplied via `setFormatTime`. 5797 * 5798 * Formats seconds as a time string (H:MM:SS or M:SS). Supplying a 5799 * guide (in seconds) will force a number of leading zeros to cover the 5800 * length of the guide. 5801 * 5802 * @example formatTime(125, 600) === "02:05" 5803 * @param {number} seconds 5804 * Number of seconds to be turned into a string 5805 * 5806 * @param {number} guide 5807 * Number (in seconds) to model the string after 5808 * 5809 * @return {string} 5810 * Time formatted as H:MM:SS or M:SS 5811 */ 5812function formatTime(seconds, guide = seconds) { 5813 return implementation(seconds, guide); 5814} 5815 5816var Time = /*#__PURE__*/ Object.freeze({ 5817 __proto__: null, 5818 createTimeRanges: createTimeRanges$1, 5819 createTimeRange: createTimeRanges$1, 5820 setFormatTime: setFormatTime, 5821 resetFormatTime: resetFormatTime, 5822 formatTime: formatTime, 5823}); 5824 5825/** 5826 * @file buffer.js 5827 * @module buffer 5828 */ 5829 5830/** @import { TimeRange } from './time' */ 5831 5832/** 5833 * Compute the percentage of the media that has been buffered. 5834 * 5835 * @param {TimeRange} buffered 5836 * The current `TimeRanges` object representing buffered time ranges 5837 * 5838 * @param {number} duration 5839 * Total duration of the media 5840 * 5841 * @return {number} 5842 * Percent buffered of the total duration in decimal form. 5843 */ 5844function bufferedPercent(buffered, duration) { 5845 let bufferedDuration = 0; 5846 let start; 5847 let end; 5848 if (!duration) { 5849 return 0; 5850 } 5851 if (!buffered || !buffered.length) { 5852 buffered = createTimeRanges$1(0, 0); 5853 } 5854 for (let i = 0; i < buffered.length; i++) { 5855 start = buffered.start(i); 5856 end = buffered.end(i); 5857 5858 // buffered end can be bigger than duration by a very small fraction 5859 if (end > duration) { 5860 end = duration; 5861 } 5862 bufferedDuration += end - start; 5863 } 5864 return bufferedDuration / duration; 5865} 5866 5867/** 5868 * @file media-error.js 5869 */ 5870 5871/** 5872 * A Custom `MediaError` class which mimics the standard HTML5 `MediaError` class. 5873 * 5874 * @param {number|string|Object|MediaError} value 5875 * This can be of multiple types: 5876 * - number: should be a standard error code 5877 * - string: an error message (the code will be 0) 5878 * - Object: arbitrary properties 5879 * - `MediaError` (native): used to populate a video.js `MediaError` object 5880 * - `MediaError` (video.js): will return itself if it's already a 5881 * video.js `MediaError` object. 5882 * 5883 * @see [MediaError Spec]{@link https://dev.w3.org/html5/spec-author-view/video.html#mediaerror} 5884 * @see [Encrypted MediaError Spec]{@link https://www.w3.org/TR/2013/WD-encrypted-media-20130510/#error-codes} 5885 * 5886 * @class MediaError 5887 */ 5888function MediaError(value) { 5889 // Allow redundant calls to this constructor to avoid having `instanceof` 5890 // checks peppered around the code. 5891 if (value instanceof MediaError) { 5892 return value; 5893 } 5894 if (typeof value === 'number') { 5895 this.code = value; 5896 } else if (typeof value === 'string') { 5897 // default code is zero, so this is a custom error 5898 this.message = value; 5899 } else if (isObject(value)) { 5900 // We assign the `code` property manually because native `MediaError` objects 5901 // do not expose it as an own/enumerable property of the object. 5902 if (typeof value.code === 'number') { 5903 this.code = value.code; 5904 } 5905 Object.assign(this, value); 5906 } 5907 if (!this.message) { 5908 this.message = MediaError.defaultMessages[this.code] || ''; 5909 } 5910} 5911 5912/** 5913 * The error code that refers two one of the defined `MediaError` types 5914 * 5915 * @type {Number} 5916 */ 5917MediaError.prototype.code = 0; 5918 5919/** 5920 * An optional message that to show with the error. Message is not part of the HTML5 5921 * video spec but allows for more informative custom errors. 5922 * 5923 * @type {String} 5924 */ 5925MediaError.prototype.message = ''; 5926 5927/** 5928 * An optional status code that can be set by plugins to allow even more detail about 5929 * the error. For example a plugin might provide a specific HTTP status code and an 5930 * error message for that code. Then when the plugin gets that error this class will 5931 * know how to display an error message for it. This allows a custom message to show 5932 * up on the `Player` error overlay. 5933 * 5934 * @type {Array} 5935 */ 5936MediaError.prototype.status = null; 5937 5938/** 5939 * An object containing an error type, as well as other information regarding the error. 5940 * 5941 * @typedef {{errorType: string, [key: string]: any}} ErrorMetadata 5942 */ 5943 5944/** 5945 * An optional object to give more detail about the error. This can be used to give 5946 * a higher level of specificity to an error versus the more generic MediaError codes. 5947 * `metadata` expects an `errorType` string that should align with the values from videojs.Error. 5948 * 5949 * @type {ErrorMetadata} 5950 */ 5951MediaError.prototype.metadata = null; 5952 5953/** 5954 * Errors indexed by the W3C standard. The order **CANNOT CHANGE**! See the 5955 * specification listed under {@link MediaError} for more information. 5956 * 5957 * @enum {array} 5958 * @readonly 5959 * @property {string} 0 - MEDIA_ERR_CUSTOM 5960 * @property {string} 1 - MEDIA_ERR_ABORTED 5961 * @property {string} 2 - MEDIA_ERR_NETWORK 5962 * @property {string} 3 - MEDIA_ERR_DECODE 5963 * @property {string} 4 - MEDIA_ERR_SRC_NOT_SUPPORTED 5964 * @property {string} 5 - MEDIA_ERR_ENCRYPTED 5965 */ 5966MediaError.errorTypes = [ 5967 'MEDIA_ERR_CUSTOM', 5968 'MEDIA_ERR_ABORTED', 5969 'MEDIA_ERR_NETWORK', 5970 'MEDIA_ERR_DECODE', 5971 'MEDIA_ERR_SRC_NOT_SUPPORTED', 5972 'MEDIA_ERR_ENCRYPTED', 5973]; 5974 5975/** 5976 * The default `MediaError` messages based on the {@link MediaError.errorTypes}. 5977 * 5978 * @type {Array} 5979 * @constant 5980 */ 5981MediaError.defaultMessages = { 5982 1: 'You aborted the media playback', 5983 2: 'A network error caused the media download to fail part-way.', 5984 3: 'The media playback was aborted due to a corruption problem or because the media used features your browser did not support.', 5985 4: 'The media could not be loaded, either because the server or network failed or because the format is not supported.', 5986 5: 'The media is encrypted and we do not have the keys to decrypt it.', 5987}; 5988 5989/** 5990 * W3C error code for any custom error. 5991 * 5992 * @member MediaError#MEDIA_ERR_CUSTOM 5993 * @constant {number} 5994 * @default 0 5995 */ 5996MediaError.MEDIA_ERR_CUSTOM = 0; 5997 5998/** 5999 * W3C error code for any custom error. 6000 * 6001 * @member MediaError.MEDIA_ERR_CUSTOM 6002 * @constant {number} 6003 * @default 0 6004 */ 6005MediaError.prototype.MEDIA_ERR_CUSTOM = 0; 6006 6007/** 6008 * W3C error code for media error aborted. 6009 * 6010 * @member MediaError#MEDIA_ERR_ABORTED 6011 * @constant {number} 6012 * @default 1 6013 */ 6014MediaError.MEDIA_ERR_ABORTED = 1; 6015 6016/** 6017 * W3C error code for media error aborted. 6018 * 6019 * @member MediaError.MEDIA_ERR_ABORTED 6020 * @constant {number} 6021 * @default 1 6022 */ 6023MediaError.prototype.MEDIA_ERR_ABORTED = 1; 6024 6025/** 6026 * W3C error code for any network error. 6027 * 6028 * @member MediaError#MEDIA_ERR_NETWORK 6029 * @constant {number} 6030 * @default 2 6031 */ 6032MediaError.MEDIA_ERR_NETWORK = 2; 6033 6034/** 6035 * W3C error code for any network error. 6036 * 6037 * @member MediaError.MEDIA_ERR_NETWORK 6038 * @constant {number} 6039 * @default 2 6040 */ 6041MediaError.prototype.MEDIA_ERR_NETWORK = 2; 6042 6043/** 6044 * W3C error code for any decoding error. 6045 * 6046 * @member MediaError#MEDIA_ERR_DECODE 6047 * @constant {number} 6048 * @default 3 6049 */ 6050MediaError.MEDIA_ERR_DECODE = 3; 6051 6052/** 6053 * W3C error code for any decoding error. 6054 * 6055 * @member MediaError.MEDIA_ERR_DECODE 6056 * @constant {number} 6057 * @default 3 6058 */ 6059MediaError.prototype.MEDIA_ERR_DECODE = 3; 6060 6061/** 6062 * W3C error code for any time that a source is not supported. 6063 * 6064 * @member MediaError#MEDIA_ERR_SRC_NOT_SUPPORTED 6065 * @constant {number} 6066 * @default 4 6067 */ 6068MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED = 4; 6069 6070/** 6071 * W3C error code for any time that a source is not supported. 6072 * 6073 * @member MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED 6074 * @constant {number} 6075 * @default 4 6076 */ 6077MediaError.prototype.MEDIA_ERR_SRC_NOT_SUPPORTED = 4; 6078 6079/** 6080 * W3C error code for any time that a source is encrypted. 6081 * 6082 * @member MediaError#MEDIA_ERR_ENCRYPTED 6083 * @constant {number} 6084 * @default 5 6085 */ 6086MediaError.MEDIA_ERR_ENCRYPTED = 5; 6087 6088/** 6089 * W3C error code for any time that a source is encrypted. 6090 * 6091 * @member MediaError.MEDIA_ERR_ENCRYPTED 6092 * @constant {number} 6093 * @default 5 6094 */ 6095MediaError.prototype.MEDIA_ERR_ENCRYPTED = 5; 6096 6097/** 6098 * Returns whether an object is `Promise`-like (i.e. has a `then` method). 6099 * 6100 * @param {Object} value 6101 * An object that may or may not be `Promise`-like. 6102 * 6103 * @return {boolean} 6104 * Whether or not the object is `Promise`-like. 6105 */ 6106function isPromise(value) { 6107 return value !== undefined && value !== null && typeof value.then === 'function'; 6108} 6109 6110/** 6111 * Silence a Promise-like object. 6112 * 6113 * This is useful for avoiding non-harmful, but potentially confusing "uncaught 6114 * play promise" rejection error messages. 6115 * 6116 * @param {Object} value 6117 * An object that may or may not be `Promise`-like. 6118 */ 6119function silencePromise(value) {
6120 if (isPromise(value)) { 6121 value.then(null, (e) => {}); 6122 } 6123} 6124 6125/** 6126 * @file text-track-list-converter.js Utilities for capturing text track state and 6127 * re-creating tracks based on a capture. 6128 * 6129 * @module text-track-list-converter 6130 */ 6131 6132/** @import Tech from '../tech/tech' */ 6133 6134/** 6135 * Examine a single {@link TextTrack} and return a JSON-compatible javascript object that 6136 * represents the {@link TextTrack}'s state. 6137 * 6138 * @param {TextTrack} track 6139 * The text track to query. 6140 * 6141 * @return {Object} 6142 * A serializable javascript representation of the TextTrack. 6143 * @private 6144 */ 6145const trackToJson_ = function (track) { 6146 const ret = [ 6147 'kind', 6148 'label', 6149 'language', 6150 'id', 6151 'inBandMetadataTrackDispatchType', 6152 'mode', 6153 'src', 6154 ].reduce( 6155 (acc, prop, i) => { 6156 if (track[prop]) { 6157 acc[prop] = track[prop]; 6158 } 6159 return acc; 6160 }, 6161 { 6162 cues: 6163 track.cues && 6164 Array.prototype.map.call(track.cues, function (cue) { 6165 return { 6166 startTime: cue.startTime, 6167 endTime: cue.endTime, 6168 text: cue.text, 6169 id: cue.id, 6170 }; 6171 }), 6172 } 6173 ); 6174 return ret; 6175}; 6176 6177/** 6178 * Examine a {@link Tech} and return a JSON-compatible javascript array that represents the 6179 * state of all {@link TextTrack}s currently configured. The return array is compatible with 6180 * {@link text-track-list-converter:jsonToTextTracks}. 6181 * 6182 * @param {Tech} tech 6183 * The tech object to query 6184 * 6185 * @return {Array} 6186 * A serializable javascript representation of the {@link Tech}s 6187 * {@link TextTrackList}. 6188 */ 6189const textTracksToJson = function (tech) { 6190 const trackEls = tech.$$('track'); 6191 const trackObjs = Array.prototype.map.call(trackEls, (t) => t.track); 6192 const tracks = Array.prototype.map.call(trackEls, function (trackEl) { 6193 const json = trackToJson_(trackEl.track); 6194 if (trackEl.src) { 6195 json.src = trackEl.src; 6196 } 6197 return json; 6198 }); 6199 return tracks.concat( 6200 Array.prototype.filter 6201 .call(tech.textTracks(), function (track) { 6202 return trackObjs.indexOf(track) === -1; 6203 }) 6204 .map(trackToJson_) 6205 ); 6206}; 6207 6208/** 6209 * Create a set of remote {@link TextTrack}s on a {@link Tech} based on an array of javascript 6210 * object {@link TextTrack} representations. 6211 * 6212 * @param {Array} json 6213 * An array of `TextTrack` representation objects, like those that would be 6214 * produced by `textTracksToJson`. 6215 * 6216 * @param {Tech} tech 6217 * The `Tech` to create the `TextTrack`s on. 6218 */ 6219const jsonToTextTracks = function (json, tech) { 6220 json.forEach(function (track) { 6221 const addedTrack = tech.addRemoteTextTrack(track).track; 6222 if (!track.src && track.cues) { 6223 track.cues.forEach((cue) => addedTrack.addCue(cue)); 6224 } 6225 }); 6226 return tech.textTracks(); 6227}; 6228var textTrackConverter = { 6229 textTracksToJson, 6230 jsonToTextTracks, 6231 trackToJson_, 6232}; 6233 6234/** 6235 * @file modal-dialog.js 6236 */ 6237 6238/** @import Player from './player' */ 6239/** @import { ContentDescriptor } from './utils/dom' */ 6240 6241const MODAL_CLASS_NAME = 'vjs-modal-dialog'; 6242 6243/** 6244 * The `ModalDialog` displays over the video and its controls, which blocks 6245 * interaction with the player until it is closed. 6246 * 6247 * Modal dialogs include a "Close" button and will close when that button 6248 * is activated - or when ESC is pressed anywhere. 6249 * 6250 * @extends Component 6251 */ 6252class ModalDialog extends Component$1 { 6253 /** 6254 * Creates an instance of this class. 6255 * 6256 * @param {Player} player 6257 * The `Player` that this class should be attached to. 6258 * 6259 * @param {Object} [options] 6260 * The key/value store of player options. 6261 * 6262 * @param {ContentDescriptor} [options.content=undefined] 6263 * Provide customized content for this modal. 6264 * 6265 * @param {string} [options.description] 6266 * A text description for the modal, primarily for accessibility. 6267 * 6268 * @param {boolean} [options.fillAlways=false] 6269 * Normally, modals are automatically filled only the first time 6270 * they open. This tells the modal to refresh its content 6271 * every time it opens. 6272 * 6273 * @param {string} [options.label] 6274 * A text label for the modal, primarily for accessibility. 6275 * 6276 * @param {boolean}
6276 [options.pauseOnOpen=true] 6277 * If `true`, playback will will be paused if playing when 6278 * the modal opens, and resumed when it closes. 6279 * 6280 * @param {boolean} [options.temporary=true] 6281 * If `true`, the modal can only be opened once; it will be 6282 * disposed as soon as it's closed. 6283 * 6284 * @param {boolean} [options.uncloseable=false] 6285 * If `true`, the user will not be able to close the modal 6286 * through the UI in the normal ways. Programmatic closing is 6287 * still possible. 6288 */ 6289 constructor(player, options) { 6290 super(player, options); 6291 this.handleKeyDown_ = (e) => this.handleKeyDown(e); 6292 this.close_ = (e) => this.close(e); 6293 this.opened_ = this.hasBeenOpened_ = this.hasBeenFilled_ = false; 6294 this.closeable(!this.options_.uncloseable); 6295 this.content(this.options_.content); 6296 6297 // Make sure the contentEl is defined AFTER any children are initialized 6298 // because we only want the contents of the modal in the contentEl 6299 // (not the UI elements like the close button). 6300 this.contentEl_ = createEl( 6301 'div', 6302 { 6303 className: `${MODAL_CLASS_NAME}-content`, 6304 }, 6305 { 6306 role: 'document', 6307 } 6308 ); 6309 this.descEl_ = createEl('p', { 6310 className: `${MODAL_CLASS_NAME}-description vjs-control-text`, 6311 id: this.el().getAttribute('aria-describedby'), 6312 }); 6313 textContent(this.descEl_, this.description()); 6314 this.el_.appendChild(this.descEl_); 6315 this.el_.appendChild(this.contentEl_); 6316 } 6317 6318 /** 6319 * Create the `ModalDialog`'s DOM element 6320 * 6321 * @return {Element} 6322 * The DOM element that gets created. 6323 */ 6324 createEl() { 6325 return super.createEl( 6326 'div', 6327 { 6328 className: this.buildCSSClass(), 6329 tabIndex: -1, 6330 }, 6331 { 6332 'aria-describedby': `${this.id()}_description`, 6333 'aria-hidden': 'true', 6334 'aria-label': this.label(), 6335 role: 'dialog', 6336 'aria-live': 'polite', 6337 } 6338 ); 6339 } 6340 dispose() { 6341 this.contentEl_ = null; 6342 this.descEl_ = null; 6343 this.previouslyActiveEl_ = null; 6344 super.dispose(); 6345 } 6346 6347 /** 6348 * Builds the default DOM `className`. 6349 * 6350 * @return {string} 6351 * The DOM `className` for this object. 6352 */ 6353 buildCSSClass() { 6354 return `${MODAL_CLASS_NAME} vjs-hidden ${super.buildCSSClass()}`; 6355 } 6356 6357 /** 6358 * Returns the label string for this modal. Primarily used for accessibility. 6359 * 6360 * @return {string} 6361 * the localized or raw label of this modal. 6362 */ 6363 label() { 6364 return this.localize(this.options_.label || 'Modal Window'); 6365 } 6366 6367 /** 6368 * Returns the description string for this modal. Primarily used for 6369 * accessibility. 6370 * 6371 * @return {string} 6372 * The localized or raw description of this modal. 6373 */ 6374 description() { 6375 let desc = this.options_.description || this.localize('This is a modal window.'); 6376 6377 // Append a universal closeability message if the modal is closeable. 6378 if (this.closeable()) { 6379 desc += 6380 ' ' + 6381 this.localize( 6382 'This modal can be closed by pressing the Escape key or activating the close button.' 6383 ); 6384 } 6385 return desc; 6386 } 6387 6388 /** 6389 * Opens the modal. 6390 * 6391 * @fires ModalDialog#beforemodalopen 6392 * @fires ModalDialog#modalopen 6393 */ 6394 open() { 6395 if (this.opened_) { 6396 if (this.options_.fillAlways) { 6397 this.fill(); 6398 } 6399 return; 6400 } 6401 const player = this.player(); 6402 6403 /** 6404 * Fired just before a `ModalDialog` is opened. 6405 * 6406 * @event ModalDialog#beforemodalopen 6407 * @type {Event} 6408 */ 6409 this.trigger('beforemodalopen'); 6410 this.opened_ = true; 6411 6412 // Fill content if the modal has never opened before and 6413 // never been filled. 6414 if (this.options_.fillAlways || (!this.hasBeenOpened_ && !this.hasBeenFilled_)) { 6415 this.fill(); 6416 } 6417 6418 // If the player was playing, pause it and take note of its previously 6419 // playing state. 6420 this.wasPlaying_ = !player.paused(); 6421 if (this.options_.pauseOnOpen && this.wasPlaying_) { 6422 player.pause(); 6423 } 6424 this.on('keydown', this.handleKeyDown_); 6425 6426 // Hide controls and note if they were enabled.
6427 this.hadControls_ = player.controls(); 6428 player.controls(false); 6429 this.show(); 6430 this.conditionalFocus_(); 6431 this.el().setAttribute('aria-hidden', 'false'); 6432 6433 /** 6434 * Fired just after a `ModalDialog` is opened. 6435 * 6436 * @event ModalDialog#modalopen 6437 * @type {Event} 6438 */ 6439 this.trigger('modalopen'); 6440 this.hasBeenOpened_ = true; 6441 } 6442 6443 /** 6444 * If the `ModalDialog` is currently open or closed. 6445 * 6446 * @param {boolean} [value] 6447 * If given, it will open (`true`) or close (`false`) the modal. 6448 * 6449 * @return {boolean} 6450 * the current open state of the modaldialog 6451 */ 6452 opened(value) { 6453 if (typeof value === 'boolean') { 6454 this[value ? 'open' : 'close'](); 6455 } 6456 return this.opened_; 6457 } 6458 6459 /** 6460 * Closes the modal, does nothing if the `ModalDialog` is 6461 * not open. 6462 * 6463 * @fires ModalDialog#beforemodalclose 6464 * @fires ModalDialog#modalclose 6465 */ 6466 close() { 6467 if (!this.opened_) { 6468 return; 6469 } 6470 const player = this.player(); 6471 6472 /** 6473 * Fired just before a `ModalDialog` is closed. 6474 * 6475 * @event ModalDialog#beforemodalclose 6476 * @type {Event} 6477 */ 6478 this.trigger('beforemodalclose'); 6479 this.opened_ = false; 6480 if (this.wasPlaying_ && this.options_.pauseOnOpen) { 6481 player.play(); 6482 } 6483 this.off('keydown', this.handleKeyDown_); 6484 if (this.hadControls_) { 6485 player.controls(true); 6486 } 6487 this.hide(); 6488 this.el().setAttribute('aria-hidden', 'true'); 6489 6490 /** 6491 * Fired just after a `ModalDialog` is closed. 6492 * 6493 * @event ModalDialog#modalclose 6494 * @type {Event} 6495 * 6496 * @property {boolean} [bubbles=true] 6497 */ 6498 this.trigger({ 6499 type: 'modalclose', 6500 bubbles: true, 6501 }); 6502 this.conditionalBlur_(); 6503 if (this.options_.temporary) { 6504 this.dispose(); 6505 } 6506 } 6507 6508 /** 6509 * Check to see if the `ModalDialog` is closeable via the UI. 6510 * 6511 * @param {boolean} [value] 6512 * If given as a boolean, it will set the `closeable` option. 6513 * 6514 * @return {boolean} 6515 * Returns the final value of the closable option. 6516 */ 6517 closeable(value) { 6518 if (typeof value === 'boolean') { 6519 const closeable = (this.closeable_ = !!value); 6520 let close = this.getChild('closeButton'); 6521 6522 // If this is being made closeable and has no close button, add one. 6523 if (closeable && !close) { 6524 // The close button should be a child of the modal - not its 6525 // content element, so temporarily change the content element. 6526 const temp = this.contentEl_; 6527 this.contentEl_ = this.el_; 6528 close = this.addChild('closeButton', { 6529 controlText: 'Close Modal Dialog', 6530 }); 6531 this.contentEl_ = temp; 6532 this.on(close, 'close', this.close_); 6533 } 6534 6535 // If this is being made uncloseable and has a close button, remove it. 6536 if (!closeable && close) { 6537 this.off(close, 'close', this.close_); 6538 this.removeChild(close); 6539 close.dispose(); 6540 } 6541 } 6542 return this.closeable_; 6543 } 6544 6545 /**
vendor: 4,228 bytes, lines 6546-6703
6546 * Fill the modal's content element with the modal's "content" option. 6547 * The content element will be emptied before this change takes place. 6548 */ 6549 fill() { 6550 this.fillWith(this.content()); 6551 } 6552 6553 /** 6554 * Fill the modal's content element with arbitrary content. 6555 * The content element will be emptied before this change takes place. 6556 * 6557 * @fires ModalDialog#beforemodalfill 6558 * @fires ModalDialog#modalfill 6559 * 6560 * @param {ContentDescriptor} [content] 6561 * The same rules apply to this as apply to the `content` option. 6562 */ 6563 fillWith(content) { 6564 const contentEl = this.contentEl(); 6565 const parentEl = contentEl.parentNode; 6566 const nextSiblingEl = contentEl.nextSibling; 6567 6568 /** 6569 * Fired just before a `ModalDialog` is filled with content. 6570 * 6571 * @event ModalDialog#beforemodalfill 6572 * @type {Event} 6573 */ 6574 this.trigger('beforemodalfill'); 6575 this.hasBeenFilled_ = true; 6576 6577 // Detach the content element from the DOM before performing 6578 // manipulation to avoid modifying the live DOM multiple times. 6579 parentEl.removeChild(contentEl); 6580 this.empty(); 6581 insertContent(contentEl, content); 6582 /** 6583 * Fired just after a `ModalDialog` is filled with content. 6584 * 6585 * @event ModalDialog#modalfill 6586 * @type {Event} 6587 */ 6588 this.trigger('modalfill'); 6589 6590 // Re-inject the re-filled content element. 6591 if (nextSiblingEl) { 6592 parentEl.insertBefore(contentEl, nextSiblingEl); 6593 } else { 6594 parentEl.appendChild(contentEl); 6595 } 6596 6597 // make sure that the close button is last in the dialog DOM 6598 const closeButton = this.getChild('closeButton'); 6599 if (closeButton) { 6600 parentEl.appendChild(closeButton.el_); 6601 } 6602 6603 /** 6604 * Fired after `ModalDialog` is re-filled with content & close button is appended. 6605 * 6606 * @event ModalDialog#aftermodalfill 6607 * @type {Event} 6608 */ 6609 this.trigger('aftermodalfill'); 6610 } 6611 6612 /** 6613 * Empties the content element. This happens anytime the modal is filled. 6614 * 6615 * @fires ModalDialog#beforemodalempty 6616 * @fires ModalDialog#modalempty 6617 */ 6618 empty() { 6619 /** 6620 * Fired just before a `ModalDialog` is emptied. 6621 * 6622 * @event ModalDialog#beforemodalempty 6623 * @type {Event} 6624 */ 6625 this.trigger('beforemodalempty'); 6626 emptyEl(this.contentEl()); 6627 6628 /** 6629 * Fired just after a `ModalDialog` is emptied. 6630 * 6631 * @event ModalDialog#modalempty 6632 * @type {Event} 6633 */ 6634 this.trigger('modalempty'); 6635 } 6636 6637 /** 6638 * Gets or sets the modal content, which gets normalized before being 6639 * rendered into the DOM. 6640 * 6641 * This does not update the DOM or fill the modal, but it is called during 6642 * that process. 6643 * 6644 * @param {ContentDescriptor} [value] 6645 * If defined, sets the internal content value to be used on the 6646 * next call(s) to `fill`. This value is normalized before being 6647 * inserted. To "clear" the internal content value, pass `null`. 6648 * 6649 * @return {ContentDescriptor} 6650 * The current content of the modal dialog 6651 */ 6652 content(value) { 6653 if (typeof value !== 'undefined') { 6654 this.content_ = value; 6655 } 6656 return this.content_; 6657 } 6658 6659 /** 6660 * conditionally focus the modal dialog if focus was previously on the player. 6661 * 6662 * @private 6663 */ 6664 conditionalFocus_() { 6665 const activeEl = document__default['default'].activeElement; 6666 const playerEl = this.player_.el_; 6667 this.previouslyActiveEl_ = null; 6668 if (playerEl.contains(activeEl) || playerEl === activeEl) { 6669 this.previouslyActiveEl_ = activeEl; 6670 this.focus(); 6671 } 6672 } 6673 6674 /** 6675 * conditionally blur the element and refocus the last focused element 6676 * 6677 * @private 6678 */ 6679 conditionalBlur_() { 6680 if (this.previouslyActiveEl_) { 6681 this.previouslyActiveEl_.focus(); 6682 this.previouslyActiveEl_ = null; 6683 } 6684 } 6685 6686 /** 6687 * Keydown handler. Attached when modal is focused. 6688 * 6689 * @listens keydown 6690 */ 6691 handleKeyDown(event) { 6692 /** 6693 * Fired a custom keyDown event that bubbles. 6694 * 6695 * @event ModalDialog#modalKeydown 6696 * @type {Event} 6697 */ 6698 this.trigger({ 6699 type: 'modalKeydown', 6700 originalEvent: event, 6701 target: this, 6702 bubbles: true, 6703 });
6704 // Do not allow keydowns to reach out of the modal dialog. 6705 event.stopPropagation(); 6706 if (event.key === 'Escape' && this.closeable()) { 6707 event.preventDefault(); 6708 this.close(); 6709 return; 6710 } 6711 6712 // exit early if it isn't a tab key 6713 if (event.key !== 'Tab') { 6714 return; 6715 } 6716 const focusableEls = this.focusableEls_(); 6717 const activeEl = this.el_.querySelector(':focus'); 6718 let focusIndex; 6719 for (let i = 0; i < focusableEls.length; i++) { 6720 if (activeEl === focusableEls[i]) { 6721 focusIndex = i; 6722 break; 6723 } 6724 } 6725 if (document__default['default'].activeElement === this.el_) { 6726 focusIndex = 0; 6727 } 6728 if (event.shiftKey && focusIndex === 0) { 6729 focusableEls[focusableEls.length - 1].focus(); 6730 event.preventDefault(); 6731 } else if (!event.shiftKey && focusIndex === focusableEls.length - 1) { 6732 focusableEls[0].focus(); 6733 event.preventDefault(); 6734 } 6735 } 6736 6737 /** 6738 * get all focusable elements 6739 * 6740 * @private 6741 */ 6742 focusableEls_() { 6743 const allChildren = this.el_.querySelectorAll('*'); 6744 return Array.prototype.filter.call(allChildren, (child) => { 6745 return ( 6746 ((child instanceof window__default['default'].HTMLAnchorElement || 6747 child instanceof window__default['default'].HTMLAreaElement) && 6748 child.hasAttribute('href')) || 6749 ((child instanceof window__default['default'].HTMLInputElement || 6750 child instanceof window__default['default'].HTMLSelectElement || 6751 child instanceof window__default['default'].HTMLTextAreaElement || 6752 child instanceof window__default['default'].HTMLButtonElement) && 6753 !child.hasAttribute('disabled')) || 6754 child instanceof window__default['default'].HTMLIFrameElement || 6755 child instanceof window__default['default'].HTMLObjectElement || 6756 child instanceof window__default['default'].HTMLEmbedElement || 6757 (child.hasAttribute('tabindex') && child.getAttribute('tabindex') !== -1) || 6758 child.hasAttribute('contenteditable') 6759 ); 6760 }); 6761 } 6762} 6763 6764/** 6765 * Default options for `ModalDialog` default options. 6766 * 6767 * @type {Object} 6768 * @private 6769 */ 6770ModalDialog.prototype.options_ = { 6771 pauseOnOpen: true, 6772 temporary: true, 6773}; 6774Component$1.registerComponent('ModalDialog', ModalDialog); 6775 6776/** 6777 * @file track-list.js 6778 */ 6779 6780/** @import Track from './track' */ 6781 6782/** 6783 * Common functionaliy between {@link TextTrackList}, {@link AudioTrackList}, and 6784 * {@link VideoTrackList} 6785 * 6786 * @extends EventTarget 6787 */ 6788class TrackList extends EventTarget$2 { 6789 /** 6790 * Create an instance of this class 6791 * 6792 * @param { Track[] } tracks 6793 * A list of tracks to initialize the list with. 6794 * 6795 * @abstract 6796 */ 6797 constructor(tracks = []) { 6798 super(); 6799 this.tracks_ = []; 6800 6801 /** 6802 * @memberof TrackList 6803 * @member {number} length 6804 * The current number of `Track`s in the this Trackist. 6805 * @instance 6806 */ 6807 Object.defineProperty(this, 'length', { 6808 get() { 6809 return this.tracks_.length; 6810 }, 6811 }); 6812 for (let i = 0; i < tracks.length; i++) { 6813 this.addTrack(tracks[i]); 6814 } 6815 } 6816 6817 /** 6818 * Add a {@link Track} to the `TrackList` 6819 * 6820 * @param {Track} track 6821 * The audio, video, or text track to add to the list. 6822 * 6823 * @fires TrackList#addtrack 6824 */ 6825 addTrack(track) { 6826 const index = this.tracks_.length; 6827 if (!('' + index in this)) { 6828 Object.defineProperty(this, index, { 6829 get() { 6830 return this.tracks_[index]; 6831 }, 6832 }); 6833 } 6834 6835 // Do not add duplicate tracks 6836 if (this.tracks_.indexOf(track) === -1) { 6837 this.tracks_.push(track); 6838 /** 6839 * Triggered when a track is added to a track list. 6840 * 6841 * @event TrackList#addtrack 6842 * @type {Event} 6843 * @property {Track} track 6844 * A reference to track that was added. 6845 */ 6846 this.trigger({ 6847 track, 6848 type: 'addtrack', 6849 target: this, 6850 }); 6851 } 6852 6853 /** 6854 * Triggered when a track label is changed. 6855 * 6856 * @event TrackList#addtrack 6857 * @type {Event} 6858 * @property {Track} track 6859 * A reference to track that was added. 6860 */ 6861 track.labelchange_ = () => { 6862 this.trigger({ 6863 track, 6864 type: 'labelchange', 6865 target: this, 6866 }); 6867 }; 6868 if (isEvented(track)) { 6869 track.addEventListener('labelchange', track.labelchange_); 6870 } 6871 } 6872 6873 /** 6874 * Remove a {@link Track} from the `TrackList` 6875 * 6876 * @param {Track} rtrack 6877 * The audio, video, or text track to remove from the list. 6878 * 6879 * @fires TrackList#removetrack 6880 */ 6881 removeTrack(rtrack) { 6882 let track; 6883 for (let i = 0, l = this.length; i < l; i++) { 6884 if (this[i] === rtrack) { 6885 track = this[i]; 6886 if (track.off) { 6887 track.off(); 6888 } 6889 this.tracks_.splice(i, 1); 6890 break; 6891 } 6892 } 6893 if (!track) { 6894 return; 6895 } 6896 6897 /** 6898 * Triggered when a track is removed from track list. 6899 * 6900 * @event TrackList#removetrack 6901 * @type {Event} 6902 * @property {Track} track 6903 * A reference to track that was removed. 6904 */ 6905 this.trigger({ 6906 track, 6907 type: 'removetrack', 6908 target: this, 6909 }); 6910 } 6911 6912 /** 6913 * Get a Track from the TrackList by a tracks id 6914 * 6915 * @param {string} id - the id of the track to get 6916 * @method getTrackById 6917 * @return {Track} 6918 * @private 6919 */ 6920 getTrackById(id) { 6921 let result = null; 6922 for (let i = 0, l = this.length; i < l; i++) { 6923 const track = this[i]; 6924 if (track.id === id) { 6925 result = track; 6926 break; 6927 } 6928 } 6929 return result; 6930 } 6931} 6932 6933/** 6934 * Triggered when a different track is selected/enabled. 6935 * 6936 * @event TrackList#change 6937 * @type {Event} 6938 */ 6939 6940/** 6941 * Events that can be called with on + eventName. See {@link EventHandler}. 6942 * 6943 * @property {Object} TrackList#allowedEvents_ 6944 * @protected 6945 */ 6946TrackList.prototype.allowedEvents_ = { 6947 change: 'change', 6948 addtrack: 'addtrack', 6949 removetrack: 'removetrack', 6950 labelchange: 'labelchange', 6951}; 6952 6953// emulate attribute EventHandler support to allow for feature detection 6954for (const event in TrackList.prototype.allowedEvents_) { 6955 TrackList.prototype['on' + event] = null; 6956} 6957 6958/** 6959 * @file audio-track-list.js 6960 */ 6961 6962/** @import AudioTrack from './audio-track' */ 6963 6964/** 6965 * Anywhere we call this function we diverge from the spec 6966 * as we only support one enabled audiotrack at a time 6967 * 6968 * @param {AudioTrackList} list 6969 * list to work on 6970 * 6971 * @param {AudioTrack} track 6972 * The track to skip 6973 * 6974 * @private 6975 */ 6976const disableOthers$1 = function (list, track) { 6977 for (let i = 0; i < list.length; i++) { 6978 if (!Object.keys(list[i]).length || track.id === list[i].id) { 6979 continue; 6980 } 6981 // another audio track is enabled, disable it 6982 list[i].enabled = false;
vendor: 12,922 bytes, lines 6983-7508
6983 } 6984}; 6985 6986/** 6987 * The current list of {@link AudioTrack} for a media file. 6988 * 6989 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotracklist} 6990 * @extends TrackList 6991 */ 6992class AudioTrackList extends TrackList { 6993 /** 6994 * Create an instance of this class. 6995 * 6996 * @param {AudioTrack[]} [tracks=[]] 6997 * A list of `AudioTrack` to instantiate the list with. 6998 */ 6999 constructor(tracks = []) { 7000 // make sure only 1 track is enabled 7001 // sorted from last index to first index 7002 for (let i = tracks.length - 1; i >= 0; i--) { 7003 if (tracks[i].enabled) { 7004 disableOthers$1(tracks, tracks[i]); 7005 break; 7006 } 7007 } 7008 super(tracks); 7009 this.changing_ = false; 7010 } 7011 7012 /** 7013 * Add an {@link AudioTrack} to the `AudioTrackList`. 7014 * 7015 * @param {AudioTrack} track 7016 * The AudioTrack to add to the list 7017 * 7018 * @fires TrackList#addtrack 7019 */ 7020 addTrack(track) { 7021 if (track.enabled) { 7022 disableOthers$1(this, track); 7023 } 7024 super.addTrack(track); 7025 // native tracks don't have this 7026 if (!track.addEventListener) { 7027 return; 7028 } 7029 track.enabledChange_ = () => { 7030 // when we are disabling other tracks (since we don't support 7031 // more than one track at a time) we will set changing_ 7032 // to true so that we don't trigger additional change events 7033 if (this.changing_) { 7034 return; 7035 } 7036 this.changing_ = true; 7037 disableOthers$1(this, track); 7038 this.changing_ = false; 7039 this.trigger('change'); 7040 }; 7041 7042 /** 7043 * @listens AudioTrack#enabledchange 7044 * @fires TrackList#change 7045 */ 7046 track.addEventListener('enabledchange', track.enabledChange_); 7047 } 7048 removeTrack(rtrack) { 7049 super.removeTrack(rtrack); 7050 if (rtrack.removeEventListener && rtrack.enabledChange_) { 7051 rtrack.removeEventListener('enabledchange', rtrack.enabledChange_); 7052 rtrack.enabledChange_ = null; 7053 } 7054 } 7055} 7056 7057/** 7058 * @file video-track-list.js 7059 */ 7060 7061/** @import VideoTrack from './video-track' */ 7062 7063/** 7064 * Un-select all other {@link VideoTrack}s that are selected. 7065 * 7066 * @param {VideoTrackList} list 7067 * list to work on 7068 * 7069 * @param {VideoTrack} track 7070 * The track to skip 7071 * 7072 * @private 7073 */ 7074const disableOthers = function (list, track) { 7075 for (let i = 0; i < list.length; i++) { 7076 if (!Object.keys(list[i]).length || track.id === list[i].id) { 7077 continue; 7078 } 7079 // another video track is enabled, disable it 7080 list[i].selected = false; 7081 } 7082}; 7083 7084/** 7085 * The current list of {@link VideoTrack} for a video. 7086 * 7087 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#videotracklist} 7088 * @extends TrackList 7089 */ 7090class VideoTrackList extends TrackList { 7091 /** 7092 * Create an instance of this class. 7093 * 7094 * @param {VideoTrack[]} [tracks=[]] 7095 * A list of `VideoTrack` to instantiate the list with. 7096 */ 7097 constructor(tracks = []) { 7098 // make sure only 1 track is enabled 7099 // sorted from last index to first index 7100 for (let i = tracks.length - 1; i >= 0; i--) { 7101 if (tracks[i].selected) { 7102 disableOthers(tracks, tracks[i]); 7103 break; 7104 } 7105 } 7106 super(tracks); 7107 this.changing_ = false; 7108 7109 /** 7110 * @member {number} VideoTrackList#selectedIndex 7111 * The current index of the selected {@link VideoTrack`}. 7112 */ 7113 Object.defineProperty(this, 'selectedIndex', { 7114 get() { 7115 for (let i = 0; i < this.length; i++) { 7116 if (this[i].selected) { 7117 return i; 7118 } 7119 } 7120 return -1; 7121 }, 7122 set() {}, 7123 }); 7124 } 7125 7126 /** 7127 * Add a {@link VideoTrack} to the `VideoTrackList`. 7128 * 7129 * @param {VideoTrack} track 7130 * The VideoTrack to add to the list 7131 * 7132 * @fires TrackList#addtrack 7133 */ 7134 addTrack(track) { 7135 if (track.selected) { 7136 disableOthers(this, track); 7137 } 7138 super.addTrack(track); 7139 // native tracks don't have this 7140 if (!track.addEventListener) { 7141 return; 7142 } 7143 track.selectedChange_ = () => { 7144 if (this.changing_) { 7145 return; 7146 } 7147 this.changing_ = true; 7148 disableOthers(this, track); 7149 this.changing_ = false; 7150 this.trigger('change'); 7151 }; 7152 7153 /** 7154 * @listens VideoTrack#selectedchange 7155 * @fires TrackList#change 7156 */ 7157 track.addEventListener('selectedchange', track.selectedChange_); 7158 } 7159 removeTrack(rtrack) { 7160 super.removeTrack(rtrack); 7161 if (rtrack.removeEventListener && rtrack.selectedChange_) { 7162 rtrack.removeEventListener('selectedchange', rtrack.selectedChange_); 7163 rtrack.selectedChange_ = null; 7164 } 7165 } 7166} 7167 7168/** 7169 * @file text-track-list.js 7170 */ 7171 7172/** @import TextTrack from './text-track' */ 7173 7174/** 7175 * The current list of {@link TextTrack} for a media file. 7176 * 7177 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttracklist} 7178 * @extends TrackList 7179 */ 7180class TextTrackList extends TrackList { 7181 /** 7182 * Add a {@link TextTrack} to the `TextTrackList` 7183 * 7184 * @param {TextTrack} track 7185 * The text track to add to the list. 7186 * 7187 * @fires TrackList#addtrack 7188 */ 7189 addTrack(track) { 7190 super.addTrack(track); 7191 if (!this.queueChange_) { 7192 this.queueChange_ = () => this.queueTrigger('change'); 7193 } 7194 if (!this.triggerSelectedlanguagechange) { 7195 this.triggerSelectedlanguagechange_ = () => this.trigger('selectedlanguagechange'); 7196 } 7197 7198 /** 7199 * @listens TextTrack#modechange 7200 * @fires TrackList#change 7201 */ 7202 track.addEventListener('modechange', this.queueChange_); 7203 const nonLanguageTextTrackKind = ['metadata', 'chapters']; 7204 if (nonLanguageTextTrackKind.indexOf(track.kind) === -1) { 7205 track.addEventListener('modechange', this.triggerSelectedlanguagechange_); 7206 } 7207 } 7208 removeTrack(rtrack) { 7209 super.removeTrack(rtrack); 7210 7211 // manually remove the event handlers we added 7212 if (rtrack.removeEventListener) { 7213 if (this.queueChange_) { 7214 rtrack.removeEventListener('modechange', this.queueChange_); 7215 } 7216 if (this.selectedlanguagechange_) { 7217 rtrack.removeEventListener('modechange', this.triggerSelectedlanguagechange_); 7218 } 7219 } 7220 } 7221} 7222 7223/** 7224 * @file html-track-element-list.js 7225 */ 7226 7227/** 7228 * The current list of {@link HtmlTrackElement}s. 7229 */ 7230class HtmlTrackElementList { 7231 /** 7232 * Create an instance of this class. 7233 * 7234 * @param {HtmlTrackElement[]} [tracks=[]] 7235 * A list of `HtmlTrackElement` to instantiate the list with. 7236 */ 7237 constructor(trackElements = []) { 7238 this.trackElements_ = []; 7239 7240 /** 7241 * @memberof HtmlTrackElementList 7242 * @member {number} length 7243 * The current number of `Track`s in the this Trackist. 7244 * @instance 7245 */ 7246 Object.defineProperty(this, 'length', { 7247 get() { 7248 return this.trackElements_.length; 7249 }, 7250 }); 7251 for (let i = 0, length = trackElements.length; i < length; i++) { 7252 this.addTrackElement_(trackElements[i]); 7253 } 7254 } 7255 7256 /** 7257 * Add an {@link HtmlTrackElement} to the `HtmlTrackElementList` 7258 * 7259 * @param {HtmlTrackElement} trackElement 7260 * The track element to add to the list. 7261 * 7262 * @private 7263 */ 7264 addTrackElement_(trackElement) { 7265 const index = this.trackElements_.length; 7266 if (!('' + index in this)) { 7267 Object.defineProperty(this, index, { 7268 get() { 7269 return this.trackElements_[index]; 7270 }, 7271 }); 7272 } 7273 7274 // Do not add duplicate elements 7275 if (this.trackElements_.indexOf(trackElement) === -1) { 7276 this.trackElements_.push(trackElement); 7277 } 7278 } 7279 7280 /** 7281 * Get an {@link HtmlTrackElement} from the `HtmlTrackElementList` given an 7282 * {@link TextTrack}. 7283 * 7284 * @param {TextTrack} track 7285 * The track associated with a track element. 7286 * 7287 * @return {HtmlTrackElement|undefined} 7288 * The track element that was found or undefined. 7289 * 7290 * @private 7291 */ 7292 getTrackElementByTrack_(track) { 7293 let trackElement_; 7294 for (let i = 0, length = this.trackElements_.length; i < length; i++) { 7295 if (track === this.trackElements_[i].track) { 7296 trackElement_ = this.trackElements_[i]; 7297 break; 7298 } 7299 } 7300 return trackElement_; 7301 } 7302 7303 /** 7304 * Remove a {@link HtmlTrackElement} from the `HtmlTrackElementList` 7305 * 7306 * @param {HtmlTrackElement} trackElement 7307 * The track element to remove from the list. 7308 * 7309 * @private 7310 */ 7311 removeTrackElement_(trackElement) { 7312 for (let i = 0, length = this.trackElements_.length; i < length; i++) { 7313 if (trackElement === this.trackElements_[i]) { 7314 if ( 7315 this.trackElements_[i].track && 7316 typeof this.trackElements_[i].track.off === 'function' 7317 ) { 7318 this.trackElements_[i].track.off(); 7319 } 7320 if (typeof this.trackElements_[i].off === 'function') { 7321 this.trackElements_[i].off(); 7322 } 7323 this.trackElements_.splice(i, 1); 7324 break; 7325 } 7326 } 7327 } 7328} 7329 7330/** 7331 * @file text-track-cue-list.js 7332 */ 7333 7334/** 7335 * @typedef {Object} TextTrackCueList~TextTrackCue 7336 * 7337 * @property {string} id 7338 * The unique id for this text track cue 7339 * 7340 * @property {number} startTime 7341 * The start time for this text track cue 7342 * 7343 * @property {number} endTime 7344 * The end time for this text track cue 7345 * 7346 * @property {boolean} pauseOnExit 7347 * Pause when the end time is reached if true. 7348 * 7349 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackcue} 7350 */ 7351 7352/** 7353 * A List of TextTrackCues. 7354 * 7355 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackcuelist} 7356 */ 7357class TextTrackCueList { 7358 /** 7359 * Create an instance of this class.. 7360 * 7361 * @param {Array} cues 7362 * A list of cues to be initialized with 7363 */ 7364 constructor(cues) { 7365 TextTrackCueList.prototype.setCues_.call(this, cues); 7366 7367 /** 7368 * @memberof TextTrackCueList 7369 * @member {number} length 7370 * The current number of `TextTrackCue`s in the TextTrackCueList. 7371 * @instance 7372 */ 7373 Object.defineProperty(this, 'length', { 7374 get() { 7375 return this.length_; 7376 }, 7377 }); 7378 } 7379 7380 /** 7381 * A setter for cues in this list. Creates getters 7382 * an an index for the cues. 7383 * 7384 * @param {Array} cues 7385 * An array of cues to set 7386 * 7387 * @private 7388 */ 7389 setCues_(cues) { 7390 const oldLength = this.length || 0; 7391 let i = 0; 7392 const l = cues.length; 7393 this.cues_ = cues; 7394 this.length_ = cues.length; 7395 const defineProp = function (index) { 7396 if (!('' + index in this)) { 7397 Object.defineProperty(this, '' + index, { 7398 get() { 7399 return this.cues_[index]; 7400 }, 7401 }); 7402 } 7403 }; 7404 if (oldLength < l) { 7405 i = oldLength; 7406 for (; i < l; i++) { 7407 defineProp.call(this, i); 7408 } 7409 } 7410 } 7411 7412 /** 7413 * Get a `TextTrackCue` that is currently in the `TextTrackCueList` by id. 7414 * 7415 * @param {string} id 7416 * The id of the cue that should be searched for. 7417 * 7418 * @return {TextTrackCueList~TextTrackCue|null} 7419 * A single cue or null if none was found. 7420 */ 7421 getCueById(id) { 7422 let result = null; 7423 for (let i = 0, l = this.length; i < l; i++) { 7424 const cue = this[i]; 7425 if (cue.id === id) { 7426 result = cue; 7427 break; 7428 } 7429 } 7430 return result; 7431 } 7432} 7433 7434/** 7435 * @file track-kinds.js 7436 */ 7437 7438/** 7439 * All possible `VideoTrackKind`s 7440 * 7441 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-videotrack-kind 7442 * @typedef VideoTrack~Kind 7443 * @enum 7444 */ 7445const VideoTrackKind = { 7446 alternative: 'alternative', 7447 captions: 'captions', 7448 main: 'main', 7449 sign: 'sign', 7450 subtitles: 'subtitles', 7451 commentary: 'commentary', 7452}; 7453 7454/** 7455 * All possible `AudioTrackKind`s 7456 * 7457 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-audiotrack-kind 7458 * @typedef AudioTrack~Kind 7459 * @enum 7460 */ 7461const AudioTrackKind = { 7462 alternative: 'alternative', 7463 descriptions: 'descriptions', 7464 main: 'main', 7465 'main-desc': 'main-desc', 7466 translation: 'translation', 7467 commentary: 'commentary', 7468}; 7469 7470/** 7471 * All possible `TextTrackKind`s 7472 * 7473 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-texttrack-kind 7474 * @typedef TextTrack~Kind 7475 * @enum 7476 */ 7477const TextTrackKind = { 7478 subtitles: 'subtitles', 7479 captions: 'captions', 7480 descriptions: 'descriptions', 7481 chapters: 'chapters', 7482 metadata: 'metadata', 7483}; 7484 7485/** 7486 * All possible `TextTrackMode`s 7487 * 7488 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackmode 7489 * @typedef TextTrack~Mode 7490 * @enum 7491 */ 7492const TextTrackMode = { 7493 disabled: 'disabled', 7494 hidden: 'hidden', 7495 showing: 'showing', 7496}; 7497 7498/** 7499 * @file track.js 7500 */ 7501 7502/** 7503 * A Track class that contains all of the common functionality for {@link AudioTrack}, 7504 * {@link VideoTrack}, and {@link TextTrack}. 7505 * 7506 * > Note: This class should not be used directly 7507 * 7508 * @see {@link https://html.spec.whatwg.org/multipage/embedded-content.html}
7509 * @extends EventTarget 7510 * @abstract 7511 */ 7512class Track extends EventTarget$2 { 7513 /** 7514 * Create an instance of this class. 7515 * 7516 * @param {Object} [options={}] 7517 * Object of option names and values 7518 * 7519 * @param {string} [options.kind=''] 7520 * A valid kind for the track type you are creating. 7521 * 7522 * @param {string} [options.id='vjs_track_' + Guid.newGUID()] 7523 * A unique id for this AudioTrack. 7524 * 7525 * @param {string} [options.label=''] 7526 * The menu label for this track. 7527 * 7528 * @param {string} [options.language=''] 7529 * A valid two character language code. 7530 * 7531 * @abstract 7532 */ 7533 constructor(options = {}) { 7534 super(); 7535 const trackProps = { 7536 id: options.id || 'vjs_track_' + newGUID(), 7537 kind: options.kind || '', 7538 language: options.language || '', 7539 }; 7540 let label = options.label || ''; 7541 7542 /** 7543 * @memberof Track 7544 * @member {string} id 7545 * The id of this track. Cannot be changed after creation. 7546 * @instance 7547 * 7548 * @readonly 7549 */ 7550 7551 /** 7552 * @memberof Track 7553 * @member {string} kind 7554 * The kind of track that this is. Cannot be changed after creation. 7555 * @instance 7556 * 7557 * @readonly 7558 */ 7559 7560 /** 7561 * @memberof Track 7562 * @member {string} language 7563 * The two letter language code for this track. Cannot be changed after 7564 * creation. 7565 * @instance 7566 * 7567 * @readonly 7568 */ 7569 7570 for (const key in trackProps) { 7571 Object.defineProperty(this, key, { 7572 get() { 7573 return trackProps[key]; 7574 }, 7575 set() {}, 7576 }); 7577 } 7578 7579 /** 7580 * @memberof Track 7581 * @member {string} label 7582 * The label of this track. Cannot be changed after creation. 7583 * @instance 7584 * 7585 * @fires Track#labelchange 7586 */ 7587 Object.defineProperty(this, 'label', { 7588 get() { 7589 return label; 7590 }, 7591 set(newLabel) { 7592 if (newLabel !== label) { 7593 label = newLabel; 7594 7595 /** 7596 * An event that fires when label changes on this track. 7597 * 7598 * > Note: This is not part of the spec! 7599 * 7600 * @event Track#labelchange 7601 * @type {Event} 7602 */ 7603 this.trigger('labelchange'); 7604 } 7605 }, 7606 }); 7607 } 7608} 7609 7610/** 7611 * @file url.js 7612 * @module url 7613 */ 7614 7615/** 7616 * Resolve and parse the elements of a URL. 7617 * 7618 * @function 7619 * @param {string} url 7620 * The url to parse 7621 * 7622 * @return {URL} 7623 * An object of url details 7624 */ 7625const parseUrl = function (url) { 7626 return new URL(url, document__default['default'].baseURI); 7627}; 7628 7629/** 7630 * Get absolute version of relative URL. 7631 * 7632 * @function 7633 * @param {string} url 7634 * URL to make absolute 7635 * 7636 * @return {string} 7637 * Absolute URL 7638 */ 7639const getAbsoluteURL = function (url) { 7640 return new URL(url, document__default['default'].baseURI).href; 7641}; 7642 7643/** 7644 * Returns the extension of the passed file name. It will return an empty string 7645 * if passed an invalid path. 7646 * 7647 * @function 7648 * @param {string} path 7649 * The fileName path like '/path/to/file.mp4' 7650 * 7651 * @return {string} 7652 * The extension in lower case or an empty string if no 7653 * extension could be found. 7654 */ 7655const getFileExtension = function (path) { 7656 if (typeof path === 'string') { 7657 const splitPathRe = /^(\/?)([\s\S]*?)((?:\.{1,2}|[^\/]+?)(\.([^\.\/\?]+)))(?:[\/]*|[\?].*)$/; 7658 const pathParts = splitPathRe.exec(path); 7659 if (pathParts) { 7660 return pathParts.pop().toLowerCase(); 7661 } 7662 } 7663 return ''; 7664}; 7665 7666/** 7667 * Returns whether the url passed is a cross domain request or not. 7668 * 7669 * @function 7670 * @param {string} url 7671 * The url to check. 7672 * 7673 * @param {URL} [winLoc] 7674 * the domain to check the url against, defaults to window.location 7675 * 7676 * @return {boolean} 7677 * Whether it is a cross domain request or not. 7678 */ 7679const isCrossOrigin = function (url, winLoc = window__default['default'].location) { 7680 return parseUrl(url).origin !== winLoc.origin; 7681}; 7682 7683var Url = /*#__PURE__*/ Object.freeze({ 7684 __proto__: null, 7685 parseUrl: parseUrl, 7686 getAbsoluteURL: getAbsoluteURL, 7687 getFileExtension: getFileExtension, 7688 isCrossOrigin: isCrossOrigin, 7689}); 7690 7691/** 7692 * @file text-track.js 7693 */ 7694 7695/** @import Tech from '../tech/tech' */ 7696 7697/** 7698 * Takes a webvtt file contents and parses it into cues 7699 * 7700 * @param {string} srcContent 7701 * webVTT file contents 7702 * 7703 * @param {TextTrack} track 7704 * TextTrack to add cues to. Cues come from the srcContent. 7705 * 7706 * @private 7707 */ 7708const parseCues = function (srcContent, track) { 7709 const parser = new window__default['default'].WebVTT.Parser( 7710 window__default['default'], 7711 window__default['default'].vttjs, 7712 window__default['default'].WebVTT.StringDecoder() 7713 ); 7714 const errors = []; 7715 parser.oncue = function (cue) { 7716 track.addCue(cue); 7717 }; 7718 parser.onparsingerror = function (error) { 7719 errors.push(error); 7720 }; 7721 parser.onflush = function () { 7722 track.trigger({ 7723 type: 'loadeddata', 7724 target: track, 7725 }); 7726 }; 7727 parser.parse(srcContent); 7728 if (errors.length > 0) { 7729 if (window__default['default'].console && window__default['default'].console.groupCollapsed) { 7730 window__default['default'].console.groupCollapsed( 7731 `Text Track parsing errors for ${track.src}` 7732 ); 7733 }
vendor: 4,270 bytes, lines 7734-7869
7734 errors.forEach((error) => log$1.error(error)); 7735 if (window__default['default'].console && window__default['default'].console.groupEnd) { 7736 window__default['default'].console.groupEnd(); 7737 } 7738 } 7739 parser.flush(); 7740}; 7741 7742/** 7743 * Load a `TextTrack` from a specified url. 7744 * 7745 * @param {string} src 7746 * Url to load track from. 7747 * 7748 * @param {TextTrack} track 7749 * Track to add cues to. Comes from the content at the end of `url`. 7750 * 7751 * @private 7752 */ 7753const loadTrack = function (src, track) { 7754 const opts = { 7755 uri: src, 7756 }; 7757 const crossOrigin = isCrossOrigin(src); 7758 if (crossOrigin) { 7759 opts.cors = crossOrigin; 7760 } 7761 const withCredentials = track.tech_.crossOrigin() === 'use-credentials'; 7762 if (withCredentials) { 7763 opts.withCredentials = withCredentials; 7764 } 7765 XHR__default['default']( 7766 opts, 7767 bind_(this, function (err, response, responseBody) { 7768 if (err) { 7769 return log$1.error(err, response); 7770 } 7771 track.loaded_ = true; 7772 7773 // Make sure that vttjs has loaded, otherwise, wait till it finished loading 7774 // NOTE: this is only used for the alt/video.novtt.js build 7775 if (typeof window__default['default'].WebVTT !== 'function') { 7776 if (track.tech_) { 7777 // to prevent use before define eslint error, we define loadHandler 7778 // as a let here 7779 track.tech_.any(['vttjsloaded', 'vttjserror'], (event) => { 7780 if (event.type === 'vttjserror') { 7781 log$1.error(`vttjs failed to load, stopping trying to process ${track.src}`); 7782 return; 7783 } 7784 return parseCues(responseBody, track); 7785 }); 7786 } 7787 } else { 7788 parseCues(responseBody, track); 7789 } 7790 }) 7791 ); 7792}; 7793 7794/** 7795 * A representation of a single `TextTrack`. 7796 * 7797 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrack} 7798 * @extends Track 7799 */ 7800class TextTrack extends Track { 7801 /** 7802 * Create an instance of this class. 7803 * 7804 * @param {Object} options={} 7805 * Object of option names and values 7806 * 7807 * @param {Tech} options.tech 7808 * A reference to the tech that owns this TextTrack. 7809 * 7810 * @param {TextTrack~Kind} [options.kind='subtitles'] 7811 * A valid text track kind. 7812 * 7813 * @param {TextTrack~Mode} [options.mode='disabled'] 7814 * A valid text track mode. 7815 * 7816 * @param {string} [options.id='vjs_track_' + Guid.newGUID()] 7817 * A unique id for this TextTrack. 7818 * 7819 * @param {string} [options.label=''] 7820 * The menu label for this track. 7821 * 7822 * @param {string} [options.language=''] 7823 * A valid two character language code. 7824 * 7825 * @param {string} [options.srclang=''] 7826 * A valid two character language code. An alternative, but deprioritized 7827 * version of `options.language` 7828 * 7829 * @param {string} [options.src] 7830 * A url to TextTrack cues. 7831 * 7832 * @param {boolean} [options.default] 7833 * If this track should default to on or off. 7834 */ 7835 constructor(options = {}) { 7836 if (!options.tech) { 7837 throw new Error('A tech was not provided.'); 7838 } 7839 const settings = merge$1(options, { 7840 kind: TextTrackKind[options.kind] || 'subtitles', 7841 language: options.language || options.srclang || '', 7842 }); 7843 let mode = TextTrackMode[settings.mode] || 'disabled'; 7844 const default_ = settings.default; 7845 if (settings.kind === 'metadata' || settings.kind === 'chapters') { 7846 mode = 'hidden'; 7847 } 7848 super(settings); 7849 this.tech_ = settings.tech; 7850 this.cues_ = []; 7851 this.activeCues_ = []; 7852 this.preload_ = this.tech_.preloadTextTracks !== false; 7853 const cues = new TextTrackCueList(this.cues_); 7854 const activeCues = new TextTrackCueList(this.activeCues_); 7855 let changed = false; 7856 this.timeupdateHandler = bind_(this, function (event = {}) { 7857 if (this.tech_.isDisposed()) { 7858 return; 7859 } 7860 if (!this.tech_.isReady_) { 7861 if (event.type !== 'timeupdate') { 7862 this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler); 7863 } 7864 return; 7865 } 7866 7867 // Accessing this.activeCues for the side-effects of updating itself 7868 // due to its nature as a getter function. Do not remove or cues will 7869 // stop updating!
vendor: 4,823 bytes, lines 7870-8035
7870 // Use the setter to prevent deletion from uglify (pure_getters rule) 7871 this.activeCues = this.activeCues; 7872 if (changed) { 7873 this.trigger('cuechange'); 7874 changed = false; 7875 } 7876 if (event.type !== 'timeupdate') { 7877 this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler); 7878 } 7879 }); 7880 const disposeHandler = () => { 7881 this.stopTracking(); 7882 }; 7883 this.tech_.one('dispose', disposeHandler); 7884 if (mode !== 'disabled') { 7885 this.startTracking(); 7886 } 7887 Object.defineProperties(this, { 7888 /** 7889 * @memberof TextTrack 7890 * @member {boolean} default 7891 * If this track was set to be on or off by default. Cannot be changed after 7892 * creation. 7893 * @instance 7894 * 7895 * @readonly 7896 */ 7897 default: { 7898 get() { 7899 return default_; 7900 }, 7901 set() {}, 7902 }, 7903 /** 7904 * @memberof TextTrack 7905 * @member {string} mode 7906 * Set the mode of this TextTrack to a valid {@link TextTrack~Mode}. Will 7907 * not be set if setting to an invalid mode. 7908 * @instance 7909 * 7910 * @fires TextTrack#modechange 7911 */ 7912 mode: { 7913 get() { 7914 return mode; 7915 }, 7916 set(newMode) { 7917 if (!TextTrackMode[newMode]) { 7918 return; 7919 } 7920 if (mode === newMode) { 7921 return; 7922 } 7923 mode = newMode; 7924 if (!this.preload_ && mode !== 'disabled' && this.cues.length === 0) { 7925 // On-demand load. 7926 loadTrack(this.src, this); 7927 } 7928 this.stopTracking(); 7929 if (mode !== 'disabled') { 7930 this.startTracking(); 7931 } 7932 /** 7933 * An event that fires when mode changes on this track. This allows 7934 * the TextTrackList that holds this track to act accordingly. 7935 * 7936 * > Note: This is not part of the spec! 7937 * 7938 * @event TextTrack#modechange 7939 * @type {Event} 7940 */ 7941 this.trigger('modechange'); 7942 }, 7943 }, 7944 /** 7945 * @memberof TextTrack 7946 * @member {TextTrackCueList} cues 7947 * The text track cue list for this TextTrack. 7948 * @instance 7949 */ 7950 cues: { 7951 get() { 7952 if (!this.loaded_) { 7953 return null; 7954 } 7955 return cues; 7956 }, 7957 set() {}, 7958 }, 7959 /** 7960 * @memberof TextTrack 7961 * @member {TextTrackCueList} activeCues 7962 * The list text track cues that are currently active for this TextTrack. 7963 * @instance 7964 */ 7965 activeCues: { 7966 get() { 7967 if (!this.loaded_) { 7968 return null; 7969 } 7970 7971 // nothing to do 7972 if (this.cues.length === 0) { 7973 return activeCues; 7974 } 7975 const ct = this.tech_.currentTime(); 7976 const active = []; 7977 for (let i = 0, l = this.cues.length; i < l; i++) { 7978 const cue = this.cues[i]; 7979 if (cue.startTime <= ct && cue.endTime >= ct) { 7980 active.push(cue); 7981 } 7982 } 7983 changed = false; 7984 if (active.length !== this.activeCues_.length) { 7985 changed = true; 7986 } else { 7987 for (let i = 0; i < active.length; i++) { 7988 if (this.activeCues_.indexOf(active[i]) === -1) { 7989 changed = true; 7990 } 7991 } 7992 } 7993 this.activeCues_ = active; 7994 activeCues.setCues_(this.activeCues_); 7995 return activeCues; 7996 }, 7997 // /!\ Keep this setter empty (see the timeupdate handler above) 7998 set() {}, 7999 }, 8000 }); 8001 if (settings.src) { 8002 this.src = settings.src; 8003 if (!this.preload_) { 8004 // Tracks will load on-demand. 8005 // Act like we're loaded for other purposes. 8006 this.loaded_ = true; 8007 } 8008 if (this.preload_ || (settings.kind !== 'subtitles' && settings.kind !== 'captions')) { 8009 loadTrack(this.src, this); 8010 } 8011 } else { 8012 this.loaded_ = true; 8013 } 8014 } 8015 startTracking() { 8016 // More precise cues based on requestVideoFrameCallback with a requestAnimationFram fallback 8017 this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler); 8018 // Also listen to timeupdate in case rVFC/rAF stops (window in background, audio in video el) 8019 this.tech_.on('timeupdate', this.timeupdateHandler); 8020 } 8021 stopTracking() { 8022 if (this.rvf_) { 8023 this.tech_.cancelVideoFrameCallback(this.rvf_); 8024 this.rvf_ = undefined; 8025 } 8026 this.tech_.off('timeupdate', this.timeupdateHandler); 8027 } 8028 8029 /** 8030 * Add a cue to the internal list of cues. 8031 * 8032 * @param {TextTrack~Cue} cue 8033 * The cue to add to our internal list 8034 */ 8035 addCue(originalCue) {
8036 let cue = originalCue; 8037 8038 // Testing if the cue is a VTTCue in a way that survives minification 8039 if (!('getCueAsHTML' in cue)) { 8040 cue = new window__default['default'].vttjs.VTTCue( 8041 originalCue.startTime, 8042 originalCue.endTime, 8043 originalCue.text 8044 ); 8045 for (const prop in originalCue) { 8046 if (!(prop in cue)) { 8047 cue[prop] = originalCue[prop]; 8048 } 8049 } 8050 8051 // make sure that `id` is copied over 8052 cue.id = originalCue.id; 8053 cue.originalCue_ = originalCue; 8054 } 8055 const tracks = this.tech_.textTracks(); 8056 for (let i = 0; i < tracks.length; i++) { 8057 if (tracks[i] !== this) { 8058 tracks[i].removeCue(cue); 8059 } 8060 } 8061 this.cues_.push(cue); 8062 this.cues.setCues_(this.cues_); 8063 } 8064 8065 /** 8066 * Remove a cue from our internal list 8067 * 8068 * @param {TextTrack~Cue} removeCue 8069 * The cue to remove from our internal list 8070 */ 8071 removeCue(removeCue) { 8072 let i = this.cues_.length; 8073 while (i--) { 8074 const cue = this.cues_[i]; 8075 if (cue === removeCue || (cue.originalCue_ && cue.originalCue_ === removeCue)) { 8076 this.cues_.splice(i, 1); 8077 this.cues.setCues_(this.cues_); 8078 break; 8079 } 8080 } 8081 } 8082} 8083 8084/** 8085 * cuechange - One or more cues in the track have become active or stopped being active. 8086 * 8087 * @protected 8088 */ 8089TextTrack.prototype.allowedEvents_ = { 8090 cuechange: 'cuechange', 8091}; 8092 8093/** 8094 * A representation of a single `AudioTrack`. If it is part of an {@link AudioTrackList} 8095 * only one `AudioTrack` in the list will be enabled at a time. 8096 * 8097 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotrack} 8098 * @extends Track 8099 */ 8100class AudioTrack extends Track { 8101 /** 8102 * Create an instance of this class. 8103 * 8104 * @param {Object} [options={}] 8105 * Object of option names and values 8106 * 8107 * @param {AudioTrack~Kind} [options.kind=''] 8108 * A valid audio track kind 8109 * 8110 * @param {string} [options.id='vjs_track_' + Guid.newGUID()] 8111 * A unique id for this AudioTrack. 8112 * 8113 * @param {string} [options.label=''] 8114 * The menu label for this track. 8115 * 8116 * @param {string} [options.language=''] 8117 * A valid two character language code. 8118 * 8119 * @param {boolean} [options.enabled] 8120 * If this track is the one that is currently playing. If this track is part of 8121 * an {@link AudioTrackList}, only one {@link AudioTrack} will be enabled. 8122 */ 8123 constructor(options = {}) { 8124 const settings = merge$1(options, { 8125 kind: AudioTrackKind[options.kind] || '', 8126 }); 8127 super(settings); 8128 let enabled = false; 8129 8130 /** 8131 * @memberof AudioTrack 8132 * @member {boolean} enabled 8133 * If this `AudioTrack` is enabled or not. When setting this will 8134 * fire {@link AudioTrack#enabledchange} if the state of enabled is changed. 8135 * @instance 8136 * 8137 * @fires VideoTrack#selectedchange 8138 */ 8139 Object.defineProperty(this, 'enabled', { 8140 get() { 8141 return enabled; 8142 }, 8143 set(newEnabled) { 8144 // an invalid or unchanged value 8145 if (typeof newEnabled !== 'boolean' || newEnabled === enabled) { 8146 return; 8147 } 8148 enabled = newEnabled; 8149 8150 /** 8151 * An event that fires when enabled changes on this track. This allows 8152 * the AudioTrackList that holds this track to act accordingly. 8153 * 8154 * > Note: This is not part of the spec! Native tracks will do 8155 * this internally without an event. 8156 * 8157 * @event AudioTrack#enabledchange 8158 * @type {Event} 8159 */ 8160 this.trigger('enabledchange'); 8161 }, 8162 }); 8163 8164 // if the user sets this track to selected then 8165 // set selected to that true value otherwise 8166 // we keep it false 8167 if (settings.enabled) { 8168 this.enabled = settings.enabled; 8169 } 8170 this.loaded_ = true; 8171 } 8172} 8173 8174/** 8175 * A representation of a single `VideoTrack`. 8176 * 8177 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#videotrack} 8178 * @extends Track 8179 */ 8180class VideoTrack extends Track { 8181 /** 8182 * Create an instance of this class. 8183 * 8184 * @param {Object} [options={}] 8185 * Object of option names and values 8186 * 8187 * @param {string} [options.kind=''] 8188 * A valid {@link VideoTrack~Kind} 8189 * 8190 * @param {string} [options.id='vjs_track_' + Guid.newGUID()] 8191 * A unique id for this AudioTrack. 8192 * 8193 * @param {string} [options.label=''] 8194 * The menu label for this track. 8195 * 8196 * @param {string} [options.language=''] 8197 * A valid two character language code. 8198 * 8199 * @param {boolean} [options.selected] 8200 * If this track is the one that is currently playing. 8201 */ 8202 constructor(options = {}) { 8203 const settings = merge$1(options, { 8204 kind: VideoTrackKind[options.kind] || '', 8205 }); 8206 super(settings); 8207 let selected = false;
8208 8209 /** 8210 * @memberof VideoTrack 8211 * @member {boolean} selected 8212 * If this `VideoTrack` is selected or not. When setting this will 8213 * fire {@link VideoTrack#selectedchange} if the state of selected changed. 8214 * @instance 8215 * 8216 * @fires VideoTrack#selectedchange 8217 */ 8218 Object.defineProperty(this, 'selected', { 8219 get() { 8220 return selected; 8221 }, 8222 set(newSelected) { 8223 // an invalid or unchanged value 8224 if (typeof newSelected !== 'boolean' || newSelected === selected) { 8225 return; 8226 } 8227 selected = newSelected; 8228 8229 /** 8230 * An event that fires when selected changes on this track. This allows 8231 * the VideoTrackList that holds this track to act accordingly. 8232 * 8233 * > Note: This is not part of the spec! Native tracks will do 8234 * this internally without an event. 8235 * 8236 * @event VideoTrack#selectedchange 8237 * @type {Event} 8238 */ 8239 this.trigger('selectedchange'); 8240 }, 8241 }); 8242 8243 // if the user sets this track to selected then 8244 // set selected to that true value otherwise 8245 // we keep it false 8246 if (settings.selected) { 8247 this.selected = settings.selected; 8248 } 8249 } 8250} 8251 8252/** 8253 * @file html-track-element.js 8254 */ 8255 8256/** @import Tech from '../tech/tech' */ 8257 8258/** 8259 * A single track represented in the DOM. 8260 * 8261 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#htmltrackelement} 8262 * @extends EventTarget 8263 */ 8264class HTMLTrackElement extends EventTarget$2 { 8265 /** 8266 * Create an instance of this class. 8267 * 8268 * @param {Object} options={} 8269 * Object of option names and values 8270 * 8271 * @param {Tech} options.tech 8272 * A reference to the tech that owns this HTMLTrackElement. 8273 * 8274 * @param {TextTrack~Kind} [options.kind='subtitles'] 8275 * A valid text track kind. 8276 * 8277 * @param {TextTrack~Mode} [options.mode='disabled'] 8278 * A valid text track mode. 8279 * 8280 * @param {string} [options.id='vjs_track_' + Guid.newGUID()] 8281 * A unique id for this TextTrack. 8282 * 8283 * @param {string} [options.label=''] 8284 * The menu label for this track. 8285 * 8286 * @param {string} [options.language=''] 8287 * A valid two character language code. 8288 * 8289 * @param {string} [options.srclang=''] 8290 * A valid two character language code. An alternative, but deprioritized 8291 * version of `options.language` 8292 * 8293 * @param {string} [options.src] 8294 * A url to TextTrack cues. 8295 * 8296 * @param {boolean} [options.default] 8297 * If this track should default to on or off. 8298 */ 8299 constructor(options = {}) { 8300 super(); 8301 let readyState; 8302 const track = new TextTrack(options); 8303 this.kind = track.kind; 8304 this.src = track.src; 8305 this.srclang = track.language; 8306 this.label = track.label; 8307 this.default = track.default; 8308 Object.defineProperties(this, { 8309 /** 8310 * @memberof HTMLTrackElement 8311 * @member {HTMLTrackElement~ReadyState} readyState 8312 * The current ready state of the track element. 8313 * @instance 8314 */ 8315 readyState: { 8316 get() { 8317 return readyState; 8318 }, 8319 }, 8320 /** 8321 * @memberof HTMLTrackElement 8322 * @member {TextTrack} track 8323 * The underlying TextTrack object. 8324 * @instance 8325 * 8326 */ 8327 track: { 8328 get() { 8329 return track; 8330 }, 8331 }, 8332 }); 8333 readyState = HTMLTrackElement.NONE; 8334 8335 /** 8336 * @listens TextTrack#loadeddata 8337 * @fires HTMLTrackElement#load 8338 */ 8339 track.addEventListener('loadeddata', () => { 8340 readyState = HTMLTrackElement.LOADED; 8341 this.trigger({ 8342 type: 'load', 8343 target: this, 8344 }); 8345 }); 8346 } 8347} 8348 8349/** 8350 * @protected 8351 */ 8352HTMLTrackElement.prototype.allowedEvents_ = { 8353 load: 'load', 8354}; 8355 8356/** 8357 * The text track not loaded state. 8358 * 8359 * @type {number} 8360 * @static 8361 */ 8362HTMLTrackElement.NONE = 0; 8363 8364/** 8365 * The text track loading state. 8366 * 8367 * @type {number} 8368 * @static 8369 */ 8370HTMLTrackElement.LOADING = 1; 8371 8372/** 8373 * The text track loaded state. 8374 * 8375 * @type {number} 8376 * @static 8377 */ 8378HTMLTrackElement.LOADED = 2; 8379 8380/** 8381 * The text track failed to load state. 8382 * 8383 * @type {number} 8384 * @static 8385 */ 8386HTMLTrackElement.ERROR = 3; 8387 8388/* 8389 * This file contains all track properties that are used in 8390 * player.js, tech.js, html5.js and possibly other techs in the future. 8391 */ 8392 8393const NORMAL = { 8394 audio: { 8395 ListClass: AudioTrackList, 8396 TrackClass: AudioTrack, 8397 capitalName: 'Audio', 8398 }, 8399 video: { 8400 ListClass: VideoTrackList, 8401 TrackClass: VideoTrack, 8402 capitalName: 'Video', 8403 }, 8404 text: { 8405 ListClass: TextTrackList, 8406 TrackClass: TextTrack, 8407 capitalName: 'Text', 8408 }, 8409};
8410Object.keys(NORMAL).forEach(function (type) { 8411 NORMAL[type].getterName = `${type}Tracks`; 8412 NORMAL[type].privateName = `${type}Tracks_`; 8413}); 8414const REMOTE = { 8415 remoteText: { 8416 ListClass: TextTrackList, 8417 TrackClass: TextTrack, 8418 capitalName: 'RemoteText', 8419 getterName: 'remoteTextTracks', 8420 privateName: 'remoteTextTracks_', 8421 }, 8422 remoteTextEl: { 8423 ListClass: HtmlTrackElementList, 8424 TrackClass: HTMLTrackElement, 8425 capitalName: 'RemoteTextTrackEls', 8426 getterName: 'remoteTextTrackEls', 8427 privateName: 'remoteTextTrackEls_', 8428 }, 8429}; 8430const ALL = Object.assign({}, NORMAL, REMOTE); 8431REMOTE.names = Object.keys(REMOTE); 8432NORMAL.names = Object.keys(NORMAL); 8433ALL.names = [].concat(REMOTE.names).concat(NORMAL.names); 8434 8435/** 8436 * @file tech.js 8437 */ 8438 8439/** @import { TimeRange } from '../utils/time' */ 8440 8441/** 8442 * An Object containing a structure like: `{src: 'url', type: 'mimetype'}` or string 8443 * that just contains the src url alone. 8444 * * `var SourceObject = {src: 'http://ex.com/video.mp4', type: 'video/mp4'};` 8445 * `var SourceString = 'http://example.com/some-video.mp4';` 8446 * 8447 * @typedef {Object|string} SourceObject 8448 * 8449 * @property {string} src 8450 * The url to the source 8451 * 8452 * @property {string} type 8453 * The mime type of the source 8454 */ 8455 8456/** 8457 * A function used by {@link Tech} to create a new {@link TextTrack}. 8458 * 8459 * @private 8460 * 8461 * @param {Tech} self 8462 * An instance of the Tech class. 8463 * 8464 * @param {string} kind 8465 * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata) 8466 * 8467 * @param {string} [label] 8468 * Label to identify the text track 8469 * 8470 * @param {string} [language] 8471 * Two letter language abbreviation 8472 * 8473 * @param {Object} [options={}] 8474 * An object with additional text track options 8475 * 8476 * @return {TextTrack} 8477 * The text track that was created. 8478 */ 8479function createTrackHelper(self, kind, label, language, options = {}) { 8480 const tracks = self.textTracks(); 8481 options.kind = kind; 8482 if (label) { 8483 options.label = label; 8484 } 8485 if (language) { 8486 options.language = language; 8487 } 8488 options.tech = self; 8489 const track = new ALL.text.TrackClass(options); 8490 tracks.addTrack(track); 8491 return track; 8492} 8493 8494/** 8495 * This is the base class for media playback technology controllers, such as 8496 * {@link HTML5} 8497 * 8498 * @extends Component 8499 */ 8500class Tech extends Component$1 { 8501 /** 8502 * Create an instance of this Tech. 8503 * 8504 * @param {Object} [options] 8505 * The key/value store of player options. 8506 * 8507 * @param {Function} [ready] 8508 * Callback function to call when the `HTML5` Tech is ready. 8509 */ 8510 constructor(options = {}, ready = function () {}) { 8511 // we don't want the tech to report user activity automatically. 8512 // This is done manually in addControlsListeners 8513 options.reportTouchActivity = false; 8514 super(null, options, ready); 8515 this.onDurationChange_ = (e) => this.onDurationChange(e); 8516 this.trackProgress_ = (e) => this.trackProgress(e); 8517 this.trackCurrentTime_ = (e) => this.trackCurrentTime(e); 8518 this.stopTrackingCurrentTime_ = (e) => this.stopTrackingCurrentTime(e); 8519 this.disposeSourceHandler_ = (e) => this.disposeSourceHandler(e); 8520 this.queuedHanders_ = new Set(); 8521 8522 // keep track of whether the current source has played at all to 8523 // implement a very limited played() 8524 this.hasStarted_ = false; 8525 this.on('playing', function () { 8526 this.hasStarted_ = true; 8527 }); 8528 this.on('loadstart', function () { 8529 this.hasStarted_ = false; 8530 }); 8531 ALL.names.forEach((name) => { 8532 const props = ALL[name]; 8533 if (options && options[props.getterName]) { 8534 this[props.privateName] = options[props.getterName]; 8535 } 8536 }); 8537 8538 // Manually track progress in cases where the browser/tech doesn't report it. 8539 if (!this.featuresProgressEvents) { 8540 this.manualProgressOn(); 8541 } 8542 8543 // Manually track timeupdates in cases where the browser/tech doesn't report it. 8544 if (!this.featuresTimeupdateEvents) { 8545 this.manualTimeUpdatesOn(); 8546 } 8547 ['Text', 'Audio', 'Video'].forEach((track) => { 8548 if (options[`native${track}Tracks`] === false) { 8549 this[`featuresNative${track}Tracks`] = false;
vendor: 5,078 bytes, lines 8550-8726
8550 } 8551 }); 8552 if (options.nativeCaptions === false || options.nativeTextTracks === false) { 8553 this.featuresNativeTextTracks = false; 8554 } else if (options.nativeCaptions === true || options.nativeTextTracks === true) { 8555 this.featuresNativeTextTracks = true; 8556 } 8557 if (!this.featuresNativeTextTracks) { 8558 this.emulateTextTracks(); 8559 } 8560 this.preloadTextTracks = options.preloadTextTracks !== false; 8561 this.autoRemoteTextTracks_ = new ALL.text.ListClass(); 8562 this.initTrackListeners(); 8563 8564 // Turn on component tap events only if not using native controls 8565 if (!options.nativeControlsForTouch) { 8566 this.emitTapEvents(); 8567 } 8568 if (this.constructor) { 8569 this.name_ = this.constructor.name || 'Unknown Tech'; 8570 } 8571 } 8572 8573 /** 8574 * A special function to trigger source set in a way that will allow player 8575 * to re-trigger if the player or tech are not ready yet. 8576 * 8577 * @fires Tech#sourceset 8578 * @param {string} src The source string at the time of the source changing. 8579 */ 8580 triggerSourceset(src) { 8581 if (!this.isReady_) { 8582 // on initial ready we have to trigger source set 8583 // 1ms after ready so that player can watch for it. 8584 this.one('ready', () => this.setTimeout(() => this.triggerSourceset(src), 1)); 8585 } 8586 8587 /** 8588 * Fired when the source is set on the tech causing the media element 8589 * to reload. 8590 * 8591 * @see {@link Player#event:sourceset} 8592 * @event Tech#sourceset 8593 * @type {Event} 8594 */ 8595 this.trigger({ 8596 src, 8597 type: 'sourceset', 8598 }); 8599 } 8600 8601 /* Fallbacks for unsupported event types 8602 ================================================================================ */ 8603 8604 /** 8605 * Polyfill the `progress` event for browsers that don't support it natively. 8606 * 8607 * @see {@link Tech#trackProgress} 8608 */ 8609 manualProgressOn() { 8610 this.on('durationchange', this.onDurationChange_); 8611 this.manualProgress = true; 8612 8613 // Trigger progress watching when a source begins loading 8614 this.one('ready', this.trackProgress_); 8615 } 8616 8617 /** 8618 * Turn off the polyfill for `progress` events that was created in 8619 * {@link Tech#manualProgressOn} 8620 */ 8621 manualProgressOff() { 8622 this.manualProgress = false; 8623 this.stopTrackingProgress(); 8624 this.off('durationchange', this.onDurationChange_); 8625 } 8626 8627 /** 8628 * This is used to trigger a `progress` event when the buffered percent changes. It 8629 * sets an interval function that will be called every 500 milliseconds to check if the 8630 * buffer end percent has changed. 8631 * 8632 * > This function is called by {@link Tech#manualProgressOn} 8633 * 8634 * @param {Event} event 8635 * The `ready` event that caused this to run. 8636 * 8637 * @listens Tech#ready 8638 * @fires Tech#progress 8639 */ 8640 trackProgress(event) { 8641 this.stopTrackingProgress(); 8642 this.progressInterval = this.setInterval( 8643 bind_(this, function () { 8644 // Don't trigger unless buffered amount is greater than last time 8645 8646 const numBufferedPercent = this.bufferedPercent(); 8647 if (this.bufferedPercent_ !== numBufferedPercent) { 8648 /** 8649 * See {@link Player#progress} 8650 * 8651 * @event Tech#progress 8652 * @type {Event} 8653 */ 8654 this.trigger('progress'); 8655 } 8656 this.bufferedPercent_ = numBufferedPercent; 8657 if (numBufferedPercent === 1) { 8658 this.stopTrackingProgress(); 8659 } 8660 }), 8661 500 8662 ); 8663 } 8664 8665 /** 8666 * Update our internal duration on a `durationchange` event by calling 8667 * {@link Tech#duration}. 8668 * 8669 * @param {Event} event 8670 * The `durationchange` event that caused this to run. 8671 * 8672 * @listens Tech#durationchange 8673 */ 8674 onDurationChange(event) { 8675 this.duration_ = this.duration(); 8676 } 8677 8678 /** 8679 * Get and create a `TimeRange` object for buffering. 8680 * 8681 * @return {TimeRange} 8682 * The time range object that was created. 8683 */ 8684 buffered() { 8685 return createTimeRanges$1(0, 0); 8686 } 8687 8688 /** 8689 * Get the percentage of the current video that is currently buffered. 8690 * 8691 * @return {number} 8692 * A number from 0 to 1 that represents the decimal percentage of the 8693 * video that is buffered. 8694 * 8695 */ 8696 bufferedPercent() { 8697 return bufferedPercent(this.buffered(), this.duration_); 8698 } 8699 8700 /** 8701 * Turn off the polyfill for `progress` events that was created in 8702 * {@link Tech#manualProgressOn} 8703 * Stop manually tracking progress events by clearing the interval that was set in 8704 * {@link Tech#trackProgress}. 8705 */ 8706 stopTrackingProgress() { 8707 this.clearInterval(this.progressInterval); 8708 } 8709 8710 /** 8711 * Polyfill the `timeupdate` event for browsers that don't support it. 8712 * 8713 * @see {@link Tech#trackCurrentTime} 8714 */ 8715 manualTimeUpdatesOn() { 8716 this.manualTimeUpdates = true; 8717 this.on('play', this.trackCurrentTime_); 8718 this.on('pause', this.stopTrackingCurrentTime_); 8719 } 8720 8721 /** 8722 * Turn off the polyfill for `timeupdate` events that was created in 8723 * {@link Tech#manualTimeUpdatesOn} 8724 */ 8725 manualTimeUpdatesOff() { 8726 this.manualTimeUpdates = false;
vendor: 6,402 bytes, lines 8727-8982
8727 this.stopTrackingCurrentTime(); 8728 this.off('play', this.trackCurrentTime_); 8729 this.off('pause', this.stopTrackingCurrentTime_); 8730 } 8731 8732 /** 8733 * Sets up an interval function to track current time and trigger `timeupdate` every 8734 * 250 milliseconds. 8735 * 8736 * @listens Tech#play 8737 * @triggers Tech#timeupdate 8738 */ 8739 trackCurrentTime() { 8740 if (this.currentTimeInterval) { 8741 this.stopTrackingCurrentTime(); 8742 } 8743 this.currentTimeInterval = this.setInterval(function () { 8744 /** 8745 * Triggered at an interval of 250ms to indicated that time is passing in the video. 8746 * 8747 * @event Tech#timeupdate 8748 * @type {Event} 8749 */ 8750 this.trigger({ 8751 type: 'timeupdate', 8752 target: this, 8753 manuallyTriggered: true, 8754 }); 8755 8756 // 42 = 24 fps // 250 is what Webkit uses // FF uses 15 8757 }, 250); 8758 } 8759 8760 /** 8761 * Stop the interval function created in {@link Tech#trackCurrentTime} so that the 8762 * `timeupdate` event is no longer triggered. 8763 * 8764 * @listens {Tech#pause} 8765 */ 8766 stopTrackingCurrentTime() { 8767 this.clearInterval(this.currentTimeInterval); 8768 8769 // #1002 - if the video ends right before the next timeupdate would happen, 8770 // the progress bar won't make it all the way to the end 8771 this.trigger({ 8772 type: 'timeupdate', 8773 target: this, 8774 manuallyTriggered: true, 8775 }); 8776 } 8777 8778 /** 8779 * Turn off all event polyfills, clear the `Tech`s {@link AudioTrackList}, 8780 * {@link VideoTrackList}, and {@link TextTrackList}, and dispose of this Tech. 8781 * 8782 * @fires Component#dispose 8783 */ 8784 dispose() { 8785 // clear out all tracks because we can't reuse them between techs 8786 this.clearTracks(NORMAL.names); 8787 8788 // Turn off any manual progress or timeupdate tracking 8789 if (this.manualProgress) { 8790 this.manualProgressOff(); 8791 } 8792 if (this.manualTimeUpdates) { 8793 this.manualTimeUpdatesOff(); 8794 } 8795 super.dispose(); 8796 } 8797 8798 /** 8799 * Clear out a single `TrackList` or an array of `TrackLists` given their names. 8800 * 8801 * > Note: Techs without source handlers should call this between sources for `video` 8802 * & `audio` tracks. You don't want to use them between tracks! 8803 * 8804 * @param {string[]|string} types 8805 * TrackList names to clear, valid names are `video`, `audio`, and 8806 * `text`. 8807 */ 8808 clearTracks(types) { 8809 types = [].concat(types); 8810 // clear out all tracks because we can't reuse them between techs 8811 types.forEach((type) => { 8812 const list = this[`${type}Tracks`]() || []; 8813 let i = list.length; 8814 while (i--) { 8815 const track = list[i]; 8816 if (type === 'text') { 8817 this.removeRemoteTextTrack(track); 8818 } 8819 list.removeTrack(track); 8820 } 8821 }); 8822 } 8823 8824 /** 8825 * Remove any TextTracks added via addRemoteTextTrack that are 8826 * flagged for automatic garbage collection 8827 */ 8828 cleanupAutoTextTracks() { 8829 const list = this.autoRemoteTextTracks_ || []; 8830 let i = list.length; 8831 while (i--) { 8832 const track = list[i]; 8833 this.removeRemoteTextTrack(track); 8834 } 8835 } 8836 8837 /** 8838 * Reset the tech, which will removes all sources and reset the internal readyState. 8839 * 8840 * @abstract 8841 */ 8842 reset() {} 8843 8844 /** 8845 * Get the value of `crossOrigin` from the tech. 8846 * 8847 * @abstract 8848 * 8849 * @see {Html5#crossOrigin} 8850 */ 8851 crossOrigin() {} 8852 8853 /** 8854 * Set the value of `crossOrigin` on the tech. 8855 * 8856 * @abstract 8857 * 8858 * @param {string} crossOrigin the crossOrigin value 8859 * @see {Html5#setCrossOrigin} 8860 */ 8861 setCrossOrigin() {} 8862 8863 /** 8864 * Get or set an error on the Tech. 8865 * 8866 * @param {MediaError} [err] 8867 * Error to set on the Tech 8868 * 8869 * @return {MediaError|null} 8870 * The current error object on the tech, or null if there isn't one. 8871 */ 8872 error(err) { 8873 if (err !== undefined) { 8874 this.error_ = new MediaError(err); 8875 this.trigger('error'); 8876 } 8877 return this.error_; 8878 } 8879 8880 /** 8881 * Returns the `TimeRange`s that have been played through for the current source. 8882 * 8883 * > NOTE: This implementation is incomplete. It does not track the played `TimeRange`. 8884 * It only checks whether the source has played at all or not. 8885 * 8886 * @return {TimeRange} 8887 * - A single time range if this video has played 8888 * - An empty set of ranges if not. 8889 */ 8890 played() { 8891 if (this.hasStarted_) { 8892 return createTimeRanges$1(0, 0); 8893 } 8894 return createTimeRanges$1(); 8895 } 8896 8897 /** 8898 * Start playback 8899 * 8900 * @abstract 8901 * 8902 * @see {Html5#play} 8903 */ 8904 play() {} 8905 8906 /** 8907 * Set whether we are scrubbing or not 8908 * 8909 * @abstract 8910 * @param {boolean} _isScrubbing 8911 * - true for we are currently scrubbing 8912 * - false for we are no longer scrubbing 8913 * 8914 * @see {Html5#setScrubbing} 8915 */ 8916 setScrubbing(_isScrubbing) {} 8917 8918 /** 8919 * Get whether we are scrubbing or not 8920 * 8921 * @abstract 8922 * 8923 * @see {Html5#scrubbing} 8924 */ 8925 scrubbing() {} 8926 8927 /** 8928 * Causes a manual time update to occur if {@link Tech#manualTimeUpdatesOn} was 8929 * previously called. 8930 * 8931 * @param {number} _seconds 8932 * Set the current time of the media to this. 8933 * @fires Tech#timeupdate 8934 */ 8935 setCurrentTime(_seconds) { 8936 // improve the accuracy of manual timeupdates 8937 if (this.manualTimeUpdates) { 8938 /** 8939 * A manual `timeupdate` event. 8940 * 8941 * @event Tech#timeupdate 8942 * @type {Event} 8943 */ 8944 this.trigger({ 8945 type: 'timeupdate', 8946 target: this, 8947 manuallyTriggered: true, 8948 }); 8949 } 8950 } 8951 8952 /** 8953 * Turn on listeners for {@link VideoTrackList}, {@link {AudioTrackList}, and 8954 * {@link TextTrackList} events. 8955 * 8956 * This adds {@link EventTarget~EventListeners} for `addtrack`, and `removetrack`. 8957 * 8958 * @fires Tech#audiotrackchange 8959 * @fires Tech#videotrackchange 8960 * @fires Tech#texttrackchange 8961 */ 8962 initTrackListeners() { 8963 /** 8964 * Triggered when tracks are added or removed on the Tech {@link AudioTrackList} 8965 * 8966 * @event Tech#audiotrackchange 8967 * @type {Event} 8968 */ 8969 8970 /** 8971 * Triggered when tracks are added or removed on the Tech {@link VideoTrackList} 8972 * 8973 * @event Tech#videotrackchange 8974 * @type {Event} 8975 */ 8976 8977 /** 8978 * Triggered when tracks are added or removed on the Tech {@link TextTrackList} 8979 * 8980 * @event Tech#texttrackchange 8981 * @type {Event} 8982 */
vendor: 5,560 bytes, lines 8983-9145
8983 NORMAL.names.forEach((name) => { 8984 const props = NORMAL[name]; 8985 const trackListChanges = () => { 8986 this.trigger(`${name}trackchange`); 8987 }; 8988 const tracks = this[props.getterName](); 8989 tracks.addEventListener('removetrack', trackListChanges); 8990 tracks.addEventListener('addtrack', trackListChanges); 8991 this.on('dispose', () => { 8992 tracks.removeEventListener('removetrack', trackListChanges); 8993 tracks.removeEventListener('addtrack', trackListChanges); 8994 }); 8995 }); 8996 } 8997 8998 /** 8999 * Emulate TextTracks using vtt.js if necessary 9000 * 9001 * @fires Tech#vttjsloaded 9002 * @fires Tech#vttjserror 9003 */ 9004 addWebVttScript_() { 9005 if (window__default['default'].WebVTT) { 9006 return; 9007 } 9008 9009 // Initially, Tech.el_ is a child of a dummy-div wait until the Component system 9010 // signals that the Tech is ready at which point Tech.el_ is part of the DOM 9011 // before inserting the WebVTT script 9012 if (document__default['default'].body.contains(this.el())) { 9013 // load via require if available and vtt.js script location was not passed in 9014 // as an option. novtt builds will turn the above require call into an empty object 9015 // which will cause this if check to always fail. 9016 if ( 9017 !this.options_['vtt.js'] && 9018 isPlain(vtt__default['default']) && 9019 Object.keys(vtt__default['default']).length > 0 9020 ) { 9021 this.trigger('vttjsloaded'); 9022 return; 9023 } 9024 9025 // load vtt.js via the script location option or the cdn of no location was 9026 // passed in 9027 const script = document__default['default'].createElement('script'); 9028 script.src = this.options_['vtt.js'] || 'https://vjs.zencdn.net/vttjs/0.14.1/vtt.min.js'; 9029 script.onload = () => { 9030 /** 9031 * Fired when vtt.js is loaded. 9032 * 9033 * @event Tech#vttjsloaded 9034 * @type {Event} 9035 */ 9036 this.trigger('vttjsloaded'); 9037 }; 9038 script.onerror = () => { 9039 /** 9040 * Fired when vtt.js was not loaded due to an error 9041 * 9042 * @event Tech#vttjsloaded 9043 * @type {Event} 9044 */ 9045 this.trigger('vttjserror'); 9046 }; 9047 this.on('dispose', () => { 9048 script.onload = null; 9049 script.onerror = null; 9050 }); 9051 // but have not loaded yet and we set it to true before the inject so that 9052 // we don't overwrite the injected window.WebVTT if it loads right away 9053 window__default['default'].WebVTT = true; 9054 this.el().parentNode.appendChild(script); 9055 } else { 9056 this.ready(this.addWebVttScript_); 9057 } 9058 } 9059 9060 /** 9061 * Emulate texttracks 9062 * 9063 */ 9064 emulateTextTracks() { 9065 const tracks = this.textTracks(); 9066 const remoteTracks = this.remoteTextTracks(); 9067 const handleAddTrack = (e) => tracks.addTrack(e.track); 9068 const handleRemoveTrack = (e) => tracks.removeTrack(e.track); 9069 remoteTracks.on('addtrack', handleAddTrack); 9070 remoteTracks.on('removetrack', handleRemoveTrack); 9071 this.addWebVttScript_(); 9072 const updateDisplay = () => this.trigger('texttrackchange'); 9073 const textTracksChanges = () => { 9074 updateDisplay(); 9075 for (let i = 0; i < tracks.length; i++) { 9076 const track = tracks[i]; 9077 track.removeEventListener('cuechange', updateDisplay); 9078 if (track.mode === 'showing') { 9079 track.addEventListener('cuechange', updateDisplay); 9080 } 9081 } 9082 }; 9083 textTracksChanges(); 9084 tracks.addEventListener('change', textTracksChanges); 9085 tracks.addEventListener('addtrack', textTracksChanges); 9086 tracks.addEventListener('removetrack', textTracksChanges); 9087 this.on('dispose', function () { 9088 remoteTracks.off('addtrack', handleAddTrack); 9089 remoteTracks.off('removetrack', handleRemoveTrack); 9090 tracks.removeEventListener('change', textTracksChanges); 9091 tracks.removeEventListener('addtrack', textTracksChanges); 9092 tracks.removeEventListener('removetrack', textTracksChanges); 9093 for (let i = 0; i < tracks.length; i++) { 9094 const track = tracks[i]; 9095 track.removeEventListener('cuechange', updateDisplay); 9096 } 9097 }); 9098 } 9099 9100 /** 9101 * Create and returns a remote {@link TextTrack} object. 9102 * 9103 * @param {string} kind 9104 * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata) 9105 * 9106 * @param {string} [label] 9107 * Label to identify the text track 9108 * 9109 * @param {string} [language] 9110 * Two letter language abbreviation 9111 * 9112 * @return {TextTrack} 9113 * The TextTrack that gets created. 9114 */ 9115 addTextTrack(kind, label, language) { 9116 if (!kind) { 9117 throw new Error('TextTrack kind is required but was not provided'); 9118 } 9119 return createTrackHelper(this, kind, label, language); 9120 } 9121 9122 /** 9123 * Create an emulated TextTrack for use by addRemoteTextTrack 9124 * 9125 * This is intended to be overridden by classes that inherit from 9126 * Tech in order to create native or custom TextTracks. 9127 * 9128 * @param {Object} options 9129 * The object should contain the options to initialize the TextTrack with. 9130 * 9131 * @param {string} [options.kind] 9132 * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata). 9133 * 9134 * @param {string} [options.label]. 9135 * Label to identify the text track 9136 * 9137 * @param {string} [options.language] 9138 * Two letter language abbreviation. 9139 * 9140 * @return {HTMLTrackElement} 9141 * The track element that gets created. 9142 */ 9143 createRemoteTextTrack(options) { 9144 const track = merge$1(options, { 9145 tech: this,
vendor: 7,926 bytes, lines 9145-9436
9145 9146 }); 9147 return new REMOTE.remoteTextEl.TrackClass(track); 9148 } 9149 9150 /** 9151 * Creates a remote text track object and returns an html track element. 9152 * 9153 * > Note: This can be an emulated {@link HTMLTrackElement} or a native one. 9154 * 9155 * @param {Object} options 9156 * See {@link Tech#createRemoteTextTrack} for more detailed properties. 9157 * 9158 * @param {boolean} [manualCleanup=false] 9159 * - When false: the TextTrack will be automatically removed from the video 9160 * element whenever the source changes 9161 * - When True: The TextTrack will have to be cleaned up manually 9162 * 9163 * @return {HTMLTrackElement} 9164 * An Html Track Element. 9165 * 9166 */ 9167 addRemoteTextTrack(options = {}, manualCleanup) { 9168 const htmlTrackElement = this.createRemoteTextTrack(options); 9169 if (typeof manualCleanup !== 'boolean') { 9170 manualCleanup = false; 9171 } 9172 9173 // store HTMLTrackElement and TextTrack to remote list 9174 this.remoteTextTrackEls().addTrackElement_(htmlTrackElement); 9175 this.remoteTextTracks().addTrack(htmlTrackElement.track); 9176 if (manualCleanup === false) { 9177 // create the TextTrackList if it doesn't exist 9178 this.ready(() => this.autoRemoteTextTracks_.addTrack(htmlTrackElement.track)); 9179 } 9180 return htmlTrackElement; 9181 } 9182 9183 /** 9184 * Remove a remote text track from the remote `TextTrackList`. 9185 * 9186 * @param {TextTrack} track 9187 * `TextTrack` to remove from the `TextTrackList` 9188 */ 9189 removeRemoteTextTrack(track) { 9190 const trackElement = this.remoteTextTrackEls().getTrackElementByTrack_(track); 9191 9192 // remove HTMLTrackElement and TextTrack from remote list 9193 this.remoteTextTrackEls().removeTrackElement_(trackElement); 9194 this.remoteTextTracks().removeTrack(track); 9195 this.autoRemoteTextTracks_.removeTrack(track); 9196 } 9197 9198 /** 9199 * Gets available media playback quality metrics as specified by the W3C's Media 9200 * Playback Quality API. 9201 * 9202 * @see [Spec]{@link https://wicg.github.io/media-playback-quality} 9203 * 9204 * @return {Object} 9205 * An object with supported media playback quality metrics 9206 * 9207 * @abstract 9208 */ 9209 getVideoPlaybackQuality() { 9210 return {}; 9211 } 9212 9213 /** 9214 * Attempt to create a floating video window always on top of other windows 9215 * so that users may continue consuming media while they interact with other 9216 * content sites, or applications on their device. 9217 * 9218 * @see [Spec]{@link https://wicg.github.io/picture-in-picture} 9219 * 9220 * @return {Promise|undefined} 9221 * A promise with a Picture-in-Picture window if the browser supports 9222 * Promises (or one was passed in as an option). It returns undefined 9223 * otherwise. 9224 * 9225 * @abstract 9226 */ 9227 requestPictureInPicture() { 9228 return Promise.reject(); 9229 } 9230 9231 /** 9232 * A method to check for the value of the 'disablePictureInPicture' <video> property. 9233 * Defaults to true, as it should be considered disabled if the tech does not support pip 9234 * 9235 * @abstract 9236 */ 9237 disablePictureInPicture() { 9238 return true; 9239 } 9240 9241 /** 9242 * A method to set or unset the 'disablePictureInPicture' <video> property. 9243 * 9244 * @abstract 9245 */ 9246 setDisablePictureInPicture() {} 9247 9248 /** 9249 * A fallback implementation of requestVideoFrameCallback using requestAnimationFrame 9250 * 9251 * @param {function} cb 9252 * @return {number} request id 9253 */ 9254 requestVideoFrameCallback(cb) { 9255 const id = newGUID(); 9256 if (!this.isReady_ || this.paused()) { 9257 this.queuedHanders_.add(id); 9258 this.one('playing', () => { 9259 if (this.queuedHanders_.has(id)) { 9260 this.queuedHanders_.delete(id); 9261 cb(); 9262 } 9263 }); 9264 } else { 9265 this.requestNamedAnimationFrame(id, cb); 9266 } 9267 return id; 9268 } 9269 9270 /** 9271 * A fallback implementation of cancelVideoFrameCallback 9272 * 9273 * @param {number} id id of callback to be cancelled 9274 */ 9275 cancelVideoFrameCallback(id) { 9276 if (this.queuedHanders_.has(id)) { 9277 this.queuedHanders_.delete(id); 9278 } else { 9279 this.cancelNamedAnimationFrame(id); 9280 } 9281 } 9282 9283 /** 9284 * A method to set a poster from a `Tech`. 9285 * 9286 * @abstract 9287 */ 9288 setPoster() {} 9289 9290 /** 9291 * A method to check for the presence of the 'playsinline' <video> attribute. 9292 * 9293 * @abstract 9294 */ 9295 playsinline() {} 9296 9297 /** 9298 * A method to set or unset the 'playsinline' <video> attribute. 9299 * 9300 * @abstract 9301 */ 9302 setPlaysinline() {} 9303 9304 /** 9305 * Attempt to force override of native audio tracks. 9306 * 9307 * @param {boolean} override - If set to true native audio will be overridden, 9308 * otherwise native audio will potentially be used. 9309 * 9310 * @abstract 9311 */ 9312 overrideNativeAudioTracks(override) {} 9313 9314 /** 9315 * Attempt to force override of native video tracks. 9316 * 9317 * @param {boolean} override - If set to true native video will be overridden, 9318 * otherwise native video will potentially be used. 9319 * 9320 * @abstract 9321 */ 9322 overrideNativeVideoTracks(override) {} 9323 9324 /** 9325 * Check if the tech can support the given mime-type. 9326 * 9327 * The base tech does not support any type, but source handlers might 9328 * overwrite this. 9329 * 9330 * @param {string} _type 9331 * The mimetype to check for support 9332 * 9333 * @return {string} 9334 * 'probably', 'maybe', or empty string 9335 * 9336 * @see [Spec]{@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/canPlayType} 9337 * 9338 * @abstract 9339 */ 9340 canPlayType(_type) { 9341 return ''; 9342 } 9343 9344 /** 9345 * Check if the type is supported by this tech. 9346 * 9347 * The base tech does not support any type, but source handlers might 9348 * overwrite this. 9349 * 9350 * @param {string} _type 9351 * The media type to check 9352 * @return {string} Returns the native video element's response 9353 */ 9354 static canPlayType(_type) { 9355 return ''; 9356 } 9357 9358 /** 9359 * Check if the tech can support the given source 9360 * 9361 * @param {Object} srcObj 9362 * The source object 9363 * @param {Object} options 9364 * The options passed to the tech 9365 * @return {string} 'probably', 'maybe', or '' (empty string) 9366 */ 9367 static canPlaySource(srcObj, options) { 9368 return Tech.canPlayType(srcObj.type); 9369 } 9370 9371 /* 9372 * Return whether the argument is a Tech or not. 9373 * Can be passed either a Class like `Html5` or a instance like `player.tech_` 9374 * 9375 * @param {Object} component 9376 * The item to check 9377 * 9378 * @return {boolean} 9379 * Whether it is a tech or not 9380 * - True if it is a tech 9381 * - False if it is not 9382 */ 9383 static isTech(component) { 9384 return component.prototype instanceof Tech || component instanceof Tech || component === Tech; 9385 } 9386 9387 /** 9388 * Registers a `Tech` into a shared list for videojs. 9389 * 9390 * @param {string} name 9391 * Name of the `Tech` to register. 9392 * 9393 * @param {Object} tech 9394 * The `Tech` class to register. 9395 */ 9396 static registerTech(name, tech) { 9397 if (!Tech.techs_) { 9398 Tech.techs_ = {}; 9399 } 9400 if (!Tech.isTech(tech)) { 9401 throw new Error(`Tech ${name} must be a Tech`); 9402 } 9403 if (!Tech.canPlayType) { 9404 throw new Error('Techs must have a static canPlayType method on them'); 9405 } 9406 if (!Tech.canPlaySource) { 9407 throw new Error('Techs must have a static canPlaySource method on them'); 9408 } 9409 name = toTitleCase$1(name); 9410 Tech.techs_[name] = tech; 9411 Tech.techs_[toLowerCase(name)] = tech; 9412 if (name !== 'Tech') { 9413 // camel case the techName for use in techOrder 9414 Tech.defaultTechOrder_.push(name); 9415 } 9416 return tech; 9417 } 9418 9419 /** 9420 * Get a `Tech` from the shared list by name. 9421 * 9422 * @param {string} name 9423 * `camelCase` or `TitleCase` name of the Tech to get 9424 * 9425 * @return {Tech|undefined} 9426 * The `Tech` or undefined if there was no tech with the name requested. 9427 */ 9428 static getTech(name) { 9429 if (!name) { 9430 return; 9431 } 9432 if (Tech.techs_ && Tech.techs_[name]) { 9433 return Tech.techs_[name]; 9434 } 9435 name = toTitleCase$1(name); 9436 if (
9436 9437 window__default['default'] && 9438 window__default['default'].videojs && 9439 window__default['default'].videojs[name] 9440 ) { 9441 log$1.warn( 9442 `The ${name} tech was added to the videojs object when it should be registered using videojs.registerTech(name, tech)` 9443 ); 9444 return window__default['default']
vendor: 11,353 bytes, lines 9444-9880
9444.videojs[name]; 9445 } 9446 } 9447} 9448 9449/** 9450 * Get the {@link VideoTrackList} 9451 * 9452 * @returns {VideoTrackList} 9453 * @method Tech.prototype.videoTracks 9454 */ 9455 9456/** 9457 * Get the {@link AudioTrackList} 9458 * 9459 * @returns {AudioTrackList} 9460 * @method Tech.prototype.audioTracks 9461 */ 9462 9463/** 9464 * Get the {@link TextTrackList} 9465 * 9466 * @returns {TextTrackList} 9467 * @method Tech.prototype.textTracks 9468 */ 9469 9470/** 9471 * Get the remote element {@link TextTrackList} 9472 * 9473 * @returns {TextTrackList} 9474 * @method Tech.prototype.remoteTextTracks 9475 */ 9476 9477/** 9478 * Get the remote element {@link HtmlTrackElementList} 9479 * 9480 * @returns {HtmlTrackElementList} 9481 * @method Tech.prototype.remoteTextTrackEls 9482 */ 9483 9484ALL.names.forEach(function (name) { 9485 const props = ALL[name]; 9486 Tech.prototype[props.getterName] = function () { 9487 this[props.privateName] = this[props.privateName] || new props.ListClass(); 9488 return this[props.privateName]; 9489 }; 9490}); 9491 9492/** 9493 * List of associated text tracks 9494 * 9495 * @type {TextTrackList} 9496 * @private 9497 * @property Tech#textTracks_ 9498 */ 9499 9500/** 9501 * List of associated audio tracks. 9502 * 9503 * @type {AudioTrackList} 9504 * @private 9505 * @property Tech#audioTracks_ 9506 */ 9507 9508/** 9509 * List of associated video tracks. 9510 * 9511 * @type {VideoTrackList} 9512 * @private 9513 * @property Tech#videoTracks_ 9514 */ 9515 9516/** 9517 * Boolean indicating whether the `Tech` supports volume control. 9518 * 9519 * @type {boolean} 9520 * @default 9521 */ 9522Tech.prototype.featuresVolumeControl = true; 9523 9524/** 9525 * Boolean indicating whether the `Tech` supports muting volume. 9526 * 9527 * @type {boolean} 9528 * @default 9529 */ 9530Tech.prototype.featuresMuteControl = true; 9531 9532/** 9533 * Boolean indicating whether the `Tech` supports fullscreen resize control. 9534 * Resizing plugins using request fullscreen reloads the plugin 9535 * 9536 * @type {boolean} 9537 * @default 9538 */ 9539Tech.prototype.featuresFullscreenResize = false; 9540 9541/** 9542 * Boolean indicating whether the `Tech` supports changing the speed at which the video 9543 * plays. Examples: 9544 * - Set player to play 2x (twice) as fast 9545 * - Set player to play 0.5x (half) as fast 9546 * 9547 * @type {boolean} 9548 * @default 9549 */ 9550Tech.prototype.featuresPlaybackRate = false; 9551 9552/** 9553 * Boolean indicating whether the `Tech` supports the `progress` event. 9554 * This will be used to determine if {@link Tech#manualProgressOn} should be called. 9555 * 9556 * @type {boolean} 9557 * @default 9558 */ 9559Tech.prototype.featuresProgressEvents = false; 9560 9561/** 9562 * Boolean indicating whether the `Tech` supports the `sourceset` event. 9563 * 9564 * A tech should set this to `true` and then use {@link Tech#triggerSourceset} 9565 * to trigger a {@link Tech#event:sourceset} at the earliest time after getting 9566 * a new source. 9567 * 9568 * @type {boolean} 9569 * @default 9570 */ 9571Tech.prototype.featuresSourceset = false; 9572 9573/** 9574 * Boolean indicating whether the `Tech` supports the `timeupdate` event. 9575 * This will be used to determine if {@link Tech#manualTimeUpdates} should be called. 9576 * 9577 * @type {boolean} 9578 * @default 9579 */ 9580Tech.prototype.featuresTimeupdateEvents = false; 9581 9582/** 9583 * Boolean indicating whether the `Tech` supports the native `TextTrack`s. 9584 * This will help us integrate with native `TextTrack`s if the browser supports them. 9585 * 9586 * @type {boolean} 9587 * @default 9588 */ 9589Tech.prototype.featuresNativeTextTracks = false; 9590 9591/** 9592 * Boolean indicating whether the `Tech` supports `requestVideoFrameCallback`. 9593 * 9594 * @type {boolean} 9595 * @default 9596 */ 9597Tech.prototype.featuresVideoFrameCallback = false; 9598 9599/** 9600 * A functional mixin for techs that want to use the Source Handler pattern. 9601 * Source handlers are scripts for handling specific formats. 9602 * The source handler pattern is used for adaptive formats (HLS, DASH) that 9603 * manually load video data and feed it into a Source Buffer (Media Source Extensions) 9604 * Example: `Tech.withSourceHandlers.call(MyTech);` 9605 * 9606 * @param {Tech} _Tech 9607 * The tech to add source handler functions to. 9608 * 9609 * @mixes Tech~SourceHandlerAdditions 9610 */ 9611Tech.withSourceHandlers = function (_Tech) { 9612 /** 9613 * Register a source handler 9614 * 9615 * @param {Function} handler 9616 * The source handler class 9617 * 9618 * @param {number} [index] 9619 * Register it at the following index 9620 */ 9621 _Tech.registerSourceHandler = function (handler, index) { 9622 let handlers = _Tech.sourceHandlers; 9623 if (!handlers) { 9624 handlers = _Tech.sourceHandlers = []; 9625 } 9626 if (index === undefined) { 9627 // add to the end of the list 9628 index = handlers.length; 9629 } 9630 handlers.splice(index, 0, handler); 9631 }; 9632 9633 /** 9634 * Check if the tech can support the given type. Also checks the 9635 * Techs sourceHandlers. 9636 * 9637 * @param {string} type 9638 * The mimetype to check. 9639 * 9640 * @return {string} 9641 * 'probably', 'maybe', or '' (empty string) 9642 */ 9643 _Tech.canPlayType = function (type) { 9644 const handlers = _Tech.sourceHandlers || []; 9645 let can; 9646 for (let i = 0; i < handlers.length; i++) { 9647 can = handlers[i].canPlayType(type); 9648 if (can) { 9649 return can; 9650 } 9651 } 9652 return ''; 9653 }; 9654 9655 /** 9656 * Returns the first source handler that supports the source. 9657 * 9658 * TODO: Answer question: should 'probably' be prioritized over 'maybe' 9659 * 9660 * @param {SourceObject} source 9661 * The source object 9662 * 9663 * @param {Object} options 9664 * The options passed to the tech 9665 * 9666 * @return {SourceHandler|null} 9667 * The first source handler that supports the source or null if 9668 * no SourceHandler supports the source 9669 */ 9670 _Tech.selectSourceHandler = function (source, options) { 9671 const handlers = _Tech.sourceHandlers || []; 9672 let can; 9673 for (let i = 0; i < handlers.length; i++) { 9674 can = handlers[i].canHandleSource(source, options); 9675 if (can) { 9676 return handlers[i]; 9677 } 9678 } 9679 return null; 9680 }; 9681 9682 /** 9683 * Check if the tech can support the given source. 9684 * 9685 * @param {SourceObject} srcObj 9686 * The source object 9687 * 9688 * @param {Object} options 9689 * The options passed to the tech 9690 * 9691 * @return {string} 9692 * 'probably', 'maybe', or '' (empty string) 9693 */ 9694 _Tech.canPlaySource = function (srcObj, options) { 9695 const sh = _Tech.selectSourceHandler(srcObj, options); 9696 if (sh) { 9697 return sh.canHandleSource(srcObj, options); 9698 } 9699 return ''; 9700 }; 9701 9702 /** 9703 * When using a source handler, prefer its implementation of 9704 * any function normally provided by the tech. 9705 */ 9706 const deferrable = ['seekable', 'seeking', 'duration']; 9707 9708 /** 9709 * A wrapper around {@link Tech#seekable} that will call a `SourceHandler`s seekable 9710 * function if it exists, with a fallback to the Techs seekable function. 9711 * 9712 * @method _Tech.seekable 9713 */ 9714 9715 /** 9716 * A wrapper around {@link Tech#duration} that will call a `SourceHandler`s duration 9717 * function if it exists, otherwise it will fallback to the techs duration function. 9718 * 9719 * @method _Tech.duration 9720 */ 9721 9722 deferrable.forEach(function (fnName) { 9723 const originalFn = this[fnName]; 9724 if (typeof originalFn !== 'function') { 9725 return; 9726 } 9727 this[fnName] = function () { 9728 if (this.sourceHandler_ && this.sourceHandler_[fnName]) { 9729 return this.sourceHandler_[fnName].apply(this.sourceHandler_, arguments); 9730 } 9731 return originalFn.apply(this, arguments); 9732 }; 9733 }, _Tech.prototype); 9734 9735 /** 9736 * Create a function for setting the source using a source object 9737 * and source handlers. 9738 * Should never be called unless a source handler was found. 9739 * 9740 * @param {SourceObject} source 9741 * A source object with src and type keys 9742 */ 9743 _Tech.prototype.setSource = function (source) { 9744 let sh = _Tech.selectSourceHandler(source, this.options_); 9745 if (!sh) { 9746 // Fall back to a native source handler when unsupported sources are 9747 // deliberately set 9748 if (_Tech.nativeSourceHandler) { 9749 sh = _Tech.nativeSourceHandler; 9750 } else { 9751 log$1.error('No source handler found for the current source.'); 9752 } 9753 } 9754 9755 // Dispose any existing source handler 9756 this.disposeSourceHandler(); 9757 this.off('dispose', this.disposeSourceHandler_); 9758 if (sh !== _Tech.nativeSourceHandler) { 9759 this.currentSource_ = source; 9760 } 9761 this.sourceHandler_ = sh.handleSource(source, this, this.options_); 9762 this.one('dispose', this.disposeSourceHandler_); 9763 }; 9764 9765 /** 9766 * Clean up any existing SourceHandlers and listeners when the Tech is disposed. 9767 * 9768 * @listens Tech#dispose 9769 */ 9770 _Tech.prototype.disposeSourceHandler = function () { 9771 // if we have a source and get another one 9772 // then we are loading something new 9773 // than clear all of our current tracks 9774 if (this.currentSource_) { 9775 this.clearTracks(['audio', 'video']); 9776 this.currentSource_ = null; 9777 } 9778 9779 // always clean up auto-text tracks 9780 this.cleanupAutoTextTracks(); 9781 if (this.sourceHandler_) { 9782 if (this.sourceHandler_.dispose) { 9783 this.sourceHandler_.dispose(); 9784 } 9785 this.sourceHandler_ = null; 9786 } 9787 }; 9788}; 9789 9790// The base Tech class needs to be registered as a Component. It is the only 9791// Tech that can be registered as a Component. 9792Component$1.registerComponent('Tech', Tech); 9793Tech.registerTech('Tech', Tech); 9794 9795/** 9796 * A list of techs that should be added to techOrder on Players 9797 * 9798 * @private 9799 */ 9800Tech.defaultTechOrder_ = []; 9801 9802/** 9803 * @file middleware.js 9804 * @module middleware 9805 */ 9806 9807/** @import Player from '../player' */ 9808/** @import Tech from '../tech/tech' */ 9809 9810const middlewares = {}; 9811const middlewareInstances = {}; 9812const TERMINATOR = {}; 9813 9814/** 9815 * A middleware object is a plain JavaScript object that has methods that 9816 * match the {@link Tech} methods found in the lists of allowed 9817 * {@link module:middleware.allowedGetters|getters}, 9818 * {@link module:middleware.allowedSetters|setters}, and 9819 * {@link module:middleware.allowedMediators|mediators}. 9820 * 9821 * @typedef {Object} MiddlewareObject 9822 */ 9823 9824/** 9825 * A middleware factory function that should return a 9826 * {@link module:middleware~MiddlewareObject|MiddlewareObject}. 9827 * 9828 * This factory will be called for each player when needed, with the player 9829 * passed in as an argument. 9830 * 9831 * @callback MiddlewareFactory 9832 * @param {Player} player 9833 * A Video.js player. 9834 */ 9835 9836/** 9837 * Define a middleware that the player should use by way of a factory function 9838 * that returns a middleware object. 9839 * 9840 * @param {string} type 9841 * The MIME type to match or `"*"` for all MIME types. 9842 * 9843 * @param {MiddlewareFactory} middleware 9844 * A middleware factory function that will be executed for 9845 * matching types. 9846 */ 9847function use(type, middleware) { 9848 middlewares[type] = middlewares[type] || []; 9849 middlewares[type].push(middleware); 9850} 9851 9852/** 9853 * Asynchronously sets a source using middleware by recursing through any 9854 * matching middlewares and calling `setSource` on each, passing along the 9855 * previous returned value each time. 9856 * 9857 * @param {Player} player 9858 * A {@link Player} instance. 9859 * 9860 * @param {Tech~SourceObject} src 9861 * A source object. 9862 * 9863 * @param {Function} 9864 * The next middleware to run. 9865 */ 9866function setSource(player, src, next) { 9867 player.setTimeout(() => setSourceHelper(src, middlewares[src.type], next, player), 1); 9868} 9869 9870/** 9871 * When the tech is set, passes the tech to each middleware's `setTech` method. 9872 * 9873 * @param {Object[]} middleware 9874 * An array of middleware instances. 9875 * 9876 * @param {Tech} tech 9877 * A Video.js tech. 9878 */ 9879function setTech(middleware, tech) { 9880 middleware.forEach(
9880(mw) => mw.setTech && mw.setTech(tech)); 9881} 9882 9883/** 9884 * Calls a getter on the tech first, through each middleware 9885 * from right to left to the player. 9886 * 9887 * @param {Object[]} middleware 9888 * An array of middleware instances. 9889 * 9890 * @param {Tech} tech 9891 * The current tech. 9892 * 9893 * @param {string} method 9894 * A method name. 9895 * 9896 * @return {*} 9897 * The final value from the tech after middleware has intercepted it. 9898 */ 9899function get(middleware, tech, method) { 9900 return middleware.reduceRight(middlewareIterator(method), tech[method]()); 9901} 9902 9903/** 9904 * Takes the argument given to the player and calls the setter method on each 9905 * middleware from left to right to the tech. 9906 * 9907 * @param {Object[]} middleware 9908 * An array of middleware instances. 9909 * 9910 * @param {Tech} tech 9911 * The current tech. 9912 * 9913 * @param {string} method 9914 * A method name. 9915 * 9916 * @param {*} arg 9917 * The value to set on the tech. 9918 * 9919 * @return {*} 9920 * The return value of the `method` of the `tech`. 9921 */ 9922function set(middleware, tech, method, arg) { 9923 return tech[method](middleware.reduce(middlewareIterator(method), arg)); 9924} 9925 9926/** 9927 * Takes the argument given to the player and calls the `call` version of the 9928 * method on each middleware from left to right. 9929 * 9930 * Then, call the passed in method on the tech and return the result unchanged 9931 * back to the player, through middleware, this time from right to left. 9932 * 9933 * @param {Object[]} middleware 9934 * An array of middleware instances. 9935 * 9936 * @param {Tech} tech 9937 * The current tech. 9938 * 9939 * @param {string} method
vendor: 7,929 bytes, lines 9940-10261
9940 * A method name. 9941 * 9942 * @param {*} arg 9943 * The value to set on the tech. 9944 * 9945 * @return {*} 9946 * The return value of the `method` of the `tech`, regardless of the 9947 * return values of middlewares. 9948 */ 9949function mediate(middleware, tech, method, arg = null) { 9950 const callMethod = 'call' + toTitleCase$1(method); 9951 const middlewareValue = middleware.reduce(middlewareIterator(callMethod), arg); 9952 const terminated = middlewareValue === TERMINATOR; 9953 // deprecated. The `null` return value should instead return TERMINATOR to 9954 // prevent confusion if a techs method actually returns null. 9955 const returnValue = terminated ? null : tech[method](middlewareValue); 9956 executeRight(middleware, method, returnValue, terminated); 9957 return returnValue; 9958} 9959 9960/** 9961 * Enumeration of allowed getters where the keys are method names. 9962 * 9963 * @type {Object} 9964 */ 9965const allowedGetters = { 9966 buffered: 1, 9967 currentTime: 1, 9968 duration: 1, 9969 muted: 1, 9970 played: 1, 9971 paused: 1, 9972 seekable: 1, 9973 volume: 1, 9974 ended: 1, 9975}; 9976 9977/** 9978 * Enumeration of allowed setters where the keys are method names. 9979 * 9980 * @type {Object} 9981 */ 9982const allowedSetters = { 9983 setCurrentTime: 1, 9984 setMuted: 1, 9985 setVolume: 1, 9986}; 9987 9988/** 9989 * Enumeration of allowed mediators where the keys are method names. 9990 * 9991 * @type {Object} 9992 */ 9993const allowedMediators = { 9994 play: 1, 9995 pause: 1, 9996}; 9997function middlewareIterator(method) { 9998 return (value, mw) => { 9999 // if the previous middleware terminated, pass along the termination 10000 if (value === TERMINATOR) { 10001 return TERMINATOR; 10002 } 10003 if (mw[method]) { 10004 return mw[method](value); 10005 } 10006 return value; 10007 }; 10008} 10009function executeRight(mws, method, value, terminated) { 10010 for (let i = mws.length - 1; i >= 0; i--) { 10011 const mw = mws[i]; 10012 if (mw[method]) { 10013 mw[method](terminated, value); 10014 } 10015 } 10016} 10017 10018/** 10019 * Clear the middleware cache for a player. 10020 * 10021 * @param {Player} player 10022 * A {@link Player} instance. 10023 */ 10024function clearCacheForPlayer(player) { 10025 if (middlewareInstances.hasOwnProperty(player.id())) { 10026 delete middlewareInstances[player.id()]; 10027 } 10028} 10029 10030/** 10031 * { 10032 * [playerId]: [[mwFactory, mwInstance], ...] 10033 * } 10034 * 10035 * @private 10036 */ 10037function getOrCreateFactory(player, mwFactory) { 10038 const mws = middlewareInstances[player.id()]; 10039 let mw = null; 10040 if (mws === undefined || mws === null) { 10041 mw = mwFactory(player); 10042 middlewareInstances[player.id()] = [[mwFactory, mw]]; 10043 return mw; 10044 } 10045 for (let i = 0; i < mws.length; i++) { 10046 const [mwf, mwi] = mws[i]; 10047 if (mwf !== mwFactory) { 10048 continue; 10049 } 10050 mw = mwi; 10051 } 10052 if (mw === null) { 10053 mw = mwFactory(player); 10054 mws.push([mwFactory, mw]); 10055 } 10056 return mw; 10057} 10058function setSourceHelper(src = {}, middleware = [], next, player, acc = [], lastRun = false) { 10059 const [mwFactory, ...mwrest] = middleware; 10060 10061 // if mwFactory is a string, then we're at a fork in the road 10062 if (typeof mwFactory === 'string') { 10063 setSourceHelper(src, middlewares[mwFactory], next, player, acc, lastRun); 10064 10065 // if we have an mwFactory, call it with the player to get the mw, 10066 // then call the mw's setSource method 10067 } else if (mwFactory) { 10068 const mw = getOrCreateFactory(player, mwFactory); 10069 10070 // if setSource isn't present, implicitly select this middleware 10071 if (!mw.setSource) { 10072 acc.push(mw); 10073 return setSourceHelper(src, mwrest, next, player, acc, lastRun); 10074 } 10075 mw.setSource(Object.assign({}, src), function (err, _src) { 10076 // something happened, try the next middleware on the current level 10077 // make sure to use the old src 10078 if (err) { 10079 return setSourceHelper(src, mwrest, next, player, acc, lastRun); 10080 } 10081 10082 // we've succeeded, now we need to go deeper 10083 acc.push(mw); 10084 10085 // if it's the same type, continue down the current chain 10086 // otherwise, we want to go down the new chain 10087 setSourceHelper( 10088 _src, 10089 src.type === _src.type ? mwrest : middlewares[_src.type], 10090 next, 10091 player, 10092 acc, 10093 lastRun 10094 ); 10095 }); 10096 } else if (mwrest.length) { 10097 setSourceHelper(src, mwrest, next, player, acc, lastRun); 10098 } else if (lastRun) { 10099 next(src, acc); 10100 } else { 10101 setSourceHelper(src, middlewares['*'], next, player, acc, true); 10102 } 10103} 10104 10105/** @import Player from '../player' */ 10106 10107/** 10108 * Mimetypes 10109 * 10110 * @see https://www.iana.org/assignments/media-types/media-types.xhtml 10111 * @typedef Mimetypes~Kind 10112 * @enum 10113 */ 10114const MimetypesKind = { 10115 opus: 'video/ogg', 10116 ogv: 'video/ogg', 10117 mp4: 'video/mp4', 10118 mov: 'video/mp4', 10119 m4v: 'video/mp4', 10120 mkv: 'video/x-matroska', 10121 m4a: 'audio/mp4', 10122 mp3: 'audio/mpeg', 10123 aac: 'audio/aac', 10124 caf: 'audio/x-caf', 10125 flac: 'audio/flac', 10126 oga: 'audio/ogg', 10127 wav: 'audio/wav', 10128 m3u8: 'application/x-mpegURL', 10129 mpd: 'application/dash+xml', 10130 jpg: 'image/jpeg', 10131 jpeg: 'image/jpeg', 10132 gif: 'image/gif', 10133 png: 'image/png', 10134 svg: 'image/svg+xml', 10135 webp: 'image/webp', 10136}; 10137 10138/** 10139 * Get the mimetype of a given src url if possible 10140 * 10141 * @param {string} src 10142 * The url to the src 10143 * 10144 * @return {string} 10145 * return the mimetype if it was known or empty string otherwise 10146 */ 10147const getMimetype = function (src = '') { 10148 const ext = getFileExtension(src); 10149 const mimetype = MimetypesKind[ext.toLowerCase()]; 10150 return mimetype || ''; 10151}; 10152 10153/** 10154 * Find the mime type of a given source string if possible. Uses the player 10155 * source cache. 10156 * 10157 * @param {Player} player 10158 * The player object 10159 * 10160 * @param {string} src 10161 * The source string 10162 * 10163 * @return {string} 10164 * The type that was found 10165 */ 10166const findMimetype = (player, src) => { 10167 if (!src) { 10168 return ''; 10169 } 10170 10171 // 1. check for the type in the `source` cache 10172 if (player.cache_.source.src === src && player.cache_.source.type) { 10173 return player.cache_.source.type; 10174 } 10175 10176 // 2. see if we have this source in our `currentSources` cache 10177 const matchingSources = player.cache_.sources.filter((s) => s.src === src); 10178 if (matchingSources.length) { 10179 return matchingSources[0].type; 10180 } 10181 10182 // 3. look for the src url in source elements and use the type there 10183 const sources = player.$$('source'); 10184 for (let i = 0; i < sources.length; i++) { 10185 const s = sources[i]; 10186 if (s.type && s.src && s.src === src) { 10187 return s.type; 10188 } 10189 } 10190 10191 // 4. finally fallback to our list of mime types based on src url extension 10192 return getMimetype(src); 10193}; 10194 10195/** 10196 * @module filter-source 10197 */ 10198 10199/** 10200 * Filter out single bad source objects or multiple source objects in an 10201 * array. Also flattens nested source object arrays into a 1 dimensional 10202 * array of source objects. 10203 * 10204 * @param {Tech~SourceObject|Tech~SourceObject[]} src 10205 * The src object to filter 10206 * 10207 * @return {Tech~SourceObject[]} 10208 * An array of sourceobjects containing only valid sources 10209 * 10210 * @private 10211 */ 10212const filterSource = function (src) { 10213 // traverse array 10214 if (Array.isArray(src)) { 10215 let newsrc = []; 10216 src.forEach(function (srcobj) { 10217 srcobj = filterSource(srcobj); 10218 if (Array.isArray(srcobj)) { 10219 newsrc = newsrc.concat(srcobj); 10220 } else if (isObject(srcobj)) { 10221 newsrc.push(srcobj); 10222 } 10223 }); 10224 src = newsrc; 10225 } else if (typeof src === 'string' && src.trim()) { 10226 // convert string into object 10227 src = [ 10228 fixSource({ 10229 src, 10230 }), 10231 ]; 10232 } else if (isObject(src) && typeof src.src === 'string' && src.src && src.src.trim()) { 10233 // src is already valid 10234 src = [fixSource(src)]; 10235 } else { 10236 // invalid source, turn it into an empty array 10237 src = []; 10238 } 10239 return src; 10240}; 10241 10242/** 10243 * Checks src mimetype, adding it when possible 10244 * 10245 * @param {Tech~SourceObject} src 10246 * The src object to check 10247 * @return {Tech~SourceObject} 10248 * src Object with known type 10249 */ 10250function fixSource(src) { 10251 if (!src.type) { 10252 const mimetype = getMimetype(src.src); 10253 if (mimetype) { 10254 src.type = mimetype; 10255 } 10256 } 10257 return src; 10258} 10259 10260var icons = 10261
vendor: 17,747 bytes, line 10261
10261'<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>'
10261; 10262 10263// /** 10264 10265// Determine the keycode for the 'back' key based on the platform 10266const backKeyCode = IS_TIZEN ? 10009 : IS_WEBOS ? 461 : 8; 10267const SpatialNavKeyCodes = { 10268 codes: { 10269 play: 415, 10270 pause: 19, 10271 ff: 417, 10272 rw: 412, 10273 back: backKeyCode, 10274 }, 10275 names: { 10276 415: 'play', 10277 19: 'pause', 10278 417: 'ff', 10279 412: 'rw', 10280 [backKeyCode]: 'back', 10281 }, 10282 isEventKey(event, keyName) { 10283 keyName = keyName.toLowerCase(); 10284 if (this.names[event.keyCode] && this.names[event.keyCode] === keyName) { 10285 return true; 10286 } 10287 return false; 10288 }, 10289 getEventName(event) { 10290 if (this.names[event.keyCode]) { 10291 return this.names[event.keyCode]; 10292 } else if (this.codes[event.code]) { 10293 const code = this.codes[event.code]; 10294 return this.names[code]; 10295 } 10296 return null; 10297 }, 10298}; 10299 10300/** 10301 * @file spatial-navigation.js 10302 */ 10303 10304/** @import Component from './component' */ 10305/** @import Player from './player' */ 10306 10307// The number of seconds the `step*` functions move the timeline. 10308const STEP_SECONDS$1 = 5; 10309 10310/** 10311 * Spatial Navigation in Video.js enhances user experience and accessibility on smartTV devices, 10312 * enabling seamless navigation through interactive elements within the player using remote control arrow keys. 10313 * This functionality allows users to effortlessly navigate through focusable components. 10314 *
vendor: 11,167 bytes, lines 10315-10639
10315 * @extends EventTarget 10316 */ 10317class SpatialNavigation extends EventTarget$2 { 10318 /** 10319 * Constructs a SpatialNavigation instance with initial settings. 10320 * Sets up the player instance, and prepares the spatial navigation system. 10321 * 10322 * @class 10323 * @param {Player} player - The Video.js player instance to which the spatial navigation is attached. 10324 */ 10325 constructor(player) { 10326 super(); 10327 this.player_ = player; 10328 this.focusableComponents = []; 10329 this.isListening_ = false; 10330 this.isPaused_ = false; 10331 this.onKeyDown_ = this.onKeyDown_.bind(this); 10332 this.lastFocusedComponent_ = null; 10333 } 10334 10335 /** 10336 * Starts the spatial navigation by adding a keydown event listener to the video container. 10337 * This method ensures that the event listener is added only once. 10338 */ 10339 start() { 10340 // If the listener is already active, exit early. 10341 if (this.isListening_) { 10342 return; 10343 } 10344 10345 // Add the event listener since the listener is not yet active. 10346 this.player_.on('keydown', this.onKeyDown_); 10347 this.player_.on('modalKeydown', this.onKeyDown_); 10348 // Listen for source change events 10349 this.player_.on('loadedmetadata', () => { 10350 this.focus(this.updateFocusableComponents()[0]); 10351 }); 10352 this.player_.on('modalclose', () => { 10353 this.refocusComponent(); 10354 }); 10355 this.player_.on('focusin', this.handlePlayerFocus_.bind(this)); 10356 this.player_.on('focusout', this.handlePlayerBlur_.bind(this)); 10357 this.isListening_ = true; 10358 if (this.player_.errorDisplay) { 10359 this.player_.errorDisplay.on('aftermodalfill', () => { 10360 this.updateFocusableComponents(); 10361 if (this.focusableComponents.length) { 10362 // The modal has focusable components: 10363 10364 if (this.focusableComponents.length > 1) { 10365 // The modal has close button + some additional buttons. 10366 // Focusing first additional button: 10367 10368 this.focusableComponents[1].focus(); 10369 } else { 10370 // The modal has only close button, 10371 // Focusing it: 10372 10373 this.focusableComponents[0].focus(); 10374 } 10375 } 10376 }); 10377 } 10378 } 10379 10380 /** 10381 * Stops the spatial navigation by removing the keydown event listener from the video container. 10382 * Also sets the `isListening_` flag to false. 10383 */ 10384 stop() { 10385 this.player_.off('keydown', this.onKeyDown_); 10386 this.isListening_ = false; 10387 } 10388 10389 /** 10390 * Responds to keydown events for spatial navigation and media control. 10391 * 10392 * Determines if spatial navigation or media control is active and handles key inputs accordingly. 10393 * 10394 * @param {KeyboardEvent} event - The keydown event to be handled. 10395 */ 10396 onKeyDown_(event) { 10397 // Determine if the event is a custom modalKeydown event 10398 const actualEvent = event.originalEvent ? event.originalEvent : event; 10399 if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(actualEvent.key)) { 10400 // Handle directional navigation 10401 if (this.isPaused_) { 10402 return; 10403 } 10404 actualEvent.preventDefault(); 10405 10406 // "ArrowLeft" => "left" etc 10407 const direction = actualEvent.key.substring(5).toLowerCase(); 10408 this.move(direction); 10409 } else if ( 10410 SpatialNavKeyCodes.isEventKey(actualEvent, 'play') || 10411 SpatialNavKeyCodes.isEventKey(actualEvent, 'pause') || 10412 SpatialNavKeyCodes.isEventKey(actualEvent, 'ff') || 10413 SpatialNavKeyCodes.isEventKey(actualEvent, 'rw') 10414 ) { 10415 // Handle media actions 10416 actualEvent.preventDefault(); 10417 const action = SpatialNavKeyCodes.getEventName(actualEvent); 10418 this.performMediaAction_(action); 10419 } else if ( 10420 SpatialNavKeyCodes.isEventKey(actualEvent, 'Back') && 10421 event.target && 10422 typeof event.target.closeable === 'function' && 10423 event.target.closeable() 10424 ) { 10425 actualEvent.preventDefault(); 10426 event.target.close(); 10427 } 10428 } 10429 10430 /** 10431 * Performs media control actions based on the given key input. 10432 * 10433 * Controls the playback and seeking functionalities of the media player. 10434 * 10435 * @param {string} key - The key representing the media action to be performed. 10436 * Accepted keys: 'play', 'pause', 'ff' (fast-forward), 'rw' (rewind). 10437 */ 10438 performMediaAction_(key) { 10439 if (this.player_) { 10440 switch (key) { 10441 case 'play': 10442 if (this.player_.paused()) { 10443 this.player_.play(); 10444 } 10445 break; 10446 case 'pause': 10447 if (!this.player_.paused()) { 10448 this.player_.pause(); 10449 } 10450 break; 10451 case 'ff': 10452 this.userSeek_(this.player_.currentTime() + STEP_SECONDS$1); 10453 break; 10454 case 'rw': 10455 this.userSeek_(this.player_.currentTime() - STEP_SECONDS$1); 10456 break; 10457 } 10458 } 10459 } 10460 10461 /** 10462 * Prevent liveThreshold from causing seeks to seem like they 10463 * are not happening from a user perspective. 10464 * 10465 * @param {number} ct 10466 * current time to seek to 10467 */ 10468 userSeek_(ct) { 10469 if (this.player_.liveTracker && this.player_.liveTracker.isLive()) { 10470 this.player_.liveTracker.nextSeekedFromUser(); 10471 } 10472 this.player_.currentTime(ct); 10473 } 10474 10475 /** 10476 * Pauses the spatial navigation functionality. 10477 * This method sets a flag that can be used to temporarily disable the navigation logic. 10478 */ 10479 pause() { 10480 this.isPaused_ = true; 10481 } 10482 10483 /** 10484 * Resumes the spatial navigation functionality if it has been paused. 10485 * This method resets the pause flag, re-enabling the navigation logic. 10486 */ 10487 resume() { 10488 this.isPaused_ = false; 10489 } 10490 10491 /** 10492 * Handles Player Blur. 10493 * 10494 * @param {string|Event|Object} event 10495 * The name of the event, an `Event`, or an object with a key of type set to 10496 * an event name. 10497 * 10498 * Calls for handling of the Player Blur if: 10499 * *The next focused element is not a child of current focused element & 10500 * The next focused element is not a child of the Player. 10501 * *There is no next focused element 10502 */ 10503 handlePlayerBlur_(event) { 10504 const nextFocusedElement = event.relatedTarget; 10505 let isChildrenOfPlayer = null; 10506 const currentComponent = this.getCurrentComponent(event.target); 10507 if (nextFocusedElement) { 10508 isChildrenOfPlayer = Boolean(nextFocusedElement.closest('.video-js')); 10509 10510 // If nextFocusedElement is the 'TextTrackSettings' component 10511 if (nextFocusedElement.classList.contains('vjs-text-track-settings') && !this.isPaused_) { 10512 this.searchForTrackSelect_(); 10513 } 10514 } 10515 if ( 10516 (!event.currentTarget.contains(event.relatedTarget) && !isChildrenOfPlayer) || 10517 !nextFocusedElement 10518 ) { 10519 if (currentComponent && currentComponent.name() === 'CloseButton') { 10520 this.refocusComponent(); 10521 } else { 10522 this.pause(); 10523 if (currentComponent && currentComponent.el()) { 10524 // Store last focused component 10525 this.lastFocusedComponent_ = currentComponent; 10526 } 10527 } 10528 } 10529 } 10530 10531 /** 10532 * Handles the Player focus event. 10533 * 10534 * Calls for handling of the Player Focus if current element is focusable. 10535 */ 10536 handlePlayerFocus_() { 10537 if (this.getCurrentComponent() && this.getCurrentComponent().getIsFocusable()) { 10538 this.resume(); 10539 } 10540 } 10541 10542 /** 10543 * Gets a set of focusable components. 10544 * 10545 * @return {Array} 10546 * Returns an array of focusable components. 10547 */ 10548 updateFocusableComponents() { 10549 const player = this.player_; 10550 const focusableComponents = []; 10551 10552 /** 10553 * Searches for children candidates. 10554 * 10555 * Pushes Components to array of 'focusableComponents'. 10556 * Calls itself if there is children elements inside iterated component. 10557 * 10558 * @param {Array} componentsArray - The array of components to search for focusable children. 10559 */ 10560 function searchForChildrenCandidates(componentsArray) { 10561 for (const i of componentsArray) { 10562 if (i.hasOwnProperty('el_') && i.getIsFocusable() && i.getIsAvailableToBeFocused(i.el())) { 10563 focusableComponents.push(i); 10564 } 10565 if (i.hasOwnProperty('children_') && i.children_.length > 0) { 10566 searchForChildrenCandidates(i.children_); 10567 } 10568 } 10569 } 10570 10571 // Iterate inside all children components of the player. 10572 player.children_.forEach((value) => { 10573 if (value.hasOwnProperty('el_')) { 10574 // If component has required functions 'getIsFocusable' & 'getIsAvailableToBeFocused', is focusable & available to be focused. 10575 if ( 10576 value.getIsFocusable && 10577 value.getIsAvailableToBeFocused && 10578 value.getIsFocusable() && 10579 value.getIsAvailableToBeFocused(value.el()) 10580 ) { 10581 focusableComponents.push(value); 10582 return; 10583 // If component has posible children components as candidates. 10584 } else if (value.hasOwnProperty('children_') && value.children_.length > 0) { 10585 searchForChildrenCandidates(value.children_); 10586 // If component has posible item components as candidates. 10587 } else if (value.hasOwnProperty('items') && value.items.length > 0) { 10588 searchForChildrenCandidates(value.items); 10589 // If there is a suitable child element within the component's DOM element. 10590 } else if (this.findSuitableDOMChild(value)) { 10591 focusableComponents.push(value); 10592 } 10593 } 10594 10595 // TODO - Refactor the following logic after refactor of videojs-errors elements to be components is done. 10596 if (value.name_ === 'ErrorDisplay' && value.opened_) { 10597 const buttonContainer = value.el_.querySelector('.vjs-errors-ok-button-container'); 10598 if (buttonContainer) { 10599 const modalButtons = buttonContainer.querySelectorAll('button'); 10600 modalButtons.forEach((element, index) => { 10601 // Add elements as objects to be handled by the spatial navigation 10602 focusableComponents.push({ 10603 name: () => { 10604 return 'ModalButton' + (index + 1); 10605 }, 10606 el: () => element, 10607 getPositions: () => { 10608 const rect = element.getBoundingClientRect(); 10609 10610 // Creating objects that mirror DOMRectReadOnly for boundingClientRect and center 10611 const boundingClientRect = { 10612 x: rect.x, 10613 y: rect.y, 10614 width: rect.width, 10615 height: rect.height, 10616 top: rect.top, 10617 right: rect.right, 10618 bottom: rect.bottom, 10619 left: rect.left, 10620 }; 10621 10622 // Calculating the center position 10623 const center = { 10624 x: rect.left + rect.width / 2, 10625 y: rect.top + rect.height / 2, 10626 width: 0, 10627 height: 0, 10628 top: rect.top + rect.height / 2, 10629 right: rect.left + rect.width / 2, 10630 bottom: rect.top + rect.height / 2, 10631 left: rect.left + rect.width / 2, 10632 }; 10633 return { 10634 boundingClientRect, 10635 center, 10636 }; 10637 }, 10638 // Asume that the following are always focusable 10639 getIsAvailableToBeFocused: () => true,
vendor: 6,854 bytes, lines 10640-10833
10640 getIsFocusable: (el) => true, 10641 focus: () => element.focus(), 10642 }); 10643 }); 10644 } 10645 } 10646 }); 10647 this.focusableComponents = focusableComponents; 10648 return this.focusableComponents; 10649 } 10650 10651 /** 10652 * Finds a suitable child element within the provided component's DOM element. 10653 * 10654 * @param {Object} component - The component containing the DOM element to search within. 10655 * @return {HTMLElement|null} Returns the suitable child element if found, or null if not found. 10656 */ 10657 findSuitableDOMChild(component) { 10658 /** 10659 * Recursively searches for a suitable child node that can be focused within a given component. 10660 * It first checks if the provided node itself can be focused according to the component's 10661 * `getIsFocusable` and `getIsAvailableToBeFocused` methods. If not, it recursively searches 10662 * through the node's children to find a suitable child node that meets the focusability criteria. 10663 * 10664 * @param {HTMLElement} node - The DOM node to start the search from. 10665 * @return {HTMLElement|null} The first child node that is focusable and available to be focused, 10666 * or `null` if no suitable child is found. 10667 */ 10668 function searchForSuitableChild(node) { 10669 if (component.getIsFocusable(node) && component.getIsAvailableToBeFocused(node)) { 10670 return node; 10671 } 10672 for (let i = 0; i < node.children.length; i++) { 10673 const child = node.children[i]; 10674 const suitableChild = searchForSuitableChild(child); 10675 if (suitableChild) { 10676 return suitableChild; 10677 } 10678 } 10679 return null; 10680 } 10681 if (component.el()) { 10682 return searchForSuitableChild(component.el()); 10683 } 10684 return null; 10685 } 10686 10687 /** 10688 * Gets the currently focused component from the list of focusable components. 10689 * If a target element is provided, it uses that element to find the corresponding 10690 * component. If no target is provided, it defaults to using the document's currently 10691 * active element. 10692 * 10693 * @param {HTMLElement} [target] - The DOM element to check against the focusable components. 10694 * If not provided, `document.activeElement` is used. 10695 * @return {Component|null} - Returns the focused component if found among the focusable components, 10696 * otherwise returns null if no matching component is found. 10697 */ 10698 getCurrentComponent(target) { 10699 this.updateFocusableComponents(); 10700 // eslint-disable-next-line 10701 const curComp = target || document.activeElement; 10702 if (this.focusableComponents.length) { 10703 for (const i of this.focusableComponents) { 10704 // If component Node is equal to the current active element. 10705 if (i.el() === curComp) { 10706 return i; 10707 } 10708 } 10709 } 10710 } 10711 10712 /** 10713 * Adds a component to the array of focusable components. 10714 * 10715 * @param {Component} component 10716 * The `Component` to be added. 10717 */ 10718 add(component) { 10719 const focusableComponents = [...this.focusableComponents]; 10720 if ( 10721 component.hasOwnProperty('el_') && 10722 component.getIsFocusable() && 10723 component.getIsAvailableToBeFocused(component.el()) 10724 ) { 10725 focusableComponents.push(component); 10726 } 10727 this.focusableComponents = focusableComponents; 10728 // Trigger the notification manually 10729 this.trigger({ 10730 type: 'focusableComponentsChanged', 10731 focusableComponents: this.focusableComponents, 10732 }); 10733 } 10734 10735 /** 10736 * Removes component from the array of focusable components. 10737 * 10738 * @param {Component} component - The component to be removed from the focusable components array. 10739 */ 10740 remove(component) { 10741 for (let i = 0; i < this.focusableComponents.length; i++) { 10742 if (this.focusableComponents[i].name() === component.name()) { 10743 this.focusableComponents.splice(i, 1); 10744 // Trigger the notification manually 10745 this.trigger({ 10746 type: 'focusableComponentsChanged', 10747 focusableComponents: this.focusableComponents, 10748 }); 10749 return; 10750 } 10751 } 10752 } 10753 10754 /** 10755 * Clears array of focusable components. 10756 */ 10757 clear() { 10758 // Check if the array is already empty to avoid unnecessary event triggering 10759 if (this.focusableComponents.length > 0) { 10760 // Clear the array 10761 this.focusableComponents = []; 10762 10763 // Trigger the notification manually 10764 this.trigger({ 10765 type: 'focusableComponentsChanged', 10766 focusableComponents: this.focusableComponents, 10767 }); 10768 } 10769 } 10770 10771 /** 10772 * Navigates to the next focusable component based on the specified direction. 10773 * 10774 * @param {string} direction 'up', 'down', 'left', 'right' 10775 */ 10776 move(direction) { 10777 const currentFocusedComponent = this.getCurrentComponent(); 10778 if (!currentFocusedComponent) { 10779 return; 10780 } 10781 const currentPositions = currentFocusedComponent.getPositions(); 10782 const candidates = this.focusableComponents.filter( 10783 (component) => 10784 component !== currentFocusedComponent && 10785 this.isInDirection_( 10786 currentPositions.boundingClientRect, 10787 component.getPositions().boundingClientRect, 10788 direction 10789 ) 10790 ); 10791 const bestCandidate = this.findBestCandidate_(currentPositions.center, candidates, direction); 10792 if (bestCandidate) { 10793 this.focus(bestCandidate); 10794 } else { 10795 this.trigger({ 10796 type: 'endOfFocusableComponents', 10797 direction, 10798 focusedComponent: currentFocusedComponent, 10799 }); 10800 } 10801 } 10802 10803 /** 10804 * Finds the best candidate on the current center position, 10805 * the list of candidates, and the specified navigation direction. 10806 * 10807 * @param {Object} currentCenter The center position of the current focused component element. 10808 * @param {Array} candidates An array of candidate components to receive focus. 10809 * @param {string} direction The direction of navigation ('up', 'down', 'left', 'right'). 10810 * @return {Object|null} The component that is the best candidate for receiving focus. 10811 */ 10812 findBestCandidate_(currentCenter, candidates, direction) { 10813 let minDistance = Infinity; 10814 let bestCandidate = null; 10815 for (const candidate of candidates) { 10816 const candidateCenter = candidate.getPositions().center; 10817 const distance = this.calculateDistance_(currentCenter, candidateCenter, direction); 10818 if (distance < minDistance) { 10819 minDistance = distance; 10820 bestCandidate = candidate; 10821 } 10822 } 10823 return bestCandidate; 10824 } 10825 10826 /** 10827 * Determines if a target rectangle is in the specified navigation direction 10828 * relative to a source rectangle. 10829 * 10830 * @param {Object} srcRect The bounding rectangle of the source element. 10831 * @param {Object} targetRect The bounding rectangle of the target element. 10832 * @param {string} direction The navigation direction ('up', 'down', 'left', 'right'). 10833 * @return {boolean}
vendor: 10,417 bytes, lines 10833-11199
10833 True if the target is in the specified direction relative to the source. 10834 */ 10835 isInDirection_(srcRect, targetRect, direction) { 10836 switch (direction) { 10837 case 'right': 10838 return targetRect.left >= srcRect.right; 10839 case 'left': 10840 return targetRect.right <= srcRect.left; 10841 case 'down': 10842 return targetRect.top >= srcRect.bottom; 10843 case 'up': 10844 return targetRect.bottom <= srcRect.top; 10845 default: 10846 return false; 10847 } 10848 } 10849 10850 /** 10851 * Focus the last focused component saved before blur on player. 10852 */ 10853 refocusComponent() { 10854 if (this.lastFocusedComponent_) { 10855 // If user is not active, set it to active. 10856 if (!this.player_.userActive()) { 10857 this.player_.userActive(true); 10858 } 10859 this.updateFocusableComponents(); 10860 10861 // Search inside array of 'focusableComponents' for a match of name of 10862 // the last focused component. 10863 for (let i = 0; i < this.focusableComponents.length; i++) { 10864 if (this.focusableComponents[i].name() === this.lastFocusedComponent_.name()) { 10865 this.focus(this.focusableComponents[i]); 10866 return; 10867 } 10868 } 10869 } else { 10870 this.focus(this.updateFocusableComponents()[0]); 10871 } 10872 } 10873 10874 /** 10875 * Focuses on a given component. 10876 * If the component is available to be focused, it focuses on the component. 10877 * If not, it attempts to find a suitable DOM child within the component and focuses on it. 10878 * 10879 * @param {Component} component - The component to be focused. 10880 */ 10881 focus(component) { 10882 if (typeof component !== 'object') { 10883 return; 10884 } 10885 if (component.getIsAvailableToBeFocused(component.el())) { 10886 component.focus(); 10887 } else if (this.findSuitableDOMChild(component)) { 10888 this.findSuitableDOMChild(component).focus(); 10889 } 10890 } 10891 10892 /** 10893 * Calculates the distance between two points, adjusting the calculation based on 10894 * the specified navigation direction. 10895 * 10896 * @param {Object} center1 The center point of the first element. 10897 * @param {Object} center2 The center point of the second element. 10898 * @param {string} direction The direction of navigation ('up', 'down', 'left', 'right'). 10899 * @return {number} The calculated distance between the two centers. 10900 */ 10901 calculateDistance_(center1, center2, direction) { 10902 const dx = Math.abs(center1.x - center2.x); 10903 const dy = Math.abs(center1.y - center2.y); 10904 let distance; 10905 switch (direction) { 10906 case 'right': 10907 case 'left': 10908 // Higher weight for vertical distance in horizontal navigation. 10909 distance = dx + dy * 100; 10910 break; 10911 case 'up': 10912 // Strongly prioritize vertical proximity for UP navigation. 10913 // Adjust the weight to ensure that elements directly above are favored. 10914 distance = dy * 2 + dx * 0.5; 10915 break; 10916 case 'down': 10917 // More balanced weight for vertical and horizontal distances. 10918 // Adjust the weights here to find the best balance. 10919 distance = dy * 5 + dx; 10920 break; 10921 default: 10922 distance = dx + dy; 10923 } 10924 return distance; 10925 } 10926 10927 /** 10928 * This gets called by 'handlePlayerBlur_' if 'spatialNavigation' is enabled. 10929 * Searches for the first 'TextTrackSelect' inside of modal to focus. 10930 * 10931 * @private 10932 */ 10933 searchForTrackSelect_() { 10934 const spatialNavigation = this; 10935 for (const component of spatialNavigation.updateFocusableComponents()) { 10936 if (component.constructor.name === 'TextTrackSelect') { 10937 spatialNavigation.focus(component); 10938 break; 10939 } 10940 } 10941 } 10942} 10943 10944/** 10945 * @file loader.js 10946 */ 10947 10948/** @import Player from '../player' */ 10949 10950/** 10951 * The `MediaLoader` is the `Component` that decides which playback technology to load 10952 * when a player is initialized. 10953 * 10954 * @extends Component 10955 */ 10956class MediaLoader extends Component$1 { 10957 /** 10958 * Create an instance of this class. 10959 * 10960 * @param {Player} player 10961 * The `Player` that this class should attach to. 10962 * 10963 * @param {Object} [options] 10964 * The key/value store of player options. 10965 * 10966 * @param {Function} [ready] 10967 * The function that is run when this component is ready. 10968 */ 10969 constructor(player, options, ready) { 10970 // MediaLoader has no element 10971 const options_ = merge$1( 10972 { 10973 createEl: false, 10974 }, 10975 options 10976 ); 10977 super(player, options_, ready); 10978 10979 // If there are no sources when the player is initialized, 10980 // load the first supported playback technology. 10981 10982 if (!options.playerOptions.sources || options.playerOptions.sources.length === 0) { 10983 for (let i = 0, j = options.playerOptions.techOrder; i < j.length; i++) { 10984 const techName = toTitleCase$1(j[i]); 10985 let tech = Tech.getTech(techName); 10986 10987 // Support old behavior of techs being registered as components. 10988 // Remove once that deprecated behavior is removed. 10989 if (!techName) { 10990 tech = Component$1.getComponent(techName); 10991 } 10992 10993 // Check if the browser supports this technology 10994 if (tech && tech.isSupported()) { 10995 player.loadTech_(techName); 10996 break; 10997 } 10998 } 10999 } else { 11000 // Loop through playback technologies (e.g. HTML5) and check for support. 11001 // Then load the best source. 11002 // A few assumptions here: 11003 // All playback technologies respect preload false. 11004 player.src(options.playerOptions.sources); 11005 } 11006 } 11007} 11008Component$1.registerComponent('MediaLoader', MediaLoader); 11009 11010/** 11011 * @file clickable-component.js 11012 */ 11013 11014/** @import Player from './player' */ 11015 11016/** 11017 * Component which is clickable or keyboard actionable, but is not a 11018 * native HTML button. 11019 * 11020 * @extends Component 11021 */ 11022class ClickableComponent extends Component$1 { 11023 /** 11024 * Creates an instance of this class. 11025 * 11026 * @param {Player} player 11027 * The `Player` that this class should be attached to. 11028 * 11029 * @param {Object} [options] 11030 * The key/value store of component options. 11031 * 11032 * @param {function} [options.clickHandler] 11033 * The function to call when the button is clicked / activated 11034 * 11035 * @param {string} [options.controlText] 11036 * The text to set on the button 11037 * 11038 * @param {string} [options.className] 11039 * A class or space separated list of classes to add the component 11040 * 11041 */ 11042 constructor(player, options) { 11043 super(player, options); 11044 if (this.options_.controlText) { 11045 this.controlText(this.options_.controlText); 11046 } 11047 this.handleMouseOver_ = (e) => this.handleMouseOver(e); 11048 this.handleMouseOut_ = (e) => this.handleMouseOut(e); 11049 this.handleClick_ = (e) => this.handleClick(e); 11050 this.handleKeyDown_ = (e) => this.handleKeyDown(e); 11051 this.emitTapEvents(); 11052 this.enable(); 11053 } 11054 11055 /** 11056 * Create the `ClickableComponent`s DOM element. 11057 * 11058 * @param {string} [tag=div] 11059 * The element's node type. 11060 * 11061 * @param {Object} [props={}] 11062 * An object of properties that should be set on the element. 11063 * 11064 * @param {Object} [attributes={}] 11065 * An object of attributes that should be set on the element. 11066 * 11067 * @return {Element} 11068 * The element that gets created. 11069 */ 11070 createEl(tag = 'div', props = {}, attributes = {}) { 11071 props = Object.assign( 11072 { 11073 className: this.buildCSSClass(), 11074 tabIndex: 0, 11075 }, 11076 props 11077 ); 11078 if (tag === 'button') { 11079 log$1.error( 11080 `Creating a ClickableComponent with an HTML element of ${tag} is not supported; use a Button instead.` 11081 ); 11082 } 11083 11084 // Add ARIA attributes for clickable element which is not a native HTML button 11085 attributes = Object.assign( 11086 { 11087 role: 'button', 11088 }, 11089 attributes 11090 ); 11091 this.tabIndex_ = props.tabIndex; 11092 const el = createEl(tag, props, attributes); 11093 if (!this.player_.options_.experimentalSvgIcons) { 11094 el.appendChild( 11095 createEl( 11096 'span', 11097 { 11098 className: 'vjs-icon-placeholder', 11099 }, 11100 { 11101 'aria-hidden': true, 11102 } 11103 ) 11104 ); 11105 } 11106 this.createControlTextEl(el); 11107 return el; 11108 } 11109 dispose() { 11110 // remove controlTextEl_ on dispose 11111 this.controlTextEl_ = null; 11112 super.dispose(); 11113 } 11114 11115 /** 11116 * Create a control text element on this `ClickableComponent` 11117 * 11118 * @param {Element} [el] 11119 * Parent element for the control text. 11120 * 11121 * @return {Element} 11122 * The control text element that gets created. 11123 */ 11124 createControlTextEl(el) { 11125 this.controlTextEl_ = createEl( 11126 'span', 11127 { 11128 className: 'vjs-control-text', 11129 }, 11130 { 11131 // let the screen reader user know that the text of the element may change 11132 'aria-live': 'polite', 11133 } 11134 ); 11135 if (el) { 11136 el.appendChild(this.controlTextEl_); 11137 } 11138 this.controlText(this.controlText_, el); 11139 return this.controlTextEl_; 11140 } 11141 11142 /** 11143 * Get or set the localize text to use for the controls on the `ClickableComponent`. 11144 * 11145 * @param {string} [text] 11146 * Control text for element. 11147 * 11148 * @param {Element} [el=this.el()] 11149 * Element to set the title on. 11150 * 11151 * @return {string} 11152 * - The control text when getting 11153 */ 11154 controlText(text, el = this.el()) { 11155 if (text === undefined) { 11156 return this.controlText_ || 'Need Text'; 11157 } 11158 const localizedText = this.localize(text); 11159 11160 /** @protected */ 11161 this.controlText_ = text; 11162 textContent(this.controlTextEl_, localizedText); 11163 if (!this.nonIconControl && !this.player_.options_.noUITitleAttributes) { 11164 // Set title attribute if only an icon is shown 11165 el.setAttribute('title', localizedText); 11166 } 11167 } 11168 11169 /** 11170 * Builds the default DOM `className`. 11171 * 11172 * @return {string} 11173 * The DOM `className` for this object. 11174 */ 11175 buildCSSClass() { 11176 return `vjs-control vjs-button ${super.buildCSSClass()}`; 11177 } 11178 11179 /** 11180 * Enable this `ClickableComponent` 11181 */ 11182 enable() { 11183 if (!this.enabled_) { 11184 this.enabled_ = true; 11185 this.removeClass('vjs-disabled'); 11186 this.el_.setAttribute('aria-disabled', 'false'); 11187 if (typeof this.tabIndex_ !== 'undefined') { 11188 this.el_.setAttribute('tabIndex', this.tabIndex_); 11189 } 11190 this.on(['tap', 'click'], this.handleClick_); 11191 this.on('keydown', this.handleKeyDown_); 11192 } 11193 } 11194 11195 /** 11196 * Disable this `ClickableComponent` 11197 */ 11198 disable() { 11199 this.enabled_ = false;
vendor: 9,188 bytes, lines 11200-11537
11200 this.addClass('vjs-disabled'); 11201 this.el_.setAttribute('aria-disabled', 'true'); 11202 if (typeof this.tabIndex_ !== 'undefined') { 11203 this.el_.removeAttribute('tabIndex'); 11204 } 11205 this.off('mouseover', this.handleMouseOver_); 11206 this.off('mouseout', this.handleMouseOut_); 11207 this.off(['tap', 'click'], this.handleClick_); 11208 this.off('keydown', this.handleKeyDown_); 11209 } 11210 11211 /** 11212 * Handles language change in ClickableComponent for the player in components 11213 * 11214 * 11215 */ 11216 handleLanguagechange() { 11217 this.controlText(this.controlText_); 11218 } 11219 11220 /** 11221 * Event handler that is called when a `ClickableComponent` receives a 11222 * `click` or `tap` event. 11223 * 11224 * @param {Event} event 11225 * The `tap` or `click` event that caused this function to be called. 11226 * 11227 * @listens tap 11228 * @listens click 11229 * @abstract 11230 */ 11231 handleClick(event) { 11232 if (this.options_.clickHandler) { 11233 this.options_.clickHandler.call(this, arguments); 11234 } 11235 } 11236 11237 /** 11238 * Event handler that is called when a `ClickableComponent` receives a 11239 * `keydown` event. 11240 * 11241 * By default, if the key is Space or Enter, it will trigger a `click` event. 11242 * 11243 * @param {KeyboardEvent} event 11244 * The `keydown` event that caused this function to be called. 11245 * 11246 * @listens keydown 11247 */ 11248 handleKeyDown(event) { 11249 // Support Space or Enter key operation to fire a click event. Also, 11250 // prevent the event from propagating through the DOM and triggering 11251 // Player hotkeys. 11252 if (event.key === ' ' || event.key === 'Enter') { 11253 event.preventDefault(); 11254 event.stopPropagation(); 11255 this.trigger('click'); 11256 } else { 11257 // Pass keypress handling up for unsupported keys 11258 super.handleKeyDown(event); 11259 } 11260 } 11261} 11262Component$1.registerComponent('ClickableComponent', ClickableComponent); 11263 11264/** 11265 * @file poster-image.js 11266 */ 11267 11268/** @import Player from './player' */ 11269 11270/** 11271 * A `ClickableComponent` that handles showing the poster image for the player. 11272 * 11273 * @extends ClickableComponent 11274 */ 11275class PosterImage extends ClickableComponent { 11276 /** 11277 * Create an instance of this class. 11278 * 11279 * @param {Player} player 11280 * The `Player` that this class should attach to. 11281 * 11282 * @param {Object} [options] 11283 * The key/value store of player options. 11284 */ 11285 constructor(player, options) { 11286 super(player, options); 11287 this.update(); 11288 this.update_ = (e) => this.update(e); 11289 player.on('posterchange', this.update_); 11290 } 11291 11292 /** 11293 * Clean up and dispose of the `PosterImage`. 11294 */ 11295 dispose() { 11296 this.player().off('posterchange', this.update_); 11297 super.dispose(); 11298 } 11299 11300 /** 11301 * Create the `PosterImage`s DOM element. 11302 * 11303 * @return {Element} 11304 * The element that gets created. 11305 */ 11306 createEl() { 11307 // The el is an empty div to keep position in the DOM 11308 // A picture and img el will be inserted when a source is set 11309 return createEl('div', { 11310 className: 'vjs-poster', 11311 }); 11312 } 11313 11314 /** 11315 * Get or set the `PosterImage`'s crossOrigin option. 11316 * 11317 * @param {string|null} [value] 11318 * The value to set the crossOrigin to. If an argument is 11319 * given, must be one of `'anonymous'` or `'use-credentials'`, or 'null'. 11320 * 11321 * @return {string|null} 11322 * - The current crossOrigin value of the `Player` when getting. 11323 * - undefined when setting 11324 */ 11325 crossOrigin(value) { 11326 // `null` can be set to unset a value 11327 if (typeof value === 'undefined') { 11328 if (this.$('img')) { 11329 // If the poster's element exists, give its value 11330 return this.$('img').crossOrigin; 11331 } else if (this.player_.tech_ && this.player_.tech_.isReady_) { 11332 // If not but the tech is ready, query the tech 11333 return this.player_.crossOrigin(); 11334 } 11335 // Otherwise check options as the poster is usually set before the state of crossorigin 11336 // can be retrieved by the getter 11337 return this.player_.options_.crossOrigin || this.player_.options_.crossorigin || null; 11338 } 11339 if (value !== null && value !== 'anonymous' && value !== 'use-credentials') { 11340 this.player_.log.warn( 11341 `crossOrigin must be null, "anonymous" or "use-credentials", given "${value}"` 11342 ); 11343 return; 11344 } 11345 if (this.$('img')) { 11346 this.$('img').crossOrigin = value; 11347 } 11348 return; 11349 } 11350 11351 /** 11352 * An {@link EventTarget~EventListener} for {@link Player#posterchange} events. 11353 * 11354 * @listens Player#posterchange 11355 * 11356 * @param {Event} [event] 11357 * The `Player#posterchange` event that triggered this function. 11358 */ 11359 update(event) { 11360 const url = this.player().poster(); 11361 this.setSrc(url); 11362 11363 // If there's no poster source we should display:none on this component 11364 // so it's not still clickable or right-clickable 11365 if (url) { 11366 this.show(); 11367 } else { 11368 this.hide(); 11369 } 11370 } 11371 11372 /** 11373 * Set the source of the `PosterImage` depending on the display method. (Re)creates 11374 * the inner picture and img elementss when needed. 11375 * 11376 * @param {string} [url] 11377 * The URL to the source for the `PosterImage`. If not specified or falsy, 11378 * any source and ant inner picture/img are removed. 11379 */ 11380 setSrc(url) { 11381 if (!url) { 11382 this.el_.textContent = ''; 11383 return; 11384 } 11385 if (!this.$('img')) { 11386 this.el_.appendChild( 11387 createEl( 11388 'picture', 11389 { 11390 className: 'vjs-poster', 11391 // Don't want poster to be tabbable. 11392 tabIndex: -1, 11393 }, 11394 {}, 11395 createEl( 11396 'img', 11397 { 11398 loading: 'lazy', 11399 crossOrigin: this.crossOrigin(), 11400 }, 11401 { 11402 alt: '', 11403 } 11404 ) 11405 ) 11406 ); 11407 } 11408 this.$('img').src = url; 11409 } 11410 11411 /** 11412 * An {@link EventTarget~EventListener} for clicks on the `PosterImage`. See 11413 * {@link ClickableComponent#handleClick} for instances where this will be triggered. 11414 * 11415 * @listens tap 11416 * @listens click 11417 * @listens keydown 11418 * 11419 * @param {Event} event 11420 + The `click`, `tap` or `keydown` event that caused this function to be called. 11421 */ 11422 handleClick(event) { 11423 // We don't want a click to trigger playback when controls are disabled 11424 if (!this.player_.controls()) { 11425 return; 11426 } 11427 if (this.player_.tech(true)) { 11428 this.player_.tech(true).focus(); 11429 } 11430 if (this.player_.paused()) { 11431 silencePromise(this.player_.play()); 11432 } else { 11433 this.player_.pause(); 11434 } 11435 } 11436} 11437 11438/** 11439 * Get or set the `PosterImage`'s crossorigin option. For the HTML5 player, this 11440 * sets the `crossOrigin` property on the `<img>` tag to control the CORS 11441 * behavior. 11442 * 11443 * @param {string|null} [value] 11444 * The value to set the `PosterImages`'s crossorigin to. If an argument is 11445 * given, must be one of `anonymous` or `use-credentials`. 11446 * 11447 * @return {string|null|undefined} 11448 * - The current crossorigin value of the `Player` when getting. 11449 * - undefined when setting 11450 */ 11451PosterImage.prototype.crossorigin = PosterImage.prototype.crossOrigin; 11452Component$1.registerComponent('PosterImage', PosterImage); 11453 11454/** 11455 * @file text-track-display.js 11456 */ 11457 11458/** @import Player from '../player' */ 11459 11460const darkGray = '#222'; 11461const lightGray = '#ccc'; 11462const fontMap = { 11463 monospace: 'monospace', 11464 sansSerif: 'sans-serif', 11465 serif: 'serif', 11466 monospaceSansSerif: '"Andale Mono", "Lucida Console", monospace', 11467 monospaceSerif: '"Courier New", monospace', 11468 proportionalSansSerif: 'sans-serif', 11469 proportionalSerif: 'serif', 11470 casual: '"Comic Sans MS", Impact, fantasy', 11471 script: '"Monotype Corsiva", cursive', 11472 smallcaps: '"Andale Mono", "Lucida Console", monospace, sans-serif', 11473}; 11474 11475/** 11476 * Construct an rgba color from a given hex color code. 11477 * 11478 * @param {number} color 11479 * Hex number for color, like #f0e or #f604e2. 11480 * 11481 * @param {number} opacity 11482 * Value for opacity, 0.0 - 1.0. 11483 * 11484 * @return {string} 11485 * The rgba color that was created, like 'rgba(255, 0, 0, 0.3)'. 11486 */ 11487function constructColor(color, opacity) { 11488 let hex; 11489 if (color.length === 4) { 11490 // color looks like "#f0e" 11491 hex = color[1] + color[1] + color[2] + color[2] + color[3] + color[3]; 11492 } else if (color.length === 7) { 11493 // color looks like "#f604e2" 11494 hex = color.slice(1); 11495 } else { 11496 throw new Error( 11497 'Invalid color code provided, ' + color + '; must be formatted as e.g. #f0e or #f604e2.' 11498 ); 11499 } 11500 return ( 11501 'rgba(' + 11502 parseInt(hex.slice(0, 2), 16) + 11503 ',' + 11504 parseInt(hex.slice(2, 4), 16) + 11505 ',' + 11506 parseInt(hex.slice(4, 6), 16) + 11507 ',' + 11508 opacity + 11509 ')' 11510 ); 11511} 11512 11513/** 11514 * Try to update the style of a DOM element. Some style changes will throw an error, 11515 * particularly in IE8. Those should be noops. 11516 * 11517 * @param {Element} el 11518 * The DOM element to be styled. 11519 * 11520 * @param {string} style 11521 * The CSS property on the element that should be styled. 11522 * 11523 * @param {string} rule 11524 * The style rule that should be applied to the property. 11525 * 11526 * @private 11527 */ 11528function tryUpdateStyle(el, style, rule) { 11529 try { 11530 el.style[style] = rule; 11531 } catch (e) { 11532 // Satisfies linter. 11533 return; 11534 } 11535} 11536 11537/**
11538 * Converts the CSS top/right/bottom/left property numeric value to string in pixels. 11539 * 11540 * @param {number} position 11541 * The CSS top/right/bottom/left property value. 11542 * 11543 * @return {string} 11544 * The CSS property value that was created, like '10px'. 11545 * 11546 * @private 11547 */ 11548function getCSSPositionValue(position) { 11549 return position ? `${position}px` : ''; 11550} 11551 11552/** 11553 * The component for displaying text track cues. 11554 * 11555 * @extends Component 11556 */ 11557class TextTrackDisplay extends Component$1 { 11558 /** 11559 * Creates an instance of this class. 11560 * 11561 * @param {Player} player 11562 * The `Player` that this class should be attached to. 11563 * 11564 * @param {Object} [options] 11565 * The key/value store of player options. 11566 * 11567 * @param {Function} [ready] 11568 * The function to call when `TextTrackDisplay` is ready. 11569 */ 11570 constructor(player, options, ready) { 11571 super(player, options, ready); 11572 const updateDisplayTextHandler = (e) => this.updateDisplay(e); 11573 const updateDisplayHandler = (e) => { 11574 this.updateDisplayOverlay(); 11575 this.updateDisplay(e); 11576 }; 11577 player.on('loadstart', (e) => this.toggleDisplay(e)); 11578 player.on('texttrackchange', updateDisplayTextHandler); 11579 player.on('loadedmetadata', (e) => { 11580 this.updateDisplayOverlay(); 11581 this.preselectTrack(e); 11582 }); 11583 11584 // This used to be called during player init, but was causing an error 11585 // if a track should show by default and the display hadn't loaded yet. 11586 // Should probably be moved to an external track loader when we support 11587 // tracks that don't need a display. 11588 player.ready( 11589 bind_(this, function () { 11590 if (player.tech_ && player.tech_.featuresNativeTextTracks) { 11591 this.hide(); 11592 return; 11593 } 11594 player.on('fullscreenchange', updateDisplayHandler); 11595 player.on('playerresize', updateDisplayHandler); 11596 const screenOrientation = 11597 window__default['default'].screen.orientation || window__default['default']; 11598 const changeOrientationEvent = window__default['default'].screen.orientation 11599 ? 'change' 11600 : 'orientationchange'; 11601 screenOrientation.addEventListener(changeOrientationEvent, updateDisplayHandler); 11602 player.on('dispose', () => 11603 screenOrientation.removeEventListener(changeOrientationEvent, updateDisplayHandler) 11604 ); 11605 const tracks = this.options_.playerOptions.tracks || []; 11606 for (let i = 0; i < tracks.length; i++) { 11607 this.player_.addRemoteTextTrack(tracks[i], true); 11608 } 11609 this.preselectTrack(); 11610 }) 11611 ); 11612 } 11613 11614 /** 11615 * Preselect a track following this precedence: 11616 * - matches the previously selected {@link TextTrack}'s language and kind 11617 * - matches the previously selected {@link TextTrack}'s language only 11618 * - is the first default captions track 11619 * - is the first default descriptions track 11620 * 11621 * @listens Player#loadstart 11622 */ 11623 preselectTrack() { 11624 const modes = { 11625 captions: 1, 11626 subtitles: 1, 11627 }; 11628 const trackList = this.player_.textTracks(); 11629 const userPref = this.player_.cache_.selectedLanguage; 11630 let firstDesc; 11631 let firstCaptions; 11632 let preferredTrack; 11633 for (let i = 0; i < trackList.length; i++) { 11634 const track = trackList[i]; 11635 if ( 11636 userPref && 11637 userPref.enabled && 11638 userPref.language && 11639 userPref.language === track.language && 11640 track.kind in modes 11641 ) { 11642 // Always choose the track that matches both language and kind 11643 if (track.kind === userPref.kind) { 11644 preferredTrack = track; 11645 // or choose the first track that matches language 11646 } else if (!preferredTrack) { 11647 preferredTrack = track; 11648 } 11649 11650 // clear everything if offTextTrackMenuItem was clicked 11651 } else if (userPref && !userPref.enabled) { 11652 preferredTrack = null; 11653 firstDesc = null; 11654 firstCaptions = null; 11655 } else if (track.default) { 11656 if (track.kind === 'descriptions' && !firstDesc) { 11657 firstDesc = track; 11658 } else if (track.kind in modes && !firstCaptions) { 11659 firstCaptions = track; 11660 } 11661 } 11662 } 11663 11664 // The preferredTrack matches the user preference and takes 11665 // precedence over all the other tracks. 11666 // So, display the preferredTrack before the first default track 11667 // and the subtitles/captions track before the descriptions track 11668 if (preferredTrack) { 11669 preferredTrack.mode = 'showing'; 11670 } else if (firstCaptions) { 11671 firstCaptions.mode = 'showing'; 11672 } else if (firstDesc) { 11673 firstDesc.mode = 'showing'; 11674 } 11675 } 11676 11677 /** 11678 * Turn display of {@link TextTrack}'s from the current state into the other state. 11679 * There are only two states: 11680 * - 'shown' 11681 * - 'hidden' 11682 * 11683 * @listens Player#loadstart 11684 */ 11685 toggleDisplay() { 11686 if (this.player_.tech_ && this.player_.tech_.featuresNativeTextTracks) { 11687 this.hide(); 11688 } else { 11689 this.show(); 11690 } 11691 } 11692 11693 /** 11694 * Create the {@link Component}'s DOM element. 11695 * 11696 * @return {Element} 11697 * The element that was created. 11698 */ 11699 createEl() { 11700 return super.createEl( 11701 'div', 11702 { 11703 className: 'vjs-text-track-display', 11704 }, 11705 { 11706 translate: 'yes', 11707 'aria-live': 'off', 11708 'aria-atomic': 'true', 11709 } 11710 ); 11711 } 11712 11713 /** 11714 * Clear all displayed {@link TextTrack}s. 11715 */ 11716 clearDisplay() { 11717 if (typeof window__default['default'].WebVTT === 'function') { 11718 window__default['default'].WebVTT.processCues(window__default['default'], [], this.el_); 11719 } 11720 } 11721 11722 /** 11723 * Update the displayed TextTrack when a either a {@link Player#texttrackchange} or 11724 * a {@link Player#fullscreenchange} is fired. 11725 * 11726 * @listens Player#texttrackchange 11727 * @listens Player#fullscreenchange 11728 */ 11729 updateDisplay() { 11730 const tracks = this.player_.textTracks(); 11731 const allowMultipleShowingTracks = this.options_.allowMultipleShowingTracks; 11732 this.clearDisplay(); 11733 if (allowMultipleShowingTracks) { 11734 const showingTracks = []; 11735 for (let i = 0; i < tracks.length; ++i) { 11736 const track = tracks[i]; 11737 if (track.mode !== 'showing') { 11738 continue; 11739 } 11740 showingTracks.push(track); 11741 } 11742 this.updateForTrack(showingTracks); 11743 return; 11744 } 11745 11746 // Track display prioritization model: if multiple tracks are 'showing', 11747 // display the first 'subtitles' or 'captions' track which is 'showing', 11748 // otherwise display the first 'descriptions' track which is 'showing' 11749 11750 let descriptionsTrack = null;
11751 let captionsSubtitlesTrack = null; 11752 let i = tracks.length; 11753 while (i--) { 11754 const track = tracks[i]; 11755 if (track.mode === 'showing') { 11756 if (track.kind === 'descriptions') { 11757 descriptionsTrack = track; 11758 } else { 11759 captionsSubtitlesTrack = track; 11760 } 11761 } 11762 } 11763 if (captionsSubtitlesTrack) { 11764 if (this.getAttribute('aria-live') !== 'off') { 11765 this.setAttribute('aria-live', 'off'); 11766 } 11767 this.updateForTrack(captionsSubtitlesTrack); 11768 } else if (descriptionsTrack) { 11769 if (this.getAttribute('aria-live') !== 'assertive') { 11770 this.setAttribute('aria-live', 'assertive'); 11771 } 11772 this.updateForTrack(descriptionsTrack); 11773 } 11774 if (!window__default['default'].CSS.supports('inset', '10px')) { 11775 const textTrackDisplay = this.el_; 11776 const vjsTextTrackCues = textTrackDisplay.querySelectorAll('.vjs-text-track-cue'); 11777 const controlBarHeight = this.player_.controlBar.el_.getBoundingClientRect().height; 11778 const playerHeight = this.player_.el_.getBoundingClientRect().height; 11779 11780 // Clear inline style before getting actual height of textTrackDisplay 11781 textTrackDisplay.style = ''; 11782 11783 // textrack style updates, this styles are required to be inline
vendor: 4,177 bytes, lines 11784-11896
11784 tryUpdateStyle(textTrackDisplay, 'position', 'relative'); 11785 tryUpdateStyle(textTrackDisplay, 'height', playerHeight - controlBarHeight + 'px'); 11786 tryUpdateStyle(textTrackDisplay, 'top', 'unset'); 11787 if (IS_SMART_TV) { 11788 tryUpdateStyle(textTrackDisplay, 'bottom', playerHeight + 'px'); 11789 } else { 11790 tryUpdateStyle(textTrackDisplay, 'bottom', '0px'); 11791 } 11792 11793 // vjsTextTrackCue style updates 11794 if (vjsTextTrackCues.length > 0) { 11795 vjsTextTrackCues.forEach((vjsTextTrackCue) => { 11796 // verify if inset styles are inline 11797 if (vjsTextTrackCue.style.inset) { 11798 const insetStyles = vjsTextTrackCue.style.inset.split(' '); 11799 11800 // expected value is always 3 11801 if (insetStyles.length === 3) { 11802 Object.assign(vjsTextTrackCue.style, { 11803 top: insetStyles[0], 11804 right: insetStyles[1], 11805 bottom: insetStyles[2], 11806 left: 'unset', 11807 }); 11808 } 11809 } 11810 }); 11811 } 11812 } 11813 } 11814 11815 /** 11816 * Updates the displayed TextTrack to be sure it overlays the video when a either 11817 * a {@link Player#texttrackchange} or a {@link Player#fullscreenchange} is fired. 11818 */ 11819 updateDisplayOverlay() { 11820 // inset-inline and inset-block are not supprted on old chrome, but these are 11821 // only likely to be used on TV devices 11822 if ( 11823 !this.player_.videoHeight() || 11824 !window__default['default'].CSS.supports('inset-inline: 10px') 11825 ) { 11826 return; 11827 } 11828 const playerWidth = this.player_.currentWidth(); 11829 const playerHeight = this.player_.currentHeight(); 11830 const playerAspectRatio = playerWidth / playerHeight; 11831 const videoAspectRatio = this.player_.videoWidth() / this.player_.videoHeight(); 11832 let insetInlineMatch = 0; 11833 let insetBlockMatch = 0; 11834 if (Math.abs(playerAspectRatio - videoAspectRatio) > 0.1) { 11835 if (playerAspectRatio > videoAspectRatio) { 11836 insetInlineMatch = Math.round((playerWidth - playerHeight * videoAspectRatio) / 2); 11837 } else { 11838 insetBlockMatch = Math.round((playerHeight - playerWidth / videoAspectRatio) / 2); 11839 } 11840 } 11841 tryUpdateStyle(this.el_, 'insetInline', getCSSPositionValue(insetInlineMatch)); 11842 tryUpdateStyle(this.el_, 'insetBlock', getCSSPositionValue(insetBlockMatch)); 11843 } 11844 11845 /** 11846 * Style {@Link TextTrack} activeCues according to {@Link TextTrackSettings}. 11847 * 11848 * @param {TextTrack} track 11849 * Text track object containing active cues to style. 11850 */ 11851 updateDisplayState(track) { 11852 const overrides = this.player_.textTrackSettings.getValues(); 11853 const cues = track.activeCues; 11854 let i = cues.length; 11855 while (i--) { 11856 const cue = cues[i]; 11857 if (!cue) { 11858 continue; 11859 } 11860 const cueDiv = cue.displayState; 11861 if (overrides.color) { 11862 cueDiv.firstChild.style.color = overrides.color; 11863 } 11864 if (overrides.textOpacity) { 11865 tryUpdateStyle( 11866 cueDiv.firstChild, 11867 'color', 11868 constructColor(overrides.color || '#fff', overrides.textOpacity) 11869 ); 11870 } 11871 if (overrides.backgroundColor) { 11872 cueDiv.firstChild.style.backgroundColor = overrides.backgroundColor; 11873 } 11874 if (overrides.backgroundOpacity) { 11875 tryUpdateStyle( 11876 cueDiv.firstChild, 11877 'backgroundColor', 11878 constructColor(overrides.backgroundColor || '#000', overrides.backgroundOpacity) 11879 ); 11880 } 11881 if (overrides.windowColor) { 11882 if (overrides.windowOpacity) { 11883 tryUpdateStyle( 11884 cueDiv, 11885 'backgroundColor', 11886 constructColor(overrides.windowColor, overrides.windowOpacity) 11887 ); 11888 } else { 11889 cueDiv.style.backgroundColor = overrides.windowColor; 11890 } 11891 } 11892 if (overrides.edgeStyle) { 11893 if (overrides.edgeStyle === 'dropshadow') { 11894 cueDiv.firstChild.style.textShadow = `2px 2px 3px ${darkGray}, 2px 2px 4px ${darkGray}, 2px 2px 5px ${darkGray}`; 11895 } else if (overrides.edgeStyle === 'raised') { 11896 cueDiv.firstChild.style.textShadow = `1px 1px ${darkGray}, 2px 2px ${darkGray}, 3px 3px ${darkGray}`;
vendor: 7,593 bytes, lines 11897-12158
11897 } else if (overrides.edgeStyle === 'depressed') { 11898 cueDiv.firstChild.style.textShadow = `1px 1px ${lightGray}, 0 1px ${lightGray}, -1px -1px ${darkGray}, 0 -1px ${darkGray}`; 11899 } else if (overrides.edgeStyle === 'uniform') { 11900 cueDiv.firstChild.style.textShadow = `0 0 4px ${darkGray}, 0 0 4px ${darkGray}, 0 0 4px ${darkGray}, 0 0 4px ${darkGray}`; 11901 } 11902 } 11903 if (overrides.fontPercent && overrides.fontPercent !== 1) { 11904 const fontSize = window__default['default'].parseFloat(cueDiv.style.fontSize); 11905 cueDiv.style.fontSize = fontSize * overrides.fontPercent + 'px'; 11906 cueDiv.style.height = 'auto'; 11907 cueDiv.style.top = 'auto'; 11908 } 11909 if (overrides.fontFamily && overrides.fontFamily !== 'default') { 11910 if (overrides.fontFamily === 'small-caps') { 11911 cueDiv.firstChild.style.fontVariant = 'small-caps'; 11912 } else { 11913 cueDiv.firstChild.style.fontFamily = fontMap[overrides.fontFamily]; 11914 } 11915 } 11916 } 11917 } 11918 11919 /** 11920 * Add an {@link TextTrack} to to the {@link Tech}s {@link TextTrackList}. 11921 * 11922 * @param {TextTrack|TextTrack[]} tracks 11923 * Text track object or text track array to be added to the list. 11924 */ 11925 updateForTrack(tracks) { 11926 if (!Array.isArray(tracks)) { 11927 tracks = [tracks]; 11928 } 11929 if ( 11930 typeof window__default['default'].WebVTT !== 'function' || 11931 tracks.every((track) => { 11932 return !track.activeCues; 11933 }) 11934 ) { 11935 return; 11936 } 11937 const cues = []; 11938 11939 // push all active track cues 11940 for (let i = 0; i < tracks.length; ++i) { 11941 const track = tracks[i]; 11942 for (let j = 0; j < track.activeCues.length; ++j) { 11943 cues.push(track.activeCues[j]); 11944 } 11945 } 11946 11947 // removes all cues before it processes new ones 11948 window__default['default'].WebVTT.processCues(window__default['default'], cues, this.el_); 11949 11950 // add unique class to each language text track & add settings styling if necessary 11951 for (let i = 0; i < tracks.length; ++i) { 11952 const track = tracks[i]; 11953 for (let j = 0; j < track.activeCues.length; ++j) { 11954 const cueEl = track.activeCues[j].displayState; 11955 addClass( 11956 cueEl, 11957 'vjs-text-track-cue', 11958 'vjs-text-track-cue-' + (track.language ? track.language : i) 11959 ); 11960 if (track.language) { 11961 setAttribute(cueEl, 'lang', track.language); 11962 } 11963 } 11964 if (this.player_.textTrackSettings) { 11965 this.updateDisplayState(track); 11966 } 11967 } 11968 } 11969} 11970Component$1.registerComponent('TextTrackDisplay', TextTrackDisplay); 11971 11972/** 11973 * @file loading-spinner.js 11974 */ 11975 11976/** 11977 * A loading spinner for use during waiting/loading events. 11978 * 11979 * @extends Component 11980 */ 11981class LoadingSpinner extends Component$1 { 11982 /** 11983 * Create the `LoadingSpinner`s DOM element. 11984 * 11985 * @return {Element} 11986 * The dom element that gets created. 11987 */ 11988 createEl() { 11989 const isAudio = this.player_.isAudio(); 11990 const playerType = this.localize(isAudio ? 'Audio Player' : 'Video Player'); 11991 const controlText = createEl('span', { 11992 className: 'vjs-control-text', 11993 textContent: this.localize('{1} is loading.', [playerType]), 11994 }); 11995 const el = super.createEl('div', { 11996 className: 'vjs-loading-spinner', 11997 dir: 'ltr', 11998 }); 11999 el.appendChild(controlText); 12000 return el; 12001 } 12002 12003 /** 12004 * Update control text on languagechange 12005 */ 12006 handleLanguagechange() { 12007 this.$('.vjs-control-text').textContent = this.localize('{1} is loading.', [ 12008 this.player_.isAudio() ? 'Audio Player' : 'Video Player', 12009 ]); 12010 } 12011} 12012Component$1.registerComponent('LoadingSpinner', LoadingSpinner); 12013 12014/** 12015 * @file button.js 12016 */ 12017 12018/** 12019 * Base class for all buttons. 12020 * 12021 * @extends ClickableComponent 12022 */ 12023class Button extends ClickableComponent { 12024 /** 12025 * Create the `Button`s DOM element. 12026 * 12027 * @param {string} [tag="button"] 12028 * The element's node type. This argument is IGNORED: no matter what 12029 * is passed, it will always create a `button` element. 12030 * 12031 * @param {Object} [props={}] 12032 * An object of properties that should be set on the element. 12033 * 12034 * @param {Object} [attributes={}] 12035 * An object of attributes that should be set on the element. 12036 * 12037 * @return {Element} 12038 * The element that gets created. 12039 */ 12040 createEl(tag, props = {}, attributes = {}) { 12041 tag = 'button'; 12042 props = Object.assign( 12043 { 12044 className: this.buildCSSClass(), 12045 }, 12046 props 12047 ); 12048 12049 // Add attributes for button element 12050 attributes = Object.assign( 12051 { 12052 // Necessary since the default button type is "submit" 12053 type: 'button', 12054 }, 12055 attributes 12056 ); 12057 const el = createEl(tag, props, attributes); 12058 if (!this.player_.options_.experimentalSvgIcons) { 12059 el.appendChild( 12060 createEl( 12061 'span', 12062 { 12063 className: 'vjs-icon-placeholder', 12064 }, 12065 { 12066 'aria-hidden': true, 12067 } 12068 ) 12069 ); 12070 } 12071 this.createControlTextEl(el); 12072 return el; 12073 } 12074 12075 /** 12076 * Add a child `Component` inside of this `Button`. 12077 * 12078 * @param {string|Component} child 12079 * The name or instance of a child to add. 12080 * 12081 * @param {Object} [options={}] 12082 * The key/value store of options that will get passed to children of 12083 * the child. 12084 * 12085 * @return {Component} 12086 * The `Component` that gets added as a child. When using a string the 12087 * `Component` will get created by this process. 12088 * 12089 * @deprecated since version 5 12090 */ 12091 addChild(child, options = {}) { 12092 const className = this.constructor.name; 12093 log$1.warn( 12094 `Adding an actionable (user controllable) child to a Button (${className}) is not supported; use a ClickableComponent instead.` 12095 ); 12096 12097 // Avoid the error message generated by ClickableComponent's addChild method 12098 return Component$1.prototype.addChild.call(this, child, options); 12099 } 12100 12101 /** 12102 * Enable the `Button` element so that it can be activated or clicked. Use this with 12103 * {@link Button#disable}. 12104 */ 12105 enable() { 12106 super.enable(); 12107 this.el_.removeAttribute('disabled'); 12108 } 12109 12110 /** 12111 * Disable the `Button` element so that it cannot be activated or clicked. Use this with 12112 * {@link Button#enable}. 12113 */ 12114 disable() { 12115 super.disable(); 12116 this.el_.setAttribute('disabled', 'disabled'); 12117 } 12118 12119 /** 12120 * This gets called when a `Button` has focus and `keydown` is triggered via a key 12121 * press. 12122 * 12123 * @param {KeyboardEvent} event 12124 * The event that caused this function to get called. 12125 * 12126 * @listens keydown 12127 */ 12128 handleKeyDown(event) { 12129 // Ignore Space or Enter key operation, which is handled by the browser for 12130 // a button - though not for its super class, ClickableComponent. Also, 12131 // prevent the event from propagating through the DOM and triggering Player 12132 // hotkeys. We do not preventDefault here because we _want_ the browser to 12133 // handle it. 12134 if (event.key === ' ' || event.key === 'Enter') { 12135 event.stopPropagation(); 12136 return; 12137 } 12138 12139 // Pass keypress handling up for unsupported keys 12140 super.handleKeyDown(event); 12141 } 12142} 12143Component$1.registerComponent('Button', Button); 12144 12145/** 12146 * @file big-play-button.js 12147 */ 12148 12149/** 12150 * The initial play button that shows before the video has played. The hiding of the 12151 * `BigPlayButton` get done via CSS and `Player` states. 12152 * 12153 * @extends Button 12154 */ 12155class BigPlayButton extends Button { 12156 constructor(player, options) { 12157 super(player, options); 12158 this.mouseused_ = false;
vendor: 4,092 bytes, lines 12159-12318
12159 this.setIcon('play'); 12160 this.on('mousedown', (e) => this.handleMouseDown(e)); 12161 } 12162 12163 /** 12164 * Builds the default DOM `className`. 12165 * 12166 * @return {string} 12167 * The DOM `className` for this object. Always returns 'vjs-big-play-button'. 12168 */ 12169 buildCSSClass() { 12170 return 'vjs-big-play-button'; 12171 } 12172 12173 /** 12174 * This gets called when a `BigPlayButton` "clicked". See {@link ClickableComponent} 12175 * for more detailed information on what a click can be. 12176 * 12177 * @param {KeyboardEvent|MouseEvent|TouchEvent} event 12178 * The `keydown`, `tap`, or `click` event that caused this function to be 12179 * called. 12180 * 12181 * @listens tap 12182 * @listens click 12183 */ 12184 handleClick(event) { 12185 const playPromise = this.player_.play(); 12186 12187 // exit early if clicked via the mouse 12188 if (this.mouseused_ && 'clientX' in event && 'clientY' in event) { 12189 silencePromise(playPromise); 12190 if (this.player_.tech(true)) { 12191 this.player_.tech(true).focus(); 12192 } 12193 return; 12194 } 12195 const cb = this.player_.getChild('controlBar'); 12196 const playToggle = cb && cb.getChild('playToggle'); 12197 if (!playToggle) { 12198 this.player_.tech(true).focus(); 12199 return; 12200 } 12201 const playFocus = () => playToggle.focus(); 12202 if (isPromise(playPromise)) { 12203 playPromise.then(playFocus, () => {}); 12204 } else { 12205 this.setTimeout(playFocus, 1); 12206 } 12207 } 12208 12209 /** 12210 * Event handler that is called when a `BigPlayButton` receives a 12211 * `keydown` event. 12212 * 12213 * @param {KeyboardEvent} event 12214 * The `keydown` event that caused this function to be called. 12215 * 12216 * @listens keydown 12217 */ 12218 handleKeyDown(event) { 12219 this.mouseused_ = false; 12220 super.handleKeyDown(event); 12221 } 12222 12223 /** 12224 * Handle `mousedown` events on the `BigPlayButton`. 12225 * 12226 * @param {MouseEvent} event 12227 * `mousedown` or `touchstart` event that triggered this function 12228 * 12229 * @listens mousedown 12230 */ 12231 handleMouseDown(event) { 12232 this.mouseused_ = true; 12233 } 12234} 12235 12236/** 12237 * The text that should display over the `BigPlayButton`s controls. Added to for localization. 12238 * 12239 * @type {string} 12240 * @protected 12241 */ 12242BigPlayButton.prototype.controlText_ = 'Play Video'; 12243Component$1.registerComponent('BigPlayButton', BigPlayButton); 12244 12245/** 12246 * @file close-button.js 12247 */ 12248 12249/** @import Player from './player' */ 12250 12251/** 12252 * The `CloseButton` is a `{@link Button}` that fires a `close` event when 12253 * it gets clicked. 12254 * 12255 * @extends Button 12256 */ 12257class CloseButton extends Button { 12258 /** 12259 * Creates an instance of the this class. 12260 * 12261 * @param {Player} player 12262 * The `Player` that this class should be attached to. 12263 * 12264 * @param {Object} [options] 12265 * The key/value store of player options. 12266 */ 12267 constructor(player, options) { 12268 super(player, options); 12269 this.setIcon('cancel'); 12270 this.controlText((options && options.controlText) || this.localize('Close')); 12271 } 12272 12273 /** 12274 * Builds the default DOM `className`. 12275 * 12276 * @return {string} 12277 * The DOM `className` for this object. 12278 */ 12279 buildCSSClass() { 12280 return `vjs-close-button ${super.buildCSSClass()}`; 12281 } 12282 12283 /** 12284 * This gets called when a `CloseButton` gets clicked. See 12285 * {@link ClickableComponent#handleClick} for more information on when 12286 * this will be triggered 12287 * 12288 * @param {Event} event 12289 * The `keydown`, `tap`, or `click` event that caused this function to be 12290 * called. 12291 * 12292 * @listens tap 12293 * @listens click 12294 * @fires CloseButton#close 12295 */ 12296 handleClick(event) { 12297 /** 12298 * Triggered when the a `CloseButton` is clicked. 12299 * 12300 * @event CloseButton#close 12301 * @type {Event} 12302 * 12303 * @property {boolean} [bubbles=false] 12304 * set to false so that the close event does not 12305 * bubble up to parents if there is no listener 12306 */ 12307 this.trigger({ 12308 type: 'close', 12309 bubbles: false, 12310 }); 12311 } 12312 /** 12313 * Event handler that is called when a `CloseButton` receives a 12314 * `keydown` event. 12315 * 12316 * By default, if the key is Esc, it will trigger a `click` event. 12317 * 12318 * @param {KeyboardEvent} event
vendor: 7,915 bytes, lines 12319-12624
12319 * The `keydown` event that caused this function to be called. 12320 * 12321 * @listens keydown 12322 */ 12323 handleKeyDown(event) { 12324 // Esc button will trigger `click` event 12325 if (event.key === 'Escape') { 12326 event.preventDefault(); 12327 event.stopPropagation(); 12328 this.trigger('click'); 12329 } else { 12330 // Pass keypress handling up for unsupported keys 12331 super.handleKeyDown(event); 12332 } 12333 } 12334} 12335Component$1.registerComponent('CloseButton', CloseButton); 12336 12337/** 12338 * @file play-toggle.js 12339 */ 12340 12341/** @import Player from './player' */ 12342 12343/** 12344 * Button to toggle between play and pause. 12345 * 12346 * @extends Button 12347 */ 12348class PlayToggle extends Button { 12349 /** 12350 * Creates an instance of this class. 12351 * 12352 * @param {Player} player 12353 * The `Player` that this class should be attached to. 12354 * 12355 * @param {Object} [options={}] 12356 * The key/value store of player options. 12357 */ 12358 constructor(player, options = {}) { 12359 super(player, options); 12360 12361 // show or hide replay icon 12362 options.replay = options.replay === undefined || options.replay; 12363 this.setIcon('play'); 12364 this.on(player, 'play', (e) => this.handlePlay(e)); 12365 this.on(player, 'pause', (e) => this.handlePause(e)); 12366 if (options.replay) { 12367 this.on(player, 'ended', (e) => this.handleEnded(e)); 12368 } 12369 } 12370 12371 /** 12372 * Builds the default DOM `className`. 12373 * 12374 * @return {string} 12375 * The DOM `className` for this object. 12376 */ 12377 buildCSSClass() { 12378 return `vjs-play-control ${super.buildCSSClass()}`; 12379 } 12380 12381 /** 12382 * This gets called when an `PlayToggle` is "clicked". See 12383 * {@link ClickableComponent} for more detailed information on what a click can be. 12384 * 12385 * @param {Event} [event] 12386 * The `keydown`, `tap`, or `click` event that caused this function to be 12387 * called. 12388 * 12389 * @listens tap 12390 * @listens click 12391 */ 12392 handleClick(event) { 12393 if (this.player_.paused()) { 12394 silencePromise(this.player_.play()); 12395 } else { 12396 this.player_.pause(); 12397 } 12398 } 12399 12400 /** 12401 * This gets called once after the video has ended and the user seeks so that 12402 * we can change the replay button back to a play button. 12403 * 12404 * @param {Event} [event] 12405 * The event that caused this function to run. 12406 * 12407 * @listens Player#seeked 12408 */ 12409 handleSeeked(event) { 12410 this.removeClass('vjs-ended'); 12411 if (this.player_.paused()) { 12412 this.handlePause(event); 12413 } else { 12414 this.handlePlay(event); 12415 } 12416 } 12417 12418 /** 12419 * Add the vjs-playing class to the element so it can change appearance. 12420 * 12421 * @param {Event} [event] 12422 * The event that caused this function to run. 12423 * 12424 * @listens Player#play 12425 */ 12426 handlePlay(event) { 12427 this.removeClass('vjs-ended', 'vjs-paused'); 12428 this.addClass('vjs-playing'); 12429 // change the button text to "Pause" 12430 this.setIcon('pause'); 12431 this.controlText('Pause'); 12432 } 12433 12434 /** 12435 * Add the vjs-paused class to the element so it can change appearance. 12436 * 12437 * @param {Event} [event] 12438 * The event that caused this function to run. 12439 * 12440 * @listens Player#pause 12441 */ 12442 handlePause(event) { 12443 this.removeClass('vjs-playing'); 12444 this.addClass('vjs-paused'); 12445 // change the button text to "Play" 12446 this.setIcon('play'); 12447 this.controlText('Play'); 12448 } 12449 12450 /** 12451 * Add the vjs-ended class to the element so it can change appearance 12452 * 12453 * @param {Event} [event] 12454 * The event that caused this function to run. 12455 * 12456 * @listens Player#ended 12457 */ 12458 handleEnded(event) { 12459 this.removeClass('vjs-playing'); 12460 this.addClass('vjs-ended'); 12461 // change the button text to "Replay" 12462 this.setIcon('replay'); 12463 this.controlText('Replay'); 12464 12465 // on the next seek remove the replay button 12466 this.one(this.player_, 'seeked', (e) => this.handleSeeked(e)); 12467 } 12468} 12469 12470/** 12471 * The text that should display over the `PlayToggle`s controls. Added for localization. 12472 * 12473 * @type {string} 12474 * @protected 12475 */ 12476PlayToggle.prototype.controlText_ = 'Play'; 12477Component$1.registerComponent('PlayToggle', PlayToggle); 12478 12479/** 12480 * @file time-display.js 12481 */ 12482 12483/** @import Player from '../../player' */ 12484 12485/** 12486 * Displays time information about the video 12487 * 12488 * @extends Component 12489 */ 12490class TimeDisplay extends Component$1 { 12491 /** 12492 * Creates an instance of this class. 12493 * 12494 * @param {Player} player 12495 * The `Player` that this class should be attached to. 12496 * 12497 * @param {Object} [options] 12498 * The key/value store of player options. 12499 */ 12500 constructor(player, options) { 12501 super(player, options); 12502 this.on(player, ['timeupdate', 'ended', 'seeking'], (e) => this.update(e)); 12503 this.updateTextNode_(); 12504 } 12505 12506 /** 12507 * Create the `Component`'s DOM element 12508 * 12509 * @return {Element} 12510 * The element that was created. 12511 */ 12512 createEl() { 12513 const className = this.buildCSSClass(); 12514 const el = super.createEl('div', { 12515 className: `${className} vjs-time-control vjs-control`, 12516 }); 12517 const span = createEl( 12518 'span', 12519 { 12520 className: 'vjs-control-text', 12521 textContent: `${this.localize(this.labelText_)}\u00a0`, 12522 }, 12523 { 12524 role: 'presentation', 12525 } 12526 ); 12527 el.appendChild(span); 12528 this.contentEl_ = createEl( 12529 'span', 12530 { 12531 className: `${className}-display`, 12532 }, 12533 { 12534 // span elements have no implicit role, but some screen readers (notably VoiceOver) 12535 // treat them as a break between items in the DOM when using arrow keys 12536 // (or left-to-right swipes on iOS) to read contents of a page. Using 12537 // role='presentation' causes VoiceOver to NOT treat this span as a break. 12538 role: 'presentation', 12539 } 12540 ); 12541 el.appendChild(this.contentEl_); 12542 return el; 12543 } 12544 dispose() { 12545 this.contentEl_ = null; 12546 this.textNode_ = null; 12547 super.dispose(); 12548 } 12549 12550 /** 12551 * Updates the displayed time according to the `updateContent` function which is defined in the child class. 12552 * 12553 * @param {Event} [event] 12554 * The `timeupdate`, `ended` or `seeking` (if enableSmoothSeeking is true) event that caused this function to be called. 12555 */ 12556 update(event) { 12557 if (!this.player_.options_.enableSmoothSeeking && event.type === 'seeking') { 12558 return; 12559 } 12560 this.updateContent(event); 12561 } 12562 12563 /** 12564 * Updates the time display text node with a new time 12565 * 12566 * @param {number} [time=0] the time to update to 12567 * 12568 * @private 12569 */ 12570 updateTextNode_(time = 0) { 12571 time = formatTime(time); 12572 if (this.formattedTime_ === time) { 12573 return; 12574 } 12575 this.formattedTime_ = time; 12576 this.requestNamedAnimationFrame('TimeDisplay#updateTextNode_', () => { 12577 if (!this.contentEl_) { 12578 return; 12579 } 12580 let oldNode = this.textNode_; 12581 if (oldNode && this.contentEl_.firstChild !== oldNode) { 12582 oldNode = null; 12583 log$1.warn( 12584 'TimeDisplay#updateTextnode_: Prevented replacement of text node element since it was no longer a child of this node. Appending a new node instead.' 12585 ); 12586 } 12587 this.textNode_ = document__default['default'].createTextNode(this.formattedTime_); 12588 if (!this.textNode_) { 12589 return; 12590 } 12591 if (oldNode) { 12592 this.contentEl_.replaceChild(this.textNode_, oldNode); 12593 } else { 12594 this.contentEl_.appendChild(this.textNode_); 12595 } 12596 }); 12597 } 12598 12599 /** 12600 * To be filled out in the child class, should update the displayed time 12601 * in accordance with the fact that the current time has changed. 12602 * 12603 * @param {Event} [event] 12604 * The `timeupdate` event that caused this to run. 12605 * 12606 * @listens Player#timeupdate 12607 */ 12608 updateContent(event) {} 12609} 12610 12611/** 12612 * The text that is added to the `TimeDisplay` for screen reader users. 12613 * 12614 * @type {string} 12615 * @private 12616 */ 12617TimeDisplay.prototype.labelText_ = 'Time'; 12618 12619/** 12620 * The text that should display over the `TimeDisplay`s controls. Added to for localization. 12621 * 12622 * @type {string} 12623 * @protected 12624 *
12625 * @deprecated in v7; controlText_ is not used in non-active display Components 12626 */ 12627TimeDisplay.prototype.controlText_ = 'Time'; 12628Component$1.registerComponent('TimeDisplay', TimeDisplay); 12629 12630/** 12631 * @file current-time-display.js 12632 */ 12633 12634/** 12635 * Displays the current time 12636 * 12637 * @extends Component 12638 */ 12639class CurrentTimeDisplay extends TimeDisplay { 12640 /** 12641 * Builds the default DOM `className`. 12642 * 12643 * @return {string} 12644 * The DOM `className` for this object. 12645 */ 12646 buildCSSClass() { 12647 return 'vjs-current-time'; 12648 } 12649 12650 /** 12651 * Update current time display 12652 * 12653 * @param {Event} [event] 12654 * The `timeupdate` event that caused this function to run. 12655 * 12656 * @listens Player#timeupdate 12657 */ 12658 updateContent(event) { 12659 // Allows for smooth scrubbing, when player can't keep up. 12660 let time; 12661 if (this.player_.ended()) { 12662 time = this.player_.duration(); 12663 } else { 12664 time = this.player_.scrubbing() 12665 ? this.player_.getCache().currentTime 12666 : this.player_.currentTime(); 12667 } 12668 this.updateTextNode_(time); 12669 } 12670} 12671 12672/** 12673 * The text that is added to the `CurrentTimeDisplay` for screen reader users. 12674 * 12675 * @type {string} 12676 * @private 12677 */ 12678CurrentTimeDisplay.prototype.labelText_ = 'Current Time'; 12679 12680/** 12681 * The text that should display over the `CurrentTimeDisplay`s controls. Added to for localization. 12682 * 12683 * @type {string} 12684 * @protected 12685 * 12686 * @deprecated in v7; controlText_ is not used in non-active display Components 12687 */ 12688CurrentTimeDisplay.prototype.controlText_ = 'Current Time'; 12689Component$1.registerComponent('CurrentTimeDisplay', CurrentTimeDisplay); 12690 12691/** 12692 * @file duration-display.js 12693 */ 12694 12695/** @import Player from '../../player' */ 12696 12697/** 12698 * Displays the duration 12699 * 12700 * @extends Component 12701 */ 12702class DurationDisplay extends TimeDisplay { 12703 /** 12704 * Creates an instance of this class. 12705 * 12706 * @param {Player} player 12707 * The `Player` that this class should be attached to. 12708 * 12709 * @param {Object} [options] 12710 * The key/value store of player options. 12711 */ 12712 constructor(player, options) { 12713 super(player, options); 12714 const updateContent = (e) => this.updateContent(e); 12715 12716 // we do not want to/need to throttle duration changes, 12717 // as they should always display the changed duration as 12718 // it has changed 12719 this.on(player, 'durationchange', updateContent); 12720 12721 // Listen to loadstart because the player duration is reset when a new media element is loaded, 12722 // but the durationchange on the user agent will not fire. 12723 // @see [Spec]{@link https://www.w3.org/TR/2011/WD-html5-20110113/video.html#media-element-load-algorithm} 12724 this.on(player, 'loadstart', updateContent); 12725 12726 // Also listen for timeupdate (in the parent) and loadedmetadata because removing those 12727 // listeners could have broken dependent applications/libraries. These 12728 // can likely be removed for 7.0. 12729 this.on(player, 'loadedmetadata', updateContent); 12730 } 12731 12732 /** 12733 * Builds the default DOM `className`. 12734 * 12735 * @return {string} 12736 * The DOM `className` for this object. 12737 */ 12738 buildCSSClass() { 12739 return 'vjs-duration'; 12740 } 12741 12742 /** 12743 * Update duration time display. 12744 * 12745 * @param {Event} [event] 12746 * The `durationchange`, `timeupdate`, or `loadedmetadata` event that caused 12747 * this function to be called. 12748 * 12749 * @listens Player#durationchange 12750 * @listens Player#timeupdate 12751 * @listens Player#loadedmetadata 12752 */ 12753 updateContent(event) { 12754 const duration = this.player_.duration(); 12755 this.updateTextNode_(duration); 12756 } 12757} 12758 12759/** 12760 * The text that is added to the `DurationDisplay` for screen reader users. 12761 * 12762 * @type {string} 12763 * @private 12764 */ 12765DurationDisplay.prototype.labelText_ = 'Duration'; 12766 12767/** 12768 * The text that should display over the `DurationDisplay`s controls. Added to for localization. 12769 * 12770 * @type {string} 12771 * @protected 12772 * 12773 * @deprecated in v7; controlText_ is not used in non-active display Components 12774 */ 12775DurationDisplay.prototype.controlText_ = 'Duration'; 12776Component$1.registerComponent('DurationDisplay', DurationDisplay); 12777 12778/** 12779 * @file time-divider.js 12780 */ 12781 12782/** 12783 * The separator between the current time and duration. 12784 * Can be hidden if it's not needed in the design. 12785 * 12786 * @extends Component 12787 */ 12788class TimeDivider extends Component$1 { 12789 /** 12790 * Create the component's DOM element 12791 * 12792 * @return {Element} 12793 * The element that was created. 12794 */ 12795 createEl() { 12796 const el = super.createEl( 12797 'div', 12798 { 12799 className: 'vjs-time-control vjs-time-divider', 12800 }, 12801 { 12802 // this element and its contents can be hidden from assistive techs since 12803 // it is made extraneous by the announcement of the control text 12804 // for the current time and duration displays 12805 'aria-hidden': true, 12806 } 12807 ); 12808 const div = super.createEl('div'); 12809 const span = super.createEl('span', { 12810 textContent: '/', 12811 }); 12812 div.appendChild(span); 12813 el.appendChild(div); 12814 return el; 12815 } 12816} 12817Component$1.registerComponent('TimeDivider', TimeDivider); 12818 12819/** 12820 * @file remaining-time-display.js 12821 */ 12822 12823/** @import Player from '../../player' */ 12824 12825/** 12826 * Displays the time left in the video 12827 * 12828 * @extends Component 12829 */ 12830class RemainingTimeDisplay extends TimeDisplay { 12831 /** 12832 * Creates an instance of this class. 12833 * 12834 * @param {Player} player 12835 * The `Player` that this class should be attached to. 12836 * 12837 * @param {Object} [options] 12838 * The key/value store of player options. 12839 */ 12840 constructor(player, options) { 12841 super(player, options); 12842 this.on(player, 'durationchange', (e) => this.updateContent(e)); 12843 } 12844 12845 /** 12846 * Builds the default DOM `className`. 12847 * 12848 * @return {string} 12849 * The DOM `className` for this object. 12850 */ 12851 buildCSSClass() { 12852 return 'vjs-remaining-time'; 12853 } 12854 12855 /**
12856 * Create the `Component`'s DOM element with the "minus" character prepend to the time 12857 * 12858 * @return {Element} 12859 * The element that was created. 12860 */ 12861 createEl() { 12862 const el = super.createEl(); 12863 if (this.options_.displayNegative !== false) { 12864 el.insertBefore( 12865 createEl( 12866 'span', 12867 {}, 12868 { 12869 'aria-hidden': true, 12870 }, 12871 '-' 12872 ), 12873 this.contentEl_ 12874 ); 12875 } 12876 return el; 12877 } 12878 12879 /** 12880 * Update remaining time display. 12881 * 12882 * @param {Event} [event] 12883 * The `timeupdate` or `durationchange` event that caused this to run. 12884 * 12885 * @listens Player#timeupdate 12886 * @listens Player#durationchange 12887 */ 12888 updateContent(event) { 12889 if (typeof this.player_.duration() !== 'number') { 12890 return; 12891 } 12892 let time; 12893 12894 // @deprecated We should only use remainingTimeDisplay 12895 // as of video.js 7 12896 if (this.player_.ended()) { 12897 time = 0; 12898 } else if (this.player_.remainingTimeDisplay) { 12899 time = this.player_.remainingTimeDisplay(); 12900 } else { 12901 time = this.player_.remainingTime(); 12902 } 12903 this.updateTextNode_(time); 12904 } 12905} 12906 12907/** 12908 * The text that is added to the `RemainingTimeDisplay` for screen reader users. 12909 * 12910 * @type {string} 12911 * @private 12912 */ 12913RemainingTimeDisplay.prototype.labelText_ = 'Remaining Time'; 12914 12915/** 12916 * The text that should display over the `RemainingTimeDisplay`s controls. Added to for localization. 12917 * 12918 * @type {string} 12919 * @protected 12920 * 12921 * @deprecated in v7; controlText_ is not used in non-active display Components 12922 */ 12923RemainingTimeDisplay.prototype.controlText_ = 'Remaining Time'; 12924Component$1.registerComponent('RemainingTimeDisplay', RemainingTimeDisplay); 12925 12926/** 12927 * @file live-display.js 12928 */ 12929 12930/** @import Player from './player' */ 12931 12932// TODO - Future make it click to snap to live 12933 12934/** 12935 * Displays the live indicator when duration is Infinity. 12936 * 12937 * @extends Component 12938 */ 12939class LiveDisplay extends Component$1 { 12940 /** 12941 * Creates an instance of this class. 12942 * 12943 * @param {Player} player 12944 * The `Player` that this class should be attached to. 12945 * 12946 * @param {Object} [options] 12947 * The key/value store of player options. 12948 */ 12949 constructor(player, options) { 12950 super(player, options); 12951 this.updateShowing(); 12952 this.on(this.player(), 'durationchange', (e) => this.updateShowing(e)); 12953 } 12954 12955 /** 12956 * Create the `Component`'s DOM element 12957 * 12958 * @return {Element} 12959 * The element that was created. 12960 */ 12961 createEl() { 12962 const el = super.createEl('div', { 12963 className: 'vjs-live-control vjs-control', 12964 }); 12965 this.contentEl_ = createEl( 12966 'div', 12967 { 12968 className: 'vjs-live-display', 12969 }, 12970 { 12971 'aria-live': 'off', 12972 } 12973 ); 12974 this.contentEl_.appendChild( 12975 createEl('span', { 12976 className: 'vjs-control-text', 12977 textContent: `${this.localize('Stream Type')}\u00a0`, 12978 }) 12979 ); 12980 this.contentEl_.appendChild(document__default['default'].createTextNode(this.localize('LIVE'))); 12981 el.appendChild(this.contentEl_); 12982 return el; 12983 } 12984 dispose() { 12985 this.contentEl_ = null; 12986 super.dispose(); 12987 } 12988 12989 /** 12990 * Check the duration to see if the LiveDisplay should be showing or not. Then show/hide 12991 * it accordingly 12992 * 12993 * @param {Event} [event] 12994 * The {@link Player#durationchange} event that caused this function to run. 12995 * 12996 * @listens Player#durationchange 12997 */ 12998 updateShowing(event) { 12999 if (this.player().duration() === Infinity) { 13000 this.show(); 13001 } else { 13002 this.hide(); 13003 } 13004 } 13005} 13006Component$1.registerComponent('LiveDisplay', LiveDisplay); 13007 13008/** 13009 * @file seek-to-live.js 13010 */ 13011 13012/** @import Player from './player' */ 13013 13014/** 13015 * Displays the live indicator when duration is Infinity. 13016 * 13017 * @extends Component 13018 */ 13019class SeekToLive extends Button { 13020 /** 13021 * Creates an instance of this class. 13022 * 13023 * @param {Player} player 13024 * The `Player` that this class should be attached to. 13025 * 13026 * @param {Object} [options] 13027 * The key/value store of player options. 13028 */ 13029 constructor(player, options) { 13030 super(player, options); 13031 this.updateLiveEdgeStatus(); 13032 if (this.player_.liveTracker) { 13033 this.updateLiveEdgeStatusHandler_ = (e) => this.updateLiveEdgeStatus(e); 13034 this.on(this.player_.liveTracker, 'liveedgechange', this.updateLiveEdgeStatusHandler_); 13035 } 13036 } 13037 13038 /** 13039 * Create the `Component`'s DOM element 13040 * 13041 * @return {Element} 13042 * The element that was created. 13043 */ 13044 createEl() { 13045 const el = super.createEl('button', { 13046 className: 'vjs-seek-to-live-control vjs-control', 13047 }); 13048 this.setIcon('circle', el); 13049 this.textEl_ = createEl( 13050 'span', 13051 { 13052 className: 'vjs-seek-to-live-text', 13053 textContent: this.localize('LIVE'), 13054 }, 13055 { 13056 'aria-hidden': 'true', 13057 } 13058 ); 13059 el.appendChild(this.textEl_); 13060 return el; 13061 } 13062 13063 /** 13064 * Update the state of this button if we are at the live edge 13065 * or not 13066 */ 13067 updateLiveEdgeStatus() { 13068 // default to live edge 13069 if (!this.player_.liveTracker || this.player_.liveTracker.atLiveEdge()) { 13070 this.setAttribute('aria-disabled', true); 13071 this.addClass('vjs-at-live-edge'); 13072 this.controlText('Seek to live, currently playing live'); 13073 } else { 13074 this.setAttribute('aria-disabled', false); 13075 this.removeClass('vjs-at-live-edge'); 13076 this.controlText('Seek to live, currently behind live'); 13077 } 13078 } 13079 13080 /** 13081 * On click bring us as near to the live point as possible. 13082 * This requires that we wait for the next `live-seekable-change` 13083 * event which will happen every segment length seconds. 13084 */ 13085 handleClick() { 13086 this.player_.liveTracker.seekToLiveEdge(); 13087 } 13088 13089 /** 13090 * Dispose of the element and stop tracking 13091 */ 13092 dispose() { 13093 if (this.player_.liveTracker) { 13094 this.off(this.player_.liveTracker, 'liveedgechange', this.updateLiveEdgeStatusHandler_); 13095 } 13096 this.textEl_ = null; 13097 super.dispose(); 13098 } 13099} 13100/** 13101 * The text that should display over the `SeekToLive`s control. Added for localization. 13102 * 13103 * @type {string} 13104 * @protected 13105 */ 13106SeekToLive.prototype.controlText_ = 'Seek to live, currently playing live'; 13107Component$1.registerComponent('SeekToLive', SeekToLive); 13108 13109/** 13110 * @file num.js 13111 * @module num 13112 */ 13113 13114/** 13115 * Keep a number between a min and a max value 13116 * 13117 * @param {number} number 13118 * The number to clamp 13119 * 13120 * @param {number} min 13121 * The minimum value 13122 * @param {number} max 13123 * The maximum value 13124 * 13125 * @return {number} 13126 * the clamped number 13127 */ 13128function clamp(number, min, max) { 13129 number = Number(number); 13130 return Math.min(max, Math.max(min, isNaN(number) ? min : number)); 13131} 13132 13133var Num = /*#__PURE__*/ Object.freeze({ 13134 __proto__: null, 13135 clamp: clamp, 13136}); 13137 13138/** 13139 * @file slider.js 13140 */ 13141 13142/** @import Player from '../player' */ 13143 13144/** 13145 * The base functionality for a slider. Can be vertical or horizontal. 13146 * For instance the volume bar or the seek bar on a video is a slider. 13147 * 13148 * @extends Component 13149 */ 13150class Slider extends Component$1 { 13151 /** 13152 * Create an instance of this class 13153 * 13154 * @param {Player} player 13155 * The `Player` that this class should be attached to. 13156 * 13157 * @param {Object} [options] 13158 * The key/value store of player options. 13159 */ 13160 constructor(player, options) { 13161 super(player, options); 13162 this.handleMouseDown_ = (e) => this.handleMouseDown(e); 13163 this.handleMouseUp_ = (e) => this.handleMouseUp(e); 13164 this.handleKeyDown_ = (e) => this.handleKeyDown(e); 13165 this.handleClick_ = (e) => this.handleClick(e); 13166 this.handleMouseMove_ = (e) => this.handleMouseMove(e); 13167 this.update_ = (e) => this.update(e); 13168 13169 // Set property names to bar to match with the child Slider class is looking for 13170 this.bar = this.getChild(this.options_.barName); 13171 13172 // Set a horizontal or vertical class on the slider depending on the slider t
vendor: 10,506 bytes, lines 13172-13528
13172ype 13173 this.vertical(!!this.options_.vertical); 13174 this.enable(); 13175 } 13176 13177 /** 13178 * Are controls are currently enabled for this slider or not. 13179 * 13180 * @return {boolean} 13181 * true if controls are enabled, false otherwise 13182 */ 13183 enabled() { 13184 return this.enabled_; 13185 } 13186 13187 /** 13188 * Enable controls for this slider if they are disabled 13189 */ 13190 enable() { 13191 if (this.enabled()) { 13192 return; 13193 } 13194 this.on('mousedown', this.handleMouseDown_); 13195 this.on('touchstart', this.handleMouseDown_); 13196 this.on('keydown', this.handleKeyDown_); 13197 this.on('click', this.handleClick_); 13198 13199 // TODO: deprecated, controlsvisible does not seem to be fired 13200 this.on(this.player_, 'controlsvisible', this.update); 13201 if (this.playerEvent) { 13202 this.on(this.player_, this.playerEvent, this.update); 13203 } 13204 this.removeClass('disabled'); 13205 this.setAttribute('tabindex', 0); 13206 this.enabled_ = true; 13207 } 13208 13209 /** 13210 * Disable controls for this slider if they are enabled 13211 */ 13212 disable() { 13213 if (!this.enabled()) { 13214 return; 13215 } 13216 const doc = this.bar.el_.ownerDocument; 13217 this.off('mousedown', this.handleMouseDown_); 13218 this.off('touchstart', this.handleMouseDown_); 13219 this.off('keydown', this.handleKeyDown_); 13220 this.off('click', this.handleClick_); 13221 this.off(this.player_, 'controlsvisible', this.update_); 13222 this.off(doc, 'mousemove', this.handleMouseMove_); 13223 this.off(doc, 'mouseup', this.handleMouseUp_); 13224 this.off(doc, 'touchmove', this.handleMouseMove_); 13225 this.off(doc, 'touchend', this.handleMouseUp_); 13226 this.removeAttribute('tabindex'); 13227 this.addClass('disabled'); 13228 if (this.playerEvent) { 13229 this.off(this.player_, this.playerEvent, this.update); 13230 } 13231 this.enabled_ = false; 13232 } 13233 13234 /** 13235 * Create the `Slider`s DOM element. 13236 * 13237 * @param {string} type 13238 * Type of element to create. 13239 * 13240 * @param {Object} [props={}] 13241 * List of properties in Object form. 13242 * 13243 * @param {Object} [attributes={}] 13244 * list of attributes in Object form. 13245 * 13246 * @return {Element} 13247 * The element that gets created. 13248 */ 13249 createEl(type, props = {}, attributes = {}) { 13250 // Add the slider element class to all sub classes 13251 props.className = props.className + ' vjs-slider'; 13252 props = Object.assign( 13253 { 13254 tabIndex: 0, 13255 }, 13256 props 13257 ); 13258 attributes = Object.assign( 13259 { 13260 role: 'slider', 13261 'aria-valuenow': 0, 13262 'aria-valuemin': 0, 13263 'aria-valuemax': 100, 13264 }, 13265 attributes 13266 ); 13267 return super.createEl(type, props, attributes); 13268 } 13269 13270 /** 13271 * Handle `mousedown` or `touchstart` events on the `Slider`. 13272 * 13273 * @param {MouseEvent} event 13274 * `mousedown` or `touchstart` event that triggered this function 13275 * 13276 * @listens mousedown 13277 * @listens touchstart 13278 * @fires Slider#slideractive 13279 */ 13280 handleMouseDown(event) { 13281 const doc = this.bar.el_.ownerDocument; 13282 if (event.type === 'mousedown') { 13283 event.preventDefault(); 13284 } 13285 // Do not call preventDefault() on touchstart in Chrome 13286 // to avoid console warnings. Use a 'touch-action: none' style 13287 // instead to prevent unintended scrolling. 13288 // https://developers.google.com/web/updates/2017/01/scrolling-intervention 13289 if (event.type === 'touchstart' && !IS_CHROME) { 13290 event.preventDefault(); 13291 } 13292 blockTextSelection(); 13293 this.addClass('vjs-sliding'); 13294 /** 13295 * Triggered when the slider is in an active state 13296 * 13297 * @event Slider#slideractive 13298 * @type {MouseEvent} 13299 */ 13300 this.trigger('slideractive'); 13301 this.on(doc, 'mousemove', this.handleMouseMove_); 13302 this.on(doc, 'mouseup', this.handleMouseUp_); 13303 this.on(doc, 'touchmove', this.handleMouseMove_); 13304 this.on(doc, 'touchend', this.handleMouseUp_); 13305 this.handleMouseMove(event, true); 13306 } 13307 13308 /** 13309 * Handle the `mousemove`, `touchmove`, and `mousedown` events on this `Slider`. 13310 * The `mousemove` and `touchmove` events will only only trigger this function during 13311 * `mousedown` and `touchstart`. This is due to {@link Slider#handleMouseDown} and 13312 * {@link Slider#handleMouseUp}. 13313 * 13314 * @param {MouseEvent} event 13315 * `mousedown`, `mousemove`, `touchstart`, or `touchmove` event that triggered 13316 * this function 13317 * @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. 13318 * 13319 * @listens mousemove 13320 * @listens touchmove 13321 */ 13322 handleMouseMove(event) {} 13323 13324 /** 13325 * Handle `mouseup` or `touchend` events on the `Slider`. 13326 * 13327 * @param {MouseEvent} event 13328 * `mouseup` or `touchend` event that triggered this function. 13329 * 13330 * @listens touchend 13331 * @listens mouseup 13332 * @fires Slider#sliderinactive 13333 */ 13334 handleMouseUp(event) { 13335 const doc = this.bar.el_.ownerDocument; 13336 unblockTextSelection(); 13337 this.removeClass('vjs-sliding'); 13338 /** 13339 * Triggered when the slider is no longer in an active state. 13340 * 13341 * @event Slider#sliderinactive 13342 * @type {Event} 13343 */ 13344 this.trigger('sliderinactive'); 13345 this.off(doc, 'mousemove', this.handleMouseMove_); 13346 this.off(doc, 'mouseup', this.handleMouseUp_); 13347 this.off(doc, 'touchmove', this.handleMouseMove_); 13348 this.off(doc, 'touchend', this.handleMouseUp_); 13349 this.update(); 13350 } 13351 13352 /** 13353 * Update the progress bar of the `Slider`. 13354 * 13355 * @return {number} 13356 * The percentage of progress the progress bar represents as a 13357 * number from 0 to 1. 13358 */ 13359 update() { 13360 // In VolumeBar init we have a setTimeout for update that pops and update 13361 // to the end of the execution stack. The player is destroyed before then 13362 // update will cause an error 13363 // If there's no bar... 13364 if (!this.el_ || !this.bar) { 13365 return; 13366 } 13367 13368 // clamp progress between 0 and 1 13369 // and only round to four decimal places, as we round to two below 13370 const progress = this.getProgress(); 13371 if (progress === this.progress_) { 13372 return progress; 13373 } 13374 this.progress_ = progress; 13375 this.requestNamedAnimationFrame('Slider#update', () => { 13376 // Set the new bar width or height 13377 const sizeKey = this.vertical() ? 'height' : 'width'; 13378 13379 // Convert to a percentage for css value 13380 this.bar.el().style[sizeKey] = (progress * 100).toFixed(2) + '%'; 13381 }); 13382 return progress; 13383 } 13384 13385 /** 13386 * Get the percentage of the bar that should be filled 13387 * but clamped and rounded. 13388 * 13389 * @return {number} 13390 * percentage filled that the slider is 13391 */ 13392 getProgress() { 13393 return Number(clamp(this.getPercent(), 0, 1).toFixed(4)); 13394 } 13395 13396 /** 13397 * Calculate distance for slider 13398 * 13399 * @param {Event} event 13400 * The event that caused this function to run. 13401 * 13402 * @return {number} 13403 * The current position of the Slider. 13404 * - position.x for vertical `Slider`s 13405 * - position.y for horizontal `Slider`s 13406 */ 13407 calculateDistance(event) { 13408 const position = getPointerPosition(this.el_, event); 13409 if (this.vertical()) { 13410 return position.y; 13411 } 13412 return position.x; 13413 } 13414 13415 /** 13416 * Handle a `keydown` event on the `Slider`. Watches for left, right, up, and down 13417 * arrow keys. This function will only be called when the slider has focus. See 13418 * {@link Slider#handleFocus} and {@link Slider#handleBlur}. 13419 * 13420 * @param {KeyboardEvent} event 13421 * the `keydown` event that caused this function to run. 13422 * 13423 * @listens keydown 13424 */ 13425 handleKeyDown(event) { 13426 const spatialNavOptions = this.options_.playerOptions.spatialNavigation; 13427 const spatialNavEnabled = spatialNavOptions && spatialNavOptions.enabled; 13428 const horizontalSeek = spatialNavOptions && spatialNavOptions.horizontalSeek; 13429 if (spatialNavEnabled) { 13430 if ( 13431 (horizontalSeek && event.key === 'ArrowLeft') || 13432 (!horizontalSeek && event.key === 'ArrowDown') 13433 ) { 13434 event.preventDefault(); 13435 event.stopPropagation(); 13436 this.stepBack(); 13437 } else if ( 13438 (horizontalSeek && event.key === 'ArrowRight') || 13439 (!horizontalSeek && event.key === 'ArrowUp') 13440 ) { 13441 event.preventDefault(); 13442 event.stopPropagation(); 13443 this.stepForward(); 13444 } else { 13445 super.handleKeyDown(event); 13446 } 13447 13448 // Left and Down Arrows 13449 } else if (event.key === 'ArrowLeft' || event.key === 'ArrowDown') { 13450 event.preventDefault(); 13451 event.stopPropagation(); 13452 this.stepBack(); 13453 13454 // Up and Right Arrows 13455 } else if (event.key === 'ArrowUp' || event.key === 'ArrowRight') { 13456 event.preventDefault(); 13457 event.stopPropagation(); 13458 this.stepForward(); 13459 } else { 13460 // Pass keydown handling up for unsupported keys 13461 super.handleKeyDown(event); 13462 } 13463 } 13464 13465 /** 13466 * Listener for click events on slider, used to prevent clicks 13467 * from bubbling up to parent elements like button menus. 13468 * 13469 * @param {Object} event 13470 * Event that caused this object to run 13471 */ 13472 handleClick(event) { 13473 event.stopPropagation(); 13474 event.preventDefault(); 13475 } 13476 13477 /** 13478 * Get/set if slider is horizontal for vertical 13479 * 13480 * @param {boolean} [bool] 13481 * - true if slider is vertical, 13482 * - false is horizontal 13483 * 13484 * @return {boolean} 13485 * - true if slider is vertical, and getting 13486 * - false if the slider is horizontal, and getting 13487 */ 13488 vertical(bool) { 13489 if (bool === undefined) { 13490 return this.vertical_ || false; 13491 } 13492 this.vertical_ = !!bool; 13493 if (this.vertical_) { 13494 this.addClass('vjs-slider-vertical'); 13495 } else { 13496 this.addClass('vjs-slider-horizontal'); 13497 } 13498 } 13499} 13500Component$1.registerComponent('Slider', Slider); 13501 13502/** 13503 * @file load-progress-bar.js 13504 */ 13505 13506/** @import Player from '../../player' */ 13507 13508// get the percent width of a time compared to the total end 13509const percentify = (time, end) => clamp((time / end) * 100, 0, 100).toFixed(2) + '%'; 13510 13511/** 13512 * Shows loading progress 13513 * 13514 * @extends Component 13515 */ 13516class LoadProgressBar extends Component$1 { 13517 /** 13518 * Creates an instance of this class. 13519 * 13520 * @param {Player} player 13521 * The `Player` that this class should be attached to. 13522 * 13523 * @param {Object} [options] 13524 * The key/value store of player options. 13525 */ 13526 constructor(player, options) { 13527 super(player, options); 13528 this.partEls_ = [];
vendor: 16,202 bytes, lines 13529-14065
13529 this.on(player, 'progress', (e) => this.update(e)); 13530 } 13531 13532 /** 13533 * Create the `Component`'s DOM element 13534 * 13535 * @return {Element} 13536 * The element that was created. 13537 */ 13538 createEl() { 13539 const el = super.createEl('div', { 13540 className: 'vjs-load-progress', 13541 }); 13542 const wrapper = createEl('span', { 13543 className: 'vjs-control-text', 13544 }); 13545 const loadedText = createEl('span', { 13546 textContent: this.localize('Loaded'), 13547 }); 13548 const separator = document__default['default'].createTextNode(': '); 13549 this.percentageEl_ = createEl('span', { 13550 className: 'vjs-control-text-loaded-percentage', 13551 textContent: '0%', 13552 }); 13553 el.appendChild(wrapper); 13554 wrapper.appendChild(loadedText); 13555 wrapper.appendChild(separator); 13556 wrapper.appendChild(this.percentageEl_); 13557 return el; 13558 } 13559 dispose() { 13560 this.partEls_ = null; 13561 this.percentageEl_ = null; 13562 super.dispose(); 13563 } 13564 13565 /** 13566 * Update progress bar 13567 * 13568 * @param {Event} [event] 13569 * The `progress` event that caused this function to run. 13570 * 13571 * @listens Player#progress 13572 */ 13573 update(event) { 13574 this.requestNamedAnimationFrame('LoadProgressBar#update', () => { 13575 const liveTracker = this.player_.liveTracker; 13576 const buffered = this.player_.buffered(); 13577 const duration = 13578 liveTracker && liveTracker.isLive() ? liveTracker.seekableEnd() : this.player_.duration(); 13579 const bufferedEnd = this.player_.bufferedEnd(); 13580 const children = this.partEls_; 13581 const percent = percentify(bufferedEnd, duration); 13582 if (this.percent_ !== percent) { 13583 // update the width of the progress bar 13584 this.el_.style.width = percent; 13585 // update the control-text 13586 textContent(this.percentageEl_, percent); 13587 this.percent_ = percent; 13588 } 13589 13590 // add child elements to represent the individual buffered time ranges 13591 for (let i = 0; i < buffered.length; i++) { 13592 const start = buffered.start(i); 13593 const end = buffered.end(i); 13594 let part = children[i]; 13595 if (!part) { 13596 part = this.el_.appendChild(createEl()); 13597 children[i] = part; 13598 } 13599 13600 // only update if changed 13601 if (part.dataset.start === start && part.dataset.end === end) { 13602 continue; 13603 } 13604 part.dataset.start = start; 13605 part.dataset.end = end; 13606 13607 // set the percent based on the width of the progress bar (bufferedEnd) 13608 part.style.left = percentify(start, bufferedEnd); 13609 part.style.width = percentify(end - start, bufferedEnd); 13610 } 13611 13612 // remove unused buffered range elements 13613 for (let i = children.length; i > buffered.length; i--) { 13614 this.el_.removeChild(children[i - 1]); 13615 } 13616 children.length = buffered.length; 13617 }); 13618 } 13619} 13620Component$1.registerComponent('LoadProgressBar', LoadProgressBar); 13621 13622/** 13623 * @file time-tooltip.js 13624 */ 13625 13626/** @import Player from '../../player' */ 13627 13628/** 13629 * Time tooltips display a time above the progress bar. 13630 * 13631 * @extends Component 13632 */ 13633class TimeTooltip extends Component$1 { 13634 /** 13635 * Creates an instance of this class. 13636 * 13637 * @param {Player} player 13638 * The {@link Player} that this class should be attached to. 13639 * 13640 * @param {Object} [options] 13641 * The key/value store of player options. 13642 */ 13643 constructor(player, options) { 13644 super(player, options); 13645 this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL); 13646 } 13647 13648 /** 13649 * Create the time tooltip DOM element 13650 * 13651 * @return {Element} 13652 * The element that was created. 13653 */ 13654 createEl() { 13655 return super.createEl( 13656 'div', 13657 { 13658 className: 'vjs-time-tooltip', 13659 }, 13660 { 13661 'aria-hidden': 'true', 13662 } 13663 ); 13664 } 13665 13666 /** 13667 * Updates the position of the time tooltip relative to the `SeekBar`. 13668 * 13669 * @param {Object} seekBarRect 13670 * The `ClientRect` for the {@link SeekBar} element. 13671 * 13672 * @param {number} seekBarPoint 13673 * A number from 0 to 1, representing a horizontal reference point 13674 * from the left edge of the {@link SeekBar} 13675 */ 13676 update(seekBarRect, seekBarPoint, content) { 13677 const tooltipRect = findPosition(this.el_); 13678 const playerRect = getBoundingClientRect(this.player_.el()); 13679 const seekBarPointPx = seekBarRect.width * seekBarPoint; 13680 13681 // do nothing if either rect isn't available 13682 // for example, if the player isn't in the DOM for testing 13683 if (!playerRect || !tooltipRect) { 13684 return; 13685 } 13686 13687 // This is the space left of the `seekBarPoint` available within the bounds 13688 // of the player. We calculate any gap between the left edge of the player 13689 // and the left edge of the `SeekBar` and add the number of pixels in the 13690 // `SeekBar` before hitting the `seekBarPoint` 13691 let spaceLeftOfPoint = seekBarRect.left - playerRect.left + seekBarPointPx; 13692 13693 // This is the space right of the `seekBarPoint` available within the bounds 13694 // of the player. We calculate the number of pixels from the `seekBarPoint` 13695 // to the right edge of the `SeekBar` and add to that any gap between the 13696 // right edge of the `SeekBar` and the player. 13697 let spaceRightOfPoint = 13698 seekBarRect.width - seekBarPointPx + (playerRect.right - seekBarRect.right); 13699 13700 // spaceRightOfPoint is always NaN for mouse time display 13701 // because the seekbarRect does not have a right property. This causes 13702 // the mouse tool tip to be truncated when it's close to the right edge of the player. 13703 // In such cases, we ignore the `playerRect.right - seekBarRect.right` value when calculating. 13704 // For the sake of consistency, we ignore seekBarRect.left - playerRect.left for the left edge. 13705 if (!spaceRightOfPoint) { 13706 spaceRightOfPoint = seekBarRect.width - seekBarPointPx; 13707 spaceLeftOfPoint = seekBarPointPx; 13708 } 13709 // This is the number of pixels by which the tooltip will need to be pulled 13710 // further to the right to center it over the `seekBarPoint`. 13711 let pullTooltipBy = tooltipRect.width / 2; 13712 13713 // Adjust the `pullTooltipBy` distance to the left or right depending on 13714 // the results of the space calculations above. 13715 if (spaceLeftOfPoint < pullTooltipBy) { 13716 pullTooltipBy += pullTooltipBy - spaceLeftOfPoint; 13717 } else if (spaceRightOfPoint < pullTooltipBy) { 13718 pullTooltipBy = spaceRightOfPoint; 13719 } 13720 13721 // Due to the imprecision of decimal/ratio based calculations and varying 13722 // rounding behaviors, there are cases where the spacing adjustment is off 13723 // by a pixel or two. This adds insurance to these calculations. 13724 if (pullTooltipBy < 0) { 13725 pullTooltipBy = 0; 13726 } else if (pullTooltipBy > tooltipRect.width) { 13727 pullTooltipBy = tooltipRect.width; 13728 } 13729 13730 // prevent small width fluctuations within 0.4px from 13731 // changing the value below. 13732 // This really helps for live to prevent the play 13733 // progress time tooltip from jittering 13734 pullTooltipBy = Math.round(pullTooltipBy); 13735 this.el_.style.right = `-${pullTooltipBy}px`; 13736 this.write(content); 13737 } 13738 13739 /** 13740 * Write the time to the tooltip DOM element. 13741 * 13742 * @param {string} content 13743 * The formatted time for the tooltip. 13744 */ 13745 write(content) { 13746 textContent(this.el_, content); 13747 } 13748 13749 /** 13750 * Updates the position of the time tooltip relative to the `SeekBar`. 13751 * 13752 * @param {Object} seekBarRect 13753 * The `ClientRect` for the {@link SeekBar} element. 13754 * 13755 * @param {number} seekBarPoint 13756 * A number from 0 to 1, representing a horizontal reference point 13757 * from the left edge of the {@link SeekBar} 13758 * 13759 * @param {number} time 13760 * The time to update the tooltip to, not used during live playback 13761 * 13762 * @param {Function} cb 13763 * A function that will be called during the request animation frame 13764 * for tooltips that need to do additional animations from the default 13765 */ 13766 updateTime(seekBarRect, seekBarPoint, time, cb) { 13767 this.requestNamedAnimationFrame('TimeTooltip#updateTime', () => { 13768 let content; 13769 const duration = this.player_.duration(); 13770 if (this.player_.liveTracker && this.player_.liveTracker.isLive()) { 13771 const liveWindow = this.player_.liveTracker.liveWindow(); 13772 const secondsBehind = liveWindow - seekBarPoint * liveWindow; 13773 content = (secondsBehind < 1 ? '' : '-') + formatTime(secondsBehind, liveWindow); 13774 } else { 13775 content = formatTime(time, duration); 13776 } 13777 this.update(seekBarRect, seekBarPoint, content); 13778 if (cb) { 13779 cb(); 13780 } 13781 }); 13782 } 13783} 13784Component$1.registerComponent('TimeTooltip', TimeTooltip); 13785 13786/** 13787 * @file play-progress-bar.js 13788 */ 13789 13790/** 13791 * Used by {@link SeekBar} to display media playback progress as part of the 13792 * {@link ProgressControl}. 13793 * 13794 * @extends Component 13795 */ 13796class PlayProgressBar extends Component$1 { 13797 /** 13798 * Creates an instance of this class. 13799 * 13800 * @param {Player} player 13801 * The {@link Player} that this class should be attached to. 13802 * 13803 * @param {Object} [options] 13804 * The key/value store of player options. 13805 */ 13806 constructor(player, options) { 13807 super(player, options); 13808 this.setIcon('circle'); 13809 this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL); 13810 } 13811 13812 /** 13813 * Create the the DOM element for this class. 13814 * 13815 * @return {Element} 13816 * The element that was created. 13817 */ 13818 createEl() { 13819 return super.createEl( 13820 'div', 13821 { 13822 className: 'vjs-play-progress vjs-slider-bar', 13823 }, 13824 { 13825 'aria-hidden': 'true', 13826 } 13827 ); 13828 } 13829 13830 /** 13831 * Enqueues updates to its own DOM as well as the DOM of its 13832 * {@link TimeTooltip} child. 13833 * 13834 * @param {Object} seekBarRect 13835 * The `ClientRect` for the {@link SeekBar} element. 13836 * 13837 * @param {number} seekBarPoint 13838 * A number from 0 to 1, representing a horizontal reference point 13839 * from the left edge of the {@link SeekBar} 13840 */ 13841 update(seekBarRect, seekBarPoint) { 13842 const timeTooltip = this.getChild('timeTooltip'); 13843 if (!timeTooltip) { 13844 return; 13845 } 13846 const time = this.player_.scrubbing() 13847 ? this.player_.getCache().currentTime 13848 : this.player_.currentTime(); 13849 timeTooltip.updateTime(seekBarRect, seekBarPoint, time); 13850 } 13851} 13852 13853/** 13854 * Default options for {@link PlayProgressBar}. 13855 * 13856 * @type {Object} 13857 * @private 13858 */ 13859PlayProgressBar.prototype.options_ = { 13860 children: [], 13861}; 13862 13863// Time tooltips should not be added to a player on mobile devices 13864if (!IS_IOS && !IS_ANDROID) { 13865 PlayProgressBar.prototype.options_.children.push('timeTooltip'); 13866} 13867Component$1.registerComponent('PlayProgressBar', PlayProgressBar); 13868 13869/** 13870 * @file mouse-time-display.js 13871 */ 13872 13873/** 13874 * The {@link MouseTimeDisplay} component tracks mouse movement over the 13875 * {@link ProgressControl}. It displays an indicator and a {@link TimeTooltip} 13876 * indicating the time which is represented by a given point in the 13877 * {@link ProgressControl}. 13878 * 13879 * @extends Component 13880 */ 13881class MouseTimeDisplay extends Component$1 { 13882 /** 13883 * Creates an instance of this class. 13884 * 13885 * @param {Player} player 13886 * The {@link Player} that this class should be attached to. 13887 * 13888 * @param {Object} [options] 13889 * The key/value store of player options. 13890 */ 13891 constructor(player, options) { 13892 super(player, options); 13893 this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL); 13894 } 13895 13896 /** 13897 * Create the DOM element for this class. 13898 * 13899 * @return {Element} 13900 * The element that was created. 13901 */ 13902 createEl() { 13903 return super.createEl('div', { 13904 className: 'vjs-mouse-display', 13905 }); 13906 } 13907 13908 /** 13909 * Enqueues updates to its own DOM as well as the DOM of its 13910 * {@link TimeTooltip} child. 13911 * 13912 * @param {Object} seekBarRect 13913 * The `ClientRect` for the {@link SeekBar} element. 13914 * 13915 * @param {number} seekBarPoint 13916 * A number from 0 to 1, representing a horizontal reference point 13917 * from the left edge of the {@link SeekBar} 13918 */ 13919 update(seekBarRect, seekBarPoint) { 13920 const time = seekBarPoint * this.player_.duration(); 13921 this.getChild('timeTooltip').updateTime(seekBarRect, seekBarPoint, time, () => { 13922 this.el_.style.left = `${seekBarRect.width * seekBarPoint}px`; 13923 }); 13924 } 13925} 13926 13927/** 13928 * Default options for `MouseTimeDisplay` 13929 * 13930 * @type {Object} 13931 * @private 13932 */ 13933MouseTimeDisplay.prototype.options_ = { 13934 children: ['timeTooltip'], 13935}; 13936Component$1.registerComponent('MouseTimeDisplay', MouseTimeDisplay); 13937 13938/** 13939 * @file seek-bar.js 13940 */ 13941 13942// The number of seconds the `step*` functions move the timeline. 13943const STEP_SECONDS = 5; 13944 13945// The multiplier of STEP_SECONDS that PgUp/PgDown move the timeline. 13946const PAGE_KEY_MULTIPLIER = 12; 13947 13948/** 13949 * Seek bar and container for the progress bars. Uses {@link PlayProgressBar} 13950 * as its `bar`. 13951 * 13952 * @extends Slider 13953 */ 13954class SeekBar extends Slider { 13955 /** 13956 * Creates an instance of this class. 13957 * 13958 * @param {Player} player 13959 * The `Player` that this class should be attached to. 13960 * 13961 * @param {Object} [options] 13962 * The key/value store of player options. 13963 */ 13964 constructor(player, options) { 13965 super(player, options); 13966 this.setEventHandlers_(); 13967 } 13968 13969 /** 13970 * Sets the event handlers 13971 * 13972 * @private 13973 */ 13974 setEventHandlers_() { 13975 this.update_ = bind_(this, this.update); 13976 this.update = throttle(this.update_, UPDATE_REFRESH_INTERVAL); 13977 this.on(this.player_, ['durationchange', 'timeupdate'], this.update); 13978 this.on(this.player_, ['ended'], this.update_); 13979 if (this.player_.liveTracker) { 13980 this.on(this.player_.liveTracker, 'liveedgechange', this.update); 13981 } 13982 13983 // when playing, let's ensure we smoothly update the play progress bar 13984 // via an interval 13985 this.updateInterval = null; 13986 this.enableIntervalHandler_ = (e) => this.enableInterval_(e); 13987 this.disableIntervalHandler_ = (e) => this.disableInterval_(e); 13988 this.on(this.player_, ['playing'], this.enableIntervalHandler_); 13989 this.on(this.player_, ['ended', 'pause', 'waiting'], this.disableIntervalHandler_); 13990 13991 // we don't need to update the play progress if the document is hidden, 13992 // also, this causes the CPU to spike and eventually crash the page on IE11. 13993 if ( 13994 'hidden' in document__default['default'] && 13995 'visibilityState' in document__default['default'] 13996 ) { 13997 this.on(document__default['default'], 'visibilitychange', this.toggleVisibility_); 13998 } 13999 } 14000 toggleVisibility_(e) { 14001 if (document__default['default'].visibilityState === 'hidden') { 14002 this.cancelNamedAnimationFrame('SeekBar#update'); 14003 this.cancelNamedAnimationFrame('Slider#update'); 14004 this.disableInterval_(e); 14005 } else { 14006 if (!this.player_.ended() && !this.player_.paused()) { 14007 this.enableInterval_(); 14008 } 14009 14010 // we just switched back to the page and someone may be looking, so, update ASAP 14011 this.update(); 14012 } 14013 } 14014 enableInterval_() { 14015 if (this.updateInterval) { 14016 return; 14017 } 14018 this.updateInterval = this.setInterval(this.update, UPDATE_REFRESH_INTERVAL); 14019 } 14020 disableInterval_(e) { 14021 if (this.player_.liveTracker && this.player_.liveTracker.isLive() && e && e.type !== 'ended') { 14022 return; 14023 } 14024 if (!this.updateInterval) { 14025 return; 14026 } 14027 this.clearInterval(this.updateInterval); 14028 this.updateInterval = null; 14029 } 14030 14031 /** 14032 * Create the `Component`'s DOM element 14033 * 14034 * @return {Element} 14035 * The element that was created. 14036 */ 14037 createEl() { 14038 return super.createEl( 14039 'div', 14040 { 14041 className: 'vjs-progress-holder', 14042 }, 14043 { 14044 'aria-label': this.localize('Progress Bar'), 14045 } 14046 ); 14047 } 14048 14049 /** 14050 * This function updates the play progress bar and accessibility 14051 * attributes to whatever is passed in. 14052 * 14053 * @param {Event} [event] 14054 * The `timeupdate` or `ended` event that caused this to run. 14055 * 14056 * @listens Player#timeupdate 14057 * 14058 * @return {number} 14059 * The current percent at a number from 0-1 14060 */ 14061 update(event) { 14062 // ignore updates while the tab is hidden 14063 if (document__default['default'].visibilityState === 'hidden') { 14064 return; 14065 }
vendor: 3,509 bytes, lines 14066-14178
14066 const percent = super.update(); 14067 this.requestNamedAnimationFrame('SeekBar#update', () => { 14068 const currentTime = this.player_.ended() ? this.player_.duration() : this.getCurrentTime_(); 14069 const liveTracker = this.player_.liveTracker; 14070 let duration = this.player_.duration(); 14071 if (liveTracker && liveTracker.isLive()) { 14072 duration = this.player_.liveTracker.liveCurrentTime(); 14073 } 14074 if (this.percent_ !== percent) { 14075 // machine readable value of progress bar (percentage complete) 14076 this.el_.setAttribute('aria-valuenow', (percent * 100).toFixed(2)); 14077 this.percent_ = percent; 14078 } 14079 if (this.currentTime_ !== currentTime || this.duration_ !== duration) { 14080 // human readable value of progress bar (time complete) 14081 this.el_.setAttribute( 14082 'aria-valuetext', 14083 this.localize( 14084 'progress bar timing: currentTime={1} duration={2}', 14085 [formatTime(currentTime, duration), formatTime(duration, duration)], 14086 '{1} of {2}' 14087 ) 14088 ); 14089 this.currentTime_ = currentTime; 14090 this.duration_ = duration; 14091 } 14092 14093 // update the progress bar time tooltip with the current time 14094 if (this.bar) { 14095 this.bar.update(getBoundingClientRect(this.el()), this.getProgress()); 14096 } 14097 }); 14098 return percent; 14099 } 14100 14101 /** 14102 * Prevent liveThreshold from causing seeks to seem like they 14103 * are not happening from a user perspective. 14104 * 14105 * @param {number} ct 14106 * current time to seek to 14107 */ 14108 userSeek_(ct) { 14109 if (this.player_.liveTracker && this.player_.liveTracker.isLive()) { 14110 this.player_.liveTracker.nextSeekedFromUser(); 14111 } 14112 this.player_.currentTime(ct); 14113 } 14114 14115 /** 14116 * Get the value of current time but allows for smooth scrubbing, 14117 * when player can't keep up. 14118 * 14119 * @return {number} 14120 * The current time value to display 14121 * 14122 * @private 14123 */ 14124 getCurrentTime_() { 14125 return this.player_.scrubbing() 14126 ? this.player_.getCache().currentTime 14127 : this.player_.currentTime(); 14128 } 14129 14130 /** 14131 * Get the percentage of media played so far. 14132 * 14133 * @return {number} 14134 * The percentage of media played so far (0 to 1). 14135 */ 14136 getPercent() { 14137 const currentTime = this.getCurrentTime_(); 14138 let percent; 14139 const liveTracker = this.player_.liveTracker; 14140 if (liveTracker && liveTracker.isLive()) { 14141 percent = (currentTime - liveTracker.seekableStart()) / liveTracker.liveWindow(); 14142 14143 // prevent the percent from changing at the live edge 14144 if (liveTracker.atLiveEdge()) { 14145 percent = 1; 14146 } 14147 } else { 14148 percent = currentTime / this.player_.duration(); 14149 } 14150 return percent; 14151 } 14152 14153 /** 14154 * Handle mouse down on seek bar 14155 * 14156 * @param {MouseEvent} event 14157 * The `mousedown` event that caused this to run. 14158 * 14159 * @listens mousedown 14160 */ 14161 handleMouseDown(event) { 14162 if (!isSingleLeftClick(event)) { 14163 return; 14164 } 14165 14166 // Stop event propagation to prevent double fire in progress-control.js 14167 event.stopPropagation(); 14168 this.videoWasPlaying = !this.player_.paused(); 14169 this.player_.pause(); 14170 super.handleMouseDown(event); 14171 } 14172 14173 /** 14174 * Handle mouse move on seek bar 14175 * 14176 * @param {MouseEvent} event 14177 * The `mousemove` event that caused this to run. 14178 * @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 c
14178oming from mouse down but should happen on regular mouse move handler. Defaults to false 14179 * 14180 * @listens mousemove 14181 */ 14182 handleMouseMove(event, mouseDown = false) { 14183 if (!isSingleLeftClick(event) || isNaN(this.player_.duration())) { 14184 return; 14185 } 14186 if (!mouseDown && !this.player_.scrubbing()) { 14187 this.player_.scrubbing(true); 14188 } 14189 let newTime; 14190 const distance = this.calculateDistance(event); 14191 const liveTracker = this.player_.liveTracker; 14192 if (!liveTracker || !liveTracker.isLive()) { 14193 newTime = distance * this.player_.duration(); 14194 14195 // Don't let video end while scrubbing. 14196 if (newTime === this.player_.duration()) { 14197 newTime = newTime - 0.1; 14198 } 14199 } else { 14200 if (distance >= 0.99) { 14201 liveTracker.seekToLiveEdge(); 14202 return; 14203 } 14204 const seekableStart = liveTracker.seekableStart(); 14205 const seekableEnd = liveTracker.liveCurrentTime(); 14206 newTime = seekableStart + distance * liveTracker.liveWindow(); 14207 14208 // Don't let video end while scrubbing. 14209 if (newTime >= seekableEnd) { 14210 newTime = seekableEnd; 14211 } 14212 14213 // Compensate for precision differences so that currentTime is not less 14214 // than seekable start 14215 if (newTime <= seekableStart) { 14216 newTime = seekableStart + 0.1; 14217 } 14218 14219 // On android seekableEnd can be Infinity sometimes, 14220 // this will cause newTime to be Infinity, which is 14221 // not a valid currentTime. 14222 if (newTime === Infinity) { 14223 return; 14224 } 14225 } 14226 14227 // Set new time (tell player to seek to new time) 14228 this.userSeek_(newTime); 14229 if (this.player_.options_.enableSmoothSeeking) { 14230 this.update(); 14231 } 14232 } 14233 enable() { 14234 super.enable(); 14235 const mouseTimeDisplay = this.getChild('mouseTimeDisplay'); 14236 if (!mouseTimeDisplay) { 14237 return; 14238 } 14239 mouseTimeDisplay.show(); 14240 } 14241 disable() { 14242 super.disable(); 14243 const mouseTimeDisplay = this.getChild('mouseTimeDisplay'); 14244 if (!mouseTimeDisplay) { 14245 return; 14246 } 14247 mouseTimeDisplay.hide(); 14248 } 14249 14250 /** 14251 * Handle mouse up on seek bar 14252 * 14253 * @param {MouseEvent} event 14254 * The `mouseup` event that caused this to run. 14255 * 14256 * @listens mouseup 14257 */ 14258 handleMouseUp(event) { 14259 super.handleMouseUp(event); 14260 14261 // Stop event propagation to prevent double fire in progress-control.js 14262 if (event) { 14263 event.stopPropagation(); 14264 } 14265 this.player_.scrubbing(false); 14266 14267 /** 14268 * Trigger timeupdate because we're done seeking and the time has changed. 14269 * This is particularly useful for if the player is paused to time the time displays. 14270 * 14271 * @event Tech#timeupdate 14272 * @type {Event} 14273 */ 14274 this.player_.trigger({ 14275 type: 'timeupdate', 14276 target: this, 14277 manuallyTriggered: true, 14278 }); 14279 if (this.videoWasPlaying) { 14280 silencePromise(this.player_.play()); 14281 } else { 14282 // We're done seeking and the time has changed. 14283 // If the player is paused, make sure we display the correct time on the seek bar. 14284 this.update_(); 14285 } 14286 } 14287 14288 /** 14289 * Move more quickly fast forward for keyboard-only users 14290 */ 14291 stepForward() { 14292 this.userSeek_(this.player_.currentTime() + STEP_SECONDS); 14293 } 14294 14295 /** 14296 * Move more quickly rewind for keyboard-only users 14297 */ 14298 stepBack() { 14299 this.userSeek_(this.player_.currentTime() - STEP_SECONDS); 14300 } 14301 14302 /** 14303 * Toggles the playback state of the player 14304 * This gets called when enter or space is used on the seekbar 14305 * 14306 * @param {KeyboardEvent} event 14307 * The `keydown` event that caused this function to be called 14308 * 14309 */ 14310 handleAction(event) { 14311 if (this.player_.paused()) { 14312 this.player_.play(); 14313 } else { 14314 this.player_.pause(); 14315 } 14316 } 14317 14318 /** 14319 * Called when this SeekBar has focus and a key gets pressed down. 14320 * Supports the following keys: 14321 * 14322 * Space or Enter key fire a click event 14323 * Home key moves to start of the timeline 14324 * End key moves to end of the timeline 14325 * Digit "0" through "9" keys move to 0%, 10% ... 80%, 90% of the timeline 14326 * PageDown key moves back a larger step than ArrowDown 14327 * PageUp key moves forward a large step 14328 * 14329 * @param {KeyboardEvent} event
vendor: 5,904 bytes, lines 14330-14519
14330 * The `keydown` event that caused this function to be called. 14331 * 14332 * @listens keydown 14333 */ 14334 handleKeyDown(event) { 14335 const liveTracker = this.player_.liveTracker; 14336 if (event.key === ' ' || event.key === 'Enter') { 14337 event.preventDefault(); 14338 event.stopPropagation(); 14339 this.handleAction(event); 14340 } else if (event.key === 'Home') { 14341 event.preventDefault(); 14342 event.stopPropagation(); 14343 this.userSeek_(0); 14344 } else if (event.key === 'End') { 14345 event.preventDefault(); 14346 event.stopPropagation(); 14347 if (liveTracker && liveTracker.isLive()) { 14348 this.userSeek_(liveTracker.liveCurrentTime()); 14349 } else { 14350 this.userSeek_(this.player_.duration()); 14351 } 14352 } else if (/^[0-9]$/.test(event.key)) { 14353 event.preventDefault(); 14354 event.stopPropagation(); 14355 const gotoFraction = parseInt(event.key, 10) * 0.1; 14356 if (liveTracker && liveTracker.isLive()) { 14357 this.userSeek_(liveTracker.seekableStart() + liveTracker.liveWindow() * gotoFraction); 14358 } else { 14359 this.userSeek_(this.player_.duration() * gotoFraction); 14360 } 14361 } else if (event.key === 'PageDown') { 14362 event.preventDefault(); 14363 event.stopPropagation(); 14364 this.userSeek_(this.player_.currentTime() - STEP_SECONDS * PAGE_KEY_MULTIPLIER); 14365 } else if (event.key === 'PageUp') { 14366 event.preventDefault(); 14367 event.stopPropagation(); 14368 this.userSeek_(this.player_.currentTime() + STEP_SECONDS * PAGE_KEY_MULTIPLIER); 14369 } else { 14370 // Pass keydown handling up for unsupported keys 14371 super.handleKeyDown(event); 14372 } 14373 } 14374 dispose() { 14375 this.disableInterval_(); 14376 this.off(this.player_, ['durationchange', 'timeupdate'], this.update); 14377 this.off(this.player_, ['ended'], this.update_); 14378 if (this.player_.liveTracker) { 14379 this.off(this.player_.liveTracker, 'liveedgechange', this.update); 14380 } 14381 this.off(this.player_, ['playing'], this.enableIntervalHandler_); 14382 this.off(this.player_, ['ended', 'pause', 'waiting'], this.disableIntervalHandler_); 14383 14384 // we don't need to update the play progress if the document is hidden, 14385 // also, this causes the CPU to spike and eventually crash the page on IE11. 14386 if ( 14387 'hidden' in document__default['default'] && 14388 'visibilityState' in document__default['default'] 14389 ) { 14390 this.off(document__default['default'], 'visibilitychange', this.toggleVisibility_); 14391 } 14392 super.dispose(); 14393 } 14394} 14395 14396/** 14397 * Default options for the `SeekBar` 14398 * 14399 * @type {Object} 14400 * @private 14401 */ 14402SeekBar.prototype.options_ = { 14403 children: ['loadProgressBar', 'playProgressBar'], 14404 barName: 'playProgressBar', 14405}; 14406 14407// MouseTimeDisplay tooltips should not be added to a player on mobile devices 14408if (!IS_IOS && !IS_ANDROID) { 14409 SeekBar.prototype.options_.children.splice(1, 0, 'mouseTimeDisplay'); 14410} 14411Component$1.registerComponent('SeekBar', SeekBar); 14412 14413/** 14414 * @file progress-control.js 14415 */ 14416 14417/** 14418 * The Progress Control component contains the seek bar, load progress, 14419 * and play progress. 14420 * 14421 * @extends Component 14422 */ 14423class ProgressControl extends Component$1 { 14424 /** 14425 * Creates an instance of this class. 14426 * 14427 * @param {Player} player 14428 * The `Player` that this class should be attached to. 14429 * 14430 * @param {Object} [options] 14431 * The key/value store of player options. 14432 */ 14433 constructor(player, options) { 14434 super(player, options); 14435 this.handleMouseMove = throttle(bind_(this, this.handleMouseMove), UPDATE_REFRESH_INTERVAL); 14436 this.throttledHandleMouseSeek = throttle( 14437 bind_(this, this.handleMouseSeek), 14438 UPDATE_REFRESH_INTERVAL 14439 ); 14440 this.handleMouseUpHandler_ = (e) => this.handleMouseUp(e); 14441 this.handleMouseDownHandler_ = (e) => this.handleMouseDown(e); 14442 this.enable(); 14443 } 14444 14445 /** 14446 * Create the `Component`'s DOM element 14447 * 14448 * @return {Element} 14449 * The element that was created. 14450 */ 14451 createEl() { 14452 return super.createEl('div', { 14453 className: 'vjs-progress-control vjs-control', 14454 }); 14455 } 14456 14457 /** 14458 * When the mouse moves over the `ProgressControl`, the pointer position 14459 * gets passed down to the `MouseTimeDisplay` component. 14460 * 14461 * @param {Event} event 14462 * The `mousemove` event that caused this function to run. 14463 * 14464 * @listen mousemove 14465 */ 14466 handleMouseMove(event) { 14467 const seekBar = this.getChild('seekBar'); 14468 if (!seekBar) { 14469 return; 14470 } 14471 const playProgressBar = seekBar.getChild('playProgressBar'); 14472 const mouseTimeDisplay = seekBar.getChild('mouseTimeDisplay'); 14473 if (!playProgressBar && !mouseTimeDisplay) { 14474 return; 14475 } 14476 const seekBarEl = seekBar.el(); 14477 const seekBarRect = findPosition(seekBarEl); 14478 let seekBarPoint = getPointerPosition(seekBarEl, event).x; 14479 14480 // The default skin has a gap on either side of the `SeekBar`. This means 14481 // that it's possible to trigger this behavior outside the boundaries of 14482 // the `SeekBar`. This ensures we stay within it at all times. 14483 seekBarPoint = clamp(seekBarPoint, 0, 1); 14484 if (mouseTimeDisplay) { 14485 mouseTimeDisplay.update(seekBarRect, seekBarPoint); 14486 } 14487 if (playProgressBar) { 14488 playProgressBar.update(seekBarRect, seekBar.getProgress()); 14489 } 14490 } 14491 14492 /** 14493 * A throttled version of the {@link ProgressControl#handleMouseSeek} listener. 14494 * 14495 * @method ProgressControl#throttledHandleMouseSeek 14496 * @param {Event} event 14497 * The `mousemove` event that caused this function to run. 14498 * 14499 * @listen mousemove 14500 * @listen touchmove 14501 */ 14502 14503 /** 14504 * Handle `mousemove` or `touchmove` events on the `ProgressControl`. 14505 * 14506 * @param {Event} event 14507 * `mousedown` or `touchstart` event that triggered this function 14508 * 14509 * @listens mousemove 14510 * @listens touchmove 14511 */ 14512 handleMouseSeek(event) { 14513 const seekBar = this.getChild('seekBar'); 14514 if (seekBar) { 14515 seekBar.handleMouseMove(event); 14516 } 14517 } 14518 14519 /**
14520 * Are controls are currently enabled for this progress control. 14521 * 14522 * @return {boolean} 14523 * true if controls are enabled, false otherwise 14524 */ 14525 enabled() { 14526 return this.enabled_; 14527 } 14528 14529 /** 14530 * Disable all controls on the progress control and its children 14531 */ 14532 disable() { 14533 this.children().forEach((child) => child.disable && child.disable()); 14534 if (!this.enabled()) { 14535 return; 14536 } 14537 this.off(['mousedown', 'touchstart'], this.handleMouseDownHandler_); 14538 this.off(this.el_, 'mousemove', this.handleMouseMove); 14539 this.removeListenersAddedOnMousedownAndTouchstart(); 14540 this.addClass('disabled'); 14541 this.enabled_ = false; 14542 14543 // Restore normal playback state if controls are disabled while scrubbing 14544 if (this.player_.scrubbing()) { 14545 const seekBar = this.getChild('seekBar'); 14546 this.player_.scrubbing(false); 14547 if (seekBar.videoWasPlaying) { 14548 silencePromise(this.player_.play()); 14549 } 14550 } 14551 } 14552 14553 /** 14554 * Enable all controls on the progress control and its children 14555 */ 14556 enable() {
14557 this.children().forEach((child) => child.enable && child.enable()); 14558 if (this.enabled()) { 14559 return; 14560 } 14561 this.on(['mousedown', 'touchstart'], this.handleMouseDownHandler_); 14562 this.on(this.el_, 'mousemove', this.handleMouseMove); 14563 this.removeClass('disabled'); 14564 this.enabled_ = true; 14565 } 14566 14567 /** 14568 * Cleanup listeners after the user finishes interacting with the progress controls 14569 */ 14570 removeListenersAddedOnMousedownAndTouchstart() { 14571 const doc = this.el_.ownerDocument; 14572 this.off(doc, 'mousemove', this.throttledHandleMouseSeek); 14573 this.off(doc, 'touchmove', this.throttledHandleMouseSeek); 14574 this.off(doc, 'mouseup', this.handleMouseUpHandler_); 14575 this.off(doc, 'touchend', this.handleMouseUpHandler_); 14576 } 14577 14578 /** 14579 * Handle `mousedown` or `touchstart` events on the `ProgressControl`. 14580 * 14581 * @param {Event} event 14582 * `mousedown` or `touchstart` event that triggered this function 14583 * 14584 * @listens mousedown 14585 * @listens touchstart 14586 */ 14587 handleMouseDown(event) { 14588 const doc = this.el_.ownerDocument; 14589 const seekBar = this.getChild('seekBar'); 14590 if (seekBar) { 14591 seekBar.handleMouseDown(event); 14592 } 14593 this.on(doc, 'mousemove', this.throttledHandleMouseSeek); 14594 this.on(doc, 'touchmove', this.throttledHandleMouseSeek); 14595 this.on(doc, 'mouseup', this.handleMouseUpHandler_); 14596 this.on(doc, 'touchend', this.handleMouseUpHandler_); 14597 } 14598 14599 /** 14600 * Handle `mouseup` or `touchend` events on the `ProgressControl`. 14601 * 14602 * @param {Event} event 14603 * `mouseup` or `touchend` event that triggered this function. 14604 * 14605 * @listens touchend 14606 * @listens mouseup 14607 */ 14608 handleMouseUp(event) { 14609 const seekBar = this.getChild('seekBar'); 14610 if (seekBar) { 14611 seekBar.handleMouseUp(event); 14612 } 14613 this.removeListenersAddedOnMousedownAndTouchstart(); 14614 } 14615} 14616 14617/** 14618 * Default options for `ProgressControl` 14619 * 14620 * @type {Object} 14621 * @private 14622 */ 14623ProgressControl.prototype.options_ = { 14624 children: ['seekBar'], 14625}; 14626Component$1.registerComponent('ProgressControl', ProgressControl); 14627 14628/** 14629 * @file picture-in-picture-toggle.js 14630 */ 14631 14632/** @import Player from './player' */ 14633 14634/** 14635 * Toggle Picture-in-Picture mode 14636 * 14637 * @extends Button 14638 */ 14639class PictureInPictureToggle extends Button { 14640 /** 14641 * Creates an instance of this class. 14642 * 14643 * @param {Player} player 14644 * The `Player` that this class should be attached to. 14645 * 14646 * @param {Object} [options] 14647 * The key/value store of player options. 14648 * 14649 * @listens Player#enterpictureinpicture 14650 * @listens Player#leavepictureinpicture 14651 */ 14652 constructor(player, options) { 14653 super(player, options); 14654 this.setIcon('picture-in-picture-enter'); 14655 this.on(player, ['enterpictureinpicture', 'leavepictureinpicture'], (e) => 14656 this.handlePictureInPictureChange(e) 14657 ); 14658 this.on(player, ['disablepictureinpicturechanged', 'loadedmetadata'], (e) => 14659 this.handlePictureInPictureEnabledChange(e) 14660 ); 14661 this.on(player, ['loadedmetadata', 'audioonlymodechange', 'audiopostermodechange'], () => 14662 this.handlePictureInPictureAudioModeChange() 14663 ); 14664 14665 // TODO: Deactivate button on player emptied event. 14666 this.disable(); 14667 } 14668 14669 /** 14670 * Builds the default DOM `className`. 14671 * 14672 * @return {string} 14673 * The DOM `className` for this object. 14674 */ 14675 buildCSSClass() { 14676 return `vjs-picture-in-picture-control vjs-hidden ${super.buildCSSClass()}`; 14677 } 14678 14679 /** 14680 * Displays or hides the button depending on the audio mode detection. 14681 * Exits picture-in-picture if it is enabled when switching to audio mode. 14682 */ 14683 handlePictureInPictureAudioModeChange() { 14684 // This audio detection will not detect HLS or DASH audio-only streams because there was no reliable way to detect them at the time 14685 const isSourceAudio = this.player_.currentType().substring(0, 5) === 'audio'; 14686 const isAudioMode = 14687 isSourceAudio || this.player_.audioPosterMode() || this.player_.audioOnlyMode(); 14688 if (!isAudioMode) { 14689 this.show(); 14690 return; 14691 } 14692 if (this.player_.isInPictureInPicture()) { 14693 this.player_.exitPictureInPicture(); 14694 } 14695 this.hide(); 14696 } 14697 14698 /** 14699 * Enables or disables button based on availability of a Picture-In-Picture mode. 14700 * 14701 * Enabled if 14702 * - `player.options().enableDocumentPictureInPicture` is true and 14703 * window.documentPictureInPicture is available; or 14704 * - `player.disablePictureInPicture()` is false and 14705 * element.requestPictureInPicture is available 14706 */ 14707 handlePictureInPictureEnabledChange() { 14708 if ( 14709 (document__default['default'].pictureInPictureEnabled && 14710 this.player_.disablePictureInPicture() === false) || 14711 (this.player_.options_.enableDocumentPictureInPicture && 14712 'documentPictureInPicture' in window__default['default']) 14713 ) { 14714 this.enable(); 14715 } else { 14716 this.disable(); 14717 } 14718 } 14719 14720 /** 14721 * Handles enterpictureinpicture and leavepictureinpicture on the player and change control text accordingly. 14722 * 14723 * @param {Event} [event] 14724 * The {@link Player#enterpictureinpicture} or {@link Player#leavepictureinpicture} event that caused this function to be 14725 * called. 14726 * 14727 * @listens Player#enterpictureinpicture 14728 * @listens Player#leavepictureinpicture 14729 */ 14730 handlePictureInPictureChange(event) { 14731 if (this.player_.isInPictureInPicture()) { 14732 this.setIcon('picture-in-picture-exit'); 14733 this.controlText('Exit Picture-in-Picture'); 14734 } else { 14735 this.setIcon('picture-in-picture-enter'); 14736 this.controlText('Picture-in-Picture'); 14737 } 14738 this.handlePictureInPictureEnabledChange(); 14739 } 14740 14741 /** 14742 * This gets called when an `PictureInPictureToggle` is "clicked". See 14743 * {@link ClickableComponent} for more detailed information on what a click can be. 14744 * 14745 * @param {Event} [event]
vendor: 6,879 bytes, lines 14746-15004
14746 * The `keydown`, `tap`, or `click` event that caused this function to be 14747 * called. 14748 * 14749 * @listens tap 14750 * @listens click 14751 */ 14752 handleClick(event) { 14753 if (!this.player_.isInPictureInPicture()) { 14754 this.player_.requestPictureInPicture(); 14755 } else { 14756 this.player_.exitPictureInPicture(); 14757 } 14758 } 14759 14760 /** 14761 * Show the `Component`s element if it is hidden by removing the 14762 * 'vjs-hidden' class name from it only in browsers that support the Picture-in-Picture API. 14763 */ 14764 show() { 14765 // Does not allow to display the pictureInPictureToggle in browsers that do not support the Picture-in-Picture API, e.g. Firefox. 14766 if (typeof document__default['default'].exitPictureInPicture !== 'function') { 14767 return; 14768 } 14769 super.show(); 14770 } 14771} 14772 14773/** 14774 * The text that should display over the `PictureInPictureToggle`s controls. Added for localization. 14775 * 14776 * @type {string} 14777 * @protected 14778 */ 14779PictureInPictureToggle.prototype.controlText_ = 'Picture-in-Picture'; 14780Component$1.registerComponent('PictureInPictureToggle', PictureInPictureToggle); 14781 14782/** 14783 * @file fullscreen-toggle.js 14784 */ 14785 14786/** @import Player from './player' */ 14787 14788/** 14789 * Toggle fullscreen video 14790 * 14791 * @extends Button 14792 */ 14793class FullscreenToggle extends Button { 14794 /** 14795 * Creates an instance of this class. 14796 * 14797 * @param {Player} player 14798 * The `Player` that this class should be attached to. 14799 * 14800 * @param {Object} [options] 14801 * The key/value store of player options. 14802 */ 14803 constructor(player, options) { 14804 super(player, options); 14805 this.setIcon('fullscreen-enter'); 14806 this.on(player, 'fullscreenchange', (e) => this.handleFullscreenChange(e)); 14807 if (document__default['default'][player.fsApi_.fullscreenEnabled] === false) { 14808 this.disable(); 14809 } 14810 } 14811 14812 /** 14813 * Builds the default DOM `className`. 14814 * 14815 * @return {string} 14816 * The DOM `className` for this object. 14817 */ 14818 buildCSSClass() { 14819 return `vjs-fullscreen-control ${super.buildCSSClass()}`; 14820 } 14821 14822 /** 14823 * Handles fullscreenchange on the player and change control text accordingly. 14824 * 14825 * @param {Event} [event] 14826 * The {@link Player#fullscreenchange} event that caused this function to be 14827 * called. 14828 * 14829 * @listens Player#fullscreenchange 14830 */ 14831 handleFullscreenChange(event) { 14832 if (this.player_.isFullscreen()) { 14833 this.controlText('Exit Fullscreen'); 14834 this.setIcon('fullscreen-exit'); 14835 } else { 14836 this.controlText('Fullscreen'); 14837 this.setIcon('fullscreen-enter'); 14838 } 14839 } 14840 14841 /** 14842 * This gets called when an `FullscreenToggle` is "clicked". See 14843 * {@link ClickableComponent} for more detailed information on what a click can be. 14844 * 14845 * @param {Event} [event] 14846 * The `keydown`, `tap`, or `click` event that caused this function to be 14847 * called. 14848 * 14849 * @listens tap 14850 * @listens click 14851 */ 14852 handleClick(event) { 14853 if (!this.player_.isFullscreen()) { 14854 this.player_.requestFullscreen(); 14855 } else { 14856 this.player_.exitFullscreen(); 14857 } 14858 } 14859} 14860 14861/** 14862 * The text that should display over the `FullscreenToggle`s controls. Added for localization. 14863 * 14864 * @type {string} 14865 * @protected 14866 */ 14867FullscreenToggle.prototype.controlText_ = 'Fullscreen'; 14868Component$1.registerComponent('FullscreenToggle', FullscreenToggle); 14869 14870/** @import Component from '../../component' */ 14871/** @import Player from '../../player' */ 14872 14873/** 14874 * Check if volume control is supported and if it isn't hide the 14875 * `Component` that was passed using the `vjs-hidden` class. 14876 * 14877 * @param {Component} self 14878 * The component that should be hidden if volume is unsupported 14879 * 14880 * @param {Player} player 14881 * A reference to the player 14882 * 14883 * @private 14884 */ 14885const checkVolumeSupport = function (self, player) { 14886 // hide volume controls when they're not supported by the current tech 14887 if (player.tech_ && !player.tech_.featuresVolumeControl) { 14888 self.addClass('vjs-hidden'); 14889 } 14890 self.on(player, 'loadstart', function () { 14891 if (!player.tech_.featuresVolumeControl) { 14892 self.addClass('vjs-hidden'); 14893 } else { 14894 self.removeClass('vjs-hidden'); 14895 } 14896 }); 14897}; 14898 14899/** 14900 * @file volume-level.js 14901 */ 14902 14903/** 14904 * Shows volume level 14905 * 14906 * @extends Component 14907 */ 14908class VolumeLevel extends Component$1 { 14909 /** 14910 * Create the `Component`'s DOM element 14911 * 14912 * @return {Element} 14913 * The element that was created. 14914 */ 14915 createEl() { 14916 const el = super.createEl('div', { 14917 className: 'vjs-volume-level', 14918 }); 14919 this.setIcon('circle', el); 14920 el.appendChild( 14921 super.createEl('span', { 14922 className: 'vjs-control-text', 14923 }) 14924 ); 14925 return el; 14926 } 14927} 14928Component$1.registerComponent('VolumeLevel', VolumeLevel); 14929 14930/** 14931 * @file volume-level-tooltip.js 14932 */ 14933 14934/** @import Player from '../../player' */ 14935 14936/** 14937 * Volume level tooltips display a volume above or side by side the volume bar. 14938 * 14939 * @extends Component 14940 */ 14941class VolumeLevelTooltip extends Component$1 { 14942 /** 14943 * Creates an instance of this class. 14944 * 14945 * @param {Player} player 14946 * The {@link Player} that this class should be attached to. 14947 * 14948 * @param {Object} [options] 14949 * The key/value store of player options. 14950 */ 14951 constructor(player, options) { 14952 super(player, options); 14953 this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL); 14954 } 14955 14956 /** 14957 * Create the volume tooltip DOM element 14958 * 14959 * @return {Element} 14960 * The element that was created. 14961 */ 14962 createEl() { 14963 return super.createEl( 14964 'div', 14965 { 14966 className: 'vjs-volume-tooltip', 14967 }, 14968 { 14969 'aria-hidden': 'true', 14970 } 14971 ); 14972 } 14973 14974 /** 14975 * Updates the position of the tooltip relative to the `VolumeBar` and 14976 * its content text. 14977 * 14978 * @param {Object} rangeBarRect 14979 * The `ClientRect` for the {@link VolumeBar} element. 14980 * 14981 * @param {number} rangeBarPoint 14982 * A number from 0 to 1, representing a horizontal/vertical reference point 14983 * from the left edge of the {@link VolumeBar} 14984 * 14985 * @param {boolean} vertical 14986 * Referees to the Volume control position 14987 * in the control bar{@link VolumeControl} 14988 * 14989 */ 14990 update(rangeBarRect, rangeBarPoint, vertical, content) { 14991 if (!vertical) { 14992 const tooltipRect = getBoundingClientRect(this.el_); 14993 const playerRect = getBoundingClientRect(this.player_.el()); 14994 const volumeBarPointPx = rangeBarRect.width * rangeBarPoint; 14995 if (!playerRect || !tooltipRect) { 14996 return; 14997 } 14998 const spaceLeftOfPoint = rangeBarRect.left - playerRect.left + volumeBarPointPx; 14999 const spaceRightOfPoint = 15000 rangeBarRect.width - volumeBarPointPx + (playerRect.right - rangeBarRect.right); 15001 let pullTooltipBy = tooltipRect.width / 2; 15002 if (spaceLeftOfPoint < pullTooltipBy) { 15003 pullTooltipBy += pullTooltipBy - spaceLeftOfPoint; 15004 }
vendor: 19,162 bytes, lines 15004-15701
15004 else if (spaceRightOfPoint < pullTooltipBy) { 15005 pullTooltipBy = spaceRightOfPoint; 15006 } 15007 if (pullTooltipBy < 0) { 15008 pullTooltipBy = 0; 15009 } else if (pullTooltipBy > tooltipRect.width) { 15010 pullTooltipBy = tooltipRect.width; 15011 } 15012 this.el_.style.right = `-${pullTooltipBy}px`; 15013 } 15014 this.write(`${content}%`); 15015 } 15016 15017 /** 15018 * Write the volume to the tooltip DOM element. 15019 * 15020 * @param {string} content 15021 * The formatted volume for the tooltip. 15022 */ 15023 write(content) { 15024 textContent(this.el_, content); 15025 } 15026 15027 /** 15028 * Updates the position of the volume tooltip relative to the `VolumeBar`. 15029 * 15030 * @param {Object} rangeBarRect 15031 * The `ClientRect` for the {@link VolumeBar} element. 15032 * 15033 * @param {number} rangeBarPoint 15034 * A number from 0 to 1, representing a horizontal/vertical reference point 15035 * from the left edge of the {@link VolumeBar} 15036 * 15037 * @param {boolean} vertical 15038 * Referees to the Volume control position 15039 * in the control bar{@link VolumeControl} 15040 * 15041 * @param {number} volume 15042 * The volume level to update the tooltip to 15043 * 15044 * @param {Function} cb 15045 * A function that will be called during the request animation frame 15046 * for tooltips that need to do additional animations from the default 15047 */ 15048 updateVolume(rangeBarRect, rangeBarPoint, vertical, volume, cb) { 15049 this.requestNamedAnimationFrame('VolumeLevelTooltip#updateVolume', () => { 15050 this.update(rangeBarRect, rangeBarPoint, vertical, volume.toFixed(0)); 15051 if (cb) { 15052 cb(); 15053 } 15054 }); 15055 } 15056} 15057Component$1.registerComponent('VolumeLevelTooltip', VolumeLevelTooltip); 15058 15059/** 15060 * @file mouse-volume-level-display.js 15061 */ 15062 15063/** 15064 * The {@link MouseVolumeLevelDisplay} component tracks mouse movement over the 15065 * {@link VolumeControl}. It displays an indicator and a {@link VolumeLevelTooltip} 15066 * indicating the volume level which is represented by a given point in the 15067 * {@link VolumeBar}. 15068 * 15069 * @extends Component 15070 */ 15071class MouseVolumeLevelDisplay extends Component$1 { 15072 /** 15073 * Creates an instance of this class. 15074 * 15075 * @param {Player} player 15076 * The {@link Player} that this class should be attached to. 15077 * 15078 * @param {Object} [options] 15079 * The key/value store of player options. 15080 */ 15081 constructor(player, options) { 15082 super(player, options); 15083 this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL); 15084 } 15085 15086 /** 15087 * Create the DOM element for this class. 15088 * 15089 * @return {Element} 15090 * The element that was created. 15091 */ 15092 createEl() { 15093 return super.createEl('div', { 15094 className: 'vjs-mouse-display', 15095 }); 15096 } 15097 15098 /** 15099 * Enquires updates to its own DOM as well as the DOM of its 15100 * {@link VolumeLevelTooltip} child. 15101 * 15102 * @param {Object} rangeBarRect 15103 * The `ClientRect` for the {@link VolumeBar} element. 15104 * 15105 * @param {number} rangeBarPoint 15106 * A number from 0 to 1, representing a horizontal/vertical reference point 15107 * from the left edge of the {@link VolumeBar} 15108 * 15109 * @param {boolean} vertical 15110 * Referees to the Volume control position 15111 * in the control bar{@link VolumeControl} 15112 * 15113 */ 15114 update(rangeBarRect, rangeBarPoint, vertical) { 15115 const volume = 100 * rangeBarPoint; 15116 this.getChild('volumeLevelTooltip').updateVolume( 15117 rangeBarRect, 15118 rangeBarPoint, 15119 vertical, 15120 volume, 15121 () => { 15122 if (vertical) { 15123 this.el_.style.bottom = `${rangeBarRect.height * rangeBarPoint}px`; 15124 } else { 15125 this.el_.style.left = `${rangeBarRect.width * rangeBarPoint}px`; 15126 } 15127 } 15128 ); 15129 } 15130} 15131 15132/** 15133 * Default options for `MouseVolumeLevelDisplay` 15134 * 15135 * @type {Object} 15136 * @private 15137 */ 15138MouseVolumeLevelDisplay.prototype.options_ = { 15139 children: ['volumeLevelTooltip'], 15140}; 15141Component$1.registerComponent('MouseVolumeLevelDisplay', MouseVolumeLevelDisplay); 15142 15143/** 15144 * @file volume-bar.js 15145 */ 15146 15147/** 15148 * The bar that contains the volume level and can be clicked on to adjust the level 15149 * 15150 * @extends Slider 15151 */ 15152class VolumeBar extends Slider { 15153 /** 15154 * Creates an instance of this class. 15155 * 15156 * @param {Player} player 15157 * The `Player` that this class should be attached to. 15158 * 15159 * @param {Object} [options] 15160 * The key/value store of player options. 15161 */ 15162 constructor(player, options) { 15163 super(player, options); 15164 this.on('slideractive', (e) => this.updateLastVolume_(e)); 15165 this.on(player, 'volumechange', (e) => this.updateARIAAttributes(e)); 15166 player.ready(() => this.updateARIAAttributes()); 15167 } 15168 15169 /** 15170 * Create the `Component`'s DOM element 15171 * 15172 * @return {Element} 15173 * The element that was created. 15174 */ 15175 createEl() { 15176 return super.createEl( 15177 'div', 15178 { 15179 className: 'vjs-volume-bar vjs-slider-bar', 15180 }, 15181 { 15182 'aria-label': this.localize('Volume Level'), 15183 'aria-live': 'polite', 15184 } 15185 ); 15186 } 15187 15188 /** 15189 * Handle mouse down on volume bar 15190 * 15191 * @param {Event} event 15192 * The `mousedown` event that caused this to run. 15193 * 15194 * @listens mousedown 15195 */ 15196 handleMouseDown(event) { 15197 if (!isSingleLeftClick(event)) { 15198 return; 15199 } 15200 super.handleMouseDown(event); 15201 } 15202 15203 /** 15204 * Handle movement events on the {@link VolumeMenuButton}. 15205 * 15206 * @param {Event} event 15207 * The event that caused this function to run. 15208 * 15209 * @listens mousemove 15210 */ 15211 handleMouseMove(event) { 15212 const mouseVolumeLevelDisplay = this.getChild('mouseVolumeLevelDisplay'); 15213 if (mouseVolumeLevelDisplay) { 15214 const volumeBarEl = this.el(); 15215 const volumeBarRect = getBoundingClientRect(volumeBarEl); 15216 const vertical = this.vertical(); 15217 let volumeBarPoint = getPointerPosition(volumeBarEl, event); 15218 volumeBarPoint = vertical ? volumeBarPoint.y : volumeBarPoint.x; 15219 // The default skin has a gap on either side of the `VolumeBar`. This means 15220 // that it's possible to trigger this behavior outside the boundaries of 15221 // the `VolumeBar`. This ensures we stay within it at all times. 15222 volumeBarPoint = clamp(volumeBarPoint, 0, 1); 15223 mouseVolumeLevelDisplay.update(volumeBarRect, volumeBarPoint, vertical); 15224 } 15225 if (!isSingleLeftClick(event)) { 15226 return; 15227 } 15228 this.checkMuted(); 15229 this.player_.volume(this.calculateDistance(event)); 15230 } 15231 15232 /** 15233 * If the player is muted unmute it. 15234 */ 15235 checkMuted() { 15236 if (this.player_.muted()) { 15237 this.player_.muted(false); 15238 } 15239 } 15240 15241 /** 15242 * Get percent of volume level 15243 * 15244 * @return {number} 15245 * Volume level percent as a decimal number. 15246 */ 15247 getPercent() { 15248 if (this.player_.muted()) { 15249 return 0; 15250 } 15251 return this.player_.volume(); 15252 } 15253 15254 /** 15255 * Increase volume level for keyboard users 15256 */ 15257 stepForward() { 15258 this.checkMuted(); 15259 this.player_.volume(this.player_.volume() + 0.1); 15260 } 15261 15262 /** 15263 * Decrease volume level for keyboard users 15264 */ 15265 stepBack() { 15266 this.checkMuted(); 15267 this.player_.volume(this.player_.volume() - 0.1); 15268 } 15269 15270 /** 15271 * Update ARIA accessibility attributes 15272 * 15273 * @param {Event} [event] 15274 * The `volumechange` event that caused this function to run. 15275 * 15276 * @listens Player#volumechange 15277 */ 15278 updateARIAAttributes(event) { 15279 const ariaValue = this.player_.muted() ? 0 : this.volumeAsPercentage_(); 15280 this.el_.setAttribute('aria-valuenow', ariaValue); 15281 this.el_.setAttribute('aria-valuetext', ariaValue + '%'); 15282 } 15283 15284 /** 15285 * Returns the current value of the player volume as a percentage 15286 * 15287 * @private 15288 */ 15289 volumeAsPercentage_() { 15290 return Math.round(this.player_.volume() * 100); 15291 } 15292 15293 /** 15294 * When user starts dragging the VolumeBar, store the volume and listen for 15295 * the end of the drag. When the drag ends, if the volume was set to zero, 15296 * set lastVolume to the stored volume. 15297 * 15298 * @listens slideractive 15299 * @private 15300 */ 15301 updateLastVolume_() { 15302 const volumeBeforeDrag = this.player_.volume(); 15303 this.one('sliderinactive', () => { 15304 if (this.player_.volume() === 0) { 15305 this.player_.lastVolume_(volumeBeforeDrag); 15306 } 15307 }); 15308 } 15309} 15310 15311/** 15312 * Default options for the `VolumeBar` 15313 * 15314 * @type {Object} 15315 * @private 15316 */ 15317VolumeBar.prototype.options_ = { 15318 children: ['volumeLevel'], 15319 barName: 'volumeLevel', 15320}; 15321 15322// MouseVolumeLevelDisplay tooltip should not be added to a player on mobile devices 15323if (!IS_IOS && !IS_ANDROID) { 15324 VolumeBar.prototype.options_.children.splice(0, 0, 'mouseVolumeLevelDisplay'); 15325} 15326 15327/** 15328 * Call the update event for this Slider when this event happens on the player. 15329 * 15330 * @type {string} 15331 */ 15332VolumeBar.prototype.playerEvent = 'volumechange'; 15333Component$1.registerComponent('VolumeBar', VolumeBar); 15334 15335/** 15336 * @file volume-control.js 15337 */ 15338 15339/** 15340 * The component for controlling the volume level 15341 * 15342 * @extends Component 15343 */ 15344class VolumeControl extends Component$1 { 15345 /** 15346 * Creates an instance of this class. 15347 * 15348 * @param {Player} player 15349 * The `Player` that this class should be attached to. 15350 * 15351 * @param {Object} [options={}] 15352 * The key/value store of player options. 15353 */ 15354 constructor(player, options = {}) { 15355 options.vertical = options.vertical || false; 15356 15357 // Pass the vertical option down to the VolumeBar if 15358 // the VolumeBar is turned on. 15359 if (typeof options.volumeBar === 'undefined' || isPlain(options.volumeBar)) { 15360 options.volumeBar = options.volumeBar || {}; 15361 options.volumeBar.vertical = options.vertical; 15362 } 15363 super(player, options); 15364 15365 // hide this control if volume support is missing 15366 checkVolumeSupport(this, player); 15367 this.throttledHandleMouseMove = throttle( 15368 bind_(this, this.handleMouseMove), 15369 UPDATE_REFRESH_INTERVAL 15370 ); 15371 this.handleMouseUpHandler_ = (e) => this.handleMouseUp(e); 15372 this.on('mousedown', (e) => this.handleMouseDown(e)); 15373 this.on('touchstart', (e) => this.handleMouseDown(e)); 15374 this.on('mousemove', (e) => this.handleMouseMove(e)); 15375 15376 // while the slider is active (the mouse has been pressed down and 15377 // is dragging) or in focus we do not want to hide the VolumeBar 15378 this.on(this.volumeBar, ['focus', 'slideractive'], () => { 15379 this.volumeBar.addClass('vjs-slider-active'); 15380 this.addClass('vjs-slider-active'); 15381 this.trigger('slideractive'); 15382 }); 15383 this.on(this.volumeBar, ['blur', 'sliderinactive'], () => { 15384 this.volumeBar.removeClass('vjs-slider-active'); 15385 this.removeClass('vjs-slider-active'); 15386 this.trigger('sliderinactive'); 15387 }); 15388 } 15389 15390 /** 15391 * Create the `Component`'s DOM element 15392 * 15393 * @return {Element} 15394 * The element that was created. 15395 */ 15396 createEl() { 15397 let orientationClass = 'vjs-volume-horizontal'; 15398 if (this.options_.vertical) { 15399 orientationClass = 'vjs-volume-vertical'; 15400 } 15401 return super.createEl('div', { 15402 className: `vjs-volume-control vjs-control ${orientationClass}`, 15403 }); 15404 } 15405 15406 /** 15407 * Handle `mousedown` or `touchstart` events on the `VolumeControl`. 15408 * 15409 * @param {Event} event 15410 * `mousedown` or `touchstart` event that triggered this function 15411 * 15412 * @listens mousedown 15413 * @listens touchstart 15414 */ 15415 handleMouseDown(event) { 15416 const doc = this.el_.ownerDocument; 15417 this.on(doc, 'mousemove', this.throttledHandleMouseMove); 15418 this.on(doc, 'touchmove', this.throttledHandleMouseMove); 15419 this.on(doc, 'mouseup', this.handleMouseUpHandler_); 15420 this.on(doc, 'touchend', this.handleMouseUpHandler_); 15421 } 15422 15423 /** 15424 * Handle `mouseup` or `touchend` events on the `VolumeControl`. 15425 * 15426 * @param {Event} event 15427 * `mouseup` or `touchend` event that triggered this function. 15428 * 15429 * @listens touchend 15430 * @listens mouseup 15431 */ 15432 handleMouseUp(event) { 15433 const doc = this.el_.ownerDocument; 15434 this.off(doc, 'mousemove', this.throttledHandleMouseMove); 15435 this.off(doc, 'touchmove', this.throttledHandleMouseMove); 15436 this.off(doc, 'mouseup', this.handleMouseUpHandler_); 15437 this.off(doc, 'touchend', this.handleMouseUpHandler_); 15438 } 15439 15440 /** 15441 * Handle `mousedown` or `touchstart` events on the `VolumeControl`. 15442 * 15443 * @param {Event} event 15444 * `mousedown` or `touchstart` event that triggered this function 15445 * 15446 * @listens mousedown 15447 * @listens touchstart 15448 */ 15449 handleMouseMove(event) { 15450 this.volumeBar.handleMouseMove(event); 15451 } 15452} 15453 15454/** 15455 * Default options for the `VolumeControl` 15456 * 15457 * @type {Object} 15458 * @private 15459 */ 15460VolumeControl.prototype.options_ = { 15461 children: ['volumeBar'], 15462}; 15463Component$1.registerComponent('VolumeControl', VolumeControl); 15464 15465/** @import Component from '../../component' */ 15466/** @import Player from '../../player' */ 15467 15468/** 15469 * Check if muting volume is supported and if it isn't hide the mute toggle 15470 * button. 15471 * 15472 * @param {Component} self 15473 * A reference to the mute toggle button 15474 * 15475 * @param {Player} player 15476 * A reference to the player 15477 * 15478 * @private 15479 */ 15480const checkMuteSupport = function (self, player) { 15481 // hide mute toggle button if it's not supported by the current tech 15482 if (player.tech_ && !player.tech_.featuresMuteControl) { 15483 self.addClass('vjs-hidden'); 15484 } 15485 self.on(player, 'loadstart', function () { 15486 if (!player.tech_.featuresMuteControl) { 15487 self.addClass('vjs-hidden'); 15488 } else { 15489 self.removeClass('vjs-hidden'); 15490 } 15491 }); 15492}; 15493 15494/** 15495 * @file mute-toggle.js 15496 */ 15497 15498/** @import Player from './player' */ 15499 15500/** 15501 * A button component for muting the audio. 15502 * 15503 * @extends Button 15504 */ 15505class MuteToggle extends Button { 15506 /** 15507 * Creates an instance of this class. 15508 * 15509 * @param {Player} player 15510 * The `Player` that this class should be attached to. 15511 * 15512 * @param {Object} [options] 15513 * The key/value store of player options. 15514 */ 15515 constructor(player, options) { 15516 super(player, options); 15517 15518 // hide this control if volume support is missing 15519 checkMuteSupport(this, player); 15520 this.on(player, ['loadstart', 'volumechange'], (e) => this.update(e)); 15521 } 15522 15523 /** 15524 * Builds the default DOM `className`. 15525 * 15526 * @return {string} 15527 * The DOM `className` for this object. 15528 */ 15529 buildCSSClass() { 15530 return `vjs-mute-control ${super.buildCSSClass()}`; 15531 } 15532 15533 /** 15534 * This gets called when an `MuteToggle` is "clicked". See 15535 * {@link ClickableComponent} for more detailed information on what a click can be. 15536 * 15537 * @param {Event} [event] 15538 * The `keydown`, `tap`, or `click` event that caused this function to be 15539 * called. 15540 * 15541 * @listens tap 15542 * @listens click 15543 */ 15544 handleClick(event) { 15545 const vol = this.player_.volume(); 15546 const lastVolume = this.player_.lastVolume_(); 15547 if (vol === 0) { 15548 const volumeToSet = lastVolume < 0.1 ? 0.1 : lastVolume; 15549 this.player_.volume(volumeToSet); 15550 this.player_.muted(false); 15551 } else { 15552 this.player_.muted(this.player_.muted() ? false : true); 15553 } 15554 } 15555 15556 /** 15557 * Update the `MuteToggle` button based on the state of `volume` and `muted` 15558 * on the player. 15559 * 15560 * @param {Event} [event] 15561 * The {@link Player#loadstart} event if this function was called 15562 * through an event. 15563 * 15564 * @listens Player#loadstart 15565 * @listens Player#volumechange 15566 */ 15567 update(event) { 15568 this.updateIcon_(); 15569 this.updateControlText_(); 15570 } 15571 15572 /** 15573 * Update the appearance of the `MuteToggle` icon. 15574 * 15575 * Possible states (given `level` variable below): 15576 * - 0: crossed out 15577 * - 1: zero bars of volume 15578 * - 2: one bar of volume 15579 * - 3: two bars of volume 15580 * 15581 * @private 15582 */ 15583 updateIcon_() { 15584 const vol = this.player_.volume(); 15585 let level = 3; 15586 this.setIcon('volume-high'); 15587 15588 // in iOS when a player is loaded with muted attribute 15589 // and volume is changed with a native mute button 15590 // we want to make sure muted state is updated 15591 if (IS_IOS && this.player_.tech_ && this.player_.tech_.el_) { 15592 this.player_.muted(this.player_.tech_.el_.muted); 15593 } 15594 if (vol === 0 || this.player_.muted()) { 15595 this.setIcon('volume-mute'); 15596 level = 0; 15597 } else if (vol < 0.33) { 15598 this.setIcon('volume-low'); 15599 level = 1; 15600 } else if (vol < 0.67) { 15601 this.setIcon('volume-medium'); 15602 level = 2; 15603 } 15604 removeClass( 15605 this.el_, 15606 [0, 1, 2, 3].reduce((str, i) => str + `${i ? ' ' : ''}vjs-vol-${i}`, '') 15607 ); 15608 addClass(this.el_, `vjs-vol-${level}`); 15609 } 15610 15611 /** 15612 * If `muted` has changed on the player, update the control text 15613 * (`title` attribute on `vjs-mute-control` element and content of 15614 * `vjs-control-text` element). 15615 * 15616 * @private 15617 */ 15618 updateControlText_() { 15619 const soundOff = this.player_.muted() || this.player_.volume() === 0; 15620 const text = soundOff ? 'Unmute' : 'Mute'; 15621 if (this.controlText() !== text) { 15622 this.controlText(text); 15623 } 15624 } 15625} 15626 15627/** 15628 * The text that should display over the `MuteToggle`s controls. Added for localization. 15629 * 15630 * @type {string} 15631 * @protected 15632 */ 15633MuteToggle.prototype.controlText_ = 'Mute'; 15634Component$1.registerComponent('MuteToggle', MuteToggle); 15635 15636/** 15637 * @file volume-control.js 15638 */ 15639 15640/** 15641 * A Component to contain the MuteToggle and VolumeControl so that 15642 * they can work together. 15643 * 15644 * @extends Component 15645 */ 15646class VolumePanel extends Component$1 { 15647 /** 15648 * Creates an instance of this class. 15649 * 15650 * @param {Player} player 15651 * The `Player` that this class should be attached to. 15652 * 15653 * @param {Object} [options={}] 15654 * The key/value store of player options. 15655 */ 15656 constructor(player, options = {}) { 15657 if (typeof options.inline !== 'undefined') { 15658 options.inline = options.inline; 15659 } else { 15660 options.inline = true; 15661 } 15662 15663 // pass the inline option down to the VolumeControl as vertical if 15664 // the VolumeControl is on. 15665 if (typeof options.volumeControl === 'undefined' || isPlain(options.volumeControl)) { 15666 options.volumeControl = options.volumeControl || {}; 15667 options.volumeControl.vertical = !options.inline; 15668 } 15669 super(player, options); 15670 15671 // this handler is used by mouse handler methods below 15672 this.handleKeyPressHandler_ = (e) => this.handleKeyPress(e); 15673 this.on(player, ['loadstart'], (e) => this.volumePanelState_(e)); 15674 this.on(this.muteToggle, 'keyup', (e) => this.handleKeyPress(e)); 15675 this.on(this.volumeControl, 'keyup', (e) => this.handleVolumeControlKeyUp(e)); 15676 this.on('keydown', (e) => this.handleKeyPress(e)); 15677 this.on('mouseover', (e) => this.handleMouseOver(e)); 15678 this.on('mouseout', (e) => this.handleMouseOut(e)); 15679 15680 // while the slider is active (the mouse has been pressed down and 15681 // is dragging) we do not want to hide the VolumeBar 15682 this.on(this.volumeControl, ['slideractive'], this.sliderActive_); 15683 this.on(this.volumeControl, ['sliderinactive'], this.sliderInactive_); 15684 } 15685 15686 /** 15687 * Add vjs-slider-active class to the VolumePanel 15688 * 15689 * @listens VolumeControl#slideractive 15690 * @private 15691 */ 15692 sliderActive_() { 15693 this.addClass('vjs-slider-active'); 15694 } 15695 15696 /** 15697 * Removes vjs-slider-active class to the VolumePanel 15698 * 15699 * @listens VolumeControl#sliderinactive 15700 * @private 15701 */
vendor: 20,592 bytes, lines 15702-16447
15702 sliderInactive_() { 15703 this.removeClass('vjs-slider-active'); 15704 } 15705 15706 /** 15707 * Adds vjs-hidden or vjs-mute-toggle-only to the VolumePanel 15708 * depending on MuteToggle and VolumeControl state 15709 * 15710 * @listens Player#loadstart 15711 * @private 15712 */ 15713 volumePanelState_() { 15714 // hide volume panel if neither volume control or mute toggle 15715 // are displayed 15716 if (this.volumeControl.hasClass('vjs-hidden') && this.muteToggle.hasClass('vjs-hidden')) { 15717 this.addClass('vjs-hidden'); 15718 } 15719 15720 // if only mute toggle is visible we don't want 15721 // volume panel expanding when hovered or active 15722 if (this.volumeControl.hasClass('vjs-hidden') && !this.muteToggle.hasClass('vjs-hidden')) { 15723 this.addClass('vjs-mute-toggle-only'); 15724 } 15725 } 15726 15727 /** 15728 * Create the `Component`'s DOM element 15729 * 15730 * @return {Element} 15731 * The element that was created. 15732 */ 15733 createEl() { 15734 let orientationClass = 'vjs-volume-panel-horizontal'; 15735 if (!this.options_.inline) { 15736 orientationClass = 'vjs-volume-panel-vertical'; 15737 } 15738 return super.createEl('div', { 15739 className: `vjs-volume-panel vjs-control ${orientationClass}`, 15740 }); 15741 } 15742 15743 /** 15744 * Dispose of the `volume-panel` and all child components. 15745 */ 15746 dispose() { 15747 this.handleMouseOut(); 15748 super.dispose(); 15749 } 15750 15751 /** 15752 * Handles `keyup` events on the `VolumeControl`, looking for ESC, which closes 15753 * the volume panel and sets focus on `MuteToggle`. 15754 * 15755 * @param {Event} event 15756 * The `keyup` event that caused this function to be called. 15757 * 15758 * @listens keyup 15759 */ 15760 handleVolumeControlKeyUp(event) { 15761 if (event.key === 'Escape') { 15762 this.muteToggle.focus(); 15763 } 15764 } 15765 15766 /** 15767 * This gets called when a `VolumePanel` gains hover via a `mouseover` event. 15768 * Turns on listening for `mouseover` event. When they happen it 15769 * calls `this.handleMouseOver`. 15770 * 15771 * @param {Event} event 15772 * The `mouseover` event that caused this function to be called. 15773 * 15774 * @listens mouseover 15775 */ 15776 handleMouseOver(event) { 15777 this.addClass('vjs-hover'); 15778 on(document__default['default'], 'keyup', this.handleKeyPressHandler_); 15779 } 15780 15781 /** 15782 * This gets called when a `VolumePanel` gains hover via a `mouseout` event. 15783 * Turns on listening for `mouseout` event. When they happen it 15784 * calls `this.handleMouseOut`. 15785 * 15786 * @param {Event} event 15787 * The `mouseout` event that caused this function to be called. 15788 * 15789 * @listens mouseout 15790 */ 15791 handleMouseOut(event) { 15792 this.removeClass('vjs-hover'); 15793 off(document__default['default'], 'keyup', this.handleKeyPressHandler_); 15794 } 15795 15796 /** 15797 * Handles `keyup` event on the document or `keydown` event on the `VolumePanel`, 15798 * looking for ESC, which hides the `VolumeControl`. 15799 * 15800 * @param {Event} event 15801 * The keypress that triggered this event. 15802 * 15803 * @listens keydown | keyup 15804 */ 15805 handleKeyPress(event) { 15806 if (event.key === 'Escape') { 15807 this.handleMouseOut(); 15808 } 15809 } 15810} 15811 15812/** 15813 * Default options for the `VolumeControl` 15814 * 15815 * @type {Object} 15816 * @private 15817 */ 15818VolumePanel.prototype.options_ = { 15819 children: ['muteToggle', 'volumeControl'], 15820}; 15821Component$1.registerComponent('VolumePanel', VolumePanel); 15822 15823/** 15824 * Button to skip forward a configurable amount of time 15825 * through a video. Renders in the control bar. 15826 * 15827 * e.g. options: {controlBar: {skipButtons: forward: 5}} 15828 * 15829 * @extends Button 15830 */ 15831class SkipForward extends Button { 15832 constructor(player, options) { 15833 super(player, options); 15834 this.validOptions = [5, 10, 30]; 15835 this.skipTime = this.getSkipForwardTime(); 15836 if (this.skipTime && this.validOptions.includes(this.skipTime)) { 15837 this.setIcon(`forward-${this.skipTime}`); 15838 this.controlText( 15839 this.localize('Skip forward {1} seconds', [this.skipTime.toLocaleString(player.language())]) 15840 ); 15841 this.show(); 15842 } else { 15843 this.hide(); 15844 } 15845 } 15846 getSkipForwardTime() { 15847 const playerOptions = this.options_.playerOptions; 15848 return ( 15849 playerOptions.controlBar && 15850 playerOptions.controlBar.skipButtons && 15851 playerOptions.controlBar.skipButtons.forward 15852 ); 15853 } 15854 buildCSSClass() { 15855 return `vjs-skip-forward-${this.getSkipForwardTime()} ${super.buildCSSClass()}`; 15856 } 15857 15858 /** 15859 * On click, skips forward in the duration/seekable range by a configurable amount of seconds. 15860 * If the time left in the duration/seekable range is less than the configured 'skip forward' time, 15861 * skips to end of duration/seekable range. 15862 * 15863 * Handle a click on a `SkipForward` button 15864 * 15865 * @param {EventTarget~Event} event 15866 * The `click` event that caused this function 15867 * to be called 15868 */ 15869 handleClick(event) { 15870 if (isNaN(this.player_.duration())) { 15871 return; 15872 } 15873 const currentVideoTime = this.player_.currentTime(); 15874 const liveTracker = this.player_.liveTracker; 15875 const duration = 15876 liveTracker && liveTracker.isLive() ? liveTracker.seekableEnd() : this.player_.duration(); 15877 let newTime; 15878 if (currentVideoTime + this.skipTime <= duration) { 15879 newTime = currentVideoTime + this.skipTime; 15880 } else { 15881 newTime = duration; 15882 } 15883 this.player_.currentTime(newTime); 15884 } 15885 15886 /** 15887 * Update control text on languagechange 15888 */ 15889 handleLanguagechange() { 15890 this.controlText(this.localize('Skip forward {1} seconds', [this.skipTime])); 15891 } 15892} 15893SkipForward.prototype.controlText_ = 'Skip Forward'; 15894Component$1.registerComponent('SkipForward', SkipForward); 15895 15896/** 15897 * Button to skip backward a configurable amount of time 15898 * through a video. Renders in the control bar. 15899 * 15900 * * e.g. options: {controlBar: {skipButtons: backward: 5}} 15901 * 15902 * @extends Button 15903 */ 15904class SkipBackward extends Button { 15905 constructor(player, options) { 15906 super(player, options); 15907 this.validOptions = [5, 10, 30]; 15908 this.skipTime = this.getSkipBackwardTime(); 15909 if (this.skipTime && this.validOptions.includes(this.skipTime)) { 15910 this.setIcon(`replay-${this.skipTime}`); 15911 this.controlText( 15912 this.localize('Skip backward {1} seconds', [ 15913 this.skipTime.toLocaleString(player.language()), 15914 ]) 15915 ); 15916 this.show(); 15917 } else { 15918 this.hide(); 15919 } 15920 } 15921 getSkipBackwardTime() { 15922 const playerOptions = this.options_.playerOptions; 15923 return ( 15924 playerOptions.controlBar && 15925 playerOptions.controlBar.skipButtons && 15926 playerOptions.controlBar.skipButtons.backward 15927 ); 15928 } 15929 buildCSSClass() { 15930 return `vjs-skip-backward-${this.getSkipBackwardTime()} ${super.buildCSSClass()}`; 15931 } 15932 15933 /** 15934 * On click, skips backward in the video by a configurable amount of seconds. 15935 * If the current time in the video is less than the configured 'skip backward' time, 15936 * skips to beginning of video or seekable range. 15937 * 15938 * Handle a click on a `SkipBackward` button 15939 * 15940 * @param {EventTarget~Event} event 15941 * The `click` event that caused this function 15942 * to be called 15943 */ 15944 handleClick(event) { 15945 const currentVideoTime = this.player_.currentTime(); 15946 const liveTracker = this.player_.liveTracker; 15947 const seekableStart = liveTracker && liveTracker.isLive() && liveTracker.seekableStart(); 15948 let newTime; 15949 if (seekableStart && currentVideoTime - this.skipTime <= seekableStart) { 15950 newTime = seekableStart; 15951 } else if (currentVideoTime >= this.skipTime) { 15952 newTime = currentVideoTime - this.skipTime; 15953 } else { 15954 newTime = 0; 15955 } 15956 this.player_.currentTime(newTime); 15957 } 15958 15959 /** 15960 * Update control text on languagechange 15961 */ 15962 handleLanguagechange() { 15963 this.controlText(this.localize('Skip backward {1} seconds', [this.skipTime])); 15964 } 15965} 15966SkipBackward.prototype.controlText_ = 'Skip Backward'; 15967Component$1.registerComponent('SkipBackward', SkipBackward); 15968 15969/** 15970 * @file menu.js 15971 */ 15972 15973/** @import Player from '../player' */ 15974 15975/** 15976 * The Menu component is used to build popup menus, including subtitle and 15977 * captions selection menus. 15978 * 15979 * @extends Component 15980 */ 15981class Menu extends Component$1 { 15982 /** 15983 * Create an instance of this class. 15984 * 15985 * @param {Player} player 15986 * the player that this component should attach to 15987 * 15988 * @param {Object} [options] 15989 * Object of option names and values 15990 * 15991 */ 15992 constructor(player, options) { 15993 super(player, options); 15994 if (options) { 15995 this.menuButton_ = options.menuButton; 15996 } 15997 this.focusedChild_ = -1; 15998 this.on('keydown', (e) => this.handleKeyDown(e)); 15999 16000 // All the menu item instances share the same blur handler provided by the menu container. 16001 this.boundHandleBlur_ = (e) => this.handleBlur(e); 16002 this.boundHandleTapClick_ = (e) => this.handleTapClick(e); 16003 } 16004 16005 /** 16006 * Add event listeners to the {@link MenuItem}. 16007 * 16008 * @param {Object} component 16009 * The instance of the `MenuItem` to add listeners to. 16010 * 16011 */ 16012 addEventListenerForItem(component) { 16013 if (!(component instanceof Component$1)) { 16014 return; 16015 } 16016 this.on(component, 'blur', this.boundHandleBlur_); 16017 this.on(component, ['tap', 'click'], this.boundHandleTapClick_); 16018 } 16019 16020 /** 16021 * Remove event listeners from the {@link MenuItem}. 16022 * 16023 * @param {Object} component 16024 * The instance of the `MenuItem` to remove listeners. 16025 * 16026 */ 16027 removeEventListenerForItem(component) { 16028 if (!(component instanceof Component$1)) { 16029 return; 16030 } 16031 this.off(component, 'blur', this.boundHandleBlur_); 16032 this.off(component, ['tap', 'click'], this.boundHandleTapClick_); 16033 } 16034 16035 /** 16036 * This method will be called indirectly when the component has been added 16037 * before the component adds to the new menu instance by `addItem`. 16038 * In this case, the original menu instance will remove the component 16039 * by calling `removeChild`. 16040 * 16041 * @param {Object} component 16042 * The instance of the `MenuItem` 16043 */ 16044 removeChild(component) { 16045 if (typeof component === 'string') { 16046 component = this.getChild(component); 16047 } 16048 this.removeEventListenerForItem(component); 16049 super.removeChild(component); 16050 } 16051 16052 /** 16053 * Add a {@link MenuItem} to the menu. 16054 * 16055 * @param {Object|string} component 16056 * The name or instance of the `MenuItem` to add. 16057 * 16058 */ 16059 addItem(component) { 16060 const childComponent = this.addChild(component); 16061 if (childComponent) { 16062 this.addEventListenerForItem(childComponent); 16063 } 16064 } 16065 16066 /** 16067 * Create the `Menu`s DOM element. 16068 * 16069 * @return {Element} 16070 * the element that was created 16071 */ 16072 createEl() { 16073 const contentElType = this.options_.contentElType || 'ul'; 16074 this.contentEl_ = createEl(contentElType, { 16075 className: 'vjs-menu-content', 16076 }); 16077 this.contentEl_.setAttribute('role', 'menu'); 16078 const el = super.createEl('div', { 16079 append: this.contentEl_, 16080 className: 'vjs-menu', 16081 }); 16082 el.appendChild(this.contentEl_); 16083 16084 // Prevent clicks from bubbling up. Needed for Menu Buttons, 16085 // where a click on the parent is significant 16086 on(el, 'click', function (event) { 16087 event.preventDefault(); 16088 event.stopImmediatePropagation(); 16089 }); 16090 return el; 16091 } 16092 dispose() { 16093 this.contentEl_ = null; 16094 this.boundHandleBlur_ = null; 16095 this.boundHandleTapClick_ = null; 16096 super.dispose(); 16097 } 16098 16099 /** 16100 * Called when a `MenuItem` loses focus. 16101 * 16102 * @param {Event} event 16103 * The `blur` event that caused this function to be called. 16104 * 16105 * @listens blur 16106 */ 16107 handleBlur(event) { 16108 const relatedTarget = event.relatedTarget || document__default['default'].activeElement; 16109 16110 // Close menu popup when a user clicks outside the menu 16111 if ( 16112 !this.children().some((element) => { 16113 return element.el() === relatedTarget; 16114 }) 16115 ) { 16116 const btn = this.menuButton_; 16117 if (btn && btn.buttonPressed_ && relatedTarget !== btn.el().firstChild) { 16118 btn.unpressButton(); 16119 } 16120 } 16121 } 16122 16123 /** 16124 * Called when a `MenuItem` gets clicked or tapped. 16125 * 16126 * @param {Event} event 16127 * The `click` or `tap` event that caused this function to be called. 16128 * 16129 * @listens click,tap 16130 */ 16131 handleTapClick(event) { 16132 // Unpress the associated MenuButton, and move focus back to it 16133 if (this.menuButton_) { 16134 this.menuButton_.unpressButton(); 16135 const childComponents = this.children(); 16136 if (!Array.isArray(childComponents)) { 16137 return; 16138 } 16139 const foundComponent = childComponents.filter( 16140 (component) => component.el() === event.target 16141 )[0]; 16142 if (!foundComponent) { 16143 return; 16144 } 16145 16146 // don't focus menu button if item is a caption settings item 16147 // because focus will move elsewhere 16148 if (foundComponent.name() !== 'CaptionSettingsMenuItem') { 16149 this.menuButton_.focus(); 16150 } 16151 } 16152 } 16153 16154 /** 16155 * Handle a `keydown` event on this menu. This listener is added in the constructor. 16156 * 16157 * @param {KeyboardEvent} event 16158 * A `keydown` event that happened on the menu. 16159 * 16160 * @listens keydown 16161 */ 16162 handleKeyDown(event) { 16163 // Left and Down Arrows 16164 if (event.key === 'ArrowLeft' || event.key === 'ArrowDown') { 16165 event.preventDefault(); 16166 event.stopPropagation(); 16167 this.stepForward(); 16168 16169 // Up and Right Arrows 16170 } else if (event.key === 'ArrowRight' || event.key === 'ArrowUp') { 16171 event.preventDefault(); 16172 event.stopPropagation(); 16173 this.stepBack(); 16174 } 16175 } 16176 16177 /** 16178 * Move to next (lower) menu item for keyboard users. 16179 */ 16180 stepForward() { 16181 let stepChild = 0; 16182 if (this.focusedChild_ !== undefined) { 16183 stepChild = this.focusedChild_ + 1; 16184 } 16185 this.focus(stepChild); 16186 } 16187 16188 /** 16189 * Move to previous (higher) menu item for keyboard users. 16190 */ 16191 stepBack() { 16192 let stepChild = 0; 16193 if (this.focusedChild_ !== undefined) { 16194 stepChild = this.focusedChild_ - 1; 16195 } 16196 this.focus(stepChild); 16197 } 16198 16199 /** 16200 * Set focus on a {@link MenuItem} in the `Menu`. 16201 * 16202 * @param {Object|string} [item=0] 16203 * Index of child item set focus on. 16204 */ 16205 focus(item = 0) { 16206 const children = this.children().slice(); 16207 const haveTitle = children.length && children[0].hasClass('vjs-menu-title'); 16208 if (haveTitle) { 16209 children.shift(); 16210 } 16211 if (children.length > 0) { 16212 if (item < 0) { 16213 item = 0; 16214 } else if (item >= children.length) { 16215 item = children.length - 1; 16216 } 16217 this.focusedChild_ = item; 16218 children[item].el_.focus(); 16219 } 16220 } 16221} 16222Component$1.registerComponent('Menu', Menu); 16223 16224/** 16225 * @file menu-button.js 16226 */ 16227 16228/** @import Player from '../player' */ 16229 16230/** 16231 * A `MenuButton` class for any popup {@link Menu}. 16232 * 16233 * @extends Component 16234 */ 16235class MenuButton extends Component$1 { 16236 /** 16237 * Creates an instance of this class. 16238 * 16239 * @param {Player} player 16240 * The `Player` that this class should be attached to. 16241 * 16242 * @param {Object} [options={}] 16243 * The key/value store of player options. 16244 */ 16245 constructor(player, options = {}) { 16246 super(player, options); 16247 this.menuButton_ = new Button(player, options); 16248 this.menuButton_.controlText(this.controlText_); 16249 this.menuButton_.el_.setAttribute('aria-haspopup', 'true'); 16250 16251 // Add buildCSSClass values to the button, not the wrapper 16252 const buttonClass = Button.prototype.buildCSSClass(); 16253 this.menuButton_.el_.className = this.buildCSSClass() + ' ' + buttonClass; 16254 this.menuButton_.removeClass('vjs-control'); 16255 this.addChild(this.menuButton_); 16256 this.update(); 16257 this.enabled_ = true; 16258 const handleClick = (e) => this.handleClick(e); 16259 this.handleMenuKeyUp_ = (e) => this.handleMenuKeyUp(e); 16260 this.on(this.menuButton_, 'tap', handleClick); 16261 this.on(this.menuButton_, 'click', handleClick); 16262 this.on(this.menuButton_, 'keydown', (e) => this.handleKeyDown(e)); 16263 this.on(this.menuButton_, 'mouseenter', () => { 16264 this.addClass('vjs-hover'); 16265 this.menu.show(); 16266 on(document__default['default'], 'keyup', this.handleMenuKeyUp_); 16267 }); 16268 this.on('mouseleave', (e) => this.handleMouseLeave(e)); 16269 this.on('keydown', (e) => this.handleSubmenuKeyDown(e)); 16270 } 16271 16272 /** 16273 * Update the menu based on the current state of its items. 16274 */ 16275 update() { 16276 const menu = this.createMenu(); 16277 if (this.menu) { 16278 this.menu.dispose(); 16279 this.removeChild(this.menu); 16280 } 16281 this.menu = menu; 16282 this.addChild(menu); 16283 16284 /** 16285 * Track the state of the menu button 16286 * 16287 * @type {Boolean} 16288 * @private 16289 */ 16290 this.buttonPressed_ = false; 16291 this.menuButton_.el_.setAttribute('aria-expanded', 'false'); 16292 if (this.items && this.items.length <= this.hideThreshold_) { 16293 this.hide(); 16294 this.menu.contentEl_.removeAttribute('role'); 16295 } else { 16296 this.show(); 16297 this.menu.contentEl_.setAttribute('role', 'menu'); 16298 } 16299 } 16300 16301 /** 16302 * Create the menu and add all items to it. 16303 * 16304 * @return {Menu} 16305 * The constructed menu 16306 */ 16307 createMenu() { 16308 const menu = new Menu(this.player_, { 16309 menuButton: this, 16310 }); 16311 16312 /** 16313 * Hide the menu if the number of items is less than or equal to this threshold. This defaults 16314 * to 0 and whenever we add items which can be hidden to the menu we'll increment it. We list 16315 * it here because every time we run `createMenu` we need to reset the value. 16316 * 16317 * @protected 16318 * @type {Number} 16319 */ 16320 this.hideThreshold_ = 0; 16321 16322 // Add a title list item to the top 16323 if (this.options_.title) { 16324 const titleEl = createEl('li', { 16325 className: 'vjs-menu-title', 16326 textContent: toTitleCase$1(this.options_.title), 16327 tabIndex: -1, 16328 }); 16329 const titleComponent = new Component$1(this.player_, { 16330 el: titleEl, 16331 }); 16332 menu.addItem(titleComponent); 16333 } 16334 this.items = this.createItems(); 16335 if (this.items) { 16336 // Add menu items to the menu 16337 for (let i = 0; i < this.items.length; i++) { 16338 menu.addItem(this.items[i]); 16339 } 16340 } 16341 return menu; 16342 } 16343 16344 /** 16345 * Create the list of menu items. Specific to each subclass. 16346 * 16347 * @abstract 16348 */ 16349 createItems() {} 16350 16351 /** 16352 * Create the `MenuButtons`s DOM element. 16353 * 16354 * @return {Element} 16355 * The element that gets created. 16356 */ 16357 createEl() { 16358 return super.createEl( 16359 'div', 16360 { 16361 className: this.buildWrapperCSSClass(), 16362 }, 16363 {} 16364 ); 16365 } 16366 16367 /** 16368 * Overwrites the `setIcon` method from `Component`. 16369 * In this case, we want the icon to be appended to the menuButton. 16370 * 16371 * @param {string} name 16372 * The icon name to be added. 16373 */ 16374 setIcon(name) { 16375 super.setIcon(name, this.menuButton_.el_); 16376 } 16377 16378 /** 16379 * Allow sub components to stack CSS class names for the wrapper element 16380 * 16381 * @return {string} 16382 * The constructed wrapper DOM `className` 16383 */ 16384 buildWrapperCSSClass() { 16385 let menuButtonClass = 'vjs-menu-button'; 16386 16387 // If the inline option is passed, we want to use different styles altogether. 16388 if (this.options_.inline === true) { 16389 menuButtonClass += '-inline'; 16390 } else { 16391 menuButtonClass += '-popup'; 16392 } 16393 16394 // TODO: Fix the CSS so that this isn't necessary 16395 const buttonClass = Button.prototype.buildCSSClass(); 16396 return `vjs-menu-button ${menuButtonClass} ${buttonClass} ${super.buildCSSClass()}`; 16397 } 16398 16399 /** 16400 * Builds the default DOM `className`. 16401 * 16402 * @return {string} 16403 * The DOM `className` for this object. 16404 */ 16405 buildCSSClass() { 16406 let menuButtonClass = 'vjs-menu-button'; 16407 16408 // If the inline option is passed, we want to use different styles altogether. 16409 if (this.options_.inline === true) { 16410 menuButtonClass += '-inline'; 16411 } else { 16412 menuButtonClass += '-popup'; 16413 } 16414 return `vjs-menu-button ${menuButtonClass} ${super.buildCSSClass()}`; 16415 } 16416 16417 /** 16418 * Get or set the localized control text that will be used for accessibility. 16419 * 16420 * > NOTE: This will come from the internal `menuButton_` element. 16421 * 16422 * @param {string} [text] 16423 * Control text for element. 16424 * 16425 * @param {Element} [el=this.menuButton_.el()] 16426 * Element to set the title on. 16427 * 16428 * @return {string} 16429 * - The control text when getting 16430 */ 16431 controlText(text, el = this.menuButton_.el()) { 16432 return this.menuButton_.controlText(text, el); 16433 } 16434 16435 /** 16436 * Dispose of the `menu-button` and all child components. 16437 */ 16438 dispose() { 16439 this.handleMouseLeave(); 16440 super.dispose(); 16441 } 16442 16443 /** 16444 * Handle a click on a `MenuButton`. 16445 * See {@link ClickableComponent#handleClick} for instances where this is called. 16446 * 16447 * @param {Event} event
16448 * The `keydown`, `tap`, or `click` event that caused this function to be 16449 * called. 16450 * 16451 * @listens tap 16452 * @listens click 16453 */ 16454 handleClick(event) { 16455 if (this.buttonPressed_) { 16456 this.unpressButton(); 16457 } else { 16458 this.pressButton(); 16459 } 16460 } 16461 16462 /** 16463 * Handle `mouseleave` for `MenuButton`. 16464 * 16465 * @param {Event} event 16466 * The `mouseleave` event that caused this function to be called. 16467 * 16468 * @listens mouseleave 16469 */ 16470 handleMouseLeave(event) { 16471 this.removeClass('vjs-hover'); 16472 off(document__default['default'], 'keyup', this.handleMenuKeyUp_); 16473 } 16474 16475 /** 16476 * Set the focus to the actual button, not to this element 16477 */ 16478 focus() { 16479 this.menuButton_.focus(); 16480 } 16481 16482 /** 16483 * Remove the focus from the actual button, not this element 16484 */ 16485 blur() { 16486 this.menuButton_.blur(); 16487 } 16488 16489 /** 16490 * Handle tab, escape, down arrow, and up arrow keys for `MenuButton`. See 16491 * {@link ClickableComponent#handleKeyDown} for instances where this is called. 16492 * 16493 * @param {Event} event 16494 * The `keydown` event that caused this function to be called. 16495 * 16496 * @listens keydown 16497 */ 16498 handleKeyDown(event) { 16499 // Escape or Tab unpress the 'button' 16500 if (event.key === 'Escape' || event.key === 'Tab') { 16501 if (this.buttonPressed_) { 16502 this.unpressButton(); 16503 } 16504 16505 // Don't preventDefault for Tab key - we still want to lose focus 16506 if (!event.key === 'Tab') { 16507 event.preventDefault(); 16508 // Set focus back to the menu button's button 16509 this.menuButton_.focus(); 16510 } 16511 // Up Arrow or Down Arrow also 'press' the button to open the menu 16512 } else if ( 16513 event.key === 'Up' || 16514 (event.key === 'Down' && 16515 !( 16516 this.player_.options_.playerOptions.spatialNavigation && 16517 this.player_.options_.playerOptions.spatialNavigation.enabled 16518 )) 16519 ) { 16520 if (!this.buttonPressed_) { 16521 event.preventDefault(); 16522 this.pressButton(); 16523 } 16524 } 16525 } 16526 16527 /** 16528 * Handle a `keyup` event on a `MenuButton`. The listener for this is added in 16529 * the constructor. 16530 * 16531 * @param {Event} event 16532 * Key press event 16533 * 16534 * @listens keyup 16535 */ 16536 handleMenuKeyUp(event) { 16537 // Escape hides popup menu 16538 if (event.key === 'Escape' || event.key === 'Tab') { 16539 this.removeClass('vjs-hover'); 16540 } 16541 } 16542 16543 /** 16544 * This method name now delegates to `handleSubmenuKeyDown`. This means 16545 * anyone calling `handleSubmenuKeyPress` will not see their method calls 16546 * stop working. 16547 * 16548 * @param {Event} event 16549 * The event that caused this function to be called. 16550 */ 16551 handleSubmenuKeyPress(event) { 16552 this.handleSubmenuKeyDown(event); 16553 } 16554 16555 /** 16556 * Handle a `keydown` event on a sub-menu. The listener for this is added in 16557 * the constructor. 16558 * 16559 * @param {Event} event 16560 * Key press event 16561 * 16562 * @listens keydown 16563 */ 16564 handleSubmenuKeyDown(event) { 16565 // Escape or Tab unpress the 'button' 16566 if (event.key === 'Escape' || event.key === 'Tab') { 16567 if (this.buttonPressed_) { 16568 this.unpressButton(); 16569 } 16570 // Don't preventDefault for Tab key - we still want to lose focus 16571 if (!event.key === 'Tab') { 16572 event.preventDefault(); 16573 // Set focus back to the menu button's button 16574 this.menuButton_.focus(); 16575 } 16576 } 16577 } 16578 16579 /** 16580 * Put the current `MenuButton` into a pressed state. 16581 */ 16582 pressButton() { 16583 if (this.enabled_) { 16584 this.buttonPressed_ = true; 16585 this.menu.show(); 16586 this.menu.lockShowing(); 16587 this.menuButton_.el_.setAttribute('aria-expanded', 'true'); 16588 16589 // set the focus into the submenu, except on iOS where it is resulting in 16590 // undesired scrolling behavior when the player is in an iframe 16591 if (IS_IOS && isInFrame()) { 16592 // Return early so that the menu isn't focused 16593 return; 16594 } 16595 this.menu.focus(); 16596 } 16597 } 16598 16599 /** 16600 * Take the current `MenuButton` out of a pressed state. 16601 */ 16602 unpressButton() { 16603 if (this.enabled_) { 16604 this.buttonPressed_ = false; 16605 this.menu.unlockShowing(); 16606 this.menu.hide(); 16607 this.menuButton_.el_.setAttribute('aria-expanded', 'false'); 16608 } 16609 } 16610 16611 /** 16612 * Disable the `MenuButton`. Don't allow it to be clicked. 16613 */ 16614 disable() { 16615 this.unpressButton(); 16616 this.enabled_ = false;
vendor: 4,450 bytes, lines 16617-16780
16617 this.addClass('vjs-disabled'); 16618 this.menuButton_.disable(); 16619 } 16620 16621 /** 16622 * Enable the `MenuButton`. Allow it to be clicked. 16623 */ 16624 enable() { 16625 this.enabled_ = true; 16626 this.removeClass('vjs-disabled'); 16627 this.menuButton_.enable(); 16628 } 16629} 16630Component$1.registerComponent('MenuButton', MenuButton); 16631 16632/** 16633 * @file track-button.js 16634 */ 16635 16636/** @import Player from './player' */ 16637 16638/** 16639 * The base class for buttons that toggle specific track types (e.g. subtitles). 16640 * 16641 * @extends MenuButton 16642 */ 16643class TrackButton extends MenuButton { 16644 /** 16645 * Creates an instance of this class. 16646 * 16647 * @param {Player} player 16648 * The `Player` that this class should be attached to. 16649 * 16650 * @param {Object} [options] 16651 * The key/value store of player options. 16652 */ 16653 constructor(player, options) { 16654 const tracks = options.tracks; 16655 super(player, options); 16656 if (this.items.length <= 1) { 16657 this.hide(); 16658 } 16659 if (!tracks) { 16660 return; 16661 } 16662 const updateHandler = bind_(this, this.update); 16663 tracks.addEventListener('removetrack', updateHandler); 16664 tracks.addEventListener('addtrack', updateHandler); 16665 tracks.addEventListener('labelchange', updateHandler); 16666 this.player_.on('ready', updateHandler); 16667 this.player_.on('dispose', function () { 16668 tracks.removeEventListener('removetrack', updateHandler); 16669 tracks.removeEventListener('addtrack', updateHandler); 16670 tracks.removeEventListener('labelchange', updateHandler); 16671 }); 16672 } 16673} 16674Component$1.registerComponent('TrackButton', TrackButton); 16675 16676/** 16677 * @file menu-item.js 16678 */ 16679 16680/** @import Player from '../player' */ 16681 16682/** 16683 * The component for a menu item. `<li>` 16684 * 16685 * @extends ClickableComponent 16686 */ 16687class MenuItem extends ClickableComponent { 16688 /** 16689 * Creates an instance of the this class. 16690 * 16691 * @param {Player} player 16692 * The `Player` that this class should be attached to. 16693 * 16694 * @param {Object} [options={}] 16695 * The key/value store of player options. 16696 * 16697 */ 16698 constructor(player, options) { 16699 super(player, options); 16700 this.selectable = options.selectable; 16701 this.isSelected_ = options.selected || false; 16702 this.multiSelectable = options.multiSelectable; 16703 this.selected(this.isSelected_); 16704 if (this.selectable) { 16705 if (this.multiSelectable) { 16706 this.el_.setAttribute('role', 'menuitemcheckbox'); 16707 } else { 16708 this.el_.setAttribute('role', 'menuitemradio'); 16709 } 16710 } else { 16711 this.el_.setAttribute('role', 'menuitem'); 16712 } 16713 } 16714 16715 /** 16716 * Create the `MenuItem's DOM element 16717 * 16718 * @param {string} [type=li] 16719 * Element's node type, not actually used, always set to `li`. 16720 * 16721 * @param {Object} [props={}] 16722 * An object of properties that should be set on the element 16723 * 16724 * @param {Object} [attrs={}] 16725 * An object of attributes that should be set on the element 16726 * 16727 * @return {Element} 16728 * The element that gets created. 16729 */ 16730 createEl(type, props, attrs) { 16731 // The control is textual, not just an icon 16732 this.nonIconControl = true; 16733 const el = super.createEl( 16734 'li', 16735 Object.assign( 16736 { 16737 className: 'vjs-menu-item', 16738 tabIndex: -1, 16739 }, 16740 props 16741 ), 16742 attrs 16743 ); 16744 16745 // swap icon with menu item text. 16746 const menuItemEl = createEl('span', { 16747 className: 'vjs-menu-item-text', 16748 textContent: this.localize(this.options_.label), 16749 }); 16750 16751 // If using SVG icons, the element with vjs-icon-placeholder will be added separately. 16752 if (this.player_.options_.experimentalSvgIcons) { 16753 el.appendChild(menuItemEl); 16754 } else { 16755 el.replaceChild(menuItemEl, el.querySelector('.vjs-icon-placeholder')); 16756 } 16757 return el; 16758 } 16759 16760 /** 16761 * Ignore keys which are used by the menu, but pass any other ones up. See 16762 * {@link ClickableComponent#handleKeyDown} for instances where this is called. 16763 * 16764 * @param {KeyboardEvent} event 16765 * The `keydown` event that caused this function to be called. 16766 * 16767 * @listens keydown 16768 */ 16769 handleKeyDown(event) { 16770 if (!['Tab', 'Escape', 'ArrowUp', 'ArrowLeft', 'ArrowRight', 'ArrowDown'].includes(event.key)) { 16771 // Pass keydown handling up for unused keys 16772 super.handleKeyDown(event); 16773 } 16774 } 16775 16776 /** 16777 * Any click on a `MenuItem` puts it into the selected state. 16778 * See {@link ClickableComponent#handleClick} for instances where this is called. 16779 * 16780 * @param {Event} event
vendor: 7,319 bytes, lines 16781-17012
16781 * The `keydown`, `tap`, or `click` event that caused this function to be 16782 * called. 16783 * 16784 * @listens tap 16785 * @listens click 16786 */ 16787 handleClick(event) { 16788 this.selected(true); 16789 } 16790 16791 /** 16792 * Set the state for this menu item as selected or not. 16793 * 16794 * @param {boolean} selected 16795 * if the menu item is selected or not 16796 */ 16797 selected(selected) { 16798 if (this.selectable) { 16799 if (selected) { 16800 this.addClass('vjs-selected'); 16801 this.el_.setAttribute('aria-checked', 'true'); 16802 // aria-checked isn't fully supported by browsers/screen readers, 16803 // so indicate selected state to screen reader in the control text. 16804 this.controlText(', selected'); 16805 this.isSelected_ = true; 16806 } else { 16807 this.removeClass('vjs-selected'); 16808 this.el_.setAttribute('aria-checked', 'false'); 16809 // Indicate un-selected state to screen reader 16810 this.controlText(''); 16811 this.isSelected_ = false; 16812 } 16813 } 16814 } 16815} 16816Component$1.registerComponent('MenuItem', MenuItem); 16817 16818/** 16819 * @file text-track-menu-item.js 16820 */ 16821 16822/** @import Player from '../../player' */ 16823 16824/** 16825 * The specific menu item type for selecting a language within a text track kind 16826 * 16827 * @extends MenuItem 16828 */ 16829class TextTrackMenuItem extends MenuItem { 16830 /** 16831 * Creates an instance of this class. 16832 * 16833 * @param {Player} player 16834 * The `Player` that this class should be attached to. 16835 * 16836 * @param {Object} [options] 16837 * The key/value store of player options. 16838 */ 16839 constructor(player, options) { 16840 const track = options.track; 16841 const tracks = player.textTracks(); 16842 16843 // Modify options for parent MenuItem class's init. 16844 options.label = track.label || track.language || 'Unknown'; 16845 options.selected = track.mode === 'showing'; 16846 super(player, options); 16847 this.track = track; 16848 // Determine the relevant kind(s) of tracks for this component and filter 16849 // out empty kinds. 16850 this.kinds = (options.kinds || [options.kind || this.track.kind]).filter(Boolean); 16851 const changeHandler = (...args) => { 16852 this.handleTracksChange.apply(this, args); 16853 }; 16854 const selectedLanguageChangeHandler = (...args) => { 16855 this.handleSelectedLanguageChange.apply(this, args); 16856 }; 16857 player.on(['loadstart', 'texttrackchange'], changeHandler); 16858 tracks.addEventListener('change', changeHandler); 16859 tracks.addEventListener('selectedlanguagechange', selectedLanguageChangeHandler); 16860 this.on('dispose', function () { 16861 player.off(['loadstart', 'texttrackchange'], changeHandler); 16862 tracks.removeEventListener('change', changeHandler); 16863 tracks.removeEventListener('selectedlanguagechange', selectedLanguageChangeHandler); 16864 }); 16865 16866 // iOS7 doesn't dispatch change events to TextTrackLists when an 16867 // associated track's mode changes. Without something like 16868 // Object.observe() (also not present on iOS7), it's not 16869 // possible to detect changes to the mode attribute and polyfill 16870 // the change event. As a poor substitute, we manually dispatch 16871 // change events whenever the controls modify the mode. 16872 if (tracks.onchange === undefined) { 16873 let event; 16874 this.on(['tap', 'click'], function () { 16875 if (typeof window__default['default'].Event !== 'object') { 16876 // Android 2.3 throws an Illegal Constructor error for window.Event 16877 try { 16878 event = new window__default['default'].Event('change'); 16879 } catch (err) { 16880 // continue regardless of error 16881 } 16882 } 16883 if (!event) { 16884 event = document__default['default'].createEvent('Event'); 16885 event.initEvent('change', true, true); 16886 } 16887 tracks.dispatchEvent(event); 16888 }); 16889 } 16890 16891 // set the default state based on current tracks 16892 this.handleTracksChange(); 16893 } 16894 16895 /** 16896 * This gets called when an `TextTrackMenuItem` is "clicked". See 16897 * {@link ClickableComponent} for more detailed information on what a click can be. 16898 * 16899 * @param {Event} event 16900 * The `keydown`, `tap`, or `click` event that caused this function to be 16901 * called. 16902 * 16903 * @listens tap 16904 * @listens click 16905 */ 16906 handleClick(event) { 16907 const referenceTrack = this.track; 16908 const tracks = this.player_.textTracks(); 16909 super.handleClick(event); 16910 if (!tracks) { 16911 return; 16912 } 16913 for (let i = 0; i < tracks.length; i++) { 16914 const track = tracks[i]; 16915 16916 // If the track from the text tracks list is not of the right kind, 16917 // skip it. We do not want to affect tracks of incompatible kind(s). 16918 if (this.kinds.indexOf(track.kind) === -1) { 16919 continue; 16920 } 16921 16922 // If this text track is the component's track and it is not showing, 16923 // set it to showing. 16924 if (track === referenceTrack) { 16925 if (track.mode !== 'showing') { 16926 track.mode = 'showing'; 16927 } 16928 16929 // If this text track is not the component's track and it is not 16930 // disabled, set it to disabled. 16931 } else if (track.mode !== 'disabled') { 16932 track.mode = 'disabled'; 16933 } 16934 } 16935 } 16936 16937 /** 16938 * Handle text track list change 16939 * 16940 * @param {Event} event 16941 * The `change` event that caused this function to be called. 16942 * 16943 * @listens TextTrackList#change 16944 */ 16945 handleTracksChange(event) { 16946 const shouldBeSelected = this.track.mode === 'showing'; 16947 16948 // Prevent redundant selected() calls because they may cause 16949 // screen readers to read the appended control text unnecessarily 16950 if (shouldBeSelected !== this.isSelected_) { 16951 this.selected(shouldBeSelected); 16952 } 16953 } 16954 handleSelectedLanguageChange(event) { 16955 if (this.track.mode === 'showing') { 16956 const selectedLanguage = this.player_.cache_.selectedLanguage; 16957 16958 // Don't replace the kind of track across the same language 16959 if ( 16960 selectedLanguage && 16961 selectedLanguage.enabled && 16962 selectedLanguage.language === this.track.language && 16963 selectedLanguage.kind !== this.track.kind 16964 ) { 16965 return; 16966 } 16967 this.player_.cache_.selectedLanguage = { 16968 enabled: true, 16969 language: this.track.language, 16970 kind: this.track.kind, 16971 }; 16972 } 16973 } 16974 dispose() { 16975 // remove reference to track object on dispose 16976 this.track = null; 16977 super.dispose(); 16978 } 16979} 16980Component$1.registerComponent('TextTrackMenuItem', TextTrackMenuItem); 16981 16982/** 16983 * @file off-text-track-menu-item.js 16984 */ 16985 16986/** @import Player from '../../player' */ 16987 16988/** 16989 * A special menu item for turning off a specific type of text track 16990 * 16991 * @extends TextTrackMenuItem 16992 */ 16993class OffTextTrackMenuItem extends TextTrackMenuItem { 16994 /** 16995 * Creates an instance of this class. 16996 * 16997 * @param {Player} player 16998 * The `Player` that this class should be attached to. 16999 * 17000 * @param {Object} [options] 17001 * The key/value store of player options. 17002 */ 17003 constructor(player, options) { 17004 // Create pseudo track info 17005 // Requires options['kind'] 17006 options.track = { 17007 player, 17008 // it is no longer necessary to store `kind` or `kinds` on the track itself 17009 // since they are now stored in the `kinds` property of all instances of 17010 // TextTrackMenuItem, but this will remain for backwards compatibility 17011 kind: options.kind, 17012 kinds: options.kinds,
vendor: 15,716 bytes, lines 17013-17607
17013 default: false, 17014 mode: 'disabled', 17015 }; 17016 if (!options.kinds) { 17017 options.kinds = [options.kind]; 17018 } 17019 if (options.label) { 17020 options.track.label = options.label; 17021 } else { 17022 options.track.label = options.kinds.join(' and ') + ' off'; 17023 } 17024 17025 // MenuItem is selectable 17026 options.selectable = true; 17027 // MenuItem is NOT multiSelectable (i.e. only one can be marked "selected" at a time) 17028 options.multiSelectable = false; 17029 super(player, options); 17030 } 17031 17032 /** 17033 * Handle text track change 17034 * 17035 * @param {Event} event 17036 * The event that caused this function to run 17037 */ 17038 handleTracksChange(event) { 17039 const tracks = this.player().textTracks(); 17040 let shouldBeSelected = true; 17041 for (let i = 0, l = tracks.length; i < l; i++) { 17042 const track = tracks[i]; 17043 if (this.options_.kinds.indexOf(track.kind) > -1 && track.mode === 'showing') { 17044 shouldBeSelected = false; 17045 break; 17046 } 17047 } 17048 17049 // Prevent redundant selected() calls because they may cause 17050 // screen readers to read the appended control text unnecessarily 17051 if (shouldBeSelected !== this.isSelected_) { 17052 this.selected(shouldBeSelected); 17053 } 17054 } 17055 handleSelectedLanguageChange(event) { 17056 const tracks = this.player().textTracks(); 17057 let allHidden = true; 17058 for (let i = 0, l = tracks.length; i < l; i++) { 17059 const track = tracks[i]; 17060 if ( 17061 ['captions', 'descriptions', 'subtitles'].indexOf(track.kind) > -1 && 17062 track.mode === 'showing' 17063 ) { 17064 allHidden = false; 17065 break; 17066 } 17067 } 17068 if (allHidden) { 17069 this.player_.cache_.selectedLanguage = { 17070 enabled: false, 17071 }; 17072 } 17073 } 17074 17075 /** 17076 * Update control text and label on languagechange 17077 */ 17078 handleLanguagechange() { 17079 this.$('.vjs-menu-item-text').textContent = this.player_.localize(this.options_.label); 17080 super.handleLanguagechange(); 17081 } 17082} 17083Component$1.registerComponent('OffTextTrackMenuItem', OffTextTrackMenuItem); 17084 17085/** 17086 * @file text-track-button.js 17087 */ 17088 17089/** @import Player from '../../player' */ 17090 17091/** 17092 * The base class for buttons that toggle specific text track types (e.g. subtitles) 17093 * 17094 * @extends MenuButton 17095 */ 17096class TextTrackButton extends TrackButton { 17097 /** 17098 * Creates an instance of this class. 17099 * 17100 * @param {Player} player 17101 * The `Player` that this class should be attached to. 17102 * 17103 * @param {Object} [options={}] 17104 * The key/value store of player options. 17105 */ 17106 constructor(player, options = {}) { 17107 options.tracks = player.textTracks(); 17108 super(player, options); 17109 } 17110 17111 /** 17112 * Create a menu item for each text track 17113 * 17114 * @param {TextTrackMenuItem[]} [items=[]] 17115 * Existing array of items to use during creation 17116 * 17117 * @return {TextTrackMenuItem[]} 17118 * Array of menu items that were created 17119 */ 17120 createItems(items = [], TrackMenuItem = TextTrackMenuItem) { 17121 // Label is an override for the [track] off label 17122 // USed to localise captions/subtitles 17123 let label; 17124 if (this.label_) { 17125 label = `${this.label_} off`; 17126 } 17127 // Add an OFF menu item to turn all tracks off 17128 items.push( 17129 new OffTextTrackMenuItem(this.player_, { 17130 kinds: this.kinds_, 17131 kind: this.kind_, 17132 label, 17133 }) 17134 ); 17135 this.hideThreshold_ += 1; 17136 const tracks = this.player_.textTracks(); 17137 if (!Array.isArray(this.kinds_)) { 17138 this.kinds_ = [this.kind_]; 17139 } 17140 for (let i = 0; i < tracks.length; i++) { 17141 const track = tracks[i]; 17142 17143 // only add tracks that are of an appropriate kind and have a label 17144 if (this.kinds_.indexOf(track.kind) > -1) { 17145 const item = new TrackMenuItem(this.player_, { 17146 track, 17147 kinds: this.kinds_, 17148 kind: this.kind_, 17149 // MenuItem is selectable 17150 selectable: true, 17151 // MenuItem is NOT multiSelectable (i.e. only one can be marked "selected" at a time) 17152 multiSelectable: false, 17153 }); 17154 item.addClass(`vjs-${track.kind}-menu-item`); 17155 items.push(item); 17156 } 17157 } 17158 return items; 17159 } 17160} 17161Component$1.registerComponent('TextTrackButton', TextTrackButton); 17162 17163/** 17164 * @file chapters-track-menu-item.js 17165 */ 17166 17167/** @import Player from '../../player' */ 17168 17169/** 17170 * The chapter track menu item 17171 * 17172 * @extends MenuItem 17173 */ 17174class ChaptersTrackMenuItem extends MenuItem { 17175 /** 17176 * Creates an instance of this class. 17177 * 17178 * @param {Player} player 17179 * The `Player` that this class should be attached to. 17180 * 17181 * @param {Object} [options] 17182 * The key/value store of player options. 17183 */ 17184 constructor(player, options) { 17185 const track = options.track; 17186 const cue = options.cue; 17187 const currentTime = player.currentTime(); 17188 17189 // Modify options for parent MenuItem class's init. 17190 options.selectable = true; 17191 options.multiSelectable = false; 17192 options.label = cue.text; 17193 options.selected = cue.startTime <= currentTime && currentTime < cue.endTime; 17194 super(player, options); 17195 this.track = track; 17196 this.cue = cue; 17197 } 17198 17199 /** 17200 * This gets called when an `ChaptersTrackMenuItem` is "clicked". See 17201 * {@link ClickableComponent} for more detailed information on what a click can be. 17202 * 17203 * @param {Event} [event] 17204 * The `keydown`, `tap`, or `click` event that caused this function to be 17205 * called. 17206 * 17207 * @listens tap 17208 * @listens click 17209 */ 17210 handleClick(event) { 17211 super.handleClick(); 17212 this.player_.currentTime(this.cue.startTime); 17213 } 17214} 17215Component$1.registerComponent('ChaptersTrackMenuItem', ChaptersTrackMenuItem); 17216 17217/** 17218 * @file chapters-button.js 17219 */ 17220 17221/** @import Player from '../../player' */ 17222/** @import Menu from '../../menu/menu' */ 17223/** @import TextTrack from '../../tracks/text-track' */ 17224/** @import TextTrackMenuItem from '../text-track-controls/text-track-menu-item' */ 17225 17226/** 17227 * The button component for toggling and selecting chapters 17228 * Chapters act much differently than other text tracks 17229 * Cues are navigation vs. other tracks of alternative languages 17230 * 17231 * @extends TextTrackButton 17232 */ 17233class ChaptersButton extends TextTrackButton { 17234 /** 17235 * Creates an instance of this class. 17236 * 17237 * @param {Player} player 17238 * The `Player` that this class should be attached to. 17239 * 17240 * @param {Object} [options] 17241 * The key/value store of player options. 17242 * 17243 * @param {Function} [ready] 17244 * The function to call when this function is ready. 17245 */ 17246 constructor(player, options, ready) { 17247 super(player, options, ready); 17248 this.setIcon('chapters'); 17249 this.selectCurrentItem_ = () => { 17250 this.items.forEach((item) => { 17251 item.selected(this.track_.activeCues[0] === item.cue); 17252 }); 17253 }; 17254 } 17255 17256 /** 17257 * Builds the default DOM `className`. 17258 * 17259 * @return {string} 17260 * The DOM `className` for this object. 17261 */ 17262 buildCSSClass() { 17263 return `vjs-chapters-button ${super.buildCSSClass()}`; 17264 } 17265 buildWrapperCSSClass() { 17266 return `vjs-chapters-button ${super.buildWrapperCSSClass()}`; 17267 } 17268 17269 /** 17270 * Update the menu based on the current state of its items. 17271 * 17272 * @param {Event} [event] 17273 * An event that triggered this function to run. 17274 * 17275 * @listens TextTrackList#addtrack 17276 * @listens TextTrackList#removetrack 17277 * @listens TextTrackList#change 17278 */ 17279 update(event) { 17280 if (event && event.track && event.track.kind !== 'chapters') { 17281 return; 17282 } 17283 const track = this.findChaptersTrack(); 17284 if (track !== this.track_) { 17285 this.setTrack(track); 17286 super.update(); 17287 } else if (!this.items || (track && track.cues && track.cues.length !== this.items.length)) { 17288 // Update the menu initially or if the number of cues has changed since set 17289 super.update(); 17290 } 17291 } 17292 17293 /** 17294 * Set the currently selected track for the chapters button. 17295 * 17296 * @param {TextTrack} track 17297 * The new track to select. Nothing will change if this is the currently selected 17298 * track. 17299 */ 17300 setTrack(track) { 17301 if (this.track_ === track) { 17302 return; 17303 } 17304 if (!this.updateHandler_) { 17305 this.updateHandler_ = this.update.bind(this); 17306 } 17307 17308 // here this.track_ refers to the old track instance 17309 if (this.track_) { 17310 const remoteTextTrackEl = this.player_ 17311 .remoteTextTrackEls() 17312 .getTrackElementByTrack_(this.track_); 17313 if (remoteTextTrackEl) { 17314 remoteTextTrackEl.removeEventListener('load', this.updateHandler_); 17315 } 17316 this.track_.removeEventListener('cuechange', this.selectCurrentItem_); 17317 this.track_ = null; 17318 } 17319 this.track_ = track; 17320 17321 // here this.track_ refers to the new track instance 17322 if (this.track_) { 17323 this.track_.mode = 'hidden'; 17324 const remoteTextTrackEl = this.player_ 17325 .remoteTextTrackEls() 17326 .getTrackElementByTrack_(this.track_); 17327 if (remoteTextTrackEl) { 17328 remoteTextTrackEl.addEventListener('load', this.updateHandler_); 17329 } 17330 this.track_.addEventListener('cuechange', this.selectCurrentItem_); 17331 } 17332 } 17333 17334 /** 17335 * Find the track object that is currently in use by this ChaptersButton 17336 * 17337 * @return {TextTrack|undefined} 17338 * The current track or undefined if none was found. 17339 */ 17340 findChaptersTrack() { 17341 const tracks = this.player_.textTracks() || []; 17342 for (let i = tracks.length - 1; i >= 0; i--) { 17343 // We will always choose the last track as our chaptersTrack 17344 const track = tracks[i]; 17345 if (track.kind === this.kind_) { 17346 return track; 17347 } 17348 } 17349 } 17350 17351 /** 17352 * Get the caption for the ChaptersButton based on the track label. This will also 17353 * use the current tracks localized kind as a fallback if a label does not exist. 17354 * 17355 * @return {string} 17356 * The tracks current label or the localized track kind. 17357 */ 17358 getMenuCaption() { 17359 if (this.track_ && this.track_.label) { 17360 return this.track_.label; 17361 } 17362 return this.localize(toTitleCase$1(this.kind_)); 17363 } 17364 17365 /** 17366 * Create menu from chapter track 17367 * 17368 * @return {Menu} 17369 * New menu for the chapter buttons 17370 */ 17371 createMenu() { 17372 this.options_.title = this.getMenuCaption(); 17373 return super.createMenu(); 17374 } 17375 17376 /** 17377 * Create a menu item for each text track 17378 * 17379 * @return {TextTrackMenuItem[]} 17380 * Array of menu items 17381 */ 17382 createItems() { 17383 const items = []; 17384 if (!this.track_) { 17385 return items; 17386 } 17387 const cues = this.track_.cues; 17388 if (!cues) { 17389 return items; 17390 } 17391 for (let i = 0, l = cues.length; i < l; i++) { 17392 const cue = cues[i]; 17393 const mi = new ChaptersTrackMenuItem(this.player_, { 17394 track: this.track_, 17395 cue, 17396 }); 17397 items.push(mi); 17398 } 17399 return items; 17400 } 17401} 17402 17403/** 17404 * `kind` of TextTrack to look for to associate it with this menu. 17405 * 17406 * @type {string} 17407 * @private 17408 */ 17409ChaptersButton.prototype.kind_ = 'chapters'; 17410 17411/** 17412 * The text that should display over the `ChaptersButton`s controls. Added for localization. 17413 * 17414 * @type {string} 17415 * @protected 17416 */ 17417ChaptersButton.prototype.controlText_ = 'Chapters'; 17418Component$1.registerComponent('ChaptersButton', ChaptersButton); 17419 17420/** 17421 * @file descriptions-button.js 17422 */ 17423 17424/** @import Player from '../../player' */ 17425 17426/** 17427 * The button component for toggling and selecting descriptions 17428 * 17429 * @extends TextTrackButton 17430 */ 17431class DescriptionsButton extends TextTrackButton { 17432 /** 17433 * Creates an instance of this class. 17434 * 17435 * @param {Player} player 17436 * The `Player` that this class should be attached to. 17437 * 17438 * @param {Object} [options] 17439 * The key/value store of player options. 17440 * 17441 * @param {Function} [ready] 17442 * The function to call when this component is ready. 17443 */ 17444 constructor(player, options, ready) { 17445 super(player, options, ready); 17446 this.setIcon('audio-description'); 17447 const tracks = player.textTracks(); 17448 const changeHandler = bind_(this, this.handleTracksChange); 17449 tracks.addEventListener('change', changeHandler); 17450 this.on('dispose', function () { 17451 tracks.removeEventListener('change', changeHandler); 17452 }); 17453 } 17454 17455 /** 17456 * Handle text track change 17457 * 17458 * @param {Event} event 17459 * The event that caused this function to run 17460 * 17461 * @listens TextTrackList#change 17462 */ 17463 handleTracksChange(event) { 17464 const tracks = this.player().textTracks(); 17465 let disabled = false; 17466 17467 // Check whether a track of a different kind is showing 17468 for (let i = 0, l = tracks.length; i < l; i++) { 17469 const track = tracks[i]; 17470 if (track.kind !== this.kind_ && track.mode === 'showing') { 17471 disabled = true; 17472 break; 17473 } 17474 } 17475 17476 // If another track is showing, disable this menu button 17477 if (disabled) { 17478 this.disable(); 17479 } else { 17480 this.enable(); 17481 } 17482 } 17483 17484 /** 17485 * Builds the default DOM `className`. 17486 * 17487 * @return {string} 17488 * The DOM `className` for this object. 17489 */ 17490 buildCSSClass() { 17491 return `vjs-descriptions-button ${super.buildCSSClass()}`; 17492 } 17493 buildWrapperCSSClass() { 17494 return `vjs-descriptions-button ${super.buildWrapperCSSClass()}`; 17495 } 17496} 17497 17498/** 17499 * `kind` of TextTrack to look for to associate it with this menu. 17500 * 17501 * @type {string} 17502 * @private 17503 */ 17504DescriptionsButton.prototype.kind_ = 'descriptions'; 17505 17506/** 17507 * The text that should display over the `DescriptionsButton`s controls. Added for localization. 17508 * 17509 * @type {string} 17510 * @protected 17511 */ 17512DescriptionsButton.prototype.controlText_ = 'Descriptions'; 17513Component$1.registerComponent('DescriptionsButton', DescriptionsButton); 17514 17515/** 17516 * @file subtitles-button.js 17517 */ 17518 17519/** @import Player from '../../player' */ 17520 17521/** 17522 * The button component for toggling and selecting subtitles 17523 * 17524 * @extends TextTrackButton 17525 */ 17526class SubtitlesButton extends TextTrackButton { 17527 /** 17528 * Creates an instance of this class. 17529 * 17530 * @param {Player} player 17531 * The `Player` that this class should be attached to. 17532 * 17533 * @param {Object} [options] 17534 * The key/value store of player options. 17535 * 17536 * @param {Function} [ready] 17537 * The function to call when this component is ready. 17538 */ 17539 constructor(player, options, ready) { 17540 super(player, options, ready); 17541 this.setIcon('subtitles'); 17542 } 17543 17544 /** 17545 * Builds the default DOM `className`. 17546 * 17547 * @return {string} 17548 * The DOM `className` for this object. 17549 */ 17550 buildCSSClass() { 17551 return `vjs-subtitles-button ${super.buildCSSClass()}`; 17552 } 17553 buildWrapperCSSClass() { 17554 return `vjs-subtitles-button ${super.buildWrapperCSSClass()}`; 17555 } 17556} 17557 17558/** 17559 * `kind` of TextTrack to look for to associate it with this menu. 17560 * 17561 * @type {string} 17562 * @private 17563 */ 17564SubtitlesButton.prototype.kind_ = 'subtitles'; 17565 17566/** 17567 * The text that should display over the `SubtitlesButton`s controls. Added for localization. 17568 * 17569 * @type {string} 17570 * @protected 17571 */ 17572SubtitlesButton.prototype.controlText_ = 'Subtitles'; 17573Component$1.registerComponent('SubtitlesButton', SubtitlesButton); 17574 17575/** 17576 * @file caption-settings-menu-item.js 17577 */ 17578 17579/** @import Player from '../../player' */ 17580 17581/** 17582 * The menu item for caption track settings menu 17583 * 17584 * @extends TextTrackMenuItem 17585 */ 17586class CaptionSettingsMenuItem extends TextTrackMenuItem { 17587 /** 17588 * Creates an instance of this class. 17589 * 17590 * @param {Player} player 17591 * The `Player` that this class should be attached to. 17592 * 17593 * @param {Object} [options] 17594 * The key/value store of player options. 17595 */ 17596 constructor(player, options) { 17597 options.track = { 17598 player, 17599 kind: options.kind, 17600 label: options.kind + ' settings', 17601 selectable: false, 17602 default: false, 17603 mode: 'disabled', 17604 }; 17605 17606 // CaptionSettingsMenuItem has no concept of 'selected' 17607 options.selectable = false;
vendor: 8,251 bytes, lines 17608-17928
17608 options.name = 'CaptionSettingsMenuItem'; 17609 super(player, options); 17610 this.addClass('vjs-texttrack-settings'); 17611 this.controlText(', opens ' + options.kind + ' settings dialog'); 17612 } 17613 17614 /** 17615 * This gets called when an `CaptionSettingsMenuItem` is "clicked". See 17616 * {@link ClickableComponent} for more detailed information on what a click can be. 17617 * 17618 * @param {Event} [event] 17619 * The `keydown`, `tap`, or `click` event that caused this function to be 17620 * called. 17621 * 17622 * @listens tap 17623 * @listens click 17624 */ 17625 handleClick(event) { 17626 this.player().getChild('textTrackSettings').open(); 17627 } 17628 17629 /** 17630 * Update control text and label on languagechange 17631 */ 17632 handleLanguagechange() { 17633 this.$('.vjs-menu-item-text').textContent = this.player_.localize( 17634 this.options_.kind + ' settings' 17635 ); 17636 super.handleLanguagechange(); 17637 } 17638} 17639Component$1.registerComponent('CaptionSettingsMenuItem', CaptionSettingsMenuItem); 17640 17641/** 17642 * @file captions-button.js 17643 */ 17644 17645/** @import Player from '../../player' */ 17646 17647/** 17648 * The button component for toggling and selecting captions 17649 * 17650 * @extends TextTrackButton 17651 */ 17652class CaptionsButton extends TextTrackButton { 17653 /** 17654 * Creates an instance of this class. 17655 * 17656 * @param {Player} player 17657 * The `Player` that this class should be attached to. 17658 * 17659 * @param {Object} [options] 17660 * The key/value store of player options. 17661 * 17662 * @param {Function} [ready] 17663 * The function to call when this component is ready. 17664 */ 17665 constructor(player, options, ready) { 17666 super(player, options, ready); 17667 this.setIcon('captions'); 17668 } 17669 17670 /** 17671 * Builds the default DOM `className`. 17672 * 17673 * @return {string} 17674 * The DOM `className` for this object. 17675 */ 17676 buildCSSClass() { 17677 return `vjs-captions-button ${super.buildCSSClass()}`; 17678 } 17679 buildWrapperCSSClass() { 17680 return `vjs-captions-button ${super.buildWrapperCSSClass()}`; 17681 } 17682 17683 /** 17684 * Create caption menu items 17685 * 17686 * @return {CaptionSettingsMenuItem[]} 17687 * The array of current menu items. 17688 */ 17689 createItems() { 17690 const items = []; 17691 if ( 17692 !(this.player().tech_ && this.player().tech_.featuresNativeTextTracks) && 17693 this.player().getChild('textTrackSettings') 17694 ) { 17695 items.push( 17696 new CaptionSettingsMenuItem(this.player_, { 17697 kind: this.kind_, 17698 }) 17699 ); 17700 this.hideThreshold_ += 1; 17701 } 17702 return super.createItems(items); 17703 } 17704} 17705 17706/** 17707 * `kind` of TextTrack to look for to associate it with this menu. 17708 * 17709 * @type {string} 17710 * @private 17711 */ 17712CaptionsButton.prototype.kind_ = 'captions'; 17713 17714/** 17715 * The text that should display over the `CaptionsButton`s controls. Added for localization. 17716 * 17717 * @type {string} 17718 * @protected 17719 */ 17720CaptionsButton.prototype.controlText_ = 'Captions'; 17721Component$1.registerComponent('CaptionsButton', CaptionsButton); 17722 17723/** 17724 * @file subs-caps-menu-item.js 17725 */ 17726 17727/** 17728 * SubsCapsMenuItem has an [cc] icon to distinguish captions from subtitles 17729 * in the SubsCapsMenu. 17730 * 17731 * @extends TextTrackMenuItem 17732 */ 17733class SubsCapsMenuItem extends TextTrackMenuItem { 17734 createEl(type, props, attrs) { 17735 const el = super.createEl(type, props, attrs); 17736 const parentSpan = el.querySelector('.vjs-menu-item-text'); 17737 if (this.options_.track.kind === 'captions') { 17738 if (this.player_.options_.experimentalSvgIcons) { 17739 this.setIcon('captions', el); 17740 } else { 17741 parentSpan.appendChild( 17742 createEl( 17743 'span', 17744 { 17745 className: 'vjs-icon-placeholder', 17746 }, 17747 { 17748 'aria-hidden': true, 17749 } 17750 ) 17751 ); 17752 } 17753 parentSpan.appendChild( 17754 createEl('span', { 17755 className: 'vjs-control-text', 17756 // space added as the text will visually flow with the 17757 // label 17758 textContent: ` ${this.localize('Captions')}`, 17759 }) 17760 ); 17761 } 17762 return el; 17763 } 17764} 17765Component$1.registerComponent('SubsCapsMenuItem', SubsCapsMenuItem); 17766 17767/** 17768 * @file sub-caps-button.js 17769 */ 17770 17771/** @import Player from '../../player' */ 17772 17773/** 17774 * The button component for toggling and selecting captions and/or subtitles 17775 * 17776 * @extends TextTrackButton 17777 */ 17778class SubsCapsButton extends TextTrackButton { 17779 /** 17780 * Creates an instance of this class. 17781 * 17782 * @param {Player} player 17783 * The `Player` that this class should be attached to. 17784 * 17785 * @param {Object} [options] 17786 * The key/value store of player options. 17787 * 17788 * @param {Function} [ready] 17789 * The function to call when this component is ready. 17790 */ 17791 constructor(player, options = {}) { 17792 super(player, options); 17793 17794 // Although North America uses "captions" in most cases for 17795 // "captions and subtitles" other locales use "subtitles" 17796 this.label_ = 'subtitles'; 17797 this.setIcon('subtitles'); 17798 if (['en', 'en-us', 'en-ca', 'fr-ca'].indexOf(this.player_.language_) > -1) { 17799 this.label_ = 'captions'; 17800 this.setIcon('captions'); 17801 } 17802 this.menuButton_.controlText(toTitleCase$1(this.label_)); 17803 } 17804 17805 /** 17806 * Builds the default DOM `className`. 17807 * 17808 * @return {string} 17809 * The DOM `className` for this object. 17810 */ 17811 buildCSSClass() { 17812 return `vjs-subs-caps-button ${super.buildCSSClass()}`; 17813 } 17814 buildWrapperCSSClass() { 17815 return `vjs-subs-caps-button ${super.buildWrapperCSSClass()}`; 17816 } 17817 17818 /** 17819 * Create caption/subtitles menu items 17820 * 17821 * @return {CaptionSettingsMenuItem[]} 17822 * The array of current menu items. 17823 */ 17824 createItems() { 17825 let items = []; 17826 if ( 17827 !(this.player().tech_ && this.player().tech_.featuresNativeTextTracks) && 17828 this.player().getChild('textTrackSettings') 17829 ) { 17830 items.push( 17831 new CaptionSettingsMenuItem(this.player_, { 17832 kind: this.label_, 17833 }) 17834 ); 17835 this.hideThreshold_ += 1; 17836 } 17837 items = super.createItems(items, SubsCapsMenuItem); 17838 return items; 17839 } 17840} 17841 17842/** 17843 * `kind`s of TextTrack to look for to associate it with this menu. 17844 * 17845 * @type {array} 17846 * @private 17847 */ 17848SubsCapsButton.prototype.kinds_ = ['captions', 'subtitles']; 17849 17850/** 17851 * The text that should display over the `SubsCapsButton`s controls. 17852 * 17853 * 17854 * @type {string} 17855 * @protected 17856 */ 17857SubsCapsButton.prototype.controlText_ = 'Subtitles'; 17858Component$1.registerComponent('SubsCapsButton', SubsCapsButton); 17859 17860/** 17861 * @file audio-track-menu-item.js 17862 */ 17863 17864/** @import Player from '../../player' */ 17865 17866/** 17867 * An {@link AudioTrack} {@link MenuItem} 17868 * 17869 * @extends MenuItem 17870 */ 17871class AudioTrackMenuItem extends MenuItem { 17872 /** 17873 * Creates an instance of this class. 17874 * 17875 * @param {Player} player 17876 * The `Player` that this class should be attached to. 17877 * 17878 * @param {Object} [options] 17879 * The key/value store of player options. 17880 */ 17881 constructor(player, options) { 17882 const track = options.track; 17883 const tracks = player.audioTracks(); 17884 17885 // Modify options for parent MenuItem class's init. 17886 options.label = track.label || track.language || 'Unknown'; 17887 options.selected = track.enabled; 17888 super(player, options); 17889 this.track = track; 17890 this.addClass(`vjs-${track.kind}-menu-item`); 17891 const changeHandler = (...args) => { 17892 this.handleTracksChange.apply(this, args); 17893 }; 17894 tracks.addEventListener('change', changeHandler); 17895 this.on('dispose', () => { 17896 tracks.removeEventListener('change', changeHandler); 17897 }); 17898 } 17899 createEl(type, props, attrs) { 17900 const el = super.createEl(type, props, attrs); 17901 const parentSpan = el.querySelector('.vjs-menu-item-text'); 17902 if (['main-desc', 'descriptions'].indexOf(this.options_.track.kind) >= 0) { 17903 parentSpan.appendChild( 17904 createEl( 17905 'span', 17906 { 17907 className: 'vjs-icon-placeholder', 17908 }, 17909 { 17910 'aria-hidden': true, 17911 } 17912 ) 17913 ); 17914 parentSpan.appendChild( 17915 createEl('span', { 17916 className: 'vjs-control-text', 17917 textContent: ' ' + this.localize('Descriptions'), 17918 }) 17919 ); 17920 } 17921 return el; 17922 } 17923 17924 /** 17925 * This gets called when an `AudioTrackMenuItem is "clicked". See {@link ClickableComponent} 17926 * for more detailed information on what a click can be. 17927 * 17928 * @param {Event} [event]
vendor: 4,223 bytes, lines 17929-18087
17929 * The `keydown`, `tap`, or `click` event that caused this function to be 17930 * called. 17931 * 17932 * @listens tap 17933 * @listens click 17934 */ 17935 handleClick(event) { 17936 super.handleClick(event); 17937 17938 // the audio track list will automatically toggle other tracks 17939 // off for us. 17940 this.track.enabled = true; 17941 17942 // when native audio tracks are used, we want to make sure that other tracks are turned off 17943 if (this.player_.tech_.featuresNativeAudioTracks) { 17944 const tracks = this.player_.audioTracks(); 17945 for (let i = 0; i < tracks.length; i++) { 17946 const track = tracks[i]; 17947 17948 // skip the current track since we enabled it above 17949 if (track === this.track) { 17950 continue; 17951 } 17952 track.enabled = track === this.track; 17953 } 17954 } 17955 } 17956 17957 /** 17958 * Handle any {@link AudioTrack} change. 17959 * 17960 * @param {Event} [event] 17961 * The {@link AudioTrackList#change} event that caused this to run. 17962 * 17963 * @listens AudioTrackList#change 17964 */ 17965 handleTracksChange(event) { 17966 this.selected(this.track.enabled); 17967 } 17968} 17969Component$1.registerComponent('AudioTrackMenuItem', AudioTrackMenuItem); 17970 17971/** 17972 * @file audio-track-button.js 17973 */ 17974 17975/** 17976 * The base class for buttons that toggle specific {@link AudioTrack} types. 17977 * 17978 * @extends TrackButton 17979 */ 17980class AudioTrackButton extends TrackButton { 17981 /** 17982 * Creates an instance of this class. 17983 * 17984 * @param {Player} player 17985 * The `Player` that this class should be attached to. 17986 * 17987 * @param {Object} [options={}] 17988 * The key/value store of player options. 17989 */ 17990 constructor(player, options = {}) { 17991 options.tracks = player.audioTracks(); 17992 super(player, options); 17993 this.setIcon('audio'); 17994 } 17995 17996 /** 17997 * Builds the default DOM `className`. 17998 * 17999 * @return {string} 18000 * The DOM `className` for this object. 18001 */ 18002 buildCSSClass() { 18003 return `vjs-audio-button ${super.buildCSSClass()}`; 18004 } 18005 buildWrapperCSSClass() { 18006 return `vjs-audio-button ${super.buildWrapperCSSClass()}`; 18007 } 18008 18009 /** 18010 * Create a menu item for each audio track 18011 * 18012 * @param {AudioTrackMenuItem[]} [items=[]] 18013 * An array of existing menu items to use. 18014 * 18015 * @return {AudioTrackMenuItem[]} 18016 * An array of menu items 18017 */ 18018 createItems(items = []) { 18019 // if there's only one audio track, there no point in showing it 18020 this.hideThreshold_ = 1; 18021 const tracks = this.player_.audioTracks(); 18022 for (let i = 0; i < tracks.length; i++) { 18023 const track = tracks[i]; 18024 items.push( 18025 new AudioTrackMenuItem(this.player_, { 18026 track, 18027 // MenuItem is selectable 18028 selectable: true, 18029 // MenuItem is NOT multiSelectable (i.e. only one can be marked "selected" at a time) 18030 multiSelectable: false, 18031 }) 18032 ); 18033 } 18034 return items; 18035 } 18036} 18037 18038/** 18039 * The text that should display over the `AudioTrackButton`s controls. Added for localization. 18040 * 18041 * @type {string} 18042 * @protected 18043 */ 18044AudioTrackButton.prototype.controlText_ = 'Audio Track'; 18045Component$1.registerComponent('AudioTrackButton', AudioTrackButton); 18046 18047/** 18048 * @file playback-rate-menu-item.js 18049 */ 18050 18051/** @import Player from '../../player' */ 18052 18053/** 18054 * The specific menu item type for selecting a playback rate. 18055 * 18056 * @extends MenuItem 18057 */ 18058class PlaybackRateMenuItem extends MenuItem { 18059 /** 18060 * Creates an instance of this class. 18061 * 18062 * @param {Player} player 18063 * The `Player` that this class should be attached to. 18064 * 18065 * @param {Object} [options] 18066 * The key/value store of player options. 18067 */ 18068 constructor(player, options) { 18069 const label = options.rate; 18070 const rate = parseFloat(label, 10); 18071 18072 // Modify options for parent MenuItem class's init. 18073 options.label = label; 18074 options.selected = rate === player.playbackRate(); 18075 options.selectable = true; 18076 options.multiSelectable = false; 18077 super(player, options); 18078 this.label = label; 18079 this.rate = rate; 18080 this.on(player, 'ratechange', (e) => this.update(e)); 18081 } 18082 18083 /** 18084 * This gets called when an `PlaybackRateMenuItem` is "clicked". See 18085 * {@link ClickableComponent} for more detailed information on what a click can be. 18086 * 18087 * @param {Event} [event]
18088 * The `keydown`, `tap`, or `click` event that caused this function to be 18089 * called. 18090 * 18091 * @listens tap 18092 * @listens click 18093 */ 18094 handleClick(event) { 18095 super.handleClick(); 18096 this.player().playbackRate(this.rate); 18097 } 18098 18099 /** 18100 * Update the PlaybackRateMenuItem when the playbackrate changes. 18101 * 18102 * @param {Event} [event] 18103 * The `ratechange` event that caused this function to run. 18104 * 18105 * @listens Player#ratechange 18106 */ 18107 update(event) { 18108 this.selected(this.player().playbackRate() === this.rate); 18109 } 18110} 18111 18112/** 18113 * The text that should display over the `PlaybackRateMenuItem`s controls. Added for localization. 18114 * 18115 * @type {string} 18116 * @private 18117 */ 18118PlaybackRateMenuItem.prototype.contentElType = 'button'; 18119Component$1.registerComponent('PlaybackRateMenuItem', PlaybackRateMenuItem); 18120 18121/** 18122 * @file playback-rate-menu-button.js 18123 */ 18124 18125/** @import Player from '../../player' */ 18126 18127/** 18128 * The component for controlling the playback rate. 18129 * 18130 * @extends MenuButton 18131 */ 18132class PlaybackRateMenuButton extends MenuButton { 18133 /** 18134 * Creates an instance of this class. 18135 * 18136 * @param {Player} player 18137 * The `Player` that this class should be attached to. 18138 * 18139 * @param {Object} [options] 18140 * The key/value store of player options. 18141 */ 18142 constructor(player, options) { 18143 super(player, options); 18144 this.menuButton_.el_.setAttribute('aria-describedby', this.labelElId_); 18145 this.updateVisibility(); 18146 this.updateLabel(); 18147 this.on(player, 'loadstart', (e) => this.updateVisibility(e)); 18148 this.on(player, 'ratechange', (e) => this.updateLabel(e)); 18149 this.on(player, 'playbackrateschange', (e) => this.handlePlaybackRateschange(e)); 18150 } 18151 18152 /** 18153 * Create the `Component`'s DOM element 18154 * 18155 * @return {Element} 18156 * The element that was created. 18157 */ 18158 createEl() { 18159 const el = super.createEl(); 18160 this.labelElId_ = 'vjs-playback-rate-value-label-' + this.id_; 18161 this.labelEl_ = createEl('div', { 18162 className: 'vjs-playback-rate-value', 18163 id: this.labelElId_, 18164 textContent: '1x', 18165 }); 18166 el.appendChild(this.labelEl_); 18167 return el; 18168 } 18169 dispose() { 18170 this.labelEl_ = null; 18171 super.dispose(); 18172 } 18173 18174 /** 18175 * Builds the default DOM `className`. 18176 * 18177 * @return {string} 18178 * The DOM `className` for this object. 18179 */ 18180 buildCSSClass() { 18181 return `vjs-playback-rate ${super.buildCSSClass()}`; 18182 } 18183 buildWrapperCSSClass() { 18184 return `vjs-playback-rate ${super.buildWrapperCSSClass()}`; 18185 } 18186 18187 /** 18188 * Create the list of menu items. Specific to each subclass. 18189 * 18190 */ 18191 createItems() { 18192 const rates = this.playbackRates(); 18193 const items = []; 18194 for (let i = rates.length - 1; i >= 0; i--) { 18195 items.push( 18196 new PlaybackRateMenuItem(this.player(), { 18197 rate: rates[i] + 'x', 18198 }) 18199 ); 18200 } 18201 return items; 18202 } 18203 18204 /** 18205 * On playbackrateschange, update the menu to account for the new items. 18206 * 18207 * @listens Player#playbackrateschange 18208 */ 18209 handlePlaybackRateschange(event) { 18210 this.update(); 18211 } 18212 18213 /** 18214 * Get possible playback rates 18215 * 18216 * @return {Array} 18217 * All possible playback rates 18218 */ 18219 playbackRates() { 18220 const player = this.player(); 18221 return (player.playbackRates && player.playbackRates()) || []; 18222 } 18223 18224 /** 18225 * Get whether playback rates is supported by the tech 18226 * and an array of playback rates exists 18227 * 18228 * @return {boolean} 18229 * Whether changing playback rate is supported 18230 */ 18231 playbackRateSupported() { 18232 return ( 18233 this.player().tech_ && 18234 this.player().tech_.featuresPlaybackRate && 18235 this.playbackRates() && 18236 this.playbackRates().length > 0 18237 ); 18238 } 18239 18240 /** 18241 * Hide playback rate controls when they're no playback rate options to select 18242 * 18243 * @param {Event} [event] 18244 * The event that caused this function to run. 18245 * 18246 * @listens Player#loadstart 18247 */ 18248 updateVisibility(event) { 18249 if (this.playbackRateSupported()) { 18250 this.removeClass('vjs-hidden'); 18251 } else { 18252 this.addClass('vjs-hidden'); 18253 } 18254 } 18255 18256 /** 18257 * Update button label when rate changed 18258 * 18259 * @param {Event} [event] 18260 * The event that caused this function to run. 18261 * 18262 * @listens Player#ratechange 18263 */ 18264 updateLabel(event) { 18265 if (this.playbackRateSupported()) { 18266 this.labelEl_.textContent = this.player().playbackRate() + 'x'; 18267 } 18268 } 18269} 18270 18271/** 18272 * The text that should display over the `PlaybackRateMenuButton`s controls. 18273 * 18274 * Added for localization. 18275 * 18276 * @type {string} 18277 * @protected 18278 */ 18279PlaybackRateMenuButton.prototype.controlText_ = 'Playback Rate'; 18280Component$1.registerComponent('PlaybackRateMenuButton', PlaybackRateMenuButton); 18281 18282/** 18283 * @file spacer.js 18284 */ 18285 18286/** 18287 * Just an empty spacer element that can be used as an append point for plugins, etc. 18288 * Also can be used to create space between elements when necessary. 18289 * 18290 * @extends Component 18291 */ 18292class Spacer extends Component$1 { 18293 /** 18294 * Builds the default DOM `className`. 18295 * 18296 * @return {string} 18297 * The DOM `className` for this object. 18298 */ 18299 buildCSSClass() { 18300 return `vjs-spacer ${super.buildCSSClass()}`; 18301 } 18302 18303 /** 18304 * Create the `Component`'s DOM element 18305 * 18306 * @return {Element} 18307 * The element that was created. 18308 */ 18309 createEl(tag = 'div', props = {}, attributes = {}) { 18310 if (!props.className) { 18311 props.className = this.buildCSSClass(); 18312 } 18313 return super.createEl(tag, props, attributes); 18314 } 18315} 18316Component$1.registerComponent('Spacer', Spacer); 18317 18318/** 18319 * @file custom-control-spacer.js 18320 */ 18321 18322/** 18323 * Spacer specifically meant to be used as an insertion point for new plugins, etc. 18324 * 18325 * @extends Spacer 18326 */ 18327class CustomControlSpacer extends Spacer { 18328 /** 18329 * Builds the default DOM `className`. 18330 * 18331 * @return {string} 18332 * The DOM `className` for this object. 18333 */ 18334 buildCSSClass() { 18335 return `vjs-custom-control-spacer ${super.buildCSSClass()}`; 18336 } 18337 18338 /** 18339 * Create the `Component`'s DOM element 18340 * 18341 * @return {Element} 18342 * The element that was created. 18343 */ 18344 createEl() { 18345 return super.createEl('div', { 18346 className: this.buildCSSClass(), 18347 // No-flex/table-cell mode requires there be some content 18348 // in the cell to fill the remaining space of the table. 18349 textContent: '\u00a0', 18350 }); 18351 } 18352} 18353Component$1.registerComponent('CustomControlSpacer', CustomControlSpacer); 18354 18355/** 18356 * @file control-bar.js 18357 */ 18358 18359/** 18360 * Container of main controls. 18361 * 18362 * @extends Component 18363 */ 18364class ControlBar extends Component$1 { 18365 /** 18366 * Create the `Component`'s DOM element 18367 * 18368 * @return {Element} 18369 * The element that was created. 18370 */ 18371 createEl() { 18372 return super.createEl('div', { 18373 className: 'vjs-control-bar', 18374 dir: 'ltr', 18375 }); 18376 } 18377} 18378 18379/** 18380 * Default options for `ControlBar` 18381 * 18382 * @type {Object} 18383 * @private 18384 */ 18385ControlBar.prototype.options_ = { 18386 children: [ 18387 'playToggle',
18388 'skipBackward', 18389 'skipForward', 18390 'volumePanel', 18391 'currentTimeDisplay', 18392 'timeDivider', 18393 'durationDisplay', 18394 'progressControl', 18395 'liveDisplay', 18396 'seekToLive', 18397 'remainingTimeDisplay', 18398 'customControlSpacer', 18399 'playbackRateMenuButton', 18400 'chaptersButton', 18401 'descriptionsButton', 18402 'subsCapsButton', 18403 'audioTrackButton', 18404 'pictureInPictureToggle', 18405 'fullscreenToggle', 18406 ], 18407}; 18408Component$1.registerComponent('ControlBar', ControlBar); 18409 18410/** 18411 * @file error-display.js 18412 */ 18413 18414/** @import Player from './player' */ 18415 18416/** 18417 * A display that indicates an error has occurred. This means that the video 18418 * is unplayable. 18419 * 18420 * @extends ModalDialog 18421 */ 18422class ErrorDisplay extends ModalDialog { 18423 /** 18424 * Creates an instance of this class. 18425 * 18426 * @param {Player} player 18427 * The `Player` that this class should be attached to. 18428 * 18429 * @param {Object} [options] 18430 * The key/value store of player options. 18431 */ 18432 constructor(player, options) { 18433 super(player, options); 18434 this.on(player, 'error', (e) => { 18435 this.open(e); 18436 }); 18437 } 18438 18439 /** 18440 * Builds the default DOM `className`. 18441 * 18442 * @return {string} 18443 * The DOM `className` for this object. 18444 * 18445 * @deprecated Since version 5. 18446 */ 18447 buildCSSClass() { 18448 return `vjs-error-display ${super.buildCSSClass()}`; 18449 } 18450 18451 /** 18452 * Gets the localized error message based on the `Player`s error. 18453 * 18454 * @return {string} 18455 * The `Player`s error message localized or an empty string. 18456 */ 18457 content() { 18458 const error = this.player().error(); 18459 return error ? this.localize(error.message) : ''; 18460 } 18461} 18462 18463/** 18464 * The default options for an `ErrorDisplay`. 18465 * 18466 * @private 18467 */ 18468ErrorDisplay.prototype.options_ = Object.assign({}, ModalDialog.prototype.options_, { 18469 pauseOnOpen: false, 18470 fillAlways: true, 18471 temporary: false, 18472 uncloseable: true, 18473}); 18474Component$1.registerComponent('ErrorDisplay', ErrorDisplay); 18475 18476/** @import Player from './player' */ 18477/** @import { ContentDescriptor } from '../utils/dom' */ 18478 18479/** 18480 * Creates DOM element of 'select' & its options. 18481 * 18482 * @extends Component 18483 */ 18484class TextTrackSelect extends Component$1 { 18485 /** 18486 * Creates an instance of this class. 18487 * 18488 * @param {Player} player 18489 * The `Player` that this class should be attached to. 18490 * 18491 * @param {Object} [options] 18492 * The key/value store of player options. 18493 * 18494 * @param {ContentDescriptor} [options.content=undefined] 18495 * Provide customized content for this modal. 18496 * 18497 * @param {string} [options.legendId] 18498 * A text with part of an string to create atribute of aria-labelledby. 18499 * 18500 * @param {string} [options.id] 18501 * A text with part of an string to create atribute of aria-labelledby. 18502 * 18503 * @param {Array} [options.SelectOptions] 18504 * Array that contains the value & textContent of for each of the 18505 * options elements. 18506 */ 18507 constructor(player, options = {}) { 18508 super(player, options); 18509 this.el_.setAttribute('aria-labelledby', this.selectLabelledbyIds); 18510 } 18511 18512 /** 18513 * Create the `TextTrackSelect`'s DOM element 18514 * 18515 * @return {Element} 18516 * The DOM element that gets created. 18517 */ 18518 createEl() { 18519 this.selectLabelledbyIds = [this.options_.legendId, this.options_.labelId].join(' ').trim(); 18520 18521 // Create select & inner options 18522 const selectoptions = createEl( 18523 'select', 18524 { 18525 id: this.options_.id, 18526 }, 18527 {}, 18528 this.options_.SelectOptions.map((optionText) => { 18529 // Constructs an id for the <option>. 18530 // For the colour settings that have two <selects> with a <label> each, generates an id based off the label value 18531 // For font size/family and edge style with one <select> and no <label>, generates an id with a guid 18532 const optionId = 18533 (this.options_.labelId ? this.options_.labelId : `vjs-track-option-${newGUID()}`) + 18534 '-' + 18535 optionText[1].replace(/\W+/g, ''); 18536 const option = createEl('option', { 18537 id: optionId, 18538 value: this.localize(optionText[0]), 18539 textContent: this.localize(optionText[1]), 18540 }); 18541 option.setAttribute('aria-labelledby', `${this.selectLabelledbyIds} ${optionId}`); 18542 return option; 18543 }) 18544 ); 18545 return selectoptions; 18546 } 18547} 18548Component$1.registerComponent('TextTrackSelect', TextTrackSelect); 18549 18550/** @import Player from './player' */ 18551/** @import { ContentDescriptor } from '../utils/dom' */ 18552 18553/** 18554 * Creates fieldset section of 'TextTrackSettings'. 18555 * Manganes two versions of fieldsets, one for type of 'colors' 18556 * & the other for 'font', Component adds diferent DOM elements 18557 * to that fieldset depending on the type. 18558 * 18559 * @extends Component 18560 */ 18561class TextTrackFieldset extends Component$1 { 18562 /** 18563 * Creates an instance of this class. 18564 * 18565 * @param {Player} player 18566 * The `Player` that this class should be attached to. 18567 * 18568 * @param {Object} [options] 18569 * The key/value store of player options. 18570 * 18571 * @param {ContentDescriptor} [options.content=undefined] 18572 * Provide customized content for this modal. 18573 * 18574 * @param {string} [options.legendId] 18575 * A text with part of an string to create atribute of aria-labelledby. 18576 * It passes to 'TextTrackSelect'. 18577 * 18578 * @param {string} [options.id] 18579 * A text with part of an string to create atribute of aria-labelledby. 18580 * It passes to 'TextTrackSelect'. 18581 * 18582 * @param {string} [options.legendText] 18583 * A text to use as the text content of the legend element. 18584 * 18585 * @param {Array} [options.selects] 18586 * Array that contains the selects that are use to create 'selects' 18587 * components. 18588 * 18589 * @param {Array} [options.SelectOptions] 18590 * Array that contains the value & textContent of for each of the 18591 * options elements, it passes to 'TextTrackSelect'. 18592 * 18593 * @param {string} [options.type] 18594 * Conditions if some DOM elements will be added to the fieldset 18595 * component. 18596 * 18597 * @param {Object} [options.selectConfigs] 18598 * Object with the following properties that are the selects configurations: 18599 * backgroundColor, backgroundOpacity, color, edgeStyle, fontFamily, 18600 * fontPercent, textOpacity, windowColor, windowOpacity. 18601 * These properties are use to configure the 'TextTrackSelect' Component. 18602 */ 18603 constructor(player, options = {}) { 18604 super(player, options); 18605 18606 // Add Components & DOM Elements 18607 const legendElement = createEl('legend', { 18608 textContent: this.localize(this.options_.legendText), 18609 id: this.options_.legendId, 18610 }); 18611 this.el().appendChild(legendElement); 18612 const selects = this.options_.selects; 18613 18614 // Iterate array of selects to create 'selects' components 18615 for (const i of selects) { 18616 const selectConfig = this.options_.selectConfigs[i]; 18617 const selectClassName = selectConfig.className; 18618 const id = selectConfig.id.replace('%s', this.options_.id_); 18619 let span = null; 18620 const guid = `vjs_select_${newGUID()}`; 18621 18622 // Conditionally create span to add on the component 18623 if (this.options_.type === 'colors') { 18624 span = createEl('span', { 18625 className: selectClassName, 18626 }); 18627 const label = createEl('label', { 18628 id, 18629 className: 'vjs-label', 18630 textContent: this.localize(selectConfig.label), 18631 }); 18632 label.setAttribute('for', guid); 18633 span.appendChild(label); 18634 } 18635 const textTrackSelect = new TextTrackSelect(player, { 18636 SelectOptions: selectConfig.options, 18637 legendId: this.options_.legendId, 18638 id: guid, 18639 labelId: id, 18640 }); 18641 this.addChild(textTrackSelect); 18642 18643 // Conditionally append to 'select' component to conditionally created span 18644 if (this.options_.type === 'colors') { 18645 span.appendChild(textTrackSelect.el()); 18646 this.el().appendChild(span); 18647 } 18648 } 18649 } 18650 18651 /** 18652 * Create the `TextTrackFieldset`'s DOM element 18653 * 18654 * @return {Element} 18655 * The DOM element that gets created. 18656 */ 18657 createEl() { 18658 const el = createEl('fieldset', { 18659 // Prefixing classes of elements within a player with "vjs-" 18660 // is a convention used in Video.js. 18661 className: this.options_.className, 18662 }); 18663 return el; 18664 } 18665} 18666Component$1.registerComponent('TextTrackFieldset', TextTrackFieldset); 18667 18668/** @import Player from './player' */ 18669/** @import { ContentDescriptor } from '../utils/dom' */ 18670 18671/**
18672 * The component 'TextTrackSettingsColors' displays a set of 'fieldsets' 18673 * using the component 'TextTrackFieldset'. 18674 * 18675 * @extends Component 18676 */ 18677class TextTrackSettingsColors extends Component$1 { 18678 /** 18679 * Creates an instance of this class. 18680 * 18681 * @param {Player} player 18682 * The `Player` that this class should be attached to. 18683 * 18684 * @param {Object} [options] 18685 * The key/value store of player options. 18686 * 18687 * @param {ContentDescriptor} [options.content=undefined] 18688 * Provide customized content for this modal. 18689 * 18690 * @param {Array} [options.fieldSets] 18691 * Array that contains the configurations for the selects. 18692 * 18693 * @param {Object} [options.selectConfigs] 18694 * Object with the following properties that are the select confugations: 18695 * backgroundColor, backgroundOpacity, color, edgeStyle, fontFamily, 18696 * fontPercent, textOpacity, windowColor, windowOpacity. 18697 * it passes to 'TextTrackFieldset'. 18698 */ 18699 constructor(player, options = {}) { 18700 super(player, options); 18701 const id_ = this.options_.textTrackComponentid; 18702 18703 // createElFgColor_ 18704 const ElFgColorFieldset = new TextTrackFieldset(player, { 18705 id_, 18706 legendId: `captions-text-legend-${id_}`, 18707 legendText: this.localize('Text'), 18708 className: 'vjs-fg vjs-track-setting', 18709 selects: this.options_.fieldSets[0], 18710 selectConfigs: this.options_.selectConfigs, 18711 type: 'colors', 18712 }); 18713 this.addChild(ElFgColorFieldset); 18714 18715 // createElBgColor_ 18716 const ElBgColorFieldset = new TextTrackFieldset(player, { 18717 id_, 18718 legendId: `captions-background-${id_}`, 18719 legendText: this.localize('Text Background'), 18720 className: 'vjs-bg vjs-track-setting', 18721 selects: this.options_.fieldSets[1], 18722 selectConfigs: this.options_.selectConfigs, 18723 type: 'colors', 18724 }); 18725 this.addChild(ElBgColorFieldset); 18726 18727 // createElWinColor_ 18728 const ElWinColorFieldset = new TextTrackFieldset(player, { 18729 id_, 18730 legendId: `captions-window-${id_}`, 18731 legendText: this.localize('Caption Area Background'), 18732 className: 'vjs-window vjs-track-setting', 18733 selects: this.options_.fieldSets[2], 18734 selectConfigs: this.options_.selectConfigs, 18735 type: 'colors', 18736 }); 18737 this.addChild(ElWinColorFieldset); 18738 } 18739 18740 /** 18741 * Create the `TextTrackSettingsColors`'s DOM element 18742 * 18743 * @return {Element} 18744 * The DOM element that gets created. 18745 */ 18746 createEl() { 18747 const el = createEl('div', { 18748 className: 'vjs-track-settings-colors', 18749 }); 18750 return el; 18751 } 18752} 18753Component$1.registerComponent('TextTrackSettingsColors', TextTrackSettingsColors); 18754 18755/** @import Player from './player' */ 18756/** @import { ContentDescriptor } from '../utils/dom' */ 18757 18758/** 18759 * The component 'TextTrackSettingsFont' displays a set of 'fieldsets' 18760 * using the component 'TextTrackFieldset'. 18761 * 18762 * @extends Component 18763 */ 18764class TextTrackSettingsFont extends Component$1 { 18765 /** 18766 * Creates an instance of this class. 18767 * 18768 * @param {Player} player 18769 * The `Player` that this class should be attached to. 18770 * 18771 * @param {Object} [options] 18772 * The key/value store of player options. 18773 * 18774 * @param {ContentDescriptor} [options.content=undefined] 18775 * Provide customized content for this modal. 18776 * 18777 * @param {Array} [options.fieldSets] 18778 * Array that contains the configurations for the selects. 18779 * 18780 * @param {Object} [options.selectConfigs] 18781 * Object with the following properties that are the select confugations: 18782 * backgroundColor, backgroundOpacity, color, edgeStyle, fontFamily, 18783 * fontPercent, textOpacity, windowColor, windowOpacity. 18784 * it passes to 'TextTrackFieldset'. 18785 */ 18786 constructor(player, options = {}) { 18787 super(player, options); 18788 const id_ = this.options_.textTrackComponentid; 18789 const ElFgColorFieldset = new TextTrackFieldset(player, { 18790 id_, 18791 legendId: `captions-font-size-${id_}`, 18792 legendText: 'Font Size', 18793 className: 'vjs-font-percent vjs-track-setting',
18794 selects: this.options_.fieldSets[0], 18795 selectConfigs: this.options_.selectConfigs, 18796 type: 'font', 18797 }); 18798 this.addChild(ElFgColorFieldset); 18799 const ElBgColorFieldset = new TextTrackFieldset(player, { 18800 id_, 18801 legendId: `captions-edge-style-${id_}`, 18802 legendText: this.localize('Text Edge Style'), 18803 className: 'vjs-edge-style vjs-track-setting', 18804 selects: this.options_.fieldSets[1], 18805 selectConfigs: this.options_.selectConfigs, 18806 type: 'font', 18807 }); 18808 this.addChild(ElBgColorFieldset); 18809 const ElWinColorFieldset = new TextTrackFieldset(player, { 18810 id_, 18811 legendId: `captions-font-family-${id_}`, 18812 legendText: this.localize('Font Family'), 18813 className: 'vjs-font-family vjs-track-setting', 18814 selects: this.options_.fieldSets[2], 18815 selectConfigs: this.options_.selectConfigs, 18816 type: 'font', 18817 }); 18818 this.addChild(ElWinColorFieldset); 18819 } 18820 18821 /** 18822 * Create the `TextTrackSettingsFont`'s DOM element 18823 * 18824 * @return {Element} 18825 * The DOM element that gets created. 18826 */ 18827 createEl() { 18828 const el = createEl('div', { 18829 className: 'vjs-track-settings-font', 18830 }); 18831 return el; 18832 } 18833} 18834Component$1.registerComponent('TextTrackSettingsFont', TextTrackSettingsFont); 18835 18836/** 18837 * Buttons of reset & done that modal 'TextTrackSettings' 18838 * uses as part of its content. 18839 * 18840 * 'Reset': Resets all settings on 'TextTrackSettings'. 18841 * 'Done': Closes 'TextTrackSettings' modal. 18842 * 18843 * @extends Component 18844 */ 18845class TrackSettingsControls extends Component$1 { 18846 constructor(player, options = {}) { 18847 super(player, options); 18848 18849 // Create DOM elements 18850 const defaultsDescription = this.localize('restore all settings to the default values'); 18851 const resetButton = new Button(player, { 18852 controlText: defaultsDescription, 18853 className: 'vjs-default-button', 18854 }); 18855 resetButton.el().classList.remove('vjs-control', 'vjs-button'); 18856 resetButton.el().textContent = this.localize('Reset'); 18857 this.addChild(resetButton); 18858 const doneButton = new Button(player, { 18859 controlText: defaultsDescription, 18860 className: 'vjs-done-button', 18861 }); 18862 18863 // Remove unrequired style classes 18864 doneButton.el().classList.remove('vjs-control', 'vjs-button'); 18865 doneButton.el().textContent = this.localize('Done'); 18866 this.addChild(doneButton); 18867 } 18868 18869 /** 18870 * Create the `TrackSettingsControls`'s DOM element 18871 * 18872 * @return {Element} 18873 * The DOM element that gets created. 18874 */ 18875 createEl() { 18876 const el = createEl('div', { 18877 className: 'vjs-track-settings-controls', 18878 }); 18879 return el; 18880 } 18881} 18882Component$1.registerComponent('TrackSettingsControls', TrackSettingsControls); 18883 18884/** 18885 * @file text-track-settings.js 18886 */ 18887 18888/** @import Player from '../player' */ 18889 18890const LOCAL_STORAGE_KEY$1 = 'vjs-text-track-settings'; 18891const COLOR_BLACK = ['#000', 'Black']; 18892const COLOR_BLUE = ['#00F', 'Blue']; 18893const COLOR_CYAN = ['#0FF', 'Cyan']; 18894const COLOR_GREEN = ['#0F0', 'Green']; 18895const COLOR_MAGENTA = ['#F0F', 'Magenta']; 18896const COLOR_RED = ['#F00', 'Red']; 18897const COLOR_WHITE = ['#FFF', 'White']; 18898const COLOR_YELLOW = ['#FF0', 'Yellow']; 18899const OPACITY_OPAQUE = ['1', 'Opaque']; 18900const OPACITY_SEMI = ['0.5', 'Semi-Transparent']; 18901const OPACITY_TRANS = ['0', 'Transparent']; 18902 18903// Configuration for the various <select> elements in the DOM of this component. 18904// 18905// Possible keys include: 18906// 18907// `default`: 18908// The default option index. Only needs to be provided if not zero. 18909// `parser`: 18910// A function which is used to parse the value from the selected option in 18911// a customized way. 18912// `selector`: 18913// The selector used to find the associated <select> element. 18914const selectConfigs = { 18915 backgroundColor: { 18916 selector: '.vjs-bg-color > select', 18917 id: 'captions-background-color-%s', 18918 label: 'Color', 18919 options: [ 18920 COLOR_BLACK, 18921 COLOR_WHITE, 18922 COLOR_RED, 18923 COLOR_GREEN, 18924 COLOR_BLUE, 18925 COLOR_YELLOW, 18926 COLOR_MAGENTA, 18927 COLOR_CYAN, 18928 ], 18929 className: 'vjs-bg-color', 18930 }, 18931 backgroundOpacity: { 18932 selector: '.vjs-bg-opacity > select', 18933 id: 'captions-background-opacity-%s', 18934 label: 'Opacity', 18935 options: [OPACITY_OPAQUE, OPACITY_SEMI, OPACITY_TRANS], 18936 className: 'vjs-bg-opacity vjs-opacity', 18937 }, 18938 color: { 18939 selector: '.vjs-text-color > select', 18940 id: 'captions-foreground-color-%s', 18941 label: 'Color', 18942 options: [ 18943 COLOR_WHITE, 18944 COLOR_BLACK, 18945 COLOR_RED, 18946 COLOR_GREEN, 18947 COLOR_BLUE, 18948 COLOR_YELLOW, 18949 COLOR_MAGENTA, 18950 COLOR_CYAN, 18951 ], 18952 className: 'vjs-text-color', 18953 }, 18954 edgeStyle: { 18955 selector: '.vjs-edge-style > select', 18956 id: '', 18957 label: 'Text Edge Style', 18958 options: [ 18959 ['none', 'None'], 18960 ['raised', 'Raised'], 18961 ['depressed', 'Depressed'], 18962 ['uniform', 'Uniform'], 18963 ['dropshadow', 'Drop shadow'], 18964 ], 18965 }, 18966 fontFamily: { 18967 selector: '.vjs-font-family > select', 18968 id: '', 18969 label: 'Font Family', 18970 options: [ 18971 ['proportionalSansSerif', 'Proportional Sans-Serif'], 18972 ['monospaceSansSerif', 'Monospace Sans-Serif'], 18973 ['proportionalSerif', 'Proportional Serif'], 18974 ['monospaceSerif', 'Monospace Serif'], 18975 ['casual', 'Casual'], 18976 ['script', 'Script'],
18977 ['small-caps', 'Small Caps'], 18978 ], 18979 }, 18980 fontPercent: { 18981 selector: '.vjs-font-percent > select', 18982 id: '', 18983 label: 'Font Size', 18984 options: [ 18985 ['0.50', '50%'], 18986 ['0.75', '75%'], 18987 ['1.00', '100%'], 18988 ['1.25', '125%'], 18989 ['1.50', '150%'], 18990 ['1.75', '175%'], 18991 ['2.00', '200%'], 18992 ['3.00', '300%'], 18993 ['4.00', '400%'], 18994 ], 18995 default: 2, 18996 parser: (v) => (v === '1.00' ? null : Number(v)), 18997 }, 18998 textOpacity: { 18999 selector: '.vjs-text-opacity > select', 19000 id: 'captions-foreground-opacity-%s', 19001 label: 'Opacity', 19002 options: [OPACITY_OPAQUE, OPACITY_SEMI], 19003 className: 'vjs-text-opacity vjs-opacity', 19004 }, 19005 // Options for this object are defined below. 19006 windowColor: { 19007 selector: '.vjs-window-color > select', 19008 id: 'captions-window-color-%s', 19009 label: 'Color', 19010 className: 'vjs-window-color', 19011 }, 19012 // Options for this object are defined below. 19013 windowOpacity: { 19014 selector: '.vjs-window-opacity > select', 19015 id: 'captions-window-opacity-%s', 19016 label: 'Opacity', 19017 options: [OPACITY_TRANS, OPACITY_SEMI, OPACITY_OPAQUE], 19018 className: 'vjs-window-opacity vjs-opacity', 19019 }, 19020}; 19021selectConfigs.windowColor.options = selectConfigs.backgroundColor.options; 19022 19023/** 19024 * Get the actual value of an option. 19025 * 19026 * @param {string} value 19027 * The value to get 19028 * 19029 * @param {Function} [parser] 19030 * Optional function to adjust the value. 19031 * 19032 * @return {*} 19033 * - Will be `undefined` if no value exists 19034 * - Will be `undefined` if the given value is "none". 19035 * - Will be the actual value otherwise. 19036 * 19037 * @private 19038 */ 19039function parseOptionValue(value, parser) { 19040 if (parser) { 19041 value = parser(value); 19042 } 19043 if (value && value !== 'none') { 19044 return value; 19045 } 19046} 19047 19048/** 19049 * Gets the value of the selected <option> element within a <select> element. 19050 * 19051 * @param {Element} el 19052 * the element to look in 19053 * 19054 * @param {Function} [parser] 19055 * Optional function to adjust the value. 19056 * 19057 * @return {*} 19058 * - Will be `undefined` if no value exists 19059 * - Will be `undefined` if the given value is "none". 19060 * - Will be the actual value otherwise. 19061 * 19062 * @private 19063 */ 19064function getSelectedOptionValue(el, parser) { 19065 const value = el.options[el.options.selectedIndex].value; 19066 return parseOptionValue(value, parser); 19067} 19068 19069/** 19070 * Sets the selected <option> element within a <select> element based on a 19071 * given value. 19072 * 19073 * @param {Element} el 19074 * The element to look in. 19075 * 19076 * @param {string} value 19077 * the property to look on. 19078 * 19079 * @param {Function} [parser] 19080 * Optional function to adjust the value before comparing. 19081 * 19082 * @private 19083 */ 19084function setSelectedOption(el, value, parser) { 19085 if (!value) { 19086 return; 19087 } 19088 for (let i = 0; i < el.options.length; i++) { 19089 if (parseOptionValue(el.options[i].value, parser) === value) { 19090 el.selectedIndex = i; 19091 break; 19092 } 19093 } 19094} 19095 19096/** 19097 * Manipulate Text Tracks settings. 19098 * 19099 * @extends ModalDialog 19100 */ 19101class TextTrackSettings extends ModalDialog { 19102 /** 19103 * Creates an instance of this class. 19104 * 19105 * @param {Player} player 19106 * The `Player` that this class should be attached to. 19107 * 19108 * @param {Object} [options] 19109 * The key/value store of player options. 19110 */ 19111 constructor(player, options) { 19112 options.temporary = false; 19113 super(player, options); 19114 this.updateDisplay = this.updateDisplay.bind(this); 19115 19116 // fill the modal and pretend we have opened it 19117 this.fill(); 19118 this.hasBeenOpened_ = this.hasBeenFilled_ = true; 19119 this.renderModalComponents(player); 19120 this.endDialog = createEl('p', { 19121 className: 'vjs-control-text', 19122 textContent: this.localize('End of dialog window.'), 19123 }); 19124 this.el().appendChild(this.endDialog); 19125 this.setDefaults(); 19126 19127 // Grab `persistTextTrackSettings` from the player options if not passed in child options 19128 if (options.persistTextTrackSettings === undefined) { 19129 this.options_.persistTextTrackSettings = this.options_.playerOptions.persistTextTrackSettings; 19130 } 19131 this.bindFunctionsToSelectsAndButtons(); 19132 if (this.options_.persistTextTrackSettings) { 19133 this.restoreSettings(); 19134 } 19135 } 19136 renderModalComponents(player) { 19137 const textTrackSettingsColors = new TextTrackSettingsColors(player, { 19138 textTrackComponentid: this.id_, 19139 selectConfigs, 19140 fieldSets: [ 19141 ['color', 'textOpacity'], 19142 ['backgroundColor', 'backgroundOpacity'], 19143 ['windowColor', 'windowOpacity'], 19144 ], 19145 }); 19146 this.addChild(textTrackSettingsColors); 19147 const textTrackSettingsFont = new TextTrackSettingsFont(player, { 19148 textTrackComponentid: this.id_, 19149 selectConfigs, 19150 fieldSets: [['fontPercent'], ['edgeStyle'], ['fontFamily']], 19151 }); 19152 this.addChild(textTrackSettingsFont);
vendor: 4,608 bytes, lines 19153-19318
19153 const trackSettingsControls = new TrackSettingsControls(player); 19154 this.addChild(trackSettingsControls); 19155 } 19156 bindFunctionsToSelectsAndButtons() { 19157 this.on(this.$('.vjs-done-button'), ['click', 'tap'], () => { 19158 this.saveSettings(); 19159 this.close(); 19160 }); 19161 this.on(this.$('.vjs-default-button'), ['click', 'tap'], () => { 19162 this.setDefaults(); 19163 this.updateDisplay(); 19164 }); 19165 each(selectConfigs, (config) => { 19166 this.on(this.$(config.selector), 'change', this.updateDisplay); 19167 }); 19168 } 19169 dispose() { 19170 this.endDialog = null; 19171 super.dispose(); 19172 } 19173 label() { 19174 return this.localize('Caption Settings Dialog'); 19175 } 19176 description() { 19177 return this.localize('Beginning of dialog window. Escape will cancel and close the window.'); 19178 } 19179 buildCSSClass() { 19180 return super.buildCSSClass() + ' vjs-text-track-settings'; 19181 } 19182 19183 /** 19184 * Gets an object of text track settings (or null). 19185 * 19186 * @return {Object} 19187 * An object with config values parsed from the DOM or localStorage. 19188 */ 19189 getValues() { 19190 return reduce( 19191 selectConfigs, 19192 (accum, config, key) => { 19193 const value = getSelectedOptionValue(this.$(config.selector), config.parser); 19194 if (value !== undefined) { 19195 accum[key] = value; 19196 } 19197 return accum; 19198 }, 19199 {} 19200 ); 19201 } 19202 19203 /** 19204 * Sets text track settings from an object of values. 19205 * 19206 * @param {Object} values 19207 * An object with config values parsed from the DOM or localStorage. 19208 */ 19209 setValues(values) { 19210 each(selectConfigs, (config, key) => { 19211 setSelectedOption(this.$(config.selector), values[key], config.parser); 19212 }); 19213 } 19214 19215 /** 19216 * Sets all `<select>` elements to their default values. 19217 */ 19218 setDefaults() { 19219 each(selectConfigs, (config) => { 19220 const index = config.hasOwnProperty('default') ? config.default : 0; 19221 this.$(config.selector).selectedIndex = index; 19222 }); 19223 } 19224 19225 /** 19226 * Restore texttrack settings from localStorage 19227 */ 19228 restoreSettings() { 19229 let values; 19230 try { 19231 values = JSON.parse(window__default['default'].localStorage.getItem(LOCAL_STORAGE_KEY$1)); 19232 } catch (err) { 19233 log$1.warn(err); 19234 } 19235 if (values) { 19236 this.setValues(values); 19237 } 19238 } 19239 19240 /** 19241 * Save text track settings to localStorage 19242 */ 19243 saveSettings() { 19244 if (!this.options_.persistTextTrackSettings) { 19245 return; 19246 } 19247 const values = this.getValues(); 19248 try { 19249 if (Object.keys(values).length) { 19250 window__default['default'].localStorage.setItem( 19251 LOCAL_STORAGE_KEY$1, 19252 JSON.stringify(values) 19253 ); 19254 } else { 19255 window__default['default'].localStorage.removeItem(LOCAL_STORAGE_KEY$1); 19256 } 19257 } catch (err) { 19258 log$1.warn(err); 19259 } 19260 } 19261 19262 /** 19263 * Update display of text track settings 19264 */ 19265 updateDisplay() { 19266 const ttDisplay = this.player_.getChild('textTrackDisplay'); 19267 if (ttDisplay) { 19268 ttDisplay.updateDisplay(); 19269 } 19270 } 19271 19272 /** 19273 * Repopulate dialog with new localizations on languagechange 19274 */ 19275 handleLanguagechange() { 19276 this.fill(); 19277 this.renderModalComponents(this.player_); 19278 this.bindFunctionsToSelectsAndButtons(); 19279 } 19280} 19281Component$1.registerComponent('TextTrackSettings', TextTrackSettings); 19282 19283/** 19284 * @file resize-manager.js 19285 */ 19286 19287/** 19288 * A Resize Manager. It is in charge of triggering `playerresize` on the player in the right conditions. 19289 * 19290 * 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}. 19291 * 19292 * If the ResizeObserver is available natively, it will be used. A polyfill can be passed in as an option. 19293 * If a `playerresize` event is not needed, the ResizeManager component can be removed from the player, see the example below. 19294 * 19295 * @example <caption>How to disable the resize manager</caption> 19296 * const player = videojs('#vid', { 19297 * resizeManager: false 19298 * }); 19299 * 19300 * @see {@link https://wicg.github.io/ResizeObserver/|ResizeObserver specification} 19301 * 19302 * @extends Component 19303 */ 19304class ResizeManager extends Component$1 { 19305 /** 19306 * Create the ResizeManager. 19307 * 19308 * @param {Object} player 19309 * The `Player` that this class should be attached to. 19310 * 19311 * @param {Object} [options] 19312 * The key/value store of ResizeManager options. 19313 * 19314 * @param {Object} [options.ResizeObserver] 19315 * A polyfill for ResizeObserver can be passed in here. 19316 * If this is set to null it will ignore the native ResizeObserver and fall back to the iframe fallback. 19317 */ 19318 constructor(player, options) {
19319 let RESIZE_OBSERVER_AVAILABLE = 19320 options.ResizeObserver || window__default['default'].ResizeObserver; 19321 19322 // if `null` was passed, we want to disable the ResizeObserver 19323 if (options.ResizeObserver === null) { 19324 RESIZE_OBSERVER_AVAILABLE = false; 19325 } 19326 19327 // Only create an element when ResizeObserver isn't available 19328 const options_ = merge$1( 19329 { 19330 createEl: !RESIZE_OBSERVER_AVAILABLE, 19331 reportTouchActivity: false, 19332 }, 19333 options 19334 ); 19335 super(player, options_); 19336 this.ResizeObserver = options.ResizeObserver || window__default['default'].ResizeObserver; 19337 this.loadListener_ = null; 19338 this.resizeObserver_ = null; 19339 this.debouncedHandler_ = debounce( 19340 () => { 19341 this.resizeHandler(); 19342 }, 19343 100, 19344 false, 19345 this 19346 ); 19347 if (RESIZE_OBSERVER_AVAILABLE) { 19348 this.resizeObserver_ = new this.ResizeObserver(this.debouncedHandler_); 19349 this.resizeObserver_.observe(player.el()); 19350 } else { 19351 this.loadListener_ = () => { 19352 if (!this.el_ || !this.el_.contentWindow) { 19353 return; 19354 } 19355 const debouncedHandler_ = this.debouncedHandler_; 19356 let unloadListener_ = (this.unloadListener_ = function () { 19357 off(this, 'resize', debouncedHandler_); 19358 off(this, 'unload', unloadListener_); 19359 unloadListener_ = null; 19360 }); 19361 19362 // safari and edge can unload the iframe before resizemanager dispose 19363 // we have to dispose of event handlers correctly before that happens 19364 on(this.el_.contentWindow, 'unload', unloadListener_); 19365 on(this.el_.contentWindow, 'resize', debouncedHandler_); 19366 }; 19367 this.one('load', this.loadListener_); 19368 } 19369 } 19370 createEl() { 19371 return super.createEl( 19372 'iframe', 19373 { 19374 className: 'vjs-resize-manager', 19375 tabIndex: -1, 19376 title: this.localize('No content'), 19377 }, 19378 { 19379 'aria-hidden': 'true', 19380 } 19381 ); 19382 } 19383 19384 /** 19385 * Called when a resize is triggered on the iframe or a resize is observed via the ResizeObserver 19386 * 19387 * @fires Player#playerresize 19388 */ 19389 resizeHandler() { 19390 /** 19391 * Called when the player size has changed 19392 * 19393 * @event Player#playerresize 19394 * @type {Event} 19395 */ 19396 // make sure player is still around to trigger 19397 // prevents this from causing an error after dispose 19398 if (!this.player_ || !this.player_.trigger) { 19399 return; 19400 } 19401 this.player_.trigger('playerresize'); 19402 } 19403 dispose() { 19404 if (this.debouncedHandler_) { 19405 this.debouncedHandler_.cancel(); 19406 } 19407 if (this.resizeObserver_) { 19408 if (this.player_.el()) { 19409 this.resizeObserver_.unobserve(this.player_.el()); 19410 } 19411 this.resizeObserver_.disconnect(); 19412 } 19413 if (this.loadListener_) { 19414 this.off('load', this.loadListener_); 19415 } 19416 if (this.el_ && this.el_.contentWindow && this.unloadListener_) { 19417 this.unloadListener_.call(this.el_.contentWindow); 19418 } 19419 this.ResizeObserver = null; 19420 this.resizeObserver = null; 19421 this.debouncedHandler_ = null; 19422 this.loadListener_ = null; 19423 super.dispose(); 19424 } 19425} 19426Component$1.registerComponent('ResizeManager', ResizeManager); 19427 19428/** @import Player from './player' */ 19429 19430const defaults$1 = { 19431 trackingThreshold: 20, 19432 liveTolerance: 15, 19433}; 19434 19435/* 19436 track when we are at the live edge, and other helpers for live playback */ 19437 19438/** 19439 * A class for checking live current time and determining when the player 19440 * is at or behind the live edge. 19441 */ 19442class LiveTracker extends Component$1 { 19443 /** 19444 * Creates an instance of this class. 19445 * 19446 * @param {Player} player 19447 * The `Player` that this class should be attached to. 19448 * 19449 * @param {Object} [options] 19450 * The key/value store of player options. 19451 * 19452 * @param {number} [options.trackingThreshold=20] 19453 * Number of seconds of live window (seekableEnd - seekableStart) that 19454 * media needs to have before the liveui will be shown. 19455 * 19456 * @param {number} [options.liveTolerance=15] 19457 * Number of seconds behind live that we have to be 19458 * before we will be considered non-live. Note that this will only 19459 * be used when playing at the live edge. This allows large seekable end 19460 * changes to not effect whether we are live or not. 19461 */ 19462 constructor(player, options) { 19463 // LiveTracker does not need an element 19464 const options_ = merge$1(defaults$1, options, { 19465 createEl: false, 19466 }); 19467 super(player, options_); 19468 this.trackLiveHandler_ = () => this.trackLive_(); 19469 this.handlePlay_ = (e) => this.handlePlay(e); 19470 this.handleFirstTimeupdate_ = (e) => this.handleFirstTimeupdate(e); 19471 this.handleSeeked_ = (e) => this.handleSeeked(e); 19472 this.seekToLiveEdge_ = (e) => this.seekToLiveEdge(e); 19473 this.reset_(); 19474 this.on(this.player_, 'durationchange', (e) => this.handleDurationchange(e)); 19475 // we should try to toggle tracking on canplay as native playback engines, like Safari 19476 // may not have the proper values for things like seekableEnd until then 19477 this.on(this.player_, 'canplay', () => this.toggleTracking()); 19478 } 19479 19480 /** 19481 * all the functionality for tracking when seek end changes 19482 * and for tracking how far past seek end we should be 19483 */ 19484 trackLive_() { 19485 const seekable = this.player_.seekable(); 19486 19487 // skip undefined seekable 19488 if (!seekable || !seekable.length) { 19489 return; 19490 } 19491 const newTime = Number(window__default['default'].performance.now().toFixed(4));
vendor: 1,414 bytes, lines 19492-19535
19492 const deltaTime = this.lastTime_ === -1 ? 0 : (newTime - this.lastTime_) / 1000; 19493 this.lastTime_ = newTime; 19494 this.pastSeekEnd_ = this.pastSeekEnd() + deltaTime; 19495 const liveCurrentTime = this.liveCurrentTime(); 19496 const currentTime = this.player_.currentTime(); 19497 19498 // we are behind live if any are true 19499 // 1. the player is paused 19500 // 2. the user seeked to a location 2 seconds away from live 19501 // 3. the difference between live and current time is greater 19502 // liveTolerance which defaults to 15s 19503 let isBehind = 19504 this.player_.paused() || 19505 this.seekedBehindLive_ || 19506 Math.abs(liveCurrentTime - currentTime) > this.options_.liveTolerance; 19507 19508 // we cannot be behind if 19509 // 1. until we have not seen a timeupdate yet 19510 // 2. liveCurrentTime is Infinity, which happens on Android and Native Safari 19511 if (!this.timeupdateSeen_ || liveCurrentTime === Infinity) { 19512 isBehind = false; 19513 } 19514 if (isBehind !== this.behindLiveEdge_) { 19515 this.behindLiveEdge_ = isBehind; 19516 this.trigger('liveedgechange'); 19517 } 19518 } 19519 19520 /** 19521 * handle a durationchange event on the player 19522 * and start/stop tracking accordingly. 19523 */ 19524 handleDurationchange() { 19525 this.toggleTracking(); 19526 } 19527 19528 /** 19529 * start/stop tracking 19530 */ 19531 toggleTracking() { 19532 if ( 19533 this.player_.duration() === Infinity && 19534 this.liveWindow() >= this.options_.trackingThreshold 19535
vendor: 6,595 bytes, lines 19535-19785
19535) { 19536 if (this.player_.options_.liveui) { 19537 this.player_.addClass('vjs-liveui'); 19538 } 19539 this.startTracking(); 19540 } else { 19541 this.player_.removeClass('vjs-liveui'); 19542 this.stopTracking(); 19543 } 19544 } 19545 19546 /** 19547 * start tracking live playback 19548 */ 19549 startTracking() { 19550 if (this.isTracking()) { 19551 return; 19552 } 19553 19554 // If we haven't seen a timeupdate, we need to check whether playback 19555 // began before this component started tracking. This can happen commonly 19556 // when using autoplay. 19557 if (!this.timeupdateSeen_) { 19558 this.timeupdateSeen_ = this.player_.hasStarted(); 19559 } 19560 this.trackingInterval_ = this.setInterval(this.trackLiveHandler_, UPDATE_REFRESH_INTERVAL); 19561 this.trackLive_(); 19562 this.on(this.player_, ['play', 'pause'], this.trackLiveHandler_); 19563 if (!this.timeupdateSeen_) { 19564 this.one(this.player_, 'play', this.handlePlay_); 19565 this.one(this.player_, 'timeupdate', this.handleFirstTimeupdate_); 19566 } else { 19567 this.on(this.player_, 'seeked', this.handleSeeked_); 19568 } 19569 } 19570 19571 /** 19572 * handle the first timeupdate on the player if it wasn't already playing 19573 * when live tracker started tracking. 19574 */ 19575 handleFirstTimeupdate() { 19576 this.timeupdateSeen_ = true; 19577 this.on(this.player_, 'seeked', this.handleSeeked_); 19578 } 19579 19580 /** 19581 * Keep track of what time a seek starts, and listen for seeked 19582 * to find where a seek ends. 19583 */ 19584 handleSeeked() { 19585 const timeDiff = Math.abs(this.liveCurrentTime() - this.player_.currentTime()); 19586 this.seekedBehindLive_ = this.nextSeekedFromUser_ && timeDiff > 2; 19587 this.nextSeekedFromUser_ = false; 19588 this.trackLive_(); 19589 } 19590 19591 /** 19592 * handle the first play on the player, and make sure that we seek 19593 * right to the live edge. 19594 */ 19595 handlePlay() { 19596 this.one(this.player_, 'timeupdate', this.seekToLiveEdge_); 19597 } 19598 19599 /** 19600 * Stop tracking, and set all internal variables to 19601 * their initial value. 19602 */ 19603 reset_() { 19604 this.lastTime_ = -1; 19605 this.pastSeekEnd_ = 0; 19606 this.lastSeekEnd_ = -1; 19607 this.behindLiveEdge_ = true; 19608 this.timeupdateSeen_ = false; 19609 this.seekedBehindLive_ = false; 19610 this.nextSeekedFromUser_ = false; 19611 this.clearInterval(this.trackingInterval_); 19612 this.trackingInterval_ = null; 19613 this.off(this.player_, ['play', 'pause'], this.trackLiveHandler_); 19614 this.off(this.player_, 'seeked', this.handleSeeked_); 19615 this.off(this.player_, 'play', this.handlePlay_); 19616 this.off(this.player_, 'timeupdate', this.handleFirstTimeupdate_); 19617 this.off(this.player_, 'timeupdate', this.seekToLiveEdge_); 19618 } 19619 19620 /** 19621 * The next seeked event is from the user. Meaning that any seek 19622 * > 2s behind live will be considered behind live for real and 19623 * liveTolerance will be ignored. 19624 */ 19625 nextSeekedFromUser() { 19626 this.nextSeekedFromUser_ = true; 19627 } 19628 19629 /** 19630 * stop tracking live playback 19631 */ 19632 stopTracking() { 19633 if (!this.isTracking()) { 19634 return; 19635 } 19636 this.reset_(); 19637 this.trigger('liveedgechange'); 19638 } 19639 19640 /** 19641 * A helper to get the player seekable end 19642 * so that we don't have to null check everywhere 19643 * 19644 * @return {number} 19645 * The furthest seekable end or Infinity. 19646 */ 19647 seekableEnd() { 19648 const seekable = this.player_.seekable(); 19649 const seekableEnds = []; 19650 let i = seekable ? seekable.length : 0; 19651 while (i--) { 19652 seekableEnds.push(seekable.end(i)); 19653 } 19654 19655 // grab the furthest seekable end after sorting, or if there are none 19656 // default to Infinity 19657 return seekableEnds.length ? seekableEnds.sort()[seekableEnds.length - 1] : Infinity; 19658 } 19659 19660 /** 19661 * A helper to get the player seekable start 19662 * so that we don't have to null check everywhere 19663 * 19664 * @return {number} 19665 * The earliest seekable start or 0. 19666 */ 19667 seekableStart() { 19668 const seekable = this.player_.seekable(); 19669 const seekableStarts = []; 19670 let i = seekable ? seekable.length : 0; 19671 while (i--) { 19672 seekableStarts.push(seekable.start(i)); 19673 } 19674 19675 // grab the first seekable start after sorting, or if there are none 19676 // default to 0 19677 return seekableStarts.length ? seekableStarts.sort()[0] : 0; 19678 } 19679 19680 /** 19681 * Get the live time window aka 19682 * the amount of time between seekable start and 19683 * live current time. 19684 * 19685 * @return {number} 19686 * The amount of seconds that are seekable in 19687 * the live video. 19688 */ 19689 liveWindow() { 19690 const liveCurrentTime = this.liveCurrentTime(); 19691 19692 // if liveCurrenTime is Infinity then we don't have a liveWindow at all 19693 if (liveCurrentTime === Infinity) { 19694 return 0; 19695 } 19696 return liveCurrentTime - this.seekableStart(); 19697 } 19698 19699 /** 19700 * Determines if the player is live, only checks if this component 19701 * is tracking live playback or not 19702 * 19703 * @return {boolean} 19704 * Whether liveTracker is tracking 19705 */ 19706 isLive() { 19707 return this.isTracking(); 19708 } 19709 19710 /** 19711 * Determines if currentTime is at the live edge and won't fall behind 19712 * on each seekableendchange 19713 * 19714 * @return {boolean} 19715 * Whether playback is at the live edge 19716 */ 19717 atLiveEdge() { 19718 return !this.behindLiveEdge(); 19719 } 19720 19721 /** 19722 * get what we expect the live current time to be 19723 * 19724 * @return {number} 19725 * The expected live current time 19726 */ 19727 liveCurrentTime() { 19728 return this.pastSeekEnd() + this.seekableEnd(); 19729 } 19730 19731 /** 19732 * The number of seconds that have occurred after seekable end 19733 * changed. This will be reset to 0 once seekable end changes. 19734 * 19735 * @return {number} 19736 * Seconds past the current seekable end 19737 */ 19738 pastSeekEnd() { 19739 const seekableEnd = this.seekableEnd(); 19740 if (this.lastSeekEnd_ !== -1 && seekableEnd !== this.lastSeekEnd_) { 19741 this.pastSeekEnd_ = 0; 19742 } 19743 this.lastSeekEnd_ = seekableEnd; 19744 return this.pastSeekEnd_; 19745 } 19746 19747 /** 19748 * If we are currently behind the live edge, aka currentTime will be 19749 * behind on a seekableendchange 19750 * 19751 * @return {boolean} 19752 * If we are behind the live edge 19753 */ 19754 behindLiveEdge() { 19755 return this.behindLiveEdge_; 19756 } 19757 19758 /** 19759 * Whether live tracker is currently tracking or not. 19760 */ 19761 isTracking() { 19762 return typeof this.trackingInterval_ === 'number'; 19763 } 19764 19765 /** 19766 * Seek to the live edge if we are behind the live edge 19767 */ 19768 seekToLiveEdge() { 19769 this.seekedBehindLive_ = false; 19770 if (this.atLiveEdge()) { 19771 return; 19772 } 19773 this.nextSeekedFromUser_ = false; 19774 this.player_.currentTime(this.liveCurrentTime()); 19775 } 19776 19777 /** 19778 * Dispose of liveTracker 19779 */ 19780 dispose() { 19781 this.stopTracking(); 19782 super.dispose(); 19783 } 19784} 19785Component
19785$1.registerComponent('LiveTracker', LiveTracker); 19786 19787/** 19788 * Displays an element over the player which contains an optional title and 19789 * description for the current content. 19790 * 19791 * Much of the code for this component originated in the now obsolete 19792 * videojs-dock plugin: https://github.com/brightcove/videojs-dock/ 19793 * 19794 * @extends Component 19795 */ 19796class TitleBar extends Component$1 { 19797 constructor(player, options) { 19798 super(player, options); 19799 this.on('statechanged', (e) => this.updateDom_()); 19800 this.updateDom_(); 19801 } 19802 19803 /** 19804 * Create the `TitleBar`'s DOM element 19805 * 19806 * @return {Element} 19807 * The element that was created. 19808 */ 19809 createEl() { 19810 this.els = { 19811 title: createEl('div', { 19812 className: 'vjs-title-bar-title', 19813 id: `vjs-title-bar-title-${newGUID()}`, 19814 }), 19815 description: createEl('div', { 19816 className: 'vjs-title-bar-description', 19817 id: `vjs-title-bar-description-${newGUID()}`, 19818 }), 19819 }; 19820 return createEl( 19821 'div', 19822 { 19823 className: 'vjs-title-bar', 19824 }, 19825 {}, 19826 values(this.els) 19827 ); 19828 } 19829 19830 /** 19831 * Updates the DOM based on the component's state object. 19832 */ 19833 updateDom_() { 19834 const tech = this.player_.tech_; 19835 const techEl = tech && tech.el_; 19836 const techAriaAttrs = { 19837 title: 'aria-labelledby', 19838 description: 'aria-describedby', 19839 }; 19840 ['title', 'description'].forEach((k) => { 19841 const value = this.state[k]; 19842 const el = this.els[k]; 19843 const techAriaAttr = techAriaAttrs[k]; 19844 emptyEl(el); 19845 if (value) { 19846 textContent(el, value); 19847 } 19848 19849 // If there is a tech element available, update its ARIA attributes 19850 // according to whether a title and/or description have been provided. 19851 if (techEl) { 19852 techEl.removeAttribute(techAriaAttr); 19853 if (value) { 19854 techEl.setAttribute(techAriaAttr, el.id); 19855 } 19856 } 19857 }); 19858 if (this.state.title || this.state.description) { 19859 this.show(); 19860 } else { 19861 this.hide(); 19862 } 19863 } 19864 19865 /** 19866 * Update the contents of the title bar component with new title and 19867 * description text. 19868 * 19869 * If both title and description are missing, the title bar will be hidden. 19870 * 19871 * If either title or description are present, the title bar will be visible. 19872 * 19873 * NOTE: Any previously set value will be preserved. To unset a previously 19874 * set value, you must pass an empty string or null. 19875 * 19876 * For example: 19877 * 19878 * ``` 19879 * update({title: 'foo', description: 'bar'}) // title: 'foo', description: 'bar' 19880 * update({description: 'bar2'}) // title: 'foo', description: 'bar2' 19881 * update({title: ''}) // title: '', description: 'bar2' 19882 * update({title: 'foo', description: null}) // title: 'foo', description: null 19883 * ``` 19884 * 19885 * @param {Object} [options={}] 19886 * An options object. When empty, the title bar will be hidden. 19887 * 19888 * @param {string} [options.title] 19889 * A title to display in the title bar. 19890 * 19891 * @param {string} [options.description] 19892 * A description to display in the title bar. 19893 */ 19894 update(options) { 19895 this.setState(options); 19896 } 19897 19898 /** 19899 * Dispose the component. 19900 */ 19901 dispose() { 19902 const tech = this.player_.tech_; 19903 const techEl = tech && tech.el_; 19904 if (techEl) { 19905 techEl.removeAttribute('aria-labelledby'); 19906 techEl.removeAttribute('aria-describedby'); 19907 } 19908 super.dispose(); 19909 this.els = null; 19910 } 19911} 19912Component$1.registerComponent('TitleBar', TitleBar); 19913 19914/** @import Player from './player' */ 19915 19916/** 19917 * @typedef {object} TransientButtonOptions 19918 * @property {string} [controlText] Control text, usually visible for these buttons 19919 * @property {number} [initialDisplay=4000] Time in ms that button should initially remain visible 19920 * @property {Array<'top'|'neartop'|'bottom'|'left'|'right'>} [position] Array of position strings to add basic styles for positioning 19921 * @property {string} [className] Class(es) to add 19922 * @property {boolean} [takeFocus=false] Whether element sohuld take focus when shown 19923 * @property {Function} [clickHandler] Function called on button activation 19924 */ 19925 19926/** @type {TransientButtonOptions} */ 19927const defaults = { 19928 initialDisplay: 4000, 19929 position: [], 19930 takeFocus: false, 19931}; 19932 19933/** 19934 * A floating transient button. 19935 * It's recommended to insert these buttons _before_ the control bar with the this argument to `addChild` 19936 * for a logical tab order. 19937 * 19938 * @example 19939 * ``` 19940 * player.addChild( 19941 * 'TransientButton', 19942 * options, 19943 * player.children().indexOf(player.getChild("ControlBar")) 19944 * ) 19945 * ``` 19946 * 19947 * @extends Button 19948 */ 19949class TransientButton extends Button { 19950 /** 19951 * TransientButton constructor 19952 * 19953 * @param {Player} player The button's player 19954 * @param {TransientButtonOptions} options Options for the transient button 19955 */ 19956 constructor(player, options) { 19957 options = merge$1(defaults, options); 19958 super(player, options); 19959 this.controlText(options.controlText); 19960 this.hide(); 19961 19962 // When shown, the float button will be visible even if the user is inactive. 19963 // Clear this if there is any interaction. 19964 this.on(this.player_, ['useractive', 'userinactive'], (e) => { 19965 this.removeClass('force-display'); 19966 }); 19967 } 19968 19969 /** 19970 * Return CSS class including position classes 19971 * 19972 * @return {string} CSS class list 19973 */ 19974 buildCSSClass() { 19975 return `vjs-transient-button focus-visible ${this.options_.position.map((c) =>
19975 `vjs-${c}`).join(' ')}`; 19976 } 19977 19978 /** 19979 * Create the button element 19980 * 19981 * @return {HTMLButtonElement} The button element 19982 */ 19983 createEl() { 19984 /** @type HTMLButtonElement */ 19985 const el = createEl( 19986 'button', 19987 {}, 19988 { 19989 type: 'button', 19990 class: this.buildCSSClass(), 19991 }, 19992 createEl('span') 19993 ); 19994 this.controlTextEl_ = el.querySelector('span'); 19995 return el; 19996 } 19997 19998 /** 19999 * Show the button. The button will remain visible for the `initialDisplay` time, default 4s, 20000 * and when there is user activity. 20001 */ 20002 show() { 20003 super.show(); 20004 this.addClass('force-display'); 20005 if (this.options_.takeFocus) { 20006 this.el().focus({ 20007 preventScroll: true, 20008 }); 20009 } 20010 this.forceDisplayTimeout = this.player_.setTimeout(() => { 20011 this.removeClass('force-display'); 20012 }, this.options_.initialDisplay); 20013 } 20014 20015 /** 20016 * Hide the display, even if during the `initialDisplay` time. 20017 */ 20018 hide() { 20019 this.removeClass('force-display'); 20020 super.hide(); 20021 } 20022 20023 /** 20024 * Dispose the component 20025 */ 20026 dispose() { 20027 this.player_.clearTimeout(this.forceDisplayTimeout); 20028 super.dispose(); 20029 } 20030} 20031Component$1.registerComponent('TransientButton', TransientButton); 20032 20033/** @import Html5 from './html5' */ 20034 20035/** 20036 * This function is used to fire a sourceset when there is something 20037 * similar to `mediaEl.load()` being called. It will try to find the source via 20038 * the `src` attribute and then the `<source>` elements. It will then fire `sourceset` 20039 * with the source that was found or empty string if we cannot know. If it cannot 20040 * find a source then `sourceset` will not be fired. 20041 * 20042 * @param {Html5} tech 20043 * The tech object that sourceset was setup on 20044 * 20045 * @return {boolean} 20046 * returns false if the sourceset was not fired and true otherwise. 20047 */ 20048const sourcesetLoad = (tech) => { 20049 const el = tech.el(); 20050 20051 // if `el.src` is set, that source will be loaded. 20052 if (el.hasAttribute('src')) { 20053 tech.triggerSourceset(el.src); 20054 return true; 20055 } 20056 20057 /** 20058 * Since there isn't a src property on the media element, source elements will be used for 20059 * implementing the source selection algorithm. This happens asynchronously and 20060 * for most cases were there is more than one source we cannot tell what source will 20061 * be loaded, without re-implementing the source selection algorithm. At this time we are not 20062 * going to do that. There are three special cases that we do handle here though: 20063 * 20064 * 1. If there are no sources, do not fire `sourceset`. 20065 * 2. If there is only one `<source>` with a `src` property/attribute that is our `src` 20066 * 3. If there is more than one `<source>` but all of them have the same `src` url. 20067 * That will be our src. 20068 */ 20069 const sources = tech.$$('source'); 20070 const srcUrls = []; 20071 let src = ''; 20072 20073 // if there are no sources, do not fire sourceset 20074 if (!sources.length) { 20075 return false; 20076 } 20077 20078 // only count valid/non-duplicate source elements 20079 for (let i = 0; i < sources.length; i++) { 20080 const url = sources[i].src; 20081 if (url && srcUrls.indexOf(url) === -1) { 20082 srcUrls.push(url); 20083 } 20084 } 20085 20086 // there were no valid sources 20087 if (!srcUrls.length) { 20088 return false; 20089 } 20090 20091 // there is only one valid source element url 20092 // use that 20093 if (srcUrls.length === 1) { 20094 src = srcUrls[0]; 20095 } 20096 tech.triggerSourceset(src); 20097 return true; 20098}; 20099 20100/** 20101 * our implementation of an `innerHTML` descriptor for browsers 20102 * that do not have one. 20103 */ 20104const innerHTMLDescriptorPolyfill = Object.defineProperty({}, 'innerHTML', { 20105 get() { 20106 return this.cloneNode(true).innerHTML; 20107 }, 20108 set(v) { 20109 // make a dummy node to use innerHTML on 20110 const dummy = document__default['default'].createElement(this.nodeName.toLowerCase()); 20111 20112 // set innerHTML to the value provided 20113 dummy.innerHTML = v; 20114 20115 // make a document fragment to hold the nodes from dummy 20116 const docFrag = document__default['default'].createDocumentFragment(); 20117 20118 // copy all of the nodes created by the innerHTML on dummy 20119 // to the document fragment 20120 while (dummy.childNodes.length) { 20121 docFrag.appendChild(dummy.childNodes[0]); 20122 } 20123 20124 // remove content 20125 this.innerText = ''; 20126 20127 // now we add all of that html in one by appending the 20128 // document fragment. This is how innerHTML does it. 20129 window__default['default'].Element.prototype.appendChild.call(this, docFrag); 20130 20131 // then return the result that innerHTML's setter would 20132 return this.innerHTML; 20133 }, 20134}); 20135 20136/** 20137 * Get a property descriptor given a list of priorities and the 20138 * property to get. 20139 */ 20140const getDescriptor = (priority, prop) => { 20141 let descriptor = {}; 20142 for (let i = 0; i < priority.length; i++) { 20143 descriptor = Object.getOwnPropertyDescriptor(priority[i], prop); 20144 if (descriptor && descriptor.set && descriptor.get) { 20145 break; 20146 } 20147 } 20148 descriptor.enumerable = true; 20149 descriptor.configurable = true; 20150 return descriptor; 20151}; 20152const getInnerHTMLDescriptor = (tech) => 20153 getDescriptor( 20154 [ 20155 tech.el(), 20156 window__default['default'].HTMLMediaElement.prototype, 20157 window__default['default'].Element.prototype, 20158 innerHTMLDescriptorPolyfill, 20159 ], 20160 'innerHTML' 20161 ); 20162 20163/** 20164 * Patches browser internal functions so that we can tell synchronously 20165 * if a `<source>` was appended to the media element. For some reason this 20166 * causes a `sourceset` if the the media element is ready and has no source. 20167 * This happens when: 20168 * - The page has just loaded and the media element does not have a source. 20169 * - The media element was emptied of all sources, then `load()` was called. 20170 * 20171 * It does this by patching the following functions/properties when they are supported: 20172 * 20173 * - `append()` - can be used to add a `<source>` element to the media element 20174 * - `appendChild()` - can be used to add a `<source>` element to the media element 20175 * - `insertAdjacentHTML()` - can be used to add a `<source>` element to the media element 20176 * - `innerHTML` - can be used to add a `<source>` element to the media element 20177 * 20178 * @param {Html5} tech 20179 * The tech object that sourceset is being setup on. 20180 */ 20181const firstSourceWatch = function (tech) { 20182 const el = tech.el(); 20183 20184 // make sure firstSourceWatch isn't setup twice. 20185 if (el.resetSourceWatch_) { 20186 return; 20187 } 20188 const old = {}; 20189 const innerDescriptor = getInnerHTMLDescriptor(tech); 20190 const appendWrapper = 20191 (appendFn) => 20192 (...args) => { 20193 const retval = appendFn.apply(el, args); 20194 sourcesetLoad(tech); 20195 return retval; 20196 }; 20197 ['append', 'appendChild', 'insertAdjacentHTML'].forEach((k) => { 20198 if (!el[k]) { 20199 return; 20200 } 20201 20202 // store the old function 20203 old[k] = el[k]; 20204 20205 // call the old function with a sourceset if a source 20206 // was loaded 20207 el[k] = appendWrapper(old[k]); 20208 }); 20209 Object.defineProperty( 20210 el, 20211 'innerHTML', 20212 merge$1(innerDescriptor, { 20213 set: appendWrapper(innerDescriptor.set), 20214 }) 20215 ); 20216 el.resetSourceWatch_ = () => { 20217 el.resetSourceWatch_ = null;
20218 Object.keys(old).forEach((k) => { 20219 el[k] = old[k]; 20220 }); 20221 Object.defineProperty(el, 'innerHTML', innerDescriptor); 20222 }; 20223 20224 // on the first sourceset, we need to revert our changes 20225 tech.one('sourceset', el.resetSourceWatch_); 20226}; 20227 20228/** 20229 * our implementation of a `src` descriptor for browsers 20230 * that do not have one 20231 */ 20232const srcDescriptorPolyfill = Object.defineProperty({}, 'src', { 20233 get() { 20234 if (this.hasAttribute('src')) { 20235 return getAbsoluteURL( 20236 window__default['default'].Element.prototype.getAttribute.call(this, 'src') 20237 ); 20238 } 20239 return ''; 20240 }, 20241 set(v) { 20242 window__default['default'].Element.prototype.setAttribute.call(this, 'src', v); 20243 return v; 20244 }, 20245}); 20246const getSrcDescriptor = (tech) => 20247 getDescriptor( 20248 [tech.el(), window__default['default'].HTMLMediaElement.prototype, srcDescriptorPolyfill], 20249 'src' 20250 ); 20251 20252/** 20253 * setup `sourceset` handling on the `Html5` tech. This function 20254 * patches the following element properties/functions: 20255 * 20256 * - `src` - to determine when `src` is set 20257 * - `setAttribute()` - to determine when `src` is set 20258 * - `load()` - this re-triggers the source selection algorithm, and can 20259 * cause a sourceset. 20260 * 20261 * If there is no source when we are adding `sourceset` support or during a `load()` 20262 * we also patch the functions listed in `firstSourceWatch`. 20263 * 20264 * @param {Html5} tech 20265 * The tech to patch 20266 */ 20267const setupSourceset = function (tech) { 20268 if (!tech.featuresSourceset) { 20269 return; 20270 } 20271 const el = tech.el(); 20272 20273 // make sure sourceset isn't setup twice. 20274 if (el.resetSourceset_) { 20275 return; 20276 } 20277 const srcDescriptor = getSrcDescriptor(tech); 20278 const oldSetAttribute = el.setAttribute; 20279 const oldLoad = el.load; 20280 Object.defineProperty( 20281 el, 20282 'src', 20283 merge$1(srcDescriptor, { 20284 set: (v) => { 20285 const retval = srcDescriptor.set.call(el, v); 20286 20287 // we use the getter here to get the actual value set on src 20288 tech.triggerSourceset(el.src); 20289 return retval; 20290 }, 20291 }) 20292 ); 20293 el.setAttribute = (n, v) => { 20294 const retval = oldSetAttribute.call(el, n, v); 20295 if (/src/i.test(n)) { 20296 tech.triggerSourceset(el.src); 20297 } 20298 return retval; 20299 }; 20300 el.load = () => { 20301 const retval = oldLoad.call(el); 20302 20303 // if load was called, but there was no source to fire 20304 // sourceset on. We have to watch for a source append 20305 // as that can trigger a `sourceset` when the media element 20306 // has no source 20307 if (!sourcesetLoad(tech)) { 20308 tech.triggerSourceset(''); 20309 firstSourceWatch(tech); 20310 } 20311 return retval; 20312 }; 20313 if (el.currentSrc) { 20314 tech.triggerSourceset(el.currentSrc); 20315 } else if (!sourcesetLoad(tech)) { 20316 firstSourceWatch(tech); 20317 } 20318 el.resetSourceset_ = () => { 20319 el.resetSourceset_ = null; 20320 el.load = oldLoad; 20321 el.setAttribute = oldSetAttribute; 20322 Object.defineProperty(el, 'src', srcDescriptor); 20323 if (el.resetSourceWatch_) { 20324 el.resetSourceWatch_(); 20325 } 20326 }; 20327}; 20328 20329/** 20330 * @file html5.js 20331 */ 20332 20333/** 20334 * HTML5 Media Controller - Wrapper for HTML5 Media API 20335 * 20336 * @mixes Tech~SourceHandlerAdditions 20337 * @extends Tech 20338 */ 20339class Html5 extends Tech { 20340 /** 20341 * Create an instance of this Tech. 20342 * 20343 * @param {Object} [options] 20344 * The key/value store of player options. 20345 * 20346 * @param {Function} [ready] 20347 * Callback function to call when the `HTML5` Tech is ready. 20348 */ 20349 constructor(options, ready) { 20350 super(options, ready); 20351 const source = options.source; 20352 let crossoriginTracks = false; 20353 this.featuresVideoFrameCallback = 20354 this.featuresVideoFrameCallback && this.el_.tagName === 'VIDEO'; 20355 20356 // Set the source if one is provided 20357 // 1) Check if the source is new (if not, we want to keep the original so playback isn't interrupted) 20358 // 2) Check to see if the network state of the tag was failed at init, and if so, reset the source 20359 // anyway so the error gets fired. 20360 if ( 20361 source && 20362 (this.el_.currentSrc !== source.src || (options.tag && options.tag.
vendor: 6,602 bytes, lines 20362-20536
20362initNetworkState_ === 3)) 20363 ) { 20364 this.setSource(source); 20365 } else { 20366 this.handleLateInit_(this.el_); 20367 } 20368 20369 // setup sourceset after late sourceset/init 20370 if (options.enableSourceset) { 20371 this.setupSourcesetHandling_(); 20372 } 20373 this.isScrubbing_ = false; 20374 if (this.el_.hasChildNodes()) { 20375 const nodes = this.el_.childNodes; 20376 let nodesLength = nodes.length; 20377 const removeNodes = []; 20378 while (nodesLength--) { 20379 const node = nodes[nodesLength]; 20380 const nodeName = node.nodeName.toLowerCase(); 20381 if (nodeName === 'track') { 20382 if (!this.featuresNativeTextTracks) { 20383 // Empty video tag tracks so the built-in player doesn't use them also. 20384 // This may not be fast enough to stop HTML5 browsers from reading the tags 20385 // so we'll need to turn off any default tracks if we're manually doing 20386 // captions and subtitles. videoElement.textTracks 20387 removeNodes.push(node); 20388 } else { 20389 // store HTMLTrackElement and TextTrack to remote list 20390 this.remoteTextTrackEls().addTrackElement_(node); 20391 this.remoteTextTracks().addTrack(node.track); 20392 this.textTracks().addTrack(node.track); 20393 if ( 20394 !crossoriginTracks && 20395 !this.el_.hasAttribute('crossorigin') && 20396 isCrossOrigin(node.src) 20397 ) { 20398 crossoriginTracks = true; 20399 } 20400 } 20401 } 20402 } 20403 for (let i = 0; i < removeNodes.length; i++) { 20404 this.el_.removeChild(removeNodes[i]); 20405 } 20406 } 20407 this.proxyNativeTracks_(); 20408 if (this.featuresNativeTextTracks && crossoriginTracks) { 20409 log$1.warn( 20410 "Text Tracks are being loaded from another origin but the crossorigin attribute isn't used.\n" + 20411 'This may prevent text tracks from loading.' 20412 ); 20413 } 20414 20415 // prevent iOS Safari from disabling metadata text tracks during native playback 20416 this.restoreMetadataTracksInIOSNativePlayer_(); 20417 20418 // Determine if native controls should be used 20419 // Our goal should be to get the custom controls on mobile solid everywhere 20420 // so we can remove this all together. Right now this will block custom 20421 // controls on touch enabled laptops like the Chrome Pixel 20422 if ((TOUCH_ENABLED || IS_IPHONE) && options.nativeControlsForTouch === true) { 20423 this.setControls(true); 20424 } 20425 20426 // on iOS, we want to proxy `webkitbeginfullscreen` and `webkitendfullscreen` 20427 // into a `fullscreenchange` event 20428 this.proxyWebkitFullscreen_(); 20429 this.triggerReady(); 20430 } 20431 20432 /** 20433 * Dispose of `HTML5` media element and remove all tracks. 20434 */ 20435 dispose() { 20436 if (this.el_ && this.el_.resetSourceset_) { 20437 this.el_.resetSourceset_(); 20438 } 20439 Html5.disposeMediaElement(this.el_); 20440 this.options_ = null; 20441 20442 // tech will handle clearing of the emulated track list 20443 super.dispose(); 20444 } 20445 20446 /** 20447 * Modify the media element so that we can detect when 20448 * the source is changed. Fires `sourceset` just after the source has changed 20449 */ 20450 setupSourcesetHandling_() { 20451 setupSourceset(this); 20452 } 20453 20454 /** 20455 * When a captions track is enabled in the iOS Safari native player, all other 20456 * tracks are disabled (including metadata tracks), which nulls all of their 20457 * associated cue points. This will restore metadata tracks to their pre-fullscreen 20458 * state in those cases so that cue points are not needlessly lost. 20459 * 20460 * @private 20461 */ 20462 restoreMetadataTracksInIOSNativePlayer_() { 20463 const textTracks = this.textTracks(); 20464 let metadataTracksPreFullscreenState; 20465 20466 // captures a snapshot of every metadata track's current state 20467 const takeMetadataTrackSnapshot = () => { 20468 metadataTracksPreFullscreenState = []; 20469 for (let i = 0; i < textTracks.length; i++) { 20470 const track = textTracks[i]; 20471 if (track.kind === 'metadata') { 20472 metadataTracksPreFullscreenState.push({ 20473 track, 20474 storedMode: track.mode, 20475 }); 20476 } 20477 } 20478 }; 20479 20480 // snapshot each metadata track's initial state, and update the snapshot 20481 // each time there is a track 'change' event 20482 takeMetadataTrackSnapshot(); 20483 textTracks.addEventListener('change', takeMetadataTrackSnapshot); 20484 this.on('dispose', () => textTracks.removeEventListener('change', takeMetadataTrackSnapshot)); 20485 const restoreTrackMode = () => { 20486 for (let i = 0; i < metadataTracksPreFullscreenState.length; i++) { 20487 const storedTrack = metadataTracksPreFullscreenState[i]; 20488 if ( 20489 storedTrack.track.mode === 'disabled' && 20490 storedTrack.track.mode !== storedTrack.storedMode 20491 ) { 20492 storedTrack.track.mode = storedTrack.storedMode; 20493 } 20494 } 20495 // we only want this handler to be executed on the first 'change' event 20496 textTracks.removeEventListener('change', restoreTrackMode); 20497 }; 20498 20499 // when we enter fullscreen playback, stop updating the snapshot and 20500 // restore all track modes to their pre-fullscreen state 20501 this.on('webkitbeginfullscreen', () => { 20502 textTracks.removeEventListener('change', takeMetadataTrackSnapshot); 20503 20504 // remove the listener before adding it just in case it wasn't previously removed 20505 textTracks.removeEventListener('change', restoreTrackMode); 20506 textTracks.addEventListener('change', restoreTrackMode); 20507 }); 20508 20509 // start updating the snapshot again after leaving fullscreen 20510 this.on('webkitendfullscreen', () => { 20511 // remove the listener before adding it just in case it wasn't previously removed 20512 textTracks.removeEventListener('change', takeMetadataTrackSnapshot); 20513 textTracks.addEventListener('change', takeMetadataTrackSnapshot); 20514 20515 // remove the restoreTrackMode handler in case it wasn't triggered during fullscreen playback 20516 textTracks.removeEventListener('change', restoreTrackMode); 20517 }); 20518 } 20519 20520 /** 20521 * Attempt to force override of tracks for the given type 20522 * 20523 * @param {string} type - Track type to override, possible values include 'Audio', 20524 * 'Video', and 'Text'. 20525 * @param {boolean} override - If set to true native audio/video will be overridden, 20526 * otherwise native audio/video will potentially be used. 20527 * @private 20528 */ 20529 overrideNative_(type, override) { 20530 // If there is no behavioral change don't add/remove listeners 20531 if (override !== this[`featuresNative${type}Tracks`]) { 20532 return; 20533 } 20534 const lowerCaseType = type.toLowerCase(); 20535 if (this[`${lowerCaseType}TracksListeners_`]) { 20536 Object.keys(this[`${lowerCaseType}
20536TracksListeners_`]).forEach((eventName) => { 20537 const elTracks = this.el()[`${lowerCaseType}Tracks`]; 20538 elTracks.removeEventListener( 20539 eventName, 20540 this[`${lowerCaseType}TracksListeners_`][eventName] 20541 ); 20542 }); 20543 } 20544 this[`featuresNative${type}Tracks`] = !override; 20545 this[`${lowerCaseType}TracksListeners_`] = null; 20546 this.proxyNativeTracksForType_(lowerCaseType); 20547 } 20548 20549 /** 20550 * Attempt to force override of native audio tracks. 20551 * 20552 * @param {boolean} override - If set to true native audio will be overridden, 20553 * otherwise native audio will potentially be used. 20554 */ 20555 overrideNativeAudioTracks(override) { 20556 this.overrideNative_('Audio', override); 20557 } 20558 20559 /** 20560 * Attempt to force override of native video tracks. 20561 * 20562 * @param {boolean} override - If set to true native video will be overridden, 20563 * otherwise native video will potentially be used. 20564 */ 20565 overrideNativeVideoTracks(override) { 20566 this.overrideNative_('Video', override); 20567 } 20568 20569 /** 20570 * Proxy native track list events for the given type to our track 20571 * lists if the browser we are playing in supports that type of track list. 20572 * 20573 * @param {string} name - Track type; values include 'audio', 'video', and 'text' 20574 * @private 20575 */ 20576 proxyNativeTracksForType_(name) { 20577 const props = NORMAL[name]; 20578 const elTracks = this.el()[props.getterName]; 20579 const techTracks = this[props.getterName](); 20580 if ( 20581 !this[`featuresNative${props.capitalName}Tracks`] || 20582 !elTracks || 20583 !elTracks.addEventListener 20584 ) { 20585 return; 20586 } 20587 const listeners = { 20588 change: (e) => { 20589 const event = { 20590 type: 'change', 20591 target: techTracks, 20592 currentTarget: techTracks, 20593 srcElement: techTracks, 20594 }; 20595 techTracks.trigger(event); 20596 20597 // if we are a text track change event, we should also notify the 20598 // remote text track list. This can potentially cause a false positive 20599 // if we were to get a change event on a non-remote track and 20600 // we triggered the event on the remote text track list which doesn't 20601 // contain that track. However, best practices mean looping through the 20602 // list of tracks and searching for the appropriate mode value, so, 20603 // this shouldn't pose an issue 20604 if (name === 'text') { 20605 this[REMOTE.remoteText.getterName]().trigger(event); 20606 } 20607 }, 20608 addtrack(e) { 20609 techTracks.addTrack(e.track); 20610 }, 20611 removetrack(e) { 20612 techTracks.removeTrack(e.track); 20613 }, 20614 }; 20615 const removeOldTracks = function () { 20616 const removeTracks = []; 20617 for (let i = 0; i < techTracks.length; i++) { 20618 let found = false; 20619 for (let j = 0; j < elTracks.length; j++) { 20620 if (elTracks[j] === techTracks[i]) { 20621 found = true; 20622 break; 20623 } 20624 } 20625 if (!found) { 20626 removeTracks.push(techTracks[i]); 20627 } 20628 } 20629 while (removeTracks.length) { 20630 techTracks.removeTrack(removeTracks.shift()); 20631 } 20632 }; 20633 this[props.getterName + 'Listeners_'] = listeners;
20634 Object.keys(listeners).forEach((eventName) => { 20635 const listener = listeners[eventName]; 20636 elTracks.addEventListener(eventName, listener); 20637 this.on('dispose', (e) => elTracks.removeEventListener(eventName, listener)); 20638 }); 20639 20640 // Remove (native) tracks that are not used anymore 20641 this.on('loadstart', removeOldTracks); 20642 this.on('dispose', (e) => this.off('loadstart', removeOldTracks)); 20643 } 20644 20645 /** 20646 * Proxy all native track list events to our track lists if the browser we are playing 20647 * in supports that type of track list. 20648 * 20649 * @private 20650 */ 20651 proxyNativeTracks_() { 20652 NORMAL.names.forEach((name) => { 20653 this.proxyNativeTracksForType_(name); 20654 }); 20655 } 20656 20657 /** 20658 * Create the `Html5` Tech's DOM element. 20659 * 20660 * @return {Element} 20661 * The element that gets created. 20662 */ 20663 createEl() { 20664 let el = this.options_.tag; 20665 20666 // Check if this browser supports moving the element into the box. 20667 // On the iPhone video will break if you move the element, 20668 // So we have to create a brand new element. 20669 // If we ingested the player div, we do not need to move the media element. 20670 if (!el || !(this.options_.playerElIngest || this.movingMediaElementInDOM)) { 20671 // If the original tag is still there, clone and remove it. 20672 if (el) { 20673 const clone = el.cloneNode(true); 20674 if (el.parentNode) { 20675 el.parentNode.insertBefore(clone, el); 20676 } 20677 Html5.disposeMediaElement(el); 20678 el = clone; 20679 } else { 20680 el = document__default['default'].createElement('video'); 20681 20682 // determine if native controls should be used 20683 const tagAttributes = this.options_.tag && getAttributes(this.options_.tag); 20684 const attributes = merge$1({}, tagAttributes); 20685 if (!TOUCH_ENABLED || this.options_.nativeControlsForTouch !== true) { 20686 delete attributes.controls; 20687 } 20688 setAttributes( 20689 el, 20690 Object.assign(attributes, { 20691 id: this.options_.techId, 20692 class: 'vjs-tech', 20693 }) 20694 ); 20695 } 20696 el.playerId = this.options_.playerId; 20697 } 20698 if (typeof this.options_.preload !== 'undefined') { 20699 setAttribute(el, 'preload', this.options_.preload); 20700 } 20701 if (this.options_.disablePictureInPicture !== undefined) { 20702 el.disablePictureInPicture = this.options_.disablePictureInPicture; 20703 } 20704 20705 // Update specific tag settings, in case they were overridden 20706 // `autoplay` has to be *last* so that `muted` and `playsinline` are present 20707 // when iOS/Safari or other browsers attempt to autoplay. 20708 const settingsAttrs = ['loop', 'muted', 'playsinline', 'autoplay']; 20709 for (let i = 0; i < settingsAttrs.length; i++) { 20710 const attr = settingsAttrs[i]; 20711 const value = this.options_[attr]; 20712 if (typeof value !== 'undefined') { 20713 if (value) { 20714 setAttribute(el, attr, attr); 20715 } else { 20716 removeAttribute(el, attr); 20717 } 20718 el[attr] = value; 20719 } 20720 } 20721 return el; 20722 } 20723 20724 /** 20725 * This will be triggered if the loadstart event has already fired, before videojs was 20726 * ready. Two known examples of when this can happen are: 20727 * 1. If we're loading the playback object after it has started loading 20728 * 2. The media is already playing the (often with autoplay on) then 20729 * 20730 * This function will fire another loadstart so that videojs can catchup. 20731 * 20732 * @fires Tech#loadstart 20733 * 20734 * @return {undefined} 20735 * returns nothing. 20736 */ 20737 handleLateInit_(el) { 20738 if (el.networkState === 0 || el.networkState === 3) { 20739 // The video element hasn't started loading the source yet 20740 // or didn't find a source 20741 return; 20742 } 20743 if (el.readyState === 0) { 20744 // NetworkState is set synchronously BUT loadstart is fired at the 20745 // end of the current stack, usually before setInterval(fn, 0). 20746 // So at this point we know loadstart may have already fired or is 20747 // about to fire, and either way the player hasn't seen it yet. 20748 // We don't want to fire loadstart prematurely here and cause a 20749 // double loadstart so we'll wait and see if it happens between now 20750 // and the next loop, and fire it if not. 20751 // HOWEVER, we also want to make sure it fires before loadedmetadata 20752 // which could also happen between now and the next loop, so we'll 20753 // watch for that also. 20754 let loadstartFired = false;
vendor: 9,400 bytes, lines 20755-21068
20755 const setLoadstartFired = function () { 20756 loadstartFired = true; 20757 }; 20758 this.on('loadstart', setLoadstartFired); 20759 const triggerLoadstart = function () { 20760 // We did miss the original loadstart. Make sure the player 20761 // sees loadstart before loadedmetadata 20762 if (!loadstartFired) { 20763 this.trigger('loadstart'); 20764 } 20765 }; 20766 this.on('loadedmetadata', triggerLoadstart); 20767 this.ready(function () { 20768 this.off('loadstart', setLoadstartFired); 20769 this.off('loadedmetadata', triggerLoadstart); 20770 if (!loadstartFired) { 20771 // We did miss the original native loadstart. Fire it now. 20772 this.trigger('loadstart'); 20773 } 20774 }); 20775 return; 20776 } 20777 20778 // From here on we know that loadstart already fired and we missed it. 20779 // The other readyState events aren't as much of a problem if we double 20780 // them, so not going to go to as much trouble as loadstart to prevent 20781 // that unless we find reason to. 20782 const eventsToTrigger = ['loadstart']; 20783 20784 // loadedmetadata: newly equal to HAVE_METADATA (1) or greater 20785 eventsToTrigger.push('loadedmetadata'); 20786 20787 // loadeddata: newly increased to HAVE_CURRENT_DATA (2) or greater 20788 if (el.readyState >= 2) { 20789 eventsToTrigger.push('loadeddata'); 20790 } 20791 20792 // canplay: newly increased to HAVE_FUTURE_DATA (3) or greater 20793 if (el.readyState >= 3) { 20794 eventsToTrigger.push('canplay'); 20795 } 20796 20797 // canplaythrough: newly equal to HAVE_ENOUGH_DATA (4) 20798 if (el.readyState >= 4) { 20799 eventsToTrigger.push('canplaythrough'); 20800 } 20801 20802 // We still need to give the player time to add event listeners 20803 this.ready(function () { 20804 eventsToTrigger.forEach(function (type) { 20805 this.trigger(type); 20806 }, this); 20807 }); 20808 } 20809 20810 /** 20811 * Set whether we are scrubbing or not. 20812 * This is used to decide whether we should use `fastSeek` or not. 20813 * `fastSeek` is used to provide trick play on Safari browsers. 20814 * 20815 * @param {boolean} isScrubbing 20816 * - true for we are currently scrubbing 20817 * - false for we are no longer scrubbing 20818 */ 20819 setScrubbing(isScrubbing) { 20820 this.isScrubbing_ = isScrubbing; 20821 } 20822 20823 /** 20824 * Get whether we are scrubbing or not. 20825 * 20826 * @return {boolean} isScrubbing 20827 * - true for we are currently scrubbing 20828 * - false for we are no longer scrubbing 20829 */ 20830 scrubbing() { 20831 return this.isScrubbing_; 20832 } 20833 20834 /** 20835 * Set current time for the `HTML5` tech. 20836 * 20837 * @param {number} seconds 20838 * Set the current time of the media to this. 20839 */ 20840 setCurrentTime(seconds) { 20841 try { 20842 if (this.isScrubbing_ && this.el_.fastSeek && IS_ANY_SAFARI) { 20843 this.el_.fastSeek(seconds); 20844 } else { 20845 this.el_.currentTime = seconds; 20846 } 20847 } catch (e) { 20848 log$1(e, 'Video is not ready. (Video.js)'); 20849 // this.warning(VideoJS.warnings.videoNotReady); 20850 } 20851 } 20852 20853 /** 20854 * Get the current duration of the HTML5 media element. 20855 * 20856 * @return {number} 20857 * The duration of the media or 0 if there is no duration. 20858 */ 20859 duration() { 20860 // Android Chrome will report duration as Infinity for VOD HLS until after 20861 // playback has started, which triggers the live display erroneously. 20862 // Return NaN if playback has not started and trigger a durationupdate once 20863 // the duration can be reliably known. 20864 if (this.el_.duration === Infinity && IS_ANDROID && IS_CHROME && this.el_.currentTime === 0) { 20865 // Wait for the first `timeupdate` with currentTime > 0 - there may be 20866 // several with 0 20867 const checkProgress = () => { 20868 if (this.el_.currentTime > 0) { 20869 // Trigger durationchange for genuinely live video 20870 if (this.el_.duration === Infinity) { 20871 this.trigger('durationchange'); 20872 } 20873 this.off('timeupdate', checkProgress); 20874 } 20875 }; 20876 this.on('timeupdate', checkProgress); 20877 return NaN; 20878 } 20879 return this.el_.duration || NaN; 20880 } 20881 20882 /** 20883 * Get the current width of the HTML5 media element. 20884 * 20885 * @return {number} 20886 * The width of the HTML5 media element. 20887 */ 20888 width() { 20889 return this.el_.offsetWidth; 20890 } 20891 20892 /** 20893 * Get the current height of the HTML5 media element. 20894 * 20895 * @return {number} 20896 * The height of the HTML5 media element. 20897 */ 20898 height() { 20899 return this.el_.offsetHeight; 20900 } 20901 20902 /** 20903 * Proxy iOS `webkitbeginfullscreen` and `webkitendfullscreen` into 20904 * `fullscreenchange` event. 20905 * 20906 * @private 20907 * @fires fullscreenchange 20908 * @listens webkitendfullscreen 20909 * @listens webkitbeginfullscreen 20910 * @listens webkitbeginfullscreen 20911 */ 20912 proxyWebkitFullscreen_() { 20913 if (!('webkitDisplayingFullscreen' in this.el_)) { 20914 return; 20915 } 20916 const endFn = function () { 20917 this.trigger('fullscreenchange', { 20918 isFullscreen: false, 20919 }); 20920 // Safari will sometimes set controls on the videoelement when existing fullscreen. 20921 if (this.el_.controls && !this.options_.nativeControlsForTouch && this.controls()) { 20922 this.el_.controls = false; 20923 } 20924 }; 20925 const beginFn = function () { 20926 if ( 20927 'webkitPresentationMode' in this.el_ && 20928 this.el_.webkitPresentationMode !== 'picture-in-picture' 20929 ) { 20930 this.one('webkitendfullscreen', endFn); 20931 this.trigger('fullscreenchange', { 20932 isFullscreen: true, 20933 // set a flag in case another tech triggers fullscreenchange 20934 nativeIOSFullscreen: true, 20935 }); 20936 } 20937 }; 20938 this.on('webkitbeginfullscreen', beginFn); 20939 this.on('dispose', () => { 20940 this.off('webkitbeginfullscreen', beginFn); 20941 this.off('webkitendfullscreen', endFn); 20942 }); 20943 } 20944 20945 /** 20946 * Check if fullscreen is supported on the video el. 20947 * 20948 * @return {boolean} 20949 * - True if fullscreen is supported. 20950 * - False if fullscreen is not supported. 20951 */ 20952 supportsFullScreen() { 20953 return typeof this.el_.webkitEnterFullScreen === 'function'; 20954 } 20955 20956 /** 20957 * Request that the `HTML5` Tech enter fullscreen. 20958 */ 20959 enterFullScreen() { 20960 const video = this.el_; 20961 if (video.paused && video.networkState <= video.HAVE_METADATA) { 20962 // attempt to prime the video element for programmatic access 20963 // this isn't necessary on the desktop but shouldn't hurt 20964 silencePromise(this.el_.play()); 20965 20966 // playing and pausing synchronously during the transition to fullscreen 20967 // can get iOS ~6.1 devices into a play/pause loop 20968 this.setTimeout(function () { 20969 video.pause(); 20970 try { 20971 video.webkitEnterFullScreen(); 20972 } catch (e) { 20973 this.trigger('fullscreenerror', e); 20974 } 20975 }, 0); 20976 } else { 20977 try { 20978 video.webkitEnterFullScreen(); 20979 } catch (e) { 20980 this.trigger('fullscreenerror', e); 20981 } 20982 } 20983 } 20984 20985 /** 20986 * Request that the `HTML5` Tech exit fullscreen. 20987 */ 20988 exitFullScreen() { 20989 if (!this.el_.webkitDisplayingFullscreen) { 20990 this.trigger('fullscreenerror', new Error('The video is not fullscreen')); 20991 return; 20992 } 20993 this.el_.webkitExitFullScreen(); 20994 } 20995 20996 /** 20997 * Create a floating video window always on top of other windows so that users may 20998 * continue consuming media while they interact with other content sites, or 20999 * applications on their device. 21000 * 21001 * @see [Spec]{@link https://wicg.github.io/picture-in-picture} 21002 * 21003 * @return {Promise} 21004 * A promise with a Picture-in-Picture window. 21005 */ 21006 requestPictureInPicture() { 21007 return this.el_.requestPictureInPicture(); 21008 } 21009 21010 /** 21011 * Native requestVideoFrameCallback if supported by browser/tech, or fallback 21012 * Don't use rVCF on Safari when DRM is playing, as it doesn't fire 21013 * Needs to be checked later than the constructor 21014 * This will be a false positive for clear sources loaded after a Fairplay source 21015 * 21016 * @param {function} cb function to call 21017 * @return {number} id of request 21018 */ 21019 requestVideoFrameCallback(cb) { 21020 if (this.featuresVideoFrameCallback && !this.el_.webkitKeys) { 21021 return this.el_.requestVideoFrameCallback(cb); 21022 } 21023 return super.requestVideoFrameCallback(cb); 21024 } 21025 21026 /** 21027 * Native or fallback requestVideoFrameCallback 21028 * 21029 * @param {number} id request id to cancel 21030 */ 21031 cancelVideoFrameCallback(id) { 21032 if (this.featuresVideoFrameCallback && !this.el_.webkitKeys) { 21033 this.el_.cancelVideoFrameCallback(id); 21034 } else { 21035 super.cancelVideoFrameCallback(id); 21036 } 21037 } 21038 21039 /** 21040 * A getter/setter for the `Html5` Tech's source object. 21041 * > Note: Please use {@link Html5#setSource} 21042 * 21043 * @param {Tech~SourceObject} [src] 21044 * The source object you want to set on the `HTML5` techs element. 21045 * 21046 * @return {Tech~SourceObject|undefined} 21047 * - The current source object when a source is not passed in. 21048 * - undefined when setting 21049 * 21050 * @deprecated Since version 5. 21051 */ 21052 src(src) { 21053 if (src === undefined) { 21054 return this.el_.src; 21055 } 21056 21057 // Setting src through `src` instead of `setSrc` will be deprecated 21058 this.setSrc(src); 21059 } 21060 21061 /** 21062 * Add a <source> element to the <video> element. 21063 * 21064 * @param {string} srcUrl 21065 * The URL of the video source. 21066 * 21067 * @param {string} [mimeType] 21068 * The MIME type of the video source. Optional but recommended.
vendor: 10,748 bytes, lines 21069-21432
21069 * 21070 * @return {boolean} 21071 * Returns true if the source element was successfully added, false otherwise. 21072 */ 21073 addSourceElement(srcUrl, mimeType) { 21074 if (!srcUrl) { 21075 log$1.error('Invalid source URL.'); 21076 return false; 21077 } 21078 const sourceAttributes = { 21079 src: srcUrl, 21080 }; 21081 if (mimeType) { 21082 sourceAttributes.type = mimeType; 21083 } 21084 const sourceElement = createEl('source', {}, sourceAttributes); 21085 this.el_.appendChild(sourceElement); 21086 return true; 21087 } 21088 21089 /** 21090 * Remove a <source> element from the <video> element by its URL. 21091 * 21092 * @param {string} srcUrl 21093 * The URL of the source to remove. 21094 * 21095 * @return {boolean} 21096 * Returns true if the source element was successfully removed, false otherwise. 21097 */ 21098 removeSourceElement(srcUrl) { 21099 if (!srcUrl) { 21100 log$1.error('Source URL is required to remove the source element.'); 21101 return false; 21102 } 21103 const sourceElements = this.el_.querySelectorAll('source'); 21104 for (const sourceElement of sourceElements) { 21105 if (sourceElement.src === srcUrl) { 21106 this.el_.removeChild(sourceElement); 21107 return true; 21108 } 21109 } 21110 log$1.warn(`No matching source element found with src: ${srcUrl}`); 21111 return false; 21112 } 21113 21114 /** 21115 * Reset the tech by removing all sources and then calling 21116 * {@link Html5.resetMediaElement}. 21117 */ 21118 reset() { 21119 Html5.resetMediaElement(this.el_); 21120 } 21121 21122 /** 21123 * Get the current source on the `HTML5` Tech. Falls back to returning the source from 21124 * the HTML5 media element. 21125 * 21126 * @return {Tech~SourceObject} 21127 * The current source object from the HTML5 tech. With a fallback to the 21128 * elements source. 21129 */ 21130 currentSrc() { 21131 if (this.currentSource_) { 21132 return this.currentSource_.src; 21133 } 21134 return this.el_.currentSrc; 21135 } 21136 21137 /** 21138 * Set controls attribute for the HTML5 media Element. 21139 * 21140 * @param {string} val 21141 * Value to set the controls attribute to 21142 */ 21143 setControls(val) { 21144 this.el_.controls = !!val; 21145 } 21146 21147 /** 21148 * Create and returns a remote {@link TextTrack} object. 21149 * 21150 * @param {string} kind 21151 * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata) 21152 * 21153 * @param {string} [label] 21154 * Label to identify the text track 21155 * 21156 * @param {string} [language] 21157 * Two letter language abbreviation 21158 * 21159 * @return {TextTrack} 21160 * The TextTrack that gets created. 21161 */ 21162 addTextTrack(kind, label, language) { 21163 if (!this.featuresNativeTextTracks) { 21164 return super.addTextTrack(kind, label, language); 21165 } 21166 return this.el_.addTextTrack(kind, label, language); 21167 } 21168 21169 /** 21170 * Creates either native TextTrack or an emulated TextTrack depending 21171 * on the value of `featuresNativeTextTracks` 21172 * 21173 * @param {Object} options 21174 * The object should contain the options to initialize the TextTrack with. 21175 * 21176 * @param {string} [options.kind] 21177 * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata). 21178 * 21179 * @param {string} [options.label] 21180 * Label to identify the text track 21181 * 21182 * @param {string} [options.language] 21183 * Two letter language abbreviation. 21184 * 21185 * @param {boolean} [options.default] 21186 * Default this track to on. 21187 * 21188 * @param {string} [options.id] 21189 * The internal id to assign this track. 21190 * 21191 * @param {string} [options.src] 21192 * A source url for the track. 21193 * 21194 * @return {HTMLTrackElement} 21195 * The track element that gets created. 21196 */ 21197 createRemoteTextTrack(options) { 21198 if (!this.featuresNativeTextTracks) { 21199 return super.createRemoteTextTrack(options); 21200 } 21201 const htmlTrackElement = document__default['default'].createElement('track'); 21202 if (options.kind) { 21203 htmlTrackElement.kind = options.kind; 21204 } 21205 if (options.label) { 21206 htmlTrackElement.label = options.label; 21207 } 21208 if (options.language || options.srclang) { 21209 htmlTrackElement.srclang = options.language || options.srclang; 21210 } 21211 if (options.default) { 21212 htmlTrackElement.default = options.default; 21213 } 21214 if (options.id) { 21215 htmlTrackElement.id = options.id; 21216 } 21217 if (options.src) { 21218 htmlTrackElement.src = options.src; 21219 } 21220 return htmlTrackElement; 21221 } 21222 21223 /** 21224 * Creates a remote text track object and returns an html track element. 21225 * 21226 * @param {Object} options The object should contain values for 21227 * kind, language, label, and src (location of the WebVTT file) 21228 * @param {boolean} [manualCleanup=false] if set to true, the TextTrack 21229 * will not be removed from the TextTrackList and HtmlTrackElementList 21230 * after a source change 21231 * @return {HTMLTrackElement} An Html Track Element. 21232 * This can be an emulated {@link HTMLTrackElement} or a native one. 21233 * 21234 */ 21235 addRemoteTextTrack(options, manualCleanup) { 21236 const htmlTrackElement = super.addRemoteTextTrack(options, manualCleanup); 21237 if (this.featuresNativeTextTracks) { 21238 this.el().appendChild(htmlTrackElement); 21239 } 21240 return htmlTrackElement; 21241 } 21242 21243 /** 21244 * Remove remote `TextTrack` from `TextTrackList` object 21245 * 21246 * @param {TextTrack} track 21247 * `TextTrack` object to remove 21248 */ 21249 removeRemoteTextTrack(track) { 21250 super.removeRemoteTextTrack(track); 21251 if (this.featuresNativeTextTracks) { 21252 const tracks = this.$$('track'); 21253 let i = tracks.length; 21254 while (i--) { 21255 if (track === tracks[i] || track === tracks[i].track) { 21256 this.el().removeChild(tracks[i]); 21257 } 21258 } 21259 } 21260 } 21261 21262 /** 21263 * Gets available media playback quality metrics as specified by the W3C's Media 21264 * Playback Quality API. 21265 * 21266 * @see [Spec]{@link https://wicg.github.io/media-playback-quality} 21267 * 21268 * @return {Object} 21269 * An object with supported media playback quality metrics 21270 */ 21271 getVideoPlaybackQuality() { 21272 if (typeof this.el().getVideoPlaybackQuality === 'function') { 21273 return this.el().getVideoPlaybackQuality(); 21274 } 21275 const videoPlaybackQuality = {}; 21276 if ( 21277 typeof this.el().webkitDroppedFrameCount !== 'undefined' && 21278 typeof this.el().webkitDecodedFrameCount !== 'undefined' 21279 ) { 21280 videoPlaybackQuality.droppedVideoFrames = this.el().webkitDroppedFrameCount; 21281 videoPlaybackQuality.totalVideoFrames = this.el().webkitDecodedFrameCount; 21282 } 21283 if (window__default['default'].performance) { 21284 videoPlaybackQuality.creationTime = window__default['default'].performance.now(); 21285 } 21286 return videoPlaybackQuality; 21287 } 21288} 21289 21290/* HTML5 Support Testing ---------------------------------------------------- */ 21291 21292/** 21293 * Element for testing browser HTML5 media capabilities 21294 * 21295 * @type {Element} 21296 * @constant 21297 * @private 21298 */ 21299defineLazyProperty(Html5, 'TEST_VID', function () { 21300 if (!isReal()) { 21301 return; 21302 } 21303 const video = document__default['default'].createElement('video'); 21304 const track = document__default['default'].createElement('track'); 21305 track.kind = 'captions'; 21306 track.srclang = 'en'; 21307 track.label = 'English'; 21308 video.appendChild(track); 21309 return video; 21310}); 21311 21312/** 21313 * Check if HTML5 media is supported by this browser/device. 21314 * 21315 * @return {boolean} 21316 * - True if HTML5 media is supported. 21317 * - False if HTML5 media is not supported. 21318 */ 21319Html5.isSupported = function () { 21320 // IE with no Media Player is a LIAR! (#984) 21321 try { 21322 Html5.TEST_VID.volume = 0.5; 21323 } catch (e) { 21324 return false; 21325 } 21326 return !!(Html5.TEST_VID && Html5.TEST_VID.canPlayType); 21327}; 21328 21329/** 21330 * Check if the tech can support the given type 21331 * 21332 * @param {string} type 21333 * The mimetype to check 21334 * @return {string} 'probably', 'maybe', or '' (empty string) 21335 */ 21336Html5.canPlayType = function (type) { 21337 return Html5.TEST_VID.canPlayType(type); 21338}; 21339 21340/** 21341 * Check if the tech can support the given source 21342 * 21343 * @param {Object} srcObj 21344 * The source object 21345 * @param {Object} options 21346 * The options passed to the tech 21347 * @return {string} 'probably', 'maybe', or '' (empty string) 21348 */ 21349Html5.canPlaySource = function (srcObj, options) { 21350 return Html5.canPlayType(srcObj.type); 21351}; 21352 21353/** 21354 * Check if the volume can be changed in this browser/device. 21355 * Volume cannot be changed in a lot of mobile devices. 21356 * Specifically, it can't be changed from 1 on iOS. 21357 * 21358 * @return {boolean} 21359 * - True if volume can be controlled 21360 * - False otherwise 21361 */ 21362Html5.canControlVolume = function () { 21363 // IE will error if Windows Media Player not installed #3315 21364 try { 21365 const volume = Html5.TEST_VID.volume; 21366 Html5.TEST_VID.volume = volume / 2 + 0.1; 21367 const canControl = volume !== Html5.TEST_VID.volume; 21368 21369 // With the introduction of iOS 15, there are cases where the volume is read as 21370 // changed but reverts back to its original state at the start of the next tick. 21371 // To determine whether volume can be controlled on iOS, 21372 // a timeout is set and the volume is checked asynchronously. 21373 // Since `features` doesn't currently work asynchronously, the value is manually set. 21374 if (canControl && IS_IOS) { 21375 window__default['default'].setTimeout(() => { 21376 if (Html5 && Html5.prototype) { 21377 Html5.prototype.featuresVolumeControl = volume !== Html5.TEST_VID.volume; 21378 } 21379 }); 21380 21381 // default iOS to false, which will be updated in the timeout above. 21382 return false; 21383 } 21384 return canControl; 21385 } catch (e) { 21386 return false; 21387 } 21388}; 21389 21390/** 21391 * Check if the volume can be muted in this browser/device. 21392 * Some devices, e.g. iOS, don't allow changing volume 21393 * but permits muting/unmuting. 21394 * 21395 * @return {boolean} 21396 * - True if volume can be muted 21397 * - False otherwise 21398 */ 21399Html5.canMuteVolume = function () { 21400 try { 21401 const muted = Html5.TEST_VID.muted; 21402 21403 // in some versions of iOS muted property doesn't always 21404 // work, so we want to set both property and attribute 21405 Html5.TEST_VID.muted = !muted; 21406 if (Html5.TEST_VID.muted) { 21407 setAttribute(Html5.TEST_VID, 'muted', 'muted'); 21408 } else { 21409 removeAttribute(Html5.TEST_VID, 'muted', 'muted'); 21410 } 21411 return muted !== Html5.TEST_VID.muted; 21412 } catch (e) { 21413 return false; 21414 } 21415}; 21416 21417/** 21418 * Check if the playback rate can be changed in this browser/device. 21419 * 21420 * @return {boolean} 21421 * - True if playback rate can be controlled 21422 * - False otherwise 21423 */ 21424Html5.canControlPlaybackRate = function () { 21425 // Playback rate API is implemented in Android Chrome, but doesn't do anything 21426 // https://github.com/videojs/video.js/issues/3180 21427 if (IS_ANDROID && IS_CHROME && CHROME_VERSION < 58) { 21428 return false; 21429 } 21430 // IE will error if Windows Media Player not installed #3315 21431 try { 21432 const playbackRate = Html5.TEST_VID.playbackRate;
21433 Html5.TEST_VID.playbackRate = playbackRate / 2 + 0.1; 21434 return playbackRate !== Html5.TEST_VID.playbackRate; 21435 } catch (e) { 21436 return false; 21437 } 21438}; 21439 21440/** 21441 * Check if we can override a video/audio elements attributes, with 21442 * Object.defineProperty. 21443 * 21444 * @return {boolean} 21445 * - True if builtin attributes can be overridden 21446 * - False otherwise 21447 */ 21448Html5.canOverrideAttributes = function () { 21449 // if we cannot overwrite the src/innerHTML property, there is no support 21450 // iOS 7 safari for instance cannot do this. 21451 try { 21452 const noop = () => {}; 21453 Object.defineProperty(document__default['default'].createElement('video'), 'src', { 21454 get: noop, 21455 set: noop, 21456 }); 21457 Object.defineProperty(document__default['default'].createElement('audio'), 'src', { 21458 get: noop, 21459 set: noop, 21460 }); 21461 Object.defineProperty(document__default['default'].createElement('video'), 'innerHTML', { 21462 get: noop, 21463 set: noop, 21464 }); 21465 Object.defineProperty(document__default['default'].createElement('audio'), 'innerHTML', { 21466 get: noop, 21467 set: noop, 21468 }); 21469 } catch (e) { 21470 return false; 21471 } 21472 return true; 21473}; 21474 21475/** 21476 * Check to see if native `TextTrack`s are supported by this browser/device. 21477 * 21478 * @return {boolean} 21479 * - True if native `TextTrack`s are supported. 21480 * - False otherwise 21481 */ 21482Html5.supportsNativeTextTracks = function () { 21483 return IS_ANY_SAFARI || (IS_IOS && IS_CHROME); 21484}; 21485 21486/** 21487 * Check to see if native `VideoTrack`s are supported by this browser/device 21488 * 21489 * @return {boolean} 21490 * - True if native `VideoTrack`s are supported. 21491 * - False otherwise 21492 */ 21493Html5.supportsNativeVideoTracks = function () { 21494 return !!(Html5.TEST_VID && Html5.TEST_VID.videoTracks); 21495}; 21496 21497/** 21498 * Check to see if native `AudioTrack`s are supported by this browser/device 21499 * 21500 * @return {boolean} 21501 * - True if native `AudioTrack`s are supported. 21502 * - False otherwise 21503 */ 21504Html5.supportsNativeAudioTracks = function () { 21505 return !!(Html5.TEST_VID && Html5.TEST_VID.audioTracks); 21506}; 21507 21508/** 21509 * An array of events available on the Html5 tech. 21510 * 21511 * @private 21512 * @type {Array} 21513 */ 21514Html5.Events = [ 21515 'loadstart', 21516 'suspend', 21517 'abort', 21518 'error', 21519 'emptied', 21520 'stalled', 21521 'loadedmetadata', 21522 'loadeddata', 21523 'canplay', 21524 'canplaythrough', 21525 'playing', 21526 'waiting', 21527 'seeking', 21528 'seeked', 21529 'ended', 21530 'durationchange', 21531 'timeupdate', 21532 'progress', 21533 'play', 21534 'pause', 21535 'ratechange', 21536 'resize', 21537 'volumechange', 21538]; 21539 21540/** 21541 * Boolean indicating whether the `Tech` supports volume control. 21542 * 21543 * @type {boolean} 21544 * @default {@link Html5.canControlVolume} 21545 */ 21546/** 21547 * Boolean indicating whether the `Tech` supports muting volume. 21548 * 21549 * @type {boolean} 21550 * @default {@link Html5.canMuteVolume} 21551 */ 21552 21553/** 21554 * Boolean indicating whether the `Tech` supports changing the speed at which the media 21555 * plays. Examples: 21556 * - Set player to play 2x (twice) as fast 21557 * - Set player to play 0.5x (half) as fast 21558 * 21559 * @type {boolean} 21560 * @default {@link Html5.canControlPlaybackRate} 21561 */ 21562 21563/** 21564 * Boolean indicating whether the `Tech` supports the `sourceset` event. 21565 * 21566 * @type {boolean} 21567 * @default 21568 */ 21569/** 21570 * Boolean indicating whether the `HTML5` tech currently supports native `TextTrack`s. 21571 * 21572 * @type {boolean} 21573 * @default {@link Html5.supportsNativeTextTracks} 21574 */ 21575/** 21576 * Boolean indicating whether the `HTML5` tech currently supports native `VideoTrack`s. 21577 * 21578 * @type {boolean} 21579 * @default {@link Html5.supportsNativeVideoTracks} 21580 */ 21581/** 21582 * Boolean indicating whether the `HTML5` tech currently supports native `AudioTrack`s. 21583 * 21584 * @type {boolean} 21585 * @default {@link Html5.supportsNativeAudioTracks} 21586 */ 21587[ 21588 ['featuresMuteControl', 'canMuteVolume'], 21589 ['featuresPlaybackRate', 'canControlPlaybackRate'], 21590 ['featuresSourceset', 'canOverrideAttributes'], 21591 ['featuresNativeTextTracks', 'supportsNativeTextTracks'], 21592 ['featuresNativeVideoTracks', 'supportsNativeVideoTracks'], 21593 ['featuresNativeAudioTracks', 'supportsNativeAudioTracks'], 21594].forEach(function ([key, fn]) { 21595 defineLazyProperty(Html5.prototype, key, () => Html5[fn](), true); 21596}); 21597Html5.prototype.featuresVolumeControl = Html5.canControlVolume(); 21598 21599/** 21600 * Boolean indicating whether the `HTML5` tech currently supports the media element 21601 * moving in the DOM. iOS breaks if you move the media element, so this is set this to 21602 * false there. Everywhere else this should be true. 21603 * 21604 * @type {boolean} 21605 * @default 21606 */ 21607Html5.prototype.movingMediaElementInDOM = !IS_IOS; 21608 21609// TODO: Previous comment: No longer appears to be used. Can probably be removed. 21610// Is this true? 21611/** 21612 * Boolean indicating whether the `HTML5` tech currently supports automatic media resize 21613 * when going into fullscreen. 21614 * 21615 * @type {boolean} 21616 * @default 21617 */ 21618Html5.prototype.featuresFullscreenResize = true; 21619 21620/** 21621 * Boolean indicating whether the `HTML5` tech currently supports the progress event. 21622 * If this is false, manual `progress` events will be triggered instead. 21623 * 21624 * @type {boolean} 21625 * @default 21626 */ 21627Html5.prototype.featuresProgressEvents = true; 21628 21629/** 21630 * Boolean indicating whether the `HTML5` tech currently supports the timeupdate event. 21631 * If this is false, manual `timeupdate` events will be triggered instead. 21632 * 21633 * @default 21634 */ 21635Html5.prototype.featuresTimeupdateEvents = true; 21636 21637/** 21638 * Whether the HTML5 el supports `requestVideoFrameCallback` 21639 * 21640 * @type {boolean} 21641 */ 21642Html5.prototype.featuresVideoFrameCallback = !!(
vendor: 5,248 bytes, lines 21643-21788
21643 Html5.TEST_VID && Html5.TEST_VID.requestVideoFrameCallback 21644); 21645Html5.disposeMediaElement = function (el) { 21646 if (!el) { 21647 return; 21648 } 21649 if (el.parentNode) { 21650 el.parentNode.removeChild(el); 21651 } 21652 21653 // remove any child track or source nodes to prevent their loading 21654 while (el.hasChildNodes()) { 21655 el.removeChild(el.firstChild); 21656 } 21657 21658 // remove any src reference. not setting `src=''` because that causes a warning 21659 // in firefox 21660 el.removeAttribute('src'); 21661 21662 // force the media element to update its loading state by calling load() 21663 // however IE on Windows 7N has a bug that throws an error so need a try/catch (#793) 21664 if (typeof el.load === 'function') { 21665 // wrapping in an iife so it's not deoptimized (#1060#discussion_r10324473) 21666 (function () { 21667 try { 21668 el.load(); 21669 } catch (e) { 21670 // not supported 21671 } 21672 })(); 21673 } 21674}; 21675Html5.resetMediaElement = function (el) { 21676 if (!el) { 21677 return; 21678 } 21679 const sources = el.querySelectorAll('source'); 21680 let i = sources.length; 21681 while (i--) { 21682 el.removeChild(sources[i]); 21683 } 21684 21685 // remove any src reference. 21686 // not setting `src=''` because that throws an error 21687 el.removeAttribute('src'); 21688 if (typeof el.load === 'function') { 21689 // wrapping in an iife so it's not deoptimized (#1060#discussion_r10324473) 21690 (function () { 21691 try { 21692 el.load(); 21693 } catch (e) { 21694 // satisfy linter 21695 } 21696 })(); 21697 } 21698}; 21699 21700/* Native HTML5 element property wrapping ----------------------------------- */ 21701// Wrap native boolean attributes with getters that check both property and attribute 21702// The list is as followed: 21703// muted, defaultMuted, autoplay, controls, loop, playsinline 21704[ 21705 /** 21706 * Get the value of `muted` from the media element. `muted` indicates 21707 * that the volume for the media should be set to silent. This does not actually change 21708 * the `volume` attribute. 21709 * 21710 * @method Html5#muted 21711 * @return {boolean} 21712 * - True if the value of `volume` should be ignored and the audio set to silent. 21713 * - False if the value of `volume` should be used. 21714 * 21715 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-muted} 21716 */ 21717 'muted', 21718 /** 21719 * Get the value of `defaultMuted` from the media element. `defaultMuted` indicates 21720 * whether the media should start muted or not. Only changes the default state of the 21721 * media. `muted` and `defaultMuted` can have different values. {@link Html5#muted} indicates the 21722 * current state. 21723 * 21724 * @method Html5#defaultMuted 21725 * @return {boolean} 21726 * - The value of `defaultMuted` from the media element. 21727 * - True indicates that the media should start muted. 21728 * - False indicates that the media should not start muted 21729 * 21730 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultmuted} 21731 */ 21732 'defaultMuted', 21733 /** 21734 * Get the value of `autoplay` from the media element. `autoplay` indicates 21735 * that the media should start to play as soon as the page is ready. 21736 * 21737 * @method Html5#autoplay 21738 * @return {boolean} 21739 * - The value of `autoplay` from the media element. 21740 * - True indicates that the media should start as soon as the page loads. 21741 * - False indicates that the media should not start as soon as the page loads. 21742 * 21743 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-autoplay} 21744 */ 21745 'autoplay', 21746 /** 21747 * Get the value of `controls` from the media element. `controls` indicates 21748 * whether the native media controls should be shown or hidden. 21749 * 21750 * @method Html5#controls 21751 * @return {boolean} 21752 * - The value of `controls` from the media element. 21753 * - True indicates that native controls should be showing. 21754 * - False indicates that native controls should be hidden. 21755 * 21756 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-controls} 21757 */ 21758 'controls', 21759 /** 21760 * Get the value of `loop` from the media element. `loop` indicates 21761 * that the media should return to the start of the media and continue playing once 21762 * it reaches the end. 21763 * 21764 * @method Html5#loop 21765 * @return {boolean} 21766 * - The value of `loop` from the media element. 21767 * - True indicates that playback should seek back to start once 21768 * the end of a media is reached. 21769 * - False indicates that playback should not loop back to the start when the 21770 * end of the media is reached. 21771 * 21772 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-loop} 21773 */ 21774 'loop', 21775 /** 21776 * Get the value of `playsinline` from the media element. `playsinline` indicates 21777 * to the browser that non-fullscreen playback is preferred when fullscreen 21778 * playback is the native default, such as in iOS Safari. 21779 * 21780 * @method Html5#playsinline 21781 * @return {boolean} 21782 * - The value of `playsinline` from the media element. 21783 * - True indicates that the media should play inline. 21784 * - False indicates that the media should not play inline. 21785 * 21786 * @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline} 21787 */ 21788 'playsinline',
21789].forEach(function (prop) { 21790 Html5.prototype[prop] = function () { 21791 return this.el_[prop] || this.el_.hasAttribute(prop); 21792 }; 21793}); 21794 21795// Wrap native boolean attributes with setters that set both property and attribute 21796// The list is as followed: 21797// setMuted, setDefaultMuted, setAutoplay, setLoop, setPlaysinline 21798// setControls is special-cased above 21799[ 21800 /** 21801 * Set the value of `muted` on the media element. `muted` indicates that the current 21802 * audio level should be silent. 21803 * 21804 * @method Html5#setMuted 21805 * @param {boolean} muted 21806 * - True if the audio should be set to silent 21807 * - False otherwise 21808 * 21809 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-muted} 21810 */ 21811 'muted', 21812 /** 21813 * Set the value of `defaultMuted` on the media element. `defaultMuted` indicates that the current 21814 * audio level should be silent, but will only effect the muted level on initial playback.. 21815 * 21816 * @method Html5.prototype.setDefaultMuted 21817 * @param {boolean} defaultMuted 21818 * - True if the audio should be set to silent 21819 * - False otherwise 21820 * 21821 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultmuted} 21822 */ 21823 'defaultMuted', 21824 /** 21825 * Set the value of `autoplay` on the media element. `autoplay` indicates 21826 * that the media should start to play as soon as the page is ready. 21827 * 21828 * @method Html5#setAutoplay 21829 * @param {boolean} autoplay 21830 * - True indicates that the media should start as soon as the page loads. 21831 * - False indicates that the media should not start as soon as the page loads. 21832 * 21833 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-autoplay} 21834 */ 21835 'autoplay', 21836 /** 21837 * Set the value of `loop` on the media element. `loop` indicates 21838 * that the media should return to the start of the media and continue playing once 21839 * it reaches the end. 21840 * 21841 * @method Html5#setLoop 21842 * @param {boolean} loop 21843 * - True indicates that playback should seek back to start once 21844 * the end of a media is reached. 21845 * - False indicates that playback should not loop back to the start when the 21846 * end of the media is reached. 21847 * 21848 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-loop} 21849 */ 21850 'loop', 21851 /** 21852 * Set the value of `playsinline` from the media element. `playsinline` indicates 21853 * to the browser that non-fullscreen playback is preferred when fullscreen 21854 * playback is the native default, such as in iOS Safari. 21855 * 21856 * @method Html5#setPlaysinline 21857 * @param {boolean} playsinline 21858 * - True indicates that the media should play inline. 21859 * - False indicates that the media should not play inline. 21860 * 21861 * @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline} 21862 */ 21863 'playsinline', 21864].forEach(function (prop) { 21865 Html5.prototype['set' + toTitleCase$1(prop)] = function (v) { 21866 this.el_[prop] = v; 21867 if (v) { 21868 this.el_.setAttribute(prop, prop); 21869 } else { 21870 this.el_.removeAttribute(prop); 21871 } 21872 }; 21873}); 21874 21875// Wrap native properties with a getter 21876// The list is as followed 21877// paused, currentTime, buffered, volume, poster, preload, error, seeking 21878// seekable, ended, playbackRate, defaultPlaybackRate, disablePictureInPicture 21879// played, networkState, readyState, videoWidth, videoHeight, crossOrigin 21880[ 21881 /** 21882 * Get the value of `paused` from the media element. `paused` indicates whether the media element 21883 * is currently paused or not. 21884 * 21885 * @method Html5#paused 21886 * @return {boolean} 21887 * The value of `paused` from the media element. 21888 * 21889 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-paused} 21890 */ 21891 'paused', 21892 /** 21893 * Get the value of `currentTime` from the media element. `currentTime` indicates 21894 * the current second that the media is at in playback. 21895 * 21896 * @method Html5#currentTime 21897 * @return {number} 21898 * The value of `currentTime` from the media element. 21899 * 21900 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-currenttime} 21901 */ 21902 'currentTime', 21903 /** 21904 * Get the value of `buffered` from the media element. `buffered` is a `TimeRange` 21905 * object that represents the parts of the media that are already downloaded and 21906 * available for playback. 21907 * 21908 * @method Html5#buffered 21909 * @return {TimeRange} 21910 * The value of `buffered` from the media element. 21911 * 21912 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-buffered} 21913 */ 21914 'buffered', 21915 /** 21916 * Get the value of `volume` from the media element. `volume` indicates 21917 * the current playback volume of audio for a media. `volume` will be a value from 0 21918 * (silent) to 1 (loudest and default). 21919 * 21920 * @method Html5#volume 21921 * @return {number} 21922 * The value of `volume` from the media element. Value will be between 0-1. 21923 * 21924 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-a-volume} 21925 */ 21926 'volume', 21927 /** 21928 * Get the value of `poster` from the media element. `poster` indicates 21929 * that the url of an image file that can/will be shown when no media data is available. 21930 * 21931 * @method Html5#poster 21932 * @return {string} 21933 * The value of `poster` from the media element. Value will be a url to an 21934 * image. 21935 * 21936 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-video-poster} 21937 */ 21938 'poster', 21939 /** 21940 * Get the value of `preload` from the media element. `preload` indicates 21941 * what should download before the media is interacted with. It can have the following 21942 * values: 21943 * - none: nothing should be downloaded 21944 * - metadata: poster and the first few frames of the media may be downloaded to get 21945 * media dimensions and other metadata 21946 * - auto: allow the media and metadata for the media to be downloaded before 21947 * interaction 21948 * 21949 * @method Html5#preload 21950 * @return {string} 21951 * The value of `preload` from the media element. Will be 'none', 'metadata', 21952 * or 'auto'. 21953 * 21954 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-preload} 21955 */ 21956 'preload', 21957 /** 21958 * Get the value of the `error` from the media element. `error` indicates any 21959 * MediaError that may have occurred during playback. If error returns null there is no 21960 * current error. 21961 * 21962 * @method Html5#error 21963 * @return {MediaError|null} 21964 * The value of `error` from the media element. Will be `MediaError` if there 21965 * is a current error and null otherwise. 21966 * 21967 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-error} 21968 */ 21969 'error', 21970 /** 21971 * Get the value of `seeking` from the media element. `seeking` indicates whether the 21972 * media is currently seeking to a new position or not. 21973 * 21974 * @method Html5#seeking 21975 * @return {boolean} 21976 * - The value of `seeking` from the media element. 21977 * - True indicates that the media is currently seeking to a new position. 21978 * - False indicates that the media is not seeking to a new position at this time. 21979 * 21980 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-seeking} 21981 */ 21982 'seeking', 21983 /** 21984 * Get the value of `seekable` from the media element. `seekable` returns a 21985 * `TimeRange` object indicating ranges of time that can currently be `seeked` to. 21986 * 21987 * @method Html5#seekable 21988 * @return {TimeRange} 21989 * The value of `seekable` from the media element. A `TimeRange` object 21990 * indicating the current ranges of time that can be seeked to. 21991 * 21992 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-seekable} 21993 */ 21994 'seekable', 21995 /** 21996 * Get the value of `ended` from the media element. `ended` indicates whether 21997 * the media has reached the end or not. 21998 * 21999 * @method Html5#ended 22000 * @return {boolean} 22001 * - The value of `ended` from the media element. 22002 * - True indicates that the media has ended. 22003 * - False indicates that the media has not ended. 22004 * 22005 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-ended} 22006 */ 22007 'ended', 22008 /** 22009 * Get the value of `playbackRate` from the media element. `playbackRate` indicates
22010 * the rate at which the media is currently playing back. Examples: 22011 * - if playbackRate is set to 2, media will play twice as fast. 22012 * - if playbackRate is set to 0.5, media will play half as fast. 22013 * 22014 * @method Html5#playbackRate 22015 * @return {number} 22016 * The value of `playbackRate` from the media element. A number indicating 22017 * the current playback speed of the media, where 1 is normal speed. 22018 * 22019 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate} 22020 */ 22021 'playbackRate', 22022 /** 22023 * Get the value of `defaultPlaybackRate` from the media element. `defaultPlaybackRate` indicates 22024 * the rate at which the media is currently playing back. This value will not indicate the current 22025 * `playbackRate` after playback has started, use {@link Html5#playbackRate} for that. 22026 * 22027 * Examples: 22028 * - if defaultPlaybackRate is set to 2, media will play twice as fast. 22029 * - if defaultPlaybackRate is set to 0.5, media will play half as fast. 22030 * 22031 * @method Html5.prototype.defaultPlaybackRate 22032 * @return {number} 22033 * The value of `defaultPlaybackRate` from the media element. A number indicating 22034 * the current playback speed of the media, where 1 is normal speed. 22035 * 22036 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate} 22037 */ 22038 'defaultPlaybackRate', 22039 /** 22040 * Get the value of 'disablePictureInPicture' from the video element. 22041 * 22042 * @method Html5#disablePictureInPicture 22043 * @return {boolean} value 22044 * - The value of `disablePictureInPicture` from the video element. 22045 * - True indicates that the video can't be played in Picture-In-Picture mode 22046 * - False indicates that the video can be played in Picture-In-Picture mode 22047 * 22048 * @see [Spec]{@link https://w3c.github.io/picture-in-picture/#disable-pip} 22049 */ 22050 'disablePictureInPicture', 22051 /** 22052 * Get the value of `played` from the media element. `played` returns a `TimeRange` 22053 * object representing points in the media timeline that have been played. 22054 * 22055 * @method Html5#played 22056 * @return {TimeRange} 22057 * The value of `played` from the media element. A `TimeRange` object indicating 22058 * the ranges of time that have been played. 22059 * 22060 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-played} 22061 */ 22062 'played', 22063 /** 22064 * Get the value of `networkState` from the media element. `networkState` indicates 22065 * the current network state. It returns an enumeration from the following list: 22066 * - 0: NETWORK_EMPTY 22067 * - 1: NETWORK_IDLE 22068 * - 2: NETWORK_LOADING 22069 * - 3: NETWORK_NO_SOURCE 22070 * 22071 * @method Html5#networkState 22072 * @return {number} 22073 * The value of `networkState` from the media element. This will be a number 22074 * from the list in the description. 22075 * 22076 * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-networkstate} 22077 */ 22078 'networkState', 22079 /** 22080 * Get the value of `readyState` from the media element. `readyState` indicates 22081 * the current state of the media element. It returns an enumeration from the 22082 * following list: 22083 * - 0: HAVE_NOTHING 22084 * - 1: HAVE_METADATA 22085 * - 2: HAVE_CURRENT_DATA 22086 * - 3: HAVE_FUTURE_DATA 22087 * - 4: HAVE_ENOUGH_DATA 22088 * 22089 * @method Html5#readyState 22090 * @return {number} 22091 * The value of `readyState` from the media element. This will be a number 22092 * from the list in the description. 22093 * 22094 * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#ready-states} 22095 */ 22096 'readyState', 22097 /** 22098 * Get the value of `videoWidth` from the video element. `videoWidth` indicates 22099 * the current width of the video in css pixels. 22100 * 22101 * @method Html5#videoWidth 22102 * @return {number} 22103 * The value of `videoWidth` from the video element. This will be a number 22104 * in css pixels. 22105 * 22106 * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-video-videowidth} 22107 */ 22108 'videoWidth', 22109 /** 22110 * Get the value of `videoHeight` from the video element. `videoHeight` indicates 22111 * the current height of the video in css pixels. 22112 * 22113 * @method Html5#videoHeight 22114 * @return {number} 22115 * The value of `videoHeight` from the video element. This will be a number 22116 * in css pixels. 22117 * 22118 * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-video-videowidth} 22119 */ 22120 'videoHeight', 22121 /** 22122 * Get the value of `crossOrigin` from the media element. `crossOrigin` indicates 22123 * to the browser that should sent the cookies along with the requests for the 22124 * different assets/playlists 22125 * 22126 * @method Html5#crossOrigin 22127 * @return {string} 22128 * - anonymous indicates that the media should not sent cookies. 22129 * - use-credentials indicates that the media should sent cookies along the requests. 22130 * 22131 * @see [Spec]{@link https://html.spec.whatwg.org/#attr-media-crossorigin} 22132 */ 22133 'crossOrigin',
22134].forEach(function (prop) { 22135 Html5.prototype[prop] = function () { 22136 return this.el_[prop]; 22137 }; 22138}); 22139 22140// Wrap native properties with a setter in this format: 22141// set + toTitleCase(name) 22142// The list is as follows: 22143// setVolume, setSrc, setPoster, setPreload, setPlaybackRate, setDefaultPlaybackRate, 22144// setDisablePictureInPicture, setCrossOrigin 22145[ 22146 /** 22147 * Set the value of `volume` on the media element. `volume` indicates the current 22148 * audio level as a percentage in decimal form. This means that 1 is 100%, 0.5 is 50%, and 22149 * so on. 22150 * 22151 * @method Html5#setVolume 22152 * @param {number} percentAsDecimal 22153 * The volume percent as a decimal. Valid range is from 0-1. 22154 * 22155 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-a-volume} 22156 */ 22157 'volume', 22158 /** 22159 * Set the value of `src` on the media element. `src` indicates the current 22160 * {@link Tech~SourceObject} for the media. 22161 * 22162 * @method Html5#setSrc 22163 * @param {Tech~SourceObject} src 22164 * The source object to set as the current source. 22165 * 22166 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-src} 22167 */ 22168 'src', 22169 /** 22170 * Set the value of `poster` on the media element. `poster` is the url to 22171 * an image file that can/will be shown when no media data is available. 22172 * 22173 * @method Html5#setPoster 22174 * @param {string} poster 22175 * The url to an image that should be used as the `poster` for the media 22176 * element. 22177 * 22178 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-poster} 22179 */ 22180 'poster', 22181 /** 22182 * Set the value of `preload` on the media element. `preload` indicates 22183 * what should download before the media is interacted with. It can have the following 22184 * values: 22185 * - none: nothing should be downloaded 22186 * - metadata: poster and the first few frames of the media may be downloaded to get 22187 * media dimensions and other metadata 22188 * - auto: allow the media and metadata for the media to be downloaded before 22189 * interaction 22190 * 22191 * @method Html5#setPreload 22192 * @param {string} preload 22193 * The value of `preload` to set on the media element. Must be 'none', 'metadata', 22194 * or 'auto'. 22195 * 22196 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-preload} 22197 */ 22198 'preload', 22199 /** 22200 * Set the value of `playbackRate` on the media element. `playbackRate` indicates 22201 * the rate at which the media should play back. Examples: 22202 * - if playbackRate is set to 2, media will play twice as fast. 22203 * - if playbackRate is set to 0.5, media will play half as fast. 22204 * 22205 * @method Html5#setPlaybackRate 22206 * @return {number} 22207 * The value of `playbackRate` from the media element. A number indicating 22208 * the current playback speed of the media, where 1 is normal speed. 22209 * 22210 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate} 22211 */ 22212 'playbackRate', 22213 /** 22214 * Set the value of `defaultPlaybackRate` on the media element. `defaultPlaybackRate` indicates 22215 * the rate at which the media should play back upon initial startup. Changing this value 22216 * after a video has started will do nothing. Instead you should used {@link Html5#setPlaybackRate}. 22217 * 22218 * Example Values: 22219 * - if playbackRate is set to 2, media will play twice as fast. 22220 * - if playbackRate is set to 0.5, media will play half as fast. 22221 * 22222 * @method Html5.prototype.setDefaultPlaybackRate 22223 * @return {number} 22224 * The value of `defaultPlaybackRate` from the media element. A number indicating 22225 * the current playback speed of the media, where 1 is normal speed. 22226 * 22227 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultplaybackrate} 22228 */ 22229 'defaultPlaybackRate', 22230 /** 22231 * Prevents the browser from suggesting a Picture-in-Picture context menu 22232 * or to request Picture-in-Picture automatically in some cases. 22233 * 22234 * @method Html5#setDisablePictureInPicture 22235 * @param {boolean} value 22236 * The true value will disable Picture-in-Picture mode. 22237 * 22238 * @see [Spec]{@link https://w3c.github.io/picture-in-picture/#disable-pip} 22239 */ 22240 'disablePictureInPicture', 22241 /** 22242 * Set the value of `crossOrigin` from the media element. `crossOrigin` indicates 22243 * to the browser that should sent the cookies along with the requests for the 22244 * different assets/playlists 22245 * 22246 * @method Html5#setCrossOrigin 22247 * @param {string} crossOrigin 22248 * - anonymous indicates that the media should not sent cookies. 22249 * - use-credentials indicates that the media should sent cookies along the requests. 22250 * 22251 * @see [Spec]{@link https://html.spec.whatwg.org/#attr-media-crossorigin} 22252 */ 22253 'crossOrigin',
22254].forEach(function (prop) { 22255 Html5.prototype['set' + toTitleCase$1(prop)] = function (v) { 22256 this.el_[prop] = v; 22257 }; 22258}); 22259 22260// wrap native functions with a function 22261// The list is as follows: 22262// pause, load, play 22263[ 22264 /** 22265 * A wrapper around the media elements `pause` function. This will call the `HTML5` 22266 * media elements `pause` function. 22267 * 22268 * @method Html5#pause 22269 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-pause} 22270 */ 22271 'pause', 22272 /** 22273 * A wrapper around the media elements `load` function. This will call the `HTML5`s 22274 * media element `load` function. 22275 * 22276 * @method Html5#load 22277 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-load} 22278 */ 22279 'load', 22280 /** 22281 * A wrapper around the media elements `play` function. This will call the `HTML5`s 22282 * media element `play` function. 22283 * 22284 * @method Html5#play 22285 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-play} 22286 */ 22287 'play', 22288].forEach(function (prop) { 22289 Html5.prototype[prop] = function () { 22290 return this.el_[prop](); 22291 }; 22292}); 22293Tech.withSourceHandlers(Html5); 22294 22295/** 22296 * Native source handler for Html5, simply passes the source to the media element. 22297 * 22298 * @property {Tech~SourceObject} source 22299 * The source object 22300 * 22301 * @property {Html5} tech 22302 * The instance of the HTML5 tech. 22303 */ 22304Html5.nativeSourceHandler = {}; 22305 22306/** 22307 * Check if the media element can play the given mime type. 22308 * 22309 * @param {string} type 22310 * The mimetype to check 22311 * 22312 * @return {string} 22313 * 'probably', 'maybe', or '' (empty string) 22314 */ 22315Html5.nativeSourceHandler.canPlayType = function (type) { 22316 // IE without MediaPlayer throws an error (#519) 22317 try { 22318 return Html5.TEST_VID.canPlayType(type); 22319 } catch (e) { 22320 return ''; 22321 } 22322}; 22323 22324/** 22325 * Check if the media element can handle a source natively. 22326 * 22327 * @param {Tech~SourceObject} source 22328 * The source object 22329 * 22330 * @param {Object} [options] 22331 * Options to be passed to the tech. 22332 * 22333 * @return {string} 22334 * 'probably', 'maybe', or '' (empty string). 22335 */ 22336Html5.nativeSourceHandler.canHandleSource = function (source, options) { 22337 // If a type was provided we should rely on that 22338 if (source.type) { 22339 return Html5.nativeSourceHandler.canPlayType(source.type); 22340 22341 // If no type, fall back to checking 'video/[EXTENSION]' 22342 } else if (source.src) { 22343 const ext = getFileExtension(source.src); 22344 return Html5.nativeSourceHandler.canPlayType(`video/${ext}`); 22345 } 22346 return ''; 22347}; 22348 22349/** 22350 * Pass the source to the native media element. 22351 * 22352 * @param {Tech~SourceObject} source 22353 * The source object 22354 * 22355 * @param {Html5} tech 22356 * The instance of the Html5 tech 22357 * 22358 * @param {Object} [options] 22359 * The options to pass to the source 22360 */ 22361Html5.nativeSourceHandler.handleSource = function (source, tech, options) { 22362 tech.setSrc(source.src); 22363}; 22364 22365/** 22366 * A noop for the native dispose function, as cleanup is not needed. 22367 */ 22368Html5.nativeSourceHandler.dispose = function () {}; 22369 22370// Register the native source handler 22371Html5.registerSourceHandler(Html5.nativeSourceHandler); 22372Tech.registerTech('Html5', Html5); 22373 22374/** 22375 * @file player.js 22376 */ 22377 22378/** @import { TimeRange } from './utils/time' */ 22379/** @import HtmlTrackElement from './tracks/html-track-element' */ 22380 22381/** 22382 * @callback PlayerReadyCallback 22383 * @this {Player} 22384 * @returns {void} 22385 */ 22386 22387// The following tech events are simply re-triggered 22388// on the player when they happen 22389const TECH_EVENTS_RETRIGGER = [ 22390 /** 22391 * Fired while the user agent is downloading media data. 22392 * 22393 * @event Player#progress 22394 * @type {Event} 22395 */ 22396 /** 22397 * Retrigger the `progress` event that was triggered by the {@link Tech}. 22398 * 22399 * @private 22400 * @method Player#handleTechProgress_ 22401 * @fires Player#progress 22402 * @listens Tech#progress 22403 */ 22404 'progress', 22405 /** 22406 * Fires when the loading of an audio/video is aborted. 22407 * 22408 * @event Player#abort 22409 * @type {Event} 22410 */ 22411 /** 22412 * Retrigger the `abort` event that was triggered by the {@link Tech}. 22413 * 22414 * @private 22415 * @method Player#handleTechAbort_ 22416 * @fires Player#abort 22417 * @listens Tech#abort 22418 */ 22419 'abort', 22420 /** 22421 * Fires when the browser is intentionally not getting media data. 22422 * 22423 * @event Player#suspend 22424 * @type {Event} 22425 */ 22426 /** 22427 * Retrigger the `suspend` event that was triggered by the {@link Tech}. 22428 * 22429 * @private 22430 * @method Player#handleTechSuspend_ 22431 * @fires Player#suspend 22432 * @listens Tech#suspend 22433 */ 22434 'suspend', 22435 /** 22436 * Fires when the current playlist is empty. 22437 * 22438 * @event Player#emptied 22439 * @type {Event} 22440 */ 22441 /** 22442 * Retrigger the `emptied` event that was triggered by the {@link Tech}. 22443 * 22444 * @private 22445 * @method Player#handleTechEmptied_ 22446 * @fires Player#emptied 22447 * @listens Tech#emptied 22448 */ 22449 'emptied', 22450 /** 22451 * Fires when the browser is trying to get media data, but data is not available. 22452 * 22453 * @event Player#stalled 22454 * @type {Event} 22455 */ 22456 /** 22457 * Retrigger the `stalled` event that was triggered by the {@link Tech}. 22458 * 22459 * @private 22460 * @method Player#handleTechStalled_ 22461 * @fires Player#stalled 22462 * @listens Tech#stalled 22463 */ 22464 'stalled', 22465 /** 22466 * Fires when the browser has loaded meta data for the audio/video. 22467 * 22468 * @event Player#loadedmetadata 22469 * @type {Event} 22470 */ 22471 /** 22472 * Retrigger the `loadedmetadata` event that was triggered by the {@link Tech}. 22473 * 22474 * @private 22475 * @method Player#handleTechLoadedmetadata_ 22476 * @fires Player#loadedmetadata 22477 * @listens Tech#loadedmetadata 22478 */ 22479 'loadedmetadata', 22480 /** 22481 * Fires when the browser has loaded the current frame of the audio/video. 22482 * 22483 * @event Player#loadeddata 22484 * @type {event} 22485 */ 22486 /** 22487 * Retrigger the `loadeddata` event that was triggered by the {@link Tech}. 22488 * 22489 * @private 22490 * @method Player#handleTechLoaddeddata_ 22491 * @fires Player#loadeddata 22492 * @listens Tech#loadeddata 22493 */ 22494 'loadeddata', 22495 /** 22496 * Fires when the current playback position has changed. 22497 * 22498 * @event Player#timeupdate 22499 * @type {event} 22500 */ 22501 /** 22502 * Retrigger the `timeupdate` event that was triggered by the {@link Tech}. 22503 * 22504 * @private 22505 * @method Player#handleTechTimeUpdate_ 22506 * @fires Player#timeupdate 22507 * @listens Tech#timeupdate 22508 */ 22509 'timeupdate', 22510 /** 22511 * Fires when the video's intrinsic dimensions change 22512 * 22513 * @event Player#resize 22514 * @type {event} 22515 */ 22516 /** 22517 * Retrigger the `resize` event that was triggered by the {@link Tech}. 22518 * 22519 * @private 22520 * @method Player#handleTechResize_ 22521 * @fires Player#resize 22522 * @listens Tech#resize 22523 */ 22524 'resize', 22525 /** 22526 * Fires when the volume has been changed 22527 * 22528 * @event Player#volumechange 22529 * @type {event} 22530 */ 22531 /** 22532 * Retrigger the `volumechange` event that was triggered by the {@link Tech}. 22533 * 22534 * @private 22535 * @method Player#handleTechVolumechange_ 22536 * @fires Player#volumechange 22537 * @listens Tech#volumechange 22538 */ 22539 'volumechange', 22540 /** 22541 * Fires when the text track has been changed 22542 * 22543 * @event Player#texttrackchange 22544 * @type {event} 22545 */ 22546 /** 22547 * Retrigger the `texttrackchange` event that was triggered by the {@link Tech}. 22548 * 22549 * @private 22550 * @method Player#handleTechTexttrackchange_ 22551 * @fires Player#texttrackchange 22552 * @listens Tech#texttrackchange 22553 */ 22554 'texttrackchange', 22555]; 22556 22557// events to queue when playback rate is zero 22558// this is a hash for the sole purpose of mapping non-camel-cased event names 22559// to camel-cased function names 22560const TECH_EVENTS_QUEUE = { 22561 canplay: 'CanPlay', 22562 canplaythrough: 'CanPlayThrough', 22563 playing: 'Playing', 22564 seeked: 'Seeked', 22565}; 22566const BREAKPOINT_ORDER = ['tiny', 'xsmall', 'small', 'medium', 'large', 'xlarge', 'huge']; 22567const BREAKPOINT_CLASSES = {}; 22568 22569// grep: vjs-layout-tiny 22570// grep: vjs-layout-x-small 22571// grep: vjs-layout-small 22572// grep: vjs-layout-medium 22573// grep: vjs-layout-large 22574// grep: vjs-layout-x-large 22575// grep: vjs-layout-huge
22576BREAKPOINT_ORDER.forEach((k) => { 22577 const v = k.charAt(0) === 'x' ? `x-${k.substring(1)}` : k; 22578 BREAKPOINT_CLASSES[k] = `vjs-layout-${v}`; 22579}); 22580const DEFAULT_BREAKPOINTS = { 22581 tiny: 210, 22582 xsmall: 320, 22583 small: 425, 22584 medium: 768, 22585 large: 1440, 22586 xlarge: 2560, 22587 huge: Infinity, 22588}; 22589 22590/** 22591 * An instance of the `Player` class is created when any of the Video.js setup methods 22592 * are used to initialize a video. 22593 * 22594 * After an instance has been created it can be accessed globally in three ways: 22595 * 1. By calling `videojs.getPlayer('example_video_1');` 22596 * 2. By calling `videojs('example_video_1');` (not recommended) 22597 * 2. By using it directly via `videojs.players.example_video_1;` 22598 * 22599 * @extends Component 22600 * @global 22601 */ 22602class Player extends Component$1 { 22603 /** 22604 * Create an instance of this class. 22605 * 22606 * @param {Element} tag 22607 * The original video DOM element used for configuring options. 22608 * 22609 * @param {Object} [options] 22610 * Object of option names and values. 22611 * 22612 * @param {PlayerReadyCallback} [ready] 22613 * Ready callback function. 22614 */ 22615 constructor(tag, options, ready) { 22616 // Make sure tag ID exists 22617 // also here.. probably better 22618 tag.id = tag.id || options.id || `vjs_video_${newGUID()}`; 22619 22620 // Set Options 22621 // The options argument overrides options set in the video tag 22622 // which overrides globally set options. 22623 // This latter part coincides with the load order 22624 // (tag must exist before Player) 22625 options = Object.assign(Player.getTagSettings(tag), options); 22626 22627 // Delay the initialization of children because we need to set up 22628 // player properties first, and can't use `this` before `super()` 22629 options.initChildren = false; 22630 22631 // Same with creating the element 22632 options.createEl = false;
22633 22634 // don't auto mixin the evented mixin 22635 options.evented = false; 22636 22637 // we don't want the player to report touch activity on itself 22638 // see enableTouchActivity in Component 22639 options.reportTouchActivity = false; 22640 22641 // If language is not set, get the closest lang attribute 22642 if (!options.language) { 22643 const closest = tag.closest('[lang]'); 22644 if (closest) { 22645 options.language = closest.getAttribute('lang'); 22646 } 22647 } 22648 22649 // Run base component initializing with new options 22650 super(null, options, ready); 22651 22652 // Create bound methods for document listeners. 22653 this.boundDocumentFullscreenChange_ = (e) => this.documentFullscreenChange_(e); 22654 this.boundFullWindowOnEscKey_ = (e) => this.fullWindowOnEscKey(e); 22655 this.boundUpdateStyleEl_ = (e) => this.updateStyleEl_(e); 22656 this.boundApplyInitTime_ = (e) => this.applyInitTime_(e); 22657 this.boundUpdateCurrentBreakpoint_ = (e) => this.updateCurrentBreakpoint_(e); 22658 this.boundHandleTechClick_ = (e) => this.handleTechClick_(e); 22659 this.boundHandleTechDoubleClick_ = (e) => this.handleTechDoubleClick_(e); 22660 this.boundHandleTechTouchStart_ = (e) => this.handleTechTouchStart_(e); 22661 this.boundHandleTechTouchMove_ = (e) => this.handleTechTouchMove_(e); 22662 this.boundHandleTechTouchEnd_ = (e) => this.handleTechTouchEnd_(e); 22663 this.boundHandleTechTap_ = (e) => this.handleTechTap_(e); 22664 this.boundUpdatePlayerHeightOnAudioOnlyMode_ = (e) => 22665 this.updatePlayerHeightOnAudioOnlyMode_(e); 22666 22667 // default isFullscreen_ to false 22668 this.isFullscreen_ = false;
vendor: 9,467 bytes, lines 22669-22951
22669 22670 // create logger 22671 this.log = createLogger(this.id_); 22672 22673 // Hold our own reference to fullscreen api so it can be mocked in tests 22674 this.fsApi_ = FullscreenApi; 22675 22676 // Tracks when a tech changes the poster 22677 this.isPosterFromTech_ = false; 22678 22679 // Holds callback info that gets queued when playback rate is zero 22680 // and a seek is happening 22681 this.queuedCallbacks_ = []; 22682 22683 // Turn off API access because we're loading a new tech that might load asynchronously 22684 this.isReady_ = false; 22685 22686 // Init state hasStarted_ 22687 this.hasStarted_ = false; 22688 22689 // Init state userActive_ 22690 this.userActive_ = false; 22691 22692 // Init debugEnabled_ 22693 this.debugEnabled_ = false; 22694 22695 // Init state audioOnlyMode_ 22696 this.audioOnlyMode_ = false; 22697 22698 // Init state audioPosterMode_ 22699 this.audioPosterMode_ = false; 22700 22701 // Init state audioOnlyCache_ 22702 this.audioOnlyCache_ = { 22703 controlBarHeight: null, 22704 playerHeight: null, 22705 hiddenChildren: [], 22706 }; 22707 22708 // if the global option object was accidentally blown away by 22709 // someone, bail early with an informative error 22710 if (!this.options_ || !this.options_.techOrder || !this.options_.techOrder.length) { 22711 throw new Error( 22712 'No techOrder specified. Did you overwrite ' + 22713 'videojs.options instead of just changing the ' + 22714 'properties you want to override?' 22715 ); 22716 } 22717 22718 // Store the original tag used to set options 22719 this.tag = tag; 22720 22721 // Store the tag attributes used to restore html5 element 22722 this.tagAttributes = tag && getAttributes(tag); 22723 22724 // Update current language 22725 this.language(this.options_.language); 22726 22727 // Update Supported Languages 22728 if (options.languages) { 22729 // Normalise player option languages to lowercase 22730 const languagesToLower = {}; 22731 Object.getOwnPropertyNames(options.languages).forEach(function (name) { 22732 languagesToLower[name.toLowerCase()] = options.languages[name]; 22733 }); 22734 this.languages_ = languagesToLower; 22735 } else { 22736 this.languages_ = Player.prototype.options_.languages; 22737 } 22738 this.resetCache_(); 22739 22740 // Set poster 22741 /** @type string */ 22742 this.poster_ = options.poster || ''; 22743 22744 // Set controls 22745 /** @type {boolean} */ 22746 this.controls_ = !!options.controls; 22747 22748 // Original tag settings stored in options 22749 // now remove immediately so native controls don't flash. 22750 // May be turned back on by HTML5 tech if nativeControlsForTouch is true 22751 tag.controls = false; 22752 tag.removeAttribute('controls'); 22753 this.changingSrc_ = false; 22754 this.playCallbacks_ = []; 22755 this.playTerminatedQueue_ = []; 22756 22757 // the attribute overrides the option 22758 if (tag.hasAttribute('autoplay')) { 22759 this.autoplay(true); 22760 } else { 22761 // otherwise use the setter to validate and 22762 // set the correct value. 22763 this.autoplay(this.options_.autoplay); 22764 } 22765 22766 // check plugins 22767 if (options.plugins) { 22768 Object.keys(options.plugins).forEach((name) => { 22769 if (typeof this[name] !== 'function') { 22770 throw new Error(`plugin "${name}" does not exist`); 22771 } 22772 }); 22773 } 22774 22775 /* 22776 * Store the internal state of scrubbing 22777 * 22778 * @private 22779 * @return {Boolean} True if the user is scrubbing 22780 */ 22781 this.scrubbing_ = false; 22782 this.el_ = this.createEl(); 22783 22784 // Make this an evented object and use `el_` as its event bus. 22785 evented(this, { 22786 eventBusKey: 'el_', 22787 }); 22788 22789 // listen to document and player fullscreenchange handlers so we receive those events 22790 // before a user can receive them so we can update isFullscreen appropriately. 22791 // make sure that we listen to fullscreenchange events before everything else to make sure that 22792 // our isFullscreen method is updated properly for internal components as well as external. 22793 if (this.fsApi_.requestFullscreen) { 22794 on( 22795 document__default['default'], 22796 this.fsApi_.fullscreenchange, 22797 this.boundDocumentFullscreenChange_ 22798 ); 22799 this.on(this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_); 22800 } 22801 if (this.fluid_) { 22802 this.on(['playerreset', 'resize'], this.boundUpdateStyleEl_); 22803 } 22804 // We also want to pass the original player options to each component and plugin 22805 // as well so they don't need to reach back into the player for options later. 22806 // We also need to do another copy of this.options_ so we don't end up with 22807 // an infinite loop. 22808 const playerOptionsCopy = merge$1(this.options_); 22809 22810 // Load plugins 22811 if (options.plugins) { 22812 Object.keys(options.plugins).forEach((name) => { 22813 this[name](options.plugins[name]); 22814 }); 22815 } 22816 22817 // Enable debug mode to fire debugon event for all plugins. 22818 if (options.debug) { 22819 this.debug(true); 22820 } 22821 this.options_.playerOptions = playerOptionsCopy; 22822 this.middleware_ = []; 22823 this.playbackRates(options.playbackRates); 22824 if (options.experimentalSvgIcons) { 22825 // Add SVG Sprite to the DOM 22826 const parser = new window__default['default'].DOMParser(); 22827 const parsedSVG = parser.parseFromString(icons, 'image/svg+xml'); 22828 const errorNode = parsedSVG.querySelector('parsererror'); 22829 if (errorNode) { 22830 log$1.warn('Failed to load SVG Icons. Falling back to Font Icons.'); 22831 this.options_.experimentalSvgIcons = null; 22832 } else { 22833 const sprite = parsedSVG.documentElement; 22834 sprite.style.display = 'none'; 22835 this.el_.appendChild(sprite); 22836 this.addClass('vjs-svg-icons-enabled'); 22837 } 22838 } 22839 this.initChildren(); 22840 22841 // Set isAudio based on whether or not an audio tag was used 22842 this.isAudio(tag.nodeName.toLowerCase() === 'audio'); 22843 22844 // Update controls className. Can't do this when the controls are initially 22845 // set because the element doesn't exist yet. 22846 if (this.controls()) { 22847 this.addClass('vjs-controls-enabled'); 22848 } else { 22849 this.addClass('vjs-controls-disabled'); 22850 } 22851 22852 // Set ARIA label and region role depending on player type 22853 this.el_.setAttribute('role', 'region'); 22854 if (this.isAudio()) { 22855 this.el_.setAttribute('aria-label', this.localize('Audio Player')); 22856 } else { 22857 this.el_.setAttribute('aria-label', this.localize('Video Player')); 22858 } 22859 if (this.isAudio()) { 22860 this.addClass('vjs-audio'); 22861 } 22862 22863 // Check if spatial navigation is enabled in the options. 22864 // If enabled, instantiate the SpatialNavigation class. 22865 if (options.spatialNavigation && options.spatialNavigation.enabled) { 22866 this.spatialNavigation = new SpatialNavigation(this); 22867 this.addClass('vjs-spatial-navigation-enabled'); 22868 } 22869 22870 // TODO: Make this smarter. Toggle user state between touching/mousing 22871 // using events, since devices can have both touch and mouse events. 22872 // TODO: Make this check be performed again when the window switches between monitors 22873 // (See https://github.com/videojs/video.js/issues/5683) 22874 if (TOUCH_ENABLED) { 22875 this.addClass('vjs-touch-enabled'); 22876 } 22877 22878 // iOS Safari has broken hover handling 22879 if (!IS_IOS) { 22880 this.addClass('vjs-workinghover'); 22881 } 22882 22883 // Make player easily findable by ID 22884 Player.players[this.id_] = this; 22885 22886 // Add a major version class to aid css in plugins 22887 const majorVersion = version$6.split('.')[0]; 22888 this.addClass(`vjs-v${majorVersion}`); 22889 22890 // When the player is first initialized, trigger activity so components 22891 // like the control bar show themselves if needed 22892 this.userActive(true); 22893 this.reportUserActivity(); 22894 this.one('play', (e) => this.listenForUserActivity_(e)); 22895 this.on('keydown', (e) => this.handleKeyDown(e)); 22896 this.on('languagechange', (e) => this.handleLanguagechange(e)); 22897 this.breakpoints(this.options_.breakpoints); 22898 this.responsive(this.options_.responsive); 22899 22900 // Calling both the audio mode methods after the player is fully 22901 // setup to be able to listen to the events triggered by them 22902 this.on('ready', () => { 22903 // Calling the audioPosterMode method first so that 22904 // the audioOnlyMode can take precedence when both options are set to true 22905 this.audioPosterMode(this.options_.audioPosterMode); 22906 this.audioOnlyMode(this.options_.audioOnlyMode); 22907 }); 22908 } 22909 22910 /** 22911 * Destroys the video player and does any necessary cleanup. 22912 * 22913 * This is especially helpful if you are dynamically adding and removing videos 22914 * to/from the DOM. 22915 * 22916 * @fires Player#dispose 22917 */ 22918 dispose() { 22919 /** 22920 * Called when the player is being disposed of. 22921 * 22922 * @event Player#dispose 22923 * @type {Event} 22924 */ 22925 this.trigger('dispose'); 22926 // prevent dispose from being called twice 22927 this.off('dispose'); 22928 22929 // Make sure all player-specific document listeners are unbound. This is 22930 off( 22931 document__default['default'], 22932 this.fsApi_.fullscreenchange, 22933 this.boundDocumentFullscreenChange_ 22934 ); 22935 off(document__default['default'], 'keydown', this.boundFullWindowOnEscKey_); 22936 if (this.styleEl_ && this.styleEl_.parentNode) { 22937 this.styleEl_.parentNode.removeChild(this.styleEl_); 22938 this.styleEl_ = null; 22939 } 22940 22941 // Kill reference to this player 22942 Player.players[this.id_] = null; 22943 if (this.tag && this.tag.player) { 22944 this.tag.player = null; 22945 } 22946 if (this.el_ && this.el_.player) { 22947 this.el_.player = null; 22948 } 22949 if (this.tech_) { 22950 this.tech_.dispose(); 22951 this.isPosterFromTech_ = false;
22952 this.poster_ = ''; 22953 } 22954 if (this.playerElIngest_) { 22955 this.playerElIngest_ = null; 22956 } 22957 if (this.tag) { 22958 this.tag = null; 22959 } 22960 clearCacheForPlayer(this); 22961 22962 // remove all event handlers for track lists 22963 // all tracks and track listeners are removed on 22964 // tech dispose 22965 ALL.names.forEach((name) => { 22966 const props = ALL[name]; 22967 const list = this[props.getterName](); 22968 22969 // if it is not a native list 22970 // we have to manually remove event listeners 22971 if (list && list.off) { 22972 list.off(); 22973 } 22974 }); 22975 22976 // the actual .el_ is removed here, or replaced if 22977 super.dispose({ 22978 restoreEl: this.options_.restoreEl, 22979 }); 22980 } 22981 22982 /** 22983 * Create the `Player`'s DOM element. 22984 * 22985 * @return {Element} 22986 * The DOM element that gets created. 22987 */ 22988 createEl() { 22989 let tag = this.tag; 22990 let el; 22991 let playerElIngest = (this.playerElIngest_ = 22992 tag.parentNode && 22993 tag.parentNode.hasAttribute && 22994 tag.parentNode.hasAttribute('data-vjs-player')); 22995 const divEmbed = this.tag.tagName.toLowerCase() === 'video-js'; 22996 if (playerElIngest) { 22997 el = this.el_ = tag.parentNode; 22998 } else if (!divEmbed) { 22999 el = this.el_ = super.createEl('div'); 23000 } 23001 23002 // Copy over all the attributes from the tag, including ID and class 23003 // ID will now reference player box, not the video tag 23004 const attrs = getAttributes(tag); 23005 if (divEmbed) { 23006 el = this.el_ = tag; 23007 tag = this.tag = document__default['default'].createElement('video'); 23008 while (el.children.length) { 23009 tag.appendChild(el.firstChild); 23010 } 23011 if (!hasClass(el, 'video-js')) { 23012 addClass(el, 'video-js'); 23013 } 23014 el.appendChild(tag); 23015 playerElIngest = this.playerElIngest_ = el; 23016 // move properties over from our custom `video-js` element 23017 // to our new `video` element. This will move things like 23018 // `src` or `controls` that were set via js before the player 23019 // was initialized. 23020 Object.keys(el).forEach((k) => { 23021 try { 23022 tag[k] = el[k]; 23023 } catch (e) { 23024 // we got a a property like outerHTML which we can't actually copy, ignore it 23025 } 23026 }); 23027 } 23028 23029 // set tabindex to -1 to remove the video element from the focus order 23030 tag.setAttribute('tabindex', '-1'); 23031 attrs.tabindex = '-1'; 23032 23033 // Workaround for #4583 on Chrome (on Windows) with JAWS. 23034 // See https://github.com/FreedomScientific/VFO-standards-support/issues/78 23035 // Note that we can't detect if JAWS is being used, but this ARIA attribute 23036 // doesn't change behavior of Chrome if JAWS is not being used 23037 if (IS_CHROME && IS_WINDOWS) { 23038 tag.setAttribute('role', 'application'); 23039 attrs.role = 'application'; 23040 } 23041 23042 // Remove width/height attrs from tag so CSS can make it 100% width/height 23043 tag.removeAttribute('width'); 23044 tag.removeAttribute('height'); 23045 if ('width' in attrs) { 23046 delete attrs.width; 23047 } 23048 if ('height' in attrs) { 23049 delete attrs.height; 23050 } 23051 Object.getOwnPropertyNames(attrs).forEach(function (attr) { 23052 // don't copy over the class attribute to the player element when we're in a div embed 23053 // the class is already set up properly in the divEmbed case 23054 // and we want to make sure that the `video-js` class doesn't get lost 23055 if (!(divEmbed && attr === 'class')) { 23056 el.setAttribute(attr, attrs[attr]); 23057 } 23058 if (divEmbed) { 23059 tag.setAttribute(attr, attrs[attr]); 23060 } 23061 }); 23062 23063 // Update tag id/class for use as HTML5 playback tech 23064 // Might think we should do this after embedding in container so .vjs-tech class 23065 // doesn't flash 100% width/height, but class only applies with .video-js parent 23066 tag.playerId = tag.id; 23067 tag.id += '_html5_api'; 23068 tag.className = 'vjs-tech'; 23069 23070 // Make player findable on elements 23071 tag.player = el.player = this; 23072 // Default state of video is paused 23073 this.addClass('vjs-paused'); 23074 const deviceClassNames = [ 23075 'IS_SMART_TV', 23076 'IS_TIZEN', 23077 'IS_WEBOS', 23078 'IS_ANDROID', 23079 'IS_IPAD', 23080 'IS_IPHONE', 23081 'IS_CHROMECAST_RECEIVER', 23082 ] 23083 .filter((key) => browser[key]) 23084 .map((key) => { 23085 return 'vjs-device-' + key.substring(3).toLowerCase().replace(/\_/g, '-'); 23086 }); 23087 this.addClass(...deviceClassNames); 23088 23089 // Add a style element in the player that we'll use to set the width/height 23090 // of the player in a way that's still overridable by CSS, just like the 23091 // video element 23092 if (window__default['default'].VIDEOJS_NO_DYNAMIC_STYLE !== true) { 23093 this.styleEl_ = createStyleElement('vjs-styles-dimensions'); 23094 const defaultsStyleEl = $('.vjs-styles-defaults'); 23095 const head = $('head'); 23096 head.insertBefore( 23097 this.styleEl_, 23098 defaultsStyleEl ? defaultsStyleEl.nextSibling : head.firstChild 23099 ); 23100 } 23101 this.fill_ = false;
vendor: 8,027 bytes, lines 23102-23357
23102 this.fluid_ = false; 23103 23104 // Pass in the width/height/aspectRatio options which will update the style el 23105 this.width(this.options_.width); 23106 this.height(this.options_.height); 23107 this.fill(this.options_.fill); 23108 this.fluid(this.options_.fluid); 23109 this.aspectRatio(this.options_.aspectRatio); 23110 // support both crossOrigin and crossorigin to reduce confusion and issues around the name 23111 this.crossOrigin(this.options_.crossOrigin || this.options_.crossorigin); 23112 23113 // Hide any links within the video/audio tag, 23114 // because IE doesn't hide them completely from screen readers. 23115 const links = tag.getElementsByTagName('a'); 23116 for (let i = 0; i < links.length; i++) { 23117 const linkEl = links.item(i); 23118 addClass(linkEl, 'vjs-hidden'); 23119 linkEl.setAttribute('hidden', 'hidden'); 23120 } 23121 23122 // insertElFirst seems to cause the networkState to flicker from 3 to 2, so 23123 // keep track of the original for later so we can know if the source originally failed 23124 tag.initNetworkState_ = tag.networkState; 23125 23126 // Wrap video tag in div (el/box) container 23127 if (tag.parentNode && !playerElIngest) { 23128 tag.parentNode.insertBefore(el, tag); 23129 } 23130 23131 // insert the tag as the first child of the player element 23132 // then manually add it to the children array so that this.addChild 23133 // will work properly for other components 23134 // 23135 // Breaks iPhone, fixed in HTML5 setup. 23136 prependTo(tag, el); 23137 this.children_.unshift(tag); 23138 23139 // Set lang attr on player to ensure CSS :lang() in consistent with player 23140 // if it's been set to something different to the doc 23141 this.el_.setAttribute('lang', this.language_); 23142 this.el_.setAttribute('translate', 'no'); 23143 this.el_ = el; 23144 return el; 23145 } 23146 23147 /** 23148 * Get or set the `Player`'s crossOrigin option. For the HTML5 player, this 23149 * sets the `crossOrigin` property on the `<video>` tag to control the CORS 23150 * behavior. 23151 * 23152 * @see [Video Element Attributes]{@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#attr-crossorigin} 23153 * 23154 * @param {string|null} [value] 23155 * The value to set the `Player`'s crossOrigin to. If an argument is 23156 * given, must be one of `'anonymous'` or `'use-credentials'`, or 'null'. 23157 * 23158 * @return {string|null|undefined} 23159 * - The current crossOrigin value of the `Player` when getting. 23160 * - undefined when setting 23161 */ 23162 crossOrigin(value) { 23163 // `null` can be set to unset a value 23164 if (typeof value === 'undefined') { 23165 return this.techGet_('crossOrigin'); 23166 } 23167 if (value !== null && value !== 'anonymous' && value !== 'use-credentials') { 23168 log$1.warn(`crossOrigin must be null, "anonymous" or "use-credentials", given "${value}"`); 23169 return; 23170 } 23171 this.techCall_('setCrossOrigin', value); 23172 if (this.posterImage) { 23173 this.posterImage.crossOrigin(value); 23174 } 23175 return; 23176 } 23177 23178 /** 23179 * A getter/setter for the `Player`'s width. Returns the player's configured value. 23180 * To get the current width use `currentWidth()`. 23181 * 23182 * @param {number|string} [value] 23183 * CSS value to set the `Player`'s width to. 23184 * 23185 * @return {number|undefined} 23186 * - The current width of the `Player` when getting. 23187 * - Nothing when setting 23188 */ 23189 width(value) { 23190 return this.dimension('width', value); 23191 } 23192 23193 /** 23194 * A getter/setter for the `Player`'s height. Returns the player's configured value. 23195 * To get the current height use `currentheight()`. 23196 * 23197 * @param {number|string} [value] 23198 * CSS value to set the `Player`'s height to. 23199 * 23200 * @return {number|undefined} 23201 * - The current height of the `Player` when getting. 23202 * - Nothing when setting 23203 */ 23204 height(value) { 23205 return this.dimension('height', value); 23206 } 23207 23208 /** 23209 * A getter/setter for the `Player`'s width & height. 23210 * 23211 * @param {string} dimension 23212 * This string can be: 23213 * - 'width' 23214 * - 'height' 23215 * 23216 * @param {number|string} [value] 23217 * Value for dimension specified in the first argument. 23218 * 23219 * @return {number} 23220 * The dimension arguments value when getting (width/height). 23221 */ 23222 dimension(dimension, value) { 23223 const privDimension = dimension + '_'; 23224 if (value === undefined) { 23225 return this[privDimension] || 0; 23226 } 23227 if (value === '' || value === 'auto') { 23228 // If an empty string is given, reset the dimension to be automatic 23229 this[privDimension] = undefined; 23230 this.updateStyleEl_(); 23231 return; 23232 } 23233 const parsedVal = parseFloat(value); 23234 if (isNaN(parsedVal)) { 23235 log$1.error(`Improper value "${value}" supplied for for ${dimension}`); 23236 return; 23237 } 23238 this[privDimension] = parsedVal; 23239 this.updateStyleEl_(); 23240 } 23241 23242 /** 23243 * A getter/setter/toggler for the vjs-fluid `className` on the `Player`. 23244 * 23245 * Turning this on will turn off fill mode. 23246 * 23247 * @param {boolean} [bool] 23248 * - A value of true adds the class. 23249 * - A value of false removes the class. 23250 * - No value will be a getter. 23251 * 23252 * @return {boolean|undefined} 23253 * - The value of fluid when getting. 23254 * - `undefined` when setting. 23255 */ 23256 fluid(bool) { 23257 if (bool === undefined) { 23258 return !!this.fluid_; 23259 } 23260 this.fluid_ = !!bool; 23261 if (isEvented(this)) { 23262 this.off(['playerreset', 'resize'], this.boundUpdateStyleEl_); 23263 } 23264 if (bool) { 23265 this.addClass('vjs-fluid'); 23266 this.fill(false); 23267 addEventedCallback(this, () => { 23268 this.on(['playerreset', 'resize'], this.boundUpdateStyleEl_); 23269 }); 23270 } else { 23271 this.removeClass('vjs-fluid'); 23272 } 23273 this.updateStyleEl_(); 23274 } 23275 23276 /** 23277 * A getter/setter/toggler for the vjs-fill `className` on the `Player`. 23278 * 23279 * Turning this on will turn off fluid mode. 23280 * 23281 * @param {boolean} [bool] 23282 * - A value of true adds the class. 23283 * - A value of false removes the class. 23284 * - No value will be a getter. 23285 * 23286 * @return {boolean|undefined} 23287 * - The value of fluid when getting. 23288 * - `undefined` when setting. 23289 */ 23290 fill(bool) { 23291 if (bool === undefined) { 23292 return !!this.fill_; 23293 } 23294 this.fill_ = !!bool; 23295 if (bool) { 23296 this.addClass('vjs-fill'); 23297 this.fluid(false); 23298 } else { 23299 this.removeClass('vjs-fill'); 23300 } 23301 } 23302 23303 /** 23304 * Get/Set the aspect ratio 23305 * 23306 * @param {string} [ratio] 23307 * Aspect ratio for player 23308 * 23309 * @return {string|undefined} 23310 * returns the current aspect ratio when getting 23311 */ 23312 23313 /** 23314 * A getter/setter for the `Player`'s aspect ratio. 23315 * 23316 * @param {string} [ratio] 23317 * The value to set the `Player`'s aspect ratio to. 23318 * 23319 * @return {string|undefined} 23320 * - The current aspect ratio of the `Player` when getting. 23321 * - undefined when setting 23322 */ 23323 aspectRatio(ratio) { 23324 if (ratio === undefined) { 23325 return this.aspectRatio_; 23326 } 23327 23328 // Check for width:height format 23329 if (!/^\d+\:\d+$/.test(ratio)) { 23330 throw new Error( 23331 'Improper value supplied for aspect ratio. The format should be width:height, for example 16:9.' 23332 ); 23333 } 23334 this.aspectRatio_ = ratio; 23335 23336 // We're assuming if you set an aspect ratio you want fluid mode, 23337 // because in fixed mode you could calculate width and height yourself. 23338 this.fluid(true); 23339 this.updateStyleEl_(); 23340 } 23341 23342 /** 23343 * Update styles of the `Player` element (height, width and aspect ratio). 23344 * 23345 * @private 23346 * @listens Tech#loadedmetadata 23347 */ 23348 updateStyleEl_() { 23349 if (window__default['default'].VIDEOJS_NO_DYNAMIC_STYLE === true) { 23350 const width = typeof this.width_ === 'number' ? this.width_ : this.options_.width; 23351 const height = typeof this.height_ === 'number' ? this.height_ : this.options_.height; 23352 const techEl = this.tech_ && this.tech_.el(); 23353 if (techEl) { 23354 if (width >= 0) { 23355 techEl.width = width; 23356 } 23357 if (height >= 0) {
vendor: 4,279 bytes, lines 23358-23484
23358 techEl.height = height; 23359 } 23360 } 23361 return; 23362 } 23363 let width; 23364 let height; 23365 let aspectRatio; 23366 let idClass; 23367 23368 // The aspect ratio is either used directly or to calculate width and height. 23369 if (this.aspectRatio_ !== undefined && this.aspectRatio_ !== 'auto') { 23370 // Use any aspectRatio that's been specifically set 23371 aspectRatio = this.aspectRatio_; 23372 } else if (this.videoWidth() > 0) { 23373 // Otherwise try to get the aspect ratio from the video metadata 23374 aspectRatio = this.videoWidth() + ':' + this.videoHeight(); 23375 } else { 23376 // Or use a default. The video element's is 2:1, but 16:9 is more common. 23377 aspectRatio = '16:9'; 23378 } 23379 23380 // Get the ratio as a decimal we can use to calculate dimensions 23381 const ratioParts = aspectRatio.split(':'); 23382 const ratioMultiplier = ratioParts[1] / ratioParts[0]; 23383 if (this.width_ !== undefined) { 23384 // Use any width that's been specifically set 23385 width = this.width_; 23386 } else if (this.height_ !== undefined) { 23387 // Or calculate the width from the aspect ratio if a height has been set 23388 width = this.height_ / ratioMultiplier; 23389 } else { 23390 // Or use the video's metadata, or use the video el's default of 300 23391 width = this.videoWidth() || 300; 23392 } 23393 if (this.height_ !== undefined) { 23394 // Use any height that's been specifically set 23395 height = this.height_; 23396 } else { 23397 // Otherwise calculate the height from the ratio and the width 23398 height = width * ratioMultiplier; 23399 } 23400 23401 // Ensure the CSS class is valid by starting with an alpha character 23402 if (/^[^a-zA-Z]/.test(this.id())) { 23403 idClass = 'dimensions-' + this.id(); 23404 } else { 23405 idClass = this.id() + '-dimensions'; 23406 } 23407 23408 // Ensure the right class is still on the player for the style element 23409 this.addClass(idClass); 23410 setTextContent( 23411 this.styleEl_, 23412 ` 23413 .${idClass} { 23414 width: ${width}px; 23415 height: ${height}px; 23416 } 23417 23418 .${idClass}.vjs-fluid:not(.vjs-audio-only-mode) { 23419 padding-top: ${ratioMultiplier * 100}%; 23420 } 23421 ` 23422 ); 23423 } 23424 23425 /** 23426 * Load/Create an instance of playback {@link Tech} including element 23427 * and API methods. Then append the `Tech` element in `Player` as a child. 23428 * 23429 * @param {string} techName 23430 * name of the playback technology 23431 * 23432 * @param {string} source 23433 * video source 23434 * 23435 * @private 23436 */ 23437 loadTech_(techName, source) { 23438 // Pause and remove current playback technology 23439 if (this.tech_) { 23440 this.unloadTech_(); 23441 } 23442 const titleTechName = toTitleCase$1(techName); 23443 const camelTechName = techName.charAt(0).toLowerCase() + techName.slice(1); 23444 23445 // get rid of the HTML5 video tag as soon as we are using another tech 23446 if (titleTechName !== 'Html5' && this.tag) { 23447 Tech.getTech('Html5').disposeMediaElement(this.tag); 23448 this.tag.player = null; 23449 this.tag = null; 23450 } 23451 this.techName_ = titleTechName; 23452 23453 // Turn off API access because we're loading a new tech that might load asynchronously 23454 this.isReady_ = false; 23455 let autoplay = this.autoplay(); 23456 23457 // if autoplay is a string (or `true` with normalizeAutoplay: true) we pass false to the tech 23458 // because the player is going to handle autoplay on `loadstart` 23459 if ( 23460 typeof this.autoplay() === 'string' || 23461 (this.autoplay() === true && this.options_.normalizeAutoplay) 23462 ) { 23463 autoplay = false; 23464 } 23465 23466 // Grab tech-specific options from player options and add source and parent element to use. 23467 const techOptions = { 23468 source, 23469 autoplay, 23470 nativeControlsForTouch: this.options_.nativeControlsForTouch, 23471 playerId: this.id(), 23472 techId: `${this.id()}_${camelTechName}_api`, 23473 playsinline: this.options_.playsinline, 23474 preload: this.options_.preload, 23475 loop: this.options_.loop, 23476 disablePictureInPicture: this.options_.disablePictureInPicture, 23477 muted: this.options_.muted, 23478 poster: this.poster(), 23479 language: this.language(), 23480 playerElIngest: this.playerElIngest_ || false, 23481 'vtt.js': this.options_['vtt.js'], 23482 canOverridePoster: !!this.options_.techCanOverridePoster, 23483 enableSourceset: this.options_.enableSourceset, 23484 };
23485 ALL.names.forEach((name) => { 23486 const props = ALL[name]; 23487 techOptions[props.getterName] = this[props.privateName]; 23488 }); 23489 Object.assign(techOptions, this.options_[titleTechName]); 23490 Object.assign(techOptions, this.options_[camelTechName]); 23491 Object.assign(techOptions, this.options_[techName.toLowerCase()]); 23492 if (this.tag) { 23493 techOptions.tag = this.tag; 23494 } 23495 if (source && source.src === this.cache_.src && this.cache_.currentTime > 0) { 23496 techOptions.startTime = this.cache_.currentTime; 23497 } 23498 23499 // Initialize tech instance 23500 const TechClass = Tech.getTech(techName); 23501 if (!TechClass) { 23502 throw new Error( 23503 `No Tech named '${titleTechName}' exists! '${titleTechName}' should be registered using videojs.registerTech()'` 23504 ); 23505 } 23506 this.tech_ = new TechClass(techOptions); 23507 23508 // player.triggerReady is always async, so don't need this to be async 23509 this.tech_.ready(bind_(this, this.handleTechReady_), true); 23510 textTrackConverter.jsonToTextTracks(this.textTracksJson_ || [], this.tech_); 23511 23512 // Listen to all HTML5-defined events and trigger them on the player 23513 TECH_EVENTS_RETRIGGER.forEach((event) => { 23514 this.on(this.tech_, event, (e) => this[`handleTech${toTitleCase$1(event)}_`](e)); 23515 }); 23516 Object.keys(TECH_EVENTS_QUEUE).forEach((event) => { 23517 this.on(this.tech_, event, (eventObj) => { 23518 if (this.tech_.playbackRate() === 0 && this.tech_.seeking()) { 23519 this.queuedCallbacks_.push({ 23520 callback: this[`handleTech${TECH_EVENTS_QUEUE[event]}_`].bind(this), 23521 event: eventObj, 23522 }); 23523 return; 23524 } 23525 this[`handleTech${TECH_EVENTS_QUEUE[event]}_`](eventObj); 23526 }); 23527 }); 23528 this.on(this.tech_, 'loadstart', (e) => this.handleTechLoadStart_(e)); 23529 this.on(this.tech_, 'sourceset', (e) => this.handleTechSourceset_(e)); 23530 this.on(this.tech_, 'waiting', (e) => this.handleTechWaiting_(e)); 23531 this.on(this.tech_, 'ended', (e) => this.handleTechEnded_(e)); 23532 this.on(this.tech_, 'seeking', (e) => this.handleTechSeeking_(e)); 23533 this.on(this.tech_, 'play', (e) => this.handleTechPlay_(e)); 23534 this.on(this.tech_, 'pause', (e) => this.handleTechPause_(e)); 23535 this.on(this.tech_, 'durationchange', (e) => this.handleTechDurationChange_(e)); 23536 this.on(this.tech_, 'fullscreenchange', (e, data) => this.handleTechFullscreenChange_(e, data)); 23537 this.on(this.tech_, 'fullscreenerror', (e, err) => this.handleTechFullscreenError_(e, err)); 23538 this.on(this.tech_, 'enterpictureinpicture', (e) => this.handleTechEnterPictureInPicture_(e)); 23539 this.on(this.tech_, 'leavepictureinpicture', (e) => this.handleTechLeavePictureInPicture_(e)); 23540 this.on(this.tech_, 'error', (e) => this.handleTechError_(e)); 23541 this.on(this.tech_, 'posterchange', (e) => this.handleTechPosterChange_(e)); 23542 this.on(this.tech_, 'textdata', (e) => this.handleTechTextData_(e)); 23543 this.on(this.tech_, 'ratechange', (e) => this.handleTechRateChange_(e)); 23544 this.on(this.tech_, 'loadedmetadata', this.boundUpdateStyleEl_); 23545 this.usingNativeControls(this.techGet_('controls')); 23546 if (this.controls() && !this.usingNativeControls()) { 23547 this.addTechControlsListeners_(); 23548 } 23549 23550 // Add the tech element in the DOM if it was not already there 23551 // Make sure to not insert the original video element if using Html5 23552 if (this.tech_.el().parentNode !== this.el() && (titleTechName !== 'Html5' || !this.tag)) { 23553 prependTo(this.tech_.el(), this.el()); 23554 } 23555 23556 // Get rid of the original video tag reference after the first tech is loaded 23557 if (this.tag) { 23558 this.tag.player = null; 23559 this.tag = null; 23560 } 23561 } 23562 23563 /** 23564 * Unload and dispose of the current playback {@link Tech}. 23565 * 23566 * @private 23567 */ 23568 unloadTech_() { 23569 // Save the current text tracks so that we can reuse the same text tracks with the next tech 23570 ALL.names.forEach((name) => { 23571 const props = ALL[name]; 23572 this[props.privateName] = this[props.getterName](); 23573 }); 23574 this.textTracksJson_ = textTrackConverter.textTracksToJson(this.tech_); 23575 this.isReady_ = false;
vendor: 20,232 bytes, lines 23576-24253
23576 this.tech_.dispose(); 23577 this.tech_ = false; 23578 if (this.isPosterFromTech_) { 23579 this.poster_ = ''; 23580 this.trigger('posterchange'); 23581 } 23582 this.isPosterFromTech_ = false; 23583 } 23584 23585 /** 23586 * Return a reference to the current {@link Tech}. 23587 * It will print a warning by default about the danger of using the tech directly 23588 * but any argument that is passed in will silence the warning. 23589 * 23590 * @param {*} [safety] 23591 * Anything passed in to silence the warning 23592 * 23593 * @return {Tech} 23594 * The Tech 23595 */ 23596 tech(safety) { 23597 if (safety === undefined) { 23598 log$1.warn( 23599 "Using the tech directly can be dangerous. I hope you know what you're doing.\n" + 23600 'See https://github.com/videojs/video.js/issues/2617 for more info.\n' 23601 ); 23602 } 23603 return this.tech_; 23604 } 23605 23606 /** 23607 * An object that contains Video.js version. 23608 * 23609 * @typedef {Object} PlayerVersion 23610 * 23611 * @property {string} 'video.js' - Video.js version 23612 */ 23613 23614 /** 23615 * Returns an object with Video.js version. 23616 * 23617 * @return {PlayerVersion} 23618 * An object with Video.js version. 23619 */ 23620 version() { 23621 return { 23622 'video.js': version$6, 23623 }; 23624 } 23625 23626 /** 23627 * Set up click and touch listeners for the playback element 23628 * 23629 * - On desktops: a click on the video itself will toggle playback 23630 * - On mobile devices: a click on the video toggles controls 23631 * which is done by toggling the user state between active and 23632 * inactive 23633 * - A tap can signal that a user has become active or has become inactive 23634 * e.g. a quick tap on an iPhone movie should reveal the controls. Another 23635 * quick tap should hide them again (signaling the user is in an inactive 23636 * viewing state) 23637 * - In addition to this, we still want the user to be considered inactive after 23638 * a few seconds of inactivity. 23639 * 23640 * > Note: the only part of iOS interaction we can't mimic with this setup 23641 * is a touch and hold on the video element counting as activity in order to 23642 * keep the controls showing, but that shouldn't be an issue. A touch and hold 23643 * on any controls will still keep the user active 23644 * 23645 * @private 23646 */ 23647 addTechControlsListeners_() { 23648 // Make sure to remove all the previous listeners in case we are called multiple times. 23649 this.removeTechControlsListeners_(); 23650 this.on(this.tech_, 'click', this.boundHandleTechClick_); 23651 this.on(this.tech_, 'dblclick', this.boundHandleTechDoubleClick_); 23652 23653 // If the controls were hidden we don't want that to change without a tap event 23654 // so we'll check if the controls were already showing before reporting user 23655 // activity 23656 this.on(this.tech_, 'touchstart', this.boundHandleTechTouchStart_); 23657 this.on(this.tech_, 'touchmove', this.boundHandleTechTouchMove_); 23658 this.on(this.tech_, 'touchend', this.boundHandleTechTouchEnd_); 23659 23660 // The tap listener needs to come after the touchend listener because the tap 23661 // listener cancels out any reportedUserActivity when setting userActive(false) 23662 this.on(this.tech_, 'tap', this.boundHandleTechTap_); 23663 } 23664 23665 /** 23666 * Remove the listeners used for click and tap controls. This is needed for 23667 * toggling to controls disabled, where a tap/touch should do nothing. 23668 * 23669 * @private 23670 */ 23671 removeTechControlsListeners_() { 23672 // We don't want to just use `this.off()` because there might be other needed 23673 // listeners added by techs that extend this. 23674 this.off(this.tech_, 'tap', this.boundHandleTechTap_); 23675 this.off(this.tech_, 'touchstart', this.boundHandleTechTouchStart_); 23676 this.off(this.tech_, 'touchmove', this.boundHandleTechTouchMove_); 23677 this.off(this.tech_, 'touchend', this.boundHandleTechTouchEnd_); 23678 this.off(this.tech_, 'click', this.boundHandleTechClick_); 23679 this.off(this.tech_, 'dblclick', this.boundHandleTechDoubleClick_); 23680 } 23681 23682 /** 23683 * Player waits for the tech to be ready 23684 * 23685 * @private 23686 */ 23687 handleTechReady_() { 23688 this.triggerReady(); 23689 23690 // Keep the same volume as before 23691 if (this.cache_.volume) { 23692 this.techCall_('setVolume', this.cache_.volume); 23693 } 23694 23695 // Look if the tech found a higher resolution poster while loading 23696 this.handleTechPosterChange_(); 23697 23698 // Update the duration if available 23699 this.handleTechDurationChange_(); 23700 } 23701 23702 /** 23703 * Retrigger the `loadstart` event that was triggered by the {@link Tech}. 23704 * 23705 * @fires Player#loadstart 23706 * @listens Tech#loadstart 23707 * @private 23708 */ 23709 handleTechLoadStart_() { 23710 // TODO: Update to use `emptied` event instead. See #1277. 23711 23712 this.removeClass('vjs-ended', 'vjs-seeking'); 23713 23714 // reset the error state 23715 this.error(null); 23716 23717 // Update the duration 23718 this.handleTechDurationChange_(); 23719 if (!this.paused()) { 23720 /** 23721 * Fired when the user agent begins looking for media data 23722 * 23723 * @event Player#loadstart 23724 * @type {Event} 23725 */ 23726 this.trigger('loadstart'); 23727 } else { 23728 // reset the hasStarted state 23729 this.hasStarted(false); 23730 this.trigger('loadstart'); 23731 } 23732 23733 // autoplay happens after loadstart for the browser, 23734 // so we mimic that behavior 23735 this.manualAutoplay_( 23736 this.autoplay() === true && this.options_.normalizeAutoplay ? 'play' : this.autoplay() 23737 ); 23738 } 23739 23740 /** 23741 * Handle autoplay string values, rather than the typical boolean 23742 * values that should be handled by the tech. Note that this is not 23743 * part of any specification. Valid values and what they do can be 23744 * found on the autoplay getter at Player#autoplay() 23745 */ 23746 manualAutoplay_(type) { 23747 if (!this.tech_ || typeof type !== 'string') { 23748 return; 23749 } 23750 23751 // Save original muted() value, set muted to true, and attempt to play(). 23752 // On promise rejection, restore muted from saved value 23753 const resolveMuted = () => { 23754 const previouslyMuted = this.muted(); 23755 this.muted(true); 23756 const restoreMuted = () => { 23757 this.muted(previouslyMuted); 23758 }; 23759 23760 // restore muted on play terminatation 23761 this.playTerminatedQueue_.push(restoreMuted); 23762 const mutedPromise = this.play(); 23763 if (!isPromise(mutedPromise)) { 23764 return; 23765 } 23766 return mutedPromise.catch((err) => { 23767 restoreMuted(); 23768 throw new Error(`Rejection at manualAutoplay. Restoring muted value. ${err ? err : ''}`); 23769 }); 23770 }; 23771 let promise; 23772 23773 // if muted defaults to true 23774 // the only thing we can do is call play 23775 if (type === 'any' && !this.muted()) { 23776 promise = this.play(); 23777 if (isPromise(promise)) { 23778 promise = promise.catch(resolveMuted); 23779 } 23780 } else if (type === 'muted' && !this.muted()) { 23781 promise = resolveMuted(); 23782 } else { 23783 promise = this.play(); 23784 } 23785 if (!isPromise(promise)) { 23786 return; 23787 } 23788 return promise 23789 .then(() => { 23790 this.trigger({ 23791 type: 'autoplay-success', 23792 autoplay: type, 23793 }); 23794 }) 23795 .catch(() => { 23796 this.trigger({ 23797 type: 'autoplay-failure', 23798 autoplay: type, 23799 }); 23800 }); 23801 } 23802 23803 /** 23804 * Update the internal source caches so that we return the correct source from 23805 * `src()`, `currentSource()`, and `currentSources()`. 23806 * 23807 * > Note: `currentSources` will not be updated if the source that is passed in exists 23808 * in the current `currentSources` cache. 23809 * 23810 * 23811 * @param {Tech~SourceObject} srcObj 23812 * A string or object source to update our caches to. 23813 */ 23814 updateSourceCaches_(srcObj = '') { 23815 let src = srcObj; 23816 let type = ''; 23817 if (typeof src !== 'string') { 23818 src = srcObj.src; 23819 type = srcObj.type; 23820 } 23821 23822 // make sure all the caches are set to default values 23823 // to prevent null checking 23824 this.cache_.source = this.cache_.source || {}; 23825 this.cache_.sources = this.cache_.sources || []; 23826 23827 // try to get the type of the src that was passed in 23828 if (src && !type) { 23829 type = findMimetype(this, src); 23830 } 23831 23832 // update `currentSource` cache always 23833 this.cache_.source = merge$1({}, srcObj, { 23834 src, 23835 type, 23836 }); 23837 const matchingSources = this.cache_.sources.filter((s) => s.src && s.src === src); 23838 const sourceElSources = []; 23839 const sourceEls = this.$$('source'); 23840 const matchingSourceEls = []; 23841 for (let i = 0; i < sourceEls.length; i++) { 23842 const sourceObj = getAttributes(sourceEls[i]); 23843 sourceElSources.push(sourceObj); 23844 if (sourceObj.src && sourceObj.src === src) { 23845 matchingSourceEls.push(sourceObj.src); 23846 } 23847 } 23848 23849 // if we have matching source els but not matching sources 23850 // the current source cache is not up to date 23851 if (matchingSourceEls.length && !matchingSources.length) { 23852 this.cache_.sources = sourceElSources; 23853 // if we don't have matching source or source els set the 23854 // sources cache to the `currentSource` cache 23855 } else if (!matchingSources.length) { 23856 this.cache_.sources = [this.cache_.source]; 23857 } 23858 23859 // update the tech `src` cache 23860 this.cache_.src = src; 23861 } 23862 23863 /** 23864 * *EXPERIMENTAL* Fired when the source is set or changed on the {@link Tech} 23865 * causing the media element to reload. 23866 * 23867 * It will fire for the initial source and each subsequent source. 23868 * This event is a custom event from Video.js and is triggered by the {@link Tech}. 23869 * 23870 * The event object for this event contains a `src` property that will contain the source 23871 * that was available when the event was triggered. This is generally only necessary if Video.js 23872 * is switching techs while the source was being changed. 23873 * 23874 * It is also fired when `load` is called on the player (or media element) 23875 * because the {@link https://html.spec.whatwg.org/multipage/media.html#dom-media-load|specification for `load`} 23876 * says that the resource selection algorithm needs to be aborted and restarted. 23877 * In this case, it is very likely that the `src` property will be set to the 23878 * empty string `""` to indicate we do not know what the source will be but 23879 * that it is changing. 23880 * 23881 * *This event is currently still experimental and may change in minor releases.* 23882 * __To use this, pass `enableSourceset` option to the player.__ 23883 * 23884 * @event Player#sourceset 23885 * @type {Event} 23886 * @prop {string} src 23887 * The source url available when the `sourceset` was triggered. 23888 * It will be an empty string if we cannot know what the source is 23889 * but know that the source will change. 23890 */ 23891 /** 23892 * Retrigger the `sourceset` event that was triggered by the {@link Tech}. 23893 * 23894 * @fires Player#sourceset 23895 * @listens Tech#sourceset 23896 * @private 23897 */ 23898 handleTechSourceset_(event) { 23899 // only update the source cache when the source 23900 // was not updated using the player api 23901 if (!this.changingSrc_) { 23902 let updateSourceCaches = (src) => this.updateSourceCaches_(src); 23903 const playerSrc = this.currentSource().src; 23904 const eventSrc = event.src; 23905 23906 // if we have a playerSrc that is not a blob, and a tech src that is a blob 23907 if (playerSrc && !/^blob:/.test(playerSrc) && /^blob:/.test(eventSrc)) { 23908 // if both the tech source and the player source were updated we assume 23909 // something like @videojs/http-streaming did the sourceset and skip updating the source cache. 23910 if ( 23911 !this.lastSource_ || 23912 (this.lastSource_.tech !== eventSrc && this.lastSource_.player !== playerSrc) 23913 ) { 23914 updateSourceCaches = () => {}; 23915 } 23916 } 23917 23918 // update the source to the initial source right away 23919 // in some cases this will be empty string 23920 updateSourceCaches(eventSrc); 23921 23922 // if the `sourceset` `src` was an empty string 23923 // wait for a `loadstart` to update the cache to `currentSrc`. 23924 // If a sourceset happens before a `loadstart`, we reset the state 23925 if (!event.src) { 23926 this.tech_.any(['sourceset', 'loadstart'], (e) => { 23927 // if a sourceset happens before a `loadstart` there 23928 // is nothing to do as this `handleTechSourceset_` 23929 // will be called again and this will be handled there. 23930 if (e.type === 'sourceset') { 23931 return; 23932 } 23933 const techSrc = this.techGet_('currentSrc'); 23934 this.lastSource_.tech = techSrc; 23935 this.updateSourceCaches_(techSrc); 23936 }); 23937 } 23938 } 23939 this.lastSource_ = { 23940 player: this.currentSource().src, 23941 tech: event.src, 23942 }; 23943 this.trigger({ 23944 src: event.src, 23945 type: 'sourceset', 23946 }); 23947 } 23948 23949 /** 23950 * Add/remove the vjs-has-started class 23951 * 23952 * 23953 * @param {boolean} request 23954 * - true: adds the class 23955 * - false: remove the class 23956 * 23957 * @return {boolean} 23958 * the boolean value of hasStarted_ 23959 */ 23960 hasStarted(request) { 23961 if (request === undefined) { 23962 // act as getter, if we have no request to change 23963 return this.hasStarted_; 23964 } 23965 if (request === this.hasStarted_) { 23966 return; 23967 } 23968 this.hasStarted_ = request; 23969 if (this.hasStarted_) { 23970 this.addClass('vjs-has-started'); 23971 } else { 23972 this.removeClass('vjs-has-started'); 23973 } 23974 } 23975 23976 /** 23977 * Fired whenever the media begins or resumes playback 23978 * 23979 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-play} 23980 * @fires Player#play 23981 * @listens Tech#play 23982 * @private 23983 */ 23984 handleTechPlay_() { 23985 this.removeClass('vjs-ended', 'vjs-paused'); 23986 this.addClass('vjs-playing'); 23987 23988 // hide the poster when the user hits play 23989 this.hasStarted(true); 23990 /** 23991 * Triggered whenever an {@link Tech#play} event happens. Indicates that 23992 * playback has started or resumed. 23993 * 23994 * @event Player#play 23995 * @type {Event} 23996 */ 23997 this.trigger('play'); 23998 } 23999 24000 /** 24001 * Retrigger the `ratechange` event that was triggered by the {@link Tech}. 24002 * 24003 * If there were any events queued while the playback rate was zero, fire 24004 * those events now. 24005 * 24006 * @private 24007 * @method Player#handleTechRateChange_ 24008 * @fires Player#ratechange 24009 * @listens Tech#ratechange 24010 */ 24011 handleTechRateChange_() { 24012 if (this.tech_.playbackRate() > 0 && this.cache_.lastPlaybackRate === 0) { 24013 this.queuedCallbacks_.forEach((queued) => queued.callback(queued.event)); 24014 this.queuedCallbacks_ = []; 24015 } 24016 this.cache_.lastPlaybackRate = this.tech_.playbackRate(); 24017 /** 24018 * Fires when the playing speed of the audio/video is changed 24019 * 24020 * @event Player#ratechange 24021 * @type {event} 24022 */ 24023 this.trigger('ratechange'); 24024 } 24025 24026 /** 24027 * Retrigger the `waiting` event that was triggered by the {@link Tech}. 24028 * 24029 * @fires Player#waiting 24030 * @listens Tech#waiting 24031 * @private 24032 */ 24033 handleTechWaiting_() { 24034 this.addClass('vjs-waiting'); 24035 /** 24036 * A readyState change on the DOM element has caused playback to stop. 24037 * 24038 * @event Player#waiting 24039 * @type {Event} 24040 */ 24041 this.trigger('waiting'); 24042 24043 // Browsers may emit a timeupdate event after a waiting event. In order to prevent 24044 // premature removal of the waiting class, wait for the time to change. 24045 const timeWhenWaiting = this.currentTime(); 24046 const timeUpdateListener = () => { 24047 if (timeWhenWaiting !== this.currentTime()) { 24048 this.removeClass('vjs-waiting'); 24049 this.off('timeupdate', timeUpdateListener); 24050 } 24051 }; 24052 this.on('timeupdate', timeUpdateListener); 24053 } 24054 24055 /** 24056 * Retrigger the `canplay` event that was triggered by the {@link Tech}. 24057 * > Note: This is not consistent between browsers. See #1351 24058 * 24059 * @fires Player#canplay 24060 * @listens Tech#canplay 24061 * @private 24062 */ 24063 handleTechCanPlay_() { 24064 this.removeClass('vjs-waiting'); 24065 /** 24066 * The media has a readyState of HAVE_FUTURE_DATA or greater. 24067 * 24068 * @event Player#canplay 24069 * @type {Event} 24070 */ 24071 this.trigger('canplay'); 24072 } 24073 24074 /** 24075 * Retrigger the `canplaythrough` event that was triggered by the {@link Tech}. 24076 * 24077 * @fires Player#canplaythrough 24078 * @listens Tech#canplaythrough 24079 * @private 24080 */ 24081 handleTechCanPlayThrough_() { 24082 this.removeClass('vjs-waiting'); 24083 /** 24084 * The media has a readyState of HAVE_ENOUGH_DATA or greater. This means that the 24085 * entire media file can be played without buffering. 24086 * 24087 * @event Player#canplaythrough 24088 * @type {Event} 24089 */ 24090 this.trigger('canplaythrough'); 24091 } 24092 24093 /** 24094 * Retrigger the `playing` event that was triggered by the {@link Tech}. 24095 * 24096 * @fires Player#playing 24097 * @listens Tech#playing 24098 * @private 24099 */ 24100 handleTechPlaying_() { 24101 this.removeClass('vjs-waiting'); 24102 /** 24103 * The media is no longer blocked from playback, and has started playing. 24104 * 24105 * @event Player#playing 24106 * @type {Event} 24107 */ 24108 this.trigger('playing'); 24109 } 24110 24111 /** 24112 * Retrigger the `seeking` event that was triggered by the {@link Tech}. 24113 * 24114 * @fires Player#seeking 24115 * @listens Tech#seeking 24116 * @private 24117 */ 24118 handleTechSeeking_() { 24119 this.addClass('vjs-seeking'); 24120 /** 24121 * Fired whenever the player is jumping to a new time 24122 * 24123 * @event Player#seeking 24124 * @type {Event} 24125 */ 24126 this.trigger('seeking'); 24127 } 24128 24129 /** 24130 * Retrigger the `seeked` event that was triggered by the {@link Tech}. 24131 * 24132 * @fires Player#seeked 24133 * @listens Tech#seeked 24134 * @private 24135 */ 24136 handleTechSeeked_() { 24137 this.removeClass('vjs-seeking', 'vjs-ended'); 24138 /** 24139 * Fired when the player has finished jumping to a new time 24140 * 24141 * @event Player#seeked 24142 * @type {Event} 24143 */ 24144 this.trigger('seeked'); 24145 } 24146 24147 /** 24148 * Retrigger the `pause` event that was triggered by the {@link Tech}. 24149 * 24150 * @fires Player#pause 24151 * @listens Tech#pause 24152 * @private 24153 */ 24154 handleTechPause_() { 24155 this.removeClass('vjs-playing'); 24156 this.addClass('vjs-paused'); 24157 /** 24158 * Fired whenever the media has been paused 24159 * 24160 * @event Player#pause 24161 * @type {Event} 24162 */ 24163 this.trigger('pause'); 24164 } 24165 24166 /** 24167 * Retrigger the `ended` event that was triggered by the {@link Tech}. 24168 * 24169 * @fires Player#ended 24170 * @listens Tech#ended 24171 * @private 24172 */ 24173 handleTechEnded_() { 24174 this.addClass('vjs-ended'); 24175 this.removeClass('vjs-waiting'); 24176 if (this.options_.loop) { 24177 this.currentTime(0); 24178 this.play(); 24179 } else if (!this.paused()) { 24180 this.pause(); 24181 } 24182 24183 /** 24184 * Fired when the end of the media resource is reached (currentTime == duration) 24185 * 24186 * @event Player#ended 24187 * @type {Event} 24188 */ 24189 this.trigger('ended'); 24190 } 24191 24192 /** 24193 * Fired when the duration of the media resource is first known or changed 24194 * 24195 * @listens Tech#durationchange 24196 * @private 24197 */ 24198 handleTechDurationChange_() { 24199 this.duration(this.techGet_('duration')); 24200 } 24201 24202 /** 24203 * Handle a click on the media element to play/pause 24204 * 24205 * @param {Event} event 24206 * the event that caused this function to trigger 24207 * 24208 * @listens Tech#click 24209 * @private 24210 */ 24211 handleTechClick_(event) { 24212 // When controls are disabled a click should not toggle playback because 24213 // the click is considered a control 24214 if (!this.controls_) { 24215 return; 24216 } 24217 if ( 24218 this.options_ === undefined || 24219 this.options_.userActions === undefined || 24220 this.options_.userActions.click === undefined || 24221 this.options_.userActions.click !== false 24222 ) { 24223 if ( 24224 this.options_ !== undefined && 24225 this.options_.userActions !== undefined && 24226 typeof this.options_.userActions.click === 'function' 24227 ) { 24228 this.options_.userActions.click.call(this, event); 24229 } else if (this.paused()) { 24230 silencePromise(this.play()); 24231 } else { 24232 this.pause(); 24233 } 24234 } 24235 } 24236 24237 /** 24238 * Handle a double-click on the media element to enter/exit fullscreen, 24239 * or exit documentPictureInPicture mode 24240 * 24241 * @param {Event} event 24242 * the event that caused this function to trigger 24243 * 24244 * @listens Tech#dblclick 24245 * @private 24246 */ 24247 handleTechDoubleClick_(event) { 24248 if (!this.controls_) { 24249 return; 24250 } 24251 24252 // we do not want to toggle fullscreen state 24253 // when double-clicking inside a control bar or a modal
vendor: 5,733 bytes, lines 24254-24486
24254 const inAllowedEls = Array.prototype.some.call( 24255 this.$$('.vjs-control-bar, .vjs-modal-dialog'), 24256 (el) => el.contains(event.target) 24257 ); 24258 if (!inAllowedEls) { 24259 /* 24260 * options.userActions.doubleClick 24261 * 24262 * If `undefined` or `true`, double-click toggles fullscreen if controls are present 24263 * Set to `false` to disable double-click handling 24264 * Set to a function to substitute an external double-click handler 24265 */ 24266 if ( 24267 this.options_ === undefined || 24268 this.options_.userActions === undefined || 24269 this.options_.userActions.doubleClick === undefined || 24270 this.options_.userActions.doubleClick !== false 24271 ) { 24272 if ( 24273 this.options_ !== undefined && 24274 this.options_.userActions !== undefined && 24275 typeof this.options_.userActions.doubleClick === 'function' 24276 ) { 24277 this.options_.userActions.doubleClick.call(this, event); 24278 } else if ( 24279 this.isInPictureInPicture() && 24280 !document__default['default'].pictureInPictureElement 24281 ) { 24282 // Checking the presence of `window.documentPictureInPicture.window` complicates 24283 // tests, checking `document.pictureInPictureElement` also works. It wouldn't 24284 // be null in regular picture in picture. 24285 // Exit picture in picture mode. This gesture can't trigger pip on the main window. 24286 this.exitPictureInPicture(); 24287 } else if (this.isFullscreen()) { 24288 this.exitFullscreen(); 24289 } else { 24290 this.requestFullscreen(); 24291 } 24292 } 24293 } 24294 } 24295 24296 /** 24297 * Handle a tap on the media element. It will toggle the user 24298 * activity state, which hides and shows the controls. 24299 * 24300 * @listens Tech#tap 24301 * @private 24302 */ 24303 handleTechTap_() { 24304 this.userActive(!this.userActive()); 24305 } 24306 24307 /** 24308 * Handle touch to start 24309 * 24310 * @listens Tech#touchstart 24311 * @private 24312 */ 24313 handleTechTouchStart_() { 24314 this.userWasActive = this.userActive(); 24315 } 24316 24317 /** 24318 * Handle touch to move 24319 * 24320 * @listens Tech#touchmove 24321 * @private 24322 */ 24323 handleTechTouchMove_() { 24324 if (this.userWasActive) { 24325 this.reportUserActivity(); 24326 } 24327 } 24328 24329 /** 24330 * Handle touch to end 24331 * 24332 * @param {Event} event 24333 * the touchend event that triggered 24334 * this function 24335 * 24336 * @listens Tech#touchend 24337 * @private 24338 */ 24339 handleTechTouchEnd_(event) { 24340 // Stop the mouse events from also happening 24341 if (event.cancelable) { 24342 event.preventDefault(); 24343 } 24344 } 24345 24346 /** 24347 * @private 24348 */ 24349 toggleFullscreenClass_() { 24350 if (this.isFullscreen()) { 24351 this.addClass('vjs-fullscreen'); 24352 } else { 24353 this.removeClass('vjs-fullscreen'); 24354 } 24355 } 24356 24357 /** 24358 * when the document fschange event triggers it calls this 24359 */ 24360 documentFullscreenChange_(e) { 24361 const targetPlayer = e.target.player; 24362 24363 // if another player was fullscreen 24364 // do a null check for targetPlayer because older firefox's would put document as e.target 24365 if (targetPlayer && targetPlayer !== this) { 24366 return; 24367 } 24368 const el = this.el(); 24369 let isFs = document__default['default'][this.fsApi_.fullscreenElement] === el; 24370 if (!isFs && el.matches) { 24371 isFs = el.matches(':' + this.fsApi_.fullscreen); 24372 } 24373 this.isFullscreen(isFs); 24374 } 24375 24376 /** 24377 * Handle Tech Fullscreen Change 24378 * 24379 * @param {Event} event 24380 * the fullscreenchange event that triggered this function 24381 * 24382 * @param {Object} data 24383 * the data that was sent with the event 24384 * 24385 * @private 24386 * @listens Tech#fullscreenchange 24387 * @fires Player#fullscreenchange 24388 */ 24389 handleTechFullscreenChange_(event, data) { 24390 if (data) { 24391 if (data.nativeIOSFullscreen) { 24392 this.addClass('vjs-ios-native-fs'); 24393 this.tech_.one('webkitendfullscreen', () => { 24394 this.removeClass('vjs-ios-native-fs'); 24395 }); 24396 } 24397 this.isFullscreen(data.isFullscreen); 24398 } 24399 } 24400 handleTechFullscreenError_(event, err) { 24401 this.trigger('fullscreenerror', err); 24402 } 24403 24404 /** 24405 * @private 24406 */ 24407 togglePictureInPictureClass_() { 24408 if (this.isInPictureInPicture()) { 24409 this.addClass('vjs-picture-in-picture'); 24410 } else { 24411 this.removeClass('vjs-picture-in-picture'); 24412 } 24413 } 24414 24415 /** 24416 * Handle Tech Enter Picture-in-Picture. 24417 * 24418 * @param {Event} event 24419 * the enterpictureinpicture event that triggered this function 24420 * 24421 * @private 24422 * @listens Tech#enterpictureinpicture 24423 */ 24424 handleTechEnterPictureInPicture_(event) { 24425 this.isInPictureInPicture(true); 24426 } 24427 24428 /** 24429 * Handle Tech Leave Picture-in-Picture. 24430 * 24431 * @param {Event} event 24432 * the leavepictureinpicture event that triggered this function 24433 * 24434 * @private 24435 * @listens Tech#leavepictureinpicture 24436 */ 24437 handleTechLeavePictureInPicture_(event) { 24438 this.isInPictureInPicture(false); 24439 } 24440 24441 /** 24442 * Fires when an error occurred during the loading of an audio/video. 24443 * 24444 * @private 24445 * @listens Tech#error 24446 */ 24447 handleTechError_() { 24448 const error = this.tech_.error(); 24449 if (error) { 24450 this.error(error); 24451 } 24452 } 24453 24454 /** 24455 * Retrigger the `textdata` event that was triggered by the {@link Tech}. 24456 * 24457 * @fires Player#textdata 24458 * @listens Tech#textdata 24459 * @private 24460 */ 24461 handleTechTextData_() { 24462 let data = null; 24463 if (arguments.length > 1) { 24464 data = arguments[1]; 24465 } 24466 24467 /** 24468 * Fires when we get a textdata event from tech 24469 * 24470 * @event Player#textdata 24471 * @type {Event} 24472 */ 24473 this.trigger('textdata', data); 24474 } 24475 24476 /** 24477 * Get object for cached values. 24478 * 24479 * @return {Object} 24480 * get the current object cache 24481 */ 24482 getCache() { 24483 return this.cache_; 24484 } 24485 24486 /**
vendor: 4,125 bytes, lines 24487-24628
24487 * Resets the internal cache object. 24488 * 24489 * Using this function outside the player constructor or reset method may 24490 * have unintended side-effects. 24491 * 24492 * @private 24493 */ 24494 resetCache_() { 24495 this.cache_ = { 24496 // Right now, the currentTime is not _really_ cached because it is always 24497 // retrieved from the tech (see: currentTime). However, for completeness, 24498 // we set it to zero here to ensure that if we do start actually caching 24499 // it, we reset it along with everything else. 24500 currentTime: 0, 24501 initTime: 0, 24502 inactivityTimeout: this.options_.inactivityTimeout, 24503 duration: NaN, 24504 lastVolume: 1, 24505 lastPlaybackRate: this.defaultPlaybackRate(), 24506 media: null, 24507 src: '', 24508 source: {}, 24509 sources: [], 24510 playbackRates: [], 24511 volume: 1, 24512 }; 24513 } 24514 24515 /** 24516 * Pass values to the playback tech 24517 * 24518 * @param {string} [method] 24519 * the method to call 24520 * 24521 * @param {Object} [arg] 24522 * the argument to pass 24523 * 24524 * @private 24525 */ 24526 techCall_(method, arg) { 24527 // If it's not ready yet, call method when it is 24528 24529 this.ready(function () { 24530 if (method in allowedSetters) { 24531 return set(this.middleware_, this.tech_, method, arg); 24532 } else if (method in allowedMediators) { 24533 return mediate(this.middleware_, this.tech_, method, arg); 24534 } 24535 try { 24536 if (this.tech_) { 24537 this.tech_[method](arg); 24538 } 24539 } catch (e) { 24540 log$1(e); 24541 throw e; 24542 } 24543 }, true); 24544 } 24545 24546 /** 24547 * Mediate attempt to call playback tech method 24548 * and return the value of the method called. 24549 * 24550 * @param {string} method 24551 * Tech method 24552 * 24553 * @return {*} 24554 * Value returned by the tech method called, undefined if tech 24555 * is not ready or tech method is not present 24556 * 24557 * @private 24558 */ 24559 techGet_(method) { 24560 if (!this.tech_ || !this.tech_.isReady_) { 24561 return; 24562 } 24563 if (method in allowedGetters) { 24564 return get(this.middleware_, this.tech_, method); 24565 } else if (method in allowedMediators) { 24566 return mediate(this.middleware_, this.tech_, method); 24567 } 24568 24569 // Log error when playback tech object is present but method 24570 // is undefined or unavailable 24571 try { 24572 return this.tech_[method](); 24573 } catch (e) { 24574 // When building additional tech libs, an expected method may not be defined yet 24575 if (this.tech_[method] === undefined) { 24576 log$1( 24577 `Video.js: ${method} method not defined for ${this.techName_} playback technology.`, 24578 e 24579 ); 24580 throw e; 24581 } 24582 24583 // When a method isn't available on the object it throws a TypeError 24584 if (e.name === 'TypeError') { 24585 log$1( 24586 `Video.js: ${method} unavailable on ${this.techName_} playback technology element.`, 24587 e 24588 ); 24589 this.tech_.isReady_ = false; 24590 throw e; 24591 } 24592 24593 // If error unknown, just log and throw 24594 log$1(e); 24595 throw e; 24596 } 24597 } 24598 24599 /** 24600 * Attempt to begin playback at the first opportunity. 24601 * 24602 * @return {Promise|undefined} 24603 * Returns a promise if the browser supports Promises (or one 24604 * was passed in as an option). This promise will be resolved on 24605 * the return value of play. If this is undefined it will fulfill the 24606 * promise chain otherwise the promise chain will be fulfilled when 24607 * the promise from play is fulfilled. 24608 */ 24609 play() { 24610 return new Promise((resolve) => { 24611 this.play_(resolve); 24612 }); 24613 } 24614 24615 /** 24616 * The actual logic for play, takes a callback that will be resolved on the 24617 * return value of play. This allows us to resolve to the play promise if there 24618 * is one on modern browsers. 24619 * 24620 * @private 24621 * @param {Function} [callback] 24622 * The callback that should be called when the techs play is actually called 24623 */ 24624 play_(callback = silencePromise) { 24625 this.playCallbacks_.push(callback); 24626 const isSrcReady = Boolean(!this.changingSrc_ && (this.src() || this.currentSrc())); 24627 const isSafariOrIOS = Boolean(IS_ANY_SAFARI || IS_IOS); 24628
24629 // treat calls to play_ somewhat like the `one` event function 24630 if (this.waitToPlay_) { 24631 this.off(['ready', 'loadstart'], this.waitToPlay_); 24632 this.waitToPlay_ = null; 24633 } 24634 24635 // if the player/tech is not ready or the src itself is not ready 24636 // queue up a call to play on `ready` or `loadstart` 24637 if (!this.isReady_ || !isSrcReady) { 24638 this.waitToPlay_ = (e)
vendor: 8,726 bytes, lines 24638-24930
24638 => { 24639 this.play_(); 24640 }; 24641 this.one(['ready', 'loadstart'], this.waitToPlay_); 24642 24643 // if we are in Safari, there is a high chance that loadstart will trigger after the gesture timeperiod 24644 // in that case, we need to prime the video element by calling load so it'll be ready in time 24645 if (!isSrcReady && isSafariOrIOS) { 24646 this.load(); 24647 } 24648 return; 24649 } 24650 24651 // If the player/tech is ready and we have a source, we can attempt playback. 24652 const val = this.techGet_('play'); 24653 24654 // For native playback, reset the progress bar if we get a play call from a replay. 24655 const isNativeReplay = isSafariOrIOS && this.hasClass('vjs-ended'); 24656 if (isNativeReplay) { 24657 this.resetProgressBar_(); 24658 } 24659 // play was terminated if the returned value is null 24660 if (val === null) { 24661 this.runPlayTerminatedQueue_(); 24662 } else { 24663 this.runPlayCallbacks_(val); 24664 } 24665 } 24666 24667 /** 24668 * These functions will be run when if play is terminated. If play 24669 * runPlayCallbacks_ is run these function will not be run. This allows us 24670 * to differentiate between a terminated play and an actual call to play. 24671 */ 24672 runPlayTerminatedQueue_() { 24673 const queue = this.playTerminatedQueue_.slice(0); 24674 this.playTerminatedQueue_ = []; 24675 queue.forEach(function (q) { 24676 q(); 24677 }); 24678 } 24679 24680 /** 24681 * When a callback to play is delayed we have to run these 24682 * callbacks when play is actually called on the tech. This function 24683 * runs the callbacks that were delayed and accepts the return value 24684 * from the tech. 24685 * 24686 * @param {undefined|Promise} val 24687 * The return value from the tech. 24688 */ 24689 runPlayCallbacks_(val) { 24690 const callbacks = this.playCallbacks_.slice(0); 24691 this.playCallbacks_ = []; 24692 // clear play terminatedQueue since we finished a real play 24693 this.playTerminatedQueue_ = []; 24694 callbacks.forEach(function (cb) { 24695 cb(val); 24696 }); 24697 } 24698 24699 /** 24700 * Pause the video playback 24701 */ 24702 pause() { 24703 this.techCall_('pause'); 24704 } 24705 24706 /** 24707 * Check if the player is paused or has yet to play 24708 * 24709 * @return {boolean} 24710 * - false: if the media is currently playing 24711 * - true: if media is not currently playing 24712 */ 24713 paused() { 24714 // The initial state of paused should be true (in Safari it's actually false) 24715 return this.techGet_('paused') === false ? false : true; 24716 } 24717 24718 /** 24719 * Get a TimeRange object representing the current ranges of time that the user 24720 * has played. 24721 * 24722 * @return {TimeRange} 24723 * A time range object that represents all the increments of time that have 24724 * been played. 24725 */ 24726 played() { 24727 return this.techGet_('played') || createTimeRanges$1(0, 0); 24728 } 24729 24730 /** 24731 * Sets or returns whether or not the user is "scrubbing". Scrubbing is 24732 * when the user has clicked the progress bar handle and is 24733 * dragging it along the progress bar. 24734 * 24735 * @param {boolean} [isScrubbing] 24736 * whether the user is or is not scrubbing 24737 * 24738 * @return {boolean|undefined} 24739 * - The value of scrubbing when getting 24740 * - Nothing when setting 24741 */ 24742 scrubbing(isScrubbing) { 24743 if (typeof isScrubbing === 'undefined') { 24744 return this.scrubbing_; 24745 } 24746 this.scrubbing_ = !!isScrubbing; 24747 this.techCall_('setScrubbing', this.scrubbing_); 24748 if (isScrubbing) { 24749 this.addClass('vjs-scrubbing'); 24750 } else { 24751 this.removeClass('vjs-scrubbing'); 24752 } 24753 } 24754 24755 /** 24756 * Get or set the current time (in seconds) 24757 * 24758 * @param {number|string} [seconds] 24759 * The time to seek to in seconds 24760 * 24761 * @return {number|undefined} 24762 * - the current time in seconds when getting 24763 * - Nothing when setting 24764 */ 24765 currentTime(seconds) { 24766 if (seconds === undefined) { 24767 // cache last currentTime and return. default to 0 seconds 24768 // 24769 // Caching the currentTime is meant to prevent a massive amount of reads on the tech's 24770 // currentTime when scrubbing, but may not provide much performance benefit after all. 24771 // Should be tested. Also something has to read the actual current time or the cache will 24772 // never get updated. 24773 this.cache_.currentTime = this.techGet_('currentTime') || 0; 24774 return this.cache_.currentTime; 24775 } 24776 if (seconds < 0) { 24777 seconds = 0; 24778 } 24779 if (!this.isReady_ || this.changingSrc_ || !this.tech_ || !this.tech_.isReady_) { 24780 this.cache_.initTime = seconds; 24781 this.off('canplay', this.boundApplyInitTime_); 24782 this.one('canplay', this.boundApplyInitTime_); 24783 return; 24784 } 24785 this.techCall_('setCurrentTime', seconds); 24786 this.cache_.initTime = 0; 24787 if (isFinite(seconds)) { 24788 this.cache_.currentTime = Number(seconds); 24789 } 24790 } 24791 24792 /** 24793 * Apply the value of initTime stored in cache as currentTime. 24794 * 24795 * @private 24796 */ 24797 applyInitTime_() { 24798 this.currentTime(this.cache_.initTime); 24799 } 24800 24801 /** 24802 * Normally gets the length in time of the video in seconds; 24803 * in all but the rarest use cases an argument will NOT be passed to the method 24804 * 24805 * > **NOTE**: The video must have started loading before the duration can be 24806 * known, and depending on preload behaviour may not be known until the video starts 24807 * playing. 24808 * 24809 * @fires Player#durationchange 24810 * 24811 * @param {number} [seconds] 24812 * The duration of the video to set in seconds 24813 * 24814 * @return {number|undefined} 24815 * - The duration of the video in seconds when getting 24816 * - Nothing when setting 24817 */ 24818 duration(seconds) { 24819 if (seconds === undefined) { 24820 // return NaN if the duration is not known 24821 return this.cache_.duration !== undefined ? this.cache_.duration : NaN; 24822 } 24823 seconds = parseFloat(seconds); 24824 24825 // Standardize on Infinity for signaling video is live 24826 if (seconds < 0) { 24827 seconds = Infinity; 24828 } 24829 if (seconds !== this.cache_.duration) { 24830 // Cache the last set value for optimized scrubbing 24831 this.cache_.duration = seconds; 24832 if (seconds === Infinity) { 24833 this.addClass('vjs-live'); 24834 } else { 24835 this.removeClass('vjs-live'); 24836 } 24837 if (!isNaN(seconds)) { 24838 // Do not fire durationchange unless the duration value is known. 24839 // @see [Spec]{@link https://www.w3.org/TR/2011/WD-html5-20110113/video.html#media-element-load-algorithm} 24840 24841 /** 24842 * @event Player#durationchange 24843 * @type {Event} 24844 */ 24845 this.trigger('durationchange'); 24846 } 24847 } 24848 } 24849 24850 /** 24851 * Calculates how much time is left in the video. Not part 24852 * of the native video API. 24853 * 24854 * @return {number} 24855 * The time remaining in seconds 24856 */ 24857 remainingTime() { 24858 return this.duration() - this.currentTime(); 24859 } 24860 24861 /** 24862 * A remaining time function that is intended to be used when 24863 * the time is to be displayed directly to the user. 24864 * 24865 * @return {number} 24866 * The rounded time remaining in seconds 24867 */ 24868 remainingTimeDisplay() { 24869 return Math.floor(this.duration()) - Math.floor(this.currentTime()); 24870 } 24871 24872 // 24873 // Kind of like an array of portions of the video that have been downloaded. 24874 24875 /** 24876 * Get a TimeRange object with an array of the times of the video 24877 * that have been downloaded. If you just want the percent of the 24878 * video that's been downloaded, use bufferedPercent. 24879 * 24880 * @see [Buffered Spec]{@link http://dev.w3.org/html5/spec/video.html#dom-media-buffered} 24881 * 24882 * @return {TimeRange} 24883 * A mock {@link TimeRanges} object (following HTML spec) 24884 */ 24885 buffered() { 24886 let buffered = this.techGet_('buffered'); 24887 if (!buffered || !buffered.length) { 24888 buffered = createTimeRanges$1(0, 0); 24889 } 24890 return buffered; 24891 } 24892 24893 /** 24894 * Get the TimeRanges of the media that are currently available 24895 * for seeking to. 24896 * 24897 * @see [Seekable Spec]{@link https://html.spec.whatwg.org/multipage/media.html#dom-media-seekable} 24898 * 24899 * @return {TimeRange} 24900 * A mock {@link TimeRanges} object (following HTML spec) 24901 */ 24902 seekable() { 24903 let seekable = this.techGet_('seekable'); 24904 if (!seekable || !seekable.length) { 24905 seekable = createTimeRanges$1(0, 0); 24906 } 24907 return seekable; 24908 } 24909 24910 /** 24911 * Returns whether the player is in the "seeking" state. 24912 * 24913 * @return {boolean} True if the player is in the seeking state, false if not. 24914 */ 24915 seeking() { 24916 return this.techGet_('seeking'); 24917 } 24918 24919 /** 24920 * Returns whether the player is in the "ended" state. 24921 * 24922 * @return {boolean} True if the player is in the ended state, false if not. 24923 */ 24924 ended() { 24925 return this.techGet_('ended'); 24926 } 24927 24928 /** 24929 * Returns the current state of network activity for the element, from 24930 * the codes in the list below.
vendor: 7,111 bytes, lines 24931-25149
24931 * - NETWORK_EMPTY (numeric value 0) 24932 * The element has not yet been initialised. All attributes are in 24933 * their initial states. 24934 * - NETWORK_IDLE (numeric value 1) 24935 * The element's resource selection algorithm is active and has 24936 * selected a resource, but it is not actually using the network at 24937 * this time. 24938 * - NETWORK_LOADING (numeric value 2) 24939 * The user agent is actively trying to download data. 24940 * - NETWORK_NO_SOURCE (numeric value 3) 24941 * The element's resource selection algorithm is active, but it has 24942 * not yet found a resource to use. 24943 * 24944 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#network-states 24945 * @return {number} the current network activity state 24946 */ 24947 networkState() { 24948 return this.techGet_('networkState'); 24949 } 24950 24951 /** 24952 * Returns a value that expresses the current state of the element 24953 * with respect to rendering the current playback position, from the 24954 * codes in the list below. 24955 * - HAVE_NOTHING (numeric value 0) 24956 * No information regarding the media resource is available. 24957 * - HAVE_METADATA (numeric value 1) 24958 * Enough of the resource has been obtained that the duration of the 24959 * resource is available. 24960 * - HAVE_CURRENT_DATA (numeric value 2) 24961 * Data for the immediate current playback position is available. 24962 * - HAVE_FUTURE_DATA (numeric value 3) 24963 * Data for the immediate current playback position is available, as 24964 * well as enough data for the user agent to advance the current 24965 * playback position in the direction of playback. 24966 * - HAVE_ENOUGH_DATA (numeric value 4) 24967 * The user agent estimates that enough data is available for 24968 * playback to proceed uninterrupted. 24969 * 24970 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-readystate 24971 * @return {number} the current playback rendering state 24972 */ 24973 readyState() { 24974 return this.techGet_('readyState'); 24975 } 24976 24977 /** 24978 * Get the percent (as a decimal) of the video that's been downloaded. 24979 * This method is not a part of the native HTML video API. 24980 * 24981 * @return {number} 24982 * A decimal between 0 and 1 representing the percent 24983 * that is buffered 0 being 0% and 1 being 100% 24984 */ 24985 bufferedPercent() { 24986 return bufferedPercent(this.buffered(), this.duration()); 24987 } 24988 24989 /** 24990 * Get the ending time of the last buffered time range 24991 * This is used in the progress bar to encapsulate all time ranges. 24992 * 24993 * @return {number} 24994 * The end of the last buffered time range 24995 */ 24996 bufferedEnd() { 24997 const buffered = this.buffered(); 24998 const duration = this.duration(); 24999 let end = buffered.end(buffered.length - 1); 25000 if (end > duration) { 25001 end = duration; 25002 } 25003 return end; 25004 } 25005 25006 /** 25007 * Get or set the current volume of the media 25008 * 25009 * @param {number} [percentAsDecimal] 25010 * The new volume as a decimal percent: 25011 * - 0 is muted/0%/off 25012 * - 1.0 is 100%/full 25013 * - 0.5 is half volume or 50% 25014 * 25015 * @return {number|undefined} 25016 * The current volume as a percent when getting 25017 */ 25018 volume(percentAsDecimal) { 25019 let vol; 25020 if (percentAsDecimal !== undefined) { 25021 // Force value to between 0 and 1 25022 vol = Math.max(0, Math.min(1, percentAsDecimal)); 25023 this.cache_.volume = vol; 25024 this.techCall_('setVolume', vol); 25025 if (vol > 0) { 25026 this.lastVolume_(vol); 25027 } 25028 return; 25029 } 25030 25031 // Default to 1 when returning current volume. 25032 vol = parseFloat(this.techGet_('volume')); 25033 return isNaN(vol) ? 1 : vol; 25034 } 25035 25036 /** 25037 * Get the current muted state, or turn mute on or off 25038 * 25039 * @param {boolean} [muted] 25040 * - true to mute 25041 * - false to unmute 25042 * 25043 * @return {boolean|undefined} 25044 * - true if mute is on and getting 25045 * - false if mute is off and getting 25046 * - nothing if setting 25047 */ 25048 muted(muted) { 25049 if (muted !== undefined) { 25050 this.techCall_('setMuted', muted); 25051 return; 25052 } 25053 return this.techGet_('muted') || false; 25054 } 25055 25056 /** 25057 * Get the current defaultMuted state, or turn defaultMuted on or off. defaultMuted 25058 * indicates the state of muted on initial playback. 25059 * 25060 * ```js 25061 * var myPlayer = videojs('some-player-id'); 25062 * 25063 * myPlayer.src("http://www.example.com/path/to/video.mp4"); 25064 * 25065 * // get, should be false 25066 * console.log(myPlayer.defaultMuted()); 25067 * // set to true 25068 * myPlayer.defaultMuted(true); 25069 * // get should be true 25070 * console.log(myPlayer.defaultMuted()); 25071 * ``` 25072 * 25073 * @param {boolean} [defaultMuted] 25074 * - true to mute 25075 * - false to unmute 25076 * 25077 * @return {boolean|undefined} 25078 * - true if defaultMuted is on and getting 25079 * - false if defaultMuted is off and getting 25080 * - Nothing when setting 25081 */ 25082 defaultMuted(defaultMuted) { 25083 if (defaultMuted !== undefined) { 25084 this.techCall_('setDefaultMuted', defaultMuted); 25085 } 25086 return this.techGet_('defaultMuted') || false; 25087 } 25088 25089 /** 25090 * Get the last volume, or set it 25091 * 25092 * @param {number} [percentAsDecimal] 25093 * The new last volume as a decimal percent: 25094 * - 0 is muted/0%/off 25095 * - 1.0 is 100%/full 25096 * - 0.5 is half volume or 50% 25097 * 25098 * @return {number|undefined} 25099 * - The current value of lastVolume as a percent when getting 25100 * - Nothing when setting 25101 * 25102 * @private 25103 */ 25104 lastVolume_(percentAsDecimal) { 25105 if (percentAsDecimal !== undefined && percentAsDecimal !== 0) { 25106 this.cache_.lastVolume = percentAsDecimal; 25107 return; 25108 } 25109 return this.cache_.lastVolume; 25110 } 25111 25112 /** 25113 * Check if current tech can support native fullscreen 25114 * (e.g. with built in controls like iOS) 25115 * 25116 * @return {boolean} 25117 * if native fullscreen is supported 25118 */ 25119 supportsFullScreen() { 25120 return this.techGet_('supportsFullScreen') || false; 25121 } 25122 25123 /** 25124 * Check if the player is in fullscreen mode or tell the player that it 25125 * is or is not in fullscreen mode. 25126 * 25127 * > NOTE: As of the latest HTML5 spec, isFullscreen is no longer an official 25128 * property and instead document.fullscreenElement is used. But isFullscreen is 25129 * still a valuable property for internal player workings. 25130 * 25131 * @param {boolean} [isFS] 25132 * Set the players current fullscreen state 25133 * 25134 * @return {boolean|undefined} 25135 * - true if fullscreen is on and getting 25136 * - false if fullscreen is off and getting 25137 * - Nothing when setting 25138 */ 25139 isFullscreen(isFS) { 25140 if (isFS !== undefined) { 25141 const oldValue = this.isFullscreen_; 25142 this.isFullscreen_ = Boolean(isFS); 25143 25144 // if we changed fullscreen state and we're in prefixed mode, trigger fullscreenchange 25145 // this is the only place where we trigger fullscreenchange events for older browsers 25146 // fullWindow mode is treated as a prefixed event and will get a fullscreenchange event as well 25147 if (this.isFullscreen_ !== oldValue && this.fsApi_.prefixed) { 25148 /** 25149
25149* @event Player#fullscreenchange 25150 * @type {Event} 25151 */ 25152 this.trigger('fullscreenchange'); 25153 } 25154 this.toggleFullscreenClass_(); 25155 return; 25156 } 25157 return this.isFullscreen_; 25158 } 25159 25160 /** 25161 * Increase the size of the video to full screen 25162 * In some browsers, full screen is not supported natively, so it enters 25163 * "full window mode", where the video fills the browser window. 25164 * In browsers and devices that support native full screen, sometimes the 25165 * browser's default controls will be shown, and not the Video.js custom skin. 25166 * This includes most mobile devices (iOS, Android) and older versions of 25167 * Safari. 25168 * 25169 * @param {Object} [fullscreenOptions] 25170 * Override the player fullscreen options 25171 * 25172 * @fires Player#fullscreenchange 25173 */ 25174 requestFullscreen(fullscreenOptions) { 25175 if (this.isInPictureInPicture()) { 25176 this.exitPictureInPicture(); 25177 } 25178 const self = this; 25179 return new Promise((resolve, reject) => { 25180 function offHandler() { 25181 self.off('fullscreenerror', errorHandler); 25182 self.off('fullscreenchange', changeHandler); 25183 } 25184 function changeHandler() { 25185 offHandler(); 25186 resolve(); 25187 } 25188 function errorHandler(e, err) { 25189 offHandler(); 25190 reject(err); 25191 } 25192 self.one('fullscreenchange', changeHandler); 25193 self.one('fullscreenerror', errorHandler); 25194 const promise = self.requestFullscreenHelper_(fullscreenOptions); 25195 if (promise) { 25196 promise.then(offHandler, offHandler); 25197 promise.then(resolve, reject); 25198 } 25199 }); 25200 } 25201 requestFullscreenHelper_(fullscreenOptions) { 25202 let fsOptions; 25203 25204 // Only pass fullscreen options to requestFullscreen in spec-compliant browsers. 25205 // Use defaults or player configured option unless passed directly to this method. 25206 if (!this.fsApi_.prefixed) { 25207 fsOptions = (this.options_.fullscreen && this.options_.fullscreen.options) || {}; 25208 if (fullscreenOptions !== undefined) { 25209 fsOptions = fullscreenOptions; 25210 } 25211 } 25212 25213 // This method works as follows: 25214 // 1. if a fullscreen api is available, use it 25215 // 1. call requestFullscreen with potential options 25216 // 2. if we got a promise from above, use it to update isFullscreen() 25217 // 2. otherwise, if the tech supports fullscreen, call `enterFullScreen` on it. 25218 // This is particularly used for iPhone, older iPads, and non-safari browser on iOS. 25219 // 3. otherwise, use "fullWindow" mode 25220 if (this.fsApi_.requestFullscreen) { 25221 const promise = this.el_[this.fsApi_.requestFullscreen](fsOptions); 25222 25223 // Even on browsers with promise support this may not return a promise 25224 if (promise) { 25225 promise.then( 25226 () => this.isFullscreen(true),
vendor: 12,843 bytes, lines 25227-25619
25227 () => this.isFullscreen(false) 25228 ); 25229 } 25230 return promise; 25231 } else if (this.tech_.supportsFullScreen() && !this.options_.preferFullWindow === true) { 25232 // we can't take the video.js controls fullscreen but we can go fullscreen 25233 // with native controls 25234 this.techCall_('enterFullScreen'); 25235 } else { 25236 // fullscreen isn't supported so we'll just stretch the video element to 25237 // fill the viewport 25238 this.enterFullWindow(); 25239 } 25240 } 25241 25242 /** 25243 * Return the video to its normal size after having been in full screen mode 25244 * 25245 * @fires Player#fullscreenchange 25246 */ 25247 exitFullscreen() { 25248 const self = this; 25249 return new Promise((resolve, reject) => { 25250 function offHandler() { 25251 self.off('fullscreenerror', errorHandler); 25252 self.off('fullscreenchange', changeHandler); 25253 } 25254 function changeHandler() { 25255 offHandler(); 25256 resolve(); 25257 } 25258 function errorHandler(e, err) { 25259 offHandler(); 25260 reject(err); 25261 } 25262 self.one('fullscreenchange', changeHandler); 25263 self.one('fullscreenerror', errorHandler); 25264 const promise = self.exitFullscreenHelper_(); 25265 if (promise) { 25266 promise.then(offHandler, offHandler); 25267 // map the promise to our resolve/reject methods 25268 promise.then(resolve, reject); 25269 } 25270 }); 25271 } 25272 exitFullscreenHelper_() { 25273 if (this.fsApi_.requestFullscreen) { 25274 const promise = document__default['default'][this.fsApi_.exitFullscreen](); 25275 25276 // Even on browsers with promise support this may not return a promise 25277 if (promise) { 25278 // we're splitting the promise here, so, we want to catch the 25279 // potential error so that this chain doesn't have unhandled errors 25280 silencePromise(promise.then(() => this.isFullscreen(false))); 25281 } 25282 return promise; 25283 } else if (this.tech_.supportsFullScreen() && !this.options_.preferFullWindow === true) { 25284 this.techCall_('exitFullScreen'); 25285 } else { 25286 this.exitFullWindow(); 25287 } 25288 } 25289 25290 /** 25291 * When fullscreen isn't supported we can stretch the 25292 * video container to as wide as the browser will let us. 25293 * 25294 * @fires Player#enterFullWindow 25295 */ 25296 enterFullWindow() { 25297 this.isFullscreen(true); 25298 this.isFullWindow = true; 25299 25300 // Storing original doc overflow value to return to when fullscreen is off 25301 this.docOrigOverflow = document__default['default'].documentElement.style.overflow; 25302 25303 // Add listener for esc key to exit fullscreen 25304 on(document__default['default'], 'keydown', this.boundFullWindowOnEscKey_); 25305 25306 // Hide any scroll bars 25307 document__default['default'].documentElement.style.overflow = 'hidden'; 25308 25309 // Apply fullscreen styles 25310 addClass(document__default['default'].body, 'vjs-full-window'); 25311 25312 /** 25313 * @event Player#enterFullWindow 25314 * @type {Event} 25315 */ 25316 this.trigger('enterFullWindow'); 25317 } 25318 25319 /** 25320 * Check for call to either exit full window or 25321 * full screen on ESC key 25322 * 25323 * @param {string} event 25324 * Event to check for key press 25325 */ 25326 fullWindowOnEscKey(event) { 25327 if (event.key === 'Escape') { 25328 if (this.isFullscreen() === true) { 25329 if (!this.isFullWindow) { 25330 this.exitFullscreen(); 25331 } else { 25332 this.exitFullWindow(); 25333 } 25334 } 25335 } 25336 } 25337 25338 /** 25339 * Exit full window 25340 * 25341 * @fires Player#exitFullWindow 25342 */ 25343 exitFullWindow() { 25344 this.isFullscreen(false); 25345 this.isFullWindow = false; 25346 off(document__default['default'], 'keydown', this.boundFullWindowOnEscKey_); 25347 25348 // Unhide scroll bars. 25349 document__default['default'].documentElement.style.overflow = this.docOrigOverflow; 25350 25351 // Remove fullscreen styles 25352 removeClass(document__default['default'].body, 'vjs-full-window'); 25353 25354 // Resize the box, controller, and poster to original sizes 25355 // this.positionAll(); 25356 /** 25357 * @event Player#exitFullWindow 25358 * @type {Event} 25359 */ 25360 this.trigger('exitFullWindow'); 25361 } 25362 25363 /** 25364 * Get or set disable Picture-in-Picture mode. 25365 * 25366 * @param {boolean} [value] 25367 * - true will disable Picture-in-Picture mode 25368 * - false will enable Picture-in-Picture mode 25369 */ 25370 disablePictureInPicture(value) { 25371 if (value === undefined) { 25372 return this.techGet_('disablePictureInPicture'); 25373 } 25374 this.techCall_('setDisablePictureInPicture', value); 25375 this.options_.disablePictureInPicture = value; 25376 this.trigger('disablepictureinpicturechanged'); 25377 } 25378 25379 /** 25380 * Check if the player is in Picture-in-Picture mode or tell the player that it 25381 * is or is not in Picture-in-Picture mode. 25382 * 25383 * @param {boolean} [isPiP] 25384 * Set the players current Picture-in-Picture state 25385 * 25386 * @return {boolean|undefined} 25387 * - true if Picture-in-Picture is on and getting 25388 * - false if Picture-in-Picture is off and getting 25389 * - nothing if setting 25390 */ 25391 isInPictureInPicture(isPiP) { 25392 if (isPiP !== undefined) { 25393 this.isInPictureInPicture_ = !!isPiP; 25394 this.togglePictureInPictureClass_(); 25395 return; 25396 } 25397 return !!this.isInPictureInPicture_; 25398 } 25399 25400 /** 25401 * Create a floating video window always on top of other windows so that users may 25402 * continue consuming media while they interact with other content sites, or 25403 * applications on their device. 25404 * 25405 * This can use document picture-in-picture or element picture in picture 25406 * 25407 * Set `enableDocumentPictureInPicture` to `true` to use docPiP on a supported browser 25408 * Else set `disablePictureInPicture` to `false` to disable elPiP on a supported browser 25409 * 25410 * 25411 * @see [Spec]{@link https://w3c.github.io/picture-in-picture/} 25412 * @see [Spec]{@link https://wicg.github.io/document-picture-in-picture/} 25413 * 25414 * @fires Player#enterpictureinpicture 25415 * 25416 * @return {Promise} 25417 * A promise with a Picture-in-Picture window. 25418 */ 25419 requestPictureInPicture() { 25420 if ( 25421 this.options_.enableDocumentPictureInPicture && 25422 window__default['default'].documentPictureInPicture 25423 ) { 25424 const pipContainer = document__default['default'].createElement(this.el().tagName); 25425 pipContainer.classList = this.el().classList; 25426 pipContainer.classList.add('vjs-pip-container'); 25427 if (this.posterImage) { 25428 pipContainer.appendChild(this.posterImage.el().cloneNode(true)); 25429 } 25430 if (this.titleBar) { 25431 pipContainer.appendChild(this.titleBar.el().cloneNode(true)); 25432 } 25433 pipContainer.appendChild( 25434 createEl( 25435 'p', 25436 { 25437 className: 'vjs-pip-text', 25438 }, 25439 {}, 25440 this.localize('Playing in picture-in-picture') 25441 ) 25442 ); 25443 return window__default['default'].documentPictureInPicture 25444 .requestWindow({ 25445 // The aspect ratio won't be correct, Chrome bug https://crbug.com/1407629 25446 width: this.videoWidth(), 25447 height: this.videoHeight(), 25448 }) 25449 .then((pipWindow) => { 25450 copyStyleSheetsToWindow(pipWindow); 25451 this.el_.parentNode.insertBefore(pipContainer, this.el_); 25452 pipWindow.document.body.appendChild(this.el_); 25453 pipWindow.document.body.classList.add('vjs-pip-window'); 25454 this.player_.isInPictureInPicture(true); 25455 this.player_.trigger({ 25456 type: 'enterpictureinpicture', 25457 pipWindow, 25458 }); 25459 25460 // Listen for the PiP closing event to move the video back. 25461 pipWindow.addEventListener('pagehide', (event) => { 25462 const pipVideo = event.target.querySelector('.video-js'); 25463 pipContainer.parentNode.replaceChild(pipVideo, pipContainer); 25464 this.player_.isInPictureInPicture(false); 25465 this.player_.trigger('leavepictureinpicture'); 25466 }); 25467 return pipWindow; 25468 }); 25469 } 25470 if ( 25471 'pictureInPictureEnabled' in document__default['default'] && 25472 this.disablePictureInPicture() === false 25473 ) { 25474 /** 25475 * This event fires when the player enters picture in picture mode 25476 * 25477 * @event Player#enterpictureinpicture 25478 * @type {Event} 25479 */ 25480 return this.techGet_('requestPictureInPicture'); 25481 } 25482 return Promise.reject('No PiP mode is available'); 25483 } 25484 25485 /** 25486 * Exit Picture-in-Picture mode. 25487 * 25488 * @see [Spec]{@link https://wicg.github.io/picture-in-picture} 25489 * 25490 * @fires Player#leavepictureinpicture 25491 * 25492 * @return {Promise} 25493 * A promise. 25494 */ 25495 exitPictureInPicture() { 25496 if ( 25497 window__default['default'].documentPictureInPicture && 25498 window__default['default'].documentPictureInPicture.window 25499 ) { 25500 // With documentPictureInPicture, Player#leavepictureinpicture is fired in the pagehide handler 25501 window__default['default'].documentPictureInPicture.window.close(); 25502 return Promise.resolve(); 25503 } 25504 if ('pictureInPictureEnabled' in document__default['default']) { 25505 /** 25506 * This event fires when the player leaves picture in picture mode 25507 * 25508 * @event Player#leavepictureinpicture 25509 * @type {Event} 25510 */ 25511 return document__default['default'].exitPictureInPicture(); 25512 } 25513 } 25514 25515 /** 25516 * Called when this Player has focus and a key gets pressed down, or when 25517 * any Component of this player receives a key press that it doesn't handle. 25518 * This allows player-wide hotkeys (either as defined below, or optionally 25519 * by an external function). 25520 * 25521 * @param {KeyboardEvent} event 25522 * The `keydown` event that caused this function to be called. 25523 * 25524 * @listens keydown 25525 */ 25526 handleKeyDown(event) { 25527 const { userActions } = this.options_; 25528 25529 // Bail out if hotkeys are not configured. 25530 if (!userActions || !userActions.hotkeys) { 25531 return; 25532 } 25533 25534 // Function that determines whether or not to exclude an element from 25535 // hotkeys handling. 25536 const excludeElement = (el) => { 25537 const tagName = el.tagName.toLowerCase(); 25538 25539 // The first and easiest test is for `contenteditable` elements. 25540 if (el.isContentEditable) { 25541 return true; 25542 } 25543 25544 // Inputs matching these types will still trigger hotkey handling as 25545 // they are not text inputs. 25546 const allowedInputTypes = ['button', 'checkbox', 'hidden', 'radio', 'reset', 'submit']; 25547 if (tagName === 'input') { 25548 return allowedInputTypes.indexOf(el.type) === -1; 25549 } 25550 25551 // The final test is by tag name. These tags will be excluded entirely. 25552 const excludedTags = ['textarea']; 25553 return excludedTags.indexOf(tagName) !== -1; 25554 }; 25555 25556 // Bail out if the user is focused on an interactive form element. 25557 if (excludeElement(this.el_.ownerDocument.activeElement)) { 25558 return; 25559 } 25560 if (typeof userActions.hotkeys === 'function') { 25561 userActions.hotkeys.call(this, event); 25562 } else { 25563 this.handleHotkeys(event); 25564 } 25565 } 25566 25567 /** 25568 * Called when this Player receives a hotkey keydown event. 25569 * Supported player-wide hotkeys are: 25570 * 25571 * f - toggle fullscreen 25572 * m - toggle mute 25573 * k or Space - toggle play/pause 25574 * 25575 * @param {Event} event 25576 * The `keydown` event that caused this function to be called. 25577 */ 25578 handleHotkeys(event) { 25579 const hotkeys = this.options_.userActions ? this.options_.userActions.hotkeys : {}; 25580 25581 // set fullscreenKey, muteKey, playPauseKey from `hotkeys`, use defaults if not set 25582 const { 25583 fullscreenKey = (keydownEvent) => event.key.toLowerCase() === 'f', 25584 muteKey = (keydownEvent) => event.key.toLowerCase() === 'm', 25585 playPauseKey = (keydownEvent) => 25586 event.key.toLowerCase() === 'k' || event.key.toLowerCase() === ' ', 25587 } = hotkeys; 25588 if (fullscreenKey.call(this, event)) { 25589 event.preventDefault(); 25590 event.stopPropagation(); 25591 const FSToggle = Component$1.getComponent('FullscreenToggle'); 25592 if (document__default['default'][this.fsApi_.fullscreenEnabled] !== false) { 25593 FSToggle.prototype.handleClick.call(this, event); 25594 } 25595 } else if (muteKey.call(this, event)) { 25596 event.preventDefault(); 25597 event.stopPropagation(); 25598 const MuteToggle = Component$1.getComponent('MuteToggle'); 25599 MuteToggle.prototype.handleClick.call(this, event); 25600 } else if (playPauseKey.call(this, event)) { 25601 event.preventDefault(); 25602 event.stopPropagation(); 25603 const PlayToggle = Component$1.getComponent('PlayToggle'); 25604 PlayToggle.prototype.handleClick.call(this, event); 25605 } 25606 } 25607 25608 /** 25609 * Check whether the player can play a given mimetype 25610 * 25611 * @see https://www.w3.org/TR/2011/WD-html5-20110113/video.html#dom-navigator-canplaytype 25612 * 25613 * @param {string} type 25614 * The mimetype to check 25615 * 25616 * @return {string} 25617 * 'probably', 'maybe', or '' (empty string) 25618 */ 25619 canPlayType(type) {
25620 let can; 25621 25622 // Loop through each playback technology in the options order 25623 for (let i = 0, j = this.options_.techOrder; i < j.length; i++) { 25624 const techName = j[i]; 25625 let tech = Tech.getTech(techName); 25626 25627 // Support old behavior of techs being registered as components. 25628 // Remove once that deprecated behavior is removed. 25629 if (!tech) { 25630 tech = Component$1.getComponent(techName); 25631 } 25632 25633 // Check if the current tech is defined before continuing 25634 if (!tech) { 25635 log$1.error( 25636 `The "${techName}" tech is undefined. Skipped browser support check for that tech.` 25637 ); 25638 continue; 25639 } 25640 25641 // Check if the browser supports this technology 25642 if (tech.isSupported()) { 25643 can = tech.canPlayType(type); 25644 if (can) { 25645 return can; 25646 } 25647 } 25648 } 25649 return ''; 25650 } 25651 25652 /** 25653 * Select source based on tech-order or source-order 25654 * Uses source-order selection if `options.sourceOrder` is truthy. Otherwise, 25655 * defaults to tech-order selection 25656 * 25657 * @param {Array} sources 25658 * The sources for a media asset 25659 * 25660 * @return {Object|boolean} 25661 * Object of source and tech order or false 25662 */ 25663 selectSource(sources) { 25664 // Get only the techs specified in `techOrder` that exist and are supported by the 25665 // current platform 25666 const techs = this.options_.techOrder 25667 .map((techName) => { 25668 return [techName, Tech.getTech(techName)]; 25669 }) 25670 .filter(([techName, tech]) => { 25671 // Check if the current tech is defined before continuing 25672 if (tech) { 25673 // Check if the browser supports this technology 25674 return tech.isSupported(); 25675 } 25676 log$1.error( 25677 `The "${techName}" tech is undefined. Skipped browser support check for that tech.` 25678 ); 25679 return false; 25680 }); 25681 25682 // Iterate over each `innerArray` element once per `outerArray` element and execute 25683 // `tester` with both. If `tester` returns a non-falsy value, exit early and return 25684 // that value. 25685 const findFirstPassingTechSourcePair = function (outerArray, innerArray, tester) { 25686 let found; 25687 outerArray.some((outerChoice) => { 25688 return innerArray.some((innerChoice) => { 25689 found = tester(outerChoice, innerChoice); 25690 if (found) { 25691 return true; 25692 } 25693 }); 25694 }); 25695 return found; 25696 }; 25697 let foundSourceAndTech; 25698 const flip = (fn) => (a, b) => fn(b, a); 25699 const finder = ([techName, tech], source) => { 25700 if (tech.canPlaySource(source, this.options_[techName.toLowerCase()])) { 25701 return { 25702 source, 25703 tech: techName, 25704 }; 25705 } 25706 }; 25707 25708 // Depending on the truthiness of `options.sourceOrder`, we swap the order of techs and sources 25709 // to select from them based on their priority. 25710 if (this.options_.sourceOrder) { 25711 // Source-first ordering 25712 foundSourceAndTech = findFirstPassingTechSourcePair(sources, techs, flip(finder)); 25713 } else { 25714 // Tech-first ordering 25715 foundSourceAndTech = findFirstPassingTechSourcePair(techs, sources, finder); 25716 } 25717 return foundSourceAndTech || false; 25718 } 25719 25720 /** 25721 * Executes source setting and getting logic 25722 * 25723 * @param {Tech~SourceObject|Tech~SourceObject[]|string} [source] 25724 * A SourceObject, an array of SourceObjects, or a string referencing 25725 * a URL to a media source. It is _highly recommended_ that an object 25726 * or array of objects is used here, so that source selection 25727 * algorithms can take the `type` into account. 25728 * 25729 * If not provided, this method acts as a getter. 25730 * @param {boolean} [isRetry] 25731 * Indicates whether this is being called internally as a result of a retry 25732 * 25733 * @return {string|undefined} 25734 * If the `source` argument is missing, returns the current source 25735 * URL. Otherwise, returns nothing/undefined. 25736 */ 25737 handleSrc_(source, isRetry) { 25738 // getter usage 25739 if (typeof source === 'undefined') { 25740 return this.cache_.src || ''; 25741 } 25742 25743 // Reset retry behavior for new source 25744 if (this.resetRetryOnError_) { 25745 this.resetRetryOnError_(); 25746 } 25747 25748 // filter out invalid sources and turn our source into 25749 // an array of source objects 25750 const sources = filterSource(source); 25751
vendor: 10,839 bytes, lines 25752-26123
25752 // if a source was passed in then it is invalid because 25753 // it was filtered to a zero length Array. So we have to 25754 // show an error 25755 if (!sources.length) { 25756 this.setTimeout(function () { 25757 this.error({ 25758 code: 4, 25759 message: this.options_.notSupportedMessage, 25760 }); 25761 }, 0); 25762 return; 25763 } 25764 25765 // initial sources 25766 this.changingSrc_ = true; 25767 25768 // Only update the cached source list if we are not retrying a new source after error, 25769 // since in that case we want to include the failed source(s) in the cache 25770 if (!isRetry) { 25771 this.cache_.sources = sources; 25772 } 25773 this.updateSourceCaches_(sources[0]); 25774 25775 // middlewareSource is the source after it has been changed by middleware 25776 setSource(this, sources[0], (middlewareSource, mws) => { 25777 this.middleware_ = mws; 25778 25779 // since sourceSet is async we have to update the cache again after we select a source since 25780 // the source that is selected could be out of order from the cache update above this callback. 25781 if (!isRetry) { 25782 this.cache_.sources = sources; 25783 } 25784 this.updateSourceCaches_(middlewareSource); 25785 const err = this.src_(middlewareSource); 25786 if (err) { 25787 if (sources.length > 1) { 25788 return this.handleSrc_(sources.slice(1)); 25789 } 25790 this.changingSrc_ = false; 25791 25792 // We need to wrap this in a timeout to give folks a chance to add error event handlers 25793 this.setTimeout(function () { 25794 this.error({ 25795 code: 4, 25796 message: this.options_.notSupportedMessage, 25797 }); 25798 }, 0); 25799 25800 // we could not find an appropriate tech, but let's still notify the delegate that this is it 25801 // this needs a better comment about why this is needed 25802 this.triggerReady(); 25803 return; 25804 } 25805 setTech(mws, this.tech_); 25806 }); 25807 25808 // Try another available source if this one fails before playback. 25809 if (sources.length > 1) { 25810 const retry = () => { 25811 // Remove the error modal 25812 this.error(null); 25813 this.handleSrc_(sources.slice(1), true); 25814 }; 25815 const stopListeningForErrors = () => { 25816 this.off('error', retry); 25817 }; 25818 this.one('error', retry); 25819 this.one('playing', stopListeningForErrors); 25820 this.resetRetryOnError_ = () => { 25821 this.off('error', retry); 25822 this.off('playing', stopListeningForErrors); 25823 }; 25824 } 25825 } 25826 25827 /** 25828 * Get or set the video source. 25829 * 25830 * @param {Tech~SourceObject|Tech~SourceObject[]|string} [source] 25831 * A SourceObject, an array of SourceObjects, or a string referencing 25832 * a URL to a media source. It is _highly recommended_ that an object 25833 * or array of objects is used here, so that source selection 25834 * algorithms can take the `type` into account. 25835 * 25836 * If not provided, this method acts as a getter. 25837 * 25838 * @return {string|undefined} 25839 * If the `source` argument is missing, returns the current source 25840 * URL. Otherwise, returns nothing/undefined. 25841 */ 25842 src(source) { 25843 return this.handleSrc_(source, false); 25844 } 25845 25846 /** 25847 * Set the source object on the tech, returns a boolean that indicates whether 25848 * there is a tech that can play the source or not 25849 * 25850 * @param {Tech~SourceObject} source 25851 * The source object to set on the Tech 25852 * 25853 * @return {boolean} 25854 * - True if there is no Tech to playback this source 25855 * - False otherwise 25856 * 25857 * @private 25858 */ 25859 src_(source) { 25860 const sourceTech = this.selectSource([source]); 25861 if (!sourceTech) { 25862 return true; 25863 } 25864 if (!titleCaseEquals(sourceTech.tech, this.techName_)) { 25865 this.changingSrc_ = true; 25866 // load this technology with the chosen source 25867 this.loadTech_(sourceTech.tech, sourceTech.source); 25868 this.tech_.ready(() => { 25869 this.changingSrc_ = false; 25870 }); 25871 return false; 25872 } 25873 25874 // wait until the tech is ready to set the source 25875 // and set it synchronously if possible (#2326) 25876 this.ready(function () { 25877 // The setSource tech method was added with source handlers 25878 // so older techs won't support it 25879 // We need to check the direct prototype for the case where subclasses 25880 // of the tech do not support source handlers 25881 if (this.tech_.constructor.prototype.hasOwnProperty('setSource')) { 25882 this.techCall_('setSource', source); 25883 } else { 25884 this.techCall_('src', source.src); 25885 } 25886 this.changingSrc_ = false; 25887 }, true); 25888 return false; 25889 } 25890 25891 /** 25892 * Add a <source> element to the <video> element. 25893 * 25894 * @param {string} srcUrl 25895 * The URL of the video source. 25896 * 25897 * @param {string} [mimeType] 25898 * The MIME type of the video source. Optional but recommended. 25899 * 25900 * @return {boolean} 25901 * Returns true if the source element was successfully added, false otherwise. 25902 */ 25903 addSourceElement(srcUrl, mimeType) { 25904 if (!this.tech_) { 25905 return false; 25906 } 25907 return this.tech_.addSourceElement(srcUrl, mimeType); 25908 } 25909 25910 /** 25911 * Remove a <source> element from the <video> element by its URL. 25912 * 25913 * @param {string} srcUrl 25914 * The URL of the source to remove. 25915 * 25916 * @return {boolean} 25917 * Returns true if the source element was successfully removed, false otherwise. 25918 */ 25919 removeSourceElement(srcUrl) { 25920 if (!this.tech_) { 25921 return false; 25922 } 25923 return this.tech_.removeSourceElement(srcUrl); 25924 } 25925 25926 /** 25927 * Begin loading the src data. 25928 */ 25929 load() { 25930 // Workaround to use the load method with the VHS. 25931 // Does not cover the case when the load method is called directly from the mediaElement. 25932 if (this.tech_ && this.tech_.vhs) { 25933 this.src(this.currentSource()); 25934 return; 25935 } 25936 this.techCall_('load'); 25937 } 25938 25939 /** 25940 * Reset the player. Loads the first tech in the techOrder, 25941 * removes all the text tracks in the existing `tech`, 25942 * and calls `reset` on the `tech`. 25943 */ 25944 reset() { 25945 if (this.paused()) { 25946 this.doReset_(); 25947 } else { 25948 const playPromise = this.play(); 25949 silencePromise(playPromise.then(() => this.doReset_())); 25950 } 25951 } 25952 doReset_() { 25953 if (this.tech_) { 25954 this.tech_.clearTracks('text'); 25955 } 25956 this.removeClass('vjs-playing'); 25957 this.addClass('vjs-paused'); 25958 this.resetCache_(); 25959 this.poster(''); 25960 this.loadTech_(this.options_.techOrder[0], null); 25961 this.techCall_('reset'); 25962 this.resetControlBarUI_(); 25963 this.error(null); 25964 if (this.titleBar) { 25965 this.titleBar.update({ 25966 title: undefined, 25967 description: undefined, 25968 }); 25969 } 25970 if (isEvented(this)) { 25971 this.trigger('playerreset'); 25972 } 25973 } 25974 25975 /** 25976 * Reset Control Bar's UI by calling sub-methods that reset 25977 * all of Control Bar's components 25978 */ 25979 resetControlBarUI_() { 25980 this.resetProgressBar_(); 25981 this.resetPlaybackRate_(); 25982 this.resetVolumeBar_(); 25983 } 25984 25985 /** 25986 * Reset tech's progress so progress bar is reset in the UI 25987 */ 25988 resetProgressBar_() { 25989 this.currentTime(0); 25990 const { currentTimeDisplay, durationDisplay, progressControl, remainingTimeDisplay } = 25991 this.controlBar || {}; 25992 const { seekBar } = progressControl || {}; 25993 if (currentTimeDisplay) { 25994 currentTimeDisplay.updateContent(); 25995 } 25996 if (durationDisplay) { 25997 durationDisplay.updateContent(); 25998 } 25999 if (remainingTimeDisplay) { 26000 remainingTimeDisplay.updateContent(); 26001 } 26002 if (seekBar) { 26003 seekBar.update(); 26004 if (seekBar.loadProgressBar) { 26005 seekBar.loadProgressBar.update(); 26006 } 26007 } 26008 } 26009 26010 /** 26011 * Reset Playback ratio 26012 */ 26013 resetPlaybackRate_() { 26014 this.playbackRate(this.defaultPlaybackRate()); 26015 this.handleTechRateChange_(); 26016 } 26017 26018 /** 26019 * Reset Volume bar 26020 */ 26021 resetVolumeBar_() { 26022 this.volume(1.0); 26023 this.trigger('volumechange'); 26024 } 26025 26026 /** 26027 * Returns all of the current source objects. 26028 * 26029 * @return {Tech~SourceObject[]} 26030 * The current source objects 26031 */ 26032 currentSources() { 26033 const source = this.currentSource(); 26034 const sources = []; 26035 26036 // assume `{}` or `{ src }` 26037 if (Object.keys(source).length !== 0) { 26038 sources.push(source); 26039 } 26040 return this.cache_.sources || sources; 26041 } 26042 26043 /** 26044 * Returns the current source object. 26045 * 26046 * @return {Tech~SourceObject} 26047 * The current source object 26048 */ 26049 currentSource() { 26050 return this.cache_.source || {}; 26051 } 26052 26053 /** 26054 * Returns the fully qualified URL of the current source value e.g. http://mysite.com/video.mp4 26055 * Can be used in conjunction with `currentType` to assist in rebuilding the current source object. 26056 * 26057 * @return {string} 26058 * The current source 26059 */ 26060 currentSrc() { 26061 return (this.currentSource() && this.currentSource().src) || ''; 26062 } 26063 26064 /** 26065 * Get the current source type e.g. video/mp4 26066 * This can allow you rebuild the current source object so that you could load the same 26067 * source and tech later 26068 * 26069 * @return {string} 26070 * The source MIME type 26071 */ 26072 currentType() { 26073 return (this.currentSource() && this.currentSource().type) || ''; 26074 } 26075 26076 /** 26077 * Get or set the preload attribute 26078 * 26079 * @param {'none'|'auto'|'metadata'} [value] 26080 * Preload mode to pass to tech 26081 * 26082 * @return {string|undefined} 26083 * - The preload attribute value when getting 26084 * - Nothing when setting 26085 */ 26086 preload(value) { 26087 if (value !== undefined) { 26088 this.techCall_('setPreload', value); 26089 this.options_.preload = value; 26090 return; 26091 } 26092 return this.techGet_('preload'); 26093 } 26094 26095 /** 26096 * Get or set the autoplay option. When this is a boolean it will 26097 * modify the attribute on the tech. When this is a string the attribute on 26098 * the tech will be removed and `Player` will handle autoplay on loadstarts. 26099 * 26100 * @param {boolean|'play'|'muted'|'any'} [value] 26101 * - true: autoplay using the browser behavior 26102 * - false: do not autoplay 26103 * - 'play': call play() on every loadstart 26104 * - 'muted': call muted() then play() on every loadstart 26105 * - 'any': call play() on every loadstart. if that fails call muted() then play(). 26106 * - *: values other than those listed here will be set `autoplay` to true 26107 * 26108 * @return {boolean|string|undefined} 26109 * - The current value of autoplay when getting 26110 * - Nothing when setting 26111 */ 26112 autoplay(value) { 26113 // getter usage 26114 if (value === undefined) { 26115 return this.options_.autoplay || false; 26116 } 26117 let techAutoplay; 26118 26119 // if the value is a valid string set it to that, or normalize `true` to 'play', if need be 26120 if ( 26121 (typeof value === 'string' && /(any|play|muted)/.test(value)) || 26122 (value === true && this.options_.normalizeAutoplay) 26123
vendor: 6,874 bytes, lines 26123-26365
26123) { 26124 this.options_.autoplay = value; 26125 this.manualAutoplay_(typeof value === 'string' ? value : 'play'); 26126 techAutoplay = false; 26127 26128 // any falsy value sets autoplay to false in the browser, 26129 // lets do the same 26130 } else if (!value) { 26131 this.options_.autoplay = false; 26132 26133 // any other value (ie truthy) sets autoplay to true 26134 } else { 26135 this.options_.autoplay = true; 26136 } 26137 techAutoplay = typeof techAutoplay === 'undefined' ? this.options_.autoplay : techAutoplay; 26138 26139 // if we don't have a tech then we do not queue up 26140 // a setAutoplay call on tech ready. We do this because the 26141 // autoplay option will be passed in the constructor and we 26142 // do not need to set it twice 26143 if (this.tech_) { 26144 this.techCall_('setAutoplay', techAutoplay); 26145 } 26146 } 26147 26148 /** 26149 * Set or unset the playsinline attribute. 26150 * Playsinline tells the browser that non-fullscreen playback is preferred. 26151 * 26152 * @param {boolean} [value] 26153 * - true means that we should try to play inline by default 26154 * - false means that we should use the browser's default playback mode, 26155 * which in most cases is inline. iOS Safari is a notable exception 26156 * and plays fullscreen by default. 26157 * 26158 * @return {string|undefined} 26159 * - the current value of playsinline 26160 * - Nothing when setting 26161 * 26162 * @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline} 26163 */ 26164 playsinline(value) { 26165 if (value !== undefined) { 26166 this.techCall_('setPlaysinline', value); 26167 this.options_.playsinline = value; 26168 } 26169 return this.techGet_('playsinline'); 26170 } 26171 26172 /** 26173 * Get or set the loop attribute on the video element. 26174 * 26175 * @param {boolean} [value] 26176 * - true means that we should loop the video 26177 * - false means that we should not loop the video 26178 * 26179 * @return {boolean|undefined} 26180 * - The current value of loop when getting 26181 * - Nothing when setting 26182 */ 26183 loop(value) { 26184 if (value !== undefined) { 26185 this.techCall_('setLoop', value); 26186 this.options_.loop = value; 26187 return; 26188 } 26189 return this.techGet_('loop'); 26190 } 26191 26192 /** 26193 * Get or set the poster image source url 26194 * 26195 * @fires Player#posterchange 26196 * 26197 * @param {string} [src] 26198 * Poster image source URL 26199 * 26200 * @return {string|undefined} 26201 * - The current value of poster when getting 26202 * - Nothing when setting 26203 */ 26204 poster(src) { 26205 if (src === undefined) { 26206 return this.poster_; 26207 } 26208 26209 // The correct way to remove a poster is to set as an empty string 26210 // other falsey values will throw errors 26211 if (!src) { 26212 src = ''; 26213 } 26214 if (src === this.poster_) { 26215 return; 26216 } 26217 26218 // update the internal poster variable 26219 this.poster_ = src; 26220 26221 // update the tech's poster 26222 this.techCall_('setPoster', src); 26223 this.isPosterFromTech_ = false; 26224 26225 // alert components that the poster has been set 26226 /** 26227 * This event fires when the poster image is changed on the player. 26228 * 26229 * @event Player#posterchange 26230 * @type {Event} 26231 */ 26232 this.trigger('posterchange'); 26233 } 26234 26235 /** 26236 * Some techs (e.g. YouTube) can provide a poster source in an 26237 * asynchronous way. We want the poster component to use this 26238 * poster source so that it covers up the tech's controls. 26239 * (YouTube's play button). However we only want to use this 26240 * source if the player user hasn't set a poster through 26241 * the normal APIs. 26242 * 26243 * @fires Player#posterchange 26244 * @listens Tech#posterchange 26245 * @private 26246 */ 26247 handleTechPosterChange_() { 26248 if ((!this.poster_ || this.options_.techCanOverridePoster) && this.tech_ && this.tech_.poster) { 26249 const newPoster = this.tech_.poster() || ''; 26250 if (newPoster !== this.poster_) { 26251 this.poster_ = newPoster; 26252 this.isPosterFromTech_ = true; 26253 26254 // Let components know the poster has changed 26255 this.trigger('posterchange'); 26256 } 26257 } 26258 } 26259 26260 /** 26261 * Get or set whether or not the controls are showing. 26262 * 26263 * @fires Player#controlsenabled 26264 * 26265 * @param {boolean} [bool] 26266 * - true to turn controls on 26267 * - false to turn controls off 26268 * 26269 * @return {boolean|undefined} 26270 * - The current value of controls when getting 26271 * - Nothing when setting 26272 */ 26273 controls(bool) { 26274 if (bool === undefined) { 26275 return !!this.controls_; 26276 } 26277 bool = !!bool; 26278 26279 // Don't trigger a change event unless it actually changed 26280 if (this.controls_ === bool) { 26281 return; 26282 } 26283 this.controls_ = bool; 26284 if (this.usingNativeControls()) { 26285 this.techCall_('setControls', bool); 26286 } 26287 if (this.controls_) { 26288 this.removeClass('vjs-controls-disabled'); 26289 this.addClass('vjs-controls-enabled'); 26290 /** 26291 * @event Player#controlsenabled 26292 * @type {Event} 26293 */ 26294 this.trigger('controlsenabled'); 26295 if (!this.usingNativeControls()) { 26296 this.addTechControlsListeners_(); 26297 } 26298 } else { 26299 this.removeClass('vjs-controls-enabled'); 26300 this.addClass('vjs-controls-disabled'); 26301 /** 26302 * @event Player#controlsdisabled 26303 * @type {Event} 26304 */ 26305 this.trigger('controlsdisabled'); 26306 if (!this.usingNativeControls()) { 26307 this.removeTechControlsListeners_(); 26308 } 26309 } 26310 } 26311 26312 /** 26313 * Toggle native controls on/off. Native controls are the controls built into 26314 * devices (e.g. default iPhone controls) or other techs 26315 * (e.g. Vimeo Controls) 26316 * **This should only be set by the current tech, because only the tech knows 26317 * if it can support native controls** 26318 * 26319 * @fires Player#usingnativecontrols 26320 * @fires Player#usingcustomcontrols 26321 * 26322 * @param {boolean} [bool] 26323 * - true to turn native controls on 26324 * - false to turn native controls off 26325 * 26326 * @return {boolean|undefined} 26327 * - The current value of native controls when getting 26328 * - Nothing when setting 26329 */ 26330 usingNativeControls(bool) { 26331 if (bool === undefined) { 26332 return !!this.usingNativeControls_; 26333 } 26334 bool = !!bool; 26335 26336 // Don't trigger a change event unless it actually changed 26337 if (this.usingNativeControls_ === bool) { 26338 return; 26339 } 26340 this.usingNativeControls_ = bool; 26341 if (this.usingNativeControls_) { 26342 this.addClass('vjs-using-native-controls'); 26343 26344 /** 26345 * player is using the native device controls 26346 * 26347 * @event Player#usingnativecontrols 26348 * @type {Event} 26349 */ 26350 this.trigger('usingnativecontrols'); 26351 } else { 26352 this.removeClass('vjs-using-native-controls'); 26353 26354 /** 26355 * player is using the custom HTML controls 26356 * 26357 * @event Player#usingcustomcontrols 26358 * @type {Event} 26359 */ 26360 this.trigger('usingcustomcontrols'); 26361 } 26362 } 26363 26364 /** 26365 * Set
26365 or get the current MediaError 26366 * 26367 * @fires Player#error 26368 * 26369 * @param {MediaError|string|number} [err] 26370 * A MediaError or a string/number to be turned 26371 * into a MediaError 26372 * 26373 * @return {MediaError|null|undefined} 26374 * - The current MediaError when getting (or null) 26375 * - Nothing when setting 26376 */ 26377 error(err) { 26378 if (err === undefined) { 26379 return this.error_ || null; 26380 } 26381 26382 // allow hooks to modify error object
vendor: 1,769 bytes, lines 26383-26442
26383 hooks('beforeerror').forEach((hookFunction) => { 26384 const newErr = hookFunction(this, err); 26385 if ( 26386 !( 26387 (isObject(newErr) && !Array.isArray(newErr)) || 26388 typeof newErr === 'string' || 26389 typeof newErr === 'number' || 26390 newErr === null 26391 ) 26392 ) { 26393 this.log.error('please return a value that MediaError expects in beforeerror hooks'); 26394 return; 26395 } 26396 err = newErr; 26397 }); 26398 26399 // Suppress the first error message for no compatible source until 26400 // user interaction 26401 if (this.options_.suppressNotSupportedError && err && err.code === 4) { 26402 const triggerSuppressedError = function () { 26403 this.error(err); 26404 }; 26405 this.options_.suppressNotSupportedError = false; 26406 this.any(['click', 'touchstart'], triggerSuppressedError); 26407 this.one('loadstart', function () { 26408 this.off(['click', 'touchstart'], triggerSuppressedError); 26409 }); 26410 return; 26411 } 26412 26413 // restoring to default 26414 if (err === null) { 26415 this.error_ = null; 26416 this.removeClass('vjs-error'); 26417 if (this.errorDisplay) { 26418 this.errorDisplay.close(); 26419 } 26420 return; 26421 } 26422 this.error_ = new MediaError(err); 26423 26424 // add the vjs-error classname to the player 26425 this.addClass('vjs-error'); 26426 26427 // log the name of the error type and any message 26428 // IE11 logs "[object object]" and required you to expand message to see error object 26429 log$1.error( 26430 `(CODE:${this.error_.code} ${MediaError.errorTypes[this.error_.code]})`, 26431 this.error_.message, 26432 this.error_ 26433 ); 26434 26435 /** 26436 * @event Player#error 26437 * @type {Event} 26438 */ 26439 this.trigger('error'); 26440 26441 // notify hooks of the per player error 26442 hooks('error').forEach((hookFunction)
vendor: 8,927 bytes, lines 26442-26705
26442 => hookFunction(this, this.error_)); 26443 return; 26444 } 26445 26446 /** 26447 * Report user activity 26448 * 26449 * @param {Object} event 26450 * Event object 26451 */ 26452 reportUserActivity(event) { 26453 this.userActivity_ = true; 26454 } 26455 26456 /** 26457 * Get/set if user is active 26458 * 26459 * @fires Player#useractive 26460 * @fires Player#userinactive 26461 * 26462 * @param {boolean} [bool] 26463 * - true if the user is active 26464 * - false if the user is inactive 26465 * 26466 * @return {boolean|undefined} 26467 * - The current value of userActive when getting 26468 * - Nothing when setting 26469 */ 26470 userActive(bool) { 26471 if (bool === undefined) { 26472 return this.userActive_; 26473 } 26474 bool = !!bool; 26475 if (bool === this.userActive_) { 26476 return; 26477 } 26478 this.userActive_ = bool; 26479 if (this.userActive_) { 26480 this.userActivity_ = true; 26481 this.removeClass('vjs-user-inactive'); 26482 this.addClass('vjs-user-active'); 26483 /** 26484 * @event Player#useractive 26485 * @type {Event} 26486 */ 26487 this.trigger('useractive'); 26488 return; 26489 } 26490 26491 // Chrome/Safari/IE have bugs where when you change the cursor it can 26492 // trigger a mousemove event. This causes an issue when you're hiding 26493 // the cursor when the user is inactive, and a mousemove signals user 26494 // activity. Making it impossible to go into inactive mode. Specifically 26495 // this happens in fullscreen when we really need to hide the cursor. 26496 // 26497 // When this gets resolved in ALL browsers it can be removed 26498 // https://code.google.com/p/chromium/issues/detail?id=103041 26499 if (this.tech_) { 26500 this.tech_.one('mousemove', function (e) { 26501 e.stopPropagation(); 26502 e.preventDefault(); 26503 }); 26504 } 26505 this.userActivity_ = false; 26506 this.removeClass('vjs-user-active'); 26507 this.addClass('vjs-user-inactive'); 26508 /** 26509 * @event Player#userinactive 26510 * @type {Event} 26511 */ 26512 this.trigger('userinactive'); 26513 } 26514 26515 /** 26516 * Listen for user activity based on timeout value 26517 * 26518 * @private 26519 */ 26520 listenForUserActivity_() { 26521 let mouseInProgress; 26522 let lastMoveX; 26523 let lastMoveY; 26524 const handleActivity = bind_(this, this.reportUserActivity); 26525 const handleMouseMove = function (e) { 26526 // #1068 - Prevent mousemove spamming 26527 // Chrome Bug: https://code.google.com/p/chromium/issues/detail?id=366970 26528 if (e.screenX !== lastMoveX || e.screenY !== lastMoveY) { 26529 lastMoveX = e.screenX; 26530 lastMoveY = e.screenY; 26531 handleActivity(); 26532 } 26533 }; 26534 const handleMouseDown = function () { 26535 handleActivity(); 26536 // For as long as the they are touching the device or have their mouse down, 26537 // we consider them active even if they're not moving their finger or mouse. 26538 // So we want to continue to update that they are active 26539 this.clearInterval(mouseInProgress); 26540 // Setting userActivity=true now and setting the interval to the same time 26541 // as the activityCheck interval (250) should ensure we never miss the 26542 // next activityCheck 26543 mouseInProgress = this.setInterval(handleActivity, 250); 26544 }; 26545 const handleMouseUpAndMouseLeave = function (event) { 26546 handleActivity(); 26547 // Stop the interval that maintains activity if the mouse/touch is down 26548 this.clearInterval(mouseInProgress); 26549 }; 26550 26551 // Any mouse movement will be considered user activity 26552 this.on('mousedown', handleMouseDown); 26553 this.on('mousemove', handleMouseMove); 26554 this.on('mouseup', handleMouseUpAndMouseLeave); 26555 this.on('mouseleave', handleMouseUpAndMouseLeave); 26556 const controlBar = this.getChild('controlBar'); 26557 26558 // Fixes bug on Android & iOS where when tapping progressBar (when control bar is displayed) 26559 // controlBar would no longer be hidden by default timeout. 26560 if (controlBar && !IS_IOS && !IS_ANDROID) { 26561 controlBar.on('mouseenter', function (event) { 26562 if (this.player().options_.inactivityTimeout !== 0) { 26563 this.player().cache_.inactivityTimeout = this.player().options_.inactivityTimeout; 26564 } 26565 this.player().options_.inactivityTimeout = 0; 26566 }); 26567 controlBar.on('mouseleave', function (event) { 26568 this.player().options_.inactivityTimeout = this.player().cache_.inactivityTimeout; 26569 }); 26570 } 26571 26572 // Listen for keyboard navigation 26573 // Shouldn't need to use inProgress interval because of key repeat 26574 this.on('keydown', handleActivity); 26575 this.on('keyup', handleActivity); 26576 26577 // Run an interval every 250 milliseconds instead of stuffing everything into 26578 // the mousemove/touchmove function itself, to prevent performance degradation. 26579 // `this.reportUserActivity` simply sets this.userActivity_ to true, which 26580 // then gets picked up by this loop 26581 // http://ejohn.org/blog/learning-from-twitter/ 26582 let inactivityTimeout; 26583 26584 /** @this Player */ 26585 const activityCheck = function () { 26586 // Check to see if mouse/touch activity has happened 26587 if (!this.userActivity_) { 26588 return; 26589 } 26590 26591 // Reset the activity tracker 26592 this.userActivity_ = false; 26593 26594 // If the user state was inactive, set the state to active 26595 this.userActive(true); 26596 26597 // Clear any existing inactivity timeout to start the timer over 26598 this.clearTimeout(inactivityTimeout); 26599 const timeout = this.options_.inactivityTimeout; 26600 if (timeout <= 0) { 26601 return; 26602 } 26603 26604 // In <timeout> milliseconds, if no more activity has occurred the 26605 // user will be considered inactive 26606 inactivityTimeout = this.setTimeout(function () { 26607 // Protect against the case where the inactivityTimeout can trigger just 26608 // before the next user activity is picked up by the activity check loop 26609 // causing a flicker 26610 if (!this.userActivity_) { 26611 this.userActive(false); 26612 } 26613 }, timeout); 26614 }; 26615 this.setInterval(activityCheck, 250); 26616 } 26617 26618 /** 26619 * Gets or sets the current playback rate. A playback rate of 26620 * 1.0 represents normal speed and 0.5 would indicate half-speed 26621 * playback, for instance. 26622 * 26623 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-playbackrate 26624 * 26625 * @param {number} [rate] 26626 * New playback rate to set. 26627 * 26628 * @return {number|undefined} 26629 * - The current playback rate when getting or 1.0 26630 * - Nothing when setting 26631 */ 26632 playbackRate(rate) { 26633 if (rate !== undefined) { 26634 // NOTE: this.cache_.lastPlaybackRate is set from the tech handler 26635 // that is registered above 26636 this.techCall_('setPlaybackRate', rate); 26637 return; 26638 } 26639 if (this.tech_ && this.tech_.featuresPlaybackRate) { 26640 return this.cache_.lastPlaybackRate || this.techGet_('playbackRate'); 26641 } 26642 return 1.0; 26643 } 26644 26645 /** 26646 * Gets or sets the current default playback rate. A default playback rate of 26647 * 1.0 represents normal speed and 0.5 would indicate half-speed playback, for instance. 26648 * defaultPlaybackRate will only represent what the initial playbackRate of a video was, not 26649 * not the current playbackRate. 26650 * 26651 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-defaultplaybackrate 26652 * 26653 * @param {number} [rate] 26654 * New default playback rate to set. 26655 * 26656 * @return {number|undefined} 26657 * - The default playback rate when getting or 1.0 26658 * - Nothing when setting 26659 */ 26660 defaultPlaybackRate(rate) { 26661 if (rate !== undefined) { 26662 return this.techCall_('setDefaultPlaybackRate', rate); 26663 } 26664 if (this.tech_ && this.tech_.featuresPlaybackRate) { 26665 return this.techGet_('defaultPlaybackRate'); 26666 } 26667 return 1.0; 26668 } 26669 26670 /** 26671 * Gets or sets the audio flag 26672 * 26673 * @param {boolean} [bool] 26674 * - true signals that this is an audio player 26675 * - false signals that this is not an audio player 26676 * 26677 * @return {boolean|undefined} 26678 * - The current value of isAudio when getting 26679 * - Nothing when setting 26680 */ 26681 isAudio(bool) { 26682 if (bool !== undefined) { 26683 this.isAudio_ = !!bool; 26684 return; 26685 } 26686 return !!this.isAudio_; 26687 } 26688 updatePlayerHeightOnAudioOnlyMode_() { 26689 const controlBar = this.getChild('ControlBar'); 26690 if (!controlBar || this.audioOnlyCache_.controlBarHeight === controlBar.currentHeight()) { 26691 return; 26692 } 26693 this.audioOnlyCache_.controlBarHeight = controlBar.currentHeight(); 26694 this.height(this.audioOnlyCache_.controlBarHeight); 26695 } 26696 enableAudioOnlyUI_() { 26697 // Update styling immediately to show the control bar so we can get its height 26698 this.addClass('vjs-audio-only-mode'); 26699 const playerChildren = this.children(); 26700 const controlBar = this.getChild('ControlBar'); 26701 const controlBarHeight = controlBar && controlBar.currentHeight(); 26702 26703 // Hide all player components except the control bar. Control bar components 26704 // needed only for video are hidden with CSS 26705 playerChildren.forEach(
26705(child) => { 26706 if (child === controlBar) { 26707 return; 26708 } 26709 if (child.el_ && !child.hasClass('vjs-hidden')) { 26710 child.hide(); 26711 this.audioOnlyCache_.hiddenChildren.push(child); 26712 } 26713 });
vendor: 6,815 bytes, lines 26714-26930
26714 this.audioOnlyCache_.playerHeight = this.currentHeight(); 26715 this.audioOnlyCache_.controlBarHeight = controlBarHeight; 26716 this.on('playerresize', this.boundUpdatePlayerHeightOnAudioOnlyMode_); 26717 26718 // Set the player height the same as the control bar 26719 this.height(controlBarHeight); 26720 this.trigger('audioonlymodechange'); 26721 } 26722 disableAudioOnlyUI_() { 26723 this.removeClass('vjs-audio-only-mode'); 26724 this.off('playerresize', this.boundUpdatePlayerHeightOnAudioOnlyMode_); 26725 26726 // Show player components that were previously hidden 26727 this.audioOnlyCache_.hiddenChildren.forEach((child) => child.show()); 26728 26729 // Reset player height 26730 this.height(this.audioOnlyCache_.playerHeight); 26731 this.trigger('audioonlymodechange'); 26732 } 26733 26734 /** 26735 * Get the current audioOnlyMode state or set audioOnlyMode to true or false. 26736 * 26737 * Setting this to `true` will hide all player components except the control bar, 26738 * as well as control bar components needed only for video. 26739 * 26740 * @param {boolean} [value] 26741 * The value to set audioOnlyMode to. 26742 * 26743 * @return {Promise|boolean} 26744 * A Promise is returned when setting the state, and a boolean when getting 26745 * the present state 26746 */ 26747 audioOnlyMode(value) { 26748 if (typeof value !== 'boolean' || value === this.audioOnlyMode_) { 26749 return this.audioOnlyMode_; 26750 } 26751 this.audioOnlyMode_ = value; 26752 26753 // Enable Audio Only Mode 26754 if (value) { 26755 const exitPromises = []; 26756 26757 // Fullscreen and PiP are not supported in audioOnlyMode, so exit if we need to. 26758 if (this.isInPictureInPicture()) { 26759 exitPromises.push(this.exitPictureInPicture()); 26760 } 26761 if (this.isFullscreen()) { 26762 exitPromises.push(this.exitFullscreen()); 26763 } 26764 if (this.audioPosterMode()) { 26765 exitPromises.push(this.audioPosterMode(false)); 26766 } 26767 return Promise.all(exitPromises).then(() => this.enableAudioOnlyUI_()); 26768 } 26769 26770 // Disable Audio Only Mode 26771 return Promise.resolve().then(() => this.disableAudioOnlyUI_()); 26772 } 26773 enablePosterModeUI_() { 26774 // Hide the video element and show the poster image to enable posterModeUI 26775 const tech = this.tech_ && this.tech_; 26776 tech.hide(); 26777 this.addClass('vjs-audio-poster-mode'); 26778 this.trigger('audiopostermodechange'); 26779 } 26780 disablePosterModeUI_() { 26781 // Show the video element and hide the poster image to disable posterModeUI 26782 const tech = this.tech_ && this.tech_; 26783 tech.show(); 26784 this.removeClass('vjs-audio-poster-mode'); 26785 this.trigger('audiopostermodechange'); 26786 } 26787 26788 /** 26789 * Get the current audioPosterMode state or set audioPosterMode to true or false 26790 * 26791 * @param {boolean} [value] 26792 * The value to set audioPosterMode to. 26793 * 26794 * @return {Promise|boolean} 26795 * A Promise is returned when setting the state, and a boolean when getting 26796 * the present state 26797 */ 26798 audioPosterMode(value) { 26799 if (typeof value !== 'boolean' || value === this.audioPosterMode_) { 26800 return this.audioPosterMode_; 26801 } 26802 this.audioPosterMode_ = value; 26803 if (value) { 26804 if (this.audioOnlyMode()) { 26805 const audioOnlyModePromise = this.audioOnlyMode(false); 26806 return audioOnlyModePromise.then(() => { 26807 // enable audio poster mode after audio only mode is disabled 26808 this.enablePosterModeUI_(); 26809 }); 26810 } 26811 return Promise.resolve().then(() => { 26812 // enable audio poster mode 26813 this.enablePosterModeUI_(); 26814 }); 26815 } 26816 return Promise.resolve().then(() => { 26817 // disable audio poster mode 26818 this.disablePosterModeUI_(); 26819 }); 26820 } 26821 26822 /** 26823 * A helper method for adding a {@link TextTrack} to our 26824 * {@link TextTrackList}. 26825 * 26826 * In addition to the W3C settings we allow adding additional info through options. 26827 * 26828 * @see http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-addtexttrack 26829 * 26830 * @param {string} [kind] 26831 * the kind of TextTrack you are adding 26832 * 26833 * @param {string} [label] 26834 * the label to give the TextTrack label 26835 * 26836 * @param {string} [language] 26837 * the language to set on the TextTrack 26838 * 26839 * @return {TextTrack|undefined} 26840 * the TextTrack that was added or undefined 26841 * if there is no tech 26842 */ 26843 addTextTrack(kind, label, language) { 26844 if (this.tech_) { 26845 return this.tech_.addTextTrack(kind, label, language); 26846 } 26847 } 26848 26849 /** 26850 * Create a remote {@link TextTrack} and an {@link HTMLTrackElement}. 26851 * 26852 * @param {Object} options 26853 * Options to pass to {@link HTMLTrackElement} during creation. See 26854 * {@link HTMLTrackElement} for object properties that you should use. 26855 * 26856 * @param {boolean} [manualCleanup=false] if set to true, the TextTrack will not be removed 26857 * from the TextTrackList and HtmlTrackElementList 26858 * after a source change 26859 * 26860 * @return {HtmlTrackElement} 26861 * the HTMLTrackElement that was created and added 26862 * to the HtmlTrackElementList and the remote 26863 * TextTrackList 26864 * 26865 */ 26866 addRemoteTextTrack(options, manualCleanup) { 26867 if (this.tech_) { 26868 return this.tech_.addRemoteTextTrack(options, manualCleanup); 26869 } 26870 } 26871 26872 /** 26873 * Remove a remote {@link TextTrack} from the respective 26874 * {@link TextTrackList} and {@link HtmlTrackElementList}. 26875 * 26876 * @param {Object} track 26877 * Remote {@link TextTrack} to remove 26878 * 26879 * @return {undefined} 26880 * does not return anything 26881 */ 26882 removeRemoteTextTrack(obj = {}) { 26883 let { track } = obj; 26884 if (!track) { 26885 track = obj; 26886 } 26887 26888 // destructure the input into an object with a track argument, defaulting to arguments[0] 26889 // default the whole argument to an empty object if nothing was passed in 26890 26891 if (this.tech_) { 26892 return this.tech_.removeRemoteTextTrack(track); 26893 } 26894 } 26895 26896 /** 26897 * Gets available media playback quality metrics as specified by the W3C's Media 26898 * Playback Quality API. 26899 * 26900 * @see [Spec]{@link https://wicg.github.io/media-playback-quality} 26901 * 26902 * @return {Object|undefined} 26903 * An object with supported media playback quality metrics or undefined if there 26904 * is no tech or the tech does not support it. 26905 */ 26906 getVideoPlaybackQuality() { 26907 return this.techGet_('getVideoPlaybackQuality'); 26908 } 26909 26910 /** 26911 * Get video width 26912 * 26913 * @return {number} 26914 * current video width 26915 */ 26916 videoWidth() { 26917 return (this.tech_ && this.tech_.videoWidth && this.tech_.videoWidth()) || 0; 26918 } 26919 26920 /** 26921 * Get video height 26922 * 26923 * @return {number} 26924 * current video height 26925 */ 26926 videoHeight() { 26927 return (this.tech_ && this.tech_.videoHeight && this.tech_.videoHeight()) || 0; 26928 } 26929 26930 /**
26931 * Set or get the player's language code. 26932 * 26933 * Changing the language will trigger 26934 * [languagechange]{@link Player#event:languagechange} 26935 * which Components can use to update control text. 26936 * ClickableComponent will update its control text by default on 26937 * [languagechange]{@link Player#event:languagechange}. 26938 * 26939 * @fires Player#languagechange 26940 * 26941 * @param {string} [code] 26942 * the language code to set the player to 26943 * 26944 * @return {string|undefined} 26945 * - The current language code when getting 26946 * - Nothing when setting 26947 */ 26948 language(code) { 26949 if (code === undefined) { 26950 return this.language_; 26951 } 26952 if (this.language_ !== String(code).toLowerCase()) { 26953 this.language_ = String(code).toLowerCase(); 26954 26955 // during first init, it's possible some things won't be evented 26956 if (isEvented(this)) { 26957 /** 26958 * fires when the player language change 26959 * 26960 * @event Player#languagechange 26961 * @type {Event} 26962 */ 26963 this.trigger('languagechange'); 26964 } 26965 } 26966 } 26967 26968 /** 26969 * Get the player's language dictionary 26970 * Merge every time, because a newly added plugin might call videojs.addLanguage() at any time 26971 * Languages specified directly in the player options have precedence 26972 * 26973 * @return {Array} 26974 * An array of of supported languages 26975 */ 26976 languages() { 26977 return merge$1(Player.prototype.options_.languages, this.languages_); 26978 } 26979 26980 /** 26981 * returns a JavaScript object representing the current track 26982 * information. **DOES not return it as JSON** 26983 * 26984 * @return {Object} 26985 * Object representing the current of track info 26986 */ 26987 toJSON() { 26988 const options = merge$1(this.options_); 26989 const tracks = options.tracks; 26990 options.tracks = []; 26991 for (let i = 0; i < tracks.length; i++) { 26992 let track = tracks[i]; 26993 26994 // deep merge tracks and null out player so no circular references 26995 track = merge$1(track); 26996 track.player = undefined; 26997 options.tracks[i] = track; 26998 } 26999 return options; 27000 } 27001 27002 /** 27003 * Creates a simple modal dialog (an instance of the {@link ModalDialog} 27004 * component) that immediately overlays the player with arbitrary 27005 * content and removes itself when closed. 27006 * 27007 * @param {string|Function|Element|Array|null} content 27008 * Same as {@link ModalDialog#content}'s param of the same name. 27009 * The most straight-forward usage is to provide a string or DOM 27010 * element. 27011 * 27012 * @param {Object} [options] 27013 * Extra options which will be passed on to the {@link ModalDialog}. 27014 * 27015 * @return {ModalDialog} 27016 * the {@link ModalDialog} that was created 27017 */ 27018 createModal(content, options) { 27019 options = options || {}; 27020 options.content = content || ''; 27021 const modal = new ModalDialog(this, options); 27022 this.addChild(modal); 27023 modal.on('dispose', () => { 27024 this.removeChild(modal); 27025 }); 27026 modal.open(); 27027 return modal; 27028 } 27029 27030 /** 27031 * Change breakpoint classes when the player resizes. 27032 * 27033 * @private 27034 */ 27035 updateCurrentBreakpoint_() { 27036 if (!this.responsive()) { 27037 return; 27038 } 27039 const currentBreakpoint = this.currentBreakpoint(); 27040 const currentWidth = this.currentWidth(); 27041 for (let i = 0; i < BREAKPOINT_ORDER.length; i++) { 27042 const candidateBreakpoint = BREAKPOINT_ORDER[i]; 27043 const maxWidth = this.breakpoints_[candidateBreakpoint]; 27044 if (currentWidth <= maxWidth) {
vendor: 5,631 bytes, lines 27045-27218
27045 // The current breakpoint did not change, nothing to do. 27046 if (currentBreakpoint === candidateBreakpoint) { 27047 return; 27048 } 27049 27050 // Only remove a class if there is a current breakpoint. 27051 if (currentBreakpoint) { 27052 this.removeClass(BREAKPOINT_CLASSES[currentBreakpoint]); 27053 } 27054 this.addClass(BREAKPOINT_CLASSES[candidateBreakpoint]); 27055 this.breakpoint_ = candidateBreakpoint; 27056 break; 27057 } 27058 } 27059 } 27060 27061 /** 27062 * Removes the current breakpoint. 27063 * 27064 * @private 27065 */ 27066 removeCurrentBreakpoint_() { 27067 const className = this.currentBreakpointClass(); 27068 this.breakpoint_ = ''; 27069 if (className) { 27070 this.removeClass(className); 27071 } 27072 } 27073 27074 /** 27075 * Get or set breakpoints on the player. 27076 * 27077 * Calling this method with an object or `true` will remove any previous 27078 * custom breakpoints and start from the defaults again. 27079 * 27080 * @param {Object|boolean} [breakpoints] 27081 * If an object is given, it can be used to provide custom 27082 * breakpoints. If `true` is given, will set default breakpoints. 27083 * If this argument is not given, will simply return the current 27084 * breakpoints. 27085 * 27086 * @param {number} [breakpoints.tiny] 27087 * The maximum width for the "vjs-layout-tiny" class. 27088 * 27089 * @param {number} [breakpoints.xsmall] 27090 * The maximum width for the "vjs-layout-x-small" class. 27091 * 27092 * @param {number} [breakpoints.small] 27093 * The maximum width for the "vjs-layout-small" class. 27094 * 27095 * @param {number} [breakpoints.medium] 27096 * The maximum width for the "vjs-layout-medium" class. 27097 * 27098 * @param {number} [breakpoints.large] 27099 * The maximum width for the "vjs-layout-large" class. 27100 * 27101 * @param {number} [breakpoints.xlarge] 27102 * The maximum width for the "vjs-layout-x-large" class. 27103 * 27104 * @param {number} [breakpoints.huge] 27105 * The maximum width for the "vjs-layout-huge" class. 27106 * 27107 * @return {Object} 27108 * An object mapping breakpoint names to maximum width values. 27109 */ 27110 breakpoints(breakpoints) { 27111 // Used as a getter. 27112 if (breakpoints === undefined) { 27113 return Object.assign(this.breakpoints_); 27114 } 27115 this.breakpoint_ = ''; 27116 this.breakpoints_ = Object.assign({}, DEFAULT_BREAKPOINTS, breakpoints); 27117 27118 // When breakpoint definitions change, we need to update the currently 27119 // selected breakpoint. 27120 this.updateCurrentBreakpoint_(); 27121 27122 // Clone the breakpoints before returning. 27123 return Object.assign(this.breakpoints_); 27124 } 27125 27126 /** 27127 * Get or set a flag indicating whether or not this player should adjust 27128 * its UI based on its dimensions. 27129 * 27130 * @param {boolean} [value] 27131 * Should be `true` if the player should adjust its UI based on its 27132 * dimensions; otherwise, should be `false`. 27133 * 27134 * @return {boolean|undefined} 27135 * Will be `true` if this player should adjust its UI based on its 27136 * dimensions; otherwise, will be `false`. 27137 * Nothing if setting 27138 */ 27139 responsive(value) { 27140 // Used as a getter. 27141 if (value === undefined) { 27142 return this.responsive_; 27143 } 27144 value = Boolean(value); 27145 const current = this.responsive_; 27146 27147 // Nothing changed. 27148 if (value === current) { 27149 return; 27150 } 27151 27152 // The value actually changed, set it. 27153 this.responsive_ = value; 27154 27155 // Start listening for breakpoints and set the initial breakpoint if the 27156 // player is now responsive. 27157 if (value) { 27158 this.on('playerresize', this.boundUpdateCurrentBreakpoint_); 27159 this.updateCurrentBreakpoint_(); 27160 27161 // Stop listening for breakpoints if the player is no longer responsive. 27162 } else { 27163 this.off('playerresize', this.boundUpdateCurrentBreakpoint_); 27164 this.removeCurrentBreakpoint_(); 27165 } 27166 return value; 27167 } 27168 27169 /** 27170 * Get current breakpoint name, if any. 27171 * 27172 * @return {string} 27173 * If there is currently a breakpoint set, returns a the key from the 27174 * breakpoints object matching it. Otherwise, returns an empty string. 27175 */ 27176 currentBreakpoint() { 27177 return this.breakpoint_; 27178 } 27179 27180 /** 27181 * Get the current breakpoint class name. 27182 * 27183 * @return {string} 27184 * The matching class name (e.g. `"vjs-layout-tiny"` or 27185 * `"vjs-layout-large"`) for the current breakpoint. Empty string if 27186 * there is no current breakpoint. 27187 */ 27188 currentBreakpointClass() { 27189 return BREAKPOINT_CLASSES[this.breakpoint_] || ''; 27190 } 27191 27192 /** 27193 * An object that describes a single piece of media. 27194 * 27195 * Properties that are not part of this type description will be retained; so, 27196 * this can be viewed as a generic metadata storage mechanism as well. 27197 * 27198 * @see {@link https://wicg.github.io/mediasession/#the-mediametadata-interface} 27199 * @typedef {Object} Player~MediaObject 27200 * 27201 * @property {string} [album] 27202 * Unused, except if this object is passed to the `MediaSession` 27203 * API. 27204 * 27205 * @property {string} [artist] 27206 * Unused, except if this object is passed to the `MediaSession` 27207 * API. 27208 * 27209 * @property {Object[]} [artwork] 27210 * Unused, except if this object is passed to the `MediaSession` 27211 * API. If not specified, will be populated via the `poster`, if 27212 * available. 27213 * 27214 * @property {string} [poster] 27215 * URL to an image that will display before playback. 27216 * 27217 * @property {Tech~SourceObject|Tech~SourceObject[]|string} [src] 27218 * A single source object, an array of source objects, or a string
27219 * referencing a URL to a media source. It is _highly recommended_ 27220 * that an object or array of objects is used here, so that source 27221 * selection algorithms can take the `type` into account. 27222 * 27223 * @property {string} [title] 27224 * Unused, except if this object is passed to the `MediaSession` 27225 * API. 27226 * 27227 * @property {Object[]} [textTracks] 27228 * An array of objects to be used to create text tracks, following 27229 * the {@link https://www.w3.org/TR/html50/embedded-content-0.html#the-track-element|native track element format}. 27230 * For ease of removal, these will be created as "remote" text 27231 * tracks and set to automatically clean up on source changes. 27232 * 27233 * These objects may have properties like `src`, `kind`, `label`, 27234 * and `language`, see {@link Tech#createRemoteTextTrack}. 27235 */ 27236 27237 /** 27238 * Populate the player using a {@link Player~MediaObject|MediaObject}. 27239 * 27240 * @param {Player~MediaObject} media 27241 * A media object. 27242 * 27243 * @param {Function} ready 27244 * A callback to be called when the player is ready. 27245 */ 27246 loadMedia(media, ready) { 27247 if (!media || typeof media !== 'object') { 27248 return; 27249 } 27250 const crossOrigin = this.crossOrigin(); 27251 this.reset(); 27252 27253 // Clone the media object so it cannot be mutated from outside. 27254 this.cache_.media = merge$1(media); 27255 const { artist, artwork, description, poster, src, textTracks, title } = this.cache_.media; 27256 27257 // If `artwork` is not given, create it using `poster`. 27258 if (!artwork && poster) { 27259 this.cache_.media.artwork = [ 27260 { 27261 src: poster, 27262 type: getMimetype(poster), 27263 }, 27264 ]; 27265 } 27266 if (crossOrigin) { 27267 this.crossOrigin(crossOrigin); 27268 } 27269 if (src) { 27270 this.src(src); 27271 } 27272 if (poster) { 27273 this.poster(poster); 27274 } 27275 if (Array.isArray(textTracks)) { 27276 textTracks.forEach((tt) => this.addRemoteTextTrack(tt, false)); 27277 } 27278 if (this.titleBar) { 27279 this.titleBar.update({ 27280 title, 27281 description: description || artist || '', 27282 }); 27283 } 27284 this.ready(ready); 27285 } 27286 27287 /** 27288 * Get a clone of the current {@link Player~MediaObject} for this player. 27289 * 27290 * If the `loadMedia` method has not been used, will attempt to return a 27291 * {@link Player~MediaObject} based on the current state of the player. 27292 * 27293 * @return {Player~MediaObject} 27294 */ 27295 getMedia() { 27296 if (!this.cache_.media) { 27297 const poster = this.poster(); 27298 const src = this.currentSources(); 27299 const textTracks = Array.prototype.map.call(this.remoteTextTracks(), (tt) => ({ 27300 kind: tt.kind, 27301 label: tt.label, 27302 language: tt.language, 27303 src: tt.src, 27304 })); 27305 const media = { 27306 src, 27307 textTracks, 27308 }; 27309 if (poster) { 27310 media.poster = poster; 27311 media.artwork = [ 27312 { 27313 src: media.poster, 27314 type: getMimetype(media.poster), 27315 }, 27316 ]; 27317 } 27318 return media; 27319 } 27320 return merge$1(this.cache_.media); 27321 } 27322 27323 /** 27324 * Gets tag settings 27325 * 27326 * @param {Element} tag 27327 * The player tag 27328 * 27329 * @return {Object} 27330 * An object containing all of the settings 27331 * for a player tag 27332 */ 27333 static getTagSettings(tag) { 27334 const baseOptions = { 27335 sources: [], 27336 tracks: [], 27337 }; 27338 const tagOptions = getAttributes(tag); 27339 const dataSetup = tagOptions['data-setup']; 27340 if (hasClass(tag, 'vjs-fill')) { 27341 tagOptions.fill = true; 27342 } 27343 if (hasClass(tag, 'vjs-fluid')) { 27344 tagOptions.fluid = true; 27345 } 27346 27347 // Check if data-setup attr exists. 27348 if (dataSetup !== null) { 27349 // Parse options JSON 27350 try { 27351 // If empty string, make it a parsable json object. 27352 Object.assign(tagOptions, JSON.parse(dataSetup || '{}')); 27353 } catch (e) { 27354 log$1.error('data-setup', e); 27355 } 27356 } 27357 Object.assign(baseOptions, tagOptions); 27358 27359 // Get tag children settings 27360 if (tag.hasChildNodes()) { 27361 const children = tag.childNodes; 27362 for (let i = 0, j = children.length; i < j; i++) { 27363 const child = children[i]; 27364 // Change case needed: http://ejohn.org/blog/nodename-case-sensitivity/ 27365 const childName = child.nodeName.toLowerCase(); 27366 if (childName === 'source') { 27367 baseOptions.sources.push(getAttributes(child)); 27368 } else if (childName === 'track') { 27369 baseOptions.tracks.push(getAttributes(child)); 27370 } 27371 } 27372 } 27373 return baseOptions; 27374 } 27375 27376 /** 27377 * Set debug mode to enable/disable logs at info level. 27378 * 27379 * @param {boolean} enabled 27380 * @fires Player#debugon 27381 * @fires Player#debugoff 27382 * @return {boolean|undefined} 27383 */ 27384 debug(enabled) { 27385 if (enabled === undefined) { 27386 return this.debugEnabled_; 27387 } 27388 if (enabled) { 27389 this.trigger('debugon'); 27390 this.previousLogLevel_ = this.log.level; 27391 this.log.level('debug'); 27392 this.debugEnabled_ = true; 27393 } else { 27394 this.trigger('debugoff'); 27395 this.log.level(this.previousLogLevel_); 27396 this.previousLogLevel_ = undefined; 27397 this.debugEnabled_ = false;
vendor: 4,984 bytes, lines 27398-27579
27398 } 27399 } 27400 27401 /** 27402 * Set or get current playback rates. 27403 * Takes an array and updates the playback rates menu with the new items. 27404 * Pass in an empty array to hide the menu. 27405 * Values other than arrays are ignored. 27406 * 27407 * @fires Player#playbackrateschange 27408 * @param {number[]} [newRates] 27409 * The new rates that the playback rates menu should update to. 27410 * An empty array will hide the menu 27411 * @return {number[]} When used as a getter will return the current playback rates 27412 */ 27413 playbackRates(newRates) { 27414 if (newRates === undefined) { 27415 return this.cache_.playbackRates; 27416 } 27417 27418 // ignore any value that isn't an array 27419 if (!Array.isArray(newRates)) { 27420 return; 27421 } 27422 27423 // ignore any arrays that don't only contain numbers 27424 if (!newRates.every((rate) => typeof rate === 'number')) { 27425 return; 27426 } 27427 this.cache_.playbackRates = newRates; 27428 27429 /** 27430 * fires when the playback rates in a player are changed 27431 * 27432 * @event Player#playbackrateschange 27433 * @type {Event} 27434 */ 27435 this.trigger('playbackrateschange'); 27436 } 27437 27438 /** 27439 * Reports whether or not a player has a plugin available. 27440 * 27441 * This does not report whether or not the plugin has ever been initialized 27442 * on this player. For that, [usingPlugin]{@link Player#usingPlugin}. 27443 * 27444 * @method hasPlugin 27445 * @param {string} name 27446 * The name of a plugin. 27447 * 27448 * @return {boolean} 27449 * Whether or not this player has the requested plugin available. 27450 */ 27451 27452 /** 27453 * Reports whether or not a player is using a plugin by name. 27454 * 27455 * For basic plugins, this only reports whether the plugin has _ever_ been 27456 * initialized on this player. 27457 * 27458 * @method Player#usingPlugin 27459 * @param {string} name 27460 * The name of a plugin. 27461 * 27462 * @return {boolean} 27463 * Whether or not this player is using the requested plugin. 27464 */ 27465} 27466 27467/** 27468 * Get the {@link VideoTrackList} 27469 * 27470 * @link https://html.spec.whatwg.org/multipage/embedded-content.html#videotracklist 27471 * 27472 * @return {VideoTrackList} 27473 * the current video track list 27474 * 27475 * @method Player.prototype.videoTracks 27476 */ 27477 27478/** 27479 * Get the {@link AudioTrackList} 27480 * 27481 * @link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotracklist 27482 * 27483 * @return {AudioTrackList} 27484 * the current audio track list 27485 * 27486 * @method Player.prototype.audioTracks 27487 */ 27488 27489/** 27490 * Get the {@link TextTrackList} 27491 * 27492 * @link http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-texttracks 27493 * 27494 * @return {TextTrackList} 27495 * the current text track list 27496 * 27497 * @method Player.prototype.textTracks 27498 */ 27499 27500/** 27501 * Get the remote {@link TextTrackList} 27502 * 27503 * @return {TextTrackList} 27504 * The current remote text track list 27505 * 27506 * @method Player.prototype.remoteTextTracks 27507 */ 27508 27509/** 27510 * Get the remote {@link HtmlTrackElementList} tracks. 27511 * 27512 * @return {HtmlTrackElementList} 27513 * The current remote text track element list 27514 * 27515 * @method Player.prototype.remoteTextTrackEls 27516 */ 27517 27518ALL.names.forEach(function (name) { 27519 const props = ALL[name]; 27520 Player.prototype[props.getterName] = function () { 27521 if (this.tech_) { 27522 return this.tech_[props.getterName](); 27523 } 27524 27525 // if we have not yet loadTech_, we create {video,audio,text}Tracks_ 27526 // these will be passed to the tech during loading 27527 this[props.privateName] = this[props.privateName] || new props.ListClass(); 27528 return this[props.privateName]; 27529 }; 27530}); 27531 27532/** 27533 * Get or set the `Player`'s crossorigin option. For the HTML5 player, this 27534 * sets the `crossOrigin` property on the `<video>` tag to control the CORS 27535 * behavior. 27536 * 27537 * @see [Video Element Attributes]{@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#attr-crossorigin} 27538 * 27539 * @param {string} [value] 27540 * The value to set the `Player`'s crossorigin to. If an argument is 27541 * given, must be one of `anonymous` or `use-credentials`. 27542 * 27543 * @return {string|undefined} 27544 * - The current crossorigin value of the `Player` when getting. 27545 * - undefined when setting 27546 */ 27547Player.prototype.crossorigin = Player.prototype.crossOrigin; 27548 27549/** 27550 * Global enumeration of players. 27551 * 27552 * The keys are the player IDs and the values are either the {@link Player} 27553 * instance or `null` for disposed players. 27554 * 27555 * @type {Object} 27556 */ 27557Player.players = {}; 27558const navigator = window__default['default'].navigator; 27559 27560/* 27561 * Player instance options, surfaced using options 27562 * options = Player.prototype.options_ 27563 * Make changes in options, not here. 27564 * 27565 * @type {Object} 27566 * @private 27567 */ 27568Player.prototype.options_ = { 27569 // Default order of fallback technology 27570 techOrder: Tech.defaultTechOrder_, 27571 html5: {}, 27572 // enable sourceset by default 27573 enableSourceset: true, 27574 // default inactivity timeout 27575 inactivityTimeout: 2000, 27576 // default playback rates 27577 playbackRates: [], 27578 // Add playback rate selection by adding rates 27579 // 'playbackRates': [0.5, 1, 1.5, 2],
27580 liveui: false, 27581 // Included control sets 27582 children: [ 27583 'mediaLoader', 27584 'posterImage', 27585 'titleBar', 27586 'textTrackDisplay', 27587 'loadingSpinner', 27588 'bigPlayButton', 27589 'liveTracker', 27590 'controlBar', 27591 'errorDisplay', 27592 'textTrackSettings', 27593 'resizeManager', 27594 ], 27595 language: 27596 (navigator && 27597 ((navigator.languages && navigator.languages[0]) || 27598 navigator.userLanguage || 27599 navigator.language)) || 27600 'en', 27601 // locales and their language translations 27602 languages: {}, 27603 // Default message to show when a video cannot be played. 27604 notSupportedMessage: 'No compatible source was found for this media.', 27605 normalizeAutoplay: false, 27606 fullscreen: { 27607 options: { 27608 navigationUI: 'hide', 27609 }, 27610 }, 27611 breakpoints: {}, 27612 responsive: false, 27613 audioOnlyMode: false, 27614 audioPosterMode: false, 27615 spatialNavigation: { 27616 enabled: false, 27617 horizontalSeek: false, 27618 }, 27619 // Default smooth seeking to false 27620 enableSmoothSeeking: false, 27621}; 27622TECH_EVENTS_RETRIGGER.forEach(function (event) { 27623 Player.prototype[`handleTech${toTitleCase$1(event)}_`] = function () { 27624 return this.trigger(event); 27625 }; 27626}); 27627 27628/** 27629 * Fired when the player has initial duration and dimension information 27630 * 27631 * @event Player#loadedmetadata 27632 * @type {Event} 27633 */ 27634 27635/** 27636 * Fired when the player has downloaded data at the current playback position 27637 * 27638 * @event Player#loadeddata 27639 * @type {Event} 27640 */ 27641 27642/** 27643 * Fired when the current playback position has changed * 27644 * During playback this is fired every 15-250 milliseconds, depending on the 27645 * playback technology in use. 27646 * 27647 * @event Player#timeupdate 27648 * @type {Event} 27649 */ 27650 27651/** 27652 * Fired when the volume changes 27653 * 27654 * @event Player#volumechange 27655 * @type {Event} 27656 */ 27657 27658Component$1.registerComponent('Player', Player); 27659 27660/** 27661 * @file plugin.js 27662 */ 27663 27664/** 27665 * The base plugin name. 27666 * 27667 * @private 27668 * @constant 27669 * @type {string} 27670 */ 27671const BASE_PLUGIN_NAME = 'plugin'; 27672 27673/** 27674 * The key on which a player's active plugins cache is stored. 27675 * 27676 * @private 27677 * @constant 27678 * @type {string} 27679 */ 27680const PLUGIN_CACHE_KEY = 'activePlugins_'; 27681 27682/** 27683 * Stores registered plugins in a private space. 27684 * 27685 * @private 27686 * @type {Object} 27687 */ 27688const pluginStorage = {}; 27689 27690/** 27691 * Reports whether or not a plugin has been registered. 27692 * 27693 * @private 27694 * @param {string} name 27695 * The name of a plugin. 27696 * 27697 * @return {boolean} 27698 * Whether or not the plugin has been registered. 27699 */ 27700const pluginExists = (name) => pluginStorage.hasOwnProperty(name); 27701 27702/** 27703 * Get a single registered plugin by name. 27704 * 27705 * @private 27706 * @param {string} name 27707 * The name of a plugin. 27708 * 27709 * @return {typeof Plugin|Function|undefined} 27710 * The plugin (or undefined). 27711 */ 27712const getPlugin = (name) => (pluginExists(name) ? pluginStorage[name] : undefined); 27713 27714/** 27715 * Marks a plugin as "active" on a player. 27716 * 27717 * Also, ensures that the player has an object for tracking active plugins. 27718 * 27719 * @private 27720 * @param {Player} player 27721 * A Video.js player instance. 27722 * 27723 * @param {string} name 27724 * The name of a plugin. 27725 */ 27726const markPluginAsActive = (player, name) => { 27727 player[PLUGIN_CACHE_KEY] = player[PLUGIN_CACHE_KEY] || {}; 27728 player[PLUGIN_CACHE_KEY][name] = true; 27729}; 27730 27731/** 27732 * Triggers a pair of plugin setup events. 27733 * 27734 * @private 27735 * @param {Player} player 27736 * A Video.js player instance. 27737 * 27738 * @param {PluginEventHash} hash 27739 * A plugin event hash. 27740 * 27741 * @param {boolean} [before] 27742 * If true, prefixes the event name with "before". In other words, 27743 * use this to trigger "beforepluginsetup" instead of "pluginsetup". 27744 */ 27745const triggerSetupEvent = (player, hash, before) => { 27746 const eventName = (before ? 'before' : '') + 'pluginsetup'; 27747 player.trigger(eventName, hash); 27748 player.trigger(eventName + ':' + hash.name, hash); 27749}; 27750 27751/** 27752 * Takes a basic plugin function and returns a wrapper function which marks 27753 * on the player that the plugin has been activated. 27754 * 27755 * @private 27756 * @param {string} name 27757 * The name of the plugin. 27758 * 27759 * @param {Function} plugin 27760 * The basic plugin. 27761 * 27762 * @return {Function} 27763 * A wrapper function for the given plugin. 27764 */ 27765const createBasicPlugin = function (name, plugin) { 27766 const basicPluginWrapper = function () { 27767 // We trigger the "beforepluginsetup" and "pluginsetup" events on the player 27768 // regardless, but we want the hash to be consistent with the hash provided 27769 // for advanced plugins. 27770 // 27771 // The only potentially counter-intuitive thing here is the `instance` in 27772 // the "pluginsetup" event is the value returned by the `plugin` function. 27773 triggerSetupEvent( 27774 this, 27775 { 27776 name, 27777 plugin, 27778 instance: null, 27779 }, 27780 true 27781 ); 27782 const instance = plugin.apply(this, arguments); 27783 markPluginAsActive(this, name); 27784 triggerSetupEvent(this, { 27785 name, 27786 plugin, 27787 instance, 27788 }); 27789 return instance; 27790 };
vendor: 7,923 bytes, lines 27791-28053
27791 Object.keys(plugin).forEach(function (prop) { 27792 basicPluginWrapper[prop] = plugin[prop]; 27793 }); 27794 return basicPluginWrapper; 27795}; 27796 27797/** 27798 * Takes a plugin sub-class and returns a factory function for generating 27799 * instances of it. 27800 * 27801 * This factory function will replace itself with an instance of the requested 27802 * sub-class of Plugin. 27803 * 27804 * @private 27805 * @param {string} name 27806 * The name of the plugin. 27807 * 27808 * @param {Plugin} PluginSubClass 27809 * The advanced plugin. 27810 * 27811 * @return {Function} 27812 */ 27813const createPluginFactory = (name, PluginSubClass) => { 27814 // Add a `name` property to the plugin prototype so that each plugin can 27815 // refer to itself by name. 27816 PluginSubClass.prototype.name = name; 27817 return function (...args) { 27818 triggerSetupEvent( 27819 this, 27820 { 27821 name, 27822 plugin: PluginSubClass, 27823 instance: null, 27824 }, 27825 true 27826 ); 27827 const instance = new PluginSubClass(...[this, ...args]); 27828 27829 // The plugin is replaced by a function that returns the current instance. 27830 this[name] = () => instance; 27831 triggerSetupEvent(this, instance.getEventHash()); 27832 return instance; 27833 }; 27834}; 27835 27836/** 27837 * Parent class for all advanced plugins. 27838 * 27839 * @mixes module:evented~EventedMixin 27840 * @mixes module:stateful~StatefulMixin 27841 * @fires Player#beforepluginsetup 27842 * @fires Player#beforepluginsetup:$name 27843 * @fires Player#pluginsetup 27844 * @fires Player#pluginsetup:$name 27845 * @listens Player#dispose 27846 * @throws {Error} 27847 * If attempting to instantiate the base {@link Plugin} class 27848 * directly instead of via a sub-class. 27849 */ 27850class Plugin { 27851 /** 27852 * Creates an instance of this class. 27853 * 27854 * Sub-classes should call `super` to ensure plugins are properly initialized. 27855 * 27856 * @param {Player} player 27857 * A Video.js player instance. 27858 */ 27859 constructor(player) { 27860 if (this.constructor === Plugin) { 27861 throw new Error('Plugin must be sub-classed; not directly instantiated.'); 27862 } 27863 this.player = player; 27864 if (!this.log) { 27865 this.log = this.player.log.createLogger(this.name); 27866 } 27867 27868 // Make this object evented, but remove the added `trigger` method so we 27869 // use the prototype version instead. 27870 evented(this); 27871 delete this.trigger; 27872 stateful(this, this.constructor.defaultState); 27873 markPluginAsActive(player, this.name); 27874 27875 // Auto-bind the dispose method so we can use it as a listener and unbind 27876 // it later easily. 27877 this.dispose = this.dispose.bind(this); 27878 27879 // If the player is disposed, dispose the plugin. 27880 player.on('dispose', this.dispose); 27881 } 27882 27883 /** 27884 * Get the version of the plugin that was set on <pluginName>.VERSION 27885 */ 27886 version() { 27887 return this.constructor.VERSION; 27888 } 27889 27890 /** 27891 * Each event triggered by plugins includes a hash of additional data with 27892 * conventional properties. 27893 * 27894 * This returns that object or mutates an existing hash. 27895 * 27896 * @param {Object} [hash={}] 27897 * An object to be used as event an event hash. 27898 * 27899 * @return {PluginEventHash} 27900 * An event hash object with provided properties mixed-in. 27901 */ 27902 getEventHash(hash = {}) { 27903 hash.name = this.name; 27904 hash.plugin = this.constructor; 27905 hash.instance = this; 27906 return hash; 27907 } 27908 27909 /** 27910 * Triggers an event on the plugin object and overrides 27911 * {@link module:evented~EventedMixin.trigger|EventedMixin.trigger}. 27912 * 27913 * @param {string|Object} event 27914 * An event type or an object with a type property. 27915 * 27916 * @param {Object} [hash={}] 27917 * Additional data hash to merge with a 27918 * {@link PluginEventHash|PluginEventHash}. 27919 * 27920 * @return {boolean} 27921 * Whether or not default was prevented. 27922 */ 27923 trigger(event, hash = {}) { 27924 return trigger(this.eventBusEl_, event, this.getEventHash(hash)); 27925 } 27926 27927 /** 27928 * Handles "statechanged" events on the plugin. No-op by default, override by 27929 * subclassing. 27930 * 27931 * @abstract 27932 * @param {Event} e 27933 * An event object provided by a "statechanged" event. 27934 * 27935 * @param {Object} e.changes 27936 * An object describing changes that occurred with the "statechanged" 27937 * event. 27938 */ 27939 handleStateChanged(e) {} 27940 27941 /** 27942 * Disposes a plugin. 27943 * 27944 * Subclasses can override this if they want, but for the sake of safety, 27945 * it's probably best to subscribe the "dispose" event. 27946 * 27947 * @fires Plugin#dispose 27948 */ 27949 dispose() { 27950 const { name, player } = this; 27951 27952 /** 27953 * Signals that a advanced plugin is about to be disposed. 27954 * 27955 * @event Plugin#dispose 27956 * @type {Event} 27957 */ 27958 this.trigger('dispose'); 27959 this.off(); 27960 player.off('dispose', this.dispose); 27961 27962 // Eliminate any possible sources of leaking memory by clearing up 27963 // references between the player and the plugin instance and nulling out 27964 // the plugin's state and replacing methods with a function that throws. 27965 player[PLUGIN_CACHE_KEY][name] = false; 27966 this.player = this.state = null; 27967 27968 // Finally, replace the plugin name on the player with a new factory 27969 // function, so that the plugin is ready to be set up again. 27970 player[name] = createPluginFactory(name, pluginStorage[name]); 27971 } 27972 27973 /** 27974 * Determines if a plugin is a basic plugin (i.e. not a sub-class of `Plugin`). 27975 * 27976 * @param {string|Function} plugin 27977 * If a string, matches the name of a plugin. If a function, will be 27978 * tested directly. 27979 * 27980 * @return {boolean} 27981 * Whether or not a plugin is a basic plugin. 27982 */ 27983 static isBasic(plugin) { 27984 const p = typeof plugin === 'string' ? getPlugin(plugin) : plugin; 27985 return typeof p === 'function' && !Plugin.prototype.isPrototypeOf(p.prototype); 27986 } 27987 27988 /** 27989 * Register a Video.js plugin. 27990 * 27991 * @param {string} name 27992 * The name of the plugin to be registered. Must be a string and 27993 * must not match an existing plugin or a method on the `Player` 27994 * prototype. 27995 * 27996 * @param {typeof Plugin|Function} plugin 27997 * A sub-class of `Plugin` or a function for basic plugins. 27998 * 27999 * @return {typeof Plugin|Function} 28000 * For advanced plugins, a factory function for that plugin. For 28001 * basic plugins, a wrapper function that initializes the plugin. 28002 */ 28003 static registerPlugin(name, plugin) { 28004 if (typeof name !== 'string') { 28005 throw new Error(`Illegal plugin name, "${name}", must be a string, was ${typeof name}.`); 28006 } 28007 if (pluginExists(name)) { 28008 log$1.warn( 28009 `A plugin named "${name}" already exists. You may want to avoid re-registering plugins!` 28010 ); 28011 } else if (Player.prototype.hasOwnProperty(name)) { 28012 throw new Error( 28013 `Illegal plugin name, "${name}", cannot share a name with an existing player method!` 28014 ); 28015 } 28016 if (typeof plugin !== 'function') { 28017 throw new Error(`Illegal plugin for "${name}", must be a function, was ${typeof plugin}.`); 28018 } 28019 pluginStorage[name] = plugin; 28020 28021 // Add a player prototype method for all sub-classed plugins (but not for 28022 // the base Plugin class). 28023 if (name !== BASE_PLUGIN_NAME) { 28024 if (Plugin.isBasic(plugin)) { 28025 Player.prototype[name] = createBasicPlugin(name, plugin); 28026 } else { 28027 Player.prototype[name] = createPluginFactory(name, plugin); 28028 } 28029 } 28030 return plugin; 28031 } 28032 28033 /** 28034 * De-register a Video.js plugin. 28035 * 28036 * @param {string} name 28037 * The name of the plugin to be de-registered. Must be a string that 28038 * matches an existing plugin. 28039 * 28040 * @throws {Error} 28041 * If an attempt is made to de-register the base plugin. 28042 */ 28043 static deregisterPlugin(name) { 28044 if (name === BASE_PLUGIN_NAME) { 28045 throw new Error('Cannot de-register base plugin.'); 28046 } 28047 if (pluginExists(name)) { 28048 delete pluginStorage[name]; 28049 delete Player.prototype[name]; 28050 } 28051 } 28052 28053 /**
28054 * Gets an object containing multiple Video.js plugins. 28055 * 28056 * @param {Array} [names] 28057 * If provided, should be an array of plugin names. Defaults to _all_ 28058 * plugin names. 28059 * 28060 * @return {Object|undefined} 28061 * An object containing plugin(s) associated with their name(s) or 28062 * `undefined` if no matching plugins exist). 28063 */ 28064 static getPlugins(names = Object.keys(pluginStorage)) { 28065 let result; 28066 names.forEach((name) => { 28067 const plugin = getPlugin(name); 28068 if (plugin) { 28069 result = result || {}; 28070 result[name] = plugin; 28071 } 28072 }); 28073 return result; 28074 } 28075 28076 /** 28077 * Gets a plugin's version, if available 28078 * 28079 * @param {string} name 28080 * The name of a plugin. 28081 * 28082 * @return {string} 28083 * The plugin's version or an empty string. 28084 */ 28085 static getPluginVersion(name) { 28086 const plugin = getPlugin(name); 28087 return (plugin && plugin.VERSION) || ''; 28088 } 28089} 28090 28091/** 28092 * Gets a plugin by name if it exists. 28093 * 28094 * @static 28095 * @method getPlugin 28096 * @memberOf Plugin 28097 * @param {string} name 28098 * The name of a plugin. 28099 * 28100 * @returns {typeof Plugin|Function|undefined} 28101 * The plugin (or `undefined`). 28102 */ 28103Plugin.getPlugin = getPlugin; 28104 28105/** 28106 * The name of the base plugin class as it is registered. 28107 * 28108 * @type {string} 28109 */ 28110Plugin.BASE_PLUGIN_NAME = BASE_PLUGIN_NAME; 28111Plugin.registerPlugin(BASE_PLUGIN_NAME, Plugin); 28112 28113/** 28114 * Documented in player.js 28115 * 28116 * @ignore 28117 */ 28118Player.prototype.usingPlugin = function (name) { 28119 return !!this[PLUGIN_CACHE_KEY] && this[PLUGIN_CACHE_KEY][name] === true; 28120}; 28121 28122/** 28123 * Documented in player.js 28124 * 28125 * @ignore 28126 */ 28127Player.prototype.hasPlugin = function (name) { 28128 return !!pluginExists(name); 28129}; 28130 28131/** 28132 * Signals that a plugin is about to be set up on a player. 28133 * 28134 * @event Player#beforepluginsetup 28135 * @type {PluginEventHash} 28136 */ 28137 28138/** 28139 * Signals that a plugin is about to be set up on a player - by name. The name 28140 * is the name of the plugin. 28141 * 28142 * @event Player#beforepluginsetup:$name 28143 * @type {PluginEventHash} 28144 */ 28145 28146/** 28147 * Signals that a plugin has just been set up on a player. 28148 * 28149 * @event Player#pluginsetup 28150 * @type {PluginEventHash} 28151 */ 28152 28153/** 28154 * Signals that a plugin has just been set up on a player - by name. The name 28155 * is the name of the plugin. 28156 * 28157 * @event Player#pluginsetup:$name 28158 * @type {PluginEventHash} 28159 */ 28160 28161/** 28162 * @typedef {Object} PluginEventHash 28163 * 28164 * @property {string} instance 28165 * For basic plugins, the return value of the plugin function. For 28166 * advanced plugins, the plugin instance on which the event is fired. 28167 * 28168 * @property {string} name 28169 * The name of the plugin. 28170 * 28171 * @property {string} plugin 28172 * For basic plugins, the plugin function. For advanced plugins, the 28173 * plugin class/constructor. 28174 */ 28175 28176/** 28177 * @file deprecate.js 28178 * @module deprecate 28179 */ 28180 28181/** 28182 * Decorate a function with a deprecation message the first time it is called. 28183 * 28184 * @param {string} message 28185 * A deprecation message to log the first time the returned function 28186 * is called. 28187 * 28188 * @param {Function} fn 28189 * The function to be deprecated. 28190 * 28191 * @return {Function} 28192 * A wrapper function that will log a deprecation warning the first 28193 * time it is called. The return value will be the return value of 28194 * the wrapped function. 28195 */ 28196function deprecate(message, fn) { 28197 let warned = false; 28198 return function (...args) { 28199 if (!warned) { 28200 log$1.warn(message); 28201 } 28202 warned = true; 28203 return fn.apply(this, args); 28204 }; 28205} 28206 28207/** 28208 * Internal function used to mark a function as deprecated in the next major 28209 * version with consistent messaging. 28210 * 28211 * @param {number} major The major version where it will be removed 28212 * @param {string} oldName The old function name 28213 * @param {string} newName The new function name 28214 * @param {Function} fn The function to deprecate 28215 * @return {Function} The decorated function 28216 */ 28217function deprecateForMajor(major, oldName, newName, fn) { 28218 return deprecate( 28219 `${oldName} is deprecated and will be removed in ${major}.0; please use ${newName} instead.`, 28220 fn 28221 ); 28222} 28223 28224var VjsErrors = { 28225 NetworkBadStatus: 'networkbadstatus', 28226 NetworkRequestFailed: 'networkrequestfailed', 28227 NetworkRequestAborted: 'networkrequestaborted', 28228 NetworkRequestTimeout: 'networkrequesttimeout', 28229 NetworkBodyParserFailed: 'networkbodyparserfailed', 28230 StreamingHlsPlaylistParserError: 'streaminghlsplaylistparsererror', 28231 StreamingDashManifestParserError: 'streamingdashmanifestparsererror', 28232 StreamingContentSteeringParserError: 'streamingcontentsteeringparsererror', 28233 StreamingVttParserError: 'streamingvttparsererror', 28234 StreamingFailedToSelectNextSegment: 'streamingfailedtoselectnextsegment', 28235 StreamingFailedToDecryptSegment: 'streamingfailedtodecryptsegment', 28236 StreamingFailedToTransmuxSegment: 'streamingfailedtotransmuxsegment', 28237 StreamingFailedToAppendSegment: 'streamingfailedtoappendsegment', 28238 StreamingCodecsChangeError: 'streamingcodecschangeerror', 28239}; 28240 28241/** 28242 * @file video.js 28243 * @module videojs 28244 */ 28245 28246/** @import { PlayerReadyCallback } from './player' */ 28247 28248/** 28249 * Normalize an `id` value by trimming off a leading `#` 28250 * 28251 * @private 28252 * @param {string} id 28253 * A string, maybe with a leading `#`. 28254 * 28255 * @return {string} 28256 * The string, without any leading `#`. 28257 */ 28258const normalizeId = (id) => (id.indexOf('#') === 0 ? id.slice(1) : id); 28259 28260/** 28261 * The `videojs()` function doubles as the main function for users to create a 28262 * {@link Player} instance as well as the main library namespace. 28263 * 28264 * It can also be used as a getter for a pre-existing {@link Player} instance. 28265 * However, we _strongly_ recommend using `videojs.getPlayer()` for this 28266 * purpose because it avoids any potential for unintended initialization. 28267 * 28268 * Due to [limitations](https://github.com/jsdoc3/jsdoc/issues/955#issuecomment-313829149) 28269 * of our JSDoc template, we cannot properly document this as both a function 28270 * and a namespace, so its function signature is documented here. 28271 * 28272 * #### Arguments 28273 * ##### id 28274 * string|Element, **required** 28275 * 28276 * Video element or video element ID. 28277 * 28278 * ##### options 28279 * Object, optional 28280 * 28281 * Options object for providing settings. 28282 * See: [Options Guide](https://docs.videojs.com/tutorial-options.html). 28283 * 28284 * ##### ready 28285 * {@link Component~ReadyCallback}, optional 28286 * 28287 * A function to be called when the {@link Player} and {@link Tech} are ready. 28288 * 28289 * #### Return Value 28290 * 28291 * The `videojs()` function returns a {@link Player} instance. 28292 * 28293 * @namespace 28294 * 28295 * @borrows AudioTrack as AudioTrack 28296 * @borrows Component.getComponent as getComponent 28297 * @borrows module:events.on as on 28298 * @borrows module:events.one as one 28299 * @borrows module:events.off as off 28300 * @borrows module:events.trigger as trigger 28301 * @borrows EventTarget as EventTarget 28302 * @borrows module:middleware.use as use 28303 * @borrows Player.players as players 28304 * @borrows Plugin.registerPlugin as registerPlugin 28305 * @borrows Plugin.deregisterPlugin as deregisterPlugin 28306 * @borrows Plugin.getPlugins as getPlugins 28307 * @borrows Plugin.getPlugin as getPlugin 28308 * @borrows Plugin.getPluginVersion as getPluginVersion 28309 * @borrows Tech.getTech as getTech 28310 * @borrows Tech.registerTech as registerTech 28311 * @borrows TextTrack as TextTrack 28312 * @borrows VideoTrack as VideoTrack 28313 * 28314 * @param {string|Element}
28314 id 28315 * Video element or video element ID. 28316 * 28317 * @param {Object} [options] 28318 * Options object for providing settings. 28319 * See: [Options Guide](https://docs.videojs.com/tutorial-options.html). 28320 * 28321 * @param {PlayerReadyCallback} [ready] 28322 * A function to be called when the {@link Player} and {@link Tech} are 28323 * ready. 28324 * 28325 * @return {Player} 28326 * The `videojs()` function returns a {@link Player|Player} instance. 28327 */ 28328function videojs(id, options, ready) { 28329 let player = videojs.getPlayer(id); 28330 if (player) { 28331 if (options) { 28332 log$1.warn(`Player "${id}" is already initialised. Options will not be applied.`); 28333 } 28334 if (ready) { 28335 player.ready(ready); 28336 } 28337 return player; 28338 } 28339 const el = typeof id === 'string' ? $('#' + normalizeId(id)) : id; 28340 if (!isEl(el)) { 28341 throw new TypeError('The element or ID supplied is not valid. (videojs)'); 28342 } 28343 28344 // document.body.contains(el) will only check if el is contained within that one document. 28345 // This causes problems for elements in iframes. 28346 // Instead, use the element's ownerDocument instead of the global document. 28347 // This will make sure that the element is indeed in the dom of that document. 28348 // Additionally, check that the document in question has a default view. 28349 // If the document is no longer attached to the dom, the defaultView of the document will be null. 28350 // If element is inside Shadow DOM (e.g. is part of a Custom element), ownerDocument.body 28351 // always returns false. Instead, use the Shadow DOM root. 28352 const inShadowDom = 28353 'getRootNode' in el ? el.getRootNode() instanceof window__default['default'].ShadowRoot : false; 28354 const rootNode = inShadowDom ? el.getRootNode() : el.ownerDocument.body; 28355 if (!el.ownerDocument.defaultView || !rootNode.contains(el)) { 28356 log$1.warn('The element supplied is not included in the DOM'); 28357 } 28358 options = options || {}; 28359 28360 // Store a copy of the el before modification, if it is to be restored in destroy() 28361 // If div ingest, store the parent div 28362 if (options.restoreEl === true) { 28363 options.restoreEl = ( 28364 el.parentNode && el.parentNode.hasAttribute && el.parentNode.hasAttribute('data-vjs-player') 28365 ? el.parentNode 28366 : el 28367 ).cloneNode(true); 28368 } 28369 hooks('beforesetup').forEach((hookFunction) => { 28370 const opts = hookFunction(el, merge$1(options)); 28371 if (!isObject(opts) || Array.isArray(opts)) { 28372 log$1.error('please return an object in beforesetup hooks'); 28373 return; 28374 } 28375 options = merge$1(options, opts); 28376 }); 28377 28378 // We get the current "Player" component here in case an integration has 28379 // replaced it with a custom player. 28380 const PlayerComponent = Component$1.getComponent('Player'); 28381 player = new PlayerComponent(el, options, ready); 28382 hooks('setup').forEach((hookFunction) => hookFunction(player)); 28383 return player; 28384} 28385videojs.hooks_ = hooks_; 28386videojs.hooks = hooks; 28387videojs.hook = hook; 28388videojs.hookOnce = hookOnce; 28389videojs.removeHook = removeHook; 28390 28391// Add default styles 28392if (window__default['default'].VIDEOJS_NO_DYNAMIC_STYLE !== true && isReal()) { 28393 let style = $('.vjs-styles-defaults'); 28394 if (!style) { 28395 style = createStyleElement('vjs-styles-defaults'); 28396 const head = $('head'); 28397 if (head) { 28398 head.insertBefore(style, head.firstChild); 28399 } 28400 setTextContent( 28401 style, 28402 ` 28403 .video-js { 28404 width: 300px; 28405 height: 150px; 28406 } 28407 28408 .vjs-fluid:not(.vjs-audio-only-mode) { 28409 padding-top: 56.25% 28410 } 28411 ` 28412 ); 28413 } 28414} 28415 28416// Run Auto-load players 28417// You have to wait at least once in case this script is loaded after your 28418// video in the DOM (weird behavior only with minified version) 28419autoSetupTimeout(1, videojs); 28420 28421/** 28422 * Current Video.js version. Follows [semantic versioning](https://semver.org/). 28423 * 28424 * @type {string} 28425 */ 28426videojs.VERSION = version$6; 28427 28428/** 28429 * The global options object. These are the settings that take effect 28430 * if no overrides are specified when the player is created. 28431 * 28432 * @type {Object} 28433 */ 28434videojs.options = Player.prototype.options_; 28435 28436/** 28437 * Get an object with the currently created players, keyed by player ID 28438 * 28439 * @return {Object} 28440 * The created players 28441 */ 28442videojs.getPlayers = () => Player.players; 28443 28444/** 28445 * Get a single player based on an ID or DOM element. 28446 * 28447 * This is useful if you want to check if an element or ID has an associated 28448 * Video.js player, but not create one if it doesn't. 28449 * 28450 * @param {string|Element}
28450 id 28451 * An HTML element - `<video>`, `<audio>`, or `<video-js>` - 28452 * or a string matching the `id` of such an element. 28453 * 28454 * @return {Player|undefined} 28455 * A player instance or `undefined` if there is no player instance 28456 * matching the argument. 28457 */ 28458videojs.getPlayer = (id) => { 28459 const players = Player.players; 28460 let tag; 28461 if (typeof id === 'string') { 28462 const nId = normalizeId(id); 28463 const player = players[nId]; 28464 if (player) { 28465 return player; 28466 } 28467 tag = $('#' + nId); 28468 } else { 28469 tag = id; 28470 } 28471 if (isEl(tag)) { 28472 const { player, playerId } = tag; 28473 28474 // Element may have a `player` property referring to an already created 28475 // player instance. If so, return that. 28476 if (player || players[playerId]) { 28477 return player || players[playerId]; 28478 } 28479 } 28480}; 28481 28482/** 28483 * Returns an array of all current players. 28484 * 28485 * @return {Array} 28486 * An array of all players. The array will be in the order that 28487 * `Object.keys` provides, which could potentially vary between 28488 * JavaScript engines. 28489 * 28490 */ 28491videojs.getAllPlayers = () => 28492 // Disposed players leave a key with a `null` value, so we need to make sure 28493 // we filter those out. 28494 Object.keys(Player.players) 28495 .map((k) => Player.players[k]) 28496 .filter(Boolean); 28497videojs.players = Player.players; 28498videojs.getComponent = Component$1.getComponent; 28499 28500/** 28501 * Register a component so it can referred to by name. Used when adding to other 28502 * components, either through addChild `component.addChild('myComponent')` or through 28503 * default children options `{ children: ['myComponent'] }`. 28504 * 28505 * > NOTE: You could also just initialize the component before adding. 28506 * `component.addChild(new MyComponent());` 28507 * 28508 * @param {string} name 28509 * The class name of the component 28510 * 28511 * @param {typeof Component} comp 28512 * The component class 28513 * 28514 * @return {typeof Component} 28515 * The newly registered component 28516 */ 28517videojs.registerComponent = (name, comp) => { 28518 if (Tech.isTech(comp)) { 28519 log$1.warn( 28520 `The ${name} tech was registered as a component. It should instead be registered using videojs.registerTech(name, tech)` 28521 ); 28522 } 28523 return Component$1.registerComponent.call(Component$1, name, comp); 28524}; 28525videojs.getTech = Tech.getTech; 28526videojs.registerTech = Tech.registerTech; 28527videojs.use = use; 28528 28529/** 28530 * An object that can be returned by a middleware to signify 28531 * that the middleware is being terminated. 28532 * 28533 * @type {object} 28534 * @property {object} middleware.TERMINATOR 28535 */ 28536Object.defineProperty(videojs, 'middleware', { 28537 value: {}, 28538 writeable: false, 28539 enumerable: true, 28540}); 28541Object.defineProperty(videojs.middleware, 'TERMINATOR', { 28542 value: TERMINATOR, 28543 writeable: false, 28544 enumerable: true, 28545}); 28546 28547/** 28548 * A reference to the {@link module:browser|browser utility module} as an object. 28549 * 28550 * @type {Object} 28551 * @see {@link module:browser|browser} 28552 */ 28553videojs.browser = browser; 28554 28555/** 28556 * A reference to the {@link module:obj|obj utility module} as an object. 28557 * 28558 * @type {Object} 28559 * @see {@link module:obj|obj} 28560 */ 28561videojs.obj = Obj; 28562 28563/** 28564 * Deprecated reference to the {@link module:obj.merge|merge function} 28565 * 28566 * @type {Function} 28567 * @see {@link module:obj.merge|merge} 28568 * @deprecated Deprecated and will be removed in 9.0. Please use videojs.obj.merge instead. 28569 */ 28570videojs.mergeOptions = deprecateForMajor(9, 'videojs.mergeOptions', 'videojs.obj.merge', merge$1); 28571 28572/** 28573 * Deprecated reference to the {@link module:obj.defineLazyProperty|defineLazyProperty function} 28574 * 28575 * @type {Function} 28576 * @see {@link module:obj.defineLazyProperty|defineLazyProperty} 28577 * @deprecated Deprecated and will be removed in 9.0. Please use videojs.obj.defineLazyProperty instead. 28578 */ 28579videojs.defineLazyProperty = deprecateForMajor( 28580 9, 28581 'videojs.defineLazyProperty', 28582 'videojs.obj.defineLazyProperty', 28583 defineLazyProperty 28584); 28585 28586/** 28587 * Deprecated reference to the {@link module:fn.bind_|fn.bind_ function} 28588 * 28589 * @type {Function} 28590 * @see {@link module:fn.bind_|fn.bind_} 28591 * @deprecated Deprecated and will be removed in 9.0. Please use native Function.prototype.bind instead. 28592 */ 28593videojs.bind = deprecateForMajor(9, 'videojs.bind', 'native Function.prototype.bind', bind_); 28594videojs.registerPlugin = Plugin.registerPlugin; 28595videojs.deregisterPlugin = Plugin.deregisterPlugin; 28596 28597/** 28598 * Deprecated method to register a plugin with Video.js 28599 * 28600 * @deprecated Deprecated and will be removed in 9.0. Use videojs.registerPlugin() instead. 28601 * 28602 * @param {string} name 28603 * The plugin name 28604 * 28605 * @param {typeof Plugin|Function} plugin 28606 * The plugin sub-class or function 28607 * 28608 * @return {typeof Plugin|Function} 28609 */ 28610videojs.plugin = (name, plugin) => { 28611 log$1.warn('videojs.plugin() is deprecated; use videojs.registerPlugin() instead'); 28612 return Plugin.registerPlugin(name, plugin); 28613}; 28614videojs.getPlugins = Plugin.getPlugins; 28615videojs.getPlugin = Plugin.getPlugin; 28616videojs.getPluginVersion = Plugin.getPluginVersion; 28617 28618/** 28619 * Adding languages so that they're available to all players. 28620 * Example: `videojs.addLanguage('es', { 'Hello': 'Hola' });` 28621 * 28622 * @param {string} code 28623 * The language code or dictionary property 28624 * 28625 * @param {Object} data 28626 * The data values to be translated 28627 * 28628 * @return {Object} 28629 * The resulting language dictionary object 28630 */ 28631videojs.addLanguage = function (code, data) { 28632 code = ('' + code).toLowerCase(); 28633 videojs.options.languages = merge$1(videojs.options.languages, { 28634 [code]: data, 28635 }); 28636 return videojs.options.languages[code]; 28637}; 28638 28639/** 28640 * A reference to the {@link module:log|log utility module} as an object. 28641 * 28642 * @type {Function} 28643 * @see {@link module:log|log} 28644 */ 28645videojs.log = log$1; 28646videojs.createLogger = createLogger; 28647 28648/** 28649 * A reference to the {@link module:time|time utility module} as an object. 28650 * 28651 * @type {Object} 28652 * @see {@link module:time|time} 28653 */ 28654videojs.time = Time; 28655 28656/** 28657 * Deprecated reference to the {@link module:time.createTimeRanges|createTimeRanges function} 28658 * 28659 * @type {Function} 28660 * @see {@link module:time.createTimeRanges|createTimeRanges} 28661 * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.createTimeRanges instead. 28662 */ 28663videojs.createTimeRange = deprecateForMajor( 28664 9, 28665 'videojs.createTimeRange', 28666 'videojs.time.createTimeRanges', 28667 createTimeRanges$1 28668); 28669 28670/** 28671 * Deprecated reference to the {@link module:time.createTimeRanges|createTimeRanges function} 28672 * 28673 * @type {Function} 28674 * @see {@link module:time.createTimeRanges|createTimeRanges} 28675 * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.createTimeRanges instead. 28676 */ 28677videojs.createTimeRanges = deprecateForMajor( 28678 9, 28679 'videojs.createTimeRanges', 28680 'videojs.time.createTimeRanges', 28681 createTimeRanges$1 28682); 28683 28684/** 28685 * Deprecated reference to the {@link module:time.formatTime|formatTime function} 28686 * 28687 * @type {Function} 28688 * @see {@link module:time.formatTime|formatTime} 28689 * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.format instead. 28690 */ 28691videojs.formatTime = deprecateForMajor( 28692 9, 28693 'videojs.formatTime', 28694 'videojs.time.formatTime', 28695 formatTime 28696); 28697 28698/** 28699 * Deprecated reference to the {@link module:time.setFormatTime|setFormatTime function} 28700 * 28701 * @type {Function} 28702 * @see {@link module:time.setFormatTime|setFormatTime} 28703 * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.setFormat instead. 28704 */ 28705videojs.setFormatTime = deprecateForMajor( 28706 9, 28707 'videojs.setFormatTime', 28708 'videojs.time.setFormatTime', 28709 setFormatTime 28710); 28711 28712/** 28713 * Deprecated reference to the {@link module:time.resetFormatTime|resetFormatTime function} 28714 * 28715 * @type {Function} 28716 * @see {@link module:time.resetFormatTime|resetFormatTime} 28717 * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.resetFormat instead. 28718 */ 28719videojs.resetFormatTime = deprecateForMajor( 28720 9, 28721 'videojs.resetFormatTime', 28722 'videojs.time.resetFormatTime', 28723 resetFormatTime 28724); 28725 28726/** 28727 * Deprecated reference to the {@link module:url.parseUrl|Url.parseUrl function} 28728 * 28729 * @type {Function} 28730 * @see {@link module:url.parseUrl|parseUrl} 28731 * @deprecated Deprecated and will be removed in 9.0. Please use videojs.url.parseUrl instead. 28732 */ 28733videojs.parseUrl = deprecateForMajor(9, 'videojs.parseUrl', 'videojs.url.parseUrl', parseUrl); 28734 28735/** 28736 * Deprecated reference to the {@link module:url.isCrossOrigin|Url.isCrossOrigin function} 28737 * 28738 * @type {Function} 28739 * @see {@link module:url.isCrossOrigin|isCrossOrigin} 28740 * @deprecated Deprecated and will be removed in 9.0. Please use videojs.url.isCrossOrigin instead. 28741 */ 28742videojs.isCrossOrigin = deprecateForMajor( 28743 9, 28744 'videojs.isCrossOrigin', 28745 'videojs.url.isCrossOrigin', 28746 isCrossOrigin 28747); 28748videojs.EventTarget = EventTarget$2; 28749videojs.any = any; 28750videojs.on = on; 28751videojs.one = one; 28752videojs.off = off; 28753videojs.trigger = trigger; 28754 28755/** 28756 * A cross-browser XMLHttpRequest wrapper. 28757 * 28758 * @function 28759 * @param {Object} options 28760 * Settings for the request. 28761 * 28762 * @return {XMLHttpRequest|XDomainRequest} 28763 * The request object. 28764 * 28765 * @see https://github.com/Raynos/xhr 28766 */ 28767videojs.xhr = XHR__default['default']; 28768videojs.TextTrack = TextTrack; 28769videojs.AudioTrack = AudioTrack; 28770videojs.VideoTrack = VideoTrack; 28771[ 28772 'isEl', 28773 'isTextNode', 28774 'createEl', 28775 'hasClass', 28776 'addClass', 28777 'removeClass', 28778 'toggleClass', 28779 'setAttributes', 28780 'getAttributes', 28781 'emptyEl', 28782 'appendContent', 28783 'insertContent',
28784].forEach((k) => { 28785 videojs[k] = function () { 28786 log$1.warn(`videojs.${k}() is deprecated; use videojs.dom.${k}() instead`); 28787 return Dom[k].apply(null, arguments); 28788 }; 28789}); 28790videojs.computedStyle = deprecateForMajor( 28791 9, 28792 'videojs.computedStyle', 28793 'videojs.dom.computedStyle', 28794 computedStyle 28795); 28796 28797/** 28798 * A reference to the {@link module:dom|DOM utility module} as an object. 28799 * 28800 * @type {Object} 28801 * @see {@link module:dom|dom} 28802 */ 28803videojs.dom = Dom; 28804 28805/** 28806 * A reference to the {@link module:fn|fn utility module} as an object. 28807 * 28808 * @type {Object} 28809 * @see {@link module:fn|fn} 28810 */ 28811videojs.fn = Fn; 28812 28813/** 28814 * A reference to the {@link module:num|num utility module} as an object. 28815 * 28816 * @type {Object} 28817 * @see {@link module:num|num} 28818 */ 28819videojs.num = Num; 28820 28821/** 28822 * A reference to the {@link module:str|str utility module} as an object. 28823 * 28824 * @type {Object} 28825 * @see {@link module:str|str} 28826 */ 28827videojs.str = Str; 28828 28829/** 28830 * A reference to the {@link module:url|URL utility module} as an object. 28831 * 28832 * @type {Object} 28833 * @see {@link module:url|url} 28834 */ 28835videojs.url = Url; 28836 28837// The list of possible error types to occur in video.js 28838videojs.Error = VjsErrors; 28839 28840/*! @name videojs-contrib-quality-levels @version 4.1.0 @license Apache-2.0 */ 28841 28842/** 28843 * A single QualityLevel. 28844 * 28845 * interface QualityLevel { 28846 * readonly attribute DOMString id; 28847 * attribute DOMString label; 28848 * readonly attribute long width; 28849 * readonly attribute long height; 28850 * readonly attribute long bitrate; 28851 * attribute boolean enabled; 28852 * }; 28853 * 28854 * @class QualityLevel 28855 */ 28856class QualityLevel { 28857 /** 28858 * Creates a QualityLevel 28859 * 28860 * @param {Representation|Object} representation The representation of the quality level 28861 * @param {string} representation.id Unique id of the QualityLevel 28862 * @param {number=} representation.width Resolution width of the QualityLevel 28863 * @param {number=} representation.height Resolution height of the QualityLevel 28864 * @param {number} representation.bandwidth Bitrate of the QualityLevel 28865 * @param {number=} representation.frameRate Frame-rate of the QualityLevel 28866 * @param {Function} representation.enabled Callback to enable/disable QualityLevel 28867 */ 28868 constructor(representation) { 28869 let level = this; // eslint-disable-line 28870 28871 level.id = representation.id; 28872 level.label = level.id; 28873 level.width = representation.width; 28874 level.height = representation.height; 28875 level.bitrate = representation.bandwidth; 28876 level.frameRate = representation.frameRate; 28877 level.enabled_ = representation.enabled; 28878 Object.defineProperty(level, 'enabled', { 28879 /** 28880 * Get whether the QualityLevel is enabled. 28881 * 28882 * @return {boolean} True if the QualityLevel is enabled. 28883 */ 28884 get() { 28885 return level.enabled_(); 28886 }, 28887 /** 28888 * Enable or disable the QualityLevel. 28889 * 28890 * @param {boolean} enable true to enable QualityLevel, false to disable. 28891 */ 28892 set(enable) { 28893 level.enabled_(enable); 28894 }, 28895 }); 28896 return level; 28897 } 28898} 28899 28900/** 28901 * A list of QualityLevels. 28902 * 28903 * interface QualityLevelList : EventTarget { 28904 * getter QualityLevel (unsigned long index); 28905 * readonly attribute unsigned long length; 28906 * readonly attribute long selectedIndex; 28907 * 28908 * void addQualityLevel(QualityLevel qualityLevel) 28909 * void removeQualityLevel(QualityLevel remove) 28910 * QualityLevel? getQualityLevelById(DOMString id); 28911 * 28912 * attribute EventHandler onchange; 28913 * attribute EventHandler onaddqualitylevel; 28914 * attribute EventHandler onremovequalitylevel; 28915 * }; 28916 * 28917 * @extends videojs.EventTarget 28918 * @class QualityLevelList 28919 */ 28920class QualityLevelList extends videojs.EventTarget { 28921 /** 28922 * Creates a QualityLevelList. 28923 */ 28924 constructor() { 28925 super(); 28926 let list = this; // eslint-disable-line 28927 28928 list.levels_ = []; 28929 list.selectedIndex_ = -1; 28930 28931 /** 28932 * Get the index of the currently selected QualityLevel. 28933 * 28934 * @returns {number} The index of the selected QualityLevel. -1 if none selected. 28935 * @readonly 28936 */ 28937 Object.defineProperty(list, 'selectedIndex', { 28938 get() { 28939 return list.selectedIndex_; 28940 }, 28941 }); 28942 28943 /** 28944 * Get the length of the list of QualityLevels. 28945 * 28946 * @returns {number} The length of the list. 28947 * @readonly 28948 */ 28949 Object.defineProperty(list, 'length', { 28950 get() { 28951 return list.levels_.length; 28952 }, 28953 }); 28954 list[Symbol.iterator] = () => list.levels_.values(); 28955 return list; 28956 } 28957 28958 /** 28959 * Adds a quality level to the list. 28960 * 28961 * @param {Representation|Object} representation The representation of the quality level 28962 * @param {string} representation.id Unique id of the QualityLevel 28963 * @param {number=} representation.width Resolution width of the QualityLevel 28964 * @param {number=} representation.height Resolution height of the QualityLevel 28965 * @param {number} representation.bandwidth Bitrate of the QualityLevel 28966 * @param {number=} representation.frameRate Frame-rate of the QualityLevel 28967 * @param {Function} representation.enabled Callback to enable/disable QualityLevel 28968 * @return {QualityLevel} the QualityLevel added to the list 28969 * @method addQualityLevel 28970 */ 28971 addQualityLevel(representation) { 28972 let qualityLevel = this.getQualityLevelById(representation.id); 28973
vendor: 4,306 bytes, lines 28974-29125
28974 // Do not add duplicate quality levels 28975 if (qualityLevel) { 28976 return qualityLevel; 28977 } 28978 const index = this.levels_.length; 28979 qualityLevel = new QualityLevel(representation); 28980 if (!('' + index in this)) { 28981 Object.defineProperty(this, index, { 28982 get() { 28983 return this.levels_[index]; 28984 }, 28985 }); 28986 } 28987 this.levels_.push(qualityLevel); 28988 this.trigger({ 28989 qualityLevel, 28990 type: 'addqualitylevel', 28991 }); 28992 return qualityLevel; 28993 } 28994 28995 /** 28996 * Removes a quality level from the list. 28997 * 28998 * @param {QualityLevel} qualityLevel The QualityLevel to remove from the list. 28999 * @return {QualityLevel|null} the QualityLevel removed or null if nothing removed 29000 * @method removeQualityLevel 29001 */ 29002 removeQualityLevel(qualityLevel) { 29003 let removed = null; 29004 for (let i = 0, l = this.length; i < l; i++) { 29005 if (this[i] === qualityLevel) { 29006 removed = this.levels_.splice(i, 1)[0]; 29007 if (this.selectedIndex_ === i) { 29008 this.selectedIndex_ = -1; 29009 } else if (this.selectedIndex_ > i) { 29010 this.selectedIndex_--; 29011 } 29012 break; 29013 } 29014 } 29015 if (removed) { 29016 this.trigger({ 29017 qualityLevel, 29018 type: 'removequalitylevel', 29019 }); 29020 } 29021 return removed; 29022 } 29023 29024 /** 29025 * Searches for a QualityLevel with the given id. 29026 * 29027 * @param {string} id The id of the QualityLevel to find. 29028 * @return {QualityLevel|null} The QualityLevel with id, or null if not found. 29029 * @method getQualityLevelById 29030 */ 29031 getQualityLevelById(id) { 29032 for (let i = 0, l = this.length; i < l; i++) { 29033 const level = this[i]; 29034 if (level.id === id) { 29035 return level; 29036 } 29037 } 29038 return null; 29039 } 29040 29041 /** 29042 * Resets the list of QualityLevels to empty 29043 * 29044 * @method dispose 29045 */ 29046 dispose() { 29047 this.selectedIndex_ = -1; 29048 this.levels_.length = 0; 29049 } 29050} 29051 29052/** 29053 * change - The selected QualityLevel has changed. 29054 * addqualitylevel - A QualityLevel has been added to the QualityLevelList. 29055 * removequalitylevel - A QualityLevel has been removed from the QualityLevelList. 29056 */ 29057QualityLevelList.prototype.allowedEvents_ = { 29058 change: 'change', 29059 addqualitylevel: 'addqualitylevel', 29060 removequalitylevel: 'removequalitylevel', 29061}; 29062 29063// emulate attribute EventHandler support to allow for feature detection 29064for (const event in QualityLevelList.prototype.allowedEvents_) { 29065 QualityLevelList.prototype['on' + event] = null; 29066} 29067var version$5 = '4.1.0'; 29068 29069/** 29070 * Initialization function for the qualityLevels plugin. Sets up the QualityLevelList and 29071 * event handlers. 29072 * 29073 * @param {Player} player Player object. 29074 * @param {Object} options Plugin options object. 29075 * @return {QualityLevelList} a list of QualityLevels 29076 */ 29077const initPlugin$1 = function (player, options) { 29078 const originalPluginFn = player.qualityLevels; 29079 const qualityLevelList = new QualityLevelList(); 29080 const disposeHandler = function () { 29081 qualityLevelList.dispose(); 29082 player.qualityLevels = originalPluginFn; 29083 player.off('dispose', disposeHandler); 29084 }; 29085 player.on('dispose', disposeHandler); 29086 player.qualityLevels = () => qualityLevelList; 29087 player.qualityLevels.VERSION = version$5; 29088 return qualityLevelList; 29089}; 29090 29091/** 29092 * A video.js plugin. 29093 * 29094 * In the plugin function, the value of `this` is a video.js `Player` 29095 * instance. You cannot rely on the player being in a "ready" state here, 29096 * depending on how the plugin is invoked. This may or may not be important 29097 * to you; if not, remove the wait for "ready"! 29098 * 29099 * @param {Object} options Plugin options object 29100 * @return {QualityLevelList} a list of QualityLevels 29101 */ 29102const qualityLevels = function (options) { 29103 return initPlugin$1(this, videojs.obj.merge({}, options)); 29104}; 29105 29106// Register the plugin with video.js. 29107videojs.registerPlugin('qualityLevels', qualityLevels); 29108 29109// Include the version number. 29110qualityLevels.VERSION = version$5; 29111 29112/*! @name @videojs/http-streaming @version 3.16.0 @license Apache-2.0 */ 29113 29114/** 29115 * @file resolve-url.js - Handling how URLs are resolved and manipulated 29116 */ 29117const resolveUrl = _resolveUrl__default['default']; 29118/** 29119 * If the xhr request was redirected, return the responseURL, otherwise, 29120 * return the original url. 29121 * 29122 * @api private 29123 * 29124 * @param {string} url - an url being requested 29125 * @param {XMLHttpRequest}
vendor: 10,079 bytes, lines 29125-29495
29125 req - xhr request result 29126 * 29127 * @return {string} 29128 */ 29129 29130const resolveManifestRedirect = (url, req) => { 29131 // To understand how the responseURL below is set and generated: 29132 // - https://fetch.spec.whatwg.org/#concept-response-url 29133 // - https://fetch.spec.whatwg.org/#atomic-http-redirect-handling 29134 if (req && req.responseURL && url !== req.responseURL) { 29135 return req.responseURL; 29136 } 29137 return url; 29138}; 29139const logger = (source) => { 29140 if (videojs.log.debug) { 29141 return videojs.log.debug.bind(videojs, 'VHS:', `${source} >`); 29142 } 29143 return function () {}; 29144}; 29145 29146/** 29147 * Provides a compatibility layer between Video.js 7 and 8 API changes for VHS. 29148 */ 29149/** 29150 * Delegates to videojs.obj.merge (Video.js 8) or 29151 * videojs.mergeOptions (Video.js 7). 29152 */ 29153 29154function merge(...args) { 29155 const context = videojs.obj || videojs; 29156 const fn = context.merge || context.mergeOptions; 29157 return fn.apply(context, args); 29158} 29159/** 29160 * Delegates to videojs.time.createTimeRanges (Video.js 8) or 29161 * videojs.createTimeRanges (Video.js 7). 29162 */ 29163 29164function createTimeRanges(...args) { 29165 const context = videojs.time || videojs; 29166 const fn = context.createTimeRanges || context.createTimeRanges; 29167 return fn.apply(context, args); 29168} 29169/** 29170 * Converts provided buffered ranges to a descriptive string 29171 * 29172 * @param {TimeRanges} buffered - received buffered time ranges 29173 * 29174 * @return {string} - descriptive string 29175 */ 29176 29177function bufferedRangesToString(buffered) { 29178 if (buffered.length === 0) { 29179 return 'Buffered Ranges are empty'; 29180 } 29181 let bufferedRangesStr = 'Buffered Ranges: \n'; 29182 for (let i = 0; i < buffered.length; i++) { 29183 const start = buffered.start(i); 29184 const end = buffered.end(i); 29185 bufferedRangesStr += `${start} --> ${end}. Duration (${end - start})\n`; 29186 } 29187 return bufferedRangesStr; 29188} 29189 29190/** 29191 * ranges 29192 * 29193 * Utilities for working with TimeRanges. 29194 * 29195 */ 29196 29197const TIME_FUDGE_FACTOR = 1 / 30; // Comparisons between time values such as current time and the end of the buffered range 29198// can be misleading because of precision differences or when the current media has poorly 29199// aligned audio and video, which can cause values to be slightly off from what you would 29200// expect. This value is what we consider to be safe to use in such comparisons to account 29201// for these scenarios. 29202 29203const SAFE_TIME_DELTA = TIME_FUDGE_FACTOR * 3; 29204const filterRanges = function (timeRanges, predicate) { 29205 const results = []; 29206 let i; 29207 if (timeRanges && timeRanges.length) { 29208 // Search for ranges that match the predicate 29209 for (i = 0; i < timeRanges.length; i++) { 29210 if (predicate(timeRanges.start(i), timeRanges.end(i))) { 29211 results.push([timeRanges.start(i), timeRanges.end(i)]); 29212 } 29213 } 29214 } 29215 return createTimeRanges(results); 29216}; 29217/** 29218 * Attempts to find the buffered TimeRange that contains the specified 29219 * time. 29220 * 29221 * @param {TimeRanges} buffered - the TimeRanges object to query 29222 * @param {number} time - the time to filter on. 29223 * @return {TimeRanges} a new TimeRanges object 29224 */ 29225 29226const findRange = function (buffered, time) { 29227 return filterRanges(buffered, function (start, end) { 29228 return start - SAFE_TIME_DELTA <= time && end + SAFE_TIME_DELTA >= time; 29229 }); 29230}; 29231/** 29232 * Returns the TimeRanges that begin later than the specified time. 29233 * 29234 * @param {TimeRanges} timeRanges - the TimeRanges object to query 29235 * @param {number} time - the time to filter on. 29236 * @return {TimeRanges} a new TimeRanges object. 29237 */ 29238 29239const findNextRange = function (timeRanges, time) { 29240 return filterRanges(timeRanges, function (start) { 29241 return start - TIME_FUDGE_FACTOR >= time; 29242 }); 29243}; 29244/** 29245 * Returns gaps within a list of TimeRanges 29246 * 29247 * @param {TimeRanges} buffered - the TimeRanges object 29248 * @return {TimeRanges} a TimeRanges object of gaps 29249 */ 29250 29251const findGaps = function (buffered) { 29252 if (buffered.length < 2) { 29253 return createTimeRanges(); 29254 } 29255 const ranges = []; 29256 for (let i = 1; i < buffered.length; i++) { 29257 const start = buffered.end(i - 1); 29258 const end = buffered.start(i); 29259 ranges.push([start, end]); 29260 } 29261 return createTimeRanges(ranges); 29262}; 29263/** 29264 * Calculate the intersection of two TimeRanges 29265 * 29266 * @param {TimeRanges} bufferA 29267 * @param {TimeRanges} bufferB 29268 * @return {TimeRanges} The interesection of `bufferA` with `bufferB` 29269 */ 29270 29271const bufferIntersection = function (bufferA, bufferB) { 29272 let start = null; 29273 let end = null; 29274 let arity = 0; 29275 const extents = []; 29276 const ranges = []; 29277 if (!bufferA || !bufferA.length || !bufferB || !bufferB.length) { 29278 return createTimeRanges(); 29279 } // Handle the case where we have both buffers and create an 29280 // intersection of the two 29281 29282 let count = bufferA.length; // A) Gather up all start and end times 29283 29284 while (count--) { 29285 extents.push({ 29286 time: bufferA.start(count), 29287 type: 'start', 29288 }); 29289 extents.push({ 29290 time: bufferA.end(count), 29291 type: 'end', 29292 }); 29293 } 29294 count = bufferB.length; 29295 while (count--) { 29296 extents.push({ 29297 time: bufferB.start(count), 29298 type: 'start', 29299 }); 29300 extents.push({ 29301 time: bufferB.end(count), 29302 type: 'end', 29303 }); 29304 } // B) Sort them by time 29305 29306 extents.sort(function (a, b) { 29307 return a.time - b.time; 29308 }); // C) Go along one by one incrementing arity for start and decrementing 29309 // arity for ends 29310 29311 for (count = 0; count < extents.length; count++) { 29312 if (extents[count].type === 'start') { 29313 arity++; // D) If arity is ever incremented to 2 we are entering an 29314 // overlapping range 29315 29316 if (arity === 2) { 29317 start = extents[count].time; 29318 } 29319 } else if (extents[count].type === 'end') { 29320 arity--; // E) If arity is ever decremented to 1 we leaving an 29321 // overlapping range 29322 29323 if (arity === 1) { 29324 end = extents[count].time; 29325 } 29326 } // F) Record overlapping ranges 29327 29328 if (start !== null && end !== null) { 29329 ranges.push([start, end]); 29330 start = null; 29331 end = null; 29332 } 29333 } 29334 return createTimeRanges(ranges); 29335}; 29336/** 29337 * Gets a human readable string for a TimeRange 29338 * 29339 * @param {TimeRange} range 29340 * @return {string} a human readable string 29341 */ 29342 29343const printableRange = (range) => { 29344 const strArr = []; 29345 if (!range || !range.length) { 29346 return ''; 29347 } 29348 for (let i = 0; i < range.length; i++) { 29349 strArr.push(range.start(i) + ' => ' + range.end(i)); 29350 } 29351 return strArr.join(', '); 29352}; 29353/** 29354 * Calculates the amount of time left in seconds until the player hits the end of the 29355 * buffer and causes a rebuffer 29356 * 29357 * @param {TimeRange} buffered 29358 * The state of the buffer 29359 * @param {Numnber} currentTime 29360 * The current time of the player 29361 * @param {number} playbackRate 29362 * The current playback rate of the player. Defaults to 1. 29363 * @return {number} 29364 * Time until the player has to start rebuffering in seconds. 29365 * @function timeUntilRebuffer 29366 */ 29367 29368const timeUntilRebuffer = function (buffered, currentTime, playbackRate = 1) { 29369 const bufferedEnd = buffered.length ? buffered.end(buffered.length - 1) : 0; 29370 return (bufferedEnd - currentTime) / playbackRate; 29371}; 29372/** 29373 * Converts a TimeRanges object into an array representation 29374 * 29375 * @param {TimeRanges} timeRanges 29376 * @return {Array} 29377 */ 29378 29379const timeRangesToArray = (timeRanges) => { 29380 const timeRangesList = []; 29381 for (let i = 0; i < timeRanges.length; i++) { 29382 timeRangesList.push({ 29383 start: timeRanges.start(i), 29384 end: timeRanges.end(i), 29385 }); 29386 } 29387 return timeRangesList; 29388}; 29389/** 29390 * Determines if two time range objects are different. 29391 * 29392 * @param {TimeRange} a 29393 * the first time range object to check 29394 * 29395 * @param {TimeRange} b 29396 * the second time range object to check 29397 * 29398 * @return {Boolean} 29399 * Whether the time range objects differ 29400 */ 29401 29402const isRangeDifferent = function (a, b) { 29403 // same object 29404 if (a === b) { 29405 return false; 29406 } // one or the other is undefined 29407 29408 if ((!a && b) || (!b && a)) { 29409 return true; 29410 } // length is different 29411 29412 if (a.length !== b.length) { 29413 return true; 29414 } // see if any start/end pair is different 29415 29416 for (let i = 0; i < a.length; i++) { 29417 if (a.start(i) !== b.start(i) || a.end(i) !== b.end(i)) { 29418 return true; 29419 } 29420 } // if the length and every pair is the same 29421 // this is the same time range 29422 29423 return false; 29424}; 29425const lastBufferedEnd = function (a) { 29426 if (!a || !a.length || !a.end) { 29427 return; 29428 } 29429 return a.end(a.length - 1); 29430}; 29431/** 29432 * A utility function to add up the amount of time in a timeRange 29433 * after a specified startTime. 29434 * ie:[[0, 10], [20, 40], [50, 60]] with a startTime 0 29435 * would return 40 as there are 40s seconds after 0 in the timeRange 29436 * 29437 * @param {TimeRange} range 29438 * The range to check against 29439 * @param {number} startTime 29440 * The time in the time range that you should start counting from 29441 * 29442 * @return {number} 29443 * The number of seconds in the buffer passed the specified time. 29444 */ 29445 29446const timeAheadOf = function (range, startTime) { 29447 let time = 0; 29448 if (!range || !range.length) { 29449 return time; 29450 } 29451 for (let i = 0; i < range.length; i++) { 29452 const start = range.start(i); 29453 const end = range.end(i); // startTime is after this range entirely 29454 29455 if (startTime > end) { 29456 continue; 29457 } // startTime is within this range 29458 29459 if (startTime > start && startTime <= end) { 29460 time += end - startTime; 29461 continue; 29462 } // startTime is before this range. 29463 29464 time += end - start; 29465 } 29466 return time; 29467}; 29468 29469/** 29470 * @file playlist.js 29471 * 29472 * Playlist related utilities. 29473 */ 29474/** 29475 * Get the duration of a segment, with special cases for 29476 * llhls segments that do not have a duration yet. 29477 * 29478 * @param {Object} playlist 29479 * the playlist that the segment belongs to. 29480 * @param {Object} segment 29481 * the segment to get a duration for. 29482 * 29483 * @return {number} 29484 * the segment duration 29485 */ 29486 29487const segmentDurationWithParts = (playlist, segment) => { 29488 // if this isn't a preload segment 29489 // then we will have a segment duration that is accurate. 29490 if (!segment.preload) { 29491 return segment.duration; 29492 } // otherwise we have to add up parts and preload hints 29493 // to get an up to date duration. 29494 29495 let result = 0;
29496 (segment.parts || []).forEach(function (p) { 29497 result += p.duration; 29498 }); // for preload hints we have to use partTargetDuration 29499 // as they won't even have a duration yet. 29500 29501 (segment.preloadHints || []).forEach(function (p) { 29502 if (p.type === 'PART') { 29503 result += playlist.partTargetDuration; 29504 } 29505 }); 29506 return result; 29507}; 29508/** 29509 * A function to get a combined list of parts and segments with durations 29510 * and indexes. 29511 * 29512 * @param {Playlist} playlist the playlist to get the list for. 29513 * 29514 * @return {Array} The part/segment list. 29515 */ 29516 29517const getPartsAndSegments = (playlist) => 29518 (playlist.segments || []).reduce((acc, segment, si) => { 29519 if (segment.parts) { 29520 segment.parts.forEach(function (part, pi) { 29521 acc.push({ 29522 duration: part.duration, 29523 segmentIndex: si, 29524 partIndex: pi, 29525 part, 29526 segment, 29527 }); 29528 }); 29529 } else { 29530 acc.push({ 29531 duration: segment.duration, 29532 segmentIndex: si, 29533 partIndex: null, 29534 segment, 29535 part: null, 29536 }); 29537 } 29538 return acc; 29539 }, []); 29540const getLastParts = (media) => { 29541 const lastSegment = 29542 media.segments && media.segments.length && media.segments[media.segments.length - 1]; 29543 return (lastSegment && lastSegment.parts) || []; 29544}; 29545const getKnownPartCount = ({ preloadSegment }) => { 29546 if (!preloadSegment) { 29547 return; 29548 } 29549 const { parts, preloadHints } = preloadSegment; 29550 let partCount = (preloadHints || []).reduce( 29551 (count, hint) => count + (hint.type === 'PART' ? 1 : 0), 29552 0 29553 ); 29554 partCount += parts && parts.length ? parts.length : 0; 29555 return partCount; 29556}; 29557/** 29558 * Get the number of seconds to delay from the end of a 29559 * live playlist. 29560 * 29561 * @param {Playlist} main the main playlist 29562 * @param {Playlist} media the media playlist 29563 * @return {number} the hold back in seconds. 29564 */ 29565 29566const liveEdgeDelay = (main, media) => { 29567 if (media.endList) { 29568 return 0; 29569 } // dash suggestedPresentationDelay trumps everything 29570 29571 if (main && main.suggestedPresentationDelay) { 29572 return main.suggestedPresentationDelay; 29573 } 29574 const hasParts = getLastParts(media).length > 0; // look for "part" delays from ll-hls first 29575 29576 if (hasParts && media.serverControl && media.serverControl.partHoldBack) { 29577 return media.serverControl.partHoldBack; 29578 } else if (hasParts && media.partTargetDuration) { 29579 return media.partTargetDuration * 3; // finally look for full segment delays 29580 } else if (media.serverControl && media.serverControl.holdBack) { 29581 return media.serverControl.holdBack; 29582 } else if (media.targetDuration) { 29583 return media.targetDuration * 3; 29584 } 29585 return 0; 29586}; 29587/** 29588 * walk backward until we find a duration we can use 29589 * or return a failure 29590 * 29591 * @param {Playlist} playlist the playlist to walk through 29592 * @param {Number} endSequence the mediaSequence to stop walking on 29593 */ 29594 29595const backwardDuration = function (playlist, endSequence) { 29596 let result = 0; 29597 let i = endSequence - playlist.mediaSequence; // if a start time is available for segment immediately following 29598 // the interval, use it 29599 29600 let segment = playlist.segments[i]; // Walk backward until we find the latest segment with timeline 29601 // information that is earlier than endSequence 29602 29603 if (segment) { 29604 if (typeof segment.start !== 'undefined') { 29605 return { 29606 result: segment.start, 29607 precise: true, 29608 }; 29609 } 29610 if (typeof segment.end !== 'undefined') { 29611 return { 29612 result: segment.end - segment.duration, 29613 precise: true, 29614 }; 29615 } 29616 } 29617 while (i--) { 29618 segment = playlist.segments[i]; 29619 if (typeof segment.end !== 'undefined') { 29620 return { 29621 result: result + segment.end, 29622 precise: true, 29623 }; 29624 } 29625 result += segmentDurationWithParts(playlist, segment); 29626 if (typeof segment.start !== 'undefined') { 29627 return { 29628 result: result + segment.start, 29629 precise: true, 29630 }; 29631 } 29632 } 29633 return { 29634 result, 29635 precise: false, 29636 }; 29637}; 29638/** 29639 * walk forward until we find a duration we can use 29640 * or return a failure 29641 * 29642 * @param {Playlist} playlist the playlist to walk through 29643 * @param {number} endSequence the mediaSequence to stop walking on 29644 */ 29645 29646const forwardDuration = function (playlist, endSequence) { 29647 let result = 0; 29648 let segment; 29649 let i = endSequence - playlist.mediaSequence; // Walk forward until we find the earliest segment with timeline 29650 // information 29651 29652 for (; i < playlist.segments.length; i++) { 29653 segment = playlist.segments[i]; 29654 if (typeof segment.start !== 'undefined') { 29655 return { 29656 result: segment.start - result, 29657 precise: true, 29658 }; 29659 } 29660 result += segmentDurationWithParts(playlist, segment); 29661 if (typeof segment.end !== 'undefined') { 29662 return { 29663 result: segment.end - result, 29664 precise: true, 29665 }; 29666 } 29667 } // indicate we didn't find a useful duration estimate 29668 29669 return {
vendor: 4,793 bytes, lines 29670-29792
29670 result: -1, 29671 precise: false, 29672 }; 29673}; 29674/** 29675 * Calculate the media duration from the segments associated with a 29676 * playlist. The duration of a subinterval of the available segments 29677 * may be calculated by specifying an end index. 29678 * 29679 * @param {Object} playlist a media playlist object 29680 * @param {number=} endSequence an exclusive upper boundary 29681 * for the playlist. Defaults to playlist length. 29682 * @param {number} expired the amount of time that has dropped 29683 * off the front of the playlist in a live scenario 29684 * @return {number} the duration between the first available segment 29685 * and end index. 29686 */ 29687 29688const intervalDuration = function (playlist, endSequence, expired) { 29689 if (typeof endSequence === 'undefined') { 29690 endSequence = playlist.mediaSequence + playlist.segments.length; 29691 } 29692 if (endSequence < playlist.mediaSequence) { 29693 return 0; 29694 } // do a backward walk to estimate the duration 29695 29696 const backward = backwardDuration(playlist, endSequence); 29697 if (backward.precise) { 29698 // if we were able to base our duration estimate on timing 29699 // information provided directly from the Media Source, return 29700 // it 29701 return backward.result; 29702 } // walk forward to see if a precise duration estimate can be made 29703 // that way 29704 29705 const forward = forwardDuration(playlist, endSequence); 29706 if (forward.precise) { 29707 // we found a segment that has been buffered and so it's 29708 // position is known precisely 29709 return forward.result; 29710 } // return the less-precise, playlist-based duration estimate 29711 29712 return backward.result + expired; 29713}; 29714/** 29715 * Calculates the duration of a playlist. If a start and end index 29716 * are specified, the duration will be for the subset of the media 29717 * timeline between those two indices. The total duration for live 29718 * playlists is always Infinity. 29719 * 29720 * @param {Object} playlist a media playlist object 29721 * @param {number=} endSequence an exclusive upper 29722 * boundary for the playlist. Defaults to the playlist media 29723 * sequence number plus its length. 29724 * @param {number=} expired the amount of time that has 29725 * dropped off the front of the playlist in a live scenario 29726 * @return {number} the duration between the start index and end 29727 * index. 29728 */ 29729 29730const duration = function (playlist, endSequence, expired) { 29731 if (!playlist) { 29732 return 0; 29733 } 29734 if (typeof expired !== 'number') { 29735 expired = 0; 29736 } // if a slice of the total duration is not requested, use 29737 // playlist-level duration indicators when they're present 29738 29739 if (typeof endSequence === 'undefined') { 29740 // if present, use the duration specified in the playlist 29741 if (playlist.totalDuration) { 29742 return playlist.totalDuration; 29743 } // duration should be Infinity for live playlists 29744 29745 if (!playlist.endList) { 29746 return window__default['default'].Infinity; 29747 } 29748 } // calculate the total duration based on the segment durations 29749 29750 return intervalDuration(playlist, endSequence, expired); 29751}; 29752/** 29753 * Calculate the time between two indexes in the current playlist 29754 * neight the start- nor the end-index need to be within the current 29755 * playlist in which case, the targetDuration of the playlist is used 29756 * to approximate the durations of the segments 29757 * 29758 * @param {Array} options.durationList list to iterate over for durations. 29759 * @param {number} options.defaultDuration duration to use for elements before or after the durationList 29760 * @param {number} options.startIndex partsAndSegments index to start 29761 * @param {number} options.endIndex partsAndSegments index to end. 29762 * @return {number} the number of seconds between startIndex and endIndex 29763 */ 29764 29765const sumDurations = function ({ defaultDuration, durationList, startIndex, endIndex }) { 29766 let durations = 0; 29767 if (startIndex > endIndex) { 29768 [startIndex, endIndex] = [endIndex, startIndex]; 29769 } 29770 if (startIndex < 0) { 29771 for (let i = startIndex; i < Math.min(0, endIndex); i++) { 29772 durations += defaultDuration; 29773 } 29774 startIndex = 0; 29775 } 29776 for (let i = startIndex; i < endIndex; i++) { 29777 durations += durationList[i].duration; 29778 } 29779 return durations; 29780}; 29781/** 29782 * Calculates the playlist end time 29783 * 29784 * @param {Object} playlist a media playlist object 29785 * @param {number=} expired the amount of time that has 29786 * dropped off the front of the playlist in a live scenario 29787 * @param {boolean|false} useSafeLiveEnd a boolean value indicating whether or not the 29788 * playlist end calculation should consider the safe live end 29789 * (truncate the playlist end by three segments). This is normally 29790 * used for calculating the end of the playlist's seekable range. 29791 * This takes into account the value of liveEdgePadding. 29792 * Setting liveEdgePadding to 0 is equivalent to setting this to fal
29792se. 29793 * @param {number} liveEdgePadding a number indicating how far from the end of the playlist we should be in seconds. 29794 * If this is provided, it is used in the safe live end calculation. 29795 * Setting useSafeLiveEnd=false or liveEdgePadding=0 are equivalent. 29796 * Corresponds to suggestedPresentationDelay in DASH manifests. 29797 * @return {number} the end time of playlist 29798 * @function playlistEnd 29799 */ 29800 29801const playlistEnd = function (playlist, expired, useSafeLiveEnd, liveEdgePadding) { 29802 if (!playlist || !playlist.segments) { 29803 return null; 29804 } 29805 if (playlist.endList) { 29806 return duration(playlist); 29807 } 29808 if (expired === null) { 29809 return null; 29810 } 29811 expired = expired || 0; 29812 let lastSegmentEndTime = intervalDuration( 29813 playlist, 29814 playlist.mediaSequence + playlist.segments.length, 29815 expired 29816 ); 29817 if (useSafeLiveEnd) { 29818 liveEdgePadding = 29819 typeof liveEdgePadding === 'number' ? liveEdgePadding : liveEdgeDelay(null, playlist); 29820 lastSegmentEndTime -= liveEdgePadding; 29821 } // don't return a time less than zero 29822 29823 return Math.max(0, lastSegmentEndTime); 29824}; 29825/** 29826 * Calculates the interval of time that is currently seekable in a 29827 * playlist. The returned time ranges are relative to the earliest 29828 * moment in the specified playlist that is still available. A full 29829 * seekable implementation for live streams would need to offset 29830 * these values by the duration of content that has expired from the 29831 * stream. 29832 * 29833 * @param {Object} playlist a media playlist object 29834 * dropped off the front of the playlist in a live scenario 29835 * @param {number=} expired the amount of time that has 29836 * dropped off the front of the playlist in a live scenario 29837 * @param {number} liveEdgePadding how far from the end of the playlist we should be in seconds. 29838 * Corresponds to suggestedPresentationDelay in DASH manifests. 29839 * @return {TimeRanges} the periods of time that are valid targets 29840 * for seeking 29841 */ 29842 29843const seekable = function (playlist, expired, liveEdgePadding) { 29844 const useSafeLiveEnd = true; 29845 const seekableStart = expired || 0; 29846 let seekableEnd = playlistEnd(playlist, expired, useSafeLiveEnd, liveEdgePadding); 29847 if (seekableEnd === null) { 29848 return createTimeRanges(); 29849 } // Clamp seekable end since it can not be less than the seekable start 29850 29851 if (seekableEnd < seekableStart) { 29852 seekableEnd = seekableStart; 29853 } 29854 return createTimeRanges(seekableStart, seekableEnd); 29855}; 29856/** 29857 * Determine the index and estimated starting time of the segment that 29858 * contains a specified playback position in a media playlist. 29859 * 29860 * @param {Object} options.playlist the media playlist to query 29861 * @param {number} options.currentTime The number of seconds since the earliest 29862 * possible position to determine the containing segment for 29863 * @param {number} options.startTime the time when the segment/part starts 29864 * @param {number} options.startingSegmentIndex the segment index to start looking at. 29865 * @param {number?} [options.startingPartIndex] the part index to look at within the segment. 29866 * 29867 * @return {Object} an object with partIndex, segmentIndex, and startTime. 29868 */ 29869 29870const getMediaInfoForTime = function ({ 29871 playlist, 29872 currentTime, 29873 startingSegmentIndex, 29874 startingPartIndex, 29875 startTime, 29876 exactManifestTimings, 29877}) { 29878 let time = currentTime - startTime; 29879 const partsAndSegments = getPartsAndSegments(playlist); 29880 let startIndex = 0; 29881 for (let i = 0; i < partsAndSegments.length; i++) { 29882 const partAndSegment = partsAndSegments[i]; 29883 if (startingSegmentIndex !== partAndSegment.segmentIndex) { 29884 continue; 29885 } // skip this if part index does not match. 29886 29887 if ( 29888 typeof startingPartIndex === 'number' && 29889 typeof partAndSegment.partIndex === 'number' && 29890 startingPartIndex !== partAndSegment.partIndex 29891 ) { 29892 continue; 29893 } 29894 startIndex = i; 29895 break; 29896 } 29897 if (time < 0) { 29898 // Walk backward from startIndex in the playlist, adding durations 29899 // until we find a segment that contains `time` and return it 29900 if (startIndex > 0) { 29901 for (let i = startIndex - 1; i >= 0; i--) { 29902 const partAndSegment = partsAndSegments[i]; 29903 time += partAndSegment.duration; 29904 if (exactManifestTimings) { 29905 if (time < 0) { 29906 continue; 29907 } 29908 } else if (time + TIME_FUDGE_FACTOR <= 0) { 29909 continue; 29910 } 29911 return { 29912 partIndex: partAndSegment.partIndex, 29913 segmentIndex: partAndSegment.segmentIndex, 29914 startTime: 29915 startTime - 29916 sumDurations({ 29917 defaultDuration: playlist.targetDuration, 29918 durationList: partsAndSegments, 29919 startIndex, 29920 endIndex: i, 29921 }), 29922 }; 29923 } 29924 } // We were unable to find a good segment within the playlist 29925 // so select the first segment 29926 29927 return { 29928 partIndex: (partsAndSegments[0] && partsAndSegments[0].partIndex) || null, 29929 segmentIndex: (partsAndSegments[0] && partsAndSegments[0].segmentIndex) || 0, 29930 startTime: currentTime, 29931 }; 29932 } // When startIndex is negative, we first walk forward to first segment 29933 // adding target durations. If we "run out of time" before getting to 29934 // the first segment, return the first segment 29935 29936 if (startIndex < 0) { 29937 for (let i = startIndex; i < 0; i++) { 29938 time -= playlist.targetDuration; 29939 if (time < 0) { 29940 return { 29941 partIndex: (partsAndSegments[0] && partsAndSegments[0].partIndex) || null, 29942 segmentIndex: (partsAndSegments[0] && partsAndSegments[0].segmentIndex) || 0, 29943 startTime: currentTime, 29944 }; 29945 } 29946 } 29947 startIndex = 0; 29948 } // Walk forward from startIndex in the playlist, subtracting durations 29949 // until we find a segment that contains `time` and return it 29950 29951 for (let i = startIndex; i < partsAndSegments.length; i++) { 29952 const partAndSegment = partsAndSegments[i]; 29953 time -= partAndSegment.duration; 29954 const canUseFudgeFactor = partAndSegment.duration > TIME_FUDGE_FACTOR; 29955 const isExactlyAtTheEnd = time === 0; 29956 const isExtremelyCloseToTheEnd = canUseFudgeFactor && time + TIME_FUDGE_FACTOR >= 0; 29957 if (isExactlyAtTheEnd || isExtremelyCloseToTheEnd) { 29958 // 1) We are exactly at the end of the current segment. 29959 // 2) We are extremely close to the end of the current segment (The difference is less than 1 / 30). 29960 // We may encounter this situation when 29961 // we don't have exact match between segment duration info in the manifest and the actual duration of the segment 29962 // For example: 29963 // We appended 3 segments 10 seconds each, meaning we should have 30 sec buffered, 29964 // but we the actual buffered is 29.99999 29965 // 29966 // In both cases: 29967 // if we passed current time -> it means that we already played current segment 29968 // if we passed buffered.end -> it means that this segment is already loaded and buffered 29969 // we should select the next segment if we have one: 29970 if (i !== partsAndSegments.length - 1) { 29971 continue; 29972 } 29973 } 29974 if (exactManifestTimings) { 29975 if (time > 0) { 29976 continue; 29977 } 29978 } else if (time - TIME_FUDGE_FACTOR >= 0) { 29979 continue; 29980 } 29981 return { 29982 partIndex: partAndSegment.partIndex, 29983 segmentIndex: partAndSegment.segmentIndex, 29984 startTime: 29985 startTime + 29986 sumDurations({ 29987 defaultDuration: playlist.targetDuration, 29988 durationList: partsAndSegments, 29989 startIndex, 29990 endIndex: i, 29991 }), 29992 }; 29993 } // We are out of possible candidates so load the last one... 29994 29995 return { 29996 segmentIndex: partsAndSegments[partsAndSegments.length - 1].segmentIndex, 29997 partIndex: partsAndSegments[partsAndSegments.length - 1].partIndex, 29998 startTime: currentTime, 29999 }; 30000}; 30001/** 30002 * Check whether the playlist is excluded or not. 30003 * 30004 * @param {Object} playlist the media playlist object 30005 * @return {boolean} whether the playlist is excluded or not 30006 * @function isExcluded 30007 */ 30008
30009const isExcluded = function (playlist) { 30010 return playlist.excludeUntil && playlist.excludeUntil > Date.now(); 30011}; 30012/** 30013 * Check whether the playlist is compatible with current playback configuration or has 30014 * been excluded permanently for being incompatible. 30015 * 30016 * @param {Object} playlist the media playlist object 30017 * @return {boolean} whether the playlist is incompatible or not 30018 * @function isIncompatible 30019 */ 30020 30021const isIncompatible = function (playlist) { 30022 return playlist.excludeUntil && playlist.excludeUntil === Infinity; 30023}; 30024/** 30025 * Check whether the playlist is enabled or not. 30026 * 30027 * @param {Object} playlist the media playlist object 30028 * @return {boolean} whether the playlist is enabled or not 30029 * @function isEnabled 30030 */ 30031 30032const isEnabled = function (playlist) { 30033 const excluded = isExcluded(playlist); 30034 return !playlist.disabled && !excluded; 30035}; 30036/** 30037 * Check whether the playlist has been manually disabled through the representations api. 30038 * 30039 * @param {Object} playlist the media playlist object 30040 * @return {boolean} whether the playlist is disabled manually or not 30041 * @function isDisabled 30042 */ 30043 30044const isDisabled = function (playlist) { 30045 return playlist.disabled; 30046}; 30047/** 30048 * Returns whether the current playlist is an AES encrypted HLS stream 30049 * 30050 * @return {boolean} true if it's an AES encrypted HLS stream 30051 */ 30052 30053const isAes = function (media) { 30054 for (let i = 0; i < media.segments.length; i++) { 30055 if (media.segments[i].key) { 30056 return true; 30057 } 30058 } 30059 return false; 30060}; 30061/** 30062 * Checks if the playlist has a value for the specified attribute 30063 * 30064 * @param {string} attr 30065 * Attribute to check for 30066 * @param {Object} playlist 30067 * The media playlist object 30068 * @return {boolean} 30069 * Whether the playlist contains a value for the attribute or not 30070 * @function hasAttribute 30071 */ 30072 30073const hasAttribute = function (attr, playlist) { 30074 return playlist.attributes && playlist.attributes[attr]; 30075}; 30076/** 30077 * Estimates the time required to complete a segment download from the specified playlist 30078 * 30079 * @param {number} segmentDuration 30080 * Duration of requested segment 30081 * @param {number} bandwidth 30082 * Current measured bandwidth of the player 30083 * @param {Object} playlist 30084 * The media playlist object 30085 * @param {number=} bytesReceived 30086 * Number of bytes already received for the request. Defaults to 0 30087 * @return {number|NaN} 30088 * The estimated time to request the segment. NaN if bandwidth information for 30089 * the given playlist is unavailable 30090 * @function estimateSegmentRequestTime 30091 */ 30092 30093const estimateSegmentRequestTime = function ( 30094 segmentDuration, 30095 bandwidth, 30096 playlist, 30097 bytesReceived = 0 30098) { 30099 if (!hasAttribute('BANDWIDTH', playlist)) { 30100 return NaN; 30101 } 30102 const size = segmentDuration * playlist.attributes.BANDWIDTH; 30103 return (size - bytesReceived * 8) / bandwidth; 30104}; 30105/* 30106 * Returns whether the current playlist is the lowest rendition 30107 * 30108 * @return {Boolean} true if on lowest rendition 30109 */ 30110 30111const isLowestEnabledRendition = (main, media) => { 30112 if (main.playlists.length === 1) { 30113 return true; 30114 } 30115 const currentBandwidth = media.attributes.BANDWIDTH || Number.MAX_VALUE; 30116 return ( 30117 main.playlists.filter((playlist) => { 30118 if (!isEnabled(playlist)) { 30119 return false; 30120 } 30121 return (playlist.attributes.BANDWIDTH || 0) < currentBandwidth; 30122 }).length === 0 30123 ); 30124}; 30125const playlistMatch = (a, b) => { 30126 // both playlits are null 30127 // or only one playlist is non-null 30128 // no match 30129 if ((!a && !b) || (!a && b) || (a && !b)) { 30130 return false; 30131 } // playlist objects are the same, match 30132 30133 if (a === b) { 30134 return true; 30135 } // first try to use id as it should be the most 30136 // accurate 30137 30138 if (a.id && b.id && a.id === b.id) { 30139 return true; 30140 } // next try to use reslovedUri as it should be the 30141 // second most accurate. 30142 30143 if (a.resolvedUri && b.resolvedUri && a.resolvedUri === b.resolvedUri) { 30144 return true; 30145 } // finally try to use uri as it should be accurate 30146 // but might miss a few cases for relative uris 30147 30148 if (a.uri && b.uri && a.uri === b.uri) { 30149 return true; 30150 } 30151 return false;
30152}; 30153const someAudioVariant = function (main, callback) { 30154 const AUDIO = (main && main.mediaGroups && main.mediaGroups.AUDIO) || {}; 30155 let found = false; 30156 for (const groupName in AUDIO) { 30157 for (const label in AUDIO[groupName]) { 30158 found = callback(AUDIO[groupName][label]); 30159 if (found) { 30160 break; 30161 } 30162 } 30163 if (found) { 30164 break; 30165 } 30166 } 30167 return !!found; 30168}; 30169const isAudioOnly = (main) => { 30170 // we are audio only if we have no main playlists but do 30171 // have media group playlists. 30172 if (!main || !main.playlists || !main.playlists.length) { 30173 // without audio variants or playlists this 30174 // is not an audio only main. 30175 const found = someAudioVariant( 30176 main, 30177 (variant) => (variant.playlists && variant.playlists.length) || variant.uri 30178 ); 30179 return found; 30180 } // if every playlist has only an audio codec it is audio only 30181 30182 for (let i = 0; i < main.playlists.length; i++) { 30183 const playlist = main.playlists[i]; 30184 const CODECS = playlist.attributes && playlist.attributes.CODECS; // all codecs are audio, this is an audio playlist. 30185 30186 if (CODECS && CODECS.split(',').every((c) => codecs_js.isAudioCodec(c))) { 30187 continue; 30188 } // playlist is in an audio group it is audio only 30189 30190 const found = someAudioVariant(main, (variant) => playlistMatch(playlist, variant)); 30191 if (found) { 30192 continue; 30193 } // if we make it here this playlist isn't audio and we 30194 // are not audio only 30195 30196 return false; 30197 } // if we make it past every playlist without returning, then 30198 // this is an audio only playlist. 30199 30200 return true; 30201}; // exports 30202 30203var Playlist = { 30204 liveEdgeDelay, 30205 duration, 30206 seekable, 30207 getMediaInfoForTime, 30208 isEnabled, 30209 isDisabled, 30210 isExcluded, 30211 isIncompatible, 30212 playlistEnd, 30213 isAes, 30214 hasAttribute, 30215 estimateSegmentRequestTime, 30216 isLowestEnabledRendition, 30217 isAudioOnly, 30218 playlistMatch, 30219 segmentDurationWithParts, 30220}; 30221const { log } = videojs; 30222const createPlaylistID = (index, uri) => { 30223 return `${index}-${uri}`; 30224}; // default function for creating a group id 30225 30226const groupID = (type, group, label) => { 30227 return `placeholder-uri-${type}-${group}-${label}`; 30228}; 30229/** 30230 * Parses a given m3u8 playlist 30231 * 30232 * @param {Function} [onwarn] 30233 * a function to call when the parser triggers a warning event. 30234 * @param {Function} [oninfo] 30235 * a function to call when the parser triggers an info event. 30236 * @param {string} manifestString 30237 * The downloaded manifest string 30238 * @param {Object[]} [customTagParsers] 30239 * An array of custom tag parsers for the m3u8-parser instance 30240 * @param {Object[]} [customTagMappers] 30241 * An array of custom tag mappers for the m3u8-parser instance 30242 * @param {boolean} [llhls] 30243 * Whether to keep ll-hls features in the manifest after parsing. 30244 * @return {Object} 30245 * The manifest object 30246 */ 30247 30248const parseManifest = ({ 30249 onwarn, 30250 oninfo, 30251 manifestString, 30252 customTagParsers = [], 30253 customTagMappers = [], 30254 llhls, 30255}) => { 30256 const parser = new m3u8Parser.Parser(); 30257 if (onwarn) { 30258 parser.on('warn', onwarn); 30259 } 30260 if (oninfo) { 30261 parser.on('info', oninfo); 30262 } 30263 customTagParsers.forEach((customParser) => parser.addParser(customParser)); 30264 customTagMappers.forEach((mapper) => parser.addTagMapper(mapper)); 30265 parser.push(manifestString); 30266 parser.end(); 30267 const manifest = parser.manifest; // remove llhls features from the parsed manifest 30268 // if we don't want llhls support. 30269 30270 if (!llhls) { 30271 [ 30272 'preloadSegment', 30273 'skip', 30274 'serverControl', 30275 'renditionReports', 30276 'partInf', 30277 'partTargetDuration', 30278 ].forEach(function (k) { 30279 if (manifest.hasOwnProperty(k)) { 30280 delete manifest[k]; 30281 } 30282 }); 30283 if (manifest.segments) { 30284 manifest.segments.forEach(function (segment) { 30285 ['parts', 'preloadHints'].forEach(function (k) { 30286 if (segment.hasOwnProperty(k)) { 30287 delete segment[k]; 30288 } 30289 }); 30290 }); 30291 } 30292 } 30293 if (!manifest.targetDuration) { 30294 let targetDuration = 10; 30295 if (manifest.segments && manifest.segments.length) { 30296 targetDuration = manifest.segments.reduce((acc, s) => Math.max(acc, s.duration), 0); 30297 } 30298 if (onwarn) { 30299 onwarn({ 30300 message: `manifest has no targetDuration defaulting to ${targetDuration}`, 30301 }); 30302 } 30303 manifest.targetDuration = targetDuration; 30304 } 30305 const parts = getLastParts(manifest); 30306 if (parts.length && !manifest.partTargetDuration) { 30307 const partTargetDuration = parts.reduce((acc, p) => Math.max(acc, p.duration), 0); 30308 if (onwarn) { 30309 onwarn({ 30310 message: `manifest has no partTargetDuration defaulting to ${partTargetDuration}`, 30311 }); 30312 log.error( 30313 'LL-HLS manifest has parts but lacks required #EXT-X-PART-INF:PART-TARGET value. See https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-09#section-4.4.3.7. Playback is not guaranteed.' 30314 ); 30315 } 30316 manifest.partTargetDuration = partTargetDuration; 30317 } 30318 return manifest; 30319}; 30320/** 30321 * Loops through all supported media groups in main and calls the provided 30322 * callback for each group 30323 * 30324 * @param {Object} main 30325 * The parsed main manifest object 30326 * @param {Function} callback 30327 * Callback to call for each media group 30328 */ 30329
vendor: 1,222 bytes, lines 30330-30366
30330const forEachMediaGroup = (main, callback) => { 30331 if (!main.mediaGroups) { 30332 return; 30333 } 30334 ['AUDIO', 'SUBTITLES'].forEach((mediaType) => { 30335 if (!main.mediaGroups[mediaType]) { 30336 return; 30337 } 30338 for (const groupKey in main.mediaGroups[mediaType]) { 30339 for (const labelKey in main.mediaGroups[mediaType][groupKey]) { 30340 const mediaProperties = main.mediaGroups[mediaType][groupKey][labelKey]; 30341 callback(mediaProperties, mediaType, groupKey, labelKey); 30342 } 30343 } 30344 }); 30345}; 30346/** 30347 * Adds properties and attributes to the playlist to keep consistent functionality for 30348 * playlists throughout VHS. 30349 * 30350 * @param {Object} config 30351 * Arguments object 30352 * @param {Object} config.playlist 30353 * The media playlist 30354 * @param {string} [config.uri] 30355 * The uri to the media playlist (if media playlist is not from within a main 30356 * playlist) 30357 * @param {string} id 30358 * ID to use for the playlist 30359 */ 30360 30361const setupMediaPlaylist = ({ playlist, uri, id }) => { 30362 playlist.id = id; 30363 playlist.playlistErrors_ = 0; 30364 if (uri) { 30365 // For media playlists, m3u8-parser does not have access to a URI, as HLS media 30366 // playlists do not contain their own source URI, but one is needed for c
30366onsistency in 30367 // VHS. 30368 playlist.uri = uri; 30369 } // For HLS main playlists, even though certain attributes MUST be defined, the 30370 // stream may still be played without them. 30371 // For HLS media playlists, m3u8-parser does not attach an attributes object to the 30372 // manifest. 30373 // 30374 // To avoid undefined reference errors through the project, and make the code easier 30375 // to write/read, add an empty attributes object for these cases. 30376 30377 playlist.attributes = playlist.attributes || {}; 30378}; 30379/** 30380 * Adds ID, resolvedUri, and attributes properties to each playlist of the main, where 30381 * necessary. In addition, creates playlist IDs for each playlist and adds playlist ID to 30382 * playlist references to the playlists array. 30383 * 30384 * @param {Object} main 30385 * The main playlist 30386 */ 30387 30388const setupMediaPlaylists = (main) => { 30389 let i = main.playlists.length; 30390 while (i--) { 30391 const playlist = main.playlists[i]; 30392 setupMediaPlaylist({ 30393 playlist, 30394 id: createPlaylistID(i, playlist.uri), 30395 }); 30396 playlist.resolvedUri = resolveUrl(main.uri, playlist.uri); 30397 main.playlists[playlist.id] = playlist; // URI reference added for backwards compatibility 30398 30399 main.playlists[playlist.uri] = playlist; // Although the spec states an #EXT-X-STREAM-INF tag MUST have a BANDWIDTH attribute, 30400 // the stream can be played without it. Although an attributes property may have been 30401 // added to the playlist to prevent undefined references, issue a warning to fix the 30402 // manifest. 30403 30404 if (!playlist.attributes.BANDWIDTH) { 30405 log.warn('Invalid playlist STREAM-INF detected. Missing BANDWIDTH attribute.'); 30406 } 30407 } 30408}; 30409/** 30410 * Adds resolvedUri properties to each media group. 30411 * 30412 * @param {Object} main 30413 * The main playlist 30414 */ 30415 30416const resolveMediaGroupUris = (main) => { 30417 forEachMediaGroup(main, (properties) => { 30418 if (properties.uri) { 30419 properties.resolvedUri = resolveUrl(main.uri, properties.uri); 30420 } 30421 }); 30422}; 30423/** 30424 * Creates a main playlist wrapper to insert a sole media playlist into. 30425 * 30426 * @param {Object} media 30427 * Media playlist 30428 * @param {string} uri 30429 * The media URI 30430 * 30431 * @return {Object} 30432 * main playlist 30433 */ 30434 30435const mainForMedia = (media, uri) => { 30436 const id = createPlaylistID(0, uri); 30437 const main = { 30438 mediaGroups: { 30439 AUDIO: {}, 30440 VIDEO: {}, 30441 'CLOSED-CAPTIONS': {}, 30442 SUBTITLES: {}, 30443 }, 30444 uri: window__default['default'].location.href, 30445 resolvedUri: window__default['default'].location.href, 30446 playlists: [ 30447 { 30448 uri, 30449 id, 30450 resolvedUri: uri, 30451 // m3u8-parser does not attach an attributes property to media playlists so make 30452 // sure that the property is attached to avoid undefined reference errors 30453 attributes: {}, 30454 }, 30455 ], 30456 }; // set up ID reference 30457 30458 main.playlists[id] = main.playlists[0]; // URI reference added for backwards compatibility 30459 30460 main.playlists[uri] = main.playlists[0]; 30461 return main; 30462}; 30463/** 30464 * Does an in-place update of the main manifest to add updated playlist URI references 30465 * as well as other properties needed by VHS that aren't included by the parser. 30466 * 30467 * @param {Object} main 30468 * main manifest object 30469 * @param {string} uri 30470 * The source URI 30471 * @param {function} createGroupID 30472 * A function to determine how to create the groupID for mediaGroups 30473 */ 30474 30475const addPropertiesToMain = (main, uri, createGroupID = groupID) => { 30476 main.uri = uri; 30477 for (let i = 0; i < main.playlists.length; i++) { 30478 if (!main.playlists[i].uri) { 30479 // Set up phony URIs for the playlists since playlists are referenced by their URIs 30480 // throughout VHS, but some formats (e.g., DASH) don't have external URIs 30481 // TODO: consider adding dummy URIs in mpd-parser 30482 const phonyUri = `placeholder-uri-${i}`; 30483 main.playlists[i].uri = phonyUri; 30484 } 30485 } 30486 const audioOnlyMain = isAudioOnly(main); 30487 forEachMediaGroup(main, (properties, mediaType, groupKey, labelKey) => { 30488 // add a playlist array under properties 30489 if (!properties.playlists || !properties.playlists.length) { 30490 // If the manifest is audio only and this media group does not have a uri, check 30491 // if the media group is located in the main list of playlists. If it is, don't add 30492 // placeholder properties as it shouldn't be considered an alternate audio track. 30493 if (audioOnlyMain && mediaType === 'AUDIO' && !properties.uri) { 30494 for (let i = 0; i < main.playlists.length; i++) { 30495 const p = main.playlists[i]; 30496 if (p.attributes && p.attributes.AUDIO && p.attributes.AUDIO === groupKey) { 30497 return; 30498 } 30499 } 30500 } 30501 properties.playlists = [_extends__default['default']({}, properties)]; 30502 }
vendor: 10,983 bytes, lines 30503-30802
30503 properties.playlists.forEach(function (p, i) { 30504 const groupId = createGroupID(mediaType, groupKey, labelKey, p); 30505 const id = createPlaylistID(i, groupId); 30506 if (p.uri) { 30507 p.resolvedUri = p.resolvedUri || resolveUrl(main.uri, p.uri); 30508 } else { 30509 // DEPRECATED, this has been added to prevent a breaking change. 30510 // previously we only ever had a single media group playlist, so 30511 // we mark the first playlist uri without prepending the index as we used to 30512 // ideally we would do all of the playlists the same way. 30513 p.uri = i === 0 ? groupId : id; // don't resolve a placeholder uri to an absolute url, just use 30514 // the placeholder again 30515 30516 p.resolvedUri = p.uri; 30517 } 30518 p.id = p.id || id; // add an empty attributes object, all playlists are 30519 // expected to have this. 30520 30521 p.attributes = p.attributes || {}; // setup ID and URI references (URI for backwards compatibility) 30522 30523 main.playlists[p.id] = p; 30524 main.playlists[p.uri] = p; 30525 }); 30526 }); 30527 setupMediaPlaylists(main); 30528 resolveMediaGroupUris(main); 30529}; 30530class DateRangesStorage { 30531 constructor() { 30532 this.offset_ = null; 30533 this.pendingDateRanges_ = new Map(); 30534 this.processedDateRanges_ = new Map(); 30535 } 30536 setOffset(segments = []) { 30537 // already set 30538 if (this.offset_ !== null) { 30539 return; 30540 } // no segment to process 30541 30542 if (!segments.length) { 30543 return; 30544 } 30545 const [firstSegment] = segments; // no program date time 30546 30547 if (firstSegment.programDateTime === undefined) { 30548 return; 30549 } // Set offset as ProgramDateTime for the very first segment of the very first playlist load: 30550 30551 this.offset_ = firstSegment.programDateTime / 1000; 30552 } 30553 setPendingDateRanges(dateRanges = []) { 30554 if (!dateRanges.length) { 30555 return; 30556 } 30557 const [dateRange] = dateRanges; 30558 const startTime = dateRange.startDate.getTime(); 30559 this.trimProcessedDateRanges_(startTime); 30560 this.pendingDateRanges_ = dateRanges.reduce((map, pendingDateRange) => { 30561 map.set(pendingDateRange.id, pendingDateRange); 30562 return map; 30563 }, new Map()); 30564 } 30565 processDateRange(dateRange) { 30566 this.pendingDateRanges_.delete(dateRange.id); 30567 this.processedDateRanges_.set(dateRange.id, dateRange); 30568 } 30569 getDateRangesToProcess() { 30570 if (this.offset_ === null) { 30571 return []; 30572 } 30573 const dateRangeClasses = {}; 30574 const dateRangesToProcess = []; 30575 this.pendingDateRanges_.forEach((dateRange, id) => { 30576 if (this.processedDateRanges_.has(id)) { 30577 return; 30578 } 30579 dateRange.startTime = dateRange.startDate.getTime() / 1000 - this.offset_; 30580 dateRange.processDateRange = () => this.processDateRange(dateRange); 30581 dateRangesToProcess.push(dateRange); 30582 if (!dateRange.class) { 30583 return; 30584 } 30585 if (dateRangeClasses[dateRange.class]) { 30586 const length = dateRangeClasses[dateRange.class].push(dateRange); 30587 dateRange.classListIndex = length - 1; 30588 } else { 30589 dateRangeClasses[dateRange.class] = [dateRange]; 30590 dateRange.classListIndex = 0; 30591 } 30592 }); 30593 for (const dateRange of dateRangesToProcess) { 30594 const classList = dateRangeClasses[dateRange.class] || []; 30595 if (dateRange.endDate) { 30596 dateRange.endTime = dateRange.endDate.getTime() / 1000 - this.offset_; 30597 } else if (dateRange.endOnNext && classList[dateRange.classListIndex + 1]) { 30598 dateRange.endTime = classList[dateRange.classListIndex + 1].startTime; 30599 } else if (dateRange.duration) { 30600 dateRange.endTime = dateRange.startTime + dateRange.duration; 30601 } else if (dateRange.plannedDuration) { 30602 dateRange.endTime = dateRange.startTime + dateRange.plannedDuration; 30603 } else { 30604 dateRange.endTime = dateRange.startTime; 30605 } 30606 } 30607 return dateRangesToProcess; 30608 } 30609 trimProcessedDateRanges_(startTime) { 30610 const copy = new Map(this.processedDateRanges_); 30611 copy.forEach((dateRange, id) => { 30612 if (dateRange.startDate.getTime() < startTime) { 30613 this.processedDateRanges_.delete(id); 30614 } 30615 }); 30616 } 30617} 30618const QUOTA_EXCEEDED_ERR = 22; 30619const getStreamingNetworkErrorMetadata = ({ requestType, request, error, parseFailure }) => { 30620 const isBadStatus = request.status < 200 || request.status > 299; 30621 const isFailure = request.status >= 400 && request.status <= 499; 30622 const errorMetadata = { 30623 uri: request.uri, 30624 requestType, 30625 }; 30626 const isBadStatusOrParseFailure = (isBadStatus && !isFailure) || parseFailure; 30627 if (error && isFailure) { 30628 // copy original error and add to the metadata. 30629 errorMetadata.error = _extends__default['default']({}, error); 30630 errorMetadata.errorType = videojs.Error.NetworkRequestFailed; 30631 } else if (request.aborted) { 30632 errorMetadata.errorType = videojs.Error.NetworkRequestAborted; 30633 } else if (request.timedout) { 30634 errorMetadata.erroType = videojs.Error.NetworkRequestTimeout; 30635 } else if (isBadStatusOrParseFailure) { 30636 const errorType = parseFailure 30637 ? videojs.Error.NetworkBodyParserFailed 30638 : videojs.Error.NetworkBadStatus; 30639 errorMetadata.errorType = errorType; 30640 errorMetadata.status = request.status; 30641 errorMetadata.headers = request.headers; 30642 } 30643 return errorMetadata; 30644}; 30645const { EventTarget: EventTarget$1 } = videojs; 30646const addLLHLSQueryDirectives = (uri, media) => { 30647 if (media.endList || !media.serverControl) { 30648 return uri; 30649 } 30650 const parameters = {}; 30651 if (media.serverControl.canBlockReload) { 30652 const { preloadSegment } = media; // next msn is a zero based value, length is not. 30653 30654 let nextMSN = media.mediaSequence + media.segments.length; // If preload segment has parts then it is likely 30655 // that we are going to request a part of that preload segment. 30656 // the logic below is used to determine that. 30657 30658 if (preloadSegment) { 30659 const parts = preloadSegment.parts || []; // _HLS_part is a zero based index 30660 30661 const nextPart = getKnownPartCount(media) - 1; // if nextPart is > -1 and not equal to just the 30662 // length of parts, then we know we had part preload hints 30663 // and we need to add the _HLS_part= query 30664 30665 if (nextPart > -1 && nextPart !== parts.length - 1) { 30666 // add existing parts to our preload hints 30667 // eslint-disable-next-line 30668 parameters._HLS_part = nextPart; 30669 } // this if statement makes sure that we request the msn 30670 // of the preload segment if: 30671 // 1. the preload segment had parts (and was not yet a full segment) 30672 // but was added to our segments array 30673 // 2. the preload segment had preload hints for parts that are not in 30674 // the manifest yet. 30675 // in all other cases we want the segment after the preload segment 30676 // which will be given by using media.segments.length because it is 1 based 30677 // rather than 0 based. 30678 30679 if (nextPart > -1 || parts.length) { 30680 nextMSN--; 30681 } 30682 } // add _HLS_msn= in front of any _HLS_part query 30683 // eslint-disable-next-line 30684 30685 parameters._HLS_msn = nextMSN; 30686 } 30687 if (media.serverControl && media.serverControl.canSkipUntil) { 30688 // add _HLS_skip= infront of all other queries. 30689 // eslint-disable-next-line 30690 parameters._HLS_skip = media.serverControl.canSkipDateranges ? 'v2' : 'YES'; 30691 } 30692 if (Object.keys(parameters).length) { 30693 const parsedUri = new window__default['default'].URL(uri); 30694 ['_HLS_skip', '_HLS_msn', '_HLS_part'].forEach(function (name) { 30695 if (!parameters.hasOwnProperty(name)) { 30696 return; 30697 } 30698 parsedUri.searchParams.set(name, parameters[name]); 30699 }); 30700 uri = parsedUri.toString(); 30701 } 30702 return uri; 30703}; 30704/** 30705 * Returns a new segment object with properties and 30706 * the parts array merged. 30707 * 30708 * @param {Object} a the old segment 30709 * @param {Object} b the new segment 30710 * 30711 * @return {Object} the merged segment 30712 */ 30713 30714const updateSegment = (a, b) => { 30715 if (!a) { 30716 return b; 30717 } 30718 const result = merge(a, b); // if only the old segment has preload hints 30719 // and the new one does not, remove preload hints. 30720 30721 if (a.preloadHints && !b.preloadHints) { 30722 delete result.preloadHints; 30723 } // if only the old segment has parts 30724 // then the parts are no longer valid 30725 30726 if (a.parts && !b.parts) { 30727 delete result.parts; // if both segments have parts 30728 // copy part propeties from the old segment 30729 // to the new one. 30730 } else if (a.parts && b.parts) { 30731 for (let i = 0; i < b.parts.length; i++) { 30732 if (a.parts && a.parts[i]) { 30733 result.parts[i] = merge(a.parts[i], b.parts[i]); 30734 } 30735 } 30736 } // set skipped to false for segments that have 30737 // have had information merged from the old segment. 30738 30739 if (!a.skipped && b.skipped) { 30740 result.skipped = false; 30741 } // set preload to false for segments that have 30742 // had information added in the new segment. 30743 30744 if (a.preload && !b.preload) { 30745 result.preload = false; 30746 } 30747 return result; 30748}; 30749/** 30750 * Returns a new array of segments that is the result of merging 30751 * properties from an older list of segments onto an updated 30752 * list. No properties on the updated playlist will be ovewritten. 30753 * 30754 * @param {Array} original the outdated list of segments 30755 * @param {Array} update the updated list of segments 30756 * @param {number=} offset the index of the first update 30757 * segment in the original segment list. For non-live playlists, 30758 * this should always be zero and does not need to be 30759 * specified. For live playlists, it should be the difference 30760 * between the media sequence numbers in the original and updated 30761 * playlists. 30762 * @return {Array} a list of merged segment objects 30763 */ 30764 30765const updateSegments = (original, update, offset) => { 30766 const oldSegments = original.slice(); 30767 const newSegments = update.slice(); 30768 offset = offset || 0; 30769 const result = []; 30770 let currentMap; 30771 for (let newIndex = 0; newIndex < newSegments.length; newIndex++) { 30772 const oldSegment = oldSegments[newIndex + offset]; 30773 const newSegment = newSegments[newIndex]; 30774 if (oldSegment) { 30775 currentMap = oldSegment.map || currentMap; 30776 result.push(updateSegment(oldSegment, newSegment)); 30777 } else { 30778 // carry over map to new segment if it is missing 30779 if (currentMap && !newSegment.map) { 30780 newSegment.map = currentMap; 30781 } 30782 result.push(newSegment); 30783 } 30784 } 30785 return result; 30786}; 30787const resolveSegmentUris = (segment, baseUri) => { 30788 // preloadSegment will not have a uri at all 30789 // as the segment isn't actually in the manifest yet, only parts 30790 if (!segment.resolvedUri && segment.uri) { 30791 segment.resolvedUri = resolveUrl(baseUri, segment.uri); 30792 } 30793 if (segment.key && !segment.key.resolvedUri) { 30794 segment.key.resolvedUri = resolveUrl(baseUri, segment.key.uri); 30795 } 30796 if (segment.map && !segment.map.resolvedUri) { 30797 segment.map.resolvedUri = resolveUrl(baseUri, segment.map.uri); 30798 } 30799 if (segment.map && segment.map.key && !segment.map.key.resolvedUri) { 30800 segment.map.key.resolvedUri = resolveUrl(baseUri, segment.map.key.uri); 30801 } 30802 if (segment.parts && segment.parts.length) {
vendor: 5,582 bytes, lines 30803-30956
30803 segment.parts.forEach((p) => { 30804 if (p.resolvedUri) { 30805 return; 30806 } 30807 p.resolvedUri = resolveUrl(baseUri, p.uri); 30808 }); 30809 } 30810 if (segment.preloadHints && segment.preloadHints.length) { 30811 segment.preloadHints.forEach((p) => { 30812 if (p.resolvedUri) { 30813 return; 30814 } 30815 p.resolvedUri = resolveUrl(baseUri, p.uri); 30816 }); 30817 } 30818}; 30819const getAllSegments = function (media) { 30820 const segments = media.segments || []; 30821 const preloadSegment = media.preloadSegment; // a preloadSegment with only preloadHints is not currently 30822 // a usable segment, only include a preloadSegment that has 30823 // parts. 30824 30825 if (preloadSegment && preloadSegment.parts && preloadSegment.parts.length) { 30826 // if preloadHints has a MAP that means that the 30827 // init segment is going to change. We cannot use any of the parts 30828 // from this preload segment. 30829 if (preloadSegment.preloadHints) { 30830 for (let i = 0; i < preloadSegment.preloadHints.length; i++) { 30831 if (preloadSegment.preloadHints[i].type === 'MAP') { 30832 return segments; 30833 } 30834 } 30835 } // set the duration for our preload segment to target duration. 30836 30837 preloadSegment.duration = media.targetDuration; 30838 preloadSegment.preload = true; 30839 segments.push(preloadSegment); 30840 } 30841 return segments; 30842}; // consider the playlist unchanged if the playlist object is the same or 30843// the number of segments is equal, the media sequence number is unchanged, 30844// and this playlist hasn't become the end of the playlist 30845 30846const isPlaylistUnchanged = (a, b) => 30847 a === b || 30848 (a.segments && 30849 b.segments && 30850 a.segments.length === b.segments.length && 30851 a.endList === b.endList && 30852 a.mediaSequence === b.mediaSequence && 30853 a.preloadSegment === b.preloadSegment); 30854/** 30855 * Returns a new main playlist that is the result of merging an 30856 * updated media playlist into the original version. If the 30857 * updated media playlist does not match any of the playlist 30858 * entries in the original main playlist, null is returned. 30859 * 30860 * @param {Object} main a parsed main M3U8 object 30861 * @param {Object} media a parsed media M3U8 object 30862 * @return {Object} a new object that represents the original 30863 * main playlist with the updated media playlist merged in, or 30864 * null if the merge produced no change. 30865 */ 30866 30867const updateMain$1 = (main, newMedia, unchangedCheck = isPlaylistUnchanged) => { 30868 const result = merge(main, {}); 30869 const oldMedia = result.playlists[newMedia.id]; 30870 if (!oldMedia) { 30871 return null; 30872 } 30873 if (unchangedCheck(oldMedia, newMedia)) { 30874 return null; 30875 } 30876 newMedia.segments = getAllSegments(newMedia); 30877 const mergedPlaylist = merge(oldMedia, newMedia); // always use the new media's preload segment 30878 30879 if (mergedPlaylist.preloadSegment && !newMedia.preloadSegment) { 30880 delete mergedPlaylist.preloadSegment; 30881 } // if the update could overlap existing segment information, merge the two segment lists 30882 30883 if (oldMedia.segments) { 30884 if (newMedia.skip) { 30885 newMedia.segments = newMedia.segments || []; // add back in objects for skipped segments, so that we merge 30886 // old properties into the new segments 30887 30888 for (let i = 0; i < newMedia.skip.skippedSegments; i++) { 30889 newMedia.segments.unshift({ 30890 skipped: true, 30891 }); 30892 } 30893 } 30894 mergedPlaylist.segments = updateSegments( 30895 oldMedia.segments, 30896 newMedia.segments, 30897 newMedia.mediaSequence - oldMedia.mediaSequence 30898 ); 30899 } // resolve any segment URIs to prevent us from having to do it later 30900 30901 mergedPlaylist.segments.forEach((segment) => { 30902 resolveSegmentUris(segment, mergedPlaylist.resolvedUri); 30903 }); // TODO Right now in the playlists array there are two references to each playlist, one 30904 // that is referenced by index, and one by URI. The index reference may no longer be 30905 // necessary. 30906 30907 for (let i = 0; i < result.playlists.length; i++) { 30908 if (result.playlists[i].id === newMedia.id) { 30909 result.playlists[i] = mergedPlaylist; 30910 } 30911 } 30912 result.playlists[newMedia.id] = mergedPlaylist; // URI reference added for backwards compatibility 30913 30914 result.playlists[newMedia.uri] = mergedPlaylist; // update media group playlist references. 30915 30916 forEachMediaGroup(main, (properties, mediaType, groupKey, labelKey) => { 30917 if (!properties.playlists) { 30918 return; 30919 } 30920 for (let i = 0; i < properties.playlists.length; i++) { 30921 if (newMedia.id === properties.playlists[i].id) { 30922 properties.playlists[i] = mergedPlaylist; 30923 } 30924 } 30925 }); 30926 return result; 30927}; 30928/** 30929 * Calculates the time to wait before refreshing a live playlist 30930 * 30931 * @param {Object} media 30932 * The current media 30933 * @param {boolean} update 30934 * True if there were any updates from the last refresh, false otherwise 30935 * @return {number} 30936 * The time in ms to wait before refreshing the live playlist 30937 */ 30938 30939const refreshDelay = (media, update) => { 30940 const segments = media.segments || []; 30941 const lastSegment = segments[segments.length - 1]; 30942 const lastPart = 30943 lastSegment && lastSegment.parts && lastSegment.parts[lastSegment.parts.length - 1]; 30944 const lastDuration = (lastPart && lastPart.duration) || (lastSegment && lastSegment.duration); 30945 if (update && lastDuration) { 30946 return lastDuration * 1000; 30947 } // if the playlist is unchanged since the last reload or last segment duration 30948 // cannot be determined, try again after half the target duration 30949 30950 return (media.partTargetDuration || media.targetDuration || 10) * 500; 30951}; 30952const playlistMetadataPayload = (playlists, type, isLive) => { 30953 if (!playlists) { 30954 return; 30955 } 30956 const renditions = [];
30957 playlists.forEach((playlist) => { 30958 // we need attributes to populate rendition data. 30959 if (!playlist.attributes) { 30960 return; 30961 } 30962 const { BANDWIDTH, RESOLUTION, CODECS } = playlist.attributes; 30963 renditions.push({ 30964 id: playlist.id, 30965 bandwidth: BANDWIDTH, 30966 resolution: RESOLUTION, 30967 codecs: CODECS, 30968 }); 30969 }); 30970 return { 30971 type, 30972 isLive, 30973 renditions, 30974 }; 30975}; 30976/** 30977 * Load a playlist from a remote location 30978 * 30979 * @class PlaylistLoader 30980 * @extends Stream 30981 * @param {string|Object} src url or object of manifest 30982 * @param {boolean} withCredentials the withCredentials xhr option 30983 * @class 30984 */ 30985 30986class PlaylistLoader extends EventTarget$1 { 30987 constructor(src, vhs, options = {}) { 30988 super(); 30989 if (!src) { 30990 throw new Error('A non-empty playlist URL or object is required'); 30991 } 30992 this.logger_ = logger('PlaylistLoader'); 30993 const { withCredentials = false } = options; 30994 this.src = src; 30995 this.vhs_ = vhs; 30996 this.withCredentials = withCredentials; 30997 this.addDateRangesToTextTrack_ = options.addDateRangesToTextTrack; 30998 const vhsOptions = vhs.options_; 30999 this.customTagParsers = (vhsOptions && vhsOptions.customTagParsers) || []; 31000 this.customTagMappers = (vhsOptions && vhsOptions.customTagMappers) || []; 31001 this.llhls = vhsOptions && vhsOptions.llhls; 31002 this.dateRangesStorage_ = new DateRangesStorage(); // initialize the loader state 31003 31004 this.state = 'HAVE_NOTHING'; // live playlist staleness timeout 31005 31006 this.handleMediaupdatetimeout_ = this.handleMediaupdatetimeout_.bind(this); 31007 this.on('mediaupdatetimeout', this.handleMediaupdatetimeout_); 31008 this.on('loadedplaylist', this.handleLoadedPlaylist_.bind(this)); 31009 } 31010 handleLoadedPlaylist_() { 31011 const mediaPlaylist = this.media(); 31012 if (!mediaPlaylist) { 31013 return; 31014 } 31015 this.dateRangesStorage_.setOffset(mediaPlaylist.segments); 31016 this.dateRangesStorage_.setPendingDateRanges(mediaPlaylist.dateRanges); 31017 const availableDateRanges = this.dateRangesStorage_.getDateRangesToProcess(); 31018 if (!availableDateRanges.length || !this.addDateRangesToTextTrack_) { 31019 return; 31020 } 31021 this.addDateRangesToTextTrack_(availableDateRanges); 31022 } 31023 handleMediaupdatetimeout_() { 31024 if (this.state !== 'HAVE_METADATA') { 31025 // only refresh the media playlist if no other activity is going on 31026 return; 31027 } 31028 const media = this.media(); 31029 let uri = resolveUrl(this.main.uri, media.uri); 31030 if (this.llhls) { 31031 uri = addLLHLSQueryDirectives(uri, media); 31032 } 31033 this.state = 'HAVE_CURRENT_METADATA'; 31034 this.request = this.vhs_.xhr( 31035 { 31036 uri, 31037 withCredentials: this.withCredentials, 31038 requestType: 'hls-playlist', 31039 }, 31040 (error, req) => { 31041 // disposed 31042 if (!this.request) { 31043 return; 31044 } 31045 if (error) { 31046 return this.playlistRequestError(this.request, this.media(), 'HAVE_METADATA'); 31047 } 31048 this.haveMetadata({ 31049 playlistString: this.request.responseText, 31050 url: this.media().uri, 31051 id: this.media().id, 31052 }); 31053 } 31054 ); 31055 } 31056 playlistRequestError(xhr, playlist, startingState) { 31057 const { uri, id } = playlist; // any in-flight request is now finished 31058 31059 this.request = null; 31060 if (startingState) { 31061 this.state = startingState; 31062 } 31063 this.error = { 31064 playlist: this.main.playlists[id], 31065 status: xhr.status, 31066 message: `HLS playlist request error at URL: ${uri}.`, 31067 responseText: xhr.responseText, 31068 code: xhr.status >= 500 ? 4 : 2, 31069 metadata: getStreamingNetworkErrorMetadata({ 31070 requestType: xhr.requestType, 31071 request: xhr, 31072 error: xhr.error, 31073 }), 31074 }; 31075 this.trigger('error'); 31076 } 31077 parseManifest_({ url, manifestString }) { 31078 try { 31079 return parseManifest({ 31080 onwarn: ({ message }) => this.logger_(`m3u8-parser warn for ${url}: ${message}`), 31081 oninfo: ({ message }) => this.logger_(`m3u8-parser info for ${url}: ${message}`), 31082 manifestString, 31083 customTagParsers: this.customTagParsers, 31084 customTagMappers: this.customTagMappers, 31085 llhls: this.llhls, 31086 }); 31087 } catch (error) { 31088 this.error = error; 31089 this.error.metadata = { 31090 errorType: videojs.Error.StreamingHlsPlaylistParserError, 31091 error, 31092 }; 31093 } 31094 } 31095 /** 31096 * Update the playlist loader's state in response to a new or updated playlist. 31097 * 31098 * @param {string} [playlistString]
vendor: 7,753 bytes, lines 31099-31336
31099 * Playlist string (if playlistObject is not provided) 31100 * @param {Object} [playlistObject] 31101 * Playlist object (if playlistString is not provided) 31102 * @param {string} url 31103 * URL of playlist 31104 * @param {string} id 31105 * ID to use for playlist 31106 */ 31107 31108 haveMetadata({ playlistString, playlistObject, url, id }) { 31109 // any in-flight request is now finished 31110 this.request = null; 31111 this.state = 'HAVE_METADATA'; 31112 const metadata = { 31113 playlistInfo: { 31114 type: 'media', 31115 uri: url, 31116 }, 31117 }; 31118 this.trigger({ 31119 type: 'playlistparsestart', 31120 metadata, 31121 }); 31122 const playlist = 31123 playlistObject || 31124 this.parseManifest_({ 31125 url, 31126 manifestString: playlistString, 31127 }); 31128 playlist.lastRequest = Date.now(); 31129 setupMediaPlaylist({ 31130 playlist, 31131 uri: url, 31132 id, 31133 }); // merge this playlist into the main manifest 31134 31135 const update = updateMain$1(this.main, playlist); 31136 this.targetDuration = playlist.partTargetDuration || playlist.targetDuration; 31137 this.pendingMedia_ = null; 31138 if (update) { 31139 this.main = update; 31140 this.media_ = this.main.playlists[id]; 31141 } else { 31142 this.trigger('playlistunchanged'); 31143 } 31144 this.updateMediaUpdateTimeout_(refreshDelay(this.media(), !!update)); 31145 metadata.parsedPlaylist = playlistMetadataPayload( 31146 this.main.playlists, 31147 metadata.playlistInfo.type, 31148 !this.media_.endList 31149 ); 31150 this.trigger({ 31151 type: 'playlistparsecomplete', 31152 metadata, 31153 }); 31154 this.trigger('loadedplaylist'); 31155 } 31156 /** 31157 * Abort any outstanding work and clean up. 31158 */ 31159 31160 dispose() { 31161 this.trigger('dispose'); 31162 this.stopRequest(); 31163 window__default['default'].clearTimeout(this.mediaUpdateTimeout); 31164 window__default['default'].clearTimeout(this.finalRenditionTimeout); 31165 this.dateRangesStorage_ = new DateRangesStorage(); 31166 this.off(); 31167 } 31168 stopRequest() { 31169 if (this.request) { 31170 const oldRequest = this.request; 31171 this.request = null; 31172 oldRequest.onreadystatechange = null; 31173 oldRequest.abort(); 31174 } 31175 } 31176 /** 31177 * When called without any arguments, returns the currently 31178 * active media playlist. When called with a single argument, 31179 * triggers the playlist loader to asynchronously switch to the 31180 * specified media playlist. Calling this method while the 31181 * loader is in the HAVE_NOTHING causes an error to be emitted 31182 * but otherwise has no effect. 31183 * 31184 * @param {Object=} playlist the parsed media playlist 31185 * object to switch to 31186 * @param {boolean=} shouldDelay whether we should delay the request by half target duration 31187 * 31188 * @return {Playlist} the current loaded media 31189 */ 31190 31191 media(playlist, shouldDelay) { 31192 // getter 31193 if (!playlist) { 31194 return this.media_; 31195 } // setter 31196 31197 if (this.state === 'HAVE_NOTHING') { 31198 throw new Error('Cannot switch media playlist from ' + this.state); 31199 } // find the playlist object if the target playlist has been 31200 // specified by URI 31201 31202 if (typeof playlist === 'string') { 31203 if (!this.main.playlists[playlist]) { 31204 throw new Error('Unknown playlist URI: ' + playlist); 31205 } 31206 playlist = this.main.playlists[playlist]; 31207 } 31208 window__default['default'].clearTimeout(this.finalRenditionTimeout); 31209 if (shouldDelay) { 31210 const delay = 31211 ((playlist.partTargetDuration || playlist.targetDuration) / 2) * 1000 || 5 * 1000; 31212 this.finalRenditionTimeout = window__default['default'].setTimeout( 31213 this.media.bind(this, playlist, false), 31214 delay 31215 ); 31216 return; 31217 } 31218 const startingState = this.state; 31219 const mediaChange = !this.media_ || playlist.id !== this.media_.id; 31220 const mainPlaylistRef = this.main.playlists[playlist.id]; // switch to fully loaded playlists immediately 31221 31222 if ( 31223 (mainPlaylistRef && mainPlaylistRef.endList) || 31224 // handle the case of a playlist object (e.g., if using vhs-json with a resolved 31225 // media playlist or, for the case of demuxed audio, a resolved audio media group) 31226 (playlist.endList && playlist.segments.length) 31227 ) { 31228 // abort outstanding playlist requests 31229 if (this.request) { 31230 this.request.onreadystatechange = null; 31231 this.request.abort(); 31232 this.request = null; 31233 } 31234 this.state = 'HAVE_METADATA'; 31235 this.media_ = playlist; // trigger media change if the active media has been updated 31236 31237 if (mediaChange) { 31238 this.trigger('mediachanging'); 31239 if (startingState === 'HAVE_MAIN_MANIFEST') { 31240 // The initial playlist was a main manifest, and the first media selected was 31241 // also provided (in the form of a resolved playlist object) as part of the 31242 // source object (rather than just a URL). Therefore, since the media playlist 31243 // doesn't need to be requested, loadedmetadata won't trigger as part of the 31244 // normal flow, and needs an explicit trigger here. 31245 this.trigger('loadedmetadata'); 31246 } else { 31247 this.trigger('mediachange'); 31248 } 31249 } 31250 return; 31251 } // We update/set the timeout here so that live playlists 31252 // that are not a media change will "start" the loader as expected. 31253 // We expect that this function will start the media update timeout 31254 // cycle again. This also prevents a playlist switch failure from 31255 // causing us to stall during live. 31256 31257 this.updateMediaUpdateTimeout_(refreshDelay(playlist, true)); // switching to the active playlist is a no-op 31258 31259 if (!mediaChange) { 31260 return; 31261 } 31262 this.state = 'SWITCHING_MEDIA'; // there is already an outstanding playlist request 31263 31264 if (this.request) { 31265 if (playlist.resolvedUri === this.request.url) { 31266 // requesting to switch to the same playlist multiple times 31267 // has no effect after the first 31268 return; 31269 } 31270 this.request.onreadystatechange = null; 31271 this.request.abort(); 31272 this.request = null; 31273 } // request the new playlist 31274 31275 if (this.media_) { 31276 this.trigger('mediachanging'); 31277 } 31278 this.pendingMedia_ = playlist; 31279 const metadata = { 31280 playlistInfo: { 31281 type: 'media', 31282 uri: playlist.uri, 31283 }, 31284 }; 31285 this.trigger({ 31286 type: 'playlistrequeststart', 31287 metadata, 31288 }); 31289 this.request = this.vhs_.xhr( 31290 { 31291 uri: playlist.resolvedUri, 31292 withCredentials: this.withCredentials, 31293 requestType: 'hls-playlist', 31294 }, 31295 (error, req) => { 31296 // disposed 31297 if (!this.request) { 31298 return; 31299 } 31300 playlist.lastRequest = Date.now(); 31301 playlist.resolvedUri = resolveManifestRedirect(playlist.resolvedUri, req); 31302 if (error) { 31303 return this.playlistRequestError(this.request, playlist, startingState); 31304 } 31305 this.trigger({ 31306 type: 'playlistrequestcomplete', 31307 metadata, 31308 }); 31309 this.haveMetadata({ 31310 playlistString: req.responseText, 31311 url: playlist.uri, 31312 id: playlist.id, 31313 }); // fire loadedmetadata the first time a media playlist is loaded 31314 31315 if (startingState === 'HAVE_MAIN_MANIFEST') { 31316 this.trigger('loadedmetadata'); 31317 } else { 31318 this.trigger('mediachange'); 31319 } 31320 } 31321 ); 31322 } 31323 /** 31324 * pause loading of the playlist 31325 */ 31326 31327 pause() { 31328 if (this.mediaUpdateTimeout) { 31329 window__default['default'].clearTimeout(this.mediaUpdateTimeout); 31330 this.mediaUpdateTimeout = null; 31331 } 31332 this.stopRequest(); 31333 if (this.state === 'HAVE_NOTHING') { 31334 // If we pause the loader before any data has been retrieved, its as if we never 31335 // started, so reset to an unstarted state. 31336 this.started = false;
31337 } // Need to restore state now that no activity is happening 31338 31339 if (this.state === 'SWITCHING_MEDIA') { 31340 // if the loader was in the process of switching media, it should either return to 31341 // HAVE_MAIN_MANIFEST or HAVE_METADATA depending on if the loader has loaded a media 31342 // playlist yet. This is determined by the existence of loader.media_ 31343 if (this.media_) { 31344 this.state = 'HAVE_METADATA'; 31345 } else { 31346 this.state = 'HAVE_MAIN_MANIFEST'; 31347 } 31348 } else if (this.state === 'HAVE_CURRENT_METADATA') { 31349 this.state = 'HAVE_METADATA'; 31350 } 31351 } 31352 /** 31353 * start loading of the playlist 31354 */ 31355 31356 load(shouldDelay) { 31357 if (this.mediaUpdateTimeout) { 31358 window__default['default'].clearTimeout(this.mediaUpdateTimeout); 31359 this.mediaUpdateTimeout = null; 31360 } 31361 const media = this.media(); 31362 if (shouldDelay) { 31363 const delay = media 31364 ? ((media.partTargetDuration || media.targetDuration) / 2) * 1000 31365 : 5 * 1000; 31366 this.mediaUpdateTimeout = window__default['default'].setTimeout(() => { 31367 this.mediaUpdateTimeout = null; 31368 this.load(); 31369 }, delay); 31370 return; 31371 } 31372 if (!this.started) { 31373 this.start(); 31374 return; 31375 } 31376 if (media && !media.endList) { 31377 this.trigger('mediaupdatetimeout'); 31378 } else { 31379 this.trigger('loadedplaylist'); 31380 } 31381 } 31382 updateMediaUpdateTimeout_(delay) { 31383 if (this.mediaUpdateTimeout) { 31384 window__default['default'].clearTimeout(this.mediaUpdateTimeout); 31385 this.mediaUpdateTimeout = null; 31386 } // we only have use mediaupdatetimeout for live playlists. 31387 31388 if (!this.media() || this.media().endList) { 31389 return; 31390 } 31391 this.mediaUpdateTimeout = window__default['default'].setTimeout(() => { 31392 this.mediaUpdateTimeout = null; 31393 this.trigger('mediaupdatetimeout'); 31394 this.updateMediaUpdateTimeout_(delay); 31395 }, delay); 31396 } 31397 /** 31398 * start loading of the playlist 31399 */ 31400 31401 start() { 31402 this.started = true; 31403 if (typeof this.src === 'object') { 31404 // in the case of an entirely constructed manifest object (meaning there's no actual 31405 // manifest on a server), default the uri to the page's href 31406 if (!this.src.uri) { 31407 this.src.uri = window__default['default'].location.href; 31408 } // resolvedUri is added on internally after the initial request. Since there's no 31409 // request for pre-resolved manifests, add on resolvedUri here. 31410 31411 this.src.resolvedUri = this.src.uri; // Since a manifest object was passed in as the source (instead of a URL), the first 31412 // request can be skipped (since the top level of the manifest, at a minimum, is 31413 // already available as a parsed manifest object). However, if the manifest object 31414 // represents a main playlist, some media playlists may need to be resolved before 31415 // the starting segment list is available. Therefore, go directly to setup of the 31416 // initial playlist, and let the normal flow continue from there. 31417 // 31418 // Note that the call to setup is asynchronous, as other sections of VHS may assume 31419 // that the first request is asynchronous. 31420 31421 setTimeout(() => { 31422 this.setupInitialPlaylist(this.src); 31423 }, 0); 31424 return; 31425 } 31426 const metadata = { 31427 playlistInfo: { 31428 type: 'multivariant', 31429 uri: this.src, 31430 }, 31431 }; 31432 this.trigger({ 31433 type: 'playlistrequeststart', 31434 metadata, 31435 }); // request the specified URL 31436 31437 this.request = this.vhs_.xhr( 31438 { 31439 uri: this.src, 31440 withCredentials: this.withCredentials, 31441 requestType: 'hls-playlist', 31442 }, 31443 (error, req) => { 31444 // disposed 31445 if (!this.request) { 31446 return; 31447 } // clear the loader's request reference 31448 31449 this.request = null; 31450 if (error) { 31451 this.error = { 31452 status: req.status, 31453 message: `HLS playlist request error at URL: ${this.src}.`, 31454 responseText: req.responseText, 31455 // MEDIA_ERR_NETWORK 31456 code: 2, 31457 metadata: getStreamingNetworkErrorMetadata({ 31458 requestType: req.requestType, 31459 request: req, 31460 error, 31461 }), 31462 }; 31463 if (this.state === 'HAVE_NOTHING') { 31464 this.started = false;
31465 } 31466 return this.trigger('error'); 31467 } 31468 this.trigger({ 31469 type: 'playlistrequestcomplete', 31470 metadata, 31471 }); 31472 this.src = resolveManifestRedirect(this.src, req); 31473 this.trigger({ 31474 type: 'playlistparsestart', 31475 metadata, 31476 }); 31477 const manifest = this.parseManifest_({ 31478 manifestString: req.responseText, 31479 url: this.src, 31480 }); // we haven't loaded any variant playlists here so we default to false for isLive. 31481 31482 metadata.parsedPlaylist = playlistMetadataPayload( 31483 manifest.playlists, 31484 metadata.playlistInfo.type, 31485 false 31486 ); 31487 this.trigger({ 31488 type: 'playlistparsecomplete', 31489 metadata, 31490 }); 31491 this.setupInitialPlaylist(manifest); 31492 } 31493 ); 31494 } 31495 srcUri() { 31496 return typeof this.src === 'string' ? this.src : this.src.uri; 31497 } 31498 /** 31499 * Given a manifest object that's either a main or media playlist, trigger the proper 31500 * events and set the state of the playlist loader. 31501 * 31502 * If the manifest object represents a main playlist, `loadedplaylist` will be 31503 * triggered to allow listeners to select a playlist. If none is selected, the loader 31504 * will default to the first one in the playlists array. 31505 * 31506 * If the manifest object represents a media playlist, `loadedplaylist` will be 31507 * triggered followed by `loadedmetadata`, as the only available playlist is loaded. 31508 * 31509 * In the case of a media playlist, a main playlist object wrapper with one playlist 31510 * will be created so that all logic can handle playlists in the same fashion (as an 31511 * assumed manifest object schema). 31512 * 31513 * @param {Object} manifest 31514 * The parsed manifest object 31515 */ 31516 31517 setupInitialPlaylist(manifest) { 31518 this.state = 'HAVE_MAIN_MANIFEST'; 31519 if (manifest.playlists) { 31520 this.main = manifest; 31521 addPropertiesToMain(this.main, this.srcUri()); // If the initial main playlist has playlists wtih segments already resolved, 31522 // then resolve URIs in advance, as they are usually done after a playlist request, 31523 // which may not happen if the playlist is resolved. 31524
vendor: 6,978 bytes, lines 31525-31692
31525 manifest.playlists.forEach((playlist) => { 31526 playlist.segments = getAllSegments(playlist); 31527 playlist.segments.forEach((segment) => { 31528 resolveSegmentUris(segment, playlist.resolvedUri); 31529 }); 31530 }); 31531 this.trigger('loadedplaylist'); 31532 if (!this.request) { 31533 // no media playlist was specifically selected so start 31534 // from the first listed one 31535 this.media(this.main.playlists[0]); 31536 } 31537 return; 31538 } // In order to support media playlists passed in as vhs-json, the case where the uri 31539 // is not provided as part of the manifest should be considered, and an appropriate 31540 // default used. 31541 31542 const uri = this.srcUri() || window__default['default'].location.href; 31543 this.main = mainForMedia(manifest, uri); 31544 this.haveMetadata({ 31545 playlistObject: manifest, 31546 url: uri, 31547 id: this.main.playlists[0].id, 31548 }); 31549 this.trigger('loadedmetadata'); 31550 } 31551 /** 31552 * Updates or deletes a preexisting pathway clone. 31553 * Ensures that all playlists related to the old pathway clone are 31554 * either updated or deleted. 31555 * 31556 * @param {Object} clone On update, the pathway clone object for the newly updated pathway clone. 31557 * On delete, the old pathway clone object to be deleted. 31558 * @param {boolean} isUpdate True if the pathway is to be updated, 31559 * false if it is meant to be deleted. 31560 */ 31561 31562 updateOrDeleteClone(clone, isUpdate) { 31563 const main = this.main; 31564 const pathway = clone.ID; 31565 let i = main.playlists.length; // Iterate backwards through the playlist so we can remove playlists if necessary. 31566 31567 while (i--) { 31568 const p = main.playlists[i]; 31569 if (p.attributes['PATHWAY-ID'] === pathway) { 31570 const oldPlaylistUri = p.resolvedUri; 31571 const oldPlaylistId = p.id; // update the indexed playlist and add new playlists by ID and URI 31572 31573 if (isUpdate) { 31574 const newPlaylistUri = this.createCloneURI_(p.resolvedUri, clone); 31575 const newPlaylistId = createPlaylistID(pathway, newPlaylistUri); 31576 const attributes = this.createCloneAttributes_(pathway, p.attributes); 31577 const updatedPlaylist = this.createClonePlaylist_(p, newPlaylistId, clone, attributes); 31578 main.playlists[i] = updatedPlaylist; 31579 main.playlists[newPlaylistId] = updatedPlaylist; 31580 main.playlists[newPlaylistUri] = updatedPlaylist; 31581 } else { 31582 // Remove the indexed playlist. 31583 main.playlists.splice(i, 1); 31584 } // Remove playlists by the old ID and URI. 31585 31586 delete main.playlists[oldPlaylistId]; 31587 delete main.playlists[oldPlaylistUri]; 31588 } 31589 } 31590 this.updateOrDeleteCloneMedia(clone, isUpdate); 31591 } 31592 /** 31593 * Updates or deletes media data based on the pathway clone object. 31594 * Due to the complexity of the media groups and playlists, in all cases 31595 * we remove all of the old media groups and playlists. 31596 * On updates, we then create new media groups and playlists based on the 31597 * new pathway clone object. 31598 * 31599 * @param {Object} clone The pathway clone object for the newly updated pathway clone. 31600 * @param {boolean} isUpdate True if the pathway is to be updated, 31601 * false if it is meant to be deleted. 31602 */ 31603 31604 updateOrDeleteCloneMedia(clone, isUpdate) { 31605 const main = this.main; 31606 const id = clone.ID; 31607 ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach((mediaType) => { 31608 if (!main.mediaGroups[mediaType] || !main.mediaGroups[mediaType][id]) { 31609 return; 31610 } 31611 for (const groupKey in main.mediaGroups[mediaType]) { 31612 // Remove all media playlists for the media group for this pathway clone. 31613 if (groupKey === id) { 31614 for (const labelKey in main.mediaGroups[mediaType][groupKey]) { 31615 const oldMedia = main.mediaGroups[mediaType][groupKey][labelKey]; 31616 oldMedia.playlists.forEach((p, i) => { 31617 const oldMediaPlaylist = main.playlists[p.id]; 31618 const oldPlaylistId = oldMediaPlaylist.id; 31619 const oldPlaylistUri = oldMediaPlaylist.resolvedUri; 31620 delete main.playlists[oldPlaylistId]; 31621 delete main.playlists[oldPlaylistUri]; 31622 }); 31623 } // Delete the old media group. 31624 31625 delete main.mediaGroups[mediaType][groupKey]; 31626 } 31627 } 31628 }); // Create the new media groups and playlists if there is an update. 31629 31630 if (isUpdate) { 31631 this.createClonedMediaGroups_(clone); 31632 } 31633 } 31634 /** 31635 * Given a pathway clone object, clones all necessary playlists. 31636 * 31637 * @param {Object} clone The pathway clone object. 31638 * @param {Object} basePlaylist The original playlist to clone from. 31639 */ 31640 31641 addClonePathway(clone, basePlaylist = {}) { 31642 const main = this.main; 31643 const index = main.playlists.length; 31644 const uri = this.createCloneURI_(basePlaylist.resolvedUri, clone); 31645 const playlistId = createPlaylistID(clone.ID, uri); 31646 const attributes = this.createCloneAttributes_(clone.ID, basePlaylist.attributes); 31647 const playlist = this.createClonePlaylist_(basePlaylist, playlistId, clone, attributes); 31648 main.playlists[index] = playlist; // add playlist by ID and URI 31649 31650 main.playlists[playlistId] = playlist; 31651 main.playlists[uri] = playlist; 31652 this.createClonedMediaGroups_(clone); 31653 } 31654 /** 31655 * Given a pathway clone object we create clones of all media. 31656 * In this function, all necessary information and updated playlists 31657 * are added to the `mediaGroup` object. 31658 * Playlists are also added to the `playlists` array so the media groups 31659 * will be properly linked. 31660 * 31661 * @param {Object} clone The pathway clone object. 31662 */ 31663 31664 createClonedMediaGroups_(clone) { 31665 const id = clone.ID; 31666 const baseID = clone['BASE-ID']; 31667 const main = this.main; 31668 ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach((mediaType) => { 31669 // If the media type doesn't exist, or there is already a clone, skip 31670 // to the next media type. 31671 if (!main.mediaGroups[mediaType] || main.mediaGroups[mediaType][id]) { 31672 return; 31673 } 31674 for (const groupKey in main.mediaGroups[mediaType]) { 31675 if (groupKey === baseID) { 31676 // Create the group. 31677 main.mediaGroups[mediaType][id] = {}; 31678 } else { 31679 // There is no need to iterate over label keys in this case. 31680 continue; 31681 } 31682 for (const labelKey in main.mediaGroups[mediaType][groupKey]) { 31683 const oldMedia = main.mediaGroups[mediaType][groupKey][labelKey]; 31684 main.mediaGroups[mediaType][id][labelKey] = _extends__default['default']({}, oldMedia); 31685 const newMedia = main.mediaGroups[mediaType][id][labelKey]; // update URIs on the media 31686 31687 const newUri = this.createCloneURI_(oldMedia.resolvedUri, clone); 31688 newMedia.resolvedUri = newUri; 31689 newMedia.uri = newUri; // Reset playlists in the new media group. 31690 31691 newMedia.playlists = []; // Create new playlists in the newly cloned media group. 31692
31693 oldMedia.playlists.forEach((p, i) => { 31694 const oldMediaPlaylist = main.playlists[p.id]; 31695 const group = groupID(mediaType, id, labelKey); 31696 const newPlaylistID = createPlaylistID(id, group); // Check to see if it already exists 31697 31698 if (oldMediaPlaylist && !main.playlists[newPlaylistID]) { 31699 const newMediaPlaylist = this.createClonePlaylist_( 31700 oldMediaPlaylist, 31701 newPlaylistID, 31702 clone 31703 ); 31704 const newPlaylistUri = newMediaPlaylist.resolvedUri; 31705 main.playlists[newPlaylistID] = newMediaPlaylist; 31706 main.playlists[newPlaylistUri] = newMediaPlaylist; 31707 } 31708 newMedia.playlists[i] = this.createClonePlaylist_(p, newPlaylistID, clone); 31709 }); 31710 } 31711 } 31712 }); 31713 } 31714 /** 31715 * Using the original playlist to be cloned, and the pathway clone object 31716 * information, we create a new playlist. 31717 * 31718 * @param {Object} basePlaylist The original playlist to be cloned from. 31719 * @param {string} id The desired id of the newly cloned playlist. 31720 * @param {Object} clone The pathway clone object. 31721 * @param {Object} attributes An optional object to populate the `attributes` property in the playlist. 31722 * 31723 * @return {Object} The combined cloned playlist. 31724 */ 31725 31726 createClonePlaylist_(basePlaylist, id, clone, attributes) { 31727 const uri = this.createCloneURI_(basePlaylist.resolvedUri, clone); 31728 const newProps = { 31729 resolvedUri: uri, 31730 uri, 31731 id, 31732 }; // Remove all segments from previous playlist in the clone. 31733 31734 if (basePlaylist.segments) { 31735 newProps.segments = []; 31736 } 31737 if (attributes) { 31738 newProps.attributes = attributes; 31739 } 31740 return merge(basePlaylist, newProps); 31741 } 31742 /** 31743 * Generates an updated URI for a cloned pathway based on the original 31744 * pathway's URI and the paramaters from the pathway clone object in the 31745 * content steering server response. 31746 * 31747 * @param {string} baseUri URI to be updated in the cloned pathway. 31748 * @param {Object} clone The pathway clone object. 31749 * 31750 * @return {string} The updated URI for the cloned pathway. 31751 */ 31752 31753 createCloneURI_(baseURI, clone) { 31754 const uri = new URL(baseURI); 31755 uri.hostname = clone['URI-REPLACEMENT'].HOST; 31756 const params = clone['URI-REPLACEMENT'].PARAMS; // Add params to the cloned URL. 31757 31758 for (const key of Object.keys(params)) { 31759 uri.searchParams.set(key, params[key]); 31760 } 31761 return uri.href; 31762 } 31763 /** 31764 * Helper function to create the attributes needed for the new clone. 31765 * This mainly adds the necessary media attributes. 31766 * 31767 * @param {string} id The pathway clone object ID. 31768 * @param {Object} oldAttributes The old attributes to compare to. 31769 * @return {Object} The new attributes to add to the playlist. 31770 */ 31771 31772 createCloneAttributes_(id, oldAttributes) { 31773 const attributes = { 31774 ['PATHWAY-ID']: id, 31775 }; 31776 ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach((mediaType) => { 31777 if (oldAttributes[mediaType]) { 31778 attributes[mediaType] = id; 31779 } 31780 }); 31781 return attributes; 31782 } 31783 /** 31784 * Returns the key ID set from a playlist 31785 * 31786 * @param {playlist} playlist to fetch the key ID set from. 31787 * @return a Set of 32 digit hex strings that represent the unique keyIds for that playlist. 31788 */ 31789 31790 getKeyIdSet(playlist) { 31791 if (playlist.contentProtection) { 31792 const keyIds = new Set(); 31793 for (const keysystem in playlist.contentProtection) { 31794 const keyId = playlist.contentProtection[keysystem].attributes.keyId; 31795 if (keyId) { 31796 keyIds.add(keyId.toLowerCase()); 31797 } 31798 } 31799 return keyIds; 31800 } 31801 } 31802} 31803 31804/** 31805 * @file xhr.js 31806 */ 31807 31808const callbackWrapper = function (request, error, response, callback) { 31809 const reqResponse = 31810 request.responseType === 'arraybuffer' ? request.response : request.responseText; 31811 if (!error && reqResponse) { 31812 request.responseTime = Date.now(); 31813 request.roundTripTime = request.responseTime - request.requestTime; 31814 request.bytesReceived = reqResponse.byteLength || reqResponse.length; 31815 if (!request.bandwidth) { 31816 request.bandwidth = Math.floor((request.bytesReceived / request.roundTripTime) * 8 * 1000); 31817 } 31818 } 31819 if (response.headers) { 31820 request.responseHeaders = response.headers; 31821 } // videojs.xhr now uses a specific code on the error
vendor: 4,835 bytes, lines 31822-31946
31822 // object to signal that a request has timed out instead 31823 // of setting a boolean on the request object 31824 31825 if (error && error.code === 'ETIMEDOUT') { 31826 request.timedout = true; 31827 } // videojs.xhr no longer considers status codes outside of 200 and 0 31828 // (for file uris) to be errors, but the old XHR did, so emulate that 31829 // behavior. Status 206 may be used in response to byterange requests. 31830 31831 if ( 31832 !error && 31833 !request.aborted && 31834 response.statusCode !== 200 && 31835 response.statusCode !== 206 && 31836 response.statusCode !== 0 31837 ) { 31838 error = new Error( 31839 'XHR Failed with a response of: ' + (request && (reqResponse || request.responseText)) 31840 ); 31841 } 31842 callback(error, request); 31843}; 31844/** 31845 * Iterates over the request hooks Set and calls them in order 31846 * 31847 * @param {Set} hooks the hook Set to iterate over 31848 * @param {Object} options the request options to pass to the xhr wrapper 31849 * @return the callback hook function return value, the modified or new options Object. 31850 */ 31851 31852const callAllRequestHooks = (requestSet, options) => { 31853 if (!requestSet || !requestSet.size) { 31854 return; 31855 } 31856 let newOptions = options; 31857 requestSet.forEach((requestCallback) => { 31858 newOptions = requestCallback(newOptions); 31859 }); 31860 return newOptions; 31861}; 31862/** 31863 * Iterates over the response hooks Set and calls them in order. 31864 * 31865 * @param {Set} hooks the hook Set to iterate over 31866 * @param {Object} request the xhr request object 31867 * @param {Object} error the xhr error object 31868 * @param {Object} response the xhr response object 31869 */ 31870 31871const callAllResponseHooks = (responseSet, request, error, response) => { 31872 if (!responseSet || !responseSet.size) { 31873 return; 31874 } 31875 responseSet.forEach((responseCallback) => { 31876 responseCallback(request, error, response); 31877 }); 31878}; 31879const xhrFactory = function () { 31880 const xhr = function XhrFunction(options, callback) { 31881 // Add a default timeout 31882 options = merge( 31883 { 31884 timeout: 45e3, 31885 }, 31886 options 31887 ); // Allow an optional user-specified function to modify the option 31888 // object before we construct the xhr request 31889 // TODO: Remove beforeRequest in the next major release. 31890 31891 const beforeRequest = XhrFunction.beforeRequest || videojs.Vhs.xhr.beforeRequest; // onRequest and onResponse hooks as a Set, at either the player or global level. 31892 // TODO: new Set added here for beforeRequest alias. Remove this when beforeRequest is removed. 31893 31894 const _requestCallbackSet = 31895 XhrFunction._requestCallbackSet || videojs.Vhs.xhr._requestCallbackSet || new Set(); 31896 const _responseCallbackSet = 31897 XhrFunction._responseCallbackSet || videojs.Vhs.xhr._responseCallbackSet; 31898 if (beforeRequest && typeof beforeRequest === 'function') { 31899 videojs.log.warn('beforeRequest is deprecated, use onRequest instead.'); 31900 _requestCallbackSet.add(beforeRequest); 31901 } // Use the standard videojs.xhr() method unless `videojs.Vhs.xhr` has been overriden 31902 // TODO: switch back to videojs.Vhs.xhr.name === 'XhrFunction' when we drop IE11 31903 31904 const xhrMethod = videojs.Vhs.xhr.original === true ? videojs.xhr : videojs.Vhs.xhr; // call all registered onRequest hooks, assign new options. 31905 31906 const beforeRequestOptions = callAllRequestHooks(_requestCallbackSet, options); // Remove the beforeRequest function from the hooks set so stale beforeRequest functions are not called. 31907 31908 _requestCallbackSet.delete(beforeRequest); // xhrMethod will call XMLHttpRequest.open and XMLHttpRequest.send 31909 31910 const request = xhrMethod(beforeRequestOptions || options, function (error, response) { 31911 // call all registered onResponse hooks 31912 callAllResponseHooks(_responseCallbackSet, request, error, response); 31913 return callbackWrapper(request, error, response, callback); 31914 }); 31915 const originalAbort = request.abort; 31916 request.abort = function () { 31917 request.aborted = true; 31918 return originalAbort.apply(request, arguments); 31919 }; 31920 request.uri = options.uri; 31921 request.requestType = options.requestType; 31922 request.requestTime = Date.now(); 31923 return request; 31924 }; 31925 xhr.original = true; 31926 return xhr; 31927}; 31928/** 31929 * Turns segment byterange into a string suitable for use in 31930 * HTTP Range requests 31931 * 31932 * @param {Object} byterange - an object with two values defining the start and end 31933 * of a byte-range 31934 */ 31935 31936const byterangeStr = function (byterange) { 31937 // `byterangeEnd` is one less than `offset + length` because the HTTP range 31938 // header uses inclusive ranges 31939 let byterangeEnd; 31940 const byterangeStart = byterange.offset; 31941 if (typeof byterange.offset === 'bigint' || typeof byterange.length === 'bigint') { 31942 byterangeEnd = 31943 window__default['default'].BigInt(byterange.offset) + 31944 window__default['default'].BigInt(byterange.length) - 31945 window__default['default'].BigInt(1); 31946 } else {
vendor: 4,101 bytes, lines 31947-32104
31947 byterangeEnd = byterange.offset + byterange.length - 1; 31948 } 31949 return 'bytes=' + byterangeStart + '-' + byterangeEnd; 31950}; 31951/** 31952 * Defines headers for use in the xhr request for a particular segment. 31953 * 31954 * @param {Object} segment - a simplified copy of the segmentInfo object 31955 * from SegmentLoader 31956 */ 31957 31958const segmentXhrHeaders = function (segment) { 31959 const headers = {}; 31960 if (segment.byterange) { 31961 headers.Range = byterangeStr(segment.byterange); 31962 } 31963 return headers; 31964}; 31965 31966/** 31967 * @file bin-utils.js 31968 */ 31969 31970/** 31971 * convert a TimeRange to text 31972 * 31973 * @param {TimeRange} range the timerange to use for conversion 31974 * @param {number} i the iterator on the range to convert 31975 * @return {string} the range in string format 31976 */ 31977 31978const textRange = function (range, i) { 31979 return range.start(i) + '-' + range.end(i); 31980}; 31981/** 31982 * format a number as hex string 31983 * 31984 * @param {number} e The number 31985 * @param {number} i the iterator 31986 * @return {string} the hex formatted number as a string 31987 */ 31988 31989const formatHexString = function (e, i) { 31990 const value = e.toString(16); 31991 return '00'.substring(0, 2 - value.length) + value + (i % 2 ? ' ' : ''); 31992}; 31993const formatAsciiString = function (e) { 31994 if (e >= 0x20 && e < 0x7e) { 31995 return String.fromCharCode(e); 31996 } 31997 return '.'; 31998}; 31999/** 32000 * Creates an object for sending to a web worker modifying properties that are TypedArrays 32001 * into a new object with seperated properties for the buffer, byteOffset, and byteLength. 32002 * 32003 * @param {Object} message 32004 * Object of properties and values to send to the web worker 32005 * @return {Object} 32006 * Modified message with TypedArray values expanded 32007 * @function createTransferableMessage 32008 */ 32009 32010const createTransferableMessage = function (message) { 32011 const transferable = {}; 32012 Object.keys(message).forEach((key) => { 32013 const value = message[key]; 32014 if (byteHelpers.isArrayBufferView(value)) { 32015 transferable[key] = { 32016 bytes: value.buffer, 32017 byteOffset: value.byteOffset, 32018 byteLength: value.byteLength, 32019 }; 32020 } else { 32021 transferable[key] = value; 32022 } 32023 }); 32024 return transferable; 32025}; 32026/** 32027 * Returns a unique string identifier for a media initialization 32028 * segment. 32029 * 32030 * @param {Object} initSegment 32031 * the init segment object. 32032 * 32033 * @return {string} the generated init segment id 32034 */ 32035 32036const initSegmentId = function (initSegment) { 32037 const byterange = initSegment.byterange || { 32038 length: Infinity, 32039 offset: 0, 32040 }; 32041 return [byterange.length, byterange.offset, initSegment.resolvedUri].join(','); 32042}; 32043/** 32044 * Returns a unique string identifier for a media segment key. 32045 * 32046 * @param {Object} key the encryption key 32047 * @return {string} the unique id for the media segment key. 32048 */ 32049 32050const segmentKeyId = function (key) { 32051 return key.resolvedUri; 32052}; 32053/** 32054 * utils to help dump binary data to the console 32055 * 32056 * @param {Array|TypedArray} data 32057 * data to dump to a string 32058 * 32059 * @return {string} the data as a hex string. 32060 */ 32061 32062const hexDump = (data) => { 32063 const bytes = Array.prototype.slice.call(data); 32064 const step = 16; 32065 let result = ''; 32066 let hex; 32067 let ascii; 32068 for (let j = 0; j < bytes.length / step; j++) { 32069 hex = bytes 32070 .slice(j * step, j * step + step) 32071 .map(formatHexString) 32072 .join(''); 32073 ascii = bytes 32074 .slice(j * step, j * step + step) 32075 .map(formatAsciiString) 32076 .join(''); 32077 result += hex + ' ' + ascii + '\n'; 32078 } 32079 return result; 32080}; 32081const tagDump = ({ bytes }) => hexDump(bytes); 32082const textRanges = (ranges) => { 32083 let result = ''; 32084 let i; 32085 for (i = 0; i < ranges.length; i++) { 32086 result += textRange(ranges, i) + ' '; 32087 } 32088 return result; 32089}; 32090var utils = /*#__PURE__*/ Object.freeze({ 32091 __proto__: null, 32092 createTransferableMessage: createTransferableMessage, 32093 initSegmentId: initSegmentId, 32094 segmentKeyId: segmentKeyId, 32095 hexDump: hexDump, 32096 tagDump: tagDump, 32097 textRanges: textRanges, 32098}); 32099 32100// TODO handle fmp4 case where the timing info is accurate and doesn't involve transmux 32101// 25% was arbitrarily chosen, and may need to be refined over time. 32102 32103const SEGMENT_END_FUDGE_PERCENT = 0.25; 32104/**
32105 * Converts a player time (any time that can be gotten/set from player.currentTime(), 32106 * e.g., any time within player.seekable().start(0) to player.seekable().end(0)) to a 32107 * program time (any time referencing the real world (e.g., EXT-X-PROGRAM-DATE-TIME)). 32108 * 32109 * The containing segment is required as the EXT-X-PROGRAM-DATE-TIME serves as an "anchor 32110 * point" (a point where we have a mapping from program time to player time, with player 32111 * time being the post transmux start of the segment). 32112 * 32113 * For more details, see [this doc](../../docs/program-time-from-player-time.md). 32114 * 32115 * @param {number} playerTime the player time 32116 * @param {Object} segment the segment which contains the player time 32117 * @return {Date} program time 32118 */ 32119 32120const playerTimeToProgramTime = (playerTime, segment) => { 32121 if (!segment.dateTimeObject) { 32122 // Can't convert without an "anchor point" for the program time (i.e., a time that can 32123 // be used to map the start of a segment with a real world time). 32124 return null; 32125 } 32126 const transmuxerPrependedSeconds = segment.videoTimingInfo.transmuxerPrependedSeconds; 32127 const transmuxedStart = segment.videoTimingInfo.transmuxedPresentationStart; // get the start of the content from before old content is prepended 32128 32129 const startOfSegment = transmuxedStart + transmuxerPrependedSeconds; 32130 const offsetFromSegmentStart = playerTime - startOfSegment; 32131 return new Date(segment.dateTimeObject.getTime() + offsetFromSegmentStart * 1000); 32132}; 32133const originalSegmentVideoDuration = (videoTimingInfo) => { 32134 return ( 32135 videoTimingInfo.transmuxedPresentationEnd - 32136 videoTimingInfo.transmuxedPresentationStart - 32137 videoTimingInfo.transmuxerPrependedSeconds 32138 ); 32139}; 32140/** 32141 * Finds a segment that contains the time requested given as an ISO-8601 string. The 32142 * returned segment might be an estimate or an accurate match. 32143 * 32144 * @param {string} programTime The ISO-8601 programTime to find a match for 32145 * @param {Object} playlist A playlist object to search within 32146 */ 32147 32148const findSegmentForProgramTime = (programTime, playlist) => { 32149 // Assumptions: 32150 // - verifyProgramDateTimeTags has already been run 32151 // - live streams have been started 32152 let dateTimeObject; 32153 try { 32154 dateTimeObject = new Date(programTime); 32155 } catch (e) { 32156 return null; 32157 } 32158 if (!playlist || !playlist.segments || playlist.segments.length === 0) { 32159 return null; 32160 } 32161 let segment = playlist.segments[0]; 32162 if (dateTimeObject < new Date(segment.dateTimeObject)) { 32163 // Requested time is before stream start. 32164 return null; 32165 } 32166 for (let i = 0; i < playlist.segments.length - 1; i++) { 32167 segment = playlist.segments[i]; 32168 const nextSegmentStart = new Date(playlist.segments[i + 1].dateTimeObject); 32169 if (dateTimeObject < nextSegmentStart) { 32170 break; 32171 } 32172 } 32173 const lastSegment = playlist.segments[playlist.segments.length - 1]; 32174 const lastSegmentStart = lastSegment.dateTimeObject; 32175 const lastSegmentDuration = lastSegment.videoTimingInfo 32176 ? originalSegmentVideoDuration(lastSegment.videoTimingInfo) 32177 : lastSegment.duration + lastSegment.duration * SEGMENT_END_FUDGE_PERCENT; 32178 const lastSegmentEnd = new Date(lastSegmentStart.getTime() + lastSegmentDuration * 1000); 32179 if (dateTimeObject > lastSegmentEnd) { 32180 // Beyond the end of the stream, or our best guess of the end of the stream. 32181 return null; 32182 } 32183 if (dateTimeObject > new Date(lastSegmentStart)) { 32184 segment = lastSegment; 32185 } 32186 return { 32187 segment, 32188 estimatedStart: segment.videoTimingInfo 32189 ? segment.videoTimingInfo.transmuxedPresentationStart 32190 : Playlist.duration(playlist, playlist.mediaSequence + playlist.segments.indexOf(segment)), 32191 // Although, given that all segments have accurate date time objects, the segment 32192 // selected should be accurate, unless the video has been transmuxed at some point 32193 // (determined by the presence of the videoTimingInfo object), the segment's "player 32194 // time" (the start time in the player) can't be considered accurate. 32195 type: segment.videoTimingInfo ? 'accurate' : 'estimate', 32196 }; 32197}; 32198/** 32199 * Finds a segment that contains the given player time(in seconds). 32200 * 32201 * @param {number} time The player time to find a match for 32202 * @param {Object} playlist A playlist object to search within 32203 */ 32204 32205const findSegmentForPlayerTime = (time, playlist) => { 32206 // Assumptions: 32207 // - there will always be a segment.duration 32208 // - we can start from zero
32209 // - segments are in time order 32210 if (!playlist || !playlist.segments || playlist.segments.length === 0) { 32211 return null; 32212 } 32213 let segmentEnd = 0; 32214 let segment; 32215 for (let i = 0; i < playlist.segments.length; i++) { 32216 segment = playlist.segments[i]; // videoTimingInfo is set after the segment is downloaded and transmuxed, and 32217 // should contain the most accurate values we have for the segment's player times. 32218 // 32219 // Use the accurate transmuxedPresentationEnd value if it is available, otherwise fall 32220 // back to an estimate based on the manifest derived (inaccurate) segment.duration, to 32221 // calculate an end value. 32222 32223 segmentEnd = segment.videoTimingInfo 32224 ? segment.videoTimingInfo.transmuxedPresentationEnd 32225 : segmentEnd + segment.duration; 32226 if (time <= segmentEnd) { 32227 break; 32228 } 32229 } 32230 const lastSegment = playlist.segments[playlist.segments.length - 1]; 32231 if (lastSegment.videoTimingInfo && lastSegment.videoTimingInfo.transmuxedPresentationEnd < time) { 32232 // The time requested is beyond the stream end. 32233 return null; 32234 } 32235 if (time > segmentEnd) { 32236 // The time is within or beyond the last segment. 32237 // 32238 // Check to see if the time is beyond a reasonable guess of the end of the stream. 32239 if (time > segmentEnd + lastSegment.duration * SEGMENT_END_FUDGE_PERCENT) { 32240 // Technically, because the duration value is only an estimate, the time may still 32241 // exist in the last segment, however, there isn't enough information to make even 32242 // a reasonable estimate. 32243 return null; 32244 } 32245 segment = lastSegment; 32246 } 32247 return { 32248 segment, 32249 estimatedStart: segment.videoTimingInfo 32250 ? segment.videoTimingInfo.transmuxedPresentationStart 32251 : segmentEnd - segment.duration, 32252 // Because videoTimingInfo is only set after transmux, it is the only way to get 32253 // accurate timing values. 32254 type: segment.videoTimingInfo ? 'accurate' : 'estimate', 32255 }; 32256}; 32257/** 32258 * Gives the offset of the comparisonTimestamp from the programTime timestamp in seconds. 32259 * If the offset returned is positive, the programTime occurs after the 32260 * comparisonTimestamp. 32261 * If the offset is negative, the programTime occurs before the comparisonTimestamp. 32262 * 32263 * @param {string} comparisonTimeStamp An ISO-8601 timestamp to compare against 32264 * @param {string} programTime The programTime as an ISO-8601 string 32265 * @return {number} offset 32266 */ 32267 32268const getOffsetFromTimestamp = (comparisonTimeStamp, programTime) => { 32269 let segmentDateTime; 32270 let programDateTime; 32271 try { 32272 segmentDateTime = new Date(comparisonTimeStamp); 32273 programDateTime = new Date(programTime); 32274 } catch (e) { 32275 // TODO handle error 32276 } 32277 const segmentTimeEpoch = segmentDateTime.getTime(); 32278 const programTimeEpoch = programDateTime.getTime(); 32279 return (programTimeEpoch - segmentTimeEpoch) / 1000; 32280}; 32281/** 32282 * Checks that all segments in this playlist have programDateTime tags. 32283 * 32284 * @param {Object} playlist A playlist object 32285 */ 32286 32287const verifyProgramDateTimeTags = (playlist) => { 32288 if (!playlist.segments || playlist.segments.length === 0) { 32289 return false; 32290 } 32291 for (let i = 0; i < playlist.segments.length; i++) { 32292 const segment = playlist.segments[i]; 32293 if (!segment.dateTimeObject) { 32294 return false; 32295 } 32296 } 32297 return true; 32298}; 32299/** 32300 * Returns the programTime of the media given a playlist and a playerTime. 32301 * The playlist must have programDateTime tags for a programDateTime tag to be returned. 32302 * If the segments containing the time requested have not been buffered yet, an estimate 32303 * may be returned to the callback. 32304 * 32305 * @param {Object} args 32306 * @param {Object} args.playlist A playlist object to search within 32307 * @param {number} time A playerTime in seconds 32308 * @param {Function} callback(err, programTime) 32309 * @return {string} err.message A detailed error message 32310 * @return {Object} programTime 32311 * @return {number} programTime.mediaSeconds The streamTime in seconds 32312 * @return {string} programTime.programDateTime The programTime as an ISO-8601 String 32313 */ 32314 32315const getProgramTime = ({ playlist, time = undefined, callback }) => { 32316 if (!callback) { 32317 throw new Error('getProgramTime: callback must be provided'); 32318 } 32319 if (!playlist || time === undefined) { 32320 return callback({ 32321 message: 'getProgramTime: playlist and time must be provided', 32322 }); 32323 } 32324 const matchedSegment = findSegmentForPlayerTime(time, playlist); 32325 if (!matchedSegment) { 32326 return callback({ 32327 message: 'valid programTime was not found', 32328 }); 32329 } 32330 if (matchedSegment.type === 'estimate') { 32331 return callback({ 32332 message: 32333 'Accurate programTime could not be determined.' + 32334 ' Please seek to e.seekTime and try again', 32335 seekTime: matchedSegment.estimatedStart, 32336 }); 32337 } 32338 const programTimeObject = { 32339 mediaSeconds: time, 32340 }; 32341 const programTime = playerTimeToProgramTime(time, matchedSegment.segment); 32342 if (programTime) { 32343 programTimeObject.programDateTime = programTime.toISOString(); 32344 } 32345 return callback(null, programTimeObject); 32346}; 32347/** 32348 * Seeks in the player to a time that matches the given programTime ISO-8601 string. 32349 * 32350 * @param {Object} args 32351 * @param {string} args.programTime A programTime to seek to as an ISO-8601 String 32352 * @param {Object} args.playlist A playlist to look within 32353 * @param {number} args.retryCount The number of times to try for an accurate seek. Default is 2. 32354 * @param {Function} args.seekTo A method to perform a seek 32355 * @param {boolean} args.pauseAfterSeek Whether to end in a paused state after seeking. Default is true. 32356 * @param {Object} args.tech The tech to seek on 32357 * @param {Function} args.callback(err, newTime) A callback to return the new time to 32358 * @return {string} err.message A detailed error message 32359 * @return {number} newTime The exact time that was seeked to in seconds 32360 */ 32361 32362const seekToProgramTime = ({ 32363 programTime, 32364 playlist, 32365 retryCount = 2, 32366 seekTo, 32367 pauseAfterSeek = true, 32368 tech, 32369 callback, 32370}) => { 32371 if (!callback) { 32372 throw new Error('seekToProgramTime: callback must be provided'); 32373 } 32374 if (typeof programTime === 'undefined' || !playlist || !seekTo) { 32375 return callback({ 32376 message: 'seekToProgramTime: programTime, seekTo and playlist must be provided', 32377 }); 32378 } 32379 if (!playlist.endList && !tech.hasStarted_) { 32380 return callback({ 32381 message: 'player must be playing a live stream to start buffering', 32382 }); 32383 } 32384 if (!verifyProgramDateTimeTags(playlist)) { 32385 return callback({ 32386 message: 'programDateTime tags must be provided in the manifest ' + playlist.resolvedUri, 32387 }); 32388 } 32389 const matchedSegment = findSegmentForProgramTime(programTime, playlist); // no match 32390 32391 if (!matchedSegment) { 32392 return callback({ 32393 message: `${programTime} was not found in the stream`, 32394 }); 32395 } 32396 const segment = matchedSegment.segment; 32397 const mediaOffset = getOffsetFromTimestamp(segment.dateTimeObject, programTime); 32398 if (matchedSegment.type === 'estimate') { 32399 // we've run out of retries 32400 if (retryCount === 0) { 32401 return callback({ 32402 message: `${programTime} is not buffered yet. Try again`, 32403 }); 32404 } 32405 seekTo(matchedSegment.estimatedStart + mediaOffset); 32406 tech.one('seeked', () => { 32407 seekToProgramTime({ 32408 programTime, 32409 playlist, 32410 retryCount: retryCount - 1, 32411 seekTo, 32412 pauseAfterSeek, 32413 tech, 32414 callback, 32415 }); 32416 }); 32417 return; 32418 } // Since the segment.start value is determined from the buffered end or ending time 32419 // of the prior segment, the seekToTime doesn't need to account for any transmuxer 32420 // modifications. 32421 32422 const seekToTime = segment.start + mediaOffset;
32423 const seekedCallback = () => { 32424 return callback(null, tech.currentTime()); 32425 }; // listen for seeked event 32426 32427 tech.one('seeked', seekedCallback); // pause before seeking as video.js will restore this state 32428 32429 if (pauseAfterSeek) { 32430 tech.pause(); 32431 } 32432 seekTo(seekToTime); 32433}; 32434 32435// which will only happen if the request is complete. 32436 32437const callbackOnCompleted = (request, cb) => { 32438 if (request.readyState === 4) { 32439 return cb(); 32440 } 32441 return; 32442}; 32443const containerRequest = (uri, xhr, cb, requestType) => { 32444 let bytes = []; 32445 let id3Offset; 32446 let finished = false; 32447 const endRequestAndCallback = function (err, req, type, _bytes) { 32448 req.abort(); 32449 finished = true; 32450 return cb(err, req, type, _bytes); 32451 }; 32452 const progressListener = function (error, request) { 32453 if (finished) { 32454 return; 32455 } 32456 if (error) { 32457 error.metadata = getStreamingNetworkErrorMetadata({ 32458 requestType, 32459 request, 32460 error, 32461 }); 32462 return endRequestAndCallback(error, request, '', bytes); 32463 } // grap the new part of content that was just downloaded 32464 32465 const newPart = request.responseText.substring( 32466 (bytes && bytes.byteLength) || 0, 32467 request.responseText.length 32468 ); // add that onto bytes 32469 32470 bytes = byteHelpers.concatTypedArrays(bytes, byteHelpers.stringToBytes(newPart, true)); 32471 id3Offset = id3Offset || id3Helpers.getId3Offset(bytes); // we need at least 10 bytes to determine a type 32472 // or we need at least two bytes after an id3Offset 32473 32474 if (bytes.length < 10 || (id3Offset && bytes.length < id3Offset + 2)) { 32475 return callbackOnCompleted(request, () => endRequestAndCallback(error, request, '', bytes)); 32476 } 32477 const type = containers.detectContainerForBytes(bytes); // if this looks like a ts segment but we don't have enough data 32478 // to see the second sync byte, wait until we have enough data 32479 // before declaring it ts 32480 32481 if (type === 'ts' && bytes.length < 188) { 32482 return callbackOnCompleted(request, () => endRequestAndCallback(error, request, '', bytes)); 32483 } // this may be an unsynced ts segment 32484 // wait for 376 bytes before detecting no container 32485 32486 if (!type && bytes.length < 376) { 32487 return callbackOnCompleted(request, () => endRequestAndCallback(error, request, '', bytes)); 32488 } 32489 return endRequestAndCallback(null, request, type, bytes); 32490 }; 32491 const options = { 32492 uri, 32493 beforeSend(request) { 32494 // this forces the browser to pass the bytes to us unprocessed 32495 request.overrideMimeType('text/plain; charset=x-user-defined'); 32496 request.addEventListener('progress', function ({ total, loaded }) { 32497 return callbackWrapper( 32498 request, 32499 null, 32500 { 32501 statusCode: request.status, 32502 }, 32503 progressListener 32504 ); 32505 }); 32506 }, 32507 }; 32508 const request = xhr(options, function (error, response) { 32509 return callbackWrapper(request, error, response, progressListener); 32510 }); 32511 return request; 32512}; 32513const { EventTarget } = videojs; 32514const dashPlaylistUnchanged = function (a, b) { 32515 if (!isPlaylistUnchanged(a, b)) { 32516 return false; 32517 } // for dash the above check will often return true in scenarios where 32518 // the playlist actually has changed because mediaSequence isn't a 32519 // dash thing, and we often set it to 1. So if the playlists have the same amount 32520 // of segments we return true. 32521 // So for dash we need to make sure that the underlying segments are different. 32522 // if sidx changed then the playlists are different. 32523 32524 if (a.sidx && b.sidx && (a.sidx.offset !== b.sidx.offset || a.sidx.length !== b.sidx.length)) { 32525 return false; 32526 } else if ((!a.sidx && b.sidx) || (a.sidx && !b.sidx)) { 32527 return false; 32528 } // one or the other does not have segments 32529 // there was a change. 32530 32531 if ((a.segments && !b.segments) || (!a.segments && b.segments)) { 32532 return false; 32533 } // neither has segments nothing changed 32534 32535 if (!a.segments && !b.segments) { 32536 return true; 32537 } // check segments themselves 32538 32539 for (let i = 0; i < a.segments.length; i++) { 32540 const aSegment = a.segments[i]; 32541 const bSegment = b.segments[i]; // if uris are different between segments there was a change 32542 32543 if (aSegment.uri !== bSegment.uri) { 32544 return false;
vendor: 19,516 bytes, lines 32545-33107
32545 } // neither segment has a byterange, there will be no byterange change. 32546 32547 if (!aSegment.byterange && !bSegment.byterange) { 32548 continue; 32549 } 32550 const aByterange = aSegment.byterange; 32551 const bByterange = bSegment.byterange; // if byterange only exists on one of the segments, there was a change. 32552 32553 if ((aByterange && !bByterange) || (!aByterange && bByterange)) { 32554 return false; 32555 } // if both segments have byterange with different offsets, there was a change. 32556 32557 if (aByterange.offset !== bByterange.offset || aByterange.length !== bByterange.length) { 32558 return false; 32559 } 32560 } // if everything was the same with segments, this is the same playlist. 32561 32562 return true; 32563}; 32564/** 32565 * Use the representation IDs from the mpd object to create groupIDs, the NAME is set to mandatory representation 32566 * ID in the parser. This allows for continuous playout across periods with the same representation IDs 32567 * (continuous periods as defined in DASH-IF 3.2.12). This is assumed in the mpd-parser as well. If we want to support 32568 * periods without continuous playback this function may need modification as well as the parser. 32569 */ 32570 32571const dashGroupId = (type, group, label, playlist) => { 32572 // If the manifest somehow does not have an ID (non-dash compliant), use the label. 32573 const playlistId = playlist.attributes.NAME || label; 32574 return `placeholder-uri-${type}-${group}-${playlistId}`; 32575}; 32576/** 32577 * Parses the main XML string and updates playlist URI references. 32578 * 32579 * @param {Object} config 32580 * Object of arguments 32581 * @param {string} config.mainXml 32582 * The mpd XML 32583 * @param {string} config.srcUrl 32584 * The mpd URL 32585 * @param {Date} config.clientOffset 32586 * A time difference between server and client 32587 * @param {Object} config.sidxMapping 32588 * SIDX mappings for moof/mdat URIs and byte ranges 32589 * @return {Object} 32590 * The parsed mpd manifest object 32591 */ 32592 32593const parseMainXml = ({ mainXml, srcUrl, clientOffset, sidxMapping, previousManifest }) => { 32594 const manifest = mpdParser.parse(mainXml, { 32595 manifestUri: srcUrl, 32596 clientOffset, 32597 sidxMapping, 32598 previousManifest, 32599 }); 32600 addPropertiesToMain(manifest, srcUrl, dashGroupId); 32601 return manifest; 32602}; 32603/** 32604 * Removes any mediaGroup labels that no longer exist in the newMain 32605 * 32606 * @param {Object} update 32607 * The previous mpd object being updated 32608 * @param {Object} newMain 32609 * The new mpd object 32610 */ 32611 32612const removeOldMediaGroupLabels = (update, newMain) => { 32613 forEachMediaGroup(update, (properties, type, group, label) => { 32614 if (!newMain.mediaGroups[type][group] || !(label in newMain.mediaGroups[type][group])) { 32615 delete update.mediaGroups[type][group][label]; 32616 } 32617 }); 32618}; 32619/** 32620 * Returns a new main manifest that is the result of merging an updated main manifest 32621 * into the original version. 32622 * 32623 * @param {Object} oldMain 32624 * The old parsed mpd object 32625 * @param {Object} newMain 32626 * The updated parsed mpd object 32627 * @return {Object} 32628 * A new object representing the original main manifest with the updated media 32629 * playlists merged in 32630 */ 32631 32632const updateMain = (oldMain, newMain, sidxMapping) => { 32633 let noChanges = true; 32634 let update = merge(oldMain, { 32635 // These are top level properties that can be updated 32636 duration: newMain.duration, 32637 minimumUpdatePeriod: newMain.minimumUpdatePeriod, 32638 timelineStarts: newMain.timelineStarts, 32639 }); // First update the playlists in playlist list 32640 32641 for (let i = 0; i < newMain.playlists.length; i++) { 32642 const playlist = newMain.playlists[i]; 32643 if (playlist.sidx) { 32644 const sidxKey = mpdParser.generateSidxKey(playlist.sidx); // add sidx segments to the playlist if we have all the sidx info already 32645 32646 if (sidxMapping && sidxMapping[sidxKey] && sidxMapping[sidxKey].sidx) { 32647 mpdParser.addSidxSegmentsToPlaylist( 32648 playlist, 32649 sidxMapping[sidxKey].sidx, 32650 playlist.sidx.resolvedUri 32651 ); 32652 } 32653 } 32654 const playlistUpdate = updateMain$1(update, playlist, dashPlaylistUnchanged); 32655 if (playlistUpdate) { 32656 update = playlistUpdate; 32657 noChanges = false; 32658 } 32659 } // Then update media group playlists 32660 32661 forEachMediaGroup(newMain, (properties, type, group, label) => { 32662 if (properties.playlists && properties.playlists.length) { 32663 const id = properties.playlists[0].id; 32664 const playlistUpdate = updateMain$1(update, properties.playlists[0], dashPlaylistUnchanged); 32665 if (playlistUpdate) { 32666 update = playlistUpdate; // add new mediaGroup label if it doesn't exist and assign the new mediaGroup. 32667 32668 if (!(label in update.mediaGroups[type][group])) { 32669 update.mediaGroups[type][group][label] = properties; 32670 } // update the playlist reference within media groups 32671 32672 update.mediaGroups[type][group][label].playlists[0] = update.playlists[id]; 32673 noChanges = false; 32674 } 32675 } 32676 }); // remove mediaGroup labels and references that no longer exist in the newMain 32677 32678 removeOldMediaGroupLabels(update, newMain); 32679 if (newMain.minimumUpdatePeriod !== oldMain.minimumUpdatePeriod) { 32680 noChanges = false; 32681 } 32682 if (noChanges) { 32683 return null; 32684 } 32685 return update; 32686}; // SIDX should be equivalent if the URI and byteranges of the SIDX match. 32687// If the SIDXs have maps, the two maps should match, 32688// both `a` and `b` missing SIDXs is considered matching. 32689// If `a` or `b` but not both have a map, they aren't matching. 32690 32691const equivalentSidx = (a, b) => { 32692 const neitherMap = Boolean(!a.map && !b.map); 32693 const equivalentMap = 32694 neitherMap || 32695 Boolean( 32696 a.map && 32697 b.map && 32698 a.map.byterange.offset === b.map.byterange.offset && 32699 a.map.byterange.length === b.map.byterange.length 32700 ); 32701 return ( 32702 equivalentMap && 32703 a.uri === b.uri && 32704 a.byterange.offset === b.byterange.offset && 32705 a.byterange.length === b.byterange.length 32706 ); 32707}; // exported for testing 32708 32709const compareSidxEntry = (playlists, oldSidxMapping) => { 32710 const newSidxMapping = {}; 32711 for (const id in playlists) { 32712 const playlist = playlists[id]; 32713 const currentSidxInfo = playlist.sidx; 32714 if (currentSidxInfo) { 32715 const key = mpdParser.generateSidxKey(currentSidxInfo); 32716 if (!oldSidxMapping[key]) { 32717 break; 32718 } 32719 const savedSidxInfo = oldSidxMapping[key].sidxInfo; 32720 if (equivalentSidx(savedSidxInfo, currentSidxInfo)) { 32721 newSidxMapping[key] = oldSidxMapping[key]; 32722 } 32723 } 32724 } 32725 return newSidxMapping; 32726}; 32727/** 32728 * A function that filters out changed items as they need to be requested separately. 32729 * 32730 * The method is exported for testing 32731 * 32732 * @param {Object} main the parsed mpd XML returned via mpd-parser 32733 * @param {Object} oldSidxMapping the SIDX to compare against 32734 */ 32735 32736const filterChangedSidxMappings = (main, oldSidxMapping) => { 32737 const videoSidx = compareSidxEntry(main.playlists, oldSidxMapping); 32738 let mediaGroupSidx = videoSidx; 32739 forEachMediaGroup(main, (properties, mediaType, groupKey, labelKey) => { 32740 if (properties.playlists && properties.playlists.length) { 32741 const playlists = properties.playlists; 32742 mediaGroupSidx = merge(mediaGroupSidx, compareSidxEntry(playlists, oldSidxMapping)); 32743 } 32744 }); 32745 return mediaGroupSidx; 32746}; 32747class DashPlaylistLoader extends EventTarget { 32748 // DashPlaylistLoader must accept either a src url or a playlist because subsequent 32749 // playlist loader setups from media groups will expect to be able to pass a playlist 32750 // (since there aren't external URLs to media playlists with DASH) 32751 constructor(srcUrlOrPlaylist, vhs, options = {}, mainPlaylistLoader) { 32752 super(); 32753 this.mainPlaylistLoader_ = mainPlaylistLoader || this; 32754 if (!mainPlaylistLoader) { 32755 this.isMain_ = true; 32756 } 32757 const { withCredentials = false } = options; 32758 this.vhs_ = vhs; 32759 this.withCredentials = withCredentials; 32760 this.addMetadataToTextTrack = options.addMetadataToTextTrack; 32761 if (!srcUrlOrPlaylist) { 32762 throw new Error('A non-empty playlist URL or object is required'); 32763 } // event naming? 32764 32765 this.on('minimumUpdatePeriod', () => { 32766 this.refreshXml_(); 32767 }); // live playlist staleness timeout 32768 32769 this.on('mediaupdatetimeout', () => { 32770 this.refreshMedia_(this.media().id); 32771 }); 32772 this.state = 'HAVE_NOTHING'; 32773 this.loadedPlaylists_ = {}; 32774 this.logger_ = logger('DashPlaylistLoader'); // initialize the loader state 32775 // The mainPlaylistLoader will be created with a string 32776 32777 if (this.isMain_) { 32778 this.mainPlaylistLoader_.srcUrl = srcUrlOrPlaylist; // TODO: reset sidxMapping between period changes 32779 // once multi-period is refactored 32780 32781 this.mainPlaylistLoader_.sidxMapping_ = {}; 32782 } else { 32783 this.childPlaylist_ = srcUrlOrPlaylist; 32784 } 32785 } 32786 requestErrored_(err, request, startingState) { 32787 // disposed 32788 if (!this.request) { 32789 return true; 32790 } // pending request is cleared 32791 32792 this.request = null; 32793 if (err) { 32794 // use the provided error object or create one 32795 // based on the request/response 32796 this.error = 32797 typeof err === 'object' && !(err instanceof Error) 32798 ? err 32799 : { 32800 status: request.status, 32801 message: 'DASH request error at URL: ' + request.uri, 32802 response: request.response, 32803 // MEDIA_ERR_NETWORK 32804 code: 2, 32805 metadata: err.metadata, 32806 }; 32807 if (startingState) { 32808 this.state = startingState; 32809 } 32810 this.trigger('error'); 32811 return true; 32812 } 32813 } 32814 /** 32815 * Verify that the container of the sidx segment can be parsed 32816 * and if it can, get and parse that segment. 32817 */ 32818 32819 addSidxSegments_(playlist, startingState, cb) { 32820 const sidxKey = playlist.sidx && mpdParser.generateSidxKey(playlist.sidx); // playlist lacks sidx or sidx segments were added to this playlist already. 32821 32822 if (!playlist.sidx || !sidxKey || this.mainPlaylistLoader_.sidxMapping_[sidxKey]) { 32823 // keep this function async 32824 this.mediaRequest_ = window__default['default'].setTimeout(() => cb(false), 0); 32825 return; 32826 } // resolve the segment URL relative to the playlist 32827 32828 const uri = resolveManifestRedirect(playlist.sidx.resolvedUri); 32829 const fin = (err, request) => { 32830 if (this.requestErrored_(err, request, startingState)) { 32831 return; 32832 } 32833 const sidxMapping = this.mainPlaylistLoader_.sidxMapping_; 32834 const { requestType } = request; 32835 let sidx; 32836 try { 32837 sidx = parseSidx__default['default'](byteHelpers.toUint8(request.response).subarray(8)); 32838 } catch (e) { 32839 e.metadata = getStreamingNetworkErrorMetadata({ 32840 requestType, 32841 request, 32842 parseFailure: true, 32843 }); // sidx parsing failed. 32844 32845 this.requestErrored_(e, request, startingState); 32846 return; 32847 } 32848 sidxMapping[sidxKey] = { 32849 sidxInfo: playlist.sidx, 32850 sidx, 32851 }; 32852 mpdParser.addSidxSegmentsToPlaylist(playlist, sidx, playlist.sidx.resolvedUri); 32853 return cb(true); 32854 }; 32855 const REQUEST_TYPE = 'dash-sidx'; 32856 this.request = containerRequest( 32857 uri, 32858 this.vhs_.xhr, 32859 (err, request, container, bytes) => { 32860 if (err) { 32861 return fin(err, request); 32862 } 32863 if (!container || container !== 'mp4') { 32864 const sidxContainer = container || 'unknown'; 32865 return fin( 32866 { 32867 status: request.status, 32868 message: `Unsupported ${sidxContainer} container type for sidx segment at URL: ${uri}`, 32869 // response is just bytes in this case 32870 // but we really don't want to return that. 32871 response: '', 32872 playlist, 32873 internal: true, 32874 playlistExclusionDuration: Infinity, 32875 // MEDIA_ERR_NETWORK 32876 code: 2, 32877 }, 32878 request 32879 ); 32880 } // if we already downloaded the sidx bytes in the container request, use them 32881 32882 const { offset, length } = playlist.sidx.byterange; 32883 if (bytes.length >= length + offset) { 32884 return fin(err, { 32885 response: bytes.subarray(offset, offset + length), 32886 status: request.status, 32887 uri: request.uri, 32888 }); 32889 } // otherwise request sidx bytes 32890 32891 this.request = this.vhs_.xhr( 32892 { 32893 uri, 32894 responseType: 'arraybuffer', 32895 requestType: 'dash-sidx', 32896 headers: segmentXhrHeaders({ 32897 byterange: playlist.sidx.byterange, 32898 }), 32899 }, 32900 fin 32901 ); 32902 }, 32903 REQUEST_TYPE 32904 ); 32905 } 32906 dispose() { 32907 this.trigger('dispose'); 32908 this.stopRequest(); 32909 this.loadedPlaylists_ = {}; 32910 window__default['default'].clearTimeout(this.minimumUpdatePeriodTimeout_); 32911 window__default['default'].clearTimeout(this.mediaRequest_); 32912 window__default['default'].clearTimeout(this.mediaUpdateTimeout); 32913 this.mediaUpdateTimeout = null; 32914 this.mediaRequest_ = null; 32915 this.minimumUpdatePeriodTimeout_ = null; 32916 if (this.mainPlaylistLoader_.createMupOnMedia_) { 32917 this.off('loadedmetadata', this.mainPlaylistLoader_.createMupOnMedia_); 32918 this.mainPlaylistLoader_.createMupOnMedia_ = null; 32919 } 32920 this.off(); 32921 } 32922 hasPendingRequest() { 32923 return this.request || this.mediaRequest_; 32924 } 32925 stopRequest() { 32926 if (this.request) { 32927 const oldRequest = this.request; 32928 this.request = null; 32929 oldRequest.onreadystatechange = null; 32930 oldRequest.abort(); 32931 } 32932 } 32933 media(playlist) { 32934 // getter 32935 if (!playlist) { 32936 return this.media_; 32937 } // setter 32938 32939 if (this.state === 'HAVE_NOTHING') { 32940 throw new Error('Cannot switch media playlist from ' + this.state); 32941 } 32942 const startingState = this.state; // find the playlist object if the target playlist has been specified by URI 32943 32944 if (typeof playlist === 'string') { 32945 if (!this.mainPlaylistLoader_.main.playlists[playlist]) { 32946 throw new Error('Unknown playlist URI: ' + playlist); 32947 } 32948 playlist = this.mainPlaylistLoader_.main.playlists[playlist]; 32949 } 32950 const mediaChange = !this.media_ || playlist.id !== this.media_.id; // switch to previously loaded playlists immediately 32951 32952 if ( 32953 mediaChange && 32954 this.loadedPlaylists_[playlist.id] && 32955 this.loadedPlaylists_[playlist.id].endList 32956 ) { 32957 this.state = 'HAVE_METADATA'; 32958 this.media_ = playlist; // trigger media change if the active media has been updated 32959 32960 if (mediaChange) { 32961 this.trigger('mediachanging'); 32962 this.trigger('mediachange'); 32963 } 32964 return; 32965 } // switching to the active playlist is a no-op 32966 32967 if (!mediaChange) { 32968 return; 32969 } // switching from an already loaded playlist 32970 32971 if (this.media_) { 32972 this.trigger('mediachanging'); 32973 } 32974 this.addSidxSegments_(playlist, startingState, (sidxChanged) => { 32975 // everything is ready just continue to haveMetadata 32976 this.haveMetadata({ 32977 startingState, 32978 playlist, 32979 }); 32980 }); 32981 } 32982 haveMetadata({ startingState, playlist }) { 32983 this.state = 'HAVE_METADATA'; 32984 this.loadedPlaylists_[playlist.id] = playlist; 32985 this.mediaRequest_ = null; // This will trigger loadedplaylist 32986 32987 this.refreshMedia_(playlist.id); // fire loadedmetadata the first time a media playlist is loaded 32988 // to resolve setup of media groups 32989 32990 if (startingState === 'HAVE_MAIN_MANIFEST') { 32991 this.trigger('loadedmetadata'); 32992 } else { 32993 // trigger media change if the active media has been updated 32994 this.trigger('mediachange'); 32995 } 32996 } 32997 pause() { 32998 if (this.mainPlaylistLoader_.createMupOnMedia_) { 32999 this.off('loadedmetadata', this.mainPlaylistLoader_.createMupOnMedia_); 33000 this.mainPlaylistLoader_.createMupOnMedia_ = null; 33001 } 33002 this.stopRequest(); 33003 window__default['default'].clearTimeout(this.mediaUpdateTimeout); 33004 this.mediaUpdateTimeout = null; 33005 if (this.isMain_) { 33006 window__default['default'].clearTimeout(this.mainPlaylistLoader_.minimumUpdatePeriodTimeout_); 33007 this.mainPlaylistLoader_.minimumUpdatePeriodTimeout_ = null; 33008 } 33009 if (this.state === 'HAVE_NOTHING') { 33010 // If we pause the loader before any data has been retrieved, its as if we never 33011 // started, so reset to an unstarted state. 33012 this.started = false; 33013 } 33014 } 33015 load(isFinalRendition) { 33016 window__default['default'].clearTimeout(this.mediaUpdateTimeout); 33017 this.mediaUpdateTimeout = null; 33018 const media = this.media(); 33019 if (isFinalRendition) { 33020 const delay = media ? (media.targetDuration / 2) * 1000 : 5 * 1000; 33021 this.mediaUpdateTimeout = window__default['default'].setTimeout(() => this.load(), delay); 33022 return; 33023 } // because the playlists are internal to the manifest, load should either load the 33024 // main manifest, or do nothing but trigger an event 33025 33026 if (!this.started) { 33027 this.start(); 33028 return; 33029 } 33030 if (media && !media.endList) { 33031 // Check to see if this is the main loader and the MUP was cleared (this happens 33032 // when the loader was paused). `media` should be set at this point since one is always 33033 // set during `start()`. 33034 if (this.isMain_ && !this.minimumUpdatePeriodTimeout_) { 33035 // Trigger minimumUpdatePeriod to refresh the main manifest 33036 this.trigger('minimumUpdatePeriod'); // Since there was no prior minimumUpdatePeriodTimeout it should be recreated 33037 33038 this.updateMinimumUpdatePeriodTimeout_(); 33039 } 33040 this.trigger('mediaupdatetimeout'); 33041 } else { 33042 this.trigger('loadedplaylist'); 33043 } 33044 } 33045 start() { 33046 this.started = true; // We don't need to request the main manifest again 33047 // Call this asynchronously to match the xhr request behavior below 33048 33049 if (!this.isMain_) { 33050 this.mediaRequest_ = window__default['default'].setTimeout(() => this.haveMain_(), 0); 33051 return; 33052 } 33053 this.requestMain_((req, mainChanged) => { 33054 this.haveMain_(); 33055 if (!this.hasPendingRequest() && !this.media_) { 33056 this.media(this.mainPlaylistLoader_.main.playlists[0]); 33057 } 33058 }); 33059 } 33060 requestMain_(cb) { 33061 const metadata = { 33062 manifestInfo: { 33063 uri: this.mainPlaylistLoader_.srcUrl, 33064 }, 33065 }; 33066 this.trigger({ 33067 type: 'manifestrequeststart', 33068 metadata, 33069 }); 33070 this.request = this.vhs_.xhr( 33071 { 33072 uri: this.mainPlaylistLoader_.srcUrl, 33073 withCredentials: this.withCredentials, 33074 requestType: 'dash-manifest', 33075 }, 33076 (error, req) => { 33077 if (error) { 33078 const { requestType } = req; 33079 error.metadata = getStreamingNetworkErrorMetadata({ 33080 requestType, 33081 request: req, 33082 error, 33083 }); 33084 } 33085 if (this.requestErrored_(error, req)) { 33086 if (this.state === 'HAVE_NOTHING') { 33087 this.started = false; 33088 } 33089 return; 33090 } 33091 this.trigger({ 33092 type: 'manifestrequestcomplete', 33093 metadata, 33094 }); 33095 const mainChanged = req.responseText !== this.mainPlaylistLoader_.mainXml_; 33096 this.mainPlaylistLoader_.mainXml_ = req.responseText; 33097 if (req.responseHeaders && req.responseHeaders.date) { 33098 this.mainLoaded_ = Date.parse(req.responseHeaders.date); 33099 } else { 33100 this.mainLoaded_ = Date.now(); 33101 } 33102 this.mainPlaylistLoader_.srcUrl = resolveManifestRedirect( 33103 this.mainPlaylistLoader_.srcUrl, 33104 req 33105 ); 33106 if (mainChanged) { 33107 this.handleMain_();
33108 this.syncClientServerClock_(() => { 33109 return cb(req, mainChanged); 33110 }); 33111 return; 33112 } 33113 return cb(req, mainChanged); 33114 } 33115 ); 33116 } 33117 /** 33118 * Parses the main xml for UTCTiming node to sync the client clock to the server 33119 * clock. If the UTCTiming node requires a HEAD or GET request, that request is made. 33120 * 33121 * @param {Function} done 33122 * Function to call when clock sync has completed 33123 */ 33124 33125 syncClientServerClock_(done) { 33126 const utcTiming = mpdParser.parseUTCTiming(this.mainPlaylistLoader_.mainXml_); // No UTCTiming element found in the mpd. Use Date header from mpd request as the 33127 // server clock 33128 33129 if (utcTiming === null) { 33130 this.mainPlaylistLoader_.clientOffset_ = this.mainLoaded_ - Date.now(); 33131 return done(); 33132 } 33133 if (utcTiming.method === 'DIRECT') { 33134 this.mainPlaylistLoader_.clientOffset_ = utcTiming.value - Date.now(); 33135 return done(); 33136 } 33137 this.request = this.vhs_.xhr( 33138 { 33139 uri: resolveUrl(this.mainPlaylistLoader_.srcUrl, utcTiming.value), 33140 method: utcTiming.method, 33141 withCredentials: this.withCredentials, 33142 requestType: 'dash-clock-sync', 33143 }, 33144 (error, req) => { 33145 // disposed 33146 if (!this.request) { 33147 return; 33148 } 33149 if (error) { 33150 const { requestType } = req; 33151 this.error.metadata = getStreamingNetworkErrorMetadata({ 33152 requestType, 33153 request: req, 33154 error, 33155 }); // sync request failed, fall back to using date header from mpd 33156 // TODO: log warning 33157 33158 this.mainPlaylistLoader_.clientOffset_ = this.mainLoaded_ - Date.now(); 33159 return done(); 33160 } 33161 let serverTime; 33162 if (utcTiming.method === 'HEAD') { 33163 if (!req.responseHeaders || !req.responseHeaders.date) { 33164 // expected date header not preset, fall back to using date header from mpd 33165 // TODO: log warning 33166 serverTime = this.mainLoaded_; 33167 } else { 33168 serverTime = Date.parse(req.responseHeaders.date); 33169 } 33170 } else { 33171 serverTime = Date.parse(req.responseText); 33172 } 33173 this.mainPlaylistLoader_.clientOffset_ = serverTime - Date.now(); 33174 done(); 33175 } 33176 ); 33177 } 33178 haveMain_() { 33179 this.state = 'HAVE_MAIN_MANIFEST'; 33180 if (this.isMain_) { 33181 // We have the main playlist at this point, so 33182 // trigger this to allow PlaylistController 33183 // to make an initial playlist selection 33184 this.trigger('loadedplaylist'); 33185 } else if (!this.media_) { 33186 // no media playlist was specifically selected so select 33187 // the one the child playlist loader was created with 33188 this.media(this.childPlaylist_); 33189 } 33190 } 33191 handleMain_() { 33192 // clear media request 33193 this.mediaRequest_ = null; 33194 const oldMain = this.mainPlaylistLoader_.main; 33195 const metadata = { 33196 manifestInfo: { 33197 uri: this.mainPlaylistLoader_.srcUrl, 33198 }, 33199 }; 33200 this.trigger({ 33201 type: 'manifestparsestart', 33202 metadata, 33203 }); 33204 let newMain; 33205 try { 33206 newMain = parseMainXml({ 33207 mainXml: this.mainPlaylistLoader_.mainXml_, 33208 srcUrl: this.mainPlaylistLoader_.srcUrl, 33209 clientOffset: this.mainPlaylistLoader_.clientOffset_, 33210 sidxMapping: this.mainPlaylistLoader_.sidxMapping_, 33211 previousManifest: oldMain, 33212 }); 33213 } catch (error) { 33214 this.error = error; 33215 this.error.metadata = { 33216 errorType: videojs.Error.StreamingDashManifestParserError, 33217 error, 33218 }; 33219 this.trigger('error'); 33220 } // if we have an old main to compare the new main against 33221 33222 if (oldMain) { 33223 newMain = updateMain(oldMain, newMain, this.mainPlaylistLoader_.sidxMapping_); 33224 } // only update main if we have a new main 33225 33226 this.mainPlaylistLoader_.main = newMain ? newMain : oldMain; 33227 const location = 33228 this.mainPlaylistLoader_.main.locations && this.mainPlaylistLoader_.main.locations[0]; 33229 if (location && location !== this.mainPlaylistLoader_.srcUrl) { 33230 this.mainPlaylistLoader_.srcUrl = location; 33231 } 33232 if (!oldMain || (newMain && newMain.minimumUpdatePeriod !== oldMain.minimumUpdatePeriod)) { 33233 this.updateMinimumUpdatePeriodTimeout_(); 33234 } 33235 this.addEventStreamToMetadataTrack_(newMain); 33236 if (newMain) { 33237 const { duration, endList } = newMain; 33238 const renditions = [];
vendor: 5,588 bytes, lines 33239-33405
33239 newMain.playlists.forEach((playlist) => { 33240 renditions.push({ 33241 id: playlist.id, 33242 bandwidth: playlist.attributes.BANDWIDTH, 33243 resolution: playlist.attributes.RESOLUTION, 33244 codecs: playlist.attributes.CODECS, 33245 }); 33246 }); 33247 const parsedManifest = { 33248 duration, 33249 isLive: !endList, 33250 renditions, 33251 }; 33252 metadata.parsedManifest = parsedManifest; 33253 this.trigger({ 33254 type: 'manifestparsecomplete', 33255 metadata, 33256 }); 33257 } 33258 return Boolean(newMain); 33259 } 33260 updateMinimumUpdatePeriodTimeout_() { 33261 const mpl = this.mainPlaylistLoader_; // cancel any pending creation of mup on media 33262 // a new one will be added if needed. 33263 33264 if (mpl.createMupOnMedia_) { 33265 mpl.off('loadedmetadata', mpl.createMupOnMedia_); 33266 mpl.createMupOnMedia_ = null; 33267 } // clear any pending timeouts 33268 33269 if (mpl.minimumUpdatePeriodTimeout_) { 33270 window__default['default'].clearTimeout(mpl.minimumUpdatePeriodTimeout_); 33271 mpl.minimumUpdatePeriodTimeout_ = null; 33272 } 33273 let mup = mpl.main && mpl.main.minimumUpdatePeriod; // If the minimumUpdatePeriod has a value of 0, that indicates that the current 33274 // MPD has no future validity, so a new one will need to be acquired when new 33275 // media segments are to be made available. Thus, we use the target duration 33276 // in this case 33277 33278 if (mup === 0) { 33279 if (mpl.media()) { 33280 mup = mpl.media().targetDuration * 1000; 33281 } else { 33282 mpl.createMupOnMedia_ = mpl.updateMinimumUpdatePeriodTimeout_; 33283 mpl.one('loadedmetadata', mpl.createMupOnMedia_); 33284 } 33285 } // if minimumUpdatePeriod is invalid or <= zero, which 33286 // can happen when a live video becomes VOD. skip timeout 33287 // creation. 33288 33289 if (typeof mup !== 'number' || mup <= 0) { 33290 if (mup < 0) { 33291 this.logger_(`found invalid minimumUpdatePeriod of ${mup}, not setting a timeout`); 33292 } 33293 return; 33294 } 33295 this.createMUPTimeout_(mup); 33296 } 33297 createMUPTimeout_(mup) { 33298 const mpl = this.mainPlaylistLoader_; 33299 mpl.minimumUpdatePeriodTimeout_ = window__default['default'].setTimeout(() => { 33300 mpl.minimumUpdatePeriodTimeout_ = null; 33301 mpl.trigger('minimumUpdatePeriod'); 33302 mpl.createMUPTimeout_(mup); 33303 }, mup); 33304 } 33305 /** 33306 * Sends request to refresh the main xml and updates the parsed main manifest 33307 */ 33308 33309 refreshXml_() { 33310 this.requestMain_((req, mainChanged) => { 33311 if (!mainChanged) { 33312 return; 33313 } 33314 if (this.media_) { 33315 this.media_ = this.mainPlaylistLoader_.main.playlists[this.media_.id]; 33316 } // This will filter out updated sidx info from the mapping 33317 33318 this.mainPlaylistLoader_.sidxMapping_ = filterChangedSidxMappings( 33319 this.mainPlaylistLoader_.main, 33320 this.mainPlaylistLoader_.sidxMapping_ 33321 ); 33322 this.addSidxSegments_(this.media(), this.state, (sidxChanged) => { 33323 // TODO: do we need to reload the current playlist? 33324 this.refreshMedia_(this.media().id); 33325 }); 33326 }); 33327 } 33328 /** 33329 * Refreshes the media playlist by re-parsing the main xml and updating playlist 33330 * references. If this is an alternate loader, the updated parsed manifest is retrieved 33331 * from the main loader. 33332 */ 33333 33334 refreshMedia_(mediaID) { 33335 if (!mediaID) { 33336 throw new Error('refreshMedia_ must take a media id'); 33337 } // for main we have to reparse the main xml 33338 // to re-create segments based on current timing values 33339 // which may change media. We only skip updating the main manifest 33340 // if this is the first time this.media_ is being set. 33341 // as main was just parsed in that case. 33342 33343 if (this.media_ && this.isMain_) { 33344 this.handleMain_(); 33345 } 33346 const playlists = this.mainPlaylistLoader_.main.playlists; 33347 const mediaChanged = !this.media_ || this.media_ !== playlists[mediaID]; 33348 if (mediaChanged) { 33349 this.media_ = playlists[mediaID]; 33350 } else { 33351 this.trigger('playlistunchanged'); 33352 } 33353 if (!this.mediaUpdateTimeout) { 33354 const createMediaUpdateTimeout = () => { 33355 if (this.media().endList) { 33356 return; 33357 } 33358 this.mediaUpdateTimeout = window__default['default'].setTimeout( 33359 () => { 33360 this.trigger('mediaupdatetimeout'); 33361 createMediaUpdateTimeout(); 33362 }, 33363 refreshDelay(this.media(), Boolean(mediaChanged)) 33364 ); 33365 }; 33366 createMediaUpdateTimeout(); 33367 } 33368 this.trigger('loadedplaylist'); 33369 } 33370 /** 33371 * Takes eventstream data from a parsed DASH manifest and adds it to the metadata text track. 33372 * 33373 * @param {manifest} newMain the newly parsed manifest 33374 */ 33375 33376 addEventStreamToMetadataTrack_(newMain) { 33377 // Only add new event stream metadata if we have a new manifest. 33378 if (newMain && this.mainPlaylistLoader_.main.eventStream) { 33379 // convert EventStream to ID3-like data. 33380 const metadataArray = this.mainPlaylistLoader_.main.eventStream.map((eventStreamNode) => { 33381 return { 33382 cueTime: eventStreamNode.start, 33383 frames: [ 33384 { 33385 data: eventStreamNode.messageData, 33386 }, 33387 ], 33388 }; 33389 }); 33390 this.addMetadataToTextTrack( 33391 'EventStream', 33392 metadataArray, 33393 this.mainPlaylistLoader_.main.duration 33394 ); 33395 } 33396 } 33397 /** 33398 * Returns the key ID set from a playlist 33399 * 33400 * @param {playlist} playlist to fetch the key ID set from. 33401 * @return a Set of 32 digit hex strings that represent the unique keyIds for that playlist. 33402 */ 33403 33404 getKeyIdSet(playlist) { 33405 if (playlist.contentProtection) {
vendor: 10,169 bytes, lines 33406-33754
33406 const keyIds = new Set(); 33407 for (const keysystem in playlist.contentProtection) { 33408 const defaultKID = playlist.contentProtection[keysystem].attributes['cenc:default_KID']; 33409 if (defaultKID) { 33410 // DASH keyIds are separated by dashes. 33411 keyIds.add(defaultKID.replace(/-/g, '').toLowerCase()); 33412 } 33413 } 33414 return keyIds; 33415 } 33416 } 33417} 33418var Config = { 33419 GOAL_BUFFER_LENGTH: 30, 33420 MAX_GOAL_BUFFER_LENGTH: 60, 33421 BACK_BUFFER_LENGTH: 30, 33422 GOAL_BUFFER_LENGTH_RATE: 1, 33423 // 0.5 MB/s 33424 INITIAL_BANDWIDTH: 4194304, 33425 // A fudge factor to apply to advertised playlist bitrates to account for 33426 // temporary flucations in client bandwidth 33427 BANDWIDTH_VARIANCE: 1.2, 33428 // How much of the buffer must be filled before we consider upswitching 33429 BUFFER_LOW_WATER_LINE: 0, 33430 MAX_BUFFER_LOW_WATER_LINE: 30, 33431 // TODO: Remove this when experimentalBufferBasedABR is removed 33432 EXPERIMENTAL_MAX_BUFFER_LOW_WATER_LINE: 16, 33433 BUFFER_LOW_WATER_LINE_RATE: 1, 33434 // If the buffer is greater than the high water line, we won't switch down 33435 BUFFER_HIGH_WATER_LINE: 30, 33436}; 33437const stringToArrayBuffer = (string) => { 33438 const view = new Uint8Array(new ArrayBuffer(string.length)); 33439 for (let i = 0; i < string.length; i++) { 33440 view[i] = string.charCodeAt(i); 33441 } 33442 return view.buffer; 33443}; 33444 33445/* global Blob, BlobBuilder, Worker */ 33446// unify worker interface 33447const browserWorkerPolyFill = function (workerObj) { 33448 // node only supports on/off 33449 workerObj.on = workerObj.addEventListener; 33450 workerObj.off = workerObj.removeEventListener; 33451 return workerObj; 33452}; 33453const createObjectURL = function (str) { 33454 try { 33455 return URL.createObjectURL( 33456 new Blob([str], { 33457 type: 'application/javascript', 33458 }) 33459 ); 33460 } catch (e) { 33461 const blob = new BlobBuilder(); 33462 blob.append(str); 33463 return URL.createObjectURL(blob.getBlob()); 33464 } 33465}; 33466const factory = function (code) { 33467 return function () { 33468 const objectUrl = createObjectURL(code); 33469 const worker = browserWorkerPolyFill(new Worker(objectUrl)); 33470 worker.objURL = objectUrl; 33471 const terminate = worker.terminate; 33472 worker.on = worker.addEventListener; 33473 worker.off = worker.removeEventListener; 33474 worker.terminate = function () { 33475 URL.revokeObjectURL(objectUrl); 33476 return terminate.call(this); 33477 }; 33478 return worker; 33479 }; 33480}; 33481const transform = function (code) { 33482 return ( 33483 `var browserWorkerPolyFill = ${browserWorkerPolyFill.toString()};\n` + 33484 'browserWorkerPolyFill(self);\n' + 33485 code 33486 ); 33487}; 33488const getWorkerString = function (fn) { 33489 return fn 33490 .toString() 33491 .replace(/^function.+?{/, '') 33492 .slice(0, -1); 33493}; 33494 33495/* rollup-plugin-worker-factory start for worker!/home/runner/work/http-streaming/http-streaming/src/transmuxer-worker.js */ 33496const workerCode$1 = transform( 33497 getWorkerString(function () { 33498 var commonjsGlobal = 33499 typeof globalThis !== 'undefined' 33500 ? globalThis 33501 : typeof window !== 'undefined' 33502 ? window 33503 : typeof global !== 'undefined' 33504 ? global 33505 : typeof self !== 'undefined' 33506 ? self 33507 : {}; 33508 /** 33509 * mux.js 33510 * 33511 * Copyright (c) Brightcove 33512 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 33513 * 33514 * A lightweight readable stream implemention that handles event dispatching. 33515 * Objects that inherit from streams should call init in their constructors. 33516 */ 33517 33518 var Stream$8 = function () { 33519 this.init = function () { 33520 var listeners = {}; 33521 /** 33522 * Add a listener for a specified event type. 33523 * @param type {string} the event name 33524 * @param listener {function} the callback to be invoked when an event of 33525 * the specified type occurs 33526 */ 33527 33528 this.on = function (type, listener) { 33529 if (!listeners[type]) { 33530 listeners[type] = []; 33531 } 33532 listeners[type] = listeners[type].concat(listener); 33533 }; 33534 /** 33535 * Remove a listener for a specified event type. 33536 * @param type {string} the event name 33537 * @param listener {function} a function previously registered for this 33538 * type of event through `on` 33539 */ 33540 33541 this.off = function (type, listener) { 33542 var index; 33543 if (!listeners[type]) { 33544 return false; 33545 } 33546 index = listeners[type].indexOf(listener); 33547 listeners[type] = listeners[type].slice(); 33548 listeners[type].splice(index, 1); 33549 return index > -1; 33550 }; 33551 /** 33552 * Trigger an event of the specified type on this stream. Any additional 33553 * arguments to this function are passed as parameters to event listeners. 33554 * @param type {string} the event name 33555 */ 33556 33557 this.trigger = function (type) { 33558 var callbacks, i, length, args; 33559 callbacks = listeners[type]; 33560 if (!callbacks) { 33561 return; 33562 } // Slicing the arguments on every invocation of this method 33563 // can add a significant amount of overhead. Avoid the 33564 // intermediate object creation for the common case of a 33565 // single callback argument 33566 33567 if (arguments.length === 2) { 33568 length = callbacks.length; 33569 for (i = 0; i < length; ++i) { 33570 callbacks[i].call(this, arguments[1]); 33571 } 33572 } else { 33573 args = []; 33574 i = arguments.length; 33575 for (i = 1; i < arguments.length; ++i) { 33576 args.push(arguments[i]); 33577 } 33578 length = callbacks.length; 33579 for (i = 0; i < length; ++i) { 33580 callbacks[i].apply(this, args); 33581 } 33582 } 33583 }; 33584 /** 33585 * Destroys the stream and cleans up. 33586 */ 33587 33588 this.dispose = function () { 33589 listeners = {}; 33590 }; 33591 }; 33592 }; 33593 /** 33594 * Forwards all `data` events on this stream to the destination stream. The 33595 * destination stream should provide a method `push` to receive the data 33596 * events as they arrive. 33597 * @param destination {stream} the stream that will receive all `data` events 33598 * @param autoFlush {boolean} if false, we will not call `flush` on the destination 33599 * when the current stream emits a 'done' event 33600 * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options 33601 */ 33602 33603 Stream$8.prototype.pipe = function (destination) { 33604 this.on('data', function (data) { 33605 destination.push(data); 33606 }); 33607 this.on('done', function (flushSource) { 33608 destination.flush(flushSource); 33609 }); 33610 this.on('partialdone', function (flushSource) { 33611 destination.partialFlush(flushSource); 33612 }); 33613 this.on('endedtimeline', function (flushSource) { 33614 destination.endTimeline(flushSource); 33615 }); 33616 this.on('reset', function (flushSource) { 33617 destination.reset(flushSource); 33618 }); 33619 return destination; 33620 }; // Default stream functions that are expected to be overridden to perform 33621 // actual work. These are provided by the prototype as a sort of no-op 33622 // implementation so that we don't have to check for their existence in the 33623 // `pipe` function above. 33624 33625 Stream$8.prototype.push = function (data) { 33626 this.trigger('data', data); 33627 }; 33628 Stream$8.prototype.flush = function (flushSource) { 33629 this.trigger('done', flushSource); 33630 }; 33631 Stream$8.prototype.partialFlush = function (flushSource) { 33632 this.trigger('partialdone', flushSource); 33633 }; 33634 Stream$8.prototype.endTimeline = function (flushSource) { 33635 this.trigger('endedtimeline', flushSource); 33636 }; 33637 Stream$8.prototype.reset = function (flushSource) { 33638 this.trigger('reset', flushSource); 33639 }; 33640 var stream = Stream$8; 33641 var MAX_UINT32$1 = Math.pow(2, 32); 33642 var getUint64$5 = function (uint8) { 33643 var dv = new DataView(uint8.buffer, uint8.byteOffset, uint8.byteLength); 33644 var value; 33645 if (dv.getBigUint64) { 33646 value = dv.getBigUint64(0); 33647 if (value < Number.MAX_SAFE_INTEGER) { 33648 return Number(value); 33649 } 33650 return value; 33651 } 33652 return dv.getUint32(0) * MAX_UINT32$1 + dv.getUint32(4); 33653 }; 33654 var numbers = { 33655 getUint64: getUint64$5, 33656 MAX_UINT32: MAX_UINT32$1, 33657 }; 33658 /** 33659 * mux.js 33660 * 33661 * Copyright (c) Brightcove 33662 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 33663 * 33664 * Functions that generate fragmented MP4s suitable for use with Media 33665 * Source Extensions. 33666 */ 33667 33668 var MAX_UINT32 = numbers.MAX_UINT32; 33669 var box, 33670 dinf, 33671 esds, 33672 ftyp, 33673 mdat, 33674 mfhd, 33675 minf, 33676 moof, 33677 moov, 33678 mvex, 33679 mvhd, 33680 trak, 33681 tkhd, 33682 mdia, 33683 mdhd, 33684 hdlr, 33685 sdtp, 33686 stbl, 33687 stsd, 33688 traf, 33689 trex, 33690 trun$1, 33691 types, 33692 MAJOR_BRAND, 33693 MINOR_VERSION, 33694 AVC1_BRAND, 33695 VIDEO_HDLR, 33696 AUDIO_HDLR, 33697 HDLR_TYPES, 33698 VMHD, 33699 SMHD, 33700 DREF, 33701 STCO, 33702 STSC, 33703 STSZ, 33704 STTS; // pre-calculate constants 33705 33706 (function () { 33707 var i; 33708 types = { 33709 avc1: [], 33710 // codingname 33711 avcC: [], 33712 btrt: [], 33713 dinf: [], 33714 dref: [], 33715 esds: [], 33716 ftyp: [], 33717 hdlr: [], 33718 mdat: [], 33719 mdhd: [], 33720 mdia: [], 33721 mfhd: [], 33722 minf: [], 33723 moof: [], 33724 moov: [], 33725 mp4a: [], 33726 // codingname 33727 mvex: [], 33728 mvhd: [], 33729 pasp: [], 33730 sdtp: [], 33731 smhd: [], 33732 stbl: [], 33733 stco: [], 33734 stsc: [], 33735 stsd: [], 33736 stsz: [], 33737 stts: [], 33738 styp: [], 33739 tfdt: [], 33740 tfhd: [], 33741 traf: [], 33742 trak: [], 33743 trun: [], 33744 trex: [], 33745 tkhd: [], 33746 vmhd: [], 33747 }; // In environments where Uint8Array is undefined (e.g., IE8), skip set up so that we 33748 // don't throw an error 33749 33750 if (typeof Uint8Array === 'undefined') { 33751 return; 33752 } 33753 for (i in types) { 33754 if (types.hasOwnProperty(i)) {
33755 types[i] = [i.charCodeAt(0), i.charCodeAt(1), i.charCodeAt(2), i.charCodeAt(3)]; 33756 } 33757 } 33758 MAJOR_BRAND = new Uint8Array([ 33759 'i'.charCodeAt(0), 33760 's'.charCodeAt(0), 33761 'o'.charCodeAt(0), 33762 'm'.charCodeAt(0), 33763 ]); 33764 AVC1_BRAND = new Uint8Array([ 33765 'a'.charCodeAt(0), 33766 'v'.charCodeAt(0), 33767 'c'.charCodeAt(0), 33768 '1'.charCodeAt(0), 33769 ]); 33770 MINOR_VERSION = new Uint8Array([0, 0, 0, 1]); 33771 VIDEO_HDLR = new Uint8Array([ 33772 0x00, 33773 // version 0 33774 0x00, 33775 0x00, 33776 0x00, 33777 // flags 33778 0x00, 33779 0x00, 33780 0x00, 33781 0x00, 33782 // pre_defined 33783 0x76, 33784 0x69, 33785 0x64, 33786 0x65, 33787 // handler_type: 'vide' 33788 0x00, 33789 0x00, 33790 0x00, 33791 0x00, 33792 // reserved 33793 0x00, 33794 0x00, 33795 0x00, 33796 0x00, 33797 // reserved 33798 0x00, 33799 0x00, 33800 0x00, 33801 0x00, 33802 // reserved 33803 0x56, 33804 0x69, 33805 0x64, 33806 0x65, 33807 0x6f, 33808 0x48, 33809 0x61, 33810 0x6e, 33811 0x64, 33812 0x6c, 33813 0x65, 33814 0x72, 33815 0x00, // name: 'VideoHandler' 33816 ]); 33817 AUDIO_HDLR = new Uint8Array([ 33818 0x00, 33819 // version 0 33820 0x00, 33821 0x00, 33822 0x00, 33823 // flags 33824 0x00, 33825 0x00, 33826 0x00, 33827 0x00, 33828 // pre_defined 33829 0x73, 33830 0x6f, 33831 0x75, 33832 0x6e, 33833 // handler_type: 'soun' 33834 0x00, 33835 0x00, 33836 0x00, 33837 0x00, 33838 // reserved 33839 0x00, 33840 0x00, 33841 0x00, 33842 0x00, 33843 // reserved 33844 0x00, 33845 0x00, 33846 0x00, 33847 0x00, 33848 // reserved 33849 0x53, 33850 0x6f, 33851 0x75, 33852 0x6e, 33853 0x64, 33854 0x48, 33855 0x61, 33856 0x6e, 33857 0x64, 33858 0x6c, 33859 0x65, 33860 0x72, 33861 0x00, // name: 'SoundHandler' 33862 ]); 33863 HDLR_TYPES = { 33864 video: VIDEO_HDLR, 33865 audio: AUDIO_HDLR, 33866 }; 33867 DREF = new Uint8Array([ 33868 0x00, 33869 // version 0 33870 0x00, 33871 0x00, 33872 0x00, 33873 // flags 33874 0x00, 33875 0x00, 33876 0x00, 33877 0x01, 33878 // entry_count 33879 0x00, 33880 0x00, 33881 0x00, 33882 0x0c, 33883 // entry_size 33884 0x75, 33885 0x72, 33886 0x6c, 33887 0x20, 33888 // 'url' type 33889 0x00, 33890 // version 0 33891 0x00, 33892 0x00, 33893 0x01, // entry_flags 33894 ]); 33895 SMHD = new Uint8Array([ 33896 0x00, 33897 // version 33898 0x00, 33899 0x00, 33900 0x00, 33901 // flags 33902 0x00, 33903 0x00, 33904 // balance, 0 means centered 33905 0x00, 33906 0x00, // reserved 33907 ]); 33908 STCO = new Uint8Array([ 33909 0x00, 33910 // version 33911 0x00, 33912 0x00, 33913 0x00, 33914 // flags 33915 0x00, 33916 0x00, 33917 0x00, 33918 0x00, // entry_count 33919 ]); 33920 STSC = STCO; 33921 STSZ = new Uint8Array([ 33922 0x00, 33923 // version 33924 0x00, 33925 0x00, 33926 0x00, 33927 // flags 33928 0x00, 33929 0x00, 33930 0x00, 33931 0x00, 33932 // sample_size 33933 0x00, 33934 0x00, 33935 0x00, 33936 0x00, // sample_count 33937 ]); 33938 STTS = STCO; 33939 VMHD = new Uint8Array([ 33940 0x00, 33941 // version 33942 0x00, 33943 0x00, 33944 0x01, 33945 // flags 33946 0x00, 33947 0x00, 33948 // graphicsmode 33949 0x00, 33950 0x00, 33951 0x00, 33952 0x00, 33953 0x00, 33954 0x00, // opcolor 33955 ]); 33956 })(); 33957 box = function (type) { 33958 var payload = [], 33959 size = 0, 33960 i, 33961 result, 33962 view; 33963 for (i = 1; i < arguments.length; i++) { 33964 payload.push(arguments[i]); 33965 } 33966 i = payload.length; // calculate the total size we need to allocate 33967 33968 while (i--) { 33969 size += payload[i].byteLength; 33970 } 33971 result = new Uint8Array(size + 8); 33972 view = new DataView(result.buffer, result.byteOffset, result.byteLength); 33973 view.setUint32(0, result.byteLength); 33974 result.set(type, 4); // copy the payload into the result 33975 33976 for (i = 0, size = 8; i < payload.length; i++) { 33977 result.set(payload[i], size); 33978 size += payload[i].byteLength; 33979 } 33980 return result; 33981 }; 33982 dinf = function () { 33983 return box(types.dinf, box(types.dref, DREF)); 33984 }; 33985 esds = function (track) { 33986 return box( 33987 types.esds, 33988 new Uint8Array([ 33989 0x00, 33990 // version 33991 0x00, 33992 0x00, 33993 0x00, 33994 // flags 33995 // ES_Descriptor 33996 0x03, 33997 // tag, ES_DescrTag 33998 0x19, 33999 // length 34000 0x00, 34001 0x00, 34002 // ES_ID 34003 0x00, 34004 // streamDependenceFlag, URL_flag, reserved, streamPriority 34005 // DecoderConfigDescriptor 34006 0x04, 34007 // tag, DecoderConfigDescrTag 34008 0x11, 34009 // length 34010 0x40, 34011 // object type 34012 0x15, 34013 // streamType 34014 0x00, 34015 0x06, 34016 0x00, 34017 // bufferSizeDB 34018 0x00, 34019 0x00, 34020 0xda, 34021 0xc0, 34022 // maxBitrate 34023 0x00, 34024 0x00, 34025 0xda, 34026 0xc0, 34027 // avgBitrate 34028 // DecoderSpecificInfo 34029 0x05, 34030 // tag, DecoderSpecificInfoTag 34031 0x02, 34032 // length 34033 // ISO/IEC 14496-3, AudioSpecificConfig 34034 // for samplingFrequencyIndex see ISO/IEC 13818-7:2006, 8.1.3.2.2, Table 35 34035 (track.audioobjecttype << 3) | (track.samplingfrequencyindex >>> 1), 34036 (track.samplingfrequencyindex << 7) | (track.channelcount << 3), 34037 0x06, 34038 0x01, 34039 0x02, // GASpecificConfig 34040 ]) 34041 ); 34042 }; 34043 ftyp = function () { 34044 return box(types.ftyp, MAJOR_BRAND, MINOR_VERSION, MAJOR_BRAND, AVC1_BRAND); 34045 }; 34046 hdlr = function (type) { 34047 return box(types.hdlr, HDLR_TYPES[type]); 34048 }; 34049 mdat = function (data) { 34050 return box(types.mdat, data); 34051 }; 34052 mdhd = function (track) { 34053 var result = new Uint8Array([ 34054 0x00, 34055 // version 0 34056 0x00, 34057 0x00, 34058 0x00, 34059 // flags 34060 0x00, 34061 0x00, 34062 0x00, 34063 0x02, 34064 // creation_time 34065 0x00, 34066 0x00, 34067 0x00, 34068 0x03, 34069 // modification_time 34070 0x00, 34071 0x01, 34072 0x5f, 34073 0x90, 34074 // timescale, 90,000 "ticks" per second 34075 (track.duration >>> 24) & 0xff, 34076 (track.duration >>> 16) & 0xff, 34077 (track.duration >>> 8) & 0xff, 34078 track.duration & 0xff, 34079 // duration 34080 0x55, 34081 0xc4, 34082 // 'und' language (undetermined) 34083 0x00, 34084 0x00, 34085 ]); // Use the sample rate from the track metadata, when it is 34086 // defined. The sample rate can be parsed out of an ADTS header, for 34087 // instance. 34088 34089 if (track.samplerate) { 34090 result[12] = (track.samplerate >>> 24) & 0xff;
vendor: 51,941 bytes, lines 34091-35772
34091 result[13] = (track.samplerate >>> 16) & 0xff; 34092 result[14] = (track.samplerate >>> 8) & 0xff; 34093 result[15] = track.samplerate & 0xff; 34094 } 34095 return box(types.mdhd, result); 34096 }; 34097 mdia = function (track) { 34098 return box(types.mdia, mdhd(track), hdlr(track.type), minf(track)); 34099 }; 34100 mfhd = function (sequenceNumber) { 34101 return box( 34102 types.mfhd, 34103 new Uint8Array([ 34104 0x00, 34105 0x00, 34106 0x00, 34107 0x00, 34108 // flags 34109 (sequenceNumber & 0xff000000) >> 24, 34110 (sequenceNumber & 0xff0000) >> 16, 34111 (sequenceNumber & 0xff00) >> 8, 34112 sequenceNumber & 0xff, // sequence_number 34113 ]) 34114 ); 34115 }; 34116 minf = function (track) { 34117 return box( 34118 types.minf, 34119 track.type === 'video' ? box(types.vmhd, VMHD) : box(types.smhd, SMHD), 34120 dinf(), 34121 stbl(track) 34122 ); 34123 }; 34124 moof = function (sequenceNumber, tracks) { 34125 var trackFragments = [], 34126 i = tracks.length; // build traf boxes for each track fragment 34127 34128 while (i--) { 34129 trackFragments[i] = traf(tracks[i]); 34130 } 34131 return box.apply(null, [types.moof, mfhd(sequenceNumber)].concat(trackFragments)); 34132 }; 34133 /** 34134 * Returns a movie box. 34135 * @param tracks {array} the tracks associated with this movie 34136 * @see ISO/IEC 14496-12:2012(E), section 8.2.1 34137 */ 34138 34139 moov = function (tracks) { 34140 var i = tracks.length, 34141 boxes = []; 34142 while (i--) { 34143 boxes[i] = trak(tracks[i]); 34144 } 34145 return box.apply(null, [types.moov, mvhd(0xffffffff)].concat(boxes).concat(mvex(tracks))); 34146 }; 34147 mvex = function (tracks) { 34148 var i = tracks.length, 34149 boxes = []; 34150 while (i--) { 34151 boxes[i] = trex(tracks[i]); 34152 } 34153 return box.apply(null, [types.mvex].concat(boxes)); 34154 }; 34155 mvhd = function (duration) { 34156 var bytes = new Uint8Array([ 34157 0x00, 34158 // version 0 34159 0x00, 34160 0x00, 34161 0x00, 34162 // flags 34163 0x00, 34164 0x00, 34165 0x00, 34166 0x01, 34167 // creation_time 34168 0x00, 34169 0x00, 34170 0x00, 34171 0x02, 34172 // modification_time 34173 0x00, 34174 0x01, 34175 0x5f, 34176 0x90, 34177 // timescale, 90,000 "ticks" per second 34178 (duration & 0xff000000) >> 24, 34179 (duration & 0xff0000) >> 16, 34180 (duration & 0xff00) >> 8, 34181 duration & 0xff, 34182 // duration 34183 0x00, 34184 0x01, 34185 0x00, 34186 0x00, 34187 // 1.0 rate 34188 0x01, 34189 0x00, 34190 // 1.0 volume 34191 0x00, 34192 0x00, 34193 // reserved 34194 0x00, 34195 0x00, 34196 0x00, 34197 0x00, 34198 // reserved 34199 0x00, 34200 0x00, 34201 0x00, 34202 0x00, 34203 // reserved 34204 0x00, 34205 0x01, 34206 0x00, 34207 0x00, 34208 0x00, 34209 0x00, 34210 0x00, 34211 0x00, 34212 0x00, 34213 0x00, 34214 0x00, 34215 0x00, 34216 0x00, 34217 0x00, 34218 0x00, 34219 0x00, 34220 0x00, 34221 0x01, 34222 0x00, 34223 0x00, 34224 0x00, 34225 0x00, 34226 0x00, 34227 0x00, 34228 0x00, 34229 0x00, 34230 0x00, 34231 0x00, 34232 0x00, 34233 0x00, 34234 0x00, 34235 0x00, 34236 0x40, 34237 0x00, 34238 0x00, 34239 0x00, 34240 // transformation: unity matrix 34241 0x00, 34242 0x00, 34243 0x00, 34244 0x00, 34245 0x00, 34246 0x00, 34247 0x00, 34248 0x00, 34249 0x00, 34250 0x00, 34251 0x00, 34252 0x00, 34253 0x00, 34254 0x00, 34255 0x00, 34256 0x00, 34257 0x00, 34258 0x00, 34259 0x00, 34260 0x00, 34261 0x00, 34262 0x00, 34263 0x00, 34264 0x00, 34265 // pre_defined 34266 0xff, 34267 0xff, 34268 0xff, 34269 0xff, // next_track_ID 34270 ]); 34271 return box(types.mvhd, bytes); 34272 }; 34273 sdtp = function (track) { 34274 var samples = track.samples || [], 34275 bytes = new Uint8Array(4 + samples.length), 34276 flags, 34277 i; // leave the full box header (4 bytes) all zero 34278 // write the sample table 34279 34280 for (i = 0; i < samples.length; i++) { 34281 flags = samples[i].flags; 34282 bytes[i + 4] = (flags.dependsOn << 4) | (flags.isDependedOn << 2) | flags.hasRedundancy; 34283 } 34284 return box(types.sdtp, bytes); 34285 }; 34286 stbl = function (track) { 34287 return box( 34288 types.stbl, 34289 stsd(track), 34290 box(types.stts, STTS), 34291 box(types.stsc, STSC), 34292 box(types.stsz, STSZ), 34293 box(types.stco, STCO) 34294 ); 34295 }; 34296 (function () { 34297 var videoSample, audioSample; 34298 stsd = function (track) { 34299 return box( 34300 types.stsd, 34301 new Uint8Array([ 34302 0x00, 34303 // version 0 34304 0x00, 0x00, 0x00, 34305 // flags 34306 0x00, 0x00, 0x00, 0x01, 34307 ]), 34308 track.type === 'video' ? videoSample(track) : audioSample(track) 34309 ); 34310 }; 34311 videoSample = function (track) { 34312 var sps = track.sps || [], 34313 pps = track.pps || [], 34314 sequenceParameterSets = [], 34315 pictureParameterSets = [], 34316 i, 34317 avc1Box; // assemble the SPSs 34318 34319 for (i = 0; i < sps.length; i++) { 34320 sequenceParameterSets.push((sps[i].byteLength & 0xff00) >>> 8); 34321 sequenceParameterSets.push(sps[i].byteLength & 0xff); // sequenceParameterSetLength 34322 34323 sequenceParameterSets = sequenceParameterSets.concat(Array.prototype.slice.call(sps[i])); // SPS 34324 } // assemble the PPSs 34325 34326 for (i = 0; i < pps.length; i++) { 34327 pictureParameterSets.push((pps[i].byteLength & 0xff00) >>> 8); 34328 pictureParameterSets.push(pps[i].byteLength & 0xff); 34329 pictureParameterSets = pictureParameterSets.concat(Array.prototype.slice.call(pps[i])); 34330 } 34331 avc1Box = [ 34332 types.avc1, 34333 new Uint8Array([ 34334 0x00, 34335 0x00, 34336 0x00, 34337 0x00, 34338 0x00, 34339 0x00, 34340 // reserved 34341 0x00, 34342 0x01, 34343 // data_reference_index 34344 0x00, 34345 0x00, 34346 // pre_defined 34347 0x00, 34348 0x00, 34349 // reserved 34350 0x00, 34351 0x00, 34352 0x00, 34353 0x00, 34354 0x00, 34355 0x00, 34356 0x00, 34357 0x00, 34358 0x00, 34359 0x00, 34360 0x00, 34361 0x00, 34362 // pre_defined 34363 (track.width & 0xff00) >> 8, 34364 track.width & 0xff, 34365 // width 34366 (track.height & 0xff00) >> 8, 34367 track.height & 0xff, 34368 // height 34369 0x00, 34370 0x48, 34371 0x00, 34372 0x00, 34373 // horizresolution 34374 0x00, 34375 0x48, 34376 0x00, 34377 0x00, 34378 // vertresolution 34379 0x00, 34380 0x00, 34381 0x00, 34382 0x00, 34383 // reserved 34384 0x00, 34385 0x01, 34386 // frame_count 34387 0x13, 34388 0x76, 34389 0x69, 34390 0x64, 34391 0x65, 34392 0x6f, 34393 0x6a, 34394 0x73, 34395 0x2d, 34396 0x63, 34397 0x6f, 34398 0x6e, 34399 0x74, 34400 0x72, 34401 0x69, 34402 0x62, 34403 0x2d, 34404 0x68, 34405 0x6c, 34406 0x73, 34407 0x00, 34408 0x00, 34409 0x00, 34410 0x00, 34411 0x00, 34412 0x00, 34413 0x00, 34414 0x00, 34415 0x00, 34416 0x00, 34417 0x00, 34418 0x00, 34419 // compressorname 34420 0x00, 34421 0x18, 34422 // depth = 24 34423 0x11, 34424 0x11, // pre_defined = -1 34425 ]), 34426 box( 34427 types.avcC, 34428 new Uint8Array( 34429 [ 34430 0x01, 34431 // configurationVersion 34432 track.profileIdc, 34433 // AVCProfileIndication 34434 track.profileCompatibility, 34435 // profile_compatibility 34436 track.levelIdc, 34437 // AVCLevelIndication 34438 0xff, // lengthSizeMinusOne, hard-coded to 4 bytes 34439 ].concat( 34440 [sps.length], 34441 // numOfSequenceParameterSets 34442 sequenceParameterSets, 34443 // "SPS" 34444 [pps.length], 34445 // numOfPictureParameterSets 34446 pictureParameterSets // "PPS" 34447 ) 34448 ) 34449 ), 34450 box( 34451 types.btrt, 34452 new Uint8Array([ 34453 0x00, 34454 0x1c, 34455 0x9c, 34456 0x80, 34457 // bufferSizeDB 34458 0x00, 34459 0x2d, 34460 0xc6, 34461 0xc0, 34462 // maxBitrate 34463 0x00, 34464 0x2d, 34465 0xc6, 34466 0xc0, // avgBitrate 34467 ]) 34468 ), 34469 ]; 34470 if (track.sarRatio) { 34471 var hSpacing = track.sarRatio[0], 34472 vSpacing = track.sarRatio[1]; 34473 avc1Box.push( 34474 box( 34475 types.pasp, 34476 new Uint8Array([ 34477 (hSpacing & 0xff000000) >> 24, 34478 (hSpacing & 0xff0000) >> 16, 34479 (hSpacing & 0xff00) >> 8, 34480 hSpacing & 0xff, 34481 (vSpacing & 0xff000000) >> 24, 34482 (vSpacing & 0xff0000) >> 16, 34483 (vSpacing & 0xff00) >> 8, 34484 vSpacing & 0xff, 34485 ]) 34486 ) 34487 ); 34488 } 34489 return box.apply(null, avc1Box); 34490 }; 34491 audioSample = function (track) { 34492 return box( 34493 types.mp4a, 34494 new Uint8Array([ 34495 // SampleEntry, ISO/IEC 14496-12 34496 0x00, 34497 0x00, 34498 0x00, 34499 0x00, 34500 0x00, 34501 0x00, 34502 // reserved 34503 0x00, 34504 0x01, 34505 // data_reference_index 34506 // AudioSampleEntry, ISO/IEC 14496-12 34507 0x00, 34508 0x00, 34509 0x00, 34510 0x00, 34511 // reserved 34512 0x00, 34513 0x00, 34514 0x00, 34515 0x00, 34516 // reserved 34517 (track.channelcount & 0xff00) >> 8, 34518 track.channelcount & 0xff, 34519 // channelcount 34520 (track.samplesize & 0xff00) >> 8, 34521 track.samplesize & 0xff, 34522 // samplesize 34523 0x00, 34524 0x00, 34525 // pre_defined 34526 0x00, 34527 0x00, 34528 // reserved 34529 (track.samplerate & 0xff00) >> 8, 34530 track.samplerate & 0xff, 34531 0x00, 34532 0x00, // samplerate, 16.16 34533 // MP4AudioSampleEntry, ISO/IEC 14496-14 34534 ]), 34535 esds(track) 34536 ); 34537 }; 34538 })(); 34539 tkhd = function (track) { 34540 var result = new Uint8Array([ 34541 0x00, 34542 // version 0 34543 0x00, 34544 0x00, 34545 0x07, 34546 // flags 34547 0x00, 34548 0x00, 34549 0x00, 34550 0x00, 34551 // creation_time 34552 0x00, 34553 0x00, 34554 0x00, 34555 0x00, 34556 // modification_time 34557 (track.id & 0xff000000) >> 24, 34558 (track.id & 0xff0000) >> 16, 34559 (track.id & 0xff00) >> 8, 34560 track.id & 0xff, 34561 // track_ID 34562 0x00, 34563 0x00, 34564 0x00, 34565 0x00, 34566 // reserved 34567 (track.duration & 0xff000000) >> 24, 34568 (track.duration & 0xff0000) >> 16, 34569 (track.duration & 0xff00) >> 8, 34570 track.duration & 0xff, 34571 // duration 34572 0x00, 34573 0x00, 34574 0x00, 34575 0x00, 34576 0x00, 34577 0x00, 34578 0x00, 34579 0x00, 34580 // reserved 34581 0x00, 34582 0x00, 34583 // layer 34584 0x00, 34585 0x00, 34586 // alternate_group 34587 0x01, 34588 0x00, 34589 // non-audio track volume 34590 0x00, 34591 0x00, 34592 // reserved 34593 0x00, 34594 0x01, 34595 0x00, 34596 0x00, 34597 0x00, 34598 0x00, 34599 0x00, 34600 0x00, 34601 0x00, 34602 0x00, 34603 0x00, 34604 0x00, 34605 0x00, 34606 0x00, 34607 0x00, 34608 0x00, 34609 0x00, 34610 0x01, 34611 0x00, 34612 0x00, 34613 0x00, 34614 0x00, 34615 0x00, 34616 0x00, 34617 0x00, 34618 0x00, 34619 0x00, 34620 0x00, 34621 0x00, 34622 0x00, 34623 0x00, 34624 0x00, 34625 0x40, 34626 0x00, 34627 0x00, 34628 0x00, 34629 // transformation: unity matrix 34630 (track.width & 0xff00) >> 8, 34631 track.width & 0xff, 34632 0x00, 34633 0x00, 34634 // width 34635 (track.height & 0xff00) >> 8, 34636 track.height & 0xff, 34637 0x00, 34638 0x00, // height 34639 ]); 34640 return box(types.tkhd, result); 34641 }; 34642 /** 34643 * Generate a track fragment (traf) box. A traf box collects metadata 34644 * about tracks in a movie fragment (moof) box. 34645 */ 34646 34647 traf = function (track) { 34648 var trackFragmentHeader, 34649 trackFragmentDecodeTime, 34650 trackFragmentRun, 34651 sampleDependencyTable, 34652 dataOffset, 34653 upperWordBaseMediaDecodeTime, 34654 lowerWordBaseMediaDecodeTime; 34655 trackFragmentHeader = box( 34656 types.tfhd, 34657 new Uint8Array([ 34658 0x00, 34659 // version 0 34660 0x00, 34661 0x00, 34662 0x3a, 34663 // flags 34664 (track.id & 0xff000000) >> 24, 34665 (track.id & 0xff0000) >> 16, 34666 (track.id & 0xff00) >> 8, 34667 track.id & 0xff, 34668 // track_ID 34669 0x00, 34670 0x00, 34671 0x00, 34672 0x01, 34673 // sample_description_index 34674 0x00, 34675 0x00, 34676 0x00, 34677 0x00, 34678 // default_sample_duration 34679 0x00, 34680 0x00, 34681 0x00, 34682 0x00, 34683 // default_sample_size 34684 0x00, 34685 0x00, 34686 0x00, 34687 0x00, // default_sample_flags 34688 ]) 34689 ); 34690 upperWordBaseMediaDecodeTime = Math.floor(track.baseMediaDecodeTime / MAX_UINT32); 34691 lowerWordBaseMediaDecodeTime = Math.floor(track.baseMediaDecodeTime % MAX_UINT32); 34692 trackFragmentDecodeTime = box( 34693 types.tfdt, 34694 new Uint8Array([ 34695 0x01, 34696 // version 1 34697 0x00, 34698 0x00, 34699 0x00, 34700 // flags 34701 // baseMediaDecodeTime 34702 (upperWordBaseMediaDecodeTime >>> 24) & 0xff, 34703 (upperWordBaseMediaDecodeTime >>> 16) & 0xff, 34704 (upperWordBaseMediaDecodeTime >>> 8) & 0xff, 34705 upperWordBaseMediaDecodeTime & 0xff, 34706 (lowerWordBaseMediaDecodeTime >>> 24) & 0xff, 34707 (lowerWordBaseMediaDecodeTime >>> 16) & 0xff, 34708 (lowerWordBaseMediaDecodeTime >>> 8) & 0xff, 34709 lowerWordBaseMediaDecodeTime & 0xff, 34710 ]) 34711 ); // the data offset specifies the number of bytes from the start of 34712 // the containing moof to the first payload byte of the associated 34713 // mdat 34714 34715 dataOffset = 34716 32 + 34717 // tfhd 34718 20 + 34719 // tfdt 34720 8 + 34721 // traf header 34722 16 + 34723 // mfhd 34724 8 + 34725 // moof header 34726 8; // mdat header 34727 // audio tracks require less metadata 34728 34729 if (track.type === 'audio') { 34730 trackFragmentRun = trun$1(track, dataOffset); 34731 return box(types.traf, trackFragmentHeader, trackFragmentDecodeTime, trackFragmentRun); 34732 } // video tracks should contain an independent and disposable samples 34733 // box (sdtp) 34734 // generate one and adjust offsets to match 34735 34736 sampleDependencyTable = sdtp(track); 34737 trackFragmentRun = trun$1(track, sampleDependencyTable.length + dataOffset); 34738 return box( 34739 types.traf, 34740 trackFragmentHeader, 34741 trackFragmentDecodeTime, 34742 trackFragmentRun, 34743 sampleDependencyTable 34744 ); 34745 }; 34746 /** 34747 * Generate a track box. 34748 * @param track {object} a track definition 34749 * @return {Uint8Array} the track box 34750 */ 34751 34752 trak = function (track) { 34753 track.duration = track.duration || 0xffffffff; 34754 return box(types.trak, tkhd(track), mdia(track)); 34755 }; 34756 trex = function (track) { 34757 var result = new Uint8Array([ 34758 0x00, 34759 // version 0 34760 0x00, 34761 0x00, 34762 0x00, 34763 // flags 34764 (track.id & 0xff000000) >> 24, 34765 (track.id & 0xff0000) >> 16, 34766 (track.id & 0xff00) >> 8, 34767 track.id & 0xff, 34768 // track_ID 34769 0x00, 34770 0x00, 34771 0x00, 34772 0x01, 34773 // default_sample_description_index 34774 0x00, 34775 0x00, 34776 0x00, 34777 0x00, 34778 // default_sample_duration 34779 0x00, 34780 0x00, 34781 0x00, 34782 0x00, 34783 // default_sample_size 34784 0x00, 34785 0x01, 34786 0x00, 34787 0x01, // default_sample_flags 34788 ]); // the last two bytes of default_sample_flags is the sample 34789 // degradation priority, a hint about the importance of this sample 34790 // relative to others. Lower the degradation priority for all sample 34791 // types other than video. 34792 34793 if (track.type !== 'video') { 34794 result[result.length - 1] = 0x00; 34795 } 34796 return box(types.trex, result); 34797 }; 34798 (function () { 34799 var audioTrun, videoTrun, trunHeader; // This method assumes all samples are uniform. That is, if a 34800 // duration is present for the first sample, it will be present for 34801 // all subsequent samples. 34802 // see ISO/IEC 14496-12:2012, Section 8.8.8.1 34803 34804 trunHeader = function (samples, offset) { 34805 var durationPresent = 0, 34806 sizePresent = 0, 34807 flagsPresent = 0, 34808 compositionTimeOffset = 0; // trun flag constants 34809 34810 if (samples.length) { 34811 if (samples[0].duration !== undefined) { 34812 durationPresent = 0x1; 34813 } 34814 if (samples[0].size !== undefined) { 34815 sizePresent = 0x2; 34816 } 34817 if (samples[0].flags !== undefined) { 34818 flagsPresent = 0x4; 34819 } 34820 if (samples[0].compositionTimeOffset !== undefined) { 34821 compositionTimeOffset = 0x8; 34822 } 34823 } 34824 return [ 34825 0x00, 34826 // version 0 34827 0x00, 34828 durationPresent | sizePresent | flagsPresent | compositionTimeOffset, 34829 0x01, 34830 // flags 34831 (samples.length & 0xff000000) >>> 24, 34832 (samples.length & 0xff0000) >>> 16, 34833 (samples.length & 0xff00) >>> 8, 34834 samples.length & 0xff, 34835 // sample_count 34836 (offset & 0xff000000) >>> 24, 34837 (offset & 0xff0000) >>> 16, 34838 (offset & 0xff00) >>> 8, 34839 offset & 0xff, // data_offset 34840 ]; 34841 }; 34842 videoTrun = function (track, offset) { 34843 var bytesOffest, bytes, header, samples, sample, i; 34844 samples = track.samples || []; 34845 offset += 8 + 12 + 16 * samples.length; 34846 header = trunHeader(samples, offset); 34847 bytes = new Uint8Array(header.length + samples.length * 16); 34848 bytes.set(header); 34849 bytesOffest = header.length; 34850 for (i = 0; i < samples.length; i++) { 34851 sample = samples[i]; 34852 bytes[bytesOffest++] = (sample.duration & 0xff000000) >>> 24; 34853 bytes[bytesOffest++] = (sample.duration & 0xff0000) >>> 16; 34854 bytes[bytesOffest++] = (sample.duration & 0xff00) >>> 8; 34855 bytes[bytesOffest++] = sample.duration & 0xff; // sample_duration 34856 34857 bytes[bytesOffest++] = (sample.size & 0xff000000) >>> 24; 34858 bytes[bytesOffest++] = (sample.size & 0xff0000) >>> 16; 34859 bytes[bytesOffest++] = (sample.size & 0xff00) >>> 8; 34860 bytes[bytesOffest++] = sample.size & 0xff; // sample_size 34861 34862 bytes[bytesOffest++] = (sample.flags.isLeading << 2) | sample.flags.dependsOn; 34863 bytes[bytesOffest++] = 34864 (sample.flags.isDependedOn << 6) | 34865 (sample.flags.hasRedundancy << 4) | 34866 (sample.flags.paddingValue << 1) | 34867 sample.flags.isNonSyncSample; 34868 bytes[bytesOffest++] = sample.flags.degradationPriority & (0xf0 << 8); 34869 bytes[bytesOffest++] = sample.flags.degradationPriority & 0x0f; // sample_flags 34870 34871 bytes[bytesOffest++] = (sample.compositionTimeOffset & 0xff000000) >>> 24; 34872 bytes[bytesOffest++] = (sample.compositionTimeOffset & 0xff0000) >>> 16; 34873 bytes[bytesOffest++] = (sample.compositionTimeOffset & 0xff00) >>> 8; 34874 bytes[bytesOffest++] = sample.compositionTimeOffset & 0xff; // sample_composition_time_offset 34875 } 34876 return box(types.trun, bytes); 34877 }; 34878 audioTrun = function (track, offset) { 34879 var bytes, bytesOffest, header, samples, sample, i; 34880 samples = track.samples || []; 34881 offset += 8 + 12 + 8 * samples.length; 34882 header = trunHeader(samples, offset); 34883 bytes = new Uint8Array(header.length + samples.length * 8); 34884 bytes.set(header); 34885 bytesOffest = header.length; 34886 for (i = 0; i < samples.length; i++) { 34887 sample = samples[i]; 34888 bytes[bytesOffest++] = (sample.duration & 0xff000000) >>> 24; 34889 bytes[bytesOffest++] = (sample.duration & 0xff0000) >>> 16; 34890 bytes[bytesOffest++] = (sample.duration & 0xff00) >>> 8; 34891 bytes[bytesOffest++] = sample.duration & 0xff; // sample_duration 34892 34893 bytes[bytesOffest++] = (sample.size & 0xff000000) >>> 24; 34894 bytes[bytesOffest++] = (sample.size & 0xff0000) >>> 16; 34895 bytes[bytesOffest++] = (sample.size & 0xff00) >>> 8; 34896 bytes[bytesOffest++] = sample.size & 0xff; // sample_size 34897 } 34898 return box(types.trun, bytes); 34899 }; 34900 trun$1 = function (track, offset) { 34901 if (track.type === 'audio') { 34902 return audioTrun(track, offset); 34903 } 34904 return videoTrun(track, offset); 34905 }; 34906 })(); 34907 var mp4Generator = { 34908 ftyp: ftyp, 34909 mdat: mdat, 34910 moof: moof, 34911 moov: moov, 34912 initSegment: function (tracks) { 34913 var fileType = ftyp(), 34914 movie = moov(tracks), 34915 result; 34916 result = new Uint8Array(fileType.byteLength + movie.byteLength); 34917 result.set(fileType); 34918 result.set(movie, fileType.byteLength); 34919 return result; 34920 }, 34921 }; 34922 /** 34923 * mux.js 34924 * 34925 * Copyright (c) Brightcove 34926 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 34927 */ 34928 // composed of the nal units that make up that frame 34929 // Also keep track of cummulative data about the frame from the nal units such 34930 // as the frame duration, starting pts, etc. 34931 34932 var groupNalsIntoFrames = function (nalUnits) { 34933 var i, 34934 currentNal, 34935 currentFrame = [], 34936 frames = []; // TODO added for LHLS, make sure this is OK 34937 34938 frames.byteLength = 0; 34939 frames.nalCount = 0; 34940 frames.duration = 0; 34941 currentFrame.byteLength = 0; 34942 for (i = 0; i < nalUnits.length; i++) { 34943 currentNal = nalUnits[i]; // Split on 'aud'-type nal units 34944 34945 if (currentNal.nalUnitType === 'access_unit_delimiter_rbsp') { 34946 // Since the very first nal unit is expected to be an AUD 34947 // only push to the frames array when currentFrame is not empty 34948 if (currentFrame.length) { 34949 currentFrame.duration = currentNal.dts - currentFrame.dts; // TODO added for LHLS, make sure this is OK 34950 34951 frames.byteLength += currentFrame.byteLength; 34952 frames.nalCount += currentFrame.length; 34953 frames.duration += currentFrame.duration; 34954 frames.push(currentFrame); 34955 } 34956 currentFrame = [currentNal]; 34957 currentFrame.byteLength = currentNal.data.byteLength; 34958 currentFrame.pts = currentNal.pts; 34959 currentFrame.dts = currentNal.dts; 34960 } else { 34961 // Specifically flag key frames for ease of use later 34962 if (currentNal.nalUnitType === 'slice_layer_without_partitioning_rbsp_idr') { 34963 currentFrame.keyFrame = true; 34964 } 34965 currentFrame.duration = currentNal.dts - currentFrame.dts; 34966 currentFrame.byteLength += currentNal.data.byteLength; 34967 currentFrame.push(currentNal); 34968 } 34969 } // For the last frame, use the duration of the previous frame if we 34970 // have nothing better to go on 34971 34972 if (frames.length && (!currentFrame.duration || currentFrame.duration <= 0)) { 34973 currentFrame.duration = frames[frames.length - 1].duration; 34974 } // Push the final frame 34975 // TODO added for LHLS, make sure this is OK 34976 34977 frames.byteLength += currentFrame.byteLength; 34978 frames.nalCount += currentFrame.length; 34979 frames.duration += currentFrame.duration; 34980 frames.push(currentFrame); 34981 return frames; 34982 }; // Convert an array of frames into an array of Gop with each Gop being composed 34983 // of the frames that make up that Gop 34984 // Also keep track of cummulative data about the Gop from the frames such as the 34985 // Gop duration, starting pts, etc. 34986 34987 var groupFramesIntoGops = function (frames) { 34988 var i, 34989 currentFrame, 34990 currentGop = [], 34991 gops = []; // We must pre-set some of the values on the Gop since we 34992 // keep running totals of these values 34993 34994 currentGop.byteLength = 0; 34995 currentGop.nalCount = 0; 34996 currentGop.duration = 0; 34997 currentGop.pts = frames[0].pts; 34998 currentGop.dts = frames[0].dts; // store some metadata about all the Gops 34999 35000 gops.byteLength = 0; 35001 gops.nalCount = 0; 35002 gops.duration = 0; 35003 gops.pts = frames[0].pts; 35004 gops.dts = frames[0].dts; 35005 for (i = 0; i < frames.length; i++) { 35006 currentFrame = frames[i]; 35007 if (currentFrame.keyFrame) { 35008 // Since the very first frame is expected to be an keyframe 35009 // only push to the gops array when currentGop is not empty 35010 if (currentGop.length) { 35011 gops.push(currentGop); 35012 gops.byteLength += currentGop.byteLength; 35013 gops.nalCount += currentGop.nalCount; 35014 gops.duration += currentGop.duration; 35015 } 35016 currentGop = [currentFrame]; 35017 currentGop.nalCount = currentFrame.length; 35018 currentGop.byteLength = currentFrame.byteLength; 35019 currentGop.pts = currentFrame.pts; 35020 currentGop.dts = currentFrame.dts; 35021 currentGop.duration = currentFrame.duration; 35022 } else { 35023 currentGop.duration += currentFrame.duration; 35024 currentGop.nalCount += currentFrame.length; 35025 currentGop.byteLength += currentFrame.byteLength; 35026 currentGop.push(currentFrame); 35027 } 35028 } 35029 if (gops.length && currentGop.duration <= 0) { 35030 currentGop.duration = gops[gops.length - 1].duration; 35031 } 35032 gops.byteLength += currentGop.byteLength; 35033 gops.nalCount += currentGop.nalCount; 35034 gops.duration += currentGop.duration; // push the final Gop 35035 35036 gops.push(currentGop); 35037 return gops; 35038 }; 35039 /* 35040 * Search for the first keyframe in the GOPs and throw away all frames 35041 * until that keyframe. Then extend the duration of the pulled keyframe 35042 * and pull the PTS and DTS of the keyframe so that it covers the time 35043 * range of the frames that were disposed. 35044 * 35045 * @param {Array} gops video GOPs 35046 * @returns {Array} modified video GOPs 35047 */ 35048 35049 var extendFirstKeyFrame = function (gops) { 35050 var currentGop; 35051 if (!gops[0][0].keyFrame && gops.length > 1) { 35052 // Remove the first GOP 35053 currentGop = gops.shift(); 35054 gops.byteLength -= currentGop.byteLength; 35055 gops.nalCount -= currentGop.nalCount; // Extend the first frame of what is now the 35056 // first gop to cover the time period of the 35057 // frames we just removed 35058 35059 gops[0][0].dts = currentGop.dts; 35060 gops[0][0].pts = currentGop.pts; 35061 gops[0][0].duration += currentGop.duration; 35062 } 35063 return gops; 35064 }; 35065 /** 35066 * Default sample object 35067 * see ISO/IEC 14496-12:2012, section 8.6.4.3 35068 */ 35069 35070 var createDefaultSample = function () { 35071 return { 35072 size: 0, 35073 flags: { 35074 isLeading: 0, 35075 dependsOn: 1, 35076 isDependedOn: 0, 35077 hasRedundancy: 0, 35078 degradationPriority: 0, 35079 isNonSyncSample: 1, 35080 }, 35081 }; 35082 }; 35083 /* 35084 * Collates information from a video frame into an object for eventual 35085 * entry into an MP4 sample table. 35086 * 35087 * @param {Object} frame the video frame 35088 * @param {Number} dataOffset the byte offset to position the sample 35089 * @return {Object} object containing sample table info for a frame 35090 */ 35091 35092 var sampleForFrame = function (frame, dataOffset) { 35093 var sample = createDefaultSample(); 35094 sample.dataOffset = dataOffset; 35095 sample.compositionTimeOffset = frame.pts - frame.dts; 35096 sample.duration = frame.duration; 35097 sample.size = 4 * frame.length; // Space for nal unit size 35098 35099 sample.size += frame.byteLength; 35100 if (frame.keyFrame) { 35101 sample.flags.dependsOn = 2; 35102 sample.flags.isNonSyncSample = 0; 35103 } 35104 return sample; 35105 }; // generate the track's sample table from an array of gops 35106 35107 var generateSampleTable$1 = function (gops, baseDataOffset) { 35108 var h, 35109 i, 35110 sample, 35111 currentGop, 35112 currentFrame, 35113 dataOffset = baseDataOffset || 0, 35114 samples = []; 35115 for (h = 0; h < gops.length; h++) { 35116 currentGop = gops[h]; 35117 for (i = 0; i < currentGop.length; i++) { 35118 currentFrame = currentGop[i]; 35119 sample = sampleForFrame(currentFrame, dataOffset); 35120 dataOffset += sample.size; 35121 samples.push(sample); 35122 } 35123 } 35124 return samples; 35125 }; // generate the track's raw mdat data from an array of gops 35126 35127 var concatenateNalData = function (gops) { 35128 var h, 35129 i, 35130 j, 35131 currentGop, 35132 currentFrame, 35133 currentNal, 35134 dataOffset = 0, 35135 nalsByteLength = gops.byteLength, 35136 numberOfNals = gops.nalCount, 35137 totalByteLength = nalsByteLength + 4 * numberOfNals, 35138 data = new Uint8Array(totalByteLength), 35139 view = new DataView(data.buffer); // For each Gop.. 35140 35141 for (h = 0; h < gops.length; h++) { 35142 currentGop = gops[h]; // For each Frame.. 35143 35144 for (i = 0; i < currentGop.length; i++) { 35145 currentFrame = currentGop[i]; // For each NAL.. 35146 35147 for (j = 0; j < currentFrame.length; j++) { 35148 currentNal = currentFrame[j]; 35149 view.setUint32(dataOffset, currentNal.data.byteLength); 35150 dataOffset += 4; 35151 data.set(currentNal.data, dataOffset); 35152 dataOffset += currentNal.data.byteLength; 35153 } 35154 } 35155 } 35156 return data; 35157 }; // generate the track's sample table from a frame 35158 35159 var generateSampleTableForFrame = function (frame, baseDataOffset) { 35160 var sample, 35161 dataOffset = baseDataOffset || 0, 35162 samples = []; 35163 sample = sampleForFrame(frame, dataOffset); 35164 samples.push(sample); 35165 return samples; 35166 }; // generate the track's raw mdat data from a frame 35167 35168 var concatenateNalDataForFrame = function (frame) { 35169 var i, 35170 currentNal, 35171 dataOffset = 0, 35172 nalsByteLength = frame.byteLength, 35173 numberOfNals = frame.length, 35174 totalByteLength = nalsByteLength + 4 * numberOfNals, 35175 data = new Uint8Array(totalByteLength), 35176 view = new DataView(data.buffer); // For each NAL.. 35177 35178 for (i = 0; i < frame.length; i++) { 35179 currentNal = frame[i]; 35180 view.setUint32(dataOffset, currentNal.data.byteLength); 35181 dataOffset += 4; 35182 data.set(currentNal.data, dataOffset); 35183 dataOffset += currentNal.data.byteLength; 35184 } 35185 return data; 35186 }; 35187 var frameUtils$1 = { 35188 groupNalsIntoFrames: groupNalsIntoFrames, 35189 groupFramesIntoGops: groupFramesIntoGops, 35190 extendFirstKeyFrame: extendFirstKeyFrame, 35191 generateSampleTable: generateSampleTable$1, 35192 concatenateNalData: concatenateNalData, 35193 generateSampleTableForFrame: generateSampleTableForFrame, 35194 concatenateNalDataForFrame: concatenateNalDataForFrame, 35195 }; 35196 /** 35197 * mux.js 35198 * 35199 * Copyright (c) Brightcove 35200 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 35201 */ 35202 35203 var highPrefix = [33, 16, 5, 32, 164, 27]; 35204 var lowPrefix = [33, 65, 108, 84, 1, 2, 4, 8, 168, 2, 4, 8, 17, 191, 252]; 35205 var zeroFill = function (count) { 35206 var a = []; 35207 while (count--) { 35208 a.push(0); 35209 } 35210 return a; 35211 }; 35212 var makeTable = function (metaTable) { 35213 return Object.keys(metaTable).reduce(function (obj, key) { 35214 obj[key] = new Uint8Array( 35215 metaTable[key].reduce(function (arr, part) { 35216 return arr.concat(part); 35217 }, []) 35218 ); 35219 return obj; 35220 }, {}); 35221 }; 35222 var silence; 35223 var silence_1 = function () { 35224 if (!silence) { 35225 // Frames-of-silence to use for filling in missing AAC frames 35226 var coneOfSilence = { 35227 96000: [highPrefix, [227, 64], zeroFill(154), [56]], 35228 88200: [highPrefix, [231], zeroFill(170), [56]], 35229 64000: [highPrefix, [248, 192], zeroFill(240), [56]], 35230 48000: [highPrefix, [255, 192], zeroFill(268), [55, 148, 128], zeroFill(54), [112]], 35231 44100: [highPrefix, [255, 192], zeroFill(268), [55, 163, 128], zeroFill(84), [112]], 35232 32000: [highPrefix, [255, 192], zeroFill(268), [55, 234], zeroFill(226), [112]], 35233 24000: [ 35234 highPrefix, 35235 [255, 192], 35236 zeroFill(268), 35237 [55, 255, 128], 35238 zeroFill(268), 35239 [111, 112], 35240 zeroFill(126), 35241 [224], 35242 ], 35243 16000: [ 35244 highPrefix, 35245 [255, 192], 35246 zeroFill(268), 35247 [55, 255, 128], 35248 zeroFill(268), 35249 [111, 255], 35250 zeroFill(269), 35251 [223, 108], 35252 zeroFill(195), 35253 [1, 192], 35254 ], 35255 12000: [ 35256 lowPrefix, 35257 zeroFill(268), 35258 [3, 127, 248], 35259 zeroFill(268), 35260 [6, 255, 240], 35261 zeroFill(268), 35262 [13, 255, 224], 35263 zeroFill(268), 35264 [27, 253, 128], 35265 zeroFill(259), 35266 [56], 35267 ], 35268 11025: [ 35269 lowPrefix, 35270 zeroFill(268), 35271 [3, 127, 248], 35272 zeroFill(268), 35273 [6, 255, 240], 35274 zeroFill(268), 35275 [13, 255, 224], 35276 zeroFill(268), 35277 [27, 255, 192], 35278 zeroFill(268), 35279 [55, 175, 128], 35280 zeroFill(108), 35281 [112], 35282 ], 35283 8000: [lowPrefix, zeroFill(268), [3, 121, 16], zeroFill(47), [7]], 35284 }; 35285 silence = makeTable(coneOfSilence); 35286 } 35287 return silence; 35288 }; 35289 /** 35290 * mux.js 35291 * 35292 * Copyright (c) Brightcove 35293 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 35294 */ 35295 35296 var ONE_SECOND_IN_TS$4 = 90000, 35297 // 90kHz clock 35298 secondsToVideoTs, 35299 secondsToAudioTs, 35300 videoTsToSeconds, 35301 audioTsToSeconds, 35302 audioTsToVideoTs, 35303 videoTsToAudioTs, 35304 metadataTsToSeconds; 35305 secondsToVideoTs = function (seconds) { 35306 return seconds * ONE_SECOND_IN_TS$4; 35307 }; 35308 secondsToAudioTs = function (seconds, sampleRate) { 35309 return seconds * sampleRate; 35310 }; 35311 videoTsToSeconds = function (timestamp) { 35312 return timestamp / ONE_SECOND_IN_TS$4; 35313 }; 35314 audioTsToSeconds = function (timestamp, sampleRate) { 35315 return timestamp / sampleRate; 35316 }; 35317 audioTsToVideoTs = function (timestamp, sampleRate) { 35318 return secondsToVideoTs(audioTsToSeconds(timestamp, sampleRate)); 35319 }; 35320 videoTsToAudioTs = function (timestamp, sampleRate) { 35321 return secondsToAudioTs(videoTsToSeconds(timestamp), sampleRate); 35322 }; 35323 /** 35324 * Adjust ID3 tag or caption timing information by the timeline pts values 35325 * (if keepOriginalTimestamps is false) and convert to seconds 35326 */ 35327 35328 metadataTsToSeconds = function (timestamp, timelineStartPts, keepOriginalTimestamps) { 35329 return videoTsToSeconds(keepOriginalTimestamps ? timestamp : timestamp - timelineStartPts); 35330 }; 35331 var clock$2 = { 35332 ONE_SECOND_IN_TS: ONE_SECOND_IN_TS$4, 35333 secondsToVideoTs: secondsToVideoTs, 35334 secondsToAudioTs: secondsToAudioTs, 35335 videoTsToSeconds: videoTsToSeconds, 35336 audioTsToSeconds: audioTsToSeconds, 35337 audioTsToVideoTs: audioTsToVideoTs, 35338 videoTsToAudioTs: videoTsToAudioTs, 35339 metadataTsToSeconds: metadataTsToSeconds, 35340 }; 35341 /** 35342 * mux.js 35343 * 35344 * Copyright (c) Brightcove 35345 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 35346 */ 35347 35348 var coneOfSilence = silence_1; 35349 var clock$1 = clock$2; 35350 /** 35351 * Sum the `byteLength` properties of the data in each AAC frame 35352 */ 35353 35354 var sumFrameByteLengths = function (array) { 35355 var i, 35356 currentObj, 35357 sum = 0; // sum the byteLength's all each nal unit in the frame 35358 35359 for (i = 0; i < array.length; i++) { 35360 currentObj = array[i]; 35361 sum += currentObj.data.byteLength; 35362 } 35363 return sum; 35364 }; // Possibly pad (prefix) the audio track with silence if appending this track 35365 // would lead to the introduction of a gap in the audio buffer 35366 35367 var prefixWithSilence = function (track, frames, audioAppendStartTs, videoBaseMediaDecodeTime) { 35368 var baseMediaDecodeTimeTs, 35369 frameDuration = 0, 35370 audioGapDuration = 0, 35371 audioFillFrameCount = 0, 35372 audioFillDuration = 0, 35373 silentFrame, 35374 i, 35375 firstFrame; 35376 if (!frames.length) { 35377 return; 35378 } 35379 baseMediaDecodeTimeTs = clock$1.audioTsToVideoTs(track.baseMediaDecodeTime, track.samplerate); // determine frame clock duration based on sample rate, round up to avoid overfills 35380 35381 frameDuration = Math.ceil(clock$1.ONE_SECOND_IN_TS / (track.samplerate / 1024)); 35382 if (audioAppendStartTs && videoBaseMediaDecodeTime) { 35383 // insert the shortest possible amount (audio gap or audio to video gap) 35384 audioGapDuration = 35385 baseMediaDecodeTimeTs - Math.max(audioAppendStartTs, videoBaseMediaDecodeTime); // number of full frames in the audio gap 35386 35387 audioFillFrameCount = Math.floor(audioGapDuration / frameDuration); 35388 audioFillDuration = audioFillFrameCount * frameDuration; 35389 } // don't attempt to fill gaps smaller than a single frame or larger 35390 // than a half second 35391 35392 if (audioFillFrameCount < 1 || audioFillDuration > clock$1.ONE_SECOND_IN_TS / 2) { 35393 return; 35394 } 35395 silentFrame = coneOfSilence()[track.samplerate]; 35396 if (!silentFrame) { 35397 // we don't have a silent frame pregenerated for the sample rate, so use a frame 35398 // from the content instead 35399 silentFrame = frames[0].data; 35400 } 35401 for (i = 0; i < audioFillFrameCount; i++) { 35402 firstFrame = frames[0]; 35403 frames.splice(0, 0, { 35404 data: silentFrame, 35405 dts: firstFrame.dts - frameDuration, 35406 pts: firstFrame.pts - frameDuration, 35407 }); 35408 } 35409 track.baseMediaDecodeTime -= Math.floor( 35410 clock$1.videoTsToAudioTs(audioFillDuration, track.samplerate) 35411 ); 35412 return audioFillDuration; 35413 }; // If the audio segment extends before the earliest allowed dts 35414 // value, remove AAC frames until starts at or after the earliest 35415 // allowed DTS so that we don't end up with a negative baseMedia- 35416 // DecodeTime for the audio track 35417 35418 var trimAdtsFramesByEarliestDts = function (adtsFrames, track, earliestAllowedDts) { 35419 if (track.minSegmentDts >= earliestAllowedDts) { 35420 return adtsFrames; 35421 } // We will need to recalculate the earliest segment Dts 35422 35423 track.minSegmentDts = Infinity; 35424 return adtsFrames.filter(function (currentFrame) { 35425 // If this is an allowed frame, keep it and record it's Dts 35426 if (currentFrame.dts >= earliestAllowedDts) { 35427 track.minSegmentDts = Math.min(track.minSegmentDts, currentFrame.dts); 35428 track.minSegmentPts = track.minSegmentDts; 35429 return true; 35430 } // Otherwise, discard it 35431 35432 return false; 35433 }); 35434 }; // generate the track's raw mdat data from an array of frames 35435 35436 var generateSampleTable = function (frames) { 35437 var i, 35438 currentFrame, 35439 samples = []; 35440 for (i = 0; i < frames.length; i++) { 35441 currentFrame = frames[i]; 35442 samples.push({ 35443 size: currentFrame.data.byteLength, 35444 duration: 1024, // For AAC audio, all samples contain 1024 samples 35445 }); 35446 } 35447 return samples; 35448 }; // generate the track's sample table from an array of frames 35449 35450 var concatenateFrameData = function (frames) { 35451 var i, 35452 currentFrame, 35453 dataOffset = 0, 35454 data = new Uint8Array(sumFrameByteLengths(frames)); 35455 for (i = 0; i < frames.length; i++) { 35456 currentFrame = frames[i]; 35457 data.set(currentFrame.data, dataOffset); 35458 dataOffset += currentFrame.data.byteLength; 35459 } 35460 return data; 35461 }; 35462 var audioFrameUtils$1 = { 35463 prefixWithSilence: prefixWithSilence, 35464 trimAdtsFramesByEarliestDts: trimAdtsFramesByEarliestDts, 35465 generateSampleTable: generateSampleTable, 35466 concatenateFrameData: concatenateFrameData, 35467 }; 35468 /** 35469 * mux.js 35470 * 35471 * Copyright (c) Brightcove 35472 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 35473 */ 35474 35475 var ONE_SECOND_IN_TS$3 = clock$2.ONE_SECOND_IN_TS; 35476 /** 35477 * Store information about the start and end of the track and the 35478 * duration for each frame/sample we process in order to calculate 35479 * the baseMediaDecodeTime 35480 */ 35481 35482 var collectDtsInfo = function (track, data) { 35483 if (typeof data.pts === 'number') { 35484 if (track.timelineStartInfo.pts === undefined) { 35485 track.timelineStartInfo.pts = data.pts; 35486 } 35487 if (track.minSegmentPts === undefined) { 35488 track.minSegmentPts = data.pts; 35489 } else { 35490 track.minSegmentPts = Math.min(track.minSegmentPts, data.pts); 35491 } 35492 if (track.maxSegmentPts === undefined) { 35493 track.maxSegmentPts = data.pts; 35494 } else { 35495 track.maxSegmentPts = Math.max(track.maxSegmentPts, data.pts); 35496 } 35497 } 35498 if (typeof data.dts === 'number') { 35499 if (track.timelineStartInfo.dts === undefined) { 35500 track.timelineStartInfo.dts = data.dts; 35501 } 35502 if (track.minSegmentDts === undefined) { 35503 track.minSegmentDts = data.dts; 35504 } else { 35505 track.minSegmentDts = Math.min(track.minSegmentDts, data.dts); 35506 } 35507 if (track.maxSegmentDts === undefined) { 35508 track.maxSegmentDts = data.dts; 35509 } else { 35510 track.maxSegmentDts = Math.max(track.maxSegmentDts, data.dts); 35511 } 35512 } 35513 }; 35514 /** 35515 * Clear values used to calculate the baseMediaDecodeTime between 35516 * tracks 35517 */ 35518 35519 var clearDtsInfo = function (track) { 35520 delete track.minSegmentDts; 35521 delete track.maxSegmentDts; 35522 delete track.minSegmentPts; 35523 delete track.maxSegmentPts; 35524 }; 35525 /** 35526 * Calculate the track's baseMediaDecodeTime based on the earliest 35527 * DTS the transmuxer has ever seen and the minimum DTS for the 35528 * current track 35529 * @param track {object} track metadata configuration 35530 * @param keepOriginalTimestamps {boolean} If true, keep the timestamps 35531 * in the source; false to adjust the first segment to start at 0. 35532 */ 35533 35534 var calculateTrackBaseMediaDecodeTime = function (track, keepOriginalTimestamps) { 35535 var baseMediaDecodeTime, 35536 scale, 35537 minSegmentDts = track.minSegmentDts; // Optionally adjust the time so the first segment starts at zero. 35538 35539 if (!keepOriginalTimestamps) { 35540 minSegmentDts -= track.timelineStartInfo.dts; 35541 } // track.timelineStartInfo.baseMediaDecodeTime is the location, in time, where 35542 // we want the start of the first segment to be placed 35543 35544 baseMediaDecodeTime = track.timelineStartInfo.baseMediaDecodeTime; // Add to that the distance this segment is from the very first 35545 35546 baseMediaDecodeTime += minSegmentDts; // baseMediaDecodeTime must not become negative 35547 35548 baseMediaDecodeTime = Math.max(0, baseMediaDecodeTime); 35549 if (track.type === 'audio') { 35550 // Audio has a different clock equal to the sampling_rate so we need to 35551 // scale the PTS values into the clock rate of the track 35552 scale = track.samplerate / ONE_SECOND_IN_TS$3; 35553 baseMediaDecodeTime *= scale; 35554 baseMediaDecodeTime = Math.floor(baseMediaDecodeTime); 35555 } 35556 return baseMediaDecodeTime; 35557 }; 35558 var trackDecodeInfo$1 = { 35559 clearDtsInfo: clearDtsInfo, 35560 calculateTrackBaseMediaDecodeTime: calculateTrackBaseMediaDecodeTime, 35561 collectDtsInfo: collectDtsInfo, 35562 }; 35563 /** 35564 * mux.js 35565 * 35566 * Copyright (c) Brightcove 35567 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 35568 * 35569 * Reads in-band caption information from a video elementary 35570 * stream. Captions must follow the CEA-708 standard for injection 35571 * into an MPEG-2 transport streams. 35572 * @see https://en.wikipedia.org/wiki/CEA-708 35573 * @see https://www.gpo.gov/fdsys/pkg/CFR-2007-title47-vol1/pdf/CFR-2007-title47-vol1-sec15-119.pdf 35574 */ 35575 // payload type field to indicate how they are to be 35576 // interpreted. CEAS-708 caption content is always transmitted with 35577 // payload type 0x04. 35578 35579 var USER_DATA_REGISTERED_ITU_T_T35 = 4, 35580 RBSP_TRAILING_BITS = 128; 35581 /** 35582 * Parse a supplemental enhancement information (SEI) NAL unit. 35583 * Stops parsing once a message of type ITU T T35 has been found. 35584 * 35585 * @param bytes {Uint8Array} the bytes of a SEI NAL unit 35586 * @return {object} the parsed SEI payload 35587 * @see Rec. ITU-T H.264, 7.3.2.3.1 35588 */ 35589 35590 var parseSei = function (bytes) { 35591 var i = 0, 35592 result = { 35593 payloadType: -1, 35594 payloadSize: 0, 35595 }, 35596 payloadType = 0, 35597 payloadSize = 0; // go through the sei_rbsp parsing each each individual sei_message 35598 35599 while (i < bytes.byteLength) { 35600 // stop once we have hit the end of the sei_rbsp 35601 if (bytes[i] === RBSP_TRAILING_BITS) { 35602 break; 35603 } // Parse payload type 35604 35605 while (bytes[i] === 0xff) { 35606 payloadType += 255; 35607 i++; 35608 } 35609 payloadType += bytes[i++]; // Parse payload size 35610 35611 while (bytes[i] === 0xff) { 35612 payloadSize += 255; 35613 i++; 35614 } 35615 payloadSize += bytes[i++]; // this sei_message is a 608/708 caption so save it and break 35616 // there can only ever be one caption message in a frame's sei 35617 35618 if (!result.payload && payloadType === USER_DATA_REGISTERED_ITU_T_T35) { 35619 var userIdentifier = String.fromCharCode( 35620 bytes[i + 3], 35621 bytes[i + 4], 35622 bytes[i + 5], 35623 bytes[i + 6] 35624 ); 35625 if (userIdentifier === 'GA94') { 35626 result.payloadType = payloadType; 35627 result.payloadSize = payloadSize; 35628 result.payload = bytes.subarray(i, i + payloadSize); 35629 break; 35630 } else { 35631 result.payload = void 0; 35632 } 35633 } // skip the payload and parse the next message 35634 35635 i += payloadSize; 35636 payloadType = 0; 35637 payloadSize = 0; 35638 } 35639 return result; 35640 }; // see ANSI/SCTE 128-1 (2013), section 8.1 35641 35642 var parseUserData = function (sei) { 35643 // itu_t_t35_contry_code must be 181 (United States) for 35644 // captions 35645 if (sei.payload[0] !== 181) { 35646 return null; 35647 } // itu_t_t35_provider_code should be 49 (ATSC) for captions 35648 35649 if (((sei.payload[1] << 8) | sei.payload[2]) !== 49) { 35650 return null; 35651 } // the user_identifier should be "GA94" to indicate ATSC1 data 35652 35653 if ( 35654 String.fromCharCode(sei.payload[3], sei.payload[4], sei.payload[5], sei.payload[6]) !== 35655 'GA94' 35656 ) { 35657 return null; 35658 } // finally, user_data_type_code should be 0x03 for caption data 35659 35660 if (sei.payload[7] !== 0x03) { 35661 return null; 35662 } // return the user_data_type_structure and strip the trailing 35663 // marker bits 35664 35665 return sei.payload.subarray(8, sei.payload.length - 1); 35666 }; // see CEA-708-D, section 4.4 35667 35668 var parseCaptionPackets = function (pts, userData) { 35669 var results = [], 35670 i, 35671 count, 35672 offset, 35673 data; // if this is just filler, return immediately 35674 35675 if (!(userData[0] & 0x40)) { 35676 return results; 35677 } // parse out the cc_data_1 and cc_data_2 fields 35678 35679 count = userData[0] & 0x1f; 35680 for (i = 0; i < count; i++) { 35681 offset = i * 3; 35682 data = { 35683 type: userData[offset + 2] & 0x03, 35684 pts: pts, 35685 }; // capture cc data when cc_valid is 1 35686 35687 if (userData[offset + 2] & 0x04) { 35688 data.ccData = (userData[offset + 3] << 8) | userData[offset + 4]; 35689 results.push(data); 35690 } 35691 } 35692 return results; 35693 }; 35694 var discardEmulationPreventionBytes$1 = function (data) { 35695 var length = data.byteLength, 35696 emulationPreventionBytesPositions = [], 35697 i = 1, 35698 newLength, 35699 newData; // Find all `Emulation Prevention Bytes` 35700 35701 while (i < length - 2) { 35702 if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0x03) { 35703 emulationPreventionBytesPositions.push(i + 2); 35704 i += 2; 35705 } else { 35706 i++; 35707 } 35708 } // If no Emulation Prevention Bytes were found just return the original 35709 // array 35710 35711 if (emulationPreventionBytesPositions.length === 0) { 35712 return data; 35713 } // Create a new array to hold the NAL unit data 35714 35715 newLength = length - emulationPreventionBytesPositions.length; 35716 newData = new Uint8Array(newLength); 35717 var sourceIndex = 0; 35718 for (i = 0; i < newLength; sourceIndex++, i++) { 35719 if (sourceIndex === emulationPreventionBytesPositions[0]) { 35720 // Skip this byte 35721 sourceIndex++; // Remove this position index 35722 35723 emulationPreventionBytesPositions.shift(); 35724 } 35725 newData[i] = data[sourceIndex]; 35726 } 35727 return newData; 35728 }; // exports 35729 35730 var captionPacketParser = { 35731 parseSei: parseSei, 35732 parseUserData: parseUserData, 35733 parseCaptionPackets: parseCaptionPackets, 35734 discardEmulationPreventionBytes: discardEmulationPreventionBytes$1, 35735 USER_DATA_REGISTERED_ITU_T_T35: USER_DATA_REGISTERED_ITU_T_T35, 35736 }; 35737 /** 35738 * mux.js 35739 * 35740 * Copyright (c) Brightcove 35741 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 35742 * 35743 * Reads in-band caption information from a video elementary 35744 * stream. Captions must follow the CEA-708 standard for injection 35745 * into an MPEG-2 transport streams. 35746 * @see https://en.wikipedia.org/wiki/CEA-708 35747 * @see https://www.gpo.gov/fdsys/pkg/CFR-2007-title47-vol1/pdf/CFR-2007-title47-vol1-sec15-119.pdf 35748 */ 35749 // Link To Transport 35750 // ----------------- 35751 35752 var Stream$7 = stream; 35753 var cea708Parser = captionPacketParser; 35754 var CaptionStream$2 = function (options) { 35755 options = options || {}; 35756 CaptionStream$2.prototype.init.call(this); // parse708captions flag, default to true 35757 35758 this.parse708captions_ = 35759 typeof options.parse708captions === 'boolean' ? options.parse708captions : true; 35760 this.captionPackets_ = []; 35761 this.ccStreams_ = [ 35762 new Cea608Stream(0, 0), 35763 // eslint-disable-line no-use-before-define 35764 new Cea608Stream(0, 1), 35765 // eslint-disable-line no-use-before-define 35766 new Cea608Stream(1, 0), 35767 // eslint-disable-line no-use-before-define 35768 new Cea608Stream(1, 1), // eslint-disable-line no-use-before-define 35769 ]; 35770 if (this.parse708captions_) { 35771 this.cc708Stream_ = new Cea708Stream({ 35772 captionServices: options.captionServices,
vendor: 5,343 bytes, lines 35772-35899
35772 35773 }); // eslint-disable-line no-use-before-define 35774 } 35775 this.reset(); // forward data and done events from CCs to this CaptionStream 35776 35777 this.ccStreams_.forEach(function (cc) { 35778 cc.on('data', this.trigger.bind(this, 'data')); 35779 cc.on('partialdone', this.trigger.bind(this, 'partialdone')); 35780 cc.on('done', this.trigger.bind(this, 'done')); 35781 }, this); 35782 if (this.parse708captions_) { 35783 this.cc708Stream_.on('data', this.trigger.bind(this, 'data')); 35784 this.cc708Stream_.on('partialdone', this.trigger.bind(this, 'partialdone')); 35785 this.cc708Stream_.on('done', this.trigger.bind(this, 'done')); 35786 } 35787 }; 35788 CaptionStream$2.prototype = new Stream$7(); 35789 CaptionStream$2.prototype.push = function (event) { 35790 var sei, userData, newCaptionPackets; // only examine SEI NALs 35791 35792 if (event.nalUnitType !== 'sei_rbsp') { 35793 return; 35794 } // parse the sei 35795 35796 sei = cea708Parser.parseSei(event.escapedRBSP); // no payload data, skip 35797 35798 if (!sei.payload) { 35799 return; 35800 } // ignore everything but user_data_registered_itu_t_t35 35801 35802 if (sei.payloadType !== cea708Parser.USER_DATA_REGISTERED_ITU_T_T35) { 35803 return; 35804 } // parse out the user data payload 35805 35806 userData = cea708Parser.parseUserData(sei); // ignore unrecognized userData 35807 35808 if (!userData) { 35809 return; 35810 } // Sometimes, the same segment # will be downloaded twice. To stop the 35811 // caption data from being processed twice, we track the latest dts we've 35812 // received and ignore everything with a dts before that. However, since 35813 // data for a specific dts can be split across packets on either side of 35814 // a segment boundary, we need to make sure we *don't* ignore the packets 35815 // from the *next* segment that have dts === this.latestDts_. By constantly 35816 // tracking the number of packets received with dts === this.latestDts_, we 35817 // know how many should be ignored once we start receiving duplicates. 35818 35819 if (event.dts < this.latestDts_) { 35820 // We've started getting older data, so set the flag. 35821 this.ignoreNextEqualDts_ = true; 35822 return; 35823 } else if (event.dts === this.latestDts_ && this.ignoreNextEqualDts_) { 35824 this.numSameDts_--; 35825 if (!this.numSameDts_) { 35826 // We've received the last duplicate packet, time to start processing again 35827 this.ignoreNextEqualDts_ = false; 35828 } 35829 return; 35830 } // parse out CC data packets and save them for later 35831 35832 newCaptionPackets = cea708Parser.parseCaptionPackets(event.pts, userData); 35833 this.captionPackets_ = this.captionPackets_.concat(newCaptionPackets); 35834 if (this.latestDts_ !== event.dts) { 35835 this.numSameDts_ = 0; 35836 } 35837 this.numSameDts_++; 35838 this.latestDts_ = event.dts; 35839 }; 35840 CaptionStream$2.prototype.flushCCStreams = function (flushType) { 35841 this.ccStreams_.forEach(function (cc) { 35842 return flushType === 'flush' ? cc.flush() : cc.partialFlush(); 35843 }, this); 35844 }; 35845 CaptionStream$2.prototype.flushStream = function (flushType) { 35846 // make sure we actually parsed captions before proceeding 35847 if (!this.captionPackets_.length) { 35848 this.flushCCStreams(flushType); 35849 return; 35850 } // In Chrome, the Array#sort function is not stable so add a 35851 // presortIndex that we can use to ensure we get a stable-sort 35852 35853 this.captionPackets_.forEach(function (elem, idx) { 35854 elem.presortIndex = idx; 35855 }); // sort caption byte-pairs based on their PTS values 35856 35857 this.captionPackets_.sort(function (a, b) { 35858 if (a.pts === b.pts) { 35859 return a.presortIndex - b.presortIndex; 35860 } 35861 return a.pts - b.pts; 35862 }); 35863 this.captionPackets_.forEach(function (packet) { 35864 if (packet.type < 2) { 35865 // Dispatch packet to the right Cea608Stream 35866 this.dispatchCea608Packet(packet); 35867 } else { 35868 // Dispatch packet to the Cea708Stream 35869 this.dispatchCea708Packet(packet); 35870 } 35871 }, this); 35872 this.captionPackets_.length = 0; 35873 this.flushCCStreams(flushType); 35874 }; 35875 CaptionStream$2.prototype.flush = function () { 35876 return this.flushStream('flush'); 35877 }; // Only called if handling partial data 35878 35879 CaptionStream$2.prototype.partialFlush = function () { 35880 return this.flushStream('partialFlush'); 35881 }; 35882 CaptionStream$2.prototype.reset = function () { 35883 this.latestDts_ = null; 35884 this.ignoreNextEqualDts_ = false; 35885 this.numSameDts_ = 0; 35886 this.activeCea608Channel_ = [null, null]; 35887 this.ccStreams_.forEach(function (ccStream) { 35888 ccStream.reset(); 35889 }); 35890 }; // From the CEA-608 spec: 35891 35892 /* 35893 * When XDS sub-packets are interleaved with other services, the end of each sub-packet shall be followed 35894 * by a control pair to change to a different service. When any of the control codes from 0x10 to 0x1F is 35895 * used to begin a control code pair, it indicates the return to captioning or Text data. The control code pair 35896 * and subsequent data should then be processed according to the FCC rules. It may be necessary for the 35897 * line 21 data encoder to automatically insert a control code pair (i.e. RCL, RU2, RU3, RU4, RDC, or RTD) 35898 * to switch to captioning or Text. 35899
35899 */ 35900 // With that in mind, we ignore any data between an XDS control code and a 35901 // subsequent closed-captioning control code. 35902 35903 CaptionStream$2.prototype.dispatchCea608Packet = function (packet) { 35904 // NOTE: packet.type is the CEA608 field 35905 if (this.setsTextOrXDSActive(packet)) { 35906 this.activeCea608Channel_[packet.type] = null; 35907 } else if (this.setsChannel1Active(packet)) { 35908 this.activeCea608Channel_[packet.type] = 0; 35909 } else if (this.setsChannel2Active(packet)) { 35910 this.activeCea608Channel_[packet.type] = 1; 35911 } 35912 if (this.activeCea608Channel_[packet.type] === null) { 35913 // If we haven't received anything to set the active channel, or the 35914 // packets are Text/XDS data, discard the data; we don't want jumbled 35915 // captions 35916 return; 35917 } 35918 this.ccStreams_[(packet.type << 1) + this.activeCea608Channel_[packet.type]].push(packet); 35919 }; 35920 CaptionStream$2.prototype.setsChannel1Active = function (packet) { 35921 return (packet.ccData & 0x7800) === 0x1000; 35922 }; 35923 CaptionStream$2.prototype.setsChannel2Active = function (packet) { 35924 return (packet.ccData & 0x7800) === 0x1800; 35925 }; 35926 CaptionStream$2.prototype.setsTextOrXDSActive = function (packet) { 35927 return ( 35928 (packet.ccData & 0x7100) === 0x0100 || 35929 (packet.ccData & 0x78fe) === 0x102a || 35930 (packet.ccData & 0x78fe) === 0x182a 35931 ); 35932 }; 35933 CaptionStream$2.prototype.dispatchCea708Packet = function (packet) { 35934 if (this.parse708captions_) { 35935 this.cc708Stream_.push(packet); 35936 } 35937 }; // ---------------------- 35938 // Session to Application 35939 // ---------------------- 35940 // This hash maps special and extended character codes to their 35941 // proper Unicode equivalent. The first one-byte key is just a 35942 // non-standard character code. The two-byte keys that follow are 35943 // the extended CEA708 character codes, along with the preceding 35944 // 0x10 extended character byte to distinguish these codes from 35945 // non-extended character codes. Every CEA708 character code that 35946 // is not in this object maps directly to a standard unicode 35947 // character code. 35948 // The transparent space and non-breaking transparent space are 35949 // technically not fully supported since there is no code to 35950 // make them transparent, so they have normal non-transparent 35951 // stand-ins. 35952 // The special closed caption (CC) character isn't a standard 35953 // unicode character, so a fairly similar unicode character was 35954 // chosen in it's place. 35955 35956 var CHARACTER_TRANSLATION_708 = { 35957 0x7f: 0x266a, 35958 // ⪠35959 0x1020: 0x20, 35960 // Transparent Space 35961 0x1021: 0xa0, 35962 // Nob-breaking Transparent Space 35963 0x1025: 0x2026, 35964 // ⦠35965 0x102a: 0x0160, 35966 // Å 35967 0x102c: 0x0152, 35968 // Å 35969 0x1030: 0x2588, 35970 // â 35971 0x1031: 0x2018, 35972 // â 35973 0x1032: 0x2019, 35974 // â 35975 0x1033: 0x201c, 35976 // â 35977 0x1034: 0x201d, 35978 // â 35979 0x1035: 0x2022, 35980 // ⢠35981 0x1039: 0x2122, 35982 // ⢠35983 0x103a: 0x0161, 35984 // Å¡ 35985 0x103c: 0x0153, 35986 // Å 35987 0x103d: 0x2120, 35988 // â 35989 0x103f: 0x0178, 35990 // Ÿ 35991 0x1076: 0x215b, 35992 // â 35993 0x1077: 0x215c, 35994 // â 35995 0x1078: 0x215d, 35996 // â 35997 0x1079: 0x215e, 35998 // â 35999 0x107a: 0x23d0, 36000 // â 36001 0x107b: 0x23a4, 36002 // ⤠36003 0x107c: 0x23a3, 36004 // ⣠36005 0x107d: 0x23af, 36006 // ⯠36007 0x107e: 0x23a6, 36008 // ⦠36009 0x107f: 0x23a1, 36010 // â¡ 36011 0x10a0: 0x3138, // 㸠(CC char) 36012 }; 36013 var get708CharFromCode = function (code) { 36014 var newCode = CHARACTER_TRANSLATION_708[code] || code; 36015 if (code & 0x1000 && code === newCode) { 36016 // Invalid extended code 36017 return ''; 36018 } 36019 return String.fromCharCode(newCode); 36020 }; 36021 var within708TextBlock = function (b) { 36022 return (0x20 <= b && b <= 0x7f) || (0xa0 <= b && b <= 0xff); 36023 }; 36024 var Cea708Window = function (windowNum) { 36025 this.windowNum = windowNum; 36026 this.reset(); 36027 }; 36028 Cea708Window.prototype.reset = function () { 36029 this.clearText(); 36030 this.pendingNewLine = false;
vendor: 26,050 bytes, lines 36031-36840
36031 this.winAttr = {}; 36032 this.penAttr = {}; 36033 this.penLoc = {}; 36034 this.penColor = {}; // These default values are arbitrary, 36035 // defineWindow will usually override them 36036 36037 this.visible = 0; 36038 this.rowLock = 0; 36039 this.columnLock = 0; 36040 this.priority = 0; 36041 this.relativePositioning = 0; 36042 this.anchorVertical = 0; 36043 this.anchorHorizontal = 0; 36044 this.anchorPoint = 0; 36045 this.rowCount = 1; 36046 this.virtualRowCount = this.rowCount + 1; 36047 this.columnCount = 41; 36048 this.windowStyle = 0; 36049 this.penStyle = 0; 36050 }; 36051 Cea708Window.prototype.getText = function () { 36052 return this.rows.join('\n'); 36053 }; 36054 Cea708Window.prototype.clearText = function () { 36055 this.rows = ['']; 36056 this.rowIdx = 0; 36057 }; 36058 Cea708Window.prototype.newLine = function (pts) { 36059 if ( 36060 this.rows.length >= this.virtualRowCount && 36061 typeof this.beforeRowOverflow === 'function' 36062 ) { 36063 this.beforeRowOverflow(pts); 36064 } 36065 if (this.rows.length > 0) { 36066 this.rows.push(''); 36067 this.rowIdx++; 36068 } // Show all virtual rows since there's no visible scrolling 36069 36070 while (this.rows.length > this.virtualRowCount) { 36071 this.rows.shift(); 36072 this.rowIdx--; 36073 } 36074 }; 36075 Cea708Window.prototype.isEmpty = function () { 36076 if (this.rows.length === 0) { 36077 return true; 36078 } else if (this.rows.length === 1) { 36079 return this.rows[0] === ''; 36080 } 36081 return false; 36082 }; 36083 Cea708Window.prototype.addText = function (text) { 36084 this.rows[this.rowIdx] += text; 36085 }; 36086 Cea708Window.prototype.backspace = function () { 36087 if (!this.isEmpty()) { 36088 var row = this.rows[this.rowIdx]; 36089 this.rows[this.rowIdx] = row.substr(0, row.length - 1); 36090 } 36091 }; 36092 var Cea708Service = function (serviceNum, encoding, stream) { 36093 this.serviceNum = serviceNum; 36094 this.text = ''; 36095 this.currentWindow = new Cea708Window(-1); 36096 this.windows = []; 36097 this.stream = stream; // Try to setup a TextDecoder if an `encoding` value was provided 36098 36099 if (typeof encoding === 'string') { 36100 this.createTextDecoder(encoding); 36101 } 36102 }; 36103 /** 36104 * Initialize service windows 36105 * Must be run before service use 36106 * 36107 * @param {Integer} pts PTS value 36108 * @param {Function} beforeRowOverflow Function to execute before row overflow of a window 36109 */ 36110 36111 Cea708Service.prototype.init = function (pts, beforeRowOverflow) { 36112 this.startPts = pts; 36113 for (var win = 0; win < 8; win++) { 36114 this.windows[win] = new Cea708Window(win); 36115 if (typeof beforeRowOverflow === 'function') { 36116 this.windows[win].beforeRowOverflow = beforeRowOverflow; 36117 } 36118 } 36119 }; 36120 /** 36121 * Set current window of service to be affected by commands 36122 * 36123 * @param {Integer} windowNum Window number 36124 */ 36125 36126 Cea708Service.prototype.setCurrentWindow = function (windowNum) { 36127 this.currentWindow = this.windows[windowNum]; 36128 }; 36129 /** 36130 * Try to create a TextDecoder if it is natively supported 36131 */ 36132 36133 Cea708Service.prototype.createTextDecoder = function (encoding) { 36134 if (typeof TextDecoder === 'undefined') { 36135 this.stream.trigger('log', { 36136 level: 'warn', 36137 message: 'The `encoding` option is unsupported without TextDecoder support', 36138 }); 36139 } else { 36140 try { 36141 this.textDecoder_ = new TextDecoder(encoding); 36142 } catch (error) { 36143 this.stream.trigger('log', { 36144 level: 'warn', 36145 message: 'TextDecoder could not be created with ' + encoding + ' encoding. ' + error, 36146 }); 36147 } 36148 } 36149 }; 36150 var Cea708Stream = function (options) { 36151 options = options || {}; 36152 Cea708Stream.prototype.init.call(this); 36153 var self = this; 36154 var captionServices = options.captionServices || {}; 36155 var captionServiceEncodings = {}; 36156 var serviceProps; // Get service encodings from captionServices option block 36157 36158 Object.keys(captionServices).forEach((serviceName) => { 36159 serviceProps = captionServices[serviceName]; 36160 if (/^SERVICE/.test(serviceName)) { 36161 captionServiceEncodings[serviceName] = serviceProps.encoding; 36162 } 36163 }); 36164 this.serviceEncodings = captionServiceEncodings; 36165 this.current708Packet = null; 36166 this.services = {}; 36167 this.push = function (packet) { 36168 if (packet.type === 3) { 36169 // 708 packet start 36170 self.new708Packet(); 36171 self.add708Bytes(packet); 36172 } else { 36173 if (self.current708Packet === null) { 36174 // This should only happen at the start of a file if there's no packet start. 36175 self.new708Packet(); 36176 } 36177 self.add708Bytes(packet); 36178 } 36179 }; 36180 }; 36181 Cea708Stream.prototype = new Stream$7(); 36182 /** 36183 * Push current 708 packet, create new 708 packet. 36184 */ 36185 36186 Cea708Stream.prototype.new708Packet = function () { 36187 if (this.current708Packet !== null) { 36188 this.push708Packet(); 36189 } 36190 this.current708Packet = { 36191 data: [], 36192 ptsVals: [], 36193 }; 36194 }; 36195 /** 36196 * Add pts and both bytes from packet into current 708 packet. 36197 */ 36198 36199 Cea708Stream.prototype.add708Bytes = function (packet) { 36200 var data = packet.ccData; 36201 var byte0 = data >>> 8; 36202 var byte1 = data & 0xff; // I would just keep a list of packets instead of bytes, but it isn't clear in the spec 36203 // that service blocks will always line up with byte pairs. 36204 36205 this.current708Packet.ptsVals.push(packet.pts); 36206 this.current708Packet.data.push(byte0); 36207 this.current708Packet.data.push(byte1); 36208 }; 36209 /** 36210 * Parse completed 708 packet into service blocks and push each service block. 36211 */ 36212 36213 Cea708Stream.prototype.push708Packet = function () { 36214 var packet708 = this.current708Packet; 36215 var packetData = packet708.data; 36216 var serviceNum = null; 36217 var blockSize = null; 36218 var i = 0; 36219 var b = packetData[i++]; 36220 packet708.seq = b >> 6; 36221 packet708.sizeCode = b & 0x3f; // 0b00111111; 36222 36223 for (; i < packetData.length; i++) { 36224 b = packetData[i++]; 36225 serviceNum = b >> 5; 36226 blockSize = b & 0x1f; // 0b00011111 36227 36228 if (serviceNum === 7 && blockSize > 0) { 36229 // Extended service num 36230 b = packetData[i++]; 36231 serviceNum = b; 36232 } 36233 this.pushServiceBlock(serviceNum, i, blockSize); 36234 if (blockSize > 0) { 36235 i += blockSize - 1; 36236 } 36237 } 36238 }; 36239 /** 36240 * Parse service block, execute commands, read text. 36241 * 36242 * Note: While many of these commands serve important purposes, 36243 * many others just parse out the parameters or attributes, but 36244 * nothing is done with them because this is not a full and complete 36245 * implementation of the entire 708 spec. 36246 * 36247 * @param {Integer} serviceNum Service number 36248 * @param {Integer} start Start index of the 708 packet data 36249 * @param {Integer} size Block size 36250 */ 36251 36252 Cea708Stream.prototype.pushServiceBlock = function (serviceNum, start, size) { 36253 var b; 36254 var i = start; 36255 var packetData = this.current708Packet.data; 36256 var service = this.services[serviceNum]; 36257 if (!service) { 36258 service = this.initService(serviceNum, i); 36259 } 36260 for (; i < start + size && i < packetData.length; i++) { 36261 b = packetData[i]; 36262 if (within708TextBlock(b)) { 36263 i = this.handleText(i, service); 36264 } else if (b === 0x18) { 36265 i = this.multiByteCharacter(i, service); 36266 } else if (b === 0x10) { 36267 i = this.extendedCommands(i, service); 36268 } else if (0x80 <= b && b <= 0x87) { 36269 i = this.setCurrentWindow(i, service); 36270 } else if (0x98 <= b && b <= 0x9f) { 36271 i = this.defineWindow(i, service); 36272 } else if (b === 0x88) { 36273 i = this.clearWindows(i, service); 36274 } else if (b === 0x8c) { 36275 i = this.deleteWindows(i, service); 36276 } else if (b === 0x89) { 36277 i = this.displayWindows(i, service); 36278 } else if (b === 0x8a) { 36279 i = this.hideWindows(i, service); 36280 } else if (b === 0x8b) { 36281 i = this.toggleWindows(i, service); 36282 } else if (b === 0x97) { 36283 i = this.setWindowAttributes(i, service); 36284 } else if (b === 0x90) { 36285 i = this.setPenAttributes(i, service); 36286 } else if (b === 0x91) { 36287 i = this.setPenColor(i, service); 36288 } else if (b === 0x92) { 36289 i = this.setPenLocation(i, service); 36290 } else if (b === 0x8f) { 36291 service = this.reset(i, service); 36292 } else if (b === 0x08) { 36293 // BS: Backspace 36294 service.currentWindow.backspace(); 36295 } else if (b === 0x0c) { 36296 // FF: Form feed 36297 service.currentWindow.clearText(); 36298 } else if (b === 0x0d) { 36299 // CR: Carriage return 36300 service.currentWindow.pendingNewLine = true; 36301 } else if (b === 0x0e) { 36302 // HCR: Horizontal carriage return 36303 service.currentWindow.clearText(); 36304 } else if (b === 0x8d) { 36305 // DLY: Delay, nothing to do 36306 i++; 36307 } else; 36308 } 36309 }; 36310 /** 36311 * Execute an extended command 36312 * 36313 * @param {Integer} i Current index in the 708 packet 36314 * @param {Service} service The service object to be affected 36315 * @return {Integer} New index after parsing 36316 */ 36317 36318 Cea708Stream.prototype.extendedCommands = function (i, service) { 36319 var packetData = this.current708Packet.data; 36320 var b = packetData[++i]; 36321 if (within708TextBlock(b)) { 36322 i = this.handleText(i, service, { 36323 isExtended: true, 36324 }); 36325 } 36326 return i; 36327 }; 36328 /** 36329 * Get PTS value of a given byte index 36330 * 36331 * @param {Integer} byteIndex Index of the byte 36332 * @return {Integer} PTS 36333 */ 36334 36335 Cea708Stream.prototype.getPts = function (byteIndex) { 36336 // There's 1 pts value per 2 bytes 36337 return this.current708Packet.ptsVals[Math.floor(byteIndex / 2)]; 36338 }; 36339 /** 36340 * Initializes a service 36341 * 36342 * @param {Integer} serviceNum Service number 36343 * @return {Service} Initialized service object 36344 */ 36345 36346 Cea708Stream.prototype.initService = function (serviceNum, i) { 36347 var serviceName = 'SERVICE' + serviceNum; 36348 var self = this; 36349 var serviceName; 36350 var encoding; 36351 if (serviceName in this.serviceEncodings) { 36352 encoding = this.serviceEncodings[serviceName]; 36353 } 36354 this.services[serviceNum] = new Cea708Service(serviceNum, encoding, self); 36355 this.services[serviceNum].init(this.getPts(i), function (pts) { 36356 self.flushDisplayed(pts, self.services[serviceNum]); 36357 }); 36358 return this.services[serviceNum]; 36359 }; 36360 /** 36361 * Execute text writing to current window 36362 * 36363 * @param {Integer} i Current index in the 708 packet 36364 * @param {Service} service The service object to be affected 36365 * @return {Integer} New index after parsing 36366 */ 36367 36368 Cea708Stream.prototype.handleText = function (i, service, options) { 36369 var isExtended = options && options.isExtended; 36370 var isMultiByte = options && options.isMultiByte; 36371 var packetData = this.current708Packet.data; 36372 var extended = isExtended ? 0x1000 : 0x0000; 36373 var currentByte = packetData[i]; 36374 var nextByte = packetData[i + 1]; 36375 var win = service.currentWindow; 36376 var char; 36377 var charCodeArray; // Converts an array of bytes to a unicode hex string. 36378 36379 function toHexString(byteArray) { 36380 return byteArray 36381 .map((byte) => { 36382 return ('0' + (byte & 0xff).toString(16)).slice(-2); 36383 }) 36384 .join(''); 36385 } 36386 if (isMultiByte) { 36387 charCodeArray = [currentByte, nextByte]; 36388 i++; 36389 } else { 36390 charCodeArray = [currentByte]; 36391 } // Use the TextDecoder if one was created for this service 36392 36393 if (service.textDecoder_ && !isExtended) { 36394 char = service.textDecoder_.decode(new Uint8Array(charCodeArray)); 36395 } else { 36396 // We assume any multi-byte char without a decoder is unicode. 36397 if (isMultiByte) { 36398 const unicode = toHexString(charCodeArray); // Takes a unicode hex string and creates a single character. 36399 36400 char = String.fromCharCode(parseInt(unicode, 16)); 36401 } else { 36402 char = get708CharFromCode(extended | currentByte); 36403 } 36404 } 36405 if (win.pendingNewLine && !win.isEmpty()) { 36406 win.newLine(this.getPts(i)); 36407 } 36408 win.pendingNewLine = false; 36409 win.addText(char); 36410 return i; 36411 }; 36412 /** 36413 * Handle decoding of multibyte character 36414 * 36415 * @param {Integer} i Current index in the 708 packet 36416 * @param {Service} service The service object to be affected 36417 * @return {Integer} New index after parsing 36418 */ 36419 36420 Cea708Stream.prototype.multiByteCharacter = function (i, service) { 36421 var packetData = this.current708Packet.data; 36422 var firstByte = packetData[i + 1]; 36423 var secondByte = packetData[i + 2]; 36424 if (within708TextBlock(firstByte) && within708TextBlock(secondByte)) { 36425 i = this.handleText(++i, service, { 36426 isMultiByte: true, 36427 }); 36428 } 36429 return i; 36430 }; 36431 /** 36432 * Parse and execute the CW# command. 36433 * 36434 * Set the current window. 36435 * 36436 * @param {Integer} i Current index in the 708 packet 36437 * @param {Service} service The service object to be affected 36438 * @return {Integer} New index after parsing 36439 */ 36440 36441 Cea708Stream.prototype.setCurrentWindow = function (i, service) { 36442 var packetData = this.current708Packet.data; 36443 var b = packetData[i]; 36444 var windowNum = b & 0x07; 36445 service.setCurrentWindow(windowNum); 36446 return i; 36447 }; 36448 /** 36449 * Parse and execute the DF# command. 36450 * 36451 * Define a window and set it as the current window. 36452 * 36453 * @param {Integer} i Current index in the 708 packet 36454 * @param {Service} service The service object to be affected 36455 * @return {Integer} New index after parsing 36456 */ 36457 36458 Cea708Stream.prototype.defineWindow = function (i, service) { 36459 var packetData = this.current708Packet.data; 36460 var b = packetData[i]; 36461 var windowNum = b & 0x07; 36462 service.setCurrentWindow(windowNum); 36463 var win = service.currentWindow; 36464 b = packetData[++i]; 36465 win.visible = (b & 0x20) >> 5; // v 36466 36467 win.rowLock = (b & 0x10) >> 4; // rl 36468 36469 win.columnLock = (b & 0x08) >> 3; // cl 36470 36471 win.priority = b & 0x07; // p 36472 36473 b = packetData[++i]; 36474 win.relativePositioning = (b & 0x80) >> 7; // rp 36475 36476 win.anchorVertical = b & 0x7f; // av 36477 36478 b = packetData[++i]; 36479 win.anchorHorizontal = b; // ah 36480 36481 b = packetData[++i]; 36482 win.anchorPoint = (b & 0xf0) >> 4; // ap 36483 36484 win.rowCount = b & 0x0f; // rc 36485 36486 b = packetData[++i]; 36487 win.columnCount = b & 0x3f; // cc 36488 36489 b = packetData[++i]; 36490 win.windowStyle = (b & 0x38) >> 3; // ws 36491 36492 win.penStyle = b & 0x07; // ps 36493 // The spec says there are (rowCount+1) "virtual rows" 36494 36495 win.virtualRowCount = win.rowCount + 1; 36496 return i; 36497 }; 36498 /** 36499 * Parse and execute the SWA command. 36500 * 36501 * Set attributes of the current window. 36502 * 36503 * @param {Integer} i Current index in the 708 packet 36504 * @param {Service} service The service object to be affected 36505 * @return {Integer} New index after parsing 36506 */ 36507 36508 Cea708Stream.prototype.setWindowAttributes = function (i, service) { 36509 var packetData = this.current708Packet.data; 36510 var b = packetData[i]; 36511 var winAttr = service.currentWindow.winAttr; 36512 b = packetData[++i]; 36513 winAttr.fillOpacity = (b & 0xc0) >> 6; // fo 36514 36515 winAttr.fillRed = (b & 0x30) >> 4; // fr 36516 36517 winAttr.fillGreen = (b & 0x0c) >> 2; // fg 36518 36519 winAttr.fillBlue = b & 0x03; // fb 36520 36521 b = packetData[++i]; 36522 winAttr.borderType = (b & 0xc0) >> 6; // bt 36523 36524 winAttr.borderRed = (b & 0x30) >> 4; // br 36525 36526 winAttr.borderGreen = (b & 0x0c) >> 2; // bg 36527 36528 winAttr.borderBlue = b & 0x03; // bb 36529 36530 b = packetData[++i]; 36531 winAttr.borderType += (b & 0x80) >> 5; // bt 36532 36533 winAttr.wordWrap = (b & 0x40) >> 6; // ww 36534 36535 winAttr.printDirection = (b & 0x30) >> 4; // pd 36536 36537 winAttr.scrollDirection = (b & 0x0c) >> 2; // sd 36538 36539 winAttr.justify = b & 0x03; // j 36540 36541 b = packetData[++i]; 36542 winAttr.effectSpeed = (b & 0xf0) >> 4; // es 36543 36544 winAttr.effectDirection = (b & 0x0c) >> 2; // ed 36545 36546 winAttr.displayEffect = b & 0x03; // de 36547 36548 return i; 36549 }; 36550 /** 36551 * Gather text from all displayed windows and push a caption to output. 36552 * 36553 * @param {Integer} i Current index in the 708 packet 36554 * @param {Service} service The service object to be affected 36555 */ 36556 36557 Cea708Stream.prototype.flushDisplayed = function (pts, service) { 36558 var displayedText = []; // TODO: Positioning not supported, displaying multiple windows will not necessarily 36559 // display text in the correct order, but sample files so far have not shown any issue. 36560 36561 for (var winId = 0; winId < 8; winId++) { 36562 if (service.windows[winId].visible && !service.windows[winId].isEmpty()) { 36563 displayedText.push(service.windows[winId].getText()); 36564 } 36565 } 36566 service.endPts = pts; 36567 service.text = displayedText.join('\n\n'); 36568 this.pushCaption(service); 36569 service.startPts = pts; 36570 }; 36571 /** 36572 * Push a caption to output if the caption contains text. 36573 * 36574 * @param {Service} service The service object to be affected 36575 */ 36576 36577 Cea708Stream.prototype.pushCaption = function (service) { 36578 if (service.text !== '') { 36579 this.trigger('data', { 36580 startPts: service.startPts, 36581 endPts: service.endPts, 36582 text: service.text, 36583 stream: 'cc708_' + service.serviceNum, 36584 }); 36585 service.text = ''; 36586 service.startPts = service.endPts; 36587 } 36588 }; 36589 /** 36590 * Parse and execute the DSW command. 36591 * 36592 * Set visible property of windows based on the parsed bitmask. 36593 * 36594 * @param {Integer} i Current index in the 708 packet 36595 * @param {Service} service The service object to be affected 36596 * @return {Integer} New index after parsing 36597 */ 36598 36599 Cea708Stream.prototype.displayWindows = function (i, service) { 36600 var packetData = this.current708Packet.data; 36601 var b = packetData[++i]; 36602 var pts = this.getPts(i); 36603 this.flushDisplayed(pts, service); 36604 for (var winId = 0; winId < 8; winId++) { 36605 if (b & (0x01 << winId)) { 36606 service.windows[winId].visible = 1; 36607 } 36608 } 36609 return i; 36610 }; 36611 /** 36612 * Parse and execute the HDW command. 36613 * 36614 * Set visible property of windows based on the parsed bitmask. 36615 * 36616 * @param {Integer} i Current index in the 708 packet 36617 * @param {Service} service The service object to be affected 36618 * @return {Integer} New index after parsing 36619 */ 36620 36621 Cea708Stream.prototype.hideWindows = function (i, service) { 36622 var packetData = this.current708Packet.data; 36623 var b = packetData[++i]; 36624 var pts = this.getPts(i); 36625 this.flushDisplayed(pts, service); 36626 for (var winId = 0; winId < 8; winId++) { 36627 if (b & (0x01 << winId)) { 36628 service.windows[winId].visible = 0; 36629 } 36630 } 36631 return i; 36632 }; 36633 /** 36634 * Parse and execute the TGW command. 36635 * 36636 * Set visible property of windows based on the parsed bitmask. 36637 * 36638 * @param {Integer} i Current index in the 708 packet 36639 * @param {Service} service The service object to be affected 36640 * @return {Integer} New index after parsing 36641 */ 36642 36643 Cea708Stream.prototype.toggleWindows = function (i, service) { 36644 var packetData = this.current708Packet.data; 36645 var b = packetData[++i]; 36646 var pts = this.getPts(i); 36647 this.flushDisplayed(pts, service); 36648 for (var winId = 0; winId < 8; winId++) { 36649 if (b & (0x01 << winId)) { 36650 service.windows[winId].visible ^= 1; 36651 } 36652 } 36653 return i; 36654 }; 36655 /** 36656 * Parse and execute the CLW command. 36657 * 36658 * Clear text of windows based on the parsed bitmask. 36659 * 36660 * @param {Integer} i Current index in the 708 packet 36661 * @param {Service} service The service object to be affected 36662 * @return {Integer} New index after parsing 36663 */ 36664 36665 Cea708Stream.prototype.clearWindows = function (i, service) { 36666 var packetData = this.current708Packet.data; 36667 var b = packetData[++i]; 36668 var pts = this.getPts(i); 36669 this.flushDisplayed(pts, service); 36670 for (var winId = 0; winId < 8; winId++) { 36671 if (b & (0x01 << winId)) { 36672 service.windows[winId].clearText(); 36673 } 36674 } 36675 return i; 36676 }; 36677 /** 36678 * Parse and execute the DLW command. 36679 * 36680 * Re-initialize windows based on the parsed bitmask. 36681 * 36682 * @param {Integer} i Current index in the 708 packet 36683 * @param {Service} service The service object to be affected 36684 * @return {Integer} New index after parsing 36685 */ 36686 36687 Cea708Stream.prototype.deleteWindows = function (i, service) { 36688 var packetData = this.current708Packet.data; 36689 var b = packetData[++i]; 36690 var pts = this.getPts(i); 36691 this.flushDisplayed(pts, service); 36692 for (var winId = 0; winId < 8; winId++) { 36693 if (b & (0x01 << winId)) { 36694 service.windows[winId].reset(); 36695 } 36696 } 36697 return i; 36698 }; 36699 /** 36700 * Parse and execute the SPA command. 36701 * 36702 * Set pen attributes of the current window. 36703 * 36704 * @param {Integer} i Current index in the 708 packet 36705 * @param {Service} service The service object to be affected 36706 * @return {Integer} New index after parsing 36707 */ 36708 36709 Cea708Stream.prototype.setPenAttributes = function (i, service) { 36710 var packetData = this.current708Packet.data; 36711 var b = packetData[i]; 36712 var penAttr = service.currentWindow.penAttr; 36713 b = packetData[++i]; 36714 penAttr.textTag = (b & 0xf0) >> 4; // tt 36715 36716 penAttr.offset = (b & 0x0c) >> 2; // o 36717 36718 penAttr.penSize = b & 0x03; // s 36719 36720 b = packetData[++i]; 36721 penAttr.italics = (b & 0x80) >> 7; // i 36722 36723 penAttr.underline = (b & 0x40) >> 6; // u 36724 36725 penAttr.edgeType = (b & 0x38) >> 3; // et 36726 36727 penAttr.fontStyle = b & 0x07; // fs 36728 36729 return i; 36730 }; 36731 /** 36732 * Parse and execute the SPC command. 36733 * 36734 * Set pen color of the current window. 36735 * 36736 * @param {Integer} i Current index in the 708 packet 36737 * @param {Service} service The service object to be affected 36738 * @return {Integer} New index after parsing 36739 */ 36740 36741 Cea708Stream.prototype.setPenColor = function (i, service) { 36742 var packetData = this.current708Packet.data; 36743 var b = packetData[i]; 36744 var penColor = service.currentWindow.penColor; 36745 b = packetData[++i]; 36746 penColor.fgOpacity = (b & 0xc0) >> 6; // fo 36747 36748 penColor.fgRed = (b & 0x30) >> 4; // fr 36749 36750 penColor.fgGreen = (b & 0x0c) >> 2; // fg 36751 36752 penColor.fgBlue = b & 0x03; // fb 36753 36754 b = packetData[++i]; 36755 penColor.bgOpacity = (b & 0xc0) >> 6; // bo 36756 36757 penColor.bgRed = (b & 0x30) >> 4; // br 36758 36759 penColor.bgGreen = (b & 0x0c) >> 2; // bg 36760 36761 penColor.bgBlue = b & 0x03; // bb 36762 36763 b = packetData[++i]; 36764 penColor.edgeRed = (b & 0x30) >> 4; // er 36765 36766 penColor.edgeGreen = (b & 0x0c) >> 2; // eg 36767 36768 penColor.edgeBlue = b & 0x03; // eb 36769 36770 return i; 36771 }; 36772 /** 36773 * Parse and execute the SPL command. 36774 * 36775 * Set pen location of the current window. 36776 * 36777 * @param {Integer} i Current index in the 708 packet 36778 * @param {Service} service The service object to be affected 36779 * @return {Integer} New index after parsing 36780 */ 36781 36782 Cea708Stream.prototype.setPenLocation = function (i, service) { 36783 var packetData = this.current708Packet.data; 36784 var b = packetData[i]; 36785 var penLoc = service.currentWindow.penLoc; // Positioning isn't really supported at the moment, so this essentially just inserts a linebreak 36786 36787 service.currentWindow.pendingNewLine = true; 36788 b = packetData[++i]; 36789 penLoc.row = b & 0x0f; // r 36790 36791 b = packetData[++i]; 36792 penLoc.column = b & 0x3f; // c 36793 36794 return i; 36795 }; 36796 /** 36797 * Execute the RST command. 36798 * 36799 * Reset service to a clean slate. Re-initialize. 36800 * 36801 * @param {Integer} i Current index in the 708 packet 36802 * @param {Service} service The service object to be affected 36803 * @return {Service} Re-initialized service 36804 */ 36805 36806 Cea708Stream.prototype.reset = function (i, service) { 36807 var pts = this.getPts(i); 36808 this.flushDisplayed(pts, service); 36809 return this.initService(service.serviceNum, i); 36810 }; // This hash maps non-ASCII, special, and extended character codes to their 36811 // proper Unicode equivalent. The first keys that are only a single byte 36812 // are the non-standard ASCII characters, which simply map the CEA608 byte 36813 // to the standard ASCII/Unicode. The two-byte keys that follow are the CEA608 36814 // character codes, but have their MSB bitmasked with 0x03 so that a lookup 36815 // can be performed regardless of the field and data channel on which the 36816 // character code was received. 36817 36818 var CHARACTER_TRANSLATION = { 36819 0x2a: 0xe1, 36820 // á 36821 0x5c: 0xe9, 36822 // é 36823 0x5e: 0xed, 36824 // à 36825 0x5f: 0xf3, 36826 // ó 36827 0x60: 0xfa, 36828 // ú 36829 0x7b: 0xe7, 36830 // ç 36831 0x7c: 0xf7, 36832 // ÷ 36833 0x7d: 0xd1, 36834 // à 36835 0x7e: 0xf1, 36836 // ñ 36837 0x7f: 0x2588, 36838 // â 36839 0x0130: 0xae, 36840 // ®
36841 0x0131: 0xb0, 36842 // ° 36843 0x0132: 0xbd, 36844 // ½ 36845 0x0133: 0xbf, 36846 // ¿ 36847 0x0134: 0x2122, 36848 // ⢠36849 0x0135: 0xa2, 36850 // ¢ 36851 0x0136: 0xa3, 36852 // £ 36853 0x0137: 0x266a, 36854 // ⪠36855 0x0138: 0xe0, 36856 // à 36857 0x0139: 0xa0, 36858 // 36859 0x013a: 0xe8, 36860 // è 36861 0x013b: 0xe2, 36862 // â 36863 0x013c: 0xea, 36864 // ê 36865 0x013d: 0xee, 36866 // î 36867 0x013e: 0xf4, 36868 // ô 36869 0x013f: 0xfb, 36870 // û 36871 0x0220: 0xc1, 36872 // à 36873 0x0221: 0xc9, 36874 // à 36875 0x0222: 0xd3, 36876 // à 36877 0x0223: 0xda, 36878 // à 36879 0x0224: 0xdc, 36880 // à 36881 0x0225: 0xfc, 36882 // ü 36883 0x0226: 0x2018, 36884 // â 36885 0x0227: 0xa1, 36886 // ¡ 36887 0x0228: 0x2a, 36888 // * 36889 0x0229: 0x27, 36890 // ' 36891 0x022a: 0x2014, 36892 // â 36893 0x022b: 0xa9, 36894 // © 36895 0x022c: 0x2120, 36896 // â 36897 0x022d: 0x2022, 36898 // ⢠36899 0x022e: 0x201c, 36900 // â 36901 0x022f: 0x201d, 36902 // â 36903 0x0230: 0xc0, 36904 // à 36905 0x0231: 0xc2, 36906 // à 36907 0x0232: 0xc7, 36908 // à 36909 0x0233: 0xc8, 36910 // à 36911 0x0234: 0xca, 36912 // à 36913 0x0235: 0xcb, 36914 // à 36915 0x0236: 0xeb, 36916 // ë 36917 0x0237: 0xce, 36918 // à 36919 0x0238: 0xcf, 36920 // à 36921 0x0239: 0xef, 36922 // ï 36923 0x023a: 0xd4, 36924 // à 36925 0x023b: 0xd9, 36926 // à 36927 0x023c: 0xf9, 36928 // ù 36929 0x023d: 0xdb, 36930 // à 36931 0x023e: 0xab, 36932 // « 36933 0x023f: 0xbb, 36934 // » 36935 0x0320: 0xc3, 36936 // à 36937 0x0321: 0xe3, 36938 // ã 36939 0x0322: 0xcd, 36940 // à 36941 0x0323: 0xcc, 36942 // à 36943 0x0324: 0xec, 36944 // ì 36945 0x0325: 0xd2, 36946 // à 36947 0x0326: 0xf2, 36948 // ò 36949 0x0327: 0xd5, 36950 // à 36951 0x0328: 0xf5, 36952 // õ 36953 0x0329: 0x7b, 36954 // { 36955 0x032a: 0x7d, 36956 // } 36957 0x032b: 0x5c, 36958 // \ 36959 0x032c: 0x5e, 36960 // ^ 36961 0x032d: 0x5f, 36962 // _ 36963 0x032e: 0x7c, 36964 // | 36965 0x032f: 0x7e, 36966 // ~ 36967 0x0330: 0xc4, 36968 // à 36969 0x0331: 0xe4, 36970 // ä 36971 0x0332: 0xd6, 36972 // à 36973 0x0333: 0xf6, 36974 // ö 36975 0x0334: 0xdf, 36976 // à 36977 0x0335: 0xa5, 36978 // Â¥ 36979 0x0336: 0xa4, 36980 // ¤ 36981 0x0337: 0x2502, 36982 // â 36983 0x0338: 0xc5, 36984 // à 36985 0x0339: 0xe5, 36986 // Ã¥ 36987 0x033a: 0xd8, 36988 // à 36989 0x033b: 0xf8, 36990 // ø 36991 0x033c: 0x250c, 36992 // â 36993 0x033d: 0x2510, 36994 // â 36995 0x033e: 0x2514, 36996 // â 36997 0x033f: 0x2518, // â 36998 }; 36999 var getCharFromCode = function (code) { 37000 if (code === null) { 37001 return ''; 37002 } 37003 code = CHARACTER_TRANSLATION[code] || code; 37004 return String.fromCharCode(code); 37005 }; // the index of the last row in a CEA-608 display buffer 37006 37007 var BOTTOM_ROW = 14; // This array is used for mapping PACs -> row #, since there's no way of 37008 // getting it through bit logic. 37009 37010 var ROWS = [ 37011 0x1100, 0x1120, 0x1200, 0x1220, 0x1500, 0x1520, 0x1600, 0x1620, 0x1700, 0x1720, 0x1000, 37012 0x1300, 0x1320, 0x1400, 0x1420, 37013 ]; // CEA-608 captions are rendered onto a 34x15 matrix of character 37014 // cells. The "bottom" row is the last element in the outer array. 37015 // We keep track of positioning information as we go by storing the 37016 // number of indentations and the tab offset in this buffer. 37017 37018 var createDisplayBuffer = function () { 37019 var result = [], 37020 i = BOTTOM_ROW + 1; 37021 while (i--) { 37022 result.push({ 37023 text: '', 37024 indent: 0, 37025 offset: 0, 37026 }); 37027 } 37028 return result; 37029 }; 37030 var Cea608Stream = function (field, dataChannel) { 37031 Cea608Stream.prototype.init.call(this); 37032 this.field_ = field || 0; 37033 this.dataChannel_ = dataChannel || 0; 37034 this.name_ = 'CC' + (((this.field_ << 1)
vendor: 8,791 bytes, lines 37034-37218
37034 | this.dataChannel_) + 1); 37035 this.setConstants(); 37036 this.reset(); 37037 this.push = function (packet) { 37038 var data, swap, char0, char1, text; // remove the parity bits 37039 37040 data = packet.ccData & 0x7f7f; // ignore duplicate control codes; the spec demands they're sent twice 37041 37042 if (data === this.lastControlCode_) { 37043 this.lastControlCode_ = null; 37044 return; 37045 } // Store control codes 37046 37047 if ((data & 0xf000) === 0x1000) { 37048 this.lastControlCode_ = data; 37049 } else if (data !== this.PADDING_) { 37050 this.lastControlCode_ = null; 37051 } 37052 char0 = data >>> 8; 37053 char1 = data & 0xff; 37054 if (data === this.PADDING_) { 37055 return; 37056 } else if (data === this.RESUME_CAPTION_LOADING_) { 37057 this.mode_ = 'popOn'; 37058 } else if (data === this.END_OF_CAPTION_) { 37059 // If an EOC is received while in paint-on mode, the displayed caption 37060 // text should be swapped to non-displayed memory as if it was a pop-on 37061 // caption. Because of that, we should explicitly switch back to pop-on 37062 // mode 37063 this.mode_ = 'popOn'; 37064 this.clearFormatting(packet.pts); // if a caption was being displayed, it's gone now 37065 37066 this.flushDisplayed(packet.pts); // flip memory 37067 37068 swap = this.displayed_; 37069 this.displayed_ = this.nonDisplayed_; 37070 this.nonDisplayed_ = swap; // start measuring the time to display the caption 37071 37072 this.startPts_ = packet.pts; 37073 } else if (data === this.ROLL_UP_2_ROWS_) { 37074 this.rollUpRows_ = 2; 37075 this.setRollUp(packet.pts); 37076 } else if (data === this.ROLL_UP_3_ROWS_) { 37077 this.rollUpRows_ = 3; 37078 this.setRollUp(packet.pts); 37079 } else if (data === this.ROLL_UP_4_ROWS_) { 37080 this.rollUpRows_ = 4; 37081 this.setRollUp(packet.pts); 37082 } else if (data === this.CARRIAGE_RETURN_) { 37083 this.clearFormatting(packet.pts); 37084 this.flushDisplayed(packet.pts); 37085 this.shiftRowsUp_(); 37086 this.startPts_ = packet.pts; 37087 } else if (data === this.BACKSPACE_) { 37088 if (this.mode_ === 'popOn') { 37089 this.nonDisplayed_[this.row_].text = this.nonDisplayed_[this.row_].text.slice(0, -1); 37090 } else { 37091 this.displayed_[this.row_].text = this.displayed_[this.row_].text.slice(0, -1); 37092 } 37093 } else if (data === this.ERASE_DISPLAYED_MEMORY_) { 37094 this.flushDisplayed(packet.pts); 37095 this.displayed_ = createDisplayBuffer(); 37096 } else if (data === this.ERASE_NON_DISPLAYED_MEMORY_) { 37097 this.nonDisplayed_ = createDisplayBuffer(); 37098 } else if (data === this.RESUME_DIRECT_CAPTIONING_) { 37099 if (this.mode_ !== 'paintOn') { 37100 // NOTE: This should be removed when proper caption positioning is 37101 // implemented 37102 this.flushDisplayed(packet.pts); 37103 this.displayed_ = createDisplayBuffer(); 37104 } 37105 this.mode_ = 'paintOn'; 37106 this.startPts_ = packet.pts; // Append special characters to caption text 37107 } else if (this.isSpecialCharacter(char0, char1)) { 37108 // Bitmask char0 so that we can apply character transformations 37109 // regardless of field and data channel. 37110 // Then byte-shift to the left and OR with char1 so we can pass the 37111 // entire character code to `getCharFromCode`. 37112 char0 = (char0 & 0x03) << 8; 37113 text = getCharFromCode(char0 | char1); 37114 this[this.mode_](packet.pts, text); 37115 this.column_++; // Append extended characters to caption text 37116 } else if (this.isExtCharacter(char0, char1)) { 37117 // Extended characters always follow their "non-extended" equivalents. 37118 // IE if a "è" is desired, you'll always receive "eè"; non-compliant 37119 // decoders are supposed to drop the "è", while compliant decoders 37120 // backspace the "e" and insert "è". 37121 // Delete the previous character 37122 if (this.mode_ === 'popOn') { 37123 this.nonDisplayed_[this.row_].text = this.nonDisplayed_[this.row_].text.slice(0, -1); 37124 } else { 37125 this.displayed_[this.row_].text = this.displayed_[this.row_].text.slice(0, -1); 37126 } // Bitmask char0 so that we can apply character transformations 37127 // regardless of field and data channel. 37128 // Then byte-shift to the left and OR with char1 so we can pass the 37129 // entire character code to `getCharFromCode`. 37130 37131 char0 = (char0 & 0x03) << 8; 37132 text = getCharFromCode(char0 | char1); 37133 this[this.mode_](packet.pts, text); 37134 this.column_++; // Process mid-row codes 37135 } else if (this.isMidRowCode(char0, char1)) { 37136 // Attributes are not additive, so clear all formatting 37137 this.clearFormatting(packet.pts); // According to the standard, mid-row codes 37138 // should be replaced with spaces, so add one now 37139 37140 this[this.mode_](packet.pts, ' '); 37141 this.column_++; 37142 if ((char1 & 0xe) === 0xe) { 37143 this.addFormatting(packet.pts, ['i']); 37144 } 37145 if ((char1 & 0x1) === 0x1) { 37146 this.addFormatting(packet.pts, ['u']); 37147 } // Detect offset control codes and adjust cursor 37148 } else if (this.isOffsetControlCode(char0, char1)) { 37149 // Cursor position is set by indent PAC (see below) in 4-column 37150 // increments, with an additional offset code of 1-3 to reach any 37151 // of the 32 columns specified by CEA-608. So all we need to do 37152 // here is increment the column cursor by the given offset. 37153 const offset = char1 & 0x03; // For an offest value 1-3, set the offset for that caption 37154 // in the non-displayed array. 37155 37156 this.nonDisplayed_[this.row_].offset = offset; 37157 this.column_ += offset; // Detect PACs (Preamble Address Codes) 37158 } else if (this.isPAC(char0, char1)) { 37159 // There's no logic for PAC -> row mapping, so we have to just 37160 // find the row code in an array and use its index :( 37161 var row = ROWS.indexOf(data & 0x1f20); // Configure the caption window if we're in roll-up mode 37162 37163 if (this.mode_ === 'rollUp') { 37164 // This implies that the base row is incorrectly set. 37165 // As per the recommendation in CEA-608(Base Row Implementation), defer to the number 37166 // of roll-up rows set. 37167 if (row - this.rollUpRows_ + 1 < 0) { 37168 row = this.rollUpRows_ - 1; 37169 } 37170 this.setRollUp(packet.pts, row); 37171 } // Ensure the row is between 0 and 14, otherwise use the most 37172 // recent or default row. 37173 37174 if (row !== this.row_ && row >= 0 && row <= 14) { 37175 // formatting is only persistent for current row 37176 this.clearFormatting(packet.pts); 37177 this.row_ = row; 37178 } // All PACs can apply underline, so detect and apply 37179 // (All odd-numbered second bytes set underline) 37180 37181 if (char1 & 0x1 && this.formatting_.indexOf('u') === -1) { 37182 this.addFormatting(packet.pts, ['u']); 37183 } 37184 if ((data & 0x10) === 0x10) { 37185 // We've got an indent level code. Each successive even number 37186 // increments the column cursor by 4, so we can get the desired 37187 // column position by bit-shifting to the right (to get n/2) 37188 // and multiplying by 4. 37189 const indentations = (data & 0xe) >> 1; 37190 this.column_ = indentations * 4; // add to the number of indentations for positioning 37191 37192 this.nonDisplayed_[this.row_].indent += indentations; 37193 } 37194 if (this.isColorPAC(char1)) { 37195 // it's a color code, though we only support white, which 37196 // can be either normal or italicized. white italics can be 37197 // either 0x4e or 0x6e depending on the row, so we just 37198 // bitwise-and with 0xe to see if italics should be turned on 37199 if ((char1 & 0xe) === 0xe) { 37200 this.addFormatting(packet.pts, ['i']); 37201 } 37202 } // We have a normal character in char0, and possibly one in char1 37203 } else if (this.isNormalChar(char0)) { 37204 if (char1 === 0x00) { 37205 char1 = null; 37206 } 37207 text = getCharFromCode(char0); 37208 text += getCharFromCode(char1); 37209 this[this.mode_](packet.pts, text); 37210 this.column_ += text.length; 37211 } // finish data processing 37212 }; 37213 }; 37214 Cea608Stream.prototype = new Stream$7(); // Trigger a cue point that captures the current state of the 37215 // display buffer 37216 37217 Cea608Stream.prototype.flushDisplayed = function (pts) { 37218 const logWarning =
vendor: 31,367 bytes, lines 37218-38027
37218(index) => { 37219 this.trigger('log', { 37220 level: 'warn', 37221 message: 'Skipping a malformed 608 caption at index ' + index + '.', 37222 }); 37223 }; 37224 const content = []; 37225 this.displayed_.forEach((row, i) => { 37226 if (row && row.text && row.text.length) { 37227 try { 37228 // remove spaces from the start and end of the string 37229 row.text = row.text.trim(); 37230 } catch (e) { 37231 // Ordinarily, this shouldn't happen. However, caption 37232 // parsing errors should not throw exceptions and 37233 // break playback. 37234 logWarning(i); 37235 } // See the below link for more details on the following fields: 37236 // https://dvcs.w3.org/hg/text-tracks/raw-file/default/608toVTT/608toVTT.html#positioning-in-cea-608 37237 37238 if (row.text.length) { 37239 content.push({ 37240 // The text to be displayed in the caption from this specific row, with whitespace removed. 37241 text: row.text, 37242 // Value between 1 and 15 representing the PAC row used to calculate line height. 37243 line: i + 1, 37244 // A number representing the indent position by percentage (CEA-608 PAC indent code). 37245 // The value will be a number between 10 and 80. Offset is used to add an aditional 37246 // value to the position if necessary. 37247 position: 10 + Math.min(70, row.indent * 10) + row.offset * 2.5, 37248 }); 37249 } 37250 } else if (row === undefined || row === null) { 37251 logWarning(i); 37252 } 37253 }); 37254 if (content.length) { 37255 this.trigger('data', { 37256 startPts: this.startPts_, 37257 endPts: pts, 37258 content, 37259 stream: this.name_, 37260 }); 37261 } 37262 }; 37263 /** 37264 * Zero out the data, used for startup and on seek 37265 */ 37266 37267 Cea608Stream.prototype.reset = function () { 37268 this.mode_ = 'popOn'; // When in roll-up mode, the index of the last row that will 37269 // actually display captions. If a caption is shifted to a row 37270 // with a lower index than this, it is cleared from the display 37271 // buffer 37272 37273 this.topRow_ = 0; 37274 this.startPts_ = 0; 37275 this.displayed_ = createDisplayBuffer(); 37276 this.nonDisplayed_ = createDisplayBuffer(); 37277 this.lastControlCode_ = null; // Track row and column for proper line-breaking and spacing 37278 37279 this.column_ = 0; 37280 this.row_ = BOTTOM_ROW; 37281 this.rollUpRows_ = 2; // This variable holds currently-applied formatting 37282 37283 this.formatting_ = []; 37284 }; 37285 /** 37286 * Sets up control code and related constants for this instance 37287 */ 37288 37289 Cea608Stream.prototype.setConstants = function () { 37290 // The following attributes have these uses: 37291 // ext_ : char0 for mid-row codes, and the base for extended 37292 // chars (ext_+0, ext_+1, and ext_+2 are char0s for 37293 // extended codes) 37294 // control_: char0 for control codes, except byte-shifted to the 37295 // left so that we can do this.control_ | CONTROL_CODE 37296 // offset_: char0 for tab offset codes 37297 // 37298 // It's also worth noting that control codes, and _only_ control codes, 37299 // differ between field 1 and field2. Field 2 control codes are always 37300 // their field 1 value plus 1. That's why there's the "| field" on the 37301 // control value. 37302 if (this.dataChannel_ === 0) { 37303 this.BASE_ = 0x10; 37304 this.EXT_ = 0x11; 37305 this.CONTROL_ = (0x14 | this.field_) << 8; 37306 this.OFFSET_ = 0x17; 37307 } else if (this.dataChannel_ === 1) { 37308 this.BASE_ = 0x18; 37309 this.EXT_ = 0x19; 37310 this.CONTROL_ = (0x1c | this.field_) << 8; 37311 this.OFFSET_ = 0x1f; 37312 } // Constants for the LSByte command codes recognized by Cea608Stream. This 37313 // list is not exhaustive. For a more comprehensive listing and semantics see 37314 // http://www.gpo.gov/fdsys/pkg/CFR-2010-title47-vol1/pdf/CFR-2010-title47-vol1-sec15-119.pdf 37315 // Padding 37316 37317 this.PADDING_ = 0x0000; // Pop-on Mode 37318 37319 this.RESUME_CAPTION_LOADING_ = this.CONTROL_ | 0x20; 37320 this.END_OF_CAPTION_ = this.CONTROL_ | 0x2f; // Roll-up Mode 37321 37322 this.ROLL_UP_2_ROWS_ = this.CONTROL_ | 0x25; 37323 this.ROLL_UP_3_ROWS_ = this.CONTROL_ | 0x26; 37324 this.ROLL_UP_4_ROWS_ = this.CONTROL_ | 0x27; 37325 this.CARRIAGE_RETURN_ = this.CONTROL_ | 0x2d; // paint-on mode 37326 37327 this.RESUME_DIRECT_CAPTIONING_ = this.CONTROL_ | 0x29; // Erasure 37328 37329 this.BACKSPACE_ = this.CONTROL_ | 0x21; 37330 this.ERASE_DISPLAYED_MEMORY_ = this.CONTROL_ | 0x2c; 37331 this.ERASE_NON_DISPLAYED_MEMORY_ = this.CONTROL_ | 0x2e; 37332 }; 37333 /** 37334 * Detects if the 2-byte packet data is a special character 37335 * 37336 * Special characters have a second byte in the range 0x30 to 0x3f, 37337 * with the first byte being 0x11 (for data channel 1) or 0x19 (for 37338 * data channel 2). 37339 * 37340 * @param {Integer} char0 The first byte 37341 * @param {Integer} char1 The second byte 37342 * @return {Boolean} Whether the 2 bytes are an special character 37343 */ 37344 37345 Cea608Stream.prototype.isSpecialCharacter = function (char0, char1) { 37346 return char0 === this.EXT_ && char1 >= 0x30 && char1 <= 0x3f; 37347 }; 37348 /** 37349 * Detects if the 2-byte packet data is an extended character 37350 * 37351 * Extended characters have a second byte in the range 0x20 to 0x3f, 37352 * with the first byte being 0x12 or 0x13 (for data channel 1) or 37353 * 0x1a or 0x1b (for data channel 2). 37354 * 37355 * @param {Integer} char0 The first byte 37356 * @param {Integer} char1 The second byte 37357 * @return {Boolean} Whether the 2 bytes are an extended character 37358 */ 37359 37360 Cea608Stream.prototype.isExtCharacter = function (char0, char1) { 37361 return (char0 === this.EXT_ + 1 || char0 === this.EXT_ + 2) && char1 >= 0x20 && char1 <= 0x3f; 37362 }; 37363 /** 37364 * Detects if the 2-byte packet is a mid-row code 37365 * 37366 * Mid-row codes have a second byte in the range 0x20 to 0x2f, with 37367 * the first byte being 0x11 (for data channel 1) or 0x19 (for data 37368 * channel 2). 37369 * 37370 * @param {Integer} char0 The first byte 37371 * @param {Integer} char1 The second byte 37372 * @return {Boolean} Whether the 2 bytes are a mid-row code 37373 */ 37374 37375 Cea608Stream.prototype.isMidRowCode = function (char0, char1) { 37376 return char0 === this.EXT_ && char1 >= 0x20 && char1 <= 0x2f; 37377 }; 37378 /** 37379 * Detects if the 2-byte packet is an offset control code 37380 * 37381 * Offset control codes have a second byte in the range 0x21 to 0x23, 37382 * with the first byte being 0x17 (for data channel 1) or 0x1f (for 37383 * data channel 2). 37384 * 37385 * @param {Integer} char0 The first byte 37386 * @param {Integer} char1 The second byte 37387 * @return {Boolean} Whether the 2 bytes are an offset control code 37388 */ 37389 37390 Cea608Stream.prototype.isOffsetControlCode = function (char0, char1) { 37391 return char0 === this.OFFSET_ && char1 >= 0x21 && char1 <= 0x23; 37392 }; 37393 /** 37394 * Detects if the 2-byte packet is a Preamble Address Code 37395 * 37396 * PACs have a first byte in the range 0x10 to 0x17 (for data channel 1) 37397 * or 0x18 to 0x1f (for data channel 2), with the second byte in the 37398 * range 0x40 to 0x7f. 37399 * 37400 * @param {Integer} char0 The first byte 37401 * @param {Integer} char1 The second byte 37402 * @return {Boolean} Whether the 2 bytes are a PAC 37403 */ 37404 37405 Cea608Stream.prototype.isPAC = function (char0, char1) { 37406 return char0 >= this.BASE_ && char0 < this.BASE_ + 8 && char1 >= 0x40 && char1 <= 0x7f; 37407 }; 37408 /** 37409 * Detects if a packet's second byte is in the range of a PAC color code 37410 * 37411 * PAC color codes have the second byte be in the range 0x40 to 0x4f, or 37412 * 0x60 to 0x6f. 37413 * 37414 * @param {Integer} char1 The second byte 37415 * @return {Boolean} Whether the byte is a color PAC 37416 */ 37417 37418 Cea608Stream.prototype.isColorPAC = function (char1) { 37419 return (char1 >= 0x40 && char1 <= 0x4f) || (char1 >= 0x60 && char1 <= 0x7f); 37420 }; 37421 /** 37422 * Detects if a single byte is in the range of a normal character 37423 * 37424 * Normal text bytes are in the range 0x20 to 0x7f. 37425 * 37426 * @param {Integer} char The byte 37427 * @return {Boolean} Whether the byte is a normal character 37428 */ 37429 37430 Cea608Stream.prototype.isNormalChar = function (char) { 37431 return char >= 0x20 && char <= 0x7f; 37432 }; 37433 /** 37434 * Configures roll-up 37435 * 37436 * @param {Integer} pts Current PTS 37437 * @param {Integer} newBaseRow Used by PACs to slide the current window to 37438 * a new position 37439 */ 37440 37441 Cea608Stream.prototype.setRollUp = function (pts, newBaseRow) { 37442 // Reset the base row to the bottom row when switching modes 37443 if (this.mode_ !== 'rollUp') { 37444 this.row_ = BOTTOM_ROW; 37445 this.mode_ = 'rollUp'; // Spec says to wipe memories when switching to roll-up 37446 37447 this.flushDisplayed(pts); 37448 this.nonDisplayed_ = createDisplayBuffer(); 37449 this.displayed_ = createDisplayBuffer(); 37450 } 37451 if (newBaseRow !== undefined && newBaseRow !== this.row_) { 37452 // move currently displayed captions (up or down) to the new base row 37453 for (var i = 0; i < this.rollUpRows_; i++) { 37454 this.displayed_[newBaseRow - i] = this.displayed_[this.row_ - i]; 37455 this.displayed_[this.row_ - i] = { 37456 text: '', 37457 indent: 0, 37458 offset: 0, 37459 }; 37460 } 37461 } 37462 if (newBaseRow === undefined) { 37463 newBaseRow = this.row_; 37464 } 37465 this.topRow_ = newBaseRow - this.rollUpRows_ + 1; 37466 }; // Adds the opening HTML tag for the passed character to the caption text, 37467 // and keeps track of it for later closing 37468 37469 Cea608Stream.prototype.addFormatting = function (pts, format) { 37470 this.formatting_ = this.formatting_.concat(format); 37471 var text = format.reduce(function (text, format) { 37472 return text + '<' + format + '>'; 37473 }, ''); 37474 this[this.mode_](pts, text); 37475 }; // Adds HTML closing tags for current formatting to caption text and 37476 // clears remembered formatting 37477 37478 Cea608Stream.prototype.clearFormatting = function (pts) { 37479 if (!this.formatting_.length) { 37480 return; 37481 } 37482 var text = this.formatting_.reverse().reduce(function (text, format) { 37483 return text + '</' + format + '>'; 37484 }, ''); 37485 this.formatting_ = []; 37486 this[this.mode_](pts, text); 37487 }; // Mode Implementations 37488 37489 Cea608Stream.prototype.popOn = function (pts, text) { 37490 var baseRow = this.nonDisplayed_[this.row_].text; // buffer characters 37491 37492 baseRow += text; 37493 this.nonDisplayed_[this.row_].text = baseRow; 37494 }; 37495 Cea608Stream.prototype.rollUp = function (pts, text) { 37496 var baseRow = this.displayed_[this.row_].text; 37497 baseRow += text; 37498 this.displayed_[this.row_].text = baseRow; 37499 }; 37500 Cea608Stream.prototype.shiftRowsUp_ = function () { 37501 var i; // clear out inactive rows 37502 37503 for (i = 0; i < this.topRow_; i++) { 37504 this.displayed_[i] = { 37505 text: '', 37506 indent: 0, 37507 offset: 0, 37508 }; 37509 } 37510 for (i = this.row_ + 1; i < BOTTOM_ROW + 1; i++) { 37511 this.displayed_[i] = { 37512 text: '', 37513 indent: 0, 37514 offset: 0, 37515 }; 37516 } // shift displayed rows up 37517 37518 for (i = this.topRow_; i < this.row_; i++) { 37519 this.displayed_[i] = this.displayed_[i + 1]; 37520 } // clear out the bottom row 37521 37522 this.displayed_[this.row_] = { 37523 text: '', 37524 indent: 0, 37525 offset: 0, 37526 }; 37527 }; 37528 Cea608Stream.prototype.paintOn = function (pts, text) { 37529 var baseRow = this.displayed_[this.row_].text; 37530 baseRow += text; 37531 this.displayed_[this.row_].text = baseRow; 37532 }; // exports 37533 37534 var captionStream = { 37535 CaptionStream: CaptionStream$2, 37536 Cea608Stream: Cea608Stream, 37537 Cea708Stream: Cea708Stream, 37538 }; 37539 /** 37540 * mux.js 37541 * 37542 * Copyright (c) Brightcove 37543 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 37544 */ 37545 37546 var streamTypes = { 37547 H264_STREAM_TYPE: 0x1b, 37548 ADTS_STREAM_TYPE: 0x0f, 37549 METADATA_STREAM_TYPE: 0x15, 37550 }; 37551 /** 37552 * mux.js 37553 * 37554 * Copyright (c) Brightcove 37555 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 37556 * 37557 * Accepts program elementary stream (PES) data events and corrects 37558 * decode and presentation time stamps to account for a rollover 37559 * of the 33 bit value. 37560 */ 37561 37562 var Stream$6 = stream; 37563 var MAX_TS = 8589934592; 37564 var RO_THRESH = 4294967296; 37565 var TYPE_SHARED = 'shared'; 37566 var handleRollover$1 = function (value, reference) { 37567 var direction = 1; 37568 if (value > reference) { 37569 // If the current timestamp value is greater than our reference timestamp and we detect a 37570 // timestamp rollover, this means the roll over is happening in the opposite direction. 37571 // Example scenario: Enter a long stream/video just after a rollover occurred. The reference 37572 // point will be set to a small number, e.g. 1. The user then seeks backwards over the 37573 // rollover point. In loading this segment, the timestamp values will be very large, 37574 // e.g. 2^33 - 1. Since this comes before the data we loaded previously, we want to adjust 37575 // the time stamp to be `value - 2^33`. 37576 direction = -1; 37577 } // Note: A seek forwards or back that is greater than the RO_THRESH (2^32, ~13 hours) will 37578 // cause an incorrect adjustment. 37579 37580 while (Math.abs(reference - value) > RO_THRESH) { 37581 value += direction * MAX_TS; 37582 } 37583 return value; 37584 }; 37585 var TimestampRolloverStream$1 = function (type) { 37586 var lastDTS, referenceDTS; 37587 TimestampRolloverStream$1.prototype.init.call(this); // The "shared" type is used in cases where a stream will contain muxed 37588 // video and audio. We could use `undefined` here, but having a string 37589 // makes debugging a little clearer. 37590 37591 this.type_ = type || TYPE_SHARED; 37592 this.push = function (data) { 37593 /** 37594 * Rollover stream expects data from elementary stream. 37595 * Elementary stream can push forward 2 types of data 37596 * - Parsed Video/Audio/Timed-metadata PES (packetized elementary stream) packets 37597 * - Tracks metadata from PMT (Program Map Table) 37598 * Rollover stream expects pts/dts info to be available, since it stores lastDTS 37599 * We should ignore non-PES packets since they may override lastDTS to undefined. 37600 * lastDTS is important to signal the next segments 37601 * about rollover from the previous segments. 37602 */ 37603 if (data.type === 'metadata') { 37604 this.trigger('data', data); 37605 return; 37606 } // Any "shared" rollover streams will accept _all_ data. Otherwise, 37607 // streams will only accept data that matches their type. 37608 37609 if (this.type_ !== TYPE_SHARED && data.type !== this.type_) { 37610 return; 37611 } 37612 if (referenceDTS === undefined) { 37613 referenceDTS = data.dts; 37614 } 37615 data.dts = handleRollover$1(data.dts, referenceDTS); 37616 data.pts = handleRollover$1(data.pts, referenceDTS); 37617 lastDTS = data.dts; 37618 this.trigger('data', data); 37619 }; 37620 this.flush = function () { 37621 referenceDTS = lastDTS; 37622 this.trigger('done'); 37623 }; 37624 this.endTimeline = function () { 37625 this.flush(); 37626 this.trigger('endedtimeline'); 37627 }; 37628 this.discontinuity = function () { 37629 referenceDTS = void 0; 37630 lastDTS = void 0; 37631 }; 37632 this.reset = function () { 37633 this.discontinuity(); 37634 this.trigger('reset'); 37635 }; 37636 }; 37637 TimestampRolloverStream$1.prototype = new Stream$6(); 37638 var timestampRolloverStream = { 37639 TimestampRolloverStream: TimestampRolloverStream$1, 37640 handleRollover: handleRollover$1, 37641 }; // Once IE11 support is dropped, this function should be removed. 37642 37643 var typedArrayIndexOf$1 = (typedArray, element, fromIndex) => { 37644 if (!typedArray) { 37645 return -1; 37646 } 37647 var currentIndex = fromIndex; 37648 for (; currentIndex < typedArray.length; currentIndex++) { 37649 if (typedArray[currentIndex] === element) { 37650 return currentIndex; 37651 } 37652 } 37653 return -1; 37654 }; 37655 var typedArray = { 37656 typedArrayIndexOf: typedArrayIndexOf$1, 37657 }; 37658 /** 37659 * mux.js 37660 * 37661 * Copyright (c) Brightcove 37662 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 37663 * 37664 * Tools for parsing ID3 frame data 37665 * @see http://id3.org/id3v2.3.0 37666 */ 37667 37668 var typedArrayIndexOf = typedArray.typedArrayIndexOf, 37669 // Frames that allow different types of text encoding contain a text 37670 // encoding description byte [ID3v2.4.0 section 4.] 37671 textEncodingDescriptionByte = { 37672 Iso88591: 0x00, 37673 // ISO-8859-1, terminated with \0. 37674 Utf16: 0x01, 37675 // UTF-16 encoded Unicode BOM, terminated with \0\0 37676 Utf16be: 0x02, 37677 // UTF-16BE encoded Unicode, without BOM, terminated with \0\0 37678 Utf8: 0x03, // UTF-8 encoded Unicode, terminated with \0 37679 }, 37680 // return a percent-encoded representation of the specified byte range 37681 // @see http://en.wikipedia.org/wiki/Percent-encoding 37682 percentEncode$1 = function (bytes, start, end) { 37683 var i, 37684 result = ''; 37685 for (i = start; i < end; i++) { 37686 result += '%' + ('00' + bytes[i].toString(16)).slice(-2); 37687 } 37688 return result; 37689 }, 37690 // return the string representation of the specified byte range, 37691 // interpreted as UTf-8. 37692 parseUtf8 = function (bytes, start, end) { 37693 return decodeURIComponent(percentEncode$1(bytes, start, end)); 37694 }, 37695 // return the string representation of the specified byte range, 37696 // interpreted as ISO-8859-1. 37697 parseIso88591$1 = function (bytes, start, end) { 37698 return unescape(percentEncode$1(bytes, start, end)); // jshint ignore:line 37699 }, 37700 parseSyncSafeInteger$1 = function (data) { 37701 return (data[0] << 21) | (data[1] << 14) | (data[2] << 7) | data[3]; 37702 }, 37703 frameParsers = { 37704 APIC: function (frame) { 37705 var i = 1, 37706 mimeTypeEndIndex, 37707 descriptionEndIndex, 37708 LINK_MIME_TYPE = '-->'; 37709 if (frame.data[0] !== textEncodingDescriptionByte.Utf8) { 37710 // ignore frames with unrecognized character encodings 37711 return; 37712 } // parsing fields [ID3v2.4.0 section 4.14.] 37713 37714 mimeTypeEndIndex = typedArrayIndexOf(frame.data, 0, i); 37715 if (mimeTypeEndIndex < 0) { 37716 // malformed frame 37717 return; 37718 } // parsing Mime type field (terminated with \0) 37719 37720 frame.mimeType = parseIso88591$1(frame.data, i, mimeTypeEndIndex); 37721 i = mimeTypeEndIndex + 1; // parsing 1-byte Picture Type field 37722 37723 frame.pictureType = frame.data[i]; 37724 i++; 37725 descriptionEndIndex = typedArrayIndexOf(frame.data, 0, i); 37726 if (descriptionEndIndex < 0) { 37727 // malformed frame 37728 return; 37729 } // parsing Description field (terminated with \0) 37730 37731 frame.description = parseUtf8(frame.data, i, descriptionEndIndex); 37732 i = descriptionEndIndex + 1; 37733 if (frame.mimeType === LINK_MIME_TYPE) { 37734 // parsing Picture Data field as URL (always represented as ISO-8859-1 [ID3v2.4.0 section 4.]) 37735 frame.url = parseIso88591$1(frame.data, i, frame.data.length); 37736 } else { 37737 // parsing Picture Data field as binary data 37738 frame.pictureData = frame.data.subarray(i, frame.data.length); 37739 } 37740 }, 37741 'T*': function (frame) { 37742 if (frame.data[0] !== textEncodingDescriptionByte.Utf8) { 37743 // ignore frames with unrecognized character encodings 37744 return; 37745 } // parse text field, do not include null terminator in the frame value 37746 // frames that allow different types of encoding contain terminated text [ID3v2.4.0 section 4.] 37747 37748 frame.value = parseUtf8(frame.data, 1, frame.data.length).replace(/\0*$/, ''); // text information frames supports multiple strings, stored as a terminator separated list [ID3v2.4.0 section 4.2.] 37749 37750 frame.values = frame.value.split('\0'); 37751 }, 37752 TXXX: function (frame) { 37753 var descriptionEndIndex; 37754 if (frame.data[0] !== textEncodingDescriptionByte.Utf8) { 37755 // ignore frames with unrecognized character encodings 37756 return; 37757 } 37758 descriptionEndIndex = typedArrayIndexOf(frame.data, 0, 1); 37759 if (descriptionEndIndex === -1) { 37760 return; 37761 } // parse the text fields 37762 37763 frame.description = parseUtf8(frame.data, 1, descriptionEndIndex); // do not include the null terminator in the tag value 37764 // frames that allow different types of encoding contain terminated text 37765 // [ID3v2.4.0 section 4.] 37766 37767 frame.value = parseUtf8(frame.data, descriptionEndIndex + 1, frame.data.length).replace( 37768 /\0*$/, 37769 '' 37770 ); 37771 frame.data = frame.value; 37772 }, 37773 'W*': function (frame) { 37774 // parse URL field; URL fields are always represented as ISO-8859-1 [ID3v2.4.0 section 4.] 37775 // if the value is followed by a string termination all the following information should be ignored [ID3v2.4.0 section 4.3] 37776 frame.url = parseIso88591$1(frame.data, 0, frame.data.length).replace(/\0.*$/, ''); 37777 }, 37778 WXXX: function (frame) { 37779 var descriptionEndIndex; 37780 if (frame.data[0] !== textEncodingDescriptionByte.Utf8) { 37781 // ignore frames with unrecognized character encodings 37782 return; 37783 } 37784 descriptionEndIndex = typedArrayIndexOf(frame.data, 0, 1); 37785 if (descriptionEndIndex === -1) { 37786 return; 37787 } // parse the description and URL fields 37788 37789 frame.description = parseUtf8(frame.data, 1, descriptionEndIndex); // URL fields are always represented as ISO-8859-1 [ID3v2.4.0 section 4.] 37790 // if the value is followed by a string termination all the following information 37791 // should be ignored [ID3v2.4.0 section 4.3] 37792 37793 frame.url = parseIso88591$1( 37794 frame.data, 37795 descriptionEndIndex + 1, 37796 frame.data.length 37797 ).replace(/\0.*$/, ''); 37798 }, 37799 PRIV: function (frame) { 37800 var i; 37801 for (i = 0; i < frame.data.length; i++) { 37802 if (frame.data[i] === 0) { 37803 // parse the description and URL fields 37804 frame.owner = parseIso88591$1(frame.data, 0, i); 37805 break; 37806 } 37807 } 37808 frame.privateData = frame.data.subarray(i + 1); 37809 frame.data = frame.privateData; 37810 }, 37811 }; 37812 var parseId3Frames$1 = function (data) { 37813 var frameSize, 37814 frameHeader, 37815 frameStart = 10, 37816 tagSize = 0, 37817 frames = []; // If we don't have enough data for a header, 10 bytes, 37818 // or 'ID3' in the first 3 bytes this is not a valid ID3 tag. 37819 37820 if ( 37821 data.length < 10 || 37822 data[0] !== 'I'.charCodeAt(0) || 37823 data[1] !== 'D'.charCodeAt(0) || 37824 data[2] !== '3'.charCodeAt(0) 37825 ) { 37826 return; 37827 } // the frame size is transmitted as a 28-bit integer in the 37828 // last four bytes of the ID3 header. 37829 // The most significant bit of each byte is dropped and the 37830 // results concatenated to recover the actual value. 37831 37832 tagSize = parseSyncSafeInteger$1(data.subarray(6, 10)); // ID3 reports the tag size excluding the header but it's more 37833 // convenient for our comparisons to include it 37834 37835 tagSize += 10; // check bit 6 of byte 5 for the extended header flag. 37836 37837 var hasExtendedHeader = data[5] & 0x40; 37838 if (hasExtendedHeader) { 37839 // advance the frame start past the extended header 37840 frameStart += 4; // header size field 37841 37842 frameStart += parseSyncSafeInteger$1(data.subarray(10, 14)); 37843 tagSize -= parseSyncSafeInteger$1(data.subarray(16, 20)); // clip any padding off the end 37844 } // parse one or more ID3 frames 37845 // http://id3.org/id3v2.3.0#ID3v2_frame_overview 37846 37847 do { 37848 // determine the number of bytes in this frame 37849 frameSize = parseSyncSafeInteger$1(data.subarray(frameStart + 4, frameStart + 8)); 37850 if (frameSize < 1) { 37851 break; 37852 } 37853 frameHeader = String.fromCharCode( 37854 data[frameStart], 37855 data[frameStart + 1], 37856 data[frameStart + 2], 37857 data[frameStart + 3] 37858 ); 37859 var frame = { 37860 id: frameHeader, 37861 data: data.subarray(frameStart + 10, frameStart + frameSize + 10), 37862 }; 37863 frame.key = frame.id; // parse frame values 37864 37865 if (frameParsers[frame.id]) { 37866 // use frame specific parser 37867 frameParsers[frame.id](frame); 37868 } else if (frame.id[0] === 'T') { 37869 // use text frame generic parser 37870 frameParsers['T*'](frame); 37871 } else if (frame.id[0] === 'W') { 37872 // use URL link frame generic parser 37873 frameParsers['W*'](frame); 37874 } 37875 frames.push(frame); 37876 frameStart += 10; // advance past the frame header 37877 37878 frameStart += frameSize; // advance past the frame body 37879 } while (frameStart < tagSize); 37880 return frames; 37881 }; 37882 var parseId3 = { 37883 parseId3Frames: parseId3Frames$1, 37884 parseSyncSafeInteger: parseSyncSafeInteger$1, 37885 frameParsers: frameParsers, 37886 }; 37887 /** 37888 * mux.js 37889 * 37890 * Copyright (c) Brightcove 37891 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 37892 * 37893 * Accepts program elementary stream (PES) data events and parses out 37894 * ID3 metadata from them, if present. 37895 * @see http://id3.org/id3v2.3.0 37896 */ 37897 37898 var Stream$5 = stream, 37899 StreamTypes$3 = streamTypes, 37900 id3 = parseId3, 37901 MetadataStream; 37902 MetadataStream = function (options) { 37903 var settings = { 37904 // the bytes of the program-level descriptor field in MP2T 37905 // see ISO/IEC 13818-1:2013 (E), section 2.6 "Program and 37906 // program element descriptors" 37907 descriptor: options && options.descriptor, 37908 }, 37909 // the total size in bytes of the ID3 tag being parsed 37910 tagSize = 0, 37911 // tag data that is not complete enough to be parsed 37912 buffer = [], 37913 // the total number of bytes currently in the buffer 37914 bufferSize = 0, 37915 i; 37916 MetadataStream.prototype.init.call(this); // calculate the text track in-band metadata track dispatch type 37917 // https://html.spec.whatwg.org/multipage/embedded-content.html#steps-to-expose-a-media-resource-specific-text-track 37918 37919 this.dispatchType = StreamTypes$3.METADATA_STREAM_TYPE.toString(16); 37920 if (settings.descriptor) { 37921 for (i = 0; i < settings.descriptor.length; i++) { 37922 this.dispatchType += ('00' + settings.descriptor[i].toString(16)).slice(-2); 37923 } 37924 } 37925 this.push = function (chunk) { 37926 var tag, frameStart, frameSize, frame, i, frameHeader; 37927 if (chunk.type !== 'timed-metadata') { 37928 return; 37929 } // if data_alignment_indicator is set in the PES header, 37930 // we must have the start of a new ID3 tag. Assume anything 37931 // remaining in the buffer was malformed and throw it out 37932 37933 if (chunk.dataAlignmentIndicator) { 37934 bufferSize = 0; 37935 buffer.length = 0; 37936 } // ignore events that don't look like ID3 data 37937 37938 if ( 37939 buffer.length === 0 && 37940 (chunk.data.length < 10 || 37941 chunk.data[0] !== 'I'.charCodeAt(0) || 37942 chunk.data[1] !== 'D'.charCodeAt(0) || 37943 chunk.data[2] !== '3'.charCodeAt(0)) 37944 ) { 37945 this.trigger('log', { 37946 level: 'warn', 37947 message: 'Skipping unrecognized metadata packet', 37948 }); 37949 return; 37950 } // add this chunk to the data we've collected so far 37951 37952 buffer.push(chunk); 37953 bufferSize += chunk.data.byteLength; // grab the size of the entire frame from the ID3 header 37954 37955 if (buffer.length === 1) { 37956 // the frame size is transmitted as a 28-bit integer in the 37957 // last four bytes of the ID3 header. 37958 // The most significant bit of each byte is dropped and the 37959 // results concatenated to recover the actual value. 37960 tagSize = id3.parseSyncSafeInteger(chunk.data.subarray(6, 10)); // ID3 reports the tag size excluding the header but it's more 37961 // convenient for our comparisons to include it 37962 37963 tagSize += 10; 37964 } // if the entire frame has not arrived, wait for more data 37965 37966 if (bufferSize < tagSize) { 37967 return; 37968 } // collect the entire frame so it can be parsed 37969 37970 tag = { 37971 data: new Uint8Array(tagSize), 37972 frames: [], 37973 pts: buffer[0].pts, 37974 dts: buffer[0].dts, 37975 }; 37976 for (i = 0; i < tagSize; ) { 37977 tag.data.set(buffer[0].data.subarray(0, tagSize - i), i); 37978 i += buffer[0].data.byteLength; 37979 bufferSize -= buffer[0].data.byteLength; 37980 buffer.shift(); 37981 } // find the start of the first frame and the end of the tag 37982 37983 frameStart = 10; 37984 if (tag.data[5] & 0x40) { 37985 // advance the frame start past the extended header 37986 frameStart += 4; // header size field 37987 37988 frameStart += id3.parseSyncSafeInteger(tag.data.subarray(10, 14)); // clip any padding off the end 37989 37990 tagSize -= id3.parseSyncSafeInteger(tag.data.subarray(16, 20)); 37991 } // parse one or more ID3 frames 37992 // http://id3.org/id3v2.3.0#ID3v2_frame_overview 37993 37994 do { 37995 // determine the number of bytes in this frame 37996 frameSize = id3.parseSyncSafeInteger(tag.data.subarray(frameStart + 4, frameStart + 8)); 37997 if (frameSize < 1) { 37998 this.trigger('log', { 37999 level: 'warn', 38000 message: 'Malformed ID3 frame encountered. Skipping remaining metadata parsing.', 38001 }); // If the frame is malformed, don't parse any further frames but allow previous valid parsed frames 38002 // to be sent along. 38003 38004 break; 38005 } 38006 frameHeader = String.fromCharCode( 38007 tag.data[frameStart], 38008 tag.data[frameStart + 1], 38009 tag.data[frameStart + 2], 38010 tag.data[frameStart + 3] 38011 ); 38012 frame = { 38013 id: frameHeader, 38014 data: tag.data.subarray(frameStart + 10, frameStart + frameSize + 10), 38015 }; 38016 frame.key = frame.id; // parse frame values 38017 38018 if (id3.frameParsers[frame.id]) { 38019 // use frame specific parser 38020 id3.frameParsers[frame.id](frame); 38021 } else if (frame.id[0] === 'T') { 38022 // use text frame generic parser 38023 id3.frameParsers['T*'](frame); 38024 } else if (frame.id[0] === 'W') { 38025 // use URL link frame generic parser 38026 id3.frameParsers['W*'](frame); 38027 } // handle the special PRIV frame used to indicate the star
vendor: 10,602 bytes, lines 38027-38287
38027t 38028 // time for raw AAC data 38029 38030 if (frame.owner === 'com.apple.streaming.transportStreamTimestamp') { 38031 var d = frame.data, 38032 size = 38033 ((d[3] & 0x01) << 30) | (d[4] << 22) | (d[5] << 14) | (d[6] << 6) | (d[7] >>> 2); 38034 size *= 4; 38035 size += d[7] & 0x03; 38036 frame.timeStamp = size; // in raw AAC, all subsequent data will be timestamped based 38037 // on the value of this frame 38038 // we couldn't have known the appropriate pts and dts before 38039 // parsing this ID3 tag so set those values now 38040 38041 if (tag.pts === undefined && tag.dts === undefined) { 38042 tag.pts = frame.timeStamp; 38043 tag.dts = frame.timeStamp; 38044 } 38045 this.trigger('timestamp', frame); 38046 } 38047 tag.frames.push(frame); 38048 frameStart += 10; // advance past the frame header 38049 38050 frameStart += frameSize; // advance past the frame body 38051 } while (frameStart < tagSize); 38052 this.trigger('data', tag); 38053 }; 38054 }; 38055 MetadataStream.prototype = new Stream$5(); 38056 var metadataStream = MetadataStream; 38057 /** 38058 * mux.js 38059 * 38060 * Copyright (c) Brightcove 38061 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 38062 * 38063 * A stream-based mp2t to mp4 converter. This utility can be used to 38064 * deliver mp4s to a SourceBuffer on platforms that support native 38065 * Media Source Extensions. 38066 */ 38067 38068 var Stream$4 = stream, 38069 CaptionStream$1 = captionStream, 38070 StreamTypes$2 = streamTypes, 38071 TimestampRolloverStream = timestampRolloverStream.TimestampRolloverStream; // object types 38072 38073 var TransportPacketStream, TransportParseStream, ElementaryStream; // constants 38074 38075 var MP2T_PACKET_LENGTH$1 = 188, 38076 // bytes 38077 SYNC_BYTE$1 = 0x47; 38078 /** 38079 * Splits an incoming stream of binary data into MPEG-2 Transport 38080 * Stream packets. 38081 */ 38082 38083 TransportPacketStream = function () { 38084 var buffer = new Uint8Array(MP2T_PACKET_LENGTH$1), 38085 bytesInBuffer = 0; 38086 TransportPacketStream.prototype.init.call(this); // Deliver new bytes to the stream. 38087 38088 /** 38089 * Split a stream of data into M2TS packets 38090 **/ 38091 38092 this.push = function (bytes) { 38093 var startIndex = 0, 38094 endIndex = MP2T_PACKET_LENGTH$1, 38095 everything; // If there are bytes remaining from the last segment, prepend them to the 38096 // bytes that were pushed in 38097 38098 if (bytesInBuffer) { 38099 everything = new Uint8Array(bytes.byteLength + bytesInBuffer); 38100 everything.set(buffer.subarray(0, bytesInBuffer)); 38101 everything.set(bytes, bytesInBuffer); 38102 bytesInBuffer = 0; 38103 } else { 38104 everything = bytes; 38105 } // While we have enough data for a packet 38106 38107 while (endIndex < everything.byteLength) { 38108 // Look for a pair of start and end sync bytes in the data.. 38109 if (everything[startIndex] === SYNC_BYTE$1 && everything[endIndex] === SYNC_BYTE$1) { 38110 // We found a packet so emit it and jump one whole packet forward in 38111 // the stream 38112 this.trigger('data', everything.subarray(startIndex, endIndex)); 38113 startIndex += MP2T_PACKET_LENGTH$1; 38114 endIndex += MP2T_PACKET_LENGTH$1; 38115 continue; 38116 } // If we get here, we have somehow become de-synchronized and we need to step 38117 // forward one byte at a time until we find a pair of sync bytes that denote 38118 // a packet 38119 38120 startIndex++; 38121 endIndex++; 38122 } // If there was some data left over at the end of the segment that couldn't 38123 // possibly be a whole packet, keep it because it might be the start of a packet 38124 // that continues in the next segment 38125 38126 if (startIndex < everything.byteLength) { 38127 buffer.set(everything.subarray(startIndex), 0); 38128 bytesInBuffer = everything.byteLength - startIndex; 38129 } 38130 }; 38131 /** 38132 * Passes identified M2TS packets to the TransportParseStream to be parsed 38133 **/ 38134 38135 this.flush = function () { 38136 // If the buffer contains a whole packet when we are being flushed, emit it 38137 // and empty the buffer. Otherwise hold onto the data because it may be 38138 // important for decoding the next segment 38139 if (bytesInBuffer === MP2T_PACKET_LENGTH$1 && buffer[0] === SYNC_BYTE$1) { 38140 this.trigger('data', buffer); 38141 bytesInBuffer = 0; 38142 } 38143 this.trigger('done'); 38144 }; 38145 this.endTimeline = function () { 38146 this.flush(); 38147 this.trigger('endedtimeline'); 38148 }; 38149 this.reset = function () { 38150 bytesInBuffer = 0; 38151 this.trigger('reset'); 38152 }; 38153 }; 38154 TransportPacketStream.prototype = new Stream$4(); 38155 /** 38156 * Accepts an MP2T TransportPacketStream and emits data events with parsed 38157 * forms of the individual transport stream packets. 38158 */ 38159 38160 TransportParseStream = function () { 38161 var parsePsi, parsePat, parsePmt, self; 38162 TransportParseStream.prototype.init.call(this); 38163 self = this; 38164 this.packetsWaitingForPmt = []; 38165 this.programMapTable = undefined; 38166 parsePsi = function (payload, psi) { 38167 var offset = 0; // PSI packets may be split into multiple sections and those 38168 // sections may be split into multiple packets. If a PSI 38169 // section starts in this packet, the payload_unit_start_indicator 38170 // will be true and the first byte of the payload will indicate 38171 // the offset from the current position to the start of the 38172 // section. 38173 38174 if (psi.payloadUnitStartIndicator) { 38175 offset += payload[offset] + 1; 38176 } 38177 if (psi.type === 'pat') { 38178 parsePat(payload.subarray(offset), psi); 38179 } else { 38180 parsePmt(payload.subarray(offset), psi); 38181 } 38182 }; 38183 parsePat = function (payload, pat) { 38184 pat.section_number = payload[7]; // eslint-disable-line camelcase 38185 38186 pat.last_section_number = payload[8]; // eslint-disable-line camelcase 38187 // skip the PSI header and parse the first PMT entry 38188 38189 self.pmtPid = ((payload[10] & 0x1f) << 8) | payload[11]; 38190 pat.pmtPid = self.pmtPid; 38191 }; 38192 /** 38193 * Parse out the relevant fields of a Program Map Table (PMT). 38194 * @param payload {Uint8Array} the PMT-specific portion of an MP2T 38195 * packet. The first byte in this array should be the table_id 38196 * field. 38197 * @param pmt {object} the object that should be decorated with 38198 * fields parsed from the PMT. 38199 */ 38200 38201 parsePmt = function (payload, pmt) { 38202 var sectionLength, tableEnd, programInfoLength, offset; // PMTs can be sent ahead of the time when they should actually 38203 // take effect. We don't believe this should ever be the case 38204 // for HLS but we'll ignore "forward" PMT declarations if we see 38205 // them. Future PMT declarations have the current_next_indicator 38206 // set to zero. 38207 38208 if (!(payload[5] & 0x01)) { 38209 return; 38210 } // overwrite any existing program map table 38211 38212 self.programMapTable = { 38213 video: null, 38214 audio: null, 38215 'timed-metadata': {}, 38216 }; // the mapping table ends at the end of the current section 38217 38218 sectionLength = ((payload[1] & 0x0f) << 8) | payload[2]; 38219 tableEnd = 3 + sectionLength - 4; // to determine where the table is, we have to figure out how 38220 // long the program info descriptors are 38221 38222 programInfoLength = ((payload[10] & 0x0f) << 8) | payload[11]; // advance the offset to the first entry in the mapping table 38223 38224 offset = 12 + programInfoLength; 38225 while (offset < tableEnd) { 38226 var streamType = payload[offset]; 38227 var pid = ((payload[offset + 1] & 0x1f) << 8) | payload[offset + 2]; // only map a single elementary_pid for audio and video stream types 38228 // TODO: should this be done for metadata too? for now maintain behavior of 38229 // multiple metadata streams 38230 38231 if ( 38232 streamType === StreamTypes$2.H264_STREAM_TYPE && 38233 self.programMapTable.video === null 38234 ) { 38235 self.programMapTable.video = pid; 38236 } else if ( 38237 streamType === StreamTypes$2.ADTS_STREAM_TYPE && 38238 self.programMapTable.audio === null 38239 ) { 38240 self.programMapTable.audio = pid; 38241 } else if (streamType === StreamTypes$2.METADATA_STREAM_TYPE) { 38242 // map pid to stream type for metadata streams 38243 self.programMapTable['timed-metadata'][pid] = streamType; 38244 } // move to the next table entry 38245 // skip past the elementary stream descriptors, if present 38246 38247 offset += (((payload[offset + 3] & 0x0f) << 8) | payload[offset + 4]) + 5; 38248 } // record the map on the packet as well 38249 38250 pmt.programMapTable = self.programMapTable; 38251 }; 38252 /** 38253 * Deliver a new MP2T packet to the next stream in the pipeline. 38254 */ 38255 38256 this.push = function (packet) { 38257 var result = {}, 38258 offset = 4; 38259 result.payloadUnitStartIndicator = !!(packet[1] & 0x40); // pid is a 13-bit field starting at the last bit of packet[1] 38260 38261 result.pid = packet[1] & 0x1f; 38262 result.pid <<= 8; 38263 result.pid |= packet[2]; // if an adaption field is present, its length is specified by the 38264 // fifth byte of the TS packet header. The adaptation field is 38265 // used to add stuffing to PES packets that don't fill a complete 38266 // TS packet, and to specify some forms of timing and control data 38267 // that we do not currently use. 38268 38269 if ((packet[3] & 0x30) >>> 4 > 0x01) { 38270 offset += packet[offset] + 1; 38271 } // parse the rest of the packet based on the type 38272 38273 if (result.pid === 0) { 38274 result.type = 'pat'; 38275 parsePsi(packet.subarray(offset), result); 38276 this.trigger('data', result); 38277 } else if (result.pid === this.pmtPid) { 38278 result.type = 'pmt'; 38279 parsePsi(packet.subarray(offset), result); 38280 this.trigger('data', result); // if there are any packets waiting for a PMT to be found, process them now 38281 38282 while (this.packetsWaitingForPmt.length) { 38283 this.processPes_.apply(this, this.packetsWaitingForPmt.shift()); 38284 } 38285 } else if (this.programMapTable === undefined) { 38286 // When we have not seen a PMT yet, defer further processing of 38287 // PES packets until one has been parsed
38288 this.packetsWaitingForPmt.push([packet, offset, result]); 38289 } else { 38290 this.processPes_(packet, offset, result); 38291 } 38292 }; 38293 this.processPes_ = function (packet, offset, result) { 38294 // set the appropriate stream type 38295 if (result.pid === this.programMapTable.video) { 38296 result.streamType = StreamTypes$2.H264_STREAM_TYPE; 38297 } else if (result.pid === this.programMapTable.audio) { 38298 result.streamType = StreamTypes$2.ADTS_STREAM_TYPE; 38299 } else { 38300 // if not video or audio, it is timed-metadata or unknown 38301 // if unknown, streamType will be undefined 38302 result.streamType = this.programMapTable['timed-metadata'][result.pid]; 38303 } 38304 result.type = 'pes'; 38305 result.data = packet.subarray(offset); 38306 this.trigger('data', result); 38307 }; 38308 }; 38309 TransportParseStream.prototype = new Stream$4(); 38310 TransportParseStream.STREAM_TYPES = { 38311 h264: 0x1b, 38312 adts: 0x0f, 38313 }; 38314 /** 38315 * Reconsistutes program elementary stream (PES) packets from parsed 38316 * transport stream packets. That is, if you pipe an 38317 * mp2t.TransportParseStream into a mp2t.ElementaryStream, the output 38318 * events will be events which capture the bytes for individual PES 38319 * packets plus relevant metadata that has been extracted from the 38320 * container. 38321 */ 38322 38323 ElementaryStream = function () { 38324 var self = this, 38325 segmentHadPmt = false, 38326 // PES packet fragments 38327 video = { 38328 data: [], 38329 size: 0, 38330 }, 38331 audio = { 38332 data: [], 38333 size: 0, 38334 }, 38335 timedMetadata = { 38336 data: [], 38337 size: 0, 38338 }, 38339 programMapTable, 38340 parsePes = function (payload, pes) { 38341 var ptsDtsFlags; 38342 const startPrefix = (payload[0] << 16) | (payload[1] << 8) | payload[2]; // default to an empty array 38343 38344 pes.data = new Uint8Array(); // In certain live streams, the start of a TS fragment has ts packets 38345 // that are frame data that is continuing from the previous fragment. This 38346 // is to check that the pes data is the start of a new pes payload 38347 38348 if (startPrefix !== 1) { 38349 return; 38350 } // get the packet length, this will be 0 for video 38351 38352 pes.packetLength = 6 + ((payload[4] << 8) | payload[5]); // find out if this packets starts a new keyframe 38353 38354 pes.dataAlignmentIndicator = (payload[6] & 0x04) !== 0; // PES packets may be annotated with a PTS value, or a PTS value 38355 // and a DTS value. Determine what combination of values is 38356 // available to work with. 38357 38358 ptsDtsFlags = payload[7]; // PTS and DTS are normally stored as a 33-bit number. Javascript 38359 // performs all bitwise operations on 32-bit integers but javascript 38360 // supports a much greater range (52-bits) of integer using standard 38361 // mathematical operations. 38362 // We construct a 31-bit value using bitwise operators over the 31 38363 // most significant bits and then multiply by 4 (equal to a left-shift 38364 // of 2) before we add the final 2 least significant bits of the 38365 // timestamp (equal to an OR.) 38366 38367 if (ptsDtsFlags & 0xc0) { 38368 // the PTS and DTS are not written out directly. For information 38369 // on how they are encoded, see 38370 // http://dvd.sourceforge.net/dvdinfo/pes-hdr.html 38371 pes.pts = 38372 ((payload[9] & 0x0e) << 27) | 38373 ((payload[10] & 0xff) << 20) | 38374 ((payload[11] & 0xfe) << 12) | 38375 ((payload[12] & 0xff) << 5) | 38376 ((payload[13] & 0xfe) >>> 3); 38377 pes.pts *= 4; // Left shift by 2 38378 38379 pes.pts += (payload[13] & 0x06) >>> 1; // OR by the two LSBs 38380 38381 pes.dts = pes.pts; 38382 if (ptsDtsFlags & 0x40) { 38383 pes.dts = 38384 ((payload[14] & 0x0e) << 27) | 38385 ((payload[15] & 0xff) << 20) | 38386 ((payload[16] & 0xfe) << 12) | 38387 ((payload[17] & 0xff) << 5) | 38388 ((payload[18] & 0xfe) >>> 3); 38389 pes.dts *= 4; // Left shift by 2 38390 38391 pes.dts += (payload[18] & 0x06) >>> 1; // OR by the two LSBs 38392 } 38393 } // the data section starts immediately after the PES header. 38394 // pes_header_data_length specifies the number of header bytes
vendor: 6,079 bytes, lines 38395-38568
38395 // that follow the last byte of the field. 38396 38397 pes.data = payload.subarray(9 + payload[8]); 38398 }, 38399 /** 38400 * Pass completely parsed PES packets to the next stream in the pipeline 38401 **/ 38402 flushStream = function (stream, type, forceFlush) { 38403 var packetData = new Uint8Array(stream.size), 38404 event = { 38405 type: type, 38406 }, 38407 i = 0, 38408 offset = 0, 38409 packetFlushable = false, 38410 fragment; // do nothing if there is not enough buffered data for a complete 38411 // PES header 38412 38413 if (!stream.data.length || stream.size < 9) { 38414 return; 38415 } 38416 event.trackId = stream.data[0].pid; // reassemble the packet 38417 38418 for (i = 0; i < stream.data.length; i++) { 38419 fragment = stream.data[i]; 38420 packetData.set(fragment.data, offset); 38421 offset += fragment.data.byteLength; 38422 } // parse assembled packet's PES header 38423 38424 parsePes(packetData, event); // non-video PES packets MUST have a non-zero PES_packet_length 38425 // check that there is enough stream data to fill the packet 38426 38427 packetFlushable = type === 'video' || event.packetLength <= stream.size; // flush pending packets if the conditions are right 38428 38429 if (forceFlush || packetFlushable) { 38430 stream.size = 0; 38431 stream.data.length = 0; 38432 } // only emit packets that are complete. this is to avoid assembling 38433 // incomplete PES packets due to poor segmentation 38434 38435 if (packetFlushable) { 38436 self.trigger('data', event); 38437 } 38438 }; 38439 ElementaryStream.prototype.init.call(this); 38440 /** 38441 * Identifies M2TS packet types and parses PES packets using metadata 38442 * parsed from the PMT 38443 **/ 38444 38445 this.push = function (data) { 38446 ({ 38447 pat: function () { 38448 // we have to wait for the PMT to arrive as well before we 38449 // have any meaningful metadata 38450 }, 38451 pes: function () { 38452 var stream, streamType; 38453 switch (data.streamType) { 38454 case StreamTypes$2.H264_STREAM_TYPE: 38455 stream = video; 38456 streamType = 'video'; 38457 break; 38458 case StreamTypes$2.ADTS_STREAM_TYPE: 38459 stream = audio; 38460 streamType = 'audio'; 38461 break; 38462 case StreamTypes$2.METADATA_STREAM_TYPE: 38463 stream = timedMetadata; 38464 streamType = 'timed-metadata'; 38465 break; 38466 default: 38467 // ignore unknown stream types 38468 return; 38469 } // if a new packet is starting, we can flush the completed 38470 // packet 38471 38472 if (data.payloadUnitStartIndicator) { 38473 flushStream(stream, streamType, true); 38474 } // buffer this fragment until we are sure we've received the 38475 // complete payload 38476 38477 stream.data.push(data); 38478 stream.size += data.data.byteLength; 38479 }, 38480 pmt: function () { 38481 var event = { 38482 type: 'metadata', 38483 tracks: [], 38484 }; 38485 programMapTable = data.programMapTable; // translate audio and video streams to tracks 38486 38487 if (programMapTable.video !== null) { 38488 event.tracks.push({ 38489 timelineStartInfo: { 38490 baseMediaDecodeTime: 0, 38491 }, 38492 id: +programMapTable.video, 38493 codec: 'avc', 38494 type: 'video', 38495 }); 38496 } 38497 if (programMapTable.audio !== null) { 38498 event.tracks.push({ 38499 timelineStartInfo: { 38500 baseMediaDecodeTime: 0, 38501 }, 38502 id: +programMapTable.audio, 38503 codec: 'adts', 38504 type: 'audio', 38505 }); 38506 } 38507 segmentHadPmt = true; 38508 self.trigger('data', event); 38509 }, 38510 })[data.type](); 38511 }; 38512 this.reset = function () { 38513 video.size = 0; 38514 video.data.length = 0; 38515 audio.size = 0; 38516 audio.data.length = 0; 38517 this.trigger('reset'); 38518 }; 38519 /** 38520 * Flush any remaining input. Video PES packets may be of variable 38521 * length. Normally, the start of a new video packet can trigger the 38522 * finalization of the previous packet. That is not possible if no 38523 * more video is forthcoming, however. In that case, some other 38524 * mechanism (like the end of the file) has to be employed. When it is 38525 * clear that no additional data is forthcoming, calling this method 38526 * will flush the buffered packets. 38527 */ 38528 38529 this.flushStreams_ = function () { 38530 // !!THIS ORDER IS IMPORTANT!! 38531 // video first then audio 38532 flushStream(video, 'video'); 38533 flushStream(audio, 'audio'); 38534 flushStream(timedMetadata, 'timed-metadata'); 38535 }; 38536 this.flush = function () { 38537 // if on flush we haven't had a pmt emitted 38538 // and we have a pmt to emit. emit the pmt 38539 // so that we trigger a trackinfo downstream. 38540 if (!segmentHadPmt && programMapTable) { 38541 var pmt = { 38542 type: 'metadata', 38543 tracks: [], 38544 }; // translate audio and video streams to tracks 38545 38546 if (programMapTable.video !== null) { 38547 pmt.tracks.push({ 38548 timelineStartInfo: { 38549 baseMediaDecodeTime: 0, 38550 }, 38551 id: +programMapTable.video, 38552 codec: 'avc', 38553 type: 'video', 38554 }); 38555 } 38556 if (programMapTable.audio !== null) { 38557 pmt.tracks.push({ 38558 timelineStartInfo: { 38559 baseMediaDecodeTime: 0, 38560 }, 38561 id: +programMapTable.audio, 38562 codec: 'adts', 38563 type: 'audio', 38564 }); 38565 } 38566 self.trigger('data', pmt); 38567 } 38568 segmentHadPmt = false;
38569 this.flushStreams_(); 38570 this.trigger('done'); 38571 }; 38572 }; 38573 ElementaryStream.prototype = new Stream$4(); 38574 var m2ts$1 = { 38575 PAT_PID: 0x0000, 38576 MP2T_PACKET_LENGTH: MP2T_PACKET_LENGTH$1, 38577 TransportPacketStream: TransportPacketStream, 38578 TransportParseStream: TransportParseStream, 38579 ElementaryStream: ElementaryStream, 38580 TimestampRolloverStream: TimestampRolloverStream, 38581 CaptionStream: CaptionStream$1.CaptionStream, 38582 Cea608Stream: CaptionStream$1.Cea608Stream, 38583 Cea708Stream: CaptionStream$1.Cea708Stream, 38584 MetadataStream: metadataStream, 38585 }; 38586 for (var type in StreamTypes$2) { 38587 if (StreamTypes$2.hasOwnProperty(type)) { 38588 m2ts$1[type] = StreamTypes$2[type]; 38589 } 38590 } 38591 var m2ts_1 = m2ts$1; 38592 /** 38593 * mux.js 38594 * 38595 * Copyright (c) Brightcove 38596 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 38597 */ 38598 38599 var Stream$3 = stream; 38600 var ONE_SECOND_IN_TS$2 = clock$2.ONE_SECOND_IN_TS; 38601 var AdtsStream$1; 38602 var ADTS_SAMPLING_FREQUENCIES$1 = [ 38603 96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350, 38604 ]; 38605 /* 38606 * Accepts a ElementaryStream and emits data events with parsed 38607 * AAC Audio Frames of the individual packets. Input audio in ADTS 38608 * format is unpacked and re-emitted as AAC frames. 38609 * 38610 * @see http://wiki.multimedia.cx/index.php?title=ADTS 38611 * @see http://wiki.multimedia.cx/?title=Understanding_AAC 38612 */ 38613 38614 AdtsStream$1 = function (handlePartialSegments) { 38615 var buffer, 38616 frameNum = 0; 38617 AdtsStream$1.prototype.init.call(this); 38618 this.skipWarn_ = function (start, end) { 38619 this.trigger('log', { 38620 level: 'warn', 38621 message: `adts skiping bytes ${start} to ${end} in frame ${frameNum} outside syncword`, 38622 }); 38623 }; 38624 this.push = function (packet) { 38625 var i = 0, 38626 frameLength, 38627 protectionSkipBytes, 38628 oldBuffer, 38629 sampleCount, 38630 adtsFrameDuration; 38631 if (!handlePartialSegments) { 38632 frameNum = 0; 38633 } 38634 if (packet.type !== 'audio') { 38635 // ignore non-audio data 38636 return; 38637 } // Prepend any data in the buffer to the input data so that we can parse 38638 // aac frames the cross a PES packet boundary 38639 38640 if (buffer && buffer.length) { 38641 oldBuffer = buffer; 38642 buffer = new Uint8Array(oldBuffer.byteLength + packet.data.byteLength); 38643 buffer.set(oldBuffer); 38644 buffer.set(packet.data, oldBuffer.byteLength); 38645 } else { 38646 buffer = packet.data; 38647 } // unpack any ADTS frames which have been fully received 38648 // for details on the ADTS header, see http://wiki.multimedia.cx/index.php?title=ADTS 38649 38650 var skip; // We use i + 7 here because we want to be able to parse the entire header. 38651 // If we don't have enough bytes to do that, then we definitely won't have a full frame. 38652 38653 while (i + 7 < buffer.length) { 38654 // Look for the start of an ADTS header.. 38655 if (buffer[i] !== 0xff || (buffer[i + 1] & 0xf6) !== 0xf0) { 38656 if (typeof skip !== 'number') { 38657 skip = i; 38658 } // If a valid header was not found, jump one forward and attempt to 38659 // find a valid ADTS header starting at the next byte 38660 38661 i++; 38662 continue; 38663 } 38664 if (typeof skip === 'number') { 38665 this.skipWarn_(skip, i); 38666 skip = null; 38667 } // The protection skip bit tells us if we have 2 bytes of CRC data at the 38668 // end of the ADTS header 38669 38670 protectionSkipBytes = (~buffer[i + 1] & 0x01) * 2; // Frame length is a 13 bit integer starting 16 bits from the 38671 // end of the sync sequence 38672 // NOTE: frame length includes the size of the header 38673 38674 frameLength = 38675 ((buffer[i + 3] & 0x03) << 11) | (buffer[i + 4] << 3) | ((buffer[i + 5] & 0xe0) >> 5); 38676 sampleCount = ((buffer[i + 6] & 0x03) + 1) * 1024; 38677 adtsFrameDuration =
38678 (sampleCount * ONE_SECOND_IN_TS$2) / 38679 ADTS_SAMPLING_FREQUENCIES$1[(buffer[i + 2] & 0x3c) >>> 2]; // If we don't have enough data to actually finish this ADTS frame, 38680 // then we have to wait for more data 38681 38682 if (buffer.byteLength - i < frameLength) { 38683 break; 38684 } // Otherwise, deliver the complete AAC frame 38685 38686 this.trigger('data', { 38687 pts: packet.pts + frameNum * adtsFrameDuration, 38688 dts: packet.dts + frameNum * adtsFrameDuration, 38689 sampleCount: sampleCount, 38690 audioobjecttype: ((buffer[i + 2] >>> 6) & 0x03) + 1, 38691 channelcount: ((buffer[i + 2] & 1) << 2) | ((buffer[i + 3] & 0xc0) >>> 6), 38692 samplerate: ADTS_SAMPLING_FREQUENCIES$1[(buffer[i + 2] & 0x3c) >>> 2], 38693 samplingfrequencyindex: (buffer[i + 2] & 0x3c) >>> 2, 38694 // assume ISO/IEC 14496-12 AudioSampleEntry default of 16 38695 samplesize: 16, 38696 // data is the frame without it's header 38697 data: buffer.subarray(i + 7 + protectionSkipBytes, i + frameLength), 38698 }); 38699 frameNum++; 38700 i += frameLength; 38701 } 38702 if (typeof skip === 'number') { 38703 this.skipWarn_(skip, i); 38704 skip = null; 38705 } // remove processed bytes from the buffer. 38706 38707 buffer = buffer.subarray(i); 38708 }; 38709 this.flush = function () { 38710 frameNum = 0; 38711 this.trigger('done'); 38712 }; 38713 this.reset = function () { 38714 buffer = void 0; 38715 this.trigger('reset'); 38716 }; 38717 this.endTimeline = function () { 38718 buffer = void 0; 38719 this.trigger('endedtimeline'); 38720 }; 38721 }; 38722 AdtsStream$1.prototype = new Stream$3(); 38723 var adts = AdtsStream$1; 38724 /** 38725 * mux.js 38726 * 38727 * Copyright (c) Brightcove 38728 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 38729 */ 38730 38731 var ExpGolomb$1; 38732 /** 38733 * Parser for exponential Golomb codes, a variable-bitwidth number encoding 38734 * scheme used by h264. 38735 */ 38736 38737 ExpGolomb$1 = function (workingData) { 38738 var // the number of bytes left to examine in workingData 38739 workingBytesAvailable = workingData.byteLength, 38740 // the current word being examined 38741 workingWord = 0, 38742 // :uint 38743 // the number of bits left to examine in the current word 38744 workingBitsAvailable = 0; // :uint; 38745 // ():uint 38746 38747 this.length = function () { 38748 return 8 * workingBytesAvailable; 38749 }; // ():uint 38750 38751 this.bitsAvailable = function () { 38752 return 8 * workingBytesAvailable + workingBitsAvailable; 38753 }; // ():void 38754 38755 this.loadWord = function () { 38756 var position = workingData.byteLength - workingBytesAvailable, 38757 workingBytes = new Uint8Array(4), 38758 availableBytes = Math.min(4, workingBytesAvailable); 38759 if (availableBytes === 0) { 38760 throw new Error('no bytes available'); 38761 } 38762 workingBytes.set(workingData.subarray(position, position + availableBytes)); 38763 workingWord = new DataView(workingBytes.buffer).getUint32(0); // track the amount of workingData that has been processed 38764 38765 workingBitsAvailable = availableBytes * 8; 38766 workingBytesAvailable -= availableBytes; 38767 }; // (count:int):void 38768 38769 this.skipBits = function (count) { 38770 var skipBytes; // :int 38771 38772 if (workingBitsAvailable > count) { 38773 workingWord <<= count; 38774 workingBitsAvailable -= count; 38775 } else { 38776 count -= workingBitsAvailable; 38777 skipBytes = Math.floor(count / 8); 38778 count -= skipBytes * 8; 38779 workingBytesAvailable -= skipBytes; 38780 this.loadWord(); 38781 workingWord <<= count; 38782 workingBitsAvailable -= count; 38783 } 38784 }; // (size:int):uint 38785 38786 this.readBits = function (size) { 38787 var bits = Math.min(workingBitsAvailable, size), 38788 // :uint 38789 valu = workingWord >>> (32 - bits); // :uint 38790 // if size > 31, handle error 38791 38792 workingBitsAvailable -= bits; 38793 if (workingBitsAvailable > 0) { 38794 workingWord <<= bits; 38795 } else if (workingBytesAvailable > 0) { 38796 this.loadWord(); 38797 } 38798 bits = size - bits; 38799 if (bits > 0) { 38800 return (valu << bits) | this.readBits(bits); 38801 } 38802 return valu; 38803 }; // ():uint 38804 38805 this.skipLeadingZeros = function () { 38806 var leadingZeroCount; // :uint 38807 38808 for (leadingZeroCount = 0; leadingZeroCount < workingBitsAvailable;
vendor: 5,870 bytes, lines 38808-38996
38808 ++leadingZeroCount) { 38809 if ((workingWord & (0x80000000 >>> leadingZeroCount)) !== 0) { 38810 // the first bit of working word is 1 38811 workingWord <<= leadingZeroCount; 38812 workingBitsAvailable -= leadingZeroCount; 38813 return leadingZeroCount; 38814 } 38815 } // we exhausted workingWord and still have not found a 1 38816 38817 this.loadWord(); 38818 return leadingZeroCount + this.skipLeadingZeros(); 38819 }; // ():void 38820 38821 this.skipUnsignedExpGolomb = function () { 38822 this.skipBits(1 + this.skipLeadingZeros()); 38823 }; // ():void 38824 38825 this.skipExpGolomb = function () { 38826 this.skipBits(1 + this.skipLeadingZeros()); 38827 }; // ():uint 38828 38829 this.readUnsignedExpGolomb = function () { 38830 var clz = this.skipLeadingZeros(); // :uint 38831 38832 return this.readBits(clz + 1) - 1; 38833 }; // ():int 38834 38835 this.readExpGolomb = function () { 38836 var valu = this.readUnsignedExpGolomb(); // :int 38837 38838 if (0x01 & valu) { 38839 // the number is odd if the low order bit is set 38840 return (1 + valu) >>> 1; // add 1 to make it even, and divide by 2 38841 } 38842 return -1 * (valu >>> 1); // divide by two then make it negative 38843 }; // Some convenience functions 38844 // :Boolean 38845 38846 this.readBoolean = function () { 38847 return this.readBits(1) === 1; 38848 }; // ():int 38849 38850 this.readUnsignedByte = function () { 38851 return this.readBits(8); 38852 }; 38853 this.loadWord(); 38854 }; 38855 var expGolomb = ExpGolomb$1; 38856 /** 38857 * mux.js 38858 * 38859 * Copyright (c) Brightcove 38860 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 38861 */ 38862 38863 var Stream$2 = stream; 38864 var ExpGolomb = expGolomb; 38865 var H264Stream$1, NalByteStream; 38866 var PROFILES_WITH_OPTIONAL_SPS_DATA; 38867 /** 38868 * Accepts a NAL unit byte stream and unpacks the embedded NAL units. 38869 */ 38870 38871 NalByteStream = function () { 38872 var syncPoint = 0, 38873 i, 38874 buffer; 38875 NalByteStream.prototype.init.call(this); 38876 /* 38877 * Scans a byte stream and triggers a data event with the NAL units found. 38878 * @param {Object} data Event received from H264Stream 38879 * @param {Uint8Array} data.data The h264 byte stream to be scanned 38880 * 38881 * @see H264Stream.push 38882 */ 38883 38884 this.push = function (data) { 38885 var swapBuffer; 38886 if (!buffer) { 38887 buffer = data.data; 38888 } else { 38889 swapBuffer = new Uint8Array(buffer.byteLength + data.data.byteLength); 38890 swapBuffer.set(buffer); 38891 swapBuffer.set(data.data, buffer.byteLength); 38892 buffer = swapBuffer; 38893 } 38894 var len = buffer.byteLength; // Rec. ITU-T H.264, Annex B 38895 // scan for NAL unit boundaries 38896 // a match looks like this: 38897 // 0 0 1 .. NAL .. 0 0 1 38898 // ^ sync point ^ i 38899 // or this: 38900 // 0 0 1 .. NAL .. 0 0 0 38901 // ^ sync point ^ i 38902 // advance the sync point to a NAL start, if necessary 38903 38904 for (; syncPoint < len - 3; syncPoint++) { 38905 if (buffer[syncPoint + 2] === 1) { 38906 // the sync point is properly aligned 38907 i = syncPoint + 5; 38908 break; 38909 } 38910 } 38911 while (i < len) { 38912 // look at the current byte to determine if we've hit the end of 38913 // a NAL unit boundary 38914 switch (buffer[i]) { 38915 case 0: 38916 // skip past non-sync sequences 38917 if (buffer[i - 1] !== 0) { 38918 i += 2; 38919 break; 38920 } else if (buffer[i - 2] !== 0) { 38921 i++; 38922 break; 38923 } // deliver the NAL unit if it isn't empty 38924 38925 if (syncPoint + 3 !== i - 2) { 38926 this.trigger('data', buffer.subarray(syncPoint + 3, i - 2)); 38927 } // drop trailing zeroes 38928 38929 do { 38930 i++; 38931 } while (buffer[i] !== 1 && i < len); 38932 syncPoint = i - 2; 38933 i += 3; 38934 break; 38935 case 1: 38936 // skip past non-sync sequences 38937 if (buffer[i - 1] !== 0 || buffer[i - 2] !== 0) { 38938 i += 3; 38939 break; 38940 } // deliver the NAL unit 38941 38942 this.trigger('data', buffer.subarray(syncPoint + 3, i - 2)); 38943 syncPoint = i - 2; 38944 i += 3; 38945 break; 38946 default: 38947 // the current byte isn't a one or zero, so it cannot be part 38948 // of a sync sequence 38949 i += 3; 38950 break; 38951 } 38952 } // filter out the NAL units that were delivered 38953 38954 buffer = buffer.subarray(syncPoint); 38955 i -= syncPoint; 38956 syncPoint = 0; 38957 }; 38958 this.reset = function () { 38959 buffer = null; 38960 syncPoint = 0; 38961 this.trigger('reset'); 38962 }; 38963 this.flush = function () { 38964 // deliver the last buffered NAL unit 38965 if (buffer && buffer.byteLength > 3) { 38966 this.trigger('data', buffer.subarray(syncPoint + 3)); 38967 } // reset the stream state 38968 38969 buffer = null; 38970 syncPoint = 0; 38971 this.trigger('done'); 38972 }; 38973 this.endTimeline = function () { 38974 this.flush(); 38975 this.trigger('endedtimeline'); 38976 }; 38977 }; 38978 NalByteStream.prototype = new Stream$2(); // values of profile_idc that indicate additional fields are included in the SPS 38979 // see Recommendation ITU-T H.264 (4/2013), 38980 // 7.3.2.1.1 Sequence parameter set data syntax 38981 38982 PROFILES_WITH_OPTIONAL_SPS_DATA = { 38983 100: true, 38984 110: true, 38985 122: true, 38986 244: true, 38987 44: true, 38988 83: true, 38989 86: true, 38990 118: true, 38991 128: true, 38992 // TODO: the three profiles below don't 38993 // appear to have sps data in the specificiation anymore? 38994 138: true, 38995 139: true, 38996 134: true,
vendor: 1,799 bytes, lines 38996-39050
38996 38997 }; 38998 /** 38999 * Accepts input from a ElementaryStream and produces H.264 NAL unit data 39000 * events. 39001 */ 39002 39003 H264Stream$1 = function () { 39004 var nalByteStream = new NalByteStream(), 39005 self, 39006 trackId, 39007 currentPts, 39008 currentDts, 39009 discardEmulationPreventionBytes, 39010 readSequenceParameterSet, 39011 skipScalingList; 39012 H264Stream$1.prototype.init.call(this); 39013 self = this; 39014 /* 39015 * Pushes a packet from a stream onto the NalByteStream 39016 * 39017 * @param {Object} packet - A packet received from a stream 39018 * @param {Uint8Array} packet.data - The raw bytes of the packet 39019 * @param {Number} packet.dts - Decode timestamp of the packet 39020 * @param {Number} packet.pts - Presentation timestamp of the packet 39021 * @param {Number} packet.trackId - The id of the h264 track this packet came from 39022 * @param {('video'|'audio')} packet.type - The type of packet 39023 * 39024 */ 39025 39026 this.push = function (packet) { 39027 if (packet.type !== 'video') { 39028 return; 39029 } 39030 trackId = packet.trackId; 39031 currentPts = packet.pts; 39032 currentDts = packet.dts; 39033 nalByteStream.push(packet); 39034 }; 39035 /* 39036 * Identify NAL unit types and pass on the NALU, trackId, presentation and decode timestamps 39037 * for the NALUs to the next stream component. 39038 * Also, preprocess caption and sequence parameter NALUs. 39039 * 39040 * @param {Uint8Array} data - A NAL unit identified by `NalByteStream.push` 39041 * @see NalByteStream.push 39042 */ 39043 39044 nalByteStream.on('data', function (data) { 39045 var event = { 39046 trackId: trackId, 39047 pts: currentPts, 39048 dts: currentDts, 39049 data: data, 39050 nalUnitTypeCode: data[0] & 0x1f
vendor: 41,398 bytes, lines 39050-40169
39050, 39051 }; 39052 switch (event.nalUnitTypeCode) { 39053 case 0x05: 39054 event.nalUnitType = 'slice_layer_without_partitioning_rbsp_idr'; 39055 break; 39056 case 0x06: 39057 event.nalUnitType = 'sei_rbsp'; 39058 event.escapedRBSP = discardEmulationPreventionBytes(data.subarray(1)); 39059 break; 39060 case 0x07: 39061 event.nalUnitType = 'seq_parameter_set_rbsp'; 39062 event.escapedRBSP = discardEmulationPreventionBytes(data.subarray(1)); 39063 event.config = readSequenceParameterSet(event.escapedRBSP); 39064 break; 39065 case 0x08: 39066 event.nalUnitType = 'pic_parameter_set_rbsp'; 39067 break; 39068 case 0x09: 39069 event.nalUnitType = 'access_unit_delimiter_rbsp'; 39070 break; 39071 } // This triggers data on the H264Stream 39072 39073 self.trigger('data', event); 39074 }); 39075 nalByteStream.on('done', function () { 39076 self.trigger('done'); 39077 }); 39078 nalByteStream.on('partialdone', function () { 39079 self.trigger('partialdone'); 39080 }); 39081 nalByteStream.on('reset', function () { 39082 self.trigger('reset'); 39083 }); 39084 nalByteStream.on('endedtimeline', function () { 39085 self.trigger('endedtimeline'); 39086 }); 39087 this.flush = function () { 39088 nalByteStream.flush(); 39089 }; 39090 this.partialFlush = function () { 39091 nalByteStream.partialFlush(); 39092 }; 39093 this.reset = function () { 39094 nalByteStream.reset(); 39095 }; 39096 this.endTimeline = function () { 39097 nalByteStream.endTimeline(); 39098 }; 39099 /** 39100 * Advance the ExpGolomb decoder past a scaling list. The scaling 39101 * list is optionally transmitted as part of a sequence parameter 39102 * set and is not relevant to transmuxing. 39103 * @param count {number} the number of entries in this scaling list 39104 * @param expGolombDecoder {object} an ExpGolomb pointed to the 39105 * start of a scaling list 39106 * @see Recommendation ITU-T H.264, Section 7.3.2.1.1.1 39107 */ 39108 39109 skipScalingList = function (count, expGolombDecoder) { 39110 var lastScale = 8, 39111 nextScale = 8, 39112 j, 39113 deltaScale; 39114 for (j = 0; j < count; j++) { 39115 if (nextScale !== 0) { 39116 deltaScale = expGolombDecoder.readExpGolomb(); 39117 nextScale = (lastScale + deltaScale + 256) % 256; 39118 } 39119 lastScale = nextScale === 0 ? lastScale : nextScale; 39120 } 39121 }; 39122 /** 39123 * Expunge any "Emulation Prevention" bytes from a "Raw Byte 39124 * Sequence Payload" 39125 * @param data {Uint8Array} the bytes of a RBSP from a NAL 39126 * unit 39127 * @return {Uint8Array} the RBSP without any Emulation 39128 * Prevention Bytes 39129 */ 39130 39131 discardEmulationPreventionBytes = function (data) { 39132 var length = data.byteLength, 39133 emulationPreventionBytesPositions = [], 39134 i = 1, 39135 newLength, 39136 newData; // Find all `Emulation Prevention Bytes` 39137 39138 while (i < length - 2) { 39139 if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0x03) { 39140 emulationPreventionBytesPositions.push(i + 2); 39141 i += 2; 39142 } else { 39143 i++; 39144 } 39145 } // If no Emulation Prevention Bytes were found just return the original 39146 // array 39147 39148 if (emulationPreventionBytesPositions.length === 0) { 39149 return data; 39150 } // Create a new array to hold the NAL unit data 39151 39152 newLength = length - emulationPreventionBytesPositions.length; 39153 newData = new Uint8Array(newLength); 39154 var sourceIndex = 0; 39155 for (i = 0; i < newLength; sourceIndex++, i++) { 39156 if (sourceIndex === emulationPreventionBytesPositions[0]) { 39157 // Skip this byte 39158 sourceIndex++; // Remove this position index 39159 39160 emulationPreventionBytesPositions.shift(); 39161 } 39162 newData[i] = data[sourceIndex]; 39163 } 39164 return newData; 39165 }; 39166 /** 39167 * Read a sequence parameter set and return some interesting video 39168 * properties. A sequence parameter set is the H264 metadata that 39169 * describes the properties of upcoming video frames. 39170 * @param data {Uint8Array} the bytes of a sequence parameter set 39171 * @return {object} an object with configuration parsed from the 39172 * sequence parameter set, including the dimensions of the 39173 * associated video frames. 39174 */ 39175 39176 readSequenceParameterSet = function (data) { 39177 var frameCropLeftOffset = 0, 39178 frameCropRightOffset = 0, 39179 frameCropTopOffset = 0, 39180 frameCropBottomOffset = 0, 39181 expGolombDecoder, 39182 profileIdc, 39183 levelIdc, 39184 profileCompatibility, 39185 chromaFormatIdc, 39186 picOrderCntType, 39187 numRefFramesInPicOrderCntCycle, 39188 picWidthInMbsMinus1, 39189 picHeightInMapUnitsMinus1, 39190 frameMbsOnlyFlag, 39191 scalingListCount, 39192 sarRatio = [1, 1], 39193 aspectRatioIdc, 39194 i; 39195 expGolombDecoder = new ExpGolomb(data); 39196 profileIdc = expGolombDecoder.readUnsignedByte(); // profile_idc 39197 39198 profileCompatibility = expGolombDecoder.readUnsignedByte(); // constraint_set[0-5]_flag 39199 39200 levelIdc = expGolombDecoder.readUnsignedByte(); // level_idc u(8) 39201 39202 expGolombDecoder.skipUnsignedExpGolomb(); // seq_parameter_set_id 39203 // some profiles have more optional data we don't need 39204 39205 if (PROFILES_WITH_OPTIONAL_SPS_DATA[profileIdc]) { 39206 chromaFormatIdc = expGolombDecoder.readUnsignedExpGolomb(); 39207 if (chromaFormatIdc === 3) { 39208 expGolombDecoder.skipBits(1); // separate_colour_plane_flag 39209 } 39210 expGolombDecoder.skipUnsignedExpGolomb(); // bit_depth_luma_minus8 39211 39212 expGolombDecoder.skipUnsignedExpGolomb(); // bit_depth_chroma_minus8 39213 39214 expGolombDecoder.skipBits(1); // qpprime_y_zero_transform_bypass_flag 39215 39216 if (expGolombDecoder.readBoolean()) { 39217 // seq_scaling_matrix_present_flag 39218 scalingListCount = chromaFormatIdc !== 3 ? 8 : 12; 39219 for (i = 0; i < scalingListCount; i++) { 39220 if (expGolombDecoder.readBoolean()) { 39221 // seq_scaling_list_present_flag[ i ] 39222 if (i < 6) { 39223 skipScalingList(16, expGolombDecoder); 39224 } else { 39225 skipScalingList(64, expGolombDecoder); 39226 } 39227 } 39228 } 39229 } 39230 } 39231 expGolombDecoder.skipUnsignedExpGolomb(); // log2_max_frame_num_minus4 39232 39233 picOrderCntType = expGolombDecoder.readUnsignedExpGolomb(); 39234 if (picOrderCntType === 0) { 39235 expGolombDecoder.readUnsignedExpGolomb(); // log2_max_pic_order_cnt_lsb_minus4 39236 } else if (picOrderCntType === 1) { 39237 expGolombDecoder.skipBits(1); // delta_pic_order_always_zero_flag 39238 39239 expGolombDecoder.skipExpGolomb(); // offset_for_non_ref_pic 39240 39241 expGolombDecoder.skipExpGolomb(); // offset_for_top_to_bottom_field 39242 39243 numRefFramesInPicOrderCntCycle = expGolombDecoder.readUnsignedExpGolomb(); 39244 for (i = 0; i < numRefFramesInPicOrderCntCycle; i++) { 39245 expGolombDecoder.skipExpGolomb(); // offset_for_ref_frame[ i ] 39246 } 39247 } 39248 expGolombDecoder.skipUnsignedExpGolomb(); // max_num_ref_frames 39249 39250 expGolombDecoder.skipBits(1); // gaps_in_frame_num_value_allowed_flag 39251 39252 picWidthInMbsMinus1 = expGolombDecoder.readUnsignedExpGolomb(); 39253 picHeightInMapUnitsMinus1 = expGolombDecoder.readUnsignedExpGolomb(); 39254 frameMbsOnlyFlag = expGolombDecoder.readBits(1); 39255 if (frameMbsOnlyFlag === 0) { 39256 expGolombDecoder.skipBits(1); // mb_adaptive_frame_field_flag 39257 } 39258 expGolombDecoder.skipBits(1); // direct_8x8_inference_flag 39259 39260 if (expGolombDecoder.readBoolean()) { 39261 // frame_cropping_flag 39262 frameCropLeftOffset = expGolombDecoder.readUnsignedExpGolomb(); 39263 frameCropRightOffset = expGolombDecoder.readUnsignedExpGolomb(); 39264 frameCropTopOffset = expGolombDecoder.readUnsignedExpGolomb(); 39265 frameCropBottomOffset = expGolombDecoder.readUnsignedExpGolomb(); 39266 } 39267 if (expGolombDecoder.readBoolean()) { 39268 // vui_parameters_present_flag 39269 if (expGolombDecoder.readBoolean()) { 39270 // aspect_ratio_info_present_flag 39271 aspectRatioIdc = expGolombDecoder.readUnsignedByte(); 39272 switch (aspectRatioIdc) { 39273 case 1: 39274 sarRatio = [1, 1]; 39275 break; 39276 case 2: 39277 sarRatio = [12, 11]; 39278 break; 39279 case 3: 39280 sarRatio = [10, 11]; 39281 break; 39282 case 4: 39283 sarRatio = [16, 11]; 39284 break; 39285 case 5: 39286 sarRatio = [40, 33]; 39287 break; 39288 case 6: 39289 sarRatio = [24, 11]; 39290 break; 39291 case 7: 39292 sarRatio = [20, 11]; 39293 break; 39294 case 8: 39295 sarRatio = [32, 11]; 39296 break; 39297 case 9: 39298 sarRatio = [80, 33]; 39299 break; 39300 case 10: 39301 sarRatio = [18, 11]; 39302 break; 39303 case 11: 39304 sarRatio = [15, 11]; 39305 break; 39306 case 12: 39307 sarRatio = [64, 33]; 39308 break; 39309 case 13: 39310 sarRatio = [160, 99]; 39311 break; 39312 case 14: 39313 sarRatio = [4, 3]; 39314 break; 39315 case 15: 39316 sarRatio = [3, 2]; 39317 break; 39318 case 16: 39319 sarRatio = [2, 1]; 39320 break; 39321 case 255: { 39322 sarRatio = [ 39323 (expGolombDecoder.readUnsignedByte() << 8) | expGolombDecoder.readUnsignedByte(), 39324 (expGolombDecoder.readUnsignedByte() << 8) | expGolombDecoder.readUnsignedByte(), 39325 ]; 39326 break; 39327 } 39328 } 39329 if (sarRatio) { 39330 sarRatio[0] / sarRatio[1]; 39331 } 39332 } 39333 } 39334 return { 39335 profileIdc: profileIdc, 39336 levelIdc: levelIdc, 39337 profileCompatibility: profileCompatibility, 39338 width: 39339 (picWidthInMbsMinus1 + 1) * 16 - frameCropLeftOffset * 2 - frameCropRightOffset * 2, 39340 height: 39341 (2 - frameMbsOnlyFlag) * (picHeightInMapUnitsMinus1 + 1) * 16 - 39342 frameCropTopOffset * 2 - 39343 frameCropBottomOffset * 2, 39344 // sar is sample aspect ratio 39345 sarRatio: sarRatio, 39346 }; 39347 }; 39348 }; 39349 H264Stream$1.prototype = new Stream$2(); 39350 var h264 = { 39351 H264Stream: H264Stream$1, 39352 NalByteStream: NalByteStream, 39353 }; 39354 /** 39355 * mux.js 39356 * 39357 * Copyright (c) Brightcove 39358 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 39359 * 39360 * Utilities to detect basic properties and metadata about Aac data. 39361 */ 39362 39363 var ADTS_SAMPLING_FREQUENCIES = [ 39364 96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350, 39365 ]; 39366 var parseId3TagSize = function (header, byteIndex) { 39367 var returnSize = 39368 (header[byteIndex + 6] << 21) | 39369 (header[byteIndex + 7] << 14) | 39370 (header[byteIndex + 8] << 7) | 39371 header[byteIndex + 9], 39372 flags = header[byteIndex + 5], 39373 footerPresent = (flags & 16) >> 4; // if we get a negative returnSize clamp it to 0 39374 39375 returnSize = returnSize >= 0 ? returnSize : 0; 39376 if (footerPresent) { 39377 return returnSize + 20; 39378 } 39379 return returnSize + 10; 39380 }; 39381 var getId3Offset = function (data, offset) { 39382 if ( 39383 data.length - offset < 10 || 39384 data[offset] !== 'I'.charCodeAt(0) || 39385 data[offset + 1] !== 'D'.charCodeAt(0) || 39386 data[offset + 2] !== '3'.charCodeAt(0) 39387 ) { 39388 return offset; 39389 } 39390 offset += parseId3TagSize(data, offset); 39391 return getId3Offset(data, offset); 39392 }; // TODO: use vhs-utils 39393 39394 var isLikelyAacData$1 = function (data) { 39395 var offset = getId3Offset(data, 0); 39396 return ( 39397 data.length >= offset + 2 && 39398 (data[offset] & 0xff) === 0xff && 39399 (data[offset + 1] & 0xf0) === 0xf0 && 39400 // verify that the 2 layer bits are 0, aka this 39401 // is not mp3 data but aac data. 39402 (data[offset + 1] & 0x16) === 0x10 39403 ); 39404 }; 39405 var parseSyncSafeInteger = function (data) { 39406 return (data[0] << 21) | (data[1] << 14) | (data[2] << 7) | data[3]; 39407 }; // return a percent-encoded representation of the specified byte range 39408 // @see http://en.wikipedia.org/wiki/Percent-encoding 39409 39410 var percentEncode = function (bytes, start, end) { 39411 var i, 39412 result = ''; 39413 for (i = start; i < end; i++) { 39414 result += '%' + ('00' + bytes[i].toString(16)).slice(-2); 39415 } 39416 return result; 39417 }; // return the string representation of the specified byte range, 39418 // interpreted as ISO-8859-1. 39419 39420 var parseIso88591 = function (bytes, start, end) { 39421 return unescape(percentEncode(bytes, start, end)); // jshint ignore:line 39422 }; 39423 var parseAdtsSize = function (header, byteIndex) { 39424 var lowThree = (header[byteIndex + 5] & 0xe0) >> 5, 39425 middle = header[byteIndex + 4] << 3, 39426 highTwo = header[byteIndex + 3] & (0x3 << 11); 39427 return highTwo | middle | lowThree; 39428 }; 39429 var parseType$5 = function (header, byteIndex) { 39430 if ( 39431 header[byteIndex] === 'I'.charCodeAt(0) && 39432 header[byteIndex + 1] === 'D'.charCodeAt(0) && 39433 header[byteIndex + 2] === '3'.charCodeAt(0) 39434 ) { 39435 return 'timed-metadata'; 39436 } else if (header[byteIndex] & (0xff === 0xff) && (header[byteIndex + 1] & 0xf0) === 0xf0) { 39437 return 'audio'; 39438 } 39439 return null; 39440 }; 39441 var parseSampleRate = function (packet) { 39442 var i = 0; 39443 while (i + 5 < packet.length) { 39444 if (packet[i] !== 0xff || (packet[i + 1] & 0xf6) !== 0xf0) { 39445 // If a valid header was not found, jump one forward and attempt to 39446 // find a valid ADTS header starting at the next byte 39447 i++; 39448 continue; 39449 } 39450 return ADTS_SAMPLING_FREQUENCIES[(packet[i + 2] & 0x3c) >>> 2]; 39451 } 39452 return null; 39453 }; 39454 var parseAacTimestamp = function (packet) { 39455 var frameStart, frameSize, frame, frameHeader; // find the start of the first frame and the end of the tag 39456 39457 frameStart = 10; 39458 if (packet[5] & 0x40) { 39459 // advance the frame start past the extended header 39460 frameStart += 4; // header size field 39461 39462 frameStart += parseSyncSafeInteger(packet.subarray(10, 14)); 39463 } // parse one or more ID3 frames 39464 // http://id3.org/id3v2.3.0#ID3v2_frame_overview 39465 39466 do { 39467 // determine the number of bytes in this frame 39468 frameSize = parseSyncSafeInteger(packet.subarray(frameStart + 4, frameStart + 8)); 39469 if (frameSize < 1) { 39470 return null; 39471 } 39472 frameHeader = String.fromCharCode( 39473 packet[frameStart], 39474 packet[frameStart + 1], 39475 packet[frameStart + 2], 39476 packet[frameStart + 3] 39477 ); 39478 if (frameHeader === 'PRIV') { 39479 frame = packet.subarray(frameStart + 10, frameStart + frameSize + 10); 39480 for (var i = 0; i < frame.byteLength; i++) { 39481 if (frame[i] === 0) { 39482 var owner = parseIso88591(frame, 0, i); 39483 if (owner === 'com.apple.streaming.transportStreamTimestamp') { 39484 var d = frame.subarray(i + 1); 39485 var size = 39486 ((d[3] & 0x01) << 30) | (d[4] << 22) | (d[5] << 14) | (d[6] << 6) | (d[7] >>> 2); 39487 size *= 4; 39488 size += d[7] & 0x03; 39489 return size; 39490 } 39491 break; 39492 } 39493 } 39494 } 39495 frameStart += 10; // advance past the frame header 39496 39497 frameStart += frameSize; // advance past the frame body 39498 } while (frameStart < packet.byteLength); 39499 return null; 39500 }; 39501 var utils = { 39502 isLikelyAacData: isLikelyAacData$1, 39503 parseId3TagSize: parseId3TagSize, 39504 parseAdtsSize: parseAdtsSize, 39505 parseType: parseType$5, 39506 parseSampleRate: parseSampleRate, 39507 parseAacTimestamp: parseAacTimestamp, 39508 }; 39509 /** 39510 * mux.js 39511 * 39512 * Copyright (c) Brightcove 39513 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 39514 * 39515 * A stream-based aac to mp4 converter. This utility can be used to 39516 * deliver mp4s to a SourceBuffer on platforms that support native 39517 * Media Source Extensions. 39518 */ 39519 39520 var Stream$1 = stream; 39521 var aacUtils = utils; // Constants 39522 39523 var AacStream$1; 39524 /** 39525 * Splits an incoming stream of binary data into ADTS and ID3 Frames. 39526 */ 39527 39528 AacStream$1 = function () { 39529 var everything = new Uint8Array(), 39530 timeStamp = 0; 39531 AacStream$1.prototype.init.call(this); 39532 this.setTimestamp = function (timestamp) { 39533 timeStamp = timestamp; 39534 }; 39535 this.push = function (bytes) { 39536 var frameSize = 0, 39537 byteIndex = 0, 39538 bytesLeft, 39539 chunk, 39540 packet, 39541 tempLength; // If there are bytes remaining from the last segment, prepend them to the 39542 // bytes that were pushed in 39543 39544 if (everything.length) { 39545 tempLength = everything.length; 39546 everything = new Uint8Array(bytes.byteLength + tempLength); 39547 everything.set(everything.subarray(0, tempLength)); 39548 everything.set(bytes, tempLength); 39549 } else { 39550 everything = bytes; 39551 } 39552 while (everything.length - byteIndex >= 3) { 39553 if ( 39554 everything[byteIndex] === 'I'.charCodeAt(0) && 39555 everything[byteIndex + 1] === 'D'.charCodeAt(0) && 39556 everything[byteIndex + 2] === '3'.charCodeAt(0) 39557 ) { 39558 // Exit early because we don't have enough to parse 39559 // the ID3 tag header 39560 if (everything.length - byteIndex < 10) { 39561 break; 39562 } // check framesize 39563 39564 frameSize = aacUtils.parseId3TagSize(everything, byteIndex); // Exit early if we don't have enough in the buffer 39565 // to emit a full packet 39566 // Add to byteIndex to support multiple ID3 tags in sequence 39567 39568 if (byteIndex + frameSize > everything.length) { 39569 break; 39570 } 39571 chunk = { 39572 type: 'timed-metadata', 39573 data: everything.subarray(byteIndex, byteIndex + frameSize), 39574 }; 39575 this.trigger('data', chunk); 39576 byteIndex += frameSize; 39577 continue; 39578 } else if ( 39579 (everything[byteIndex] & 0xff) === 0xff && 39580 (everything[byteIndex + 1] & 0xf0) === 0xf0 39581 ) { 39582 // Exit early because we don't have enough to parse 39583 // the ADTS frame header 39584 if (everything.length - byteIndex < 7) { 39585 break; 39586 } 39587 frameSize = aacUtils.parseAdtsSize(everything, byteIndex); // Exit early if we don't have enough in the buffer 39588 // to emit a full packet 39589 39590 if (byteIndex + frameSize > everything.length) { 39591 break; 39592 } 39593 packet = { 39594 type: 'audio', 39595 data: everything.subarray(byteIndex, byteIndex + frameSize), 39596 pts: timeStamp, 39597 dts: timeStamp, 39598 }; 39599 this.trigger('data', packet); 39600 byteIndex += frameSize; 39601 continue; 39602 } 39603 byteIndex++; 39604 } 39605 bytesLeft = everything.length - byteIndex; 39606 if (bytesLeft > 0) { 39607 everything = everything.subarray(byteIndex); 39608 } else { 39609 everything = new Uint8Array(); 39610 } 39611 }; 39612 this.reset = function () { 39613 everything = new Uint8Array(); 39614 this.trigger('reset'); 39615 }; 39616 this.endTimeline = function () { 39617 everything = new Uint8Array(); 39618 this.trigger('endedtimeline'); 39619 }; 39620 }; 39621 AacStream$1.prototype = new Stream$1(); 39622 var aac = AacStream$1; 39623 var AUDIO_PROPERTIES$1 = [ 39624 'audioobjecttype', 39625 'channelcount', 39626 'samplerate', 39627 'samplingfrequencyindex', 39628 'samplesize', 39629 ]; 39630 var audioProperties = AUDIO_PROPERTIES$1; 39631 var VIDEO_PROPERTIES$1 = [ 39632 'width', 39633 'height', 39634 'profileIdc', 39635 'levelIdc', 39636 'profileCompatibility', 39637 'sarRatio', 39638 ]; 39639 var videoProperties = VIDEO_PROPERTIES$1; 39640 /** 39641 * mux.js 39642 * 39643 * Copyright (c) Brightcove 39644 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 39645 * 39646 * A stream-based mp2t to mp4 converter. This utility can be used to 39647 * deliver mp4s to a SourceBuffer on platforms that support native 39648 * Media Source Extensions. 39649 */ 39650 39651 var Stream = stream; 39652 var mp4 = mp4Generator; 39653 var frameUtils = frameUtils$1; 39654 var audioFrameUtils = audioFrameUtils$1; 39655 var trackDecodeInfo = trackDecodeInfo$1; 39656 var m2ts = m2ts_1; 39657 var clock = clock$2; 39658 var AdtsStream = adts; 39659 var H264Stream = h264.H264Stream; 39660 var AacStream = aac; 39661 var isLikelyAacData = utils.isLikelyAacData; 39662 var ONE_SECOND_IN_TS$1 = clock$2.ONE_SECOND_IN_TS; 39663 var AUDIO_PROPERTIES = audioProperties; 39664 var VIDEO_PROPERTIES = videoProperties; // object types 39665 39666 var VideoSegmentStream, AudioSegmentStream, Transmuxer, CoalesceStream; 39667 var retriggerForStream = function (key, event) { 39668 event.stream = key; 39669 this.trigger('log', event); 39670 }; 39671 var addPipelineLogRetriggers = function (transmuxer, pipeline) { 39672 var keys = Object.keys(pipeline); 39673 for (var i = 0; i < keys.length; i++) { 39674 var key = keys[i]; // skip non-stream keys and headOfPipeline 39675 // which is just a duplicate 39676 39677 if (key === 'headOfPipeline' || !pipeline[key].on) { 39678 continue; 39679 } 39680 pipeline[key].on('log', retriggerForStream.bind(transmuxer, key)); 39681 } 39682 }; 39683 /** 39684 * Compare two arrays (even typed) for same-ness 39685 */ 39686 39687 var arrayEquals = function (a, b) { 39688 var i; 39689 if (a.length !== b.length) { 39690 return false; 39691 } // compare the value of each element in the array 39692 39693 for (i = 0; i < a.length; i++) { 39694 if (a[i] !== b[i]) { 39695 return false; 39696 } 39697 } 39698 return true; 39699 }; 39700 var generateSegmentTimingInfo = function ( 39701 baseMediaDecodeTime, 39702 startDts, 39703 startPts, 39704 endDts, 39705 endPts, 39706 prependedContentDuration 39707 ) { 39708 var ptsOffsetFromDts = startPts - startDts, 39709 decodeDuration = endDts - startDts, 39710 presentationDuration = endPts - startPts; // The PTS and DTS values are based on the actual stream times from the segment, 39711 // however, the player time values will reflect a start from the baseMediaDecodeTime. 39712 // In order to provide relevant values for the player times, base timing info on the 39713 // baseMediaDecodeTime and the DTS and PTS durations of the segment. 39714 39715 return { 39716 start: { 39717 dts: baseMediaDecodeTime, 39718 pts: baseMediaDecodeTime + ptsOffsetFromDts, 39719 }, 39720 end: { 39721 dts: baseMediaDecodeTime + decodeDuration, 39722 pts: baseMediaDecodeTime + presentationDuration, 39723 }, 39724 prependedContentDuration: prependedContentDuration, 39725 baseMediaDecodeTime: baseMediaDecodeTime, 39726 }; 39727 }; 39728 /** 39729 * Constructs a single-track, ISO BMFF media segment from AAC data 39730 * events. The output of this stream can be fed to a SourceBuffer 39731 * configured with a suitable initialization segment. 39732 * @param track {object} track metadata configuration 39733 * @param options {object} transmuxer options object 39734 * @param options.keepOriginalTimestamps {boolean} If true, keep the timestamps 39735 * in the source; false to adjust the first segment to start at 0. 39736 */ 39737 39738 AudioSegmentStream = function (track, options) { 39739 var adtsFrames = [], 39740 sequenceNumber, 39741 earliestAllowedDts = 0, 39742 audioAppendStartTs = 0, 39743 videoBaseMediaDecodeTime = Infinity; 39744 options = options || {}; 39745 sequenceNumber = options.firstSequenceNumber || 0; 39746 AudioSegmentStream.prototype.init.call(this); 39747 this.push = function (data) { 39748 trackDecodeInfo.collectDtsInfo(track, data); 39749 if (track) { 39750 AUDIO_PROPERTIES.forEach(function (prop) { 39751 track[prop] = data[prop]; 39752 }); 39753 } // buffer audio data until end() is called 39754 39755 adtsFrames.push(data); 39756 }; 39757 this.setEarliestDts = function (earliestDts) { 39758 earliestAllowedDts = earliestDts; 39759 }; 39760 this.setVideoBaseMediaDecodeTime = function (baseMediaDecodeTime) { 39761 videoBaseMediaDecodeTime = baseMediaDecodeTime; 39762 }; 39763 this.setAudioAppendStart = function (timestamp) { 39764 audioAppendStartTs = timestamp; 39765 }; 39766 this.flush = function () { 39767 var frames, 39768 moof, 39769 mdat, 39770 boxes, 39771 frameDuration, 39772 segmentDuration, 39773 videoClockCyclesOfSilencePrefixed; // return early if no audio data has been observed 39774 39775 if (adtsFrames.length === 0) { 39776 this.trigger('done', 'AudioSegmentStream'); 39777 return; 39778 } 39779 frames = audioFrameUtils.trimAdtsFramesByEarliestDts(adtsFrames, track, earliestAllowedDts); 39780 track.baseMediaDecodeTime = trackDecodeInfo.calculateTrackBaseMediaDecodeTime( 39781 track, 39782 options.keepOriginalTimestamps 39783 ); // amount of audio filled but the value is in video clock rather than audio clock 39784 39785 videoClockCyclesOfSilencePrefixed = audioFrameUtils.prefixWithSilence( 39786 track, 39787 frames, 39788 audioAppendStartTs, 39789 videoBaseMediaDecodeTime 39790 ); // we have to build the index from byte locations to 39791 // samples (that is, adts frames) in the audio data 39792 39793 track.samples = audioFrameUtils.generateSampleTable(frames); // concatenate the audio data to constuct the mdat 39794 39795 mdat = mp4.mdat(audioFrameUtils.concatenateFrameData(frames)); 39796 adtsFrames = []; 39797 moof = mp4.moof(sequenceNumber, [track]); 39798 boxes = new Uint8Array(moof.byteLength + mdat.byteLength); // bump the sequence number for next time 39799 39800 sequenceNumber++; 39801 boxes.set(moof); 39802 boxes.set(mdat, moof.byteLength); 39803 trackDecodeInfo.clearDtsInfo(track); 39804 frameDuration = Math.ceil((ONE_SECOND_IN_TS$1 * 1024) / track.samplerate); // TODO this check was added to maintain backwards compatibility (particularly with 39805 // tests) on adding the timingInfo event. However, it seems unlikely that there's a 39806 // valid use-case where an init segment/data should be triggered without associated 39807 // frames. Leaving for now, but should be looked into. 39808 39809 if (frames.length) { 39810 segmentDuration = frames.length * frameDuration; 39811 this.trigger( 39812 'segmentTimingInfo', 39813 generateSegmentTimingInfo( 39814 // The audio track's baseMediaDecodeTime is in audio clock cycles, but the 39815 // frame info is in video clock cycles. Convert to match expectation of 39816 // listeners (that all timestamps will be based on video clock cycles). 39817 clock.audioTsToVideoTs(track.baseMediaDecodeTime, track.samplerate), 39818 // frame times are already in video clock, as is segment duration 39819 frames[0].dts, 39820 frames[0].pts, 39821 frames[0].dts + segmentDuration, 39822 frames[0].pts + segmentDuration, 39823 videoClockCyclesOfSilencePrefixed || 0 39824 ) 39825 ); 39826 this.trigger('timingInfo', { 39827 start: frames[0].pts, 39828 end: frames[0].pts + segmentDuration, 39829 }); 39830 } 39831 this.trigger('data', { 39832 track: track, 39833 boxes: boxes, 39834 }); 39835 this.trigger('done', 'AudioSegmentStream'); 39836 }; 39837 this.reset = function () { 39838 trackDecodeInfo.clearDtsInfo(track); 39839 adtsFrames = []; 39840 this.trigger('reset'); 39841 }; 39842 }; 39843 AudioSegmentStream.prototype = new Stream(); 39844 /** 39845 * Constructs a single-track, ISO BMFF media segment from H264 data 39846 * events. The output of this stream can be fed to a SourceBuffer 39847 * configured with a suitable initialization segment. 39848 * @param track {object} track metadata configuration 39849 * @param options {object} transmuxer options object 39850 * @param options.alignGopsAtEnd {boolean} If true, start from the end of the 39851 * gopsToAlignWith list when attempting to align gop pts 39852 * @param options.keepOriginalTimestamps {boolean} If true, keep the timestamps 39853 * in the source; false to adjust the first segment to start at 0. 39854 */ 39855 39856 VideoSegmentStream = function (track, options) { 39857 var sequenceNumber, 39858 nalUnits = [], 39859 gopsToAlignWith = [], 39860 config, 39861 pps; 39862 options = options || {}; 39863 sequenceNumber = options.firstSequenceNumber || 0; 39864 VideoSegmentStream.prototype.init.call(this); 39865 delete track.minPTS; 39866 this.gopCache_ = []; 39867 /** 39868 * Constructs a ISO BMFF segment given H264 nalUnits 39869 * @param {Object} nalUnit A data event representing a nalUnit 39870 * @param {String} nalUnit.nalUnitType 39871 * @param {Object} nalUnit.config Properties for a mp4 track 39872 * @param {Uint8Array} nalUnit.data The nalUnit bytes 39873 * @see lib/codecs/h264.js 39874 **/ 39875 39876 this.push = function (nalUnit) { 39877 trackDecodeInfo.collectDtsInfo(track, nalUnit); // record the track config 39878 39879 if (nalUnit.nalUnitType === 'seq_parameter_set_rbsp' && !config) { 39880 config = nalUnit.config; 39881 track.sps = [nalUnit.data]; 39882 VIDEO_PROPERTIES.forEach(function (prop) { 39883 track[prop] = config[prop]; 39884 }, this); 39885 } 39886 if (nalUnit.nalUnitType === 'pic_parameter_set_rbsp' && !pps) { 39887 pps = nalUnit.data; 39888 track.pps = [nalUnit.data]; 39889 } // buffer video until flush() is called 39890 39891 nalUnits.push(nalUnit); 39892 }; 39893 /** 39894 * Pass constructed ISO BMFF track and boxes on to the 39895 * next stream in the pipeline 39896 **/ 39897 39898 this.flush = function () { 39899 var frames, 39900 gopForFusion, 39901 gops, 39902 moof, 39903 mdat, 39904 boxes, 39905 prependedContentDuration = 0, 39906 firstGop, 39907 lastGop; // Throw away nalUnits at the start of the byte stream until 39908 // we find the first AUD 39909 39910 while (nalUnits.length) { 39911 if (nalUnits[0].nalUnitType === 'access_unit_delimiter_rbsp') { 39912 break; 39913 } 39914 nalUnits.shift(); 39915 } // Return early if no video data has been observed 39916 39917 if (nalUnits.length === 0) { 39918 this.resetStream_(); 39919 this.trigger('done', 'VideoSegmentStream'); 39920 return; 39921 } // Organize the raw nal-units into arrays that represent 39922 // higher-level constructs such as frames and gops 39923 // (group-of-pictures) 39924 39925 frames = frameUtils.groupNalsIntoFrames(nalUnits); 39926 gops = frameUtils.groupFramesIntoGops(frames); // If the first frame of this fragment is not a keyframe we have 39927 // a problem since MSE (on Chrome) requires a leading keyframe. 39928 // 39929 // We have two approaches to repairing this situation: 39930 // 1) GOP-FUSION: 39931 // This is where we keep track of the GOPS (group-of-pictures) 39932 // from previous fragments and attempt to find one that we can 39933 // prepend to the current fragment in order to create a valid 39934 // fragment. 39935 // 2) KEYFRAME-PULLING: 39936 // Here we search for the first keyframe in the fragment and 39937 // throw away all the frames between the start of the fragment 39938 // and that keyframe. We then extend the duration and pull the 39939 // PTS of the keyframe forward so that it covers the time range 39940 // of the frames that were disposed of. 39941 // 39942 // #1 is far prefereable over #2 which can cause "stuttering" but 39943 // requires more things to be just right. 39944 39945 if (!gops[0][0].keyFrame) { 39946 // Search for a gop for fusion from our gopCache 39947 gopForFusion = this.getGopForFusion_(nalUnits[0], track); 39948 if (gopForFusion) { 39949 // in order to provide more accurate timing information about the segment, save 39950 // the number of seconds prepended to the original segment due to GOP fusion 39951 prependedContentDuration = gopForFusion.duration; 39952 gops.unshift(gopForFusion); // Adjust Gops' metadata to account for the inclusion of the 39953 // new gop at the beginning 39954 39955 gops.byteLength += gopForFusion.byteLength; 39956 gops.nalCount += gopForFusion.nalCount; 39957 gops.pts = gopForFusion.pts; 39958 gops.dts = gopForFusion.dts; 39959 gops.duration += gopForFusion.duration; 39960 } else { 39961 // If we didn't find a candidate gop fall back to keyframe-pulling 39962 gops = frameUtils.extendFirstKeyFrame(gops); 39963 } 39964 } // Trim gops to align with gopsToAlignWith 39965 39966 if (gopsToAlignWith.length) { 39967 var alignedGops; 39968 if (options.alignGopsAtEnd) { 39969 alignedGops = this.alignGopsAtEnd_(gops); 39970 } else { 39971 alignedGops = this.alignGopsAtStart_(gops); 39972 } 39973 if (!alignedGops) { 39974 // save all the nals in the last GOP into the gop cache 39975 this.gopCache_.unshift({ 39976 gop: gops.pop(), 39977 pps: track.pps, 39978 sps: track.sps, 39979 }); // Keep a maximum of 6 GOPs in the cache 39980 39981 this.gopCache_.length = Math.min(6, this.gopCache_.length); // Clear nalUnits 39982 39983 nalUnits = []; // return early no gops can be aligned with desired gopsToAlignWith 39984 39985 this.resetStream_(); 39986 this.trigger('done', 'VideoSegmentStream'); 39987 return; 39988 } // Some gops were trimmed. clear dts info so minSegmentDts and pts are correct 39989 // when recalculated before sending off to CoalesceStream 39990 39991 trackDecodeInfo.clearDtsInfo(track); 39992 gops = alignedGops; 39993 } 39994 trackDecodeInfo.collectDtsInfo(track, gops); // First, we have to build the index from byte locations to 39995 // samples (that is, frames) in the video data 39996 39997 track.samples = frameUtils.generateSampleTable(gops); // Concatenate the video data and construct the mdat 39998 39999 mdat = mp4.mdat(frameUtils.concatenateNalData(gops)); 40000 track.baseMediaDecodeTime = trackDecodeInfo.calculateTrackBaseMediaDecodeTime( 40001 track, 40002 options.keepOriginalTimestamps 40003 ); 40004 this.trigger( 40005 'processedGopsInfo', 40006 gops.map(function (gop) { 40007 return { 40008 pts: gop.pts, 40009 dts: gop.dts, 40010 byteLength: gop.byteLength, 40011 }; 40012 }) 40013 ); 40014 firstGop = gops[0]; 40015 lastGop = gops[gops.length - 1]; 40016 this.trigger( 40017 'segmentTimingInfo', 40018 generateSegmentTimingInfo( 40019 track.baseMediaDecodeTime, 40020 firstGop.dts, 40021 firstGop.pts, 40022 lastGop.dts + lastGop.duration, 40023 lastGop.pts + lastGop.duration, 40024 prependedContentDuration 40025 ) 40026 ); 40027 this.trigger('timingInfo', { 40028 start: gops[0].pts, 40029 end: gops[gops.length - 1].pts + gops[gops.length - 1].duration, 40030 }); // save all the nals in the last GOP into the gop cache 40031 40032 this.gopCache_.unshift({ 40033 gop: gops.pop(), 40034 pps: track.pps, 40035 sps: track.sps, 40036 }); // Keep a maximum of 6 GOPs in the cache 40037 40038 this.gopCache_.length = Math.min(6, this.gopCache_.length); // Clear nalUnits 40039 40040 nalUnits = []; 40041 this.trigger('baseMediaDecodeTime', track.baseMediaDecodeTime); 40042 this.trigger('timelineStartInfo', track.timelineStartInfo); 40043 moof = mp4.moof(sequenceNumber, [track]); // it would be great to allocate this array up front instead of 40044 // throwing away hundreds of media segment fragments 40045 40046 boxes = new Uint8Array(moof.byteLength + mdat.byteLength); // Bump the sequence number for next time 40047 40048 sequenceNumber++; 40049 boxes.set(moof); 40050 boxes.set(mdat, moof.byteLength); 40051 this.trigger('data', { 40052 track: track, 40053 boxes: boxes, 40054 }); 40055 this.resetStream_(); // Continue with the flush process now 40056 40057 this.trigger('done', 'VideoSegmentStream'); 40058 }; 40059 this.reset = function () { 40060 this.resetStream_(); 40061 nalUnits = []; 40062 this.gopCache_.length = 0; 40063 gopsToAlignWith.length = 0; 40064 this.trigger('reset'); 40065 }; 40066 this.resetStream_ = function () { 40067 trackDecodeInfo.clearDtsInfo(track); // reset config and pps because they may differ across segments 40068 // for instance, when we are rendition switching 40069 40070 config = undefined; 40071 pps = undefined; 40072 }; // Search for a candidate Gop for gop-fusion from the gop cache and 40073 // return it or return null if no good candidate was found 40074 40075 this.getGopForFusion_ = function (nalUnit) { 40076 var halfSecond = 45000, 40077 // Half-a-second in a 90khz clock 40078 allowableOverlap = 10000, 40079 // About 3 frames @ 30fps 40080 nearestDistance = Infinity, 40081 dtsDistance, 40082 nearestGopObj, 40083 currentGop, 40084 currentGopObj, 40085 i; // Search for the GOP nearest to the beginning of this nal unit 40086 40087 for (i = 0; i < this.gopCache_.length; i++) { 40088 currentGopObj = this.gopCache_[i]; 40089 currentGop = currentGopObj.gop; // Reject Gops with different SPS or PPS 40090 40091 if ( 40092 !(track.pps && arrayEquals(track.pps[0], currentGopObj.pps[0])) || 40093 !(track.sps && arrayEquals(track.sps[0], currentGopObj.sps[0])) 40094 ) { 40095 continue; 40096 } // Reject Gops that would require a negative baseMediaDecodeTime 40097 40098 if (currentGop.dts < track.timelineStartInfo.dts) { 40099 continue; 40100 } // The distance between the end of the gop and the start of the nalUnit 40101 40102 dtsDistance = nalUnit.dts - currentGop.dts - currentGop.duration; // Only consider GOPS that start before the nal unit and end within 40103 // a half-second of the nal unit 40104 40105 if (dtsDistance >= -allowableOverlap && dtsDistance <= halfSecond) { 40106 // Always use the closest GOP we found if there is more than 40107 // one candidate 40108 if (!nearestGopObj || nearestDistance > dtsDistance) { 40109 nearestGopObj = currentGopObj; 40110 nearestDistance = dtsDistance; 40111 } 40112 } 40113 } 40114 if (nearestGopObj) { 40115 return nearestGopObj.gop; 40116 } 40117 return null; 40118 }; // trim gop list to the first gop found that has a matching pts with a gop in the list 40119 // of gopsToAlignWith starting from the START of the list 40120 40121 this.alignGopsAtStart_ = function (gops) { 40122 var alignIndex, gopIndex, align, gop, byteLength, nalCount, duration, alignedGops; 40123 byteLength = gops.byteLength; 40124 nalCount = gops.nalCount; 40125 duration = gops.duration; 40126 alignIndex = gopIndex = 0; 40127 while (alignIndex < gopsToAlignWith.length && gopIndex < gops.length) { 40128 align = gopsToAlignWith[alignIndex]; 40129 gop = gops[gopIndex]; 40130 if (align.pts === gop.pts) { 40131 break; 40132 } 40133 if (gop.pts > align.pts) { 40134 // this current gop starts after the current gop we want to align on, so increment 40135 // align index 40136 alignIndex++; 40137 continue; 40138 } // current gop starts before the current gop we want to align on. so increment gop 40139 // index 40140 40141 gopIndex++; 40142 byteLength -= gop.byteLength; 40143 nalCount -= gop.nalCount; 40144 duration -= gop.duration; 40145 } 40146 if (gopIndex === 0) { 40147 // no gops to trim 40148 return gops; 40149 } 40150 if (gopIndex === gops.length) { 40151 // all gops trimmed, skip appending all gops 40152 return null; 40153 } 40154 alignedGops = gops.slice(gopIndex); 40155 alignedGops.byteLength = byteLength; 40156 alignedGops.duration = duration; 40157 alignedGops.nalCount = nalCount; 40158 alignedGops.pts = alignedGops[0].pts; 40159 alignedGops.dts = alignedGops[0].dts; 40160 return alignedGops; 40161 }; // trim gop list to the first gop found that has a matching pts with a gop in the list 40162 // of gopsToAlignWith starting from the END of the list 40163 40164 this.alignGopsAtEnd_ = function (gops) { 40165 var alignIndex, gopIndex, align, gop, alignEndIndex, matchFound; 40166 alignIndex = gopsToAlignWith.length - 1; 40167 gopIndex = gops.length - 1; 40168 alignEndIndex = null; 40169 matchFound = false;
vendor: 4,634 bytes, lines 40170-40297
40170 while (alignIndex >= 0 && gopIndex >= 0) { 40171 align = gopsToAlignWith[alignIndex]; 40172 gop = gops[gopIndex]; 40173 if (align.pts === gop.pts) { 40174 matchFound = true; 40175 break; 40176 } 40177 if (align.pts > gop.pts) { 40178 alignIndex--; 40179 continue; 40180 } 40181 if (alignIndex === gopsToAlignWith.length - 1) { 40182 // gop.pts is greater than the last alignment candidate. If no match is found 40183 // by the end of this loop, we still want to append gops that come after this 40184 // point 40185 alignEndIndex = gopIndex; 40186 } 40187 gopIndex--; 40188 } 40189 if (!matchFound && alignEndIndex === null) { 40190 return null; 40191 } 40192 var trimIndex; 40193 if (matchFound) { 40194 trimIndex = gopIndex; 40195 } else { 40196 trimIndex = alignEndIndex; 40197 } 40198 if (trimIndex === 0) { 40199 return gops; 40200 } 40201 var alignedGops = gops.slice(trimIndex); 40202 var metadata = alignedGops.reduce( 40203 function (total, gop) { 40204 total.byteLength += gop.byteLength; 40205 total.duration += gop.duration; 40206 total.nalCount += gop.nalCount; 40207 return total; 40208 }, 40209 { 40210 byteLength: 0, 40211 duration: 0, 40212 nalCount: 0, 40213 } 40214 ); 40215 alignedGops.byteLength = metadata.byteLength; 40216 alignedGops.duration = metadata.duration; 40217 alignedGops.nalCount = metadata.nalCount; 40218 alignedGops.pts = alignedGops[0].pts; 40219 alignedGops.dts = alignedGops[0].dts; 40220 return alignedGops; 40221 }; 40222 this.alignGopsWith = function (newGopsToAlignWith) { 40223 gopsToAlignWith = newGopsToAlignWith; 40224 }; 40225 }; 40226 VideoSegmentStream.prototype = new Stream(); 40227 /** 40228 * A Stream that can combine multiple streams (ie. audio & video) 40229 * into a single output segment for MSE. Also supports audio-only 40230 * and video-only streams. 40231 * @param options {object} transmuxer options object 40232 * @param options.keepOriginalTimestamps {boolean} If true, keep the timestamps 40233 * in the source; false to adjust the first segment to start at media timeline start. 40234 */ 40235 40236 CoalesceStream = function (options, metadataStream) { 40237 // Number of Tracks per output segment 40238 // If greater than 1, we combine multiple 40239 // tracks into a single segment 40240 this.numberOfTracks = 0; 40241 this.metadataStream = metadataStream; 40242 options = options || {}; 40243 if (typeof options.remux !== 'undefined') { 40244 this.remuxTracks = !!options.remux; 40245 } else { 40246 this.remuxTracks = true; 40247 } 40248 if (typeof options.keepOriginalTimestamps === 'boolean') { 40249 this.keepOriginalTimestamps = options.keepOriginalTimestamps; 40250 } else { 40251 this.keepOriginalTimestamps = false; 40252 } 40253 this.pendingTracks = []; 40254 this.videoTrack = null; 40255 this.pendingBoxes = []; 40256 this.pendingCaptions = []; 40257 this.pendingMetadata = []; 40258 this.pendingBytes = 0; 40259 this.emittedTracks = 0; 40260 CoalesceStream.prototype.init.call(this); // Take output from multiple 40261 40262 this.push = function (output) { 40263 // buffer incoming captions until the associated video segment 40264 // finishes 40265 if (output.content || output.text) { 40266 return this.pendingCaptions.push(output); 40267 } // buffer incoming id3 tags until the final flush 40268 40269 if (output.frames) { 40270 return this.pendingMetadata.push(output); 40271 } // Add this track to the list of pending tracks and store 40272 // important information required for the construction of 40273 // the final segment 40274 40275 this.pendingTracks.push(output.track); 40276 this.pendingBytes += output.boxes.byteLength; // TODO: is there an issue for this against chrome? 40277 // We unshift audio and push video because 40278 // as of Chrome 75 when switching from 40279 // one init segment to another if the video 40280 // mdat does not appear after the audio mdat 40281 // only audio will play for the duration of our transmux. 40282 40283 if (output.track.type === 'video') { 40284 this.videoTrack = output.track; 40285 this.pendingBoxes.push(output.boxes); 40286 } 40287 if (output.track.type === 'audio') { 40288 this.audioTrack = output.track; 40289 this.pendingBoxes.unshift(output.boxes); 40290 } 40291 }; 40292 }; 40293 CoalesceStream.prototype = new Stream(); 40294 CoalesceStream.prototype.flush = function (flushSource) { 40295 var offset = 0, 40296 event = { 40297 captions: [],
vendor: 25,981 bytes, lines 40298-40934
40298 captionStreams: {}, 40299 metadata: [], 40300 info: {}, 40301 }, 40302 caption, 40303 id3, 40304 initSegment, 40305 timelineStartPts = 0, 40306 i; 40307 if (this.pendingTracks.length < this.numberOfTracks) { 40308 if (flushSource !== 'VideoSegmentStream' && flushSource !== 'AudioSegmentStream') { 40309 // Return because we haven't received a flush from a data-generating 40310 // portion of the segment (meaning that we have only recieved meta-data 40311 // or captions.) 40312 return; 40313 } else if (this.remuxTracks) { 40314 // Return until we have enough tracks from the pipeline to remux (if we 40315 // are remuxing audio and video into a single MP4) 40316 return; 40317 } else if (this.pendingTracks.length === 0) { 40318 // In the case where we receive a flush without any data having been 40319 // received we consider it an emitted track for the purposes of coalescing 40320 // `done` events. 40321 // We do this for the case where there is an audio and video track in the 40322 // segment but no audio data. (seen in several playlists with alternate 40323 // audio tracks and no audio present in the main TS segments.) 40324 this.emittedTracks++; 40325 if (this.emittedTracks >= this.numberOfTracks) { 40326 this.trigger('done'); 40327 this.emittedTracks = 0; 40328 } 40329 return; 40330 } 40331 } 40332 if (this.videoTrack) { 40333 timelineStartPts = this.videoTrack.timelineStartInfo.pts; 40334 VIDEO_PROPERTIES.forEach(function (prop) { 40335 event.info[prop] = this.videoTrack[prop]; 40336 }, this); 40337 } else if (this.audioTrack) { 40338 timelineStartPts = this.audioTrack.timelineStartInfo.pts; 40339 AUDIO_PROPERTIES.forEach(function (prop) { 40340 event.info[prop] = this.audioTrack[prop]; 40341 }, this); 40342 } 40343 if (this.videoTrack || this.audioTrack) { 40344 if (this.pendingTracks.length === 1) { 40345 event.type = this.pendingTracks[0].type; 40346 } else { 40347 event.type = 'combined'; 40348 } 40349 this.emittedTracks += this.pendingTracks.length; 40350 initSegment = mp4.initSegment(this.pendingTracks); // Create a new typed array to hold the init segment 40351 40352 event.initSegment = new Uint8Array(initSegment.byteLength); // Create an init segment containing a moov 40353 // and track definitions 40354 40355 event.initSegment.set(initSegment); // Create a new typed array to hold the moof+mdats 40356 40357 event.data = new Uint8Array(this.pendingBytes); // Append each moof+mdat (one per track) together 40358 40359 for (i = 0; i < this.pendingBoxes.length; i++) { 40360 event.data.set(this.pendingBoxes[i], offset); 40361 offset += this.pendingBoxes[i].byteLength; 40362 } // Translate caption PTS times into second offsets to match the 40363 // video timeline for the segment, and add track info 40364 40365 for (i = 0; i < this.pendingCaptions.length; i++) { 40366 caption = this.pendingCaptions[i]; 40367 caption.startTime = clock.metadataTsToSeconds( 40368 caption.startPts, 40369 timelineStartPts, 40370 this.keepOriginalTimestamps 40371 ); 40372 caption.endTime = clock.metadataTsToSeconds( 40373 caption.endPts, 40374 timelineStartPts, 40375 this.keepOriginalTimestamps 40376 ); 40377 event.captionStreams[caption.stream] = true; 40378 event.captions.push(caption); 40379 } // Translate ID3 frame PTS times into second offsets to match the 40380 // video timeline for the segment 40381 40382 for (i = 0; i < this.pendingMetadata.length; i++) { 40383 id3 = this.pendingMetadata[i]; 40384 id3.cueTime = clock.metadataTsToSeconds( 40385 id3.pts, 40386 timelineStartPts, 40387 this.keepOriginalTimestamps 40388 ); 40389 event.metadata.push(id3); 40390 } // We add this to every single emitted segment even though we only need 40391 // it for the first 40392 40393 event.metadata.dispatchType = this.metadataStream.dispatchType; // Reset stream state 40394 40395 this.pendingTracks.length = 0; 40396 this.videoTrack = null; 40397 this.pendingBoxes.length = 0; 40398 this.pendingCaptions.length = 0; 40399 this.pendingBytes = 0; 40400 this.pendingMetadata.length = 0; // Emit the built segment 40401 // We include captions and ID3 tags for backwards compatibility, 40402 // ideally we should send only video and audio in the data event 40403 40404 this.trigger('data', event); // Emit each caption to the outside world 40405 // Ideally, this would happen immediately on parsing captions, 40406 // but we need to ensure that video data is sent back first 40407 // so that caption timing can be adjusted to match video timing 40408 40409 for (i = 0; i < event.captions.length; i++) { 40410 caption = event.captions[i]; 40411 this.trigger('caption', caption); 40412 } // Emit each id3 tag to the outside world 40413 // Ideally, this would happen immediately on parsing the tag, 40414 // but we need to ensure that video data is sent back first 40415 // so that ID3 frame timing can be adjusted to match video timing 40416 40417 for (i = 0; i < event.metadata.length; i++) { 40418 id3 = event.metadata[i]; 40419 this.trigger('id3Frame', id3); 40420 } 40421 } // Only emit `done` if all tracks have been flushed and emitted 40422 40423 if (this.emittedTracks >= this.numberOfTracks) { 40424 this.trigger('done'); 40425 this.emittedTracks = 0; 40426 } 40427 }; 40428 CoalesceStream.prototype.setRemux = function (val) { 40429 this.remuxTracks = val; 40430 }; 40431 /** 40432 * A Stream that expects MP2T binary data as input and produces 40433 * corresponding media segments, suitable for use with Media Source 40434 * Extension (MSE) implementations that support the ISO BMFF byte 40435 * stream format, like Chrome. 40436 */ 40437 40438 Transmuxer = function (options) { 40439 var self = this, 40440 hasFlushed = true, 40441 videoTrack, 40442 audioTrack; 40443 Transmuxer.prototype.init.call(this); 40444 options = options || {}; 40445 this.baseMediaDecodeTime = options.baseMediaDecodeTime || 0; 40446 this.transmuxPipeline_ = {}; 40447 this.setupAacPipeline = function () { 40448 var pipeline = {}; 40449 this.transmuxPipeline_ = pipeline; 40450 pipeline.type = 'aac'; 40451 pipeline.metadataStream = new m2ts.MetadataStream(); // set up the parsing pipeline 40452 40453 pipeline.aacStream = new AacStream(); 40454 pipeline.audioTimestampRolloverStream = new m2ts.TimestampRolloverStream('audio'); 40455 pipeline.timedMetadataTimestampRolloverStream = new m2ts.TimestampRolloverStream( 40456 'timed-metadata' 40457 ); 40458 pipeline.adtsStream = new AdtsStream(); 40459 pipeline.coalesceStream = new CoalesceStream(options, pipeline.metadataStream); 40460 pipeline.headOfPipeline = pipeline.aacStream; 40461 pipeline.aacStream.pipe(pipeline.audioTimestampRolloverStream).pipe(pipeline.adtsStream); 40462 pipeline.aacStream 40463 .pipe(pipeline.timedMetadataTimestampRolloverStream) 40464 .pipe(pipeline.metadataStream) 40465 .pipe(pipeline.coalesceStream); 40466 pipeline.metadataStream.on('timestamp', function (frame) { 40467 pipeline.aacStream.setTimestamp(frame.timeStamp); 40468 }); 40469 pipeline.aacStream.on('data', function (data) { 40470 if ( 40471 (data.type !== 'timed-metadata' && data.type !== 'audio') || 40472 pipeline.audioSegmentStream 40473 ) { 40474 return; 40475 } 40476 audioTrack = audioTrack || { 40477 timelineStartInfo: { 40478 baseMediaDecodeTime: self.baseMediaDecodeTime, 40479 }, 40480 codec: 'adts', 40481 type: 'audio', 40482 }; // hook up the audio segment stream to the first track with aac data 40483 40484 pipeline.coalesceStream.numberOfTracks++; 40485 pipeline.audioSegmentStream = new AudioSegmentStream(audioTrack, options); 40486 pipeline.audioSegmentStream.on('log', self.getLogTrigger_('audioSegmentStream')); 40487 pipeline.audioSegmentStream.on('timingInfo', self.trigger.bind(self, 'audioTimingInfo')); // Set up the final part of the audio pipeline 40488 40489 pipeline.adtsStream.pipe(pipeline.audioSegmentStream).pipe(pipeline.coalesceStream); // emit pmt info 40490 40491 self.trigger('trackinfo', { 40492 hasAudio: !!audioTrack, 40493 hasVideo: !!videoTrack, 40494 }); 40495 }); // Re-emit any data coming from the coalesce stream to the outside world 40496 40497 pipeline.coalesceStream.on('data', this.trigger.bind(this, 'data')); // Let the consumer know we have finished flushing the entire pipeline 40498 40499 pipeline.coalesceStream.on('done', this.trigger.bind(this, 'done')); 40500 addPipelineLogRetriggers(this, pipeline); 40501 }; 40502 this.setupTsPipeline = function () { 40503 var pipeline = {}; 40504 this.transmuxPipeline_ = pipeline; 40505 pipeline.type = 'ts'; 40506 pipeline.metadataStream = new m2ts.MetadataStream(); // set up the parsing pipeline 40507 40508 pipeline.packetStream = new m2ts.TransportPacketStream(); 40509 pipeline.parseStream = new m2ts.TransportParseStream(); 40510 pipeline.elementaryStream = new m2ts.ElementaryStream(); 40511 pipeline.timestampRolloverStream = new m2ts.TimestampRolloverStream(); 40512 pipeline.adtsStream = new AdtsStream(); 40513 pipeline.h264Stream = new H264Stream(); 40514 pipeline.captionStream = new m2ts.CaptionStream(options); 40515 pipeline.coalesceStream = new CoalesceStream(options, pipeline.metadataStream); 40516 pipeline.headOfPipeline = pipeline.packetStream; // disassemble MPEG2-TS packets into elementary streams 40517 40518 pipeline.packetStream 40519 .pipe(pipeline.parseStream) 40520 .pipe(pipeline.elementaryStream) 40521 .pipe(pipeline.timestampRolloverStream); // !!THIS ORDER IS IMPORTANT!! 40522 // demux the streams 40523 40524 pipeline.timestampRolloverStream.pipe(pipeline.h264Stream); 40525 pipeline.timestampRolloverStream.pipe(pipeline.adtsStream); 40526 pipeline.timestampRolloverStream 40527 .pipe(pipeline.metadataStream) 40528 .pipe(pipeline.coalesceStream); // Hook up CEA-608/708 caption stream 40529 40530 pipeline.h264Stream.pipe(pipeline.captionStream).pipe(pipeline.coalesceStream); 40531 pipeline.elementaryStream.on('data', function (data) { 40532 var i; 40533 if (data.type === 'metadata') { 40534 i = data.tracks.length; // scan the tracks listed in the metadata 40535 40536 while (i--) { 40537 if (!videoTrack && data.tracks[i].type === 'video') { 40538 videoTrack = data.tracks[i]; 40539 videoTrack.timelineStartInfo.baseMediaDecodeTime = self.baseMediaDecodeTime; 40540 } else if (!audioTrack && data.tracks[i].type === 'audio') { 40541 audioTrack = data.tracks[i]; 40542 audioTrack.timelineStartInfo.baseMediaDecodeTime = self.baseMediaDecodeTime; 40543 } 40544 } // hook up the video segment stream to the first track with h264 data 40545 40546 if (videoTrack && !pipeline.videoSegmentStream) { 40547 pipeline.coalesceStream.numberOfTracks++; 40548 pipeline.videoSegmentStream = new VideoSegmentStream(videoTrack, options); 40549 pipeline.videoSegmentStream.on('log', self.getLogTrigger_('videoSegmentStream')); 40550 pipeline.videoSegmentStream.on('timelineStartInfo', function (timelineStartInfo) { 40551 // When video emits timelineStartInfo data after a flush, we forward that 40552 // info to the AudioSegmentStream, if it exists, because video timeline 40553 // data takes precedence. Do not do this if keepOriginalTimestamps is set, 40554 // because this is a particularly subtle form of timestamp alteration. 40555 if (audioTrack && !options.keepOriginalTimestamps) { 40556 audioTrack.timelineStartInfo = timelineStartInfo; // On the first segment we trim AAC frames that exist before the 40557 // very earliest DTS we have seen in video because Chrome will 40558 // interpret any video track with a baseMediaDecodeTime that is 40559 // non-zero as a gap. 40560 40561 pipeline.audioSegmentStream.setEarliestDts( 40562 timelineStartInfo.dts - self.baseMediaDecodeTime 40563 ); 40564 } 40565 }); 40566 pipeline.videoSegmentStream.on( 40567 'processedGopsInfo', 40568 self.trigger.bind(self, 'gopInfo') 40569 ); 40570 pipeline.videoSegmentStream.on( 40571 'segmentTimingInfo', 40572 self.trigger.bind(self, 'videoSegmentTimingInfo') 40573 ); 40574 pipeline.videoSegmentStream.on('baseMediaDecodeTime', function (baseMediaDecodeTime) { 40575 if (audioTrack) { 40576 pipeline.audioSegmentStream.setVideoBaseMediaDecodeTime(baseMediaDecodeTime); 40577 } 40578 }); 40579 pipeline.videoSegmentStream.on( 40580 'timingInfo', 40581 self.trigger.bind(self, 'videoTimingInfo') 40582 ); // Set up the final part of the video pipeline 40583 40584 pipeline.h264Stream.pipe(pipeline.videoSegmentStream).pipe(pipeline.coalesceStream); 40585 } 40586 if (audioTrack && !pipeline.audioSegmentStream) { 40587 // hook up the audio segment stream to the first track with aac data 40588 pipeline.coalesceStream.numberOfTracks++; 40589 pipeline.audioSegmentStream = new AudioSegmentStream(audioTrack, options); 40590 pipeline.audioSegmentStream.on('log', self.getLogTrigger_('audioSegmentStream')); 40591 pipeline.audioSegmentStream.on( 40592 'timingInfo', 40593 self.trigger.bind(self, 'audioTimingInfo') 40594 ); 40595 pipeline.audioSegmentStream.on( 40596 'segmentTimingInfo', 40597 self.trigger.bind(self, 'audioSegmentTimingInfo') 40598 ); // Set up the final part of the audio pipeline 40599 40600 pipeline.adtsStream.pipe(pipeline.audioSegmentStream).pipe(pipeline.coalesceStream); 40601 } // emit pmt info 40602 40603 self.trigger('trackinfo', { 40604 hasAudio: !!audioTrack, 40605 hasVideo: !!videoTrack, 40606 }); 40607 } 40608 }); // Re-emit any data coming from the coalesce stream to the outside world 40609 40610 pipeline.coalesceStream.on('data', this.trigger.bind(this, 'data')); 40611 pipeline.coalesceStream.on('id3Frame', function (id3Frame) { 40612 id3Frame.dispatchType = pipeline.metadataStream.dispatchType; 40613 self.trigger('id3Frame', id3Frame); 40614 }); 40615 pipeline.coalesceStream.on('caption', this.trigger.bind(this, 'caption')); // Let the consumer know we have finished flushing the entire pipeline 40616 40617 pipeline.coalesceStream.on('done', this.trigger.bind(this, 'done')); 40618 addPipelineLogRetriggers(this, pipeline); 40619 }; // hook up the segment streams once track metadata is delivered 40620 40621 this.setBaseMediaDecodeTime = function (baseMediaDecodeTime) { 40622 var pipeline = this.transmuxPipeline_; 40623 if (!options.keepOriginalTimestamps) { 40624 this.baseMediaDecodeTime = baseMediaDecodeTime; 40625 } 40626 if (audioTrack) { 40627 audioTrack.timelineStartInfo.dts = undefined; 40628 audioTrack.timelineStartInfo.pts = undefined; 40629 trackDecodeInfo.clearDtsInfo(audioTrack); 40630 if (pipeline.audioTimestampRolloverStream) { 40631 pipeline.audioTimestampRolloverStream.discontinuity(); 40632 } 40633 } 40634 if (videoTrack) { 40635 if (pipeline.videoSegmentStream) { 40636 pipeline.videoSegmentStream.gopCache_ = []; 40637 } 40638 videoTrack.timelineStartInfo.dts = undefined; 40639 videoTrack.timelineStartInfo.pts = undefined; 40640 trackDecodeInfo.clearDtsInfo(videoTrack); 40641 pipeline.captionStream.reset(); 40642 } 40643 if (pipeline.timestampRolloverStream) { 40644 pipeline.timestampRolloverStream.discontinuity(); 40645 } 40646 }; 40647 this.setAudioAppendStart = function (timestamp) { 40648 if (audioTrack) { 40649 this.transmuxPipeline_.audioSegmentStream.setAudioAppendStart(timestamp); 40650 } 40651 }; 40652 this.setRemux = function (val) { 40653 var pipeline = this.transmuxPipeline_; 40654 options.remux = val; 40655 if (pipeline && pipeline.coalesceStream) { 40656 pipeline.coalesceStream.setRemux(val); 40657 } 40658 }; 40659 this.alignGopsWith = function (gopsToAlignWith) { 40660 if (videoTrack && this.transmuxPipeline_.videoSegmentStream) { 40661 this.transmuxPipeline_.videoSegmentStream.alignGopsWith(gopsToAlignWith); 40662 } 40663 }; 40664 this.getLogTrigger_ = function (key) { 40665 var self = this; 40666 return function (event) { 40667 event.stream = key; 40668 self.trigger('log', event); 40669 }; 40670 }; // feed incoming data to the front of the parsing pipeline 40671 40672 this.push = function (data) { 40673 if (hasFlushed) { 40674 var isAac = isLikelyAacData(data); 40675 if (isAac && this.transmuxPipeline_.type !== 'aac') { 40676 this.setupAacPipeline(); 40677 } else if (!isAac && this.transmuxPipeline_.type !== 'ts') { 40678 this.setupTsPipeline(); 40679 } 40680 hasFlushed = false; 40681 } 40682 this.transmuxPipeline_.headOfPipeline.push(data); 40683 }; // flush any buffered data 40684 40685 this.flush = function () { 40686 hasFlushed = true; // Start at the top of the pipeline and flush all pending work 40687 40688 this.transmuxPipeline_.headOfPipeline.flush(); 40689 }; 40690 this.endTimeline = function () { 40691 this.transmuxPipeline_.headOfPipeline.endTimeline(); 40692 }; 40693 this.reset = function () { 40694 if (this.transmuxPipeline_.headOfPipeline) { 40695 this.transmuxPipeline_.headOfPipeline.reset(); 40696 } 40697 }; // Caption data has to be reset when seeking outside buffered range 40698 40699 this.resetCaptions = function () { 40700 if (this.transmuxPipeline_.captionStream) { 40701 this.transmuxPipeline_.captionStream.reset(); 40702 } 40703 }; 40704 }; 40705 Transmuxer.prototype = new Stream(); 40706 var transmuxer = { 40707 Transmuxer: Transmuxer, 40708 VideoSegmentStream: VideoSegmentStream, 40709 AudioSegmentStream: AudioSegmentStream, 40710 AUDIO_PROPERTIES: AUDIO_PROPERTIES, 40711 VIDEO_PROPERTIES: VIDEO_PROPERTIES, 40712 // exported for testing 40713 generateSegmentTimingInfo: generateSegmentTimingInfo, 40714 }; 40715 /** 40716 * mux.js 40717 * 40718 * Copyright (c) Brightcove 40719 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 40720 */ 40721 40722 var toUnsigned$3 = function (value) { 40723 return value >>> 0; 40724 }; 40725 var toHexString$1 = function (value) { 40726 return ('00' + value.toString(16)).slice(-2); 40727 }; 40728 var bin = { 40729 toUnsigned: toUnsigned$3, 40730 toHexString: toHexString$1, 40731 }; 40732 var parseType$4 = function (buffer) { 40733 var result = ''; 40734 result += String.fromCharCode(buffer[0]); 40735 result += String.fromCharCode(buffer[1]); 40736 result += String.fromCharCode(buffer[2]); 40737 result += String.fromCharCode(buffer[3]); 40738 return result; 40739 }; 40740 var parseType_1 = parseType$4; 40741 var toUnsigned$2 = bin.toUnsigned; 40742 var parseType$3 = parseType_1; 40743 var findBox$5 = function (data, path) { 40744 var results = [], 40745 i, 40746 size, 40747 type, 40748 end, 40749 subresults; 40750 if (!path.length) { 40751 // short-circuit the search for empty paths 40752 return null; 40753 } 40754 for (i = 0; i < data.byteLength; ) { 40755 size = toUnsigned$2( 40756 (data[i] << 24) | (data[i + 1] << 16) | (data[i + 2] << 8) | data[i + 3] 40757 ); 40758 type = parseType$3(data.subarray(i + 4, i + 8)); 40759 end = size > 1 ? i + size : data.byteLength; 40760 if (type === path[0]) { 40761 if (path.length === 1) { 40762 // this is the end of the path and we've found the box we were 40763 // looking for 40764 results.push(data.subarray(i + 8, end)); 40765 } else { 40766 // recursively search for the next box along the path 40767 subresults = findBox$5(data.subarray(i + 8, end), path.slice(1)); 40768 if (subresults.length) { 40769 results = results.concat(subresults); 40770 } 40771 } 40772 } 40773 i = end; 40774 } // we've finished searching all of data 40775 40776 return results; 40777 }; 40778 var findBox_1 = findBox$5; 40779 var toUnsigned$1 = bin.toUnsigned; 40780 var getUint64$4 = numbers.getUint64; 40781 var tfdt = function (data) { 40782 var result = { 40783 version: data[0], 40784 flags: new Uint8Array(data.subarray(1, 4)), 40785 }; 40786 if (result.version === 1) { 40787 result.baseMediaDecodeTime = getUint64$4(data.subarray(4)); 40788 } else { 40789 result.baseMediaDecodeTime = toUnsigned$1( 40790 (data[4] << 24) | (data[5] << 16) | (data[6] << 8) | data[7] 40791 ); 40792 } 40793 return result; 40794 }; 40795 var parseTfdt$3 = tfdt; 40796 var tfhd = function (data) { 40797 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 40798 result = { 40799 version: data[0], 40800 flags: new Uint8Array(data.subarray(1, 4)), 40801 trackId: view.getUint32(4), 40802 }, 40803 baseDataOffsetPresent = result.flags[2] & 0x01, 40804 sampleDescriptionIndexPresent = result.flags[2] & 0x02, 40805 defaultSampleDurationPresent = result.flags[2] & 0x08, 40806 defaultSampleSizePresent = result.flags[2] & 0x10, 40807 defaultSampleFlagsPresent = result.flags[2] & 0x20, 40808 durationIsEmpty = result.flags[0] & 0x010000, 40809 defaultBaseIsMoof = result.flags[0] & 0x020000, 40810 i; 40811 i = 8; 40812 if (baseDataOffsetPresent) { 40813 i += 4; // truncate top 4 bytes 40814 // FIXME: should we read the full 64 bits? 40815 40816 result.baseDataOffset = view.getUint32(12); 40817 i += 4; 40818 } 40819 if (sampleDescriptionIndexPresent) { 40820 result.sampleDescriptionIndex = view.getUint32(i); 40821 i += 4; 40822 } 40823 if (defaultSampleDurationPresent) { 40824 result.defaultSampleDuration = view.getUint32(i); 40825 i += 4; 40826 } 40827 if (defaultSampleSizePresent) { 40828 result.defaultSampleSize = view.getUint32(i); 40829 i += 4; 40830 } 40831 if (defaultSampleFlagsPresent) { 40832 result.defaultSampleFlags = view.getUint32(i); 40833 } 40834 if (durationIsEmpty) { 40835 result.durationIsEmpty = true; 40836 } 40837 if (!baseDataOffsetPresent && defaultBaseIsMoof) { 40838 result.baseDataOffsetIsMoof = true; 40839 } 40840 return result; 40841 }; 40842 var parseTfhd$2 = tfhd; 40843 var getUint64$3 = numbers.getUint64; 40844 var parseSidx = function (data) { 40845 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 40846 result = { 40847 version: data[0], 40848 flags: new Uint8Array(data.subarray(1, 4)), 40849 references: [], 40850 referenceId: view.getUint32(4), 40851 timescale: view.getUint32(8), 40852 }, 40853 i = 12; 40854 if (result.version === 0) { 40855 result.earliestPresentationTime = view.getUint32(i); 40856 result.firstOffset = view.getUint32(i + 4); 40857 i += 8; 40858 } else { 40859 // read 64 bits 40860 result.earliestPresentationTime = getUint64$3(data.subarray(i)); 40861 result.firstOffset = getUint64$3(data.subarray(i + 8)); 40862 i += 16; 40863 } 40864 i += 2; // reserved 40865 40866 var referenceCount = view.getUint16(i); 40867 i += 2; // start of references 40868 40869 for (; referenceCount > 0; i += 12, referenceCount--) { 40870 result.references.push({ 40871 referenceType: (data[i] & 0x80) >>> 7, 40872 referencedSize: view.getUint32(i) & 0x7fffffff, 40873 subsegmentDuration: view.getUint32(i + 4), 40874 startsWithSap: !!(data[i + 8] & 0x80), 40875 sapType: (data[i + 8] & 0x70) >>> 4, 40876 sapDeltaTime: view.getUint32(i + 8) & 0x0fffffff, 40877 }); 40878 } 40879 return result; 40880 }; 40881 var parseSidx_1 = parseSidx; 40882 var parseSampleFlags$1 = function (flags) { 40883 return { 40884 isLeading: (flags[0] & 0x0c) >>> 2, 40885 dependsOn: flags[0] & 0x03, 40886 isDependedOn: (flags[1] & 0xc0) >>> 6, 40887 hasRedundancy: (flags[1] & 0x30) >>> 4, 40888 paddingValue: (flags[1] & 0x0e) >>> 1, 40889 isNonSyncSample: flags[1] & 0x01, 40890 degradationPriority: (flags[2] << 8) | flags[3], 40891 }; 40892 }; 40893 var parseSampleFlags_1 = parseSampleFlags$1; 40894 var parseSampleFlags = parseSampleFlags_1; 40895 var trun = function (data) { 40896 var result = { 40897 version: data[0], 40898 flags: new Uint8Array(data.subarray(1, 4)), 40899 samples: [], 40900 }, 40901 view = new DataView(data.buffer, data.byteOffset, data.byteLength), 40902 // Flag interpretation 40903 dataOffsetPresent = result.flags[2] & 0x01, 40904 // compare with 2nd byte of 0x1 40905 firstSampleFlagsPresent = result.flags[2] & 0x04, 40906 // compare with 2nd byte of 0x4 40907 sampleDurationPresent = result.flags[1] & 0x01, 40908 // compare with 2nd byte of 0x100 40909 sampleSizePresent = result.flags[1] & 0x02, 40910 // compare with 2nd byte of 0x200 40911 sampleFlagsPresent = result.flags[1] & 0x04, 40912 // compare with 2nd byte of 0x400 40913 sampleCompositionTimeOffsetPresent = result.flags[1] & 0x08, 40914 // compare with 2nd byte of 0x800 40915 sampleCount = view.getUint32(4), 40916 offset = 8, 40917 sample; 40918 if (dataOffsetPresent) { 40919 // 32 bit signed integer 40920 result.dataOffset = view.getInt32(offset); 40921 offset += 4; 40922 } // Overrides the flags for the first sample only. The order of 40923 // optional values will be: duration, size, compositionTimeOffset 40924 40925 if (firstSampleFlagsPresent && sampleCount) { 40926 sample = { 40927 flags: parseSampleFlags(data.subarray(offset, offset + 4)), 40928 }; 40929 offset += 4; 40930 if (sampleDurationPresent) { 40931 sample.duration = view.getUint32(offset); 40932 offset += 4; 40933 } 40934 if (sampleSizePresent) {
40935 sample.size = view.getUint32(offset); 40936 offset += 4; 40937 } 40938 if (sampleCompositionTimeOffsetPresent) { 40939 if (result.version === 1) { 40940 sample.compositionTimeOffset = view.getInt32(offset); 40941 } else { 40942 sample.compositionTimeOffset = view.getUint32(offset); 40943 } 40944 offset += 4; 40945 } 40946 result.samples.push(sample); 40947 sampleCount--; 40948 } 40949 while (sampleCount--) { 40950 sample = {}; 40951 if (sampleDurationPresent) { 40952 sample.duration = view.getUint32(offset); 40953 offset += 4; 40954 } 40955 if (sampleSizePresent) { 40956 sample.size = view.getUint32(offset); 40957 offset += 4; 40958 } 40959 if (sampleFlagsPresent) { 40960 sample.flags = parseSampleFlags(data.subarray(offset, offset + 4)); 40961 offset += 4; 40962 } 40963 if (sampleCompositionTimeOffsetPresent) { 40964 if (result.version === 1) { 40965 sample.compositionTimeOffset = view.getInt32(offset); 40966 } else { 40967 sample.compositionTimeOffset = view.getUint32(offset); 40968 } 40969 offset += 4; 40970 } 40971 result.samples.push(sample); 40972 } 40973 return result; 40974 }; 40975 var parseTrun$2 = trun; 40976 /** 40977 * mux.js 40978 * 40979 * Copyright (c) Brightcove 40980 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 40981 * 40982 * Parse the internal MP4 structure into an equivalent javascript 40983 * object. 40984 */ 40985 40986 var numberHelpers = numbers; 40987 var getUint64$2 = numberHelpers.getUint64; 40988 var inspectMp4, 40989 textifyMp4, 40990 parseMp4Date = function (seconds) { 40991 return new Date(seconds * 1000 - 2082844800000); 40992 }, 40993 parseType$2 = parseType_1, 40994 findBox$4 = findBox_1, 40995 nalParse = function (avcStream) { 40996 var avcView = new DataView(avcStream.buffer, avcStream.byteOffset, avcStream.byteLength), 40997 result = [], 40998 i, 40999 length; 41000 for (i = 0; i + 4 < avcStream.length; i += length) { 41001 length = avcView.getUint32(i); 41002 i += 4; // bail if this doesn't appear to be an H264 stream 41003 41004 if (length <= 0) { 41005 result.push("<span style='color:red;'>MALFORMED DATA</span>"); 41006 continue; 41007 } 41008 switch (avcStream[i] & 0x1f) { 41009 case 0x01: 41010 result.push('slice_layer_without_partitioning_rbsp'); 41011 break; 41012 case 0x05: 41013 result.push('slice_layer_without_partitioning_rbsp_idr'); 41014 break; 41015 case 0x06: 41016 result.push('sei_rbsp'); 41017 break; 41018 case 0x07: 41019 result.push('seq_parameter_set_rbsp'); 41020 break; 41021 case 0x08: 41022 result.push('pic_parameter_set_rbsp'); 41023 break; 41024 case 0x09: 41025 result.push('access_unit_delimiter_rbsp'); 41026 break; 41027 default: 41028 result.push(('UNKNOWN NAL - ' + avcStream[i]) & 0x1f); 41029 break; 41030 } 41031 } 41032 return result; 41033 }, 41034 // registry of handlers for individual mp4 box types 41035 parse = { 41036 // codingname, not a first-class box type. stsd entries share the 41037 // same format as real boxes so the parsing infrastructure can be 41038 // shared 41039 avc1: function (data) { 41040 var view = new DataView(data.buffer, data.byteOffset, data.byteLength); 41041 return { 41042 dataReferenceIndex: view.getUint16(6), 41043 width: view.getUint16(24), 41044 height: view.getUint16(26), 41045 horizresolution: view.getUint16(28) + view.getUint16(30) / 16, 41046 vertresolution: view.getUint16(32) + view.getUint16(34) / 16, 41047 frameCount: view.getUint16(40), 41048 depth: view.getUint16(74), 41049 config: inspectMp4(data.subarray(78, data.byteLength)), 41050 }; 41051 }, 41052 avcC: function (data) { 41053 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 41054 result = { 41055 configurationVersion: data[0], 41056 avcProfileIndication: data[1], 41057 profileCompatibility: data[2], 41058 avcLevelIndication: data[3], 41059 lengthSizeMinusOne: data[4] & 0x03, 41060 sps: [], 41061 pps: [], 41062 }, 41063 numOfSequenceParameterSets = data[5] & 0x1f, 41064 numOfPictureParameterSets, 41065 nalSize, 41066 offset, 41067 i; // iterate past any SPSs 41068 41069 offset = 6; 41070 for (i = 0; i < numOfSequenceParameterSets; i++) { 41071 nalSize = view.getUint16(offset); 41072 offset += 2; 41073 result.sps.push(new Uint8Array(data.subarray(offset, offset + nalSize))); 41074 offset += nalSize; 41075 } // iterate past any PPSs 41076 41077 numOfPictureParameterSets = data[offset]; 41078 offset++; 41079 for (i = 0; i < numOfPictureParameterSets; i++) { 41080 nalSize = view.getUint16(offset); 41081 offset += 2; 41082 result.pps.push(new Uint8Array(data.subarray(offset, offset + nalSize))); 41083 offset += nalSize; 41084 } 41085 return result; 41086 }, 41087 btrt: function (data) { 41088 var view = new DataView(data.buffer, data.byteOffset, data.byteLength); 41089 return { 41090 bufferSizeDB: view.getUint32(0), 41091 maxBitrate: view.getUint32(4), 41092 avgBitrate: view.getUint32(8), 41093 }; 41094 }, 41095 edts: function edts(data) { 41096 return { 41097 boxes: inspectMp4(data), 41098 }; 41099 }, 41100 elst: function elst(data) { 41101 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 41102 result = { 41103 version: view.getUint8(0), 41104 flags: new Uint8Array(data.subarray(1, 4)), 41105 edits: [], 41106 }, 41107 entryCount = view.getUint32(4), 41108 i; 41109 for (i = 8; entryCount; entryCount--) { 41110 if (result.version === 0) { 41111 result.edits.push({ 41112 segmentDuration: view.getUint32(i), 41113 mediaTime: view.getInt32(i + 4), 41114 mediaRate: view.getUint16(i + 8) + view.getUint16(i + 10) / (256 * 256), 41115 }); 41116 i += 12; 41117 } else { 41118 result.edits.push({ 41119 segmentDuration: getUint64$2(data.subarray(i)), 41120 mediaTime: getUint64$2(data.subarray(i + 8)), 41121 mediaRate: view.getUint16(i + 16) + view.getUint16(i + 18) / (256 * 256), 41122 }); 41123 i += 20; 41124 } 41125 } 41126 return result; 41127 }, 41128 esds: function (data) { 41129 return { 41130 version: data[0], 41131 flags: new Uint8Array(data.subarray(1, 4)), 41132 esId: (data[6] << 8) | data[7], 41133 streamPriority: data[8] & 0x1f, 41134 decoderConfig: { 41135 objectProfileIndication: data[11], 41136 streamType: (data[12] >>> 2) & 0x3f, 41137 bufferSize: (data[13] << 16) | (data[14] << 8) | data[15], 41138 maxBitrate: (data[16] << 24) | (data[17] << 16) | (data[18] << 8) | data[19], 41139 avgBitrate: (data[20] << 24) | (data[21] << 16) | (data[22] << 8) | data[23], 41140 decoderConfigDescriptor: { 41141 tag: data[24], 41142 length: data[25], 41143 audioObjectType: (data[26] >>> 3) & 0x1f, 41144 samplingFrequencyIndex: ((data[26] & 0x07) << 1) | ((data[27] >>> 7) & 0x01), 41145 channelConfiguration: (data[27] >>> 3) & 0x0f, 41146 }, 41147 }, 41148 }; 41149 }, 41150 ftyp: function (data) { 41151 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 41152 result = { 41153 majorBrand: parseType$2(data.subarray(0, 4)), 41154 minorVersion: view.getUint32(4), 41155 compatibleBrands: [], 41156 }, 41157 i = 8; 41158 while (i < data.byteLength) { 41159 result.compatibleBrands.push(parseType$2(data.subarray(i, i + 4))); 41160 i += 4; 41161 } 41162 return result; 41163 }, 41164 dinf: function (data) { 41165 return { 41166 boxes: inspectMp4(data), 41167 }; 41168 }, 41169 dref: function (data) { 41170 return { 41171 version: data[0], 41172 flags: new Uint8Array(data.subarray(1, 4)), 41173 dataReferences: inspectMp4(data.subarray(8)), 41174 }; 41175 }, 41176 hdlr: function (data) { 41177 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 41178 result = { 41179 version: view.getUint8(0), 41180 flags: new Uint8Array(data.subarray(1, 4)), 41181 handlerType: parseType$2(data.subarray(8, 12)), 41182 name: '', 41183 }, 41184 i = 8; // parse out the name field 41185 41186 for (i = 24; i < data.byteLength; i++) { 41187 if (data[i] === 0x00) { 41188 // the name field is null-terminated 41189 i++; 41190 break; 41191 } 41192 result.name += String.fromCharCode(data[i]); 41193 } // decode UTF-8 to javascript's internal representation 41194 // see http://ecmanaut.blogspot.com/2006/07/encoding-decoding-utf8-in-javascript.html 41195 41196 result.name = decodeURIComponent(escape(result.name)); 41197 return result; 41198 }, 41199 mdat: function (data) { 41200 return { 41201 byteLength: data.byteLength, 41202 nals: nalParse(data), 41203 }; 41204 }, 41205 mdhd: function (data) { 41206 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 41207 i = 4, 41208 language, 41209 result = { 41210 version: view.getUint8(0), 41211 flags: new Uint8Array(data.subarray(1, 4)), 41212 language: '', 41213 }; 41214 if (result.version === 1) { 41215 i += 4; 41216 result.creationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes 41217 41218 i += 8; 41219 result.modificationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes 41220 41221 i += 4; 41222 result.timescale = view.getUint32(i); 41223 i += 8; 41224 result.duration = view.getUint32(i); // truncating top 4 bytes 41225 } else { 41226 result.creationTime = parseMp4Date(view.getUint32(i)); 41227 i += 4; 41228 result.modificationTime = parseMp4Date(view.getUint32(i)); 41229 i += 4; 41230 result.timescale = view.getUint32(i); 41231 i += 4; 41232 result.duration = view.getUint32(i); 41233 } 41234 i += 4; // language is stored as an ISO-639-2/T code in an array of three 5-bit fields 41235 // each field is the packed difference between its ASCII value and 0x60 41236 41237 language = view.getUint16(i); 41238 result.language += String.fromCharCode((language >> 10) + 0x60); 41239 result.language += String.fromCharCode(((language & 0x03e0) >> 5) + 0x60); 41240 result.language += String.fromCharCode((language & 0x1f) + 0x60); 41241 return result; 41242 }, 41243 mdia: function (data) { 41244 return { 41245 boxes: inspectMp4(data), 41246 }; 41247 }, 41248 mfhd: function (data) { 41249 return { 41250 version: data[0], 41251 flags: new Uint8Array(data.subarray(1, 4)), 41252 sequenceNumber: (data[4] << 24) | (data[5] << 16) | (data[6] << 8) | data[7], 41253 }; 41254 }, 41255 minf: function (data) { 41256 return { 41257 boxes: inspectMp4(data), 41258 }; 41259 }, 41260 // codingname, not a first-class box type. stsd entries share the
vendor: 21,065 bytes, lines 41261-41874
41261 // same format as real boxes so the parsing infrastructure can be 41262 // shared 41263 mp4a: function (data) { 41264 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 41265 result = { 41266 // 6 bytes reserved 41267 dataReferenceIndex: view.getUint16(6), 41268 // 4 + 4 bytes reserved 41269 channelcount: view.getUint16(16), 41270 samplesize: view.getUint16(18), 41271 // 2 bytes pre_defined 41272 // 2 bytes reserved 41273 samplerate: view.getUint16(24) + view.getUint16(26) / 65536, 41274 }; // if there are more bytes to process, assume this is an ISO/IEC 41275 // 14496-14 MP4AudioSampleEntry and parse the ESDBox 41276 41277 if (data.byteLength > 28) { 41278 result.streamDescriptor = inspectMp4(data.subarray(28))[0]; 41279 } 41280 return result; 41281 }, 41282 moof: function (data) { 41283 return { 41284 boxes: inspectMp4(data), 41285 }; 41286 }, 41287 moov: function (data) { 41288 return { 41289 boxes: inspectMp4(data), 41290 }; 41291 }, 41292 mvex: function (data) { 41293 return { 41294 boxes: inspectMp4(data), 41295 }; 41296 }, 41297 mvhd: function (data) { 41298 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 41299 i = 4, 41300 result = { 41301 version: view.getUint8(0), 41302 flags: new Uint8Array(data.subarray(1, 4)), 41303 }; 41304 if (result.version === 1) { 41305 i += 4; 41306 result.creationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes 41307 41308 i += 8; 41309 result.modificationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes 41310 41311 i += 4; 41312 result.timescale = view.getUint32(i); 41313 i += 8; 41314 result.duration = view.getUint32(i); // truncating top 4 bytes 41315 } else { 41316 result.creationTime = parseMp4Date(view.getUint32(i)); 41317 i += 4; 41318 result.modificationTime = parseMp4Date(view.getUint32(i)); 41319 i += 4; 41320 result.timescale = view.getUint32(i); 41321 i += 4; 41322 result.duration = view.getUint32(i); 41323 } 41324 i += 4; // convert fixed-point, base 16 back to a number 41325 41326 result.rate = view.getUint16(i) + view.getUint16(i + 2) / 16; 41327 i += 4; 41328 result.volume = view.getUint8(i) + view.getUint8(i + 1) / 8; 41329 i += 2; 41330 i += 2; 41331 i += 2 * 4; 41332 result.matrix = new Uint32Array(data.subarray(i, i + 9 * 4)); 41333 i += 9 * 4; 41334 i += 6 * 4; 41335 result.nextTrackId = view.getUint32(i); 41336 return result; 41337 }, 41338 pdin: function (data) { 41339 var view = new DataView(data.buffer, data.byteOffset, data.byteLength); 41340 return { 41341 version: view.getUint8(0), 41342 flags: new Uint8Array(data.subarray(1, 4)), 41343 rate: view.getUint32(4), 41344 initialDelay: view.getUint32(8), 41345 }; 41346 }, 41347 sdtp: function (data) { 41348 var result = { 41349 version: data[0], 41350 flags: new Uint8Array(data.subarray(1, 4)), 41351 samples: [], 41352 }, 41353 i; 41354 for (i = 4; i < data.byteLength; i++) { 41355 result.samples.push({ 41356 dependsOn: (data[i] & 0x30) >> 4, 41357 isDependedOn: (data[i] & 0x0c) >> 2, 41358 hasRedundancy: data[i] & 0x03, 41359 }); 41360 } 41361 return result; 41362 }, 41363 sidx: parseSidx_1, 41364 smhd: function (data) { 41365 return { 41366 version: data[0], 41367 flags: new Uint8Array(data.subarray(1, 4)), 41368 balance: data[4] + data[5] / 256, 41369 }; 41370 }, 41371 stbl: function (data) { 41372 return { 41373 boxes: inspectMp4(data), 41374 }; 41375 }, 41376 ctts: function (data) { 41377 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 41378 result = { 41379 version: view.getUint8(0), 41380 flags: new Uint8Array(data.subarray(1, 4)), 41381 compositionOffsets: [], 41382 }, 41383 entryCount = view.getUint32(4), 41384 i; 41385 for (i = 8; entryCount; i += 8, entryCount--) { 41386 result.compositionOffsets.push({ 41387 sampleCount: view.getUint32(i), 41388 sampleOffset: view[result.version === 0 ? 'getUint32' : 'getInt32'](i + 4), 41389 }); 41390 } 41391 return result; 41392 }, 41393 stss: function (data) { 41394 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 41395 result = { 41396 version: view.getUint8(0), 41397 flags: new Uint8Array(data.subarray(1, 4)), 41398 syncSamples: [], 41399 }, 41400 entryCount = view.getUint32(4), 41401 i; 41402 for (i = 8; entryCount; i += 4, entryCount--) { 41403 result.syncSamples.push(view.getUint32(i)); 41404 } 41405 return result; 41406 }, 41407 stco: function (data) { 41408 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 41409 result = { 41410 version: data[0], 41411 flags: new Uint8Array(data.subarray(1, 4)), 41412 chunkOffsets: [], 41413 }, 41414 entryCount = view.getUint32(4), 41415 i; 41416 for (i = 8; entryCount; i += 4, entryCount--) { 41417 result.chunkOffsets.push(view.getUint32(i)); 41418 } 41419 return result; 41420 }, 41421 stsc: function (data) { 41422 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 41423 entryCount = view.getUint32(4), 41424 result = { 41425 version: data[0], 41426 flags: new Uint8Array(data.subarray(1, 4)), 41427 sampleToChunks: [], 41428 }, 41429 i; 41430 for (i = 8; entryCount; i += 12, entryCount--) { 41431 result.sampleToChunks.push({ 41432 firstChunk: view.getUint32(i), 41433 samplesPerChunk: view.getUint32(i + 4), 41434 sampleDescriptionIndex: view.getUint32(i + 8), 41435 }); 41436 } 41437 return result; 41438 }, 41439 stsd: function (data) { 41440 return { 41441 version: data[0], 41442 flags: new Uint8Array(data.subarray(1, 4)), 41443 sampleDescriptions: inspectMp4(data.subarray(8)), 41444 }; 41445 }, 41446 stsz: function (data) { 41447 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 41448 result = { 41449 version: data[0], 41450 flags: new Uint8Array(data.subarray(1, 4)), 41451 sampleSize: view.getUint32(4), 41452 entries: [], 41453 }, 41454 i; 41455 for (i = 12; i < data.byteLength; i += 4) { 41456 result.entries.push(view.getUint32(i)); 41457 } 41458 return result; 41459 }, 41460 stts: function (data) { 41461 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 41462 result = { 41463 version: data[0], 41464 flags: new Uint8Array(data.subarray(1, 4)), 41465 timeToSamples: [], 41466 }, 41467 entryCount = view.getUint32(4), 41468 i; 41469 for (i = 8; entryCount; i += 8, entryCount--) { 41470 result.timeToSamples.push({ 41471 sampleCount: view.getUint32(i), 41472 sampleDelta: view.getUint32(i + 4), 41473 }); 41474 } 41475 return result; 41476 }, 41477 styp: function (data) { 41478 return parse.ftyp(data); 41479 }, 41480 tfdt: parseTfdt$3, 41481 tfhd: parseTfhd$2, 41482 tkhd: function (data) { 41483 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 41484 i = 4, 41485 result = { 41486 version: view.getUint8(0), 41487 flags: new Uint8Array(data.subarray(1, 4)), 41488 }; 41489 if (result.version === 1) { 41490 i += 4; 41491 result.creationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes 41492 41493 i += 8; 41494 result.modificationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes 41495 41496 i += 4; 41497 result.trackId = view.getUint32(i); 41498 i += 4; 41499 i += 8; 41500 result.duration = view.getUint32(i); // truncating top 4 bytes 41501 } else { 41502 result.creationTime = parseMp4Date(view.getUint32(i)); 41503 i += 4; 41504 result.modificationTime = parseMp4Date(view.getUint32(i)); 41505 i += 4; 41506 result.trackId = view.getUint32(i); 41507 i += 4; 41508 i += 4; 41509 result.duration = view.getUint32(i); 41510 } 41511 i += 4; 41512 i += 2 * 4; 41513 result.layer = view.getUint16(i); 41514 i += 2; 41515 result.alternateGroup = view.getUint16(i); 41516 i += 2; // convert fixed-point, base 16 back to a number 41517 41518 result.volume = view.getUint8(i) + view.getUint8(i + 1) / 8; 41519 i += 2; 41520 i += 2; 41521 result.matrix = new Uint32Array(data.subarray(i, i + 9 * 4)); 41522 i += 9 * 4; 41523 result.width = view.getUint16(i) + view.getUint16(i + 2) / 65536; 41524 i += 4; 41525 result.height = view.getUint16(i) + view.getUint16(i + 2) / 65536; 41526 return result; 41527 }, 41528 traf: function (data) { 41529 return { 41530 boxes: inspectMp4(data), 41531 }; 41532 }, 41533 trak: function (data) { 41534 return { 41535 boxes: inspectMp4(data), 41536 }; 41537 }, 41538 trex: function (data) { 41539 var view = new DataView(data.buffer, data.byteOffset, data.byteLength); 41540 return { 41541 version: data[0], 41542 flags: new Uint8Array(data.subarray(1, 4)), 41543 trackId: view.getUint32(4), 41544 defaultSampleDescriptionIndex: view.getUint32(8), 41545 defaultSampleDuration: view.getUint32(12), 41546 defaultSampleSize: view.getUint32(16), 41547 sampleDependsOn: data[20] & 0x03, 41548 sampleIsDependedOn: (data[21] & 0xc0) >> 6, 41549 sampleHasRedundancy: (data[21] & 0x30) >> 4, 41550 samplePaddingValue: (data[21] & 0x0e) >> 1, 41551 sampleIsDifferenceSample: !!(data[21] & 0x01), 41552 sampleDegradationPriority: view.getUint16(22), 41553 }; 41554 }, 41555 trun: parseTrun$2, 41556 'url ': function (data) { 41557 return { 41558 version: data[0], 41559 flags: new Uint8Array(data.subarray(1, 4)), 41560 }; 41561 }, 41562 vmhd: function (data) { 41563 var view = new DataView(data.buffer, data.byteOffset, data.byteLength); 41564 return { 41565 version: data[0], 41566 flags: new Uint8Array(data.subarray(1, 4)), 41567 graphicsmode: view.getUint16(4), 41568 opcolor: new Uint16Array([view.getUint16(6), view.getUint16(8), view.getUint16(10)]), 41569 }; 41570 }, 41571 }; 41572 /** 41573 * Return a javascript array of box objects parsed from an ISO base 41574 * media file. 41575 * @param data {Uint8Array} the binary data of the media to be inspected 41576 * @return {array} a javascript array of potentially nested box objects 41577 */ 41578 41579 inspectMp4 = function (data) { 41580 var i = 0, 41581 result = [], 41582 view, 41583 size, 41584 type, 41585 end, 41586 box; // Convert data from Uint8Array to ArrayBuffer, to follow Dataview API 41587 41588 var ab = new ArrayBuffer(data.length); 41589 var v = new Uint8Array(ab); 41590 for (var z = 0; z < data.length; ++z) { 41591 v[z] = data[z]; 41592 } 41593 view = new DataView(ab); 41594 while (i < data.byteLength) { 41595 // parse box data 41596 size = view.getUint32(i); 41597 type = parseType$2(data.subarray(i + 4, i + 8)); 41598 end = size > 1 ? i + size : data.byteLength; // parse type-specific data 41599 41600 box = ( 41601 parse[type] || 41602 function (data) { 41603 return { 41604 data: data, 41605 }; 41606 } 41607 )(data.subarray(i + 8, end)); 41608 box.size = size; 41609 box.type = type; // store this box and move to the next 41610 41611 result.push(box); 41612 i = end; 41613 } 41614 return result; 41615 }; 41616 /** 41617 * Returns a textual representation of the javascript represtentation 41618 * of an MP4 file. You can use it as an alternative to 41619 * JSON.stringify() to compare inspected MP4s. 41620 * @param inspectedMp4 {array} the parsed array of boxes in an MP4 41621 * file 41622 * @param depth {number} (optional) the number of ancestor boxes of 41623 * the elements of inspectedMp4. Assumed to be zero if unspecified. 41624 * @return {string} a text representation of the parsed MP4 41625 */ 41626 41627 textifyMp4 = function (inspectedMp4, depth) { 41628 var indent; 41629 depth = depth || 0; 41630 indent = new Array(depth * 2 + 1).join(' '); // iterate over all the boxes 41631 41632 return inspectedMp4 41633 .map(function (box, index) { 41634 // list the box type first at the current indentation level 41635 return ( 41636 indent + 41637 box.type + 41638 '\n' + 41639 // the type is already included and handle child boxes separately 41640 Object.keys(box) 41641 .filter(function (key) { 41642 return key !== 'type' && key !== 'boxes'; // output all the box properties 41643 }) 41644 .map(function (key) { 41645 var prefix = indent + ' ' + key + ': ', 41646 value = box[key]; // print out raw bytes as hexademical 41647 41648 if (value instanceof Uint8Array || value instanceof Uint32Array) { 41649 var bytes = Array.prototype.slice 41650 .call(new Uint8Array(value.buffer, value.byteOffset, value.byteLength)) 41651 .map(function (byte) { 41652 return ' ' + ('00' + byte.toString(16)).slice(-2); 41653 }) 41654 .join('') 41655 .match(/.{1,24}/g); 41656 if (!bytes) { 41657 return prefix + '<>'; 41658 } 41659 if (bytes.length === 1) { 41660 return prefix + '<' + bytes.join('').slice(1) + '>'; 41661 } 41662 return ( 41663 prefix + 41664 '<\n' + 41665 bytes 41666 .map(function (line) { 41667 return indent + ' ' + line; 41668 }) 41669 .join('\n') + 41670 '\n' + 41671 indent + 41672 ' >' 41673 ); 41674 } // stringify generic objects 41675 41676 return ( 41677 prefix + 41678 JSON.stringify(value, null, 2) 41679 .split('\n') 41680 .map(function (line, index) { 41681 if (index === 0) { 41682 return line; 41683 } 41684 return indent + ' ' + line; 41685 }) 41686 .join('\n') 41687 ); 41688 }) 41689 .join('\n') + 41690 // recursively textify the child boxes 41691 (box.boxes ? '\n' + textifyMp4(box.boxes, depth + 1) : '') 41692 ); 41693 }) 41694 .join('\n'); 41695 }; 41696 var mp4Inspector = { 41697 inspect: inspectMp4, 41698 textify: textifyMp4, 41699 parseType: parseType$2, 41700 findBox: findBox$4, 41701 parseTraf: parse.traf, 41702 parseTfdt: parse.tfdt, 41703 parseHdlr: parse.hdlr, 41704 parseTfhd: parse.tfhd, 41705 parseTrun: parse.trun, 41706 parseSidx: parse.sidx, 41707 }; 41708 /** 41709 * Returns the first string in the data array ending with a null char '\0' 41710 * @param {UInt8} data 41711 * @returns the string with the null char 41712 */ 41713 41714 var uint8ToCString$1 = function (data) { 41715 var index = 0; 41716 var curChar = String.fromCharCode(data[index]); 41717 var retString = ''; 41718 while (curChar !== '\0') { 41719 retString += curChar; 41720 index++; 41721 curChar = String.fromCharCode(data[index]); 41722 } // Add nullChar 41723 41724 retString += curChar; 41725 return retString; 41726 }; 41727 var string = { 41728 uint8ToCString: uint8ToCString$1, 41729 }; 41730 var uint8ToCString = string.uint8ToCString; 41731 var getUint64$1 = numbers.getUint64; 41732 /** 41733 * Based on: ISO/IEC 23009 Section: 5.10.3.3 41734 * References: 41735 * https://dashif-documents.azurewebsites.net/Events/master/event.html#emsg-format 41736 * https://aomediacodec.github.io/id3-emsg/ 41737 * 41738 * Takes emsg box data as a uint8 array and returns a emsg box object 41739 * @param {UInt8Array} boxData data from emsg box 41740 * @returns A parsed emsg box object 41741 */ 41742 41743 var parseEmsgBox = function (boxData) { 41744 // version + flags 41745 var offset = 4; 41746 var version = boxData[0]; 41747 var scheme_id_uri, 41748 value, 41749 timescale, 41750 presentation_time, 41751 presentation_time_delta, 41752 event_duration, 41753 id, 41754 message_data; 41755 if (version === 0) { 41756 scheme_id_uri = uint8ToCString(boxData.subarray(offset)); 41757 offset += scheme_id_uri.length; 41758 value = uint8ToCString(boxData.subarray(offset)); 41759 offset += value.length; 41760 var dv = new DataView(boxData.buffer); 41761 timescale = dv.getUint32(offset); 41762 offset += 4; 41763 presentation_time_delta = dv.getUint32(offset); 41764 offset += 4; 41765 event_duration = dv.getUint32(offset); 41766 offset += 4; 41767 id = dv.getUint32(offset); 41768 offset += 4; 41769 } else if (version === 1) { 41770 var dv = new DataView(boxData.buffer); 41771 timescale = dv.getUint32(offset); 41772 offset += 4; 41773 presentation_time = getUint64$1(boxData.subarray(offset)); 41774 offset += 8; 41775 event_duration = dv.getUint32(offset); 41776 offset += 4; 41777 id = dv.getUint32(offset); 41778 offset += 4; 41779 scheme_id_uri = uint8ToCString(boxData.subarray(offset)); 41780 offset += scheme_id_uri.length; 41781 value = uint8ToCString(boxData.subarray(offset)); 41782 offset += value.length; 41783 } 41784 message_data = new Uint8Array(boxData.subarray(offset, boxData.byteLength)); 41785 var emsgBox = { 41786 scheme_id_uri, 41787 value, 41788 // if timescale is undefined or 0 set to 1 41789 timescale: timescale ? timescale : 1, 41790 presentation_time, 41791 presentation_time_delta, 41792 event_duration, 41793 id, 41794 message_data, 41795 }; 41796 return isValidEmsgBox(version, emsgBox) ? emsgBox : undefined; 41797 }; 41798 /** 41799 * Scales a presentation time or time delta with an offset with a provided timescale 41800 * @param {number} presentationTime 41801 * @param {number} timescale 41802 * @param {number} timeDelta 41803 * @param {number} offset 41804 * @returns the scaled time as a number 41805 */ 41806 41807 var scaleTime = function (presentationTime, timescale, timeDelta, offset) { 41808 return presentationTime || presentationTime === 0 41809 ? presentationTime / timescale 41810 : offset + timeDelta / timescale; 41811 }; 41812 /** 41813 * Checks the emsg box data for validity based on the version 41814 * @param {number} version of the emsg box to validate 41815 * @param {Object} emsg the emsg data to validate 41816 * @returns if the box is valid as a boolean 41817 */ 41818 41819 var isValidEmsgBox = function (version, emsg) { 41820 var hasScheme = emsg.scheme_id_uri !== '\0'; 41821 var isValidV0Box = version === 0 && isDefined(emsg.presentation_time_delta) && hasScheme; 41822 var isValidV1Box = version === 1 && isDefined(emsg.presentation_time) && hasScheme; // Only valid versions of emsg are 0 and 1 41823 41824 return (!(version > 1) && isValidV0Box) || isValidV1Box; 41825 }; // Utility function to check if an object is defined 41826 41827 var isDefined = function (data) { 41828 return data !== undefined || data !== null; 41829 }; 41830 var emsg$1 = { 41831 parseEmsgBox: parseEmsgBox, 41832 scaleTime: scaleTime, 41833 }; 41834 var win; 41835 if (typeof window !== 'undefined') { 41836 win = window; 41837 } else if (typeof commonjsGlobal !== 'undefined') { 41838 win = commonjsGlobal; 41839 } else if (typeof self !== 'undefined') { 41840 win = self; 41841 } else { 41842 win = {}; 41843 } 41844 var window_1 = win; 41845 /** 41846 * mux.js 41847 * 41848 * Copyright (c) Brightcove 41849 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 41850 * 41851 * Utilities to detect basic properties and metadata about MP4s. 41852 */ 41853 41854 var toUnsigned = bin.toUnsigned; 41855 var toHexString = bin.toHexString; 41856 var findBox$3 = findBox_1; 41857 var parseType$1 = parseType_1; 41858 var emsg = emsg$1; 41859 var parseTfhd$1 = parseTfhd$2; 41860 var parseTrun$1 = parseTrun$2; 41861 var parseTfdt$2 = parseTfdt$3; 41862 var getUint64 = numbers.getUint64; 41863 var timescale, 41864 startTime, 41865 compositionStartTime, 41866 getVideoTrackIds, 41867 getTracks, 41868 getTimescaleFromMediaHeader$1, 41869 getEmsgID3; 41870 var window$2 = window_1; 41871 var parseId3Frames = parseId3.parseId3Frames; 41872 /** 41873 * Parses an MP4 initialization segment and extracts the timescale 41874 * values for any declared tracks. Timescale values indicate the
41875 * number of clock ticks per second to assume for time-based values 41876 * elsewhere in the MP4. 41877 * 41878 * To determine the start time of an MP4, you need two pieces of 41879 * information: the timescale unit and the earliest base media decode 41880 * time. Multiple timescales can be specified within an MP4 but the 41881 * base media decode time is always expressed in the timescale from 41882 * the media header box for the track: 41883 * ``` 41884 * moov > trak > mdia > mdhd.timescale 41885 * ``` 41886 * @param init {Uint8Array} the bytes of the init segment 41887 * @return {object} a hash of track ids to timescale values or null if 41888 * the init segment is malformed. 41889 */ 41890 41891 timescale = function (init) { 41892 var result = {}, 41893 traks = findBox$3(init, ['moov', 'trak']); // mdhd timescale 41894 41895 return traks.reduce(function (result, trak) { 41896 var tkhd, version, index, id, mdhd; 41897 tkhd = findBox$3(trak, ['tkhd'])[0]; 41898 if (!tkhd) { 41899 return null; 41900 } 41901 version = tkhd[0]; 41902 index = version === 0 ? 12 : 20; 41903 id = toUnsigned( 41904 (tkhd[index] << 24) | (tkhd[index + 1] << 16) | (tkhd[index + 2] << 8) | tkhd[index + 3] 41905 ); 41906 mdhd = findBox$3(trak, ['mdia', 'mdhd'])[0]; 41907 if (!mdhd) { 41908 return null; 41909 } 41910 version = mdhd[0]; 41911 index = version === 0 ? 12 : 20; 41912 result[id] = toUnsigned( 41913 (mdhd[index] << 24) | (mdhd[index + 1] << 16) | (mdhd[index + 2] << 8) | mdhd[index + 3] 41914 ); 41915 return result; 41916 }, result); 41917 }; 41918 /** 41919 * Determine the base media decode start time, in seconds, for an MP4 41920 * fragment. If multiple fragments are specified, the earliest time is 41921 * returned. 41922 * 41923 * The base media decode time can be parsed from track fragment 41924 * metadata: 41925 * ``` 41926 * moof > traf > tfdt.baseMediaDecodeTime 41927 * ``` 41928 * It requires the timescale value from the mdhd to interpret. 41929 * 41930 * @param timescale {object} a hash of track ids to timescale values. 41931 * @return {number} the earliest base media decode start time for the 41932 * fragment, in seconds 41933 */ 41934 41935 startTime = function (timescale, fragment) { 41936 var trafs; // we need info from two childrend of each track fragment box 41937 41938 trafs = findBox$3(fragment, ['moof', 'traf']); // determine the start times for each track 41939 41940 var lowestTime = trafs.reduce(function (acc, traf) { 41941 var tfhd = findBox$3(traf, ['tfhd'])[0]; // get the track id from the tfhd 41942 41943 var id = toUnsigned((tfhd[4] << 24) | (tfhd[5] << 16) | (tfhd[6] << 8) | tfhd[7]); // assume a 90kHz clock if no timescale was specified 41944 41945 var scale = timescale[id] || 90e3; // get the base media decode time from the tfdt 41946 41947 var tfdt = findBox$3(traf, ['tfdt'])[0]; 41948 var dv = new DataView(tfdt.buffer, tfdt.byteOffset, tfdt.byteLength); 41949 var baseTime; // version 1 is 64 bit 41950 41951 if (tfdt[0] === 1) { 41952 baseTime = getUint64(tfdt.subarray(4, 12)); 41953 } else { 41954 baseTime = dv.getUint32(4); 41955 }
vendor: 14,631 bytes, lines 41955-42324
41955 // convert base time to seconds if it is a valid number. 41956 41957 let seconds; 41958 if (typeof baseTime === 'bigint') { 41959 seconds = baseTime / window$2.BigInt(scale); 41960 } else if (typeof baseTime === 'number' && !isNaN(baseTime)) { 41961 seconds = baseTime / scale; 41962 } 41963 if (seconds < Number.MAX_SAFE_INTEGER) { 41964 seconds = Number(seconds); 41965 } 41966 if (seconds < acc) { 41967 acc = seconds; 41968 } 41969 return acc; 41970 }, Infinity); 41971 return typeof lowestTime === 'bigint' || isFinite(lowestTime) ? lowestTime : 0; 41972 }; 41973 /** 41974 * Determine the composition start, in seconds, for an MP4 41975 * fragment. 41976 * 41977 * The composition start time of a fragment can be calculated using the base 41978 * media decode time, composition time offset, and timescale, as follows: 41979 * 41980 * compositionStartTime = (baseMediaDecodeTime + compositionTimeOffset) / timescale 41981 * 41982 * All of the aforementioned information is contained within a media fragment's 41983 * `traf` box, except for timescale info, which comes from the initialization 41984 * segment, so a track id (also contained within a `traf`) is also necessary to 41985 * associate it with a timescale 41986 * 41987 * 41988 * @param timescales {object} - a hash of track ids to timescale values. 41989 * @param fragment {Unit8Array} - the bytes of a media segment 41990 * @return {number} the composition start time for the fragment, in seconds 41991 **/ 41992 41993 compositionStartTime = function (timescales, fragment) { 41994 var trafBoxes = findBox$3(fragment, ['moof', 'traf']); 41995 var baseMediaDecodeTime = 0; 41996 var compositionTimeOffset = 0; 41997 var trackId; 41998 if (trafBoxes && trafBoxes.length) { 41999 // The spec states that track run samples contained within a `traf` box are contiguous, but 42000 // it does not explicitly state whether the `traf` boxes themselves are contiguous. 42001 // We will assume that they are, so we only need the first to calculate start time. 42002 var tfhd = findBox$3(trafBoxes[0], ['tfhd'])[0]; 42003 var trun = findBox$3(trafBoxes[0], ['trun'])[0]; 42004 var tfdt = findBox$3(trafBoxes[0], ['tfdt'])[0]; 42005 if (tfhd) { 42006 var parsedTfhd = parseTfhd$1(tfhd); 42007 trackId = parsedTfhd.trackId; 42008 } 42009 if (tfdt) { 42010 var parsedTfdt = parseTfdt$2(tfdt); 42011 baseMediaDecodeTime = parsedTfdt.baseMediaDecodeTime; 42012 } 42013 if (trun) { 42014 var parsedTrun = parseTrun$1(trun); 42015 if (parsedTrun.samples && parsedTrun.samples.length) { 42016 compositionTimeOffset = parsedTrun.samples[0].compositionTimeOffset || 0; 42017 } 42018 } 42019 } // Get timescale for this specific track. Assume a 90kHz clock if no timescale was 42020 // specified. 42021 42022 var timescale = timescales[trackId] || 90e3; // return the composition start time, in seconds 42023 42024 if (typeof baseMediaDecodeTime === 'bigint') { 42025 compositionTimeOffset = window$2.BigInt(compositionTimeOffset); 42026 timescale = window$2.BigInt(timescale); 42027 } 42028 var result = (baseMediaDecodeTime + compositionTimeOffset) / timescale; 42029 if (typeof result === 'bigint' && result < Number.MAX_SAFE_INTEGER) { 42030 result = Number(result); 42031 } 42032 return result; 42033 }; 42034 /** 42035 * Find the trackIds of the video tracks in this source. 42036 * Found by parsing the Handler Reference and Track Header Boxes: 42037 * moov > trak > mdia > hdlr 42038 * moov > trak > tkhd 42039 * 42040 * @param {Uint8Array} init - The bytes of the init segment for this source 42041 * @return {Number[]} A list of trackIds 42042 * 42043 * @see ISO-BMFF-12/2015, Section 8.4.3 42044 **/ 42045 42046 getVideoTrackIds = function (init) { 42047 var traks = findBox$3(init, ['moov', 'trak']); 42048 var videoTrackIds = []; 42049 traks.forEach(function (trak) { 42050 var hdlrs = findBox$3(trak, ['mdia', 'hdlr']); 42051 var tkhds = findBox$3(trak, ['tkhd']); 42052 hdlrs.forEach(function (hdlr, index) { 42053 var handlerType = parseType$1(hdlr.subarray(8, 12)); 42054 var tkhd = tkhds[index]; 42055 var view; 42056 var version; 42057 var trackId; 42058 if (handlerType === 'vide') { 42059 view = new DataView(tkhd.buffer, tkhd.byteOffset, tkhd.byteLength); 42060 version = view.getUint8(0); 42061 trackId = version === 0 ? view.getUint32(12) : view.getUint32(20); 42062 videoTrackIds.push(trackId); 42063 } 42064 }); 42065 }); 42066 return videoTrackIds; 42067 }; 42068 getTimescaleFromMediaHeader$1 = function (mdhd) { 42069 // mdhd is a FullBox, meaning it will have its own version as the first byte 42070 var version = mdhd[0]; 42071 var index = version === 0 ? 12 : 20; 42072 return toUnsigned( 42073 (mdhd[index] << 24) | (mdhd[index + 1] << 16) | (mdhd[index + 2] << 8) | mdhd[index + 3] 42074 ); 42075 }; 42076 /** 42077 * Get all the video, audio, and hint tracks from a non fragmented 42078 * mp4 segment 42079 */ 42080 42081 getTracks = function (init) { 42082 var traks = findBox$3(init, ['moov', 'trak']); 42083 var tracks = []; 42084 traks.forEach(function (trak) { 42085 var track = {}; 42086 var tkhd = findBox$3(trak, ['tkhd'])[0]; 42087 var view, tkhdVersion; // id 42088 42089 if (tkhd) { 42090 view = new DataView(tkhd.buffer, tkhd.byteOffset, tkhd.byteLength); 42091 tkhdVersion = view.getUint8(0); 42092 track.id = tkhdVersion === 0 ? view.getUint32(12) : view.getUint32(20); 42093 } 42094 var hdlr = findBox$3(trak, ['mdia', 'hdlr'])[0]; // type 42095 42096 if (hdlr) { 42097 var type = parseType$1(hdlr.subarray(8, 12)); 42098 if (type === 'vide') { 42099 track.type = 'video'; 42100 } else if (type === 'soun') { 42101 track.type = 'audio'; 42102 } else { 42103 track.type = type; 42104 } 42105 } // codec 42106 42107 var stsd = findBox$3(trak, ['mdia', 'minf', 'stbl', 'stsd'])[0]; 42108 if (stsd) { 42109 var sampleDescriptions = stsd.subarray(8); // gives the codec type string 42110 42111 track.codec = parseType$1(sampleDescriptions.subarray(4, 8)); 42112 var codecBox = findBox$3(sampleDescriptions, [track.codec])[0]; 42113 var codecConfig, codecConfigType; 42114 if (codecBox) { 42115 // https://tools.ietf.org/html/rfc6381#section-3.3 42116 if (/^[asm]vc[1-9]$/i.test(track.codec)) { 42117 // we don't need anything but the "config" parameter of the 42118 // avc1 codecBox 42119 codecConfig = codecBox.subarray(78); 42120 codecConfigType = parseType$1(codecConfig.subarray(4, 8)); 42121 if (codecConfigType === 'avcC' && codecConfig.length > 11) { 42122 track.codec += '.'; // left padded with zeroes for single digit hex 42123 // profile idc 42124 42125 track.codec += toHexString(codecConfig[9]); // the byte containing the constraint_set flags 42126 42127 track.codec += toHexString(codecConfig[10]); // level idc 42128 42129 track.codec += toHexString(codecConfig[11]); 42130 } else { 42131 // TODO: show a warning that we couldn't parse the codec 42132 // and are using the default 42133 track.codec = 'avc1.4d400d'; 42134 } 42135 } else if (/^mp4[a,v]$/i.test(track.codec)) { 42136 // we do not need anything but the streamDescriptor of the mp4a codecBox 42137 codecConfig = codecBox.subarray(28); 42138 codecConfigType = parseType$1(codecConfig.subarray(4, 8)); 42139 if (codecConfigType === 'esds' && codecConfig.length > 20 && codecConfig[19] !== 0) { 42140 track.codec += '.' + toHexString(codecConfig[19]); // this value is only a single digit 42141 42142 track.codec += '.' + toHexString((codecConfig[20] >>> 2) & 0x3f).replace(/^0/, ''); 42143 } else { 42144 // TODO: show a warning that we couldn't parse the codec 42145 // and are using the default 42146 track.codec = 'mp4a.40.2'; 42147 } 42148 } else { 42149 // flac, opus, etc 42150 track.codec = track.codec.toLowerCase(); 42151 } 42152 } 42153 } 42154 var mdhd = findBox$3(trak, ['mdia', 'mdhd'])[0]; 42155 if (mdhd) { 42156 track.timescale = getTimescaleFromMediaHeader$1(mdhd); 42157 } 42158 tracks.push(track); 42159 }); 42160 return tracks; 42161 }; 42162 /** 42163 * Returns an array of emsg ID3 data from the provided segmentData. 42164 * An offset can also be provided as the Latest Arrival Time to calculate 42165 * the Event Start Time of v0 EMSG boxes. 42166 * See: https://dashif-documents.azurewebsites.net/Events/master/event.html#Inband-event-timing 42167 * 42168 * @param {Uint8Array} segmentData the segment byte array. 42169 * @param {number} offset the segment start time or Latest Arrival Time, 42170 * @return {Object[]} an array of ID3 parsed from EMSG boxes 42171 */ 42172 42173 getEmsgID3 = function (segmentData, offset = 0) { 42174 var emsgBoxes = findBox$3(segmentData, ['emsg']); 42175 return emsgBoxes.map((data) => { 42176 var parsedBox = emsg.parseEmsgBox(new Uint8Array(data)); 42177 var parsedId3Frames = parseId3Frames(parsedBox.message_data); 42178 return { 42179 cueTime: emsg.scaleTime( 42180 parsedBox.presentation_time, 42181 parsedBox.timescale, 42182 parsedBox.presentation_time_delta, 42183 offset 42184 ), 42185 duration: emsg.scaleTime(parsedBox.event_duration, parsedBox.timescale), 42186 frames: parsedId3Frames, 42187 }; 42188 }); 42189 }; 42190 var probe$2 = { 42191 // export mp4 inspector's findBox and parseType for backwards compatibility 42192 findBox: findBox$3, 42193 parseType: parseType$1, 42194 timescale: timescale, 42195 startTime: startTime, 42196 compositionStartTime: compositionStartTime, 42197 videoTrackIds: getVideoTrackIds, 42198 tracks: getTracks, 42199 getTimescaleFromMediaHeader: getTimescaleFromMediaHeader$1, 42200 getEmsgID3: getEmsgID3, 42201 }; 42202 const { parseTrun } = mp4Inspector; 42203 const { findBox: findBox$2 } = probe$2; 42204 var window$1 = window_1; 42205 /** 42206 * Utility function for parsing data from mdat boxes. 42207 * @param {Array<Uint8Array>} segment the segment data to create mdat/traf pairs from. 42208 * @returns mdat and traf boxes paired up for easier parsing. 42209 */ 42210 42211 var getMdatTrafPairs$2 = function (segment) { 42212 var trafs = findBox$2(segment, ['moof', 'traf']); 42213 var mdats = findBox$2(segment, ['mdat']); 42214 var mdatTrafPairs = []; // Pair up each traf with a mdat as moofs and mdats are in pairs 42215 42216 mdats.forEach(function (mdat, index) { 42217 var matchingTraf = trafs[index]; 42218 mdatTrafPairs.push({ 42219 mdat: mdat, 42220 traf: matchingTraf, 42221 }); 42222 }); 42223 return mdatTrafPairs; 42224 }; 42225 /** 42226 * Parses sample information out of Track Run Boxes and calculates 42227 * the absolute presentation and decode timestamps of each sample. 42228 * 42229 * @param {Array<Uint8Array>} truns - The Trun Run boxes to be parsed 42230 * @param {Number|BigInt} baseMediaDecodeTime - base media decode time from tfdt 42231 @see ISO-BMFF-12/2015, Section 8.8.12 42232 * @param {Object} tfhd - The parsed Track Fragment Header 42233 * @see inspect.parseTfhd 42234 * @return {Object[]} the parsed samples 42235 * 42236 * @see ISO-BMFF-12/2015, Section 8.8.8 42237 **/ 42238 42239 var parseSamples$2 = function (truns, baseMediaDecodeTime, tfhd) { 42240 var currentDts = baseMediaDecodeTime; 42241 var defaultSampleDuration = tfhd.defaultSampleDuration || 0; 42242 var defaultSampleSize = tfhd.defaultSampleSize || 0; 42243 var trackId = tfhd.trackId; 42244 var allSamples = []; 42245 truns.forEach(function (trun) { 42246 // Note: We currently do not parse the sample table as well 42247 // as the trun. It's possible some sources will require this. 42248 // moov > trak > mdia > minf > stbl 42249 var trackRun = parseTrun(trun); 42250 var samples = trackRun.samples; 42251 samples.forEach(function (sample) { 42252 if (sample.duration === undefined) { 42253 sample.duration = defaultSampleDuration; 42254 } 42255 if (sample.size === undefined) { 42256 sample.size = defaultSampleSize; 42257 } 42258 sample.trackId = trackId; 42259 sample.dts = currentDts; 42260 if (sample.compositionTimeOffset === undefined) { 42261 sample.compositionTimeOffset = 0; 42262 } 42263 if (typeof currentDts === 'bigint') { 42264 sample.pts = currentDts + window$1.BigInt(sample.compositionTimeOffset); 42265 currentDts += window$1.BigInt(sample.duration); 42266 } else { 42267 sample.pts = currentDts + sample.compositionTimeOffset; 42268 currentDts += sample.duration; 42269 } 42270 }); 42271 allSamples = allSamples.concat(samples); 42272 }); 42273 return allSamples; 42274 }; 42275 var samples = { 42276 getMdatTrafPairs: getMdatTrafPairs$2, 42277 parseSamples: parseSamples$2, 42278 }; 42279 /** 42280 * mux.js 42281 * 42282 * Copyright (c) Brightcove 42283 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 42284 * 42285 * Reads in-band CEA-708 captions out of FMP4 segments. 42286 * @see https://en.wikipedia.org/wiki/CEA-708 42287 */ 42288 42289 var discardEmulationPreventionBytes = captionPacketParser.discardEmulationPreventionBytes; 42290 var CaptionStream = captionStream.CaptionStream; 42291 var findBox$1 = findBox_1; 42292 var parseTfdt$1 = parseTfdt$3; 42293 var parseTfhd = parseTfhd$2; 42294 var { getMdatTrafPairs: getMdatTrafPairs$1, parseSamples: parseSamples$1 } = samples; 42295 /** 42296 * Maps an offset in the mdat to a sample based on the the size of the samples. 42297 * Assumes that `parseSamples` has been called first. 42298 * 42299 * @param {Number} offset - The offset into the mdat 42300 * @param {Object[]} samples - An array of samples, parsed using `parseSamples` 42301 * @return {?Object} The matching sample, or null if no match was found. 42302 * 42303 * @see ISO-BMFF-12/2015, Section 8.8.8 42304 **/ 42305 42306 var mapToSample = function (offset, samples) { 42307 var approximateOffset = offset; 42308 for (var i = 0; i < samples.length; i++) { 42309 var sample = samples[i]; 42310 if (approximateOffset < sample.size) { 42311 return sample; 42312 } 42313 approximateOffset -= sample.size; 42314 } 42315 return null; 42316 }; 42317 /** 42318 * Finds SEI nal units contained in a Media Data Box. 42319 * Assumes that `parseSamples` has been called first. 42320 * 42321 * @param {Uint8Array} avcStream - The bytes of the mdat 42322 * @param {Object[]} samples - The samples parsed out by `parseSamples` 42323 * @param {Number} trackId - The trackId of this video track 42324 * @return {Object[]}
42324 seiNals - the parsed SEI NALUs found. 42325 * The contents of the seiNal should match what is expected by 42326 * CaptionStream.push (nalUnitType, size, data, escapedRBSP, pts, dts) 42327 * 42328 * @see ISO-BMFF-12/2015, Section 8.1.1 42329 * @see Rec. ITU-T H.264, 7.3.2.3.1 42330 **/ 42331 42332 var findSeiNals = function (avcStream, samples, trackId) { 42333 var avcView = new DataView(avcStream.buffer, avcStream.byteOffset, avcStream.byteLength), 42334 result = { 42335 logs: [], 42336 seiNals: [], 42337 }, 42338 seiNal, 42339 i, 42340 length, 42341 lastMatchedSample; 42342 for (i = 0; i + 4 < avcStream.length; i += length) { 42343 length = avcView.getUint32(i); 42344 i += 4; // Bail if this doesn't appear to be an H264 stream 42345 42346 if (length <= 0) { 42347 continue; 42348 } 42349 switch (avcStream[i] & 0x1f) { 42350 case 0x06: 42351 var data = avcStream.subarray(i + 1, i + 1 + length); 42352 var matchingSample = mapToSample(i, samples); 42353 seiNal = { 42354 nalUnitType: 'sei_rbsp', 42355 size: length, 42356 data: data, 42357 escapedRBSP: discardEmulationPreventionBytes(data), 42358 trackId: trackId, 42359 }; 42360 if (matchingSample) { 42361 seiNal.pts = matchingSample.pts; 42362 seiNal.dts = matchingSample.dts; 42363 lastMatchedSample = matchingSample; 42364 } else if (lastMatchedSample) { 42365 // If a matching sample cannot be found, use the last 42366 // sample's values as they should be as close as possible 42367 seiNal.pts = lastMatchedSample.pts; 42368 seiNal.dts = lastMatchedSample.dts; 42369 } else { 42370 result.logs.push({ 42371 level: 'warn', 42372 message: 42373 "We've encountered a nal unit without data at " + 42374 i + 42375 ' for trackId ' + 42376 trackId + 42377 '. See mux.js#223.', 42378 }); 42379 break; 42380 } 42381 result.seiNals.push(seiNal); 42382 break; 42383 } 42384 } 42385 return result; 42386 }; 42387 /** 42388 * Parses out caption nals from an FMP4 segment's video tracks. 42389 * 42390 * @param {Uint8Array} segment - The bytes of a single segment 42391 * @param {Number} videoTrackId - The trackId of a video track in the segment 42392 * @return {Object.<Number, Object[]>} A mapping of video trackId to 42393 * a list of seiNals found in that track 42394 **/ 42395 42396 var parseCaptionNals = function (segment, videoTrackId) { 42397 var captionNals = {}; 42398 var mdatTrafPairs = getMdatTrafPairs$1(segment); 42399 mdatTrafPairs.forEach(function (pair) { 42400 var mdat = pair.mdat; 42401 var traf = pair.traf; 42402 var tfhd = findBox$1(traf, ['tfhd']); // Exactly 1 tfhd per traf 42403 42404 var headerInfo = parseTfhd(tfhd[0]); 42405 var trackId = headerInfo.trackId; 42406 var tfdt = findBox$1(traf, ['tfdt']); // Either 0 or 1 tfdt per traf 42407 42408 var baseMediaDecodeTime = tfdt.length > 0 ? parseTfdt$1(tfdt[0]).baseMediaDecodeTime : 0; 42409 var truns = findBox$1(traf, ['trun']); 42410 var samples; 42411 var result; // Only parse video data for the chosen video track 42412 42413 if (videoTrackId === trackId && truns.length > 0) { 42414 samples = parseSamples$1(truns, baseMediaDecodeTime, headerInfo); 42415 result = findSeiNals(mdat, samples, trackId); 42416 if (!captionNals[trackId]) { 42417 captionNals[trackId] = { 42418 seiNals: [], 42419 logs: [], 42420 }; 42421 } 42422 captionNals[trackId].seiNals = captionNals[trackId].seiNals.concat(result.seiNals); 42423 captionNals[trackId].logs = captionNals[trackId].logs.concat(result.logs); 42424 } 42425 }); 42426 return captionNals; 42427 }; 42428 /** 42429 * Parses out inband captions from an MP4 container and returns 42430 * caption objects that can be used by WebVTT and the TextTrack API. 42431 * @see https://developer.mozilla.org/en-US/docs/Web/API/VTTCue 42432 * @see https://developer.mozilla.org/en-US/docs/Web/API/TextTrack 42433 * Assumes that `probe.getVideoTrackIds` and `probe.timescale` have been called first 42434 * 42435 * @param {Uint8Array} segment - The fmp4 segment containing embedded captions 42436 * @param {Number} trackId - The id of the video track to parse 42437 * @param {Number} timescale - The timescale for the video track from the init segment 42438 * 42439 * @return {?Object[]} parsedCaptions - A list of captions or null if no video tracks 42440 * @return {Number} parsedCaptions[].startTime - The time to show the caption in seconds 42441 * @return {Number} parsedCaptions[].endTime - The time to stop showing the caption in seconds 42442 * @return {Object[]} parsedCaptions[].content - A list of individual caption segments 42443 * @return {String} parsedCaptions[].content.text - The visible content of the caption segment 42444 * @return {Number} parsedCaptions[].content.line - The line height from 1-15 for positioning of the caption segment 42445 * @return {Number} parsedCaptions[].content.position - The column indent percentage for cue positioning from 10-80 42446 **/ 42447 42448 var parseEmbeddedCaptions = function (segment, trackId, timescale) { 42449 var captionNals; // the ISO-BMFF spec says that trackId can't be zero, but there's some broken content out there 42450 42451 if (trackId === null) { 42452 return null; 42453 } 42454 captionNals = parseCaptionNals(segment, trackId); 42455 var trackNals = captionNals[trackId] || {}; 42456 return { 42457 seiNals: trackNals.seiNals, 42458 logs: trackNals.logs, 42459 timescale: timescale, 42460 }; 42461 }; 42462 /** 42463 * Converts SEI NALUs into captions that can be used by video.js 42464 **/ 42465 42466 var CaptionParser = function () { 42467 var isInitialized = false;
vendor: 34,761 bytes, lines 42468-43420
42468 var captionStream; // Stores segments seen before trackId and timescale are set 42469 42470 var segmentCache; // Stores video track ID of the track being parsed 42471 42472 var trackId; // Stores the timescale of the track being parsed 42473 42474 var timescale; // Stores captions parsed so far 42475 42476 var parsedCaptions; // Stores whether we are receiving partial data or not 42477 42478 var parsingPartial; 42479 /** 42480 * A method to indicate whether a CaptionParser has been initalized 42481 * @returns {Boolean} 42482 **/ 42483 42484 this.isInitialized = function () { 42485 return isInitialized; 42486 }; 42487 /** 42488 * Initializes the underlying CaptionStream, SEI NAL parsing 42489 * and management, and caption collection 42490 **/ 42491 42492 this.init = function (options) { 42493 captionStream = new CaptionStream(); 42494 isInitialized = true; 42495 parsingPartial = options ? options.isPartial : false; // Collect dispatched captions 42496 42497 captionStream.on('data', function (event) { 42498 // Convert to seconds in the source's timescale 42499 event.startTime = event.startPts / timescale; 42500 event.endTime = event.endPts / timescale; 42501 parsedCaptions.captions.push(event); 42502 parsedCaptions.captionStreams[event.stream] = true; 42503 }); 42504 captionStream.on('log', function (log) { 42505 parsedCaptions.logs.push(log); 42506 }); 42507 }; 42508 /** 42509 * Determines if a new video track will be selected 42510 * or if the timescale changed 42511 * @return {Boolean} 42512 **/ 42513 42514 this.isNewInit = function (videoTrackIds, timescales) { 42515 if ( 42516 (videoTrackIds && videoTrackIds.length === 0) || 42517 (timescales && typeof timescales === 'object' && Object.keys(timescales).length === 0) 42518 ) { 42519 return false; 42520 } 42521 return trackId !== videoTrackIds[0] || timescale !== timescales[trackId]; 42522 }; 42523 /** 42524 * Parses out SEI captions and interacts with underlying 42525 * CaptionStream to return dispatched captions 42526 * 42527 * @param {Uint8Array} segment - The fmp4 segment containing embedded captions 42528 * @param {Number[]} videoTrackIds - A list of video tracks found in the init segment 42529 * @param {Object.<Number, Number>} timescales - The timescales found in the init segment 42530 * @see parseEmbeddedCaptions 42531 * @see m2ts/caption-stream.js 42532 **/ 42533 42534 this.parse = function (segment, videoTrackIds, timescales) { 42535 var parsedData; 42536 if (!this.isInitialized()) { 42537 return null; // This is not likely to be a video segment 42538 } else if (!videoTrackIds || !timescales) { 42539 return null; 42540 } else if (this.isNewInit(videoTrackIds, timescales)) { 42541 // Use the first video track only as there is no 42542 // mechanism to switch to other video tracks 42543 trackId = videoTrackIds[0]; 42544 timescale = timescales[trackId]; // If an init segment has not been seen yet, hold onto segment 42545 // data until we have one. 42546 // the ISO-BMFF spec says that trackId can't be zero, but there's some broken content out there 42547 } else if (trackId === null || !timescale) { 42548 segmentCache.push(segment); 42549 return null; 42550 } // Now that a timescale and trackId is set, parse cached segments 42551 42552 while (segmentCache.length > 0) { 42553 var cachedSegment = segmentCache.shift(); 42554 this.parse(cachedSegment, videoTrackIds, timescales); 42555 } 42556 parsedData = parseEmbeddedCaptions(segment, trackId, timescale); 42557 if (parsedData && parsedData.logs) { 42558 parsedCaptions.logs = parsedCaptions.logs.concat(parsedData.logs); 42559 } 42560 if (parsedData === null || !parsedData.seiNals) { 42561 if (parsedCaptions.logs.length) { 42562 return { 42563 logs: parsedCaptions.logs, 42564 captions: [], 42565 captionStreams: [], 42566 }; 42567 } 42568 return null; 42569 } 42570 this.pushNals(parsedData.seiNals); // Force the parsed captions to be dispatched 42571 42572 this.flushStream(); 42573 return parsedCaptions; 42574 }; 42575 /** 42576 * Pushes SEI NALUs onto CaptionStream 42577 * @param {Object[]} nals - A list of SEI nals parsed using `parseCaptionNals` 42578 * Assumes that `parseCaptionNals` has been called first 42579 * @see m2ts/caption-stream.js 42580 **/ 42581 42582 this.pushNals = function (nals) { 42583 if (!this.isInitialized() || !nals || nals.length === 0) { 42584 return null; 42585 } 42586 nals.forEach(function (nal) { 42587 captionStream.push(nal); 42588 }); 42589 }; 42590 /** 42591 * Flushes underlying CaptionStream to dispatch processed, displayable captions 42592 * @see m2ts/caption-stream.js 42593 **/ 42594 42595 this.flushStream = function () { 42596 if (!this.isInitialized()) { 42597 return null; 42598 } 42599 if (!parsingPartial) { 42600 captionStream.flush(); 42601 } else { 42602 captionStream.partialFlush(); 42603 } 42604 }; 42605 /** 42606 * Reset caption buckets for new data 42607 **/ 42608 42609 this.clearParsedCaptions = function () { 42610 parsedCaptions.captions = []; 42611 parsedCaptions.captionStreams = {}; 42612 parsedCaptions.logs = []; 42613 }; 42614 /** 42615 * Resets underlying CaptionStream 42616 * @see m2ts/caption-stream.js 42617 **/ 42618 42619 this.resetCaptionStream = function () { 42620 if (!this.isInitialized()) { 42621 return null; 42622 } 42623 captionStream.reset(); 42624 }; 42625 /** 42626 * Convenience method to clear all captions flushed from the 42627 * CaptionStream and still being parsed 42628 * @see m2ts/caption-stream.js 42629 **/ 42630 42631 this.clearAllCaptions = function () { 42632 this.clearParsedCaptions(); 42633 this.resetCaptionStream(); 42634 }; 42635 /** 42636 * Reset caption parser 42637 **/ 42638 42639 this.reset = function () { 42640 segmentCache = []; 42641 trackId = null; 42642 timescale = null; 42643 if (!parsedCaptions) { 42644 parsedCaptions = { 42645 captions: [], 42646 // CC1, CC2, CC3, CC4 42647 captionStreams: {}, 42648 logs: [], 42649 }; 42650 } else { 42651 this.clearParsedCaptions(); 42652 } 42653 this.resetCaptionStream(); 42654 }; 42655 this.reset(); 42656 }; 42657 var captionParser = CaptionParser; 42658 const { parseTfdt } = mp4Inspector; 42659 const findBox = findBox_1; 42660 const { getTimescaleFromMediaHeader } = probe$2; 42661 const { parseSamples, getMdatTrafPairs } = samples; 42662 /** 42663 * Module for parsing WebVTT text and styles from FMP4 segments. 42664 * Based on the ISO/IEC 14496-30. 42665 */ 42666 42667 const WebVttParser = function () { 42668 // default timescale to 90k 42669 let timescale = 90e3; 42670 /** 42671 * Parses the timescale from the init segment. 42672 * @param {Array<Uint8Array>} segment The initialization segment to parse the timescale from. 42673 */ 42674 42675 this.init = function (segment) { 42676 // We just need the timescale from the init segment. 42677 const mdhd = findBox(segment, ['moov', 'trak', 'mdia', 'mdhd'])[0]; 42678 if (mdhd) { 42679 timescale = getTimescaleFromMediaHeader(mdhd); 42680 } 42681 }; 42682 /** 42683 * Parses a WebVTT FMP4 segment. 42684 * @param {Array<Uint8Array>} segment The content segment to parse the WebVTT cues from. 42685 * @returns The WebVTT cue text, styling, and timing info as an array of cue objects. 42686 */ 42687 42688 this.parseSegment = function (segment) { 42689 const vttCues = []; 42690 const mdatTrafPairs = getMdatTrafPairs(segment); 42691 let baseMediaDecodeTime = 0; 42692 mdatTrafPairs.forEach(function (pair) { 42693 const mdatBox = pair.mdat; 42694 const trafBox = pair.traf; // zero or one. 42695 42696 const tfdtBox = findBox(trafBox, ['tfdt'])[0]; // zero or one. 42697 42698 const tfhdBox = findBox(trafBox, ['tfhd'])[0]; // zero or more. 42699 42700 const trunBoxes = findBox(trafBox, ['trun']); 42701 if (tfdtBox) { 42702 const tfdt = parseTfdt(tfdtBox); 42703 baseMediaDecodeTime = tfdt.baseMediaDecodeTime; 42704 } 42705 if (trunBoxes.length && tfhdBox) { 42706 const samples = parseSamples(trunBoxes, baseMediaDecodeTime, tfhdBox); 42707 let mdatOffset = 0; 42708 samples.forEach(function (sample) { 42709 // decode utf8 payload 42710 const UTF_8 = 'utf-8'; 42711 const textDecoder = new TextDecoder(UTF_8); // extract sample data from the mdat box. 42712 // WebVTT Sample format: 42713 // Exactly one VTTEmptyCueBox box 42714 // OR one or more VTTCueBox boxes. 42715 42716 const sampleData = mdatBox.slice(mdatOffset, mdatOffset + sample.size); // single vtte box. 42717 42718 const vtteBox = findBox(sampleData, ['vtte'])[0]; // empty box 42719 42720 if (vtteBox) { 42721 mdatOffset += sample.size; 42722 return; 42723 } // TODO: Support 'vtta' boxes. 42724 // VTTAdditionalTextBoxes can be interleaved between VTTCueBoxes. 42725 42726 const vttcBoxes = findBox(sampleData, ['vttc']); 42727 vttcBoxes.forEach(function (vttcBox) { 42728 // mandatory payload box. 42729 const paylBox = findBox(vttcBox, ['payl'])[0]; // optional settings box 42730 42731 const sttgBox = findBox(vttcBox, ['sttg'])[0]; 42732 const start = sample.pts / timescale; 42733 const end = (sample.pts + sample.duration) / timescale; 42734 let cueText, settings; // contains cue text. 42735 42736 if (paylBox) { 42737 try { 42738 cueText = textDecoder.decode(paylBox); 42739 } catch (e) { 42740 console.error(e); 42741 } 42742 } // settings box contains styling. 42743 42744 if (sttgBox) { 42745 try { 42746 settings = textDecoder.decode(sttgBox); 42747 } catch (e) { 42748 console.error(e); 42749 } 42750 } 42751 if (sample.duration && cueText) { 42752 vttCues.push({ 42753 cueText, 42754 start, 42755 end, 42756 settings, 42757 }); 42758 } 42759 }); 42760 mdatOffset += sample.size; 42761 }); 42762 } 42763 }); 42764 return vttCues; 42765 }; 42766 }; 42767 var webvttParser = WebVttParser; 42768 /** 42769 * mux.js 42770 * 42771 * Copyright (c) Brightcove 42772 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 42773 * 42774 * Utilities to detect basic properties and metadata about TS Segments. 42775 */ 42776 42777 var StreamTypes$1 = streamTypes; 42778 var parsePid = function (packet) { 42779 var pid = packet[1] & 0x1f; 42780 pid <<= 8; 42781 pid |= packet[2]; 42782 return pid; 42783 }; 42784 var parsePayloadUnitStartIndicator = function (packet) { 42785 return !!(packet[1] & 0x40); 42786 }; 42787 var parseAdaptionField = function (packet) { 42788 var offset = 0; // if an adaption field is present, its length is specified by the 42789 // fifth byte of the TS packet header. The adaptation field is 42790 // used to add stuffing to PES packets that don't fill a complete 42791 // TS packet, and to specify some forms of timing and control data 42792 // that we do not currently use. 42793 42794 if ((packet[3] & 0x30) >>> 4 > 0x01) { 42795 offset += packet[4] + 1; 42796 } 42797 return offset; 42798 }; 42799 var parseType = function (packet, pmtPid) { 42800 var pid = parsePid(packet); 42801 if (pid === 0) { 42802 return 'pat'; 42803 } else if (pid === pmtPid) { 42804 return 'pmt'; 42805 } else if (pmtPid) { 42806 return 'pes'; 42807 } 42808 return null; 42809 }; 42810 var parsePat = function (packet) { 42811 var pusi = parsePayloadUnitStartIndicator(packet); 42812 var offset = 4 + parseAdaptionField(packet); 42813 if (pusi) { 42814 offset += packet[offset] + 1; 42815 } 42816 return ((packet[offset + 10] & 0x1f) << 8) | packet[offset + 11]; 42817 }; 42818 var parsePmt = function (packet) { 42819 var programMapTable = {}; 42820 var pusi = parsePayloadUnitStartIndicator(packet); 42821 var payloadOffset = 4 + parseAdaptionField(packet); 42822 if (pusi) { 42823 payloadOffset += packet[payloadOffset] + 1; 42824 } // PMTs can be sent ahead of the time when they should actually 42825 // take effect. We don't believe this should ever be the case 42826 // for HLS but we'll ignore "forward" PMT declarations if we see 42827 // them. Future PMT declarations have the current_next_indicator 42828 // set to zero. 42829 42830 if (!(packet[payloadOffset + 5] & 0x01)) { 42831 return; 42832 } 42833 var sectionLength, tableEnd, programInfoLength; // the mapping table ends at the end of the current section 42834 42835 sectionLength = ((packet[payloadOffset + 1] & 0x0f) << 8) | packet[payloadOffset + 2]; 42836 tableEnd = 3 + sectionLength - 4; // to determine where the table is, we have to figure out how 42837 // long the program info descriptors are 42838 42839 programInfoLength = ((packet[payloadOffset + 10] & 0x0f) << 8) | packet[payloadOffset + 11]; // advance the offset to the first entry in the mapping table 42840 42841 var offset = 12 + programInfoLength; 42842 while (offset < tableEnd) { 42843 var i = payloadOffset + offset; // add an entry that maps the elementary_pid to the stream_type 42844 42845 programMapTable[((packet[i + 1] & 0x1f) << 8) | packet[i + 2]] = packet[i]; // move to the next table entry 42846 // skip past the elementary stream descriptors, if present 42847 42848 offset += (((packet[i + 3] & 0x0f) << 8) | packet[i + 4]) + 5; 42849 } 42850 return programMapTable; 42851 }; 42852 var parsePesType = function (packet, programMapTable) { 42853 var pid = parsePid(packet); 42854 var type = programMapTable[pid]; 42855 switch (type) { 42856 case StreamTypes$1.H264_STREAM_TYPE: 42857 return 'video'; 42858 case StreamTypes$1.ADTS_STREAM_TYPE: 42859 return 'audio'; 42860 case StreamTypes$1.METADATA_STREAM_TYPE: 42861 return 'timed-metadata'; 42862 default: 42863 return null; 42864 } 42865 }; 42866 var parsePesTime = function (packet) { 42867 var pusi = parsePayloadUnitStartIndicator(packet); 42868 if (!pusi) { 42869 return null; 42870 } 42871 var offset = 4 + parseAdaptionField(packet); 42872 if (offset >= packet.byteLength) { 42873 // From the H 222.0 MPEG-TS spec 42874 // "For transport stream packets carrying PES packets, stuffing is needed when there 42875 // is insufficient PES packet data to completely fill the transport stream packet 42876 // payload bytes. Stuffing is accomplished by defining an adaptation field longer than 42877 // the sum of the lengths of the data elements in it, so that the payload bytes 42878 // remaining after the adaptation field exactly accommodates the available PES packet 42879 // data." 42880 // 42881 // If the offset is >= the length of the packet, then the packet contains no data 42882 // and instead is just adaption field stuffing bytes 42883 return null; 42884 } 42885 var pes = null; 42886 var ptsDtsFlags; // PES packets may be annotated with a PTS value, or a PTS value 42887 // and a DTS value. Determine what combination of values is 42888 // available to work with. 42889 42890 ptsDtsFlags = packet[offset + 7]; // PTS and DTS are normally stored as a 33-bit number. Javascript 42891 // performs all bitwise operations on 32-bit integers but javascript 42892 // supports a much greater range (52-bits) of integer using standard 42893 // mathematical operations. 42894 // We construct a 31-bit value using bitwise operators over the 31 42895 // most significant bits and then multiply by 4 (equal to a left-shift 42896 // of 2) before we add the final 2 least significant bits of the 42897 // timestamp (equal to an OR.) 42898 42899 if (ptsDtsFlags & 0xc0) { 42900 pes = {}; // the PTS and DTS are not written out directly. For information 42901 // on how they are encoded, see 42902 // http://dvd.sourceforge.net/dvdinfo/pes-hdr.html 42903 42904 pes.pts = 42905 ((packet[offset + 9] & 0x0e) << 27) | 42906 ((packet[offset + 10] & 0xff) << 20) | 42907 ((packet[offset + 11] & 0xfe) << 12) | 42908 ((packet[offset + 12] & 0xff) << 5) | 42909 ((packet[offset + 13] & 0xfe) >>> 3); 42910 pes.pts *= 4; // Left shift by 2 42911 42912 pes.pts += (packet[offset + 13] & 0x06) >>> 1; // OR by the two LSBs 42913 42914 pes.dts = pes.pts; 42915 if (ptsDtsFlags & 0x40) { 42916 pes.dts = 42917 ((packet[offset + 14] & 0x0e) << 27) | 42918 ((packet[offset + 15] & 0xff) << 20) | 42919 ((packet[offset + 16] & 0xfe) << 12) | 42920 ((packet[offset + 17] & 0xff) << 5) | 42921 ((packet[offset + 18] & 0xfe) >>> 3); 42922 pes.dts *= 4; // Left shift by 2 42923 42924 pes.dts += (packet[offset + 18] & 0x06) >>> 1; // OR by the two LSBs 42925 } 42926 } 42927 return pes; 42928 }; 42929 var parseNalUnitType = function (type) { 42930 switch (type) { 42931 case 0x05: 42932 return 'slice_layer_without_partitioning_rbsp_idr'; 42933 case 0x06: 42934 return 'sei_rbsp'; 42935 case 0x07: 42936 return 'seq_parameter_set_rbsp'; 42937 case 0x08: 42938 return 'pic_parameter_set_rbsp'; 42939 case 0x09: 42940 return 'access_unit_delimiter_rbsp'; 42941 default: 42942 return null; 42943 } 42944 }; 42945 var videoPacketContainsKeyFrame = function (packet) { 42946 var offset = 4 + parseAdaptionField(packet); 42947 var frameBuffer = packet.subarray(offset); 42948 var frameI = 0; 42949 var frameSyncPoint = 0; 42950 var foundKeyFrame = false; 42951 var nalType; // advance the sync point to a NAL start, if necessary 42952 42953 for (; frameSyncPoint < frameBuffer.byteLength - 3; frameSyncPoint++) { 42954 if (frameBuffer[frameSyncPoint + 2] === 1) { 42955 // the sync point is properly aligned 42956 frameI = frameSyncPoint + 5; 42957 break; 42958 } 42959 } 42960 while (frameI < frameBuffer.byteLength) { 42961 // look at the current byte to determine if we've hit the end of 42962 // a NAL unit boundary 42963 switch (frameBuffer[frameI]) { 42964 case 0: 42965 // skip past non-sync sequences 42966 if (frameBuffer[frameI - 1] !== 0) { 42967 frameI += 2; 42968 break; 42969 } else if (frameBuffer[frameI - 2] !== 0) { 42970 frameI++; 42971 break; 42972 } 42973 if (frameSyncPoint + 3 !== frameI - 2) { 42974 nalType = parseNalUnitType(frameBuffer[frameSyncPoint + 3] & 0x1f); 42975 if (nalType === 'slice_layer_without_partitioning_rbsp_idr') { 42976 foundKeyFrame = true; 42977 } 42978 } // drop trailing zeroes 42979 42980 do { 42981 frameI++; 42982 } while (frameBuffer[frameI] !== 1 && frameI < frameBuffer.length); 42983 frameSyncPoint = frameI - 2; 42984 frameI += 3; 42985 break; 42986 case 1: 42987 // skip past non-sync sequences 42988 if (frameBuffer[frameI - 1] !== 0 || frameBuffer[frameI - 2] !== 0) { 42989 frameI += 3; 42990 break; 42991 } 42992 nalType = parseNalUnitType(frameBuffer[frameSyncPoint + 3] & 0x1f); 42993 if (nalType === 'slice_layer_without_partitioning_rbsp_idr') { 42994 foundKeyFrame = true; 42995 } 42996 frameSyncPoint = frameI - 2; 42997 frameI += 3; 42998 break; 42999 default: 43000 // the current byte isn't a one or zero, so it cannot be part 43001 // of a sync sequence 43002 frameI += 3; 43003 break; 43004 } 43005 } 43006 frameBuffer = frameBuffer.subarray(frameSyncPoint); 43007 frameI -= frameSyncPoint; 43008 frameSyncPoint = 0; // parse the final nal 43009 43010 if (frameBuffer && frameBuffer.byteLength > 3) { 43011 nalType = parseNalUnitType(frameBuffer[frameSyncPoint + 3] & 0x1f); 43012 if (nalType === 'slice_layer_without_partitioning_rbsp_idr') { 43013 foundKeyFrame = true; 43014 } 43015 } 43016 return foundKeyFrame; 43017 }; 43018 var probe$1 = { 43019 parseType: parseType, 43020 parsePat: parsePat, 43021 parsePmt: parsePmt, 43022 parsePayloadUnitStartIndicator: parsePayloadUnitStartIndicator, 43023 parsePesType: parsePesType, 43024 parsePesTime: parsePesTime, 43025 videoPacketContainsKeyFrame: videoPacketContainsKeyFrame, 43026 }; 43027 /** 43028 * mux.js 43029 * 43030 * Copyright (c) Brightcove 43031 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 43032 * 43033 * Parse mpeg2 transport stream packets to extract basic timing information 43034 */ 43035 43036 var StreamTypes = streamTypes; 43037 var handleRollover = timestampRolloverStream.handleRollover; 43038 var probe = {}; 43039 probe.ts = probe$1; 43040 probe.aac = utils; 43041 var ONE_SECOND_IN_TS = clock$2.ONE_SECOND_IN_TS; 43042 var MP2T_PACKET_LENGTH = 188, 43043 // bytes 43044 SYNC_BYTE = 0x47; 43045 /** 43046 * walks through segment data looking for pat and pmt packets to parse out 43047 * program map table information 43048 */ 43049 43050 var parsePsi_ = function (bytes, pmt) { 43051 var startIndex = 0, 43052 endIndex = MP2T_PACKET_LENGTH, 43053 packet, 43054 type; 43055 while (endIndex < bytes.byteLength) { 43056 // Look for a pair of start and end sync bytes in the data.. 43057 if (bytes[startIndex] === SYNC_BYTE && bytes[endIndex] === SYNC_BYTE) { 43058 // We found a packet 43059 packet = bytes.subarray(startIndex, endIndex); 43060 type = probe.ts.parseType(packet, pmt.pid); 43061 switch (type) { 43062 case 'pat': 43063 pmt.pid = probe.ts.parsePat(packet); 43064 break; 43065 case 'pmt': 43066 var table = probe.ts.parsePmt(packet); 43067 pmt.table = pmt.table || {}; 43068 Object.keys(table).forEach(function (key) { 43069 pmt.table[key] = table[key]; 43070 }); 43071 break; 43072 } 43073 startIndex += MP2T_PACKET_LENGTH; 43074 endIndex += MP2T_PACKET_LENGTH; 43075 continue; 43076 } // If we get here, we have somehow become de-synchronized and we need to step 43077 // forward one byte at a time until we find a pair of sync bytes that denote 43078 // a packet 43079 43080 startIndex++; 43081 endIndex++; 43082 } 43083 }; 43084 /** 43085 * walks through the segment data from the start and end to get timing information 43086 * for the first and last audio pes packets 43087 */ 43088 43089 var parseAudioPes_ = function (bytes, pmt, result) { 43090 var startIndex = 0, 43091 endIndex = MP2T_PACKET_LENGTH, 43092 packet, 43093 type, 43094 pesType, 43095 pusi, 43096 parsed; 43097 var endLoop = false; // Start walking from start of segment to get first audio packet 43098 43099 while (endIndex <= bytes.byteLength) { 43100 // Look for a pair of start and end sync bytes in the data.. 43101 if ( 43102 bytes[startIndex] === SYNC_BYTE && 43103 (bytes[endIndex] === SYNC_BYTE || endIndex === bytes.byteLength) 43104 ) { 43105 // We found a packet 43106 packet = bytes.subarray(startIndex, endIndex); 43107 type = probe.ts.parseType(packet, pmt.pid); 43108 switch (type) { 43109 case 'pes': 43110 pesType = probe.ts.parsePesType(packet, pmt.table); 43111 pusi = probe.ts.parsePayloadUnitStartIndicator(packet); 43112 if (pesType === 'audio' && pusi) { 43113 parsed = probe.ts.parsePesTime(packet); 43114 if (parsed) { 43115 parsed.type = 'audio'; 43116 result.audio.push(parsed); 43117 endLoop = true; 43118 } 43119 } 43120 break; 43121 } 43122 if (endLoop) { 43123 break; 43124 } 43125 startIndex += MP2T_PACKET_LENGTH; 43126 endIndex += MP2T_PACKET_LENGTH; 43127 continue; 43128 } // If we get here, we have somehow become de-synchronized and we need to step 43129 // forward one byte at a time until we find a pair of sync bytes that denote 43130 // a packet 43131 43132 startIndex++; 43133 endIndex++; 43134 } // Start walking from end of segment to get last audio packet 43135 43136 endIndex = bytes.byteLength; 43137 startIndex = endIndex - MP2T_PACKET_LENGTH; 43138 endLoop = false; 43139 while (startIndex >= 0) { 43140 // Look for a pair of start and end sync bytes in the data.. 43141 if ( 43142 bytes[startIndex] === SYNC_BYTE && 43143 (bytes[endIndex] === SYNC_BYTE || endIndex === bytes.byteLength) 43144 ) { 43145 // We found a packet 43146 packet = bytes.subarray(startIndex, endIndex); 43147 type = probe.ts.parseType(packet, pmt.pid); 43148 switch (type) { 43149 case 'pes': 43150 pesType = probe.ts.parsePesType(packet, pmt.table); 43151 pusi = probe.ts.parsePayloadUnitStartIndicator(packet); 43152 if (pesType === 'audio' && pusi) { 43153 parsed = probe.ts.parsePesTime(packet); 43154 if (parsed) { 43155 parsed.type = 'audio'; 43156 result.audio.push(parsed); 43157 endLoop = true; 43158 } 43159 } 43160 break; 43161 } 43162 if (endLoop) { 43163 break; 43164 } 43165 startIndex -= MP2T_PACKET_LENGTH; 43166 endIndex -= MP2T_PACKET_LENGTH; 43167 continue; 43168 } // If we get here, we have somehow become de-synchronized and we need to step 43169 // forward one byte at a time until we find a pair of sync bytes that denote 43170 // a packet 43171 43172 startIndex--; 43173 endIndex--; 43174 } 43175 }; 43176 /** 43177 * walks through the segment data from the start and end to get timing information 43178 * for the first and last video pes packets as well as timing information for the first 43179 * key frame. 43180 */ 43181 43182 var parseVideoPes_ = function (bytes, pmt, result) { 43183 var startIndex = 0, 43184 endIndex = MP2T_PACKET_LENGTH, 43185 packet, 43186 type, 43187 pesType, 43188 pusi, 43189 parsed, 43190 frame, 43191 i, 43192 pes; 43193 var endLoop = false; 43194 var currentFrame = { 43195 data: [], 43196 size: 0, 43197 }; // Start walking from start of segment to get first video packet 43198 43199 while (endIndex < bytes.byteLength) { 43200 // Look for a pair of start and end sync bytes in the data.. 43201 if (bytes[startIndex] === SYNC_BYTE && bytes[endIndex] === SYNC_BYTE) { 43202 // We found a packet 43203 packet = bytes.subarray(startIndex, endIndex); 43204 type = probe.ts.parseType(packet, pmt.pid); 43205 switch (type) { 43206 case 'pes': 43207 pesType = probe.ts.parsePesType(packet, pmt.table); 43208 pusi = probe.ts.parsePayloadUnitStartIndicator(packet); 43209 if (pesType === 'video') { 43210 if (pusi && !endLoop) { 43211 parsed = probe.ts.parsePesTime(packet); 43212 if (parsed) { 43213 parsed.type = 'video'; 43214 result.video.push(parsed); 43215 endLoop = true; 43216 } 43217 } 43218 if (!result.firstKeyFrame) { 43219 if (pusi) { 43220 if (currentFrame.size !== 0) { 43221 frame = new Uint8Array(currentFrame.size); 43222 i = 0; 43223 while (currentFrame.data.length) { 43224 pes = currentFrame.data.shift(); 43225 frame.set(pes, i); 43226 i += pes.byteLength; 43227 } 43228 if (probe.ts.videoPacketContainsKeyFrame(frame)) { 43229 var firstKeyFrame = probe.ts.parsePesTime(frame); // PTS/DTS may not be available. Simply *not* setting 43230 // the keyframe seems to work fine with HLS playback 43231 // and definitely preferable to a crash with TypeError... 43232 43233 if (firstKeyFrame) { 43234 result.firstKeyFrame = firstKeyFrame; 43235 result.firstKeyFrame.type = 'video'; 43236 } else { 43237 // eslint-disable-next-line 43238 console.warn( 43239 'Failed to extract PTS/DTS from PES at first keyframe. ' + 43240 'This could be an unusual TS segment, or else mux.js did not ' + 43241 'parse your TS segment correctly. If you know your TS ' + 43242 'segments do contain PTS/DTS on keyframes please file a bug ' + 43243 'report! You can try ffprobe to double check for yourself.' 43244 ); 43245 } 43246 } 43247 currentFrame.size = 0; 43248 } 43249 } 43250 currentFrame.data.push(packet); 43251 currentFrame.size += packet.byteLength; 43252 } 43253 } 43254 break; 43255 } 43256 if (endLoop && result.firstKeyFrame) { 43257 break; 43258 } 43259 startIndex += MP2T_PACKET_LENGTH; 43260 endIndex += MP2T_PACKET_LENGTH; 43261 continue; 43262 } // If we get here, we have somehow become de-synchronized and we need to step 43263 // forward one byte at a time until we find a pair of sync bytes that denote 43264 // a packet 43265 43266 startIndex++; 43267 endIndex++; 43268 } // Start walking from end of segment to get last video packet 43269 43270 endIndex = bytes.byteLength; 43271 startIndex = endIndex - MP2T_PACKET_LENGTH; 43272 endLoop = false; 43273 while (startIndex >= 0) { 43274 // Look for a pair of start and end sync bytes in the data.. 43275 if (bytes[startIndex] === SYNC_BYTE && bytes[endIndex] === SYNC_BYTE) { 43276 // We found a packet 43277 packet = bytes.subarray(startIndex, endIndex); 43278 type = probe.ts.parseType(packet, pmt.pid); 43279 switch (type) { 43280 case 'pes': 43281 pesType = probe.ts.parsePesType(packet, pmt.table); 43282 pusi = probe.ts.parsePayloadUnitStartIndicator(packet); 43283 if (pesType === 'video' && pusi) { 43284 parsed = probe.ts.parsePesTime(packet); 43285 if (parsed) { 43286 parsed.type = 'video'; 43287 result.video.push(parsed); 43288 endLoop = true; 43289 } 43290 } 43291 break; 43292 } 43293 if (endLoop) { 43294 break; 43295 } 43296 startIndex -= MP2T_PACKET_LENGTH; 43297 endIndex -= MP2T_PACKET_LENGTH; 43298 continue; 43299 } // If we get here, we have somehow become de-synchronized and we need to step 43300 // forward one byte at a time until we find a pair of sync bytes that denote 43301 // a packet 43302 43303 startIndex--; 43304 endIndex--; 43305 } 43306 }; 43307 /** 43308 * Adjusts the timestamp information for the segment to account for 43309 * rollover and convert to seconds based on pes packet timescale (90khz clock) 43310 */ 43311 43312 var adjustTimestamp_ = function (segmentInfo, baseTimestamp) { 43313 if (segmentInfo.audio && segmentInfo.audio.length) { 43314 var audioBaseTimestamp = baseTimestamp; 43315 if (typeof audioBaseTimestamp === 'undefined' || isNaN(audioBaseTimestamp)) { 43316 audioBaseTimestamp = segmentInfo.audio[0].dts; 43317 } 43318 segmentInfo.audio.forEach(function (info) { 43319 info.dts = handleRollover(info.dts, audioBaseTimestamp); 43320 info.pts = handleRollover(info.pts, audioBaseTimestamp); // time in seconds 43321 43322 info.dtsTime = info.dts / ONE_SECOND_IN_TS; 43323 info.ptsTime = info.pts / ONE_SECOND_IN_TS; 43324 }); 43325 } 43326 if (segmentInfo.video && segmentInfo.video.length) { 43327 var videoBaseTimestamp = baseTimestamp; 43328 if (typeof videoBaseTimestamp === 'undefined' || isNaN(videoBaseTimestamp)) { 43329 videoBaseTimestamp = segmentInfo.video[0].dts; 43330 } 43331 segmentInfo.video.forEach(function (info) { 43332 info.dts = handleRollover(info.dts, videoBaseTimestamp); 43333 info.pts = handleRollover(info.pts, videoBaseTimestamp); // time in seconds 43334 43335 info.dtsTime = info.dts / ONE_SECOND_IN_TS; 43336 info.ptsTime = info.pts / ONE_SECOND_IN_TS; 43337 }); 43338 if (segmentInfo.firstKeyFrame) { 43339 var frame = segmentInfo.firstKeyFrame; 43340 frame.dts = handleRollover(frame.dts, videoBaseTimestamp); 43341 frame.pts = handleRollover(frame.pts, videoBaseTimestamp); // time in seconds 43342 43343 frame.dtsTime = frame.dts / ONE_SECOND_IN_TS; 43344 frame.ptsTime = frame.pts / ONE_SECOND_IN_TS; 43345 } 43346 } 43347 }; 43348 /** 43349 * inspects the aac data stream for start and end time information 43350 */ 43351 43352 var inspectAac_ = function (bytes) { 43353 var endLoop = false, 43354 audioCount = 0, 43355 sampleRate = null, 43356 timestamp = null, 43357 frameSize = 0, 43358 byteIndex = 0, 43359 packet; 43360 while (bytes.length - byteIndex >= 3) { 43361 var type = probe.aac.parseType(bytes, byteIndex); 43362 switch (type) { 43363 case 'timed-metadata': 43364 // Exit early because we don't have enough to parse 43365 // the ID3 tag header 43366 if (bytes.length - byteIndex < 10) { 43367 endLoop = true; 43368 break; 43369 } 43370 frameSize = probe.aac.parseId3TagSize(bytes, byteIndex); // Exit early if we don't have enough in the buffer 43371 // to emit a full packet 43372 43373 if (frameSize > bytes.length) { 43374 endLoop = true; 43375 break; 43376 } 43377 if (timestamp === null) { 43378 packet = bytes.subarray(byteIndex, byteIndex + frameSize); 43379 timestamp = probe.aac.parseAacTimestamp(packet); 43380 } 43381 byteIndex += frameSize; 43382 break; 43383 case 'audio': 43384 // Exit early because we don't have enough to parse 43385 // the ADTS frame header 43386 if (bytes.length - byteIndex < 7) { 43387 endLoop = true; 43388 break; 43389 } 43390 frameSize = probe.aac.parseAdtsSize(bytes, byteIndex); // Exit early if we don't have enough in the buffer 43391 // to emit a full packet 43392 43393 if (frameSize > bytes.length) { 43394 endLoop = true; 43395 break; 43396 } 43397 if (sampleRate === null) { 43398 packet = bytes.subarray(byteIndex, byteIndex + frameSize); 43399 sampleRate = probe.aac.parseSampleRate(packet); 43400 } 43401 audioCount++; 43402 byteIndex += frameSize; 43403 break; 43404 default: 43405 byteIndex++; 43406 break; 43407 } 43408 if (endLoop) { 43409 return null; 43410 } 43411 } 43412 if (sampleRate === null || timestamp === null) { 43413 return null; 43414 } 43415 var audioTimescale = ONE_SECOND_IN_TS / sampleRate; 43416 var result = { 43417 audio: [ 43418 { 43419 type: 'audio', 43420 dts: timestamp,
43421 pts: timestamp, 43422 }, 43423 { 43424 type: 'audio', 43425 dts: timestamp + audioCount * 1024 * audioTimescale, 43426 pts: timestamp + audioCount * 1024 * audioTimescale, 43427 }, 43428 ], 43429 }; 43430 return result; 43431 }; 43432 /** 43433 * inspects the transport stream segment data for start and end time information 43434 * of the audio and video tracks (when present) as well as the first key frame's 43435 * start time. 43436 */ 43437 43438 var inspectTs_ = function (bytes) { 43439 var pmt = { 43440 pid: null, 43441 table: null, 43442 }; 43443 var result = {}; 43444 parsePsi_(bytes, pmt); 43445 for (var pid in pmt.table) { 43446 if (pmt.table.hasOwnProperty(pid)) { 43447 var type = pmt.table[pid]; 43448 switch (type) { 43449 case StreamTypes.H264_STREAM_TYPE: 43450 result.video = []; 43451 parseVideoPes_(bytes, pmt, result); 43452 if (result.video.length === 0) { 43453 delete result.video; 43454 } 43455 break; 43456 case StreamTypes.ADTS_STREAM_TYPE: 43457 result.audio = []; 43458 parseAudioPes_(bytes, pmt, result); 43459 if (result.audio.length === 0) { 43460 delete result.audio; 43461 } 43462 break; 43463 } 43464 } 43465 } 43466 return result; 43467 }; 43468 /** 43469 * Inspects segment byte data and returns an object with start and end timing information 43470 * 43471 * @param {Uint8Array} bytes The segment byte data 43472 * @param {Number} baseTimestamp Relative reference timestamp used when adjusting frame 43473 * timestamps for rollover. This value must be in 90khz clock. 43474 * @return {Object} Object containing start and end frame timing info of segment. 43475 */ 43476 43477 var inspect = function (bytes, baseTimestamp) { 43478 var isAacData = probe.aac.isLikelyAacData(bytes); 43479 var result; 43480 if (isAacData) { 43481 result = inspectAac_(bytes); 43482 } else { 43483 result = inspectTs_(bytes); 43484 } 43485 if (!result || (!result.audio && !result.video)) { 43486 return null; 43487 } 43488 adjustTimestamp_(result, baseTimestamp); 43489 return result; 43490 }; 43491 var tsInspector = { 43492 inspect: inspect, 43493 parseAudioPes_: parseAudioPes_, 43494 }; 43495 /* global self */ 43496 43497 /** 43498 * Re-emits transmuxer events by converting them into messages to the 43499 * world outside the worker. 43500 * 43501 * @param {Object} transmuxer the transmuxer to wire events on 43502 * @private 43503 */ 43504 43505 const wireTransmuxerEvents = function (self, transmuxer) { 43506 transmuxer.on('data', function (segment) { 43507 // transfer ownership of the underlying ArrayBuffer 43508 // instead of doing a copy to save memory 43509 // ArrayBuffers are transferable but generic TypedArrays are not 43510 // @link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers#Passing_data_by_transferring_ownership_(transferable_objects) 43511 const initArray = segment.initSegment; 43512 segment.initSegment = { 43513 data: initArray.buffer, 43514 byteOffset: initArray.byteOffset, 43515 byteLength: initArray.byteLength, 43516 }; 43517 const typedArray = segment.data; 43518 segment.data = typedArray.buffer; 43519 self.postMessage( 43520 { 43521 action: 'data', 43522 segment, 43523 byteOffset: typedArray.byteOffset, 43524 byteLength: typedArray.byteLength, 43525 }, 43526 [segment.data] 43527 ); 43528 }); 43529 transmuxer.on('done', function (data) { 43530 self.postMessage({ 43531 action: 'done', 43532 }); 43533 }); 43534 transmuxer.on('gopInfo', function (gopInfo) { 43535 self.postMessage({ 43536 action: 'gopInfo', 43537 gopInfo, 43538 }); 43539 }); 43540 transmuxer.on('videoSegmentTimingInfo', function (timingInfo) { 43541 const videoSegmentTimingInfo = { 43542 start: { 43543 decode: clock$2.videoTsToSeconds(timingInfo.start.dts), 43544 presentation: clock$2.videoTsToSeconds(timingInfo.start.pts), 43545 }, 43546 end: { 43547 decode: clock$2.videoTsToSeconds(timingInfo.end.dts), 43548 presentation: clock$2.videoTsToSeconds(timingInfo.end.pts), 43549 }, 43550 baseMediaDecodeTime: clock$2.videoTsToSeconds(timingInfo.baseMediaDecodeTime), 43551 }; 43552 if (timingInfo.prependedContentDuration) { 43553 videoSegmentTimingInfo.prependedContentDuration = clock$2.videoTsToSeconds( 43554 timingInfo.prependedContentDuration 43555 ); 43556 } 43557 self.postMessage({ 43558 action: 'videoSegmentTimingInfo', 43559 videoSegmentTimingInfo, 43560 }); 43561 }); 43562 transmuxer.on('audioSegmentTimingInfo', function (timingInfo) { 43563 // Note that all times for [audio/video]SegmentTimingInfo events are in video clock 43564 const audioSegmentTimingInfo = { 43565 start: { 43566 decode: clock$2.videoTsToSeconds(timingInfo.start.dts), 43567 presentation: clock$2.videoTsToSeconds(timingInfo.start.pts), 43568 }, 43569 end: { 43570 decode: clock$2.videoTsToSeconds(timingInfo.end.dts), 43571 presentation: clock$2.videoTsToSeconds(timingInfo.end.pts), 43572 }, 43573 baseMediaDecodeTime: clock$2.videoTsToSeconds(timingInfo.baseMediaDecodeTime), 43574 }; 43575 if (timingInfo.prependedContentDuration) { 43576 audioSegmentTimingInfo.prependedContentDuration = clock$2.videoTsToSeconds( 43577 timingInfo.prependedContentDuration 43578 ); 43579 } 43580 self.postMessage({ 43581 action: 'audioSegmentTimingInfo', 43582 audioSegmentTimingInfo, 43583 }); 43584 }); 43585 transmuxer.on('id3Frame', function (id3Frame) { 43586 self.postMessage({ 43587 action: 'id3Frame', 43588 id3Frame, 43589 }); 43590 }); 43591 transmuxer.on('caption', function (caption) { 43592 self.postMessage({ 43593 action: 'caption', 43594 caption, 43595 }); 43596 }); 43597 transmuxer.on('trackinfo', function (trackInfo) { 43598 self.postMessage({ 43599 action: 'trackinfo', 43600 trackInfo, 43601 }); 43602 }); 43603 transmuxer.on('audioTimingInfo', function (audioTimingInfo) { 43604 // convert to video TS since we prioritize video time over audio 43605 self.postMessage({ 43606 action: 'audioTimingInfo', 43607 audioTimingInfo: { 43608 start: clock$2.videoTsToSeconds(audioTimingInfo.start), 43609 end: clock$2.videoTsToSeconds(audioTimingInfo.end), 43610 }, 43611 }); 43612 }); 43613 transmuxer.on('videoTimingInfo', function (videoTimingInfo) { 43614 self.postMessage({ 43615 action: 'videoTimingInfo', 43616 videoTimingInfo: { 43617 start: clock$2.videoTsToSeconds(videoTimingInfo.start), 43618 end: clock$2.videoTsToSeconds(videoTimingInfo.end), 43619 }, 43620 }); 43621 }); 43622 transmuxer.on('log', function (log) { 43623 self.postMessage({ 43624 action: 'log', 43625 log, 43626 }); 43627 }); 43628 }; 43629 /** 43630 * All incoming messages route through this hash. If no function exists 43631 * to handle an incoming message, then we ignore the message. 43632 * 43633 * @class MessageHandlers 43634 * @param {Object} options the options to initialize with 43635 */ 43636 43637 class MessageHandlers { 43638 constructor(self, options) { 43639 this.options = options || {}; 43640 this.self = self; 43641 this.init(); 43642 } 43643 /** 43644 * initialize our web worker and wire all the events. 43645 */ 43646 43647 init() { 43648 if (this.transmuxer) { 43649 this.transmuxer.dispose(); 43650 } 43651 this.transmuxer = new transmuxer.Transmuxer(this.options); 43652 wireTransmuxerEvents(this.self, this.transmuxer); 43653 } 43654 pushMp4Captions(data) { 43655 if (!this.captionParser) { 43656 this.captionParser = new captionParser(); 43657 this.captionParser.init(); 43658 } 43659 const segment = new Uint8Array(data.data, data.byteOffset, data.byteLength); 43660 const parsed = this.captionParser.parse(segment, data.trackIds, data.timescales); 43661 this.self.postMessage( 43662 { 43663 action: 'mp4Captions', 43664 captions: (parsed && parsed.captions) || [], 43665 logs: (parsed && parsed.logs) || [], 43666 data: segment.buffer, 43667 }, 43668 [segment.buffer] 43669 ); 43670 } 43671 /** 43672 * Initializes the WebVttParser and passes the init segment. 43673 * 43674 * @param {Uint8Array} data mp4 boxed WebVTT init segment data 43675 */ 43676 43677 initMp4WebVttParser(data) { 43678 if (!this.webVttParser) { 43679 this.webVttParser = new webvttParser(); 43680 } 43681 const segment = new Uint8Array(data.data, data.byteOffset, data.byteLength); // Set the timescale for the parser. 43682 // This can be called repeatedly in order to set and re-set the timescale. 43683 43684 this.webVttParser.init(segment); 43685 } 43686 /** 43687 * Parse an mp4 encapsulated WebVTT segment and return an array of cues. 43688 * 43689 * @param {Uint8Array} data a text/webvtt segment 43690 * @return {Object[]} an array of parsed cue objects 43691 */ 43692 43693 getMp4WebVttText(data) { 43694 if (!this.webVttParser) { 43695 // timescale might not be set yet if the parser is created before an init segment is passed. 43696 // default timescale is 90k. 43697 this.webVttParser = new webvttParser(); 43698 } 43699 const segment = new Uint8Array(data.data, data.byteOffset, data.byteLength); 43700 const parsed = this.webVttParser.parseSegment(segment); 43701 this.self.postMessage( 43702 { 43703 action: 'getMp4WebVttText', 43704 mp4VttCues: parsed || [], 43705 data: segment.buffer, 43706 }, 43707 [segment.buffer] 43708 ); 43709 } 43710 probeMp4StartTime({ timescales, data }) { 43711 const startTime = probe$2.startTime(timescales, data); 43712 this.self.postMessage( 43713 { 43714 action: 'probeMp4StartTime', 43715 startTime, 43716 data, 43717 }, 43718 [data.buffer] 43719 ); 43720 } 43721 probeMp4Tracks({ data }) { 43722 const tracks = probe$2.tracks(data); 43723 this.self.postMessage( 43724 { 43725 action: 'probeMp4Tracks', 43726 tracks, 43727 data, 43728 }, 43729 [data.buffer] 43730 ); 43731 } 43732 /** 43733 * Probes an mp4 segment for EMSG boxes containing ID3 data. 43734 * https://aomediacodec.github.io/id3-emsg/ 43735 * 43736 * @param {Uint8Array} data segment data 43737 * @param {number} offset segment start time 43738 * @return {Object[]} an array of ID3 frames 43739 */ 43740 43741 probeEmsgID3({ data, offset }) { 43742 const id3Frames = probe$2.getEmsgID3(data, offset); 43743 this.self.postMessage( 43744 { 43745 action: 'probeEmsgID3', 43746 id3Frames, 43747 emsgData: data, 43748 }, 43749 [data.buffer] 43750 ); 43751 } 43752 /** 43753 * Probe an mpeg2-ts segment to determine the start time of the segment in it's
43754 * internal "media time," as well as whether it contains video and/or audio. 43755 * 43756 * @private 43757 * @param {Uint8Array} bytes - segment bytes 43758 * @param {number} baseStartTime 43759 * Relative reference timestamp used when adjusting frame timestamps for rollover. 43760 * This value should be in seconds, as it's converted to a 90khz clock within the 43761 * function body. 43762 * @return {Object} The start time of the current segment in "media time" as well as 43763 * whether it contains video and/or audio 43764 */ 43765 43766 probeTs({ data, baseStartTime }) { 43767 const tsStartTime = 43768 typeof baseStartTime === 'number' && !isNaN(baseStartTime) 43769 ? baseStartTime * clock$2.ONE_SECOND_IN_TS 43770 : void 0; 43771 const timeInfo = tsInspector.inspect(data, tsStartTime); 43772 let result = null; 43773 if (timeInfo) { 43774 result = { 43775 // each type's time info comes back as an array of 2 times, start and end 43776 hasVideo: (timeInfo.video && timeInfo.video.length === 2) || false, 43777 hasAudio: (timeInfo.audio && timeInfo.audio.length === 2) || false, 43778 }; 43779 if (result.hasVideo) { 43780 result.videoStart = timeInfo.video[0].ptsTime; 43781 } 43782 if (result.hasAudio) { 43783 result.audioStart = timeInfo.audio[0].ptsTime; 43784 } 43785 } 43786 this.self.postMessage( 43787 { 43788 action: 'probeTs', 43789 result, 43790 data, 43791 }, 43792 [data.buffer] 43793 ); 43794 } 43795 clearAllMp4Captions() { 43796 if (this.captionParser) { 43797 this.captionParser.clearAllCaptions(); 43798 } 43799 } 43800 clearParsedMp4Captions() { 43801 if (this.captionParser) { 43802 this.captionParser.clearParsedCaptions(); 43803 } 43804 } 43805 /** 43806 * Adds data (a ts segment) to the start of the transmuxer pipeline for 43807 * processing. 43808 * 43809 * @param {ArrayBuffer} data data to push into the muxer 43810 */ 43811 43812 push(data) { 43813 // Cast array buffer to correct type for transmuxer 43814 const segment = new Uint8Array(data.data, data.byteOffset, data.byteLength); 43815 this.transmuxer.push(segment); 43816 } 43817 /** 43818 * Recreate the transmuxer so that the next segment added via `push` 43819 * start with a fresh transmuxer. 43820 */ 43821 43822 reset() { 43823 this.transmuxer.reset(); 43824 } 43825 /** 43826 * Set the value that will be used as the `baseMediaDecodeTime` time for the 43827 * next segment pushed in. Subsequent segments will have their `baseMediaDecodeTime` 43828 * set relative to the first based on the PTS values. 43829 * 43830 * @param {Object} data used to set the timestamp offset in the muxer 43831 */ 43832 43833 setTimestampOffset(data) { 43834 const timestampOffset = data.timestampOffset || 0; 43835 this.transmuxer.setBaseMediaDecodeTime( 43836 Math.round(clock$2.secondsToVideoTs(timestampOffset)) 43837 ); 43838 } 43839 setAudioAppendStart(data) { 43840 this.transmuxer.setAudioAppendStart(Math.ceil(clock$2.secondsToVideoTs(data.appendStart))); 43841 } 43842 setRemux(data) { 43843 this.transmuxer.setRemux(data.remux); 43844 } 43845 /** 43846 * Forces the pipeline to finish processing the last segment and emit it's 43847 * results. 43848 * 43849 * @param {Object} data event data, not really used 43850 */ 43851 43852 flush(data) { 43853 this.transmuxer.flush(); // transmuxed done action is fired after both audio/video pipelines are flushed 43854 43855 self.postMessage({ 43856 action: 'done', 43857 type: 'transmuxed', 43858 }); 43859 } 43860 endTimeline() { 43861 this.transmuxer.endTimeline(); // transmuxed endedtimeline action is fired after both audio/video pipelines end their 43862 // timelines 43863 43864 self.postMessage({ 43865 action: 'endedtimeline', 43866 type: 'transmuxed', 43867 }); 43868 } 43869 alignGopsWith(data) { 43870 this.transmuxer.alignGopsWith(data.gopsToAlignWith.slice()); 43871 } 43872 } 43873 /** 43874 * Our web worker interface so that things can talk to mux.js 43875 * that will be running in a web worker. the scope is passed to this by 43876 * webworkify. 43877 * 43878 * @param {Object} self the scope for the web worker 43879 */ 43880 43881 self.onmessage = function (event) {
43882 if (event.data.action === 'init' && event.data.options) { 43883 this.messageHandlers = new MessageHandlers(self, event.data.options); 43884 return; 43885 } 43886 if (!this.messageHandlers) { 43887 this.messageHandlers = new MessageHandlers(self); 43888 } 43889 if (event.data && event.data.action && event.data.action !== 'init') { 43890 if (this.messageHandlers[event.data.action]) { 43891 this.messageHandlers[event.data.action](event.data); 43892 } 43893 } 43894 }; 43895 }) 43896); 43897var TransmuxWorker = factory(workerCode$1); 43898/* rollup-plugin-worker-factory end for worker!/home/runner/work/http-streaming/http-streaming/src/transmuxer-worker.js */ 43899 43900const handleData_ = (event, transmuxedData, callback) => { 43901 const { 43902 type, 43903 initSegment, 43904 captions, 43905 captionStreams, 43906 metadata, 43907 videoFrameDtsTime, 43908 videoFramePtsTime, 43909 } = event.data.segment; 43910 transmuxedData.buffer.push({ 43911 captions, 43912 captionStreams, 43913 metadata, 43914 }); 43915 const boxes = event.data.segment.boxes || { 43916 data: event.data.segment.data, 43917 }; 43918 const result = { 43919 type, 43920 // cast ArrayBuffer to TypedArray 43921 data: new Uint8Array(boxes.data, boxes.data.byteOffset, boxes.data.byteLength), 43922 initSegment: new Uint8Array(initSegment.data, initSegment.byteOffset, initSegment.byteLength), 43923 }; 43924 if (typeof videoFrameDtsTime !== 'undefined') { 43925 result.videoFrameDtsTime = videoFrameDtsTime; 43926 } 43927 if (typeof videoFramePtsTime !== 'undefined') { 43928 result.videoFramePtsTime = videoFramePtsTime; 43929 } 43930 callback(result); 43931}; 43932const handleDone_ = ({ transmuxedData, callback }) => { 43933 // Previously we only returned data on data events, 43934 // not on done events. Clear out the buffer to keep that consistent. 43935 transmuxedData.buffer = []; // all buffers should have been flushed from the muxer, so start processing anything we 43936 // have received 43937 43938 callback(transmuxedData); 43939}; 43940const handleGopInfo_ = (event, transmuxedData) => { 43941 transmuxedData.gopInfo = event.data.gopInfo; 43942}; 43943const processTransmux = (options) => { 43944 const { 43945 transmuxer, 43946 bytes, 43947 audioAppendStart, 43948 gopsToAlignWith, 43949 remux, 43950 onData, 43951 onTrackInfo, 43952 onAudioTimingInfo, 43953 onVideoTimingInfo, 43954 onVideoSegmentTimingInfo, 43955 onAudioSegmentTimingInfo, 43956 onId3, 43957 onCaptions, 43958 onDone, 43959 onEndedTimeline, 43960 onTransmuxerLog, 43961 isEndOfTimeline, 43962 segment, 43963 triggerSegmentEventFn, 43964 } = options; 43965 const transmuxedData = { 43966 buffer: [], 43967 }; 43968 let waitForEndedTimelineEvent = isEndOfTimeline; 43969 const handleMessage = (event) => { 43970 if (transmuxer.currentTransmux !== options) { 43971 // disposed 43972 return; 43973 } 43974 if (event.data.action === 'data') { 43975 handleData_(event, transmuxedData, onData); 43976 } 43977 if (event.data.action === 'trackinfo') { 43978 onTrackInfo(event.data.trackInfo); 43979 } 43980 if (event.data.action === 'gopInfo') { 43981 handleGopInfo_(event, transmuxedData); 43982 } 43983 if (event.data.action === 'audioTimingInfo') { 43984 onAudioTimingInfo(event.data.audioTimingInfo); 43985 } 43986 if (event.data.action === 'videoTimingInfo') { 43987 onVideoTimingInfo(event.data.videoTimingInfo); 43988 } 43989 if (event.data.action === 'videoSegmentTimingInfo') { 43990 onVideoSegmentTimingInfo(event.data.videoSegmentTimingInfo); 43991 } 43992 if (event.data.action === 'audioSegmentTimingInfo') { 43993 onAudioSegmentTimingInfo(event.data.audioSegmentTimingInfo); 43994 } 43995 if (event.data.action === 'id3Frame') { 43996 onId3([event.data.id3Frame], event.data.id3Frame.dispatchType); 43997 } 43998 if (event.data.action === 'caption') { 43999 onCaptions(event.data.caption); 44000 } 44001 if (event.data.action === 'endedtimeline') { 44002 waitForEndedTimelineEvent = false; 44003 onEndedTimeline(); 44004 } 44005 if (event.data.action === 'log') { 44006 onTransmuxerLog(event.data.log); 44007 } // wait for the transmuxed event since we may have audio and video 44008 44009 if (event.data.type !== 'transmuxed') { 44010 return; 44011 } // If the "endedtimeline" event has not yet fired, and this segment represents the end 44012 // of a timeline, that means there may still be data events before the segment 44013 // processing can be considerred complete. In that case, the final event should be 44014 // an "endedtimeline" event with the type "transmuxed." 44015 44016 if (waitForEndedTimelineEvent) { 44017 return; 44018 } 44019 transmuxer.onmessage = null; 44020 handleDone_({ 44021 transmuxedData, 44022 callback: onDone, 44023 });
44024 /* eslint-disable no-use-before-define */ 44025 44026 dequeue(transmuxer); 44027 /* eslint-enable */ 44028 }; 44029 const handleError = () => { 44030 const error = { 44031 message: 'Received an error message from the transmuxer worker', 44032 metadata: { 44033 errorType: videojs.Error.StreamingFailedToTransmuxSegment, 44034 segmentInfo: segmentInfoPayload({ 44035 segment, 44036 }), 44037 }, 44038 }; 44039 onDone(null, error); 44040 }; 44041 transmuxer.onmessage = handleMessage; 44042 transmuxer.onerror = handleError; 44043 if (audioAppendStart) { 44044 transmuxer.postMessage({ 44045 action: 'setAudioAppendStart', 44046 appendStart: audioAppendStart, 44047 }); 44048 } // allow empty arrays to be passed to clear out GOPs 44049 44050 if (Array.isArray(gopsToAlignWith)) { 44051 transmuxer.postMessage({ 44052 action: 'alignGopsWith', 44053 gopsToAlignWith, 44054 }); 44055 } 44056 if (typeof remux !== 'undefined') { 44057 transmuxer.postMessage({ 44058 action: 'setRemux', 44059 remux, 44060 }); 44061 } 44062 if (bytes.byteLength) { 44063 const buffer = bytes instanceof ArrayBuffer ? bytes : bytes.buffer; 44064 const byteOffset = bytes instanceof ArrayBuffer ? 0 : bytes.byteOffset; 44065 triggerSegmentEventFn({ 44066 type: 'segmenttransmuxingstart', 44067 segment, 44068 }); 44069 transmuxer.postMessage( 44070 { 44071 action: 'push', 44072 // Send the typed-array of data as an ArrayBuffer so that 44073 // it can be sent as a "Transferable" and avoid the costly 44074 // memory copy 44075 data: buffer, 44076 // To recreate the original typed-array, we need information 44077 // about what portion of the ArrayBuffer it was a view into 44078 byteOffset, 44079 byteLength: bytes.byteLength, 44080 }, 44081 [buffer] 44082 ); 44083 } 44084 if (isEndOfTimeline) { 44085 transmuxer.postMessage({ 44086 action: 'endTimeline', 44087 }); 44088 } // even if we didn't push any bytes, we have to make sure we flush in case we reached 44089 // the end of the segment 44090 44091 transmuxer.postMessage({ 44092 action: 'flush', 44093 }); 44094}; 44095const dequeue = (transmuxer) => { 44096 transmuxer.currentTransmux = null; 44097 if (transmuxer.transmuxQueue.length) { 44098 transmuxer.currentTransmux = transmuxer.transmuxQueue.shift(); 44099 if (typeof transmuxer.currentTransmux === 'function') { 44100 transmuxer.currentTransmux(); 44101 } else { 44102 processTransmux(transmuxer.currentTransmux); 44103 } 44104 } 44105}; 44106const processAction = (transmuxer, action) => { 44107 transmuxer.postMessage({ 44108 action, 44109 }); 44110 dequeue(transmuxer); 44111}; 44112const enqueueAction = (action, transmuxer) => { 44113 if (!transmuxer.currentTransmux) { 44114 transmuxer.currentTransmux = action; 44115 processAction(transmuxer, action); 44116 return; 44117 } 44118 transmuxer.transmuxQueue.push(processAction.bind(null, transmuxer, action)); 44119}; 44120const reset = (transmuxer) => { 44121 enqueueAction('reset', transmuxer); 44122}; 44123const endTimeline = (transmuxer) => { 44124 enqueueAction('endTimeline', transmuxer); 44125}; 44126const transmux = (options) => { 44127 if (!options.transmuxer.currentTransmux) { 44128 options.transmuxer.currentTransmux = options; 44129 processTransmux(options); 44130 return; 44131 } 44132 options.transmuxer.transmuxQueue.push(options); 44133}; 44134const createTransmuxer = (options) => { 44135 const transmuxer = new TransmuxWorker(); 44136 transmuxer.currentTransmux = null; 44137 transmuxer.transmuxQueue = []; 44138 const term = transmuxer.terminate; 44139 transmuxer.terminate = () => { 44140 transmuxer.currentTransmux = null; 44141 transmuxer.transmuxQueue.length = 0; 44142 return term.call(transmuxer); 44143 }; 44144 transmuxer.postMessage({ 44145 action: 'init', 44146 options, 44147 }); 44148 return transmuxer; 44149}; 44150var segmentTransmuxer = { 44151 reset, 44152 endTimeline, 44153 transmux, 44154 createTransmuxer, 44155}; 44156const workerCallback = function (options) { 44157 const transmuxer = options.transmuxer; 44158 const endAction = options.endAction || options.action; 44159 const callback = options.callback; 44160 const message = _extends__default['default']({}, options, { 44161 endAction: null, 44162 transmuxer: null, 44163 callback: null, 44164 }); 44165 const listenForEndEvent = (event) => { 44166 if (event.data.action !== endAction) { 44167 return; 44168 } 44169 transmuxer.removeEventListener('message', listenForEndEvent); // transfer ownership of bytes back to us. 44170 44171 if (event.data.data) { 44172 event.data.data = new Uint8Array( 44173 event.data.data, 44174 options.byteOffset || 0, 44175 options.byteLength || event.data.data.byteLength 44176 ); 44177 if (options.data) { 44178 options.data = event.data.data; 44179 } 44180 } 44181 callback(event.data); 44182 }; 44183 transmuxer.addEventListener('message', listenForEndEvent); 44184 if (options.data) { 44185 const isArrayBuffer = options.data instanceof ArrayBuffer; 44186 message.byteOffset = isArrayBuffer ? 0 : options.data.byteOffset; 44187 message.byteLength = options.data.byteLength; 44188 const transfers = [isArrayBuffer ? options.data : options.data.buffer]; 44189 transmuxer.postMessage(message, transfers); 44190 } else { 44191 transmuxer.postMessage(message); 44192 } 44193}; 44194const REQUEST_ERRORS = { 44195 FAILURE: 2, 44196 TIMEOUT: -101, 44197 ABORTED: -102, 44198}; 44199const WEB_VTT_CODEC = 'wvtt'; 44200/** 44201 * Abort all requests 44202 * 44203 * @param {Object} activeXhrs - an object that tracks all XHR requests 44204 */ 44205 44206const abortAll = (activeXhrs) => {
44207 activeXhrs.forEach((xhr) => { 44208 xhr.abort(); 44209 }); 44210}; 44211/** 44212 * Gather important bandwidth stats once a request has completed 44213 * 44214 * @param {Object} request - the XHR request from which to gather stats 44215 */ 44216 44217const getRequestStats = (request) => { 44218 return { 44219 bandwidth: request.bandwidth, 44220 bytesReceived: request.bytesReceived || 0, 44221 roundTripTime: request.roundTripTime || 0, 44222 }; 44223}; 44224/** 44225 * If possible gather bandwidth stats as a request is in 44226 * progress 44227 * 44228 * @param {Event} progressEvent - an event object from an XHR's progress event 44229 */ 44230 44231const getProgressStats = (progressEvent) => { 44232 const request = progressEvent.target; 44233 const roundTripTime = Date.now() - request.requestTime; 44234 const stats = { 44235 bandwidth: Infinity, 44236 bytesReceived: 0, 44237 roundTripTime: roundTripTime || 0, 44238 }; 44239 stats.bytesReceived = progressEvent.loaded; // This can result in Infinity if stats.roundTripTime is 0 but that is ok 44240 // because we should only use bandwidth stats on progress to determine when 44241 // abort a request early due to insufficient bandwidth 44242 44243 stats.bandwidth = Math.floor((stats.bytesReceived / stats.roundTripTime) * 8 * 1000); 44244 return stats; 44245}; 44246/** 44247 * Handle all error conditions in one place and return an object 44248 * with all the information 44249 * 44250 * @param {Error|null} error - if non-null signals an error occured with the XHR 44251 * @param {Object} request - the XHR request that possibly generated the error 44252 */ 44253 44254const handleErrors = (error, request) => { 44255 const { requestType } = request; 44256 const metadata = getStreamingNetworkErrorMetadata({ 44257 requestType, 44258 request, 44259 error, 44260 }); 44261 if (request.timedout) { 44262 return { 44263 status: request.status, 44264 message: 'HLS request timed-out at URL: ' + request.uri, 44265 code: REQUEST_ERRORS.TIMEOUT, 44266 xhr: request, 44267 metadata, 44268 }; 44269 } 44270 if (request.aborted) { 44271 return { 44272 status: request.status, 44273 message: 'HLS request aborted at URL: ' + request.uri, 44274 code: REQUEST_ERRORS.ABORTED, 44275 xhr: request, 44276 metadata, 44277 }; 44278 } 44279 if (error) { 44280 return { 44281 status: request.status, 44282 message: 'HLS request errored at URL: ' + request.uri, 44283 code: REQUEST_ERRORS.FAILURE, 44284 xhr: request, 44285 metadata, 44286 }; 44287 } 44288 if (request.responseType === 'arraybuffer' && request.response.byteLength === 0) { 44289 return { 44290 status: request.status, 44291 message: 'Empty HLS response at URL: ' + request.uri, 44292 code: REQUEST_ERRORS.FAILURE, 44293 xhr: request, 44294 metadata, 44295 }; 44296 } 44297 return null; 44298}; 44299/** 44300 * Handle responses for key data and convert the key data to the correct format 44301 * for the decryption step later 44302 * 44303 * @param {Object} segment - a simplified copy of the segmentInfo object 44304 * from SegmentLoader 44305 * @param {Array} objects - objects to add the key bytes to. 44306 * @param {Function} finishProcessingFn - a callback to execute to continue processing 44307 * this request 44308 */ 44309 44310const handleKeyResponse = 44311 (segment, objects, finishProcessingFn, triggerSegmentEventFn) => (error, request) => { 44312 const response = request.response; 44313 const errorObj = handleErrors(error, request); 44314 if (errorObj) { 44315 return finishProcessingFn(errorObj, segment); 44316 } 44317 if (response.byteLength !== 16) { 44318 return finishProcessingFn( 44319 { 44320 status: request.status, 44321 message: 'Invalid HLS key at URL: ' + request.uri, 44322 code: REQUEST_ERRORS.FAILURE, 44323 xhr: request, 44324 }, 44325 segment 44326 ); 44327 } 44328 const view = new DataView(response); 44329 const bytes = new Uint32Array([ 44330 view.getUint32(0), 44331 view.getUint32(4), 44332 view.getUint32(8), 44333 view.getUint32(12), 44334 ]); 44335 for (let i = 0; i < objects.length; i++) { 44336 objects[i].bytes = bytes; 44337 } 44338 const keyInfo = { 44339 uri: request.uri, 44340 }; 44341 triggerSegmentEventFn({ 44342 type: 'segmentkeyloadcomplete', 44343 segment, 44344 keyInfo, 44345 }); 44346 return finishProcessingFn(null, segment); 44347 }; 44348/** 44349 * Processes an mp4 init segment depending on the codec through the transmuxer. 44350 * 44351 * @param {Object} segment init segment to process 44352 * @param {string} codec the codec of the text segments 44353 */ 44354 44355const initMp4Text = (segment, codec) => { 44356 if (codec === WEB_VTT_CODEC) { 44357 segment.transmuxer.postMessage({ 44358 action: 'initMp4WebVttParser', 44359 data: segment.map.bytes, 44360 }); 44361 } 44362}; 44363/** 44364 * Parses an mp4 text segment with the transmuxer and calls the doneFn from 44365 * the segment loader. 44366 * 44367 * @param {Object} segment the text segment to parse 44368 * @param {string} codec the codec of the text segment 44369 * @param {Function} doneFn the doneFn passed from the segment loader 44370 */ 44371 44372const parseMp4TextSegment = (segment, codec, doneFn) => { 44373 if (codec === WEB_VTT_CODEC) { 44374 workerCallback({ 44375 action: 'getMp4WebVttText', 44376 data: segment.bytes, 44377 transmuxer: segment.transmuxer, 44378 callback: ({ data, mp4VttCues }) => { 44379 segment.bytes = data; 44380 doneFn(null, segment, { 44381 mp4VttCues, 44382 }); 44383 }, 44384 }); 44385 } 44386}; 44387const parseInitSegment = (segment, callback) => { 44388 const type = containers.detectContainerForBytes(segment.map.bytes); // TODO: We should also handle ts init segments here, but we 44389 // only know how to parse mp4 init segments at the moment 44390 44391 if (type !== 'mp4') { 44392 const uri = segment.map.resolvedUri || segment.map.uri; 44393 const mediaType = type || 'unknown'; 44394 return callback({ 44395 internal: true, 44396 message: `Found unsupported ${mediaType} container for initialization segment at URL: ${uri}`, 44397 code: REQUEST_ERRORS.FAILURE, 44398 metadata: { 44399 mediaType, 44400 }, 44401 }); 44402 } 44403 workerCallback({ 44404 action: 'probeMp4Tracks', 44405 data: segment.map.bytes, 44406 transmuxer: segment.transmuxer, 44407 callback: ({ tracks, data }) => { 44408 // transfer bytes back to us 44409 segment.map.bytes = data; 44410 tracks.forEach(function (track) { 44411 segment.map.tracks = segment.map.tracks || {}; // only support one track of each type for now 44412 44413 if (segment.map.tracks[track.type]) { 44414 return; 44415 } 44416 segment.map.tracks[track.type] = track; 44417 if (typeof track.id === 'number' && track.timescale) { 44418 segment.map.timescales = segment.map.timescales || {}; 44419 segment.map.timescales[track.id] = track.timescale; 44420 } 44421 if (track.type === 'text') { 44422 initMp4Text(segment, track.codec); 44423 } 44424 }); 44425 return callback(null); 44426 }, 44427 }); 44428}; 44429/** 44430 * Handle init-segment responses 44431 * 44432 * @param {Object} segment - a simplified copy of the segmentInfo object 44433 * from SegmentLoader 44434 * @param {Function} finishProcessingFn - a callback to execute to continue processing 44435 * this request 44436 */ 44437 44438const handleInitSegmentResponse = 44439 ({ segment, finishProcessingFn, triggerSegmentEventFn }) => 44440 (error, request) => { 44441 const errorObj = handleErrors(error, request); 44442 if (errorObj) { 44443 return finishProcessingFn(errorObj, segment); 44444 } 44445 const bytes = new Uint8Array(request.response); 44446 triggerSegmentEventFn({ 44447 type: 'segmentloaded', 44448 segment, 44449 }); // init segment is encypted, we will have to wait 44450 // until the key request is done to decrypt. 44451 44452 if (segment.map.key) { 44453 segment.map.encryptedBytes = bytes; 44454 return finishProcessingFn(null, segment); 44455 } 44456 segment.map.bytes = bytes; 44457 parseInitSegment(segment, function (parseError) { 44458 if (parseError) { 44459 parseError.xhr = request; 44460 parseError.status = request.status; 44461 return finishProcessingFn(parseError, segment); 44462 } 44463 finishProcessingFn(null, segment); 44464 }); 44465 }; 44466/** 44467 * Response handler for segment-requests being sure to set the correct
44468 * property depending on whether the segment is encryped or not 44469 * Also records and keeps track of stats that are used for ABR purposes 44470 * 44471 * @param {Object} segment - a simplified copy of the segmentInfo object 44472 * from SegmentLoader 44473 * @param {Function} finishProcessingFn - a callback to execute to continue processing 44474 * this request 44475 */ 44476 44477const handleSegmentResponse = 44478 ({ segment, finishProcessingFn, responseType, triggerSegmentEventFn }) => 44479 (error, request) => { 44480 const errorObj = handleErrors(error, request); 44481 if (errorObj) { 44482 return finishProcessingFn(errorObj, segment); 44483 } 44484 triggerSegmentEventFn({ 44485 type: 'segmentloaded', 44486 segment, 44487 }); 44488 const newBytes = 44489 // although responseText "should" exist, this guard serves to prevent an error being 44490 // thrown for two primary cases: 44491 // 1. the mime type override stops working, or is not implemented for a specific 44492 // browser 44493 // 2. when using mock XHR libraries like sinon that do not allow the override behavior 44494 responseType === 'arraybuffer' || !request.responseText 44495 ? request.response 44496 : stringToArrayBuffer(request.responseText.substring(segment.lastReachedChar || 0)); 44497 segment.stats = getRequestStats(request); 44498 if (segment.key) { 44499 segment.encryptedBytes = new Uint8Array(newBytes); 44500 } else { 44501 segment.bytes = new Uint8Array(newBytes); 44502 } 44503 return finishProcessingFn(null, segment); 44504 }; 44505const transmuxAndNotify = ({ 44506 segment, 44507 bytes, 44508 trackInfoFn, 44509 timingInfoFn, 44510 videoSegmentTimingInfoFn, 44511 audioSegmentTimingInfoFn, 44512 id3Fn, 44513 captionsFn, 44514 isEndOfTimeline, 44515 endedTimelineFn, 44516 dataFn, 44517 doneFn, 44518 onTransmuxerLog, 44519 triggerSegmentEventFn, 44520}) => { 44521 const fmp4Tracks = (segment.map && segment.map.tracks) || {}; 44522 const isMuxed = Boolean(fmp4Tracks.audio && fmp4Tracks.video); // Keep references to each function so we can null them out after we're done with them. 44523 // One reason for this is that in the case of full segments, we want to trust start 44524 // times from the probe, rather than the transmuxer. 44525 44526 let audioStartFn = timingInfoFn.bind(null, segment, 'audio', 'start'); 44527 const audioEndFn = timingInfoFn.bind(null, segment, 'audio', 'end'); 44528 let videoStartFn = timingInfoFn.bind(null, segment, 'video', 'start'); 44529 const videoEndFn = timingInfoFn.bind(null, segment, 'video', 'end'); 44530 const finish = () => 44531 transmux({ 44532 bytes, 44533 transmuxer: segment.transmuxer, 44534 audioAppendStart: segment.audioAppendStart, 44535 gopsToAlignWith: segment.gopsToAlignWith, 44536 remux: isMuxed, 44537 onData: (result) => { 44538 result.type = result.type === 'combined' ? 'video' : result.type; 44539 dataFn(segment, result); 44540 }, 44541 onTrackInfo: (trackInfo) => { 44542 if (trackInfoFn) { 44543 if (isMuxed) { 44544 trackInfo.isMuxed = true; 44545 } 44546 trackInfoFn(segment, trackInfo); 44547 } 44548 }, 44549 onAudioTimingInfo: (audioTimingInfo) => { 44550 // we only want the first start value we encounter 44551 if (audioStartFn && typeof audioTimingInfo.start !== 'undefined') { 44552 audioStartFn(audioTimingInfo.start); 44553 audioStartFn = null; 44554 } // we want to continually update the end time 44555 44556 if (audioEndFn && typeof audioTimingInfo.end !== 'undefined') { 44557 audioEndFn(audioTimingInfo.end); 44558 } 44559 }, 44560 onVideoTimingInfo: (videoTimingInfo) => { 44561 // we only want the first start value we encounter 44562 if (videoStartFn && typeof videoTimingInfo.start !== 'undefined') { 44563 videoStartFn(videoTimingInfo.start); 44564 videoStartFn = null; 44565 } // we want to continually update the end time 44566 44567 if (videoEndFn && typeof videoTimingInfo.end !== 'undefined') { 44568 videoEndFn(videoTimingInfo.end); 44569 } 44570 }, 44571 onVideoSegmentTimingInfo: (videoSegmentTimingInfo) => { 44572 const timingInfo = { 44573 pts: { 44574 start: videoSegmentTimingInfo.start.presentation, 44575 end: videoSegmentTimingInfo.end.presentation, 44576 }, 44577 dts: { 44578 start: videoSegmentTimingInfo.start.decode, 44579 end: videoSegmentTimingInfo.end.decode, 44580 }, 44581 }; 44582 triggerSegmentEventFn({ 44583 type: 'segmenttransmuxingtiminginfoavailable', 44584 segment, 44585 timingInfo, 44586 }); 44587 videoSegmentTimingInfoFn(videoSegmentTimingInfo); 44588 }, 44589 onAudioSegmentTimingInfo: (audioSegmentTimingInfo) => { 44590 const timingInfo = { 44591 pts: { 44592 start: audioSegmentTimingInfo.start.pts, 44593 end: audioSegmentTimingInfo.end.pts, 44594 }, 44595 dts: { 44596 start: audioSegmentTimingInfo.start.dts, 44597 end: audioSegmentTimingInfo.end.dts, 44598 }, 44599 }; 44600 triggerSegmentEventFn({ 44601 type: 'segmenttransmuxingtiminginfoavailable', 44602 segment, 44603 timingInfo, 44604 }); 44605 audioSegmentTimingInfoFn(audioSegmentTimingInfo); 44606 }, 44607 onId3: (id3Frames, dispatchType) => { 44608 id3Fn(segment, id3Frames, dispatchType); 44609 }, 44610 onCaptions: (captions) => { 44611 captionsFn(segment, [captions]); 44612 }, 44613 isEndOfTimeline, 44614 onEndedTimeline: () => { 44615 endedTimelineFn(); 44616 }, 44617 onTransmuxerLog, 44618 onDone: (result, error) => { 44619 if (!doneFn) { 44620 return; 44621 } 44622 result.type = result.type === 'combined' ? 'video' : result.type; 44623 triggerSegmentEventFn({ 44624 type: 'segmenttransmuxingcomplete', 44625 segment, 44626 }); 44627 doneFn(error, segment, result); 44628 }, 44629 segment, 44630 triggerSegmentEventFn, 44631 }); // In the transmuxer, we don't yet have the ability to extract a "proper" start time. 44632 // Meaning cached frame data may corrupt our notion of where this segment 44633 // really starts. To get around this, probe for the info needed. 44634 44635 workerCallback({ 44636 action: 'probeTs', 44637 transmuxer: segment.transmuxer, 44638 data: bytes, 44639 baseStartTime: segment.baseStartTime, 44640 callback: (data) => { 44641 segment.bytes = bytes = data.data; 44642 const probeResult = data.result; 44643 if (probeResult) { 44644 trackInfoFn(segment, { 44645 hasAudio: probeResult.hasAudio, 44646 hasVideo: probeResult.hasVideo, 44647 isMuxed, 44648 }); 44649 trackInfoFn = null; 44650 } 44651 finish(); 44652 }, 44653 }); 44654}; 44655const handleSegmentBytes = ({ 44656 segment, 44657 bytes, 44658 trackInfoFn, 44659 timingInfoFn, 44660 videoSegmentTimingInfoFn, 44661 audioSegmentTimingInfoFn, 44662 id3Fn,
vendor: 4,304 bytes, lines 44663-44773
44663 captionsFn, 44664 isEndOfTimeline, 44665 endedTimelineFn, 44666 dataFn, 44667 doneFn, 44668 onTransmuxerLog, 44669 triggerSegmentEventFn, 44670}) => { 44671 let bytesAsUint8Array = new Uint8Array(bytes); // TODO: 44672 // We should have a handler that fetches the number of bytes required 44673 // to check if something is fmp4. This will allow us to save bandwidth 44674 // because we can only exclude a playlist and abort requests 44675 // by codec after trackinfo triggers. 44676 44677 if (containers.isLikelyFmp4MediaSegment(bytesAsUint8Array)) { 44678 segment.isFmp4 = true; 44679 const { tracks } = segment.map; 44680 const isMp4TextSegment = tracks.text && (!tracks.audio || !tracks.video); 44681 if (isMp4TextSegment) { 44682 dataFn(segment, { 44683 data: bytesAsUint8Array, 44684 type: 'text', 44685 }); 44686 parseMp4TextSegment(segment, tracks.text.codec, doneFn); 44687 return; 44688 } 44689 const trackInfo = { 44690 isFmp4: true, 44691 hasVideo: !!tracks.video, 44692 hasAudio: !!tracks.audio, 44693 }; // if we have a audio track, with a codec that is not set to 44694 // encrypted audio 44695 44696 if (tracks.audio && tracks.audio.codec && tracks.audio.codec !== 'enca') { 44697 trackInfo.audioCodec = tracks.audio.codec; 44698 } // if we have a video track, with a codec that is not set to 44699 // encrypted video 44700 44701 if (tracks.video && tracks.video.codec && tracks.video.codec !== 'encv') { 44702 trackInfo.videoCodec = tracks.video.codec; 44703 } 44704 if (tracks.video && tracks.audio) { 44705 trackInfo.isMuxed = true; 44706 } // since we don't support appending fmp4 data on progress, we know we have the full 44707 // segment here 44708 44709 trackInfoFn(segment, trackInfo); // The probe doesn't provide the segment end time, so only callback with the start 44710 // time. The end time can be roughly calculated by the receiver using the duration. 44711 // 44712 // Note that the start time returned by the probe reflects the baseMediaDecodeTime, as 44713 // that is the true start of the segment (where the playback engine should begin 44714 // decoding). 44715 44716 const finishLoading = (captions, id3Frames) => { 44717 // if the track still has audio at this point it is only possible 44718 // for it to be audio only. See `tracks.video && tracks.audio` if statement 44719 // above. 44720 // we make sure to use segment.bytes here as that 44721 dataFn(segment, { 44722 data: bytesAsUint8Array, 44723 type: trackInfo.hasAudio && !trackInfo.isMuxed ? 'audio' : 'video', 44724 }); 44725 if (id3Frames && id3Frames.length) { 44726 id3Fn(segment, id3Frames); 44727 } 44728 if (captions && captions.length) { 44729 captionsFn(segment, captions); 44730 } 44731 doneFn(null, segment, {}); 44732 }; 44733 workerCallback({ 44734 action: 'probeMp4StartTime', 44735 timescales: segment.map.timescales, 44736 data: bytesAsUint8Array, 44737 transmuxer: segment.transmuxer, 44738 callback: ({ data, startTime }) => { 44739 // transfer bytes back to us 44740 bytes = data.buffer; 44741 segment.bytes = bytesAsUint8Array = data; 44742 if (trackInfo.hasAudio && !trackInfo.isMuxed) { 44743 timingInfoFn(segment, 'audio', 'start', startTime); 44744 } 44745 if (trackInfo.hasVideo) { 44746 timingInfoFn(segment, 'video', 'start', startTime); 44747 } 44748 workerCallback({ 44749 action: 'probeEmsgID3', 44750 data: bytesAsUint8Array, 44751 transmuxer: segment.transmuxer, 44752 offset: startTime, 44753 callback: ({ emsgData, id3Frames }) => { 44754 // transfer bytes back to us 44755 bytes = emsgData.buffer; 44756 segment.bytes = bytesAsUint8Array = emsgData; // Run through the CaptionParser in case there are captions. 44757 // Initialize CaptionParser if it hasn't been yet 44758 44759 if (!tracks.video || !emsgData.byteLength || !segment.transmuxer) { 44760 finishLoading(undefined, id3Frames); 44761 return; 44762 } 44763 workerCallback({ 44764 action: 'pushMp4Captions', 44765 endAction: 'mp4Captions', 44766 transmuxer: segment.transmuxer, 44767 data: bytesAsUint8Array, 44768 timescales: segment.map.timescales, 44769 trackIds: [tracks.video.id], 44770 callback: (message) => { 44771 // transfer bytes back to us 44772 bytes = message.data.buffer; 44773 segment.bytes = bytesAsUint8Array = message.data;
44774 message.logs.forEach(function (log) { 44775 onTransmuxerLog( 44776 merge(log, { 44777 stream: 'mp4CaptionParser', 44778 }) 44779 ); 44780 }); 44781 finishLoading(message.captions, id3Frames); 44782 }, 44783 }); 44784 }, 44785 }); 44786 }, 44787 }); 44788 return; 44789 } // VTT or other segments that don't need processing 44790 44791 if (!segment.transmuxer) { 44792 doneFn(null, segment, {}); 44793 return; 44794 } 44795 if (typeof segment.container === 'undefined') { 44796 segment.container = containers.detectContainerForBytes(bytesAsUint8Array); 44797 } 44798 if (segment.container !== 'ts' && segment.container !== 'aac') { 44799 trackInfoFn(segment, { 44800 hasAudio: false, 44801 hasVideo: false, 44802 }); 44803 doneFn(null, segment, {}); 44804 return; 44805 } // ts or aac 44806 44807 transmuxAndNotify({ 44808 segment, 44809 bytes, 44810 trackInfoFn, 44811 timingInfoFn, 44812 videoSegmentTimingInfoFn, 44813 audioSegmentTimingInfoFn, 44814 id3Fn, 44815 captionsFn, 44816 isEndOfTimeline, 44817 endedTimelineFn, 44818 dataFn, 44819 doneFn, 44820 onTransmuxerLog, 44821 triggerSegmentEventFn, 44822 }); 44823}; 44824const decrypt = function ( 44825 { id, key, encryptedBytes, decryptionWorker, segment, doneFn }, 44826 callback 44827) { 44828 const decryptionHandler = (event) => { 44829 if (event.data.source === id) { 44830 decryptionWorker.removeEventListener('message', decryptionHandler); 44831 const decrypted = event.data.decrypted; 44832 callback(new Uint8Array(decrypted.bytes, decrypted.byteOffset, decrypted.byteLength)); 44833 } 44834 }; 44835 decryptionWorker.onerror = () => { 44836 const message = 'An error occurred in the decryption worker'; 44837 const segmentInfo = segmentInfoPayload({ 44838 segment, 44839 }); 44840 const decryptError = { 44841 message, 44842 metadata: { 44843 error: new Error(message), 44844 errorType: videojs.Error.StreamingFailedToDecryptSegment, 44845 segmentInfo, 44846 keyInfo: { 44847 uri: segment.key.resolvedUri || segment.map.key.resolvedUri, 44848 }, 44849 }, 44850 }; 44851 doneFn(decryptError, segment); 44852 }; 44853 decryptionWorker.addEventListener('message', decryptionHandler); 44854 let keyBytes; 44855 if (key.bytes.slice) { 44856 keyBytes = key.bytes.slice(); 44857 } else { 44858 keyBytes = new Uint32Array(Array.prototype.slice.call(key.bytes)); 44859 } // incrementally decrypt the bytes 44860 44861 decryptionWorker.postMessage( 44862 createTransferableMessage({ 44863 source: id, 44864 encrypted: encryptedBytes, 44865 key: keyBytes, 44866 iv: key.iv, 44867 }), 44868 [encryptedBytes.buffer, keyBytes.buffer] 44869 ); 44870}; 44871/** 44872 * Decrypt the segment via the decryption web worker 44873 * 44874 * @param {WebWorker} decryptionWorker - a WebWorker interface to AES-128 decryption 44875 * routines 44876 * @param {Object} segment - a simplified copy of the segmentInfo object 44877 * from SegmentLoader 44878 * @param {Function} trackInfoFn - a callback that receives track info 44879 * @param {Function} timingInfoFn - a callback that receives timing info 44880 * @param {Function} videoSegmentTimingInfoFn 44881 * a callback that receives video timing info based on media times and 44882 * any adjustments made by the transmuxer 44883 * @param {Function} audioSegmentTimingInfoFn 44884 * a callback that receives audio timing info based on media times and 44885 * any adjustments made by the transmuxer 44886 * @param {boolean} isEndOfTimeline 44887 * true if this segment represents the last segment in a timeline 44888 * @param {Function} endedTimelineFn 44889 * a callback made when a timeline is ended, will only be called if 44890 * isEndOfTimeline is true 44891 * @param {Function} dataFn - a callback that is executed when segment bytes are available 44892 * and ready to use 44893 * @param {Function} doneFn - a callback that is executed after decryption has completed 44894 */ 44895 44896const decryptSegment = ({ 44897 decryptionWorker, 44898 segment, 44899 trackInfoFn, 44900 timingInfoFn, 44901 videoSegmentTimingInfoFn, 44902 audioSegmentTimingInfoFn, 44903 id3Fn, 44904 captionsFn, 44905 isEndOfTimeline, 44906 endedTimelineFn, 44907 dataFn, 44908 doneFn, 44909 onTransmuxerLog, 44910 triggerSegmentEventFn, 44911}) => { 44912 triggerSegmentEventFn({ 44913 type: 'segmentdecryptionstart', 44914 }); 44915 decrypt( 44916 { 44917 id: segment.requestId, 44918 key: segment.key, 44919 encryptedBytes: segment.encryptedBytes, 44920 decryptionWorker, 44921 segment, 44922 doneFn, 44923 }, 44924 (decryptedBytes) => { 44925 segment.bytes = decryptedBytes; 44926 triggerSegmentEventFn({ 44927 type: 'segmentdecryptioncomplete', 44928 segment, 44929 }); 44930 handleSegmentBytes({ 44931 segment, 44932 bytes: segment.bytes, 44933 trackInfoFn, 44934 timingInfoFn, 44935 videoSegmentTimingInfoFn, 44936 audioSegmentTimingInfoFn, 44937 id3Fn, 44938 captionsFn, 44939 isEndOfTimeline, 44940 endedTimelineFn, 44941 dataFn, 44942 doneFn, 44943 onTransmuxerLog, 44944 triggerSegmentEventFn, 44945 }); 44946 } 44947 ); 44948}; 44949/** 44950 * This function waits for all XHRs to finish (with either success or failure) 44951 * before continueing processing via it's callback. The function gathers errors 44952 * from each request into a single errors array so that the error status for 44953 * each request can be examined later. 44954 * 44955 * @param {Object} activeXhrs - an object that tracks all XHR requests 44956 * @param {WebWorker} decryptionWorker - a WebWorker interface to AES-128 decryption 44957 * routines 44958 * @param {Function} trackInfoFn - a callback that receives track info 44959 * @param {Function} timingInfoFn - a callback that receives timing info 44960 * @param {Function} videoSegmentTimingInfoFn 44961 * a callback that receives video timing info based on media times and 44962 * any adjustments made by the transmuxer 44963 * @param {Function} audioSegmentTimingInfoFn 44964 * a callback that receives audio timing info based on media times and 44965 * any adjustments made by the transmuxer 44966 * @param {Function} id3Fn - a callback that receives ID3 metadata 44967 * @param {Function} captionsFn - a callback that receives captions 44968 * @param {boolean} isEndOfTimeline 44969 * true if this segment represents the last segment in a timeline 44970 * @param {Function} endedTimelineFn 44971 * a callback made when a timeline is ended, will only be called if 44972 * isEndOfTimeline is true 44973 * @param {Function} dataFn - a callback that is executed when segment bytes are available 44974 * and ready to use 44975 * @param {Function} doneFn - a callback that is executed after all resources have been 44976 * downloaded and any decryption completed 44977 */ 44978 44979const waitForCompletion = ({ 44980 activeXhrs, 44981 decryptionWorker, 44982 trackInfoFn, 44983 timingInfoFn, 44984 videoSegmentTimingInfoFn, 44985 audioSegmentTimingInfoFn, 44986 id3Fn,
44987 captionsFn, 44988 isEndOfTimeline, 44989 endedTimelineFn, 44990 dataFn, 44991 doneFn, 44992 onTransmuxerLog, 44993 triggerSegmentEventFn, 44994}) => { 44995 let count = 0; 44996 let didError = false; 44997 return (error, segment) => { 44998 if (didError) { 44999 return; 45000 } 45001 if (error) { 45002 didError = true; // If there are errors, we have to abort any outstanding requests 45003 45004 abortAll(activeXhrs); // Even though the requests above are aborted, and in theory we could wait until we 45005 // handle the aborted events from those requests, there are some cases where we may 45006 // never get an aborted event. For instance, if the network connection is lost and 45007 // there were two requests, the first may have triggered an error immediately, while 45008 // the second request remains unsent. In that case, the aborted algorithm will not 45009 // trigger an abort: see https://xhr.spec.whatwg.org/#the-abort()-method 45010 // 45011 // We also can't rely on the ready state of the XHR, since the request that 45012 // triggered the connection error may also show as a ready state of 0 (unsent). 45013 // Therefore, we have to finish this group of requests immediately after the first 45014 // seen error. 45015 45016 return doneFn(error, segment); 45017 } 45018 count += 1; 45019 if (count === activeXhrs.length) { 45020 const segmentFinish = function () { 45021 if (segment.encryptedBytes) { 45022 return decryptSegment({ 45023 decryptionWorker, 45024 segment, 45025 trackInfoFn, 45026 timingInfoFn, 45027 videoSegmentTimingInfoFn, 45028 audioSegmentTimingInfoFn, 45029 id3Fn, 45030 captionsFn, 45031 isEndOfTimeline, 45032 endedTimelineFn, 45033 dataFn, 45034 doneFn, 45035 onTransmuxerLog, 45036 triggerSegmentEventFn, 45037 }); 45038 } // Otherwise, everything is ready just continue 45039 45040 handleSegmentBytes({ 45041 segment, 45042 bytes: segment.bytes, 45043 trackInfoFn, 45044 timingInfoFn, 45045 videoSegmentTimingInfoFn, 45046 audioSegmentTimingInfoFn, 45047 id3Fn, 45048 captionsFn, 45049 isEndOfTimeline, 45050 endedTimelineFn, 45051 dataFn, 45052 doneFn, 45053 onTransmuxerLog, 45054 triggerSegmentEventFn, 45055 }); 45056 }; // Keep track of when *all* of the requests have completed 45057 45058 segment.endOfAllRequests = Date.now(); 45059 if (segment.map && segment.map.encryptedBytes && !segment.map.bytes) { 45060 triggerSegmentEventFn({ 45061 type: 'segmentdecryptionstart', 45062 segment, 45063 }); 45064 return decrypt( 45065 { 45066 decryptionWorker, 45067 // add -init to the "id" to differentiate between segment 45068 // and init segment decryption, just in case they happen 45069 // at the same time at some point in the future. 45070 id: segment.requestId + '-init', 45071 encryptedBytes: segment.map.encryptedBytes, 45072 key: segment.map.key, 45073 segment, 45074 doneFn, 45075 }, 45076 (decryptedBytes) => { 45077 segment.map.bytes = decryptedBytes; 45078 triggerSegmentEventFn({ 45079 type: 'segmentdecryptioncomplete', 45080 segment, 45081 }); 45082 parseInitSegment(segment, (parseError) => { 45083 if (parseError) { 45084 abortAll(activeXhrs); 45085 return doneFn(parseError, segment); 45086 } 45087 segmentFinish(); 45088 }); 45089 } 45090 ); 45091 } 45092 segmentFinish(); 45093 } 45094 }; 45095}; 45096/** 45097 * Calls the abort callback if any request within the batch was aborted. Will only call 45098 * the callback once per batch of requests, even if multiple were aborted. 45099 * 45100 * @param {Object} loadendState - state to check to see if the abort function was called 45101 * @param {Function} abortFn - callback to call for abort 45102 */ 45103 45104const handleLoadEnd = 45105 ({ loadendState, abortFn }) => 45106 (event) => { 45107 const request = event.target; 45108 if (request.aborted && abortFn && !loadendState.calledAbortFn) { 45109 abortFn(); 45110 loadendState.calledAbortFn = true; 45111 } 45112 }; 45113/** 45114 * Simple progress event callback handler that gathers some stats before 45115 * executing a provided callback with the `segment` object 45116 * 45117 * @param {Object} segment - a simplified copy of the segmentInfo object 45118 * from SegmentLoader 45119 * @param {Function} progressFn - a callback that is executed each time a progress event 45120 * is received 45121 * @param {Function} trackInfoFn - a callback that receives track info 45122 * @param {Function} timingInfoFn - a callback that receives timing info 45123 * @param {Function} videoSegmentTimingInfoFn 45124 * a callback that receives video timing info based on media times and 45125 * any adjustments made by the transmuxer 45126 * @param {Function} audioSegmentTimingInfoFn 45127 * a callback that receives audio timing info based on media times and 45128 * any adjustments made by the transmuxer 45129 * @param {boolean} isEndOfTimeline 45130 * true if this segment represents the last segment in a timeline 45131 * @param {Function} endedTimelineFn 45132 * a callback made when a timeline is ended, will only be called if 45133 * isEndOfTimeline is true 45134 * @param {Function} dataFn - a callback that is executed when segment bytes are available 45135 * and ready to use 45136 * @param {Event} event - the progress event object from XMLHttpRequest 45137 */ 45138 45139const handleProgress = 45140 ({ 45141 segment, 45142 progressFn, 45143 trackInfoFn, 45144 timingInfoFn, 45145 videoSegmentTimingInfoFn, 45146 audioSegmentTimingInfoFn, 45147 id3Fn, 45148 captionsFn, 45149 isEndOfTimeline, 45150 endedTimelineFn, 45151 dataFn, 45152 }) => 45153 (event) => { 45154 const request = event.target; 45155 if (request.aborted) { 45156 return; 45157 } 45158 segment.stats = merge(segment.stats, getProgressStats(event)); // record the time that we receive the first byte of data 45159 45160 if (!segment.stats.firstBytesReceivedAt && segment.stats.bytesReceived) { 45161 segment.stats.firstBytesReceivedAt = Date.now(); 45162 } 45163 return progressFn(event, segment); 45164 }; 45165/** 45166 * Load all resources and does any processing necessary for a media-segment 45167 * 45168 * Features: 45169 * decrypts the media-segment if it has a key uri and an iv 45170 * aborts *all* requests if *any* one request fails 45171 * 45172 * The segment object, at minimum, has the following format: 45173 * { 45174 * resolvedUri: String, 45175 * [transmuxer]: Object, 45176 * [byterange]: { 45177 * offset: Number, 45178 * length: Number 45179 * }, 45180 * [key]: { 45181 * resolvedUri: String 45182 * [byterange]: { 45183 * offset: Number, 45184 * length: Number 45185 * }, 45186 * iv: { 45187 * bytes: Uint32Array 45188 * } 45189 * }, 45190 * [map]: { 45191 * resolvedUri: String, 45192 * [byterange]: { 45193 * offset: Number, 45194 * length: Number 45195 * }, 45196 * [bytes]: Uint8Array 45197 * } 45198 * } 45199 * ...where [name] denotes optional properties 45200 * 45201 * @param {Function} xhr - an instance of the xhr wrapper in xhr.js 45202 * @param {Object} xhrOptions - the base options to provide to all xhr requests 45203 * @param {WebWorker} decryptionWorker - a WebWorker interface to AES-128 45204 * decryption routines 45205 * @param {Object} segment - a simplified copy of the segmentInfo object 45206 * from SegmentLoader 45207 * @param {Function} abortFn - a callback called (only once) if any piece of a request was 45208 * aborted 45209 * @param {Function} progressFn - a callback that receives progress events from the main 45210 * segment's xhr request 45211 * @param {Function} trackInfoFn - a callback that receives track info 45212 * @param {Function} timingInfoFn - a callback that receives timing info 45213 * @param {Function} videoSegmentTimingInfoFn 45214 * a callback that receives video timing info based on media times and 45215 * any adjustments made by the transmuxer 45216 * @param {Function} audioSegmentTimingInfoFn 45217 * a callback that receives audio timing info based on media times and 45218 * any adjustments made by the transmuxer 45219 * @param {Function} id3Fn - a callback that receives ID3 metadata 45220 * @param {Function} captionsFn - a callback that receives captions 45221 * @param {boolean} isEndOfTimeline 45222 * true if this segment represents the last segment in a timeline 45223 * @param {Function} endedTimelineFn 45224 * a callback made when a timeline is ended, will only be called if 45225 * isEndOfTimeline is true 45226 * @param {Function} dataFn - a callback that receives data from the main segment's xhr 45227 * request, transmuxed if needed 45228 * @param {Function} doneFn - a callback that is executed only once all requests have 45229 * succeeded or failed 45230 * @return {Function} a function that, when invoked, immediately aborts all 45231 * outstanding requests 45232 */ 45233 45234const mediaSegmentRequest = ({ 45235 xhr, 45236 xhrOptions, 45237 decryptionWorker, 45238 segment, 45239 abortFn, 45240 progressFn, 45241 trackInfoFn, 45242 timingInfoFn, 45243 videoSegmentTimingInfoFn, 45244 audioSegmentTimingInfoFn, 45245 id3Fn,
45246 captionsFn, 45247 isEndOfTimeline, 45248 endedTimelineFn, 45249 dataFn, 45250 doneFn, 45251 onTransmuxerLog, 45252 triggerSegmentEventFn, 45253}) => { 45254 const activeXhrs = []; 45255 const finishProcessingFn = waitForCompletion({ 45256 activeXhrs, 45257 decryptionWorker, 45258 trackInfoFn, 45259 timingInfoFn, 45260 videoSegmentTimingInfoFn, 45261 audioSegmentTimingInfoFn, 45262 id3Fn, 45263 captionsFn, 45264 isEndOfTimeline, 45265 endedTimelineFn, 45266 dataFn, 45267 doneFn, 45268 onTransmuxerLog, 45269 triggerSegmentEventFn, 45270 }); // optionally, request the decryption key 45271 45272 if (segment.key && !segment.key.bytes) { 45273 const objects = [segment.key]; 45274 if ( 45275 segment.map && 45276 !segment.map.bytes && 45277 segment.map.key && 45278 segment.map.key.resolvedUri === segment.key.resolvedUri 45279 ) { 45280 objects.push(segment.map.key); 45281 } 45282 const keyRequestOptions = merge(xhrOptions, { 45283 uri: segment.key.resolvedUri, 45284 responseType: 'arraybuffer', 45285 requestType: 'segment-key', 45286 }); 45287 const keyRequestCallback = handleKeyResponse( 45288 segment, 45289 objects, 45290 finishProcessingFn, 45291 triggerSegmentEventFn 45292 ); 45293 const keyInfo = { 45294 uri: segment.key.resolvedUri, 45295 }; 45296 triggerSegmentEventFn({ 45297 type: 'segmentkeyloadstart', 45298 segment, 45299 keyInfo, 45300 }); 45301 const keyXhr = xhr(keyRequestOptions, keyRequestCallback); 45302 activeXhrs.push(keyXhr); 45303 } // optionally, request the associated media init segment 45304 45305 if (segment.map && !segment.map.bytes) { 45306 const differentMapKey = 45307 segment.map.key && (!segment.key || segment.key.resolvedUri !== segment.map.key.resolvedUri); 45308 if (differentMapKey) { 45309 const mapKeyRequestOptions = merge(xhrOptions, { 45310 uri: segment.map.key.resolvedUri, 45311 responseType: 'arraybuffer', 45312 requestType: 'segment-key', 45313 }); 45314 const mapKeyRequestCallback = handleKeyResponse( 45315 segment, 45316 [segment.map.key], 45317 finishProcessingFn, 45318 triggerSegmentEventFn 45319 ); 45320 const keyInfo = { 45321 uri: segment.map.key.resolvedUri, 45322 }; 45323 triggerSegmentEventFn({ 45324 type: 'segmentkeyloadstart', 45325 segment, 45326 keyInfo, 45327 }); 45328 const mapKeyXhr = xhr(mapKeyRequestOptions, mapKeyRequestCallback); 45329 activeXhrs.push(mapKeyXhr); 45330 } 45331 const initSegmentOptions = merge(xhrOptions, { 45332 uri: segment.map.resolvedUri, 45333 responseType: 'arraybuffer', 45334 headers: segmentXhrHeaders(segment.map), 45335 requestType: 'segment-media-initialization', 45336 }); 45337 const initSegmentRequestCallback = handleInitSegmentResponse({ 45338 segment, 45339 finishProcessingFn, 45340 triggerSegmentEventFn, 45341 }); 45342 triggerSegmentEventFn({ 45343 type: 'segmentloadstart', 45344 segment, 45345 }); 45346 const initSegmentXhr = xhr(initSegmentOptions, initSegmentRequestCallback); 45347 activeXhrs.push(initSegmentXhr); 45348 } 45349 const segmentRequestOptions = merge(xhrOptions, { 45350 uri: (segment.part && segment.part.resolvedUri) || segment.resolvedUri, 45351 responseType: 'arraybuffer', 45352 headers: segmentXhrHeaders(segment), 45353 requestType: 'segment', 45354 }); 45355 const segmentRequestCallback = handleSegmentResponse({ 45356 segment, 45357 finishProcessingFn, 45358 responseType: segmentRequestOptions.responseType, 45359 triggerSegmentEventFn, 45360 }); 45361 triggerSegmentEventFn({ 45362 type: 'segmentloadstart', 45363 segment, 45364 }); 45365 const segmentXhr = xhr(segmentRequestOptions, segmentRequestCallback); 45366 segmentXhr.addEventListener( 45367 'progress', 45368 handleProgress({ 45369 segment, 45370 progressFn, 45371 trackInfoFn, 45372 timingInfoFn, 45373 videoSegmentTimingInfoFn, 45374 audioSegmentTimingInfoFn, 45375 id3Fn, 45376 captionsFn, 45377 isEndOfTimeline, 45378 endedTimelineFn, 45379 dataFn, 45380 }) 45381 ); 45382 activeXhrs.push(segmentXhr); // since all parts of the request must be considered, but should not make callbacks 45383 // multiple times, provide a shared state object 45384 45385 const loadendState = {}; 45386 activeXhrs.forEach((activeXhr) => { 45387 activeXhr.addEventListener( 45388 'loadend', 45389 handleLoadEnd({ 45390 loadendState, 45391 abortFn, 45392 }) 45393 ); 45394 }); 45395 return () => abortAll(activeXhrs); 45396}; 45397 45398/** 45399 * @file - codecs.js - Handles tasks regarding codec strings such as translating them to 45400 * codec strings, or translating codec strings into objects that can be examined. 45401 */ 45402const logFn$1 = logger('CodecUtils'); 45403/** 45404 * Returns a set of codec strings parsed from the playlist or the default 45405 * codec strings if no codecs were specified in the playlist 45406 * 45407 * @param {Playlist} media the current media playlist 45408 * @return {Object} an object with the video and audio codecs 45409 */ 45410 45411const getCodecs = function (media) { 45412 // if the codecs were explicitly specified, use them instead of the 45413 // defaults 45414 const mediaAttributes = media.attributes || {}; 45415 if (mediaAttributes.CODECS) { 45416 return codecs_js.parseCodecs(mediaAttributes.CODECS); 45417 } 45418}; 45419const isMaat = (main, media) => { 45420 const mediaAttributes = media.attributes || {}; 45421 return ( 45422 main && 45423 main.mediaGroups && 45424 main.mediaGroups.AUDIO && 45425 mediaAttributes.AUDIO && 45426 main.mediaGroups.AUDIO[mediaAttributes.AUDIO] 45427 ); 45428}; 45429const isMuxed = (main, media) => { 45430 if (!isMaat(main, media)) { 45431 return true; 45432 } 45433 const mediaAttributes = media.attributes || {}; 45434 const audioGroup = main.mediaGroups.AUDIO[mediaAttributes.AUDIO]; 45435 for (const groupId in audioGroup) { 45436 // If an audio group has a URI (the case for HLS, as HLS will use external playlists), 45437 // or there are listed playlists (the case for DASH, as the manifest will have already 45438 // provided all of the details necessary to generate the audio playlist, as opposed to 45439 // HLS' externally requested playlists), then the content is demuxed. 45440 if (!audioGroup[groupId].uri && !audioGroup[groupId].playlists) { 45441 return true; 45442 } 45443 } 45444 return false;
vendor: 5,133 bytes, lines 45445-45599
45445}; 45446const unwrapCodecList = function (codecList) { 45447 const codecs = {}; 45448 codecList.forEach(({ mediaType, type, details }) => { 45449 codecs[mediaType] = codecs[mediaType] || []; 45450 codecs[mediaType].push(codecs_js.translateLegacyCodec(`${type}${details}`)); 45451 }); 45452 Object.keys(codecs).forEach(function (mediaType) { 45453 if (codecs[mediaType].length > 1) { 45454 logFn$1( 45455 `multiple ${mediaType} codecs found as attributes: ${codecs[mediaType].join(', ')}. Setting playlist codecs to null so that we wait for mux.js to probe segments for real codecs.` 45456 ); 45457 codecs[mediaType] = null; 45458 return; 45459 } 45460 codecs[mediaType] = codecs[mediaType][0]; 45461 }); 45462 return codecs; 45463}; 45464const codecCount = function (codecObj) { 45465 let count = 0; 45466 if (codecObj.audio) { 45467 count++; 45468 } 45469 if (codecObj.video) { 45470 count++; 45471 } 45472 return count; 45473}; 45474/** 45475 * Calculates the codec strings for a working configuration of 45476 * SourceBuffers to play variant streams in a main playlist. If 45477 * there is no possible working configuration, an empty object will be 45478 * returned. 45479 * 45480 * @param main {Object} the m3u8 object for the main playlist 45481 * @param media {Object} the m3u8 object for the variant playlist 45482 * @return {Object} the codec strings. 45483 * 45484 * @private 45485 */ 45486 45487const codecsForPlaylist = function (main, media) { 45488 const mediaAttributes = media.attributes || {}; 45489 const codecInfo = unwrapCodecList(getCodecs(media) || []); // HLS with multiple-audio tracks must always get an audio codec. 45490 // Put another way, there is no way to have a video-only multiple-audio HLS! 45491 45492 if (isMaat(main, media) && !codecInfo.audio) { 45493 if (!isMuxed(main, media)) { 45494 // It is possible for codecs to be specified on the audio media group playlist but 45495 // not on the rendition playlist. This is mostly the case for DASH, where audio and 45496 // video are always separate (and separately specified). 45497 const defaultCodecs = unwrapCodecList( 45498 codecs_js.codecsFromDefault(main, mediaAttributes.AUDIO) || [] 45499 ); 45500 if (defaultCodecs.audio) { 45501 codecInfo.audio = defaultCodecs.audio; 45502 } 45503 } 45504 } 45505 return codecInfo; 45506}; 45507const logFn = logger('PlaylistSelector'); 45508const representationToString = function (representation) { 45509 if (!representation || !representation.playlist) { 45510 return; 45511 } 45512 const playlist = representation.playlist; 45513 return JSON.stringify({ 45514 id: playlist.id, 45515 bandwidth: representation.bandwidth, 45516 width: representation.width, 45517 height: representation.height, 45518 codecs: (playlist.attributes && playlist.attributes.CODECS) || '', 45519 }); 45520}; // Utilities 45521 45522/** 45523 * Returns the CSS value for the specified property on an element 45524 * using `getComputedStyle`. Firefox has a long-standing issue where 45525 * getComputedStyle() may return null when running in an iframe with 45526 * `display: none`. 45527 * 45528 * @see https://bugzilla.mozilla.org/show_bug.cgi?id=548397 45529 * @param {HTMLElement} el the htmlelement to work on 45530 * @param {string} the proprety to get the style for 45531 */ 45532 45533const safeGetComputedStyle = function (el, property) { 45534 if (!el) { 45535 return ''; 45536 } 45537 const result = window__default['default'].getComputedStyle(el); 45538 if (!result) { 45539 return ''; 45540 } 45541 return result[property]; 45542}; 45543/** 45544 * Resuable stable sort function 45545 * 45546 * @param {Playlists} array 45547 * @param {Function} sortFn Different comparators 45548 * @function stableSort 45549 */ 45550 45551const stableSort = function (array, sortFn) { 45552 const newArray = array.slice(); 45553 array.sort(function (left, right) { 45554 const cmp = sortFn(left, right); 45555 if (cmp === 0) { 45556 return newArray.indexOf(left) - newArray.indexOf(right); 45557 } 45558 return cmp; 45559 }); 45560}; 45561/** 45562 * A comparator function to sort two playlist object by bandwidth. 45563 * 45564 * @param {Object} left a media playlist object 45565 * @param {Object} right a media playlist object 45566 * @return {number} Greater than zero if the bandwidth attribute of 45567 * left is greater than the corresponding attribute of right. Less 45568 * than zero if the bandwidth of right is greater than left and 45569 * exactly zero if the two are equal. 45570 */ 45571 45572const comparePlaylistBandwidth = function (left, right) { 45573 let leftBandwidth; 45574 let rightBandwidth; 45575 if (left.attributes.BANDWIDTH) { 45576 leftBandwidth = left.attributes.BANDWIDTH; 45577 } 45578 leftBandwidth = leftBandwidth || window__default['default'].Number.MAX_VALUE; 45579 if (right.attributes.BANDWIDTH) { 45580 rightBandwidth = right.attributes.BANDWIDTH; 45581 } 45582 rightBandwidth = rightBandwidth || window__default['default'].Number.MAX_VALUE; 45583 return leftBandwidth - rightBandwidth; 45584}; 45585/** 45586 * A comparator function to sort two playlist object by resolution (width). 45587 * 45588 * @param {Object} left a media playlist object 45589 * @param {Object} right a media playlist object 45590 * @return {number} Greater than zero if the resolution.width attribute of 45591 * left is greater than the corresponding attribute of right. Less 45592 * than zero if the resolution.width of right is greater than left and 45593 * exactly zero if the two are equal. 45594 */ 45595 45596const comparePlaylistResolution = function (left, right) { 45597 let leftWidth; 45598 let rightWidth; 45599 if (left.attributes.RESOLUTION && left.attributes.RESOLUTION.width) {
45600 leftWidth = left.attributes.RESOLUTION.width; 45601 } 45602 leftWidth = leftWidth || window__default['default'].Number.MAX_VALUE; 45603 if (right.attributes.RESOLUTION && right.attributes.RESOLUTION.width) { 45604 rightWidth = right.attributes.RESOLUTION.width; 45605 } 45606 rightWidth = rightWidth || window__default['default'].Number.MAX_VALUE; // NOTE - Fallback to bandwidth sort as appropriate in cases where multiple renditions 45607 // have the same media dimensions/ resolution 45608 45609 if (leftWidth === rightWidth && left.attributes.BANDWIDTH && right.attributes.BANDWIDTH) { 45610 return left.attributes.BANDWIDTH - right.attributes.BANDWIDTH; 45611 } 45612 return leftWidth - rightWidth; 45613}; 45614/** 45615 * Chooses the appropriate media playlist based on bandwidth and player size 45616 * 45617 * @param {Object} main 45618 * Object representation of the main manifest 45619 * @param {number} playerBandwidth 45620 * Current calculated bandwidth of the player 45621 * @param {number} playerWidth 45622 * Current width of the player element (should account for the device pixel ratio) 45623 * @param {number} playerHeight 45624 * Current height of the player element (should account for the device pixel ratio) 45625 * @param {boolean} limitRenditionByPlayerDimensions 45626 * True if the player width and height should be used during the selection, false otherwise 45627 * @param {Object} playlistController 45628 * the current playlistController object 45629 * @return {Playlist} the highest bitrate playlist less than the 45630 * currently detected bandwidth, accounting for some amount of 45631 * bandwidth variance 45632 */ 45633 45634let simpleSelector = function ( 45635 main, 45636 playerBandwidth, 45637 playerWidth, 45638 playerHeight, 45639 limitRenditionByPlayerDimensions, 45640 playlistController 45641) { 45642 // If we end up getting called before `main` is available, exit early 45643 if (!main) { 45644 return; 45645 } 45646 const options = { 45647 bandwidth: playerBandwidth, 45648 width: playerWidth,
45649 height: playerHeight, 45650 limitRenditionByPlayerDimensions, 45651 }; 45652 let playlists = main.playlists; // if playlist is audio only, select between currently active audio group playlists. 45653 45654 if (Playlist.isAudioOnly(main)) { 45655 playlists = playlistController.getAudioTrackPlaylists_(); // add audioOnly to options so that we log audioOnly: true 45656 // at the buttom of this function for debugging. 45657 45658 options.audioOnly = true; 45659 } // convert the playlists to an intermediary representation to make comparisons easier 45660 45661 let sortedPlaylistReps = playlists.map((playlist) => { 45662 let bandwidth; 45663 const width = 45664 playlist.attributes && playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.width; 45665 const height = 45666 playlist.attributes && 45667 playlist.attributes.RESOLUTION && 45668 playlist.attributes.RESOLUTION.height; 45669 bandwidth = playlist.attributes && playlist.attributes.BANDWIDTH; 45670 bandwidth = bandwidth || window__default['default'].Number.MAX_VALUE; 45671 return { 45672 bandwidth, 45673 width, 45674 height, 45675 playlist, 45676 }; 45677 }); 45678 stableSort(sortedPlaylistReps, (left, right) => left.bandwidth - right.bandwidth); // filter out any playlists that have been excluded due to 45679 // incompatible configurations 45680 45681 sortedPlaylistReps = sortedPlaylistReps.filter((rep) => !Playlist.isIncompatible(rep.playlist)); // filter out any playlists that have been disabled manually through the representations 45682 // api or excluded temporarily due to playback errors. 45683 45684 let enabledPlaylistReps = sortedPlaylistReps.filter((rep) => Playlist.isEnabled(rep.playlist)); 45685 if (!enabledPlaylistReps.length) { 45686 // if there are no enabled playlists, then they have all been excluded or disabled 45687 // by the user through the representations api. In this case, ignore exclusion and 45688 // fallback to what the user wants by using playlists the user has not disabled. 45689 enabledPlaylistReps = sortedPlaylistReps.filter((rep) => !Playlist.isDisabled(rep.playlist)); 45690 } // filter out any variant that has greater effective bitrate 45691 // than the current estimated bandwidth 45692 45693 const bandwidthPlaylistReps = enabledPlaylistReps.filter( 45694 (rep) => rep.bandwidth * Config.BANDWIDTH_VARIANCE < playerBandwidth 45695 ); 45696 let highestRemainingBandwidthRep = bandwidthPlaylistReps[bandwidthPlaylistReps.length - 1]; // get all of the renditions with the same (highest) bandwidth 45697 // and then taking the very first element 45698 45699 const bandwidthBestRep = bandwidthPlaylistReps.filter( 45700 (rep) => rep.bandwidth === highestRemainingBandwidthRep.bandwidth 45701 )[0]; // if we're not going to limit renditions by player size, make an early decision. 45702 45703 if (limitRenditionByPlayerDimensions === false) { 45704 const chosenRep = bandwidthBestRep || enabledPlaylistReps[0] || sortedPlaylistReps[0]; 45705 if (chosenRep && chosenRep.playlist) { 45706 let type = 'sortedPlaylistReps'; 45707 if (bandwidthBestRep) { 45708 type = 'bandwidthBestRep'; 45709 } 45710 if (enabledPlaylistReps[0]) { 45711 type = 'enabledPlaylistReps'; 45712 } 45713 logFn(`choosing ${representationToString(chosenRep)} using ${type} with options`, options); 45714 return chosenRep.playlist; 45715 } 45716 logFn('could not choose a playlist with options', options); 45717 return null; 45718 } // filter out playlists without resolution information 45719 45720 const haveResolution = bandwidthPlaylistReps.filter((rep) => rep.width && rep.height); // sort variants by resolution 45721 45722 stableSort(haveResolution, (left, right) => left.width - right.width); // if we have the exact resolution as the player use it 45723 45724 const resolutionBestRepList = haveResolution.filter( 45725 (rep) => rep.width === playerWidth && rep.height === playerHeight 45726 ); 45727 highestRemainingBandwidthRep = resolutionBestRepList[resolutionBestRepList.length - 1]; // ensure that we pick the highest bandwidth variant that have exact resolution 45728 45729 const resolutionBestRep = resolutionBestRepList.filter( 45730 (rep) => rep.bandwidth === highestRemainingBandwidthRep.bandwidth 45731 )[0]; 45732 let resolutionPlusOneList; 45733 let resolutionPlusOneSmallest; 45734 let resolutionPlusOneRep; // find the smallest variant that is larger than the player 45735 // if there is no match of exact resolution 45736 45737 if (!resolutionBestRep) { 45738 resolutionPlusOneList = haveResolution.filter( 45739 (rep) => rep.width > playerWidth || rep.height >
vendor: 6,685 bytes, lines 45739-45917
45739 playerHeight 45740 ); // find all the variants have the same smallest resolution 45741 45742 resolutionPlusOneSmallest = resolutionPlusOneList.filter( 45743 (rep) => 45744 rep.width === resolutionPlusOneList[0].width && 45745 rep.height === resolutionPlusOneList[0].height 45746 ); // ensure that we also pick the highest bandwidth variant that 45747 // is just-larger-than the video player 45748 45749 highestRemainingBandwidthRep = resolutionPlusOneSmallest[resolutionPlusOneSmallest.length - 1]; 45750 resolutionPlusOneRep = resolutionPlusOneSmallest.filter( 45751 (rep) => rep.bandwidth === highestRemainingBandwidthRep.bandwidth 45752 )[0]; 45753 } 45754 let leastPixelDiffRep; // If this selector proves to be better than others, 45755 // resolutionPlusOneRep and resolutionBestRep and all 45756 // the code involving them should be removed. 45757 45758 if (playlistController.leastPixelDiffSelector) { 45759 // find the variant that is closest to the player's pixel size 45760 const leastPixelDiffList = haveResolution.map((rep) => { 45761 rep.pixelDiff = Math.abs(rep.width - playerWidth) + Math.abs(rep.height - playerHeight); 45762 return rep; 45763 }); // get the highest bandwidth, closest resolution playlist 45764 45765 stableSort(leastPixelDiffList, (left, right) => { 45766 // sort by highest bandwidth if pixelDiff is the same 45767 if (left.pixelDiff === right.pixelDiff) { 45768 return right.bandwidth - left.bandwidth; 45769 } 45770 return left.pixelDiff - right.pixelDiff; 45771 }); 45772 leastPixelDiffRep = leastPixelDiffList[0]; 45773 } // fallback chain of variants 45774 45775 const chosenRep = 45776 leastPixelDiffRep || 45777 resolutionPlusOneRep || 45778 resolutionBestRep || 45779 bandwidthBestRep || 45780 enabledPlaylistReps[0] || 45781 sortedPlaylistReps[0]; 45782 if (chosenRep && chosenRep.playlist) { 45783 let type = 'sortedPlaylistReps'; 45784 if (leastPixelDiffRep) { 45785 type = 'leastPixelDiffRep'; 45786 } else if (resolutionPlusOneRep) { 45787 type = 'resolutionPlusOneRep'; 45788 } else if (resolutionBestRep) { 45789 type = 'resolutionBestRep'; 45790 } else if (bandwidthBestRep) { 45791 type = 'bandwidthBestRep'; 45792 } else if (enabledPlaylistReps[0]) { 45793 type = 'enabledPlaylistReps'; 45794 } 45795 logFn(`choosing ${representationToString(chosenRep)} using ${type} with options`, options); 45796 return chosenRep.playlist; 45797 } 45798 logFn('could not choose a playlist with options', options); 45799 return null; 45800}; 45801 45802/** 45803 * Chooses the appropriate media playlist based on the most recent 45804 * bandwidth estimate and the player size. 45805 * 45806 * Expects to be called within the context of an instance of VhsHandler 45807 * 45808 * @return {Playlist} the highest bitrate playlist less than the 45809 * currently detected bandwidth, accounting for some amount of 45810 * bandwidth variance 45811 */ 45812 45813const lastBandwidthSelector = function () { 45814 let pixelRatio = this.useDevicePixelRatio ? window__default['default'].devicePixelRatio || 1 : 1; 45815 if (!isNaN(this.customPixelRatio)) { 45816 pixelRatio = this.customPixelRatio; 45817 } 45818 return simpleSelector( 45819 this.playlists.main, 45820 this.systemBandwidth, 45821 parseInt(safeGetComputedStyle(this.tech_.el(), 'width'), 10) * pixelRatio, 45822 parseInt(safeGetComputedStyle(this.tech_.el(), 'height'), 10) * pixelRatio, 45823 this.limitRenditionByPlayerDimensions, 45824 this.playlistController_ 45825 ); 45826}; 45827/** 45828 * Chooses the appropriate media playlist based on an 45829 * exponential-weighted moving average of the bandwidth after 45830 * filtering for player size. 45831 * 45832 * Expects to be called within the context of an instance of VhsHandler 45833 * 45834 * @param {number} decay - a number between 0 and 1. Higher values of 45835 * this parameter will cause previous bandwidth estimates to lose 45836 * significance more quickly. 45837 * @return {Function} a function which can be invoked to create a new 45838 * playlist selector function. 45839 * @see https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average 45840 */ 45841 45842const movingAverageBandwidthSelector = function (decay) { 45843 let average = -1; 45844 let lastSystemBandwidth = -1; 45845 if (decay < 0 || decay > 1) { 45846 throw new Error('Moving average bandwidth decay must be between 0 and 1.'); 45847 } 45848 return function () { 45849 let pixelRatio = this.useDevicePixelRatio 45850 ? window__default['default'].devicePixelRatio || 1 45851 : 1; 45852 if (!isNaN(this.customPixelRatio)) { 45853 pixelRatio = this.customPixelRatio; 45854 } 45855 if (average < 0) { 45856 average = this.systemBandwidth; 45857 lastSystemBandwidth = this.systemBandwidth; 45858 } // stop the average value from decaying for every 250ms 45859 // when the systemBandwidth is constant 45860 // and 45861 // stop average from setting to a very low value when the 45862 // systemBandwidth becomes 0 in case of chunk cancellation 45863 45864 if (this.systemBandwidth > 0 && this.systemBandwidth !== lastSystemBandwidth) { 45865 average = decay * this.systemBandwidth + (1 - decay) * average; 45866 lastSystemBandwidth = this.systemBandwidth; 45867 } 45868 return simpleSelector( 45869 this.playlists.main, 45870 average, 45871 parseInt(safeGetComputedStyle(this.tech_.el(), 'width'), 10) * pixelRatio, 45872 parseInt(safeGetComputedStyle(this.tech_.el(), 'height'), 10) * pixelRatio, 45873 this.limitRenditionByPlayerDimensions, 45874 this.playlistController_ 45875 ); 45876 }; 45877}; 45878/** 45879 * Chooses the appropriate media playlist based on the potential to rebuffer 45880 * 45881 * @param {Object} settings 45882 * Object of information required to use this selector 45883 * @param {Object} settings.main 45884 * Object representation of the main manifest 45885 * @param {number} settings.currentTime 45886 * The current time of the player 45887 * @param {number} settings.bandwidth 45888 * Current measured bandwidth 45889 * @param {number} settings.duration 45890 * Duration of the media 45891 * @param {number} settings.segmentDuration 45892 * Segment duration to be used in round trip time calculations 45893 * @param {number} settings.timeUntilRebuffer 45894 * Time left in seconds until the player has to rebuffer 45895 * @param {number} settings.currentTimeline 45896 * The current timeline segments are being loaded from 45897 * @param {SyncController} settings.syncController 45898 * SyncController for determining if we have a sync point for a given playlist 45899 * @return {Object|null} 45900 * {Object} return.playlist 45901 * The highest bandwidth playlist with the least amount of rebuffering 45902 * {Number} return.rebufferingImpact 45903 * The amount of time in seconds switching to this playlist will rebuffer. A 45904 * negative value means that switching will cause zero rebuffering. 45905 */ 45906 45907const minRebufferMaxBandwidthSelector = function (settings) { 45908 const { 45909 main, 45910 currentTime, 45911 bandwidth, 45912 duration, 45913 segmentDuration, 45914 timeUntilRebuffer, 45915 currentTimeline, 45916 syncController, 45917 }
45917 = settings; // filter out any playlists that have been excluded due to 45918 // incompatible configurations 45919 45920 const compatiblePlaylists = main.playlists.filter( 45921 (playlist) => !Playlist.isIncompatible(playlist) 45922 ); // filter out any playlists that have been disabled manually through the representations 45923 // api or excluded temporarily due to playback errors. 45924 45925 let enabledPlaylists = compatiblePlaylists.filter(Playlist.isEnabled); 45926 if (!enabledPlaylists.length) { 45927 // if there are no enabled playlists, then they have all been excluded or disabled 45928 // by the user through the representations api. In this case, ignore exclusion and 45929 // fallback to what the user wants by using playlists the user has not disabled. 45930 enabledPlaylists = compatiblePlaylists.filter((playlist) => !Playlist.isDisabled(playlist)); 45931 } 45932 const bandwidthPlaylists = enabledPlaylists.filter(Playlist.hasAttribute.bind(null, 'BANDWIDTH')); 45933 const rebufferingEstimates = bandwidthPlaylists.map((playlist) => { 45934 const syncPoint = syncController.getSyncPoint(playlist, duration, currentTimeline, currentTime); // If there is no sync point for this playlist, switching to it will require a 45935 // sync request first. This will double the request time 45936 45937 const numRequests = syncPoint ? 1 : 2; 45938 const requestTimeEstimate = Playlist.estimateSegmentRequestTime( 45939 segmentDuration, 45940 bandwidth, 45941 playlist 45942 ); 45943 const rebufferingImpact = requestTimeEstimate * numRequests - timeUntilRebuffer; 45944 return { 45945 playlist, 45946 rebufferingImpact, 45947 }; 45948 }); 45949 const noRebufferingPlaylists = rebufferingEstimates.filter( 45950 (estimate) => estimate.rebufferingImpact <= 0 45951 ); // Sort by bandwidth DESC 45952 45953 stableSort(noRebufferingPlaylists, (a, b) => comparePlaylistBandwidth(b.playlist, a.playlist)); 45954 if (noRebufferingPlaylists.length) { 45955 return noRebufferingPlaylists[0]; 45956 } 45957 stableSort(rebufferingEstimates, (a, b) => a.rebufferingImpact - b.rebufferingImpact); 45958 return rebufferingEstimates[0] || null; 45959}; 45960/** 45961 * Chooses the appropriate media playlist, which in this case is the lowest bitrate 45962 * one with video. If no renditions with video exist, return the lowest audio rendition. 45963 * 45964 * Expects to be called within the context of an instance of VhsHandler 45965 * 45966 * @return {Object|null} 45967 * {Object} return.playlist 45968 * The lowest bitrate playlist that contains a video codec. If no such rendition 45969 * exists pick the lowest audio rendition. 45970 */ 45971 45972const lowestBitrateCompatibleVariantSelector = function () { 45973 // filter out any playlists that have been excluded due to 45974 // incompatible configurations or playback errors 45975 const playlists = this.playlists.main.playlists.filter(Playlist.isEnabled); // Sort ascending by bitrate 45976 45977 stableSort(playlists, (a, b) => comparePlaylistBandwidth(a, b)); // Parse and assume that playlists with no video codec have no video 45978 // (this is not necessarily true, although it is generally true). 45979 // 45980 // If an entire manifest has no valid videos everything will get filtered 45981 // out. 45982 45983 const playlistsWithVideo = playlists.filter( 45984 (playlist) => !!codecsForPlaylist(this.playlists.main, playlist).video 45985 ); 45986 return playlistsWithVideo[0] || null; 45987}; 45988 45989/** 45990 * Combine all segments into a single Uint8Array 45991 * 45992 * @param {Object} segmentObj 45993 * @return {Uint8Array} concatenated bytes 45994 * @private 45995 */ 45996const concatSegments = (segmentObj) => { 45997 let offset = 0; 45998 let tempBuffer; 45999 if (segmentObj.bytes) { 46000 tempBuffer = new Uint8Array(segmentObj.bytes); // combine the individual segments into one large typed-array 46001 46002 segmentObj.segments.forEach((segment) => { 46003 tempBuffer.set(segment, offset); 46004 offset += segment.byteLength; 46005 }); 46006 } 46007 return tempBuffer; 46008}; 46009/** 46010 * Example: 46011 * https://host.com/path1/path2/path3/segment.ts?arg1=val1 46012 * --> 46013 * path3/segment.ts 46014 * 46015 * @param resolvedUri 46016 * @return {string} 46017 */ 46018 46019function compactSegmentUrlDescription(resolvedUri) { 46020 try { 46021 return new URL(resolvedUri).pathname.split('/').slice(-2).join('/'); 46022 } catch (e) { 46023 return ''; 46024 } 46025} 46026 46027/** 46028 * @file text-tracks.js 46029 */ 46030/** 46031 * Create captions text tracks on video.js if they do not exist 46032 * 46033 * @param {Object} inbandTextTracks a reference to current inbandTextTracks 46034 * @param {Object} tech the video.js tech 46035 * @param {Object} captionStream the caption stream to create 46036 * @private 46037 */ 46038 46039const createCaptionsTrackIfNotExists = function (inbandTextTracks, tech, captionStream) { 46040 if (!inbandTextTracks[captionStream]) { 46041 tech.trigger({ 46042 type: 'usage', 46043 name: 'vhs-608', 46044 }); 46045 let instreamId = captionStream; // we need to translate SERVICEn for 708 to how mux.js currently labels them 46046 46047 if (/^cc708_/.test(captionStream)) { 46048 instreamId = 'SERVICE' + captionStream.split('_')[1]; 46049 } 46050 const track = tech.textTracks().getTrackById(instreamId); 46051 if (track) { 46052 // Resuse an existing track with a CC# id because this was 46053 // very likely created by videojs-contrib-hls from information 46054 // in the m3u8 for us to use 46055 inbandTextTracks[captionStream] = track; 46056 } else { 46057 // This section gets called when we have caption services that aren't specified in the manifest. 46058 // Manifest level caption services are handled in media-groups.js under CLOSED-CAPTIONS. 46059 const captionServices = (tech.options_.vhs && tech.options_.vhs.captionServices) || {}; 46060 let label = captionStream; 46061 let language = captionStream; 46062 let def = false;
46063 const captionService = captionServices[instreamId]; 46064 if (captionService) { 46065 label = captionService.label; 46066 language = captionService.language; 46067 def = captionService.default; 46068 } // Otherwise, create a track with the default `CC#` label and 46069 // without a language 46070 46071 inbandTextTracks[captionStream] = tech.addRemoteTextTrack( 46072 { 46073 kind: 'captions', 46074 id: instreamId, 46075 // TODO: investigate why this doesn't seem to turn the caption on by default 46076 default: def, 46077 label, 46078 language, 46079 }, 46080 false 46081 ).track; 46082 } 46083 } 46084}; 46085/** 46086 * Add caption text track data to a source handler given an array of captions 46087 * 46088 * @param {Object} 46089 * @param {Object} inbandTextTracks the inband text tracks 46090 * @param {number} timestampOffset the timestamp offset of the source buffer 46091 * @param {Array} captionArray an array of caption data 46092 * @private 46093 */ 46094 46095const addCaptionData = function ({ inbandTextTracks, captionArray, timestampOffset }) { 46096 if (!captionArray) { 46097 return; 46098 } 46099 const Cue = window__default['default'].WebKitDataCue || window__default['default'].VTTCue; 46100 captionArray.forEach((caption) => { 46101 const track = caption.stream; // in CEA 608 captions, video.js/mux.js sends a content array 46102 // with positioning data 46103 46104 if (caption.content) { 46105 caption.content.forEach((value) => { 46106 const cue = new Cue( 46107 caption.startTime + timestampOffset, 46108 caption.endTime + timestampOffset, 46109 value.text 46110 ); 46111 cue.line = value.line; 46112 cue.align = 'left'; 46113 cue.position = value.position; 46114 cue.positionAlign = 'line-left'; 46115 inbandTextTracks[track].addCue(cue); 46116 }); 46117 } else { 46118 // otherwise, a text value with combined captions is sent 46119 inbandTextTracks[track].addCue( 46120 new Cue( 46121 caption.startTime + timestampOffset, 46122 caption.endTime + timestampOffset, 46123 caption.text 46124 ) 46125 ); 46126 } 46127 }); 46128}; 46129/** 46130 * Define properties on a cue for backwards compatability, 46131 * but warn the user that the way that they are using it 46132 * is depricated and will be removed at a later date. 46133 * 46134 * @param {Cue} cue the cue to add the properties on 46135 * @private 46136 */ 46137 46138const deprecateOldCue = function (cue) { 46139 Object.defineProperties(cue.frame, { 46140 id: { 46141 get() { 46142 videojs.log.warn('cue.frame.id is deprecated. Use cue.value.key instead.'); 46143 return cue.value.key; 46144 }, 46145 }, 46146 value: { 46147 get() { 46148 videojs.log.warn('cue.frame.value is deprecated. Use cue.value.data instead.'); 46149 return cue.value.data; 46150 }, 46151 }, 46152 privateData: { 46153 get() { 46154 videojs.log.warn('cue.frame.privateData is deprecated. Use cue.value.data instead.'); 46155 return cue.value.data; 46156 }, 46157 }, 46158 }); 46159}; 46160/** 46161 * Add metadata text track data to a source handler given an array of metadata 46162 * 46163 * @param {Object} 46164 * @param {Object} inbandTextTracks the inband text tracks 46165 * @param {Array} metadataArray an array of meta data 46166 * @param {number} timestampOffset the timestamp offset of the source buffer 46167 * @param {number} videoDuration the duration of the video 46168 * @private 46169 */ 46170 46171const addMetadata = ({ inbandTextTracks, metadataArray, timestampOffset, videoDuration }) => { 46172 if (!metadataArray) { 46173 return; 46174 } 46175 const Cue = window__default['default'].WebKitDataCue || window__default['default'].VTTCue; 46176 const metadataTrack = inbandTextTracks.metadataTrack_; 46177 if (!metadataTrack) { 46178 return; 46179 } 46180 metadataArray.forEach((metadata) => { 46181 const time = metadata.cueTime + timestampOffset; // if time isn't a finite number between 0 and Infinity, like NaN, 46182 // ignore this bit of metadata. 46183 // This likely occurs when you have an non-timed ID3 tag like TIT2, 46184 // which is the "Title/Songname/Content description" frame 46185 46186 if ( 46187 typeof time !== 'number' || 46188 window__default['default'].isNaN(time) || 46189 time < 0 || 46190 !(time < Infinity) 46191 ) { 46192 return; 46193 } // If we have no frames, we can't create a cue. 46194 46195 if (!metadata.frames || !metadata.frames.length) { 46196 return; 46197 }
vendor: 7,255 bytes, lines 46198-46441
46198 metadata.frames.forEach((frame) => { 46199 const cue = new Cue(time, time, frame.value || frame.url || frame.data || ''); 46200 cue.frame = frame; 46201 cue.value = frame; 46202 deprecateOldCue(cue); 46203 metadataTrack.addCue(cue); 46204 }); 46205 }); 46206 if (!metadataTrack.cues || !metadataTrack.cues.length) { 46207 return; 46208 } // Updating the metadeta cues so that 46209 // the endTime of each cue is the startTime of the next cue 46210 // the endTime of last cue is the duration of the video 46211 46212 const cues = metadataTrack.cues; 46213 const cuesArray = []; // Create a copy of the TextTrackCueList... 46214 // ...disregarding cues with a falsey value 46215 46216 for (let i = 0; i < cues.length; i++) { 46217 if (cues[i]) { 46218 cuesArray.push(cues[i]); 46219 } 46220 } // Group cues by their startTime value 46221 46222 const cuesGroupedByStartTime = cuesArray.reduce((obj, cue) => { 46223 const timeSlot = obj[cue.startTime] || []; 46224 timeSlot.push(cue); 46225 obj[cue.startTime] = timeSlot; 46226 return obj; 46227 }, {}); // Sort startTimes by ascending order 46228 46229 const sortedStartTimes = Object.keys(cuesGroupedByStartTime).sort( 46230 (a, b) => Number(a) - Number(b) 46231 ); // Map each cue group's endTime to the next group's startTime 46232 46233 sortedStartTimes.forEach((startTime, idx) => { 46234 const cueGroup = cuesGroupedByStartTime[startTime]; 46235 const finiteDuration = isFinite(videoDuration) ? videoDuration : startTime; 46236 const nextTime = Number(sortedStartTimes[idx + 1]) || finiteDuration; // Map each cue's endTime the next group's startTime 46237 46238 cueGroup.forEach((cue) => { 46239 cue.endTime = nextTime; 46240 }); 46241 }); 46242}; // object for mapping daterange attributes 46243 46244const dateRangeAttr = { 46245 id: 'ID', 46246 class: 'CLASS', 46247 startDate: 'START-DATE', 46248 duration: 'DURATION', 46249 endDate: 'END-DATE', 46250 endOnNext: 'END-ON-NEXT', 46251 plannedDuration: 'PLANNED-DURATION', 46252 scte35Out: 'SCTE35-OUT', 46253 scte35In: 'SCTE35-IN', 46254}; 46255const dateRangeKeysToOmit = new Set([ 46256 'id', 46257 'class', 46258 'startDate', 46259 'duration', 46260 'endDate', 46261 'endOnNext', 46262 'startTime', 46263 'endTime', 46264 'processDateRange', 46265]); 46266/** 46267 * Add DateRange metadata text track to a source handler given an array of metadata 46268 * 46269 * @param {Object} 46270 * @param {Object} inbandTextTracks the inband text tracks 46271 * @param {Array} dateRanges parsed media playlist 46272 * @private 46273 */ 46274 46275const addDateRangeMetadata = ({ inbandTextTracks, dateRanges }) => { 46276 const metadataTrack = inbandTextTracks.metadataTrack_; 46277 if (!metadataTrack) { 46278 return; 46279 } 46280 const Cue = window__default['default'].WebKitDataCue || window__default['default'].VTTCue; 46281 dateRanges.forEach((dateRange) => { 46282 // we generate multiple cues for each date range with different attributes 46283 for (const key of Object.keys(dateRange)) { 46284 if (dateRangeKeysToOmit.has(key)) { 46285 continue; 46286 } 46287 const cue = new Cue(dateRange.startTime, dateRange.endTime, ''); 46288 cue.id = dateRange.id; 46289 cue.type = 'com.apple.quicktime.HLS'; 46290 cue.value = { 46291 key: dateRangeAttr[key], 46292 data: dateRange[key], 46293 }; 46294 if (key === 'scte35Out' || key === 'scte35In') { 46295 cue.value.data = new Uint8Array(cue.value.data.match(/[\da-f]{2}/gi)).buffer; 46296 } 46297 metadataTrack.addCue(cue); 46298 } 46299 dateRange.processDateRange(); 46300 }); 46301}; 46302/** 46303 * Create metadata text track on video.js if it does not exist 46304 * 46305 * @param {Object} inbandTextTracks a reference to current inbandTextTracks 46306 * @param {string} dispatchType the inband metadata track dispatch type 46307 * @param {Object} tech the video.js tech 46308 * @private 46309 */ 46310 46311const createMetadataTrackIfNotExists = (inbandTextTracks, dispatchType, tech) => { 46312 if (inbandTextTracks.metadataTrack_) { 46313 return; 46314 } 46315 inbandTextTracks.metadataTrack_ = tech.addRemoteTextTrack( 46316 { 46317 kind: 'metadata', 46318 label: 'Timed Metadata', 46319 }, 46320 false 46321 ).track; 46322 if (!videojs.browser.IS_ANY_SAFARI) { 46323 inbandTextTracks.metadataTrack_.inBandMetadataTrackDispatchType = dispatchType; 46324 } 46325}; 46326/** 46327 * Remove cues from a track on video.js. 46328 * 46329 * @param {Double} start start of where we should remove the cue 46330 * @param {Double} end end of where the we should remove the cue 46331 * @param {Object} track the text track to remove the cues from 46332 * @private 46333 */ 46334 46335const removeCuesFromTrack = function (start, end, track) { 46336 let i; 46337 let cue; 46338 if (!track) { 46339 return; 46340 } 46341 if (!track.cues) { 46342 return; 46343 } 46344 i = track.cues.length; 46345 while (i--) { 46346 cue = track.cues[i]; // Remove any cue within the provided start and end time 46347 46348 if (cue.startTime >= start && cue.endTime <= end) { 46349 track.removeCue(cue); 46350 } 46351 } 46352}; 46353/** 46354 * Remove duplicate cues from a track on video.js (a cue is considered a 46355 * duplicate if it has the same time interval and text as another) 46356 * 46357 * @param {Object} track the text track to remove the duplicate cues from 46358 * @private 46359 */ 46360 46361const removeDuplicateCuesFromTrack = function (track) { 46362 const cues = track.cues; 46363 if (!cues) { 46364 return; 46365 } 46366 const uniqueCues = {}; 46367 for (let i = cues.length - 1; i >= 0; i--) { 46368 const cue = cues[i]; 46369 const cueKey = `${cue.startTime}-${cue.endTime}-${cue.text}`; 46370 if (uniqueCues[cueKey]) { 46371 track.removeCue(cue); 46372 } else { 46373 uniqueCues[cueKey] = cue; 46374 } 46375 } 46376}; 46377 46378/** 46379 * Returns a list of gops in the buffer that have a pts value of 3 seconds or more in 46380 * front of current time. 46381 * 46382 * @param {Array} buffer 46383 * The current buffer of gop information 46384 * @param {number} currentTime 46385 * The current time 46386 * @param {Double} mapping 46387 * Offset to map display time to stream presentation time 46388 * @return {Array} 46389 * List of gops considered safe to append over 46390 */ 46391 46392const gopsSafeToAlignWith = (buffer, currentTime, mapping) => { 46393 if (typeof currentTime === 'undefined' || currentTime === null || !buffer.length) { 46394 return []; 46395 } // pts value for current time + 3 seconds to give a bit more wiggle room 46396 46397 const currentTimePts = Math.ceil((currentTime - mapping + 3) * clock.ONE_SECOND_IN_TS); 46398 let i; 46399 for (i = 0; i < buffer.length; i++) { 46400 if (buffer[i].pts > currentTimePts) { 46401 break; 46402 } 46403 } 46404 return buffer.slice(i); 46405}; 46406/** 46407 * Appends gop information (timing and byteLength) received by the transmuxer for the 46408 * gops appended in the last call to appendBuffer 46409 * 46410 * @param {Array} buffer 46411 * The current buffer of gop information 46412 * @param {Array} gops 46413 * List of new gop information 46414 * @param {boolean} replace 46415 * If true, replace the buffer with the new gop information. If false, append the 46416 * new gop information to the buffer in the right location of time. 46417 * @return {Array} 46418 * Updated list of gop information 46419 */ 46420 46421const updateGopBuffer = (buffer, gops, replace) => { 46422 if (!gops.length) { 46423 return buffer; 46424 } 46425 if (replace) { 46426 // If we are in safe append mode, then completely overwrite the gop buffer 46427 // with the most recent appeneded data. This will make sure that when appending 46428 // future segments, we only try to align with gops that are both ahead of current 46429 // time and in the last segment appended. 46430 return gops.slice(); 46431 } 46432 const start = gops[0].pts; 46433 let i = 0; 46434 for (i; i < buffer.length; i++) { 46435 if (buffer[i].pts >= start) { 46436 break; 46437 } 46438 } 46439 return buffer.slice(0, i).concat(gops); 46440}; 46441/**
46442 * Removes gop information in buffer that overlaps with provided start and end 46443 * 46444 * @param {Array} buffer 46445 * The current buffer of gop information 46446 * @param {Double} start 46447 * position to start the remove at 46448 * @param {Double} end 46449 * position to end the remove at 46450 * @param {Double} mapping 46451 * Offset to map display time to stream presentation time 46452 */ 46453 46454const removeGopBuffer = (buffer, start, end, mapping) => { 46455 const startPts = Math.ceil((start - mapping) * clock.ONE_SECOND_IN_TS); 46456 const endPts = Math.ceil((end - mapping) * clock.ONE_SECOND_IN_TS); 46457 const updatedBuffer = buffer.slice(); 46458 let i = buffer.length; 46459 while (i--) { 46460 if (buffer[i].pts <= endPts) { 46461 break; 46462 } 46463 } 46464 if (i === -1) { 46465 // no removal because end of remove range is before start of buffer 46466 return updatedBuffer; 46467 } 46468 let j = i + 1; 46469 while (j--) { 46470 if (buffer[j].pts <= startPts) { 46471 break; 46472 } 46473 } // clamp remove range start to 0 index 46474 46475 j = Math.max(j, 0); 46476 updatedBuffer.splice(j, i - j + 1); 46477 return updatedBuffer; 46478}; 46479const shallowEqual = function (a, b) { 46480 // if both are undefined 46481 // or one or the other is undefined 46482 // they are not equal 46483 if ((!a && !b) || (!a && b) || (a && !b)) { 46484 return false; 46485 } // they are the same object and thus, equal 46486 46487 if (a === b) { 46488 return true; 46489 } // sort keys so we can make sure they have 46490 // all the same keys later. 46491 46492 const akeys = Object.keys(a).sort(); 46493 const bkeys = Object.keys(b).sort(); // different number of keys, not equal 46494 46495 if (akeys.length !== bkeys.length) { 46496 return false; 46497 } 46498 for (let i = 0; i < akeys.length; i++) { 46499 const key = akeys[i]; // different sorted keys, not equal 46500 46501 if (key !== bkeys[i]) { 46502 return false; 46503 } // different values, not equal 46504 46505 if (a[key] !== b[key]) { 46506 return false; 46507 } 46508 } 46509 return true; 46510}; 46511 46512/** 46513 * The segment loader has no recourse except to fetch a segment in the 46514 * current playlist and use the internal timestamps in that segment to 46515 * generate a syncPoint. This function returns a good candidate index 46516 * for that process. 46517 * 46518 * @param {Array} segments - the segments array from a playlist. 46519 * @return {number} An index of a segment from the playlist to load 46520 */ 46521 46522const getSyncSegmentCandidate = function (currentTimeline, segments, targetTime) { 46523 segments = segments || []; 46524 const timelineSegments = []; 46525 let time = 0; 46526 for (let i = 0; i < segments.length; i++) { 46527 const segment = segments[i]; 46528 if (currentTimeline === segment.timeline) { 46529 timelineSegments.push(i); 46530 time += segment.duration; 46531 if (time > targetTime) { 46532 return i; 46533 } 46534 } 46535 } 46536 if (timelineSegments.length === 0) { 46537 return 0; 46538 } // default to the last timeline segment 46539 46540 return timelineSegments[timelineSegments.length - 1]; 46541}; // In the event of a quota exceeded error, keep at least one second of back buffer. This 46542// number was arbitrarily chosen and may be updated in the future, but seemed reasonable 46543// as a start to prevent any potential issues with removing content too close to the 46544// playhead. 46545 46546const MIN_BACK_BUFFER = 1; // in ms 46547 46548const CHECK_BUFFER_DELAY = 500; 46549const finite = (num) => typeof num === 'number' && isFinite(num); // With most content hovering around 30fps, if a segment has a duration less than a half 46550// frame at 30fps or one frame at 60fps, the bandwidth and throughput calculations will 46551// not accurately reflect the rest of the content. 46552 46553const MIN_SEGMENT_DURATION_TO_SAVE_STATS = 1 / 60; 46554const illegalMediaSwitch = (loaderType, startingMedia, trackInfo) => { 46555 // Although these checks should most likely cover non 'main' types, for now it narrows 46556 // the scope of our checks. 46557 if (loaderType !== 'main' || !startingMedia || !trackInfo) { 46558 return null; 46559 } 46560 if (!trackInfo.hasAudio && !trackInfo.hasVideo) { 46561 return 'Neither audio nor video found in segment.'; 46562 } 46563 if (startingMedia.hasVideo && !trackInfo.hasVideo) { 46564 return ( 46565 'Only audio found in segment when we expected video.' + 46566 " We can't switch to audio only from a stream that had video." + 46567 ' To get rid of this message, please add codec information to the manifest.' 46568 ); 46569 } 46570 if (!startingMedia.hasVideo && trackInfo.hasVideo) { 46571 return (
46572 'Video found in segment when we expected only audio.' + 46573 " We can't switch to a stream with video from an audio only stream." + 46574 ' To get rid of this message, please add codec information to the manifest.' 46575 ); 46576 } 46577 return null; 46578}; 46579/** 46580 * Calculates a time value that is safe to remove from the back buffer without interrupting 46581 * playback. 46582 * 46583 * @param {TimeRange} seekable 46584 * The current seekable range 46585 * @param {number} currentTime 46586 * The current time of the player 46587 * @param {number} targetDuration 46588 * The target duration of the current playlist 46589 * @return {number} 46590 * Time that is safe to remove from the back buffer without interrupting playback 46591 */ 46592 46593const safeBackBufferTrimTime = (seekable, currentTime, targetDuration) => { 46594 // 30 seconds before the playhead provides a safe default for trimming. 46595 // 46596 // Choosing a reasonable default is particularly important for high bitrate content and 46597 // VOD videos/live streams with large windows, as the buffer may end up overfilled and 46598 // throw an APPEND_BUFFER_ERR. 46599 let trimTime = currentTime - Config.BACK_BUFFER_LENGTH; 46600 if (seekable.length) { 46601 // Some live playlists may have a shorter window of content than the full allowed back 46602 // buffer. For these playlists, don't save content that's no longer within the window. 46603 trimTime = Math.max(trimTime, seekable.start(0)); 46604 } // Don't remove within target duration of the current time to avoid the possibility of 46605 // removing the GOP currently being played, as removing it can cause playback stalls. 46606 46607 const maxTrimTime = currentTime - targetDuration; 46608 return Math.min(maxTrimTime, trimTime); 46609}; 46610const segmentInfoString = (segmentInfo) => { 46611 const { 46612 startOfSegment, 46613 duration, 46614 segment, 46615 part, 46616 playlist: { mediaSequence: seq, id, segments = [] }, 46617 mediaIndex: index, 46618 partIndex, 46619 timeline, 46620 } = segmentInfo; 46621 const segmentLen = segments.length - 1; 46622 let selection = 'mediaIndex/partIndex increment'; 46623 if (segmentInfo.getMediaInfoForTime) { 46624 selection = `getMediaInfoForTime (${segmentInfo.getMediaInfoForTime})`; 46625 } else if (segmentInfo.isSyncRequest) { 46626 selection = 'getSyncSegmentCandidate (isSyncRequest)'; 46627 } 46628 if (segmentInfo.independent) { 46629 selection += ` with independent ${segmentInfo.independent}`; 46630 } 46631 const hasPartIndex = typeof partIndex === 'number'; 46632 const name = segmentInfo.segment.uri ? 'segment' : 'pre-segment'; 46633 const zeroBasedPartCount = hasPartIndex 46634 ? getKnownPartCount({ 46635 preloadSegment: segment, 46636 }) - 1 46637 : 0; 46638 return ( 46639 `${name} [${seq + index}/${seq + segmentLen}]` + 46640 (hasPartIndex ? ` part [${partIndex}/${zeroBasedPartCount}]` : '') + 46641 ` segment start/end [${segment.start} => ${segment.end}]` + 46642 (hasPartIndex ? ` part start/end [${part.start} => ${part.end}]` : '') + 46643 ` startOfSegment [${startOfSegment}]` + 46644 ` duration [${duration}]` + 46645 ` timeline [${timeline}]` + 46646 ` selected by [${selection}]` + 46647 ` playlist [${id}]` 46648 ); 46649}; 46650const timingInfoPropertyForMedia = (mediaType) => `${mediaType}TimingInfo`; 46651/** 46652 * Returns the timestamp offset to use for the segment. 46653 * 46654 * @param {number} segmentTimeline 46655 * The timeline of the segment 46656 * @param {number} currentTimeline 46657 * The timeline currently being followed by the loader 46658 * @param {number} startOfSegment 46659 * The estimated segment start 46660 * @param {TimeRange[]} buffered 46661 * The loader's buffer 46662 * @param {boolean} overrideCheck 46663 * If true, no checks are made to see if the timestamp offset value should be set, 46664 * but sets it directly to a value. 46665 * 46666 * @return {number|null} 46667 * Either a number representing a new timestamp offset, or null if the segment is 46668 * part of the same timeline 46669 */ 46670 46671const timestampOffsetForSegment = ({ 46672 segmentTimeline, 46673 currentTimeline, 46674 startOfSegment, 46675 buffered, 46676 overrideCheck, 46677}) => { 46678 // Check to see if we are crossing a discontinuity to see if we need to set the 46679 // timestamp offset on the transmuxer and source buffer. 46680 // 46681 // Previously, we changed the timestampOffset if the start of this segment was less than 46682 // the currently set timestampOffset, but this isn't desirable as it can produce bad 46683 // behavior, especially around long running live streams. 46684 if (!overrideCheck && segmentTimeline === currentTimeline) { 46685 return null; 46686 } // When changing renditions, it's possible to request a segment on an older timeline. For 46687 // instance, given two renditions with the following: 46688 // 46689 // #EXTINF:10 46690 // segment1 46691 // #EXT-X-DISCONTINUITY 46692 // #EXTINF:10 46693 // segment2 46694 // #EXTINF:10 46695 // segment3 46696 // 46697 // And the current player state: 46698 // 46699 // current time: 8 46700 // buffer: 0 => 20 46701 // 46702 // The next segment on the current rendition would be segment3, filling the buffer from 46703 // 20s onwards. However, if a rendition switch happens after segment2 was requested, 46704 // then the next segment to be requested will be segment1 from the new rendition in 46705 // order to fill time 8 and onwards. Using the buffered end would result in repeated 46706 // content (since it would position segment1 of the new rendition starting at 20s). This 46707 // case can be identified when the new segment's timeline is a prior value. Instead of 46708 // using the buffered end, the startOfSegment can be used, which, hopefully, will be 46709 // more accurate to the actual start time of the segment. 46710 46711 if (segmentTimeline < currentTimeline) { 46712 return startOfSegment; 46713 } // segmentInfo.startOfSegment used to be used as the timestamp offset, however, that 46714 // value uses the end of the last segment if it is available. While this value 46715 // should often be correct, it's better to rely on the buffered end, as the new 46716 // content post discontinuity should line up with the buffered end as if it were 46717 // time 0 for the new content. 46718 46719 return buffered.length ? buffered.end(buffered.length - 1) : startOfSegment; 46720}; 46721/** 46722 * Returns whether or not the loader should wait for a timeline change from the timeline 46723 * change controller before processing the segment. 46724 * 46725 * Primary timing in VHS goes by video. This is different from most media players, as 46726 * audio is more often used as the primary timing source. For the foreseeable future, VHS 46727 * will continue to use video as the primary timing source, due to the current logic and 46728 * expectations built around it. 46729 46730 * Since the timing follows video, in order to maintain sync, the video loader is 46731 * responsible for setting both audio and video source buffer timestamp offsets. 46732 * 46733 * Setting different values for audio and video source buffers could lead to 46734 * desyncing. The following examples demonstrate some of the situations where this 46735 * distinction is important. Note that all of these cases involve demuxed content. When 46736 * content is muxed, the audio and video are packaged together, therefore syncing 46737 * separate media playlists is not an issue. 46738 * 46739 * CASE 1: Audio prepares to load a new timeline before video: 46740 * 46741 * Timeline: 0 1
46742 * Audio Segments: 0 1 2 3 4 5 DISCO 6 7 8 9 46743 * Audio Loader: ^ 46744 * Video Segments: 0 1 2 3 4 5 DISCO 6 7 8 9 46745 * Video Loader ^ 46746 * 46747 * In the above example, the audio loader is preparing to load the 6th segment, the first 46748 * after a discontinuity, while the video loader is still loading the 5th segment, before 46749 * the discontinuity. 46750 * 46751 * If the audio loader goes ahead and loads and appends the 6th segment before the video 46752 * loader crosses the discontinuity, then when appended, the 6th audio segment will use 46753 * the timestamp offset from timeline 0. This will likely lead to desyncing. In addition, 46754 * the audio loader must provide the audioAppendStart value to trim the content in the 46755 * transmuxer, and that value relies on the audio timestamp offset. Since the audio 46756 * timestamp offset is set by the video (main) loader, the audio loader shouldn't load the 46757 * segment until that value is provided. 46758 * 46759 * CASE 2: Video prepares to load a new timeline before audio: 46760 * 46761 * Timeline: 0 1 46762 * Audio Segments: 0 1 2 3 4 5 DISCO 6 7 8 9 46763 * Audio Loader: ^ 46764 * Video Segments: 0 1 2 3 4 5 DISCO 6 7 8 9 46765 * Video Loader ^ 46766 * 46767 * In the above example, the video loader is preparing to load the 6th segment, the first 46768 * after a discontinuity, while the audio loader is still loading the 5th segment, before 46769 * the discontinuity. 46770 * 46771 * If the video loader goes ahead and loads and appends the 6th segment, then once the 46772 * segment is loaded and processed, both the video and audio timestamp offsets will be 46773 * set, since video is used as the primary timing source. This is to ensure content lines 46774 * up appropriately, as any modifications to the video timing are reflected by audio when 46775 * the video loader sets the audio and video timestamp offsets to the same value. However, 46776 * setting the timestamp offset for audio before audio has had a chance to change 46777 * timelines will likely lead to desyncing, as the audio loader will append segment 5 with 46778 * a timestamp intended to apply to segments from timeline 1 rather than timeline 0. 46779 * 46780 * CASE 3: When seeking, audio prepares to load a new timeline before video 46781 * 46782 * Timeline: 0 1 46783 * Audio Segments: 0 1 2 3 4 5 DISCO 6 7 8 9 46784 * Audio Loader: ^ 46785 * Video Segments: 0 1 2 3 4 5 DISCO 6 7 8 9 46786 * Video Loader ^ 46787 * 46788 * In the above example, both audio and video loaders are loading segments from timeline 46789 * 0, but imagine that the seek originated from timeline 1. 46790 * 46791 * When seeking to a new timeline, the timestamp offset will be set based on the expected 46792 * segment start of the loaded video segment. In order to maintain sync, the audio loader 46793 * must wait for the video loader to load its segment and update both the audio and video 46794 * timestamp offsets before it may load and append its own segment. This is the case 46795 * whether the seek results in a mismatched segment request (e.g., the audio loader 46796 * chooses to load segment 3 and the video loader chooses to load segment 4) or the 46797 * loaders choose to load the same segment index from each playlist, as the segments may 46798 * not be aligned perfectly, even for matching segment indexes. 46799 * 46800 * @param {Object} timelinechangeController 46801 * @param {number} currentTimeline 46802 * The timeline currently being followed by the loader 46803 * @param {number} segmentTimeline 46804 * The timeline of the segment being loaded 46805 * @param {('main'|'audio')} loaderType 46806 * The loader type 46807 * @param {boolean} audioDisabled 46808 * Whether the audio is disabled for the loader. This should only be true when the 46809 * loader may have muxed audio in its segment, but should not append it, e.g., for 46810 * the main loader when an alternate audio playlist is active. 46811 * 46812 * @return {boolean} 46813 * Whether the loader should wait for a timeline change from the timeline change 46814 * controller before processing the segment 46815 */ 46816 46817const shouldWaitForTimelineChange = ({ 46818 timelineChangeController, 46819 currentTimeline, 46820 segmentTimeline, 46821 loaderType, 46822 audioDisabled, 46823}) => { 46824 if (currentTimeline === segmentTimeline) { 46825 return false;
46826 } 46827 if (loaderType === 'audio') { 46828 const lastMainTimelineChange = timelineChangeController.lastTimelineChange({ 46829 type: 'main', 46830 }); // Audio loader should wait if: 46831 // 46832 // * main hasn't had a timeline change yet (thus has not loaded its first segment) 46833 // * main hasn't yet changed to the timeline audio is looking to load 46834 46835 return !lastMainTimelineChange || lastMainTimelineChange.to !== segmentTimeline; 46836 } // The main loader only needs to wait for timeline changes if there's demuxed audio. 46837 // Otherwise, there's nothing to wait for, since audio would be muxed into the main 46838 // loader's segments (or the content is audio/video only and handled by the main 46839 // loader). 46840 46841 if (loaderType === 'main' && audioDisabled) { 46842 const pendingAudioTimelineChange = timelineChangeController.pendingTimelineChange({ 46843 type: 'audio', 46844 }); // Main loader should wait for the audio loader if audio is not pending a timeline 46845 // change to the current timeline. 46846 // 46847 // Since the main loader is responsible for setting the timestamp offset for both 46848 // audio and video, the main loader must wait for audio to be about to change to its 46849 // timeline before setting the offset, otherwise, if audio is behind in loading, 46850 // segments from the previous timeline would be adjusted by the new timestamp offset. 46851 // 46852 // This requirement means that video will not cross a timeline until the audio is 46853 // about to cross to it, so that way audio and video will always cross the timeline 46854 // together. 46855 // 46856 // In addition to normal timeline changes, these rules also apply to the start of a 46857 // stream (going from a non-existent timeline, -1, to timeline 0). It's important 46858 // that these rules apply to the first timeline change because if they did not, it's 46859 // possible that the main loader will cross two timelines before the audio loader has 46860 // crossed one. Logic may be implemented to handle the startup as a special case, but 46861 // it's easier to simply treat all timeline changes the same. 46862 46863 if (pendingAudioTimelineChange && pendingAudioTimelineChange.to === segmentTimeline) { 46864 return false; 46865 } 46866 return true; 46867 } 46868 return false; 46869}; 46870const shouldFixBadTimelineChanges = (timelineChangeController) => { 46871 if (!timelineChangeController) { 46872 return false; 46873 } 46874 const pendingAudioTimelineChange = timelineChangeController.pendingTimelineChange({ 46875 type: 'audio', 46876 }); 46877 const pendingMainTimelineChange = timelineChangeController.pendingTimelineChange({ 46878 type: 'main', 46879 }); 46880 const hasPendingTimelineChanges = pendingAudioTimelineChange && pendingMainTimelineChange; 46881 const differentPendingChanges = 46882 hasPendingTimelineChanges && pendingAudioTimelineChange.to !== pendingMainTimelineChange.to; 46883 const isNotInitialPendingTimelineChange = 46884 hasPendingTimelineChanges && 46885 pendingAudioTimelineChange.from !== -1 && 46886 pendingMainTimelineChange.from !== -1; 46887 if (isNotInitialPendingTimelineChange && differentPendingChanges) { 46888 return true; 46889 } 46890 return false; 46891}; 46892/** 46893 * Fixes certain bad timeline scenarios by resetting the loader. 46894 * 46895 * @param {SegmentLoader} segmentLoader 46896 */ 46897 46898const fixBadTimelineChange = (segmentLoader) => { 46899 if (!segmentLoader) { 46900 return; 46901 } 46902 segmentLoader.pause(); 46903 segmentLoader.resetEverything(); 46904 segmentLoader.load(); 46905}; 46906/** 46907 * Check if the pending audio timeline change is behind the 46908 * pending main timeline change. 46909 * 46910 * @param {SegmentLoader} segmentLoader 46911 * @return {boolean} 46912 */ 46913 46914const isAudioTimelineBehind = (segmentLoader) => { 46915 const pendingAudioTimelineChange = segmentLoader.timelineChangeController_.pendingTimelineChange({ 46916 type: 'audio', 46917 }); 46918 const pendingMainTimelineChange = segmentLoader.timelineChangeController_.pendingTimelineChange({ 46919 type: 'main', 46920 }); 46921 const hasPendingTimelineChanges = pendingAudioTimelineChange && pendingMainTimelineChange; 46922 return hasPendingTimelineChanges && pendingAudioTimelineChange.to < pendingMainTimelineChange.to; 46923}; 46924/** 46925 * A method to check if the player is waiting for a timeline change, and fixes 46926 * certain scenarios where the timelines need to be updated. 46927 * 46928 * @param {SegmentLoader} segmentLoader 46929 */ 46930 46931const checkAndFixTimelines = (segmentLoader) => { 46932 const segmentInfo = segmentLoader.pendingSegment_; 46933 if (!segmentInfo) { 46934 return; 46935 } 46936 const waitingForTimelineChange = shouldWaitForTimelineChange({ 46937 timelineChangeController: segmentLoader.timelineChangeController_, 46938 currentTimeline: segmentLoader.currentTimeline_, 46939 segmentTimeline: segmentInfo.timeline, 46940 loaderType: segmentLoader.loaderType_, 46941 audioDisabled: segmentLoader.audioDisabled_, 46942 }); 46943 if ( 46944 waitingForTimelineChange && 46945 shouldFixBadTimelineChanges(segmentLoader.timelineChangeController_) 46946 ) { 46947 if (isAudioTimelineBehind(segmentLoader)) { 46948 segmentLoader.timelineChangeController_.trigger('audioTimelineBehind'); 46949 return; 46950 } 46951 fixBadTimelineChange(segmentLoader); 46952 } 46953}; 46954const mediaDuration = (timingInfos) => { 46955 let maxDuration = 0; 46956 ['video', 'audio'].forEach(function (type) { 46957 const typeTimingInfo = timingInfos[`${type}TimingInfo`]; 46958 if (!typeTimingInfo) { 46959 return; 46960 } 46961 const { start, end } = typeTimingInfo; 46962 let duration; 46963 if (typeof start === 'bigint' || typeof end === 'bigint') { 46964 duration = window__default['default'].BigInt(end) - window__default['default'].BigInt(start); 46965 } else if (typeof start === 'number' && typeof end === 'number') { 46966 duration = end - start; 46967 } 46968 if (typeof duration !== 'undefined' && duration > maxDuration) { 46969 maxDuration = duration; 46970 } 46971 }); // convert back to a number if it is lower than MAX_SAFE_INTEGER 46972 // as we only need BigInt when we are above that. 46973 46974 if (typeof maxDuration === 'bigint' && maxDuration < Number.MAX_SAFE_INTEGER) { 46975 maxDuration = Number(maxDuration); 46976 } 46977 return maxDuration; 46978}; 46979const segmentTooLong = ({ segmentDuration, maxDuration }) => { 46980 // 0 duration segments are most likely due to metadata only segments or a lack of 46981 // information. 46982 if (!segmentDuration) { 46983 return false;
46984 } // For HLS: 46985 // 46986 // https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.3.1 46987 // The EXTINF duration of each Media Segment in the Playlist 46988 // file, when rounded to the nearest integer, MUST be less than or equal 46989 // to the target duration; longer segments can trigger playback stalls 46990 // or other errors. 46991 // 46992 // For DASH, the mpd-parser uses the largest reported segment duration as the target 46993 // duration. Although that reported duration is occasionally approximate (i.e., not 46994 // exact), a strict check may report that a segment is too long more often in DASH. 46995 46996 return Math.round(segmentDuration) > maxDuration + TIME_FUDGE_FACTOR; 46997}; 46998const getTroublesomeSegmentDurationMessage = (segmentInfo, sourceType) => { 46999 // Right now we aren't following DASH's timing model exactly, so only perform 47000 // this check for HLS content. 47001 if (sourceType !== 'hls') { 47002 return null; 47003 } 47004 const segmentDuration = mediaDuration({ 47005 audioTimingInfo: segmentInfo.audioTimingInfo, 47006 videoTimingInfo: segmentInfo.videoTimingInfo, 47007 }); // Don't report if we lack information. 47008 // 47009 // If the segment has a duration of 0 it is either a lack of information or a 47010 // metadata only segment and shouldn't be reported here. 47011 47012 if (!segmentDuration) { 47013 return null; 47014 } 47015 const targetDuration = segmentInfo.playlist.targetDuration; 47016 const isSegmentWayTooLong = segmentTooLong({ 47017 segmentDuration, 47018 maxDuration: targetDuration * 2, 47019 }); 47020 const isSegmentSlightlyTooLong = segmentTooLong({ 47021 segmentDuration, 47022 maxDuration: targetDuration, 47023 }); 47024 const segmentTooLongMessage = 47025 `Segment with index ${segmentInfo.mediaIndex} ` + 47026 `from playlist ${segmentInfo.playlist.id} ` + 47027 `has a duration of ${segmentDuration} ` + 47028 `when the reported duration is ${segmentInfo.duration} ` + 47029 `and the target duration is ${targetDuration}. ` + 47030 'For HLS content, a duration in excess of the target duration may result in ' + 47031 'playback issues. See the HLS specification section on EXT-X-TARGETDURATION for ' + 47032 'more details: ' + 47033 'https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.3.1'; 47034 if (isSegmentWayTooLong || isSegmentSlightlyTooLong) { 47035 return { 47036 severity: isSegmentWayTooLong ? 'warn' : 'info', 47037 message: segmentTooLongMessage, 47038 }; 47039 } 47040 return null; 47041}; 47042/** 47043 * 47044 * @param {Object} options type of segment loader and segment either segmentInfo or simple segment 47045 * @return a segmentInfo payload for events or errors. 47046 */ 47047 47048const segmentInfoPayload = ({ type, segment }) => { 47049 if (!segment) { 47050 return; 47051 } 47052 const isEncrypted = Boolean(segment.key || (segment.map && segment.map.ke)); 47053 const isMediaInitialization = Boolean(segment.map && !segment.map.bytes); 47054 const start = segment.startOfSegment === undefined ? segment.start : segment.startOfSegment; 47055 return { 47056 type: type || segment.type, 47057 uri: segment.resolvedUri || segment.uri, 47058 start, 47059 duration: segment.duration, 47060 isEncrypted, 47061 isMediaInitialization, 47062 }; 47063}; 47064/** 47065 * An object that manages segment loading and appending. 47066 * 47067 * @class SegmentLoader 47068 * @param {Object} options required and optional options 47069 * @extends videojs.EventTarget 47070 */ 47071 47072class SegmentLoader extends videojs.EventTarget { 47073 constructor(settings, options = {}) { 47074 super(); // check pre-conditions 47075 47076 if (!settings) { 47077 throw new TypeError('Initialization settings are required'); 47078 } 47079 if (typeof settings.currentTime !== 'function') { 47080 throw new TypeError('No currentTime getter specified'); 47081 } 47082 if (!settings.mediaSource) { 47083 throw new TypeError('No MediaSource specified'); 47084 } // public properties 47085 47086 this.bandwidth = settings.bandwidth; 47087 this.throughput = { 47088 rate: 0, 47089 count: 0, 47090 }; 47091 this.roundTrip = NaN; 47092 this.resetStats_(); 47093 this.mediaIndex = null; 47094 this.partIndex = null; // private settings 47095 47096 this.hasPlayed_ = settings.hasPlayed; 47097 this.currentTime_ = settings.currentTime; 47098 this.seekable_ = settings.seekable; 47099 this.seeking_ = settings.seeking; 47100 this.duration_ = settings.duration; 47101 this.mediaSource_ = settings.mediaSource; 47102 this.vhs_ = settings.vhs; 47103 this.loaderType_ = settings.loaderType; 47104 this.currentMediaInfo_ = void 0; 47105 this.startingMediaInfo_ = void 0; 47106 this.segmentMetadataTrack_ = settings.segmentMetadataTrack; 47107 this.goalBufferLength_ = settings.goalBufferLength; 47108 this.sourceType_ = settings.sourceType; 47109 this.sourceUpdater_ = settings.sourceUpdater; 47110 this.inbandTextTracks_ = settings.inbandTextTracks; 47111 this.state_ = 'INIT'; 47112 this.timelineChangeController_ = settings.timelineChangeController; 47113 this.shouldSaveSegmentTimingInfo_ = true; 47114 this.parse708captions_ = settings.parse708captions; 47115 this.useDtsForTimestampOffset_ = settings.useDtsForTimestampOffset; 47116 this.captionServices_ = settings.captionServices; 47117 this.exactManifestTimings = settings.exactManifestTimings; 47118 this.addMetadataToTextTrack = settings.addMetadataToTextTrack; // private instance variables 47119 47120 this.checkBufferTimeout_ = null; 47121 this.error_ = void 0; 47122 this.currentTimeline_ = -1; 47123 this.shouldForceTimestampOffsetAfterResync_ = false;
47124 this.pendingSegment_ = null; 47125 this.xhrOptions_ = null; 47126 this.pendingSegments_ = []; 47127 this.audioDisabled_ = false; 47128 this.isPendingTimestampOffset_ = false; // TODO possibly move gopBuffer and timeMapping info to a separate controller 47129 47130 this.gopBuffer_ = []; 47131 this.timeMapping_ = 0; 47132 this.safeAppend_ = false; 47133 this.appendInitSegment_ = { 47134 audio: true, 47135 video: true, 47136 }; 47137 this.playlistOfLastInitSegment_ = { 47138 audio: null, 47139 video: null, 47140 }; 47141 this.callQueue_ = []; // If the segment loader prepares to load a segment, but does not have enough 47142 // information yet to start the loading process (e.g., if the audio loader wants to 47143 // load a segment from the next timeline but the main loader hasn't yet crossed that 47144 // timeline), then the load call will be added to the queue until it is ready to be 47145 // processed. 47146 47147 this.loadQueue_ = []; 47148 this.metadataQueue_ = { 47149 id3: [], 47150 caption: [], 47151 }; 47152 this.waitingOnRemove_ = false; 47153 this.quotaExceededErrorRetryTimeout_ = null; // Fragmented mp4 playback 47154 47155 this.activeInitSegmentId_ = null; 47156 this.initSegments_ = {}; // HLSe playback 47157 47158 this.cacheEncryptionKeys_ = settings.cacheEncryptionKeys; 47159 this.keyCache_ = {}; 47160 this.decrypter_ = settings.decrypter; // Manages the tracking and generation of sync-points, mappings 47161 // between a time in the display time and a segment index within 47162 // a playlist 47163 47164 this.syncController_ = settings.syncController; 47165 this.syncPoint_ = { 47166 segmentIndex: 0, 47167 time: 0, 47168 }; 47169 this.transmuxer_ = this.createTransmuxer_(); 47170 this.triggerSyncInfoUpdate_ = () => this.trigger('syncinfoupdate'); 47171 this.syncController_.on('syncinfoupdate', this.triggerSyncInfoUpdate_); 47172 this.mediaSource_.addEventListener('sourceopen', () => { 47173 if (!this.isEndOfStream_()) { 47174 this.ended_ = false; 47175 } 47176 }); // ...for determining the fetch location 47177 47178 this.fetchAtBuffer_ = false; 47179 this.logger_ = logger(`SegmentLoader[${this.loaderType_}]`); 47180 Object.defineProperty(this, 'state', { 47181 get() { 47182 return this.state_; 47183 }, 47184 set(newState) { 47185 if (newState !== this.state_) { 47186 this.logger_(`${this.state_} -> ${newState}`); 47187 this.state_ = newState; 47188 this.trigger('statechange'); 47189 } 47190 }, 47191 }); 47192 this.sourceUpdater_.on('ready', () => { 47193 if (this.hasEnoughInfoToAppend_()) { 47194 this.processCallQueue_(); 47195 } else { 47196 checkAndFixTimelines(this); 47197 } 47198 }); 47199 this.sourceUpdater_.on('codecschange', (metadata) => { 47200 this.trigger( 47201 _extends__default['default']( 47202 { 47203 type: 'codecschange', 47204 }, 47205 metadata 47206 ) 47207 ); 47208 }); // Only the main loader needs to listen for pending timeline changes, as the main 47209 // loader should wait for audio to be ready to change its timeline so that both main 47210 // and audio timelines change together. For more details, see the 47211 // shouldWaitForTimelineChange function. 47212 47213 if (this.loaderType_ === 'main') { 47214 this.timelineChangeController_.on('pendingtimelinechange', () => { 47215 if (this.hasEnoughInfoToAppend_()) { 47216 this.processCallQueue_(); 47217 } else { 47218 checkAndFixTimelines(this); 47219 } 47220 }); 47221 } // The main loader only listens on pending timeline changes, but the audio loader, 47222 // since its loads follow main, needs to listen on timeline changes. For more details, 47223 // see the shouldWaitForTimelineChange function. 47224 47225 if (this.loaderType_ === 'audio') { 47226 this.timelineChangeController_.on('timelinechange', (metadata) => { 47227 this.trigger( 47228 _extends__default['default']( 47229 { 47230 type: 'timelinechange', 47231 }, 47232 metadata 47233 ) 47234 ); 47235 if (this.hasEnoughInfoToLoad_()) { 47236 this.processLoadQueue_(); 47237 } else { 47238 checkAndFixTimelines(this); 47239 } 47240 if (this.hasEnoughInfoToAppend_()) { 47241 this.processCallQueue_(); 47242 } else { 47243 checkAndFixTimelines(this); 47244 } 47245 }); 47246 } 47247 } 47248 /** 47249 * TODO: Current sync controller consists of many hls-specific strategies 47250 * media sequence sync is also hls-specific, and we would like to be protocol-agnostic on this level 47251 * this should be a part of the sync-controller and sync controller should expect different strategy list based on the protocol. 47252 * 47253 * @return {MediaSequenceSync|null} 47254 * @private 47255 */ 47256 47257 get mediaSequenceSync_() { 47258 return this.syncController_.getMediaSequenceSync(this.loaderType_); 47259 } 47260 createTransmuxer_() { 47261 return segmentTransmuxer.createTransmuxer({ 47262 remux: false, 47263 alignGopsAtEnd: this.safeAppend_, 47264 keepOriginalTimestamps: true, 47265 parse708captions: this.parse708captions_, 47266 captionServices: this.captionServices_, 47267 }); 47268 } 47269 /** 47270 * reset all of our media stats 47271 * 47272 * @private 47273 */ 47274 47275 resetStats_() { 47276 this.mediaBytesTransferred = 0; 47277 this.mediaRequests = 0; 47278 this.mediaRequestsAborted = 0; 47279 this.mediaRequestsTimedout = 0; 47280 this.mediaRequestsErrored = 0; 47281 this.mediaTransferDuration = 0; 47282 this.mediaSecondsLoaded = 0; 47283 this.mediaAppends = 0; 47284 } 47285 /** 47286 * dispose of the SegmentLoader and reset to the default state 47287 */ 47288 47289 dispose() { 47290 this.trigger('dispose'); 47291 this.state = 'DISPOSED'; 47292 this.pause(); 47293 this.abort_(); 47294 if (this.transmuxer_) { 47295 this.transmuxer_.terminate(); 47296 } 47297 this.resetStats_(); 47298 if (this.checkBufferTimeout_) { 47299 window__default['default'].clearTimeout(this.checkBufferTimeout_); 47300 } 47301 if (this.syncController_ && this.triggerSyncInfoUpdate_) { 47302 this.syncController_.off('syncinfoupdate', this.triggerSyncInfoUpdate_); 47303 } 47304 this.off(); 47305 } 47306 setAudio(enable) { 47307 this.audioDisabled_ = !enable; 47308 if (enable) { 47309 this.appendInitSegment_.audio = true; 47310 } else { 47311 // remove current track audio if it gets disabled 47312 this.sourceUpdater_.removeAudio(0, this.duration_()); 47313 } 47314 } 47315 /** 47316 * abort anything that is currently doing on with the SegmentLoader 47317 * and reset to a default state 47318 */ 47319 47320 abort() { 47321 if (this.state !== 'WAITING') { 47322 if (this.pendingSegment_) { 47323 this.pendingSegment_ = null; 47324 } 47325 return; 47326 } 47327 this.abort_(); // We aborted the requests we were waiting on, so reset the loader's state to READY 47328 // since we are no longer "waiting" on any requests. XHR callback is not always run 47329 // when the request is aborted. This will prevent the loader from being stuck in the 47330 // WAITING state indefinitely. 47331 47332 this.state = 'READY'; // don't wait for buffer check timeouts to begin fetching the 47333 // next segment 47334 47335 if (!this.paused()) { 47336 this.monitorBuffer_(); 47337 } 47338 } 47339 /**
vendor: 13,095 bytes, lines 47340-47704
47340 * abort all pending xhr requests and null any pending segements 47341 * 47342 * @private 47343 */ 47344 47345 abort_() { 47346 if (this.pendingSegment_ && this.pendingSegment_.abortRequests) { 47347 this.pendingSegment_.abortRequests(); 47348 } // clear out the segment being processed 47349 47350 this.pendingSegment_ = null; 47351 this.callQueue_ = []; 47352 this.loadQueue_ = []; 47353 this.metadataQueue_.id3 = []; 47354 this.metadataQueue_.caption = []; 47355 this.timelineChangeController_.clearPendingTimelineChange(this.loaderType_); 47356 this.waitingOnRemove_ = false; 47357 window__default['default'].clearTimeout(this.quotaExceededErrorRetryTimeout_); 47358 this.quotaExceededErrorRetryTimeout_ = null; 47359 } 47360 checkForAbort_(requestId) { 47361 // If the state is APPENDING, then aborts will not modify the state, meaning the first 47362 // callback that happens should reset the state to READY so that loading can continue. 47363 if (this.state === 'APPENDING' && !this.pendingSegment_) { 47364 this.state = 'READY'; 47365 return true; 47366 } 47367 if (!this.pendingSegment_ || this.pendingSegment_.requestId !== requestId) { 47368 return true; 47369 } 47370 return false; 47371 } 47372 /** 47373 * set an error on the segment loader and null out any pending segements 47374 * 47375 * @param {Error} error the error to set on the SegmentLoader 47376 * @return {Error} the error that was set or that is currently set 47377 */ 47378 47379 error(error) { 47380 if (typeof error !== 'undefined') { 47381 this.logger_('error occurred:', error); 47382 this.error_ = error; 47383 } 47384 this.pendingSegment_ = null; 47385 return this.error_; 47386 } 47387 endOfStream() { 47388 this.ended_ = true; 47389 if (this.transmuxer_) { 47390 // need to clear out any cached data to prepare for the new segment 47391 segmentTransmuxer.reset(this.transmuxer_); 47392 } 47393 this.gopBuffer_.length = 0; 47394 this.pause(); 47395 this.trigger('ended'); 47396 } 47397 /** 47398 * Indicates which time ranges are buffered 47399 * 47400 * @return {TimeRange} 47401 * TimeRange object representing the current buffered ranges 47402 */ 47403 47404 buffered_() { 47405 const trackInfo = this.getMediaInfo_(); 47406 if (!this.sourceUpdater_ || !trackInfo) { 47407 return createTimeRanges(); 47408 } 47409 if (this.loaderType_ === 'main') { 47410 const { hasAudio, hasVideo, isMuxed } = trackInfo; 47411 if (hasVideo && hasAudio && !this.audioDisabled_ && !isMuxed) { 47412 return this.sourceUpdater_.buffered(); 47413 } 47414 if (hasVideo) { 47415 return this.sourceUpdater_.videoBuffered(); 47416 } 47417 } // One case that can be ignored for now is audio only with alt audio, 47418 // as we don't yet have proper support for that. 47419 47420 return this.sourceUpdater_.audioBuffered(); 47421 } 47422 /** 47423 * Gets and sets init segment for the provided map 47424 * 47425 * @param {Object} map 47426 * The map object representing the init segment to get or set 47427 * @param {boolean=} set 47428 * If true, the init segment for the provided map should be saved 47429 * @return {Object} 47430 * map object for desired init segment 47431 */ 47432 47433 initSegmentForMap(map, set = false) { 47434 if (!map) { 47435 return null; 47436 } 47437 const id = initSegmentId(map); 47438 let storedMap = this.initSegments_[id]; 47439 if (set && !storedMap && map.bytes) { 47440 this.initSegments_[id] = storedMap = { 47441 resolvedUri: map.resolvedUri, 47442 byterange: map.byterange, 47443 bytes: map.bytes, 47444 tracks: map.tracks, 47445 timescales: map.timescales, 47446 }; 47447 } 47448 return storedMap || map; 47449 } 47450 /** 47451 * Gets and sets key for the provided key 47452 * 47453 * @param {Object} key 47454 * The key object representing the key to get or set 47455 * @param {boolean=} set 47456 * If true, the key for the provided key should be saved 47457 * @return {Object} 47458 * Key object for desired key 47459 */ 47460 47461 segmentKey(key, set = false) { 47462 if (!key) { 47463 return null; 47464 } 47465 const id = segmentKeyId(key); 47466 let storedKey = this.keyCache_[id]; // TODO: We should use the HTTP Expires header to invalidate our cache per 47467 // https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-6.2.3 47468 47469 if (this.cacheEncryptionKeys_ && set && !storedKey && key.bytes) { 47470 this.keyCache_[id] = storedKey = { 47471 resolvedUri: key.resolvedUri, 47472 bytes: key.bytes, 47473 }; 47474 } 47475 const result = { 47476 resolvedUri: (storedKey || key).resolvedUri, 47477 }; 47478 if (storedKey) { 47479 result.bytes = storedKey.bytes; 47480 } 47481 return result; 47482 } 47483 /** 47484 * Returns true if all configuration required for loading is present, otherwise false. 47485 * 47486 * @return {boolean} True if the all configuration is ready for loading 47487 * @private 47488 */ 47489 47490 couldBeginLoading_() { 47491 return this.playlist_ && !this.paused(); 47492 } 47493 /** 47494 * load a playlist and start to fill the buffer 47495 */ 47496 47497 load() { 47498 // un-pause 47499 this.monitorBuffer_(); // if we don't have a playlist yet, keep waiting for one to be 47500 // specified 47501 47502 if (!this.playlist_) { 47503 return; 47504 } // if all the configuration is ready, initialize and begin loading 47505 47506 if (this.state === 'INIT' && this.couldBeginLoading_()) { 47507 return this.init_(); 47508 } // if we're in the middle of processing a segment already, don't 47509 // kick off an additional segment request 47510 47511 if (!this.couldBeginLoading_() || (this.state !== 'READY' && this.state !== 'INIT')) { 47512 return; 47513 } 47514 this.state = 'READY'; 47515 } 47516 /** 47517 * Once all the starting parameters have been specified, begin 47518 * operation. This method should only be invoked from the INIT 47519 * state. 47520 * 47521 * @private 47522 */ 47523 47524 init_() { 47525 this.state = 'READY'; // if this is the audio segment loader, and it hasn't been inited before, then any old 47526 // audio data from the muxed content should be removed 47527 47528 this.resetEverything(); 47529 return this.monitorBuffer_(); 47530 } 47531 /** 47532 * set a playlist on the segment loader 47533 * 47534 * @param {PlaylistLoader} media the playlist to set on the segment loader 47535 */ 47536 47537 playlist(newPlaylist, options = {}) { 47538 if (!newPlaylist) { 47539 return; 47540 } 47541 const oldPlaylist = this.playlist_; 47542 const segmentInfo = this.pendingSegment_; 47543 this.playlist_ = newPlaylist; 47544 this.xhrOptions_ = options; // when we haven't started playing yet, the start of a live playlist 47545 // is always our zero-time so force a sync update each time the playlist 47546 // is refreshed from the server 47547 // 47548 // Use the INIT state to determine if playback has started, as the playlist sync info 47549 // should be fixed once requests begin (as sync points are generated based on sync 47550 // info), but not before then. 47551 47552 if (this.state === 'INIT') { 47553 newPlaylist.syncInfo = { 47554 mediaSequence: newPlaylist.mediaSequence, 47555 time: 0, 47556 }; // Setting the date time mapping means mapping the program date time (if available) 47557 // to time 0 on the player's timeline. The playlist's syncInfo serves a similar 47558 // purpose, mapping the initial mediaSequence to time zero. Since the syncInfo can 47559 // be updated as the playlist is refreshed before the loader starts loading, the 47560 // program date time mapping needs to be updated as well. 47561 // 47562 // This mapping is only done for the main loader because a program date time should 47563 // map equivalently between playlists. 47564 47565 if (this.loaderType_ === 'main') { 47566 this.syncController_.setDateTimeMappingForStart(newPlaylist); 47567 } 47568 } 47569 let oldId = null; 47570 if (oldPlaylist) { 47571 if (oldPlaylist.id) { 47572 oldId = oldPlaylist.id; 47573 } else if (oldPlaylist.uri) { 47574 oldId = oldPlaylist.uri; 47575 } 47576 } 47577 this.logger_(`playlist update [${oldId} => ${newPlaylist.id || newPlaylist.uri}]`); 47578 if (this.mediaSequenceSync_) { 47579 this.mediaSequenceSync_.update(newPlaylist, this.currentTime_()); 47580 this.logger_( 47581 `Playlist update: 47582currentTime: ${this.currentTime_()} 47583bufferedEnd: ${lastBufferedEnd(this.buffered_())} 47584`, 47585 this.mediaSequenceSync_.diagnostics 47586 ); 47587 } // in VOD, this is always a rendition switch (or we updated our syncInfo above) 47588 // in LIVE, we always want to update with new playlists (including refreshes) 47589 47590 this.trigger('syncinfoupdate'); // if we were unpaused but waiting for a playlist, start 47591 // buffering now 47592 47593 if (this.state === 'INIT' && this.couldBeginLoading_()) { 47594 return this.init_(); 47595 } 47596 if (!oldPlaylist || oldPlaylist.uri !== newPlaylist.uri) { 47597 if (this.mediaIndex !== null) { 47598 // we must reset/resync the segment loader when we switch renditions and 47599 // the segment loader is already synced to the previous rendition 47600 // We only want to reset the loader here for LLHLS playback, as resetLoader sets fetchAtBuffer_ 47601 // to false, resulting in fetching segments at currentTime and causing repeated 47602 // same-segment requests on playlist change. This erroneously drives up the playback watcher 47603 // stalled segment count, as re-requesting segments at the currentTime or browser cached segments 47604 // will not change the buffer. 47605 // Reference for LLHLS fixes: https://github.com/videojs/http-streaming/pull/1201 47606 const isLLHLS = !newPlaylist.endList && typeof newPlaylist.partTargetDuration === 'number'; 47607 if (isLLHLS) { 47608 this.resetLoader(); 47609 } else { 47610 this.resyncLoader(); 47611 } 47612 } 47613 this.currentMediaInfo_ = void 0; 47614 this.trigger('playlistupdate'); // the rest of this function depends on `oldPlaylist` being defined 47615 47616 return; 47617 } // we reloaded the same playlist so we are in a live scenario 47618 // and we will likely need to adjust the mediaIndex 47619 47620 const mediaSequenceDiff = newPlaylist.mediaSequence - oldPlaylist.mediaSequence; 47621 this.logger_(`live window shift [${mediaSequenceDiff}]`); // update the mediaIndex on the SegmentLoader 47622 // this is important because we can abort a request and this value must be 47623 // equal to the last appended mediaIndex 47624 47625 if (this.mediaIndex !== null) { 47626 this.mediaIndex -= mediaSequenceDiff; // this can happen if we are going to load the first segment, but get a playlist 47627 // update during that. mediaIndex would go from 0 to -1 if mediaSequence in the 47628 // new playlist was incremented by 1. 47629 47630 if (this.mediaIndex < 0) { 47631 this.mediaIndex = null; 47632 this.partIndex = null; 47633 } else { 47634 const segment = this.playlist_.segments[this.mediaIndex]; // partIndex should remain the same for the same segment 47635 // unless parts fell off of the playlist for this segment. 47636 // In that case we need to reset partIndex and resync 47637 47638 if ( 47639 this.partIndex && 47640 (!segment.parts || !segment.parts.length || !segment.parts[this.partIndex]) 47641 ) { 47642 const mediaIndex = this.mediaIndex; 47643 this.logger_(`currently processing part (index ${this.partIndex}) no longer exists.`); 47644 this.resetLoader(); // We want to throw away the partIndex and the data associated with it, 47645 // as the part was dropped from our current playlists segment. 47646 // The mediaIndex will still be valid so keep that around. 47647 47648 this.mediaIndex = mediaIndex; 47649 } 47650 } 47651 } // update the mediaIndex on the SegmentInfo object 47652 // this is important because we will update this.mediaIndex with this value 47653 // in `handleAppendsDone_` after the segment has been successfully appended 47654 47655 if (segmentInfo) { 47656 segmentInfo.mediaIndex -= mediaSequenceDiff; 47657 if (segmentInfo.mediaIndex < 0) { 47658 segmentInfo.mediaIndex = null; 47659 segmentInfo.partIndex = null; 47660 } else { 47661 // we need to update the referenced segment so that timing information is 47662 // saved for the new playlist's segment, however, if the segment fell off the 47663 // playlist, we can leave the old reference and just lose the timing info 47664 if (segmentInfo.mediaIndex >= 0) { 47665 segmentInfo.segment = newPlaylist.segments[segmentInfo.mediaIndex]; 47666 } 47667 if (segmentInfo.partIndex >= 0 && segmentInfo.segment.parts) { 47668 segmentInfo.part = segmentInfo.segment.parts[segmentInfo.partIndex]; 47669 } 47670 } 47671 } 47672 this.syncController_.saveExpiredSegmentInfo(oldPlaylist, newPlaylist); 47673 } 47674 /** 47675 * Prevent the loader from fetching additional segments. If there 47676 * is a segment request outstanding, it will finish processing 47677 * before the loader halts. A segment loader can be unpaused by 47678 * calling load(). 47679 */ 47680 47681 pause() { 47682 if (this.checkBufferTimeout_) { 47683 window__default['default'].clearTimeout(this.checkBufferTimeout_); 47684 this.checkBufferTimeout_ = null; 47685 } 47686 } 47687 /** 47688 * Returns whether the segment loader is fetching additional 47689 * segments when given the opportunity. This property can be 47690 * modified through calls to pause() and load(). 47691 */ 47692 47693 paused() { 47694 return this.checkBufferTimeout_ === null; 47695 } 47696 /** 47697 * Delete all the buffered data and reset the SegmentLoader 47698 * 47699 * @param {Function} [done] an optional callback to be executed when the remove 47700 * operation is complete 47701 */ 47702 47703 resetEverything(done) { 47704 this.ended_ = false;
vendor: 4,091 bytes, lines 47705-47817
47705 this.activeInitSegmentId_ = null; 47706 this.appendInitSegment_ = { 47707 audio: true, 47708 video: true, 47709 }; 47710 this.resetLoader(); // remove from 0, the earliest point, to Infinity, to signify removal of everything. 47711 // VTT Segment Loader doesn't need to do anything but in the regular SegmentLoader, 47712 // we then clamp the value to duration if necessary. 47713 47714 this.remove(0, Infinity, done); // clears fmp4 captions 47715 47716 if (this.transmuxer_) { 47717 this.transmuxer_.postMessage({ 47718 action: 'clearAllMp4Captions', 47719 }); // reset the cache in the transmuxer 47720 47721 this.transmuxer_.postMessage({ 47722 action: 'reset', 47723 }); 47724 } 47725 } 47726 /** 47727 * Force the SegmentLoader to resync and start loading around the currentTime instead 47728 * of starting at the end of the buffer 47729 * 47730 * Useful for fast quality changes 47731 */ 47732 47733 resetLoader() { 47734 this.fetchAtBuffer_ = false; 47735 if (this.mediaSequenceSync_) { 47736 this.mediaSequenceSync_.resetAppendedStatus(); 47737 } 47738 this.resyncLoader(); 47739 } 47740 /** 47741 * Force the SegmentLoader to restart synchronization and make a conservative guess 47742 * before returning to the simple walk-forward method 47743 */ 47744 47745 resyncLoader() { 47746 if (this.transmuxer_) { 47747 // need to clear out any cached data to prepare for the new segment 47748 segmentTransmuxer.reset(this.transmuxer_); 47749 } 47750 this.mediaIndex = null; 47751 this.partIndex = null; 47752 this.syncPoint_ = null; 47753 this.isPendingTimestampOffset_ = false; // this is mainly to sync timing-info when switching between renditions with and without timestamp-rollover, 47754 // so we don't want it for DASH or fragmented mp4 segments. 47755 47756 const isFmp4 = this.currentMediaInfo_ && this.currentMediaInfo_.isFmp4; 47757 const isHlsTs = this.sourceType_ === 'hls' && !isFmp4; 47758 if (isHlsTs) { 47759 this.shouldForceTimestampOffsetAfterResync_ = true; 47760 } 47761 this.callQueue_ = []; 47762 this.loadQueue_ = []; 47763 this.metadataQueue_.id3 = []; 47764 this.metadataQueue_.caption = []; 47765 this.abort(); 47766 if (this.transmuxer_) { 47767 this.transmuxer_.postMessage({ 47768 action: 'clearParsedMp4Captions', 47769 }); 47770 } 47771 } 47772 /** 47773 * Remove any data in the source buffer between start and end times 47774 * 47775 * @param {number} start - the start time of the region to remove from the buffer 47776 * @param {number} end - the end time of the region to remove from the buffer 47777 * @param {Function} [done] - an optional callback to be executed when the remove 47778 * @param {boolean} force - force all remove operations to happen 47779 * operation is complete 47780 */ 47781 47782 remove(start, end, done = () => {}, force = false) { 47783 // clamp end to duration if we need to remove everything. 47784 // This is due to a browser bug that causes issues if we remove to Infinity. 47785 // videojs/videojs-contrib-hls#1225 47786 if (end === Infinity) { 47787 end = this.duration_(); 47788 } // skip removes that would throw an error 47789 // commonly happens during a rendition switch at the start of a video 47790 // from start 0 to end 0 47791 47792 if (end <= start) { 47793 this.logger_('skipping remove because end ${end} is <= start ${start}'); 47794 return; 47795 } 47796 if (!this.sourceUpdater_ || !this.getMediaInfo_()) { 47797 this.logger_('skipping remove because no source updater or starting media info'); // nothing to remove if we haven't processed any media 47798 47799 return; 47800 } // set it to one to complete this function's removes 47801 47802 let removesRemaining = 1; 47803 const removeFinished = () => { 47804 removesRemaining--; 47805 if (removesRemaining === 0) { 47806 done(); 47807 } 47808 }; 47809 if (force || !this.audioDisabled_) { 47810 removesRemaining++; 47811 this.sourceUpdater_.removeAudio(start, end, removeFinished); 47812 } // While it would be better to only remove video if the main loader has video, this 47813 // should be safe with audio only as removeVideo will call back even if there's no 47814 // video buffer. 47815 // 47816 // In theory we can check to see if there's video before calling the remove, but in 47817 // the event that we're switching between renditions and from video to audio only
vendor: 6,466 bytes, lines 47818-48009
47818 // (when we add support for that), we may need to clear the video contents despite 47819 // what the new media will contain. 47820 47821 if (force || this.loaderType_ === 'main') { 47822 this.gopBuffer_ = removeGopBuffer(this.gopBuffer_, start, end, this.timeMapping_); 47823 removesRemaining++; 47824 this.sourceUpdater_.removeVideo(start, end, removeFinished); 47825 } // remove any captions and ID3 tags 47826 47827 for (const track in this.inbandTextTracks_) { 47828 removeCuesFromTrack(start, end, this.inbandTextTracks_[track]); 47829 } 47830 removeCuesFromTrack(start, end, this.segmentMetadataTrack_); // finished this function's removes 47831 47832 removeFinished(); 47833 } 47834 /** 47835 * (re-)schedule monitorBufferTick_ to run as soon as possible 47836 * 47837 * @private 47838 */ 47839 47840 monitorBuffer_() { 47841 if (this.checkBufferTimeout_) { 47842 window__default['default'].clearTimeout(this.checkBufferTimeout_); 47843 } 47844 this.checkBufferTimeout_ = window__default['default'].setTimeout( 47845 this.monitorBufferTick_.bind(this), 47846 1 47847 ); 47848 } 47849 /** 47850 * As long as the SegmentLoader is in the READY state, periodically 47851 * invoke fillBuffer_(). 47852 * 47853 * @private 47854 */ 47855 47856 monitorBufferTick_() { 47857 if (this.state === 'READY') { 47858 this.fillBuffer_(); 47859 } 47860 if (this.checkBufferTimeout_) { 47861 window__default['default'].clearTimeout(this.checkBufferTimeout_); 47862 } 47863 this.checkBufferTimeout_ = window__default['default'].setTimeout( 47864 this.monitorBufferTick_.bind(this), 47865 CHECK_BUFFER_DELAY 47866 ); 47867 } 47868 /** 47869 * fill the buffer with segements unless the sourceBuffers are 47870 * currently updating 47871 * 47872 * Note: this function should only ever be called by monitorBuffer_ 47873 * and never directly 47874 * 47875 * @private 47876 */ 47877 47878 fillBuffer_() { 47879 // TODO since the source buffer maintains a queue, and we shouldn't call this function 47880 // except when we're ready for the next segment, this check can most likely be removed 47881 if (this.sourceUpdater_.updating()) { 47882 return; 47883 } // see if we need to begin loading immediately 47884 47885 const segmentInfo = this.chooseNextRequest_(); 47886 if (!segmentInfo) { 47887 return; 47888 } 47889 const metadata = { 47890 segmentInfo: segmentInfoPayload({ 47891 type: this.loaderType_, 47892 segment: segmentInfo, 47893 }), 47894 }; 47895 this.trigger({ 47896 type: 'segmentselected', 47897 metadata, 47898 }); 47899 if (typeof segmentInfo.timestampOffset === 'number') { 47900 this.isPendingTimestampOffset_ = false; 47901 this.timelineChangeController_.pendingTimelineChange({ 47902 type: this.loaderType_, 47903 from: this.currentTimeline_, 47904 to: segmentInfo.timeline, 47905 }); 47906 } 47907 this.loadSegment_(segmentInfo); 47908 } 47909 /** 47910 * Determines if we should call endOfStream on the media source based 47911 * on the state of the buffer or if appened segment was the final 47912 * segment in the playlist. 47913 * 47914 * @param {number} [mediaIndex] the media index of segment we last appended 47915 * @param {Object} [playlist] a media playlist object 47916 * @return {boolean} do we need to call endOfStream on the MediaSource 47917 */ 47918 47919 isEndOfStream_( 47920 mediaIndex = this.mediaIndex, 47921 playlist = this.playlist_, 47922 partIndex = this.partIndex 47923 ) { 47924 if (!playlist || !this.mediaSource_) { 47925 return false; 47926 } 47927 const segment = typeof mediaIndex === 'number' && playlist.segments[mediaIndex]; // mediaIndex is zero based but length is 1 based 47928 47929 const appendedLastSegment = mediaIndex + 1 === playlist.segments.length; // true if there are no parts, or this is the last part. 47930 47931 const appendedLastPart = !segment || !segment.parts || partIndex + 1 === segment.parts.length; // if we've buffered to the end of the video, we need to call endOfStream 47932 // so that MediaSources can trigger the `ended` event when it runs out of 47933 // buffered data instead of waiting for me 47934 47935 return ( 47936 playlist.endList && 47937 this.mediaSource_.readyState === 'open' && 47938 appendedLastSegment && 47939 appendedLastPart 47940 ); 47941 } 47942 /** 47943 * Determines what request should be made given current segment loader state. 47944 * 47945 * @return {Object} a request object that describes the segment/part to load 47946 */ 47947 47948 chooseNextRequest_() { 47949 const buffered = this.buffered_(); 47950 const bufferedEnd = lastBufferedEnd(buffered) || 0; 47951 const bufferedTime = timeAheadOf(buffered, this.currentTime_()); 47952 const preloaded = !this.hasPlayed_() && bufferedTime >= 1; 47953 const haveEnoughBuffer = bufferedTime >= this.goalBufferLength_(); 47954 const segments = this.playlist_.segments; // return no segment if: 47955 // 1. we don't have segments 47956 // 2. The video has not yet played and we already downloaded a segment 47957 // 3. we already have enough buffered time 47958 47959 if (!segments.length || preloaded || haveEnoughBuffer) { 47960 return null; 47961 } 47962 this.syncPoint_ = 47963 this.syncPoint_ || 47964 this.syncController_.getSyncPoint( 47965 this.playlist_, 47966 this.duration_(), 47967 this.currentTimeline_, 47968 this.currentTime_(), 47969 this.loaderType_ 47970 ); 47971 const next = { 47972 partIndex: null, 47973 mediaIndex: null, 47974 startOfSegment: null, 47975 playlist: this.playlist_, 47976 isSyncRequest: Boolean(!this.syncPoint_), 47977 }; 47978 if (next.isSyncRequest) { 47979 next.mediaIndex = getSyncSegmentCandidate(this.currentTimeline_, segments, bufferedEnd); 47980 this.logger_( 47981 `choose next request. Can not find sync point. Fallback to media Index: ${next.mediaIndex}` 47982 ); 47983 } else if (this.mediaIndex !== null) { 47984 const segment = segments[this.mediaIndex]; 47985 const partIndex = typeof this.partIndex === 'number' ? this.partIndex : -1; 47986 next.startOfSegment = segment.end ? segment.end : bufferedEnd; 47987 if (segment.parts && segment.parts[partIndex + 1]) { 47988 next.mediaIndex = this.mediaIndex; 47989 next.partIndex = partIndex + 1; 47990 } else { 47991 next.mediaIndex = this.mediaIndex + 1; 47992 } 47993 } else { 47994 let segmentIndex; 47995 let partIndex; 47996 let startTime; 47997 const targetTime = this.fetchAtBuffer_ ? bufferedEnd : this.currentTime_(); 47998 if (this.mediaSequenceSync_) { 47999 this.logger_( 48000 `chooseNextRequest_ request after Quality Switch: 48001For TargetTime: ${targetTime}. 48002CurrentTime: ${this.currentTime_()} 48003BufferedEnd: ${bufferedEnd} 48004Fetch At Buffer: ${this.fetchAtBuffer_} 48005`, 48006 this.mediaSequenceSync_.diagnostics 48007 ); 48008 } 48009 if (this.mediaSequenceSync_ && this.mediaSequenceSync_.isReliable) {
vendor: 4,420 bytes, lines 48010-48112
48010 const syncInfo = this.getSyncInfoFromMediaSequenceSync_(targetTime); 48011 if (!syncInfo) { 48012 const message = 'No sync info found while using media sequence sync'; 48013 this.error({ 48014 message, 48015 metadata: { 48016 errorType: videojs.Error.StreamingFailedToSelectNextSegment, 48017 error: new Error(message), 48018 }, 48019 }); 48020 this.logger_('chooseNextRequest_ - no sync info found using media sequence sync'); // no match 48021 48022 return null; 48023 } 48024 this.logger_( 48025 `chooseNextRequest_ mediaSequence syncInfo (${syncInfo.start} --> ${syncInfo.end})` 48026 ); 48027 segmentIndex = syncInfo.segmentIndex; 48028 partIndex = syncInfo.partIndex; 48029 startTime = syncInfo.start; 48030 } else { 48031 this.logger_( 48032 'chooseNextRequest_ - fallback to a regular segment selection algorithm, based on a syncPoint.' 48033 ); // fallback 48034 48035 const mediaInfoForTime = Playlist.getMediaInfoForTime({ 48036 exactManifestTimings: this.exactManifestTimings, 48037 playlist: this.playlist_, 48038 currentTime: targetTime, 48039 startingPartIndex: this.syncPoint_.partIndex, 48040 startingSegmentIndex: this.syncPoint_.segmentIndex, 48041 startTime: this.syncPoint_.time, 48042 }); 48043 segmentIndex = mediaInfoForTime.segmentIndex; 48044 partIndex = mediaInfoForTime.partIndex; 48045 startTime = mediaInfoForTime.startTime; 48046 } 48047 next.getMediaInfoForTime = this.fetchAtBuffer_ 48048 ? `bufferedEnd ${targetTime}` 48049 : `currentTime ${targetTime}`; 48050 next.mediaIndex = segmentIndex; 48051 next.startOfSegment = startTime; 48052 next.partIndex = partIndex; 48053 this.logger_( 48054 `choose next request. Playlist switched and we have a sync point. Media Index: ${next.mediaIndex} ` 48055 ); 48056 } 48057 const nextSegment = segments[next.mediaIndex]; 48058 let nextPart = 48059 nextSegment && 48060 typeof next.partIndex === 'number' && 48061 nextSegment.parts && 48062 nextSegment.parts[next.partIndex]; // if the next segment index is invalid or 48063 // the next partIndex is invalid do not choose a next segment. 48064 48065 if (!nextSegment || (typeof next.partIndex === 'number' && !nextPart)) { 48066 return null; 48067 } // if the next segment has parts, and we don't have a partIndex. 48068 // Set partIndex to 0 48069 48070 if (typeof next.partIndex !== 'number' && nextSegment.parts) { 48071 next.partIndex = 0; 48072 nextPart = nextSegment.parts[0]; 48073 } // independentSegments applies to every segment in a playlist. If independentSegments appears in a main playlist, 48074 // it applies to each segment in each media playlist. 48075 // https://datatracker.ietf.org/doc/html/draft-pantos-http-live-streaming-23#section-4.3.5.1 48076 48077 const hasIndependentSegments = 48078 (this.vhs_.playlists && 48079 this.vhs_.playlists.main && 48080 this.vhs_.playlists.main.independentSegments) || 48081 this.playlist_.independentSegments; // if we have no buffered data then we need to make sure 48082 // that the next part we append is "independent" if possible. 48083 // So we check if the previous part is independent, and request 48084 // it if it is. 48085 48086 if (!bufferedTime && nextPart && !hasIndependentSegments && !nextPart.independent) { 48087 if (next.partIndex === 0) { 48088 const lastSegment = segments[next.mediaIndex - 1]; 48089 const lastSegmentLastPart = 48090 lastSegment.parts && 48091 lastSegment.parts.length && 48092 lastSegment.parts[lastSegment.parts.length - 1]; 48093 if (lastSegmentLastPart && lastSegmentLastPart.independent) { 48094 next.mediaIndex -= 1; 48095 next.partIndex = lastSegment.parts.length - 1; 48096 next.independent = 'previous segment'; 48097 } 48098 } else if (nextSegment.parts[next.partIndex - 1].independent) { 48099 next.partIndex -= 1; 48100 next.independent = 'previous part'; 48101 } 48102 } 48103 const ended = this.mediaSource_ && this.mediaSource_.readyState === 'ended'; // do not choose a next segment if all of the following: 48104 // 1. this is the last segment in the playlist 48105 // 2. end of stream has been called on the media source already 48106 // 3. the player is not seeking 48107 48108 if (next.mediaIndex >= segments.length - 1 && ended && !this.seeking_()) { 48109 return null; 48110 } 48111 if (this.shouldForceTimestampOffsetAfterResync_) { 48112 this.shouldForceTimestampOffsetAfterResync_ = false;
48113 next.forceTimestampOffset = true; 48114 this.logger_('choose next request. Force timestamp offset after loader resync'); 48115 } 48116 return this.generateSegmentInfo_(next); 48117 } 48118 getSyncInfoFromMediaSequenceSync_(targetTime) { 48119 if (!this.mediaSequenceSync_) { 48120 return null; 48121 } // we should pull the target time to the least available time if we drop out of sync for any reason 48122 48123 const finalTargetTime = Math.max(targetTime, this.mediaSequenceSync_.start); 48124 if (targetTime !== finalTargetTime) { 48125 this.logger_( 48126 `getSyncInfoFromMediaSequenceSync_. Pulled target time from ${targetTime} to ${finalTargetTime}` 48127 ); 48128 } 48129 const mediaSequenceSyncInfo = this.mediaSequenceSync_.getSyncInfoForTime(finalTargetTime); 48130 if (!mediaSequenceSyncInfo) { 48131 // no match at all 48132 return null; 48133 } 48134 if (!mediaSequenceSyncInfo.isAppended) { 48135 // has a perfect match 48136 return mediaSequenceSyncInfo; 48137 } // has match, but segment was already appended. 48138 // attempt to auto-advance to the nearest next segment: 48139 48140 const nextMediaSequenceSyncInfo = this.mediaSequenceSync_.getSyncInfoForTime( 48141 mediaSequenceSyncInfo.end 48142 ); 48143 if (!nextMediaSequenceSyncInfo) { 48144 // no match at all 48145 return null; 48146 } 48147 if (nextMediaSequenceSyncInfo.isAppended) { 48148 this.logger_( 48149 'getSyncInfoFromMediaSequenceSync_: We encounter unexpected scenario where next media sequence sync info is also appended!' 48150 ); 48151 } // got match with the nearest next segment 48152 48153 return nextMediaSequenceSyncInfo; 48154 } 48155 generateSegmentInfo_(options) { 48156 const { 48157 independent, 48158 playlist, 48159 mediaIndex, 48160 startOfSegment, 48161 isSyncRequest, 48162 partIndex, 48163 forceTimestampOffset, 48164 getMediaInfoForTime, 48165 } = options; 48166 const segment = playlist.segments[mediaIndex]; 48167 const part = typeof partIndex === 'number' && segment.parts[partIndex]; 48168 const segmentInfo = { 48169 requestId: 'segment-loader-' + Math.random(), 48170 // resolve the segment URL relative to the playlist 48171 uri: (part && part.resolvedUri) || segment.resolvedUri, 48172 // the segment's mediaIndex at the time it was requested 48173 mediaIndex, 48174 partIndex: part ? partIndex : null, 48175 // whether or not to update the SegmentLoader's state with this 48176 // segment's mediaIndex 48177 isSyncRequest, 48178 startOfSegment, 48179 // the segment's playlist 48180 playlist, 48181 // unencrypted bytes of the segment 48182 bytes: null, 48183 // when a key is defined for this segment, the encrypted bytes 48184 encryptedBytes: null, 48185 // The target timestampOffset for this segment when we append it 48186 // to the source buffer 48187 timestampOffset: null, 48188 // The timeline that the segment is in 48189 timeline: segment.timeline, 48190 // The expected duration of the segment in seconds 48191 duration: (part && part.duration) || segment.duration, 48192 // retain the segment in case the playlist updates while doing an async process 48193 segment, 48194 part, 48195 byteLength: 0, 48196 transmuxer: this.transmuxer_, 48197 // type of getMediaInfoForTime that was used to get this segment 48198 getMediaInfoForTime, 48199 independent, 48200 }; 48201 const overrideCheck = 48202 typeof forceTimestampOffset !== 'undefined' 48203 ? forceTimestampOffset 48204 : this.isPendingTimestampOffset_; 48205 segmentInfo.timestampOffset = this.timestampOffsetForSegment_({ 48206 segmentTimeline: segment.timeline, 48207 currentTimeline: this.currentTimeline_, 48208 startOfSegment, 48209 buffered: this.buffered_(), 48210 overrideCheck, 48211 }); 48212 const audioBufferedEnd = lastBufferedEnd(this.sourceUpdater_.audioBuffered()); 48213 if (typeof audioBufferedEnd === 'number') { 48214 // since the transmuxer is using the actual timing values, but the buffer is 48215 // adjusted by the timestamp offset, we must adjust the value here 48216 segmentInfo.audioAppendStart = audioBufferedEnd - this.sourceUpdater_.audioTimestampOffset(); 48217 } 48218 if (this.sourceUpdater_.videoBuffered().length) { 48219 segmentInfo.gopsToAlignWith = gopsSafeToAlignWith( 48220 this.gopBuffer_, 48221 // since the transmuxer is using the actual timing values, but the time is
vendor: 4,209 bytes, lines 48222-48326
48222 // adjusted by the timestmap offset, we must adjust the value here 48223 this.currentTime_() - this.sourceUpdater_.videoTimestampOffset(), 48224 this.timeMapping_ 48225 ); 48226 } 48227 return segmentInfo; 48228 } // get the timestampoffset for a segment, 48229 // added so that vtt segment loader can override and prevent 48230 // adding timestamp offsets. 48231 48232 timestampOffsetForSegment_(options) { 48233 return timestampOffsetForSegment(options); 48234 } 48235 /** 48236 * Determines if the network has enough bandwidth to complete the current segment 48237 * request in a timely manner. If not, the request will be aborted early and bandwidth 48238 * updated to trigger a playlist switch. 48239 * 48240 * @param {Object} stats 48241 * Object containing stats about the request timing and size 48242 * @private 48243 */ 48244 48245 earlyAbortWhenNeeded_(stats) { 48246 if ( 48247 this.vhs_.tech_.paused() || 48248 // Don't abort if the current playlist is on the lowestEnabledRendition 48249 // TODO: Replace using timeout with a boolean indicating whether this playlist is 48250 // the lowestEnabledRendition. 48251 !this.xhrOptions_.timeout || 48252 // Don't abort if we have no bandwidth information to estimate segment sizes 48253 !this.playlist_.attributes.BANDWIDTH 48254 ) { 48255 return; 48256 } // Wait at least 1 second since the first byte of data has been received before 48257 // using the calculated bandwidth from the progress event to allow the bitrate 48258 // to stabilize 48259 48260 if (Date.now() - (stats.firstBytesReceivedAt || Date.now()) < 1000) { 48261 return; 48262 } 48263 const currentTime = this.currentTime_(); 48264 const measuredBandwidth = stats.bandwidth; 48265 const segmentDuration = this.pendingSegment_.duration; 48266 const requestTimeRemaining = Playlist.estimateSegmentRequestTime( 48267 segmentDuration, 48268 measuredBandwidth, 48269 this.playlist_, 48270 stats.bytesReceived 48271 ); // Subtract 1 from the timeUntilRebuffer so we still consider an early abort 48272 // if we are only left with less than 1 second when the request completes. 48273 // A negative timeUntilRebuffering indicates we are already rebuffering 48274 48275 const timeUntilRebuffer$1 = 48276 timeUntilRebuffer(this.buffered_(), currentTime, this.vhs_.tech_.playbackRate()) - 1; // Only consider aborting early if the estimated time to finish the download 48277 // is larger than the estimated time until the player runs out of forward buffer 48278 48279 if (requestTimeRemaining <= timeUntilRebuffer$1) { 48280 return; 48281 } 48282 const switchCandidate = minRebufferMaxBandwidthSelector({ 48283 main: this.vhs_.playlists.main, 48284 currentTime, 48285 bandwidth: measuredBandwidth, 48286 duration: this.duration_(), 48287 segmentDuration, 48288 timeUntilRebuffer: timeUntilRebuffer$1, 48289 currentTimeline: this.currentTimeline_, 48290 syncController: this.syncController_, 48291 }); 48292 if (!switchCandidate) { 48293 return; 48294 } 48295 const rebufferingImpact = requestTimeRemaining - timeUntilRebuffer$1; 48296 const timeSavedBySwitching = rebufferingImpact - switchCandidate.rebufferingImpact; 48297 let minimumTimeSaving = 0.5; // If we are already rebuffering, increase the amount of variance we add to the 48298 // potential round trip time of the new request so that we are not too aggressive 48299 // with switching to a playlist that might save us a fraction of a second. 48300 48301 if (timeUntilRebuffer$1 <= TIME_FUDGE_FACTOR) { 48302 minimumTimeSaving = 1; 48303 } 48304 if ( 48305 !switchCandidate.playlist || 48306 switchCandidate.playlist.uri === this.playlist_.uri || 48307 timeSavedBySwitching < minimumTimeSaving 48308 ) { 48309 return; 48310 } // set the bandwidth to that of the desired playlist being sure to scale by 48311 // BANDWIDTH_VARIANCE and add one so the playlist selector does not exclude it 48312 // don't trigger a bandwidthupdate as the bandwidth is artifial 48313 48314 this.bandwidth = switchCandidate.playlist.attributes.BANDWIDTH * Config.BANDWIDTH_VARIANCE + 1; 48315 this.trigger('earlyabort'); 48316 } 48317 handleAbort_(segmentInfo) { 48318 this.logger_(`Aborting ${segmentInfoString(segmentInfo)}`); 48319 this.mediaRequestsAborted += 1; 48320 } 48321 /** 48322 * XHR `progress` event handler 48323 * 48324 * @param {Event} 48325 * The XHR `progress` event 48326 * @param {Object} simpleSegment
48327 * A simplified segment object copy 48328 * @private 48329 */ 48330 48331 handleProgress_(event, simpleSegment) { 48332 this.earlyAbortWhenNeeded_(simpleSegment.stats); 48333 if (this.checkForAbort_(simpleSegment.requestId)) { 48334 return; 48335 } 48336 this.trigger('progress'); 48337 } 48338 handleTrackInfo_(simpleSegment, trackInfo) { 48339 const { hasAudio, hasVideo } = trackInfo; 48340 const metadata = { 48341 segmentInfo: segmentInfoPayload({ 48342 type: this.loaderType_, 48343 segment: simpleSegment, 48344 }), 48345 trackInfo: { 48346 hasAudio, 48347 hasVideo, 48348 }, 48349 }; 48350 this.trigger({ 48351 type: 'segmenttransmuxingtrackinfoavailable', 48352 metadata, 48353 }); 48354 this.earlyAbortWhenNeeded_(simpleSegment.stats); 48355 if (this.checkForAbort_(simpleSegment.requestId)) { 48356 return; 48357 } 48358 if (this.checkForIllegalMediaSwitch(trackInfo)) { 48359 return; 48360 } 48361 trackInfo = trackInfo || {}; // When we have track info, determine what media types this loader is dealing with. 48362 // Guard against cases where we're not getting track info at all until we are 48363 // certain that all streams will provide it. 48364 48365 if (!shallowEqual(this.currentMediaInfo_, trackInfo)) { 48366 this.appendInitSegment_ = { 48367 audio: true, 48368 video: true, 48369 }; 48370 this.startingMediaInfo_ = trackInfo; 48371 this.currentMediaInfo_ = trackInfo; 48372 this.logger_('trackinfo update', trackInfo); 48373 this.trigger('trackinfo'); 48374 } // trackinfo may cause an abort if the trackinfo 48375 // causes a codec change to an unsupported codec. 48376 48377 if (this.checkForAbort_(simpleSegment.requestId)) { 48378 return; 48379 } // set trackinfo on the pending segment so that 48380 // it can append. 48381 48382 this.pendingSegment_.trackInfo = trackInfo; // check if any calls were waiting on the track info 48383 48384 if (this.hasEnoughInfoToAppend_()) { 48385 this.processCallQueue_(); 48386 } else { 48387 checkAndFixTimelines(this); 48388 } 48389 } 48390 handleTimingInfo_(simpleSegment, mediaType, timeType, time) { 48391 this.earlyAbortWhenNeeded_(simpleSegment.stats); 48392 if (this.checkForAbort_(simpleSegment.requestId)) { 48393 return; 48394 } 48395 const segmentInfo = this.pendingSegment_; 48396 const timingInfoProperty = timingInfoPropertyForMedia(mediaType); 48397 segmentInfo[timingInfoProperty] = segmentInfo[timingInfoProperty] || {}; 48398 segmentInfo[timingInfoProperty][timeType] = time; 48399 this.logger_(`timinginfo: ${mediaType} - ${timeType} - ${time}`); // check if any calls were waiting on the timing info 48400 48401 if (this.hasEnoughInfoToAppend_()) { 48402 this.processCallQueue_(); 48403 } else { 48404 checkAndFixTimelines(this); 48405 } 48406 } 48407 handleCaptions_(simpleSegment, captionData) { 48408 this.earlyAbortWhenNeeded_(simpleSegment.stats); 48409 if (this.checkForAbort_(simpleSegment.requestId)) { 48410 return; 48411 } // This could only happen with fmp4 segments, but 48412 // should still not happen in general 48413 48414 if (captionData.length === 0) { 48415 this.logger_('SegmentLoader received no captions from a caption event'); 48416 return; 48417 } 48418 const segmentInfo = this.pendingSegment_; // Wait until we have some video data so that caption timing 48419 // can be adjusted by the timestamp offset 48420 48421 if (!segmentInfo.hasAppendedData_) { 48422 this.metadataQueue_.caption.push(this.handleCaptions_.bind(this, simpleSegment, captionData)); 48423 return; 48424 } 48425 const timestampOffset = 48426 this.sourceUpdater_.videoTimestampOffset() === null 48427 ? this.sourceUpdater_.audioTimestampOffset() 48428 : this.sourceUpdater_.videoTimestampOffset(); 48429 const captionTracks = {}; // get total start/end and captions for each track/stream 48430 48431 captionData.forEach((caption) => { 48432 // caption.stream is actually a track name... 48433 // set to the existing values in tracks or default values 48434 captionTracks[caption.stream] = captionTracks[caption.stream] || { 48435 // Infinity, as any other value will be less than this 48436 startTime: Infinity, 48437 captions: [], 48438 // 0 as an other value will be more than this 48439 endTime: 0, 48440 }; 48441 const captionTrack = captionTracks[caption.stream]; 48442 captionTrack.startTime = Math.min( 48443 captionTrack.startTime, 48444 caption.startTime + timestampOffset 48445 ); 48446 captionTrack.endTime = Math.max(captionTrack.endTime, caption.endTime + timestampOffset); 48447 captionTrack.captions.push(caption); 48448 });
48449 Object.keys(captionTracks).forEach((trackName) => { 48450 const { startTime, endTime, captions } = captionTracks[trackName]; 48451 const inbandTextTracks = this.inbandTextTracks_; 48452 this.logger_(`adding cues from ${startTime} -> ${endTime} for ${trackName}`); 48453 createCaptionsTrackIfNotExists(inbandTextTracks, this.vhs_.tech_, trackName); // clear out any cues that start and end at the same time period for the same track. 48454 // We do this because a rendition change that also changes the timescale for captions 48455 // will result in captions being re-parsed for certain segments. If we add them again 48456 // without clearing we will have two of the same captions visible. 48457 48458 removeCuesFromTrack(startTime, endTime, inbandTextTracks[trackName]); 48459 addCaptionData({ 48460 captionArray: captions, 48461 inbandTextTracks, 48462 timestampOffset, 48463 }); 48464 }); // Reset stored captions since we added parsed 48465 // captions to a text track at this point 48466 48467 if (this.transmuxer_) { 48468 this.transmuxer_.postMessage({ 48469 action: 'clearParsedMp4Captions', 48470 }); 48471 } 48472 } 48473 handleId3_(simpleSegment, id3Frames, dispatchType) { 48474 this.earlyAbortWhenNeeded_(simpleSegment.stats); 48475 if (this.checkForAbort_(simpleSegment.requestId)) { 48476 return; 48477 } 48478 const segmentInfo = this.pendingSegment_; // we need to have appended data in order for the timestamp offset to be set 48479 48480 if (!segmentInfo.hasAppendedData_) { 48481 this.metadataQueue_.id3.push( 48482 this.handleId3_.bind(this, simpleSegment, id3Frames, dispatchType) 48483 ); 48484 return; 48485 } 48486 this.addMetadataToTextTrack(dispatchType, id3Frames, this.duration_()); 48487 } 48488 processMetadataQueue_() { 48489 this.metadataQueue_.id3.forEach((fn) => fn()); 48490 this.metadataQueue_.caption.forEach((fn) => fn()); 48491 this.metadataQueue_.id3 = []; 48492 this.metadataQueue_.caption = []; 48493 } 48494 processCallQueue_() { 48495 const callQueue = this.callQueue_; // Clear out the queue before the queued functions are run, since some of the 48496 // functions may check the length of the load queue and default to pushing themselves 48497 // back onto the queue. 48498 48499 this.callQueue_ = []; 48500 callQueue.forEach((fun) => fun()); 48501 } 48502 processLoadQueue_() { 48503 const loadQueue = this.loadQueue_; // Clear out the queue before the queued functions are run, since some of the 48504 // functions may check the length of the load queue and default to pushing themselves 48505 // back onto the queue. 48506 48507 this.loadQueue_ = []; 48508 loadQueue.forEach((fun) => fun()); 48509 } 48510 /** 48511 * Determines whether the loader has enough info to load the next segment. 48512 * 48513 * @return {boolean} 48514 * Whether or not the loader has enough info to load the next segment 48515 */ 48516 48517 hasEnoughInfoToLoad_() { 48518 // Since primary timing goes by video, only the audio loader potentially needs to wait 48519 // to load. 48520 if (this.loaderType_ !== 'audio') { 48521 return true; 48522 } 48523 const segmentInfo = this.pendingSegment_; // A fill buffer must have already run to establish a pending segment before there's 48524 // enough info to load. 48525 48526 if (!segmentInfo) { 48527 return false; 48528 } // The first segment can and should be loaded immediately so that source buffers are 48529 // created together (before appending). Source buffer creation uses the presence of 48530 // audio and video data to determine whether to create audio/video source buffers, and 48531 // uses processed (transmuxed or parsed) media to determine the types required. 48532 48533 if (!this.getCurrentMediaInfo_()) { 48534 return true; 48535 } 48536 if ( 48537 // Technically, instead of waiting to load a segment on timeline changes, a segment 48538 // can be requested and downloaded and only wait before it is transmuxed or parsed. 48539 // But in practice, there are a few reasons why it is better to wait until a loader 48540 // is ready to append that segment before requesting and downloading: 48541 // 48542 // 1. Because audio and main loaders cross discontinuities together, if this loader 48543 // is waiting for the other to catch up, then instead of requesting another 48544 // segment and using up more bandwidth, by not yet loading, more bandwidth is 48545 // allotted to the loader currently behind. 48546 // 2. media-segment-request doesn't have to have logic to consider whether a segment 48547 // is ready to be processed or not, isolating the queueing behavior to the loader. 48548 // 3. The audio loader bases some of its segment properties on timing information 48549 // provided by the main loader, meaning that, if the logic for waiting on 48550 // processing was in media-segment-request, then it would also need to know how 48551 // to re-generate the segment information after the main loader caught up. 48552 shouldWaitForTimelineChange({ 48553 timelineChangeController: this.timelineChangeController_, 48554 currentTimeline: this.currentTimeline_, 48555 segmentTimeline: segmentInfo.timeline, 48556 loaderType: this.loaderType_, 48557 audioDisabled: this.audioDisabled_, 48558 }) 48559 ) { 48560 return false;
vendor: 8,605 bytes, lines 48561-48752
48561 } 48562 return true; 48563 } 48564 getCurrentMediaInfo_(segmentInfo = this.pendingSegment_) { 48565 return (segmentInfo && segmentInfo.trackInfo) || this.currentMediaInfo_; 48566 } 48567 getMediaInfo_(segmentInfo = this.pendingSegment_) { 48568 return this.getCurrentMediaInfo_(segmentInfo) || this.startingMediaInfo_; 48569 } 48570 getPendingSegmentPlaylist() { 48571 return this.pendingSegment_ ? this.pendingSegment_.playlist : null; 48572 } 48573 hasEnoughInfoToAppend_() { 48574 if (!this.sourceUpdater_.ready()) { 48575 return false; 48576 } // If content needs to be removed or the loader is waiting on an append reattempt, 48577 // then no additional content should be appended until the prior append is resolved. 48578 48579 if (this.waitingOnRemove_ || this.quotaExceededErrorRetryTimeout_) { 48580 return false; 48581 } 48582 const segmentInfo = this.pendingSegment_; 48583 const trackInfo = this.getCurrentMediaInfo_(); // no segment to append any data for or 48584 // we do not have information on this specific 48585 // segment yet 48586 48587 if (!segmentInfo || !trackInfo) { 48588 return false; 48589 } 48590 const { hasAudio, hasVideo, isMuxed } = trackInfo; 48591 if (hasVideo && !segmentInfo.videoTimingInfo) { 48592 return false; 48593 } // muxed content only relies on video timing information for now. 48594 48595 if (hasAudio && !this.audioDisabled_ && !isMuxed && !segmentInfo.audioTimingInfo) { 48596 return false; 48597 } // we need to allow an append here even if we're moving to different timelines. 48598 48599 if ( 48600 shouldWaitForTimelineChange({ 48601 timelineChangeController: this.timelineChangeController_, 48602 currentTimeline: this.currentTimeline_, 48603 segmentTimeline: segmentInfo.timeline, 48604 loaderType: this.loaderType_, 48605 audioDisabled: this.audioDisabled_, 48606 }) 48607 ) { 48608 return false; 48609 } 48610 return true; 48611 } 48612 handleData_(simpleSegment, result) { 48613 this.earlyAbortWhenNeeded_(simpleSegment.stats); 48614 if (this.checkForAbort_(simpleSegment.requestId)) { 48615 return; 48616 } // If there's anything in the call queue, then this data came later and should be 48617 // executed after the calls currently queued. 48618 48619 if (this.callQueue_.length || !this.hasEnoughInfoToAppend_()) { 48620 checkAndFixTimelines(this); 48621 this.callQueue_.push(this.handleData_.bind(this, simpleSegment, result)); 48622 return; 48623 } 48624 const segmentInfo = this.pendingSegment_; // update the time mapping so we can translate from display time to media time 48625 48626 this.setTimeMapping_(segmentInfo.timeline); // for tracking overall stats 48627 48628 this.updateMediaSecondsLoaded_(segmentInfo.part || segmentInfo.segment); // Note that the state isn't changed from loading to appending. This is because abort 48629 // logic may change behavior depending on the state, and changing state too early may 48630 // inflate our estimates of bandwidth. In the future this should be re-examined to 48631 // note more granular states. 48632 // don't process and append data if the mediaSource is closed 48633 48634 if (this.mediaSource_.readyState === 'closed') { 48635 return; 48636 } // if this request included an initialization segment, save that data 48637 // to the initSegment cache 48638 48639 if (simpleSegment.map) { 48640 simpleSegment.map = this.initSegmentForMap(simpleSegment.map, true); // move over init segment properties to media request 48641 48642 segmentInfo.segment.map = simpleSegment.map; 48643 } // if this request included a segment key, save that data in the cache 48644 48645 if (simpleSegment.key) { 48646 this.segmentKey(simpleSegment.key, true); 48647 } 48648 segmentInfo.isFmp4 = simpleSegment.isFmp4; 48649 segmentInfo.timingInfo = segmentInfo.timingInfo || {}; 48650 if (segmentInfo.isFmp4) { 48651 this.trigger('fmp4'); 48652 segmentInfo.timingInfo.start = segmentInfo[timingInfoPropertyForMedia(result.type)].start; 48653 } else { 48654 const trackInfo = this.getCurrentMediaInfo_(); 48655 const useVideoTimingInfo = this.loaderType_ === 'main' && trackInfo && trackInfo.hasVideo; 48656 let firstVideoFrameTimeForData; 48657 if (useVideoTimingInfo) { 48658 firstVideoFrameTimeForData = segmentInfo.videoTimingInfo.start; 48659 } // Segment loader knows more about segment timing than the transmuxer (in certain 48660 // aspects), so make any changes required for a more accurate start time. 48661 // Don't set the end time yet, as the segment may not be finished processing. 48662 48663 segmentInfo.timingInfo.start = this.trueSegmentStart_({ 48664 currentStart: segmentInfo.timingInfo.start, 48665 playlist: segmentInfo.playlist, 48666 mediaIndex: segmentInfo.mediaIndex, 48667 currentVideoTimestampOffset: this.sourceUpdater_.videoTimestampOffset(), 48668 useVideoTimingInfo, 48669 firstVideoFrameTimeForData, 48670 videoTimingInfo: segmentInfo.videoTimingInfo, 48671 audioTimingInfo: segmentInfo.audioTimingInfo, 48672 }); 48673 } // Init segments for audio and video only need to be appended in certain cases. Now 48674 // that data is about to be appended, we can check the final cases to determine 48675 // whether we should append an init segment. 48676 48677 this.updateAppendInitSegmentStatus(segmentInfo, result.type); // Timestamp offset should be updated once we get new data and have its timing info, 48678 // as we use the start of the segment to offset the best guess (playlist provided) 48679 // timestamp offset. 48680 48681 this.updateSourceBufferTimestampOffset_(segmentInfo); // if this is a sync request we need to determine whether it should 48682 // be appended or not. 48683 48684 if (segmentInfo.isSyncRequest) { 48685 // first save/update our timing info for this segment. 48686 // this is what allows us to choose an accurate segment 48687 // and the main reason we make a sync request. 48688 this.updateTimingInfoEnd_(segmentInfo); 48689 this.syncController_.saveSegmentTimingInfo({ 48690 segmentInfo, 48691 shouldSaveTimelineMapping: this.loaderType_ === 'main', 48692 }); 48693 const next = this.chooseNextRequest_(); // If the sync request isn't the segment that would be requested next 48694 // after taking into account its timing info, do not append it. 48695 48696 if (next.mediaIndex !== segmentInfo.mediaIndex || next.partIndex !== segmentInfo.partIndex) { 48697 this.logger_('sync segment was incorrect, not appending'); 48698 return; 48699 } // otherwise append it like any other segment as our guess was correct. 48700 48701 this.logger_('sync segment was correct, appending'); 48702 } // Save some state so that in the future anything waiting on first append (and/or 48703 // timestamp offset(s)) can process immediately. While the extra state isn't optimal, 48704 // we need some notion of whether the timestamp offset or other relevant information 48705 // has had a chance to be set. 48706 48707 segmentInfo.hasAppendedData_ = true; // Now that the timestamp offset should be set, we can append any waiting ID3 tags. 48708 48709 this.processMetadataQueue_(); 48710 this.appendData_(segmentInfo, result); 48711 } 48712 updateAppendInitSegmentStatus(segmentInfo, type) { 48713 // alt audio doesn't manage timestamp offset 48714 if ( 48715 this.loaderType_ === 'main' && 48716 typeof segmentInfo.timestampOffset === 'number' && 48717 // in the case that we're handling partial data, we don't want to append an init 48718 // segment for each chunk 48719 !segmentInfo.changedTimestampOffset 48720 ) { 48721 // if the timestamp offset changed, the timeline may have changed, so we have to re- 48722 // append init segments 48723 this.appendInitSegment_ = { 48724 audio: true, 48725 video: true, 48726 }; 48727 } 48728 if (this.playlistOfLastInitSegment_[type] !== segmentInfo.playlist) { 48729 // make sure we append init segment on playlist changes, in case the media config 48730 // changed 48731 this.appendInitSegment_[type] = true; 48732 } 48733 } 48734 getInitSegmentAndUpdateState_({ type, initSegment, map, playlist }) { 48735 // "The EXT-X-MAP tag specifies how to obtain the Media Initialization Section 48736 // (Section 3) required to parse the applicable Media Segments. It applies to every 48737 // Media Segment that appears after it in the Playlist until the next EXT-X-MAP tag 48738 // or until the end of the playlist." 48739 // https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.5 48740 if (map) { 48741 const id = initSegmentId(map); 48742 if (this.activeInitSegmentId_ === id) { 48743 // don't need to re-append the init segment if the ID matches 48744 return null; 48745 } // a map-specified init segment takes priority over any transmuxed (or otherwise 48746 // obtained) init segment 48747 // 48748 // this also caches the init segment for later use 48749 48750 initSegment = this.initSegmentForMap(map, true).bytes; 48751 this.activeInitSegmentId_ = id; 48752 }
vendor: 6,867 bytes, lines 48752-48899
48752 // We used to always prepend init segments for video, however, that shouldn't be 48753 // necessary. Instead, we should only append on changes, similar to what we've always 48754 // done for audio. This is more important (though may not be that important) for 48755 // frame-by-frame appending for LHLS, simply because of the increased quantity of 48756 // appends. 48757 48758 if (initSegment && this.appendInitSegment_[type]) { 48759 // Make sure we track the playlist that we last used for the init segment, so that 48760 // we can re-append the init segment in the event that we get data from a new 48761 // playlist. Discontinuities and track changes are handled in other sections. 48762 this.playlistOfLastInitSegment_[type] = playlist; // Disable future init segment appends for this type. Until a change is necessary. 48763 48764 this.appendInitSegment_[type] = false; // we need to clear out the fmp4 active init segment id, since 48765 // we are appending the muxer init segment 48766 48767 this.activeInitSegmentId_ = null; 48768 return initSegment; 48769 } 48770 return null; 48771 } 48772 handleQuotaExceededError_({ segmentInfo, type, bytes }, error) { 48773 const audioBuffered = this.sourceUpdater_.audioBuffered(); 48774 const videoBuffered = this.sourceUpdater_.videoBuffered(); // For now we're ignoring any notion of gaps in the buffer, but they, in theory, 48775 // should be cleared out during the buffer removals. However, log in case it helps 48776 // debug. 48777 48778 if (audioBuffered.length > 1) { 48779 this.logger_( 48780 'On QUOTA_EXCEEDED_ERR, found gaps in the audio buffer: ' + 48781 timeRangesToArray(audioBuffered).join(', ') 48782 ); 48783 } 48784 if (videoBuffered.length > 1) { 48785 this.logger_( 48786 'On QUOTA_EXCEEDED_ERR, found gaps in the video buffer: ' + 48787 timeRangesToArray(videoBuffered).join(', ') 48788 ); 48789 } 48790 const audioBufferStart = audioBuffered.length ? audioBuffered.start(0) : 0; 48791 const audioBufferEnd = audioBuffered.length ? audioBuffered.end(audioBuffered.length - 1) : 0; 48792 const videoBufferStart = videoBuffered.length ? videoBuffered.start(0) : 0; 48793 const videoBufferEnd = videoBuffered.length ? videoBuffered.end(videoBuffered.length - 1) : 0; 48794 if ( 48795 audioBufferEnd - audioBufferStart <= MIN_BACK_BUFFER && 48796 videoBufferEnd - videoBufferStart <= MIN_BACK_BUFFER 48797 ) { 48798 // Can't remove enough buffer to make room for new segment (or the browser doesn't 48799 // allow for appends of segments this size). In the future, it may be possible to 48800 // split up the segment and append in pieces, but for now, error out this playlist 48801 // in an attempt to switch to a more manageable rendition. 48802 this.logger_( 48803 'On QUOTA_EXCEEDED_ERR, single segment too large to append to ' + 48804 'buffer, triggering an error. ' + 48805 `Appended byte length: ${bytes.byteLength}, ` + 48806 `audio buffer: ${timeRangesToArray(audioBuffered).join(', ')}, ` + 48807 `video buffer: ${timeRangesToArray(videoBuffered).join(', ')}, ` 48808 ); 48809 this.error({ 48810 message: 'Quota exceeded error with append of a single segment of content', 48811 excludeUntil: Infinity, 48812 }); 48813 this.trigger('error'); 48814 return; 48815 } // To try to resolve the quota exceeded error, clear back buffer and retry. This means 48816 // that the segment-loader should block on future events until this one is handled, so 48817 // that it doesn't keep moving onto further segments. Adding the call to the call 48818 // queue will prevent further appends until waitingOnRemove_ and 48819 // quotaExceededErrorRetryTimeout_ are cleared. 48820 // 48821 // Note that this will only block the current loader. In the case of demuxed content, 48822 // the other load may keep filling as fast as possible. In practice, this should be 48823 // OK, as it is a rare case when either audio has a high enough bitrate to fill up a 48824 // source buffer, or video fills without enough room for audio to append (and without 48825 // the availability of clearing out seconds of back buffer to make room for audio). 48826 // But it might still be good to handle this case in the future as a TODO. 48827 48828 this.waitingOnRemove_ = true; 48829 this.callQueue_.push( 48830 this.appendToSourceBuffer_.bind(this, { 48831 segmentInfo, 48832 type, 48833 bytes, 48834 }) 48835 ); 48836 const currentTime = this.currentTime_(); // Try to remove as much audio and video as possible to make room for new content 48837 // before retrying. 48838 48839 const timeToRemoveUntil = currentTime - MIN_BACK_BUFFER; 48840 this.logger_(`On QUOTA_EXCEEDED_ERR, removing audio/video from 0 to ${timeToRemoveUntil}`); 48841 this.remove( 48842 0, 48843 timeToRemoveUntil, 48844 () => { 48845 this.logger_(`On QUOTA_EXCEEDED_ERR, retrying append in ${MIN_BACK_BUFFER}s`); 48846 this.waitingOnRemove_ = false; // wait the length of time alotted in the back buffer to prevent wasted 48847 // attempts (since we can't clear less than the minimum) 48848 48849 this.quotaExceededErrorRetryTimeout_ = window__default['default'].setTimeout(() => { 48850 this.logger_('On QUOTA_EXCEEDED_ERR, re-processing call queue'); 48851 this.quotaExceededErrorRetryTimeout_ = null; 48852 this.processCallQueue_(); 48853 }, MIN_BACK_BUFFER * 1000); 48854 }, 48855 true 48856 ); 48857 } 48858 handleAppendError_({ segmentInfo, type, bytes }, error) { 48859 // if there's no error, nothing to do 48860 if (!error) { 48861 return; 48862 } 48863 if (error.code === QUOTA_EXCEEDED_ERR) { 48864 this.handleQuotaExceededError_({ 48865 segmentInfo, 48866 type, 48867 bytes, 48868 }); // A quota exceeded error should be recoverable with a future re-append, so no need 48869 // to trigger an append error. 48870 48871 return; 48872 } 48873 this.logger_('Received non QUOTA_EXCEEDED_ERR on append', error); // If an append errors, we often can't recover. 48874 // (see https://w3c.github.io/media-source/#sourcebuffer-append-error). 48875 // 48876 // Trigger a special error so that it can be handled separately from normal, 48877 // recoverable errors. 48878 48879 this.error({ 48880 message: 48881 `${type} append of ${bytes.length}b failed for segment ` + 48882 `#${segmentInfo.mediaIndex} in playlist ${segmentInfo.playlist.id}`, 48883 metadata: { 48884 errorType: videojs.Error.StreamingFailedToAppendSegment, 48885 }, 48886 }); 48887 this.trigger('appenderror'); 48888 } 48889 appendToSourceBuffer_({ segmentInfo, type, initSegment, data, bytes }) { 48890 // If this is a re-append, bytes were already created and don't need to be recreated 48891 if (!bytes) { 48892 const segments = [data]; 48893 let byteLength = data.byteLength; 48894 if (initSegment) { 48895 // if the media initialization segment is changing, append it before the content 48896 // segment 48897 segments.unshift(initSegment); 48898 byteLength += initSegment.byteLength; 48899 } // Technically we should be OK appending the init segment separately, however, we
48900 // haven't yet tested that, and prepending is how we have always done things. 48901 48902 bytes = concatSegments({ 48903 bytes: byteLength, 48904 segments, 48905 }); 48906 } 48907 const metadata = { 48908 segmentInfo: segmentInfoPayload({ 48909 type: this.loaderType_, 48910 segment: segmentInfo, 48911 }), 48912 }; 48913 this.trigger({ 48914 type: 'segmentappendstart', 48915 metadata, 48916 }); 48917 this.sourceUpdater_.appendBuffer( 48918 { 48919 segmentInfo, 48920 type, 48921 bytes, 48922 }, 48923 this.handleAppendError_.bind(this, { 48924 segmentInfo, 48925 type, 48926 bytes, 48927 }) 48928 ); 48929 } 48930 handleSegmentTimingInfo_(type, requestId, segmentTimingInfo) { 48931 if (!this.pendingSegment_ || requestId !== this.pendingSegment_.requestId) { 48932 return; 48933 } 48934 const segment = this.pendingSegment_.segment; 48935 const timingInfoProperty = `${type}TimingInfo`; 48936 if (!segment[timingInfoProperty]) { 48937 segment[timingInfoProperty] = {}; 48938 } 48939 segment[timingInfoProperty].transmuxerPrependedSeconds = 48940 segmentTimingInfo.prependedContentDuration || 0; 48941 segment[timingInfoProperty].transmuxedPresentationStart = segmentTimingInfo.start.presentation; 48942 segment[timingInfoProperty].transmuxedDecodeStart = segmentTimingInfo.start.decode; 48943 segment[timingInfoProperty].transmuxedPresentationEnd = segmentTimingInfo.end.presentation; 48944 segment[timingInfoProperty].transmuxedDecodeEnd = segmentTimingInfo.end.decode; // mainly used as a reference for debugging 48945 48946 segment[timingInfoProperty].baseMediaDecodeTime = segmentTimingInfo.baseMediaDecodeTime; 48947 } 48948 appendData_(segmentInfo, result) { 48949 const { type, data } = result; 48950 if (!data || !data.byteLength) { 48951 return; 48952 } 48953 if (type === 'audio' && this.audioDisabled_) { 48954 return; 48955 } 48956 const initSegment = this.getInitSegmentAndUpdateState_({ 48957 type, 48958 initSegment: result.initSegment, 48959 playlist: segmentInfo.playlist, 48960 map: segmentInfo.isFmp4 ? segmentInfo.segment.map : null, 48961 }); 48962 this.appendToSourceBuffer_({ 48963 segmentInfo, 48964 type, 48965 initSegment, 48966 data, 48967 }); 48968 } 48969 /** 48970 * load a specific segment from a request into the buffer 48971 * 48972 * @private 48973 */ 48974 48975 loadSegment_(segmentInfo) { 48976 this.state = 'WAITING'; 48977 this.pendingSegment_ = segmentInfo; 48978 this.trimBackBuffer_(segmentInfo); 48979 if (typeof segmentInfo.timestampOffset === 'number') { 48980 if (this.transmuxer_) { 48981 this.transmuxer_.postMessage({ 48982 action: 'clearAllMp4Captions', 48983 }); 48984 } 48985 } 48986 if (!this.hasEnoughInfoToLoad_()) { 48987 checkAndFixTimelines(this); 48988 this.loadQueue_.push(() => { 48989 // regenerate the audioAppendStart, timestampOffset, etc as they 48990 // may have changed since this function was added to the queue. 48991 const options = _extends__default['default']({}, segmentInfo, { 48992 forceTimestampOffset: true, 48993 }); 48994 _extends__default['default'](segmentInfo, this.generateSegmentInfo_(options)); 48995 this.isPendingTimestampOffset_ = false; 48996 this.updateTransmuxerAndRequestSegment_(segmentInfo); 48997 }); 48998 return; 48999 } 49000 this.updateTransmuxerAndRequestSegment_(segmentInfo); 49001 } 49002 updateTransmuxerAndRequestSegment_(segmentInfo) { 49003 // We'll update the source buffer's timestamp offset once we have transmuxed data, but 49004 // the transmuxer still needs to be updated before then. 49005 // 49006 // Even though keepOriginalTimestamps is set to true for the transmuxer, timestamp 49007 // offset must be passed to the transmuxer for stream correcting adjustments. 49008 if (this.shouldUpdateTransmuxerTimestampOffset_(segmentInfo.timestampOffset)) { 49009 this.gopBuffer_.length = 0; // gopsToAlignWith was set before the GOP buffer was cleared 49010 49011 segmentInfo.gopsToAlignWith = []; 49012 this.timeMapping_ = 0; // reset values in the transmuxer since a discontinuity should start fresh 49013 49014 this.transmuxer_.postMessage({ 49015 action: 'reset', 49016 }); 49017 this.transmuxer_.postMessage({ 49018 action: 'setTimestampOffset', 49019 timestampOffset: segmentInfo.timestampOffset, 49020 }); 49021 } 49022 const simpleSegment = this.createSimplifiedSegmentObj_(segmentInfo);
49023 const isEndOfStream = this.isEndOfStream_( 49024 segmentInfo.mediaIndex, 49025 segmentInfo.playlist, 49026 segmentInfo.partIndex 49027 ); 49028 const isWalkingForward = this.mediaIndex !== null; 49029 const isDiscontinuity = 49030 segmentInfo.timeline !== this.currentTimeline_ && 49031 // currentTimeline starts at -1, so we shouldn't end the timeline switching to 0, 49032 // the first timeline 49033 segmentInfo.timeline > 0; 49034 const isEndOfTimeline = isEndOfStream || (isWalkingForward && isDiscontinuity); 49035 this.logger_(`Requesting 49036${compactSegmentUrlDescription(segmentInfo.uri)} 49037${segmentInfoString(segmentInfo)}`); // If there's an init segment associated with this segment, but it is not cached (identified by a lack of bytes), 49038 // then this init segment has never been seen before and should be appended. 49039 // 49040 // At this point the content type (audio/video or both) is not yet known, but it should be safe to set 49041 // both to true and leave the decision of whether to append the init segment to append time. 49042 49043 if (simpleSegment.map && !simpleSegment.map.bytes) { 49044 this.logger_('going to request init segment.'); 49045 this.appendInitSegment_ = { 49046 video: true, 49047 audio: true, 49048 }; 49049 } 49050 segmentInfo.abortRequests = mediaSegmentRequest({ 49051 xhr: this.vhs_.xhr, 49052 xhrOptions: this.xhrOptions_, 49053 decryptionWorker: this.decrypter_, 49054 segment: simpleSegment, 49055 abortFn: this.handleAbort_.bind(this, segmentInfo), 49056 progressFn: this.handleProgress_.bind(this), 49057 trackInfoFn: this.handleTrackInfo_.bind(this), 49058 timingInfoFn: this.handleTimingInfo_.bind(this), 49059 videoSegmentTimingInfoFn: this.handleSegmentTimingInfo_.bind( 49060 this, 49061 'video', 49062 segmentInfo.requestId 49063 ), 49064 audioSegmentTimingInfoFn: this.handleSegmentTimingInfo_.bind( 49065 this, 49066 'audio', 49067 segmentInfo.requestId 49068 ), 49069 captionsFn: this.handleCaptions_.bind(this), 49070 isEndOfTimeline, 49071 endedTimelineFn: () => { 49072 this.logger_('received endedtimeline callback'); 49073 }, 49074 id3Fn: this.handleId3_.bind(this), 49075 dataFn: this.handleData_.bind(this), 49076 doneFn: this.segmentRequestFinished_.bind(this), 49077 onTransmuxerLog: ({ message, level, stream }) => { 49078 this.logger_( 49079 `${segmentInfoString(segmentInfo)} logged from transmuxer stream ${stream} as a ${level}: ${message}` 49080 ); 49081 }, 49082 triggerSegmentEventFn: ({ type, segment, keyInfo, trackInfo, timingInfo }) => { 49083 const segInfo = segmentInfoPayload({ 49084 segment, 49085 }); 49086 const metadata = { 49087 segmentInfo: segInfo, 49088 }; // add other properties if necessary. 49089 49090 if (keyInfo) { 49091 metadata.keyInfo = keyInfo; 49092 } 49093 if (trackInfo) { 49094 metadata.trackInfo = trackInfo; 49095 } 49096 if (timingInfo) { 49097 metadata.timingInfo = timingInfo; 49098 } 49099 this.trigger({ 49100 type, 49101 metadata, 49102 }); 49103 }, 49104 }); 49105 } 49106 /** 49107 * trim the back buffer so that we don't have too much data 49108 * in the source buffer 49109 * 49110 * @private 49111 * 49112 * @param {Object} segmentInfo - the current segment 49113 */ 49114 49115 trimBackBuffer_(segmentInfo) { 49116 const removeToTime = safeBackBufferTrimTime( 49117 this.seekable_(), 49118 this.currentTime_(), 49119 this.playlist_.targetDuration || 10 49120 ); // Chrome has a hard limit of 150MB of 49121 // buffer and a very conservative "garbage collector" 49122 // We manually clear out the old buffer to ensure 49123 // we don't trigger the QuotaExceeded error 49124 // on the source buffer during subsequent appends 49125 49126 if (removeToTime > 0) { 49127 this.remove(0, removeToTime); 49128 } 49129 } 49130 /** 49131 * created a simplified copy of the segment object with just the 49132 * information necessary to perform the XHR and decryption 49133 * 49134 * @private 49135 * 49136 * @param {Object} segmentInfo - the current segment 49137 * @return {Object} a simplified segment object copy 49138 */ 49139 49140 createSimplifiedSegmentObj_(segmentInfo) { 49141 const segment = segmentInfo.segment; 49142 const part = segmentInfo.part; 49143 const isEncrypted = 49144 segmentInfo.segment.key || (segmentInfo.segment.map && segmentInfo.segment.map.key); 49145 const isMediaInitialization = segmentInfo.segment.map && !segmentInfo.segment.map.bytes; 49146 const simpleSegment = { 49147 resolvedUri: part ? part.resolvedUri : segment.resolvedUri, 49148 byterange: part ? part.byterange : segment.byterange, 49149 requestId: segmentInfo.requestId, 49150 transmuxer: segmentInfo.transmuxer, 49151 audioAppendStart: segmentInfo.audioAppendStart, 49152 gopsToAlignWith: segmentInfo.gopsToAlignWith, 49153 part: segmentInfo.part, 49154 type: this.loaderType_, 49155 start: segmentInfo.startOfSegment, 49156 duration: segmentInfo.duration, 49157 isEncrypted, 49158 isMediaInitialization, 49159 }; 49160 const previousSegment = segmentInfo.playlist.segments[segmentInfo.mediaIndex - 1]; 49161 if (previousSegment && previousSegment.timeline === segment.timeline) { 49162 // The baseStartTime of a segment is used to handle rollover when probing the TS 49163 // segment to retrieve timing information. Since the probe only looks at the media's 49164 // times (e.g., PTS and DTS values of the segment), and doesn't consider the 49165 // player's time (e.g., player.currentTime()), baseStartTime should reflect the 49166 // media time as well. transmuxedDecodeEnd represents the end time of a segment, in 49167 // seconds of media time, so should be used here. The previous segment is used since 49168 // the end of the previous segment should represent the beginning of the current 49169 // segment, so long as they are on the same timeline. 49170 if (previousSegment.videoTimingInfo) { 49171 simpleSegment.baseStartTime = previousSegment.videoTimingInfo.transmuxedDecodeEnd; 49172 } else if (previousSegment.audioTimingInfo) { 49173 simpleSegment.baseStartTime = previousSegment.audioTimingInfo.transmuxedDecodeEnd; 49174 } 49175 } 49176 if (segment.key) { 49177 // if the media sequence is greater than 2^32, the IV will be incorrect 49178 // assuming 10s segments, that would be about 1300 years 49179 const iv = 49180 segment.key.iv || 49181 new Uint32Array([0, 0, 0, segmentInfo.mediaIndex + segmentInfo.playlist.mediaSequence]); 49182 simpleSegment.key = this.segmentKey(segment.key); 49183 simpleSegment.key.iv = iv; 49184 } 49185 if (segment.map) { 49186 simpleSegment.map = this.initSegmentForMap(segment.map); 49187 } 49188 return simpleSegment; 49189 } 49190 saveTransferStats_(stats) { 49191 // every request counts as a media request even if it has been aborted 49192 // or canceled due to a timeout 49193 this.mediaRequests += 1; 49194 if (stats) { 49195 this.mediaBytesTransferred += stats.bytesReceived; 49196 this.mediaTransferDuration += stats.roundTripTime; 49197 } 49198 } 49199 saveBandwidthRelatedStats_(duration, stats) { 49200 // byteLength will be used for throughput, and should be based on bytes receieved, 49201 // which we only know at the end of the request and should reflect total bytes 49202 // downloaded rather than just bytes processed from components of the segment 49203 this.pendingSegment_.byteLength = stats.bytesReceived; 49204 if (duration < MIN_SEGMENT_DURATION_TO_SAVE_STATS) { 49205 this.logger_( 49206 `Ignoring segment's bandwidth because its duration of ${duration}` + 49207 ` is less than the min to record ${MIN_SEGMENT_DURATION_TO_SAVE_STATS}` 49208 ); 49209 return; 49210 } 49211 const metadata = { 49212 bandwidthInfo: { 49213 from: this.bandwidth,
vendor: 17,992 bytes, lines 49214-49669
49214 to: stats.bandwidth, 49215 }, 49216 }; // player event with payload 49217 49218 this.trigger({ 49219 type: 'bandwidthupdated', 49220 metadata, 49221 }); 49222 this.bandwidth = stats.bandwidth; 49223 this.roundTrip = stats.roundTripTime; 49224 } 49225 handleTimeout_() { 49226 // although the VTT segment loader bandwidth isn't really used, it's good to 49227 // maintain functinality between segment loaders 49228 this.mediaRequestsTimedout += 1; 49229 this.bandwidth = 1; 49230 this.roundTrip = NaN; 49231 this.trigger('bandwidthupdate'); 49232 this.trigger('timeout'); 49233 } 49234 /** 49235 * Handle the callback from the segmentRequest function and set the 49236 * associated SegmentLoader state and errors if necessary 49237 * 49238 * @private 49239 */ 49240 49241 segmentRequestFinished_(error, simpleSegment, result) { 49242 // TODO handle special cases, e.g., muxed audio/video but only audio in the segment 49243 // check the call queue directly since this function doesn't need to deal with any 49244 // data, and can continue even if the source buffers are not set up and we didn't get 49245 // any data from the segment 49246 if (this.callQueue_.length) { 49247 this.callQueue_.push(this.segmentRequestFinished_.bind(this, error, simpleSegment, result)); 49248 return; 49249 } 49250 this.saveTransferStats_(simpleSegment.stats); // The request was aborted and the SegmentLoader has already been reset 49251 49252 if (!this.pendingSegment_) { 49253 return; 49254 } // the request was aborted and the SegmentLoader has already started 49255 // another request. this can happen when the timeout for an aborted 49256 // request triggers due to a limitation in the XHR library 49257 // do not count this as any sort of request or we risk double-counting 49258 49259 if (simpleSegment.requestId !== this.pendingSegment_.requestId) { 49260 return; 49261 } // an error occurred from the active pendingSegment_ so reset everything 49262 49263 if (error) { 49264 this.pendingSegment_ = null; 49265 this.state = 'READY'; // aborts are not a true error condition and nothing corrective needs to be done 49266 49267 if (error.code === REQUEST_ERRORS.ABORTED) { 49268 return; 49269 } 49270 this.pause(); // the error is really just that at least one of the requests timed-out 49271 // set the bandwidth to a very low value and trigger an ABR switch to 49272 // take emergency action 49273 49274 if (error.code === REQUEST_ERRORS.TIMEOUT) { 49275 this.handleTimeout_(); 49276 return; 49277 } // if control-flow has arrived here, then the error is real 49278 // emit an error event to exclude the current playlist 49279 49280 this.mediaRequestsErrored += 1; 49281 this.error(error); 49282 this.trigger('error'); 49283 return; 49284 } 49285 const segmentInfo = this.pendingSegment_; // the response was a success so set any bandwidth stats the request 49286 // generated for ABR purposes 49287 49288 this.saveBandwidthRelatedStats_(segmentInfo.duration, simpleSegment.stats); 49289 segmentInfo.endOfAllRequests = simpleSegment.endOfAllRequests; 49290 if (result.gopInfo) { 49291 this.gopBuffer_ = updateGopBuffer(this.gopBuffer_, result.gopInfo, this.safeAppend_); 49292 } // Although we may have already started appending on progress, we shouldn't switch the 49293 // state away from loading until we are officially done loading the segment data. 49294 49295 this.state = 'APPENDING'; // used for testing 49296 49297 this.trigger('appending'); 49298 this.waitForAppendsToComplete_(segmentInfo); 49299 } 49300 setTimeMapping_(timeline) { 49301 const timelineMapping = this.syncController_.mappingForTimeline(timeline); 49302 if (timelineMapping !== null) { 49303 this.timeMapping_ = timelineMapping; 49304 } 49305 } 49306 updateMediaSecondsLoaded_(segment) { 49307 if (typeof segment.start === 'number' && typeof segment.end === 'number') { 49308 this.mediaSecondsLoaded += segment.end - segment.start; 49309 } else { 49310 this.mediaSecondsLoaded += segment.duration; 49311 } 49312 } 49313 shouldUpdateTransmuxerTimestampOffset_(timestampOffset) { 49314 if (timestampOffset === null) { 49315 return false; 49316 } // note that we're potentially using the same timestamp offset for both video and 49317 // audio 49318 49319 if ( 49320 this.loaderType_ === 'main' && 49321 timestampOffset !== this.sourceUpdater_.videoTimestampOffset() 49322 ) { 49323 return true; 49324 } 49325 if (!this.audioDisabled_ && timestampOffset !== this.sourceUpdater_.audioTimestampOffset()) { 49326 return true; 49327 } 49328 return false; 49329 } 49330 trueSegmentStart_({ 49331 currentStart, 49332 playlist, 49333 mediaIndex, 49334 firstVideoFrameTimeForData, 49335 currentVideoTimestampOffset, 49336 useVideoTimingInfo, 49337 videoTimingInfo, 49338 audioTimingInfo, 49339 }) { 49340 if (typeof currentStart !== 'undefined') { 49341 // if start was set once, keep using it 49342 return currentStart; 49343 } 49344 if (!useVideoTimingInfo) { 49345 return audioTimingInfo.start; 49346 } 49347 const previousSegment = playlist.segments[mediaIndex - 1]; // The start of a segment should be the start of the first full frame contained 49348 // within that segment. Since the transmuxer maintains a cache of incomplete data 49349 // from and/or the last frame seen, the start time may reflect a frame that starts 49350 // in the previous segment. Check for that case and ensure the start time is 49351 // accurate for the segment. 49352 49353 if ( 49354 mediaIndex === 0 || 49355 !previousSegment || 49356 typeof previousSegment.start === 'undefined' || 49357 previousSegment.end !== firstVideoFrameTimeForData + currentVideoTimestampOffset 49358 ) { 49359 return firstVideoFrameTimeForData; 49360 } 49361 return videoTimingInfo.start; 49362 } 49363 waitForAppendsToComplete_(segmentInfo) { 49364 const trackInfo = this.getCurrentMediaInfo_(segmentInfo); 49365 if (!trackInfo) { 49366 this.error({ 49367 message: 'No starting media returned, likely due to an unsupported media format.', 49368 playlistExclusionDuration: Infinity, 49369 }); 49370 this.trigger('error'); 49371 return; 49372 } // Although transmuxing is done, appends may not yet be finished. Throw a marker 49373 // on each queue this loader is responsible for to ensure that the appends are 49374 // complete. 49375 49376 const { hasAudio, hasVideo, isMuxed } = trackInfo; 49377 const waitForVideo = this.loaderType_ === 'main' && hasVideo; 49378 const waitForAudio = !this.audioDisabled_ && hasAudio && !isMuxed; 49379 segmentInfo.waitingOnAppends = 0; // segments with no data 49380 49381 if (!segmentInfo.hasAppendedData_) { 49382 if (!segmentInfo.timingInfo && typeof segmentInfo.timestampOffset === 'number') { 49383 // When there's no audio or video data in the segment, there's no audio or video 49384 // timing information. 49385 // 49386 // If there's no audio or video timing information, then the timestamp offset 49387 // can't be adjusted to the appropriate value for the transmuxer and source 49388 // buffers. 49389 // 49390 // Therefore, the next segment should be used to set the timestamp offset. 49391 this.isPendingTimestampOffset_ = true; 49392 } // override settings for metadata only segments 49393 49394 segmentInfo.timingInfo = { 49395 start: 0, 49396 }; 49397 segmentInfo.waitingOnAppends++; 49398 if (!this.isPendingTimestampOffset_) { 49399 // update the timestampoffset 49400 this.updateSourceBufferTimestampOffset_(segmentInfo); // make sure the metadata queue is processed even though we have 49401 // no video/audio data. 49402 49403 this.processMetadataQueue_(); 49404 } // append is "done" instantly with no data. 49405 49406 this.checkAppendsDone_(segmentInfo); 49407 return; 49408 } // Since source updater could call back synchronously, do the increments first. 49409 49410 if (waitForVideo) { 49411 segmentInfo.waitingOnAppends++; 49412 } 49413 if (waitForAudio) { 49414 segmentInfo.waitingOnAppends++; 49415 } 49416 if (waitForVideo) { 49417 this.sourceUpdater_.videoQueueCallback(this.checkAppendsDone_.bind(this, segmentInfo)); 49418 } 49419 if (waitForAudio) { 49420 this.sourceUpdater_.audioQueueCallback(this.checkAppendsDone_.bind(this, segmentInfo)); 49421 } 49422 } 49423 checkAppendsDone_(segmentInfo) { 49424 if (this.checkForAbort_(segmentInfo.requestId)) { 49425 return; 49426 } 49427 segmentInfo.waitingOnAppends--; 49428 if (segmentInfo.waitingOnAppends === 0) { 49429 this.handleAppendsDone_(); 49430 } 49431 } 49432 checkForIllegalMediaSwitch(trackInfo) { 49433 const illegalMediaSwitchError = illegalMediaSwitch( 49434 this.loaderType_, 49435 this.getCurrentMediaInfo_(), 49436 trackInfo 49437 ); 49438 if (illegalMediaSwitchError) { 49439 this.error({ 49440 message: illegalMediaSwitchError, 49441 playlistExclusionDuration: Infinity, 49442 }); 49443 this.trigger('error'); 49444 return true; 49445 } 49446 return false; 49447 } 49448 updateSourceBufferTimestampOffset_(segmentInfo) { 49449 if ( 49450 segmentInfo.timestampOffset === null || 49451 // we don't yet have the start for whatever media type (video or audio) has 49452 // priority, timing-wise, so we must wait 49453 typeof segmentInfo.timingInfo.start !== 'number' || 49454 // already updated the timestamp offset for this segment 49455 segmentInfo.changedTimestampOffset || 49456 // the alt audio loader should not be responsible for setting the timestamp offset 49457 this.loaderType_ !== 'main' 49458 ) { 49459 return; 49460 } 49461 let didChange = false; // Primary timing goes by video, and audio is trimmed in the transmuxer, meaning that 49462 // the timing info here comes from video. In the event that the audio is longer than 49463 // the video, this will trim the start of the audio. 49464 // This also trims any offset from 0 at the beginning of the media 49465 49466 segmentInfo.timestampOffset -= this.getSegmentStartTimeForTimestampOffsetCalculation_({ 49467 videoTimingInfo: segmentInfo.segment.videoTimingInfo, 49468 audioTimingInfo: segmentInfo.segment.audioTimingInfo, 49469 timingInfo: segmentInfo.timingInfo, 49470 }); // In the event that there are part segment downloads, each will try to update the 49471 // timestamp offset. Retaining this bit of state prevents us from updating in the 49472 // future (within the same segment), however, there may be a better way to handle it. 49473 49474 segmentInfo.changedTimestampOffset = true; 49475 if (segmentInfo.timestampOffset !== this.sourceUpdater_.videoTimestampOffset()) { 49476 this.sourceUpdater_.videoTimestampOffset(segmentInfo.timestampOffset); 49477 didChange = true; 49478 } 49479 if (segmentInfo.timestampOffset !== this.sourceUpdater_.audioTimestampOffset()) { 49480 this.sourceUpdater_.audioTimestampOffset(segmentInfo.timestampOffset); 49481 didChange = true; 49482 } 49483 if (didChange) { 49484 this.trigger('timestampoffset'); 49485 } 49486 } 49487 getSegmentStartTimeForTimestampOffsetCalculation_({ 49488 videoTimingInfo, 49489 audioTimingInfo, 49490 timingInfo, 49491 }) { 49492 if (!this.useDtsForTimestampOffset_) { 49493 return timingInfo.start; 49494 } 49495 if (videoTimingInfo && typeof videoTimingInfo.transmuxedDecodeStart === 'number') { 49496 return videoTimingInfo.transmuxedDecodeStart; 49497 } // handle audio only 49498 49499 if (audioTimingInfo && typeof audioTimingInfo.transmuxedDecodeStart === 'number') { 49500 return audioTimingInfo.transmuxedDecodeStart; 49501 } // handle content not transmuxed (e.g., MP4) 49502 49503 return timingInfo.start; 49504 } 49505 updateTimingInfoEnd_(segmentInfo) { 49506 segmentInfo.timingInfo = segmentInfo.timingInfo || {}; 49507 const trackInfo = this.getMediaInfo_(); 49508 const useVideoTimingInfo = this.loaderType_ === 'main' && trackInfo && trackInfo.hasVideo; 49509 const prioritizedTimingInfo = 49510 useVideoTimingInfo && segmentInfo.videoTimingInfo 49511 ? segmentInfo.videoTimingInfo 49512 : segmentInfo.audioTimingInfo; 49513 if (!prioritizedTimingInfo) { 49514 return; 49515 } 49516 segmentInfo.timingInfo.end = 49517 typeof prioritizedTimingInfo.end === 'number' 49518 ? // End time may not exist in a case where we aren't parsing the full segment (one 49519 // current example is the case of fmp4), so use the rough duration to calculate an 49520 // end time. 49521 prioritizedTimingInfo.end 49522 : prioritizedTimingInfo.start + segmentInfo.duration; 49523 } 49524 /** 49525 * callback to run when appendBuffer is finished. detects if we are 49526 * in a good state to do things with the data we got, or if we need 49527 * to wait for more 49528 * 49529 * @private 49530 */ 49531 49532 handleAppendsDone_() { 49533 // appendsdone can cause an abort 49534 if (this.pendingSegment_) { 49535 const metadata = { 49536 segmentInfo: segmentInfoPayload({ 49537 type: this.loaderType_, 49538 segment: this.pendingSegment_, 49539 }), 49540 }; 49541 this.trigger({ 49542 type: 'appendsdone', 49543 metadata, 49544 }); 49545 } 49546 if (!this.pendingSegment_) { 49547 this.state = 'READY'; // TODO should this move into this.checkForAbort to speed up requests post abort in 49548 // all appending cases? 49549 49550 if (!this.paused()) { 49551 this.monitorBuffer_(); 49552 } 49553 return; 49554 } 49555 const segmentInfo = this.pendingSegment_; 49556 if (segmentInfo.part && segmentInfo.part.syncInfo) { 49557 // low-latency flow 49558 segmentInfo.part.syncInfo.markAppended(); 49559 } else if (segmentInfo.segment.syncInfo) { 49560 // normal flow 49561 segmentInfo.segment.syncInfo.markAppended(); 49562 } // Now that the end of the segment has been reached, we can set the end time. It's 49563 // best to wait until all appends are done so we're sure that the primary media is 49564 // finished (and we have its end time). 49565 49566 this.updateTimingInfoEnd_(segmentInfo); 49567 if (this.shouldSaveSegmentTimingInfo_) { 49568 // Timeline mappings should only be saved for the main loader. This is for multiple 49569 // reasons: 49570 // 49571 // 1) Only one mapping is saved per timeline, meaning that if both the audio loader 49572 // and the main loader try to save the timeline mapping, whichever comes later 49573 // will overwrite the first. In theory this is OK, as the mappings should be the 49574 // same, however, it breaks for (2) 49575 // 2) In the event of a live stream, the initial live point will make for a somewhat 49576 // arbitrary mapping. If audio and video streams are not perfectly in-sync, then 49577 // the mapping will be off for one of the streams, dependent on which one was 49578 // first saved (see (1)). 49579 // 3) Primary timing goes by video in VHS, so the mapping should be video. 49580 // 49581 // Since the audio loader will wait for the main loader to load the first segment, 49582 // the main loader will save the first timeline mapping, and ensure that there won't 49583 // be a case where audio loads two segments without saving a mapping (thus leading 49584 // to missing segment timing info). 49585 this.syncController_.saveSegmentTimingInfo({ 49586 segmentInfo, 49587 shouldSaveTimelineMapping: this.loaderType_ === 'main', 49588 }); 49589 } 49590 const segmentDurationMessage = getTroublesomeSegmentDurationMessage( 49591 segmentInfo, 49592 this.sourceType_ 49593 ); 49594 if (segmentDurationMessage) { 49595 if (segmentDurationMessage.severity === 'warn') { 49596 videojs.log.warn(segmentDurationMessage.message); 49597 } else { 49598 this.logger_(segmentDurationMessage.message); 49599 } 49600 } 49601 this.recordThroughput_(segmentInfo); 49602 this.pendingSegment_ = null; 49603 this.state = 'READY'; 49604 if (segmentInfo.isSyncRequest) { 49605 this.trigger('syncinfoupdate'); // if the sync request was not appended 49606 // then it was not the correct segment. 49607 // throw it away and use the data it gave us 49608 // to get the correct one. 49609 49610 if (!segmentInfo.hasAppendedData_) { 49611 this.logger_(`Throwing away un-appended sync request ${segmentInfoString(segmentInfo)}`); 49612 return; 49613 } 49614 } 49615 this.logger_(`Appended ${segmentInfoString(segmentInfo)}`); 49616 this.addSegmentMetadataCue_(segmentInfo); 49617 this.fetchAtBuffer_ = true; 49618 if (this.currentTimeline_ !== segmentInfo.timeline) { 49619 this.timelineChangeController_.lastTimelineChange({ 49620 type: this.loaderType_, 49621 from: this.currentTimeline_, 49622 to: segmentInfo.timeline, 49623 }); // If audio is not disabled, the main segment loader is responsible for updating 49624 // the audio timeline as well. If the content is video only, this won't have any 49625 // impact. 49626 49627 if (this.loaderType_ === 'main' && !this.audioDisabled_) { 49628 this.timelineChangeController_.lastTimelineChange({ 49629 type: 'audio', 49630 from: this.currentTimeline_, 49631 to: segmentInfo.timeline, 49632 }); 49633 } 49634 } 49635 this.currentTimeline_ = segmentInfo.timeline; // We must update the syncinfo to recalculate the seekable range before 49636 // the following conditional otherwise it may consider this a bad "guess" 49637 // and attempt to resync when the post-update seekable window and live 49638 // point would mean that this was the perfect segment to fetch 49639 49640 this.trigger('syncinfoupdate'); 49641 const segment = segmentInfo.segment; 49642 const part = segmentInfo.part; 49643 const badSegmentGuess = 49644 segment.end && this.currentTime_() - segment.end > segmentInfo.playlist.targetDuration * 3; 49645 const badPartGuess = 49646 part && 49647 part.end && 49648 this.currentTime_() - part.end > segmentInfo.playlist.partTargetDuration * 3; // If we previously appended a segment/part that ends more than 3 part/targetDurations before 49649 // the currentTime_ that means that our conservative guess was too conservative. 49650 // In that case, reset the loader state so that we try to use any information gained 49651 // from the previous request to create a new, more accurate, sync-point. 49652 49653 if (badSegmentGuess || badPartGuess) { 49654 this.logger_(`bad ${badSegmentGuess ? 'segment' : 'part'} ${segmentInfoString(segmentInfo)}`); 49655 this.resetEverything(); 49656 return; 49657 } 49658 const isWalkingForward = this.mediaIndex !== null; // Don't do a rendition switch unless we have enough time to get a sync segment 49659 // and conservatively guess 49660 49661 if (isWalkingForward) { 49662 this.trigger('bandwidthupdate'); 49663 } 49664 this.trigger('progress'); 49665 this.mediaIndex = segmentInfo.mediaIndex; 49666 this.partIndex = segmentInfo.partIndex; // any time an update finishes and the last segment is in the 49667 // buffer, end the stream. this ensures the "ended" event will 49668 // fire if playback reaches that point. 49669
vendor: 4,772 bytes, lines 49670-49801
49670 if (this.isEndOfStream_(segmentInfo.mediaIndex, segmentInfo.playlist, segmentInfo.partIndex)) { 49671 this.endOfStream(); 49672 } // used for testing 49673 49674 this.trigger('appended'); 49675 if (segmentInfo.hasAppendedData_) { 49676 this.mediaAppends++; 49677 } 49678 if (!this.paused()) { 49679 this.monitorBuffer_(); 49680 } 49681 } 49682 /** 49683 * Records the current throughput of the decrypt, transmux, and append 49684 * portion of the semgment pipeline. `throughput.rate` is a the cumulative 49685 * moving average of the throughput. `throughput.count` is the number of 49686 * data points in the average. 49687 * 49688 * @private 49689 * @param {Object} segmentInfo the object returned by loadSegment 49690 */ 49691 49692 recordThroughput_(segmentInfo) { 49693 if (segmentInfo.duration < MIN_SEGMENT_DURATION_TO_SAVE_STATS) { 49694 this.logger_( 49695 `Ignoring segment's throughput because its duration of ${segmentInfo.duration}` + 49696 ` is less than the min to record ${MIN_SEGMENT_DURATION_TO_SAVE_STATS}` 49697 ); 49698 return; 49699 } 49700 const rate = this.throughput.rate; // Add one to the time to ensure that we don't accidentally attempt to divide 49701 // by zero in the case where the throughput is ridiculously high 49702 49703 const segmentProcessingTime = Date.now() - segmentInfo.endOfAllRequests + 1; // Multiply by 8000 to convert from bytes/millisecond to bits/second 49704 49705 const segmentProcessingThroughput = Math.floor( 49706 (segmentInfo.byteLength / segmentProcessingTime) * 8 * 1000 49707 ); // This is just a cumulative moving average calculation: 49708 // newAvg = oldAvg + (sample - oldAvg) / (sampleCount + 1) 49709 49710 this.throughput.rate += (segmentProcessingThroughput - rate) / ++this.throughput.count; 49711 } 49712 /** 49713 * Adds a cue to the segment-metadata track with some metadata information about the 49714 * segment 49715 * 49716 * @private 49717 * @param {Object} segmentInfo 49718 * the object returned by loadSegment 49719 * @method addSegmentMetadataCue_ 49720 */ 49721 49722 addSegmentMetadataCue_(segmentInfo) { 49723 if (!this.segmentMetadataTrack_) { 49724 return; 49725 } 49726 const segment = segmentInfo.segment; 49727 const start = segment.start; 49728 const end = segment.end; // Do not try adding the cue if the start and end times are invalid. 49729 49730 if (!finite(start) || !finite(end)) { 49731 return; 49732 } 49733 removeCuesFromTrack(start, end, this.segmentMetadataTrack_); 49734 const Cue = window__default['default'].WebKitDataCue || window__default['default'].VTTCue; 49735 const value = { 49736 custom: segment.custom, 49737 dateTimeObject: segment.dateTimeObject, 49738 dateTimeString: segment.dateTimeString, 49739 programDateTime: segment.programDateTime, 49740 bandwidth: segmentInfo.playlist.attributes.BANDWIDTH, 49741 resolution: segmentInfo.playlist.attributes.RESOLUTION, 49742 codecs: segmentInfo.playlist.attributes.CODECS, 49743 byteLength: segmentInfo.byteLength, 49744 uri: segmentInfo.uri, 49745 timeline: segmentInfo.timeline, 49746 playlist: segmentInfo.playlist.id, 49747 start, 49748 end, 49749 }; 49750 const data = JSON.stringify(value); 49751 const cue = new Cue(start, end, data); // Attach the metadata to the value property of the cue to keep consistency between 49752 // the differences of WebKitDataCue in safari and VTTCue in other browsers 49753 49754 cue.value = value; 49755 this.segmentMetadataTrack_.addCue(cue); 49756 } 49757} 49758function noop() {} 49759const toTitleCase = function (string) { 49760 if (typeof string !== 'string') { 49761 return string; 49762 } 49763 return string.replace(/./, (w) => w.toUpperCase()); 49764}; 49765 49766/** 49767 * @file source-updater.js 49768 */ 49769const bufferTypes = ['video', 'audio']; 49770const updating = (type, sourceUpdater) => { 49771 const sourceBuffer = sourceUpdater[`${type}Buffer`]; 49772 return (sourceBuffer && sourceBuffer.updating) || sourceUpdater.queuePending[type]; 49773}; 49774const nextQueueIndexOfType = (type, queue) => { 49775 for (let i = 0; i < queue.length; i++) { 49776 const queueEntry = queue[i]; 49777 if (queueEntry.type === 'mediaSource') { 49778 // If the next entry is a media source entry (uses multiple source buffers), block 49779 // processing to allow it to go through first. 49780 return null; 49781 } 49782 if (queueEntry.type === type) { 49783 return i; 49784 } 49785 } 49786 return null; 49787}; 49788const shiftQueue = (type, sourceUpdater) => { 49789 if (sourceUpdater.queue.length === 0) { 49790 return; 49791 } 49792 let queueIndex = 0; 49793 let queueEntry = sourceUpdater.queue[queueIndex]; 49794 if (queueEntry.type === 'mediaSource') { 49795 if (!sourceUpdater.updating() && sourceUpdater.mediaSource.readyState !== 'closed') { 49796 sourceUpdater.queue.shift(); 49797 queueEntry.action(sourceUpdater); 49798 if (queueEntry.doneFn) { 49799 queueEntry.doneFn(); 49800 } // Only specific source buffer actions must wait for async updateend events. Media 49801 // Source actions process synchronously. Therefore, both audio and video source
vendor: 4,943 bytes, lines 49802-49920
49802 // buffers are now clear to process the next queue entries. 49803 49804 shiftQueue('audio', sourceUpdater); 49805 shiftQueue('video', sourceUpdater); 49806 } // Media Source actions require both source buffers, so if the media source action 49807 // couldn't process yet (because one or both source buffers are busy), block other 49808 // queue actions until both are available and the media source action can process. 49809 49810 return; 49811 } 49812 if (type === 'mediaSource') { 49813 // If the queue was shifted by a media source action (this happens when pushing a 49814 // media source action onto the queue), then it wasn't from an updateend event from an 49815 // audio or video source buffer, so there's no change from previous state, and no 49816 // processing should be done. 49817 return; 49818 } // Media source queue entries don't need to consider whether the source updater is 49819 // started (i.e., source buffers are created) as they don't need the source buffers, but 49820 // source buffer queue entries do. 49821 49822 if ( 49823 !sourceUpdater.ready() || 49824 sourceUpdater.mediaSource.readyState === 'closed' || 49825 updating(type, sourceUpdater) 49826 ) { 49827 return; 49828 } 49829 if (queueEntry.type !== type) { 49830 queueIndex = nextQueueIndexOfType(type, sourceUpdater.queue); 49831 if (queueIndex === null) { 49832 // Either there's no queue entry that uses this source buffer type in the queue, or 49833 // there's a media source queue entry before the next entry of this type, in which 49834 // case wait for that action to process first. 49835 return; 49836 } 49837 queueEntry = sourceUpdater.queue[queueIndex]; 49838 } 49839 sourceUpdater.queue.splice(queueIndex, 1); // Keep a record that this source buffer type is in use. 49840 // 49841 // The queue pending operation must be set before the action is performed in the event 49842 // that the action results in a synchronous event that is acted upon. For instance, if 49843 // an exception is thrown that can be handled, it's possible that new actions will be 49844 // appended to an empty queue and immediately executed, but would not have the correct 49845 // pending information if this property was set after the action was performed. 49846 49847 sourceUpdater.queuePending[type] = queueEntry; 49848 queueEntry.action(type, sourceUpdater); 49849 if (!queueEntry.doneFn) { 49850 // synchronous operation, process next entry 49851 sourceUpdater.queuePending[type] = null; 49852 shiftQueue(type, sourceUpdater); 49853 return; 49854 } 49855}; 49856const cleanupBuffer = (type, sourceUpdater) => { 49857 const buffer = sourceUpdater[`${type}Buffer`]; 49858 const titleType = toTitleCase(type); 49859 if (!buffer) { 49860 return; 49861 } 49862 buffer.removeEventListener('updateend', sourceUpdater[`on${titleType}UpdateEnd_`]); 49863 buffer.removeEventListener('error', sourceUpdater[`on${titleType}Error_`]); 49864 sourceUpdater.codecs[type] = null; 49865 sourceUpdater[`${type}Buffer`] = null; 49866}; 49867const inSourceBuffers = (mediaSource, sourceBuffer) => 49868 mediaSource && 49869 sourceBuffer && 49870 Array.prototype.indexOf.call(mediaSource.sourceBuffers, sourceBuffer) !== -1; 49871const actions = { 49872 appendBuffer: (bytes, segmentInfo, onError) => (type, sourceUpdater) => { 49873 const sourceBuffer = sourceUpdater[`${type}Buffer`]; // can't do anything if the media source / source buffer is null 49874 // or the media source does not contain this source buffer. 49875 49876 if (!inSourceBuffers(sourceUpdater.mediaSource, sourceBuffer)) { 49877 return; 49878 } 49879 sourceUpdater.logger_( 49880 `Appending segment ${segmentInfo.mediaIndex}'s ${bytes.length} bytes to ${type}Buffer` 49881 ); 49882 try { 49883 sourceBuffer.appendBuffer(bytes); 49884 } catch (e) { 49885 sourceUpdater.logger_( 49886 `Error with code ${e.code} ` + 49887 (e.code === QUOTA_EXCEEDED_ERR ? '(QUOTA_EXCEEDED_ERR) ' : '') + 49888 `when appending segment ${segmentInfo.mediaIndex} to ${type}Buffer` 49889 ); 49890 sourceUpdater.queuePending[type] = null; 49891 onError(e); 49892 } 49893 }, 49894 remove: (start, end) => (type, sourceUpdater) => { 49895 const sourceBuffer = sourceUpdater[`${type}Buffer`]; // can't do anything if the media source / source buffer is null 49896 // or the media source does not contain this source buffer. 49897 49898 if (!inSourceBuffers(sourceUpdater.mediaSource, sourceBuffer)) { 49899 return; 49900 } 49901 sourceUpdater.logger_(`Removing ${start} to ${end} from ${type}Buffer`); 49902 try { 49903 sourceBuffer.remove(start, end); 49904 } catch (e) { 49905 sourceUpdater.logger_(`Remove ${start} to ${end} from ${type}Buffer failed`); 49906 } 49907 }, 49908 timestampOffset: (offset) => (type, sourceUpdater) => { 49909 const sourceBuffer = sourceUpdater[`${type}Buffer`]; // can't do anything if the media source / source buffer is null 49910 // or the media source does not contain this source buffer. 49911 49912 if (!inSourceBuffers(sourceUpdater.mediaSource, sourceBuffer)) { 49913 return; 49914 } 49915 sourceUpdater.logger_(`Setting ${type}timestampOffset to ${offset}`); 49916 sourceBuffer.timestampOffset = offset; 49917 }, 49918 callback: (callback) => (type, sourceUpdater) => { 49919 callback(); 49920 },
49921 endOfStream: (error) => (sourceUpdater) => { 49922 if (sourceUpdater.mediaSource.readyState !== 'open') { 49923 return; 49924 } 49925 sourceUpdater.logger_(`Calling mediaSource endOfStream(${error || ''})`); 49926 try { 49927 sourceUpdater.mediaSource.endOfStream(error); 49928 } catch (e) { 49929 videojs.log.warn('Failed to call media source endOfStream', e); 49930 } 49931 }, 49932 duration: (duration) => (sourceUpdater) => { 49933 sourceUpdater.logger_(`Setting mediaSource duration to ${duration}`); 49934 try { 49935 sourceUpdater.mediaSource.duration = duration; 49936 } catch (e) { 49937 videojs.log.warn('Failed to set media source duration', e); 49938 } 49939 }, 49940 abort: () => (type, sourceUpdater) => { 49941 if (sourceUpdater.mediaSource.readyState !== 'open') { 49942 return; 49943 } 49944 const sourceBuffer = sourceUpdater[`${type}Buffer`]; // can't do anything if the media source / source buffer is null 49945 // or the media source does not contain this source buffer. 49946 49947 if (!inSourceBuffers(sourceUpdater.mediaSource, sourceBuffer)) { 49948 return; 49949 } 49950 sourceUpdater.logger_(`calling abort on ${type}Buffer`); 49951 try { 49952 sourceBuffer.abort(); 49953 } catch (e) { 49954 videojs.log.warn(`Failed to abort on ${type}Buffer`, e); 49955 } 49956 }, 49957 addSourceBuffer: (type, codec) => (sourceUpdater) => { 49958 const titleType = toTitleCase(type); 49959 const mime = codecs_js.getMimeForCodec(codec); 49960 sourceUpdater.logger_(`Adding ${type}Buffer with codec ${codec} to mediaSource`); 49961 const sourceBuffer = sourceUpdater.mediaSource.addSourceBuffer(mime); 49962 sourceBuffer.addEventListener('updateend', sourceUpdater[`on${titleType}UpdateEnd_`]); 49963 sourceBuffer.addEventListener('error', sourceUpdater[`on${titleType}Error_`]); 49964 sourceUpdater.codecs[type] = codec; 49965 sourceUpdater[`${type}Buffer`] = sourceBuffer; 49966 }, 49967 removeSourceBuffer: (type) => (sourceUpdater) => { 49968 const sourceBuffer = sourceUpdater[`${type}Buffer`]; 49969 cleanupBuffer(type, sourceUpdater); // can't do anything if the media source / source buffer is null 49970 // or the media source does not contain this source buffer. 49971 49972 if (!inSourceBuffers(sourceUpdater.mediaSource, sourceBuffer)) { 49973 return; 49974 } 49975 sourceUpdater.logger_( 49976 `Removing ${type}Buffer with codec ${sourceUpdater.codecs[type]} from mediaSource` 49977 ); 49978 try { 49979 sourceUpdater.mediaSource.removeSourceBuffer(sourceBuffer); 49980 } catch (e) { 49981 videojs.log.warn(`Failed to removeSourceBuffer ${type}Buffer`, e); 49982 } 49983 }, 49984 changeType: (codec) => (type, sourceUpdater) => { 49985 const sourceBuffer = sourceUpdater[`${type}Buffer`]; 49986 const mime = codecs_js.getMimeForCodec(codec); // can't do anything if the media source / source buffer is null 49987 // or the media source does not contain this source buffer. 49988 49989 if (!inSourceBuffers(sourceUpdater.mediaSource, sourceBuffer)) { 49990 return; 49991 } // do not update codec if we don't need to. 49992 // Only update if we change the codec base. 49993 // For example, going from avc1.640028 to avc1.64001f does not require a changeType call. 49994 49995 const newCodecBase = codec.substring(0, codec.indexOf('.')); 49996 const oldCodec = sourceUpdater.codecs[type]; 49997 const oldCodecBase = oldCodec.substring(0, oldCodec.indexOf('.')); 49998 if (oldCodecBase === newCodecBase) { 49999 return; 50000 } 50001 const metadata = { 50002 codecsChangeInfo: { 50003 from: oldCodec, 50004 to: codec, 50005 }, 50006 }; 50007 sourceUpdater.trigger({ 50008 type: 'codecschange', 50009 metadata, 50010 }); 50011 sourceUpdater.logger_(`changing ${type}Buffer codec from ${oldCodec} to ${codec}`); // check if change to the provided type is supported 50012 50013 try { 50014 sourceBuffer.changeType(mime); 50015 sourceUpdater.codecs[type] = codec; 50016 } catch (e) { 50017 metadata.errorType = videojs.Error.StreamingCodecsChangeError; 50018 metadata.error = e; 50019 e.metadata = metadata; 50020 sourceUpdater.error_ = e; 50021 sourceUpdater.trigger('error'); 50022 videojs.log.warn(`Failed to changeType on ${type}Buffer`, e); 50023 } 50024 }, 50025}; 50026const pushQueue = ({ type, sourceUpdater, action, doneFn, name }) => { 50027 sourceUpdater.queue.push({ 50028 type, 50029 action, 50030 doneFn, 50031 name, 50032 }); 50033 shiftQueue(type, sourceUpdater); 50034}; 50035const onUpdateend = (type, sourceUpdater) => (e) => { 50036 // Although there should, in theory, be a pending action for any updateend receieved, 50037 // there are some actions that may trigger updateend events without set definitions in 50038 // the w3c spec. For instance, setting the duration on the media source may trigger 50039 // updateend events on source buffers. This does not appear to be in the spec. As such, 50040 // if we encounter an updateend without a corresponding pending action from our queue 50041 // for that source buffer type, process the next action.
50042 const bufferedRangesForType = sourceUpdater[`${type}Buffered`](); 50043 const descriptiveString = bufferedRangesToString(bufferedRangesForType); 50044 sourceUpdater.logger_( 50045 `received "updateend" event for ${type} Source Buffer: `, 50046 descriptiveString 50047 ); 50048 if (sourceUpdater.queuePending[type]) { 50049 const doneFn = sourceUpdater.queuePending[type].doneFn; 50050 sourceUpdater.queuePending[type] = null; 50051 if (doneFn) { 50052 // if there's an error, report it 50053 doneFn(sourceUpdater[`${type}Error_`]); 50054 } 50055 } 50056 shiftQueue(type, sourceUpdater); 50057}; 50058/** 50059 * A queue of callbacks to be serialized and applied when a 50060 * MediaSource and its associated SourceBuffers are not in the 50061 * updating state. It is used by the segment loader to update the 50062 * underlying SourceBuffers when new data is loaded, for instance. 50063 * 50064 * @class SourceUpdater 50065 * @param {MediaSource} mediaSource the MediaSource to create the SourceBuffer from 50066 * @param {string} mimeType the desired MIME type of the underlying SourceBuffer 50067 */ 50068 50069class SourceUpdater extends videojs.EventTarget { 50070 constructor(mediaSource) { 50071 super(); 50072 this.mediaSource = mediaSource; 50073 this.sourceopenListener_ = () => shiftQueue('mediaSource', this); 50074 this.mediaSource.addEventListener('sourceopen', this.sourceopenListener_); 50075 this.logger_ = logger('SourceUpdater'); // initial timestamp offset is 0 50076 50077 this.audioTimestampOffset_ = 0; 50078 this.videoTimestampOffset_ = 0; 50079 this.queue = []; 50080 this.queuePending = { 50081 audio: null, 50082 video: null, 50083 }; 50084 this.delayedAudioAppendQueue_ = []; 50085 this.videoAppendQueued_ = false; 50086 this.codecs = {}; 50087 this.onVideoUpdateEnd_ = onUpdateend('video', this); 50088 this.onAudioUpdateEnd_ = onUpdateend('audio', this); 50089 this.onVideoError_ = (e) => { 50090 // used for debugging 50091 this.videoError_ = e; 50092 }; 50093 this.onAudioError_ = (e) => { 50094 // used for debugging 50095 this.audioError_ = e; 50096 }; 50097 this.createdSourceBuffers_ = false; 50098 this.initializedEme_ = false; 50099 this.triggeredReady_ = false; 50100 } 50101 initializedEme() { 50102 this.initializedEme_ = true; 50103 this.triggerReady(); 50104 } 50105 hasCreatedSourceBuffers() { 50106 // if false, likely waiting on one of the segment loaders to get enough data to create 50107 // source buffers 50108 return this.createdSourceBuffers_; 50109 } 50110 hasInitializedAnyEme() { 50111 return this.initializedEme_; 50112 } 50113 ready() { 50114 return this.hasCreatedSourceBuffers() && this.hasInitializedAnyEme(); 50115 } 50116 createSourceBuffers(codecs) { 50117 if (this.hasCreatedSourceBuffers()) { 50118 // already created them before 50119 return; 50120 } // the intial addOrChangeSourceBuffers will always be 50121 // two add buffers. 50122 50123 this.addOrChangeSourceBuffers(codecs); 50124 this.createdSourceBuffers_ = true; 50125 this.trigger('createdsourcebuffers'); 50126 this.triggerReady(); 50127 } 50128 triggerReady() { 50129 // only allow ready to be triggered once, this prevents the case 50130 // where: 50131 // 1. we trigger createdsourcebuffers 50132 // 2. ie 11 synchronously initializates eme 50133 // 3. the synchronous initialization causes us to trigger ready 50134 // 4. We go back to the ready check in createSourceBuffers and ready is triggered again. 50135 if (this.ready() && !this.triggeredReady_) { 50136 this.triggeredReady_ = true; 50137 this.trigger('ready'); 50138 } 50139 } 50140 /** 50141 * Add a type of source buffer to the media source. 50142 * 50143 * @param {string} type 50144 * The type of source buffer to add. 50145 * 50146 * @param {string} codec 50147 * The codec to add the source buffer with. 50148 */ 50149 50150 addSourceBuffer(type, codec) { 50151 pushQueue({ 50152 type: 'mediaSource', 50153 sourceUpdater: this, 50154 action: actions.addSourceBuffer(type, codec), 50155 name: 'addSourceBuffer', 50156 }); 50157 } 50158 /** 50159 * call abort on a source buffer. 50160 * 50161 * @param {string} type 50162 * The type of source buffer to call abort on. 50163 */ 50164 50165 abort(type) { 50166 pushQueue({ 50167 type, 50168 sourceUpdater: this, 50169 action: actions.abort(type), 50170 name: 'abort', 50171 }); 50172 } 50173 /** 50174 * Call removeSourceBuffer and remove a specific type 50175 * of source buffer on the mediaSource. 50176 * 50177 * @param {string} type 50178 * The type of source buffer to remove. 50179 */ 50180 50181 removeSourceBuffer(type) { 50182 if (!this.canRemoveSourceBuffer()) { 50183 videojs.log.error('removeSourceBuffer is not supported!'); 50184 return; 50185 } 50186 pushQueue({ 50187 type: 'mediaSource', 50188 sourceUpdater: this, 50189 action: actions.removeSourceBuffer(type), 50190 name: 'removeSourceBuffer', 50191 }); 50192 } 50193 /** 50194 * Whether or not the removeSourceBuffer function is supported 50195 * on the mediaSource. 50196 * 50197 * @return {boolean} 50198 * if removeSourceBuffer can be called. 50199 */ 50200 50201 canRemoveSourceBuffer() { 50202 // As of Firefox 83 removeSourceBuffer 50203 // throws errors, so we report that it does not support this. 50204 return ( 50205 !videojs.browser.IS_FIREFOX && 50206 window__default['default'].MediaSource && 50207 window__default['default'].MediaSource.prototype && 50208 typeof window__default['default'].MediaSource.prototype.removeSourceBuffer === 'function' 50209 ); 50210 } 50211 /** 50212 * Whether or not the changeType function is supported 50213 * on our SourceBuffers. 50214 * 50215 * @return {boolean} 50216 * if changeType can be called. 50217 */ 50218 50219 static canChangeType() { 50220 return ( 50221 window__default['default'].SourceBuffer && 50222 window__default['default'].SourceBuffer.prototype && 50223 typeof window__default['default'].SourceBuffer.prototype.changeType === 'function' 50224 ); 50225 } 50226 /** 50227 * Whether or not the changeType function is supported 50228 * on our SourceBuffers. 50229 * 50230 * @return {boolean} 50231 * if changeType can be called. 50232 */ 50233 50234 canChangeType() { 50235 return this.constructor.canChangeType(); 50236 } 50237 /** 50238 * Call the changeType function on a source buffer, given the code and type. 50239 * 50240 * @param {string} type 50241 * The type of source buffer to call changeType on. 50242 * 50243 * @param {string} codec 50244 * The codec string to change type with on the source buffer. 50245 */ 50246
vendor: 18,343 bytes, lines 50247-50843
50247 changeType(type, codec) { 50248 if (!this.canChangeType()) { 50249 videojs.log.error('changeType is not supported!'); 50250 return; 50251 } 50252 pushQueue({ 50253 type, 50254 sourceUpdater: this, 50255 action: actions.changeType(codec), 50256 name: 'changeType', 50257 }); 50258 } 50259 /** 50260 * Add source buffers with a codec or, if they are already created, 50261 * call changeType on source buffers using changeType. 50262 * 50263 * @param {Object} codecs 50264 * Codecs to switch to 50265 */ 50266 50267 addOrChangeSourceBuffers(codecs) { 50268 if (!codecs || typeof codecs !== 'object' || Object.keys(codecs).length === 0) { 50269 throw new Error('Cannot addOrChangeSourceBuffers to undefined codecs'); 50270 } 50271 Object.keys(codecs).forEach((type) => { 50272 const codec = codecs[type]; 50273 if (!this.hasCreatedSourceBuffers()) { 50274 return this.addSourceBuffer(type, codec); 50275 } 50276 if (this.canChangeType()) { 50277 this.changeType(type, codec); 50278 } 50279 }); 50280 } 50281 /** 50282 * Queue an update to append an ArrayBuffer. 50283 * 50284 * @param {MediaObject} object containing audioBytes and/or videoBytes 50285 * @param {Function} done the function to call when done 50286 * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-appendBuffer-void-ArrayBuffer-data 50287 */ 50288 50289 appendBuffer(options, doneFn) { 50290 const { segmentInfo, type, bytes } = options; 50291 this.processedAppend_ = true; 50292 if (type === 'audio' && this.videoBuffer && !this.videoAppendQueued_) { 50293 this.delayedAudioAppendQueue_.push([options, doneFn]); 50294 this.logger_(`delayed audio append of ${bytes.length} until video append`); 50295 return; 50296 } // In the case of certain errors, for instance, QUOTA_EXCEEDED_ERR, updateend will 50297 // not be fired. This means that the queue will be blocked until the next action 50298 // taken by the segment-loader. Provide a mechanism for segment-loader to handle 50299 // these errors by calling the doneFn with the specific error. 50300 50301 const onError = doneFn; 50302 pushQueue({ 50303 type, 50304 sourceUpdater: this, 50305 action: actions.appendBuffer( 50306 bytes, 50307 segmentInfo || { 50308 mediaIndex: -1, 50309 }, 50310 onError 50311 ), 50312 doneFn, 50313 name: 'appendBuffer', 50314 }); 50315 if (type === 'video') { 50316 this.videoAppendQueued_ = true; 50317 if (!this.delayedAudioAppendQueue_.length) { 50318 return; 50319 } 50320 const queue = this.delayedAudioAppendQueue_.slice(); 50321 this.logger_(`queuing delayed audio ${queue.length} appendBuffers`); 50322 this.delayedAudioAppendQueue_.length = 0; 50323 queue.forEach((que) => { 50324 this.appendBuffer.apply(this, que); 50325 }); 50326 } 50327 } 50328 /** 50329 * Get the audio buffer's buffered timerange. 50330 * 50331 * @return {TimeRange} 50332 * The audio buffer's buffered time range 50333 */ 50334 50335 audioBuffered() { 50336 // no media source/source buffer or it isn't in the media sources 50337 // source buffer list 50338 if (!inSourceBuffers(this.mediaSource, this.audioBuffer)) { 50339 return createTimeRanges(); 50340 } 50341 return this.audioBuffer.buffered ? this.audioBuffer.buffered : createTimeRanges(); 50342 } 50343 /** 50344 * Get the video buffer's buffered timerange. 50345 * 50346 * @return {TimeRange} 50347 * The video buffer's buffered time range 50348 */ 50349 50350 videoBuffered() { 50351 // no media source/source buffer or it isn't in the media sources 50352 // source buffer list 50353 if (!inSourceBuffers(this.mediaSource, this.videoBuffer)) { 50354 return createTimeRanges(); 50355 } 50356 return this.videoBuffer.buffered ? this.videoBuffer.buffered : createTimeRanges(); 50357 } 50358 /** 50359 * Get a combined video/audio buffer's buffered timerange. 50360 * 50361 * @return {TimeRange} 50362 * the combined time range 50363 */ 50364 50365 buffered() { 50366 const video = inSourceBuffers(this.mediaSource, this.videoBuffer) ? this.videoBuffer : null; 50367 const audio = inSourceBuffers(this.mediaSource, this.audioBuffer) ? this.audioBuffer : null; 50368 if (audio && !video) { 50369 return this.audioBuffered(); 50370 } 50371 if (video && !audio) { 50372 return this.videoBuffered(); 50373 } 50374 return bufferIntersection(this.audioBuffered(), this.videoBuffered()); 50375 } 50376 /** 50377 * Add a callback to the queue that will set duration on the mediaSource. 50378 * 50379 * @param {number} duration 50380 * The duration to set 50381 * 50382 * @param {Function} [doneFn] 50383 * function to run after duration has been set. 50384 */ 50385 50386 setDuration(duration, doneFn = noop) { 50387 // In order to set the duration on the media source, it's necessary to wait for all 50388 // source buffers to no longer be updating. "If the updating attribute equals true on 50389 // any SourceBuffer in sourceBuffers, then throw an InvalidStateError exception and 50390 // abort these steps." (source: https://www.w3.org/TR/media-source/#attributes). 50391 pushQueue({ 50392 type: 'mediaSource', 50393 sourceUpdater: this, 50394 action: actions.duration(duration), 50395 name: 'duration', 50396 doneFn, 50397 }); 50398 } 50399 /** 50400 * Add a mediaSource endOfStream call to the queue 50401 * 50402 * @param {Error} [error] 50403 * Call endOfStream with an error 50404 * 50405 * @param {Function} [doneFn] 50406 * A function that should be called when the 50407 * endOfStream call has finished. 50408 */ 50409 50410 endOfStream(error = null, doneFn = noop) { 50411 if (typeof error !== 'string') { 50412 error = undefined; 50413 } // In order to set the duration on the media source, it's necessary to wait for all 50414 // source buffers to no longer be updating. "If the updating attribute equals true on 50415 // any SourceBuffer in sourceBuffers, then throw an InvalidStateError exception and 50416 // abort these steps." (source: https://www.w3.org/TR/media-source/#attributes). 50417 50418 pushQueue({ 50419 type: 'mediaSource', 50420 sourceUpdater: this, 50421 action: actions.endOfStream(error), 50422 name: 'endOfStream', 50423 doneFn, 50424 }); 50425 } 50426 /** 50427 * Queue an update to remove a time range from the buffer. 50428 * 50429 * @param {number} start where to start the removal 50430 * @param {number} end where to end the removal 50431 * @param {Function} [done=noop] optional callback to be executed when the remove 50432 * operation is complete 50433 * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-remove-void-double-start-unrestricted-double-end 50434 */ 50435 50436 removeAudio(start, end, done = noop) { 50437 if (!this.audioBuffered().length || this.audioBuffered().end(0) === 0) { 50438 done(); 50439 return; 50440 } 50441 pushQueue({ 50442 type: 'audio', 50443 sourceUpdater: this, 50444 action: actions.remove(start, end), 50445 doneFn: done, 50446 name: 'remove', 50447 }); 50448 } 50449 /** 50450 * Queue an update to remove a time range from the buffer. 50451 * 50452 * @param {number} start where to start the removal 50453 * @param {number} end where to end the removal 50454 * @param {Function} [done=noop] optional callback to be executed when the remove 50455 * operation is complete 50456 * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-remove-void-double-start-unrestricted-double-end 50457 */ 50458 50459 removeVideo(start, end, done = noop) { 50460 if (!this.videoBuffered().length || this.videoBuffered().end(0) === 0) { 50461 done(); 50462 return; 50463 } 50464 pushQueue({ 50465 type: 'video', 50466 sourceUpdater: this, 50467 action: actions.remove(start, end), 50468 doneFn: done, 50469 name: 'remove', 50470 }); 50471 } 50472 /** 50473 * Whether the underlying sourceBuffer is updating or not 50474 * 50475 * @return {boolean} the updating status of the SourceBuffer 50476 */ 50477 50478 updating() { 50479 // the audio/video source buffer is updating 50480 if (updating('audio', this) || updating('video', this)) { 50481 return true; 50482 } 50483 return false; 50484 } 50485 /** 50486 * Set/get the timestampoffset on the audio SourceBuffer 50487 * 50488 * @return {number} the timestamp offset 50489 */ 50490 50491 audioTimestampOffset(offset) { 50492 if ( 50493 typeof offset !== 'undefined' && 50494 this.audioBuffer && 50495 // no point in updating if it's the same 50496 this.audioTimestampOffset_ !== offset 50497 ) { 50498 pushQueue({ 50499 type: 'audio', 50500 sourceUpdater: this, 50501 action: actions.timestampOffset(offset), 50502 name: 'timestampOffset', 50503 }); 50504 this.audioTimestampOffset_ = offset; 50505 } 50506 return this.audioTimestampOffset_; 50507 } 50508 /** 50509 * Set/get the timestampoffset on the video SourceBuffer 50510 * 50511 * @return {number} the timestamp offset 50512 */ 50513 50514 videoTimestampOffset(offset) { 50515 if ( 50516 typeof offset !== 'undefined' && 50517 this.videoBuffer && 50518 // no point in updating if it's the same 50519 this.videoTimestampOffset_ !== offset 50520 ) { 50521 pushQueue({ 50522 type: 'video', 50523 sourceUpdater: this, 50524 action: actions.timestampOffset(offset), 50525 name: 'timestampOffset', 50526 }); 50527 this.videoTimestampOffset_ = offset; 50528 } 50529 return this.videoTimestampOffset_; 50530 } 50531 /** 50532 * Add a function to the queue that will be called 50533 * when it is its turn to run in the audio queue. 50534 * 50535 * @param {Function} callback 50536 * The callback to queue. 50537 */ 50538 50539 audioQueueCallback(callback) { 50540 if (!this.audioBuffer) { 50541 return; 50542 } 50543 pushQueue({ 50544 type: 'audio', 50545 sourceUpdater: this, 50546 action: actions.callback(callback), 50547 name: 'callback', 50548 }); 50549 } 50550 /** 50551 * Add a function to the queue that will be called 50552 * when it is its turn to run in the video queue. 50553 * 50554 * @param {Function} callback 50555 * The callback to queue. 50556 */ 50557 50558 videoQueueCallback(callback) { 50559 if (!this.videoBuffer) { 50560 return; 50561 } 50562 pushQueue({ 50563 type: 'video', 50564 sourceUpdater: this, 50565 action: actions.callback(callback), 50566 name: 'callback', 50567 }); 50568 } 50569 /** 50570 * dispose of the source updater and the underlying sourceBuffer 50571 */ 50572 50573 dispose() { 50574 this.trigger('dispose'); 50575 bufferTypes.forEach((type) => { 50576 this.abort(type); 50577 if (this.canRemoveSourceBuffer()) { 50578 this.removeSourceBuffer(type); 50579 } else { 50580 this[`${type}QueueCallback`](() => cleanupBuffer(type, this)); 50581 } 50582 }); 50583 this.videoAppendQueued_ = false; 50584 this.delayedAudioAppendQueue_.length = 0; 50585 if (this.sourceopenListener_) { 50586 this.mediaSource.removeEventListener('sourceopen', this.sourceopenListener_); 50587 } 50588 this.off(); 50589 } 50590} 50591const uint8ToUtf8 = (uintArray) => 50592 decodeURIComponent(escape(String.fromCharCode.apply(null, uintArray))); 50593const bufferToHexString = (buffer) => { 50594 const uInt8Buffer = new Uint8Array(buffer); 50595 return Array.from(uInt8Buffer) 50596 .map((byte) => byte.toString(16).padStart(2, '0')) 50597 .join(''); 50598}; 50599 50600/** 50601 * @file vtt-segment-loader.js 50602 */ 50603const VTT_LINE_TERMINATORS = new Uint8Array('\n\n'.split('').map((char) => char.charCodeAt(0))); 50604class NoVttJsError extends Error { 50605 constructor() { 50606 super('Trying to parse received VTT cues, but there is no WebVTT. Make sure vtt.js is loaded.'); 50607 } 50608} 50609/** 50610 * An object that manages segment loading and appending. 50611 * 50612 * @class VTTSegmentLoader 50613 * @param {Object} options required and optional options 50614 * @extends videojs.EventTarget 50615 */ 50616 50617class VTTSegmentLoader extends SegmentLoader { 50618 constructor(settings, options = {}) { 50619 super(settings, options); // SegmentLoader requires a MediaSource be specified or it will throw an error; 50620 // however, VTTSegmentLoader has no need of a media source, so delete the reference 50621 50622 this.mediaSource_ = null; 50623 this.subtitlesTrack_ = null; 50624 this.featuresNativeTextTracks_ = settings.featuresNativeTextTracks; 50625 this.loadVttJs = settings.loadVttJs; // The VTT segment will have its own time mappings. Saving VTT segment timing info in 50626 // the sync controller leads to improper behavior. 50627 50628 this.shouldSaveSegmentTimingInfo_ = false; 50629 } 50630 /** 50631 * Indicates which time ranges are buffered 50632 * 50633 * @return {TimeRange} 50634 * TimeRange object representing the current buffered ranges 50635 */ 50636 50637 buffered_() { 50638 if (!this.subtitlesTrack_ || !this.subtitlesTrack_.cues || !this.subtitlesTrack_.cues.length) { 50639 return createTimeRanges(); 50640 } 50641 const cues = this.subtitlesTrack_.cues; 50642 const start = cues[0].startTime; 50643 const end = cues[cues.length - 1].startTime; 50644 return createTimeRanges([[start, end]]); 50645 } 50646 /** 50647 * Gets and sets init segment for the provided map 50648 * 50649 * @param {Object} map 50650 * The map object representing the init segment to get or set 50651 * @param {boolean=} set 50652 * If true, the init segment for the provided map should be saved 50653 * @return {Object} 50654 * map object for desired init segment 50655 */ 50656 50657 initSegmentForMap(map, set = false) { 50658 if (!map) { 50659 return null; 50660 } 50661 const id = initSegmentId(map); 50662 let storedMap = this.initSegments_[id]; 50663 if (set && !storedMap && map.bytes) { 50664 // append WebVTT line terminators to the media initialization segment if it exists 50665 // to follow the WebVTT spec (https://w3c.github.io/webvtt/#file-structure) that 50666 // requires two or more WebVTT line terminators between the WebVTT header and the 50667 // rest of the file 50668 const combinedByteLength = VTT_LINE_TERMINATORS.byteLength + map.bytes.byteLength; 50669 const combinedSegment = new Uint8Array(combinedByteLength); 50670 combinedSegment.set(map.bytes); 50671 combinedSegment.set(VTT_LINE_TERMINATORS, map.bytes.byteLength); 50672 this.initSegments_[id] = storedMap = { 50673 resolvedUri: map.resolvedUri, 50674 byterange: map.byterange, 50675 bytes: combinedSegment, 50676 }; 50677 } 50678 return storedMap || map; 50679 } 50680 /** 50681 * Returns true if all configuration required for loading is present, otherwise false. 50682 * 50683 * @return {boolean} True if the all configuration is ready for loading 50684 * @private 50685 */ 50686 50687 couldBeginLoading_() { 50688 return this.playlist_ && this.subtitlesTrack_ && !this.paused(); 50689 } 50690 /** 50691 * Once all the starting parameters have been specified, begin 50692 * operation. This method should only be invoked from the INIT 50693 * state. 50694 * 50695 * @private 50696 */ 50697 50698 init_() { 50699 this.state = 'READY'; 50700 this.resetEverything(); 50701 return this.monitorBuffer_(); 50702 } 50703 /** 50704 * Set a subtitle track on the segment loader to add subtitles to 50705 * 50706 * @param {TextTrack=} track 50707 * The text track to add loaded subtitles to 50708 * @return {TextTrack} 50709 * Returns the subtitles track 50710 */ 50711 50712 track(track) { 50713 if (typeof track === 'undefined') { 50714 return this.subtitlesTrack_; 50715 } 50716 this.subtitlesTrack_ = track; // if we were unpaused but waiting for a sourceUpdater, start 50717 // buffering now 50718 50719 if (this.state === 'INIT' && this.couldBeginLoading_()) { 50720 this.init_(); 50721 } 50722 return this.subtitlesTrack_; 50723 } 50724 /** 50725 * Remove any data in the source buffer between start and end times 50726 * 50727 * @param {number} start - the start time of the region to remove from the buffer 50728 * @param {number} end - the end time of the region to remove from the buffer 50729 */ 50730 50731 remove(start, end) { 50732 removeCuesFromTrack(start, end, this.subtitlesTrack_); 50733 } 50734 /** 50735 * fill the buffer with segements unless the sourceBuffers are 50736 * currently updating 50737 * 50738 * Note: this function should only ever be called by monitorBuffer_ 50739 * and never directly 50740 * 50741 * @private 50742 */ 50743 50744 fillBuffer_() { 50745 // see if we need to begin loading immediately 50746 const segmentInfo = this.chooseNextRequest_(); 50747 if (!segmentInfo) { 50748 return; 50749 } 50750 if (this.syncController_.timestampOffsetForTimeline(segmentInfo.timeline) === null) { 50751 // We don't have the timestamp offset that we need to sync subtitles. 50752 // Rerun on a timestamp offset or user interaction. 50753 const checkTimestampOffset = () => { 50754 this.state = 'READY'; 50755 if (!this.paused()) { 50756 // if not paused, queue a buffer check as soon as possible 50757 this.monitorBuffer_(); 50758 } 50759 }; 50760 this.syncController_.one('timestampoffset', checkTimestampOffset); 50761 this.state = 'WAITING_ON_TIMELINE'; 50762 return; 50763 } 50764 this.loadSegment_(segmentInfo); 50765 } // never set a timestamp offset for vtt segments. 50766 50767 timestampOffsetForSegment_() { 50768 return null; 50769 } 50770 chooseNextRequest_() { 50771 return this.skipEmptySegments_(super.chooseNextRequest_()); 50772 } 50773 /** 50774 * Prevents the segment loader from requesting segments we know contain no subtitles 50775 * by walking forward until we find the next segment that we don't know whether it is 50776 * empty or not. 50777 * 50778 * @param {Object} segmentInfo 50779 * a segment info object that describes the current segment 50780 * @return {Object} 50781 * a segment info object that describes the current segment 50782 */ 50783 50784 skipEmptySegments_(segmentInfo) { 50785 while (segmentInfo && segmentInfo.segment.empty) { 50786 // stop at the last possible segmentInfo 50787 if (segmentInfo.mediaIndex + 1 >= segmentInfo.playlist.segments.length) { 50788 segmentInfo = null; 50789 break; 50790 } 50791 segmentInfo = this.generateSegmentInfo_({ 50792 playlist: segmentInfo.playlist, 50793 mediaIndex: segmentInfo.mediaIndex + 1, 50794 startOfSegment: segmentInfo.startOfSegment + segmentInfo.duration, 50795 isSyncRequest: segmentInfo.isSyncRequest, 50796 }); 50797 } 50798 return segmentInfo; 50799 } 50800 stopForError(error) { 50801 this.error(error); 50802 this.state = 'READY'; 50803 this.pause(); 50804 this.trigger('error'); 50805 } 50806 /** 50807 * append a decrypted segement to the SourceBuffer through a SourceUpdater 50808 * 50809 * @private 50810 */ 50811 50812 segmentRequestFinished_(error, simpleSegment, result) { 50813 if (!this.subtitlesTrack_) { 50814 this.state = 'READY'; 50815 return; 50816 } 50817 this.saveTransferStats_(simpleSegment.stats); // the request was aborted 50818 50819 if (!this.pendingSegment_) { 50820 this.state = 'READY'; 50821 this.mediaRequestsAborted += 1; 50822 return; 50823 } 50824 if (error) { 50825 if (error.code === REQUEST_ERRORS.TIMEOUT) { 50826 this.handleTimeout_(); 50827 } 50828 if (error.code === REQUEST_ERRORS.ABORTED) { 50829 this.mediaRequestsAborted += 1; 50830 } else { 50831 this.mediaRequestsErrored += 1; 50832 } 50833 this.stopForError(error); 50834 return; 50835 } 50836 const segmentInfo = this.pendingSegment_; 50837 const isMp4WebVttSegmentWithCues = result.mp4VttCues && result.mp4VttCues.length; 50838 if (isMp4WebVttSegmentWithCues) { 50839 segmentInfo.mp4VttCues = result.mp4VttCues; 50840 } // although the VTT segment loader bandwidth isn't really used, it's good to 50841 // maintain functionality between segment loaders 50842 50843 this.saveBandwidthRelatedStats_(segmentInfo.duration, simpleSegment.stats);
vendor: 12,870 bytes, lines 50843-51225
50843 // if this request included a segment key, save that data in the cache 50844 50845 if (simpleSegment.key) { 50846 this.segmentKey(simpleSegment.key, true); 50847 } 50848 this.state = 'APPENDING'; // used for tests 50849 50850 this.trigger('appending'); 50851 const segment = segmentInfo.segment; 50852 if (segment.map) { 50853 segment.map.bytes = simpleSegment.map.bytes; 50854 } 50855 segmentInfo.bytes = simpleSegment.bytes; // Make sure that vttjs has loaded, otherwise, load it and wait till it finished loading 50856 50857 if ( 50858 typeof window__default['default'].WebVTT !== 'function' && 50859 typeof this.loadVttJs === 'function' 50860 ) { 50861 this.state = 'WAITING_ON_VTTJS'; // should be fine to call multiple times 50862 // script will be loaded once but multiple listeners will be added to the queue, which is expected. 50863 50864 this.loadVttJs().then( 50865 () => this.segmentRequestFinished_(error, simpleSegment, result), 50866 () => 50867 this.stopForError({ 50868 message: 'Error loading vtt.js', 50869 }) 50870 ); 50871 return; 50872 } 50873 segment.requested = true; 50874 try { 50875 this.parseVTTCues_(segmentInfo); 50876 } catch (e) { 50877 this.stopForError({ 50878 message: e.message, 50879 metadata: { 50880 errorType: videojs.Error.StreamingVttParserError, 50881 error: e, 50882 }, 50883 }); 50884 return; 50885 } 50886 if (!isMp4WebVttSegmentWithCues) { 50887 this.updateTimeMapping_( 50888 segmentInfo, 50889 this.syncController_.timelines[segmentInfo.timeline], 50890 this.playlist_ 50891 ); 50892 } 50893 if (segmentInfo.cues.length) { 50894 segmentInfo.timingInfo = { 50895 start: segmentInfo.cues[0].startTime, 50896 end: segmentInfo.cues[segmentInfo.cues.length - 1].endTime, 50897 }; 50898 } else { 50899 segmentInfo.timingInfo = { 50900 start: segmentInfo.startOfSegment, 50901 end: segmentInfo.startOfSegment + segmentInfo.duration, 50902 }; 50903 } 50904 if (segmentInfo.isSyncRequest) { 50905 this.trigger('syncinfoupdate'); 50906 this.pendingSegment_ = null; 50907 this.state = 'READY'; 50908 return; 50909 } 50910 segmentInfo.byteLength = segmentInfo.bytes.byteLength; 50911 this.mediaSecondsLoaded += segment.duration; // Create VTTCue instances for each cue in the new segment and add them to 50912 // the subtitle track 50913 50914 segmentInfo.cues.forEach((cue) => { 50915 this.subtitlesTrack_.addCue( 50916 this.featuresNativeTextTracks_ 50917 ? new window__default['default'].VTTCue(cue.startTime, cue.endTime, cue.text) 50918 : cue 50919 ); 50920 }); // Remove any duplicate cues from the subtitle track. The WebVTT spec allows 50921 // cues to have identical time-intervals, but if the text is also identical 50922 // we can safely assume it is a duplicate that can be removed (ex. when a cue 50923 // "overlaps" VTT segments) 50924 50925 removeDuplicateCuesFromTrack(this.subtitlesTrack_); 50926 this.handleAppendsDone_(); 50927 } 50928 handleData_(simpleSegment, result) { 50929 const isVttType = simpleSegment && simpleSegment.type === 'vtt'; 50930 const isTextResult = result && result.type === 'text'; 50931 const isFmp4VttSegment = isVttType && isTextResult; // handle segment data for fmp4 encapsulated webvtt 50932 50933 if (isFmp4VttSegment) { 50934 super.handleData_(simpleSegment, result); 50935 } 50936 } 50937 updateTimingInfoEnd_() { 50938 // noop 50939 } 50940 /** 50941 * Utility function for converting mp4 webvtt cue objects into VTTCues. 50942 * 50943 * @param {Object} segmentInfo with mp4 webvtt cues for parsing into VTTCue objecs 50944 */ 50945 50946 parseMp4VttCues_(segmentInfo) { 50947 const timestampOffset = 50948 this.sourceUpdater_.videoTimestampOffset() === null 50949 ? this.sourceUpdater_.audioTimestampOffset() 50950 : this.sourceUpdater_.videoTimestampOffset(); 50951 segmentInfo.mp4VttCues.forEach((cue) => { 50952 const start = cue.start + timestampOffset; 50953 const end = cue.end + timestampOffset; 50954 const vttCue = new window__default['default'].VTTCue(start, end, cue.cueText); 50955 if (cue.settings) { 50956 cue.settings.split(' ').forEach((cueSetting) => { 50957 const keyValString = cueSetting.split(':'); 50958 const key = keyValString[0]; 50959 const value = keyValString[1]; 50960 vttCue[key] = isNaN(value) ? value : Number(value); 50961 }); 50962 } 50963 segmentInfo.cues.push(vttCue); 50964 }); 50965 } 50966 /** 50967 * Uses the WebVTT parser to parse the segment response 50968 * 50969 * @throws NoVttJsError 50970 * 50971 * @param {Object} segmentInfo 50972 * a segment info object that describes the current segment 50973 * @private 50974 */ 50975 50976 parseVTTCues_(segmentInfo) { 50977 let decoder; 50978 let decodeBytesToString = false; 50979 if (typeof window__default['default'].WebVTT !== 'function') { 50980 // caller is responsible for exception handling. 50981 throw new NoVttJsError(); 50982 } 50983 segmentInfo.cues = []; 50984 segmentInfo.timestampmap = { 50985 MPEGTS: 0, 50986 LOCAL: 0, 50987 }; 50988 if (segmentInfo.mp4VttCues) { 50989 this.parseMp4VttCues_(segmentInfo); 50990 return; 50991 } 50992 if (typeof window__default['default'].TextDecoder === 'function') { 50993 decoder = new window__default['default'].TextDecoder('utf8'); 50994 } else { 50995 decoder = window__default['default'].WebVTT.StringDecoder(); 50996 decodeBytesToString = true; 50997 } 50998 const parser = new window__default['default'].WebVTT.Parser( 50999 window__default['default'], 51000 window__default['default'].vttjs, 51001 decoder 51002 ); 51003 parser.oncue = segmentInfo.cues.push.bind(segmentInfo.cues); 51004 parser.ontimestampmap = (map) => { 51005 segmentInfo.timestampmap = map; 51006 }; 51007 parser.onparsingerror = (error) => { 51008 videojs.log.warn('Error encountered when parsing cues: ' + error.message); 51009 }; 51010 if (segmentInfo.segment.map) { 51011 let mapData = segmentInfo.segment.map.bytes; 51012 if (decodeBytesToString) { 51013 mapData = uint8ToUtf8(mapData); 51014 } 51015 parser.parse(mapData); 51016 } 51017 let segmentData = segmentInfo.bytes; 51018 if (decodeBytesToString) { 51019 segmentData = uint8ToUtf8(segmentData); 51020 } 51021 parser.parse(segmentData); 51022 parser.flush(); 51023 } 51024 /** 51025 * Updates the start and end times of any cues parsed by the WebVTT parser using 51026 * the information parsed from the X-TIMESTAMP-MAP header and a TS to media time mapping 51027 * from the SyncController 51028 * 51029 * @param {Object} segmentInfo 51030 * a segment info object that describes the current segment 51031 * @param {Object} mappingObj 51032 * object containing a mapping from TS to media time 51033 * @param {Object} playlist 51034 * the playlist object containing the segment 51035 * @private 51036 */ 51037 51038 updateTimeMapping_(segmentInfo, mappingObj, playlist) { 51039 const segment = segmentInfo.segment; 51040 if (!mappingObj) { 51041 // If the sync controller does not have a mapping of TS to Media Time for the 51042 // timeline, then we don't have enough information to update the cue 51043 // start/end times 51044 return; 51045 } 51046 if (!segmentInfo.cues.length) { 51047 // If there are no cues, we also do not have enough information to figure out 51048 // segment timing. Mark that the segment contains no cues so we don't re-request 51049 // an empty segment. 51050 segment.empty = true; 51051 return; 51052 } 51053 const { MPEGTS, LOCAL } = segmentInfo.timestampmap; 51054 /** 51055 * From the spec: 51056 * The MPEGTS media timestamp MUST use a 90KHz timescale, 51057 * even when non-WebVTT Media Segments use a different timescale. 51058 */ 51059 51060 const mpegTsInSeconds = MPEGTS / clock.ONE_SECOND_IN_TS; 51061 const diff = mpegTsInSeconds - LOCAL + mappingObj.mapping; 51062 segmentInfo.cues.forEach((cue) => { 51063 const duration = cue.endTime - cue.startTime; 51064 const startTime = this.handleRollover_(cue.startTime + diff, mappingObj.time); 51065 cue.startTime = Math.max(startTime, 0); 51066 cue.endTime = Math.max(startTime + duration, 0); 51067 }); 51068 if (!playlist.syncInfo) { 51069 const firstStart = segmentInfo.cues[0].startTime; 51070 const lastStart = segmentInfo.cues[segmentInfo.cues.length - 1].startTime; 51071 playlist.syncInfo = { 51072 mediaSequence: playlist.mediaSequence + segmentInfo.mediaIndex, 51073 time: Math.min(firstStart, lastStart - segment.duration), 51074 }; 51075 } 51076 } 51077 /** 51078 * MPEG-TS PES timestamps are limited to 2^33. 51079 * Once they reach 2^33, they roll over to 0. 51080 * mux.js handles PES timestamp rollover for the following scenarios: 51081 * [forward rollover(right)] -> 51082 * PES timestamps monotonically increase, and once they reach 2^33, they roll over to 0 51083 * [backward rollover(left)] --> 51084 * we seek back to position before rollover. 51085 * 51086 * According to the HLS SPEC: 51087 * When synchronizing WebVTT with PES timestamps, clients SHOULD account 51088 * for cases where the 33-bit PES timestamps have wrapped and the WebVTT 51089 * cue times have not. When the PES timestamp wraps, the WebVTT Segment 51090 * SHOULD have a X-TIMESTAMP-MAP header that maps the current WebVTT 51091 * time to the new (low valued) PES timestamp. 51092 * 51093 * So we want to handle rollover here and align VTT Cue start/end time to the player's time. 51094 */ 51095 51096 handleRollover_(value, reference) { 51097 if (reference === null) { 51098 return value; 51099 } 51100 let valueIn90khz = value * clock.ONE_SECOND_IN_TS; 51101 const referenceIn90khz = reference * clock.ONE_SECOND_IN_TS; 51102 let offset; 51103 if (referenceIn90khz < valueIn90khz) { 51104 // - 2^33 51105 offset = -8589934592; 51106 } else { 51107 // + 2^33 51108 offset = 8589934592; 51109 } // distance(value - reference) > 2^32 51110 51111 while (Math.abs(valueIn90khz - referenceIn90khz) > 4294967296) { 51112 valueIn90khz += offset; 51113 } 51114 return valueIn90khz / clock.ONE_SECOND_IN_TS; 51115 } 51116} 51117 51118/** 51119 * @file ad-cue-tags.js 51120 */ 51121/** 51122 * Searches for an ad cue that overlaps with the given mediaTime 51123 * 51124 * @param {Object} track 51125 * the track to find the cue for 51126 * 51127 * @param {number} mediaTime 51128 * the time to find the cue at 51129 * 51130 * @return {Object|null} 51131 * the found cue or null 51132 */ 51133 51134const findAdCue = function (track, mediaTime) { 51135 const cues = track.cues; 51136 for (let i = 0; i < cues.length; i++) { 51137 const cue = cues[i]; 51138 if (mediaTime >= cue.adStartTime && mediaTime <= cue.adEndTime) { 51139 return cue; 51140 } 51141 } 51142 return null; 51143}; 51144const updateAdCues = function (media, track, offset = 0) { 51145 if (!media.segments) { 51146 return; 51147 } 51148 let mediaTime = offset; 51149 let cue; 51150 for (let i = 0; i < media.segments.length; i++) { 51151 const segment = media.segments[i]; 51152 if (!cue) { 51153 // Since the cues will span for at least the segment duration, adding a fudge 51154 // factor of half segment duration will prevent duplicate cues from being 51155 // created when timing info is not exact (e.g. cue start time initialized 51156 // at 10.006677, but next call mediaTime is 10.003332 ) 51157 cue = findAdCue(track, mediaTime + segment.duration / 2); 51158 } 51159 if (cue) { 51160 if ('cueIn' in segment) { 51161 // Found a CUE-IN so end the cue 51162 cue.endTime = mediaTime; 51163 cue.adEndTime = mediaTime; 51164 mediaTime += segment.duration; 51165 cue = null; 51166 continue; 51167 } 51168 if (mediaTime < cue.endTime) { 51169 // Already processed this mediaTime for this cue 51170 mediaTime += segment.duration; 51171 continue; 51172 } // otherwise extend cue until a CUE-IN is found 51173 51174 cue.endTime += segment.duration; 51175 } else { 51176 if ('cueOut' in segment) { 51177 cue = new window__default['default'].VTTCue( 51178 mediaTime, 51179 mediaTime + segment.duration, 51180 segment.cueOut 51181 ); 51182 cue.adStartTime = mediaTime; // Assumes tag format to be 51183 // #EXT-X-CUE-OUT:30 51184 51185 cue.adEndTime = mediaTime + parseFloat(segment.cueOut); 51186 track.addCue(cue); 51187 } 51188 if ('cueOutCont' in segment) { 51189 // Entered into the middle of an ad cue 51190 // Assumes tag formate to be 51191 // #EXT-X-CUE-OUT-CONT:10/30 51192 const [adOffset, adTotal] = segment.cueOutCont.split('/').map(parseFloat); 51193 cue = new window__default['default'].VTTCue(mediaTime, mediaTime + segment.duration, ''); 51194 cue.adStartTime = mediaTime - adOffset; 51195 cue.adEndTime = cue.adStartTime + adTotal; 51196 track.addCue(cue); 51197 } 51198 } 51199 mediaTime += segment.duration; 51200 } 51201}; 51202class SyncInfo { 51203 /** 51204 * @param {number} start - media sequence start 51205 * @param {number} end - media sequence end 51206 * @param {number} segmentIndex - index for associated segment 51207 * @param {number|null} [partIndex] - index for associated part 51208 * @param {boolean} [appended] - appended indicator 51209 * 51210 */ 51211 constructor({ start, end, segmentIndex, partIndex = null, appended = false }) { 51212 this.start_ = start; 51213 this.end_ = end; 51214 this.segmentIndex_ = segmentIndex; 51215 this.partIndex_ = partIndex; 51216 this.appended_ = appended; 51217 } 51218 isInRange(targetTime) { 51219 return targetTime >= this.start && targetTime < this.end; 51220 } 51221 markAppended() { 51222 this.appended_ = true; 51223 } 51224 resetAppendedStatus() { 51225 this.appended_ = false;
vendor: 27,055 bytes, lines 51226-51983
51226 } 51227 get isAppended() { 51228 return this.appended_; 51229 } 51230 get start() { 51231 return this.start_; 51232 } 51233 get end() { 51234 return this.end_; 51235 } 51236 get segmentIndex() { 51237 return this.segmentIndex_; 51238 } 51239 get partIndex() { 51240 return this.partIndex_; 51241 } 51242} 51243class SyncInfoData { 51244 /** 51245 * 51246 * @param {SyncInfo} segmentSyncInfo - sync info for a given segment 51247 * @param {Array<SyncInfo>} [partsSyncInfo] - sync infos for a list of parts for a given segment 51248 */ 51249 constructor(segmentSyncInfo, partsSyncInfo = []) { 51250 this.segmentSyncInfo_ = segmentSyncInfo; 51251 this.partsSyncInfo_ = partsSyncInfo; 51252 } 51253 get segmentSyncInfo() { 51254 return this.segmentSyncInfo_; 51255 } 51256 get partsSyncInfo() { 51257 return this.partsSyncInfo_; 51258 } 51259 get hasPartsSyncInfo() { 51260 return this.partsSyncInfo_.length > 0; 51261 } 51262 resetAppendStatus() { 51263 this.segmentSyncInfo_.resetAppendedStatus(); 51264 this.partsSyncInfo_.forEach((partSyncInfo) => partSyncInfo.resetAppendedStatus()); 51265 } 51266} 51267class MediaSequenceSync { 51268 constructor() { 51269 /** 51270 * @type {Map<number, SyncInfoData>} 51271 * @protected 51272 */ 51273 this.storage_ = new Map(); 51274 this.diagnostics_ = ''; 51275 this.isReliable_ = false; 51276 this.start_ = -Infinity; 51277 this.end_ = Infinity; 51278 } 51279 get start() { 51280 return this.start_; 51281 } 51282 get end() { 51283 return this.end_; 51284 } 51285 get diagnostics() { 51286 return this.diagnostics_; 51287 } 51288 get isReliable() { 51289 return this.isReliable_; 51290 } 51291 resetAppendedStatus() { 51292 this.storage_.forEach((syncInfoData) => syncInfoData.resetAppendStatus()); 51293 } 51294 /** 51295 * update sync storage 51296 * 51297 * @param {Object} playlist 51298 * @param {number} currentTime 51299 * 51300 * @return {void} 51301 */ 51302 51303 update(playlist, currentTime) { 51304 const { mediaSequence, segments } = playlist; 51305 this.isReliable_ = this.isReliablePlaylist_(mediaSequence, segments); 51306 if (!this.isReliable_) { 51307 return; 51308 } 51309 return this.updateStorage_( 51310 segments, 51311 mediaSequence, 51312 this.calculateBaseTime_(mediaSequence, segments, currentTime) 51313 ); 51314 } 51315 /** 51316 * @param {number} targetTime 51317 * @return {SyncInfo|null} 51318 */ 51319 51320 getSyncInfoForTime(targetTime) { 51321 for (const { segmentSyncInfo, partsSyncInfo } of this.storage_.values()) { 51322 // Normal segment flow: 51323 if (!partsSyncInfo.length) { 51324 if (segmentSyncInfo.isInRange(targetTime)) { 51325 return segmentSyncInfo; 51326 } 51327 } else { 51328 // Low latency flow: 51329 for (const partSyncInfo of partsSyncInfo) { 51330 if (partSyncInfo.isInRange(targetTime)) { 51331 return partSyncInfo; 51332 } 51333 } 51334 } 51335 } 51336 return null; 51337 } 51338 getSyncInfoForMediaSequence(mediaSequence) { 51339 return this.storage_.get(mediaSequence); 51340 } 51341 updateStorage_(segments, startingMediaSequence, startingTime) { 51342 const newStorage = new Map(); 51343 let newDiagnostics = '\n'; 51344 let currentStart = startingTime; 51345 let currentMediaSequence = startingMediaSequence; 51346 this.start_ = currentStart; 51347 segments.forEach((segment, segmentIndex) => { 51348 const prevSyncInfoData = this.storage_.get(currentMediaSequence); 51349 const segmentStart = currentStart; 51350 const segmentEnd = segmentStart + segment.duration; 51351 const segmentIsAppended = Boolean( 51352 prevSyncInfoData && 51353 prevSyncInfoData.segmentSyncInfo && 51354 prevSyncInfoData.segmentSyncInfo.isAppended 51355 ); 51356 const segmentSyncInfo = new SyncInfo({ 51357 start: segmentStart, 51358 end: segmentEnd, 51359 appended: segmentIsAppended, 51360 segmentIndex, 51361 }); 51362 segment.syncInfo = segmentSyncInfo; 51363 let currentPartStart = currentStart; 51364 const partsSyncInfo = (segment.parts || []).map((part, partIndex) => { 51365 const partStart = currentPartStart; 51366 const partEnd = currentPartStart + part.duration; 51367 const partIsAppended = Boolean( 51368 prevSyncInfoData && 51369 prevSyncInfoData.partsSyncInfo && 51370 prevSyncInfoData.partsSyncInfo[partIndex] && 51371 prevSyncInfoData.partsSyncInfo[partIndex].isAppended 51372 ); 51373 const partSyncInfo = new SyncInfo({ 51374 start: partStart, 51375 end: partEnd, 51376 appended: partIsAppended, 51377 segmentIndex, 51378 partIndex, 51379 }); 51380 currentPartStart = partEnd; 51381 newDiagnostics += `Media Sequence: ${currentMediaSequence}.${partIndex} | Range: ${partStart} --> ${partEnd} | Appended: ${partIsAppended}\n`; 51382 part.syncInfo = partSyncInfo; 51383 return partSyncInfo; 51384 }); 51385 newStorage.set(currentMediaSequence, new SyncInfoData(segmentSyncInfo, partsSyncInfo)); 51386 newDiagnostics += `${compactSegmentUrlDescription(segment.resolvedUri)} | Media Sequence: ${currentMediaSequence} | Range: ${segmentStart} --> ${segmentEnd} | Appended: ${segmentIsAppended}\n`; 51387 currentMediaSequence++; 51388 currentStart = segmentEnd; 51389 }); 51390 this.end_ = currentStart; 51391 this.storage_ = newStorage; 51392 this.diagnostics_ = newDiagnostics; 51393 } 51394 calculateBaseTime_(mediaSequence, segments, fallback) { 51395 if (!this.storage_.size) { 51396 // Initial setup flow. 51397 return 0; 51398 } 51399 if (this.storage_.has(mediaSequence)) { 51400 // Normal flow. 51401 return this.storage_.get(mediaSequence).segmentSyncInfo.start; 51402 } 51403 const minMediaSequenceFromStorage = Math.min(...this.storage_.keys()); // This case captures a race condition that can occur if we switch to a new media playlist that is out of date 51404 // and still has an older Media Sequence. If this occurs, we extrapolate backwards to get the base time. 51405 51406 if (mediaSequence < minMediaSequenceFromStorage) { 51407 const mediaSequenceDiff = minMediaSequenceFromStorage - mediaSequence; 51408 let baseTime = this.storage_.get(minMediaSequenceFromStorage).segmentSyncInfo.start; 51409 for (let i = 0; i < mediaSequenceDiff; i++) { 51410 const segment = segments[i]; 51411 baseTime -= segment.duration; 51412 } 51413 return baseTime; 51414 } // Fallback flow. 51415 // There is a gap between last recorded playlist and a new one received. 51416 51417 return fallback; 51418 } 51419 isReliablePlaylist_(mediaSequence, segments) { 51420 return ( 51421 mediaSequence !== undefined && 51422 mediaSequence !== null && 51423 Array.isArray(segments) && 51424 segments.length 51425 ); 51426 } 51427} 51428class DependantMediaSequenceSync extends MediaSequenceSync { 51429 constructor(parent) { 51430 super(); 51431 this.parent_ = parent; 51432 } 51433 calculateBaseTime_(mediaSequence, segments, fallback) { 51434 if (!this.storage_.size) { 51435 const info = this.parent_.getSyncInfoForMediaSequence(mediaSequence); 51436 if (info) { 51437 return info.segmentSyncInfo.start; 51438 } 51439 return 0; 51440 } 51441 return super.calculateBaseTime_(mediaSequence, segments, fallback); 51442 } 51443} 51444 51445/** 51446 * @file sync-controller.js 51447 */ 51448// synchronize expired playlist segments. 51449// the max media sequence diff is 48 hours of live stream 51450// content with two second segments. Anything larger than that 51451// will likely be invalid. 51452 51453const MAX_MEDIA_SEQUENCE_DIFF_FOR_SYNC = 86400; 51454const syncPointStrategies = [ 51455 // Stategy "VOD": Handle the VOD-case where the sync-point is *always* 51456 // the equivalence display-time 0 === segment-index 0 51457 { 51458 name: 'VOD', 51459 run: (syncController, playlist, duration, currentTimeline, currentTime) => { 51460 if (duration !== Infinity) { 51461 const syncPoint = { 51462 time: 0, 51463 segmentIndex: 0, 51464 partIndex: null, 51465 }; 51466 return syncPoint; 51467 } 51468 return null; 51469 }, 51470 }, 51471 { 51472 name: 'MediaSequence', 51473 /** 51474 * run media sequence strategy 51475 * 51476 * @param {SyncController} syncController 51477 * @param {Object} playlist 51478 * @param {number} duration 51479 * @param {number} currentTimeline 51480 * @param {number} currentTime 51481 * @param {string} type 51482 */ 51483 run: (syncController, playlist, duration, currentTimeline, currentTime, type) => { 51484 const mediaSequenceSync = syncController.getMediaSequenceSync(type); 51485 if (!mediaSequenceSync) { 51486 return null; 51487 } 51488 if (!mediaSequenceSync.isReliable) { 51489 return null; 51490 } 51491 const syncInfo = mediaSequenceSync.getSyncInfoForTime(currentTime); 51492 if (!syncInfo) { 51493 return null; 51494 } 51495 return { 51496 time: syncInfo.start, 51497 partIndex: syncInfo.partIndex, 51498 segmentIndex: syncInfo.segmentIndex, 51499 }; 51500 }, 51501 }, 51502 // Stategy "ProgramDateTime": We have a program-date-time tag in this playlist 51503 { 51504 name: 'ProgramDateTime', 51505 run: (syncController, playlist, duration, currentTimeline, currentTime) => { 51506 if (!Object.keys(syncController.timelineToDatetimeMappings).length) { 51507 return null; 51508 } 51509 let syncPoint = null; 51510 let lastDistance = null; 51511 const partsAndSegments = getPartsAndSegments(playlist); 51512 currentTime = currentTime || 0; 51513 for (let i = 0; i < partsAndSegments.length; i++) { 51514 // start from the end and loop backwards for live 51515 // or start from the front and loop forwards for non-live 51516 const index = playlist.endList || currentTime === 0 ? i : partsAndSegments.length - (i + 1); 51517 const partAndSegment = partsAndSegments[index]; 51518 const segment = partAndSegment.segment; 51519 const datetimeMapping = syncController.timelineToDatetimeMappings[segment.timeline]; 51520 if (!datetimeMapping || !segment.dateTimeObject) { 51521 continue; 51522 } 51523 const segmentTime = segment.dateTimeObject.getTime() / 1000; 51524 let start = segmentTime + datetimeMapping; // take part duration into account. 51525 51526 if (segment.parts && typeof partAndSegment.partIndex === 'number') { 51527 for (let z = 0; z < partAndSegment.partIndex; z++) { 51528 start += segment.parts[z].duration; 51529 } 51530 } 51531 const distance = Math.abs(currentTime - start); // Once the distance begins to increase, or if distance is 0, we have passed 51532 // currentTime and can stop looking for better candidates 51533 51534 if (lastDistance !== null && (distance === 0 || lastDistance < distance)) { 51535 break; 51536 } 51537 lastDistance = distance; 51538 syncPoint = { 51539 time: start, 51540 segmentIndex: partAndSegment.segmentIndex, 51541 partIndex: partAndSegment.partIndex, 51542 }; 51543 } 51544 return syncPoint; 51545 }, 51546 }, 51547 // Stategy "Segment": We have a known time mapping for a timeline and a 51548 // segment in the current timeline with timing data 51549 { 51550 name: 'Segment', 51551 run: (syncController, playlist, duration, currentTimeline, currentTime) => { 51552 let syncPoint = null; 51553 let lastDistance = null; 51554 currentTime = currentTime || 0; 51555 const partsAndSegments = getPartsAndSegments(playlist); 51556 for (let i = 0; i < partsAndSegments.length; i++) { 51557 // start from the end and loop backwards for live 51558 // or start from the front and loop forwards for non-live 51559 const index = playlist.endList || currentTime === 0 ? i : partsAndSegments.length - (i + 1); 51560 const partAndSegment = partsAndSegments[index]; 51561 const segment = partAndSegment.segment; 51562 const start = 51563 (partAndSegment.part && partAndSegment.part.start) || (segment && segment.start); 51564 if (segment.timeline === currentTimeline && typeof start !== 'undefined') { 51565 const distance = Math.abs(currentTime - start); // Once the distance begins to increase, we have passed 51566 // currentTime and can stop looking for better candidates 51567 51568 if (lastDistance !== null && lastDistance < distance) { 51569 break; 51570 } 51571 if (!syncPoint || lastDistance === null || lastDistance >= distance) { 51572 lastDistance = distance; 51573 syncPoint = { 51574 time: start, 51575 segmentIndex: partAndSegment.segmentIndex, 51576 partIndex: partAndSegment.partIndex, 51577 }; 51578 } 51579 } 51580 } 51581 return syncPoint; 51582 }, 51583 }, 51584 // Stategy "Discontinuity": We have a discontinuity with a known 51585 // display-time 51586 { 51587 name: 'Discontinuity', 51588 run: (syncController, playlist, duration, currentTimeline, currentTime) => { 51589 let syncPoint = null; 51590 currentTime = currentTime || 0; 51591 if (playlist.discontinuityStarts && playlist.discontinuityStarts.length) { 51592 let lastDistance = null; 51593 for (let i = 0; i < playlist.discontinuityStarts.length; i++) { 51594 const segmentIndex = playlist.discontinuityStarts[i]; 51595 const discontinuity = playlist.discontinuitySequence + i + 1; 51596 const discontinuitySync = syncController.discontinuities[discontinuity]; 51597 if (discontinuitySync) { 51598 const distance = Math.abs(currentTime - discontinuitySync.time); // Once the distance begins to increase, we have passed 51599 // currentTime and can stop looking for better candidates 51600 51601 if (lastDistance !== null && lastDistance < distance) { 51602 break; 51603 } 51604 if (!syncPoint || lastDistance === null || lastDistance >= distance) { 51605 lastDistance = distance; 51606 syncPoint = { 51607 time: discontinuitySync.time, 51608 segmentIndex, 51609 partIndex: null, 51610 }; 51611 } 51612 } 51613 } 51614 } 51615 return syncPoint; 51616 }, 51617 }, 51618 // Stategy "Playlist": We have a playlist with a known mapping of 51619 // segment index to display time 51620 { 51621 name: 'Playlist', 51622 run: (syncController, playlist, duration, currentTimeline, currentTime) => { 51623 if (playlist.syncInfo) { 51624 const syncPoint = { 51625 time: playlist.syncInfo.time, 51626 segmentIndex: playlist.syncInfo.mediaSequence - playlist.mediaSequence, 51627 partIndex: null, 51628 }; 51629 return syncPoint; 51630 } 51631 return null; 51632 }, 51633 }, 51634]; 51635class SyncController extends videojs.EventTarget { 51636 constructor(options = {}) { 51637 super(); // ...for synching across variants 51638 51639 this.timelines = []; 51640 this.discontinuities = []; 51641 this.timelineToDatetimeMappings = {}; // TODO: this map should be only available for HLS. Since only HLS has MediaSequence. 51642 // For some reason this map helps with syncing between quality switch for MPEG-DASH as well. 51643 // Moreover if we disable this map for MPEG-DASH - quality switch will be broken. 51644 // MPEG-DASH should have its own separate sync strategy 51645 51646 const main = new MediaSequenceSync(); 51647 const audio = new DependantMediaSequenceSync(main); 51648 const vtt = new DependantMediaSequenceSync(main); 51649 this.mediaSequenceStorage_ = { 51650 main, 51651 audio, 51652 vtt, 51653 }; 51654 this.logger_ = logger('SyncController'); 51655 } 51656 /** 51657 * 51658 * @param {string} loaderType 51659 * @return {MediaSequenceSync|null} 51660 */ 51661 51662 getMediaSequenceSync(loaderType) { 51663 return this.mediaSequenceStorage_[loaderType] || null; 51664 } 51665 /** 51666 * Find a sync-point for the playlist specified 51667 * 51668 * A sync-point is defined as a known mapping from display-time to 51669 * a segment-index in the current playlist. 51670 * 51671 * @param {Playlist} playlist 51672 * The playlist that needs a sync-point 51673 * @param {number} duration 51674 * Duration of the MediaSource (Infinite if playing a live source) 51675 * @param {number} currentTimeline 51676 * The last timeline from which a segment was loaded 51677 * @param {number} currentTime 51678 * Current player's time 51679 * @param {string} type 51680 * Segment loader type 51681 * @return {Object} 51682 * A sync-point object 51683 */ 51684 51685 getSyncPoint(playlist, duration, currentTimeline, currentTime, type) { 51686 // Always use VOD sync point for VOD 51687 if (duration !== Infinity) { 51688 const vodSyncPointStrategy = syncPointStrategies.find(({ name }) => name === 'VOD'); 51689 return vodSyncPointStrategy.run(this, playlist, duration); 51690 } 51691 const syncPoints = this.runStrategies_(playlist, duration, currentTimeline, currentTime, type); 51692 if (!syncPoints.length) { 51693 // Signal that we need to attempt to get a sync-point manually 51694 // by fetching a segment in the playlist and constructing 51695 // a sync-point from that information 51696 return null; 51697 } // If we have exact match just return it instead of finding the nearest distance 51698 51699 for (const syncPointInfo of syncPoints) { 51700 const { syncPoint, strategy } = syncPointInfo; 51701 const { segmentIndex, time } = syncPoint; 51702 if (segmentIndex < 0) { 51703 continue; 51704 } 51705 const selectedSegment = playlist.segments[segmentIndex]; 51706 const start = time; 51707 const end = start + selectedSegment.duration; 51708 this.logger_( 51709 `Strategy: ${strategy}. Current time: ${currentTime}. selected segment: ${segmentIndex}. Time: [${start} -> ${end}]}` 51710 ); 51711 if (currentTime >= start && currentTime < end) { 51712 this.logger_('Found sync point with exact match: ', syncPoint); 51713 return syncPoint; 51714 } 51715 } // Now find the sync-point that is closest to the currentTime because 51716 // that should result in the most accurate guess about which segment 51717 // to fetch 51718 51719 return this.selectSyncPoint_(syncPoints, { 51720 key: 'time', 51721 value: currentTime, 51722 }); 51723 } 51724 /** 51725 * Calculate the amount of time that has expired off the playlist during playback 51726 * 51727 * @param {Playlist} playlist 51728 * Playlist object to calculate expired from 51729 * @param {number} duration 51730 * Duration of the MediaSource (Infinity if playling a live source) 51731 * @return {number|null} 51732 * The amount of time that has expired off the playlist during playback. Null 51733 * if no sync-points for the playlist can be found. 51734 */ 51735 51736 getExpiredTime(playlist, duration) { 51737 if (!playlist || !playlist.segments) { 51738 return null; 51739 } 51740 const syncPoints = this.runStrategies_(playlist, duration, playlist.discontinuitySequence, 0); // Without sync-points, there is not enough information to determine the expired time 51741 51742 if (!syncPoints.length) { 51743 return null; 51744 } 51745 const syncPoint = this.selectSyncPoint_(syncPoints, { 51746 key: 'segmentIndex', 51747 value: 0, 51748 }); // If the sync-point is beyond the start of the playlist, we want to subtract the 51749 // duration from index 0 to syncPoint.segmentIndex instead of adding. 51750 51751 if (syncPoint.segmentIndex > 0) { 51752 syncPoint.time *= -1; 51753 } 51754 return Math.abs( 51755 syncPoint.time + 51756 sumDurations({ 51757 defaultDuration: playlist.targetDuration, 51758 durationList: playlist.segments, 51759 startIndex: syncPoint.segmentIndex, 51760 endIndex: 0, 51761 }) 51762 ); 51763 } 51764 /** 51765 * Runs each sync-point strategy and returns a list of sync-points returned by the 51766 * strategies 51767 * 51768 * @private 51769 * @param {Playlist} playlist 51770 * The playlist that needs a sync-point 51771 * @param {number} duration 51772 * Duration of the MediaSource (Infinity if playing a live source) 51773 * @param {number} currentTimeline 51774 * The last timeline from which a segment was loaded 51775 * @param {number} currentTime 51776 * Current player's time 51777 * @param {string} type 51778 * Segment loader type 51779 * @return {Array} 51780 * A list of sync-point objects 51781 */ 51782 51783 runStrategies_(playlist, duration, currentTimeline, currentTime, type) { 51784 const syncPoints = []; // Try to find a sync-point in by utilizing various strategies... 51785 51786 for (let i = 0; i < syncPointStrategies.length; i++) { 51787 const strategy = syncPointStrategies[i]; 51788 const syncPoint = strategy.run(this, playlist, duration, currentTimeline, currentTime, type); 51789 if (syncPoint) { 51790 syncPoint.strategy = strategy.name; 51791 syncPoints.push({ 51792 strategy: strategy.name, 51793 syncPoint, 51794 }); 51795 } 51796 } 51797 return syncPoints; 51798 } 51799 /** 51800 * Selects the sync-point nearest the specified target 51801 * 51802 * @private 51803 * @param {Array} syncPoints 51804 * List of sync-points to select from 51805 * @param {Object} target 51806 * Object specifying the property and value we are targeting 51807 * @param {string} target.key 51808 * Specifies the property to target. Must be either 'time' or 'segmentIndex' 51809 * @param {number} target.value 51810 * The value to target for the specified key. 51811 * @return {Object} 51812 * The sync-point nearest the target 51813 */ 51814 51815 selectSyncPoint_(syncPoints, target) { 51816 let bestSyncPoint = syncPoints[0].syncPoint; 51817 let bestDistance = Math.abs(syncPoints[0].syncPoint[target.key] - target.value); 51818 let bestStrategy = syncPoints[0].strategy; 51819 for (let i = 1; i < syncPoints.length; i++) { 51820 const newDistance = Math.abs(syncPoints[i].syncPoint[target.key] - target.value); 51821 if (newDistance < bestDistance) { 51822 bestDistance = newDistance; 51823 bestSyncPoint = syncPoints[i].syncPoint; 51824 bestStrategy = syncPoints[i].strategy; 51825 } 51826 } 51827 this.logger_( 51828 `syncPoint for [${target.key}: ${target.value}] chosen with strategy` + 51829 ` [${bestStrategy}]: [time:${bestSyncPoint.time},` + 51830 ` segmentIndex:${bestSyncPoint.segmentIndex}` + 51831 (typeof bestSyncPoint.partIndex === 'number' 51832 ? `,partIndex:${bestSyncPoint.partIndex}` 51833 : '') + 51834 ']' 51835 ); 51836 return bestSyncPoint; 51837 } 51838 /** 51839 * Save any meta-data present on the segments when segments leave 51840 * the live window to the playlist to allow for synchronization at the 51841 * playlist level later. 51842 * 51843 * @param {Playlist} oldPlaylist - The previous active playlist 51844 * @param {Playlist} newPlaylist - The updated and most current playlist 51845 */ 51846 51847 saveExpiredSegmentInfo(oldPlaylist, newPlaylist) { 51848 const mediaSequenceDiff = newPlaylist.mediaSequence - oldPlaylist.mediaSequence; // Ignore large media sequence gaps 51849 51850 if (mediaSequenceDiff > MAX_MEDIA_SEQUENCE_DIFF_FOR_SYNC) { 51851 videojs.log.warn( 51852 `Not saving expired segment info. Media sequence gap ${mediaSequenceDiff} is too large.` 51853 ); 51854 return; 51855 } // When a segment expires from the playlist and it has a start time 51856 // save that information as a possible sync-point reference in future 51857 51858 for (let i = mediaSequenceDiff - 1; i >= 0; i--) { 51859 const lastRemovedSegment = oldPlaylist.segments[i]; 51860 if (lastRemovedSegment && typeof lastRemovedSegment.start !== 'undefined') { 51861 newPlaylist.syncInfo = { 51862 mediaSequence: oldPlaylist.mediaSequence + i, 51863 time: lastRemovedSegment.start, 51864 }; 51865 this.logger_( 51866 `playlist refresh sync: [time:${newPlaylist.syncInfo.time},` + 51867 ` mediaSequence: ${newPlaylist.syncInfo.mediaSequence}]` 51868 ); 51869 this.trigger('syncinfoupdate'); 51870 break; 51871 } 51872 } 51873 } 51874 /** 51875 * Save the mapping from playlist's ProgramDateTime to display. This should only happen 51876 * before segments start to load. 51877 * 51878 * @param {Playlist} playlist - The currently active playlist 51879 */ 51880 51881 setDateTimeMappingForStart(playlist) { 51882 // It's possible for the playlist to be updated before playback starts, meaning time 51883 // zero is not yet set. If, during these playlist refreshes, a discontinuity is 51884 // crossed, then the old time zero mapping (for the prior timeline) would be retained 51885 // unless the mappings are cleared. 51886 this.timelineToDatetimeMappings = {}; 51887 if (playlist.segments && playlist.segments.length && playlist.segments[0].dateTimeObject) { 51888 const firstSegment = playlist.segments[0]; 51889 const playlistTimestamp = firstSegment.dateTimeObject.getTime() / 1000; 51890 this.timelineToDatetimeMappings[firstSegment.timeline] = -playlistTimestamp; 51891 } 51892 } 51893 /** 51894 * Calculates and saves timeline mappings, playlist sync info, and segment timing values 51895 * based on the latest timing information. 51896 * 51897 * @param {Object} options 51898 * Options object 51899 * @param {SegmentInfo} options.segmentInfo 51900 * The current active request information 51901 * @param {boolean} options.shouldSaveTimelineMapping 51902 * If there's a timeline change, determines if the timeline mapping should be 51903 * saved for timeline mapping and program date time mappings. 51904 */ 51905 51906 saveSegmentTimingInfo({ segmentInfo, shouldSaveTimelineMapping }) { 51907 const didCalculateSegmentTimeMapping = this.calculateSegmentTimeMapping_( 51908 segmentInfo, 51909 segmentInfo.timingInfo, 51910 shouldSaveTimelineMapping 51911 ); 51912 const segment = segmentInfo.segment; 51913 if (didCalculateSegmentTimeMapping) { 51914 this.saveDiscontinuitySyncInfo_(segmentInfo); // If the playlist does not have sync information yet, record that information 51915 // now with segment timing information 51916 51917 if (!segmentInfo.playlist.syncInfo) { 51918 segmentInfo.playlist.syncInfo = { 51919 mediaSequence: segmentInfo.playlist.mediaSequence + segmentInfo.mediaIndex, 51920 time: segment.start, 51921 }; 51922 } 51923 } 51924 const dateTime = segment.dateTimeObject; 51925 if (segment.discontinuity && shouldSaveTimelineMapping && dateTime) { 51926 this.timelineToDatetimeMappings[segment.timeline] = -(dateTime.getTime() / 1000); 51927 } 51928 } 51929 timestampOffsetForTimeline(timeline) { 51930 if (typeof this.timelines[timeline] === 'undefined') { 51931 return null; 51932 } 51933 return this.timelines[timeline].time; 51934 } 51935 mappingForTimeline(timeline) { 51936 if (typeof this.timelines[timeline] === 'undefined') { 51937 return null; 51938 } 51939 return this.timelines[timeline].mapping; 51940 } 51941 /** 51942 * Use the "media time" for a segment to generate a mapping to "display time" and 51943 * save that display time to the segment. 51944 * 51945 * @private 51946 * @param {SegmentInfo} segmentInfo 51947 * The current active request information 51948 * @param {Object} timingInfo 51949 * The start and end time of the current segment in "media time" 51950 * @param {boolean} shouldSaveTimelineMapping 51951 * If there's a timeline change, determines if the timeline mapping should be 51952 * saved in timelines. 51953 * @return {boolean} 51954 * Returns false if segment time mapping could not be calculated 51955 */ 51956 51957 calculateSegmentTimeMapping_(segmentInfo, timingInfo, shouldSaveTimelineMapping) { 51958 // TODO: remove side effects 51959 const segment = segmentInfo.segment; 51960 const part = segmentInfo.part; 51961 let mappingObj = this.timelines[segmentInfo.timeline]; 51962 let start; 51963 let end; 51964 if (typeof segmentInfo.timestampOffset === 'number') { 51965 mappingObj = { 51966 time: segmentInfo.startOfSegment, 51967 mapping: segmentInfo.startOfSegment - timingInfo.start, 51968 }; 51969 if (shouldSaveTimelineMapping) { 51970 this.timelines[segmentInfo.timeline] = mappingObj; 51971 this.trigger('timestampoffset'); 51972 this.logger_( 51973 `time mapping for timeline ${segmentInfo.timeline}: ` + 51974 `[time: ${mappingObj.time}] [mapping: ${mappingObj.mapping}]` 51975 ); 51976 } 51977 start = segmentInfo.startOfSegment; 51978 end = timingInfo.end + mappingObj.mapping; 51979 } else if (mappingObj) { 51980 start = timingInfo.start + mappingObj.mapping; 51981 end = timingInfo.end + mappingObj.mapping; 51982 } else { 51983 return false;
51984 } 51985 if (part) { 51986 part.start = start; 51987 part.end = end; 51988 } // If we don't have a segment start yet or the start value we got 51989 // is less than our current segment.start value, save a new start value. 51990 // We have to do this because parts will have segment timing info saved 51991 // multiple times and we want segment start to be the earliest part start 51992 // value for that segment. 51993 51994 if (!segment.start || start < segment.start) { 51995 segment.start = start; 51996 } 51997 segment.end = end; 51998 return true; 51999 } 52000 /** 52001 * Each time we have discontinuity in the playlist, attempt to calculate the location 52002 * in display of the start of the discontinuity and save that. We also save an accuracy 52003 * value so that we save values with the most accuracy (closest to 0.) 52004 * 52005 * @private 52006 * @param {SegmentInfo} segmentInfo - The current active request information 52007 */ 52008 52009 saveDiscontinuitySyncInfo_(segmentInfo) { 52010 const playlist = segmentInfo.playlist; 52011 const segment = segmentInfo.segment; // If the current segment is a discontinuity then we know exactly where 52012 // the start of the range and it's accuracy is 0 (greater accuracy values 52013 // mean more approximation) 52014 52015 if (segment.discontinuity) { 52016 this.discontinuities[segment.timeline] = { 52017 time: segment.start, 52018 accuracy: 0, 52019 }; 52020 } else if (playlist.discontinuityStarts && playlist.discontinuityStarts.length) { 52021 // Search for future discontinuities that we can provide better timing 52022 // information for and save that information for sync purposes 52023 for (let i = 0; i < playlist.discontinuityStarts.length; i++) { 52024 const segmentIndex = playlist.discontinuityStarts[i]; 52025 const discontinuity = playlist.discontinuitySequence + i + 1; 52026 const mediaIndexDiff = segmentIndex - segmentInfo.mediaIndex; 52027 const accuracy = Math.abs(mediaIndexDiff); 52028 if ( 52029 !this.discontinuities[discontinuity] || 52030 this.discontinuities[discontinuity].accuracy > accuracy 52031 ) { 52032 let time; 52033 if (mediaIndexDiff < 0) { 52034 time = 52035 segment.start - 52036 sumDurations({ 52037 defaultDuration: playlist.targetDuration, 52038 durationList: playlist.segments, 52039 startIndex: segmentInfo.mediaIndex, 52040 endIndex: segmentIndex, 52041 }); 52042 } else { 52043 time = 52044 segment.end + 52045 sumDurations({ 52046 defaultDuration: playlist.targetDuration, 52047 durationList: playlist.segments, 52048 startIndex: segmentInfo.mediaIndex + 1, 52049 endIndex: segmentIndex, 52050 }); 52051 } 52052 this.discontinuities[discontinuity] = { 52053 time, 52054 accuracy, 52055 }; 52056 } 52057 } 52058 } 52059 } 52060 dispose() { 52061 this.trigger('dispose'); 52062 this.off(); 52063 } 52064} 52065 52066/** 52067 * The TimelineChangeController acts as a source for segment loaders to listen for and 52068 * keep track of latest and pending timeline changes. This is useful to ensure proper 52069 * sync, as each loader may need to make a consideration for what timeline the other 52070 * loader is on before making changes which could impact the other loader's media. 52071 * 52072 * @class TimelineChangeController 52073 * @extends videojs.EventTarget 52074 */ 52075 52076class TimelineChangeController extends videojs.EventTarget { 52077 constructor() { 52078 super(); 52079 this.pendingTimelineChanges_ = {}; 52080 this.lastTimelineChanges_ = {}; 52081 } 52082 clearPendingTimelineChange(type) { 52083 this.pendingTimelineChanges_[type] = null; 52084 this.trigger('pendingtimelinechange'); 52085 } 52086 pendingTimelineChange({ type, from, to }) { 52087 if (typeof from === 'number' && typeof to === 'number') { 52088 this.pendingTimelineChanges_[type] = { 52089 type, 52090 from, 52091 to, 52092 }; 52093 this.trigger('pendingtimelinechange'); 52094 } 52095 return this.pendingTimelineChanges_[type]; 52096 } 52097 lastTimelineChange({ type, from, to }) { 52098 if (typeof from === 'number' && typeof to === 'number') { 52099 this.lastTimelineChanges_[type] = { 52100 type, 52101 from, 52102 to, 52103 }; 52104 delete this.pendingTimelineChanges_[type]; 52105 const metadata = { 52106 timelineChangeInfo: { 52107 from, 52108 to, 52109 }, 52110 }; 52111 this.trigger({ 52112 type: 'timelinechange', 52113 metadata, 52114 }); 52115 } 52116 return this.lastTimelineChanges_[type]; 52117 } 52118 dispose() { 52119 this.trigger('dispose'); 52120 this.pendingTimelineChanges_ = {}; 52121 this.lastTimelineChanges_ = {}; 52122 this.off(); 52123 } 52124} 52125 52126/* rollup-plugin-worker-factory start for worker!/home/runner/work/http-streaming/http-streaming/src/decrypter-worker.js */ 52127const workerCode = transform( 52128 getWorkerString(function () { 52129 /** 52130 * @file stream.js 52131 */ 52132 52133 /** 52134 * A lightweight readable stream implemention that handles event dispatching. 52135 * 52136 * @class Stream 52137 */ 52138 52139 var Stream = /*#__PURE__*/ (function () { 52140 function Stream() { 52141 this.listeners = {}; 52142 } 52143 /** 52144 * Add a listener for a specified event type. 52145 * 52146 * @param {string} type the event name 52147 * @param {Function} listener the callback to be invoked when an event of 52148 * the specified type occurs 52149 */ 52150 52151 var _proto = Stream.prototype; 52152 _proto.on = function on(type, listener) { 52153 if (!this.listeners[type]) { 52154 this.listeners[type] = []; 52155 } 52156 this.listeners[type].push(listener); 52157 }; 52158 /** 52159 * Remove a listener for a specified event type. 52160 * 52161 * @param {string} type the event name 52162 * @param {Function} listener a function previously registered for this 52163 * type of event through `on` 52164 * @return {boolean} if we could turn it off or not 52165 */ 52166 _proto.off = function off(type, listener) { 52167 if (!this.listeners[type]) { 52168 return false;
vendor: 4,120 bytes, lines 52169-52273
52169 } 52170 var index = this.listeners[type].indexOf(listener); // TODO: which is better? 52171 // In Video.js we slice listener functions 52172 // on trigger so that it does not mess up the order 52173 // while we loop through. 52174 // 52175 // Here we slice on off so that the loop in trigger 52176 // can continue using it's old reference to loop without 52177 // messing up the order. 52178 52179 this.listeners[type] = this.listeners[type].slice(0); 52180 this.listeners[type].splice(index, 1); 52181 return index > -1; 52182 }; 52183 /** 52184 * Trigger an event of the specified type on this stream. Any additional 52185 * arguments to this function are passed as parameters to event listeners. 52186 * 52187 * @param {string} type the event name 52188 */ 52189 _proto.trigger = function trigger(type) { 52190 var callbacks = this.listeners[type]; 52191 if (!callbacks) { 52192 return; 52193 } // Slicing the arguments on every invocation of this method 52194 // can add a significant amount of overhead. Avoid the 52195 // intermediate object creation for the common case of a 52196 // single callback argument 52197 52198 if (arguments.length === 2) { 52199 var length = callbacks.length; 52200 for (var i = 0; i < length; ++i) { 52201 callbacks[i].call(this, arguments[1]); 52202 } 52203 } else { 52204 var args = Array.prototype.slice.call(arguments, 1); 52205 var _length = callbacks.length; 52206 for (var _i = 0; _i < _length; ++_i) { 52207 callbacks[_i].apply(this, args); 52208 } 52209 } 52210 }; 52211 /** 52212 * Destroys the stream and cleans up. 52213 */ 52214 _proto.dispose = function dispose() { 52215 this.listeners = {}; 52216 }; 52217 /** 52218 * Forwards all `data` events on this stream to the destination stream. The 52219 * destination stream should provide a method `push` to receive the data 52220 * events as they arrive. 52221 * 52222 * @param {Stream} destination the stream that will receive all `data` events 52223 * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options 52224 */ 52225 _proto.pipe = function pipe(destination) { 52226 this.on('data', function (data) { 52227 destination.push(data); 52228 }); 52229 }; 52230 return Stream; 52231 })(); 52232 /*! @name pkcs7 @version 1.0.4 @license Apache-2.0 */ 52233 52234 /** 52235 * Returns the subarray of a Uint8Array without PKCS#7 padding. 52236 * 52237 * @param padded {Uint8Array} unencrypted bytes that have been padded 52238 * @return {Uint8Array} the unpadded bytes 52239 * @see http://tools.ietf.org/html/rfc5652 52240 */ 52241 52242 function unpad(padded) { 52243 return padded.subarray(0, padded.byteLength - padded[padded.byteLength - 1]); 52244 } 52245 /*! @name aes-decrypter @version 4.0.2 @license Apache-2.0 */ 52246 52247 /** 52248 * @file aes.js 52249 * 52250 * This file contains an adaptation of the AES decryption algorithm 52251 * from the Standford Javascript Cryptography Library. That work is 52252 * covered by the following copyright and permissions notice: 52253 * 52254 * Copyright 2009-2010 Emily Stark, Mike Hamburg, Dan Boneh. 52255 * All rights reserved. 52256 * 52257 * Redistribution and use in source and binary forms, with or without 52258 * modification, are permitted provided that the following conditions are 52259 * met: 52260 * 52261 * 1. Redistributions of source code must retain the above copyright 52262 * notice, this list of conditions and the following disclaimer. 52263 * 52264 * 2. Redistributions in binary form must reproduce the above 52265 * copyright notice, this list of conditions and the following 52266 * disclaimer in the documentation and/or other materials provided 52267 * with the distribution. 52268 * 52269 * THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR 52270 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 52271 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 52272 * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> OR CONTRIBUTORS BE 52273 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
52274 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 52275 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 52276 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 52277 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 52278 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN 52279 * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 52280 * 52281 * The views and conclusions contained in the software and documentation 52282 * are those of the authors and should not be interpreted as representing 52283 * official policies, either expressed or implied, of the authors. 52284 */ 52285 52286 /** 52287 * Expand the S-box tables. 52288 * 52289 * @private 52290 */ 52291 52292 const precompute = function () { 52293 const tables = [ 52294 [[], [], [], [], []], 52295 [[], [], [], [], []], 52296 ]; 52297 const encTable = tables[0]; 52298 const decTable = tables[1]; 52299 const sbox = encTable[4]; 52300 const sboxInv = decTable[4]; 52301 let i; 52302 let x; 52303 let xInv; 52304 const d = []; 52305 const th = []; 52306 let x2; 52307 let x4; 52308 let x8; 52309 let s; 52310 let tEnc; 52311 let tDec; // Compute double and third tables 52312 52313 for (i = 0; i < 256; i++) { 52314 th[(d[i] = (i << 1) ^ ((i >> 7) * 283)) ^ i] = i; 52315 } 52316 for (x = xInv = 0; !sbox[x]; x ^= x2 || 1, xInv = th[xInv] || 1) { 52317 // Compute sbox 52318 s = xInv ^ (xInv << 1) ^ (xInv << 2) ^ (xInv << 3) ^ (xInv << 4); 52319 s = (s >> 8) ^ (s & 255) ^ 99; 52320 sbox[x] = s; 52321 sboxInv[s] = x; // Compute MixColumns 52322 52323 x8 = d[(x4 = d[(x2 = d[x])])]; 52324 tDec = (x8 * 0x1010101) ^ (x4 * 0x10001) ^ (x2 * 0x101) ^ (x * 0x1010100); 52325 tEnc = (d[s] * 0x101) ^ (s * 0x1010100); 52326 for (i = 0; i < 4; i++) { 52327 encTable[i][x] = tEnc = (tEnc << 24) ^ (tEnc >>> 8); 52328 decTable[i][s] = tDec = (tDec << 24) ^ (tDec >>> 8); 52329 } 52330 } // Compactify. Considerable speedup on Firefox. 52331 52332 for (i = 0; i < 5; i++) { 52333 encTable[i] = encTable[i].slice(0); 52334 decTable[i] = decTable[i].slice(0); 52335 } 52336 return tables; 52337 }; 52338 let aesTables = null; 52339 /** 52340 * Schedule out an AES key for both encryption and decryption. This 52341 * is a low-level class. Use a cipher mode to do bulk encryption. 52342 * 52343 * @class AES 52344 * @param key {Array} The key as an array of 4, 6 or 8 words. 52345 */ 52346 52347 class AES { 52348 constructor(key) { 52349 /** 52350 * The expanded S-box and inverse S-box tables. These will be computed 52351 * on the client so that we don't have to send them down the wire. 52352 * 52353 * There are two tables, _tables[0] is for encryption and 52354 * _tables[1] is for decryption. 52355 * 52356 * The first 4 sub-tables are the expanded S-box with MixColumns. The 52357 * last (_tables[01][4]) is the S-box itself. 52358 * 52359 * @private 52360 */ 52361 // if we have yet to precompute the S-box tables 52362 // do so now 52363 if (!aesTables) { 52364 aesTables = precompute(); 52365 } // then make a copy of that object for use 52366 52367 this._tables = [ 52368 [ 52369 aesTables[0][0].slice(), 52370 aesTables[0][1].slice(), 52371 aesTables[0][2].slice(), 52372 aesTables[0][3].slice(), 52373 aesTables[0][4].slice(), 52374 ], 52375 [ 52376 aesTables[1][0].slice(), 52377 aesTables[1][1].slice(), 52378 aesTables[1][2].slice(), 52379 aesTables[1][3].slice(), 52380 aesTables[1][4].slice(), 52381 ], 52382 ]; 52383 let i; 52384 let j; 52385 let tmp; 52386 const sbox = this._tables[0][4]; 52387 const decTable = this._tables[1]; 52388 const keyLen = key.length; 52389 let rcon = 1; 52390 if (keyLen !== 4 && keyLen !== 6 && keyLen !== 8) { 52391 throw new Error('Invalid aes key size'); 52392 } 52393 const encKey = key.slice(0); 52394 const decKey = []; 52395 this._key = [encKey, decKey]; // schedule encryption keys 52396 52397 for (i = keyLen; i < 4 * keyLen + 28; i++) { 52398 tmp = encKey[i - 1]; // apply sbox 52399
52400 if (i % keyLen === 0 || (keyLen === 8 && i % keyLen === 4)) { 52401 tmp = 52402 (sbox[tmp >>> 24] << 24) ^ 52403 (sbox[(tmp >> 16) & 255] << 16) ^ 52404 (sbox[(tmp >> 8) & 255] << 8) ^ 52405 sbox[tmp & 255]; // shift rows and add rcon 52406 52407 if (i % keyLen === 0) { 52408 tmp = (tmp << 8) ^ (tmp >>> 24) ^ (rcon << 24); 52409 rcon = (rcon << 1) ^ ((rcon >> 7) * 283); 52410 } 52411 } 52412 encKey[i] = encKey[i - keyLen] ^ tmp; 52413 } // schedule decryption keys 52414 52415 for (j = 0; i; j++, i--) { 52416 tmp = encKey[j & 3 ? i : i - 4]; 52417 if (i <= 4 || j < 4) { 52418 decKey[j] = tmp; 52419 } else { 52420 decKey[j] = 52421 decTable[0][sbox[tmp >>> 24]] ^ 52422 decTable[1][sbox[(tmp >> 16) & 255]] ^ 52423 decTable[2][sbox[(tmp >> 8) & 255]] ^ 52424 decTable[3][sbox[tmp & 255]]; 52425 } 52426 } 52427 } 52428 /** 52429 * Decrypt 16 bytes, specified as four 32-bit words. 52430 * 52431 * @param {number} encrypted0 the first word to decrypt 52432 * @param {number} encrypted1 the second word to decrypt 52433 * @param {number} encrypted2 the third word to decrypt 52434 * @param {number} encrypted3 the fourth word to decrypt 52435 * @param {Int32Array} out the array to write the decrypted words 52436 * into 52437 * @param {number} offset the offset into the output array to start 52438 * writing results 52439 * @return {Array} The plaintext. 52440 */ 52441 52442 decrypt(encrypted0, encrypted1, encrypted2, encrypted3, out, offset) { 52443 const key = this._key[1]; // state variables a,b,c,d are loaded with pre-whitened data 52444 52445 let a = encrypted0 ^ key[0]; 52446 let b = encrypted3 ^ key[1]; 52447 let c = encrypted2 ^ key[2]; 52448 let d = encrypted1 ^ key[3]; 52449 let a2; 52450 let b2; 52451 let c2; // key.length === 2 ? 52452 52453 const nInnerRounds = key.length / 4 - 2; 52454 let i; 52455 let kIndex = 4; 52456 const table = this._tables[1]; // load up the tables 52457 52458 const table0 = table[0]; 52459 const table1 = table[1]; 52460 const table2 = table[2]; 52461 const table3 = table[3]; 52462 const sbox = table[4]; // Inner rounds. Cribbed from OpenSSL. 52463 52464 for (i = 0; i < nInnerRounds; i++) { 52465 a2 = 52466 table0[a >>> 24] ^ 52467 table1[(b >> 16) & 255] ^ 52468 table2[(c >> 8) & 255] ^ 52469 table3[d & 255] ^ 52470 key[kIndex]; 52471 b2 = 52472 table0[b >>> 24] ^ 52473 table1[(c >> 16) & 255] ^ 52474 table2[(d >> 8) & 255] ^ 52475 table3[a & 255] ^ 52476 key[kIndex + 1]; 52477 c2 = 52478 table0[c >>> 24] ^ 52479 table1[(d >> 16) & 255] ^ 52480 table2[(a >> 8) & 255] ^ 52481 table3[b & 255] ^ 52482 key[kIndex + 2]; 52483 d = 52484 table0[d >>> 24] ^ 52485 table1[(a >> 16) & 255] ^ 52486 table2[(b >> 8) & 255] ^ 52487 table3[c & 255] ^ 52488 key[kIndex + 3]; 52489 kIndex += 4; 52490 a = a2; 52491 b = b2; 52492 c = c2; 52493 } // Last round. 52494 52495 for (i = 0; i < 4; i++) { 52496 out[(3 & -i) + offset] = 52497 (sbox[a >>> 24] << 24) ^ 52498 (sbox[(b >> 16) & 255] << 16) ^ 52499 (sbox[(c >> 8) & 255] << 8) ^ 52500 sbox[d & 255] ^ 52501 key[kIndex++]; 52502 a2 = a; 52503 a = b; 52504 b = c; 52505 c = d; 52506 d = a2; 52507 } 52508 } 52509 } 52510 /** 52511 * @file async-stream.js 52512 */ 52513 52514 /** 52515 * A wrapper around the Stream class to use setTimeout 52516 * and run stream "jobs" Asynchronously 52517 * 52518 * @class AsyncStream 52519 * @extends Stream 52520 */ 52521 52522 class AsyncStream extends Stream { 52523 constructor() { 52524 super(Stream); 52525 this.jobs = []; 52526 this.delay = 1; 52527 this.timeout_ = null; 52528 } 52529 /** 52530 * process an async job 52531 * 52532 * @private 52533 */ 52534 52535 processJob_() { 52536 this.jobs.shift()(); 52537 if (this.jobs.length) { 52538 this.timeout_ = setTimeout(this.processJob_.bind(this), this.delay); 52539 } else { 52540 this.timeout_ = null; 52541 } 52542 } 52543 /** 52544 * push a job into the stream 52545 * 52546 * @param {Function} job the job to push into the stream 52547 */ 52548 52549 push(job) { 52550 this.jobs.push(job); 52551 if (!this.timeout_) { 52552 this.timeout_ = setTimeout(this.processJob_.bind(this), this.delay); 52553 } 52554 } 52555 } 52556 /** 52557 * @file decrypter.js 52558 * 52559 * An asynchronous implementation of AES-128 CBC decryption with 52560 * PKCS#7 padding. 52561 */ 52562 52563 /** 52564 * Convert network-order (big-endian) bytes into their little-endian 52565 * representation. 52566 */ 52567 52568 const ntoh = function (word) { 52569 return (word << 24) | ((word & 0xff00) << 8) | ((word & 0xff0000) >> 8) | (word >>> 24); 52570 }; 52571 /** 52572 * Decrypt bytes using AES-128 with CBC and PKCS#7 padding. 52573 * 52574 * @param {Uint8Array} encrypted the encrypted bytes 52575 * @param {Uint32Array} key the bytes of the decryption key 52576 * @param {Uint32Array} initVector the initialization vector (IV) to
vendor: 15,759 bytes, lines 52577-53032
52577 * use for the first round of CBC. 52578 * @return {Uint8Array} the decrypted bytes 52579 * 52580 * @see http://en.wikipedia.org/wiki/Advanced_Encryption_Standard 52581 * @see http://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_Block_Chaining_.28CBC.29 52582 * @see https://tools.ietf.org/html/rfc2315 52583 */ 52584 52585 const decrypt = function (encrypted, key, initVector) { 52586 // word-level access to the encrypted bytes 52587 const encrypted32 = new Int32Array( 52588 encrypted.buffer, 52589 encrypted.byteOffset, 52590 encrypted.byteLength >> 2 52591 ); 52592 const decipher = new AES(Array.prototype.slice.call(key)); // byte and word-level access for the decrypted output 52593 52594 const decrypted = new Uint8Array(encrypted.byteLength); 52595 const decrypted32 = new Int32Array(decrypted.buffer); // temporary variables for working with the IV, encrypted, and 52596 // decrypted data 52597 52598 let init0; 52599 let init1; 52600 let init2; 52601 let init3; 52602 let encrypted0; 52603 let encrypted1; 52604 let encrypted2; 52605 let encrypted3; // iteration variable 52606 52607 let wordIx; // pull out the words of the IV to ensure we don't modify the 52608 // passed-in reference and easier access 52609 52610 init0 = initVector[0]; 52611 init1 = initVector[1]; 52612 init2 = initVector[2]; 52613 init3 = initVector[3]; // decrypt four word sequences, applying cipher-block chaining (CBC) 52614 // to each decrypted block 52615 52616 for (wordIx = 0; wordIx < encrypted32.length; wordIx += 4) { 52617 // convert big-endian (network order) words into little-endian 52618 // (javascript order) 52619 encrypted0 = ntoh(encrypted32[wordIx]); 52620 encrypted1 = ntoh(encrypted32[wordIx + 1]); 52621 encrypted2 = ntoh(encrypted32[wordIx + 2]); 52622 encrypted3 = ntoh(encrypted32[wordIx + 3]); // decrypt the block 52623 52624 decipher.decrypt(encrypted0, encrypted1, encrypted2, encrypted3, decrypted32, wordIx); // XOR with the IV, and restore network byte-order to obtain the 52625 // plaintext 52626 52627 decrypted32[wordIx] = ntoh(decrypted32[wordIx] ^ init0); 52628 decrypted32[wordIx + 1] = ntoh(decrypted32[wordIx + 1] ^ init1); 52629 decrypted32[wordIx + 2] = ntoh(decrypted32[wordIx + 2] ^ init2); 52630 decrypted32[wordIx + 3] = ntoh(decrypted32[wordIx + 3] ^ init3); // setup the IV for the next round 52631 52632 init0 = encrypted0; 52633 init1 = encrypted1; 52634 init2 = encrypted2; 52635 init3 = encrypted3; 52636 } 52637 return decrypted; 52638 }; 52639 /** 52640 * The `Decrypter` class that manages decryption of AES 52641 * data through `AsyncStream` objects and the `decrypt` 52642 * function 52643 * 52644 * @param {Uint8Array} encrypted the encrypted bytes 52645 * @param {Uint32Array} key the bytes of the decryption key 52646 * @param {Uint32Array} initVector the initialization vector (IV) to 52647 * @param {Function} done the function to run when done 52648 * @class Decrypter 52649 */ 52650 52651 class Decrypter { 52652 constructor(encrypted, key, initVector, done) { 52653 const step = Decrypter.STEP; 52654 const encrypted32 = new Int32Array(encrypted.buffer); 52655 const decrypted = new Uint8Array(encrypted.byteLength); 52656 let i = 0; 52657 this.asyncStream_ = new AsyncStream(); // split up the encryption job and do the individual chunks asynchronously 52658 52659 this.asyncStream_.push( 52660 this.decryptChunk_(encrypted32.subarray(i, i + step), key, initVector, decrypted) 52661 ); 52662 for (i = step; i < encrypted32.length; i += step) { 52663 initVector = new Uint32Array([ 52664 ntoh(encrypted32[i - 4]), 52665 ntoh(encrypted32[i - 3]), 52666 ntoh(encrypted32[i - 2]), 52667 ntoh(encrypted32[i - 1]), 52668 ]); 52669 this.asyncStream_.push( 52670 this.decryptChunk_(encrypted32.subarray(i, i + step), key, initVector, decrypted) 52671 ); 52672 } // invoke the done() callback when everything is finished 52673 52674 this.asyncStream_.push(function () { 52675 // remove pkcs#7 padding from the decrypted bytes 52676 done(null, unpad(decrypted)); 52677 }); 52678 } 52679 /** 52680 * a getter for step the maximum number of bytes to process at one time 52681 * 52682 * @return {number} the value of step 32000 52683 */ 52684 52685 static get STEP() { 52686 // 4 * 8000; 52687 return 32000; 52688 } 52689 /** 52690 * @private 52691 */ 52692 52693 decryptChunk_(encrypted, key, initVector, decrypted) { 52694 return function () { 52695 const bytes = decrypt(encrypted, key, initVector); 52696 decrypted.set(bytes, encrypted.byteOffset); 52697 }; 52698 } 52699 } 52700 var commonjsGlobal = 52701 typeof globalThis !== 'undefined' 52702 ? globalThis 52703 : typeof window !== 'undefined' 52704 ? window 52705 : typeof global !== 'undefined' 52706 ? global 52707 : typeof self !== 'undefined' 52708 ? self 52709 : {}; 52710 var win; 52711 if (typeof window !== 'undefined') { 52712 win = window; 52713 } else if (typeof commonjsGlobal !== 'undefined') { 52714 win = commonjsGlobal; 52715 } else if (typeof self !== 'undefined') { 52716 win = self; 52717 } else { 52718 win = {}; 52719 } 52720 var window_1 = win; 52721 var isArrayBufferView = function isArrayBufferView(obj) { 52722 if (ArrayBuffer.isView === 'function') { 52723 return ArrayBuffer.isView(obj); 52724 } 52725 return obj && obj.buffer instanceof ArrayBuffer; 52726 }; 52727 var BigInt = window_1.BigInt || Number; 52728 [ 52729 BigInt('0x1'), 52730 BigInt('0x100'), 52731 BigInt('0x10000'), 52732 BigInt('0x1000000'), 52733 BigInt('0x100000000'), 52734 BigInt('0x10000000000'), 52735 BigInt('0x1000000000000'), 52736 BigInt('0x100000000000000'), 52737 BigInt('0x10000000000000000'), 52738 ]; 52739 (function () { 52740 var a = new Uint16Array([0xffcc]); 52741 var b = new Uint8Array(a.buffer, a.byteOffset, a.byteLength); 52742 if (b[0] === 0xff) { 52743 return 'big'; 52744 } 52745 if (b[0] === 0xcc) { 52746 return 'little'; 52747 } 52748 return 'unknown'; 52749 })(); 52750 /** 52751 * Creates an object for sending to a web worker modifying properties that are TypedArrays 52752 * into a new object with seperated properties for the buffer, byteOffset, and byteLength. 52753 * 52754 * @param {Object} message 52755 * Object of properties and values to send to the web worker 52756 * @return {Object} 52757 * Modified message with TypedArray values expanded 52758 * @function createTransferableMessage 52759 */ 52760 52761 const createTransferableMessage = function (message) { 52762 const transferable = {}; 52763 Object.keys(message).forEach((key) => { 52764 const value = message[key]; 52765 if (isArrayBufferView(value)) { 52766 transferable[key] = { 52767 bytes: value.buffer, 52768 byteOffset: value.byteOffset, 52769 byteLength: value.byteLength, 52770 }; 52771 } else { 52772 transferable[key] = value; 52773 } 52774 }); 52775 return transferable; 52776 }; 52777 /* global self */ 52778 52779 /** 52780 * Our web worker interface so that things can talk to aes-decrypter 52781 * that will be running in a web worker. the scope is passed to this by 52782 * webworkify. 52783 */ 52784 52785 self.onmessage = function (event) { 52786 const data = event.data; 52787 const encrypted = new Uint8Array( 52788 data.encrypted.bytes, 52789 data.encrypted.byteOffset, 52790 data.encrypted.byteLength 52791 ); 52792 const key = new Uint32Array(data.key.bytes, data.key.byteOffset, data.key.byteLength / 4); 52793 const iv = new Uint32Array(data.iv.bytes, data.iv.byteOffset, data.iv.byteLength / 4); 52794 /* eslint-disable no-new, handle-callback-err */ 52795 52796 new Decrypter(encrypted, key, iv, function (err, bytes) { 52797 self.postMessage( 52798 createTransferableMessage({ 52799 source: data.source, 52800 decrypted: bytes, 52801 }), 52802 [bytes.buffer] 52803 ); 52804 }); 52805 /* eslint-enable */ 52806 }; 52807 }) 52808); 52809var Decrypter = factory(workerCode); 52810/* rollup-plugin-worker-factory end for worker!/home/runner/work/http-streaming/http-streaming/src/decrypter-worker.js */ 52811 52812/** 52813 * Convert the properties of an HLS track into an audioTrackKind. 52814 * 52815 * @private 52816 */ 52817 52818const audioTrackKind_ = (properties) => { 52819 let kind = properties.default ? 'main' : 'alternative'; 52820 if ( 52821 properties.characteristics && 52822 properties.characteristics.indexOf('public.accessibility.describes-video') >= 0 52823 ) { 52824 kind = 'main-desc'; 52825 } 52826 return kind; 52827}; 52828/** 52829 * Pause provided segment loader and playlist loader if active 52830 * 52831 * @param {SegmentLoader} segmentLoader 52832 * SegmentLoader to pause 52833 * @param {Object} mediaType 52834 * Active media type 52835 * @function stopLoaders 52836 */ 52837 52838const stopLoaders = (segmentLoader, mediaType) => { 52839 segmentLoader.abort(); 52840 segmentLoader.pause(); 52841 if (mediaType && mediaType.activePlaylistLoader) { 52842 mediaType.activePlaylistLoader.pause(); 52843 mediaType.activePlaylistLoader = null; 52844 } 52845}; 52846/** 52847 * Start loading provided segment loader and playlist loader 52848 * 52849 * @param {PlaylistLoader} playlistLoader 52850 * PlaylistLoader to start loading 52851 * @param {Object} mediaType 52852 * Active media type 52853 * @function startLoaders 52854 */ 52855 52856const startLoaders = (playlistLoader, mediaType) => { 52857 // Segment loader will be started after `loadedmetadata` or `loadedplaylist` from the 52858 // playlist loader 52859 mediaType.activePlaylistLoader = playlistLoader; 52860 playlistLoader.load(); 52861}; 52862/** 52863 * Returns a function to be called when the media group changes. It performs a 52864 * non-destructive (preserve the buffer) resync of the SegmentLoader. This is because a 52865 * change of group is merely a rendition switch of the same content at another encoding, 52866 * rather than a change of content, such as switching audio from English to Spanish. 52867 * 52868 * @param {string} type 52869 * MediaGroup type 52870 * @param {Object} settings 52871 * Object containing required information for media groups 52872 * @return {Function} 52873 * Handler for a non-destructive resync of SegmentLoader when the active media 52874 * group changes. 52875 * @function onGroupChanged 52876 */ 52877 52878const onGroupChanged = (type, settings) => () => { 52879 const { 52880 segmentLoaders: { [type]: segmentLoader, main: mainSegmentLoader }, 52881 mediaTypes: { [type]: mediaType }, 52882 } = settings; 52883 const activeTrack = mediaType.activeTrack(); 52884 const activeGroup = mediaType.getActiveGroup(); 52885 const previousActiveLoader = mediaType.activePlaylistLoader; 52886 const lastGroup = mediaType.lastGroup_; // the group did not change do nothing 52887 52888 if (activeGroup && lastGroup && activeGroup.id === lastGroup.id) { 52889 return; 52890 } 52891 mediaType.lastGroup_ = activeGroup; 52892 mediaType.lastTrack_ = activeTrack; 52893 stopLoaders(segmentLoader, mediaType); 52894 if (!activeGroup || activeGroup.isMainPlaylist) { 52895 // there is no group active or active group is a main playlist and won't change 52896 return; 52897 } 52898 if (!activeGroup.playlistLoader) { 52899 if (previousActiveLoader) { 52900 // The previous group had a playlist loader but the new active group does not 52901 // this means we are switching from demuxed to muxed audio. In this case we want to 52902 // do a destructive reset of the main segment loader and not restart the audio 52903 // loaders. 52904 mainSegmentLoader.resetEverything(); 52905 } 52906 return; 52907 } // Non-destructive resync 52908 52909 segmentLoader.resyncLoader(); 52910 startLoaders(activeGroup.playlistLoader, mediaType); 52911}; 52912const onGroupChanging = (type, settings) => () => { 52913 const { 52914 segmentLoaders: { [type]: segmentLoader }, 52915 mediaTypes: { [type]: mediaType }, 52916 } = settings; 52917 mediaType.lastGroup_ = null; 52918 segmentLoader.abort(); 52919 segmentLoader.pause(); 52920}; 52921/** 52922 * Returns a function to be called when the media track changes. It performs a 52923 * destructive reset of the SegmentLoader to ensure we start loading as close to 52924 * currentTime as possible. 52925 * 52926 * @param {string} type 52927 * MediaGroup type 52928 * @param {Object} settings 52929 * Object containing required information for media groups 52930 * @return {Function} 52931 * Handler for a destructive reset of SegmentLoader when the active media 52932 * track changes. 52933 * @function onTrackChanged 52934 */ 52935 52936const onTrackChanged = (type, settings) => () => { 52937 const { 52938 mainPlaylistLoader, 52939 segmentLoaders: { [type]: segmentLoader, main: mainSegmentLoader }, 52940 mediaTypes: { [type]: mediaType }, 52941 } = settings; 52942 const activeTrack = mediaType.activeTrack(); 52943 const activeGroup = mediaType.getActiveGroup(); 52944 const previousActiveLoader = mediaType.activePlaylistLoader; 52945 const lastTrack = mediaType.lastTrack_; // track did not change, do nothing 52946 52947 if (lastTrack && activeTrack && lastTrack.id === activeTrack.id) { 52948 return; 52949 } 52950 mediaType.lastGroup_ = activeGroup; 52951 mediaType.lastTrack_ = activeTrack; 52952 stopLoaders(segmentLoader, mediaType); 52953 if (!activeGroup) { 52954 // there is no group active so we do not want to restart loaders 52955 return; 52956 } 52957 if (activeGroup.isMainPlaylist) { 52958 // track did not change, do nothing 52959 if (!activeTrack || !lastTrack || activeTrack.id === lastTrack.id) { 52960 return; 52961 } 52962 const pc = settings.vhs.playlistController_; 52963 const newPlaylist = pc.selectPlaylist(); // media will not change do nothing 52964 52965 if (pc.media() === newPlaylist) { 52966 return; 52967 } 52968 mediaType.logger_( 52969 `track change. Switching main audio from ${lastTrack.id} to ${activeTrack.id}` 52970 ); 52971 mainPlaylistLoader.pause(); 52972 mainSegmentLoader.resetEverything(); 52973 pc.fastQualityChange_(newPlaylist); 52974 return; 52975 } 52976 if (type === 'AUDIO') { 52977 if (!activeGroup.playlistLoader) { 52978 // when switching from demuxed audio/video to muxed audio/video (noted by no 52979 // playlist loader for the audio group), we want to do a destructive reset of the 52980 // main segment loader and not restart the audio loaders 52981 mainSegmentLoader.setAudio(true); // don't have to worry about disabling the audio of the audio segment loader since 52982 // it should be stopped 52983 52984 mainSegmentLoader.resetEverything(); 52985 return; 52986 } // although the segment loader is an audio segment loader, call the setAudio 52987 // function to ensure it is prepared to re-append the init segment (or handle other 52988 // config changes) 52989 52990 segmentLoader.setAudio(true); 52991 mainSegmentLoader.setAudio(false); 52992 } 52993 if (previousActiveLoader === activeGroup.playlistLoader) { 52994 // Nothing has actually changed. This can happen because track change events can fire 52995 // multiple times for a "single" change. One for enabling the new active track, and 52996 // one for disabling the track that was active 52997 startLoaders(activeGroup.playlistLoader, mediaType); 52998 return; 52999 } 53000 if (segmentLoader.track) { 53001 // For WebVTT, set the new text track in the segmentloader 53002 segmentLoader.track(activeTrack); 53003 } // destructive reset 53004 53005 segmentLoader.resetEverything(); 53006 startLoaders(activeGroup.playlistLoader, mediaType); 53007}; 53008const onError = { 53009 /** 53010 * Returns a function to be called when a SegmentLoader or PlaylistLoader encounters 53011 * an error. 53012 * 53013 * @param {string} type 53014 * MediaGroup type 53015 * @param {Object} settings 53016 * Object containing required information for media groups 53017 * @return {Function} 53018 * Error handler. Logs warning (or error if the playlist is excluded) to 53019 * console and switches back to default audio track. 53020 * @function onError.AUDIO 53021 */ 53022 AUDIO: (type, settings) => () => { 53023 const { 53024 mediaTypes: { [type]: mediaType }, 53025 excludePlaylist, 53026 } = settings; // switch back to default audio track 53027 53028 const activeTrack = mediaType.activeTrack(); 53029 const activeGroup = mediaType.activeGroup(); 53030 const id = (activeGroup.filter((group) => group.default)[0] || activeGroup[0]).id; 53031 const defaultTrack = mediaType.tracks[id]; 53032 if (activeTrack === defaultTrack) {
53033 // Default track encountered an error. All we can do now is exclude the current 53034 // rendition and hope another will switch audio groups 53035 excludePlaylist({ 53036 error: { 53037 message: 'Problem encountered loading the default audio track.', 53038 }, 53039 }); 53040 return; 53041 } 53042 videojs.log.warn( 53043 'Problem encountered loading the alternate audio track.' + 'Switching back to default.' 53044 ); 53045 for (const trackId in mediaType.tracks) { 53046 mediaType.tracks[trackId].enabled = mediaType.tracks[trackId] === defaultTrack; 53047 } 53048 mediaType.onTrackChanged(); 53049 }, 53050 /** 53051 * Returns a function to be called when a SegmentLoader or PlaylistLoader encounters 53052 * an error. 53053 * 53054 * @param {string} type 53055 * MediaGroup type 53056 * @param {Object} settings 53057 * Object containing required information for media groups 53058 * @return {Function} 53059 * Error handler. Logs warning to console and disables the active subtitle track 53060 * @function onError.SUBTITLES 53061 */ 53062 SUBTITLES: (type, settings) => () => { 53063 const { 53064 mediaTypes: { [type]: mediaType }, 53065 } = settings; 53066 videojs.log.warn( 53067 'Problem encountered loading the subtitle track.' + 'Disabling subtitle track.' 53068 ); 53069 const track = mediaType.activeTrack(); 53070 if (track) { 53071 track.mode = 'disabled'; 53072 } 53073 mediaType.onTrackChanged(); 53074 }, 53075}; 53076const setupListeners = { 53077 /** 53078 * Setup event listeners for audio playlist loader 53079 * 53080 * @param {string} type 53081 * MediaGroup type 53082 * @param {PlaylistLoader|null} playlistLoader 53083 * PlaylistLoader to register listeners on 53084 * @param {Object} settings 53085 * Object containing required information for media groups 53086 * @function setupListeners.AUDIO 53087 */ 53088 AUDIO: (type, playlistLoader, settings) => { 53089 if (!playlistLoader) { 53090 // no playlist loader means audio will be muxed with the video 53091 return; 53092 } 53093 const { 53094 tech, 53095 requestOptions, 53096 segmentLoaders: { [type]: segmentLoader }, 53097 } = settings; 53098 playlistLoader.on('loadedmetadata', () => { 53099 const media = playlistLoader.media(); 53100 segmentLoader.playlist(media, requestOptions); // if the video is already playing, or if this isn't a live video and preload 53101 // permits, start downloading segments 53102 53103 if (!tech.paused() || (media.endList && tech.preload() !== 'none')) { 53104 segmentLoader.load(); 53105 } 53106 }); 53107 playlistLoader.on('loadedplaylist', () => { 53108 segmentLoader.playlist(playlistLoader.media(), requestOptions); // If the player isn't paused, ensure that the segment loader is running 53109 53110 if (!tech.paused()) { 53111 segmentLoader.load(); 53112 } 53113 }); 53114 playlistLoader.on('error', onError[type](type, settings)); 53115 }, 53116 /** 53117 * Setup event listeners for subtitle playlist loader 53118 * 53119 * @param {string} type 53120 * MediaGroup type 53121 * @param {PlaylistLoader|null} playlistLoader 53122 * PlaylistLoader to register listeners on 53123 * @param {Object} settings 53124 * Object containing required information for media groups 53125 * @function setupListeners.SUBTITLES 53126 */ 53127 SUBTITLES: (type, playlistLoader, settings) => { 53128 const { 53129 tech, 53130 requestOptions, 53131 segmentLoaders: { [type]: segmentLoader }, 53132 mediaTypes: { [type]: mediaType }, 53133 } = settings; 53134 playlistLoader.on('loadedmetadata', () => { 53135 const media = playlistLoader.media(); 53136 segmentLoader.playlist(media, requestOptions); 53137 segmentLoader.track(mediaType.activeTrack()); // if the video is already playing, or if this isn't a live video and preload 53138 // permits, start downloading segments 53139 53140 if (!tech.paused() || (media.endList && tech.preload() !== 'none')) { 53141 segmentLoader.load(); 53142 } 53143 }); 53144 playlistLoader.on('loadedplaylist', () => { 53145 segmentLoader.playlist(playlistLoader.media(), requestOptions); // If the player isn't paused, ensure that the segment loader is running 53146 53147 if (!tech.paused()) { 53148 segmentLoader.load(); 53149 } 53150 }); 53151 playlistLoader.on('error', onError[type](type, settings)); 53152 }, 53153}; 53154const initialize = { 53155 /** 53156 * Setup PlaylistLoaders and AudioTracks for the audio groups 53157 * 53158 * @param {string} type 53159 * MediaGroup type 53160 * @param {Object} settings 53161 * Object containing required information for media groups 53162 * @function initialize.AUDIO 53163 */ 53164 AUDIO: (type, settings) => { 53165 const { 53166 vhs, 53167 sourceType, 53168 segmentLoaders: { [type]: segmentLoader }, 53169 requestOptions, 53170 main: { mediaGroups }, 53171 mediaTypes: { 53172 [type]: { groups, tracks, logger_ }, 53173 }, 53174 mainPlaylistLoader, 53175 } = settings; 53176 const audioOnlyMain = isAudioOnly(mainPlaylistLoader.main); // force a default if we have none 53177 53178 if (!mediaGroups[type] || Object.keys(mediaGroups[type]).length === 0) { 53179 mediaGroups[type] = { 53180 main: { 53181 default: { 53182 default: true, 53183 }, 53184 }, 53185 }; 53186 if (audioOnlyMain) { 53187 mediaGroups[type].main.default.playlists = mainPlaylistLoader.main.playlists; 53188 } 53189 } 53190 for (const groupId in mediaGroups[type]) { 53191 if (!groups[groupId]) { 53192 groups[groupId] = []; 53193 } 53194 for (const variantLabel in mediaGroups[type][groupId]) { 53195 let properties = mediaGroups[type][groupId][variantLabel]; 53196 let playlistLoader; 53197 if (audioOnlyMain) { 53198 logger_(`AUDIO group '${groupId}' label '${variantLabel}' is a main playlist`); 53199 properties.isMainPlaylist = true; 53200 playlistLoader = null; // if vhs-json was provided as the source, and the media playlist was resolved, 53201 // use the resolved media playlist object 53202 } else if (sourceType === 'vhs-json' && properties.playlists) { 53203 playlistLoader = new PlaylistLoader(properties.playlists[0], vhs, requestOptions); 53204 } else if (properties.resolvedUri) { 53205 playlistLoader = new PlaylistLoader(properties.resolvedUri, vhs, requestOptions); // TODO: dash isn't the only type with properties.playlists 53206 // should we even have properties.playlists in this check. 53207 } else if (properties.playlists && sourceType === 'dash') { 53208 playlistLoader = new DashPlaylistLoader( 53209 properties.playlists[0], 53210 vhs, 53211 requestOptions, 53212 mainPlaylistLoader 53213 ); 53214 } else { 53215 // no resolvedUri means the audio is muxed with the video when using this 53216 // audio track 53217 playlistLoader = null; 53218 } 53219 properties = merge( 53220 { 53221 id: variantLabel, 53222 playlistLoader, 53223 }, 53224 properties 53225 ); 53226 setupListeners[type](type, properties.playlistLoader, settings); 53227 groups[groupId].push(properties); 53228 if (typeof tracks[variantLabel] === 'undefined') { 53229 const track = new videojs.AudioTrack({ 53230 id: variantLabel, 53231 kind: audioTrackKind_(properties), 53232 enabled: false, 53233 language: properties.language, 53234 default: properties.default, 53235 label: variantLabel, 53236 }); 53237 tracks[variantLabel] = track; 53238 } 53239 } 53240 } // setup single error event handler for the segment loader 53241 53242 segmentLoader.on('error', onError[type](type, settings)); 53243 }, 53244 /** 53245 * Setup PlaylistLoaders and TextTracks for the subtitle groups 53246 * 53247 * @param {string} type 53248 * MediaGroup type 53249 * @param {Object} settings 53250 * Object containing required information for media groups 53251 * @function initialize.SUBTITLES 53252 */ 53253 SUBTITLES: (type, settings) => { 53254 const { 53255 tech, 53256 vhs, 53257 sourceType, 53258 segmentLoaders: { [type]: segmentLoader }, 53259 requestOptions, 53260 main: { mediaGroups }, 53261 mediaTypes: { 53262 [type]: { groups, tracks },
53263 }, 53264 mainPlaylistLoader, 53265 } = settings; 53266 for (const groupId in mediaGroups[type]) { 53267 if (!groups[groupId]) { 53268 groups[groupId] = []; 53269 } 53270 for (const variantLabel in mediaGroups[type][groupId]) { 53271 if (!vhs.options_.useForcedSubtitles && mediaGroups[type][groupId][variantLabel].forced) { 53272 // Subtitle playlists with the forced attribute are not selectable in Safari. 53273 // According to Apple's HLS Authoring Specification: 53274 // If content has forced subtitles and regular subtitles in a given language, 53275 // the regular subtitles track in that language MUST contain both the forced 53276 // subtitles and the regular subtitles for that language. 53277 // Because of this requirement and that Safari does not add forced subtitles, 53278 // forced subtitles are skipped here to maintain consistent experience across 53279 // all platforms 53280 continue; 53281 } 53282 let properties = mediaGroups[type][groupId][variantLabel]; 53283 let playlistLoader; 53284 if (sourceType === 'hls') { 53285 playlistLoader = new PlaylistLoader(properties.resolvedUri, vhs, requestOptions); 53286 } else if (sourceType === 'dash') { 53287 const playlists = properties.playlists.filter((p) => p.excludeUntil !== Infinity); 53288 if (!playlists.length) { 53289 return; 53290 } 53291 playlistLoader = new DashPlaylistLoader( 53292 properties.playlists[0], 53293 vhs, 53294 requestOptions, 53295 mainPlaylistLoader 53296 ); 53297 } else if (sourceType === 'vhs-json') { 53298 playlistLoader = new PlaylistLoader( 53299 // if the vhs-json object included the media playlist, use the media playlist 53300 // as provided, otherwise use the resolved URI to load the playlist 53301 properties.playlists ? properties.playlists[0] : properties.resolvedUri, 53302 vhs, 53303 requestOptions 53304 ); 53305 } 53306 properties = merge( 53307 { 53308 id: variantLabel, 53309 playlistLoader, 53310 }, 53311 properties 53312 ); 53313 setupListeners[type](type, properties.playlistLoader, settings); 53314 groups[groupId].push(properties); 53315 if (typeof tracks[variantLabel] === 'undefined') { 53316 const track = tech.addRemoteTextTrack( 53317 { 53318 id: variantLabel, 53319 kind: 'subtitles', 53320 default: properties.default && properties.autoselect, 53321 language: properties.language, 53322 label: variantLabel, 53323 }, 53324 false 53325 ).track; 53326 tracks[variantLabel] = track; 53327 } 53328 } 53329 } // setup single error event handler for the segment loader 53330 53331 segmentLoader.on('error', onError[type](type, settings)); 53332 }, 53333 /** 53334 * Setup TextTracks for the closed-caption groups 53335 * 53336 * @param {String} type 53337 * MediaGroup type 53338 * @param {Object} settings 53339 * Object containing required information for media groups 53340 * @function initialize['CLOSED-CAPTIONS'] 53341 */ 53342 'CLOSED-CAPTIONS': (type, settings) => { 53343 const { 53344 tech, 53345 main: { mediaGroups }, 53346 mediaTypes: { 53347 [type]: { groups, tracks }, 53348 }, 53349 } = settings; 53350 for (const groupId in mediaGroups[type]) { 53351 if (!groups[groupId]) { 53352 groups[groupId] = []; 53353 } 53354 for (const variantLabel in mediaGroups[type][groupId]) { 53355 const properties = mediaGroups[type][groupId][variantLabel]; // Look for either 608 (CCn) or 708 (SERVICEn) caption services 53356 53357 if (!/^(?:CC|SERVICE)/.test(properties.instreamId)) { 53358 continue; 53359 } 53360 const captionServices = (tech.options_.vhs && tech.options_.vhs.captionServices) || {}; 53361 let newProps = { 53362 label: variantLabel, 53363 language: properties.language, 53364 instreamId: properties.instreamId, 53365 default: properties.default && properties.autoselect, 53366 }; 53367 if (captionServices[newProps.instreamId]) { 53368 newProps = merge(newProps, captionServices[newProps.instreamId]); 53369 } 53370 if (newProps.default === undefined) { 53371 delete newProps.default; 53372 } // No PlaylistLoader is required for Closed-Captions because the captions are 53373 // embedded within the video stream 53374 53375 groups[groupId].push( 53376 merge( 53377 { 53378 id: variantLabel, 53379 }, 53380 properties 53381 ) 53382 ); 53383 if (typeof tracks[variantLabel] === 'undefined') { 53384 const track = tech.addRemoteTextTrack( 53385 { 53386 id: newProps.instreamId, 53387 kind: 'captions', 53388 default: newProps.default, 53389 language: newProps.language, 53390 label: newProps.label, 53391 }, 53392 false 53393 ).track; 53394 tracks[variantLabel] = track; 53395 } 53396 } 53397 } 53398 }, 53399}; 53400const groupMatch = (list, media) => { 53401 for (let i = 0; i < list.length; i++) { 53402 if (playlistMatch(media, list[i])) { 53403 return true; 53404 } 53405 if (list[i].playlists && groupMatch(list[i].playlists, media)) { 53406 return true; 53407 } 53408 } 53409 return false;
vendor: 9,168 bytes, lines 53410-53676
53410}; 53411/** 53412 * Returns a function used to get the active group of the provided type 53413 * 53414 * @param {string} type 53415 * MediaGroup type 53416 * @param {Object} settings 53417 * Object containing required information for media groups 53418 * @return {Function} 53419 * Function that returns the active media group for the provided type. Takes an 53420 * optional parameter {TextTrack} track. If no track is provided, a list of all 53421 * variants in the group, otherwise the variant corresponding to the provided 53422 * track is returned. 53423 * @function activeGroup 53424 */ 53425 53426const activeGroup = (type, settings) => (track) => { 53427 const { 53428 mainPlaylistLoader, 53429 mediaTypes: { 53430 [type]: { groups }, 53431 }, 53432 } = settings; 53433 const media = mainPlaylistLoader.media(); 53434 if (!media) { 53435 return null; 53436 } 53437 let variants = null; // set to variants to main media active group 53438 53439 if (media.attributes[type]) { 53440 variants = groups[media.attributes[type]]; 53441 } 53442 const groupKeys = Object.keys(groups); 53443 if (!variants) { 53444 // find the mainPlaylistLoader media 53445 // that is in a media group if we are dealing 53446 // with audio only 53447 if (type === 'AUDIO' && groupKeys.length > 1 && isAudioOnly(settings.main)) { 53448 for (let i = 0; i < groupKeys.length; i++) { 53449 const groupPropertyList = groups[groupKeys[i]]; 53450 if (groupMatch(groupPropertyList, media)) { 53451 variants = groupPropertyList; 53452 break; 53453 } 53454 } // use the main group if it exists 53455 } else if (groups.main) { 53456 variants = groups.main; // only one group, use that one 53457 } else if (groupKeys.length === 1) { 53458 variants = groups[groupKeys[0]]; 53459 } 53460 } 53461 if (typeof track === 'undefined') { 53462 return variants; 53463 } 53464 if (track === null || !variants) { 53465 // An active track was specified so a corresponding group is expected. track === null 53466 // means no track is currently active so there is no corresponding group 53467 return null; 53468 } 53469 return variants.filter((props) => props.id === track.id)[0] || null; 53470}; 53471const activeTrack = { 53472 /** 53473 * Returns a function used to get the active track of type provided 53474 * 53475 * @param {string} type 53476 * MediaGroup type 53477 * @param {Object} settings 53478 * Object containing required information for media groups 53479 * @return {Function} 53480 * Function that returns the active media track for the provided type. Returns 53481 * null if no track is active 53482 * @function activeTrack.AUDIO 53483 */ 53484 AUDIO: (type, settings) => () => { 53485 const { 53486 mediaTypes: { 53487 [type]: { tracks }, 53488 }, 53489 } = settings; 53490 for (const id in tracks) { 53491 if (tracks[id].enabled) { 53492 return tracks[id]; 53493 } 53494 } 53495 return null; 53496 }, 53497 /** 53498 * Returns a function used to get the active track of type provided 53499 * 53500 * @param {string} type 53501 * MediaGroup type 53502 * @param {Object} settings 53503 * Object containing required information for media groups 53504 * @return {Function} 53505 * Function that returns the active media track for the provided type. Returns 53506 * null if no track is active 53507 * @function activeTrack.SUBTITLES 53508 */ 53509 SUBTITLES: (type, settings) => () => { 53510 const { 53511 mediaTypes: { 53512 [type]: { tracks }, 53513 }, 53514 } = settings; 53515 for (const id in tracks) { 53516 if (tracks[id].mode === 'showing' || tracks[id].mode === 'hidden') { 53517 return tracks[id]; 53518 } 53519 } 53520 return null; 53521 }, 53522}; 53523const getActiveGroup = 53524 (type, { mediaTypes }) => 53525 () => { 53526 const activeTrack_ = mediaTypes[type].activeTrack(); 53527 if (!activeTrack_) { 53528 return null; 53529 } 53530 return mediaTypes[type].activeGroup(activeTrack_); 53531 }; 53532/** 53533 * Setup PlaylistLoaders and Tracks for media groups (Audio, Subtitles, 53534 * Closed-Captions) specified in the main manifest. 53535 * 53536 * @param {Object} settings 53537 * Object containing required information for setting up the media groups 53538 * @param {Tech} settings.tech 53539 * The tech of the player 53540 * @param {Object} settings.requestOptions 53541 * XHR request options used by the segment loaders 53542 * @param {PlaylistLoader} settings.mainPlaylistLoader 53543 * PlaylistLoader for the main source 53544 * @param {VhsHandler} settings.vhs 53545 * VHS SourceHandler 53546 * @param {Object} settings.main 53547 * The parsed main manifest 53548 * @param {Object} settings.mediaTypes 53549 * Object to store the loaders, tracks, and utility methods for each media type 53550 * @param {Function} settings.excludePlaylist 53551 * Excludes the current rendition and forces a rendition switch. 53552 * @function setupMediaGroups 53553 */ 53554 53555const setupMediaGroups = (settings) => { 53556 ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach((type) => { 53557 initialize[type](type, settings); 53558 }); 53559 const { 53560 mediaTypes, 53561 mainPlaylistLoader, 53562 tech, 53563 vhs, 53564 segmentLoaders: { ['AUDIO']: audioSegmentLoader, main: mainSegmentLoader }, 53565 } = settings; // setup active group and track getters and change event handlers 53566 53567 ['AUDIO', 'SUBTITLES'].forEach((type) => { 53568 mediaTypes[type].activeGroup = activeGroup(type, settings); 53569 mediaTypes[type].activeTrack = activeTrack[type](type, settings); 53570 mediaTypes[type].onGroupChanged = onGroupChanged(type, settings); 53571 mediaTypes[type].onGroupChanging = onGroupChanging(type, settings); 53572 mediaTypes[type].onTrackChanged = onTrackChanged(type, settings); 53573 mediaTypes[type].getActiveGroup = getActiveGroup(type, settings); 53574 }); // DO NOT enable the default subtitle or caption track. 53575 // DO enable the default audio track 53576 53577 const audioGroup = mediaTypes.AUDIO.activeGroup(); 53578 if (audioGroup) { 53579 const groupId = (audioGroup.filter((group) => group.default)[0] || audioGroup[0]).id; 53580 mediaTypes.AUDIO.tracks[groupId].enabled = true; 53581 mediaTypes.AUDIO.onGroupChanged(); 53582 mediaTypes.AUDIO.onTrackChanged(); 53583 const activeAudioGroup = mediaTypes.AUDIO.getActiveGroup(); // a similar check for handling setAudio on each loader is run again each time the 53584 // track is changed, but needs to be handled here since the track may not be considered 53585 // changed on the first call to onTrackChanged 53586 53587 if (!activeAudioGroup.playlistLoader) { 53588 // either audio is muxed with video or the stream is audio only 53589 mainSegmentLoader.setAudio(true); 53590 } else { 53591 // audio is demuxed 53592 mainSegmentLoader.setAudio(false); 53593 audioSegmentLoader.setAudio(true); 53594 } 53595 } 53596 mainPlaylistLoader.on('mediachange', () => { 53597 ['AUDIO', 'SUBTITLES'].forEach((type) => mediaTypes[type].onGroupChanged()); 53598 }); 53599 mainPlaylistLoader.on('mediachanging', () => { 53600 ['AUDIO', 'SUBTITLES'].forEach((type) => mediaTypes[type].onGroupChanging()); 53601 }); // custom audio track change event handler for usage event 53602 53603 const onAudioTrackChanged = () => { 53604 mediaTypes.AUDIO.onTrackChanged(); 53605 tech.trigger({ 53606 type: 'usage', 53607 name: 'vhs-audio-change', 53608 }); 53609 }; 53610 tech.audioTracks().addEventListener('change', onAudioTrackChanged); 53611 tech.remoteTextTracks().addEventListener('change', mediaTypes.SUBTITLES.onTrackChanged); 53612 vhs.on('dispose', () => { 53613 tech.audioTracks().removeEventListener('change', onAudioTrackChanged); 53614 tech.remoteTextTracks().removeEventListener('change', mediaTypes.SUBTITLES.onTrackChanged); 53615 }); // clear existing audio tracks and add the ones we just created 53616 53617 tech.clearTracks('audio'); 53618 for (const id in mediaTypes.AUDIO.tracks) { 53619 tech.audioTracks().addTrack(mediaTypes.AUDIO.tracks[id]); 53620 } 53621}; 53622/** 53623 * Creates skeleton object used to store the loaders, tracks, and utility methods for each 53624 * media type 53625 * 53626 * @return {Object} 53627 * Object to store the loaders, tracks, and utility methods for each media type 53628 * @function createMediaTypes 53629 */ 53630 53631const createMediaTypes = () => { 53632 const mediaTypes = {}; 53633 ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach((type) => { 53634 mediaTypes[type] = { 53635 groups: {}, 53636 tracks: {}, 53637 activePlaylistLoader: null, 53638 activeGroup: noop, 53639 activeTrack: noop, 53640 getActiveGroup: noop, 53641 onGroupChanged: noop, 53642 onTrackChanged: noop, 53643 lastTrack_: null, 53644 logger_: logger(`MediaGroups[${type}]`), 53645 }; 53646 }); 53647 return mediaTypes; 53648}; 53649 53650/** 53651 * A utility class for setting properties and maintaining the state of the content steering manifest. 53652 * 53653 * Content Steering manifest format: 53654 * VERSION: number (required) currently only version 1 is supported. 53655 * TTL: number in seconds (optional) until the next content steering manifest reload. 53656 * RELOAD-URI: string (optional) uri to fetch the next content steering manifest. 53657 * SERVICE-LOCATION-PRIORITY or PATHWAY-PRIORITY a non empty array of unique string values. 53658 * PATHWAY-CLONES: array (optional) (HLS only) pathway clone objects to copy from other playlists. 53659 */ 53660 53661class SteeringManifest { 53662 constructor() { 53663 this.priority_ = []; 53664 this.pathwayClones_ = new Map(); 53665 } 53666 set version(number) { 53667 // Only version 1 is currently supported for both DASH and HLS. 53668 if (number === 1) { 53669 this.version_ = number; 53670 } 53671 } 53672 set ttl(seconds) { 53673 // TTL = time-to-live, default = 300 seconds. 53674 this.ttl_ = seconds || 300; 53675 } 53676 set reloadUri(uri) {
53677 if (uri) { 53678 // reload URI can be relative to the previous reloadUri. 53679 this.reloadUri_ = resolveUrl(this.reloadUri_, uri); 53680 } 53681 } 53682 set priority(array) { 53683 // priority must be non-empty and unique values. 53684 if (array && array.length) { 53685 this.priority_ = array; 53686 } 53687 } 53688 set pathwayClones(array) { 53689 // pathwayClones must be non-empty. 53690 if (array && array.length) { 53691 this.pathwayClones_ = new Map(array.map((clone) => [clone.ID, clone])); 53692 } 53693 } 53694 get version() { 53695 return this.version_; 53696 } 53697 get ttl() { 53698 return this.ttl_; 53699 } 53700 get reloadUri() { 53701 return this.reloadUri_; 53702 } 53703 get priority() { 53704 return this.priority_; 53705 } 53706 get pathwayClones() { 53707 return this.pathwayClones_; 53708 } 53709} 53710/** 53711 * This class represents a content steering manifest and associated state. See both HLS and DASH specifications. 53712 * HLS: https://developer.apple.com/streaming/HLSContentSteeringSpecification.pdf and 53713 * https://datatracker.ietf.org/doc/draft-pantos-hls-rfc8216bis/ section 4.4.6.6. 53714 * DASH: https://dashif.org/docs/DASH-IF-CTS-00XX-Content-Steering-Community-Review.pdf 53715 * 53716 * @param {function} xhr for making a network request from the browser. 53717 * @param {function} bandwidth for fetching the current bandwidth from the main segment loader. 53718 */ 53719 53720class ContentSteeringController extends videojs.EventTarget { 53721 constructor(xhr, bandwidth) { 53722 super(); 53723 this.currentPathway = null; 53724 this.defaultPathway = null; 53725 this.queryBeforeStart = false;
vendor: 3,962 bytes, lines 53726-53818
53726 this.availablePathways_ = new Set(); 53727 this.steeringManifest = new SteeringManifest(); 53728 this.proxyServerUrl_ = null; 53729 this.manifestType_ = null; 53730 this.ttlTimeout_ = null; 53731 this.request_ = null; 53732 this.currentPathwayClones = new Map(); 53733 this.nextPathwayClones = new Map(); 53734 this.excludedSteeringManifestURLs = new Set(); 53735 this.logger_ = logger('Content Steering'); 53736 this.xhr_ = xhr; 53737 this.getBandwidth_ = bandwidth; 53738 } 53739 /** 53740 * Assigns the content steering tag properties to the steering controller 53741 * 53742 * @param {string} baseUrl the baseURL from the main manifest for resolving the steering manifest url 53743 * @param {Object} steeringTag the content steering tag from the main manifest 53744 */ 53745 53746 assignTagProperties(baseUrl, steeringTag) { 53747 this.manifestType_ = steeringTag.serverUri ? 'HLS' : 'DASH'; // serverUri is HLS serverURL is DASH 53748 53749 const steeringUri = steeringTag.serverUri || steeringTag.serverURL; 53750 if (!steeringUri) { 53751 this.logger_(`steering manifest URL is ${steeringUri}, cannot request steering manifest.`); 53752 this.trigger('error'); 53753 return; 53754 } // Content steering manifests can be encoded as a data URI. We can decode, parse and return early if that's the case. 53755 53756 if (steeringUri.startsWith('data:')) { 53757 this.decodeDataUriManifest_(steeringUri.substring(steeringUri.indexOf(',') + 1)); 53758 return; 53759 } // reloadUri is the resolution of the main manifest URL and steering URL. 53760 53761 this.steeringManifest.reloadUri = resolveUrl(baseUrl, steeringUri); // pathwayId is HLS defaultServiceLocation is DASH 53762 53763 this.defaultPathway = steeringTag.pathwayId || steeringTag.defaultServiceLocation; // currently only DASH supports the following properties on <ContentSteering> tags. 53764 53765 this.queryBeforeStart = steeringTag.queryBeforeStart; 53766 this.proxyServerUrl_ = steeringTag.proxyServerURL; // trigger a steering event if we have a pathway from the content steering tag. 53767 // this tells VHS which segment pathway to start with. 53768 // If queryBeforeStart is true we need to wait for the steering manifest response. 53769 53770 if (this.defaultPathway && !this.queryBeforeStart) { 53771 this.trigger('content-steering'); 53772 } 53773 } 53774 /** 53775 * Requests the content steering manifest and parse the response. This should only be called after 53776 * assignTagProperties was called with a content steering tag. 53777 * 53778 * @param {string} initialUri The optional uri to make the request with. 53779 * If set, the request should be made with exactly what is passed in this variable. 53780 * This scenario should only happen once on initalization. 53781 */ 53782 53783 requestSteeringManifest(initial) { 53784 const reloadUri = this.steeringManifest.reloadUri; 53785 if (!reloadUri) { 53786 return; 53787 } // We currently don't support passing MPD query parameters directly to the content steering URL as this requires 53788 // ExtUrlQueryInfo tag support. See the DASH content steering spec section 8.1. 53789 // This request URI accounts for manifest URIs that have been excluded. 53790 53791 const uri = initial ? reloadUri : this.getRequestURI(reloadUri); // If there are no valid manifest URIs, we should stop content steering. 53792 53793 if (!uri) { 53794 this.logger_('No valid content steering manifest URIs. Stopping content steering.'); 53795 this.trigger('error'); 53796 this.dispose(); 53797 return; 53798 } 53799 const metadata = { 53800 contentSteeringInfo: { 53801 uri, 53802 }, 53803 }; 53804 this.trigger({ 53805 type: 'contentsteeringloadstart', 53806 metadata, 53807 }); 53808 this.request_ = this.xhr_( 53809 { 53810 uri, 53811 requestType: 'content-steering-manifest', 53812 }, 53813 (error, errorInfo) => { 53814 if (error) { 53815 // If the client receives HTTP 410 Gone in response to a manifest request, 53816 // it MUST NOT issue another request for that URI for the remainder of the 53817 // playback session. It MAY continue to use the most-recently obtained set 53818 // of Pathways.
vendor: 5,889 bytes, lines 53819-53954
53819 if (errorInfo.status === 410) { 53820 this.logger_(`manifest request 410 ${error}.`); 53821 this.logger_(`There will be no more content steering requests to ${uri} this session.`); 53822 this.excludedSteeringManifestURLs.add(uri); 53823 return; 53824 } // If the client receives HTTP 429 Too Many Requests with a Retry-After 53825 // header in response to a manifest request, it SHOULD wait until the time 53826 // specified by the Retry-After header to reissue the request. 53827 53828 if (errorInfo.status === 429) { 53829 const retrySeconds = errorInfo.responseHeaders['retry-after']; 53830 this.logger_(`manifest request 429 ${error}.`); 53831 this.logger_(`content steering will retry in ${retrySeconds} seconds.`); 53832 this.startTTLTimeout_(parseInt(retrySeconds, 10)); 53833 return; 53834 } // If the Steering Manifest cannot be loaded and parsed correctly, the 53835 // client SHOULD continue to use the previous values and attempt to reload 53836 // it after waiting for the previously-specified TTL (or 5 minutes if 53837 // none). 53838 53839 this.logger_(`manifest failed to load ${error}.`); 53840 this.startTTLTimeout_(); 53841 return; 53842 } 53843 this.trigger({ 53844 type: 'contentsteeringloadcomplete', 53845 metadata, 53846 }); 53847 let steeringManifestJson; 53848 try { 53849 steeringManifestJson = JSON.parse(this.request_.responseText); 53850 } catch (parseError) { 53851 const errorMetadata = { 53852 errorType: videojs.Error.StreamingContentSteeringParserError, 53853 error: parseError, 53854 }; 53855 this.trigger({ 53856 type: 'error', 53857 metadata: errorMetadata, 53858 }); 53859 } 53860 this.assignSteeringProperties_(steeringManifestJson); 53861 const parsedMetadata = { 53862 contentSteeringInfo: metadata.contentSteeringInfo, 53863 contentSteeringManifest: { 53864 version: this.steeringManifest.version, 53865 reloadUri: this.steeringManifest.reloadUri, 53866 priority: this.steeringManifest.priority, 53867 }, 53868 }; 53869 this.trigger({ 53870 type: 'contentsteeringparsed', 53871 metadata: parsedMetadata, 53872 }); 53873 this.startTTLTimeout_(); 53874 } 53875 ); 53876 } 53877 /** 53878 * Set the proxy server URL and add the steering manifest url as a URI encoded parameter. 53879 * 53880 * @param {string} steeringUrl the steering manifest url 53881 * @return the steering manifest url to a proxy server with all parameters set 53882 */ 53883 53884 setProxyServerUrl_(steeringUrl) { 53885 const steeringUrlObject = new window__default['default'].URL(steeringUrl); 53886 const proxyServerUrlObject = new window__default['default'].URL(this.proxyServerUrl_); 53887 proxyServerUrlObject.searchParams.set('url', encodeURI(steeringUrlObject.toString())); 53888 return this.setSteeringParams_(proxyServerUrlObject.toString()); 53889 } 53890 /** 53891 * Decodes and parses the data uri encoded steering manifest 53892 * 53893 * @param {string} dataUri the data uri to be decoded and parsed. 53894 */ 53895 53896 decodeDataUriManifest_(dataUri) { 53897 const steeringManifestJson = JSON.parse(window__default['default'].atob(dataUri)); 53898 this.assignSteeringProperties_(steeringManifestJson); 53899 } 53900 /** 53901 * Set the HLS or DASH content steering manifest request query parameters. For example: 53902 * _HLS_pathway="<CURRENT-PATHWAY-ID>" and _HLS_throughput=<THROUGHPUT> 53903 * _DASH_pathway and _DASH_throughput 53904 * 53905 * @param {string} uri to add content steering server parameters to. 53906 * @return a new uri as a string with the added steering query parameters. 53907 */ 53908 53909 setSteeringParams_(url) { 53910 const urlObject = new window__default['default'].URL(url); 53911 const path = this.getPathway(); 53912 const networkThroughput = this.getBandwidth_(); 53913 if (path) { 53914 const pathwayKey = `_${this.manifestType_}_pathway`; 53915 urlObject.searchParams.set(pathwayKey, path); 53916 } 53917 if (networkThroughput) { 53918 const throughputKey = `_${this.manifestType_}_throughput`; 53919 urlObject.searchParams.set(throughputKey, networkThroughput); 53920 } 53921 return urlObject.toString(); 53922 } 53923 /** 53924 * Assigns the current steering manifest properties and to the SteeringManifest object 53925 * 53926 * @param {Object} steeringJson the raw JSON steering manifest 53927 */ 53928 53929 assignSteeringProperties_(steeringJson) { 53930 this.steeringManifest.version = steeringJson.VERSION; 53931 if (!this.steeringManifest.version) { 53932 this.logger_(`manifest version is ${steeringJson.VERSION}, which is not supported.`); 53933 this.trigger('error'); 53934 return; 53935 } 53936 this.steeringManifest.ttl = steeringJson.TTL; 53937 this.steeringManifest.reloadUri = steeringJson['RELOAD-URI']; // HLS = PATHWAY-PRIORITY required. DASH = SERVICE-LOCATION-PRIORITY optional 53938 53939 this.steeringManifest.priority = 53940 steeringJson['PATHWAY-PRIORITY'] || steeringJson['SERVICE-LOCATION-PRIORITY']; // Pathway clones to be created/updated in HLS. 53941 // See section 7.2 https://datatracker.ietf.org/doc/draft-pantos-hls-rfc8216bis/ 53942 53943 this.steeringManifest.pathwayClones = steeringJson['PATHWAY-CLONES']; 53944 this.nextPathwayClones = this.steeringManifest.pathwayClones; // 1. apply first pathway from the array. 53945 // 2. if first pathway doesn't exist in manifest, try next pathway. 53946 // a. if all pathways are exhausted, ignore the steering manifest priority. 53947 // 3. if segments fail from an established pathway, try all variants/renditions, then exclude the failed pathway. 53948 // a. exclude a pathway for a minimum of the last TTL duration. Meaning, from the next steering response, 53949 // the excluded pathway will be ignored. 53950 // See excludePathway usage in excludePlaylist(). 53951 // If there are no available pathways, we need to stop content steering. 53952 53953 if (!this.availablePathways_.size) { 53954 this.logger_(
53955 'There are no available pathways for content steering. Ending content steering.' 53956 ); 53957 this.trigger('error'); 53958 this.dispose(); 53959 } 53960 const chooseNextPathway = (pathwaysByPriority) => { 53961 for (const path of pathwaysByPriority) { 53962 if (this.availablePathways_.has(path)) { 53963 return path; 53964 } 53965 } // If no pathway matches, ignore the manifest and choose the first available. 53966 53967 return [...this.availablePathways_][0]; 53968 }; 53969 const nextPathway = chooseNextPathway(this.steeringManifest.priority); 53970 if (this.currentPathway !== nextPathway) { 53971 this.currentPathway = nextPathway; 53972 this.trigger('content-steering'); 53973 } 53974 } 53975 /** 53976 * Returns the pathway to use for steering decisions 53977 * 53978 * @return {string} returns the current pathway or the default 53979 */ 53980 53981 getPathway() { 53982 return this.currentPathway || this.defaultPathway; 53983 } 53984 /** 53985 * Chooses the manifest request URI based on proxy URIs and server URLs. 53986 * Also accounts for exclusion on certain manifest URIs. 53987 * 53988 * @param {string} reloadUri the base uri before parameters 53989 * 53990 * @return {string} the final URI for the request to the manifest server. 53991 */ 53992 53993 getRequestURI(reloadUri) { 53994 if (!reloadUri) { 53995 return null; 53996 } 53997 const isExcluded = (uri) => this.excludedSteeringManifestURLs.has(uri); 53998 if (this.proxyServerUrl_) { 53999 const proxyURI = this.setProxyServerUrl_(reloadUri); 54000 if (!isExcluded(proxyURI)) { 54001 return proxyURI; 54002 } 54003 } 54004 const steeringURI = this.setSteeringParams_(reloadUri); 54005 if (!isExcluded(steeringURI)) { 54006 return steeringURI; 54007 } // Return nothing if all valid manifest URIs are excluded. 54008 54009 return null; 54010 } 54011 /** 54012 * Start the timeout for re-requesting the steering manifest at the TTL interval. 54013 * 54014 * @param {number} ttl time in seconds of the timeout. Defaults to the 54015 * ttl interval in the steering manifest 54016 */ 54017 54018 startTTLTimeout_(ttl = this.steeringManifest.ttl) { 54019 // 300 (5 minutes) is the default value. 54020 const ttlMS = ttl * 1000; 54021 this.ttlTimeout_ = window__default['default'].setTimeout(() => { 54022 this.requestSteeringManifest(); 54023 }, ttlMS); 54024 } 54025 /** 54026 * Clear the TTL timeout if necessary. 54027 */ 54028 54029 clearTTLTimeout_() { 54030 window__default['default'].clearTimeout(this.ttlTimeout_); 54031 this.ttlTimeout_ = null; 54032 } 54033 /** 54034 * aborts any current steering xhr and sets the current request object to null 54035 */ 54036 54037 abort() { 54038 if (this.request_) { 54039 this.request_.abort(); 54040 } 54041 this.request_ = null; 54042 } 54043 /** 54044 * aborts steering requests clears the ttl timeout and resets all properties. 54045 */ 54046 54047 dispose() { 54048 this.off('content-steering'); 54049 this.off('error'); 54050 this.abort(); 54051 this.clearTTLTimeout_(); 54052 this.currentPathway = null; 54053 this.defaultPathway = null; 54054 this.queryBeforeStart = null; 54055 this.proxyServerUrl_ = null; 54056 this.manifestType_ = null; 54057 this.ttlTimeout_ = null; 54058 this.request_ = null; 54059 this.excludedSteeringManifestURLs = new Set(); 54060 this.availablePathways_ = new Set(); 54061 this.steeringManifest = new SteeringManifest(); 54062 } 54063 /** 54064 * adds a pathway to the available pathways set 54065 * 54066 * @param {string} pathway the pathway string to add 54067 */ 54068 54069 addAvailablePathway(pathway) { 54070 if (pathway) { 54071 this.availablePathways_.add(pathway); 54072 } 54073 } 54074 /** 54075 * Clears all pathways from the available pathways set 54076 */ 54077 54078 clearAvailablePathways() { 54079 this.availablePathways_.clear(); 54080 } 54081 /** 54082 * Removes a pathway from the available pathways set. 54083 */ 54084 54085 excludePathway(pathway) { 54086 return this.availablePathways_.delete(pathway); 54087 } 54088 /** 54089 * Checks the refreshed DASH manifest content steering tag for changes. 54090 * 54091 * @param {string} baseURL new steering tag on DASH manifest refresh 54092 * @param {Object} newTag the new tag to check for changes 54093 * @return a true or false whether the new tag has different values 54094 */ 54095 54096 didDASHTagChange(baseURL, newTag) { 54097 return ( 54098 (!newTag && this.steeringManifest.reloadUri) || 54099 (newTag && 54100 (resolveUrl(baseURL, newTag.serverURL) !== this.steeringManifest.reloadUri || 54101 newTag.defaultServiceLocation !== this.defaultPathway || 54102 newTag.queryBeforeStart !== this.queryBeforeStart || 54103 newTag.proxyServerURL !== this.proxyServerUrl_)) 54104 ); 54105 } 54106 getAvailablePathways() { 54107 return this.availablePathways_; 54108 } 54109} 54110const ABORT_EARLY_EXCLUSION_SECONDS = 10; 54111let Vhs$1; // SegmentLoader stats that need to have each loader's 54112// values summed to calculate the final value 54113 54114const loaderStats = [ 54115 'mediaRequests', 54116 'mediaRequestsAborted', 54117 'mediaRequestsTimedout', 54118 'mediaRequestsErrored', 54119 'mediaTransferDuration', 54120 'mediaBytesTransferred', 54121 'mediaAppends', 54122]; 54123const sumLoaderStat = function (stat) { 54124 return this.audioSegmentLoader_[stat] + this.mainSegmentLoader_[stat]; 54125}; 54126const shouldSwitchToMedia = function ({ 54127 currentPlaylist, 54128 buffered, 54129 currentTime, 54130 nextPlaylist, 54131 bufferLowWaterLine, 54132 bufferHighWaterLine, 54133 duration, 54134 bufferBasedABR, 54135 log, 54136}) { 54137 // we have no other playlist to switch to 54138 if (!nextPlaylist) { 54139 videojs.log.warn('We received no playlist to switch to. Please check your stream.'); 54140 return false;
vendor: 6,210 bytes, lines 54141-54287
54141 } 54142 const sharedLogLine = `allowing switch ${(currentPlaylist && currentPlaylist.id) || 'null'} -> ${nextPlaylist.id}`; 54143 if (!currentPlaylist) { 54144 log(`${sharedLogLine} as current playlist is not set`); 54145 return true; 54146 } // no need to switch if playlist is the same 54147 54148 if (nextPlaylist.id === currentPlaylist.id) { 54149 return false; 54150 } // determine if current time is in a buffered range. 54151 54152 const isBuffered = Boolean(findRange(buffered, currentTime).length); // If the playlist is live, then we want to not take low water line into account. 54153 // This is because in LIVE, the player plays 3 segments from the end of the 54154 // playlist, and if `BUFFER_LOW_WATER_LINE` is greater than the duration availble 54155 // in those segments, a viewer will never experience a rendition upswitch. 54156 54157 if (!currentPlaylist.endList) { 54158 // For LLHLS live streams, don't switch renditions before playback has started, as it almost 54159 // doubles the time to first playback. 54160 if (!isBuffered && typeof currentPlaylist.partTargetDuration === 'number') { 54161 log( 54162 `not ${sharedLogLine} as current playlist is live llhls, but currentTime isn't in buffered.` 54163 ); 54164 return false; 54165 } 54166 log(`${sharedLogLine} as current playlist is live`); 54167 return true; 54168 } 54169 const forwardBuffer = timeAheadOf(buffered, currentTime); 54170 const maxBufferLowWaterLine = bufferBasedABR 54171 ? Config.EXPERIMENTAL_MAX_BUFFER_LOW_WATER_LINE 54172 : Config.MAX_BUFFER_LOW_WATER_LINE; // For the same reason as LIVE, we ignore the low water line when the VOD 54173 // duration is below the max potential low water line 54174 54175 if (duration < maxBufferLowWaterLine) { 54176 log( 54177 `${sharedLogLine} as duration < max low water line (${duration} < ${maxBufferLowWaterLine})` 54178 ); 54179 return true; 54180 } 54181 const nextBandwidth = nextPlaylist.attributes.BANDWIDTH; 54182 const currBandwidth = currentPlaylist.attributes.BANDWIDTH; // when switching down, if our buffer is lower than the high water line, 54183 // we can switch down 54184 54185 if (nextBandwidth < currBandwidth && (!bufferBasedABR || forwardBuffer < bufferHighWaterLine)) { 54186 let logLine = `${sharedLogLine} as next bandwidth < current bandwidth (${nextBandwidth} < ${currBandwidth})`; 54187 if (bufferBasedABR) { 54188 logLine += ` and forwardBuffer < bufferHighWaterLine (${forwardBuffer} < ${bufferHighWaterLine})`; 54189 } 54190 log(logLine); 54191 return true; 54192 } // and if our buffer is higher than the low water line, 54193 // we can switch up 54194 54195 if ((!bufferBasedABR || nextBandwidth > currBandwidth) && forwardBuffer >= bufferLowWaterLine) { 54196 let logLine = `${sharedLogLine} as forwardBuffer >= bufferLowWaterLine (${forwardBuffer} >= ${bufferLowWaterLine})`; 54197 if (bufferBasedABR) { 54198 logLine += ` and next bandwidth > current bandwidth (${nextBandwidth} > ${currBandwidth})`; 54199 } 54200 log(logLine); 54201 return true; 54202 } 54203 log(`not ${sharedLogLine} as no switching criteria met`); 54204 return false; 54205}; 54206/** 54207 * the main playlist controller controller all interactons 54208 * between playlists and segmentloaders. At this time this mainly 54209 * involves a main playlist and a series of audio playlists 54210 * if they are available 54211 * 54212 * @class PlaylistController 54213 * @extends videojs.EventTarget 54214 */ 54215 54216class PlaylistController extends videojs.EventTarget { 54217 constructor(options) { 54218 super(); 54219 const { 54220 src, 54221 withCredentials, 54222 tech, 54223 bandwidth, 54224 externVhs, 54225 useCueTags, 54226 playlistExclusionDuration, 54227 enableLowInitialPlaylist, 54228 sourceType, 54229 cacheEncryptionKeys, 54230 bufferBasedABR, 54231 leastPixelDiffSelector, 54232 captionServices, 54233 experimentalUseMMS, 54234 } = options; 54235 if (!src) { 54236 throw new Error('A non-empty playlist URL or JSON manifest string is required'); 54237 } 54238 let { maxPlaylistRetries } = options; 54239 if (maxPlaylistRetries === null || typeof maxPlaylistRetries === 'undefined') { 54240 maxPlaylistRetries = Infinity; 54241 } 54242 Vhs$1 = externVhs; 54243 this.bufferBasedABR = Boolean(bufferBasedABR); 54244 this.leastPixelDiffSelector = Boolean(leastPixelDiffSelector); 54245 this.withCredentials = withCredentials; 54246 this.tech_ = tech; 54247 this.vhs_ = tech.vhs; 54248 this.player_ = options.player_; 54249 this.sourceType_ = sourceType; 54250 this.useCueTags_ = useCueTags; 54251 this.playlistExclusionDuration = playlistExclusionDuration; 54252 this.maxPlaylistRetries = maxPlaylistRetries; 54253 this.enableLowInitialPlaylist = enableLowInitialPlaylist; 54254 if (this.useCueTags_) { 54255 this.cueTagsTrack_ = this.tech_.addTextTrack('metadata', 'ad-cues'); 54256 this.cueTagsTrack_.inBandMetadataTrackDispatchType = ''; 54257 } 54258 this.requestOptions_ = { 54259 withCredentials, 54260 maxPlaylistRetries, 54261 timeout: null, 54262 }; 54263 this.on('error', this.pauseLoading); 54264 this.mediaTypes_ = createMediaTypes(); 54265 if (experimentalUseMMS && window__default['default'].ManagedMediaSource) { 54266 // Airplay source not yet implemented. Remote playback must be disabled. 54267 this.tech_.el_.disableRemotePlayback = true; 54268 this.mediaSource = new window__default['default'].ManagedMediaSource(); 54269 videojs.log('Using ManagedMediaSource'); 54270 } else if (window__default['default'].MediaSource) { 54271 this.mediaSource = new window__default['default'].MediaSource(); 54272 } 54273 this.handleDurationChange_ = this.handleDurationChange_.bind(this); 54274 this.handleSourceOpen_ = this.handleSourceOpen_.bind(this); 54275 this.handleSourceEnded_ = this.handleSourceEnded_.bind(this); 54276 this.load = this.load.bind(this); 54277 this.pause = this.pause.bind(this); 54278 this.mediaSource.addEventListener('durationchange', this.handleDurationChange_); // load the media source into the player 54279 54280 this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen_); 54281 this.mediaSource.addEventListener('sourceended', this.handleSourceEnded_); 54282 this.mediaSource.addEventListener('startstreaming', this.load); 54283 this.mediaSource.addEventListener('endstreaming', this.pause); // we don't have to handle sourceclose since dispose will handle termination of 54284 // everything, and the MediaSource should not be detached without a proper disposal 54285 54286 this.seekable_ = createTimeRanges(); 54287 this.hasPlayed_ = false;
54288 this.syncController_ = new SyncController(options); 54289 this.segmentMetadataTrack_ = tech.addRemoteTextTrack( 54290 { 54291 kind: 'metadata', 54292 label: 'segment-metadata', 54293 }, 54294 false 54295 ).track; 54296 this.decrypter_ = new Decrypter(); 54297 this.sourceUpdater_ = new SourceUpdater(this.mediaSource); 54298 this.inbandTextTracks_ = {}; 54299 this.timelineChangeController_ = new TimelineChangeController(); 54300 this.keyStatusMap_ = new Map(); 54301 const segmentLoaderSettings = { 54302 vhs: this.vhs_, 54303 parse708captions: options.parse708captions, 54304 useDtsForTimestampOffset: options.useDtsForTimestampOffset, 54305 captionServices, 54306 mediaSource: this.mediaSource, 54307 currentTime: this.tech_.currentTime.bind(this.tech_), 54308 seekable: () => this.seekable(), 54309 seeking: () => this.tech_.seeking(), 54310 duration: () => this.duration(), 54311 hasPlayed: () => this.hasPlayed_, 54312 goalBufferLength: () => this.goalBufferLength(), 54313 bandwidth, 54314 syncController: this.syncController_, 54315 decrypter: this.decrypter_, 54316 sourceType: this.sourceType_, 54317 inbandTextTracks: this.inbandTextTracks_, 54318 cacheEncryptionKeys, 54319 sourceUpdater: this.sourceUpdater_, 54320 timelineChangeController: this.timelineChangeController_, 54321 exactManifestTimings: options.exactManifestTimings, 54322 addMetadataToTextTrack: this.addMetadataToTextTrack.bind(this), 54323 }; // The source type check not only determines whether a special DASH playlist loader 54324 // should be used, but also covers the case where the provided src is a vhs-json 54325 // manifest object (instead of a URL). In the case of vhs-json, the default 54326 // PlaylistLoader should be used. 54327 54328 this.mainPlaylistLoader_ = 54329 this.sourceType_ === 'dash' 54330 ? new DashPlaylistLoader( 54331 src, 54332 this.vhs_, 54333 merge(this.requestOptions_, { 54334 addMetadataToTextTrack: this.addMetadataToTextTrack.bind(this), 54335 }) 54336 ) 54337 : new PlaylistLoader( 54338 src, 54339 this.vhs_, 54340 merge(this.requestOptions_, { 54341 addDateRangesToTextTrack: this.addDateRangesToTextTrack_.bind(this), 54342 }) 54343 ); 54344 this.setupMainPlaylistLoaderListeners_(); // setup segment loaders 54345 // combined audio/video or just video when alternate audio track is selected 54346 54347 this.mainSegmentLoader_ = new SegmentLoader( 54348 merge(segmentLoaderSettings, { 54349 segmentMetadataTrack: this.segmentMetadataTrack_, 54350 loaderType: 'main', 54351 }), 54352 options 54353 ); // alternate audio track 54354 54355 this.audioSegmentLoader_ = new SegmentLoader( 54356 merge(segmentLoaderSettings, { 54357 loaderType: 'audio', 54358 }), 54359 options 54360 ); 54361 this.subtitleSegmentLoader_ = new VTTSegmentLoader( 54362 merge(segmentLoaderSettings, { 54363 loaderType: 'vtt', 54364 featuresNativeTextTracks: this.tech_.featuresNativeTextTracks, 54365 loadVttJs: () => 54366 new Promise((resolve, reject) => { 54367 function onLoad() { 54368 tech.off('vttjserror', onError); 54369 resolve(); 54370 } 54371 function onError() { 54372 tech.off('vttjsloaded', onLoad); 54373 reject(); 54374 } 54375 tech.one('vttjsloaded', onLoad); 54376 tech.one('vttjserror', onError); // safe to call multiple times, script will be loaded only once: 54377 54378 tech.addWebVttScript_(); 54379 }), 54380 }), 54381 options 54382 ); 54383 const getBandwidth = () => { 54384 return this.mainSegmentLoader_.bandwidth; 54385 }; 54386 this.contentSteeringController_ = new ContentSteeringController(this.vhs_.xhr, getBandwidth); 54387 this.setupSegmentLoaderListeners_(); 54388 if (this.bufferBasedABR) { 54389 this.mainPlaylistLoader_.one('loadedplaylist', () => this.startABRTimer_()); 54390 this.tech_.on('pause', () => this.stopABRTimer_()); 54391 this.tech_.on('play', () => this.startABRTimer_()); 54392 } // Create SegmentLoader stat-getters 54393 // mediaRequests_ 54394 // mediaRequestsAborted_ 54395 // mediaRequestsTimedout_ 54396 // mediaRequestsErrored_ 54397 // mediaTransferDuration_ 54398 // mediaBytesTransferred_ 54399 // mediaAppends_ 54400
vendor: 6,245 bytes, lines 54401-54583
54401 loaderStats.forEach((stat) => { 54402 this[stat + '_'] = sumLoaderStat.bind(this, stat); 54403 }); 54404 this.logger_ = logger('pc'); 54405 this.triggeredFmp4Usage = false; 54406 if (this.tech_.preload() === 'none') { 54407 this.loadOnPlay_ = () => { 54408 this.loadOnPlay_ = null; 54409 this.mainPlaylistLoader_.load(); 54410 }; 54411 this.tech_.one('play', this.loadOnPlay_); 54412 } else { 54413 this.mainPlaylistLoader_.load(); 54414 } 54415 this.timeToLoadedData__ = -1; 54416 this.mainAppendsToLoadedData__ = -1; 54417 this.audioAppendsToLoadedData__ = -1; 54418 const event = this.tech_.preload() === 'none' ? 'play' : 'loadstart'; // start the first frame timer on loadstart or play (for preload none) 54419 54420 this.tech_.one(event, () => { 54421 const timeToLoadedDataStart = Date.now(); 54422 this.tech_.one('loadeddata', () => { 54423 this.timeToLoadedData__ = Date.now() - timeToLoadedDataStart; 54424 this.mainAppendsToLoadedData__ = this.mainSegmentLoader_.mediaAppends; 54425 this.audioAppendsToLoadedData__ = this.audioSegmentLoader_.mediaAppends; 54426 }); 54427 }); 54428 } 54429 mainAppendsToLoadedData_() { 54430 return this.mainAppendsToLoadedData__; 54431 } 54432 audioAppendsToLoadedData_() { 54433 return this.audioAppendsToLoadedData__; 54434 } 54435 appendsToLoadedData_() { 54436 const main = this.mainAppendsToLoadedData_(); 54437 const audio = this.audioAppendsToLoadedData_(); 54438 if (main === -1 || audio === -1) { 54439 return -1; 54440 } 54441 return main + audio; 54442 } 54443 timeToLoadedData_() { 54444 return this.timeToLoadedData__; 54445 } 54446 /** 54447 * Run selectPlaylist and switch to the new playlist if we should 54448 * 54449 * @param {string} [reason=abr] a reason for why the ABR check is made 54450 * @private 54451 */ 54452 54453 checkABR_(reason = 'abr') { 54454 const nextPlaylist = this.selectPlaylist(); 54455 if (nextPlaylist && this.shouldSwitchToMedia_(nextPlaylist)) { 54456 this.switchMedia_(nextPlaylist, reason); 54457 } 54458 } 54459 switchMedia_(playlist, cause, delay) { 54460 const oldMedia = this.media(); 54461 const oldId = oldMedia && (oldMedia.id || oldMedia.uri); 54462 const newId = playlist && (playlist.id || playlist.uri); 54463 if (oldId && oldId !== newId) { 54464 this.logger_(`switch media ${oldId} -> ${newId} from ${cause}`); 54465 const metadata = { 54466 renditionInfo: { 54467 id: newId, 54468 bandwidth: playlist.attributes.BANDWIDTH, 54469 resolution: playlist.attributes.RESOLUTION, 54470 codecs: playlist.attributes.CODECS, 54471 }, 54472 cause, 54473 }; 54474 this.trigger({ 54475 type: 'renditionselected', 54476 metadata, 54477 }); 54478 this.tech_.trigger({ 54479 type: 'usage', 54480 name: `vhs-rendition-change-${cause}`, 54481 }); 54482 } 54483 this.mainPlaylistLoader_.media(playlist, delay); 54484 } 54485 /** 54486 * A function that ensures we switch our playlists inside of `mediaTypes` 54487 * to match the current `serviceLocation` provided by the contentSteering controller. 54488 * We want to check media types of `AUDIO`, `SUBTITLES`, and `CLOSED-CAPTIONS`. 54489 * 54490 * This should only be called on a DASH playback scenario while using content steering. 54491 * This is necessary due to differences in how media in HLS manifests are generally tied to 54492 * a video playlist, where in DASH that is not always the case. 54493 */ 54494 54495 switchMediaForDASHContentSteering_() { 54496 ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach((type) => { 54497 const mediaType = this.mediaTypes_[type]; 54498 const activeGroup = mediaType ? mediaType.activeGroup() : null; 54499 const pathway = this.contentSteeringController_.getPathway(); 54500 if (activeGroup && pathway) { 54501 // activeGroup can be an array or a single group 54502 const mediaPlaylists = activeGroup.length 54503 ? activeGroup[0].playlists 54504 : activeGroup.playlists; 54505 const dashMediaPlaylists = mediaPlaylists.filter( 54506 (p) => p.attributes.serviceLocation === pathway 54507 ); // Switch the current active playlist to the correct CDN 54508 54509 if (dashMediaPlaylists.length) { 54510 this.mediaTypes_[type].activePlaylistLoader.media(dashMediaPlaylists[0]); 54511 } 54512 } 54513 }); 54514 } 54515 /** 54516 * Start a timer that periodically calls checkABR_ 54517 * 54518 * @private 54519 */ 54520 54521 startABRTimer_() { 54522 this.stopABRTimer_(); 54523 this.abrTimer_ = window__default['default'].setInterval(() => this.checkABR_(), 250); 54524 } 54525 /** 54526 * Stop the timer that periodically calls checkABR_ 54527 * 54528 * @private 54529 */ 54530 54531 stopABRTimer_() { 54532 // if we're scrubbing, we don't need to pause. 54533 // This getter will be added to Video.js in version 7.11. 54534 if (this.tech_.scrubbing && this.tech_.scrubbing()) { 54535 return; 54536 } 54537 window__default['default'].clearInterval(this.abrTimer_); 54538 this.abrTimer_ = null; 54539 } 54540 /** 54541 * Get a list of playlists for the currently selected audio playlist 54542 * 54543 * @return {Array} the array of audio playlists 54544 */ 54545 54546 getAudioTrackPlaylists_() { 54547 const main = this.main(); 54548 const defaultPlaylists = (main && main.playlists) || []; // if we don't have any audio groups then we can only 54549 // assume that the audio tracks are contained in main 54550 // playlist array, use that or an empty array. 54551 54552 if (!main || !main.mediaGroups || !main.mediaGroups.AUDIO) { 54553 return defaultPlaylists; 54554 } 54555 const AUDIO = main.mediaGroups.AUDIO; 54556 const groupKeys = Object.keys(AUDIO); 54557 let track; // get the current active track 54558 54559 if (Object.keys(this.mediaTypes_.AUDIO.groups).length) { 54560 track = this.mediaTypes_.AUDIO.activeTrack(); // or get the default track from main if mediaTypes_ isn't setup yet 54561 } else { 54562 // default group is `main` or just the first group. 54563 const defaultGroup = AUDIO.main || (groupKeys.length && AUDIO[groupKeys[0]]); 54564 for (const label in defaultGroup) { 54565 if (defaultGroup[label].default) { 54566 track = { 54567 label, 54568 }; 54569 break; 54570 } 54571 } 54572 } // no active track no playlists. 54573 54574 if (!track) { 54575 return defaultPlaylists; 54576 } 54577 const playlists = []; // get all of the playlists that are possible for the 54578 // active track. 54579 54580 for (const group in AUDIO) { 54581 if (AUDIO[group][track.label]) { 54582 const properties = AUDIO[group][track.label]; 54583 if (properties.playlists && properties.playlists.length) {
vendor: 5,584 bytes, lines 54584-54727
54584 playlists.push.apply(playlists, properties.playlists); 54585 } else if (properties.uri) { 54586 playlists.push(properties); 54587 } else if (main.playlists.length) { 54588 // if an audio group does not have a uri 54589 // see if we have main playlists that use it as a group. 54590 // if we do then add those to the playlists list. 54591 for (let i = 0; i < main.playlists.length; i++) { 54592 const playlist = main.playlists[i]; 54593 if ( 54594 playlist.attributes && 54595 playlist.attributes.AUDIO && 54596 playlist.attributes.AUDIO === group 54597 ) { 54598 playlists.push(playlist); 54599 } 54600 } 54601 } 54602 } 54603 } 54604 if (!playlists.length) { 54605 return defaultPlaylists; 54606 } 54607 return playlists; 54608 } 54609 /** 54610 * Register event handlers on the main playlist loader. A helper 54611 * function for construction time. 54612 * 54613 * @private 54614 */ 54615 54616 setupMainPlaylistLoaderListeners_() { 54617 this.mainPlaylistLoader_.on('loadedmetadata', () => { 54618 const media = this.mainPlaylistLoader_.media(); 54619 const requestTimeout = media.targetDuration * 1.5 * 1000; // If we don't have any more available playlists, we don't want to 54620 // timeout the request. 54621 54622 if ( 54623 isLowestEnabledRendition(this.mainPlaylistLoader_.main, this.mainPlaylistLoader_.media()) 54624 ) { 54625 this.requestOptions_.timeout = 0; 54626 } else { 54627 this.requestOptions_.timeout = requestTimeout; 54628 } // if this isn't a live video and preload permits, start 54629 // downloading segments 54630 54631 if (media.endList && this.tech_.preload() !== 'none') { 54632 this.mainSegmentLoader_.playlist(media, this.requestOptions_); 54633 this.mainSegmentLoader_.load(); 54634 } 54635 setupMediaGroups({ 54636 sourceType: this.sourceType_, 54637 segmentLoaders: { 54638 AUDIO: this.audioSegmentLoader_, 54639 SUBTITLES: this.subtitleSegmentLoader_, 54640 main: this.mainSegmentLoader_, 54641 }, 54642 tech: this.tech_, 54643 requestOptions: this.requestOptions_, 54644 mainPlaylistLoader: this.mainPlaylistLoader_, 54645 vhs: this.vhs_, 54646 main: this.main(), 54647 mediaTypes: this.mediaTypes_, 54648 excludePlaylist: this.excludePlaylist.bind(this), 54649 }); 54650 this.triggerPresenceUsage_(this.main(), media); 54651 this.setupFirstPlay(); 54652 if ( 54653 !this.mediaTypes_.AUDIO.activePlaylistLoader || 54654 this.mediaTypes_.AUDIO.activePlaylistLoader.media() 54655 ) { 54656 this.trigger('selectedinitialmedia'); 54657 } else { 54658 // We must wait for the active audio playlist loader to 54659 // finish setting up before triggering this event so the 54660 // representations API and EME setup is correct 54661 this.mediaTypes_.AUDIO.activePlaylistLoader.one('loadedmetadata', () => { 54662 this.trigger('selectedinitialmedia'); 54663 }); 54664 } 54665 }); 54666 this.mainPlaylistLoader_.on('loadedplaylist', () => { 54667 if (this.loadOnPlay_) { 54668 this.tech_.off('play', this.loadOnPlay_); 54669 } 54670 let updatedPlaylist = this.mainPlaylistLoader_.media(); 54671 if (!updatedPlaylist) { 54672 // Add content steering listeners on first load and init. 54673 this.attachContentSteeringListeners_(); 54674 this.initContentSteeringController_(); // exclude any variants that are not supported by the browser before selecting 54675 // an initial media as the playlist selectors do not consider browser support 54676 54677 this.excludeUnsupportedVariants_(); 54678 let selectedMedia; 54679 if (this.enableLowInitialPlaylist) { 54680 selectedMedia = this.selectInitialPlaylist(); 54681 } 54682 if (!selectedMedia) { 54683 selectedMedia = this.selectPlaylist(); 54684 } 54685 if (!selectedMedia || !this.shouldSwitchToMedia_(selectedMedia)) { 54686 return; 54687 } 54688 this.initialMedia_ = selectedMedia; 54689 this.switchMedia_(this.initialMedia_, 'initial'); // Under the standard case where a source URL is provided, loadedplaylist will 54690 // fire again since the playlist will be requested. In the case of vhs-json 54691 // (where the manifest object is provided as the source), when the media 54692 // playlist's `segments` list is already available, a media playlist won't be 54693 // requested, and loadedplaylist won't fire again, so the playlist handler must be 54694 // called on its own here. 54695 54696 const haveJsonSource = this.sourceType_ === 'vhs-json' && this.initialMedia_.segments; 54697 if (!haveJsonSource) { 54698 return; 54699 } 54700 updatedPlaylist = this.initialMedia_; 54701 } 54702 this.handleUpdatedMediaPlaylist(updatedPlaylist); 54703 }); 54704 this.mainPlaylistLoader_.on('error', () => { 54705 const error = this.mainPlaylistLoader_.error; 54706 this.excludePlaylist({ 54707 playlistToExclude: error.playlist, 54708 error, 54709 }); 54710 }); 54711 this.mainPlaylistLoader_.on('mediachanging', () => { 54712 this.mainSegmentLoader_.abort(); 54713 this.mainSegmentLoader_.pause(); 54714 }); 54715 this.mainPlaylistLoader_.on('mediachange', () => { 54716 const media = this.mainPlaylistLoader_.media(); 54717 const requestTimeout = media.targetDuration * 1.5 * 1000; // If we don't have any more available playlists, we don't want to 54718 // timeout the request. 54719 54720 if ( 54721 isLowestEnabledRendition(this.mainPlaylistLoader_.main, this.mainPlaylistLoader_.media()) 54722 ) { 54723 this.requestOptions_.timeout = 0; 54724 } else { 54725 this.requestOptions_.timeout = requestTimeout; 54726 } 54727 if (this.sourceType_ === 'dash') {
54728 // we don't want to re-request the same hls playlist right after it was changed 54729 this.mainPlaylistLoader_.load(); 54730 } // TODO: Create a new event on the PlaylistLoader that signals 54731 // that the segments have changed in some way and use that to 54732 // update the SegmentLoader instead of doing it twice here and 54733 // on `loadedplaylist` 54734 54735 this.mainSegmentLoader_.pause(); 54736 this.mainSegmentLoader_.playlist(media, this.requestOptions_); 54737 if (this.waitingForFastQualityPlaylistReceived_) { 54738 this.runFastQualitySwitch_(); 54739 } else { 54740 this.mainSegmentLoader_.load(); 54741 } 54742 this.tech_.trigger({ 54743 type: 'mediachange', 54744 bubbles: true, 54745 }); 54746 }); 54747 this.mainPlaylistLoader_.on('playlistunchanged', () => { 54748 const updatedPlaylist = this.mainPlaylistLoader_.media(); // ignore unchanged playlists that have already been 54749 // excluded for not-changing. We likely just have a really slowly updating 54750 // playlist. 54751 54752 if (updatedPlaylist.lastExcludeReason_ === 'playlist-unchanged') { 54753 return; 54754 } 54755 const playlistOutdated = this.stuckAtPlaylistEnd_(updatedPlaylist); 54756 if (playlistOutdated) { 54757 // Playlist has stopped updating and we're stuck at its end. Try to 54758 // exclude it and switch to another playlist in the hope that that 54759 // one is updating (and give the player a chance to re-adjust to the 54760 // safe live point). 54761 this.excludePlaylist({ 54762 error: { 54763 message: 'Playlist no longer updating.', 54764 reason: 'playlist-unchanged', 54765 }, 54766 }); // useful for monitoring QoS 54767 54768 this.tech_.trigger('playliststuck'); 54769 } 54770 }); 54771 this.mainPlaylistLoader_.on('renditiondisabled', () => { 54772 this.tech_.trigger({ 54773 type: 'usage', 54774 name: 'vhs-rendition-disabled', 54775 }); 54776 }); 54777 this.mainPlaylistLoader_.on('renditionenabled', () => { 54778 this.tech_.trigger({ 54779 type: 'usage', 54780 name: 'vhs-rendition-enabled', 54781 }); 54782 }); 54783 const playlistLoaderEvents = [ 54784 'manifestrequeststart', 54785 'manifestrequestcomplete', 54786 'manifestparsestart', 54787 'manifestparsecomplete', 54788 'playlistrequeststart', 54789 'playlistrequestcomplete', 54790 'playlistparsestart', 54791 'playlistparsecomplete', 54792 'renditiondisabled', 54793 'renditionenabled', 54794 ]; 54795 playlistLoaderEvents.forEach((eventName) => { 54796 this.mainPlaylistLoader_.on(eventName, (metadata) => { 54797 // trigger directly on the player to ensure early events are fired. 54798 this.player_.trigger(_extends__default['default']({}, metadata)); 54799 }); 54800 }); 54801 } 54802 /** 54803 * Given an updated media playlist (whether it was loaded for the first time, or 54804 * refreshed for live playlists), update any relevant properties and state to reflect 54805 * changes in the media that should be accounted for (e.g., cues and duration). 54806 * 54807 * @param {Object} updatedPlaylist the updated media playlist object 54808 * 54809 * @private 54810 */ 54811 54812 handleUpdatedMediaPlaylist(updatedPlaylist) { 54813 if (this.useCueTags_) { 54814 this.updateAdCues_(updatedPlaylist); 54815 } // TODO: Create a new event on the PlaylistLoader that signals 54816 // that the segments have changed in some way and use that to 54817 // update the SegmentLoader instead of doing it twice here and 54818 // on `mediachange` 54819 54820 this.mainSegmentLoader_.pause(); 54821 this.mainSegmentLoader_.playlist(updatedPlaylist, this.requestOptions_); 54822 if (this.waitingForFastQualityPlaylistReceived_) { 54823 this.runFastQualitySwitch_(); 54824 } 54825 this.updateDuration(!updatedPlaylist.endList); // If the player isn't paused, ensure that the segment loader is running, 54826 // as it is possible that it was temporarily stopped while waiting for 54827 // a playlist (e.g., in case the playlist errored and we re-requested it). 54828 54829 if (!this.tech_.paused()) { 54830 this.mainSegmentLoader_.load(); 54831 if (this.audioSegmentLoader_) { 54832 this.audioSegmentLoader_.load(); 54833 } 54834 } 54835 } 54836 /** 54837 * A helper function for triggerring presence usage events once per source 54838 * 54839 * @private 54840 */ 54841 54842 triggerPresenceUsage_(main, media) { 54843 const mediaGroups = main.mediaGroups || {}; 54844 let defaultDemuxed = true; 54845 const audioGroupKeys = Object.keys(mediaGroups.AUDIO); 54846 for (const mediaGroup in mediaGroups.AUDIO) { 54847 for (const label in mediaGroups.AUDIO[mediaGroup]) { 54848 const properties = mediaGroups.AUDIO[mediaGroup][label]; 54849 if (!properties.uri) { 54850 defaultDemuxed = false;
54851 } 54852 } 54853 } 54854 if (defaultDemuxed) { 54855 this.tech_.trigger({ 54856 type: 'usage', 54857 name: 'vhs-demuxed', 54858 }); 54859 } 54860 if (Object.keys(mediaGroups.SUBTITLES).length) { 54861 this.tech_.trigger({ 54862 type: 'usage', 54863 name: 'vhs-webvtt', 54864 }); 54865 } 54866 if (Vhs$1.Playlist.isAes(media)) { 54867 this.tech_.trigger({ 54868 type: 'usage', 54869 name: 'vhs-aes', 54870 }); 54871 } 54872 if (audioGroupKeys.length && Object.keys(mediaGroups.AUDIO[audioGroupKeys[0]]).length > 1) { 54873 this.tech_.trigger({ 54874 type: 'usage', 54875 name: 'vhs-alternate-audio', 54876 }); 54877 } 54878 if (this.useCueTags_) { 54879 this.tech_.trigger({ 54880 type: 'usage', 54881 name: 'vhs-playlist-cue-tags', 54882 }); 54883 } 54884 } 54885 shouldSwitchToMedia_(nextPlaylist) { 54886 const currentPlaylist = 54887 this.mainPlaylistLoader_.media() || this.mainPlaylistLoader_.pendingMedia_; 54888 const currentTime = this.tech_.currentTime(); 54889 const bufferLowWaterLine = this.bufferLowWaterLine(); 54890 const bufferHighWaterLine = this.bufferHighWaterLine(); 54891 const buffered = this.tech_.buffered(); 54892 return shouldSwitchToMedia({ 54893 buffered, 54894 currentTime, 54895 currentPlaylist, 54896 nextPlaylist, 54897 bufferLowWaterLine, 54898 bufferHighWaterLine, 54899 duration: this.duration(), 54900 bufferBasedABR: this.bufferBasedABR, 54901 log: this.logger_, 54902 }); 54903 } 54904 /** 54905 * Register event handlers on the segment loaders. A helper function 54906 * for construction time. 54907 * 54908 * @private 54909 */ 54910 54911 setupSegmentLoaderListeners_() { 54912 this.mainSegmentLoader_.on('bandwidthupdate', () => { 54913 // Whether or not buffer based ABR or another ABR is used, on a bandwidth change it's 54914 // useful to check to see if a rendition switch should be made. 54915 this.checkABR_('bandwidthupdate'); 54916 this.tech_.trigger('bandwidthupdate'); 54917 }); 54918 this.mainSegmentLoader_.on('timeout', () => { 54919 if (this.bufferBasedABR) { 54920 // If a rendition change is needed, then it would've be done on `bandwidthupdate`. 54921 // Here the only consideration is that for buffer based ABR there's no guarantee 54922 // of an immediate switch (since the bandwidth is averaged with a timeout 54923 // bandwidth value of 1), so force a load on the segment loader to keep it going. 54924 this.mainSegmentLoader_.load(); 54925 } 54926 }); // `progress` events are not reliable enough of a bandwidth measure to trigger buffer 54927 // based ABR. 54928 54929 if (!this.bufferBasedABR) { 54930 this.mainSegmentLoader_.on('progress', () => { 54931 this.trigger('progress'); 54932 }); 54933 } 54934 this.mainSegmentLoader_.on('error', () => { 54935 const error = this.mainSegmentLoader_.error(); 54936 this.excludePlaylist({ 54937 playlistToExclude: error.playlist, 54938 error, 54939 }); 54940 }); 54941 this.mainSegmentLoader_.on('appenderror', () => { 54942 this.error = this.mainSegmentLoader_.error_; 54943 this.trigger('error'); 54944 }); 54945 this.mainSegmentLoader_.on('syncinfoupdate', () => { 54946 this.onSyncInfoUpdate_(); 54947 }); 54948 this.mainSegmentLoader_.on('timestampoffset', () => { 54949 this.tech_.trigger({ 54950 type: 'usage', 54951 name: 'vhs-timestamp-offset', 54952 }); 54953 }); 54954 this.audioSegmentLoader_.on('syncinfoupdate', () => { 54955 this.onSyncInfoUpdate_(); 54956 }); 54957 this.audioSegmentLoader_.on('appenderror', () => { 54958 this.error = this.audioSegmentLoader_.error_; 54959 this.trigger('error'); 54960 }); 54961 this.mainSegmentLoader_.on('ended', () => { 54962 this.logger_('main segment loader ended');
54963 this.onEndOfStream(); 54964 }); // There is the possibility of the video segment and the audio segment 54965 // at a current time to be on different timelines. When this occurs, the player 54966 // forwards playback to a point where these two segment types are back on the same 54967 // timeline. This time will be just after the end of the audio segment that is on 54968 // a previous timeline. 54969 54970 this.timelineChangeController_.on('audioTimelineBehind', () => { 54971 const segmentInfo = this.audioSegmentLoader_.pendingSegment_; 54972 if (!segmentInfo || !segmentInfo.segment || !segmentInfo.segment.syncInfo) { 54973 return; 54974 } // Update the current time to just after the faulty audio segment. 54975 // This moves playback to a spot where both audio and video segments 54976 // are on the same timeline. 54977 54978 const newTime = segmentInfo.segment.syncInfo.end + 0.01; 54979 this.tech_.setCurrentTime(newTime); 54980 }); 54981 this.mainSegmentLoader_.on('earlyabort', (event) => { 54982 // never try to early abort with the new ABR algorithm 54983 if (this.bufferBasedABR) { 54984 return; 54985 } 54986 this.delegateLoaders_('all', ['abort']); 54987 this.excludePlaylist({ 54988 error: { 54989 message: 54990 "Aborted early because there isn't enough bandwidth to complete " + 54991 'the request without rebuffering.', 54992 }, 54993 playlistExclusionDuration: ABORT_EARLY_EXCLUSION_SECONDS, 54994 }); 54995 }); 54996 const updateCodecs = () => { 54997 if (!this.sourceUpdater_.hasCreatedSourceBuffers()) { 54998 return this.tryToCreateSourceBuffers_(); 54999 } 55000 const codecs = this.getCodecsOrExclude_(); // no codecs means that the playlist was excluded 55001 55002 if (!codecs) { 55003 return; 55004 } 55005 this.sourceUpdater_.addOrChangeSourceBuffers(codecs); 55006 }; 55007 this.mainSegmentLoader_.on('trackinfo', updateCodecs); 55008 this.audioSegmentLoader_.on('trackinfo', updateCodecs); 55009 this.mainSegmentLoader_.on('fmp4', () => { 55010 if (!this.triggeredFmp4Usage) { 55011 this.tech_.trigger({ 55012 type: 'usage', 55013 name: 'vhs-fmp4', 55014 }); 55015 this.triggeredFmp4Usage = true; 55016 } 55017 }); 55018 this.audioSegmentLoader_.on('fmp4', () => { 55019 if (!this.triggeredFmp4Usage) { 55020 this.tech_.trigger({ 55021 type: 'usage', 55022 name: 'vhs-fmp4', 55023 }); 55024 this.triggeredFmp4Usage = true; 55025 } 55026 }); 55027 this.audioSegmentLoader_.on('ended', () => { 55028 this.logger_('audioSegmentLoader ended');
vendor: 4,411 bytes, lines 55029-55163
55029 this.onEndOfStream(); 55030 }); 55031 const segmentLoaderEvents = [ 55032 'segmentselected', 55033 'segmentloadstart', 55034 'segmentloaded', 55035 'segmentkeyloadstart', 55036 'segmentkeyloadcomplete', 55037 'segmentdecryptionstart', 55038 'segmentdecryptioncomplete', 55039 'segmenttransmuxingstart', 55040 'segmenttransmuxingcomplete', 55041 'segmenttransmuxingtrackinfoavailable', 55042 'segmenttransmuxingtiminginfoavailable', 55043 'segmentappendstart', 55044 'appendsdone', 55045 'bandwidthupdated', 55046 'timelinechange', 55047 'codecschange', 55048 ]; 55049 segmentLoaderEvents.forEach((eventName) => { 55050 this.mainSegmentLoader_.on(eventName, (metadata) => { 55051 this.player_.trigger(_extends__default['default']({}, metadata)); 55052 }); 55053 this.audioSegmentLoader_.on(eventName, (metadata) => { 55054 this.player_.trigger(_extends__default['default']({}, metadata)); 55055 }); 55056 this.subtitleSegmentLoader_.on(eventName, (metadata) => { 55057 this.player_.trigger(_extends__default['default']({}, metadata)); 55058 }); 55059 }); 55060 } 55061 mediaSecondsLoaded_() { 55062 return Math.max( 55063 this.audioSegmentLoader_.mediaSecondsLoaded + this.mainSegmentLoader_.mediaSecondsLoaded 55064 ); 55065 } 55066 /** 55067 * Call load on our SegmentLoaders 55068 */ 55069 55070 load() { 55071 this.mainSegmentLoader_.load(); 55072 if (this.mediaTypes_.AUDIO.activePlaylistLoader) { 55073 this.audioSegmentLoader_.load(); 55074 } 55075 if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) { 55076 this.subtitleSegmentLoader_.load(); 55077 } 55078 } 55079 /** 55080 * Call pause on our SegmentLoaders 55081 */ 55082 55083 pause() { 55084 this.mainSegmentLoader_.pause(); 55085 if (this.mediaTypes_.AUDIO.activePlaylistLoader) { 55086 this.audioSegmentLoader_.pause(); 55087 } 55088 if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) { 55089 this.subtitleSegmentLoader_.pause(); 55090 } 55091 } 55092 /** 55093 * Re-tune playback quality level for the current player 55094 * conditions. This method will perform destructive actions like removing 55095 * already buffered content in order to readjust the currently active 55096 * playlist quickly. This is good for manual quality changes 55097 * 55098 * @private 55099 */ 55100 55101 fastQualityChange_(media = this.selectPlaylist()) { 55102 if (media && media === this.mainPlaylistLoader_.media()) { 55103 this.logger_('skipping fastQualityChange because new media is same as old'); 55104 return; 55105 } 55106 this.switchMedia_(media, 'fast-quality'); // we would like to avoid race condition when we call fastQuality, 55107 // reset everything and start loading segments from prev segments instead of new because new playlist is not received yet 55108 55109 this.waitingForFastQualityPlaylistReceived_ = true; 55110 } 55111 runFastQualitySwitch_() { 55112 this.waitingForFastQualityPlaylistReceived_ = false; // Delete all buffered data to allow an immediate quality switch. 55113 55114 this.mainSegmentLoader_.pause(); 55115 this.mainSegmentLoader_.resetEverything(() => { 55116 this.mainSegmentLoader_.load(); 55117 }); // don't need to reset audio as it is reset when media changes 55118 } 55119 /** 55120 * Begin playback. 55121 */ 55122 55123 play() { 55124 if (this.setupFirstPlay()) { 55125 return; 55126 } 55127 if (this.tech_.ended()) { 55128 this.tech_.setCurrentTime(0); 55129 } 55130 if (this.hasPlayed_) { 55131 this.load(); 55132 } 55133 const seekable = this.tech_.seekable(); // if the viewer has paused and we fell out of the live window, 55134 // seek forward to the live point 55135 55136 if (this.tech_.duration() === Infinity) { 55137 if (this.tech_.currentTime() < seekable.start(0)) { 55138 return this.tech_.setCurrentTime(seekable.end(seekable.length - 1)); 55139 } 55140 } 55141 } 55142 /** 55143 * Seek to the latest media position if this is a live video and the 55144 * player and video are loaded and initialized. 55145 */ 55146 55147 setupFirstPlay() { 55148 const media = this.mainPlaylistLoader_.media(); // Check that everything is ready to begin buffering for the first call to play 55149 // If 1) there is no active media 55150 // 2) the player is paused 55151 // 3) the first play has already been setup 55152 // then exit early 55153 55154 if (!media || this.tech_.paused() || this.hasPlayed_) { 55155 return false; 55156 } // when the video is a live stream and/or has a start time 55157 55158 if (!media.endList || media.start) { 55159 const seekable = this.seekable(); 55160 if (!seekable.length) { 55161 // without a seekable range, the player cannot seek to begin buffering at the 55162 // live or start point 55163 return false;
vendor: 10,145 bytes, lines 55164-55423
55164 } 55165 const seekableEnd = seekable.end(0); 55166 let startPoint = seekableEnd; 55167 if (media.start) { 55168 const offset = media.start.timeOffset; 55169 if (offset < 0) { 55170 startPoint = Math.max(seekableEnd + offset, seekable.start(0)); 55171 } else { 55172 startPoint = Math.min(seekableEnd, offset); 55173 } 55174 } // trigger firstplay to inform the source handler to ignore the next seek event 55175 55176 this.trigger('firstplay'); // seek to the live point 55177 55178 this.tech_.setCurrentTime(startPoint); 55179 } 55180 this.hasPlayed_ = true; // we can begin loading now that everything is ready 55181 55182 this.load(); 55183 return true; 55184 } 55185 /** 55186 * handle the sourceopen event on the MediaSource 55187 * 55188 * @private 55189 */ 55190 55191 handleSourceOpen_() { 55192 // Only attempt to create the source buffer if none already exist. 55193 // handleSourceOpen is also called when we are "re-opening" a source buffer 55194 // after `endOfStream` has been called (in response to a seek for instance) 55195 this.tryToCreateSourceBuffers_(); // if autoplay is enabled, begin playback. This is duplicative of 55196 // code in video.js but is required because play() must be invoked 55197 // *after* the media source has opened. 55198 55199 if (this.tech_.autoplay()) { 55200 const playPromise = this.tech_.play(); // Catch/silence error when a pause interrupts a play request 55201 // on browsers which return a promise 55202 55203 if (typeof playPromise !== 'undefined' && typeof playPromise.then === 'function') { 55204 playPromise.then(null, (e) => {}); 55205 } 55206 } 55207 this.trigger('sourceopen'); 55208 } 55209 /** 55210 * handle the sourceended event on the MediaSource 55211 * 55212 * @private 55213 */ 55214 55215 handleSourceEnded_() { 55216 if (!this.inbandTextTracks_.metadataTrack_) { 55217 return; 55218 } 55219 const cues = this.inbandTextTracks_.metadataTrack_.cues; 55220 if (!cues || !cues.length) { 55221 return; 55222 } 55223 const duration = this.duration(); 55224 cues[cues.length - 1].endTime = 55225 isNaN(duration) || Math.abs(duration) === Infinity ? Number.MAX_VALUE : duration; 55226 } 55227 /** 55228 * handle the durationchange event on the MediaSource 55229 * 55230 * @private 55231 */ 55232 55233 handleDurationChange_() { 55234 this.tech_.trigger('durationchange'); 55235 } 55236 /** 55237 * Calls endOfStream on the media source when all active stream types have called 55238 * endOfStream 55239 * 55240 * @param {string} streamType 55241 * Stream type of the segment loader that called endOfStream 55242 * @private 55243 */ 55244 55245 onEndOfStream() { 55246 let isEndOfStream = this.mainSegmentLoader_.ended_; 55247 if (this.mediaTypes_.AUDIO.activePlaylistLoader) { 55248 const mainMediaInfo = this.mainSegmentLoader_.getCurrentMediaInfo_(); // if the audio playlist loader exists, then alternate audio is active 55249 55250 if (!mainMediaInfo || mainMediaInfo.hasVideo) { 55251 // if we do not know if the main segment loader contains video yet or if we 55252 // definitively know the main segment loader contains video, then we need to wait 55253 // for both main and audio segment loaders to call endOfStream 55254 isEndOfStream = isEndOfStream && this.audioSegmentLoader_.ended_; 55255 } else { 55256 // otherwise just rely on the audio loader 55257 isEndOfStream = this.audioSegmentLoader_.ended_; 55258 } 55259 } 55260 if (!isEndOfStream) { 55261 return; 55262 } 55263 this.stopABRTimer_(); 55264 this.sourceUpdater_.endOfStream(); 55265 } 55266 /** 55267 * Check if a playlist has stopped being updated 55268 * 55269 * @param {Object} playlist the media playlist object 55270 * @return {boolean} whether the playlist has stopped being updated or not 55271 */ 55272 55273 stuckAtPlaylistEnd_(playlist) { 55274 const seekable = this.seekable(); 55275 if (!seekable.length) { 55276 // playlist doesn't have enough information to determine whether we are stuck 55277 return false; 55278 } 55279 const expired = this.syncController_.getExpiredTime(playlist, this.duration()); 55280 if (expired === null) { 55281 return false; 55282 } // does not use the safe live end to calculate playlist end, since we 55283 // don't want to say we are stuck while there is still content 55284 55285 const absolutePlaylistEnd = Vhs$1.Playlist.playlistEnd(playlist, expired); 55286 const currentTime = this.tech_.currentTime(); 55287 const buffered = this.tech_.buffered(); 55288 if (!buffered.length) { 55289 // return true if the playhead reached the absolute end of the playlist 55290 return absolutePlaylistEnd - currentTime <= SAFE_TIME_DELTA; 55291 } 55292 const bufferedEnd = buffered.end(buffered.length - 1); // return true if there is too little buffer left and buffer has reached absolute 55293 // end of playlist 55294 55295 return ( 55296 bufferedEnd - currentTime <= SAFE_TIME_DELTA && 55297 absolutePlaylistEnd - bufferedEnd <= SAFE_TIME_DELTA 55298 ); 55299 } 55300 /** 55301 * Exclude a playlist for a set amount of time, making it unavailable for selection by 55302 * the rendition selection algorithm, then force a new playlist (rendition) selection. 55303 * 55304 * @param {Object=} playlistToExclude 55305 * the playlist to exclude, defaults to the currently selected playlist 55306 * @param {Object=} error 55307 * an optional error 55308 * @param {number=} playlistExclusionDuration 55309 * an optional number of seconds to exclude the playlist 55310 */ 55311 55312 excludePlaylist({ 55313 playlistToExclude = this.mainPlaylistLoader_.media(), 55314 error = {}, 55315 playlistExclusionDuration, 55316 }) { 55317 // If the `error` was generated by the playlist loader, it will contain 55318 // the playlist we were trying to load (but failed) and that should be 55319 // excluded instead of the currently selected playlist which is likely 55320 // out-of-date in this scenario 55321 playlistToExclude = playlistToExclude || this.mainPlaylistLoader_.media(); 55322 playlistExclusionDuration = 55323 playlistExclusionDuration || 55324 error.playlistExclusionDuration || 55325 this.playlistExclusionDuration; // If there is no current playlist, then an error occurred while we were 55326 // trying to load the main OR while we were disposing of the tech 55327 55328 if (!playlistToExclude) { 55329 this.error = error; 55330 if (this.mediaSource.readyState !== 'open') { 55331 this.trigger('error'); 55332 } else { 55333 this.sourceUpdater_.endOfStream('network'); 55334 } 55335 return; 55336 } 55337 playlistToExclude.playlistErrors_++; 55338 const playlists = this.mainPlaylistLoader_.main.playlists; 55339 const enabledPlaylists = playlists.filter(isEnabled); 55340 const isFinalRendition = 55341 enabledPlaylists.length === 1 && enabledPlaylists[0] === playlistToExclude; // Don't exclude the only playlist unless it was excluded 55342 // forever 55343 55344 if (playlists.length === 1 && playlistExclusionDuration !== Infinity) { 55345 videojs.log.warn( 55346 `Problem encountered with playlist ${playlistToExclude.id}. ` + 55347 'Trying again since it is the only playlist.' 55348 ); 55349 this.tech_.trigger('retryplaylist'); // if this is a final rendition, we should delay 55350 55351 return this.mainPlaylistLoader_.load(isFinalRendition); 55352 } 55353 if (isFinalRendition) { 55354 // If we're content steering, try other pathways. 55355 if (this.main().contentSteering) { 55356 const pathway = this.pathwayAttribute_(playlistToExclude); // Ignore at least 1 steering manifest refresh. 55357 55358 const reIncludeDelay = this.contentSteeringController_.steeringManifest.ttl * 1000; 55359 this.contentSteeringController_.excludePathway(pathway); 55360 this.excludeThenChangePathway_(); 55361 setTimeout(() => { 55362 this.contentSteeringController_.addAvailablePathway(pathway); 55363 }, reIncludeDelay); 55364 return; 55365 } // Since we're on the final non-excluded playlist, and we're about to exclude 55366 // it, instead of erring the player or retrying this playlist, clear out the current 55367 // exclusion list. This allows other playlists to be attempted in case any have been 55368 // fixed. 55369 55370 let reincluded = false; 55371 playlists.forEach((playlist) => { 55372 // skip current playlist which is about to be excluded 55373 if (playlist === playlistToExclude) { 55374 return; 55375 } 55376 const excludeUntil = playlist.excludeUntil; // a playlist cannot be reincluded if it wasn't excluded to begin with. 55377 55378 if (typeof excludeUntil !== 'undefined' && excludeUntil !== Infinity) { 55379 reincluded = true; 55380 delete playlist.excludeUntil; 55381 } 55382 }); 55383 if (reincluded) { 55384 videojs.log.warn( 55385 'Removing other playlists from the exclusion list because the last ' + 55386 'rendition is about to be excluded.' 55387 ); // Technically we are retrying a playlist, in that we are simply retrying a previous 55388 // playlist. This is needed for users relying on the retryplaylist event to catch a 55389 // case where the player might be stuck and looping through "dead" playlists. 55390 55391 this.tech_.trigger('retryplaylist'); 55392 } 55393 } // Exclude this playlist 55394 55395 let excludeUntil; 55396 if (playlistToExclude.playlistErrors_ > this.maxPlaylistRetries) { 55397 excludeUntil = Infinity; 55398 } else { 55399 excludeUntil = Date.now() + playlistExclusionDuration * 1000; 55400 } 55401 playlistToExclude.excludeUntil = excludeUntil; 55402 if (error.reason) { 55403 playlistToExclude.lastExcludeReason_ = error.reason; 55404 } 55405 this.tech_.trigger('excludeplaylist'); 55406 this.tech_.trigger({ 55407 type: 'usage', 55408 name: 'vhs-rendition-excluded', 55409 }); // TODO: only load a new playlist if we're excluding the current playlist 55410 // If this function was called with a playlist that's not the current active playlist 55411 // (e.g., media().id !== playlistToExclude.id), 55412 // then a new playlist should not be selected and loaded, as there's nothing wrong with the current playlist. 55413 55414 const nextPlaylist = this.selectPlaylist(); 55415 if (!nextPlaylist) { 55416 this.error = 'Playback cannot continue. No available working or supported playlists.'; 55417 this.trigger('error'); 55418 return; 55419 } 55420 const logFn = error.internal ? this.logger_ : videojs.log.warn; 55421 const errorMessage = error.message ? ' ' + error.message : ''; 55422 logFn( 55423 `${error.internal ? 'Internal problem' : 'Problem'} encountered with playlist ${playlistToExclude.id}
55423.` + 55424 `${errorMessage} Switching to playlist ${nextPlaylist.id}.` 55425 ); // if audio group changed reset audio loaders 55426 55427 if (nextPlaylist.attributes.AUDIO !== playlistToExclude.attributes.AUDIO) { 55428 this.delegateLoaders_('audio', ['abort', 'pause']); 55429 } // if subtitle group changed reset subtitle loaders 55430 55431 if (nextPlaylist.attributes.SUBTITLES !== playlistToExclude.attributes.SUBTITLES) { 55432 this.delegateLoaders_('subtitle', ['abort', 'pause']); 55433 } 55434 this.delegateLoaders_('main', ['abort', 'pause']); 55435 const delayDuration = (nextPlaylist.targetDuration / 2) * 1000 || 5 * 1000; 55436 const shouldDelay = 55437 typeof nextPlaylist.lastRequest === 'number' && 55438 Date.now() - nextPlaylist.lastRequest <= delayDuration; // delay if it's a final rendition or if the last refresh is sooner than half targetDuration 55439 55440 return this.switchMedia_(nextPlaylist, 'exclude', isFinalRendition || shouldDelay); 55441 } 55442 /** 55443 * Pause all segment/playlist loaders 55444 */ 55445 55446 pauseLoading() { 55447 this.delegateLoaders_('all', ['abort', 'pause']); 55448 this.stopABRTimer_(); 55449 } 55450 /** 55451 * Call a set of functions in order on playlist loaders, segment loaders, 55452 * or both types of loaders. 55453 * 55454 * @param {string} filter 55455 * Filter loaders that should call fnNames using a string. Can be: 55456 * * all - run on all loaders 55457 * * audio - run on all audio loaders 55458 * * subtitle - run on all subtitle loaders 55459 * * main - run on the main loaders 55460 * 55461 * @param {Array|string} fnNames 55462 * A string or array of function names to call. 55463 */ 55464 55465 delegateLoaders_(filter, fnNames) { 55466 const loaders = []; 55467 const dontFilterPlaylist = filter === 'all'; 55468 if (dontFilterPlaylist || filter === 'main') { 55469 loaders.push(this.mainPlaylistLoader_); 55470 } 55471 const mediaTypes = []; 55472 if (dontFilterPlaylist || filter === 'audio') { 55473 mediaTypes.push('AUDIO'); 55474 } 55475 if (dontFilterPlaylist || filter === 'subtitle') { 55476 mediaTypes.push('CLOSED-CAPTIONS'); 55477 mediaTypes.push('SUBTITLES'); 55478 } 55479 mediaTypes.forEach((mediaType) => { 55480 const loader = 55481 this.mediaTypes_[mediaType] && this.mediaTypes_[mediaType].activePlaylistLoader; 55482 if (loader) { 55483 loaders.push(loader); 55484 } 55485 }); 55486 ['main', 'audio', 'subtitle'].forEach((name) => { 55487 const loader = this[`${name}SegmentLoader_`]; 55488 if (loader && (filter === name || filter === 'all')) { 55489 loaders.push(loader); 55490 } 55491 }); 55492 loaders.forEach((loader) => 55493 fnNames.forEach((fnName) => { 55494 if (typeof loader[fnName] === 'function') { 55495 loader[fnName](); 55496 } 55497 }) 55498 ); 55499 } 55500 /** 55501 * set the current time on all segment loaders 55502 * 55503 * @param {TimeRange} currentTime the current time to set 55504 * @return {TimeRange} the current time 55505 */ 55506 55507 setCurrentTime(currentTime) { 55508 const buffered = findRange(this.tech_.buffered(), currentTime); 55509 if (!(this.mainPlaylistLoader_ && this.mainPlaylistLoader_.media())) { 55510 // return immediately if the metadata is not ready yet 55511 return 0; 55512 } // it's clearly an edge-case but don't thrown an error if asked to 55513 // seek within an empty playlist 55514 55515 if (!this.mainPlaylistLoader_.media().segments) { 55516 return 0; 55517 } // if the seek location is already buffered, continue buffering as usual 55518 55519 if (buffered && buffered.length) { 55520 return currentTime; 55521 } // cancel outstanding requests so we begin buffering at the new 55522 // location 55523 55524 this.mainSegmentLoader_.pause(); 55525 this.mainSegmentLoader_.resetEverything(); 55526 if (this.mediaTypes_.AUDIO.activePlaylistLoader) { 55527 this.audioSegmentLoader_.pause(); 55528 this.audioSegmentLoader_.resetEverything(); 55529 } 55530 if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) { 55531 this.subtitleSegmentLoader_.pause(); 55532 this.subtitleSegmentLoader_.resetEverything(); 55533 } // start segment loader loading in case they are paused 55534 55535 this.load(); 55536 } 55537 /** 55538 * get the current duration 55539 * 55540 * @return {TimeRange} the duration 55541 */ 55542 55543 duration() { 55544 if (!this.mainPlaylistLoader_) { 55545 return 0; 55546 } 55547 const media = this.mainPlaylistLoader_.media(); 55548 if (!media) { 55549 // no playlists loaded yet, so can't determine a duration 55550 return 0; 55551 } // Don't rely on the media source for duration in the case of a live playlist since 55552 // setting the native MediaSource's duration to infinity ends up with consequences to 55553 // seekable behavior. See https://github.com/w3c/media-source/issues/5 for details. 55554 //
vendor: 10,334 bytes, lines 55555-55812
55555 // This is resolved in the spec by https://github.com/w3c/media-source/pull/92, 55556 // however, few browsers have support for setLiveSeekableRange() 55557 // https://developer.mozilla.org/en-US/docs/Web/API/MediaSource/setLiveSeekableRange 55558 // 55559 // Until a time when the duration of the media source can be set to infinity, and a 55560 // seekable range specified across browsers, just return Infinity. 55561 55562 if (!media.endList) { 55563 return Infinity; 55564 } // Since this is a VOD video, it is safe to rely on the media source's duration (if 55565 // available). If it's not available, fall back to a playlist-calculated estimate. 55566 55567 if (this.mediaSource) { 55568 return this.mediaSource.duration; 55569 } 55570 return Vhs$1.Playlist.duration(media); 55571 } 55572 /** 55573 * check the seekable range 55574 * 55575 * @return {TimeRange} the seekable range 55576 */ 55577 55578 seekable() { 55579 return this.seekable_; 55580 } 55581 getSeekableRange_(playlistLoader, mediaType) { 55582 const media = playlistLoader.media(); 55583 if (!media) { 55584 return null; 55585 } 55586 const mediaSequenceSync = this.syncController_.getMediaSequenceSync(mediaType); 55587 if (mediaSequenceSync && mediaSequenceSync.isReliable) { 55588 const start = mediaSequenceSync.start; 55589 const end = mediaSequenceSync.end; 55590 if (!isFinite(start) || !isFinite(end)) { 55591 return null; 55592 } 55593 const liveEdgeDelay = Vhs$1.Playlist.liveEdgeDelay(this.mainPlaylistLoader_.main, media); // Make sure our seekable end is not negative 55594 55595 const calculatedEnd = Math.max(0, end - liveEdgeDelay); 55596 if (calculatedEnd < start) { 55597 return null; 55598 } 55599 return createTimeRanges([[start, calculatedEnd]]); 55600 } 55601 const expired = this.syncController_.getExpiredTime(media, this.duration()); 55602 if (expired === null) { 55603 return null; 55604 } 55605 const seekable = Vhs$1.Playlist.seekable( 55606 media, 55607 expired, 55608 Vhs$1.Playlist.liveEdgeDelay(this.mainPlaylistLoader_.main, media) 55609 ); 55610 return seekable.length ? seekable : null; 55611 } 55612 computeFinalSeekable_(mainSeekable, audioSeekable) { 55613 if (!audioSeekable) { 55614 return mainSeekable; 55615 } 55616 const mainStart = mainSeekable.start(0); 55617 const mainEnd = mainSeekable.end(0); 55618 const audioStart = audioSeekable.start(0); 55619 const audioEnd = audioSeekable.end(0); 55620 if (audioStart > mainEnd || mainStart > audioEnd) { 55621 // Seekables are far apart, rely on main 55622 return mainSeekable; 55623 } // Return the overlapping seekable range 55624 55625 return createTimeRanges([[Math.max(mainStart, audioStart), Math.min(mainEnd, audioEnd)]]); 55626 } 55627 onSyncInfoUpdate_() { 55628 // TODO check for creation of both source buffers before updating seekable 55629 // 55630 // A fix was made to this function where a check for 55631 // this.sourceUpdater_.hasCreatedSourceBuffers 55632 // was added to ensure that both source buffers were created before seekable was 55633 // updated. However, it originally had a bug where it was checking for a true and 55634 // returning early instead of checking for false. Setting it to check for false to 55635 // return early though created other issues. A call to play() would check for seekable 55636 // end without verifying that a seekable range was present. In addition, even checking 55637 // for that didn't solve some issues, as handleFirstPlay is sometimes worked around 55638 // due to a media update calling load on the segment loaders, skipping a seek to live, 55639 // thereby starting live streams at the beginning of the stream rather than at the end. 55640 // 55641 // This conditional should be fixed to wait for the creation of two source buffers at 55642 // the same time as the other sections of code are fixed to properly seek to live and 55643 // not throw an error due to checking for a seekable end when no seekable range exists. 55644 // 55645 // For now, fall back to the older behavior, with the understanding that the seekable 55646 // range may not be completely correct, leading to a suboptimal initial live point. 55647 if (!this.mainPlaylistLoader_) { 55648 return; 55649 } 55650 const mainSeekable = this.getSeekableRange_(this.mainPlaylistLoader_, 'main'); 55651 if (!mainSeekable) { 55652 return; 55653 } 55654 let audioSeekable; 55655 if (this.mediaTypes_.AUDIO.activePlaylistLoader) { 55656 audioSeekable = this.getSeekableRange_(this.mediaTypes_.AUDIO.activePlaylistLoader, 'audio'); 55657 if (!audioSeekable) { 55658 return; 55659 } 55660 } 55661 const oldSeekable = this.seekable_; 55662 this.seekable_ = this.computeFinalSeekable_(mainSeekable, audioSeekable); 55663 if (!this.seekable_) { 55664 return; 55665 } 55666 if (oldSeekable && oldSeekable.length && this.seekable_.length) { 55667 if ( 55668 oldSeekable.start(0) === this.seekable_.start(0) && 55669 oldSeekable.end(0) === this.seekable_.end(0) 55670 ) { 55671 // Seekable range hasn't changed 55672 return; 55673 } 55674 } 55675 this.logger_(`seekable updated [${printableRange(this.seekable_)}]`); 55676 const metadata = { 55677 seekableRanges: this.seekable_, 55678 }; 55679 this.trigger({ 55680 type: 'seekablerangeschanged', 55681 metadata, 55682 }); 55683 this.tech_.trigger('seekablechanged'); 55684 } 55685 /** 55686 * Update the player duration 55687 */ 55688 55689 updateDuration(isLive) { 55690 if (this.updateDuration_) { 55691 this.mediaSource.removeEventListener('sourceopen', this.updateDuration_); 55692 this.updateDuration_ = null; 55693 } 55694 if (this.mediaSource.readyState !== 'open') { 55695 this.updateDuration_ = this.updateDuration.bind(this, isLive); 55696 this.mediaSource.addEventListener('sourceopen', this.updateDuration_); 55697 return; 55698 } 55699 if (isLive) { 55700 const seekable = this.seekable(); 55701 if (!seekable.length) { 55702 return; 55703 } // Even in the case of a live playlist, the native MediaSource's duration should not 55704 // be set to Infinity (even though this would be expected for a live playlist), since 55705 // setting the native MediaSource's duration to infinity ends up with consequences to 55706 // seekable behavior. See https://github.com/w3c/media-source/issues/5 for details. 55707 // 55708 // This is resolved in the spec by https://github.com/w3c/media-source/pull/92, 55709 // however, few browsers have support for setLiveSeekableRange() 55710 // https://developer.mozilla.org/en-US/docs/Web/API/MediaSource/setLiveSeekableRange 55711 // 55712 // Until a time when the duration of the media source can be set to infinity, and a 55713 // seekable range specified across browsers, the duration should be greater than or 55714 // equal to the last possible seekable value. 55715 // MediaSource duration starts as NaN 55716 // It is possible (and probable) that this case will never be reached for many 55717 // sources, since the MediaSource reports duration as the highest value without 55718 // accounting for timestamp offset. For example, if the timestamp offset is -100 and 55719 // we buffered times 0 to 100 with real times of 100 to 200, even though current 55720 // time will be between 0 and 100, the native media source may report the duration 55721 // as 200. However, since we report duration separate from the media source (as 55722 // Infinity), and as long as the native media source duration value is greater than 55723 // our reported seekable range, seeks will work as expected. The large number as 55724 // duration for live is actually a strategy used by some players to work around the 55725 // issue of live seekable ranges cited above. 55726 55727 if ( 55728 isNaN(this.mediaSource.duration) || 55729 this.mediaSource.duration < seekable.end(seekable.length - 1) 55730 ) { 55731 this.sourceUpdater_.setDuration(seekable.end(seekable.length - 1)); 55732 } 55733 return; 55734 } 55735 const buffered = this.tech_.buffered(); 55736 let duration = Vhs$1.Playlist.duration(this.mainPlaylistLoader_.media()); 55737 if (buffered.length > 0) { 55738 duration = Math.max(duration, buffered.end(buffered.length - 1)); 55739 } 55740 if (this.mediaSource.duration !== duration) { 55741 this.sourceUpdater_.setDuration(duration); 55742 } 55743 } 55744 /** 55745 * dispose of the PlaylistController and everything 55746 * that it controls 55747 */ 55748 55749 dispose() { 55750 this.trigger('dispose'); 55751 this.decrypter_.terminate(); 55752 this.mainPlaylistLoader_.dispose(); 55753 this.mainSegmentLoader_.dispose(); 55754 this.contentSteeringController_.dispose(); 55755 this.keyStatusMap_.clear(); 55756 if (this.loadOnPlay_) { 55757 this.tech_.off('play', this.loadOnPlay_); 55758 } 55759 ['AUDIO', 'SUBTITLES'].forEach((type) => { 55760 const groups = this.mediaTypes_[type].groups; 55761 for (const id in groups) { 55762 groups[id].forEach((group) => { 55763 if (group.playlistLoader) { 55764 group.playlistLoader.dispose(); 55765 } 55766 }); 55767 } 55768 }); 55769 this.audioSegmentLoader_.dispose(); 55770 this.subtitleSegmentLoader_.dispose(); 55771 this.sourceUpdater_.dispose(); 55772 this.timelineChangeController_.dispose(); 55773 this.stopABRTimer_(); 55774 if (this.updateDuration_) { 55775 this.mediaSource.removeEventListener('sourceopen', this.updateDuration_); 55776 } 55777 this.mediaSource.removeEventListener('durationchange', this.handleDurationChange_); // load the media source into the player 55778 55779 this.mediaSource.removeEventListener('sourceopen', this.handleSourceOpen_); 55780 this.mediaSource.removeEventListener('sourceended', this.handleSourceEnded_); 55781 this.off(); 55782 } 55783 /** 55784 * return the main playlist object if we have one 55785 * 55786 * @return {Object} the main playlist object that we parsed 55787 */ 55788 55789 main() { 55790 return this.mainPlaylistLoader_.main; 55791 } 55792 /** 55793 * return the currently selected playlist 55794 * 55795 * @return {Object} the currently selected playlist object that we parsed 55796 */ 55797 55798 media() { 55799 // playlist loader will not return media if it has not been fully loaded 55800 return this.mainPlaylistLoader_.media() || this.initialMedia_; 55801 } 55802 areMediaTypesKnown_() { 55803 const usingAudioLoader = !!this.mediaTypes_.AUDIO.activePlaylistLoader; 55804 const hasMainMediaInfo = !!this.mainSegmentLoader_.getCurrentMediaInfo_(); // if we are not using an audio loader, then we have audio media info 55805 // otherwise check on the segment loader. 55806 55807 const hasAudioMediaInfo = !usingAudioLoader 55808 ? true 55809 : !!this.audioSegmentLoader_.getCurrentMediaInfo_(); // one or both loaders has not loaded sufficently to get codecs 55810 55811 if (!hasMainMediaInfo || !hasAudioMediaInfo) { 55812 return false;
55813 } 55814 return true; 55815 } // find from and to for codec switch event 55816 55817 getCodecsOrExclude_() { 55818 const media = { 55819 main: this.mainSegmentLoader_.getCurrentMediaInfo_() || {}, 55820 audio: this.audioSegmentLoader_.getCurrentMediaInfo_() || {}, 55821 }; 55822 const playlist = this.mainSegmentLoader_.getPendingSegmentPlaylist() || this.media(); // set "main" media equal to video 55823 55824 media.video = media.main; 55825 const playlistCodecs = codecsForPlaylist(this.main(), playlist); 55826 const codecs = {}; 55827 const usingAudioLoader = !!this.mediaTypes_.AUDIO.activePlaylistLoader; 55828 if (media.main.hasVideo) { 55829 codecs.video = playlistCodecs.video || media.main.videoCodec || codecs_js.DEFAULT_VIDEO_CODEC; 55830 } 55831 if (media.main.isMuxed) { 55832 codecs.video += `,${playlistCodecs.audio || media.main.audioCodec || codecs_js.DEFAULT_AUDIO_CODEC}`; 55833 } 55834 if ((media.main.hasAudio && !media.main.isMuxed) || media.audio.hasAudio || usingAudioLoader) { 55835 codecs.audio = 55836 playlistCodecs.audio || 55837 media.main.audioCodec || 55838 media.audio.audioCodec || 55839 codecs_js.DEFAULT_AUDIO_CODEC; // set audio isFmp4 so we use the correct "supports" function below 55840 55841 media.audio.isFmp4 = 55842 media.main.hasAudio && !media.main.isMuxed ? media.main.isFmp4 : media.audio.isFmp4; 55843 } // no codecs, no playback. 55844 55845 if (!codecs.audio && !codecs.video) { 55846 this.excludePlaylist({ 55847 playlistToExclude: playlist, 55848 error: { 55849 message: 'Could not determine codecs for playlist.', 55850 }, 55851 playlistExclusionDuration: Infinity, 55852 }); 55853 return; 55854 } // fmp4 relies on browser support, while ts relies on muxer support 55855 55856 const supportFunction = (isFmp4, codec) => 55857 isFmp4 ? codecs_js.browserSupportsCodec(codec) : codecs_js.muxerSupportsCodec(codec); 55858 const unsupportedCodecs = {}; 55859 let unsupportedAudio; 55860 ['video', 'audio'].forEach(function (type) { 55861 if (codecs.hasOwnProperty(type) && !supportFunction(media[type].isFmp4, codecs[type])) { 55862 const supporter = media[type].isFmp4 ? 'browser' : 'muxer'; 55863 unsupportedCodecs[supporter] = unsupportedCodecs[supporter] || []; 55864 unsupportedCodecs[supporter].push(codecs[type]); 55865 if (type === 'audio') { 55866 unsupportedAudio = supporter; 55867 } 55868 } 55869 }); 55870 if (usingAudioLoader && unsupportedAudio && playlist.attributes.AUDIO) { 55871 const audioGroup = playlist.attributes.AUDIO;
vendor: 4,664 bytes, lines 55872-56002
55872 this.main().playlists.forEach((variant) => { 55873 const variantAudioGroup = variant.attributes && variant.attributes.AUDIO; 55874 if (variantAudioGroup === audioGroup && variant !== playlist) { 55875 variant.excludeUntil = Infinity; 55876 } 55877 }); 55878 this.logger_( 55879 `excluding audio group ${audioGroup} as ${unsupportedAudio} does not support codec(s): "${codecs.audio}"` 55880 ); 55881 } // if we have any unsupported codecs exclude this playlist. 55882 55883 if (Object.keys(unsupportedCodecs).length) { 55884 const message = 55885 Object.keys(unsupportedCodecs).reduce((acc, supporter) => { 55886 if (acc) { 55887 acc += ', '; 55888 } 55889 acc += `${supporter} does not support codec(s): "${unsupportedCodecs[supporter].join(',')}"`; 55890 return acc; 55891 }, '') + '.'; 55892 this.excludePlaylist({ 55893 playlistToExclude: playlist, 55894 error: { 55895 internal: true, 55896 message, 55897 }, 55898 playlistExclusionDuration: Infinity, 55899 }); 55900 return; 55901 } // check if codec switching is happening 55902 55903 if (this.sourceUpdater_.hasCreatedSourceBuffers() && !this.sourceUpdater_.canChangeType()) { 55904 const switchMessages = []; 55905 ['video', 'audio'].forEach((type) => { 55906 const newCodec = (codecs_js.parseCodecs(this.sourceUpdater_.codecs[type] || '')[0] || {}) 55907 .type; 55908 const oldCodec = (codecs_js.parseCodecs(codecs[type] || '')[0] || {}).type; 55909 if (newCodec && oldCodec && newCodec.toLowerCase() !== oldCodec.toLowerCase()) { 55910 switchMessages.push(`"${this.sourceUpdater_.codecs[type]}" -> "${codecs[type]}"`); 55911 } 55912 }); 55913 if (switchMessages.length) { 55914 this.excludePlaylist({ 55915 playlistToExclude: playlist, 55916 error: { 55917 message: `Codec switching not supported: ${switchMessages.join(', ')}.`, 55918 internal: true, 55919 }, 55920 playlistExclusionDuration: Infinity, 55921 }); 55922 return; 55923 } 55924 } // TODO: when using the muxer shouldn't we just return 55925 // the codecs that the muxer outputs? 55926 55927 return codecs; 55928 } 55929 /** 55930 * Create source buffers and exlude any incompatible renditions. 55931 * 55932 * @private 55933 */ 55934 55935 tryToCreateSourceBuffers_() { 55936 // media source is not ready yet or sourceBuffers are already 55937 // created. 55938 if (this.mediaSource.readyState !== 'open' || this.sourceUpdater_.hasCreatedSourceBuffers()) { 55939 return; 55940 } 55941 if (!this.areMediaTypesKnown_()) { 55942 return; 55943 } 55944 const codecs = this.getCodecsOrExclude_(); // no codecs means that the playlist was excluded 55945 55946 if (!codecs) { 55947 return; 55948 } 55949 this.sourceUpdater_.createSourceBuffers(codecs); 55950 const codecString = [codecs.video, codecs.audio].filter(Boolean).join(','); 55951 this.excludeIncompatibleVariants_(codecString); 55952 } 55953 /** 55954 * Excludes playlists with codecs that are unsupported by the muxer and browser. 55955 */ 55956 55957 excludeUnsupportedVariants_() { 55958 const playlists = this.main().playlists; 55959 const ids = []; // TODO: why don't we have a property to loop through all 55960 // playlist? Why did we ever mix indexes and keys? 55961 55962 Object.keys(playlists).forEach((key) => { 55963 const variant = playlists[key]; // check if we already processed this playlist. 55964 55965 if (ids.indexOf(variant.id) !== -1) { 55966 return; 55967 } 55968 ids.push(variant.id); 55969 const codecs = codecsForPlaylist(this.main, variant); 55970 const unsupported = []; 55971 if ( 55972 codecs.audio && 55973 !codecs_js.muxerSupportsCodec(codecs.audio) && 55974 !codecs_js.browserSupportsCodec(codecs.audio) 55975 ) { 55976 unsupported.push(`audio codec ${codecs.audio}`); 55977 } 55978 if ( 55979 codecs.video && 55980 !codecs_js.muxerSupportsCodec(codecs.video) && 55981 !codecs_js.browserSupportsCodec(codecs.video) 55982 ) { 55983 unsupported.push(`video codec ${codecs.video}`); 55984 } 55985 if (codecs.text && codecs.text === 'stpp.ttml.im1t') { 55986 unsupported.push(`text codec ${codecs.text}`); 55987 } 55988 if (unsupported.length) { 55989 variant.excludeUntil = Infinity; 55990 this.logger_(`excluding ${variant.id} for unsupported: ${unsupported.join(', ')}`); 55991 } 55992 }); 55993 } 55994 /** 55995 * Exclude playlists that are known to be codec or 55996 * stream-incompatible with the SourceBuffer configuration. For 55997 * instance, Media Source Extensions would cause the video element to 55998 * stall waiting for video data if you switched from a variant with 55999 * video and audio to an audio-only one. 56000 * 56001 * @param {Object} media a media playlist compatible with the current 56002 * set of SourceBuffers. Variants in the current main playlist that
56003 * do not appear to have compatible codec or stream configurations 56004 * will be excluded from the default playlist selection algorithm 56005 * indefinitely. 56006 * @private 56007 */ 56008 56009 excludeIncompatibleVariants_(codecString) { 56010 const ids = []; 56011 const playlists = this.main().playlists; 56012 const codecs = unwrapCodecList(codecs_js.parseCodecs(codecString)); 56013 const codecCount_ = codecCount(codecs); 56014 const videoDetails = (codecs.video && codecs_js.parseCodecs(codecs.video)[0]) || null; 56015 const audioDetails = (codecs.audio && codecs_js.parseCodecs(codecs.audio)[0]) || null; 56016 Object.keys(playlists).forEach((key) => { 56017 const variant = playlists[key]; // check if we already processed this playlist. 56018 // or it if it is already excluded forever. 56019 56020 if (ids.indexOf(variant.id) !== -1 || variant.excludeUntil === Infinity) { 56021 return; 56022 } 56023 ids.push(variant.id); 56024 const exclusionReasons = []; // get codecs from the playlist for this variant 56025 56026 const variantCodecs = codecsForPlaylist(this.mainPlaylistLoader_.main, variant); 56027 const variantCodecCount = codecCount(variantCodecs); // if no codecs are listed, we cannot determine that this 56028 // variant is incompatible. Wait for mux.js to probe 56029 56030 if (!variantCodecs.audio && !variantCodecs.video) { 56031 return; 56032 } // TODO: we can support this by removing the 56033 // old media source and creating a new one, but it will take some work. 56034 // The number of streams cannot change 56035 56036 if (variantCodecCount !== codecCount_) { 56037 exclusionReasons.push(`codec count "${variantCodecCount}" !== "${codecCount_}"`); 56038 } // only exclude playlists by codec change, if codecs cannot switch 56039 // during playback. 56040 56041 if (!this.sourceUpdater_.canChangeType()) { 56042 const variantVideoDetails = 56043 (variantCodecs.video && codecs_js.parseCodecs(variantCodecs.video)[0]) || null; 56044 const variantAudioDetails = 56045 (variantCodecs.audio && codecs_js.parseCodecs(variantCodecs.audio)[0]) || null; // the video codec cannot change 56046 56047 if ( 56048 variantVideoDetails && 56049 videoDetails && 56050 variantVideoDetails.type.toLowerCase() !== videoDetails.type.toLowerCase() 56051 ) { 56052 exclusionReasons.push( 56053 `video codec "${variantVideoDetails.type}" !== "${videoDetails.type}"` 56054 ); 56055 } // the audio codec cannot change 56056 56057 if ( 56058 variantAudioDetails && 56059 audioDetails && 56060 variantAudioDetails.type.toLowerCase() !== audioDetails.type.toLowerCase() 56061 ) { 56062 exclusionReasons.push( 56063 `audio codec "${variantAudioDetails.type}" !== "${audioDetails.type}"` 56064 ); 56065 } 56066 } 56067 if (exclusionReasons.length) { 56068 variant.excludeUntil = Infinity; 56069 this.logger_(`excluding ${variant.id}: ${exclusionReasons.join(' && ')}`); 56070 } 56071 }); 56072 } 56073 updateAdCues_(media) { 56074 let offset = 0; 56075 const seekable = this.seekable(); 56076 if (seekable.length) { 56077 offset = seekable.start(0); 56078 } 56079 updateAdCues(media, this.cueTagsTrack_, offset); 56080 } 56081 /** 56082 * Calculates the desired forward buffer length based on current time 56083 * 56084 * @return {number} Desired forward buffer length in seconds 56085 */ 56086 56087 goalBufferLength() { 56088 const currentTime = this.tech_.currentTime(); 56089 const initial = Config.GOAL_BUFFER_LENGTH; 56090 const rate = Config.GOAL_BUFFER_LENGTH_RATE; 56091 const max = Math.max(initial, Config.MAX_GOAL_BUFFER_LENGTH); 56092 return Math.min(initial + currentTime * rate, max); 56093 } 56094 /** 56095 * Calculates the desired buffer low water line based on current time 56096 * 56097 * @return {number} Desired buffer low water line in seconds 56098 */ 56099 56100 bufferLowWaterLine() { 56101 const currentTime = this.tech_.currentTime(); 56102 const initial = Config.BUFFER_LOW_WATER_LINE; 56103 const rate = Config.BUFFER_LOW_WATER_LINE_RATE; 56104 const max = Math.max(initial, Config.MAX_BUFFER_LOW_WATER_LINE); 56105 const newMax = Math.max(initial, Config.EXPERIMENTAL_MAX_BUFFER_LOW_WATER_LINE); 56106 return Math.min(initial + currentTime * rate, this.bufferBasedABR ? newMax : max); 56107 } 56108 bufferHighWaterLine() { 56109 return Config.BUFFER_HIGH_WATER_LINE; 56110 } 56111 addDateRangesToTextTrack_(dateRanges) { 56112 createMetadataTrackIfNotExists(this.inbandTextTracks_, 'com.apple.streaming', this.tech_); 56113 addDateRangeMetadata({ 56114 inbandTextTracks: this.inbandTextTracks_, 56115 dateRanges, 56116 }); 56117 } 56118 addMetadataToTextTrack(dispatchType, metadataArray, videoDuration) { 56119 const timestampOffset = this.sourceUpdater_.videoBuffer 56120 ? this.sourceUpdater_.videoTimestampOffset() 56121 : this.sourceUpdater_.audioTimestampOffset(); // There's potentially an issue where we could double add metadata if there's a muxed 56122 // audio/video source with a metadata track, and an alt audio with a metadata track. 56123 // However, this probably won't happen, and if it does it can be handled then. 56124 56125 createMetadataTrackIfNotExists(this.inbandTextTracks_, dispatchType, this.tech_); 56126 addMetadata({ 56127 inbandTextTracks: this.inbandTextTracks_, 56128 metadataArray, 56129 timestampOffset, 56130 videoDuration, 56131 }); 56132 } 56133 /** 56134 * Utility for getting the pathway or service location from an HLS or DASH playlist. 56135 * 56136 * @param {Object} playlist for getting pathway from. 56137 * @return the pathway attribute of a playlist 56138 */ 56139 56140 pathwayAttribute_(playlist) { 56141 return playlist.attributes['PATHWAY-ID'] || playlist.attributes.serviceLocation; 56142 } 56143 /** 56144 * Initialize available pathways and apply the tag properties. 56145 */ 56146
vendor: 5,109 bytes, lines 56147-56285
56147 initContentSteeringController_() { 56148 const main = this.main(); 56149 if (!main.contentSteering) { 56150 return; 56151 } 56152 for (const playlist of main.playlists) { 56153 this.contentSteeringController_.addAvailablePathway(this.pathwayAttribute_(playlist)); 56154 } 56155 this.contentSteeringController_.assignTagProperties(main.uri, main.contentSteering); // request the steering manifest immediately if queryBeforeStart is set. 56156 56157 if (this.contentSteeringController_.queryBeforeStart) { 56158 // When queryBeforeStart is true, initial request should omit steering parameters. 56159 this.contentSteeringController_.requestSteeringManifest(true); 56160 return; 56161 } // otherwise start content steering after playback starts 56162 56163 this.tech_.one('canplay', () => { 56164 this.contentSteeringController_.requestSteeringManifest(); 56165 }); 56166 } 56167 /** 56168 * Reset the content steering controller and re-init. 56169 */ 56170 56171 resetContentSteeringController_() { 56172 this.contentSteeringController_.clearAvailablePathways(); 56173 this.contentSteeringController_.dispose(); 56174 this.initContentSteeringController_(); 56175 } 56176 /** 56177 * Attaches the listeners for content steering. 56178 */ 56179 56180 attachContentSteeringListeners_() { 56181 this.contentSteeringController_.on( 56182 'content-steering', 56183 this.excludeThenChangePathway_.bind(this) 56184 ); 56185 const contentSteeringEvents = [ 56186 'contentsteeringloadstart', 56187 'contentsteeringloadcomplete', 56188 'contentsteeringparsed', 56189 ]; 56190 contentSteeringEvents.forEach((eventName) => { 56191 this.contentSteeringController_.on(eventName, (metadata) => { 56192 this.trigger(_extends__default['default']({}, metadata)); 56193 }); 56194 }); 56195 if (this.sourceType_ === 'dash') { 56196 this.mainPlaylistLoader_.on('loadedplaylist', () => { 56197 const main = this.main(); // check if steering tag or pathways changed. 56198 56199 const didDashTagChange = this.contentSteeringController_.didDASHTagChange( 56200 main.uri, 56201 main.contentSteering 56202 ); 56203 const didPathwaysChange = () => { 56204 const availablePathways = this.contentSteeringController_.getAvailablePathways(); 56205 const newPathways = []; 56206 for (const playlist of main.playlists) { 56207 const serviceLocation = playlist.attributes.serviceLocation; 56208 if (serviceLocation) { 56209 newPathways.push(serviceLocation); 56210 if (!availablePathways.has(serviceLocation)) { 56211 return true; 56212 } 56213 } 56214 } // If we have no new serviceLocations and previously had availablePathways 56215 56216 if (!newPathways.length && availablePathways.size) { 56217 return true; 56218 } 56219 return false; 56220 }; 56221 if (didDashTagChange || didPathwaysChange()) { 56222 this.resetContentSteeringController_(); 56223 } 56224 }); 56225 } 56226 } 56227 /** 56228 * Simple exclude and change playlist logic for content steering. 56229 */ 56230 56231 excludeThenChangePathway_() { 56232 const currentPathway = this.contentSteeringController_.getPathway(); 56233 if (!currentPathway) { 56234 return; 56235 } 56236 this.handlePathwayClones_(); 56237 const main = this.main(); 56238 const playlists = main.playlists; 56239 const ids = new Set(); 56240 let didEnablePlaylists = false; 56241 Object.keys(playlists).forEach((key) => { 56242 const variant = playlists[key]; 56243 const pathwayId = this.pathwayAttribute_(variant); 56244 const differentPathwayId = pathwayId && currentPathway !== pathwayId; 56245 const steeringExclusion = 56246 variant.excludeUntil === Infinity && variant.lastExcludeReason_ === 'content-steering'; 56247 if (steeringExclusion && !differentPathwayId) { 56248 delete variant.excludeUntil; 56249 delete variant.lastExcludeReason_; 56250 didEnablePlaylists = true; 56251 } 56252 const noExcludeUntil = !variant.excludeUntil && variant.excludeUntil !== Infinity; 56253 const shouldExclude = !ids.has(variant.id) && differentPathwayId && noExcludeUntil; 56254 if (!shouldExclude) { 56255 return; 56256 } 56257 ids.add(variant.id); 56258 variant.excludeUntil = Infinity; 56259 variant.lastExcludeReason_ = 'content-steering'; // TODO: kind of spammy, maybe move this. 56260 56261 this.logger_(`excluding ${variant.id} for ${variant.lastExcludeReason_}`); 56262 }); 56263 if (this.contentSteeringController_.manifestType_ === 'DASH') { 56264 Object.keys(this.mediaTypes_).forEach((key) => { 56265 const type = this.mediaTypes_[key]; 56266 if (type.activePlaylistLoader) { 56267 const currentPlaylist = type.activePlaylistLoader.media_; // Check if the current media playlist matches the current CDN 56268 56269 if (currentPlaylist && currentPlaylist.attributes.serviceLocation !== currentPathway) { 56270 didEnablePlaylists = true; 56271 } 56272 } 56273 }); 56274 } 56275 if (didEnablePlaylists) { 56276 this.changeSegmentPathway_(); 56277 } 56278 } 56279 /** 56280 * Add, update, or delete playlists and media groups for 56281 * the pathway clones for HLS Content Steering. 56282 * 56283 * See https://datatracker.ietf.org/doc/draft-pantos-hls-rfc8216bis/ 56284 * 56285 * NOTE: Pathway cloning does not currently support the `PER_VARIA
56285NT_URIS` and 56286 * `PER_RENDITION_URIS` as we do not handle `STABLE-VARIANT-ID` or 56287 * `STABLE-RENDITION-ID` values. 56288 */ 56289 56290 handlePathwayClones_() { 56291 const main = this.main(); 56292 const playlists = main.playlists; 56293 const currentPathwayClones = this.contentSteeringController_.currentPathwayClones; 56294 const nextPathwayClones = this.contentSteeringController_.nextPathwayClones; 56295 const hasClones = 56296 (currentPathwayClones && currentPathwayClones.size) || 56297 (nextPathwayClones && nextPathwayClones.size); 56298 if (!hasClones) { 56299 return; 56300 } 56301 for (const [id, clone] of currentPathwayClones.entries()) { 56302 const newClone = nextPathwayClones.get(id); // Delete the old pathway clone. 56303 56304 if (!newClone) { 56305 this.mainPlaylistLoader_.updateOrDeleteClone(clone); 56306 this.contentSteeringController_.excludePathway(id); 56307 } 56308 } 56309 for (const [id, clone] of nextPathwayClones.entries()) { 56310 const oldClone = currentPathwayClones.get(id); // Create a new pathway if it is a new pathway clone object. 56311 56312 if (!oldClone) { 56313 const playlistsToClone = playlists.filter((p) => { 56314 return p.attributes['PATHWAY-ID'] === clone['BASE-ID']; 56315 }); 56316 playlistsToClone.forEach((p) => { 56317 this.mainPlaylistLoader_.addClonePathway(clone, p); 56318 }); 56319 this.contentSteeringController_.addAvailablePathway(id); 56320 continue; 56321 } // There have not been changes to the pathway clone object, so skip. 56322 56323 if (this.equalPathwayClones_(oldClone, clone)) { 56324 continue; 56325 } // Update a preexisting cloned pathway. 56326 // True is set for the update flag. 56327 56328 this.mainPlaylistLoader_.updateOrDeleteClone(clone, true); 56329 this.contentSteeringController_.addAvailablePathway(id); 56330 } // Deep copy contents of next to current pathways. 56331 56332 this.contentSteeringController_.currentPathwayClones = new Map( 56333 JSON.parse(JSON.stringify([...nextPathwayClones])) 56334 ); 56335 } 56336 /** 56337 * Determines whether two pathway clone objects are equivalent. 56338 * 56339 * @param {Object} a The first pathway clone object. 56340 * @param {Object} b The second pathway clone object. 56341 * @return {boolean} True if the pathway clone objects are equal, false otherwise. 56342 */ 56343 56344 equalPathwayClones_(a, b) { 56345 if ( 56346 a['BASE-ID'] !== b['BASE-ID'] || 56347 a.ID !== b.ID || 56348 a['URI-REPLACEMENT'].HOST !== b['URI-REPLACEMENT'].HOST 56349 ) { 56350 return false; 56351 } 56352 const aParams = a['URI-REPLACEMENT'].PARAMS; 56353 const bParams = b['URI-REPLACEMENT'].PARAMS; // We need to iterate through both lists of params because one could be 56354 // missing a parameter that the other has. 56355 56356 for (const p in aParams) { 56357 if (aParams[p] !== bParams[p]) { 56358 return false; 56359 } 56360 } 56361 for (const p in bParams) { 56362 if (aParams[p] !== bParams[p]) { 56363 return false; 56364 } 56365 } 56366 return true; 56367 } 56368 /** 56369 * Changes the current playlists for audio, video and subtitles after a new pathway 56370 * is chosen from content steering. 56371 */ 56372 56373 changeSegmentPathway_() { 56374 const nextPlaylist = this.selectPlaylist(); 56375 this.pauseLoading(); // Switch audio and text track playlists if necessary in DASH 56376 56377 if (this.contentSteeringController_.manifestType_ === 'DASH') { 56378 this.switchMediaForDASHContentSteering_(); 56379 } 56380 this.switchMedia_(nextPlaylist, 'content-steering'); 56381 } 56382 /** 56383 * Iterates through playlists and check their keyId set and compare with the 56384 * keyStatusMap, only enable playlists that have a usable key. If the playlist 56385 * has no keyId leave it enabled by default. 56386 */ 56387 56388 excludeNonUsablePlaylistsByKeyId_() { 56389 if (!this.mainPlaylistLoader_ || !this.mainPlaylistLoader_.main) { 56390 return; 56391 } 56392 let nonUsableKeyStatusCount = 0; 56393 const NON_USABLE = 'non-usable';
vendor: 9,158 bytes, lines 56394-56635
56394 this.mainPlaylistLoader_.main.playlists.forEach((playlist) => { 56395 const keyIdSet = this.mainPlaylistLoader_.getKeyIdSet(playlist); // If the playlist doesn't have keyIDs lets not exclude it. 56396 56397 if (!keyIdSet || !keyIdSet.size) { 56398 return; 56399 } 56400 keyIdSet.forEach((key) => { 56401 const USABLE = 'usable'; 56402 const hasUsableKeyStatus = 56403 this.keyStatusMap_.has(key) && this.keyStatusMap_.get(key) === USABLE; 56404 const nonUsableExclusion = 56405 playlist.lastExcludeReason_ === NON_USABLE && playlist.excludeUntil === Infinity; 56406 if (!hasUsableKeyStatus) { 56407 // Only exclude playlists that haven't already been excluded as non-usable. 56408 if (playlist.excludeUntil !== Infinity && playlist.lastExcludeReason_ !== NON_USABLE) { 56409 playlist.excludeUntil = Infinity; 56410 playlist.lastExcludeReason_ = NON_USABLE; 56411 this.logger_( 56412 `excluding playlist ${playlist.id} because the key ID ${key} doesn't exist in the keyStatusMap or is not ${USABLE}` 56413 ); 56414 } // count all nonUsableKeyStatus 56415 56416 nonUsableKeyStatusCount++; 56417 } else if (hasUsableKeyStatus && nonUsableExclusion) { 56418 delete playlist.excludeUntil; 56419 delete playlist.lastExcludeReason_; 56420 this.logger_(`enabling playlist ${playlist.id} because key ID ${key} is ${USABLE}`); 56421 } 56422 }); 56423 }); // If for whatever reason every playlist has a non usable key status. Lets try re-including the SD renditions as a failsafe. 56424 56425 if (nonUsableKeyStatusCount >= this.mainPlaylistLoader_.main.playlists.length) { 56426 this.mainPlaylistLoader_.main.playlists.forEach((playlist) => { 56427 const isNonHD = 56428 playlist && 56429 playlist.attributes && 56430 playlist.attributes.RESOLUTION && 56431 playlist.attributes.RESOLUTION.height < 720; 56432 const excludedForNonUsableKey = 56433 playlist.excludeUntil === Infinity && playlist.lastExcludeReason_ === NON_USABLE; 56434 if (isNonHD && excludedForNonUsableKey) { 56435 // Only delete the excludeUntil so we don't try and re-exclude these playlists. 56436 delete playlist.excludeUntil; 56437 videojs.log.warn( 56438 `enabling non-HD playlist ${playlist.id} because all playlists were excluded due to ${NON_USABLE} key IDs` 56439 ); 56440 } 56441 }); 56442 } 56443 } 56444 /** 56445 * Adds a keystatus to the keystatus map, tries to convert to string if necessary. 56446 * 56447 * @param {any} keyId the keyId to add a status for 56448 * @param {string} status the status of the keyId 56449 */ 56450 56451 addKeyStatus_(keyId, status) { 56452 const isString = typeof keyId === 'string'; 56453 const keyIdHexString = isString ? keyId : bufferToHexString(keyId); 56454 const formattedKeyIdString = keyIdHexString.slice(0, 32).toLowerCase(); 56455 this.logger_( 56456 `KeyStatus '${status}' with key ID ${formattedKeyIdString} added to the keyStatusMap` 56457 ); 56458 this.keyStatusMap_.set(formattedKeyIdString, status); 56459 } 56460 /** 56461 * Utility function for adding key status to the keyStatusMap and filtering usable encrypted playlists. 56462 * 56463 * @param {any} keyId the keyId from the keystatuschange event 56464 * @param {string} status the key status string 56465 */ 56466 56467 updatePlaylistByKeyStatus(keyId, status) { 56468 this.addKeyStatus_(keyId, status); 56469 if (!this.waitingForFastQualityPlaylistReceived_) { 56470 this.excludeNonUsableThenChangePlaylist_(); 56471 } // Listen to loadedplaylist with a single listener and check for new contentProtection elements when a playlist is updated. 56472 56473 this.mainPlaylistLoader_.off( 56474 'loadedplaylist', 56475 this.excludeNonUsableThenChangePlaylist_.bind(this) 56476 ); 56477 this.mainPlaylistLoader_.on( 56478 'loadedplaylist', 56479 this.excludeNonUsableThenChangePlaylist_.bind(this) 56480 ); 56481 } 56482 excludeNonUsableThenChangePlaylist_() { 56483 this.excludeNonUsablePlaylistsByKeyId_(); 56484 this.fastQualityChange_(); 56485 } 56486} 56487 56488/** 56489 * Returns a function that acts as the Enable/disable playlist function. 56490 * 56491 * @param {PlaylistLoader} loader - The main playlist loader 56492 * @param {string} playlistID - id of the playlist 56493 * @param {Function} changePlaylistFn - A function to be called after a 56494 * playlist's enabled-state has been changed. Will NOT be called if a 56495 * playlist's enabled-state is unchanged 56496 * @param {boolean=} enable - Value to set the playlist enabled-state to 56497 * or if undefined returns the current enabled-state for the playlist 56498 * @return {Function} Function for setting/getting enabled 56499 */ 56500 56501const enableFunction = (loader, playlistID, changePlaylistFn) => (enable) => { 56502 const playlist = loader.main.playlists[playlistID]; 56503 const incompatible = isIncompatible(playlist); 56504 const currentlyEnabled = isEnabled(playlist); 56505 if (typeof enable === 'undefined') { 56506 return currentlyEnabled; 56507 } 56508 if (enable) { 56509 delete playlist.disabled; 56510 } else { 56511 playlist.disabled = true; 56512 } 56513 const metadata = { 56514 renditionInfo: { 56515 id: playlistID, 56516 bandwidth: playlist.attributes.BANDWIDTH, 56517 resolution: playlist.attributes.RESOLUTION, 56518 codecs: playlist.attributes.CODECS, 56519 }, 56520 cause: 'fast-quality', 56521 }; 56522 if (enable !== currentlyEnabled && !incompatible) { 56523 // Ensure the outside world knows about our changes 56524 changePlaylistFn(playlist); 56525 if (enable) { 56526 loader.trigger({ 56527 type: 'renditionenabled', 56528 metadata, 56529 }); 56530 } else { 56531 loader.trigger({ 56532 type: 'renditiondisabled', 56533 metadata, 56534 }); 56535 } 56536 } 56537 return enable; 56538}; 56539/** 56540 * The representation object encapsulates the publicly visible information 56541 * in a media playlist along with a setter/getter-type function (enabled) 56542 * for changing the enabled-state of a particular playlist entry 56543 * 56544 * @class Representation 56545 */ 56546 56547class Representation { 56548 constructor(vhsHandler, playlist, id) { 56549 const { playlistController_: pc } = vhsHandler; 56550 const qualityChangeFunction = pc.fastQualityChange_.bind(pc); // some playlist attributes are optional 56551 56552 if (playlist.attributes) { 56553 const resolution = playlist.attributes.RESOLUTION; 56554 this.width = resolution && resolution.width; 56555 this.height = resolution && resolution.height; 56556 this.bandwidth = playlist.attributes.BANDWIDTH; 56557 this.frameRate = playlist.attributes['FRAME-RATE']; 56558 } 56559 this.codecs = codecsForPlaylist(pc.main(), playlist); 56560 this.playlist = playlist; // The id is simply the ordinality of the media playlist 56561 // within the main playlist 56562 56563 this.id = id; // Partially-apply the enableFunction to create a playlist- 56564 // specific variant 56565 56566 this.enabled = enableFunction(vhsHandler.playlists, playlist.id, qualityChangeFunction); 56567 } 56568} 56569/** 56570 * A mixin function that adds the `representations` api to an instance 56571 * of the VhsHandler class 56572 * 56573 * @param {VhsHandler} vhsHandler - An instance of VhsHandler to add the 56574 * representation API into 56575 */ 56576 56577const renditionSelectionMixin = function (vhsHandler) { 56578 // Add a single API-specific function to the VhsHandler instance 56579 vhsHandler.representations = () => { 56580 const main = vhsHandler.playlistController_.main(); 56581 const playlists = isAudioOnly(main) 56582 ? vhsHandler.playlistController_.getAudioTrackPlaylists_() 56583 : main.playlists; 56584 if (!playlists) { 56585 return []; 56586 } 56587 return playlists 56588 .filter((media) => !isIncompatible(media)) 56589 .map((e, i) => new Representation(vhsHandler, e, e.id)); 56590 }; 56591}; 56592 56593/** 56594 * @file playback-watcher.js 56595 * 56596 * Playback starts, and now my watch begins. It shall not end until my death. I shall 56597 * take no wait, hold no uncleared timeouts, father no bad seeks. I shall wear no crowns 56598 * and win no glory. I shall live and die at my post. I am the corrector of the underflow. 56599 * I am the watcher of gaps. I am the shield that guards the realms of seekable. I pledge 56600 * my life and honor to the Playback Watch, for this Player and all the Players to come. 56601 */ 56602 56603const timerCancelEvents = ['seeking', 'seeked', 'pause', 'playing', 'error']; 56604/** 56605 * @class PlaybackWatcher 56606 */ 56607 56608class PlaybackWatcher extends videojs.EventTarget { 56609 /** 56610 * Represents an PlaybackWatcher object. 56611 * 56612 * @class 56613 * @param {Object} options an object that includes the tech and settings 56614 */ 56615 constructor(options) { 56616 super(); 56617 this.playlistController_ = options.playlistController; 56618 this.tech_ = options.tech; 56619 this.seekable = options.seekable; 56620 this.allowSeeksWithinUnsafeLiveWindow = options.allowSeeksWithinUnsafeLiveWindow; 56621 this.liveRangeSafeTimeDelta = options.liveRangeSafeTimeDelta; 56622 this.media = options.media; 56623 this.playedRanges_ = []; 56624 this.consecutiveUpdates = 0; 56625 this.lastRecordedTime = null; 56626 this.checkCurrentTimeTimeout_ = null; 56627 this.logger_ = logger('PlaybackWatcher'); 56628 this.logger_('initialize'); 56629 const playHandler = () => this.monitorCurrentTime_(); 56630 const canPlayHandler = () => this.monitorCurrentTime_(); 56631 const waitingHandler = () => this.techWaiting_(); 56632 const cancelTimerHandler = () => this.resetTimeUpdate_(); 56633 const pc = this.playlistController_; 56634 const loaderTypes = ['main', 'subtitle', 'audio']; 56635 const loaderChecks = {};
vendor: 8,958 bytes, lines 56636-56875
56636 loaderTypes.forEach((type) => { 56637 loaderChecks[type] = { 56638 reset: () => this.resetSegmentDownloads_(type), 56639 updateend: () => this.checkSegmentDownloads_(type), 56640 }; 56641 pc[`${type}SegmentLoader_`].on('appendsdone', loaderChecks[type].updateend); // If a rendition switch happens during a playback stall where the buffer 56642 // isn't changing we want to reset. We cannot assume that the new rendition 56643 // will also be stalled, until after new appends. 56644 56645 pc[`${type}SegmentLoader_`].on('playlistupdate', loaderChecks[type].reset); // Playback stalls should not be detected right after seeking. 56646 // This prevents one segment playlists (single vtt or single segment content) 56647 // from being detected as stalling. As the buffer will not change in those cases, since 56648 // the buffer is the entire video duration. 56649 56650 this.tech_.on(['seeked', 'seeking'], loaderChecks[type].reset); 56651 }); 56652 /** 56653 * We check if a seek was into a gap through the following steps: 56654 * 1. We get a seeking event and we do not get a seeked event. This means that 56655 * a seek was attempted but not completed. 56656 * 2. We run `fixesBadSeeks_` on segment loader appends. This means that we already 56657 * removed everything from our buffer and appended a segment, and should be ready 56658 * to check for gaps. 56659 */ 56660 56661 const setSeekingHandlers = (fn) => { 56662 ['main', 'audio'].forEach((type) => { 56663 pc[`${type}SegmentLoader_`][fn]('appended', this.seekingAppendCheck_); 56664 }); 56665 }; 56666 this.seekingAppendCheck_ = () => { 56667 if (this.fixesBadSeeks_()) { 56668 this.consecutiveUpdates = 0; 56669 this.lastRecordedTime = this.tech_.currentTime(); 56670 setSeekingHandlers('off'); 56671 } 56672 }; 56673 this.clearSeekingAppendCheck_ = () => setSeekingHandlers('off'); 56674 this.watchForBadSeeking_ = () => { 56675 this.clearSeekingAppendCheck_(); 56676 setSeekingHandlers('on'); 56677 }; 56678 this.tech_.on('seeked', this.clearSeekingAppendCheck_); 56679 this.tech_.on('seeking', this.watchForBadSeeking_); 56680 this.tech_.on('waiting', waitingHandler); 56681 this.tech_.on(timerCancelEvents, cancelTimerHandler); 56682 this.tech_.on('canplay', canPlayHandler); 56683 /* 56684 An edge case exists that results in gaps not being skipped when they exist at the beginning of a stream. This case 56685 is surfaced in one of two ways: 56686 1) The `waiting` event is fired before the player has buffered content, making it impossible 56687 to find or skip the gap. The `waiting` event is followed by a `play` event. On first play 56688 we can check if playback is stalled due to a gap, and skip the gap if necessary. 56689 2) A source with a gap at the beginning of the stream is loaded programatically while the player 56690 is in a playing state. To catch this case, it's important that our one-time play listener is setup 56691 even if the player is in a playing state 56692 */ 56693 56694 this.tech_.one('play', playHandler); // Define the dispose function to clean up our events 56695 56696 this.dispose = () => { 56697 this.clearSeekingAppendCheck_(); 56698 this.logger_('dispose'); 56699 this.tech_.off('waiting', waitingHandler); 56700 this.tech_.off(timerCancelEvents, cancelTimerHandler); 56701 this.tech_.off('canplay', canPlayHandler); 56702 this.tech_.off('play', playHandler); 56703 this.tech_.off('seeking', this.watchForBadSeeking_); 56704 this.tech_.off('seeked', this.clearSeekingAppendCheck_); 56705 loaderTypes.forEach((type) => { 56706 pc[`${type}SegmentLoader_`].off('appendsdone', loaderChecks[type].updateend); 56707 pc[`${type}SegmentLoader_`].off('playlistupdate', loaderChecks[type].reset); 56708 this.tech_.off(['seeked', 'seeking'], loaderChecks[type].reset); 56709 }); 56710 if (this.checkCurrentTimeTimeout_) { 56711 window__default['default'].clearTimeout(this.checkCurrentTimeTimeout_); 56712 } 56713 this.resetTimeUpdate_(); 56714 }; 56715 } 56716 /** 56717 * Periodically check current time to see if playback stopped 56718 * 56719 * @private 56720 */ 56721 56722 monitorCurrentTime_() { 56723 this.checkCurrentTime_(); 56724 if (this.checkCurrentTimeTimeout_) { 56725 window__default['default'].clearTimeout(this.checkCurrentTimeTimeout_); 56726 } // 42 = 24 fps // 250 is what Webkit uses // FF uses 15 56727 56728 this.checkCurrentTimeTimeout_ = window__default['default'].setTimeout( 56729 this.monitorCurrentTime_.bind(this), 56730 250 56731 ); 56732 } 56733 /** 56734 * Reset stalled download stats for a specific type of loader 56735 * 56736 * @param {string} type 56737 * The segment loader type to check. 56738 * 56739 * @listens SegmentLoader#playlistupdate 56740 * @listens Tech#seeking 56741 * @listens Tech#seeked 56742 */ 56743 56744 resetSegmentDownloads_(type) { 56745 const loader = this.playlistController_[`${type}SegmentLoader_`]; 56746 if (this[`${type}StalledDownloads_`] > 0) { 56747 this.logger_(`resetting possible stalled download count for ${type} loader`); 56748 } 56749 this[`${type}StalledDownloads_`] = 0; 56750 this[`${type}Buffered_`] = loader.buffered_(); 56751 } 56752 /** 56753 * Checks on every segment `appendsdone` to see 56754 * if segment appends are making progress. If they are not 56755 * and we are still downloading bytes. We exclude the playlist. 56756 * 56757 * @param {string} type 56758 * The segment loader type to check. 56759 * 56760 * @listens SegmentLoader#appendsdone 56761 */ 56762 56763 checkSegmentDownloads_(type) { 56764 const pc = this.playlistController_; 56765 const loader = pc[`${type}SegmentLoader_`]; 56766 const buffered = loader.buffered_(); 56767 const isBufferedDifferent = isRangeDifferent(this[`${type}Buffered_`], buffered); 56768 this[`${type}Buffered_`] = buffered; // if another watcher is going to fix the issue or 56769 // the buffered value for this loader changed 56770 // appends are working 56771 56772 if (isBufferedDifferent) { 56773 const metadata = { 56774 bufferedRanges: buffered, 56775 }; 56776 pc.trigger({ 56777 type: 'bufferedrangeschanged', 56778 metadata, 56779 }); 56780 this.resetSegmentDownloads_(type); 56781 return; 56782 } 56783 this[`${type}StalledDownloads_`]++; 56784 this.logger_( 56785 `found #${this[`${type}StalledDownloads_`]} ${type} appends that did not increase buffer (possible stalled download)`, 56786 { 56787 playlistId: loader.playlist_ && loader.playlist_.id, 56788 buffered: timeRangesToArray(buffered), 56789 } 56790 ); // after 10 possibly stalled appends with no reset, exclude 56791 56792 if (this[`${type}StalledDownloads_`] < 10) { 56793 return; 56794 } 56795 this.logger_(`${type} loader stalled download exclusion`); 56796 this.resetSegmentDownloads_(type); 56797 this.tech_.trigger({ 56798 type: 'usage', 56799 name: `vhs-${type}-download-exclusion`, 56800 }); 56801 if (type === 'subtitle') { 56802 return; 56803 } // TODO: should we exclude audio tracks rather than main tracks 56804 // when type is audio? 56805 56806 pc.excludePlaylist({ 56807 error: { 56808 message: `Excessive ${type} segment downloading detected.`, 56809 }, 56810 playlistExclusionDuration: Infinity, 56811 }); 56812 } 56813 /** 56814 * The purpose of this function is to emulate the "waiting" event on 56815 * browsers that do not emit it when they are waiting for more 56816 * data to continue playback 56817 * 56818 * @private 56819 */ 56820 56821 checkCurrentTime_() { 56822 if (this.tech_.paused() || this.tech_.seeking()) { 56823 return; 56824 } 56825 const currentTime = this.tech_.currentTime(); 56826 const buffered = this.tech_.buffered(); 56827 if ( 56828 this.lastRecordedTime === currentTime && 56829 (!buffered.length || currentTime + SAFE_TIME_DELTA >= buffered.end(buffered.length - 1)) 56830 ) { 56831 // If current time is at the end of the final buffered region, then any playback 56832 // stall is most likely caused by buffering in a low bandwidth environment. The tech 56833 // should fire a `waiting` event in this scenario, but due to browser and tech 56834 // inconsistencies. Calling `techWaiting_` here allows us to simulate 56835 // responding to a native `waiting` event when the tech fails to emit one. 56836 return this.techWaiting_(); 56837 } 56838 if (this.consecutiveUpdates >= 5 && currentTime === this.lastRecordedTime) { 56839 this.consecutiveUpdates++; 56840 this.waiting_(); 56841 } else if (currentTime === this.lastRecordedTime) { 56842 this.consecutiveUpdates++; 56843 } else { 56844 this.playedRanges_.push(createTimeRanges([this.lastRecordedTime, currentTime])); 56845 const metadata = { 56846 playedRanges: this.playedRanges_, 56847 }; 56848 this.playlistController_.trigger({ 56849 type: 'playedrangeschanged', 56850 metadata, 56851 }); 56852 this.consecutiveUpdates = 0; 56853 this.lastRecordedTime = currentTime; 56854 } 56855 } 56856 /** 56857 * Resets the 'timeupdate' mechanism designed to detect that we are stalled 56858 * 56859 * @private 56860 */ 56861 56862 resetTimeUpdate_() { 56863 this.consecutiveUpdates = 0; 56864 } 56865 /** 56866 * Fixes situations where there's a bad seek 56867 * 56868 * @return {boolean} whether an action was taken to fix the seek 56869 * @private 56870 */ 56871 56872 fixesBadSeeks_() { 56873 const seeking = this.tech_.seeking(); 56874 if (!seeking) { 56875 return false;
vendor: 7,242 bytes, lines 56876-57052
56876 } // TODO: It's possible that these seekable checks should be moved out of this function 56877 // and into a function that runs on seekablechange. It's also possible that we only need 56878 // afterSeekableWindow as the buffered check at the bottom is good enough to handle before 56879 // seekable range. 56880 56881 const seekable = this.seekable(); 56882 const currentTime = this.tech_.currentTime(); 56883 const isAfterSeekableRange = this.afterSeekableWindow_( 56884 seekable, 56885 currentTime, 56886 this.media(), 56887 this.allowSeeksWithinUnsafeLiveWindow 56888 ); 56889 let seekTo; 56890 if (isAfterSeekableRange) { 56891 const seekableEnd = seekable.end(seekable.length - 1); // sync to live point (if VOD, our seekable was updated and we're simply adjusting) 56892 56893 seekTo = seekableEnd; 56894 } 56895 if (this.beforeSeekableWindow_(seekable, currentTime)) { 56896 const seekableStart = seekable.start(0); // sync to the beginning of the live window 56897 // provide a buffer of .1 seconds to handle rounding/imprecise numbers 56898 56899 seekTo = 56900 seekableStart + 56901 // if the playlist is too short and the seekable range is an exact time (can 56902 // happen in live with a 3 segment playlist), then don't use a time delta 56903 (seekableStart === seekable.end(0) ? 0 : SAFE_TIME_DELTA); 56904 } 56905 if (typeof seekTo !== 'undefined') { 56906 this.logger_( 56907 `Trying to seek outside of seekable at time ${currentTime} with ` + 56908 `seekable range ${printableRange(seekable)}. Seeking to ` + 56909 `${seekTo}.` 56910 ); 56911 this.tech_.setCurrentTime(seekTo); 56912 return true; 56913 } 56914 const sourceUpdater = this.playlistController_.sourceUpdater_; 56915 const buffered = this.tech_.buffered(); 56916 const audioBuffered = sourceUpdater.audioBuffer ? sourceUpdater.audioBuffered() : null; 56917 const videoBuffered = sourceUpdater.videoBuffer ? sourceUpdater.videoBuffered() : null; 56918 const media = this.media(); // verify that at least two segment durations or one part duration have been 56919 // appended before checking for a gap. 56920 56921 const minAppendedDuration = media.partTargetDuration 56922 ? media.partTargetDuration 56923 : (media.targetDuration - TIME_FUDGE_FACTOR) * 2; // verify that at least two segment durations have been 56924 // appended before checking for a gap. 56925 56926 const bufferedToCheck = [audioBuffered, videoBuffered]; 56927 for (let i = 0; i < bufferedToCheck.length; i++) { 56928 // skip null buffered 56929 if (!bufferedToCheck[i]) { 56930 continue; 56931 } 56932 const timeAhead = timeAheadOf(bufferedToCheck[i], currentTime); // if we are less than two video/audio segment durations or one part 56933 // duration behind we haven't appended enough to call this a bad seek. 56934 56935 if (timeAhead < minAppendedDuration) { 56936 return false; 56937 } 56938 } 56939 const nextRange = findNextRange(buffered, currentTime); // we have appended enough content, but we don't have anything buffered 56940 // to seek over the gap 56941 56942 if (nextRange.length === 0) { 56943 return false; 56944 } 56945 seekTo = nextRange.start(0) + SAFE_TIME_DELTA; 56946 this.logger_( 56947 `Buffered region starts (${nextRange.start(0)}) ` + 56948 ` just beyond seek point (${currentTime}). Seeking to ${seekTo}.` 56949 ); 56950 this.tech_.setCurrentTime(seekTo); 56951 return true; 56952 } 56953 /** 56954 * Handler for situations when we determine the player is waiting. 56955 * 56956 * @private 56957 */ 56958 56959 waiting_() { 56960 if (this.techWaiting_()) { 56961 return; 56962 } // All tech waiting checks failed. Use last resort correction 56963 56964 const currentTime = this.tech_.currentTime(); 56965 const buffered = this.tech_.buffered(); 56966 const currentRange = findRange(buffered, currentTime); // Sometimes the player can stall for unknown reasons within a contiguous buffered 56967 // region with no indication that anything is amiss (seen in Firefox). Seeking to 56968 // currentTime is usually enough to kickstart the player. This checks that the player 56969 // is currently within a buffered region before attempting a corrective seek. 56970 // Chrome does not appear to continue `timeupdate` events after a `waiting` event 56971 // until there is ~ 3 seconds of forward buffer available. PlaybackWatcher should also 56972 // make sure there is ~3 seconds of forward buffer before taking any corrective action 56973 // to avoid triggering an `unknownwaiting` event when the network is slow. 56974 56975 if (currentRange.length && currentTime + 3 <= currentRange.end(0)) { 56976 this.resetTimeUpdate_(); 56977 this.tech_.setCurrentTime(currentTime); 56978 this.logger_( 56979 `Stopped at ${currentTime} while inside a buffered region ` + 56980 `[${currentRange.start(0)} -> ${currentRange.end(0)}]. Attempting to resume ` + 56981 'playback by seeking to the current time.' 56982 ); // unknown waiting corrections may be useful for monitoring QoS 56983 56984 this.tech_.trigger({ 56985 type: 'usage', 56986 name: 'vhs-unknown-waiting', 56987 }); 56988 return; 56989 } 56990 } 56991 /** 56992 * Handler for situations when the tech fires a `waiting` event 56993 * 56994 * @return {boolean} 56995 * True if an action (or none) was needed to correct the waiting. False if no 56996 * checks passed 56997 * @private 56998 */ 56999 57000 techWaiting_() { 57001 const seekable = this.seekable(); 57002 const currentTime = this.tech_.currentTime(); 57003 if (this.tech_.seeking()) { 57004 // Tech is seeking or already waiting on another action, no action needed 57005 return true; 57006 } 57007 if (this.beforeSeekableWindow_(seekable, currentTime)) { 57008 const livePoint = seekable.end(seekable.length - 1); 57009 this.logger_( 57010 `Fell out of live window at time ${currentTime}. Seeking to ` + 57011 `live point (seekable end) ${livePoint}` 57012 ); 57013 this.resetTimeUpdate_(); 57014 this.tech_.setCurrentTime(livePoint); // live window resyncs may be useful for monitoring QoS 57015 57016 this.tech_.trigger({ 57017 type: 'usage', 57018 name: 'vhs-live-resync', 57019 }); 57020 return true; 57021 } 57022 const sourceUpdater = this.tech_.vhs.playlistController_.sourceUpdater_; 57023 const buffered = this.tech_.buffered(); 57024 const videoUnderflow = this.videoUnderflow_({ 57025 audioBuffered: sourceUpdater.audioBuffered(), 57026 videoBuffered: sourceUpdater.videoBuffered(), 57027 currentTime, 57028 }); 57029 if (videoUnderflow) { 57030 // Even though the video underflowed and was stuck in a gap, the audio overplayed 57031 // the gap, leading currentTime into a buffered range. Seeking to currentTime 57032 // allows the video to catch up to the audio position without losing any audio 57033 // (only suffering ~3 seconds of frozen video and a pause in audio playback). 57034 this.resetTimeUpdate_(); 57035 this.tech_.setCurrentTime(currentTime); // video underflow may be useful for monitoring QoS 57036 57037 this.tech_.trigger({ 57038 type: 'usage', 57039 name: 'vhs-video-underflow', 57040 }); 57041 return true; 57042 } 57043 const nextRange = findNextRange(buffered, currentTime); // check for gap 57044 57045 if (nextRange.length > 0) { 57046 this.logger_(`Stopped at ${currentTime} and seeking to ${nextRange.start(0)}`); 57047 this.resetTimeUpdate_(); 57048 this.skipTheGap_(currentTime); 57049 return true; 57050 } // All checks failed. Returning false to indicate failure to correct waiting 57051 57052 return false;
57053 } 57054 afterSeekableWindow_(seekable, currentTime, playlist, allowSeeksWithinUnsafeLiveWindow = false) { 57055 if (!seekable.length) { 57056 // we can't make a solid case if there's no seekable, default to false 57057 return false; 57058 } 57059 let allowedEnd = seekable.end(seekable.length - 1) + SAFE_TIME_DELTA; 57060 const isLive = !playlist.endList; 57061 const isLLHLS = typeof playlist.partTargetDuration === 'number'; 57062 if (isLive && (isLLHLS || allowSeeksWithinUnsafeLiveWindow)) { 57063 allowedEnd = seekable.end(seekable.length - 1) + playlist.targetDuration * 3; 57064 } 57065 if (currentTime > allowedEnd) { 57066 return true; 57067 } 57068 return false; 57069 } 57070 beforeSeekableWindow_(seekable, currentTime) { 57071 if ( 57072 seekable.length && 57073 // can't fall before 0 and 0 seekable start identifies VOD stream 57074 seekable.start(0) > 0 && 57075 currentTime < seekable.start(0) - this.liveRangeSafeTimeDelta 57076 ) { 57077 return true; 57078 } 57079 return false; 57080 } 57081 videoUnderflow_({ videoBuffered, audioBuffered, currentTime }) { 57082 // audio only content will not have video underflow :) 57083 if (!videoBuffered) { 57084 return; 57085 } 57086 let gap; // find a gap in demuxed content. 57087 57088 if (videoBuffered.length && audioBuffered.length) { 57089 // in Chrome audio will continue to play for ~3s when we run out of video 57090 // so we have to check that the video buffer did have some buffer in the 57091 // past. 57092 const lastVideoRange = findRange(videoBuffered, currentTime - 3); 57093 const videoRange = findRange(videoBuffered, currentTime); 57094 const audioRange = findRange(audioBuffered, currentTime); 57095 if (audioRange.length && !videoRange.length && lastVideoRange.length) { 57096 gap = { 57097 start: lastVideoRange.end(0), 57098 end: audioRange.end(0), 57099 }; 57100 } // find a gap in muxed content. 57101 } else { 57102 const nextRange = findNextRange(videoBuffered, currentTime); // Even if there is no available next range, there is still a possibility we are 57103 // stuck in a gap due to video underflow. 57104 57105 if (!nextRange.length) { 57106 gap = this.gapFromVideoUnderflow_(videoBuffered, currentTime); 57107 } 57108 } 57109 if (gap) { 57110 this.logger_( 57111 `Encountered a gap in video from ${gap.start} to ${gap.end}. ` + 57112 `Seeking to current time ${currentTime}` 57113 ); 57114 return true; 57115 } 57116 return false; 57117 } 57118 /** 57119 * Timer callback. If playback still has not proceeded, then we seek 57120 * to the start of the next buffered region. 57121 * 57122 * @private 57123 */ 57124 57125 skipTheGap_(scheduledCurrentTime) { 57126 const buffered = this.tech_.buffered(); 57127 const currentTime = this.tech_.currentTime(); 57128 const nextRange = findNextRange(buffered, currentTime); 57129 this.resetTimeUpdate_(); 57130 if (nextRange.length === 0 || currentTime !== scheduledCurrentTime) { 57131 return; 57132 } 57133 this.logger_( 57134 'skipTheGap_:', 57135 'currentTime:', 57136 currentTime, 57137 'scheduled currentTime:', 57138 scheduledCurrentTime, 57139 'nextRange start:', 57140 nextRange.start(0) 57141 ); // only seek if we still have not played 57142 57143 this.tech_.setCurrentTime(nextRange.start(0) + TIME_FUDGE_FACTOR); 57144 const metadata = { 57145 gapInfo: { 57146 from: currentTime, 57147 to: nextRange.start(0), 57148 }, 57149 }; 57150 this.playlistController_.trigger({ 57151 type: 'gapjumped', 57152 metadata, 57153 }); 57154 this.tech_.trigger({ 57155 type: 'usage', 57156 name: 'vhs-gap-skip', 57157 }); 57158 } 57159 gapFromVideoUnderflow_(buffered, currentTime) { 57160 // At least in Chrome, if there is a gap in the video buffer, the audio will continue 57161 // playing for ~3 seconds after the video gap starts. This is done to account for 57162 // video buffer underflow/underrun (note that this is not done when there is audio 57163 // buffer underflow/underrun -- in that case the video will stop as soon as it 57164 // encounters the gap, as audio stalls are more noticeable/jarring to a user than 57165 // video stalls). The player's time will reflect the playthrough of audio, so the 57166 // time will appear as if we are in a buffered region, even if we are stuck in a 57167 // "gap." 57168 // 57169 // Example: 57170 // video buffer: 0 => 10.1, 10.2 => 20 57171 // audio buffer: 0 => 20 57172 // overall buffer: 0 => 10.1, 10.2 => 20 57173 // current time: 13 57174 // 57175 // Chrome's video froze at 10 seconds, where the video buffer encountered the gap, 57176 // however, the audio continued playing until it reached ~3 seconds past the gap 57177 // (13 seconds), at which point it stops as well. Since current time is past the 57178 // gap, findNextRange will return no ranges. 57179 // 57180 // To check for this issue, we see if there is a gap that starts somewhere within 57181 // a 3 second range (3 seconds +/- 1 second) back from our current time. 57182 const gaps = findGaps(buffered); 57183 for (let i = 0; i < gaps.length; i++) { 57184 const start = gaps.start(i); 57185 const end = gaps.end(i); // gap is starts no more than 4 seconds back 57186 57187 if (currentTime - start < 4 && currentTime - start > 2) { 57188 return { 57189 start, 57190 end, 57191 }; 57192 } 57193 } 57194 return null; 57195 } 57196} 57197const defaultOptions = { 57198 errorInterval: 30, 57199 getSource(next) { 57200 const tech = this.tech({ 57201 IWillNotUseThisInPlugins: true, 57202 }); 57203 const sourceObj = tech.currentSource_ || this.currentSource(); 57204 return next(sourceObj); 57205 }, 57206}; 57207/** 57208 * Main entry point for the plugin 57209 * 57210 * @param {Player} player a reference to a videojs Player instance 57211 * @param {Object} [options] an object with plugin options 57212 * @private 57213 */ 57214 57215const initPlugin = function (player, options) { 57216 let lastCalled = 0; 57217 let seekTo = 0; 57218 const localOptions = merge(defaultOptions, options); 57219 player.ready(() => { 57220 player.trigger({ 57221 type: 'usage', 57222 name: 'vhs-error-reload-initialized', 57223 }); 57224 }); 57225 /** 57226 * Player modifications to perform that must wait until `loadedmetadata` 57227 * has been triggered 57228 * 57229 * @private 57230 */ 57231 57232 const loadedMetadataHandler = function () { 57233 if (seekTo) { 57234 player.currentTime(seekTo); 57235 } 57236 }; 57237 /** 57238 * Set the source on the player element, play, and seek if necessary 57239 * 57240 * @param {Object} sourceObj An object specifying the source url and mime-type to play 57241 * @private 57242 */ 57243 57244 const setSource = function (sourceObj) { 57245 if (sourceObj === null || sourceObj === undefined) { 57246 return; 57247 } 57248 seekTo = (player.duration() !== Infinity && player.currentTime()) || 0; 57249 player.one('loadedmetadata', loadedMetadataHandler); 57250 player.src(sourceObj); 57251 player.trigger({ 57252 type: 'usage', 57253 name: 'vhs-error-reload', 57254 }); 57255 player.play(); 57256 }; 57257 /** 57258 * Attempt to get a source from either the built-in getSource function 57259 * or a custom function provided via the options 57260 * 57261 * @private 57262 */ 57263 57264 const errorHandler = function () {
vendor: 4,135 bytes, lines 57265-57395
57265 // Do not attempt to reload the source if a source-reload occurred before 57266 // 'errorInterval' time has elapsed since the last source-reload 57267 if (Date.now() - lastCalled < localOptions.errorInterval * 1000) { 57268 player.trigger({ 57269 type: 'usage', 57270 name: 'vhs-error-reload-canceled', 57271 }); 57272 return; 57273 } 57274 if (!localOptions.getSource || typeof localOptions.getSource !== 'function') { 57275 videojs.log.error('ERROR: reloadSourceOnError - The option getSource must be a function!'); 57276 return; 57277 } 57278 lastCalled = Date.now(); 57279 return localOptions.getSource.call(player, setSource); 57280 }; 57281 /** 57282 * Unbind any event handlers that were bound by the plugin 57283 * 57284 * @private 57285 */ 57286 57287 const cleanupEvents = function () { 57288 player.off('loadedmetadata', loadedMetadataHandler); 57289 player.off('error', errorHandler); 57290 player.off('dispose', cleanupEvents); 57291 }; 57292 /** 57293 * Cleanup before re-initializing the plugin 57294 * 57295 * @param {Object} [newOptions] an object with plugin options 57296 * @private 57297 */ 57298 57299 const reinitPlugin = function (newOptions) { 57300 cleanupEvents(); 57301 initPlugin(player, newOptions); 57302 }; 57303 player.on('error', errorHandler); 57304 player.on('dispose', cleanupEvents); // Overwrite the plugin function so that we can correctly cleanup before 57305 // initializing the plugin 57306 57307 player.reloadSourceOnError = reinitPlugin; 57308}; 57309/** 57310 * Reload the source when an error is detected as long as there 57311 * wasn't an error previously within the last 30 seconds 57312 * 57313 * @param {Object} [options] an object with plugin options 57314 */ 57315 57316const reloadSourceOnError = function (options) { 57317 initPlugin(this, options); 57318}; 57319var version$4 = '3.16.0'; 57320var version$3 = '7.1.0'; 57321var version$2 = '1.3.1'; 57322var version$1 = '7.2.0'; 57323var version = '4.0.2'; 57324const Vhs = { 57325 PlaylistLoader, 57326 Playlist, 57327 utils, 57328 STANDARD_PLAYLIST_SELECTOR: lastBandwidthSelector, 57329 INITIAL_PLAYLIST_SELECTOR: lowestBitrateCompatibleVariantSelector, 57330 lastBandwidthSelector, 57331 movingAverageBandwidthSelector, 57332 comparePlaylistBandwidth, 57333 comparePlaylistResolution, 57334 xhr: xhrFactory(), 57335}; // Define getter/setters for config properties 57336 57337Object.keys(Config).forEach((prop) => { 57338 Object.defineProperty(Vhs, prop, { 57339 get() { 57340 videojs.log.warn(`using Vhs.${prop} is UNSAFE be sure you know what you are doing`); 57341 return Config[prop]; 57342 }, 57343 set(value) { 57344 videojs.log.warn(`using Vhs.${prop} is UNSAFE be sure you know what you are doing`); 57345 if (typeof value !== 'number' || value < 0) { 57346 videojs.log.warn(`value of Vhs.${prop} must be greater than or equal to 0`); 57347 return; 57348 } 57349 Config[prop] = value; 57350 }, 57351 }); 57352}); 57353const LOCAL_STORAGE_KEY = 'videojs-vhs'; 57354/** 57355 * Updates the selectedIndex of the QualityLevelList when a mediachange happens in vhs. 57356 * 57357 * @param {QualityLevelList} qualityLevels The QualityLevelList to update. 57358 * @param {PlaylistLoader} playlistLoader PlaylistLoader containing the new media info. 57359 * @function handleVhsMediaChange 57360 */ 57361 57362const handleVhsMediaChange = function (qualityLevels, playlistLoader) { 57363 const newPlaylist = playlistLoader.media(); 57364 let selectedIndex = -1; 57365 for (let i = 0; i < qualityLevels.length; i++) { 57366 if (qualityLevels[i].id === newPlaylist.id) { 57367 selectedIndex = i; 57368 break; 57369 } 57370 } 57371 qualityLevels.selectedIndex_ = selectedIndex; 57372 qualityLevels.trigger({ 57373 selectedIndex, 57374 type: 'change', 57375 }); 57376}; 57377/** 57378 * Adds quality levels to list once playlist metadata is available 57379 * 57380 * @param {QualityLevelList} qualityLevels The QualityLevelList to attach events to. 57381 * @param {Object} vhs Vhs object to listen to for media events. 57382 * @function handleVhsLoadedMetadata 57383 */ 57384 57385const handleVhsLoadedMetadata = function (qualityLevels, vhs) { 57386 vhs.representations().forEach((rep) => { 57387 qualityLevels.addQualityLevel(rep); 57388 }); 57389 handleVhsMediaChange(qualityLevels, vhs.playlists); 57390}; // VHS is a source handler, not a tech. Make sure attempts to use it 57391// as one do not cause exceptions. 57392 57393Vhs.canPlaySource = function () { 57394 return videojs.log.warn( 57395 'VHS is no longer a tech. Please remove it from ' + "your player'
vendor: 22,936 bytes, lines 57395-58048
57395s techOrder." 57396 ); 57397}; 57398const emeKeySystems = (keySystemOptions, mainPlaylist, audioPlaylist) => { 57399 if (!keySystemOptions) { 57400 return keySystemOptions; 57401 } 57402 let codecs = {}; 57403 if (mainPlaylist && mainPlaylist.attributes && mainPlaylist.attributes.CODECS) { 57404 codecs = unwrapCodecList(codecs_js.parseCodecs(mainPlaylist.attributes.CODECS)); 57405 } 57406 if (audioPlaylist && audioPlaylist.attributes && audioPlaylist.attributes.CODECS) { 57407 codecs.audio = audioPlaylist.attributes.CODECS; 57408 } 57409 const videoContentType = codecs_js.getMimeForCodec(codecs.video); 57410 const audioContentType = codecs_js.getMimeForCodec(codecs.audio); // upsert the content types based on the selected playlist 57411 57412 const keySystemContentTypes = {}; 57413 for (const keySystem in keySystemOptions) { 57414 keySystemContentTypes[keySystem] = {}; 57415 if (audioContentType) { 57416 keySystemContentTypes[keySystem].audioContentType = audioContentType; 57417 } 57418 if (videoContentType) { 57419 keySystemContentTypes[keySystem].videoContentType = videoContentType; 57420 } // Default to using the video playlist's PSSH even though they may be different, as 57421 // videojs-contrib-eme will only accept one in the options. 57422 // 57423 // This shouldn't be an issue for most cases as early intialization will handle all 57424 // unique PSSH values, and if they aren't, then encrypted events should have the 57425 // specific information needed for the unique license. 57426 57427 if ( 57428 mainPlaylist.contentProtection && 57429 mainPlaylist.contentProtection[keySystem] && 57430 mainPlaylist.contentProtection[keySystem].pssh 57431 ) { 57432 keySystemContentTypes[keySystem].pssh = mainPlaylist.contentProtection[keySystem].pssh; 57433 } // videojs-contrib-eme accepts the option of specifying: 'com.some.cdm': 'url' 57434 // so we need to prevent overwriting the URL entirely 57435 57436 if (typeof keySystemOptions[keySystem] === 'string') { 57437 keySystemContentTypes[keySystem].url = keySystemOptions[keySystem]; 57438 } 57439 } 57440 return merge(keySystemOptions, keySystemContentTypes); 57441}; 57442/** 57443 * @typedef {Object} KeySystems 57444 * 57445 * keySystems configuration for https://github.com/videojs/videojs-contrib-eme 57446 * Note: not all options are listed here. 57447 * 57448 * @property {Uint8Array} [pssh] 57449 * Protection System Specific Header 57450 */ 57451 57452/** 57453 * Goes through all the playlists and collects an array of KeySystems options objects 57454 * containing each playlist's keySystems and their pssh values, if available. 57455 * 57456 * @param {Object[]} playlists 57457 * The playlists to look through 57458 * @param {string[]} keySystems 57459 * The keySystems to collect pssh values for 57460 * 57461 * @return {KeySystems[]} 57462 * An array of KeySystems objects containing available key systems and their 57463 * pssh values 57464 */ 57465 57466const getAllPsshKeySystemsOptions = (playlists, keySystems) => { 57467 return playlists.reduce((keySystemsArr, playlist) => { 57468 if (!playlist.contentProtection) { 57469 return keySystemsArr; 57470 } 57471 const keySystemsOptions = keySystems.reduce((keySystemsObj, keySystem) => { 57472 const keySystemOptions = playlist.contentProtection[keySystem]; 57473 if (keySystemOptions && keySystemOptions.pssh) { 57474 keySystemsObj[keySystem] = { 57475 pssh: keySystemOptions.pssh, 57476 }; 57477 } 57478 return keySystemsObj; 57479 }, {}); 57480 if (Object.keys(keySystemsOptions).length) { 57481 keySystemsArr.push(keySystemsOptions); 57482 } 57483 return keySystemsArr; 57484 }, []); 57485}; 57486/** 57487 * Returns a promise that waits for the 57488 * [eme plugin](https://github.com/videojs/videojs-contrib-eme) to create a key session. 57489 * 57490 * Works around https://bugs.chromium.org/p/chromium/issues/detail?id=895449 in non-IE11 57491 * browsers. 57492 * 57493 * As per the above ticket, this is particularly important for Chrome, where, if 57494 * unencrypted content is appended before encrypted content and the key session has not 57495 * been created, a MEDIA_ERR_DECODE will be thrown once the encrypted content is reached 57496 * during playback. 57497 * 57498 * @param {Object} player 57499 * The player instance 57500 * @param {Object[]} sourceKeySystems 57501 * The key systems options from the player source 57502 * @param {Object} [audioMedia] 57503 * The active audio media playlist (optional) 57504 * @param {Object[]} mainPlaylists 57505 * The playlists found on the main playlist object 57506 * 57507 * @return {Object} 57508 * Promise that resolves when the key session has been created 57509 */ 57510 57511const waitForKeySessionCreation = ({ player, sourceKeySystems, audioMedia, mainPlaylists }) => { 57512 if (!player.eme.initializeMediaKeys) { 57513 return Promise.resolve(); 57514 } // TODO should all audio PSSH values be initialized for DRM? 57515 // 57516 // All unique video rendition pssh values are initialized for DRM, but here only 57517 // the initial audio playlist license is initialized. In theory, an encrypted 57518 // event should be fired if the user switches to an alternative audio playlist 57519 // where a license is required, but this case hasn't yet been tested. In addition, there 57520 // may be many alternate audio playlists unlikely to be used (e.g., multiple different 57521 // languages). 57522 57523 const playlists = audioMedia ? mainPlaylists.concat([audioMedia]) : mainPlaylists; 57524 const keySystemsOptionsArr = getAllPsshKeySystemsOptions( 57525 playlists, 57526 Object.keys(sourceKeySystems) 57527 ); 57528 const initializationFinishedPromises = []; 57529 const keySessionCreatedPromises = []; // Since PSSH values are interpreted as initData, EME will dedupe any duplicates. The 57530 // only place where it should not be deduped is for ms-prefixed APIs, but 57531 // the existence of modern EME APIs in addition to 57532 // ms-prefixed APIs on Edge should prevent this from being a concern. 57533 // initializeMediaKeys also won't use the webkit-prefixed APIs. 57534 57535 keySystemsOptionsArr.forEach((keySystemsOptions) => { 57536 keySessionCreatedPromises.push( 57537 new Promise((resolve, reject) => { 57538 player.tech_.one('keysessioncreated', resolve); 57539 }) 57540 ); 57541 initializationFinishedPromises.push( 57542 new Promise((resolve, reject) => { 57543 player.eme.initializeMediaKeys( 57544 { 57545 keySystems: keySystemsOptions, 57546 }, 57547 (err) => { 57548 if (err) { 57549 reject(err); 57550 return; 57551 } 57552 resolve(); 57553 } 57554 ); 57555 }) 57556 ); 57557 }); // The reasons Promise.race is chosen over Promise.any: 57558 // 57559 // * Promise.any is only available in Safari 14+. 57560 // * None of these promises are expected to reject. If they do reject, it might be 57561 // better here for the race to surface the rejection, rather than mask it by using 57562 // Promise.any. 57563 57564 return Promise.race([ 57565 // If a session was previously created, these will all finish resolving without 57566 // creating a new session, otherwise it will take until the end of all license 57567 // requests, which is why the key session check is used (to make setup much faster). 57568 Promise.all(initializationFinishedPromises), 57569 // Once a single session is created, the browser knows DRM will be used. 57570 Promise.race(keySessionCreatedPromises), 57571 ]); 57572}; 57573/** 57574 * If the [eme](https://github.com/videojs/videojs-contrib-eme) plugin is available, and 57575 * there are keySystems on the source, sets up source options to prepare the source for 57576 * eme. 57577 * 57578 * @param {Object} player 57579 * The player instance 57580 * @param {Object[]} sourceKeySystems 57581 * The key systems options from the player source 57582 * @param {Object} media 57583 * The active media playlist 57584 * @param {Object} [audioMedia] 57585 * The active audio media playlist (optional) 57586 * 57587 * @return {boolean} 57588 * Whether or not options were configured and EME is available 57589 */ 57590 57591const setupEmeOptions = ({ player, sourceKeySystems, media, audioMedia }) => { 57592 const sourceOptions = emeKeySystems(sourceKeySystems, media, audioMedia); 57593 if (!sourceOptions) { 57594 return false; 57595 } 57596 player.currentSource().keySystems = sourceOptions; // eme handles the rest of the setup, so if it is missing 57597 // do nothing. 57598 57599 if (sourceOptions && !player.eme) { 57600 videojs.log.warn('DRM encrypted source cannot be decrypted without a DRM plugin'); 57601 return false; 57602 } 57603 return true; 57604}; 57605const getVhsLocalStorage = () => { 57606 if (!window__default['default'].localStorage) { 57607 return null; 57608 } 57609 const storedObject = window__default['default'].localStorage.getItem(LOCAL_STORAGE_KEY); 57610 if (!storedObject) { 57611 return null; 57612 } 57613 try { 57614 return JSON.parse(storedObject); 57615 } catch (e) { 57616 // someone may have tampered with the value 57617 return null; 57618 } 57619}; 57620const updateVhsLocalStorage = (options) => { 57621 if (!window__default['default'].localStorage) { 57622 return false; 57623 } 57624 let objectToStore = getVhsLocalStorage(); 57625 objectToStore = objectToStore ? merge(objectToStore, options) : options; 57626 try { 57627 window__default['default'].localStorage.setItem( 57628 LOCAL_STORAGE_KEY, 57629 JSON.stringify(objectToStore) 57630 ); 57631 } catch (e) { 57632 // Throws if storage is full (e.g., always on iOS 5+ Safari private mode, where 57633 // storage is set to 0). 57634 // https://developer.mozilla.org/en-US/docs/Web/API/Storage/setItem#Exceptions 57635 // No need to perform any operation. 57636 return false; 57637 } 57638 return objectToStore; 57639}; 57640/** 57641 * Parses VHS-supported media types from data URIs. See 57642 * https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs 57643 * for information on data URIs. 57644 * 57645 * @param {string} dataUri 57646 * The data URI 57647 * 57648 * @return {string|Object} 57649 * The parsed object/string, or the original string if no supported media type 57650 * was found 57651 */ 57652 57653const expandDataUri = (dataUri) => { 57654 if (dataUri.toLowerCase().indexOf('data:application/vnd.videojs.vhs+json,') === 0) { 57655 return JSON.parse(dataUri.substring(dataUri.indexOf(',') + 1)); 57656 } // no known case for this data URI, return the string as-is 57657 57658 return dataUri; 57659}; 57660/** 57661 * Adds a request hook to an xhr object 57662 * 57663 * @param {Object} xhr object to add the onRequest hook to 57664 * @param {function} callback hook function for an xhr request 57665 */ 57666 57667const addOnRequestHook = (xhr, callback) => { 57668 if (!xhr._requestCallbackSet) { 57669 xhr._requestCallbackSet = new Set(); 57670 } 57671 xhr._requestCallbackSet.add(callback); 57672}; 57673/** 57674 * Adds a response hook to an xhr object 57675 * 57676 * @param {Object} xhr object to add the onResponse hook to 57677 * @param {function} callback hook function for an xhr response 57678 */ 57679 57680const addOnResponseHook = (xhr, callback) => { 57681 if (!xhr._responseCallbackSet) { 57682 xhr._responseCallbackSet = new Set(); 57683 } 57684 xhr._responseCallbackSet.add(callback); 57685}; 57686/** 57687 * Removes a request hook on an xhr object, deletes the onRequest set if empty. 57688 * 57689 * @param {Object} xhr object to remove the onRequest hook from 57690 * @param {function} callback hook function to remove 57691 */ 57692 57693const removeOnRequestHook = (xhr, callback) => { 57694 if (!xhr._requestCallbackSet) { 57695 return; 57696 } 57697 xhr._requestCallbackSet.delete(callback); 57698 if (!xhr._requestCallbackSet.size) { 57699 delete xhr._requestCallbackSet; 57700 } 57701}; 57702/** 57703 * Removes a response hook on an xhr object, deletes the onResponse set if empty. 57704 * 57705 * @param {Object} xhr object to remove the onResponse hook from 57706 * @param {function} callback hook function to remove 57707 */ 57708 57709const removeOnResponseHook = (xhr, callback) => { 57710 if (!xhr._responseCallbackSet) { 57711 return; 57712 } 57713 xhr._responseCallbackSet.delete(callback); 57714 if (!xhr._responseCallbackSet.size) { 57715 delete xhr._responseCallbackSet; 57716 } 57717}; 57718/** 57719 * Whether the browser has built-in HLS support. 57720 */ 57721 57722Vhs.supportsNativeHls = (function () { 57723 if (!document__default['default'] || !document__default['default'].createElement) { 57724 return false; 57725 } 57726 const video = document__default['default'].createElement('video'); // native HLS is definitely not supported if HTML5 video isn't 57727 57728 if (!videojs.getTech('Html5').isSupported()) { 57729 return false; 57730 } // HLS manifests can go by many mime-types 57731 57732 const canPlay = [ 57733 // Apple santioned 57734 'application/vnd.apple.mpegurl', 57735 // Apple sanctioned for backwards compatibility 57736 'audio/mpegurl', 57737 // Very common 57738 'audio/x-mpegurl', 57739 // Very common 57740 'application/x-mpegurl', 57741 // Included for completeness 57742 'video/x-mpegurl', 57743 'video/mpegurl', 57744 'application/mpegurl', 57745 ]; 57746 return canPlay.some(function (canItPlay) { 57747 return /maybe|probably/i.test(video.canPlayType(canItPlay)); 57748 }); 57749})(); 57750Vhs.supportsNativeDash = (function () { 57751 if ( 57752 !document__default['default'] || 57753 !document__default['default'].createElement || 57754 !videojs.getTech('Html5').isSupported() 57755 ) { 57756 return false; 57757 } 57758 return /maybe|probably/i.test( 57759 document__default['default'].createElement('video').canPlayType('application/dash+xml') 57760 ); 57761})(); 57762Vhs.supportsTypeNatively = (type) => { 57763 if (type === 'hls') { 57764 return Vhs.supportsNativeHls; 57765 } 57766 if (type === 'dash') { 57767 return Vhs.supportsNativeDash; 57768 } 57769 return false; 57770}; 57771/** 57772 * VHS is a source handler, not a tech. Make sure attempts to use it 57773 * as one do not cause exceptions. 57774 */ 57775 57776Vhs.isSupported = function () { 57777 return videojs.log.warn( 57778 'VHS is no longer a tech. Please remove it from ' + "your player's techOrder." 57779 ); 57780}; 57781/** 57782 * A global function for setting an onRequest hook 57783 * 57784 * @param {function} callback for request modifiction 57785 */ 57786 57787Vhs.xhr.onRequest = function (callback) { 57788 addOnRequestHook(Vhs.xhr, callback); 57789}; 57790/** 57791 * A global function for setting an onResponse hook 57792 * 57793 * @param {callback} callback for response data retrieval 57794 */ 57795 57796Vhs.xhr.onResponse = function (callback) { 57797 addOnResponseHook(Vhs.xhr, callback); 57798}; 57799/** 57800 * Deletes a global onRequest callback if it exists 57801 * 57802 * @param {function} callback to delete from the global set 57803 */ 57804 57805Vhs.xhr.offRequest = function (callback) { 57806 removeOnRequestHook(Vhs.xhr, callback); 57807}; 57808/** 57809 * Deletes a global onResponse callback if it exists 57810 * 57811 * @param {function} callback to delete from the global set 57812 */ 57813 57814Vhs.xhr.offResponse = function (callback) { 57815 removeOnResponseHook(Vhs.xhr, callback); 57816}; 57817const Component = videojs.getComponent('Component'); 57818/** 57819 * The Vhs Handler object, where we orchestrate all of the parts 57820 * of VHS to interact with video.js 57821 * 57822 * @class VhsHandler 57823 * @extends videojs.Component 57824 * @param {Object} source the soruce object 57825 * @param {Tech} tech the parent tech object 57826 * @param {Object} options optional and required options 57827 */ 57828 57829class VhsHandler extends Component { 57830 constructor(source, tech, options) { 57831 super(tech, options.vhs); // if a tech level `initialBandwidth` option was passed 57832 // use that over the VHS level `bandwidth` option 57833 57834 if (typeof options.initialBandwidth === 'number') { 57835 this.options_.bandwidth = options.initialBandwidth; 57836 } 57837 this.logger_ = logger('VhsHandler'); // we need access to the player in some cases, 57838 // so, get it from Video.js via the `playerId` 57839 57840 if (tech.options_ && tech.options_.playerId) { 57841 const _player = videojs.getPlayer(tech.options_.playerId); 57842 this.player_ = _player; 57843 } 57844 this.tech_ = tech; 57845 this.source_ = source; 57846 this.stats = {}; 57847 this.ignoreNextSeekingEvent_ = false; 57848 this.setOptions_(); 57849 if ( 57850 this.options_.overrideNative && 57851 tech.overrideNativeAudioTracks && 57852 tech.overrideNativeVideoTracks 57853 ) { 57854 tech.overrideNativeAudioTracks(true); 57855 tech.overrideNativeVideoTracks(true); 57856 } else if ( 57857 this.options_.overrideNative && 57858 (tech.featuresNativeVideoTracks || tech.featuresNativeAudioTracks) 57859 ) { 57860 // overriding native VHS only works if audio tracks have been emulated 57861 // error early if we're misconfigured 57862 throw new Error( 57863 'Overriding native VHS requires emulated tracks. ' + 'See https://git.io/vMpjB' 57864 ); 57865 } // listen for fullscreenchange events for this player so that we 57866 // can adjust our quality selection quickly 57867 57868 this.on( 57869 document__default['default'], 57870 ['fullscreenchange', 'webkitfullscreenchange', 'mozfullscreenchange', 'MSFullscreenChange'], 57871 (event) => { 57872 const fullscreenElement = 57873 document__default['default'].fullscreenElement || 57874 document__default['default'].webkitFullscreenElement || 57875 document__default['default'].mozFullScreenElement || 57876 document__default['default'].msFullscreenElement; 57877 if (fullscreenElement && fullscreenElement.contains(this.tech_.el())) { 57878 this.playlistController_.fastQualityChange_(); 57879 } else { 57880 // When leaving fullscreen, since the in page pixel dimensions should be smaller 57881 // than full screen, see if there should be a rendition switch down to preserve 57882 // bandwidth. 57883 this.playlistController_.checkABR_(); 57884 } 57885 } 57886 ); 57887 this.on(this.tech_, 'seeking', function () { 57888 if (this.ignoreNextSeekingEvent_) { 57889 this.ignoreNextSeekingEvent_ = false; 57890 return; 57891 } 57892 this.setCurrentTime(this.tech_.currentTime()); 57893 }); 57894 this.on(this.tech_, 'error', function () { 57895 // verify that the error was real and we are loaded 57896 // enough to have pc loaded. 57897 if (this.tech_.error() && this.playlistController_) { 57898 this.playlistController_.pauseLoading(); 57899 } 57900 }); 57901 this.on(this.tech_, 'play', this.play); 57902 } 57903 /** 57904 * Set VHS options based on options from configuration, as well as partial 57905 * options to be passed at a later time. 57906 * 57907 * @param {Object} options A partial chunk of config options 57908 */ 57909 57910 setOptions_(options = {}) { 57911 this.options_ = merge(this.options_, options); // defaults 57912 57913 this.options_.withCredentials = this.options_.withCredentials || false; 57914 this.options_.limitRenditionByPlayerDimensions = 57915 this.options_.limitRenditionByPlayerDimensions === false ? false : true; 57916 this.options_.useDevicePixelRatio = this.options_.useDevicePixelRatio || false; 57917 this.options_.useBandwidthFromLocalStorage = 57918 typeof this.source_.useBandwidthFromLocalStorage !== 'undefined' 57919 ? this.source_.useBandwidthFromLocalStorage 57920 : this.options_.useBandwidthFromLocalStorage || false; 57921 this.options_.useForcedSubtitles = this.options_.useForcedSubtitles || false; 57922 this.options_.useNetworkInformationApi = this.options_.useNetworkInformationApi || false; 57923 this.options_.useDtsForTimestampOffset = this.options_.useDtsForTimestampOffset || false; 57924 this.options_.customTagParsers = this.options_.customTagParsers || []; 57925 this.options_.customTagMappers = this.options_.customTagMappers || []; 57926 this.options_.cacheEncryptionKeys = this.options_.cacheEncryptionKeys || false; 57927 this.options_.llhls = this.options_.llhls === false ? false : true; 57928 this.options_.bufferBasedABR = this.options_.bufferBasedABR || false; 57929 if (typeof this.options_.playlistExclusionDuration !== 'number') { 57930 this.options_.playlistExclusionDuration = 60; 57931 } 57932 if (typeof this.options_.bandwidth !== 'number') { 57933 if (this.options_.useBandwidthFromLocalStorage) { 57934 const storedObject = getVhsLocalStorage(); 57935 if (storedObject && storedObject.bandwidth) { 57936 this.options_.bandwidth = storedObject.bandwidth; 57937 this.tech_.trigger({ 57938 type: 'usage', 57939 name: 'vhs-bandwidth-from-local-storage', 57940 }); 57941 } 57942 if (storedObject && storedObject.throughput) { 57943 this.options_.throughput = storedObject.throughput; 57944 this.tech_.trigger({ 57945 type: 'usage', 57946 name: 'vhs-throughput-from-local-storage', 57947 }); 57948 } 57949 } 57950 } // if bandwidth was not set by options or pulled from local storage, start playlist 57951 // selection at a reasonable bandwidth 57952 57953 if (typeof this.options_.bandwidth !== 'number') { 57954 this.options_.bandwidth = Config.INITIAL_BANDWIDTH; 57955 } // If the bandwidth number is unchanged from the initial setting 57956 // then this takes precedence over the enableLowInitialPlaylist option 57957 57958 this.options_.enableLowInitialPlaylist = 57959 this.options_.enableLowInitialPlaylist && 57960 this.options_.bandwidth === Config.INITIAL_BANDWIDTH; // grab options passed to player.src 57961 57962 [ 57963 'withCredentials', 57964 'useDevicePixelRatio', 57965 'customPixelRatio', 57966 'limitRenditionByPlayerDimensions', 57967 'bandwidth', 57968 'customTagParsers', 57969 'customTagMappers', 57970 'cacheEncryptionKeys', 57971 'playlistSelector', 57972 'initialPlaylistSelector', 57973 'bufferBasedABR', 57974 'liveRangeSafeTimeDelta', 57975 'llhls', 57976 'useForcedSubtitles', 57977 'useNetworkInformationApi', 57978 'useDtsForTimestampOffset', 57979 'exactManifestTimings', 57980 'leastPixelDiffSelector', 57981 ].forEach((option) => { 57982 if (typeof this.source_[option] !== 'undefined') { 57983 this.options_[option] = this.source_[option]; 57984 } 57985 }); 57986 this.limitRenditionByPlayerDimensions = this.options_.limitRenditionByPlayerDimensions; 57987 this.useDevicePixelRatio = this.options_.useDevicePixelRatio; 57988 const customPixelRatio = this.options_.customPixelRatio; // Ensure the custom pixel ratio is a number greater than or equal to 0 57989 57990 if (typeof customPixelRatio === 'number' && customPixelRatio >= 0) { 57991 this.customPixelRatio = customPixelRatio; 57992 } 57993 } // alias for public method to set options 57994 57995 setOptions(options = {}) { 57996 this.setOptions_(options); 57997 } 57998 /** 57999 * called when player.src gets called, handle a new source 58000 * 58001 * @param {Object} src the source object to handle 58002 */ 58003 58004 src(src, type) { 58005 // do nothing if the src is falsey 58006 if (!src) { 58007 return; 58008 } 58009 this.setOptions_(); // add main playlist controller options 58010 58011 this.options_.src = expandDataUri(this.source_.src); 58012 this.options_.tech = this.tech_; 58013 this.options_.externVhs = Vhs; 58014 this.options_.sourceType = mediaTypes_js.simpleTypeFromSourceType(type); // Whenever we seek internally, we should update the tech 58015 58016 this.options_.seekTo = (time) => { 58017 this.tech_.setCurrentTime(time); 58018 }; // pass player to allow for player level eventing on construction. 58019 58020 this.options_.player_ = this.player_; 58021 this.playlistController_ = new PlaylistController(this.options_); 58022 const playbackWatcherOptions = merge( 58023 { 58024 liveRangeSafeTimeDelta: SAFE_TIME_DELTA, 58025 }, 58026 this.options_, 58027 { 58028 seekable: () => this.seekable(), 58029 media: () => this.playlistController_.media(), 58030 playlistController: this.playlistController_, 58031 } 58032 ); 58033 this.playbackWatcher_ = new PlaybackWatcher(playbackWatcherOptions); 58034 this.attachStreamingEventListeners_(); 58035 this.playlistController_.on('error', () => { 58036 const player = videojs.players[this.tech_.options_.playerId]; 58037 let error = this.playlistController_.error; 58038 if (typeof error === 'object' && !error.code) { 58039 error.code = 3; 58040 } else if (typeof error === 'string') { 58041 error = { 58042 message: error, 58043 code: 3, 58044 }; 58045 } 58046 player.error(error); 58047 }); 58048 const defaultSelector = this.options_.bufferBasedABR
58049 ? Vhs.movingAverageBandwidthSelector(0.55) 58050 : Vhs.STANDARD_PLAYLIST_SELECTOR; // `this` in selectPlaylist should be the VhsHandler for backwards 58051 // compatibility with < v2 58052 58053 this.playlistController_.selectPlaylist = this.selectPlaylist 58054 ? this.selectPlaylist.bind(this) 58055 : defaultSelector.bind(this); 58056 this.playlistController_.selectInitialPlaylist = Vhs.INITIAL_PLAYLIST_SELECTOR.bind(this); // re-expose some internal objects for backwards compatibility with < v2 58057 58058 this.playlists = this.playlistController_.mainPlaylistLoader_; 58059 this.mediaSource = this.playlistController_.mediaSource; // Proxy assignment of some properties to the main playlist 58060 // controller. Using a custom property for backwards compatibility 58061 // with < v2 58062 58063 Object.defineProperties(this, { 58064 selectPlaylist: { 58065 get() { 58066 return this.playlistController_.selectPlaylist; 58067 }, 58068 set(selectPlaylist) { 58069 this.playlistController_.selectPlaylist = selectPlaylist.bind(this); 58070 }, 58071 }, 58072 throughput: { 58073 get() { 58074 return this.playlistController_.mainSegmentLoader_.throughput.rate; 58075 }, 58076 set(throughput) { 58077 this.playlistController_.mainSegmentLoader_.throughput.rate = throughput; // By setting `count` to 1 the throughput value becomes the starting value 58078 // for the cumulative average 58079 58080 this.playlistController_.mainSegmentLoader_.throughput.count = 1; 58081 }, 58082 }, 58083 bandwidth: { 58084 get() { 58085 let playerBandwidthEst = this.playlistController_.mainSegmentLoader_.bandwidth; 58086 const networkInformation = 58087 window__default['default'].navigator.connection || 58088 window__default['default'].navigator.mozConnection || 58089 window__default['default'].navigator.webkitConnection; 58090 const tenMbpsAsBitsPerSecond = 10e6; 58091 if (this.options_.useNetworkInformationApi && networkInformation) { 58092 // downlink returns Mbps 58093 // https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/downlink 58094 const networkInfoBandwidthEstBitsPerSec = networkInformation.downlink * 1000 * 1000; // downlink maxes out at 10 Mbps. In the event that both networkInformationApi and the player 58095 // estimate a bandwidth greater than 10 Mbps, use the larger of the two estimates to ensure that 58096 // high quality streams are not filtered out. 58097 58098 if ( 58099 networkInfoBandwidthEstBitsPerSec >= tenMbpsAsBitsPerSecond && 58100 playerBandwidthEst >= tenMbpsAsBitsPerSecond 58101 ) { 58102 playerBandwidthEst = Math.max(playerBandwidthEst, networkInfoBandwidthEstBitsPerSec); 58103 } else { 58104 playerBandwidthEst = networkInfoBandwidthEstBitsPerSec; 58105 } 58106 } 58107 return playerBandwidthEst; 58108 }, 58109 set(bandwidth) { 58110 this.playlistController_.mainSegmentLoader_.bandwidth = bandwidth; // setting the bandwidth manually resets the throughput counter 58111 // `count` is set to zero that current value of `rate` isn't included 58112 // in the cumulative average 58113 58114 this.playlistController_.mainSegmentLoader_.throughput = { 58115 rate: 0, 58116 count: 0, 58117 }; 58118 }, 58119 }, 58120 /** 58121 * `systemBandwidth` is a combination of two serial processes bit-rates. The first 58122 * is the network bitrate provided by `bandwidth` and the second is the bitrate of 58123 * the entire process after that - decryption, transmuxing, and appending - provided 58124 * by `throughput`. 58125 * 58126 * Since the two process are serial, the overall system bandwidth is given by: 58127 * sysBandwidth = 1 / (1 / bandwidth + 1 / throughput) 58128 */ 58129 systemBandwidth: { 58130 get() { 58131 const invBandwidth = 1 / (this.bandwidth || 1); 58132 let invThroughput; 58133 if (this.throughput > 0) { 58134 invThroughput = 1 / this.throughput; 58135 } else { 58136 invThroughput = 0; 58137 } 58138 const systemBitrate = Math.floor(1 / (invBandwidth + invThroughput)); 58139 return systemBitrate; 58140 }, 58141 set() { 58142 videojs.log.error('The "systemBandwidth" property is read-only'); 58143 }, 58144 }, 58145 }); 58146 if (this.options_.bandwidth) { 58147 this.bandwidth = this.options_.bandwidth; 58148 } 58149 if (this.options_.throughput) { 58150 this.throughput = this.options_.throughput; 58151 } 58152 Object.defineProperties(this.stats, { 58153 bandwidth: { 58154 get: () => this.bandwidth || 0, 58155 enumerable: true, 58156 }, 58157 mediaRequests: { 58158 get: () => this.playlistController_.mediaRequests_() || 0, 58159 enumerable: true, 58160 }, 58161 mediaRequestsAborted: { 58162 get: () => this.playlistController_.mediaRequestsAborted_() || 0, 58163 enumerable: true, 58164 }, 58165 mediaRequestsTimedout: { 58166 get: () => this.playlistController_.mediaRequestsTimedout_() || 0, 58167 enumerable: true, 58168 }, 58169 mediaRequestsErrored: { 58170 get: () => this.playlistController_.mediaRequestsErrored_() || 0, 58171 enumerable: true, 58172 }, 58173 mediaTransferDuration: { 58174 get: () => this.playlistController_.mediaTransferDuration_() || 0, 58175 enumerable: true, 58176 }, 58177 mediaBytesTransferred: { 58178 get: () => this.playlistController_.mediaBytesTransferred_() || 0, 58179 enumerable: true, 58180 }, 58181 mediaSecondsLoaded: { 58182 get: () => this.playlistController_.mediaSecondsLoaded_() || 0, 58183 enumerable: true, 58184 }, 58185 mediaAppends: { 58186 get: () => this.playlistController_.mediaAppends_() || 0, 58187 enumerable: true, 58188 }, 58189 mainAppendsToLoadedData: { 58190 get: () => this.playlistController_.mainAppendsToLoadedData_() || 0, 58191 enumerable: true, 58192 }, 58193 audioAppendsToLoadedData: { 58194 get: () => this.playlistController_.audioAppendsToLoadedData_() || 0, 58195 enumerable: true, 58196 }, 58197 appendsToLoadedData: { 58198 get: () => this.playlistController_.appendsToLoadedData_() || 0, 58199 enumerable: true, 58200 }, 58201 timeToLoadedData: { 58202 get: () => this.playlistController_.timeToLoadedData_() || 0, 58203 enumerable: true, 58204 }, 58205 buffered: { 58206 get: () => timeRangesToArray(this.tech_.buffered()), 58207 enumerable: true, 58208 }, 58209 currentTime: { 58210 get: () => this.tech_.currentTime(), 58211 enumerable: true, 58212 }, 58213 currentSource: { 58214 get: () => this.tech_.currentSource_, 58215 enumerable: true, 58216 }, 58217 currentTech: { 58218 get: () => this.tech_.name_, 58219 enumerable: true, 58220 }, 58221 duration: { 58222 get: () => this.tech_.duration(), 58223 enumerable: true, 58224 }, 58225 main: { 58226 get: () => this.playlists.main, 58227 enumerable: true, 58228 }, 58229 playerDimensions: { 58230 get: () => this.tech_.currentDimensions(), 58231 enumerable: true, 58232 }, 58233 seekable: { 58234 get: () => timeRangesToArray(this.tech_.seekable()), 58235 enumerable: true, 58236 }, 58237 timestamp: { 58238 get: () => Date.now(), 58239 enumerable: true, 58240 }, 58241 videoPlaybackQuality: { 58242 get: () => this.tech_.getVideoPlaybackQuality(), 58243 enumerable: true, 58244 }, 58245 }); 58246 this.tech_.one( 58247 'canplay', 58248 this.playlistController_.setupFirstPlay.bind(this.playlistController_) 58249 ); 58250 this.tech_.on('bandwidthupdate', () => { 58251 if (this.options_.useBandwidthFromLocalStorage) { 58252 updateVhsLocalStorage({ 58253 bandwidth: this.bandwidth, 58254 throughput: Math.round(this.throughput), 58255 }); 58256 } 58257 }); 58258 this.playlistController_.on('selectedinitialmedia', () => {
vendor: 4,586 bytes, lines 58259-58370
58259 // Add the manual rendition mix-in to VhsHandler 58260 renditionSelectionMixin(this); 58261 }); 58262 this.playlistController_.sourceUpdater_.on('createdsourcebuffers', () => { 58263 this.setupEme_(); 58264 }); // the bandwidth of the primary segment loader is our best 58265 // estimate of overall bandwidth 58266 58267 this.on(this.playlistController_, 'progress', function () { 58268 this.tech_.trigger('progress'); 58269 }); // In the live case, we need to ignore the very first `seeking` event since 58270 // that will be the result of the seek-to-live behavior 58271 58272 this.on(this.playlistController_, 'firstplay', function () { 58273 this.ignoreNextSeekingEvent_ = true; 58274 }); 58275 this.setupQualityLevels_(); // do nothing if the tech has been disposed already 58276 // this can occur if someone sets the src in player.ready(), for instance 58277 58278 if (!this.tech_.el()) { 58279 return; 58280 } 58281 this.mediaSourceUrl_ = window__default['default'].URL.createObjectURL( 58282 this.playlistController_.mediaSource 58283 ); // If we are playing HLS with MSE in Safari, add source elements for both the blob and manifest URLs. 58284 // The latter will enable Airplay playback on receiver devices. 58285 58286 if ( 58287 (videojs.browser.IS_ANY_SAFARI || videojs.browser.IS_IOS) && 58288 this.options_.overrideNative && 58289 this.options_.sourceType === 'hls' && 58290 typeof this.tech_.addSourceElement === 'function' 58291 ) { 58292 this.tech_.addSourceElement(this.mediaSourceUrl_); 58293 this.tech_.addSourceElement(this.source_.src); 58294 } else { 58295 this.tech_.src(this.mediaSourceUrl_); 58296 } 58297 } 58298 createKeySessions_() { 58299 const audioPlaylistLoader = this.playlistController_.mediaTypes_.AUDIO.activePlaylistLoader; 58300 this.logger_('waiting for EME key session creation'); 58301 waitForKeySessionCreation({ 58302 player: this.player_, 58303 sourceKeySystems: this.source_.keySystems, 58304 audioMedia: audioPlaylistLoader && audioPlaylistLoader.media(), 58305 mainPlaylists: this.playlists.main.playlists, 58306 }) 58307 .then(() => { 58308 this.logger_('created EME key session'); 58309 this.playlistController_.sourceUpdater_.initializedEme(); 58310 }) 58311 .catch((err) => { 58312 this.logger_('error while creating EME key session', err); 58313 this.player_.error({ 58314 message: 'Failed to initialize media keys for EME', 58315 code: 3, 58316 }); 58317 }); 58318 } 58319 handleWaitingForKey_() { 58320 // If waitingforkey is fired, it's possible that the data that's necessary to retrieve 58321 // the key is in the manifest. While this should've happened on initial source load, it 58322 // may happen again in live streams where the keys change, and the manifest info 58323 // reflects the update. 58324 // 58325 // Because videojs-contrib-eme compares the PSSH data we send to that of PSSH data it's 58326 // already requested keys for, we don't have to worry about this generating extraneous 58327 // requests. 58328 this.logger_('waitingforkey fired, attempting to create any new key sessions'); 58329 this.createKeySessions_(); 58330 } 58331 /** 58332 * If necessary and EME is available, sets up EME options and waits for key session 58333 * creation. 58334 * 58335 * This function also updates the source updater so taht it can be used, as for some 58336 * browsers, EME must be configured before content is appended (if appending unencrypted 58337 * content before encrypted content). 58338 */ 58339 58340 setupEme_() { 58341 const audioPlaylistLoader = this.playlistController_.mediaTypes_.AUDIO.activePlaylistLoader; 58342 const didSetupEmeOptions = setupEmeOptions({ 58343 player: this.player_, 58344 sourceKeySystems: this.source_.keySystems, 58345 media: this.playlists.media(), 58346 audioMedia: audioPlaylistLoader && audioPlaylistLoader.media(), 58347 }); 58348 this.player_.tech_.on('keystatuschange', (e) => { 58349 this.playlistController_.updatePlaylistByKeyStatus(e.keyId, e.status); 58350 }); 58351 this.handleWaitingForKey_ = this.handleWaitingForKey_.bind(this); 58352 this.player_.tech_.on('waitingforkey', this.handleWaitingForKey_); 58353 if (!didSetupEmeOptions) { 58354 // If EME options were not set up, we've done all we could to initialize EME. 58355 this.playlistController_.sourceUpdater_.initializedEme(); 58356 return; 58357 } 58358 this.createKeySessions_(); 58359 } 58360 /** 58361 * Initializes the quality levels and sets listeners to update them. 58362 * 58363 * @method setupQualityLevels_ 58364 * @private 58365 */ 58366 58367 setupQualityLevels_() { 58368 const player = videojs.players[this.tech_.options_.playerId]; // if there isn't a player or there isn't a qualityLevels plugin 58369 // or qualityLevels_ listeners have already been setup, do nothing. 58370
58371 if (!player || !player.qualityLevels || this.qualityLevels_) { 58372 return; 58373 } 58374 this.qualityLevels_ = player.qualityLevels(); 58375 this.playlistController_.on('selectedinitialmedia', () => { 58376 handleVhsLoadedMetadata(this.qualityLevels_, this); 58377 }); 58378 this.playlists.on('mediachange', () => { 58379 handleVhsMediaChange(this.qualityLevels_, this.playlists); 58380 }); 58381 } 58382 /** 58383 * return the version 58384 */ 58385 58386 static version() { 58387 return { 58388 '@videojs/http-streaming': version$4, 58389 'mux.js': version$3, 58390 'mpd-parser': version$2, 58391 'm3u8-parser': version$1, 58392 'aes-decrypter': version, 58393 }; 58394 } 58395 /** 58396 * return the version 58397 */ 58398 58399 version() { 58400 return this.constructor.version(); 58401 } 58402 canChangeType() { 58403 return SourceUpdater.canChangeType(); 58404 } 58405 /** 58406 * Begin playing the video. 58407 */ 58408 58409 play() { 58410 this.playlistController_.play(); 58411 } 58412 /** 58413 * a wrapper around the function in PlaylistController 58414 */ 58415 58416 setCurrentTime(currentTime) { 58417 this.playlistController_.setCurrentTime(currentTime); 58418 } 58419 /** 58420 * a wrapper around the function in PlaylistController 58421 */ 58422 58423 duration() { 58424 return this.playlistController_.duration(); 58425 } 58426 /** 58427 * a wrapper around the function in PlaylistController 58428 */ 58429 58430 seekable() { 58431 return this.playlistController_.seekable(); 58432 } 58433 /** 58434 * Abort all outstanding work and cleanup. 58435 */ 58436 58437 dispose() { 58438 if (this.playbackWatcher_) { 58439 this.playbackWatcher_.dispose(); 58440 } 58441 if (this.playlistController_) { 58442 this.playlistController_.dispose(); 58443 }
vendor: 5,801 bytes, lines 58444-58618
58444 if (this.qualityLevels_) { 58445 this.qualityLevels_.dispose(); 58446 } 58447 if (this.tech_ && this.tech_.vhs) { 58448 delete this.tech_.vhs; 58449 } 58450 if (this.mediaSourceUrl_ && window__default['default'].URL.revokeObjectURL) { 58451 window__default['default'].URL.revokeObjectURL(this.mediaSourceUrl_); 58452 this.mediaSourceUrl_ = null; 58453 } 58454 if (this.tech_) { 58455 this.tech_.off('waitingforkey', this.handleWaitingForKey_); 58456 } 58457 super.dispose(); 58458 } 58459 convertToProgramTime(time, callback) { 58460 return getProgramTime({ 58461 playlist: this.playlistController_.media(), 58462 time, 58463 callback, 58464 }); 58465 } // the player must be playing before calling this 58466 58467 seekToProgramTime(programTime, callback, pauseAfterSeek = true, retryCount = 2) { 58468 return seekToProgramTime({ 58469 programTime, 58470 playlist: this.playlistController_.media(), 58471 retryCount, 58472 pauseAfterSeek, 58473 seekTo: this.options_.seekTo, 58474 tech: this.options_.tech, 58475 callback, 58476 }); 58477 } 58478 /** 58479 * Adds the onRequest, onResponse, offRequest and offResponse functions 58480 * to the VhsHandler xhr Object. 58481 */ 58482 58483 setupXhrHooks_() { 58484 /** 58485 * A player function for setting an onRequest hook 58486 * 58487 * @param {function} callback for request modifiction 58488 */ 58489 this.xhr.onRequest = (callback) => { 58490 addOnRequestHook(this.xhr, callback); 58491 }; 58492 /** 58493 * A player function for setting an onResponse hook 58494 * 58495 * @param {callback} callback for response data retrieval 58496 */ 58497 58498 this.xhr.onResponse = (callback) => { 58499 addOnResponseHook(this.xhr, callback); 58500 }; 58501 /** 58502 * Deletes a player onRequest callback if it exists 58503 * 58504 * @param {function} callback to delete from the player set 58505 */ 58506 58507 this.xhr.offRequest = (callback) => { 58508 removeOnRequestHook(this.xhr, callback); 58509 }; 58510 /** 58511 * Deletes a player onResponse callback if it exists 58512 * 58513 * @param {function} callback to delete from the player set 58514 */ 58515 58516 this.xhr.offResponse = (callback) => { 58517 removeOnResponseHook(this.xhr, callback); 58518 }; // Trigger an event on the player to notify the user that vhs is ready to set xhr hooks. 58519 // This allows hooks to be set before the source is set to vhs when handleSource is called. 58520 58521 this.player_.trigger('xhr-hooks-ready'); 58522 } 58523 attachStreamingEventListeners_() { 58524 const playlistControllerEvents = [ 58525 'seekablerangeschanged', 58526 'bufferedrangeschanged', 58527 'contentsteeringloadstart', 58528 'contentsteeringloadcomplete', 58529 'contentsteeringparsed', 58530 ]; 58531 const playbackWatcher = ['gapjumped', 'playedrangeschanged']; // re-emit streaming events and payloads on the player. 58532 58533 playlistControllerEvents.forEach((eventName) => { 58534 this.playlistController_.on(eventName, (metadata) => { 58535 this.player_.trigger(_extends__default['default']({}, metadata)); 58536 }); 58537 }); 58538 playbackWatcher.forEach((eventName) => { 58539 this.playbackWatcher_.on(eventName, (metadata) => { 58540 this.player_.trigger(_extends__default['default']({}, metadata)); 58541 }); 58542 }); 58543 } 58544} 58545/** 58546 * The Source Handler object, which informs video.js what additional 58547 * MIME types are supported and sets up playback. It is registered 58548 * automatically to the appropriate tech based on the capabilities of 58549 * the browser it is running in. It is not necessary to use or modify 58550 * this object in normal usage. 58551 */ 58552 58553const VhsSourceHandler = { 58554 name: 'videojs-http-streaming', 58555 VERSION: version$4, 58556 canHandleSource(srcObj, options = {}) { 58557 const localOptions = merge(videojs.options, options); // If not opting to experimentalUseMMS, and playback is only supported with MediaSource, cannot handle source 58558 58559 if ( 58560 !localOptions.vhs.experimentalUseMMS && 58561 !codecs_js.browserSupportsCodec('avc1.4d400d,mp4a.40.2', false) 58562 ) { 58563 return false; 58564 } 58565 return VhsSourceHandler.canPlayType(srcObj.type, localOptions); 58566 }, 58567 handleSource(source, tech, options = {}) { 58568 const localOptions = merge(videojs.options, options); 58569 tech.vhs = new VhsHandler(source, tech, localOptions); 58570 tech.vhs.xhr = xhrFactory(); 58571 tech.vhs.setupXhrHooks_(); 58572 tech.vhs.src(source.src, source.type); 58573 return tech.vhs; 58574 }, 58575 canPlayType(type, options) { 58576 const simpleType = mediaTypes_js.simpleTypeFromSourceType(type); 58577 if (!simpleType) { 58578 return ''; 58579 } 58580 const overrideNative = VhsSourceHandler.getOverrideNative(options); 58581 const supportsTypeNatively = Vhs.supportsTypeNatively(simpleType); 58582 const canUseMsePlayback = !supportsTypeNatively || overrideNative; 58583 return canUseMsePlayback ? 'maybe' : ''; 58584 }, 58585 getOverrideNative(options = {}) { 58586 const { vhs = {} } = options; 58587 const defaultOverrideNative = !(videojs.browser.IS_ANY_SAFARI || videojs.browser.IS_IOS); 58588 const { overrideNative = defaultOverrideNative } = vhs; 58589 return overrideNative; 58590 }, 58591}; 58592/** 58593 * Check to see if either the native MediaSource or ManagedMediaSource 58594 * objectx exist and support an MP4 container with both H.264 video 58595 * and AAC-LC audio. 58596 * 58597 * @return {boolean} if native media sources are supported 58598 */ 58599 58600const supportsNativeMediaSources = () => { 58601 return codecs_js.browserSupportsCodec('avc1.4d400d,mp4a.40.2', true); 58602}; // register source handlers with the appropriate techs 58603 58604if (supportsNativeMediaSources()) { 58605 videojs.getTech('Html5').registerSourceHandler(VhsSourceHandler, 0); 58606} 58607videojs.VhsHandler = VhsHandler; 58608videojs.VhsSourceHandler = VhsSourceHandler; 58609videojs.Vhs = Vhs; 58610if (!videojs.use) { 58611 videojs.registerComponent('Vhs', Vhs); 58612} 58613videojs.options.vhs = videojs.options.vhs || {}; 58614if (!videojs.getPlugin || !videojs.getPlugin('reloadSourceOnError')) { 58615 videojs.registerPlugin('reloadSourceOnError', reloadSourceOnError); 58616} 58617 58618module.exports = videojs;
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.