vendor: 4,677 bytes, lines 1-164
1/** 2 * @license 3 * Video.js 8.9.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(function (global, factory) { 14 typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 15 typeof define === 'function' && define.amd ? define(factory) : 16 (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.videojs = factory()); 17})(this, (function () { 'use strict'; 18 19 var version$5 = "8.9.0"; 20 21 /** 22 * An Object that contains lifecycle hooks as keys which point to an array 23 * of functions that are run when a lifecycle is triggered 24 * 25 * @private 26 */ 27 const hooks_ = {}; 28 29 /** 30 * Get a list of hooks for a specific lifecycle 31 * 32 * @param {string} type 33 * the lifecycle to get hooks from 34 * 35 * @param {Function|Function[]} [fn] 36 * Optionally add a hook (or hooks) to the lifecycle that your are getting. 37 * 38 * @return {Array} 39 * an array of hooks, or an empty array if there are none. 40 */ 41 const hooks = function (type, fn) { 42 hooks_[type] = hooks_[type] || []; 43 if (fn) { 44 hooks_[type] = hooks_[type].concat(fn); 45 } 46 return hooks_[type]; 47 }; 48 49 /** 50 * Add a function hook to a specific videojs lifecycle. 51 * 52 * @param {string} type 53 * the lifecycle to hook the function to. 54 * 55 * @param {Function|Function[]} 56 * The function or array of functions to attach. 57 */ 58 const hook = function (type, fn) { 59 hooks(type, fn); 60 }; 61 62 /** 63 * Remove a hook from a specific videojs lifecycle. 64 * 65 * @param {string} type 66 * the lifecycle that the function hooked to 67 * 68 * @param {Function} fn 69 * The hooked function to remove 70 * 71 * @return {boolean} 72 * The function that was removed or undef 73 */ 74 const removeHook = function (type, fn) { 75 const index = hooks(type).indexOf(fn); 76 if (index <= -1) { 77 return false; 78 } 79 hooks_[type] = hooks_[type].slice(); 80 hooks_[type].splice(index, 1); 81 return true; 82 }; 83 84 /** 85 * Add a function hook that will only run once to a specific videojs lifecycle. 86 * 87 * @param {string} type 88 * the lifecycle to hook the function to. 89 * 90 * @param {Function|Function[]} 91 * The function or array of functions to attach. 92 */ 93 const hookOnce = function (type, fn) { 94 hooks(type, [].concat(fn).map(original => { 95 const wrapper = (...args) => { 96 removeHook(type, wrapper); 97 return original(...args); 98 }; 99 return wrapper; 100 })); 101 }; 102 103 /** 104 * @file fullscreen-api.js 105 * @module fullscreen-api 106 */ 107 108 /** 109 * Store the browser-specific methods for the fullscreen API. 110 * 111 * @type {Object} 112 * @see [Specification]{@link https://fullscreen.spec.whatwg.org} 113 * @see [Map Approach From Screenfull.js]{@link https://github.com/sindresorhus/screenfull.js} 114 */ 115 const FullscreenApi = { 116 prefixed: true 117 }; 118 119 // browser API methods 120 const apiMap = [['requestFullscreen', 'exitFullscreen', 'fullscreenElement', 'fullscreenEnabled', 'fullscreenchange', 'fullscreenerror', 'fullscreen'], 121 // WebKit 122 ['webkitRequestFullscreen', 'webkitExitFullscreen', 'webkitFullscreenElement', 'webkitFullscreenEnabled', 'webkitfullscreenchange', 'webkitfullscreenerror', '-webkit-full-screen']]; 123 const specApi = apiMap[0]; 124 let browserApi; 125 126 // determine the supported set of functions 127 for (let i = 0; i < apiMap.length; i++) { 128 // check for exitFullscreen function 129 if (apiMap[i][1] in document) { 130 browserApi = apiMap[i]; 131 break; 132 } 133 } 134 135 // map the browser API names to the spec API names 136 if (browserApi) { 137 for (let i = 0; i < browserApi.length; i++) { 138 FullscreenApi[specApi[i]] = browserApi[i]; 139 } 140 FullscreenApi.prefixed = browserApi[0] !== specApi[0]; 141 } 142 143 /** 144 * @file create-logger.js 145 * @module create-logger 146 */ 147 148 // This is the private tracking variable for the logging history. 149 let history = []; 150 151 /** 152 * Log messages to the console and history based on the type of message 153 * 154 * @private 155 * @param {string} name 156 * The name of the console method to use. 157 * 158 * @param {Object} log 159 * The arguments to be passed to the matching console method. 160 * 161 * @param {string} [styles] 162 * styles for name 163 */ 164 const LogByTypeFactory = (name, log, styles) =>
vendor: 2,797 bytes, lines 164-250
164 (type, level, args) => { 165 const lvl = log.levels[level]; 166 const lvlRegExp = new RegExp(`^(${lvl})$`); 167 let resultName = name; 168 if (type !== 'log') { 169 // Add the type to the front of the message when it's not "log". 170 args.unshift(type.toUpperCase() + ':'); 171 } 172 if (styles) { 173 resultName = `%c${name}`; 174 args.unshift(styles); 175 } 176 177 // Add console prefix after adding to history. 178 args.unshift(resultName + ':'); 179 180 // Add a clone of the args at this point to history. 181 if (history) { 182 history.push([].concat(args)); 183 184 // only store 1000 history entries 185 const splice = history.length - 1000; 186 history.splice(0, splice > 0 ? splice : 0); 187 } 188 189 // If there's no console then don't try to output messages, but they will 190 // still be stored in history. 191 if (!window.console) { 192 return; 193 } 194 195 // Was setting these once outside of this function, but containing them 196 // in the function makes it easier to test cases where console doesn't exist 197 // when the module is executed. 198 let fn = window.console[type]; 199 if (!fn && type === 'debug') { 200 // Certain browsers don't have support for console.debug. For those, we 201 // should default to the closest comparable log. 202 fn = window.console.info || window.console.log; 203 } 204 205 // Bail out if there's no console or if this type is not allowed by the 206 // current logging level. 207 if (!fn || !lvl || !lvlRegExp.test(type)) { 208 return; 209 } 210 fn[Array.isArray(args) ? 'apply' : 'call'](window.console, args); 211 }; 212 function createLogger$1(name, delimiter = ':', styles = '') { 213 // This is the private tracking variable for logging level. 214 let level = 'info'; 215 216 // the curried logByType bound to the specific log and history 217 let logByType; 218 219 /** 220 * Logs plain debug messages. Similar to `console.log`. 221 * 222 * Due to [limitations](https://github.com/jsdoc3/jsdoc/issues/955#issuecomment-313829149) 223 * of our JSDoc template, we cannot properly document this as both a function 224 * and a namespace, so its function signature is documented here. 225 * 226 * #### Arguments 227 * ##### *args 228 * *[] 229 * 230 * Any combination of values that could be passed to `console.log()`. 231 * 232 * #### Return Value 233 * 234 * `undefined` 235 * 236 * @namespace 237 * @param {...*} args 238 * One or more messages or objects that should be logged. 239 */ 240 const log = function (...args) { 241 logByType('log', level, args); 242 }; 243 244 // This is the logByType helper that the logging methods below use 245 logByType = LogByTypeFactory(name, log, styles); 246 247 /** 248 * Create a new subLogger which chains the old name to the new name. 249 * 250 * For example, doing `
vendor: 12,595 bytes, lines 250-725
250videojs.log.createLogger('player')` and then using that logger will log the following: 251 * ```js 252 * mylogger('foo'); 253 * // > VIDEOJS: player: foo 254 * ``` 255 * 256 * @param {string} subName 257 * The name to add call the new logger 258 * @param {string} [subDelimiter] 259 * Optional delimiter 260 * @param {string} [subStyles] 261 * Optional styles 262 * @return {Object} 263 */ 264 log.createLogger = (subName, subDelimiter, subStyles) => { 265 const resultDelimiter = subDelimiter !== undefined ? subDelimiter : delimiter; 266 const resultStyles = subStyles !== undefined ? subStyles : styles; 267 const resultName = `${name} ${resultDelimiter} ${subName}`; 268 return createLogger$1(resultName, resultDelimiter, resultStyles); 269 }; 270 271 /** 272 * Create a new logger. 273 * 274 * @param {string} newName 275 * The name for the new logger 276 * @param {string} [newDelimiter] 277 * Optional delimiter 278 * @param {string} [newStyles] 279 * Optional styles 280 * @return {Object} 281 */ 282 log.createNewLogger = (newName, newDelimiter, newStyles) => { 283 return createLogger$1(newName, newDelimiter, newStyles); 284 }; 285 286 /** 287 * Enumeration of available logging levels, where the keys are the level names 288 * and the values are `|`-separated strings containing logging methods allowed 289 * in that logging level. These strings are used to create a regular expression 290 * matching the function name being called. 291 * 292 * Levels provided by Video.js are: 293 * 294 * - `off`: Matches no calls. Any value that can be cast to `false` will have 295 * this effect. The most restrictive. 296 * - `all`: Matches only Video.js-provided functions (`debug`, `log`, 297 * `log.warn`, and `log.error`). 298 * - `debug`: Matches `log.debug`, `log`, `log.warn`, and `log.error` calls. 299 * - `info` (default): Matches `log`, `log.warn`, and `log.error` calls. 300 * - `warn`: Matches `log.warn` and `log.error` calls. 301 * - `error`: Matches only `log.error` calls. 302 * 303 * @type {Object} 304 */ 305 log.levels = { 306 all: 'debug|log|warn|error', 307 off: '', 308 debug: 'debug|log|warn|error', 309 info: 'log|warn|error', 310 warn: 'warn|error', 311 error: 'error', 312 DEFAULT: level 313 }; 314 315 /** 316 * Get or set the current logging level. 317 * 318 * If a string matching a key from {@link module:log.levels} is provided, acts 319 * as a setter. 320 * 321 * @param {'all'|'debug'|'info'|'warn'|'error'|'off'} [lvl] 322 * Pass a valid level to set a new logging level. 323 * 324 * @return {string} 325 * The current logging level. 326 */ 327 log.level = lvl => { 328 if (typeof lvl === 'string') { 329 if (!log.levels.hasOwnProperty(lvl)) { 330 throw new Error(`"${lvl}" in not a valid log level`); 331 } 332 level = lvl; 333 } 334 return level; 335 }; 336 337 /** 338 * Returns an array containing everything that has been logged to the history. 339 * 340 * This array is a shallow clone of the internal history record. However, its 341 * contents are _not_ cloned; so, mutating objects inside this array will 342 * mutate them in history. 343 * 344 * @return {Array} 345 */ 346 log.history = () => history ? [].concat(history) : []; 347 348 /** 349 * Allows you to filter the history by the given logger name 350 * 351 * @param {string} fname 352 * The name to filter by 353 * 354 * @return {Array} 355 * The filtered list to return 356 */ 357 log.history.filter = fname => { 358 return (history || []).filter(historyItem => { 359 // if the first item in each historyItem includes `fname`, then it's a match 360 return new RegExp(`.*${fname}.*`).test(historyItem[0]); 361 }); 362 }; 363 364 /** 365 * Clears the internal history tracking, but does not prevent further history 366 * tracking. 367 */ 368 log.history.clear = () => { 369 if (history) { 370 history.length = 0; 371 } 372 }; 373 374 /** 375 * Disable history tracking if it is currently enabled. 376 */ 377 log.history.disable = () => { 378 if (history !== null) { 379 history.length = 0; 380 history = null; 381 } 382 }; 383 384 /** 385 * Enable history tracking if it is currently disabled. 386 */ 387 log.history.enable = () => { 388 if (history === null) { 389 history = []; 390 } 391 }; 392 393 /** 394 * Logs error messages. Similar to `console.error`. 395 * 396 * @param {...*} args 397 * One or more messages or objects that should be logged as an error 398 */ 399 log.error = (...args) => logByType('error', level, args); 400 401 /** 402 * Logs warning messages. Similar to `console.warn`. 403 * 404 * @param {...*} args 405 * One or more messages or objects that should be logged as a warning. 406 */ 407 log.warn = (...args) => logByType('warn', level, args); 408 409 /** 410 * Logs debug messages. Similar to `console.debug`, but may also act as a comparable 411 * log if `console.debug` is not available 412 * 413 * @param {...*} args 414 * One or more messages or objects that should be logged as debug. 415 */ 416 log.debug = (...args) => logByType('debug', level, args); 417 return log; 418 } 419 420 /** 421 * @file log.js 422 * @module log 423 */ 424 const log$1 = createLogger$1('VIDEOJS'); 425 const createLogger = log$1.createLogger; 426 427 /** 428 * @file obj.js 429 * @module obj 430 */ 431 432 /** 433 * @callback obj:EachCallback 434 * 435 * @param {*} value 436 * The current key for the object that is being iterated over. 437 * 438 * @param {string} key 439 * The current key-value for object that is being iterated over 440 */ 441 442 /** 443 * @callback obj:ReduceCallback 444 * 445 * @param {*} accum 446 * The value that is accumulating over the reduce loop. 447 * 448 * @param {*} value 449 * The current key for the object that is being iterated over. 450 * 451 * @param {string} key 452 * The current key-value for object that is being iterated over 453 * 454 * @return {*} 455 * The new accumulated value. 456 */ 457 const toString$1 = Object.prototype.toString; 458 459 /** 460 * Get the keys of an Object 461 * 462 * @param {Object} 463 * The Object to get the keys from 464 * 465 * @return {string[]} 466 * An array of the keys from the object. Returns an empty array if the 467 * object passed in was invalid or had no keys. 468 * 469 * @private 470 */ 471 const keys = function (object) { 472 return isObject$1(object) ? Object.keys(object) : []; 473 }; 474 475 /** 476 * Array-like iteration for objects. 477 * 478 * @param {Object} object 479 * The object to iterate over 480 * 481 * @param {obj:EachCallback} fn 482 * The callback function which is called for each key in the object. 483 */ 484 function each(object, fn) { 485 keys(object).forEach(key => fn(object[key], key)); 486 } 487 488 /** 489 * Array-like reduce for objects. 490 * 491 * @param {Object} object 492 * The Object that you want to reduce. 493 * 494 * @param {Function} fn 495 * A callback function which is called for each key in the object. It 496 * receives the accumulated value and the per-iteration value and key 497 * as arguments. 498 * 499 * @param {*} [initial = 0] 500 * Starting value 501 * 502 * @return {*} 503 * The final accumulated value. 504 */ 505 function reduce(object, fn, initial = 0) { 506 return keys(object).reduce((accum, key) => fn(accum, object[key], key), initial); 507 } 508 509 /** 510 * Returns whether a value is an object of any kind - including DOM nodes, 511 * arrays, regular expressions, etc. Not functions, though. 512 * 513 * This avoids the gotcha where using `typeof` on a `null` value 514 * results in `'object'`. 515 * 516 * @param {Object} value 517 * @return {boolean} 518 */ 519 function isObject$1(value) { 520 return !!value && typeof value === 'object'; 521 } 522 523 /** 524 * Returns whether an object appears to be a "plain" object - that is, a 525 * direct instance of `Object`. 526 * 527 * @param {Object} value 528 * @return {boolean} 529 */ 530 function isPlain(value) { 531 return isObject$1(value) && toString$1.call(value) === '[object Object]' && value.constructor === Object; 532 } 533 534 /** 535 * Merge two objects recursively. 536 * 537 * Performs a deep merge like 538 * {@link https://lodash.com/docs/4.17.10#merge|lodash.merge}, but only merges 539 * plain objects (not arrays, elements, or anything else). 540 * 541 * Non-plain object values will be copied directly from the right-most 542 * argument. 543 * 544 * @param {Object[]} sources 545 * One or more objects to merge into a new object. 546 * 547 * @return {Object} 548 * A new object that is the merged result of all sources. 549 */ 550 function merge$2(...sources) { 551 const result = {}; 552 sources.forEach(source => { 553 if (!source) { 554 return; 555 } 556 each(source, (value, key) => { 557 if (!isPlain(value)) { 558 result[key] = value; 559 return; 560 } 561 if (!isPlain(result[key])) { 562 result[key] = {}; 563 } 564 result[key] = merge$2(result[key], value); 565 }); 566 }); 567 return result; 568 } 569 570 /** 571 * Returns an array of values for a given object 572 * 573 * @param {Object} source - target object 574 * @return {Array<unknown>} - object values 575 */ 576 function values$1(source = {}) { 577 const result = []; 578 for (const key in source) { 579 if (source.hasOwnProperty(key)) { 580 const value = source[key]; 581 result.push(value); 582 } 583 } 584 return result; 585 } 586 587 /** 588 * Object.defineProperty but "lazy", which means that the value is only set after 589 * it is retrieved the first time, rather than being set right away. 590 * 591 * @param {Object} obj the object to set the property on 592 * @param {string} key the key for the property to set 593 * @param {Function} getValue the function used to get the value when it is needed. 594 * @param {boolean} setter whether a setter should be allowed or not 595 */ 596 function defineLazyProperty(obj, key, getValue, setter = true) { 597 const set = value => Object.defineProperty(obj, key, { 598 value, 599 enumerable: true, 600 writable: true 601 }); 602 const options = { 603 configurable: true, 604 enumerable: true, 605 get() { 606 const value = getValue(); 607 set(value); 608 return value; 609 } 610 }; 611 if (setter) { 612 options.set = set; 613 } 614 return Object.defineProperty(obj, key, options); 615 } 616 617 var Obj = /*#__PURE__*/Object.freeze({ 618 __proto__: null, 619 each: each, 620 reduce: reduce, 621 isObject: isObject$1, 622 isPlain: isPlain, 623 merge: merge$2, 624 values: values$1, 625 defineLazyProperty: defineLazyProperty 626 }); 627 628 /** 629 * @file browser.js 630 * @module browser 631 */ 632 633 /** 634 * Whether or not this device is an iPod. 635 * 636 * @static 637 * @type {Boolean} 638 */ 639 let IS_IPOD = false; 640 641 /** 642 * The detected iOS version - or `null`. 643 * 644 * @static 645 * @type {string|null} 646 */ 647 let IOS_VERSION = null; 648 649 /** 650 * Whether or not this is an Android device. 651 * 652 * @static 653 * @type {Boolean} 654 */ 655 let IS_ANDROID = false; 656 657 /** 658 * The detected Android version - or `null` if not Android or indeterminable. 659 * 660 * @static 661 * @type {number|string|null} 662 */ 663 let ANDROID_VERSION; 664 665 /** 666 * Whether or not this is Mozilla Firefox. 667 * 668 * @static 669 * @type {Boolean} 670 */ 671 let IS_FIREFOX = false; 672 673 /** 674 * Whether or not this is Microsoft Edge. 675 * 676 * @static 677 * @type {Boolean} 678 */ 679 let IS_EDGE = false; 680 681 /** 682 * Whether or not this is any Chromium Browser 683 * 684 * @static 685 * @type {Boolean} 686 */ 687 let IS_CHROMIUM = false; 688 689 /** 690 * Whether or not this is any Chromium browser that is not Edge. 691 * 692 * This will also be `true` for Chrome on iOS, which will have different support 693 * as it is actually Safari under the hood. 694 * 695 * Deprecated, as the behaviour to not match Edge was to prevent Legacy Edge's UA matching. 696 * IS_CHROMIUM should be used instead. 697 * "Chromium but not Edge" could be explicitly tested with IS_CHROMIUM && !IS_EDGE 698 * 699 * @static 700 * @deprecated 701 * @type {Boolean} 702 */ 703 let IS_CHROME = false; 704 705 /** 706 * The detected Chromium version - or `null`. 707 * 708 * @static 709 * @type {number|null} 710 */ 711 let CHROMIUM_VERSION = null; 712 713 /** 714 * The detected Google Chrome version - or `null`. 715 * This has always been the _Chromium_ version, i.e. would return on Chromium Edge. 716 * Deprecated, use CHROMIUM_VERSION instead. 717 * 718 * @static 719 * @deprecated 720 * @type {number|null} 721 */ 722 let CHROME_VERSION = null; 723 724 /** 725 *
725The detected Internet Explorer version - or `null`. 726 * 727 * @static 728 * @deprecated 729 * @type {number|null} 730 */ 731 let IE_VERSION = null; 732 733 /** 734 * Whether or not this is desktop Safari. 735 * 736 * @static 737 * @type {Boolean} 738 */ 739 let IS_SAFARI = false;
vendor: 5,042 bytes, lines 740-880
740 741 /** 742 * Whether or not this is a Windows machine. 743 * 744 * @static 745 * @type {Boolean} 746 */ 747 let IS_WINDOWS = false; 748 749 /** 750 * Whether or not this device is an iPad. 751 * 752 * @static 753 * @type {Boolean} 754 */ 755 let IS_IPAD = false; 756 757 /** 758 * Whether or not this device is an iPhone. 759 * 760 * @static 761 * @type {Boolean} 762 */ 763 // The Facebook app's UIWebView identifies as both an iPhone and iPad, so 764 // to identify iPhones, we need to exclude iPads. 765 // http://artsy.github.io/blog/2012/10/18/the-perils-of-ios-user-agent-sniffing/ 766 let IS_IPHONE = false; 767 768 /** 769 * Whether or not this device is touch-enabled. 770 * 771 * @static 772 * @const 773 * @type {Boolean} 774 */ 775 const TOUCH_ENABLED = Boolean(isReal() && ('ontouchstart' in window || window.navigator.maxTouchPoints || window.DocumentTouch && window.document instanceof window.DocumentTouch)); 776 const UAD = window.navigator && window.navigator.userAgentData; 777 if (UAD && UAD.platform && UAD.brands) { 778 // If userAgentData is present, use it instead of userAgent to avoid warnings 779 // Currently only implemented on Chromium 780 // userAgentData does not expose Android version, so ANDROID_VERSION remains `null` 781 782 IS_ANDROID = UAD.platform === 'Android'; 783 IS_EDGE = Boolean(UAD.brands.find(b => b.brand === 'Microsoft Edge')); 784 IS_CHROMIUM = Boolean(UAD.brands.find(b => b.brand === 'Chromium')); 785 IS_CHROME = !IS_EDGE && IS_CHROMIUM; 786 CHROMIUM_VERSION = CHROME_VERSION = (UAD.brands.find(b => b.brand === 'Chromium') || {}).version || null; 787 IS_WINDOWS = UAD.platform === 'Windows'; 788 } 789 790 // If the browser is not Chromium, either userAgentData is not present which could be an old Chromium browser, 791 // or it's a browser that has added userAgentData since that we don't have tests for yet. In either case, 792 // the checks need to be made agiainst the regular userAgent string. 793 if (!IS_CHROMIUM) { 794 const USER_AGENT = window.navigator && window.navigator.userAgent || ''; 795 IS_IPOD = /iPod/i.test(USER_AGENT); 796 IOS_VERSION = function () { 797 const match = USER_AGENT.match(/OS (\d+)_/i); 798 if (match && match[1]) { 799 return match[1]; 800 } 801 return null; 802 }(); 803 IS_ANDROID = /Android/i.test(USER_AGENT); 804 ANDROID_VERSION = function () { 805 // This matches Android Major.Minor.Patch versions 806 // ANDROID_VERSION is Major.Minor as a Number, if Minor isn't available, then only Major is returned 807 const match = USER_AGENT.match(/Android (\d+)(?:\.(\d+))?(?:\.(\d+))*/i); 808 if (!match) { 809 return null; 810 } 811 const major = match[1] && parseFloat(match[1]); 812 const minor = match[2] && parseFloat(match[2]); 813 if (major && minor) { 814 return parseFloat(match[1] + '.' + match[2]); 815 } else if (major) { 816 return major; 817 } 818 return null; 819 }(); 820 IS_FIREFOX = /Firefox/i.test(USER_AGENT); 821 IS_EDGE = /Edg/i.test(USER_AGENT); 822 IS_CHROMIUM = /Chrome/i.test(USER_AGENT) || /CriOS/i.test(USER_AGENT); 823 IS_CHROME = !IS_EDGE && IS_CHROMIUM; 824 CHROMIUM_VERSION = CHROME_VERSION = function () { 825 const match = USER_AGENT.match(/(Chrome|CriOS)\/(\d+)/); 826 if (match && match[2]) { 827 return parseFloat(match[2]); 828 } 829 return null; 830 }(); 831 IE_VERSION = function () { 832 const result = /MSIE\s(\d+)\.\d/.exec(USER_AGENT); 833 let version = result && parseFloat(result[1]); 834 if (!version && /Trident\/7.0/i.test(USER_AGENT) && /rv:11.0/.test(USER_AGENT)) { 835 // IE 11 has a different user agent string than other IE versions 836 version = 11.0; 837 } 838 return version; 839 }(); 840 IS_SAFARI = /Safari/i.test(USER_AGENT) && !IS_CHROME && !IS_ANDROID && !IS_EDGE; 841 IS_WINDOWS = /Windows/i.test(USER_AGENT); 842 IS_IPAD = /iPad/i.test(USER_AGENT) || IS_SAFARI && TOUCH_ENABLED && !/iPhone/i.test(USER_AGENT); 843 IS_IPHONE = /iPhone/i.test(USER_AGENT) && !IS_IPAD; 844 } 845 846 /** 847 * Whether or not this is an iOS device. 848 * 849 * @static 850 * @const 851 * @type {Boolean} 852 */ 853 const IS_IOS = IS_IPHONE || IS_IPAD || IS_IPOD; 854 855 /** 856 * Whether or not this is any flavor of Safari - including iOS. 857 * 858 * @static 859 * @const 860 * @type {Boolean} 861 */ 862 const IS_ANY_SAFARI = (IS_SAFARI || IS_IOS) && !IS_CHROME; 863 864 var browser = /*#__PURE__*/Object.freeze({ 865 __proto__: null, 866 get IS_IPOD () { return IS_IPOD; }, 867 get IOS_VERSION () { return IOS_VERSION; }, 868 get IS_ANDROID () { return IS_ANDROID; }, 869 get ANDROID_VERSION () { return ANDROID_VERSION; }, 870 get IS_FIREFOX () { return IS_FIREFOX; }, 871 get IS_EDGE () { return IS_EDGE; }, 872 get IS_CHROMIUM () { return IS_CHROMIUM; }, 873 get IS_CHROME () { return IS_CHROME; }, 874 get CHROMIUM_VERSION () { return CHROMIUM_VERSION; }, 875 get CHROME_VERSION () { return CHROME_VERSION; }, 876 get IE_VERSION () { return IE_VERSION; }, 877 get IS_SAFARI () { return IS_SAFARI; }, 878 get IS_WINDOWS () { return IS_WINDOWS; }, 879 get IS_IPAD () { return IS_IPAD; }, 880 get IS_IPHONE () { return IS_IPHONE
vendor: 7,050 bytes, lines 880-1133
880; }, 881 TOUCH_ENABLED: TOUCH_ENABLED, 882 IS_IOS: IS_IOS, 883 IS_ANY_SAFARI: IS_ANY_SAFARI 884 }); 885 886 /** 887 * @file dom.js 888 * @module dom 889 */ 890 891 /** 892 * Detect if a value is a string with any non-whitespace characters. 893 * 894 * @private 895 * @param {string} str 896 * The string to check 897 * 898 * @return {boolean} 899 * Will be `true` if the string is non-blank, `false` otherwise. 900 * 901 */ 902 function isNonBlankString(str) { 903 // we use str.trim as it will trim any whitespace characters 904 // from the front or back of non-whitespace characters. aka 905 // Any string that contains non-whitespace characters will 906 // still contain them after `trim` but whitespace only strings 907 // will have a length of 0, failing this check. 908 return typeof str === 'string' && Boolean(str.trim()); 909 } 910 911 /** 912 * Throws an error if the passed string has whitespace. This is used by 913 * class methods to be relatively consistent with the classList API. 914 * 915 * @private 916 * @param {string} str 917 * The string to check for whitespace. 918 * 919 * @throws {Error} 920 * Throws an error if there is whitespace in the string. 921 */ 922 function throwIfWhitespace(str) { 923 // str.indexOf instead of regex because str.indexOf is faster performance wise. 924 if (str.indexOf(' ') >= 0) { 925 throw new Error('class has illegal whitespace characters'); 926 } 927 } 928 929 /** 930 * Whether the current DOM interface appears to be real (i.e. not simulated). 931 * 932 * @return {boolean} 933 * Will be `true` if the DOM appears to be real, `false` otherwise. 934 */ 935 function isReal() { 936 // Both document and window will never be undefined thanks to `global`. 937 return document === window.document; 938 } 939 940 /** 941 * Determines, via duck typing, whether or not a value is a DOM element. 942 * 943 * @param {*} value 944 * The value to check. 945 * 946 * @return {boolean} 947 * Will be `true` if the value is a DOM element, `false` otherwise. 948 */ 949 function isEl(value) { 950 return isObject$1(value) && value.nodeType === 1; 951 } 952 953 /** 954 * Determines if the current DOM is embedded in an iframe. 955 * 956 * @return {boolean} 957 * Will be `true` if the DOM is embedded in an iframe, `false` 958 * otherwise. 959 */ 960 function isInFrame() { 961 // We need a try/catch here because Safari will throw errors when attempting 962 // to get either `parent` or `self` 963 try { 964 return window.parent !== window.self; 965 } catch (x) { 966 return true; 967 } 968 } 969 970 /** 971 * Creates functions to query the DOM using a given method. 972 * 973 * @private 974 * @param {string} method 975 * The method to create the query with. 976 * 977 * @return {Function} 978 * The query method 979 */ 980 function createQuerier(method) { 981 return function (selector, context) { 982 if (!isNonBlankString(selector)) { 983 return document[method](null); 984 } 985 if (isNonBlankString(context)) { 986 context = document.querySelector(context); 987 } 988 const ctx = isEl(context) ? context : document; 989 return ctx[method] && ctx[method](selector); 990 }; 991 } 992 993 /** 994 * Creates an element and applies properties, attributes, and inserts content. 995 * 996 * @param {string} [tagName='div'] 997 * Name of tag to be created. 998 * 999 * @param {Object} [properties={}] 1000 * Element properties to be applied. 1001 * 1002 * @param {Object} [attributes={}] 1003 * Element attributes to be applied. 1004 * 1005 * @param {ContentDescriptor} [content] 1006 * A content descriptor object. 1007 * 1008 * @return {Element} 1009 * The element that was created. 1010 */ 1011 function createEl(tagName = 'div', properties = {}, attributes = {}, content) { 1012 const el = document.createElement(tagName); 1013 Object.getOwnPropertyNames(properties).forEach(function (propName) { 1014 const val = properties[propName]; 1015 1016 // Handle textContent since it's not supported everywhere and we have a 1017 // method for it. 1018 if (propName === 'textContent') { 1019 textContent(el, val); 1020 } else if (el[propName] !== val || propName === 'tabIndex') { 1021 el[propName] = val; 1022 } 1023 }); 1024 Object.getOwnPropertyNames(attributes).forEach(function (attrName) { 1025 el.setAttribute(attrName, attributes[attrName]); 1026 }); 1027 if (content) { 1028 appendContent(el, content); 1029 } 1030 return el; 1031 } 1032 1033 /** 1034 * Injects text into an element, replacing any existing contents entirely. 1035 * 1036 * @param {HTMLElement} el 1037 * The element to add text content into 1038 * 1039 * @param {string} text 1040 * The text content to add. 1041 * 1042 * @return {Element} 1043 * The element with added text content. 1044 */ 1045 function textContent(el, text) { 1046 if (typeof el.textContent === 'undefined') { 1047 el.innerText = text; 1048 } else { 1049 el.textContent = text; 1050 } 1051 return el; 1052 } 1053 1054 /** 1055 * Insert an element as the first child node of another 1056 * 1057 * @param {Element} child 1058 * Element to insert 1059 * 1060 * @param {Element} parent 1061 * Element to insert child into 1062 */ 1063 function prependTo(child, parent) { 1064 if (parent.firstChild) { 1065 parent.insertBefore(child, parent.firstChild); 1066 } else { 1067 parent.appendChild(child); 1068 } 1069 } 1070 1071 /** 1072 * Check if an element has a class name. 1073 * 1074 * @param {Element} element 1075 * Element to check 1076 * 1077 * @param {string} classToCheck 1078 * Class name to check for 1079 * 1080 * @return {boolean} 1081 * Will be `true` if the element has a class, `false` otherwise. 1082 * 1083 * @throws {Error} 1084 * Throws an error if `classToCheck` has white space. 1085 */ 1086 function hasClass(element, classToCheck) { 1087 throwIfWhitespace(classToCheck); 1088 return element.classList.contains(classToCheck); 1089 } 1090 1091 /** 1092 * Add a class name to an element. 1093 * 1094 * @param {Element} element 1095 * Element to add class name to. 1096 * 1097 * @param {...string} classesToAdd 1098 * One or more class name to add. 1099 * 1100 * @return {Element} 1101 * The DOM element with the added class name. 1102 */ 1103 function addClass(element, ...classesToAdd) { 1104 element.classList.add(...classesToAdd.reduce((prev, current) => prev.concat(current.split(/\s+/)), [])); 1105 return element; 1106 } 1107 1108 /** 1109 * Remove a class name from an element. 1110 * 1111 * @param {Element} element 1112 * Element to remove a class name from. 1113 * 1114 * @param {...string} classesToRemove 1115 * One or more class name to remove. 1116 * 1117 * @return {Element} 1118 * The DOM element with class name removed. 1119 */ 1120 function removeClass(element, ...classesToRemove) { 1121 // Protect in case the player gets disposed 1122 if (!element) { 1123 log$1.warn("removeClass was called with an element that doesn't exist"); 1124 return null; 1125 } 1126 element.classList.remove(...classesToRemove.reduce((prev, current) => prev.concat(current.split(/\s+/)), [])); 1127 return element; 1128 } 1129 1130 /** 1131 * The callback definition for toggleClass. 1132 * 1133 * @callback
1133module:dom~PredicateCallback 1134 * @param {Element} element 1135 * The DOM element of the Component. 1136 * 1137 * @param {string} classToToggle 1138 * The `className` that wants to be toggled 1139 * 1140 * @return {boolean|undefined} 1141 * If `true` is returned, the `classToToggle` will be added to the 1142 * `element`. If `false`, the `classToToggle` will be removed from 1143 * the `element`. If `undefined`, the callback will be ignored. 1144 */ 1145 1146 /** 1147 * Adds or removes a class name to/from an element depending on an optional 1148 * condition or the presence/absence of the class name. 1149 * 1150 * @param {Element} element 1151 * The element to toggle a class name on. 1152 * 1153 * @param {string} classToToggle 1154 * The class that should be toggled. 1155 * 1156 * @param {boolean|module:dom~
vendor: 7,975 bytes, lines 1156-1432
1156PredicateCallback} [predicate] 1157 * See the return value for {@link module:dom~PredicateCallback} 1158 * 1159 * @return {Element} 1160 * The element with a class that has been toggled. 1161 */ 1162 function toggleClass(element, classToToggle, predicate) { 1163 if (typeof predicate === 'function') { 1164 predicate = predicate(element, classToToggle); 1165 } 1166 if (typeof predicate !== 'boolean') { 1167 predicate = undefined; 1168 } 1169 classToToggle.split(/\s+/).forEach(className => element.classList.toggle(className, predicate)); 1170 return element; 1171 } 1172 1173 /** 1174 * Apply attributes to an HTML element. 1175 * 1176 * @param {Element} el 1177 * Element to add attributes to. 1178 * 1179 * @param {Object} [attributes] 1180 * Attributes to be applied. 1181 */ 1182 function setAttributes(el, attributes) { 1183 Object.getOwnPropertyNames(attributes).forEach(function (attrName) { 1184 const attrValue = attributes[attrName]; 1185 if (attrValue === null || typeof attrValue === 'undefined' || attrValue === false) { 1186 el.removeAttribute(attrName); 1187 } else { 1188 el.setAttribute(attrName, attrValue === true ? '' : attrValue); 1189 } 1190 }); 1191 } 1192 1193 /** 1194 * Get an element's attribute values, as defined on the HTML tag. 1195 * 1196 * Attributes are not the same as properties. They're defined on the tag 1197 * or with setAttribute. 1198 * 1199 * @param {Element} tag 1200 * Element from which to get tag attributes. 1201 * 1202 * @return {Object} 1203 * All attributes of the element. Boolean attributes will be `true` or 1204 * `false`, others will be strings. 1205 */ 1206 function getAttributes(tag) { 1207 const obj = {}; 1208 1209 // known boolean attributes 1210 // we can check for matching boolean properties, but not all browsers 1211 // and not all tags know about these attributes, so, we still want to check them manually 1212 const knownBooleans = ['autoplay', 'controls', 'playsinline', 'loop', 'muted', 'default', 'defaultMuted']; 1213 if (tag && tag.attributes && tag.attributes.length > 0) { 1214 const attrs = tag.attributes; 1215 for (let i = attrs.length - 1; i >= 0; i--) { 1216 const attrName = attrs[i].name; 1217 /** @type {boolean|string} */ 1218 let attrVal = attrs[i].value; 1219 1220 // check for known booleans 1221 // the matching element property will return a value for typeof 1222 if (knownBooleans.includes(attrName)) { 1223 // the value of an included boolean attribute is typically an empty 1224 // string ('') which would equal false if we just check for a false value. 1225 // we also don't want support bad code like autoplay='false' 1226 attrVal = attrVal !== null ? true : false; 1227 } 1228 obj[attrName] = attrVal; 1229 } 1230 } 1231 return obj; 1232 } 1233 1234 /** 1235 * Get the value of an element's attribute. 1236 * 1237 * @param {Element} el 1238 * A DOM element. 1239 * 1240 * @param {string} attribute 1241 * Attribute to get the value of. 1242 * 1243 * @return {string} 1244 * The value of the attribute. 1245 */ 1246 function getAttribute(el, attribute) { 1247 return el.getAttribute(attribute); 1248 } 1249 1250 /** 1251 * Set the value of an element's attribute. 1252 * 1253 * @param {Element} el 1254 * A DOM element. 1255 * 1256 * @param {string} attribute 1257 * Attribute to set. 1258 * 1259 * @param {string} value 1260 * Value to set the attribute to. 1261 */ 1262 function setAttribute(el, attribute, value) { 1263 el.setAttribute(attribute, value); 1264 } 1265 1266 /** 1267 * Remove an element's attribute. 1268 * 1269 * @param {Element} el 1270 * A DOM element. 1271 * 1272 * @param {string} attribute 1273 * Attribute to remove. 1274 */ 1275 function removeAttribute(el, attribute) { 1276 el.removeAttribute(attribute); 1277 } 1278 1279 /** 1280 * Attempt to block the ability to select text. 1281 */ 1282 function blockTextSelection() { 1283 document.body.focus(); 1284 document.onselectstart = function () { 1285 return false; 1286 }; 1287 } 1288 1289 /** 1290 * Turn off text selection blocking. 1291 */ 1292 function unblockTextSelection() { 1293 document.onselectstart = function () { 1294 return true; 1295 }; 1296 } 1297 1298 /** 1299 * Identical to the native `getBoundingClientRect` function, but ensures that 1300 * the method is supported at all (it is in all browsers we claim to support) 1301 * and that the element is in the DOM before continuing. 1302 * 1303 * This wrapper function also shims properties which are not provided by some 1304 * older browsers (namely, IE8). 1305 * 1306 * Additionally, some browsers do not support adding properties to a 1307 * `ClientRect`/`DOMRect` object; so, we shallow-copy it with the standard 1308 * properties (except `x` and `y` which are not widely supported). This helps 1309 * avoid implementations where keys are non-enumerable. 1310 * 1311 * @param {Element} el 1312 * Element whose `ClientRect` we want to calculate. 1313 * 1314 * @return {Object|undefined} 1315 * Always returns a plain object - or `undefined` if it cannot. 1316 */ 1317 function getBoundingClientRect(el) { 1318 if (el && el.getBoundingClientRect && el.parentNode) { 1319 const rect = el.getBoundingClientRect(); 1320 const result = {}; 1321 ['bottom', 'height', 'left', 'right', 'top', 'width'].forEach(k => { 1322 if (rect[k] !== undefined) { 1323 result[k] = rect[k]; 1324 } 1325 }); 1326 if (!result.height) { 1327 result.height = parseFloat(computedStyle(el, 'height')); 1328 } 1329 if (!result.width) { 1330 result.width = parseFloat(computedStyle(el, 'width')); 1331 } 1332 return result; 1333 } 1334 } 1335 1336 /** 1337 * Represents the position of a DOM element on the page. 1338 * 1339 * @typedef {Object} module:dom~Position 1340 * 1341 * @property {number} left 1342 * Pixels to the left. 1343 * 1344 * @property {number} top 1345 * Pixels from the top. 1346 */ 1347 1348 /** 1349 * Get the position of an element in the DOM. 1350 * 1351 * Uses `getBoundingClientRect` technique from John Resig. 1352 * 1353 * @see http://ejohn.org/blog/getboundingclientrect-is-awesome/ 1354 * 1355 * @param {Element} el 1356 * Element from which to get offset. 1357 * 1358 * @return {module:dom~Position} 1359 * The position of the element that was passed in. 1360 */ 1361 function findPosition(el) { 1362 if (!el || el && !el.offsetParent) { 1363 return { 1364 left: 0, 1365 top: 0, 1366 width: 0, 1367 height: 0 1368 }; 1369 } 1370 const width = el.offsetWidth; 1371 const height = el.offsetHeight; 1372 let left = 0; 1373 let top = 0; 1374 while (el.offsetParent && el !== document[FullscreenApi.fullscreenElement]) { 1375 left += el.offsetLeft; 1376 top += el.offsetTop; 1377 el = el.offsetParent; 1378 } 1379 return { 1380 left, 1381 top, 1382 width, 1383 height 1384 }; 1385 } 1386 1387 /** 1388 * Represents x and y coordinates for a DOM element or mouse pointer. 1389 * 1390 * @typedef {Object} module:dom~Coordinates 1391 * 1392 * @property {number} x 1393 * x coordinate in pixels 1394 * 1395 * @property {number} y 1396 * y coordinate in pixels 1397 */ 1398 1399 /** 1400 * Get the pointer position within an element. 1401 * 1402 * The base on the coordinates are the bottom left of the element. 1403 * 1404 * @param {Element} el 1405 * Element on which to get the pointer position on. 1406 * 1407 * @param {Event} event 1408 * Event object. 1409 * 1410 * @return {module:dom~Coordinates} 1411 * A coordinates object corresponding to the mouse position. 1412 * 1413 */ 1414 function getPointerPosition(el, event) { 1415 const translated = { 1416 x: 0, 1417 y: 0 1418 }; 1419 if (IS_IOS) { 1420 let item = el; 1421 while (item && item.nodeName.toLowerCase() !== 'html') { 1422 const transform = computedStyle(item, 'transform'); 1423 if (/^matrix/.test(transform)) { 1424 const values = transform.slice(7, -1).split(/,\s/).map(Number); 1425 translated.x += values[4]; 1426 translated.y += values[5]; 1427 } else if (/^matrix3d/.test(transform)) { 1428 const values = transform.slice(9, -1).split(/,\s/).map(Number); 1429 translated.x += values[12]; 1430 translated.y += values[13]; 1431 } 1432 i
vendor: 6,178 bytes, lines 1432-1612
1432tem = item.parentNode; 1433 } 1434 } 1435 const position = {}; 1436 const boxTarget = findPosition(event.target); 1437 const box = findPosition(el); 1438 const boxW = box.width; 1439 const boxH = box.height; 1440 let offsetY = event.offsetY - (box.top - boxTarget.top); 1441 let offsetX = event.offsetX - (box.left - boxTarget.left); 1442 if (event.changedTouches) { 1443 offsetX = event.changedTouches[0].pageX - box.left; 1444 offsetY = event.changedTouches[0].pageY + box.top; 1445 if (IS_IOS) { 1446 offsetX -= translated.x; 1447 offsetY -= translated.y; 1448 } 1449 } 1450 position.y = 1 - Math.max(0, Math.min(1, offsetY / boxH)); 1451 position.x = Math.max(0, Math.min(1, offsetX / boxW)); 1452 return position; 1453 } 1454 1455 /** 1456 * Determines, via duck typing, whether or not a value is a text node. 1457 * 1458 * @param {*} value 1459 * Check if this value is a text node. 1460 * 1461 * @return {boolean} 1462 * Will be `true` if the value is a text node, `false` otherwise. 1463 */ 1464 function isTextNode$1(value) { 1465 return isObject$1(value) && value.nodeType === 3; 1466 } 1467 1468 /** 1469 * Empties the contents of an element. 1470 * 1471 * @param {Element} el 1472 * The element to empty children from 1473 * 1474 * @return {Element} 1475 * The element with no children 1476 */ 1477 function emptyEl(el) { 1478 while (el.firstChild) { 1479 el.removeChild(el.firstChild); 1480 } 1481 return el; 1482 } 1483 1484 /** 1485 * This is a mixed value that describes content to be injected into the DOM 1486 * via some method. It can be of the following types: 1487 * 1488 * Type | Description 1489 * -----------|------------- 1490 * `string` | The value will be normalized into a text node. 1491 * `Element` | The value will be accepted as-is. 1492 * `Text` | A TextNode. The value will be accepted as-is. 1493 * `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). 1494 * `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. 1495 * 1496 * @typedef {string|Element|Text|Array|Function} ContentDescriptor 1497 */ 1498 1499 /** 1500 * Normalizes content for eventual insertion into the DOM. 1501 * 1502 * This allows a wide range of content definition methods, but helps protect 1503 * from falling into the trap of simply writing to `innerHTML`, which could 1504 * be an XSS concern. 1505 * 1506 * The content for an element can be passed in multiple types and 1507 * combinations, whose behavior is as follows: 1508 * 1509 * @param {ContentDescriptor} content 1510 * A content descriptor value. 1511 * 1512 * @return {Array} 1513 * All of the content that was passed in, normalized to an array of 1514 * elements or text nodes. 1515 */ 1516 function normalizeContent(content) { 1517 // First, invoke content if it is a function. If it produces an array, 1518 // that needs to happen before normalization. 1519 if (typeof content === 'function') { 1520 content = content(); 1521 } 1522 1523 // Next up, normalize to an array, so one or many items can be normalized, 1524 // filtered, and returned. 1525 return (Array.isArray(content) ? content : [content]).map(value => { 1526 // First, invoke value if it is a function to produce a new value, 1527 // which will be subsequently normalized to a Node of some kind. 1528 if (typeof value === 'function') { 1529 value = value(); 1530 } 1531 if (isEl(value) || isTextNode$1(value)) { 1532 return value; 1533 } 1534 if (typeof value === 'string' && /\S/.test(value)) { 1535 return document.createTextNode(value); 1536 } 1537 }).filter(value => value); 1538 } 1539 1540 /** 1541 * Normalizes and appends content to an element. 1542 * 1543 * @param {Element} el 1544 * Element to append normalized content to. 1545 * 1546 * @param {ContentDescriptor} content 1547 * A content descriptor value. 1548 * 1549 * @return {Element} 1550 * The element with appended normalized content. 1551 */ 1552 function appendContent(el, content) { 1553 normalizeContent(content).forEach(node => el.appendChild(node)); 1554 return el; 1555 } 1556 1557 /** 1558 * Normalizes and inserts content into an element; this is identical to 1559 * `appendContent()`, except it empties the element first. 1560 * 1561 * @param {Element} el 1562 * Element to insert normalized content into. 1563 * 1564 * @param {ContentDescriptor} content 1565 * A content descriptor value. 1566 * 1567 * @return {Element} 1568 * The element with inserted normalized content. 1569 */ 1570 function insertContent(el, content) { 1571 return appendContent(emptyEl(el), content); 1572 } 1573 1574 /** 1575 * Check if an event was a single left click. 1576 * 1577 * @param {MouseEvent} event 1578 * Event object. 1579 * 1580 * @return {boolean} 1581 * Will be `true` if a single left click, `false` otherwise. 1582 */ 1583 function isSingleLeftClick(event) { 1584 // Note: if you create something draggable, be sure to 1585 // call it on both `mousedown` and `mousemove` event, 1586 // otherwise `mousedown` should be enough for a button 1587 1588 if (event.button === undefined && event.buttons === undefined) { 1589 // Why do we need `buttons` ? 1590 // Because, middle mouse sometimes have this: 1591 // e.button === 0 and e.buttons === 4 1592 // Furthermore, we want to prevent combination click, something like 1593 // HOLD middlemouse then left click, that would be 1594 // e.button === 0, e.buttons === 5 1595 // just `button` is not gonna work 1596 1597 // Alright, then what this block does ? 1598 // this is for chrome `simulate mobile devices` 1599 // I want to support this as well 1600 1601 return true; 1602 } 1603 if (event.button === 0 && event.buttons === undefined) { 1604 // Touch screen, sometimes on some specific device, `buttons` 1605 // doesn't have anything (safari on ios, blackberry...) 1606 1607 return true; 1608 } 1609 1610 // `mouseup` event on a single left click has 1611 // `button` and `buttons` equal to 0 1612 if (event.type === 'mouseup
vendor: 12,922 bytes, lines 1612-2043
1612' && event.button === 0 && event.buttons === 0) { 1613 return true; 1614 } 1615 if (event.button !== 0 || event.buttons !== 1) { 1616 // This is the reason we have those if else block above 1617 // if any special case we can catch and let it slide 1618 // we do it above, when get to here, this definitely 1619 // is-not-left-click 1620 1621 return false; 1622 } 1623 return true; 1624 } 1625 1626 /** 1627 * Finds a single DOM element matching `selector` within the optional 1628 * `context` of another DOM element (defaulting to `document`). 1629 * 1630 * @param {string} selector 1631 * A valid CSS selector, which will be passed to `querySelector`. 1632 * 1633 * @param {Element|String} [context=document] 1634 * A DOM element within which to query. Can also be a selector 1635 * string in which case the first matching element will be used 1636 * as context. If missing (or no element matches selector), falls 1637 * back to `document`. 1638 * 1639 * @return {Element|null} 1640 * The element that was found or null. 1641 */ 1642 const $ = createQuerier('querySelector'); 1643 1644 /** 1645 * Finds a all DOM elements matching `selector` within the optional 1646 * `context` of another DOM element (defaulting to `document`). 1647 * 1648 * @param {string} selector 1649 * A valid CSS selector, which will be passed to `querySelectorAll`. 1650 * 1651 * @param {Element|String} [context=document] 1652 * A DOM element within which to query. Can also be a selector 1653 * string in which case the first matching element will be used 1654 * as context. If missing (or no element matches selector), falls 1655 * back to `document`. 1656 * 1657 * @return {NodeList} 1658 * A element list of elements that were found. Will be empty if none 1659 * were found. 1660 * 1661 */ 1662 const $$ = createQuerier('querySelectorAll'); 1663 1664 /** 1665 * A safe getComputedStyle. 1666 * 1667 * This is needed because in Firefox, if the player is loaded in an iframe with 1668 * `display:none`, then `getComputedStyle` returns `null`, so, we do a 1669 * null-check to make sure that the player doesn't break in these cases. 1670 * 1671 * @param {Element} el 1672 * The element you want the computed style of 1673 * 1674 * @param {string} prop 1675 * The property name you want 1676 * 1677 * @see https://bugzilla.mozilla.org/show_bug.cgi?id=548397 1678 */ 1679 function computedStyle(el, prop) { 1680 if (!el || !prop) { 1681 return ''; 1682 } 1683 if (typeof window.getComputedStyle === 'function') { 1684 let computedStyleValue; 1685 try { 1686 computedStyleValue = window.getComputedStyle(el); 1687 } catch (e) { 1688 return ''; 1689 } 1690 return computedStyleValue ? computedStyleValue.getPropertyValue(prop) || computedStyleValue[prop] : ''; 1691 } 1692 return ''; 1693 } 1694 1695 /** 1696 * Copy document style sheets to another window. 1697 * 1698 * @param {Window} win 1699 * The window element you want to copy the document style sheets to. 1700 * 1701 */ 1702 function copyStyleSheetsToWindow(win) { 1703 [...document.styleSheets].forEach(styleSheet => { 1704 try { 1705 const cssRules = [...styleSheet.cssRules].map(rule => rule.cssText).join(''); 1706 const style = document.createElement('style'); 1707 style.textContent = cssRules; 1708 win.document.head.appendChild(style); 1709 } catch (e) { 1710 const link = document.createElement('link'); 1711 link.rel = 'stylesheet'; 1712 link.type = styleSheet.type; 1713 // For older Safari this has to be the string; on other browsers setting the MediaList works 1714 link.media = styleSheet.media.mediaText; 1715 link.href = styleSheet.href; 1716 win.document.head.appendChild(link); 1717 } 1718 }); 1719 } 1720 1721 var Dom = /*#__PURE__*/Object.freeze({ 1722 __proto__: null, 1723 isReal: isReal, 1724 isEl: isEl, 1725 isInFrame: isInFrame, 1726 createEl: createEl, 1727 textContent: textContent, 1728 prependTo: prependTo, 1729 hasClass: hasClass, 1730 addClass: addClass, 1731 removeClass: removeClass, 1732 toggleClass: toggleClass, 1733 setAttributes: setAttributes, 1734 getAttributes: getAttributes, 1735 getAttribute: getAttribute, 1736 setAttribute: setAttribute, 1737 removeAttribute: removeAttribute, 1738 blockTextSelection: blockTextSelection, 1739 unblockTextSelection: unblockTextSelection, 1740 getBoundingClientRect: getBoundingClientRect, 1741 findPosition: findPosition, 1742 getPointerPosition: getPointerPosition, 1743 isTextNode: isTextNode$1, 1744 emptyEl: emptyEl, 1745 normalizeContent: normalizeContent, 1746 appendContent: appendContent, 1747 insertContent: insertContent, 1748 isSingleLeftClick: isSingleLeftClick, 1749 $: $, 1750 $$: $$, 1751 computedStyle: computedStyle, 1752 copyStyleSheetsToWindow: copyStyleSheetsToWindow 1753 }); 1754 1755 /** 1756 * @file setup.js - Functions for setting up a player without 1757 * user interaction based on the data-setup `attribute` of the video tag. 1758 * 1759 * @module setup 1760 */ 1761 let _windowLoaded = false; 1762 let videojs$1; 1763 1764 /** 1765 * Set up any tags that have a data-setup `attribute` when the player is started. 1766 */ 1767 const autoSetup = function () { 1768 if (videojs$1.options.autoSetup === false) { 1769 return; 1770 } 1771 const vids = Array.prototype.slice.call(document.getElementsByTagName('video')); 1772 const audios = Array.prototype.slice.call(document.getElementsByTagName('audio')); 1773 const divs = Array.prototype.slice.call(document.getElementsByTagName('video-js')); 1774 const mediaEls = vids.concat(audios, divs); 1775 1776 // Check if any media elements exist 1777 if (mediaEls && mediaEls.length > 0) { 1778 for (let i = 0, e = mediaEls.length; i < e; i++) { 1779 const mediaEl = mediaEls[i]; 1780 1781 // Check if element exists, has getAttribute func. 1782 if (mediaEl && mediaEl.getAttribute) { 1783 // Make sure this player hasn't already been set up. 1784 if (mediaEl.player === undefined) { 1785 const options = mediaEl.getAttribute('data-setup'); 1786 1787 // Check if data-setup attr exists. 1788 // We only auto-setup if they've added the data-setup attr. 1789 if (options !== null) { 1790 // Create new video.js instance. 1791 videojs$1(mediaEl); 1792 } 1793 } 1794 1795 // If getAttribute isn't defined, we need to wait for the DOM. 1796 } else { 1797 autoSetupTimeout(1); 1798 break; 1799 } 1800 } 1801 1802 // No videos were found, so keep looping unless page is finished loading. 1803 } else if (!_windowLoaded) { 1804 autoSetupTimeout(1); 1805 } 1806 }; 1807 1808 /** 1809 * Wait until the page is loaded before running autoSetup. This will be called in 1810 * autoSetup if `hasLoaded` returns false. 1811 * 1812 * @param {number} wait 1813 * How long to wait in ms 1814 * 1815 * @param {module:videojs} [vjs] 1816 * The videojs library function 1817 */ 1818 function autoSetupTimeout(wait, vjs) { 1819 // Protect against breakage in non-browser environments 1820 if (!isReal()) { 1821 return; 1822 } 1823 if (vjs) { 1824 videojs$1 = vjs; 1825 } 1826 window.setTimeout(autoSetup, wait); 1827 } 1828 1829 /** 1830 * Used to set the internal tracking of window loaded state to true. 1831 * 1832 * @private 1833 */ 1834 function setWindowLoaded() { 1835 _windowLoaded = true; 1836 window.removeEventListener('load', setWindowLoaded); 1837 } 1838 if (isReal()) { 1839 if (document.readyState === 'complete') { 1840 setWindowLoaded(); 1841 } else { 1842 /** 1843 * Listen for the load event on window, and set _windowLoaded to true. 1844 * 1845 * We use a standard event listener here to avoid incrementing the GUID 1846 * before any players are created. 1847 * 1848 * @listens load 1849 */ 1850 window.addEventListener('load', setWindowLoaded); 1851 } 1852 } 1853 1854 /** 1855 * @file stylesheet.js 1856 * @module stylesheet 1857 */ 1858 1859 /** 1860 * Create a DOM style element given a className for it. 1861 * 1862 * @param {string} className 1863 * The className to add to the created style element. 1864 * 1865 * @return {Element} 1866 * The element that was created. 1867 */ 1868 const createStyleElement = function (className) { 1869 const style = document.createElement('style'); 1870 style.className = className; 1871 return style; 1872 }; 1873 1874 /** 1875 * Add text to a DOM element. 1876 * 1877 * @param {Element} el 1878 * The Element to add text content to. 1879 * 1880 * @param {string} content 1881 * The text to add to the element. 1882 */ 1883 const setTextContent = function (el, content) { 1884 if (el.styleSheet) { 1885 el.styleSheet.cssText = content; 1886 } else { 1887 el.textContent = content; 1888 } 1889 }; 1890 1891 /** 1892 * @file dom-data.js 1893 * @module dom-data 1894 */ 1895 1896 /** 1897 * Element Data Store. 1898 * 1899 * Allows for binding data to an element without putting it directly on the 1900 * element. Ex. Event listeners are stored here. 1901 * (also from jsninja.com, slightly modified and updated for closure compiler) 1902 * 1903 * @type {Object} 1904 * @private 1905 */ 1906 var DomData = new WeakMap(); 1907 1908 /** 1909 * @file guid.js 1910 * @module guid 1911 */ 1912 1913 // Default value for GUIDs. This allows us to reset the GUID counter in tests. 1914 // 1915 // The initial GUID is 3 because some users have come to rely on the first 1916 // default player ID ending up as `vjs_video_3`. 1917 // 1918 // See: https://github.com/videojs/video.js/pull/6216 1919 const _initialGuid = 3; 1920 1921 /** 1922 * Unique ID for an element or function 1923 * 1924 * @type {Number} 1925 */ 1926 let _guid = _initialGuid; 1927 1928 /** 1929 * Get a unique auto-incrementing ID by number that has not been returned before. 1930 * 1931 * @return {number} 1932 * A new unique ID. 1933 */ 1934 function newGUID() { 1935 return _guid++; 1936 } 1937 1938 /** 1939 * @file events.js. An Event System (John Resig - Secrets of a JS Ninja http://jsninja.com/) 1940 * (Original book version wasn't completely usable, so fixed some things and made Closure Compiler compatible) 1941 * This should work very similarly to jQuery's events, however it's based off the book version which isn't as 1942 * robust as jquery's, so there's probably some differences. 1943 * 1944 * @file events.js 1945 * @module events 1946 */ 1947 1948 /** 1949 * Clean up the listener cache and dispatchers 1950 * 1951 * @param {Element|Object} elem 1952 * Element to clean up 1953 * 1954 * @param {string} type 1955 * Type of event to clean up 1956 */ 1957 function _cleanUpEvents(elem, type) { 1958 if (!DomData.has(elem)) { 1959 return; 1960 } 1961 const data = DomData.get(elem); 1962 1963 // Remove the events of a particular type if there are none left 1964 if (data.handlers[type].length === 0) { 1965 delete data.handlers[type]; 1966 // data.handlers[type] = null; 1967 // Setting to null was causing an error with data.handlers 1968 1969 // Remove the meta-handler from the element 1970 if (elem.removeEventListener) { 1971 elem.removeEventListener(type, data.dispatcher, false); 1972 } else if (elem.detachEvent) { 1973 elem.detachEvent('on' + type, data.dispatcher); 1974 } 1975 } 1976 1977 // Remove the events object if there are no types left 1978 if (Object.getOwnPropertyNames(data.handlers).length <= 0) { 1979 delete data.handlers; 1980 delete data.dispatcher; 1981 delete data.disabled; 1982 } 1983 1984 // Finally remove the element data if there is no data left 1985 if (Object.getOwnPropertyNames(data).length === 0) { 1986 DomData.delete(elem); 1987 } 1988 } 1989 1990 /** 1991 * Loops through an array of event types and calls the requested method for each type. 1992 * 1993 * @param {Function} fn 1994 * The event method we want to use. 1995 * 1996 * @param {Element|Object} elem 1997 * Element or object to bind listeners to 1998 * 1999 * @param {string[]} types 2000 * Type of event to bind to. 2001 * 2002 * @param {Function} callback 2003 * Event listener. 2004 */ 2005 function _handleMultipleEvents(fn, elem, types, callback) { 2006 types.forEach(function (type) { 2007 // Call the event method for each one of the types 2008 fn(elem, type, callback); 2009 }); 2010 } 2011 2012 /** 2013 * Fix a native event to have standard property values 2014 * 2015 * @param {Object} event 2016 * Event object to fix. 2017 * 2018 * @return {Object} 2019 * Fixed event object. 2020 */ 2021 function fixEvent(event) { 2022 if (event.fixed_) { 2023 return event; 2024 } 2025 function returnTrue() { 2026 return true; 2027 } 2028 function returnFalse() { 2029 return false; 2030 } 2031 2032 // Test if fixing up is needed 2033 // Used to check if !event.stopPropagation instead of isPropagationStopped 2034 // But native events return true for stopPropagation, but don't have 2035 // other expected methods like isPropagationStopped. Seems to be a problem 2036 // with the Javascript Ninja code. So we're just overriding all events now. 2037 if (!event || !event.isPropagationStopped || !event.isImmediatePropagationStopped) { 2038 const old = event || window.event; 2039 event = {}; 2040 // Clone the old object so that we can modify the values event = {}; 2041 // IE8 Doesn't like when you mess with native event properties 2042 // Firefox returns false for event.hasOwnProperty('type') and other props 2043 // which makes copying more difficult.
2044 // TODO: Probably best to create a whitelist of event props 2045 for (const key in old) { 2046 // Safari 6.0.3 warns you if you try to copy deprecated layerX/Y 2047 // Chrome warns you if you try to copy deprecated keyboardEvent.keyLocation 2048 // and webkitMovementX/Y 2049 // Lighthouse complains if Event.path is copied 2050 if (key !== 'layerX' && key !== 'layerY' && key !== 'keyLocation' && key !== 'webkitMovementX' && key !== 'webkitMovementY' && key !== 'path') { 2051 // Chrome 32+ warns if you try to copy deprecated returnValue, but 2052 // we still want to if preventDefault isn't supported (IE8). 2053 if (!(key === 'returnValue' && old.preventDefault)) { 2054 event[key] = old[key]; 2055 } 2056 } 2057 } 2058 2059 // The event occurred on this element 2060 if (!event.target) { 2061 event.target = event.srcElement || document; 2062 } 2063 2064 // Handle which other element the event is related to 2065 if (!event.relatedTarget) { 2066 event.relatedTarget = event.fromElement === event.target ? event.toElement : event.fromElement; 2067 } 2068 2069 // Stop the default browser action 2070 event.preventDefault = function () { 2071 if (old.preventDefault) { 2072 old.preventDefault(); 2073 } 2074 event.returnValue = false;
vendor: 2,327 bytes, lines 2075-2142
2075 old.returnValue = false; 2076 event.defaultPrevented = true; 2077 }; 2078 event.defaultPrevented = false; 2079 2080 // Stop the event from bubbling 2081 event.stopPropagation = function () { 2082 if (old.stopPropagation) { 2083 old.stopPropagation(); 2084 } 2085 event.cancelBubble = true; 2086 old.cancelBubble = true; 2087 event.isPropagationStopped = returnTrue; 2088 }; 2089 event.isPropagationStopped = returnFalse; 2090 2091 // Stop the event from bubbling and executing other handlers 2092 event.stopImmediatePropagation = function () { 2093 if (old.stopImmediatePropagation) { 2094 old.stopImmediatePropagation(); 2095 } 2096 event.isImmediatePropagationStopped = returnTrue; 2097 event.stopPropagation(); 2098 }; 2099 event.isImmediatePropagationStopped = returnFalse; 2100 2101 // Handle mouse position 2102 if (event.clientX !== null && event.clientX !== undefined) { 2103 const doc = document.documentElement; 2104 const body = document.body; 2105 event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0); 2106 event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0); 2107 } 2108 2109 // Handle key presses 2110 event.which = event.charCode || event.keyCode; 2111 2112 // Fix button for mouse clicks: 2113 // 0 == left; 1 == middle; 2 == right 2114 if (event.button !== null && event.button !== undefined) { 2115 // The following is disabled because it does not pass videojs-standard 2116 // and... yikes. 2117 /* eslint-disable */ 2118 event.button = event.button & 1 ? 0 : event.button & 4 ? 1 : event.button & 2 ? 2 : 0; 2119 /* eslint-enable */ 2120 } 2121 } 2122 2123 event.fixed_ = true; 2124 // Returns fixed-up instance 2125 return event; 2126 } 2127 2128 /** 2129 * Whether passive event listeners are supported 2130 */ 2131 let _supportsPassive; 2132 const supportsPassive = function () { 2133 if (typeof _supportsPassive !== 'boolean') { 2134 _supportsPassive = false; 2135 try { 2136 const opts = Object.defineProperty({}, 'passive', { 2137 get() { 2138 _supportsPassive = true; 2139 } 2140 }); 2141 window.addEventListener('test', null, opts); 2142 window
vendor: 10,076 bytes, lines 2142-2490
2142.removeEventListener('test', null, opts); 2143 } catch (e) { 2144 // disregard 2145 } 2146 } 2147 return _supportsPassive; 2148 }; 2149 2150 /** 2151 * Touch events Chrome expects to be passive 2152 */ 2153 const passiveEvents = ['touchstart', 'touchmove']; 2154 2155 /** 2156 * Add an event listener to element 2157 * It stores the handler function in a separate cache object 2158 * and adds a generic handler to the element's event, 2159 * along with a unique id (guid) to the element. 2160 * 2161 * @param {Element|Object} elem 2162 * Element or object to bind listeners to 2163 * 2164 * @param {string|string[]} type 2165 * Type of event to bind to. 2166 * 2167 * @param {Function} fn 2168 * Event listener. 2169 */ 2170 function on(elem, type, fn) { 2171 if (Array.isArray(type)) { 2172 return _handleMultipleEvents(on, elem, type, fn); 2173 } 2174 if (!DomData.has(elem)) { 2175 DomData.set(elem, {}); 2176 } 2177 const data = DomData.get(elem); 2178 2179 // We need a place to store all our handler data 2180 if (!data.handlers) { 2181 data.handlers = {}; 2182 } 2183 if (!data.handlers[type]) { 2184 data.handlers[type] = []; 2185 } 2186 if (!fn.guid) { 2187 fn.guid = newGUID(); 2188 } 2189 data.handlers[type].push(fn); 2190 if (!data.dispatcher) { 2191 data.disabled = false; 2192 data.dispatcher = function (event, hash) { 2193 if (data.disabled) { 2194 return; 2195 } 2196 event = fixEvent(event); 2197 const handlers = data.handlers[event.type]; 2198 if (handlers) { 2199 // Copy handlers so if handlers are added/removed during the process it doesn't throw everything off. 2200 const handlersCopy = handlers.slice(0); 2201 for (let m = 0, n = handlersCopy.length; m < n; m++) { 2202 if (event.isImmediatePropagationStopped()) { 2203 break; 2204 } else { 2205 try { 2206 handlersCopy[m].call(elem, event, hash); 2207 } catch (e) { 2208 log$1.error(e); 2209 } 2210 } 2211 } 2212 } 2213 }; 2214 } 2215 if (data.handlers[type].length === 1) { 2216 if (elem.addEventListener) { 2217 let options = false; 2218 if (supportsPassive() && passiveEvents.indexOf(type) > -1) { 2219 options = { 2220 passive: true 2221 }; 2222 } 2223 elem.addEventListener(type, data.dispatcher, options); 2224 } else if (elem.attachEvent) { 2225 elem.attachEvent('on' + type, data.dispatcher); 2226 } 2227 } 2228 } 2229 2230 /** 2231 * Removes event listeners from an element 2232 * 2233 * @param {Element|Object} elem 2234 * Object to remove listeners from. 2235 * 2236 * @param {string|string[]} [type] 2237 * Type of listener to remove. Don't include to remove all events from element. 2238 * 2239 * @param {Function} [fn] 2240 * Specific listener to remove. Don't include to remove listeners for an event 2241 * type. 2242 */ 2243 function off(elem, type, fn) { 2244 // Don't want to add a cache object through getElData if not needed 2245 if (!DomData.has(elem)) { 2246 return; 2247 } 2248 const data = DomData.get(elem); 2249 2250 // If no events exist, nothing to unbind 2251 if (!data.handlers) { 2252 return; 2253 } 2254 if (Array.isArray(type)) { 2255 return _handleMultipleEvents(off, elem, type, fn); 2256 } 2257 2258 // Utility function 2259 const removeType = function (el, t) { 2260 data.handlers[t] = []; 2261 _cleanUpEvents(el, t); 2262 }; 2263 2264 // Are we removing all bound events? 2265 if (type === undefined) { 2266 for (const t in data.handlers) { 2267 if (Object.prototype.hasOwnProperty.call(data.handlers || {}, t)) { 2268 removeType(elem, t); 2269 } 2270 } 2271 return; 2272 } 2273 const handlers = data.handlers[type]; 2274 2275 // If no handlers exist, nothing to unbind 2276 if (!handlers) { 2277 return; 2278 } 2279 2280 // If no listener was provided, remove all listeners for type 2281 if (!fn) { 2282 removeType(elem, type); 2283 return; 2284 } 2285 2286 // We're only removing a single handler 2287 if (fn.guid) { 2288 for (let n = 0; n < handlers.length; n++) { 2289 if (handlers[n].guid === fn.guid) { 2290 handlers.splice(n--, 1); 2291 } 2292 } 2293 } 2294 _cleanUpEvents(elem, type); 2295 } 2296 2297 /** 2298 * Trigger an event for an element 2299 * 2300 * @param {Element|Object} elem 2301 * Element to trigger an event on 2302 * 2303 * @param {EventTarget~Event|string} event 2304 * A string (the type) or an event object with a type attribute 2305 * 2306 * @param {Object} [hash] 2307 * data hash to pass along with the event 2308 * 2309 * @return {boolean|undefined} 2310 * Returns the opposite of `defaultPrevented` if default was 2311 * prevented. Otherwise, returns `undefined` 2312 */ 2313 function trigger(elem, event, hash) { 2314 // Fetches element data and a reference to the parent (for bubbling). 2315 // Don't want to add a data object to cache for every parent, 2316 // so checking hasElData first. 2317 const elemData = DomData.has(elem) ? DomData.get(elem) : {}; 2318 const parent = elem.parentNode || elem.ownerDocument; 2319 // type = event.type || event, 2320 // handler; 2321 2322 // If an event name was passed as a string, creates an event out of it 2323 if (typeof event === 'string') { 2324 event = { 2325 type: event, 2326 target: elem 2327 }; 2328 } else if (!event.target) { 2329 event.target = elem; 2330 } 2331 2332 // Normalizes the event properties. 2333 event = fixEvent(event); 2334 2335 // If the passed element has a dispatcher, executes the established handlers. 2336 if (elemData.dispatcher) { 2337 elemData.dispatcher.call(elem, event, hash); 2338 } 2339 2340 // Unless explicitly stopped or the event does not bubble (e.g. media events) 2341 // recursively calls this function to bubble the event up the DOM. 2342 if (parent && !event.isPropagationStopped() && event.bubbles === true) { 2343 trigger.call(null, parent, event, hash); 2344 2345 // If at the top of the DOM, triggers the default action unless disabled. 2346 } else if (!parent && !event.defaultPrevented && event.target && event.target[event.type]) { 2347 if (!DomData.has(event.target)) { 2348 DomData.set(event.target, {}); 2349 } 2350 const targetData = DomData.get(event.target); 2351 2352 // Checks if the target has a default action for this event. 2353 if (event.target[event.type]) { 2354 // Temporarily disables event dispatching on the target as we have already executed the handler. 2355 targetData.disabled = true; 2356 // Executes the default action. 2357 if (typeof event.target[event.type] === 'function') { 2358 event.target[event.type](); 2359 } 2360 // Re-enables event dispatching. 2361 targetData.disabled = false; 2362 } 2363 } 2364 2365 // Inform the triggerer if the default was prevented by returning false 2366 return !event.defaultPrevented; 2367 } 2368 2369 /** 2370 * Trigger a listener only once for an event. 2371 * 2372 * @param {Element|Object} elem 2373 * Element or object to bind to. 2374 * 2375 * @param {string|string[]} type 2376 * Name/type of event 2377 * 2378 * @param {Event~EventListener} fn 2379 * Event listener function 2380 */ 2381 function one(elem, type, fn) { 2382 if (Array.isArray(type)) { 2383 return _handleMultipleEvents(one, elem, type, fn); 2384 } 2385 const func = function () { 2386 off(elem, type, func); 2387 fn.apply(this, arguments); 2388 }; 2389 2390 // copy the guid to the new function so it can removed using the original function's ID 2391 func.guid = fn.guid = fn.guid || newGUID(); 2392 on(elem, type, func); 2393 } 2394 2395 /** 2396 * Trigger a listener only once and then turn if off for all 2397 * configured events 2398 * 2399 * @param {Element|Object} elem 2400 * Element or object to bind to. 2401 * 2402 * @param {string|string[]} type 2403 * Name/type of event 2404 * 2405 * @param {Event~EventListener} fn 2406 * Event listener function 2407 */ 2408 function any(elem, type, fn) { 2409 const func = function () { 2410 off(elem, type, func); 2411 fn.apply(this, arguments); 2412 }; 2413 2414 // copy the guid to the new function so it can removed using the original function's ID 2415 func.guid = fn.guid = fn.guid || newGUID(); 2416 2417 // multiple ons, but one off for everything 2418 on(elem, type, func); 2419 } 2420 2421 var Events = /*#__PURE__*/Object.freeze({ 2422 __proto__: null, 2423 fixEvent: fixEvent, 2424 on: on, 2425 off: off, 2426 trigger: trigger, 2427 one: one, 2428 any: any 2429 }); 2430 2431 /** 2432 * @file fn.js 2433 * @module fn 2434 */ 2435 const UPDATE_REFRESH_INTERVAL = 30; 2436 2437 /** 2438 * A private, internal-only function for changing the context of a function. 2439 * 2440 * It also stores a unique id on the function so it can be easily removed from 2441 * events. 2442 * 2443 * @private 2444 * @function 2445 * @param {*} context 2446 * The object to bind as scope. 2447 * 2448 * @param {Function} fn 2449 * The function to be bound to a scope. 2450 * 2451 * @param {number} [uid] 2452 * An optional unique ID for the function to be set 2453 * 2454 * @return {Function} 2455 * The new function that will be bound into the context given 2456 */ 2457 const bind_ = function (context, fn, uid) { 2458 // Make sure the function has a unique ID 2459 if (!fn.guid) { 2460 fn.guid = newGUID(); 2461 } 2462 2463 // Create the new function that changes the context 2464 const bound = fn.bind(context); 2465 2466 // Allow for the ability to individualize this function 2467 // Needed in the case where multiple objects might share the same prototype 2468 // IF both items add an event listener with the same function, then you try to remove just one 2469 // it will remove both because they both have the same guid. 2470 // when using this, you need to use the bind method when you remove the listener as well. 2471 // currently used in text tracks 2472 bound.guid = uid ? uid + '_' + fn.guid : fn.guid; 2473 return bound; 2474 }; 2475 2476 /** 2477 * Wraps the given function, `fn`, with a new function that only invokes `fn` 2478 * at most once per every `wait` milliseconds. 2479 * 2480 * @function 2481 * @param {Function} fn 2482 * The function to be throttled. 2483 * 2484 * @param {number} wait 2485 * The number of milliseconds by which to throttle. 2486 * 2487 * @return {Function} 2488 */ 2489 const throttle = function (fn, wait) { 2490 let last = window
vendor: 2,592 bytes, lines 2490-2578
2490.performance.now(); 2491 const throttled = function (...args) { 2492 const now = window.performance.now(); 2493 if (now - last >= wait) { 2494 fn(...args); 2495 last = now; 2496 } 2497 }; 2498 return throttled; 2499 }; 2500 2501 /** 2502 * Creates a debounced function that delays invoking `func` until after `wait` 2503 * milliseconds have elapsed since the last time the debounced function was 2504 * invoked. 2505 * 2506 * Inspired by lodash and underscore implementations. 2507 * 2508 * @function 2509 * @param {Function} func 2510 * The function to wrap with debounce behavior. 2511 * 2512 * @param {number} wait 2513 * The number of milliseconds to wait after the last invocation. 2514 * 2515 * @param {boolean} [immediate] 2516 * Whether or not to invoke the function immediately upon creation. 2517 * 2518 * @param {Object} [context=window] 2519 * The "context" in which the debounced function should debounce. For 2520 * example, if this function should be tied to a Video.js player, 2521 * the player can be passed here. Alternatively, defaults to the 2522 * global `window` object. 2523 * 2524 * @return {Function} 2525 * A debounced function. 2526 */ 2527 const debounce = function (func, wait, immediate, context = window) { 2528 let timeout; 2529 const cancel = () => { 2530 context.clearTimeout(timeout); 2531 timeout = null; 2532 }; 2533 2534 /* eslint-disable consistent-this */ 2535 const debounced = function () { 2536 const self = this; 2537 const args = arguments; 2538 let later = function () { 2539 timeout = null; 2540 later = null; 2541 if (!immediate) { 2542 func.apply(self, args); 2543 } 2544 }; 2545 if (!timeout && immediate) { 2546 func.apply(self, args); 2547 } 2548 context.clearTimeout(timeout); 2549 timeout = context.setTimeout(later, wait); 2550 }; 2551 /* eslint-enable consistent-this */ 2552 2553 debounced.cancel = cancel; 2554 return debounced; 2555 }; 2556 2557 var Fn = /*#__PURE__*/Object.freeze({ 2558 __proto__: null, 2559 UPDATE_REFRESH_INTERVAL: UPDATE_REFRESH_INTERVAL, 2560 bind_: bind_, 2561 throttle: throttle, 2562 debounce: debounce 2563 }); 2564 2565 /** 2566 * @file src/js/event-target.js 2567 */ 2568 let EVENT_MAP; 2569 2570 /** 2571 * `EventTarget` is a class that can have the same API as the DOM `EventTarget`. It 2572 * adds shorthand functions that wrap around lengthy functions. For example: 2573 * the `on` function is a wrapper around `addEventListener`. 2574 * 2575 * @see [EventTarget Spec]{@link https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventTarget} 2576 * @class EventTarget 2577 */ 2578 class EventTarget$2
vendor: 4,713 bytes, lines 2578-2712
2578 { 2579 /** 2580 * Adds an `event listener` to an instance of an `EventTarget`. An `event listener` is a 2581 * function that will get called when an event with a certain name gets triggered. 2582 * 2583 * @param {string|string[]} type 2584 * An event name or an array of event names. 2585 * 2586 * @param {Function} fn 2587 * The function to call with `EventTarget`s 2588 */ 2589 on(type, fn) { 2590 // Remove the addEventListener alias before calling Events.on 2591 // so we don't get into an infinite type loop 2592 const ael = this.addEventListener; 2593 this.addEventListener = () => {}; 2594 on(this, type, fn); 2595 this.addEventListener = ael; 2596 } 2597 /** 2598 * Removes an `event listener` for a specific event from an instance of `EventTarget`. 2599 * This makes it so that the `event listener` will no longer get called when the 2600 * named event happens. 2601 * 2602 * @param {string|string[]} type 2603 * An event name or an array of event names. 2604 * 2605 * @param {Function} fn 2606 * The function to remove. 2607 */ 2608 off(type, fn) { 2609 off(this, type, fn); 2610 } 2611 /** 2612 * This function will add an `event listener` that gets triggered only once. After the 2613 * first trigger it will get removed. This is like adding an `event listener` 2614 * with {@link EventTarget#on} that calls {@link EventTarget#off} on itself. 2615 * 2616 * @param {string|string[]} type 2617 * An event name or an array of event names. 2618 * 2619 * @param {Function} fn 2620 * The function to be called once for each event name. 2621 */ 2622 one(type, fn) { 2623 // Remove the addEventListener aliasing Events.on 2624 // so we don't get into an infinite type loop 2625 const ael = this.addEventListener; 2626 this.addEventListener = () => {}; 2627 one(this, type, fn); 2628 this.addEventListener = ael; 2629 } 2630 /** 2631 * This function will add an `event listener` that gets triggered only once and is 2632 * removed from all events. This is like adding an array of `event listener`s 2633 * with {@link EventTarget#on} that calls {@link EventTarget#off} on all events the 2634 * first time it is triggered. 2635 * 2636 * @param {string|string[]} type 2637 * An event name or an array of event names. 2638 * 2639 * @param {Function} fn 2640 * The function to be called once for each event name. 2641 */ 2642 any(type, fn) { 2643 // Remove the addEventListener aliasing Events.on 2644 // so we don't get into an infinite type loop 2645 const ael = this.addEventListener; 2646 this.addEventListener = () => {}; 2647 any(this, type, fn); 2648 this.addEventListener = ael; 2649 } 2650 /** 2651 * This function causes an event to happen. This will then cause any `event listeners` 2652 * that are waiting for that event, to get called. If there are no `event listeners` 2653 * for an event then nothing will happen. 2654 * 2655 * If the name of the `Event` that is being triggered is in `EventTarget.allowedEvents_`. 2656 * Trigger will also call the `on` + `uppercaseEventName` function. 2657 * 2658 * Example: 2659 * 'click' is in `EventTarget.allowedEvents_`, so, trigger will attempt to call 2660 * `onClick` if it exists. 2661 * 2662 * @param {string|EventTarget~Event|Object} event 2663 * The name of the event, an `Event`, or an object with a key of type set to 2664 * an event name. 2665 */ 2666 trigger(event) { 2667 const type = event.type || event; 2668 2669 // deprecation 2670 // In a future version we should default target to `this` 2671 // similar to how we default the target to `elem` in 2672 // `Events.trigger`. Right now the default `target` will be 2673 // `document` due to the `Event.fixEvent` call. 2674 if (typeof event === 'string') { 2675 event = { 2676 type 2677 }; 2678 } 2679 event = fixEvent(event); 2680 if (this.allowedEvents_[type] && this['on' + type]) { 2681 this['on' + type](event); 2682 } 2683 trigger(this, event); 2684 } 2685 queueTrigger(event) { 2686 // only set up EVENT_MAP if it'll be used 2687 if (!EVENT_MAP) { 2688 EVENT_MAP = new Map(); 2689 } 2690 const type = event.type || event; 2691 let map = EVENT_MAP.get(this); 2692 if (!map) { 2693 map = new Map(); 2694 EVENT_MAP.set(this, map); 2695 } 2696 const oldTimeout = map.get(type); 2697 map.delete(type); 2698 window.clearTimeout(oldTimeout); 2699 const timeout = window.setTimeout(() => { 2700 map.delete(type); 2701 // if we cleared out all timeouts for the current target, delete its map 2702 if (map.size === 0) { 2703 map = null; 2704 EVENT_MAP.delete(this); 2705 } 2706 this.trigger(event); 2707 }, 0); 2708 map.set(type, timeout); 2709 } 2710 } 2711 2712 /**
vendor: 6,549 bytes, lines 2713-2936
2713 * A Custom DOM event. 2714 * 2715 * @typedef {CustomEvent} Event 2716 * @see [Properties]{@link https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent} 2717 */ 2718 2719 /** 2720 * All event listeners should follow the following format. 2721 * 2722 * @callback EventListener 2723 * @this {EventTarget} 2724 * 2725 * @param {Event} event 2726 * the event that triggered this function 2727 * 2728 * @param {Object} [hash] 2729 * hash of data sent during the event 2730 */ 2731 2732 /** 2733 * An object containing event names as keys and booleans as values. 2734 * 2735 * > NOTE: If an event name is set to a true value here {@link EventTarget#trigger} 2736 * will have extra functionality. See that function for more information. 2737 * 2738 * @property EventTarget.prototype.allowedEvents_ 2739 * @protected 2740 */ 2741 EventTarget$2.prototype.allowedEvents_ = {}; 2742 2743 /** 2744 * An alias of {@link EventTarget#on}. Allows `EventTarget` to mimic 2745 * the standard DOM API. 2746 * 2747 * @function 2748 * @see {@link EventTarget#on} 2749 */ 2750 EventTarget$2.prototype.addEventListener = EventTarget$2.prototype.on; 2751 2752 /** 2753 * An alias of {@link EventTarget#off}. Allows `EventTarget` to mimic 2754 * the standard DOM API. 2755 * 2756 * @function 2757 * @see {@link EventTarget#off} 2758 */ 2759 EventTarget$2.prototype.removeEventListener = EventTarget$2.prototype.off; 2760 2761 /** 2762 * An alias of {@link EventTarget#trigger}. Allows `EventTarget` to mimic 2763 * the standard DOM API. 2764 * 2765 * @function 2766 * @see {@link EventTarget#trigger} 2767 */ 2768 EventTarget$2.prototype.dispatchEvent = EventTarget$2.prototype.trigger; 2769 2770 /** 2771 * @file mixins/evented.js 2772 * @module evented 2773 */ 2774 const objName = obj => { 2775 if (typeof obj.name === 'function') { 2776 return obj.name(); 2777 } 2778 if (typeof obj.name === 'string') { 2779 return obj.name; 2780 } 2781 if (obj.name_) { 2782 return obj.name_; 2783 } 2784 if (obj.constructor && obj.constructor.name) { 2785 return obj.constructor.name; 2786 } 2787 return typeof obj; 2788 }; 2789 2790 /** 2791 * Returns whether or not an object has had the evented mixin applied. 2792 * 2793 * @param {Object} object 2794 * An object to test. 2795 * 2796 * @return {boolean} 2797 * Whether or not the object appears to be evented. 2798 */ 2799 const isEvented = object => object instanceof EventTarget$2 || !!object.eventBusEl_ && ['on', 'one', 'off', 'trigger'].every(k => typeof object[k] === 'function'); 2800 2801 /** 2802 * Adds a callback to run after the evented mixin applied. 2803 * 2804 * @param {Object} target 2805 * An object to Add 2806 * @param {Function} callback 2807 * The callback to run. 2808 */ 2809 const addEventedCallback = (target, callback) => { 2810 if (isEvented(target)) { 2811 callback(); 2812 } else { 2813 if (!target.eventedCallbacks) { 2814 target.eventedCallbacks = []; 2815 } 2816 target.eventedCallbacks.push(callback); 2817 } 2818 }; 2819 2820 /** 2821 * Whether a value is a valid event type - non-empty string or array. 2822 * 2823 * @private 2824 * @param {string|Array} type 2825 * The type value to test. 2826 * 2827 * @return {boolean} 2828 * Whether or not the type is a valid event type. 2829 */ 2830 const isValidEventType = type => 2831 // The regex here verifies that the `type` contains at least one non- 2832 // whitespace character. 2833 typeof type === 'string' && /\S/.test(type) || Array.isArray(type) && !!type.length; 2834 2835 /** 2836 * Validates a value to determine if it is a valid event target. Throws if not. 2837 * 2838 * @private 2839 * @throws {Error} 2840 * If the target does not appear to be a valid event target. 2841 * 2842 * @param {Object} target 2843 * The object to test. 2844 * 2845 * @param {Object} obj 2846 * The evented object we are validating for 2847 * 2848 * @param {string} fnName 2849 * The name of the evented mixin function that called this. 2850 */ 2851 const validateTarget = (target, obj, fnName) => { 2852 if (!target || !target.nodeName && !isEvented(target)) { 2853 throw new Error(`Invalid target for ${objName(obj)}#${fnName}; must be a DOM node or evented object.`); 2854 } 2855 }; 2856 2857 /** 2858 * Validates a value to determine if it is a valid event target. Throws if not. 2859 * 2860 * @private 2861 * @throws {Error} 2862 * If the type does not appear to be a valid event type. 2863 * 2864 * @param {string|Array} type 2865 * The type to test. 2866 * 2867 * @param {Object} obj 2868 * The evented object we are validating for 2869 * 2870 * @param {string} fnName 2871 * The name of the evented mixin function that called this. 2872 */ 2873 const validateEventType = (type, obj, fnName) => { 2874 if (!isValidEventType(type)) { 2875 throw new Error(`Invalid event type for ${objName(obj)}#${fnName}; must be a non-empty string or array.`); 2876 } 2877 }; 2878 2879 /** 2880 * Validates a value to determine if it is a valid listener. Throws if not. 2881 * 2882 * @private 2883 * @throws {Error} 2884 * If the listener is not a function. 2885 * 2886 * @param {Function} listener 2887 * The listener to test. 2888 * 2889 * @param {Object} obj 2890 * The evented object we are validating for 2891 * 2892 * @param {string} fnName 2893 * The name of the evented mixin function that called this. 2894 */ 2895 const validateListener = (listener, obj, fnName) => { 2896 if (typeof listener !== 'function') { 2897 throw new Error(`Invalid listener for ${objName(obj)}#${fnName}; must be a function.`); 2898 } 2899 }; 2900 2901 /** 2902 * Takes an array of arguments given to `on()` or `one()`, validates them, and 2903 * normalizes them into an object. 2904 * 2905 * @private 2906 * @param {Object} self 2907 * The evented object on which `on()` or `one()` was called. This 2908 * object will be bound as the `this` value for the listener. 2909 * 2910 * @param {Array} args 2911 * An array of arguments passed to `on()` or `one()`. 2912 * 2913 * @param {string} fnName 2914 * The name of the evented mixin function that called this. 2915 * 2916 * @return {Object} 2917 * An object containing useful values for `on()` or `one()` calls. 2918 */ 2919 const normalizeListenArgs = (self, args, fnName) => { 2920 // If the number of arguments is less than 3, the target is always the 2921 // evented object itself. 2922 const isTargetingSelf = args.length < 3 || args[0] === self || args[0] === self.eventBusEl_; 2923 let target; 2924 let type; 2925 let listener; 2926 if (isTargetingSelf) { 2927 target = self.eventBusEl_; 2928 2929 // Deal with cases where we got 3 arguments, but we are still listening to 2930 // the evented object itself. 2931 if (args.length >= 3) { 2932 args.shift(); 2933 } 2934 [type, listener] = args; 2935 } else { 2936
2936[target, type, listener] = args
vendor: 11,498 bytes, lines 2936-3256
2936; 2937 } 2938 validateTarget(target, self, fnName); 2939 validateEventType(type, self, fnName); 2940 validateListener(listener, self, fnName); 2941 listener = bind_(self, listener); 2942 return { 2943 isTargetingSelf, 2944 target, 2945 type, 2946 listener 2947 }; 2948 }; 2949 2950 /** 2951 * Adds the listener to the event type(s) on the target, normalizing for 2952 * the type of target. 2953 * 2954 * @private 2955 * @param {Element|Object} target 2956 * A DOM node or evented object. 2957 * 2958 * @param {string} method 2959 * The event binding method to use ("on" or "one"). 2960 * 2961 * @param {string|Array} type 2962 * One or more event type(s). 2963 * 2964 * @param {Function} listener 2965 * A listener function. 2966 */ 2967 const listen = (target, method, type, listener) => { 2968 validateTarget(target, target, method); 2969 if (target.nodeName) { 2970 Events[method](target, type, listener); 2971 } else { 2972 target[method](type, listener); 2973 } 2974 }; 2975 2976 /** 2977 * Contains methods that provide event capabilities to an object which is passed 2978 * to {@link module:evented|evented}. 2979 * 2980 * @mixin EventedMixin 2981 */ 2982 const EventedMixin = { 2983 /** 2984 * Add a listener to an event (or events) on this object or another evented 2985 * object. 2986 * 2987 * @param {string|Array|Element|Object} targetOrType 2988 * If this is a string or array, it represents the event type(s) 2989 * that will trigger the listener. 2990 * 2991 * Another evented object can be passed here instead, which will 2992 * cause the listener to listen for events on _that_ object. 2993 * 2994 * In either case, the listener's `this` value will be bound to 2995 * this object. 2996 * 2997 * @param {string|Array|Function} typeOrListener 2998 * If the first argument was a string or array, this should be the 2999 * listener function. Otherwise, this is a string or array of event 3000 * type(s). 3001 * 3002 * @param {Function} [listener] 3003 * If the first argument was another evented object, this will be 3004 * the listener function. 3005 */ 3006 on(...args) { 3007 const { 3008 isTargetingSelf, 3009 target, 3010 type, 3011 listener 3012 } = normalizeListenArgs(this, args, 'on'); 3013 listen(target, 'on', type, listener); 3014 3015 // If this object is listening to another evented object. 3016 if (!isTargetingSelf) { 3017 // If this object is disposed, remove the listener. 3018 const removeListenerOnDispose = () => this.off(target, type, listener); 3019 3020 // Use the same function ID as the listener so we can remove it later it 3021 // using the ID of the original listener. 3022 removeListenerOnDispose.guid = listener.guid; 3023 3024 // Add a listener to the target's dispose event as well. This ensures 3025 // that if the target is disposed BEFORE this object, we remove the 3026 // removal listener that was just added. Otherwise, we create a memory leak. 3027 const removeRemoverOnTargetDispose = () => this.off('dispose', removeListenerOnDispose); 3028 3029 // Use the same function ID as the listener so we can remove it later 3030 // it using the ID of the original listener. 3031 removeRemoverOnTargetDispose.guid = listener.guid; 3032 listen(this, 'on', 'dispose', removeListenerOnDispose); 3033 listen(target, 'on', 'dispose', removeRemoverOnTargetDispose); 3034 } 3035 }, 3036 /** 3037 * Add a listener to an event (or events) on this object or another evented 3038 * object. The listener will be called once per event and then removed. 3039 * 3040 * @param {string|Array|Element|Object} targetOrType 3041 * If this is a string or array, it represents the event type(s) 3042 * that will trigger the listener. 3043 * 3044 * Another evented object can be passed here instead, which will 3045 * cause the listener to listen for events on _that_ object. 3046 * 3047 * In either case, the listener's `this` value will be bound to 3048 * this object. 3049 * 3050 * @param {string|Array|Function} typeOrListener 3051 * If the first argument was a string or array, this should be the 3052 * listener function. Otherwise, this is a string or array of event 3053 * type(s). 3054 * 3055 * @param {Function} [listener] 3056 * If the first argument was another evented object, this will be 3057 * the listener function. 3058 */ 3059 one(...args) { 3060 const { 3061 isTargetingSelf, 3062 target, 3063 type, 3064 listener 3065 } = normalizeListenArgs(this, args, 'one'); 3066 3067 // Targeting this evented object. 3068 if (isTargetingSelf) { 3069 listen(target, 'one', type, listener); 3070 3071 // Targeting another evented object. 3072 } else { 3073 // TODO: This wrapper is incorrect! It should only 3074 // remove the wrapper for the event type that called it. 3075 // Instead all listeners are removed on the first trigger! 3076 // see https://github.com/videojs/video.js/issues/5962 3077 const wrapper = (...largs) => { 3078 this.off(target, type, wrapper); 3079 listener.apply(null, largs); 3080 }; 3081 3082 // Use the same function ID as the listener so we can remove it later 3083 // it using the ID of the original listener. 3084 wrapper.guid = listener.guid; 3085 listen(target, 'one', type, wrapper); 3086 } 3087 }, 3088 /** 3089 * Add a listener to an event (or events) on this object or another evented 3090 * object. The listener will only be called once for the first event that is triggered 3091 * then removed. 3092 * 3093 * @param {string|Array|Element|Object} targetOrType 3094 * If this is a string or array, it represents the event type(s) 3095 * that will trigger the listener. 3096 * 3097 * Another evented object can be passed here instead, which will 3098 * cause the listener to listen for events on _that_ object. 3099 * 3100 * In either case, the listener's `this` value will be bound to 3101 * this object. 3102 * 3103 * @param {string|Array|Function} typeOrListener 3104 * If the first argument was a string or array, this should be the 3105 * listener function. Otherwise, this is a string or array of event 3106 * type(s). 3107 * 3108 * @param {Function} [listener] 3109 * If the first argument was another evented object, this will be 3110 * the listener function. 3111 */ 3112 any(...args) { 3113 const { 3114 isTargetingSelf, 3115 target, 3116 type, 3117 listener 3118 } = normalizeListenArgs(this, args, 'any'); 3119 3120 // Targeting this evented object. 3121 if (isTargetingSelf) { 3122 listen(target, 'any', type, listener); 3123 3124 // Targeting another evented object. 3125 } else { 3126 const wrapper = (...largs) => { 3127 this.off(target, type, wrapper); 3128 listener.apply(null, largs); 3129 }; 3130 3131 // Use the same function ID as the listener so we can remove it later 3132 // it using the ID of the original listener. 3133 wrapper.guid = listener.guid; 3134 listen(target, 'any', type, wrapper); 3135 } 3136 }, 3137 /** 3138 * Removes listener(s) from event(s) on an evented object. 3139 * 3140 * @param {string|Array|Element|Object} [targetOrType] 3141 * If this is a string or array, it represents the event type(s). 3142 * 3143 * Another evented object can be passed here instead, in which case 3144 * ALL 3 arguments are _required_. 3145 * 3146 * @param {string|Array|Function} [typeOrListener] 3147 * If the first argument was a string or array, this may be the 3148 * listener function. Otherwise, this is a string or array of event 3149 * type(s). 3150 * 3151 * @param {Function} [listener] 3152 * If the first argument was another evented object, this will be 3153 * the listener function; otherwise, _all_ listeners bound to the 3154 * event type(s) will be removed. 3155 */ 3156 off(targetOrType, typeOrListener, listener) { 3157 // Targeting this evented object. 3158 if (!targetOrType || isValidEventType(targetOrType)) { 3159 off(this.eventBusEl_, targetOrType, typeOrListener); 3160 3161 // Targeting another evented object. 3162 } else { 3163 const target = targetOrType; 3164 const type = typeOrListener; 3165 3166 // Fail fast and in a meaningful way! 3167 validateTarget(target, this, 'off'); 3168 validateEventType(type, this, 'off'); 3169 validateListener(listener, this, 'off'); 3170 3171 // Ensure there's at least a guid, even if the function hasn't been used 3172 listener = bind_(this, listener); 3173 3174 // Remove the dispose listener on this evented object, which was given 3175 // the same guid as the event listener in on(). 3176 this.off('dispose', listener); 3177 if (target.nodeName) { 3178 off(target, type, listener); 3179 off(target, 'dispose', listener); 3180 } else if (isEvented(target)) { 3181 target.off(type, listener); 3182 target.off('dispose', listener); 3183 } 3184 } 3185 }, 3186 /** 3187 * Fire an event on this evented object, causing its listeners to be called. 3188 * 3189 * @param {string|Object} event 3190 * An event type or an object with a type property. 3191 * 3192 * @param {Object} [hash] 3193 * An additional object to pass along to listeners. 3194 * 3195 * @return {boolean} 3196 * Whether or not the default behavior was prevented. 3197 */ 3198 trigger(event, hash) { 3199 validateTarget(this.eventBusEl_, this, 'trigger'); 3200 const type = event && typeof event !== 'string' ? event.type : event; 3201 if (!isValidEventType(type)) { 3202 throw new Error(`Invalid event type for ${objName(this)}#trigger; ` + 'must be a non-empty string or object with a type key that has a non-empty value.'); 3203 } 3204 return trigger(this.eventBusEl_, event, hash); 3205 } 3206 }; 3207 3208 /** 3209 * Applies {@link module:evented~EventedMixin|EventedMixin} to a target object. 3210 * 3211 * @param {Object} target 3212 * The object to which to add event methods. 3213 * 3214 * @param {Object} [options={}] 3215 * Options for customizing the mixin behavior. 3216 * 3217 * @param {string} [options.eventBusKey] 3218 * By default, adds a `eventBusEl_` DOM element to the target object, 3219 * which is used as an event bus. If the target object already has a 3220 * DOM element that should be used, pass its key here. 3221 * 3222 * @return {Object} 3223 * The target object. 3224 */ 3225 function evented(target, options = {}) { 3226 const { 3227 eventBusKey 3228 } = options; 3229 3230 // Set or create the eventBusEl_. 3231 if (eventBusKey) { 3232 if (!target[eventBusKey].nodeName) { 3233 throw new Error(`The eventBusKey "${eventBusKey}" does not refer to an element.`); 3234 } 3235 target.eventBusEl_ = target[eventBusKey]; 3236 } else { 3237 target.eventBusEl_ = createEl('span', { 3238 className: 'vjs-event-bus' 3239 }); 3240 } 3241 Object.assign(target, EventedMixin); 3242 if (target.eventedCallbacks) { 3243 target.eventedCallbacks.forEach(callback => { 3244 callback(); 3245 }); 3246 } 3247 3248 // When any evented object is disposed, it removes all its listeners. 3249 target.on('dispose', () => { 3250 target.off(); 3251 [target, target.el_, target.eventBusEl_].forEach(function (val) { 3252 if (val && DomData.has(val)) { 3253 DomData.delete(val); 3254 } 3255 }); 3256 window
vendor: 5,486 bytes, lines 3256-3439
3256.setTimeout(() => { 3257 target.eventBusEl_ = null; 3258 }, 0); 3259 }); 3260 return target; 3261 } 3262 3263 /** 3264 * @file mixins/stateful.js 3265 * @module stateful 3266 */ 3267 3268 /** 3269 * Contains methods that provide statefulness to an object which is passed 3270 * to {@link module:stateful}. 3271 * 3272 * @mixin StatefulMixin 3273 */ 3274 const StatefulMixin = { 3275 /** 3276 * A hash containing arbitrary keys and values representing the state of 3277 * the object. 3278 * 3279 * @type {Object} 3280 */ 3281 state: {}, 3282 /** 3283 * Set the state of an object by mutating its 3284 * {@link module:stateful~StatefulMixin.state|state} object in place. 3285 * 3286 * @fires module:stateful~StatefulMixin#statechanged 3287 * @param {Object|Function} stateUpdates 3288 * A new set of properties to shallow-merge into the plugin state. 3289 * Can be a plain object or a function returning a plain object. 3290 * 3291 * @return {Object|undefined} 3292 * An object containing changes that occurred. If no changes 3293 * occurred, returns `undefined`. 3294 */ 3295 setState(stateUpdates) { 3296 // Support providing the `stateUpdates` state as a function. 3297 if (typeof stateUpdates === 'function') { 3298 stateUpdates = stateUpdates(); 3299 } 3300 let changes; 3301 each(stateUpdates, (value, key) => { 3302 // Record the change if the value is different from what's in the 3303 // current state. 3304 if (this.state[key] !== value) { 3305 changes = changes || {}; 3306 changes[key] = { 3307 from: this.state[key], 3308 to: value 3309 }; 3310 } 3311 this.state[key] = value; 3312 }); 3313 3314 // Only trigger "statechange" if there were changes AND we have a trigger 3315 // function. This allows us to not require that the target object be an 3316 // evented object. 3317 if (changes && isEvented(this)) { 3318 /** 3319 * An event triggered on an object that is both 3320 * {@link module:stateful|stateful} and {@link module:evented|evented} 3321 * indicating that its state has changed. 3322 * 3323 * @event module:stateful~StatefulMixin#statechanged 3324 * @type {Object} 3325 * @property {Object} changes 3326 * A hash containing the properties that were changed and 3327 * the values they were changed `from` and `to`. 3328 */ 3329 this.trigger({ 3330 changes, 3331 type: 'statechanged' 3332 }); 3333 } 3334 return changes; 3335 } 3336 }; 3337 3338 /** 3339 * Applies {@link module:stateful~StatefulMixin|StatefulMixin} to a target 3340 * object. 3341 * 3342 * If the target object is {@link module:evented|evented} and has a 3343 * `handleStateChanged` method, that method will be automatically bound to the 3344 * `statechanged` event on itself. 3345 * 3346 * @param {Object} target 3347 * The object to be made stateful. 3348 * 3349 * @param {Object} [defaultState] 3350 * A default set of properties to populate the newly-stateful object's 3351 * `state` property. 3352 * 3353 * @return {Object} 3354 * Returns the `target`. 3355 */ 3356 function stateful(target, defaultState) { 3357 Object.assign(target, StatefulMixin); 3358 3359 // This happens after the mixing-in because we need to replace the `state` 3360 // added in that step. 3361 target.state = Object.assign({}, target.state, defaultState); 3362 3363 // Auto-bind the `handleStateChanged` method of the target object if it exists. 3364 if (typeof target.handleStateChanged === 'function' && isEvented(target)) { 3365 target.on('statechanged', target.handleStateChanged); 3366 } 3367 return target; 3368 } 3369 3370 /** 3371 * @file str.js 3372 * @module to-lower-case 3373 */ 3374 3375 /** 3376 * Lowercase the first letter of a string. 3377 * 3378 * @param {string} string 3379 * String to be lowercased 3380 * 3381 * @return {string} 3382 * The string with a lowercased first letter 3383 */ 3384 const toLowerCase = function (string) { 3385 if (typeof string !== 'string') { 3386 return string; 3387 } 3388 return string.replace(/./, w => w.toLowerCase()); 3389 }; 3390 3391 /** 3392 * Uppercase the first letter of a string. 3393 * 3394 * @param {string} string 3395 * String to be uppercased 3396 * 3397 * @return {string} 3398 * The string with an uppercased first letter 3399 */ 3400 const toTitleCase$1 = function (string) { 3401 if (typeof string !== 'string') { 3402 return string; 3403 } 3404 return string.replace(/./, w => w.toUpperCase()); 3405 }; 3406 3407 /** 3408 * Compares the TitleCase versions of the two strings for equality. 3409 * 3410 * @param {string} str1 3411 * The first string to compare 3412 * 3413 * @param {string} str2 3414 * The second string to compare 3415 * 3416 * @return {boolean} 3417 * Whether the TitleCase versions of the strings are equal 3418 */ 3419 const titleCaseEquals = function (str1, str2) { 3420 return toTitleCase$1(str1) === toTitleCase$1(str2); 3421 }; 3422 3423 var Str = /*#__PURE__*/Object.freeze({ 3424 __proto__: null, 3425 toLowerCase: toLowerCase, 3426 toTitleCase: toTitleCase$1, 3427 titleCaseEquals: titleCaseEquals 3428 }); 3429 3430 var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; 3431 3432 function unwrapExports (x) { 3433 return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; 3434 } 3435 3436 function createCommonjsModule(fn, module) { 3437 return module = { exports: {} }, fn(module, module.exports), module.exports; 3438 } 3439
vendor: 4,223 bytes, lines 3440-3610
3440 var keycode = createCommonjsModule(function (module, exports) { 3441 // Source: http://jsfiddle.net/vWx8V/ 3442 // http://stackoverflow.com/questions/5603195/full-list-of-javascript-keycodes 3443 3444 /** 3445 * Conenience method returns corresponding value for given keyName or keyCode. 3446 * 3447 * @param {Mixed} keyCode {Number} or keyName {String} 3448 * @return {Mixed} 3449 * @api public 3450 */ 3451 3452 function keyCode(searchInput) { 3453 // Keyboard Events 3454 if (searchInput && 'object' === typeof searchInput) { 3455 var hasKeyCode = searchInput.which || searchInput.keyCode || searchInput.charCode; 3456 if (hasKeyCode) searchInput = hasKeyCode; 3457 } 3458 3459 // Numbers 3460 if ('number' === typeof searchInput) return names[searchInput]; 3461 3462 // Everything else (cast to string) 3463 var search = String(searchInput); 3464 3465 // check codes 3466 var foundNamedKey = codes[search.toLowerCase()]; 3467 if (foundNamedKey) return foundNamedKey; 3468 3469 // check aliases 3470 var foundNamedKey = aliases[search.toLowerCase()]; 3471 if (foundNamedKey) return foundNamedKey; 3472 3473 // weird character? 3474 if (search.length === 1) return search.charCodeAt(0); 3475 return undefined; 3476 } 3477 3478 /** 3479 * Compares a keyboard event with a given keyCode or keyName. 3480 * 3481 * @param {Event} event Keyboard event that should be tested 3482 * @param {Mixed} keyCode {Number} or keyName {String} 3483 * @return {Boolean} 3484 * @api public 3485 */ 3486 keyCode.isEventKey = function isEventKey(event, nameOrCode) { 3487 if (event && 'object' === typeof event) { 3488 var keyCode = event.which || event.keyCode || event.charCode; 3489 if (keyCode === null || keyCode === undefined) { 3490 return false; 3491 } 3492 if (typeof nameOrCode === 'string') { 3493 // check codes 3494 var foundNamedKey = codes[nameOrCode.toLowerCase()]; 3495 if (foundNamedKey) { 3496 return foundNamedKey === keyCode; 3497 } 3498 3499 // check aliases 3500 var foundNamedKey = aliases[nameOrCode.toLowerCase()]; 3501 if (foundNamedKey) { 3502 return foundNamedKey === keyCode; 3503 } 3504 } else if (typeof nameOrCode === 'number') { 3505 return nameOrCode === keyCode; 3506 } 3507 return false; 3508 } 3509 }; 3510 exports = module.exports = keyCode; 3511 3512 /** 3513 * Get by name 3514 * 3515 * exports.code['enter'] // => 13 3516 */ 3517 3518 var codes = exports.code = exports.codes = { 3519 'backspace': 8, 3520 'tab': 9, 3521 'enter': 13, 3522 'shift': 16, 3523 'ctrl': 17, 3524 'alt': 18, 3525 'pause/break': 19, 3526 'caps lock': 20, 3527 'esc': 27, 3528 'space': 32, 3529 'page up': 33, 3530 'page down': 34, 3531 'end': 35, 3532 'home': 36, 3533 'left': 37, 3534 'up': 38, 3535 'right': 39, 3536 'down': 40, 3537 'insert': 45, 3538 'delete': 46, 3539 'command': 91, 3540 'left command': 91, 3541 'right command': 93, 3542 'numpad *': 106, 3543 'numpad +': 107, 3544 'numpad -': 109, 3545 'numpad .': 110, 3546 'numpad /': 111, 3547 'num lock': 144, 3548 'scroll lock': 145, 3549 'my computer': 182, 3550 'my calculator': 183, 3551 ';': 186, 3552 '=': 187, 3553 ',': 188, 3554 '-': 189, 3555 '.': 190, 3556 '/': 191, 3557 '`': 192, 3558 '[': 219, 3559 '\\': 220, 3560 ']': 221, 3561 "'": 222 3562 }; 3563 3564 // Helper aliases 3565 3566 var aliases = exports.aliases = { 3567 'windows': 91, 3568 'â§': 16, 3569 'â¥': 18, 3570 'â': 17, 3571 'â': 91, 3572 'ctl': 17, 3573 'control': 17, 3574 'option': 18, 3575 'pause': 19, 3576 'break': 19, 3577 'caps': 20, 3578 'return': 13, 3579 'escape': 27, 3580 'spc': 32, 3581 'spacebar': 32, 3582 'pgup': 33, 3583 'pgdn': 34, 3584 'ins': 45, 3585 'del': 46, 3586 'cmd': 91 3587 }; 3588 3589 /*! 3590 * Programatically add the following 3591 */ 3592 3593 // lower case chars 3594 for (i = 97; i < 123; i++) codes[String.fromCharCode(i)] = i - 32; 3595 3596 // numbers 3597 for (var i = 48; i < 58; i++) codes[i - 48] = i; 3598 3599 // function keys 3600 for (i = 1; i < 13; i++) codes['f' + i] = i + 111; 3601 3602 // numpad keys 3603 for (i = 0; i < 10; i++) codes['numpad ' + i] = i + 96; 3604 3605 /** 3606 * Get by code 3607 * 3608 * exports.name[13] // => 'Enter' 3609 */ 3610
vendor: 4,217 bytes, lines 3611-3732
3611 var names = exports.names = exports.title = {}; // title for backward compat 3612 3613 // Create reverse mapping 3614 for (i in codes) names[codes[i]] = i; 3615 3616 // Add aliases 3617 for (var alias in aliases) { 3618 codes[alias] = aliases[alias]; 3619 } 3620 }); 3621 keycode.code; 3622 keycode.codes; 3623 keycode.aliases; 3624 keycode.names; 3625 keycode.title; 3626 3627 /** 3628 * Player Component - Base class for all UI objects 3629 * 3630 * @file component.js 3631 */ 3632 3633 /** 3634 * Base class for all UI Components. 3635 * Components are UI objects which represent both a javascript object and an element 3636 * in the DOM. They can be children of other components, and can have 3637 * children themselves. 3638 * 3639 * Components can also use methods from {@link EventTarget} 3640 */ 3641 class Component$1 { 3642 /** 3643 * A callback that is called when a component is ready. Does not have any 3644 * parameters and any callback value will be ignored. 3645 * 3646 * @callback ReadyCallback 3647 * @this Component 3648 */ 3649 3650 /** 3651 * Creates an instance of this class. 3652 * 3653 * @param { import('./player').default } player 3654 * The `Player` that this class should be attached to. 3655 * 3656 * @param {Object} [options] 3657 * The key/value store of component options. 3658 * 3659 * @param {Object[]} [options.children] 3660 * An array of children objects to initialize this component with. Children objects have 3661 * a name property that will be used if more than one component of the same type needs to be 3662 * added. 3663 * 3664 * @param {string} [options.className] 3665 * A class or space separated list of classes to add the component 3666 * 3667 * @param {ReadyCallback} [ready] 3668 * Function that gets called when the `Component` is ready. 3669 */ 3670 constructor(player, options, ready) { 3671 // The component might be the player itself and we can't pass `this` to super 3672 if (!player && this.play) { 3673 this.player_ = player = this; // eslint-disable-line 3674 } else { 3675 this.player_ = player; 3676 } 3677 this.isDisposed_ = false; 3678 3679 // Hold the reference to the parent component via `addChild` method 3680 this.parentComponent_ = null; 3681 3682 // Make a copy of prototype.options_ to protect against overriding defaults 3683 this.options_ = merge$2({}, this.options_); 3684 3685 // Updated options with supplied options 3686 options = this.options_ = merge$2(this.options_, options); 3687 3688 // Get ID from options or options element if one is supplied 3689 this.id_ = options.id || options.el && options.el.id; 3690 3691 // If there was no ID from the options, generate one 3692 if (!this.id_) { 3693 // Don't require the player ID function in the case of mock players 3694 const id = player && player.id && player.id() || 'no_player'; 3695 this.id_ = `${id}_component_${newGUID()}`; 3696 } 3697 this.name_ = options.name || null; 3698 3699 // Create element if one wasn't provided in options 3700 if (options.el) { 3701 this.el_ = options.el; 3702 } else if (options.createEl !== false) { 3703 this.el_ = this.createEl(); 3704 } 3705 if (options.className && this.el_) { 3706 options.className.split(' ').forEach(c => this.addClass(c)); 3707 } 3708 3709 // Remove the placeholder event methods. If the component is evented, the 3710 // real methods are added next 3711 ['on', 'off', 'one', 'any', 'trigger'].forEach(fn => { 3712 this[fn] = undefined; 3713 }); 3714 3715 // if evented is anything except false, we want to mixin in evented 3716 if (options.evented !== false) { 3717 // Make this an evented object and use `el_`, if available, as its event bus 3718 evented(this, { 3719 eventBusKey: this.el_ ? 'el_' : null 3720 }); 3721 this.handleLanguagechange = this.handleLanguagechange.bind(this); 3722 this.on(this.player_, 'languagechange', this.handleLanguagechange); 3723 } 3724 stateful(this, this.constructor.defaultState); 3725 this.children_ = []; 3726 this.childIndex_ = {}; 3727 this.childNameIndex_ = {}; 3728 this.setTimeoutIds_ = new Set(); 3729 this.setIntervalIds_ = new Set(); 3730 this.rafIds_ = new Set(); 3731 this.namedRafs_ = new Map(); 3732 this.clearingTimersOnDispose_ = false;
vendor: 5,328 bytes, lines 3733-3897
3733 3734 // Add any child components in options 3735 if (options.initChildren !== false) { 3736 this.initChildren(); 3737 } 3738 3739 // Don't want to trigger ready here or it will go before init is actually 3740 // finished for all children that run this constructor 3741 this.ready(ready); 3742 if (options.reportTouchActivity !== false) { 3743 this.enableTouchActivity(); 3744 } 3745 } 3746 3747 // `on`, `off`, `one`, `any` and `trigger` are here so tsc includes them in definitions. 3748 // They are replaced or removed in the constructor 3749 3750 /** 3751 * Adds an `event listener` to an instance of an `EventTarget`. An `event listener` is a 3752 * function that will get called when an event with a certain name gets triggered. 3753 * 3754 * @param {string|string[]} type 3755 * An event name or an array of event names. 3756 * 3757 * @param {Function} fn 3758 * The function to call with `EventTarget`s 3759 */ 3760 on(type, fn) {} 3761 3762 /** 3763 * Removes an `event listener` for a specific event from an instance of `EventTarget`. 3764 * This makes it so that the `event listener` will no longer get called when the 3765 * named event happens. 3766 * 3767 * @param {string|string[]} type 3768 * An event name or an array of event names. 3769 * 3770 * @param {Function} [fn] 3771 * The function to remove. If not specified, all listeners managed by Video.js will be removed. 3772 */ 3773 off(type, fn) {} 3774 3775 /** 3776 * This function will add an `event listener` that gets triggered only once. After the 3777 * first trigger it will get removed. This is like adding an `event listener` 3778 * with {@link EventTarget#on} that calls {@link EventTarget#off} on itself. 3779 * 3780 * @param {string|string[]} type 3781 * An event name or an array of event names. 3782 * 3783 * @param {Function} fn 3784 * The function to be called once for each event name. 3785 */ 3786 one(type, fn) {} 3787 3788 /** 3789 * This function will add an `event listener` that gets triggered only once and is 3790 * removed from all events. This is like adding an array of `event listener`s 3791 * with {@link EventTarget#on} that calls {@link EventTarget#off} on all events the 3792 * first time it is triggered. 3793 * 3794 * @param {string|string[]} type 3795 * An event name or an array of event names. 3796 * 3797 * @param {Function} fn 3798 * The function to be called once for each event name. 3799 */ 3800 any(type, fn) {} 3801 3802 /** 3803 * This function causes an event to happen. This will then cause any `event listeners` 3804 * that are waiting for that event, to get called. If there are no `event listeners` 3805 * for an event then nothing will happen. 3806 * 3807 * If the name of the `Event` that is being triggered is in `EventTarget.allowedEvents_`. 3808 * Trigger will also call the `on` + `uppercaseEventName` function. 3809 * 3810 * Example: 3811 * 'click' is in `EventTarget.allowedEvents_`, so, trigger will attempt to call 3812 * `onClick` if it exists. 3813 * 3814 * @param {string|Event|Object} event 3815 * The name of the event, an `Event`, or an object with a key of type set to 3816 * an event name. 3817 * 3818 * @param {Object} [hash] 3819 * Optionally extra argument to pass through to an event listener 3820 */ 3821 trigger(event, hash) {} 3822 3823 /** 3824 * Dispose of the `Component` and all child components. 3825 * 3826 * @fires Component#dispose 3827 * 3828 * @param {Object} options 3829 * @param {Element} options.originalEl element with which to replace player element 3830 */ 3831 dispose(options = {}) { 3832 // Bail out if the component has already been disposed. 3833 if (this.isDisposed_) { 3834 return; 3835 } 3836 if (this.readyQueue_) { 3837 this.readyQueue_.length = 0; 3838 } 3839 3840 /** 3841 * Triggered when a `Component` is disposed. 3842 * 3843 * @event Component#dispose 3844 * @type {Event} 3845 * 3846 * @property {boolean} [bubbles=false] 3847 * set to false so that the dispose event does not 3848 * bubble up 3849 */ 3850 this.trigger({ 3851 type: 'dispose', 3852 bubbles: false 3853 }); 3854 this.isDisposed_ = true; 3855 3856 // Dispose all children. 3857 if (this.children_) { 3858 for (let i = this.children_.length - 1; i >= 0; i--) { 3859 if (this.children_[i].dispose) { 3860 this.children_[i].dispose(); 3861 } 3862 } 3863 } 3864 3865 // Delete child references 3866 this.children_ = null; 3867 this.childIndex_ = null; 3868 this.childNameIndex_ = null; 3869 this.parentComponent_ = null; 3870 if (this.el_) { 3871 // Remove element from DOM 3872 if (this.el_.parentNode) { 3873 if (options.restoreEl) { 3874 this.el_.parentNode.replaceChild(options.restoreEl, this.el_); 3875 } else { 3876 this.el_.parentNode.removeChild(this.el_); 3877 } 3878 } 3879 this.el_ = null; 3880 } 3881 3882 // remove reference to the player after disposing of the element 3883 this.player_ = null; 3884 } 3885 3886 /** 3887 * Determine whether or not this component has been disposed. 3888 * 3889 * @return {boolean} 3890 * If the component has been disposed, will be `true`. Otherwise, `false`. 3891 */ 3892 isDisposed() { 3893 return Boolean(this.isDisposed_); 3894 } 3895 3896 /** 3897 * Return the {@link Player}
3897 that the `Component` has attached to. 3898 * 3899 * @return { import('./player').default
vendor: 12,133 bytes, lines 3899-4260
3899} 3900 * The player that this `Component` has attached to. 3901 */ 3902 player() { 3903 return this.player_; 3904 } 3905 3906 /** 3907 * Deep merge of options objects with new options. 3908 * > Note: When both `obj` and `options` contain properties whose values are objects. 3909 * The two properties get merged using {@link module:obj.merge} 3910 * 3911 * @param {Object} obj 3912 * The object that contains new options. 3913 * 3914 * @return {Object} 3915 * A new object of `this.options_` and `obj` merged together. 3916 */ 3917 options(obj) { 3918 if (!obj) { 3919 return this.options_; 3920 } 3921 this.options_ = merge$2(this.options_, obj); 3922 return this.options_; 3923 } 3924 3925 /** 3926 * Get the `Component`s DOM element 3927 * 3928 * @return {Element} 3929 * The DOM element for this `Component`. 3930 */ 3931 el() { 3932 return this.el_; 3933 } 3934 3935 /** 3936 * Create the `Component`s DOM element. 3937 * 3938 * @param {string} [tagName] 3939 * Element's DOM node type. e.g. 'div' 3940 * 3941 * @param {Object} [properties] 3942 * An object of properties that should be set. 3943 * 3944 * @param {Object} [attributes] 3945 * An object of attributes that should be set. 3946 * 3947 * @return {Element} 3948 * The element that gets created. 3949 */ 3950 createEl(tagName, properties, attributes) { 3951 return createEl(tagName, properties, attributes); 3952 } 3953 3954 /** 3955 * Localize a string given the string in english. 3956 * 3957 * If tokens are provided, it'll try and run a simple token replacement on the provided string. 3958 * The tokens it looks for look like `{1}` with the index being 1-indexed into the tokens array. 3959 * 3960 * If a `defaultValue` is provided, it'll use that over `string`, 3961 * if a value isn't found in provided language files. 3962 * This is useful if you want to have a descriptive key for token replacement 3963 * but have a succinct localized string and not require `en.json` to be included. 3964 * 3965 * Currently, it is used for the progress bar timing. 3966 * ```js 3967 * { 3968 * "progress bar timing: currentTime={1} duration={2}": "{1} of {2}" 3969 * } 3970 * ``` 3971 * It is then used like so: 3972 * ```js 3973 * this.localize('progress bar timing: currentTime={1} duration{2}', 3974 * [this.player_.currentTime(), this.player_.duration()], 3975 * '{1} of {2}'); 3976 * ``` 3977 * 3978 * Which outputs something like: `01:23 of 24:56`. 3979 * 3980 * 3981 * @param {string} string 3982 * The string to localize and the key to lookup in the language files. 3983 * @param {string[]} [tokens] 3984 * If the current item has token replacements, provide the tokens here. 3985 * @param {string} [defaultValue] 3986 * Defaults to `string`. Can be a default value to use for token replacement 3987 * if the lookup key is needed to be separate. 3988 * 3989 * @return {string} 3990 * The localized string or if no localization exists the english string. 3991 */ 3992 localize(string, tokens, defaultValue = string) { 3993 const code = this.player_.language && this.player_.language(); 3994 const languages = this.player_.languages && this.player_.languages(); 3995 const language = languages && languages[code]; 3996 const primaryCode = code && code.split('-')[0]; 3997 const primaryLang = languages && languages[primaryCode]; 3998 let localizedString = defaultValue; 3999 if (language && language[string]) { 4000 localizedString = language[string]; 4001 } else if (primaryLang && primaryLang[string]) { 4002 localizedString = primaryLang[string]; 4003 } 4004 if (tokens) { 4005 localizedString = localizedString.replace(/\{(\d+)\}/g, function (match, index) { 4006 const value = tokens[index - 1]; 4007 let ret = value; 4008 if (typeof value === 'undefined') { 4009 ret = match; 4010 } 4011 return ret; 4012 }); 4013 } 4014 return localizedString; 4015 } 4016 4017 /** 4018 * Handles language change for the player in components. Should be overridden by sub-components. 4019 * 4020 * @abstract 4021 */ 4022 handleLanguagechange() {} 4023 4024 /** 4025 * Return the `Component`s DOM element. This is where children get inserted. 4026 * This will usually be the the same as the element returned in {@link Component#el}. 4027 * 4028 * @return {Element} 4029 * The content element for this `Component`. 4030 */ 4031 contentEl() { 4032 return this.contentEl_ || this.el_; 4033 } 4034 4035 /** 4036 * Get this `Component`s ID 4037 * 4038 * @return {string} 4039 * The id of this `Component` 4040 */ 4041 id() { 4042 return this.id_; 4043 } 4044 4045 /** 4046 * Get the `Component`s name. The name gets used to reference the `Component` 4047 * and is set during registration. 4048 * 4049 * @return {string} 4050 * The name of this `Component`. 4051 */ 4052 name() { 4053 return this.name_; 4054 } 4055 4056 /** 4057 * Get an array of all child components 4058 * 4059 * @return {Array} 4060 * The children 4061 */ 4062 children() { 4063 return this.children_; 4064 } 4065 4066 /** 4067 * Returns the child `Component` with the given `id`. 4068 * 4069 * @param {string} id 4070 * The id of the child `Component` to get. 4071 * 4072 * @return {Component|undefined} 4073 * The child `Component` with the given `id` or undefined. 4074 */ 4075 getChildById(id) { 4076 return this.childIndex_[id]; 4077 } 4078 4079 /** 4080 * Returns the child `Component` with the given `name`. 4081 * 4082 * @param {string} name 4083 * The name of the child `Component` to get. 4084 * 4085 * @return {Component|undefined} 4086 * The child `Component` with the given `name` or undefined. 4087 */ 4088 getChild(name) { 4089 if (!name) { 4090 return; 4091 } 4092 return this.childNameIndex_[name]; 4093 } 4094 4095 /** 4096 * Returns the descendant `Component` following the givent 4097 * descendant `names`. For instance ['foo', 'bar', 'baz'] would 4098 * try to get 'foo' on the current component, 'bar' on the 'foo' 4099 * component and 'baz' on the 'bar' component and return undefined 4100 * if any of those don't exist. 4101 * 4102 * @param {...string[]|...string} names 4103 * The name of the child `Component` to get. 4104 * 4105 * @return {Component|undefined} 4106 * The descendant `Component` following the given descendant 4107 * `names` or undefined. 4108 */ 4109 getDescendant(...names) { 4110 // flatten array argument into the main array 4111 names = names.reduce((acc, n) => acc.concat(n), []); 4112 let currentChild = this; 4113 for (let i = 0; i < names.length; i++) { 4114 currentChild = currentChild.getChild(names[i]); 4115 if (!currentChild || !currentChild.getChild) { 4116 return; 4117 } 4118 } 4119 return currentChild; 4120 } 4121 4122 /** 4123 * Adds an SVG icon element to another element or component. 4124 * 4125 * @param {string} iconName 4126 * The name of icon. A list of all the icon names can be found at 'sandbox/svg-icons.html' 4127 * 4128 * @param {Element} [el=this.el()] 4129 * Element to set the title on. Defaults to the current Component's element. 4130 * 4131 * @return {Element} 4132 * The newly created icon element. 4133 */ 4134 setIcon(iconName, el = this.el()) { 4135 // TODO: In v9 of video.js, we will want to remove font icons entirely. 4136 // This means this check, as well as the others throughout the code, and 4137 // the unecessary CSS for font icons, will need to be removed. 4138 // See https://github.com/videojs/video.js/pull/8260 as to which components 4139 // need updating. 4140 if (!this.player_.options_.experimentalSvgIcons) { 4141 return; 4142 } 4143 const xmlnsURL = 'http://www.w3.org/2000/svg'; 4144 4145 // The below creates an element in the format of: 4146 // <span><svg><use>....</use></svg></span> 4147 const iconContainer = createEl('span', { 4148 className: 'vjs-icon-placeholder vjs-svg-icon' 4149 }, { 4150 'aria-hidden': 'true' 4151 }); 4152 const svgEl = document.createElementNS(xmlnsURL, 'svg'); 4153 svgEl.setAttributeNS(null, 'viewBox', '0 0 512 512'); 4154 const useEl = document.createElementNS(xmlnsURL, 'use'); 4155 svgEl.appendChild(useEl); 4156 useEl.setAttributeNS(null, 'href', `#vjs-icon-${iconName}`); 4157 iconContainer.appendChild(svgEl); 4158 4159 // Replace a pre-existing icon if one exists. 4160 if (this.iconIsSet_) { 4161 el.replaceChild(iconContainer, el.querySelector('.vjs-icon-placeholder')); 4162 } else { 4163 el.appendChild(iconContainer); 4164 } 4165 this.iconIsSet_ = true; 4166 return iconContainer; 4167 } 4168 4169 /** 4170 * Add a child `Component` inside the current `Component`. 4171 * 4172 * @param {string|Component} child 4173 * The name or instance of a child to add. 4174 * 4175 * @param {Object} [options={}] 4176 * The key/value store of options that will get passed to children of 4177 * the child. 4178 * 4179 * @param {number} [index=this.children_.length] 4180 * The index to attempt to add a child into. 4181 * 4182 * 4183 * @return {Component} 4184 * The `Component` that gets added as a child. When using a string the 4185 * `Component` will get created by this process. 4186 */ 4187 addChild(child, options = {}, index = this.children_.length) { 4188 let component; 4189 let componentName; 4190 4191 // If child is a string, create component with options 4192 if (typeof child === 'string') { 4193 componentName = toTitleCase$1(child); 4194 const componentClassName = options.componentClass || componentName; 4195 4196 // Set name through options 4197 options.name = componentName; 4198 4199 // Create a new object & element for this controls set 4200 // If there's no .player_, this is a player 4201 const ComponentClass = Component$1.getComponent(componentClassName); 4202 if (!ComponentClass) { 4203 throw new Error(`Component ${componentClassName} does not exist`); 4204 } 4205 4206 // data stored directly on the videojs object may be 4207 // misidentified as a component to retain 4208 // backwards-compatibility with 4.x. check to make sure the 4209 // component class can be instantiated. 4210 if (typeof ComponentClass !== 'function') { 4211 return null; 4212 } 4213 component = new ComponentClass(this.player_ || this, options); 4214 4215 // child is a component instance 4216 } else { 4217 component = child; 4218 } 4219 if (component.parentComponent_) { 4220 component.parentComponent_.removeChild(component); 4221 } 4222 this.children_.splice(index, 0, component); 4223 component.parentComponent_ = this; 4224 if (typeof component.id === 'function') { 4225 this.childIndex_[component.id()] = component; 4226 } 4227 4228 // If a name wasn't used to create the component, check if we can use the 4229 // name function of the component 4230 componentName = componentName || component.name && toTitleCase$1(component.name()); 4231 if (componentName) { 4232 this.childNameIndex_[componentName] = component; 4233 this.childNameIndex_[toLowerCase(componentName)] = component; 4234 } 4235 4236 // Add the UI object's element to the container div (box) 4237 // Having an element is not required 4238 if (typeof component.el === 'function' && component.el()) { 4239 // If inserting before a component, insert before that component's element 4240 let refNode = null; 4241 if (this.children_[index + 1]) { 4242 // Most children are components, but the video tech is an HTML element 4243 if (this.children_[index + 1].el_) { 4244 refNode = this.children_[index + 1].el_; 4245 } else if (isEl(this.children_[index + 1])) { 4246 refNode = this.children_[index + 1]; 4247 } 4248 } 4249 this.contentEl().insertBefore(component.el(), refNode); 4250 } 4251 4252 // Return so it can stored on parent object if desired. 4253 return component; 4254 } 4255 4256 /** 4257 * Remove a child `Component` from this `Component`s list of children. Also removes 4258 * the child `Component`s element from this `Component`s element. 4259 * 4260 * @param {
vendor: 9,854 bytes, lines 4260-4561
4260Component} component 4261 * The child `Component` to remove. 4262 */ 4263 removeChild(component) { 4264 if (typeof component === 'string') { 4265 component = this.getChild(component); 4266 } 4267 if (!component || !this.children_) { 4268 return; 4269 } 4270 let childFound = false; 4271 for (let i = this.children_.length - 1; i >= 0; i--) { 4272 if (this.children_[i] === component) { 4273 childFound = true; 4274 this.children_.splice(i, 1); 4275 break; 4276 } 4277 } 4278 if (!childFound) { 4279 return; 4280 } 4281 component.parentComponent_ = null; 4282 this.childIndex_[component.id()] = null; 4283 this.childNameIndex_[toTitleCase$1(component.name())] = null; 4284 this.childNameIndex_[toLowerCase(component.name())] = null; 4285 const compEl = component.el(); 4286 if (compEl && compEl.parentNode === this.contentEl()) { 4287 this.contentEl().removeChild(component.el()); 4288 } 4289 } 4290 4291 /** 4292 * Add and initialize default child `Component`s based upon options. 4293 */ 4294 initChildren() { 4295 const children = this.options_.children; 4296 if (children) { 4297 // `this` is `parent` 4298 const parentOptions = this.options_; 4299 const handleAdd = child => { 4300 const name = child.name; 4301 let opts = child.opts; 4302 4303 // Allow options for children to be set at the parent options 4304 // e.g. videojs(id, { controlBar: false }); 4305 // instead of videojs(id, { children: { controlBar: false }); 4306 if (parentOptions[name] !== undefined) { 4307 opts = parentOptions[name]; 4308 } 4309 4310 // Allow for disabling default components 4311 // e.g. options['children']['posterImage'] = false 4312 if (opts === false) { 4313 return; 4314 } 4315 4316 // Allow options to be passed as a simple boolean if no configuration 4317 // is necessary. 4318 if (opts === true) { 4319 opts = {}; 4320 } 4321 4322 // We also want to pass the original player options 4323 // to each component as well so they don't need to 4324 // reach back into the player for options later. 4325 opts.playerOptions = this.options_.playerOptions; 4326 4327 // Create and add the child component. 4328 // Add a direct reference to the child by name on the parent instance. 4329 // If two of the same component are used, different names should be supplied 4330 // for each 4331 const newChild = this.addChild(name, opts); 4332 if (newChild) { 4333 this[name] = newChild; 4334 } 4335 }; 4336 4337 // Allow for an array of children details to passed in the options 4338 let workingChildren; 4339 const Tech = Component$1.getComponent('Tech'); 4340 if (Array.isArray(children)) { 4341 workingChildren = children; 4342 } else { 4343 workingChildren = Object.keys(children); 4344 } 4345 workingChildren 4346 // children that are in this.options_ but also in workingChildren would 4347 // give us extra children we do not want. So, we want to filter them out. 4348 .concat(Object.keys(this.options_).filter(function (child) { 4349 return !workingChildren.some(function (wchild) { 4350 if (typeof wchild === 'string') { 4351 return child === wchild; 4352 } 4353 return child === wchild.name; 4354 }); 4355 })).map(child => { 4356 let name; 4357 let opts; 4358 if (typeof child === 'string') { 4359 name = child; 4360 opts = children[name] || this.options_[name] || {}; 4361 } else { 4362 name = child.name; 4363 opts = child; 4364 } 4365 return { 4366 name, 4367 opts 4368 }; 4369 }).filter(child => { 4370 // we have to make sure that child.name isn't in the techOrder since 4371 // techs are registered as Components but can't aren't compatible 4372 // See https://github.com/videojs/video.js/issues/2772 4373 const c = Component$1.getComponent(child.opts.componentClass || toTitleCase$1(child.name)); 4374 return c && !Tech.isTech(c); 4375 }).forEach(handleAdd); 4376 } 4377 } 4378 4379 /** 4380 * Builds the default DOM class name. Should be overridden by sub-components. 4381 * 4382 * @return {string} 4383 * The DOM class name for this object. 4384 * 4385 * @abstract 4386 */ 4387 buildCSSClass() { 4388 // Child classes can include a function that does: 4389 // return 'CLASS NAME' + this._super(); 4390 return ''; 4391 } 4392 4393 /** 4394 * Bind a listener to the component's ready state. 4395 * Different from event listeners in that if the ready event has already happened 4396 * it will trigger the function immediately. 4397 * 4398 * @param {ReadyCallback} fn 4399 * Function that gets called when the `Component` is ready. 4400 * 4401 * @return {Component} 4402 * Returns itself; method can be chained. 4403 */ 4404 ready(fn, sync = false) { 4405 if (!fn) { 4406 return; 4407 } 4408 if (!this.isReady_) { 4409 this.readyQueue_ = this.readyQueue_ || []; 4410 this.readyQueue_.push(fn); 4411 return; 4412 } 4413 if (sync) { 4414 fn.call(this); 4415 } else { 4416 // Call the function asynchronously by default for consistency 4417 this.setTimeout(fn, 1); 4418 } 4419 } 4420 4421 /** 4422 * Trigger all the ready listeners for this `Component`. 4423 * 4424 * @fires Component#ready 4425 */ 4426 triggerReady() { 4427 this.isReady_ = true; 4428 4429 // Ensure ready is triggered asynchronously 4430 this.setTimeout(function () { 4431 const readyQueue = this.readyQueue_; 4432 4433 // Reset Ready Queue 4434 this.readyQueue_ = []; 4435 if (readyQueue && readyQueue.length > 0) { 4436 readyQueue.forEach(function (fn) { 4437 fn.call(this); 4438 }, this); 4439 } 4440 4441 // Allow for using event listeners also 4442 /** 4443 * Triggered when a `Component` is ready. 4444 * 4445 * @event Component#ready 4446 * @type {Event} 4447 */ 4448 this.trigger('ready'); 4449 }, 1); 4450 } 4451 4452 /** 4453 * Find a single DOM element matching a `selector`. This can be within the `Component`s 4454 * `contentEl()` or another custom context. 4455 * 4456 * @param {string} selector 4457 * A valid CSS selector, which will be passed to `querySelector`. 4458 * 4459 * @param {Element|string} [context=this.contentEl()] 4460 * A DOM element within which to query. Can also be a selector string in 4461 * which case the first matching element will get used as context. If 4462 * missing `this.contentEl()` gets used. If `this.contentEl()` returns 4463 * nothing it falls back to `document`. 4464 * 4465 * @return {Element|null} 4466 * the dom element that was found, or null 4467 * 4468 * @see [Information on CSS Selectors](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors) 4469 */ 4470 $(selector, context) { 4471 return $(selector, context || this.contentEl()); 4472 } 4473 4474 /** 4475 * Finds all DOM element matching a `selector`. This can be within the `Component`s 4476 * `contentEl()` or another custom context. 4477 * 4478 * @param {string} selector 4479 * A valid CSS selector, which will be passed to `querySelectorAll`. 4480 * 4481 * @param {Element|string} [context=this.contentEl()] 4482 * A DOM element within which to query. Can also be a selector string in 4483 * which case the first matching element will get used as context. If 4484 * missing `this.contentEl()` gets used. If `this.contentEl()` returns 4485 * nothing it falls back to `document`. 4486 * 4487 * @return {NodeList} 4488 * a list of dom elements that were found 4489 * 4490 * @see [Information on CSS Selectors](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors) 4491 */ 4492 $$(selector, context) { 4493 return $$(selector, context || this.contentEl()); 4494 } 4495 4496 /** 4497 * Check if a component's element has a CSS class name. 4498 * 4499 * @param {string} classToCheck 4500 * CSS class name to check. 4501 * 4502 * @return {boolean} 4503 * - True if the `Component` has the class. 4504 * - False if the `Component` does not have the class` 4505 */ 4506 hasClass(classToCheck) { 4507 return hasClass(this.el_, classToCheck); 4508 } 4509 4510 /** 4511 * Add a CSS class name to the `Component`s element. 4512 * 4513 * @param {...string} classesToAdd 4514 * One or more CSS class name to add. 4515 */ 4516 addClass(...classesToAdd) { 4517 addClass(this.el_, ...classesToAdd); 4518 } 4519 4520 /** 4521 * Remove a CSS class name from the `Component`s element. 4522 * 4523 * @param {...string} classesToRemove 4524 * One or more CSS class name to remove. 4525 */ 4526 removeClass(...classesToRemove) { 4527 removeClass(this.el_, ...classesToRemove); 4528 } 4529 4530 /** 4531 * Add or remove a CSS class name from the component's element. 4532 * - `classToToggle` gets added when {@link Component#hasClass} would return false. 4533 * - `classToToggle` gets removed when {@link Component#hasClass} would return true. 4534 * 4535 * @param {string} classToToggle 4536 * The class to add or remove based on (@link Component#hasClass} 4537 * 4538 * @param {boolean|Dom~predicate} [predicate] 4539 * An {@link Dom~predicate} function or a boolean 4540 */ 4541 toggleClass(classToToggle, predicate) { 4542 toggleClass(this.el_, classToToggle, predicate); 4543 } 4544 4545 /** 4546 * Show the `Component`s element if it is hidden by removing the 4547 * 'vjs-hidden' class name from it. 4548 */ 4549 show() { 4550 this.removeClass('vjs-hidden'); 4551 } 4552 4553 /** 4554 * Hide the `Component`s element if it is currently showing by adding the 4555 * 'vjs-hidden` class name to it. 4556 */ 4557 hide() { 4558 this.addClass('vjs-hidden'); 4559 } 4560 4561 /**
4562 * Lock a `Component`s element in its visible state by adding the 'vjs-lock-showing' 4563 * class name to it. Used during fadeIn/fadeOut. 4564 * 4565 * @private 4566 */ 4567 lockShowing() { 4568 this.addClass('vjs-lock-showing'); 4569 } 4570 4571 /** 4572 * Unlock a `Component`s element from its visible state by removing the 'vjs-lock-showing' 4573 * class name from it. Used during fadeIn/fadeOut. 4574 * 4575 * @private 4576 */ 4577 unlockShowing() { 4578 this.removeClass('vjs-lock-showing'); 4579 } 4580 4581 /** 4582 * Get the value of an attribute on the `Component`s element. 4583 * 4584 * @param {string} attribute 4585 * Name of the attribute to get the value from. 4586 * 4587 * @return {string|null} 4588 * - The value of the attribute that was asked for. 4589 * - Can be an empty string on some browsers if the attribute does not exist 4590 * or has no value 4591 * - Most browsers will return null if the attribute does not exist or has 4592 * no value. 4593 * 4594 * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute} 4595 */ 4596 getAttribute(attribute) { 4597 return getAttribute(this.el_, attribute); 4598 } 4599 4600 /** 4601 * Set the value of an attribute on the `Component`'s element 4602 * 4603 * @param {string} attribute 4604 * Name of the attribute to set. 4605 * 4606 * @param {string} value 4607 * Value to set the attribute to. 4608 * 4609 * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute} 4610 */ 4611 setAttribute(attribute, value) { 4612 setAttribute(this.el_, attribute, value); 4613 } 4614 4615 /** 4616 * Remove an attribute from the `Component`s element. 4617 * 4618 * @param {string} attribute 4619 * Name of the attribute to remove. 4620 * 4621 * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/removeAttribute} 4622 */ 4623 removeAttribute(attribute) { 4624 removeAttribute(this.el_, attribute); 4625 } 4626 4627 /** 4628 * Get or set the width of the component based upon the CSS styles. 4629 * See {@link Component#dimension} for more detailed information. 4630 * 4631 * @param {number|string} [num] 4632 * The width that you want to set postfixed with '%', 'px' or nothing. 4633 * 4634 * @param {boolean} [skipListeners] 4635 * Skip the componentresize event trigger 4636 * 4637 * @return {number|undefined} 4638 * The width when getting, zero if there is no width 4639 */ 4640 width(num, skipListeners) { 4641 return this.dimension('width', num, skipListeners); 4642 } 4643 4644 /** 4645 * Get or set the height of the component based upon the CSS styles. 4646 * See {@link Component#dimension} for more detailed information. 4647 * 4648 * @param {number|string} [num] 4649 * The height that you want to set postfixed with '%', 'px' or nothing. 4650 * 4651 * @param {boolean} [skipListeners] 4652 * Skip the componentresize event trigger 4653 * 4654 * @return {number|undefined} 4655 * The height when getting, zero if there is no height 4656 */ 4657 height(num, skipListeners) { 4658 return this.dimension('height', num, skipListeners); 4659 } 4660 4661 /** 4662 * Set both the width and height of the `Component` element at the same time. 4663 * 4664 * @param {number|string} width 4665 * Width to set the `Component`s element to. 4666 * 4667 * @param {number|string} height 4668 * Height to set the `Component`s element to. 4669 */ 4670 dimensions(width, height) { 4671 // Skip componentresize listeners on width for optimization 4672 this.width(width, true); 4673 this.height(height); 4674 } 4675 4676 /** 4677 * Get or set width or height of the `Component` element. This is the shared code 4678 * for the {@link Component#width} and {@link Component#height}. 4679 * 4680 * Things to know: 4681 * - If the width or height in an number this will return the number postfixed with 'px'. 4682 * - If the width/height is a percent this will return the percent postfixed with '%' 4683 * - Hidden elements have a width of 0 with `window.getComputedStyle`. This function 4684 * defaults to the `Component`s `style.width` and falls back to `window.getComputedStyle`. 4685 * See [this]{@link http://www.foliotek.com/devblog/getting-the-width-of-a-hidden-element-with-jquery-u
vendor: 21,506 bytes, lines 4685-5272
4685sing-width/} 4686 * for more information 4687 * - If you want the computed style of the component, use {@link Component#currentWidth} 4688 * and {@link {Component#currentHeight} 4689 * 4690 * @fires Component#componentresize 4691 * 4692 * @param {string} widthOrHeight 4693 8 'width' or 'height' 4694 * 4695 * @param {number|string} [num] 4696 8 New dimension 4697 * 4698 * @param {boolean} [skipListeners] 4699 * Skip componentresize event trigger 4700 * 4701 * @return {number|undefined} 4702 * The dimension when getting or 0 if unset 4703 */ 4704 dimension(widthOrHeight, num, skipListeners) { 4705 if (num !== undefined) { 4706 // Set to zero if null or literally NaN (NaN !== NaN) 4707 if (num === null || num !== num) { 4708 num = 0; 4709 } 4710 4711 // Check if using css width/height (% or px) and adjust 4712 if (('' + num).indexOf('%') !== -1 || ('' + num).indexOf('px') !== -1) { 4713 this.el_.style[widthOrHeight] = num; 4714 } else if (num === 'auto') { 4715 this.el_.style[widthOrHeight] = ''; 4716 } else { 4717 this.el_.style[widthOrHeight] = num + 'px'; 4718 } 4719 4720 // skipListeners allows us to avoid triggering the resize event when setting both width and height 4721 if (!skipListeners) { 4722 /** 4723 * Triggered when a component is resized. 4724 * 4725 * @event Component#componentresize 4726 * @type {Event} 4727 */ 4728 this.trigger('componentresize'); 4729 } 4730 return; 4731 } 4732 4733 // Not setting a value, so getting it 4734 // Make sure element exists 4735 if (!this.el_) { 4736 return 0; 4737 } 4738 4739 // Get dimension value from style 4740 const val = this.el_.style[widthOrHeight]; 4741 const pxIndex = val.indexOf('px'); 4742 if (pxIndex !== -1) { 4743 // Return the pixel value with no 'px' 4744 return parseInt(val.slice(0, pxIndex), 10); 4745 } 4746 4747 // No px so using % or no style was set, so falling back to offsetWidth/height 4748 // If component has display:none, offset will return 0 4749 // TODO: handle display:none and no dimension style using px 4750 return parseInt(this.el_['offset' + toTitleCase$1(widthOrHeight)], 10); 4751 } 4752 4753 /** 4754 * Get the computed width or the height of the component's element. 4755 * 4756 * Uses `window.getComputedStyle`. 4757 * 4758 * @param {string} widthOrHeight 4759 * A string containing 'width' or 'height'. Whichever one you want to get. 4760 * 4761 * @return {number} 4762 * The dimension that gets asked for or 0 if nothing was set 4763 * for that dimension. 4764 */ 4765 currentDimension(widthOrHeight) { 4766 let computedWidthOrHeight = 0; 4767 if (widthOrHeight !== 'width' && widthOrHeight !== 'height') { 4768 throw new Error('currentDimension only accepts width or height value'); 4769 } 4770 computedWidthOrHeight = computedStyle(this.el_, widthOrHeight); 4771 4772 // remove 'px' from variable and parse as integer 4773 computedWidthOrHeight = parseFloat(computedWidthOrHeight); 4774 4775 // if the computed value is still 0, it's possible that the browser is lying 4776 // and we want to check the offset values. 4777 // This code also runs wherever getComputedStyle doesn't exist. 4778 if (computedWidthOrHeight === 0 || isNaN(computedWidthOrHeight)) { 4779 const rule = `offset${toTitleCase$1(widthOrHeight)}`; 4780 computedWidthOrHeight = this.el_[rule]; 4781 } 4782 return computedWidthOrHeight; 4783 } 4784 4785 /** 4786 * An object that contains width and height values of the `Component`s 4787 * computed style. Uses `window.getComputedStyle`. 4788 * 4789 * @typedef {Object} Component~DimensionObject 4790 * 4791 * @property {number} width 4792 * The width of the `Component`s computed style. 4793 * 4794 * @property {number} height 4795 * The height of the `Component`s computed style. 4796 */ 4797 4798 /** 4799 * Get an object that contains computed width and height values of the 4800 * component's element. 4801 * 4802 * Uses `window.getComputedStyle`. 4803 * 4804 * @return {Component~DimensionObject} 4805 * The computed dimensions of the component's element. 4806 */ 4807 currentDimensions() { 4808 return { 4809 width: this.currentDimension('width'), 4810 height: this.currentDimension('height') 4811 }; 4812 } 4813 4814 /** 4815 * Get the computed width of the component's element. 4816 * 4817 * Uses `window.getComputedStyle`. 4818 * 4819 * @return {number} 4820 * The computed width of the component's element. 4821 */ 4822 currentWidth() { 4823 return this.currentDimension('width'); 4824 } 4825 4826 /** 4827 * Get the computed height of the component's element. 4828 * 4829 * Uses `window.getComputedStyle`. 4830 * 4831 * @return {number} 4832 * The computed height of the component's element. 4833 */ 4834 currentHeight() { 4835 return this.currentDimension('height'); 4836 } 4837 4838 /** 4839 * Set the focus to this component 4840 */ 4841 focus() { 4842 this.el_.focus(); 4843 } 4844 4845 /** 4846 * Remove the focus from this component 4847 */ 4848 blur() { 4849 this.el_.blur(); 4850 } 4851 4852 /** 4853 * When this Component receives a `keydown` event which it does not process, 4854 * it passes the event to the Player for handling. 4855 * 4856 * @param {KeyboardEvent} event 4857 * The `keydown` event that caused this function to be called. 4858 */ 4859 handleKeyDown(event) { 4860 if (this.player_) { 4861 // We only stop propagation here because we want unhandled events to fall 4862 // back to the browser. Exclude Tab for focus trapping. 4863 if (!keycode.isEventKey(event, 'Tab')) { 4864 event.stopPropagation(); 4865 } 4866 this.player_.handleKeyDown(event); 4867 } 4868 } 4869 4870 /** 4871 * Many components used to have a `handleKeyPress` method, which was poorly 4872 * named because it listened to a `keydown` event. This method name now 4873 * delegates to `handleKeyDown`. This means anyone calling `handleKeyPress` 4874 * will not see their method calls stop working. 4875 * 4876 * @param {KeyboardEvent} event 4877 * The event that caused this function to be called. 4878 */ 4879 handleKeyPress(event) { 4880 this.handleKeyDown(event); 4881 } 4882 4883 /** 4884 * Emit a 'tap' events when touch event support gets detected. This gets used to 4885 * support toggling the controls through a tap on the video. They get enabled 4886 * because every sub-component would have extra overhead otherwise. 4887 * 4888 * @protected 4889 * @fires Component#tap 4890 * @listens Component#touchstart 4891 * @listens Component#touchmove 4892 * @listens Component#touchleave 4893 * @listens Component#touchcancel 4894 * @listens Component#touchend 4895 */ 4896 emitTapEvents() { 4897 // Track the start time so we can determine how long the touch lasted 4898 let touchStart = 0; 4899 let firstTouch = null; 4900 4901 // Maximum movement allowed during a touch event to still be considered a tap 4902 // Other popular libs use anywhere from 2 (hammer.js) to 15, 4903 // so 10 seems like a nice, round number. 4904 const tapMovementThreshold = 10; 4905 4906 // The maximum length a touch can be while still being considered a tap 4907 const touchTimeThreshold = 200; 4908 let couldBeTap; 4909 this.on('touchstart', function (event) { 4910 // If more than one finger, don't consider treating this as a click 4911 if (event.touches.length === 1) { 4912 // Copy pageX/pageY from the object 4913 firstTouch = { 4914 pageX: event.touches[0].pageX, 4915 pageY: event.touches[0].pageY 4916 }; 4917 // Record start time so we can detect a tap vs. "touch and hold" 4918 touchStart = window.performance.now(); 4919 // Reset couldBeTap tracking 4920 couldBeTap = true; 4921 } 4922 }); 4923 this.on('touchmove', function (event) { 4924 // If more than one finger, don't consider treating this as a click 4925 if (event.touches.length > 1) { 4926 couldBeTap = false; 4927 } else if (firstTouch) { 4928 // Some devices will throw touchmoves for all but the slightest of taps. 4929 // So, if we moved only a small distance, this could still be a tap 4930 const xdiff = event.touches[0].pageX - firstTouch.pageX; 4931 const ydiff = event.touches[0].pageY - firstTouch.pageY; 4932 const touchDistance = Math.sqrt(xdiff * xdiff + ydiff * ydiff); 4933 if (touchDistance > tapMovementThreshold) { 4934 couldBeTap = false; 4935 } 4936 } 4937 }); 4938 const noTap = function () { 4939 couldBeTap = false; 4940 }; 4941 4942 // TODO: Listen to the original target. http://youtu.be/DujfpXOKUp8?t=13m8s 4943 this.on('touchleave', noTap); 4944 this.on('touchcancel', noTap); 4945 4946 // When the touch ends, measure how long it took and trigger the appropriate 4947 // event 4948 this.on('touchend', function (event) { 4949 firstTouch = null; 4950 // Proceed only if the touchmove/leave/cancel event didn't happen 4951 if (couldBeTap === true) { 4952 // Measure how long the touch lasted 4953 const touchTime = window.performance.now() - touchStart; 4954 4955 // Make sure the touch was less than the threshold to be considered a tap 4956 if (touchTime < touchTimeThreshold) { 4957 // Don't let browser turn this into a click 4958 event.preventDefault(); 4959 /** 4960 * Triggered when a `Component` is tapped. 4961 * 4962 * @event Component#tap 4963 * @type {MouseEvent} 4964 */ 4965 this.trigger('tap'); 4966 // It may be good to copy the touchend event object and change the 4967 // type to tap, if the other event properties aren't exact after 4968 // Events.fixEvent runs (e.g. event.target) 4969 } 4970 } 4971 }); 4972 } 4973 4974 /** 4975 * This function reports user activity whenever touch events happen. This can get 4976 * turned off by any sub-components that wants touch events to act another way. 4977 * 4978 * Report user touch activity when touch events occur. User activity gets used to 4979 * determine when controls should show/hide. It is simple when it comes to mouse 4980 * events, because any mouse event should show the controls. So we capture mouse 4981 * events that bubble up to the player and report activity when that happens. 4982 * With touch events it isn't as easy as `touchstart` and `touchend` toggle player 4983 * controls. So touch events can't help us at the player level either. 4984 * 4985 * User activity gets checked asynchronously. So what could happen is a tap event 4986 * on the video turns the controls off. Then the `touchend` event bubbles up to 4987 * the player. Which, if it reported user activity, would turn the controls right 4988 * back on. We also don't want to completely block touch events from bubbling up. 4989 * Furthermore a `touchmove` event and anything other than a tap, should not turn 4990 * controls back on. 4991 * 4992 * @listens Component#touchstart 4993 * @listens Component#touchmove 4994 * @listens Component#touchend 4995 * @listens Component#touchcancel 4996 */ 4997 enableTouchActivity() { 4998 // Don't continue if the root player doesn't support reporting user activity 4999 if (!this.player() || !this.player().reportUserActivity) { 5000 return; 5001 } 5002 5003 // listener for reporting that the user is active 5004 const report = bind_(this.player(), this.player().reportUserActivity); 5005 let touchHolding; 5006 this.on('touchstart', function () { 5007 report(); 5008 // For as long as the they are touching the device or have their mouse down, 5009 // we consider them active even if they're not moving their finger or mouse. 5010 // So we want to continue to update that they are active 5011 this.clearInterval(touchHolding); 5012 // report at the same interval as activityCheck 5013 touchHolding = this.setInterval(report, 250); 5014 }); 5015 const touchEnd = function (event) { 5016 report(); 5017 // stop the interval that maintains activity if the touch is holding 5018 this.clearInterval(touchHolding); 5019 }; 5020 this.on('touchmove', report); 5021 this.on('touchend', touchEnd); 5022 this.on('touchcancel', touchEnd); 5023 } 5024 5025 /** 5026 * A callback that has no parameters and is bound into `Component`s context. 5027 * 5028 * @callback Component~GenericCallback 5029 * @this Component 5030 */ 5031 5032 /** 5033 * Creates a function that runs after an `x` millisecond timeout. This function is a 5034 * wrapper around `window.setTimeout`. There are a few reasons to use this one 5035 * instead though: 5036 * 1. It gets cleared via {@link Component#clearTimeout} when 5037 * {@link Component#dispose} gets called. 5038 * 2. The function callback will gets turned into a {@link Component~GenericCallback} 5039 * 5040 * > Note: You can't use `window.clearTimeout` on the id returned by this function. This 5041 * will cause its dispose listener not to get cleaned up! Please use 5042 * {@link Component#clearTimeout} or {@link Component#dispose} instead. 5043 * 5044 * @param {Component~GenericCallback} fn 5045 * The function that will be run after `timeout`. 5046 * 5047 * @param {number} timeout 5048 * Timeout in milliseconds to delay before executing the specified function. 5049 * 5050 * @return {number} 5051 * Returns a timeout ID that gets used to identify the timeout. It can also 5052 * get used in {@link Component#clearTimeout} to clear the timeout that 5053 * was set. 5054 * 5055 * @listens Component#dispose 5056 * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setTimeout} 5057 */ 5058 setTimeout(fn, timeout) { 5059 // declare as variables so they are properly available in timeout function 5060 // eslint-disable-next-line 5061 var timeoutId; 5062 fn = bind_(this, fn); 5063 this.clearTimersOnDispose_(); 5064 timeoutId = window.setTimeout(() => { 5065 if (this.setTimeoutIds_.has(timeoutId)) { 5066 this.setTimeoutIds_.delete(timeoutId); 5067 } 5068 fn(); 5069 }, timeout); 5070 this.setTimeoutIds_.add(timeoutId); 5071 return timeoutId; 5072 } 5073 5074 /** 5075 * Clears a timeout that gets created via `window.setTimeout` or 5076 * {@link Component#setTimeout}. If you set a timeout via {@link Component#setTimeout} 5077 * use this function instead of `window.clearTimout`. If you don't your dispose 5078 * listener will not get cleaned up until {@link Component#dispose}! 5079 * 5080 * @param {number} timeoutId 5081 * The id of the timeout to clear. The return value of 5082 * {@link Component#setTimeout} or `window.setTimeout`. 5083 * 5084 * @return {number} 5085 * Returns the timeout id that was cleared. 5086 * 5087 * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearTimeout} 5088 */ 5089 clearTimeout(timeoutId) { 5090 if (this.setTimeoutIds_.has(timeoutId)) { 5091 this.setTimeoutIds_.delete(timeoutId); 5092 window.clearTimeout(timeoutId); 5093 } 5094 return timeoutId; 5095 } 5096 5097 /** 5098 * Creates a function that gets run every `x` milliseconds. This function is a wrapper 5099 * around `window.setInterval`. There are a few reasons to use this one instead though. 5100 * 1. It gets cleared via {@link Component#clearInterval} when 5101 * {@link Component#dispose} gets called. 5102 * 2. The function callback will be a {@link Component~GenericCallback} 5103 * 5104 * @param {Component~GenericCallback} fn 5105 * The function to run every `x` seconds. 5106 * 5107 * @param {number} interval 5108 * Execute the specified function every `x` milliseconds. 5109 * 5110 * @return {number} 5111 * Returns an id that can be used to identify the interval. It can also be be used in 5112 * {@link Component#clearInterval} to clear the interval. 5113 * 5114 * @listens Component#dispose 5115 * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setInterval} 5116 */ 5117 setInterval(fn, interval) { 5118 fn = bind_(this, fn); 5119 this.clearTimersOnDispose_(); 5120 const intervalId = window.setInterval(fn, interval); 5121 this.setIntervalIds_.add(intervalId); 5122 return intervalId; 5123 } 5124 5125 /** 5126 * Clears an interval that gets created via `window.setInterval` or 5127 * {@link Component#setInterval}. If you set an interval via {@link Component#setInterval} 5128 * use this function instead of `window.clearInterval`. If you don't your dispose 5129 * listener will not get cleaned up until {@link Component#dispose}! 5130 * 5131 * @param {number} intervalId 5132 * The id of the interval to clear. The return value of 5133 * {@link Component#setInterval} or `window.setInterval`. 5134 * 5135 * @return {number} 5136 * Returns the interval id that was cleared. 5137 * 5138 * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearInterval} 5139 */ 5140 clearInterval(intervalId) { 5141 if (this.setIntervalIds_.has(intervalId)) { 5142 this.setIntervalIds_.delete(intervalId); 5143 window.clearInterval(intervalId); 5144 } 5145 return intervalId; 5146 } 5147 5148 /** 5149 * Queues up a callback to be passed to requestAnimationFrame (rAF), but 5150 * with a few extra bonuses: 5151 * 5152 * - Supports browsers that do not support rAF by falling back to 5153 * {@link Component#setTimeout}. 5154 * 5155 * - The callback is turned into a {@link Component~GenericCallback} (i.e. 5156 * bound to the component). 5157 * 5158 * - Automatic cancellation of the rAF callback is handled if the component 5159 * is disposed before it is called. 5160 * 5161 * @param {Component~GenericCallback} fn 5162 * A function that will be bound to this component and executed just 5163 * before the browser's next repaint. 5164 * 5165 * @return {number} 5166 * Returns an rAF ID that gets used to identify the timeout. It can 5167 * also be used in {@link Component#cancelAnimationFrame} to cancel 5168 * the animation frame callback. 5169 * 5170 * @listens Component#dispose 5171 * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame} 5172 */ 5173 requestAnimationFrame(fn) { 5174 this.clearTimersOnDispose_(); 5175 5176 // declare as variables so they are properly available in rAF function 5177 // eslint-disable-next-line 5178 var id; 5179 fn = bind_(this, fn); 5180 id = window.requestAnimationFrame(() => { 5181 if (this.rafIds_.has(id)) { 5182 this.rafIds_.delete(id); 5183 } 5184 fn(); 5185 }); 5186 this.rafIds_.add(id); 5187 return id; 5188 } 5189 5190 /** 5191 * Request an animation frame, but only one named animation 5192 * frame will be queued. Another will never be added until 5193 * the previous one finishes. 5194 * 5195 * @param {string} name 5196 * The name to give this requestAnimationFrame 5197 * 5198 * @param {Component~GenericCallback} fn 5199 * A function that will be bound to this component and executed just 5200 * before the browser's next repaint. 5201 */ 5202 requestNamedAnimationFrame(name, fn) { 5203 if (this.namedRafs_.has(name)) { 5204 return; 5205 } 5206 this.clearTimersOnDispose_(); 5207 fn = bind_(this, fn); 5208 const id = this.requestAnimationFrame(() => { 5209 fn(); 5210 if (this.namedRafs_.has(name)) { 5211 this.namedRafs_.delete(name); 5212 } 5213 }); 5214 this.namedRafs_.set(name, id); 5215 return name; 5216 } 5217 5218 /** 5219 * Cancels a current named animation frame if it exists. 5220 * 5221 * @param {string} name 5222 * The name of the requestAnimationFrame to cancel. 5223 */ 5224 cancelNamedAnimationFrame(name) { 5225 if (!this.namedRafs_.has(name)) { 5226 return; 5227 } 5228 this.cancelAnimationFrame(this.namedRafs_.get(name)); 5229 this.namedRafs_.delete(name); 5230 } 5231 5232 /** 5233 * Cancels a queued callback passed to {@link Component#requestAnimationFrame} 5234 * (rAF). 5235 * 5236 * If you queue an rAF callback via {@link Component#requestAnimationFrame}, 5237 * use this function instead of `window.cancelAnimationFrame`. If you don't, 5238 * your dispose listener will not get cleaned up until {@link Component#dispose}! 5239 * 5240 * @param {number} id 5241 * The rAF ID to clear. The return value of {@link Component#requestAnimationFrame}. 5242 * 5243 * @return {number} 5244 * Returns the rAF ID that was cleared. 5245 * 5246 * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/cancelAnimationFrame} 5247 */ 5248 cancelAnimationFrame(id) { 5249 if (this.rafIds_.has(id)) { 5250 this.rafIds_.delete(id); 5251 window.cancelAnimationFrame(id); 5252 } 5253 return id; 5254 } 5255 5256 /** 5257 * A function to setup `requestAnimationFrame`, `setTimeout`, 5258 * and `setInterval`, clearing on dispose. 5259 * 5260 * > Previously each timer added and removed dispose listeners on it's own. 5261 * For better performance it was decided to batch them all, and use `Set`s 5262 * to track outstanding timer ids. 5263 * 5264 * @private 5265 */ 5266 clearTimersOnDispose_() { 5267 if (this.clearingTimersOnDispose_) { 5268 return; 5269 } 5270 this.clearingTimersOnDispose_ = true; 5271 this.one('dispose', () => { 5272 [['namedRafs_', 'cancelNamedAnimationFrame'], ['rafIds_', 'cancelAnimationFrame'], ['setTimeoutIds_', 'clearTimeout'], ['setIntervalIds_', 'clearInterval']].forEac
vendor: 8,709 bytes, lines 5272-5529
5272h(([idName, cancelName]) => { 5273 // for a `Set` key will actually be the value again 5274 // so forEach((val, val) =>` but for maps we want to use 5275 // the key. 5276 this[idName].forEach((val, key) => this[cancelName](key)); 5277 }); 5278 this.clearingTimersOnDispose_ = false; 5279 }); 5280 } 5281 5282 /** 5283 * Register a `Component` with `videojs` given the name and the component. 5284 * 5285 * > NOTE: {@link Tech}s should not be registered as a `Component`. {@link Tech}s 5286 * should be registered using {@link Tech.registerTech} or 5287 * {@link videojs:videojs.registerTech}. 5288 * 5289 * > NOTE: This function can also be seen on videojs as 5290 * {@link videojs:videojs.registerComponent}. 5291 * 5292 * @param {string} name 5293 * The name of the `Component` to register. 5294 * 5295 * @param {Component} ComponentToRegister 5296 * The `Component` class to register. 5297 * 5298 * @return {Component} 5299 * The `Component` that was registered. 5300 */ 5301 static registerComponent(name, ComponentToRegister) { 5302 if (typeof name !== 'string' || !name) { 5303 throw new Error(`Illegal component name, "${name}"; must be a non-empty string.`); 5304 } 5305 const Tech = Component$1.getComponent('Tech'); 5306 5307 // We need to make sure this check is only done if Tech has been registered. 5308 const isTech = Tech && Tech.isTech(ComponentToRegister); 5309 const isComp = Component$1 === ComponentToRegister || Component$1.prototype.isPrototypeOf(ComponentToRegister.prototype); 5310 if (isTech || !isComp) { 5311 let reason; 5312 if (isTech) { 5313 reason = 'techs must be registered using Tech.registerTech()'; 5314 } else { 5315 reason = 'must be a Component subclass'; 5316 } 5317 throw new Error(`Illegal component, "${name}"; ${reason}.`); 5318 } 5319 name = toTitleCase$1(name); 5320 if (!Component$1.components_) { 5321 Component$1.components_ = {}; 5322 } 5323 const Player = Component$1.getComponent('Player'); 5324 if (name === 'Player' && Player && Player.players) { 5325 const players = Player.players; 5326 const playerNames = Object.keys(players); 5327 5328 // If we have players that were disposed, then their name will still be 5329 // in Players.players. So, we must loop through and verify that the value 5330 // for each item is not null. This allows registration of the Player component 5331 // after all players have been disposed or before any were created. 5332 if (players && playerNames.length > 0 && playerNames.map(pname => players[pname]).every(Boolean)) { 5333 throw new Error('Can not register Player component after player has been created.'); 5334 } 5335 } 5336 Component$1.components_[name] = ComponentToRegister; 5337 Component$1.components_[toLowerCase(name)] = ComponentToRegister; 5338 return ComponentToRegister; 5339 } 5340 5341 /** 5342 * Get a `Component` based on the name it was registered with. 5343 * 5344 * @param {string} name 5345 * The Name of the component to get. 5346 * 5347 * @return {typeof Component} 5348 * The `Component` that got registered under the given name. 5349 */ 5350 static getComponent(name) { 5351 if (!name || !Component$1.components_) { 5352 return; 5353 } 5354 return Component$1.components_[name]; 5355 } 5356 } 5357 Component$1.registerComponent('Component', Component$1); 5358 5359 /** 5360 * @file time.js 5361 * @module time 5362 */ 5363 5364 /** 5365 * Returns the time for the specified index at the start or end 5366 * of a TimeRange object. 5367 * 5368 * @typedef {Function} TimeRangeIndex 5369 * 5370 * @param {number} [index=0] 5371 * The range number to return the time for. 5372 * 5373 * @return {number} 5374 * The time offset at the specified index. 5375 * 5376 * @deprecated The index argument must be provided. 5377 * In the future, leaving it out will throw an error. 5378 */ 5379 5380 /** 5381 * An object that contains ranges of time, which mimics {@link TimeRanges}. 5382 * 5383 * @typedef {Object} TimeRange 5384 * 5385 * @property {number} length 5386 * The number of time ranges represented by this object. 5387 * 5388 * @property {module:time~TimeRangeIndex} start 5389 * Returns the time offset at which a specified time range begins. 5390 * 5391 * @property {module:time~TimeRangeIndex} end 5392 * Returns the time offset at which a specified time range ends. 5393 * 5394 * @see https://developer.mozilla.org/en-US/docs/Web/API/TimeRanges 5395 */ 5396 5397 /** 5398 * Check if any of the time ranges are over the maximum index. 5399 * 5400 * @private 5401 * @param {string} fnName 5402 * The function name to use for logging 5403 * 5404 * @param {number} index 5405 * The index to check 5406 * 5407 * @param {number} maxIndex 5408 * The maximum possible index 5409 * 5410 * @throws {Error} if the timeRanges provided are over the maxIndex 5411 */ 5412 function rangeCheck(fnName, index, maxIndex) { 5413 if (typeof index !== 'number' || index < 0 || index > maxIndex) { 5414 throw new Error(`Failed to execute '${fnName}' on 'TimeRanges': The index provided (${index}) is non-numeric or out of bounds (0-${maxIndex}).`); 5415 } 5416 } 5417 5418 /** 5419 * Get the time for the specified index at the start or end 5420 * of a TimeRange object. 5421 * 5422 * @private 5423 * @param {string} fnName 5424 * The function name to use for logging 5425 * 5426 * @param {string} valueIndex 5427 * The property that should be used to get the time. should be 5428 * 'start' or 'end' 5429 * 5430 * @param {Array} ranges 5431 * An array of time ranges 5432 * 5433 * @param {Array} [rangeIndex=0] 5434 * The index to start the search at 5435 * 5436 * @return {number} 5437 * The time that offset at the specified index. 5438 * 5439 * @deprecated rangeIndex must be set to a value, in the future this will throw an error. 5440 * @throws {Error} if rangeIndex is more than the length of ranges 5441 */ 5442 function getRange(fnName, valueIndex, ranges, rangeIndex) { 5443 rangeCheck(fnName, rangeIndex, ranges.length - 1); 5444 return ranges[rangeIndex][valueIndex]; 5445 } 5446 5447 /** 5448 * Create a time range object given ranges of time. 5449 * 5450 * @private 5451 * @param {Array} [ranges] 5452 * An array of time ranges. 5453 * 5454 * @return {TimeRange} 5455 */ 5456 function createTimeRangesObj(ranges) { 5457 let timeRangesObj; 5458 if (ranges === undefined || ranges.length === 0) { 5459 timeRangesObj = { 5460 length: 0, 5461 start() { 5462 throw new Error('This TimeRanges object is empty'); 5463 }, 5464 end() { 5465 throw new Error('This TimeRanges object is empty'); 5466 } 5467 }; 5468 } else { 5469 timeRangesObj = { 5470 length: ranges.length, 5471 start: getRange.bind(null, 'start', 0, ranges), 5472 end: getRange.bind(null, 'end', 1, ranges) 5473 }; 5474 } 5475 if (window.Symbol && window.Symbol.iterator) { 5476 timeRangesObj[window.Symbol.iterator] = () => (ranges || []).values(); 5477 } 5478 return timeRangesObj; 5479 } 5480 5481 /** 5482 * Create a `TimeRange` object which mimics an 5483 * {@link https://developer.mozilla.org/en-US/docs/Web/API/TimeRanges|HTML5 TimeRanges instance}. 5484 * 5485 * @param {number|Array[]} start 5486 * The start of a single range (a number) or an array of ranges (an 5487 * array of arrays of two numbers each). 5488 * 5489 * @param {number} end 5490 * The end of a single range. Cannot be used with the array form of 5491 * the `start` argument. 5492 * 5493 * @return {TimeRange} 5494 */ 5495 function createTimeRanges$1(start, end) { 5496 if (Array.isArray(start)) { 5497 return createTimeRangesObj(start); 5498 } else if (start === undefined || end === undefined) { 5499 return createTimeRangesObj(); 5500 } 5501 return createTimeRangesObj([[start, end]]); 5502 } 5503 5504 /** 5505 * Format seconds as a time string, H:MM:SS or M:SS. Supplying a guide (in 5506 * seconds) will force a number of leading zeros to cover the length of the 5507 * guide. 5508 * 5509 * @private 5510 * @param {number} seconds 5511 * Number of seconds to be turned into a string 5512 * 5513 * @param {number} guide 5514 * Number (in seconds) to model the string after 5515 * 5516 * @return {string} 5517 * Time formatted as H:MM:SS or M:SS 5518 */ 5519 const defaultImplementation = function (seconds, guide) { 5520 seconds = seconds < 0 ? 0 : seconds; 5521 let s = Math.floor(seconds % 60); 5522 let m = Math.floor(seconds / 60 % 60); 5523 let h = Math.floor(seconds / 3600); 5524 const gm = Math.floor(guide / 60 % 60); 5525 const gh = Math.floor(guide / 3600); 5526 5527 // handle invalid times 5528 if (isNaN(seconds) || seconds === Infinity) { 5529 // '-' is false for all relational operators (e.g. <, >
vendor: 4,311 bytes, lines 5529-5668
5529=) so this setting 5530 // will add the minimum number of fields specified by the guide 5531 h = m = s = '-'; 5532 } 5533 5534 // Check if we need to show hours 5535 h = h > 0 || gh > 0 ? h + ':' : ''; 5536 5537 // If hours are showing, we may need to add a leading zero. 5538 // Always show at least one digit of minutes. 5539 m = ((h || gm >= 10) && m < 10 ? '0' + m : m) + ':'; 5540 5541 // Check if leading zero is need for seconds 5542 s = s < 10 ? '0' + s : s; 5543 return h + m + s; 5544 }; 5545 5546 // Internal pointer to the current implementation. 5547 let implementation = defaultImplementation; 5548 5549 /** 5550 * Replaces the default formatTime implementation with a custom implementation. 5551 * 5552 * @param {Function} customImplementation 5553 * A function which will be used in place of the default formatTime 5554 * implementation. Will receive the current time in seconds and the 5555 * guide (in seconds) as arguments. 5556 */ 5557 function setFormatTime(customImplementation) { 5558 implementation = customImplementation; 5559 } 5560 5561 /** 5562 * Resets formatTime to the default implementation. 5563 */ 5564 function resetFormatTime() { 5565 implementation = defaultImplementation; 5566 } 5567 5568 /** 5569 * Delegates to either the default time formatting function or a custom 5570 * function supplied via `setFormatTime`. 5571 * 5572 * Formats seconds as a time string (H:MM:SS or M:SS). Supplying a 5573 * guide (in seconds) will force a number of leading zeros to cover the 5574 * length of the guide. 5575 * 5576 * @example formatTime(125, 600) === "02:05" 5577 * @param {number} seconds 5578 * Number of seconds to be turned into a string 5579 * 5580 * @param {number} guide 5581 * Number (in seconds) to model the string after 5582 * 5583 * @return {string} 5584 * Time formatted as H:MM:SS or M:SS 5585 */ 5586 function formatTime(seconds, guide = seconds) { 5587 return implementation(seconds, guide); 5588 } 5589 5590 var Time = /*#__PURE__*/Object.freeze({ 5591 __proto__: null, 5592 createTimeRanges: createTimeRanges$1, 5593 createTimeRange: createTimeRanges$1, 5594 setFormatTime: setFormatTime, 5595 resetFormatTime: resetFormatTime, 5596 formatTime: formatTime 5597 }); 5598 5599 /** 5600 * @file buffer.js 5601 * @module buffer 5602 */ 5603 5604 /** 5605 * Compute the percentage of the media that has been buffered. 5606 * 5607 * @param { import('./time').TimeRange } buffered 5608 * The current `TimeRanges` object representing buffered time ranges 5609 * 5610 * @param {number} duration 5611 * Total duration of the media 5612 * 5613 * @return {number} 5614 * Percent buffered of the total duration in decimal form. 5615 */ 5616 function bufferedPercent(buffered, duration) { 5617 let bufferedDuration = 0; 5618 let start; 5619 let end; 5620 if (!duration) { 5621 return 0; 5622 } 5623 if (!buffered || !buffered.length) { 5624 buffered = createTimeRanges$1(0, 0); 5625 } 5626 for (let i = 0; i < buffered.length; i++) { 5627 start = buffered.start(i); 5628 end = buffered.end(i); 5629 5630 // buffered end can be bigger than duration by a very small fraction 5631 if (end > duration) { 5632 end = duration; 5633 } 5634 bufferedDuration += end - start; 5635 } 5636 return bufferedDuration / duration; 5637 } 5638 5639 /** 5640 * @file media-error.js 5641 */ 5642 5643 /** 5644 * A Custom `MediaError` class which mimics the standard HTML5 `MediaError` class. 5645 * 5646 * @param {number|string|Object|MediaError} value 5647 * This can be of multiple types: 5648 * - number: should be a standard error code 5649 * - string: an error message (the code will be 0) 5650 * - Object: arbitrary properties 5651 * - `MediaError` (native): used to populate a video.js `MediaError` object 5652 * - `MediaError` (video.js): will return itself if it's already a 5653 * video.js `MediaError` object. 5654 * 5655 * @see [MediaError Spec]{@link https://dev.w3.org/html5/spec-author-view/video.html#mediaerror} 5656 * @see [Encrypted MediaError Spec]{@link https://www.w3.org/TR/2013/WD-encrypted-media-20130510/#error-codes} 5657 * 5658 * @class MediaError 5659 */ 5660 function MediaError(value) { 5661 // Allow redundant calls to this constructor to avoid having `instanceof` 5662 // checks peppered around the code. 5663 if (value instanceof MediaError) { 5664 return value; 5665 } 5666 if (typeof value === 'number') { 5667 this.code = value; 5668 } else if (typeof value === 'string') {
5669 // default code is zero, so this is a custom error 5670 this.message = value; 5671 } else if (isObject$1(value)) { 5672 // We assign the `code` property manually because native `MediaError` objects 5673 // do not expose it as an own/enumerable property of the object. 5674 if (typeof value.code === 'number') { 5675 this.code = value.code; 5676 } 5677 Object.assign(this, value); 5678 } 5679 if (!this.message) { 5680 this.message = MediaError.defaultMessages[this.code] || ''; 5681 } 5682 } 5683 5684 /** 5685 * The error code that refers two one of the defined `MediaError` types 5686 * 5687 * @type {Number} 5688 */ 5689 MediaError.prototype.code = 0; 5690 5691 /** 5692 * An optional message that to show with the error. Message is not part of the HTML5 5693 * video spec but allows for more informative custom errors. 5694 * 5695 * @type {String} 5696 */ 5697 MediaError.prototype.message = ''; 5698 5699 /** 5700 * An optional status code that can be set by plugins to allow even more detail about 5701 * the error. For example a plugin might provide a specific HTTP status code and an 5702 * error message for that code. Then when the plugin gets that error this class will 5703 * know how to display an error message for it. This allows a custom message to show 5704 * up on the `Player` error overlay. 5705 * 5706 * @type {Array} 5707 */ 5708 MediaError.prototype.status = null; 5709 5710 /** 5711 * Errors indexed by the W3C standard. The order **CANNOT CHANGE**! See the 5712 * specification listed under {@link MediaError} for more information. 5713 * 5714 * @enum {array} 5715 * @readonly 5716 * @property {string} 0 - MEDIA_ERR_CUSTOM 5717 * @property {string} 1 - MEDIA_ERR_ABORTED 5718 * @property {string} 2 - MEDIA_ERR_NETWORK 5719 * @property {string} 3 - MEDIA_ERR_DECODE 5720 * @property {string} 4 - MEDIA_ERR_SRC_NOT_SUPPORTED 5721 * @property {string} 5 - MEDIA_ERR_ENCRYPTED 5722 */ 5723 MediaError.errorTypes = ['MEDIA_ERR_CUSTOM', 'MEDIA_ERR_ABORTED', 'MEDIA_ERR_NETWORK', 'MEDIA_ERR_DECODE', 'MEDIA_ERR_SRC_NOT_SUPPORTED', 'MEDIA_ERR_ENCRYPTED']; 5724 5725 /** 5726 * The default `MediaError` messages based on the {@link MediaError.errorTypes}. 5727 * 5728 * @type {Array} 5729 * @constant 5730 */ 5731 MediaError.defaultMessages = { 5732 1: 'You aborted the media playback', 5733 2: 'A network error caused the media download to fail part-way.', 5734 3: 'The media playback was aborted due to a corruption problem or because the media used features your browser did not support.', 5735 4: 'The media could not be loaded, either because the server or network failed or because the format is not supported.', 5736 5: 'The media is encrypted and we do not have the keys to decrypt it.' 5737 }; 5738 5739 // Add types as properties on MediaError 5740 // e.g. MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED = 4; 5741 for (let errNum = 0; errNum < MediaError.errorTypes.length; errNum++) { 5742 MediaError[MediaError.errorTypes[errNum]] = errNum; 5743 // values should be accessible on both the class and instance 5744 MediaError.prototype[MediaError.errorTypes[errNum]] = errNum; 5745 } 5746 5747 var tuple = SafeParseTuple; 5748 function SafeParseTuple(obj, reviver) { 5749 var json; 5750 var error = null; 5751 try { 5752 json = JSON.parse(obj, reviver); 5753 } catch (err) { 5754 error = err; 5755 } 5756 return [error, json]; 5757 } 5758 5759 /** 5760 * Returns whether an object is `Promise`-like (i.e. has a `then` method). 5761 * 5762 * @param {Object} value 5763 * An object that may or may not be `Promise`-like. 5764 * 5765 * @return {boolean} 5766 * Whether or not the object is `Promise`-like. 5767 */ 5768 function isPromise(value) { 5769 return value !== undefined && value !== null && typeof value.then === 'function'; 5770 } 5771 5772 /** 5773 * Silence a Promise-like object. 5774 * 5775 * This is useful for avoiding non-harmful, but potentially confusing "uncaught 5776 * play promise" rejection error messages. 5777 * 5778 * @param {Object} value 5779 * An object that may or may not be `Promise`-like. 5780 */ 5781 function silencePromise(value) { 5782 if (isPromise(value)) { 5783 value.then(null, e => {}); 5784 } 5785 } 5786 5787 /** 5788 * @file text-track-list-converter.js Utilities for capturing text track state and 5789 * re-creating tracks based on a capture. 5790 * 5791 * @module text-track-list-converter 5792 */ 5793 5794 /** 5795 * Examine a single {@link TextTrack} and return a JSON-compatible javascript object that 5796 * represents the {@link TextTrack}'s state. 5797 * 5798 * @param {TextTrack} track 5799 * The text track to query. 5800 * 5801 * @return {Object} 5802 * A serializable javascript representation of the TextTrack. 5803 * @private 5804 */ 5805 const trackToJson_ = function (track) { 5806 const ret = ['kind', 'label', 'language', 'id', 'inBandMetadataTrackDispatchType', 'mode', 'src'].reduce((acc, prop, i) => { 5807 if (track[prop]) { 5808 acc[prop] = track[prop]; 5809 } 5810 return acc; 5811 }, {
vendor: 4,253 bytes, lines 5812-5932
5812 cues: track.cues && Array.prototype.map.call(track.cues, function (cue) { 5813 return { 5814 startTime: cue.startTime, 5815 endTime: cue.endTime, 5816 text: cue.text, 5817 id: cue.id 5818 }; 5819 }) 5820 }); 5821 return ret; 5822 }; 5823 5824 /** 5825 * Examine a {@link Tech} and return a JSON-compatible javascript array that represents the 5826 * state of all {@link TextTrack}s currently configured. The return array is compatible with 5827 * {@link text-track-list-converter:jsonToTextTracks}. 5828 * 5829 * @param { import('../tech/tech').default } tech 5830 * The tech object to query 5831 * 5832 * @return {Array} 5833 * A serializable javascript representation of the {@link Tech}s 5834 * {@link TextTrackList}. 5835 */ 5836 const textTracksToJson = function (tech) { 5837 const trackEls = tech.$$('track'); 5838 const trackObjs = Array.prototype.map.call(trackEls, t => t.track); 5839 const tracks = Array.prototype.map.call(trackEls, function (trackEl) { 5840 const json = trackToJson_(trackEl.track); 5841 if (trackEl.src) { 5842 json.src = trackEl.src; 5843 } 5844 return json; 5845 }); 5846 return tracks.concat(Array.prototype.filter.call(tech.textTracks(), function (track) { 5847 return trackObjs.indexOf(track) === -1; 5848 }).map(trackToJson_)); 5849 }; 5850 5851 /** 5852 * Create a set of remote {@link TextTrack}s on a {@link Tech} based on an array of javascript 5853 * object {@link TextTrack} representations. 5854 * 5855 * @param {Array} json 5856 * An array of `TextTrack` representation objects, like those that would be 5857 * produced by `textTracksToJson`. 5858 * 5859 * @param {Tech} tech 5860 * The `Tech` to create the `TextTrack`s on. 5861 */ 5862 const jsonToTextTracks = function (json, tech) { 5863 json.forEach(function (track) { 5864 const addedTrack = tech.addRemoteTextTrack(track).track; 5865 if (!track.src && track.cues) { 5866 track.cues.forEach(cue => addedTrack.addCue(cue)); 5867 } 5868 }); 5869 return tech.textTracks(); 5870 }; 5871 var textTrackConverter = { 5872 textTracksToJson, 5873 jsonToTextTracks, 5874 trackToJson_ 5875 }; 5876 5877 /** 5878 * @file modal-dialog.js 5879 */ 5880 const MODAL_CLASS_NAME = 'vjs-modal-dialog'; 5881 5882 /** 5883 * The `ModalDialog` displays over the video and its controls, which blocks 5884 * interaction with the player until it is closed. 5885 * 5886 * Modal dialogs include a "Close" button and will close when that button 5887 * is activated - or when ESC is pressed anywhere. 5888 * 5889 * @extends Component 5890 */ 5891 class ModalDialog extends Component$1 { 5892 /** 5893 * Create an instance of this class. 5894 * 5895 * @param { import('./player').default } player 5896 * The `Player` that this class should be attached to. 5897 * 5898 * @param {Object} [options] 5899 * The key/value store of player options. 5900 * 5901 * @param { import('./utils/dom').ContentDescriptor} [options.content=undefined] 5902 * Provide customized content for this modal. 5903 * 5904 * @param {string} [options.description] 5905 * A text description for the modal, primarily for accessibility. 5906 * 5907 * @param {boolean} [options.fillAlways=false] 5908 * Normally, modals are automatically filled only the first time 5909 * they open. This tells the modal to refresh its content 5910 * every time it opens. 5911 * 5912 * @param {string} [options.label] 5913 * A text label for the modal, primarily for accessibility. 5914 * 5915 * @param {boolean} [options.pauseOnOpen=true] 5916 * If `true`, playback will will be paused if playing when 5917 * the modal opens, and resumed when it closes. 5918 * 5919 * @param {boolean} [options.temporary=true] 5920 * If `true`, the modal can only be opened once; it will be 5921 * disposed as soon as it's closed. 5922 * 5923 * @param {boolean} [options.uncloseable=false] 5924 * If `true`, the user will not be able to close the modal 5925 * through the UI in the normal ways. Programmatic closing is 5926 * still possible. 5927 */ 5928 constructor(player, options) { 5929 super(player, options); 5930 this.handleKeyDown_ = e => this.handleKeyDown(e); 5931 this.close_ = e => this.close(e); 5932 this.opened_ = this.hasBeenOpened_ = this.hasBeenFilled_ = false;
vendor: 4,595 bytes, lines 5933-6094
5933 this.closeable(!this.options_.uncloseable); 5934 this.content(this.options_.content); 5935 5936 // Make sure the contentEl is defined AFTER any children are initialized 5937 // because we only want the contents of the modal in the contentEl 5938 // (not the UI elements like the close button). 5939 this.contentEl_ = createEl('div', { 5940 className: `${MODAL_CLASS_NAME}-content` 5941 }, { 5942 role: 'document' 5943 }); 5944 this.descEl_ = createEl('p', { 5945 className: `${MODAL_CLASS_NAME}-description vjs-control-text`, 5946 id: this.el().getAttribute('aria-describedby') 5947 }); 5948 textContent(this.descEl_, this.description()); 5949 this.el_.appendChild(this.descEl_); 5950 this.el_.appendChild(this.contentEl_); 5951 } 5952 5953 /** 5954 * Create the `ModalDialog`'s DOM element 5955 * 5956 * @return {Element} 5957 * The DOM element that gets created. 5958 */ 5959 createEl() { 5960 return super.createEl('div', { 5961 className: this.buildCSSClass(), 5962 tabIndex: -1 5963 }, { 5964 'aria-describedby': `${this.id()}_description`, 5965 'aria-hidden': 'true', 5966 'aria-label': this.label(), 5967 'role': 'dialog' 5968 }); 5969 } 5970 dispose() { 5971 this.contentEl_ = null; 5972 this.descEl_ = null; 5973 this.previouslyActiveEl_ = null; 5974 super.dispose(); 5975 } 5976 5977 /** 5978 * Builds the default DOM `className`. 5979 * 5980 * @return {string} 5981 * The DOM `className` for this object. 5982 */ 5983 buildCSSClass() { 5984 return `${MODAL_CLASS_NAME} vjs-hidden ${super.buildCSSClass()}`; 5985 } 5986 5987 /** 5988 * Returns the label string for this modal. Primarily used for accessibility. 5989 * 5990 * @return {string} 5991 * the localized or raw label of this modal. 5992 */ 5993 label() { 5994 return this.localize(this.options_.label || 'Modal Window'); 5995 } 5996 5997 /** 5998 * Returns the description string for this modal. Primarily used for 5999 * accessibility. 6000 * 6001 * @return {string} 6002 * The localized or raw description of this modal. 6003 */ 6004 description() { 6005 let desc = this.options_.description || this.localize('This is a modal window.'); 6006 6007 // Append a universal closeability message if the modal is closeable. 6008 if (this.closeable()) { 6009 desc += ' ' + this.localize('This modal can be closed by pressing the Escape key or activating the close button.'); 6010 } 6011 return desc; 6012 } 6013 6014 /** 6015 * Opens the modal. 6016 * 6017 * @fires ModalDialog#beforemodalopen 6018 * @fires ModalDialog#modalopen 6019 */ 6020 open() { 6021 if (!this.opened_) { 6022 const player = this.player(); 6023 6024 /** 6025 * Fired just before a `ModalDialog` is opened. 6026 * 6027 * @event ModalDialog#beforemodalopen 6028 * @type {Event} 6029 */ 6030 this.trigger('beforemodalopen'); 6031 this.opened_ = true; 6032 6033 // Fill content if the modal has never opened before and 6034 // never been filled. 6035 if (this.options_.fillAlways || !this.hasBeenOpened_ && !this.hasBeenFilled_) { 6036 this.fill(); 6037 } 6038 6039 // If the player was playing, pause it and take note of its previously 6040 // playing state. 6041 this.wasPlaying_ = !player.paused(); 6042 if (this.options_.pauseOnOpen && this.wasPlaying_) { 6043 player.pause(); 6044 } 6045 this.on('keydown', this.handleKeyDown_); 6046 6047 // Hide controls and note if they were enabled. 6048 this.hadControls_ = player.controls(); 6049 player.controls(false); 6050 this.show(); 6051 this.conditionalFocus_(); 6052 this.el().setAttribute('aria-hidden', 'false'); 6053 6054 /** 6055 * Fired just after a `ModalDialog` is opened. 6056 * 6057 * @event ModalDialog#modalopen 6058 * @type {Event} 6059 */ 6060 this.trigger('modalopen'); 6061 this.hasBeenOpened_ = true; 6062 } 6063 } 6064 6065 /** 6066 * If the `ModalDialog` is currently open or closed. 6067 * 6068 * @param {boolean} [value] 6069 * If given, it will open (`true`) or close (`false`) the modal. 6070 * 6071 * @return {boolean} 6072 * the current open state of the modaldialog 6073 */ 6074 opened(value) { 6075 if (typeof value === 'boolean') { 6076 this[value ? 'open' : 'close'](); 6077 } 6078 return this.opened_; 6079 } 6080 6081 /** 6082 * Closes the modal, does nothing if the `ModalDialog` is 6083 * not open. 6084 * 6085 * @fires ModalDialog#beforemodalclose 6086 * @fires ModalDialog#modalclose 6087 */ 6088 close() { 6089 if (!this.opened_) { 6090 return; 6091 } 6092 const player = this.player(); 6093 6094 /**
vendor: 30,943 bytes, lines 6095-7231
6095 * Fired just before a `ModalDialog` is closed. 6096 * 6097 * @event ModalDialog#beforemodalclose 6098 * @type {Event} 6099 */ 6100 this.trigger('beforemodalclose'); 6101 this.opened_ = false; 6102 if (this.wasPlaying_ && this.options_.pauseOnOpen) { 6103 player.play(); 6104 } 6105 this.off('keydown', this.handleKeyDown_); 6106 if (this.hadControls_) { 6107 player.controls(true); 6108 } 6109 this.hide(); 6110 this.el().setAttribute('aria-hidden', 'true'); 6111 6112 /** 6113 * Fired just after a `ModalDialog` is closed. 6114 * 6115 * @event ModalDialog#modalclose 6116 * @type {Event} 6117 */ 6118 this.trigger('modalclose'); 6119 this.conditionalBlur_(); 6120 if (this.options_.temporary) { 6121 this.dispose(); 6122 } 6123 } 6124 6125 /** 6126 * Check to see if the `ModalDialog` is closeable via the UI. 6127 * 6128 * @param {boolean} [value] 6129 * If given as a boolean, it will set the `closeable` option. 6130 * 6131 * @return {boolean} 6132 * Returns the final value of the closable option. 6133 */ 6134 closeable(value) { 6135 if (typeof value === 'boolean') { 6136 const closeable = this.closeable_ = !!value; 6137 let close = this.getChild('closeButton'); 6138 6139 // If this is being made closeable and has no close button, add one. 6140 if (closeable && !close) { 6141 // The close button should be a child of the modal - not its 6142 // content element, so temporarily change the content element. 6143 const temp = this.contentEl_; 6144 this.contentEl_ = this.el_; 6145 close = this.addChild('closeButton', { 6146 controlText: 'Close Modal Dialog' 6147 }); 6148 this.contentEl_ = temp; 6149 this.on(close, 'close', this.close_); 6150 } 6151 6152 // If this is being made uncloseable and has a close button, remove it. 6153 if (!closeable && close) { 6154 this.off(close, 'close', this.close_); 6155 this.removeChild(close); 6156 close.dispose(); 6157 } 6158 } 6159 return this.closeable_; 6160 } 6161 6162 /** 6163 * Fill the modal's content element with the modal's "content" option. 6164 * The content element will be emptied before this change takes place. 6165 */ 6166 fill() { 6167 this.fillWith(this.content()); 6168 } 6169 6170 /** 6171 * Fill the modal's content element with arbitrary content. 6172 * The content element will be emptied before this change takes place. 6173 * 6174 * @fires ModalDialog#beforemodalfill 6175 * @fires ModalDialog#modalfill 6176 * 6177 * @param { import('./utils/dom').ContentDescriptor} [content] 6178 * The same rules apply to this as apply to the `content` option. 6179 */ 6180 fillWith(content) { 6181 const contentEl = this.contentEl(); 6182 const parentEl = contentEl.parentNode; 6183 const nextSiblingEl = contentEl.nextSibling; 6184 6185 /** 6186 * Fired just before a `ModalDialog` is filled with content. 6187 * 6188 * @event ModalDialog#beforemodalfill 6189 * @type {Event} 6190 */ 6191 this.trigger('beforemodalfill'); 6192 this.hasBeenFilled_ = true; 6193 6194 // Detach the content element from the DOM before performing 6195 // manipulation to avoid modifying the live DOM multiple times. 6196 parentEl.removeChild(contentEl); 6197 this.empty(); 6198 insertContent(contentEl, content); 6199 /** 6200 * Fired just after a `ModalDialog` is filled with content. 6201 * 6202 * @event ModalDialog#modalfill 6203 * @type {Event} 6204 */ 6205 this.trigger('modalfill'); 6206 6207 // Re-inject the re-filled content element. 6208 if (nextSiblingEl) { 6209 parentEl.insertBefore(contentEl, nextSiblingEl); 6210 } else { 6211 parentEl.appendChild(contentEl); 6212 } 6213 6214 // make sure that the close button is last in the dialog DOM 6215 const closeButton = this.getChild('closeButton'); 6216 if (closeButton) { 6217 parentEl.appendChild(closeButton.el_); 6218 } 6219 } 6220 6221 /** 6222 * Empties the content element. This happens anytime the modal is filled. 6223 * 6224 * @fires ModalDialog#beforemodalempty 6225 * @fires ModalDialog#modalempty 6226 */ 6227 empty() { 6228 /** 6229 * Fired just before a `ModalDialog` is emptied. 6230 * 6231 * @event ModalDialog#beforemodalempty 6232 * @type {Event} 6233 */ 6234 this.trigger('beforemodalempty'); 6235 emptyEl(this.contentEl()); 6236 6237 /** 6238 * Fired just after a `ModalDialog` is emptied. 6239 * 6240 * @event ModalDialog#modalempty 6241 * @type {Event} 6242 */ 6243 this.trigger('modalempty'); 6244 } 6245 6246 /** 6247 * Gets or sets the modal content, which gets normalized before being 6248 * rendered into the DOM. 6249 * 6250 * This does not update the DOM or fill the modal, but it is called during 6251 * that process. 6252 * 6253 * @param { import('./utils/dom').ContentDescriptor} [value] 6254 * If defined, sets the internal content value to be used on the 6255 * next call(s) to `fill`. This value is normalized before being 6256 * inserted. To "clear" the internal content value, pass `null`. 6257 * 6258 * @return { import('./utils/dom').ContentDescriptor} 6259 * The current content of the modal dialog 6260 */ 6261 content(value) { 6262 if (typeof value !== 'undefined') { 6263 this.content_ = value; 6264 } 6265 return this.content_; 6266 } 6267 6268 /** 6269 * conditionally focus the modal dialog if focus was previously on the player. 6270 * 6271 * @private 6272 */ 6273 conditionalFocus_() { 6274 const activeEl = document.activeElement; 6275 const playerEl = this.player_.el_; 6276 this.previouslyActiveEl_ = null; 6277 if (playerEl.contains(activeEl) || playerEl === activeEl) { 6278 this.previouslyActiveEl_ = activeEl; 6279 this.focus(); 6280 } 6281 } 6282 6283 /** 6284 * conditionally blur the element and refocus the last focused element 6285 * 6286 * @private 6287 */ 6288 conditionalBlur_() { 6289 if (this.previouslyActiveEl_) { 6290 this.previouslyActiveEl_.focus(); 6291 this.previouslyActiveEl_ = null; 6292 } 6293 } 6294 6295 /** 6296 * Keydown handler. Attached when modal is focused. 6297 * 6298 * @listens keydown 6299 */ 6300 handleKeyDown(event) { 6301 // Do not allow keydowns to reach out of the modal dialog. 6302 event.stopPropagation(); 6303 if (keycode.isEventKey(event, 'Escape') && this.closeable()) { 6304 event.preventDefault(); 6305 this.close(); 6306 return; 6307 } 6308 6309 // exit early if it isn't a tab key 6310 if (!keycode.isEventKey(event, 'Tab')) { 6311 return; 6312 } 6313 const focusableEls = this.focusableEls_(); 6314 const activeEl = this.el_.querySelector(':focus'); 6315 let focusIndex; 6316 for (let i = 0; i < focusableEls.length; i++) { 6317 if (activeEl === focusableEls[i]) { 6318 focusIndex = i; 6319 break; 6320 } 6321 } 6322 if (document.activeElement === this.el_) { 6323 focusIndex = 0; 6324 } 6325 if (event.shiftKey && focusIndex === 0) { 6326 focusableEls[focusableEls.length - 1].focus(); 6327 event.preventDefault(); 6328 } else if (!event.shiftKey && focusIndex === focusableEls.length - 1) { 6329 focusableEls[0].focus(); 6330 event.preventDefault(); 6331 } 6332 } 6333 6334 /** 6335 * get all focusable elements 6336 * 6337 * @private 6338 */ 6339 focusableEls_() { 6340 const allChildren = this.el_.querySelectorAll('*'); 6341 return Array.prototype.filter.call(allChildren, child => { 6342 return (child instanceof window.HTMLAnchorElement || child instanceof window.HTMLAreaElement) && child.hasAttribute('href') || (child instanceof window.HTMLInputElement || child instanceof window.HTMLSelectElement || child instanceof window.HTMLTextAreaElement || child instanceof window.HTMLButtonElement) && !child.hasAttribute('disabled') || child instanceof window.HTMLIFrameElement || child instanceof window.HTMLObjectElement || child instanceof window.HTMLEmbedElement || child.hasAttribute('tabindex') && child.getAttribute('tabindex') !== -1 || child.hasAttribute('contenteditable'); 6343 }); 6344 } 6345 } 6346 6347 /** 6348 * Default options for `ModalDialog` default options. 6349 * 6350 * @type {Object} 6351 * @private 6352 */ 6353 ModalDialog.prototype.options_ = { 6354 pauseOnOpen: true, 6355 temporary: true 6356 }; 6357 Component$1.registerComponent('ModalDialog', ModalDialog); 6358 6359 /** 6360 * @file track-list.js 6361 */ 6362 6363 /** 6364 * Common functionaliy between {@link TextTrackList}, {@link AudioTrackList}, and 6365 * {@link VideoTrackList} 6366 * 6367 * @extends EventTarget 6368 */ 6369 class TrackList extends EventTarget$2 { 6370 /** 6371 * Create an instance of this class 6372 * 6373 * @param { import('./track').default[] } tracks 6374 * A list of tracks to initialize the list with. 6375 * 6376 * @abstract 6377 */ 6378 constructor(tracks = []) { 6379 super(); 6380 this.tracks_ = []; 6381 6382 /** 6383 * @memberof TrackList 6384 * @member {number} length 6385 * The current number of `Track`s in the this Trackist. 6386 * @instance 6387 */ 6388 Object.defineProperty(this, 'length', { 6389 get() { 6390 return this.tracks_.length; 6391 } 6392 }); 6393 for (let i = 0; i < tracks.length; i++) { 6394 this.addTrack(tracks[i]); 6395 } 6396 } 6397 6398 /** 6399 * Add a {@link Track} to the `TrackList` 6400 * 6401 * @param { import('./track').default } track 6402 * The audio, video, or text track to add to the list. 6403 * 6404 * @fires TrackList#addtrack 6405 */ 6406 addTrack(track) { 6407 const index = this.tracks_.length; 6408 if (!('' + index in this)) { 6409 Object.defineProperty(this, index, { 6410 get() { 6411 return this.tracks_[index]; 6412 } 6413 }); 6414 } 6415 6416 // Do not add duplicate tracks 6417 if (this.tracks_.indexOf(track) === -1) { 6418 this.tracks_.push(track); 6419 /** 6420 * Triggered when a track is added to a track list. 6421 * 6422 * @event TrackList#addtrack 6423 * @type {Event} 6424 * @property {Track} track 6425 * A reference to track that was added. 6426 */ 6427 this.trigger({ 6428 track, 6429 type: 'addtrack', 6430 target: this 6431 }); 6432 } 6433 6434 /** 6435 * Triggered when a track label is changed. 6436 * 6437 * @event TrackList#addtrack 6438 * @type {Event} 6439 * @property {Track} track 6440 * A reference to track that was added. 6441 */ 6442 track.labelchange_ = () => { 6443 this.trigger({ 6444 track, 6445 type: 'labelchange', 6446 target: this 6447 }); 6448 }; 6449 if (isEvented(track)) { 6450 track.addEventListener('labelchange', track.labelchange_); 6451 } 6452 } 6453 6454 /** 6455 * Remove a {@link Track} from the `TrackList` 6456 * 6457 * @param { import('./track').default } rtrack 6458 * The audio, video, or text track to remove from the list. 6459 * 6460 * @fires TrackList#removetrack 6461 */ 6462 removeTrack(rtrack) { 6463 let track; 6464 for (let i = 0, l = this.length; i < l; i++) { 6465 if (this[i] === rtrack) { 6466 track = this[i]; 6467 if (track.off) { 6468 track.off(); 6469 } 6470 this.tracks_.splice(i, 1); 6471 break; 6472 } 6473 } 6474 if (!track) { 6475 return; 6476 } 6477 6478 /** 6479 * Triggered when a track is removed from track list. 6480 * 6481 * @event TrackList#removetrack 6482 * @type {Event} 6483 * @property {Track} track 6484 * A reference to track that was removed. 6485 */ 6486 this.trigger({ 6487 track, 6488 type: 'removetrack', 6489 target: this 6490 }); 6491 } 6492 6493 /** 6494 * Get a Track from the TrackList by a tracks id 6495 * 6496 * @param {string} id - the id of the track to get 6497 * @method getTrackById 6498 * @return { import('./track').default } 6499 * @private 6500 */ 6501 getTrackById(id) { 6502 let result = null; 6503 for (let i = 0, l = this.length; i < l; i++) { 6504 const track = this[i]; 6505 if (track.id === id) { 6506 result = track; 6507 break; 6508 } 6509 } 6510 return result; 6511 } 6512 } 6513 6514 /** 6515 * Triggered when a different track is selected/enabled. 6516 * 6517 * @event TrackList#change 6518 * @type {Event} 6519 */ 6520 6521 /** 6522 * Events that can be called with on + eventName. See {@link EventHandler}. 6523 * 6524 * @property {Object} TrackList#allowedEvents_ 6525 * @protected 6526 */ 6527 TrackList.prototype.allowedEvents_ = { 6528 change: 'change', 6529 addtrack: 'addtrack', 6530 removetrack: 'removetrack', 6531 labelchange: 'labelchange' 6532 }; 6533 6534 // emulate attribute EventHandler support to allow for feature detection 6535 for (const event in TrackList.prototype.allowedEvents_) { 6536 TrackList.prototype['on' + event] = null; 6537 } 6538 6539 /** 6540 * @file audio-track-list.js 6541 */ 6542 6543 /** 6544 * Anywhere we call this function we diverge from the spec 6545 * as we only support one enabled audiotrack at a time 6546 * 6547 * @param {AudioTrackList} list 6548 * list to work on 6549 * 6550 * @param { import('./audio-track').default } track 6551 * The track to skip 6552 * 6553 * @private 6554 */ 6555 const disableOthers$1 = function (list, track) { 6556 for (let i = 0; i < list.length; i++) { 6557 if (!Object.keys(list[i]).length || track.id === list[i].id) { 6558 continue; 6559 } 6560 // another audio track is enabled, disable it 6561 list[i].enabled = false; 6562 } 6563 }; 6564 6565 /** 6566 * The current list of {@link AudioTrack} for a media file. 6567 * 6568 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotracklist} 6569 * @extends TrackList 6570 */ 6571 class AudioTrackList extends TrackList { 6572 /** 6573 * Create an instance of this class. 6574 * 6575 * @param { import('./audio-track').default[] } [tracks=[]] 6576 * A list of `AudioTrack` to instantiate the list with. 6577 */ 6578 constructor(tracks = []) { 6579 // make sure only 1 track is enabled 6580 // sorted from last index to first index 6581 for (let i = tracks.length - 1; i >= 0; i--) { 6582 if (tracks[i].enabled) { 6583 disableOthers$1(tracks, tracks[i]); 6584 break; 6585 } 6586 } 6587 super(tracks); 6588 this.changing_ = false; 6589 } 6590 6591 /** 6592 * Add an {@link AudioTrack} to the `AudioTrackList`. 6593 * 6594 * @param { import('./audio-track').default } track 6595 * The AudioTrack to add to the list 6596 * 6597 * @fires TrackList#addtrack 6598 */ 6599 addTrack(track) { 6600 if (track.enabled) { 6601 disableOthers$1(this, track); 6602 } 6603 super.addTrack(track); 6604 // native tracks don't have this 6605 if (!track.addEventListener) { 6606 return; 6607 } 6608 track.enabledChange_ = () => { 6609 // when we are disabling other tracks (since we don't support 6610 // more than one track at a time) we will set changing_ 6611 // to true so that we don't trigger additional change events 6612 if (this.changing_) { 6613 return; 6614 } 6615 this.changing_ = true; 6616 disableOthers$1(this, track); 6617 this.changing_ = false; 6618 this.trigger('change'); 6619 }; 6620 6621 /** 6622 * @listens AudioTrack#enabledchange 6623 * @fires TrackList#change 6624 */ 6625 track.addEventListener('enabledchange', track.enabledChange_); 6626 } 6627 removeTrack(rtrack) { 6628 super.removeTrack(rtrack); 6629 if (rtrack.removeEventListener && rtrack.enabledChange_) { 6630 rtrack.removeEventListener('enabledchange', rtrack.enabledChange_); 6631 rtrack.enabledChange_ = null; 6632 } 6633 } 6634 } 6635 6636 /** 6637 * @file video-track-list.js 6638 */ 6639 6640 /** 6641 * Un-select all other {@link VideoTrack}s that are selected. 6642 * 6643 * @param {VideoTrackList} list 6644 * list to work on 6645 * 6646 * @param { import('./video-track').default } track 6647 * The track to skip 6648 * 6649 * @private 6650 */ 6651 const disableOthers = function (list, track) { 6652 for (let i = 0; i < list.length; i++) { 6653 if (!Object.keys(list[i]).length || track.id === list[i].id) { 6654 continue; 6655 } 6656 // another video track is enabled, disable it 6657 list[i].selected = false; 6658 } 6659 }; 6660 6661 /** 6662 * The current list of {@link VideoTrack} for a video. 6663 * 6664 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#videotracklist} 6665 * @extends TrackList 6666 */ 6667 class VideoTrackList extends TrackList { 6668 /** 6669 * Create an instance of this class. 6670 * 6671 * @param {VideoTrack[]} [tracks=[]] 6672 * A list of `VideoTrack` to instantiate the list with. 6673 */ 6674 constructor(tracks = []) { 6675 // make sure only 1 track is enabled 6676 // sorted from last index to first index 6677 for (let i = tracks.length - 1; i >= 0; i--) { 6678 if (tracks[i].selected) { 6679 disableOthers(tracks, tracks[i]); 6680 break; 6681 } 6682 } 6683 super(tracks); 6684 this.changing_ = false; 6685 6686 /** 6687 * @member {number} VideoTrackList#selectedIndex 6688 * The current index of the selected {@link VideoTrack`}. 6689 */ 6690 Object.defineProperty(this, 'selectedIndex', { 6691 get() { 6692 for (let i = 0; i < this.length; i++) { 6693 if (this[i].selected) { 6694 return i; 6695 } 6696 } 6697 return -1; 6698 }, 6699 set() {} 6700 }); 6701 } 6702 6703 /** 6704 * Add a {@link VideoTrack} to the `VideoTrackList`. 6705 * 6706 * @param { import('./video-track').default } track 6707 * The VideoTrack to add to the list 6708 * 6709 * @fires TrackList#addtrack 6710 */ 6711 addTrack(track) { 6712 if (track.selected) { 6713 disableOthers(this, track); 6714 } 6715 super.addTrack(track); 6716 // native tracks don't have this 6717 if (!track.addEventListener) { 6718 return; 6719 } 6720 track.selectedChange_ = () => { 6721 if (this.changing_) { 6722 return; 6723 } 6724 this.changing_ = true; 6725 disableOthers(this, track); 6726 this.changing_ = false; 6727 this.trigger('change'); 6728 }; 6729 6730 /** 6731 * @listens VideoTrack#selectedchange 6732 * @fires TrackList#change 6733 */ 6734 track.addEventListener('selectedchange', track.selectedChange_); 6735 } 6736 removeTrack(rtrack) { 6737 super.removeTrack(rtrack); 6738 if (rtrack.removeEventListener && rtrack.selectedChange_) { 6739 rtrack.removeEventListener('selectedchange', rtrack.selectedChange_); 6740 rtrack.selectedChange_ = null; 6741 } 6742 } 6743 } 6744 6745 /** 6746 * @file text-track-list.js 6747 */ 6748 6749 /** 6750 * The current list of {@link TextTrack} for a media file. 6751 * 6752 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttracklist} 6753 * @extends TrackList 6754 */ 6755 class TextTrackList extends TrackList { 6756 /** 6757 * Add a {@link TextTrack} to the `TextTrackList` 6758 * 6759 * @param { import('./text-track').default } track 6760 * The text track to add to the list. 6761 * 6762 * @fires TrackList#addtrack 6763 */ 6764 addTrack(track) { 6765 super.addTrack(track); 6766 if (!this.queueChange_) { 6767 this.queueChange_ = () => this.queueTrigger('change'); 6768 } 6769 if (!this.triggerSelectedlanguagechange) { 6770 this.triggerSelectedlanguagechange_ = () => this.trigger('selectedlanguagechange'); 6771 } 6772 6773 /** 6774 * @listens TextTrack#modechange 6775 * @fires TrackList#change 6776 */ 6777 track.addEventListener('modechange', this.queueChange_); 6778 const nonLanguageTextTrackKind = ['metadata', 'chapters']; 6779 if (nonLanguageTextTrackKind.indexOf(track.kind) === -1) { 6780 track.addEventListener('modechange', this.triggerSelectedlanguagechange_); 6781 } 6782 } 6783 removeTrack(rtrack) { 6784 super.removeTrack(rtrack); 6785 6786 // manually remove the event handlers we added 6787 if (rtrack.removeEventListener) { 6788 if (this.queueChange_) { 6789 rtrack.removeEventListener('modechange', this.queueChange_); 6790 } 6791 if (this.selectedlanguagechange_) { 6792 rtrack.removeEventListener('modechange', this.triggerSelectedlanguagechange_); 6793 } 6794 } 6795 } 6796 } 6797 6798 /** 6799 * @file html-track-element-list.js 6800 */ 6801 6802 /** 6803 * The current list of {@link HtmlTrackElement}s. 6804 */ 6805 class HtmlTrackElementList { 6806 /** 6807 * Create an instance of this class. 6808 * 6809 * @param {HtmlTrackElement[]} [tracks=[]] 6810 * A list of `HtmlTrackElement` to instantiate the list with. 6811 */ 6812 constructor(trackElements = []) { 6813 this.trackElements_ = []; 6814 6815 /** 6816 * @memberof HtmlTrackElementList 6817 * @member {number} length 6818 * The current number of `Track`s in the this Trackist. 6819 * @instance 6820 */ 6821 Object.defineProperty(this, 'length', { 6822 get() { 6823 return this.trackElements_.length; 6824 } 6825 }); 6826 for (let i = 0, length = trackElements.length; i < length; i++) { 6827 this.addTrackElement_(trackElements[i]); 6828 } 6829 } 6830 6831 /** 6832 * Add an {@link HtmlTrackElement} to the `HtmlTrackElementList` 6833 * 6834 * @param {HtmlTrackElement} trackElement 6835 * The track element to add to the list. 6836 * 6837 * @private 6838 */ 6839 addTrackElement_(trackElement) { 6840 const index = this.trackElements_.length; 6841 if (!('' + index in this)) { 6842 Object.defineProperty(this, index, { 6843 get() { 6844 return this.trackElements_[index]; 6845 } 6846 }); 6847 } 6848 6849 // Do not add duplicate elements 6850 if (this.trackElements_.indexOf(trackElement) === -1) { 6851 this.trackElements_.push(trackElement); 6852 } 6853 } 6854 6855 /** 6856 * Get an {@link HtmlTrackElement} from the `HtmlTrackElementList` given an 6857 * {@link TextTrack}. 6858 * 6859 * @param {TextTrack} track 6860 * The track associated with a track element. 6861 * 6862 * @return {HtmlTrackElement|undefined} 6863 * The track element that was found or undefined. 6864 * 6865 * @private 6866 */ 6867 getTrackElementByTrack_(track) { 6868 let trackElement_; 6869 for (let i = 0, length = this.trackElements_.length; i < length; i++) { 6870 if (track === this.trackElements_[i].track) { 6871 trackElement_ = this.trackElements_[i]; 6872 break; 6873 } 6874 } 6875 return trackElement_; 6876 } 6877 6878 /** 6879 * Remove a {@link HtmlTrackElement} from the `HtmlTrackElementList` 6880 * 6881 * @param {HtmlTrackElement} trackElement 6882 * The track element to remove from the list. 6883 * 6884 * @private 6885 */ 6886 removeTrackElement_(trackElement) { 6887 for (let i = 0, length = this.trackElements_.length; i < length; i++) { 6888 if (trackElement === this.trackElements_[i]) { 6889 if (this.trackElements_[i].track && typeof this.trackElements_[i].track.off === 'function') { 6890 this.trackElements_[i].track.off(); 6891 } 6892 if (typeof this.trackElements_[i].off === 'function') { 6893 this.trackElements_[i].off(); 6894 } 6895 this.trackElements_.splice(i, 1); 6896 break; 6897 } 6898 } 6899 } 6900 } 6901 6902 /** 6903 * @file text-track-cue-list.js 6904 */ 6905 6906 /** 6907 * @typedef {Object} TextTrackCueList~TextTrackCue 6908 * 6909 * @property {string} id 6910 * The unique id for this text track cue 6911 * 6912 * @property {number} startTime 6913 * The start time for this text track cue 6914 * 6915 * @property {number} endTime 6916 * The end time for this text track cue 6917 * 6918 * @property {boolean} pauseOnExit 6919 * Pause when the end time is reached if true. 6920 * 6921 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackcue} 6922 */ 6923 6924 /** 6925 * A List of TextTrackCues. 6926 * 6927 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackcuelist} 6928 */ 6929 class TextTrackCueList { 6930 /** 6931 * Create an instance of this class.. 6932 * 6933 * @param {Array} cues 6934 * A list of cues to be initialized with 6935 */ 6936 constructor(cues) { 6937 TextTrackCueList.prototype.setCues_.call(this, cues); 6938 6939 /** 6940 * @memberof TextTrackCueList 6941 * @member {number} length 6942 * The current number of `TextTrackCue`s in the TextTrackCueList. 6943 * @instance 6944 */ 6945 Object.defineProperty(this, 'length', { 6946 get() { 6947 return this.length_; 6948 } 6949 }); 6950 } 6951 6952 /** 6953 * A setter for cues in this list. Creates getters 6954 * an an index for the cues. 6955 * 6956 * @param {Array} cues 6957 * An array of cues to set 6958 * 6959 * @private 6960 */ 6961 setCues_(cues) { 6962 const oldLength = this.length || 0; 6963 let i = 0; 6964 const l = cues.length; 6965 this.cues_ = cues; 6966 this.length_ = cues.length; 6967 const defineProp = function (index) { 6968 if (!('' + index in this)) { 6969 Object.defineProperty(this, '' + index, { 6970 get() { 6971 return this.cues_[index]; 6972 } 6973 }); 6974 } 6975 }; 6976 if (oldLength < l) { 6977 i = oldLength; 6978 for (; i < l; i++) { 6979 defineProp.call(this, i); 6980 } 6981 } 6982 } 6983 6984 /** 6985 * Get a `TextTrackCue` that is currently in the `TextTrackCueList` by id. 6986 * 6987 * @param {string} id 6988 * The id of the cue that should be searched for. 6989 * 6990 * @return {TextTrackCueList~TextTrackCue|null} 6991 * A single cue or null if none was found. 6992 */ 6993 getCueById(id) { 6994 let result = null; 6995 for (let i = 0, l = this.length; i < l; i++) { 6996 const cue = this[i]; 6997 if (cue.id === id) { 6998 result = cue; 6999 break; 7000 } 7001 } 7002 return result; 7003 } 7004 } 7005 7006 /** 7007 * @file track-kinds.js 7008 */ 7009 7010 /** 7011 * All possible `VideoTrackKind`s 7012 * 7013 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-videotrack-kind 7014 * @typedef VideoTrack~Kind 7015 * @enum 7016 */ 7017 const VideoTrackKind = { 7018 alternative: 'alternative', 7019 captions: 'captions', 7020 main: 'main', 7021 sign: 'sign', 7022 subtitles: 'subtitles', 7023 commentary: 'commentary' 7024 }; 7025 7026 /** 7027 * All possible `AudioTrackKind`s 7028 * 7029 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-audiotrack-kind 7030 * @typedef AudioTrack~Kind 7031 * @enum 7032 */ 7033 const AudioTrackKind = { 7034 'alternative': 'alternative', 7035 'descriptions': 'descriptions', 7036 'main': 'main', 7037 'main-desc': 'main-desc', 7038 'translation': 'translation', 7039 'commentary': 'commentary' 7040 }; 7041 7042 /** 7043 * All possible `TextTrackKind`s 7044 * 7045 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-texttrack-kind 7046 * @typedef TextTrack~Kind 7047 * @enum 7048 */ 7049 const TextTrackKind = { 7050 subtitles: 'subtitles', 7051 captions: 'captions', 7052 descriptions: 'descriptions', 7053 chapters: 'chapters', 7054 metadata: 'metadata' 7055 }; 7056 7057 /** 7058 * All possible `TextTrackMode`s 7059 * 7060 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackmode 7061 * @typedef TextTrack~Mode 7062 * @enum 7063 */ 7064 const TextTrackMode = { 7065 disabled: 'disabled', 7066 hidden: 'hidden', 7067 showing: 'showing' 7068 }; 7069 7070 /** 7071 * @file track.js 7072 */ 7073 7074 /** 7075 * A Track class that contains all of the common functionality for {@link AudioTrack}, 7076 * {@link VideoTrack}, and {@link TextTrack}. 7077 * 7078 * > Note: This class should not be used directly 7079 * 7080 * @see {@link https://html.spec.whatwg.org/multipage/embedded-content.html} 7081 * @extends EventTarget 7082 * @abstract 7083 */ 7084 class Track extends EventTarget$2 { 7085 /** 7086 * Create an instance of this class. 7087 * 7088 * @param {Object} [options={}] 7089 * Object of option names and values 7090 * 7091 * @param {string} [options.kind=''] 7092 * A valid kind for the track type you are creating. 7093 * 7094 * @param {string} [options.id='vjs_track_' + Guid.newGUID()] 7095 * A unique id for this AudioTrack. 7096 * 7097 * @param {string} [options.label=''] 7098 * The menu label for this track. 7099 * 7100 * @param {string} [options.language=''] 7101 * A valid two character language code. 7102 * 7103 * @abstract 7104 */ 7105 constructor(options = {}) { 7106 super(); 7107 const trackProps = { 7108 id: options.id || 'vjs_track_' + newGUID(), 7109 kind: options.kind || '', 7110 language: options.language || '' 7111 }; 7112 let label = options.label || ''; 7113 7114 /** 7115 * @memberof Track 7116 * @member {string} id 7117 * The id of this track. Cannot be changed after creation. 7118 * @instance 7119 * 7120 * @readonly 7121 */ 7122 7123 /** 7124 * @memberof Track 7125 * @member {string} kind 7126 * The kind of track that this is. Cannot be changed after creation. 7127 * @instance 7128 * 7129 * @readonly 7130 */ 7131 7132 /** 7133 * @memberof Track 7134 * @member {string} language 7135 * The two letter language code for this track. Cannot be changed after 7136 * creation. 7137 * @instance 7138 * 7139 * @readonly 7140 */ 7141 7142 for (const key in trackProps) { 7143 Object.defineProperty(this, key, { 7144 get() { 7145 return trackProps[key]; 7146 }, 7147 set() {} 7148 }); 7149 } 7150 7151 /** 7152 * @memberof Track 7153 * @member {string} label 7154 * The label of this track. Cannot be changed after creation. 7155 * @instance 7156 * 7157 * @fires Track#labelchange 7158 */ 7159 Object.defineProperty(this, 'label', { 7160 get() { 7161 return label; 7162 }, 7163 set(newLabel) { 7164 if (newLabel !== label) { 7165 label = newLabel; 7166 7167 /** 7168 * An event that fires when label changes on this track. 7169 * 7170 * > Note: This is not part of the spec! 7171 * 7172 * @event Track#labelchange 7173 * @type {Event} 7174 */ 7175 this.trigger('labelchange'); 7176 } 7177 } 7178 }); 7179 } 7180 } 7181 7182 /** 7183 * @file url.js 7184 * @module url 7185 */ 7186 7187 /** 7188 * @typedef {Object} url:URLObject 7189 * 7190 * @property {string} protocol 7191 * The protocol of the url that was parsed. 7192 * 7193 * @property {string} hostname 7194 * The hostname of the url that was parsed. 7195 * 7196 * @property {string} port 7197 * The port of the url that was parsed. 7198 * 7199 * @property {string} pathname 7200 * The pathname of the url that was parsed. 7201 * 7202 * @property {string} search 7203 * The search query of the url that was parsed. 7204 * 7205 * @property {string} hash 7206 * The hash of the url that was parsed. 7207 * 7208 * @property {string} host 7209 * The host of the url that was parsed. 7210 */ 7211 7212 /** 7213 * Resolve and parse the elements of a URL. 7214 * 7215 * @function 7216 * @param {String} url 7217 * The url to parse 7218 * 7219 * @return {url:URLObject} 7220 * An object of url details 7221 */ 7222 const parseUrl = function (url) { 7223 // This entire method can be replace with URL once we are able to drop IE11 7224 7225 const props = ['protocol', 'hostname', 'port', 'pathname', 'search', 'hash', 'host']; 7226 7227 // add the url to an anchor and let the browser parse the URL 7228 const a = document.createElement('a'); 7229 a.href = url; 7230 7231 // Copy the specific URL properties to a new object
7232 // This is also needed for IE because the anchor loses its 7233 // properties when it's removed from the dom 7234 const details = {}; 7235 for (let i = 0; i < props.length; i++) { 7236 details[props[i]] = a[props[i]]; 7237 } 7238 7239 // IE adds the port to the host property unlike everyone else. If 7240 // a port identifier is added for standard ports, strip it. 7241 if (details.protocol === 'http:') { 7242 details.host = details.host.replace(/:80$/, ''); 7243 } 7244 if (details.protocol === 'https:') { 7245 details.host = details.host.replace(/:443$/, ''); 7246 } 7247 if (!details.protocol) { 7248 details.protocol = window.location.protocol; 7249 } 7250 7251 /* istanbul ignore if */ 7252 if (!details.host) { 7253 details.host = window.location.host; 7254 } 7255 return details; 7256 }; 7257 7258 /** 7259 * Get absolute version of relative URL. 7260 * 7261 * @function 7262 * @param {string} url 7263 * URL to make absolute 7264 * 7265 * @return {string} 7266 * Absolute URL 7267 * 7268 * @see http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue 7269 */ 7270 const getAbsoluteURL = function (url) { 7271 // Check if absolute URL 7272 if (!url.match(/^https?:\/\//)) { 7273 // Add the url to an anchor and let the browser parse it to convert to an absolute url 7274 const a = document.createElement('a'); 7275 a.href = url; 7276 url = a.href; 7277 } 7278 return url; 7279 }; 7280 7281 /** 7282 * Returns the extension of the passed file name. It will return an empty string 7283 * if passed an invalid path. 7284 * 7285 * @function 7286 * @param {string} path 7287 * The fileName path like '/path/to/file.mp4' 7288 * 7289 * @return {string} 7290 * The extension in lower case or an empty string if no 7291 * extension could be found. 7292 */ 7293 const getFileExtension = function (path) { 7294 if (typeof path === 'string') { 7295 const splitPathRe = /^(\/?)([\s\S]*?)((?:\.{1,2}|[^\/]+?)(\.([^\.\/\?]+)))(?:[\/]*|[\?].*)$/; 7296 const pathParts = splitPathRe.exec(path); 7297 if (pathParts) { 7298 return pathParts.pop().toLowerCase(); 7299 } 7300 } 7301 return ''; 7302 }; 7303 7304 /** 7305 * Returns whether the url passed is a cross domain request or not. 7306 * 7307 * @function 7308 * @param {string} url 7309 * The url to check. 7310 * 7311 * @param {Object} [winLoc] 7312 * the domain to check the url against, defaults to window.location 7313 * 7314 * @param {string} [winLoc.protocol] 7315 * The window location protocol defaults to window.location.protocol 7316 * 7317 * @param {string} [winLoc.host] 7318 * The window location host defaults to window.location.host 7319 * 7320 * @return {boolean} 7321 * Whether it is a cross domain request or not. 7322 */ 7323 const isCrossOrigin = function (url, winLoc = window.location) { 7324 const urlInfo = parseUrl(url); 7325 7326 // IE8 protocol relative urls will return ':' for protocol 7327 const srcProtocol = urlInfo.protocol === ':' ? winLoc.protocol : urlInfo.protocol; 7328 7329 // Check if url is for another domain/origin 7330 // IE8 doesn't know location.origin, so we won't rely on it here 7331 const crossOrigin = srcProtocol + urlInfo.host !== winLoc.protocol + winLoc.host; 7332 return crossOrigin; 7333 }; 7334 7335 var Url = /*#__PURE__*/Object.freeze({ 7336 __proto__: null, 7337 parseUrl: parseUrl, 7338 getAbsoluteURL: getAbsoluteURL, 7339 getFileExtension: getFileExtension, 7340 isCrossOrigin: isCrossOrigin 7341 }); 7342 7343 var win; 7344 if (typeof window !== "undefined") { 7345 win = window; 7346 } else if (typeof commonjsGlobal !== "undefined") { 7347 win = commonjsGlobal; 7348 } else if (typeof self !== "undefined") { 7349 win = self; 7350 } else { 7351 win = {}; 7352 } 7353 var window_1 = win; 7354 7355 var _extends_1 = createCommonjsModule(function (module) { 7356 function _extends() { 7357 module.exports = _extends = Object.assign ? Object.assign.bind() : function (target) { 7358 for (var i = 1; i < arguments.length; i++) { 7359 var source = arguments[i]; 7360 for (var key in source) { 7361 if (Object.prototype.hasOwnProperty.call(source, key)) { 7362 target[key] = source[key]; 7363 } 7364 } 7365 } 7366 return target; 7367 }, module.exports.__esModule = true, module.exports["default"] = module.exports; 7368 return _extends.apply(this, arguments); 7369 } 7370 module.exports = _extends, module.exports.__esModule = true, module.exports["default"] = module.exports; 7371 }); 7372 var _extends$1 = unwrapExports(_extends_1); 7373 7374 var isFunction_1 = isFunction; 7375 var toString = Object.prototype.toString; 7376 function isFunction(fn) { 7377 if (!fn) { 7378 return false;
vendor: 4,288 bytes, lines 7379-7510
7379 } 7380 var string = toString.call(fn); 7381 return string === '[object Function]' || typeof fn === 'function' && string !== '[object RegExp]' || typeof window !== 'undefined' && ( 7382 // IE8 and below 7383 fn === window.setTimeout || fn === window.alert || fn === window.confirm || fn === window.prompt); 7384 } 7385 7386 var httpResponseHandler = function httpResponseHandler(callback, decodeResponseBody) { 7387 if (decodeResponseBody === void 0) { 7388 decodeResponseBody = false; 7389 } 7390 return function (err, response, responseBody) { 7391 // if the XHR failed, return that error 7392 if (err) { 7393 callback(err); 7394 return; 7395 } // if the HTTP status code is 4xx or 5xx, the request also failed 7396 7397 if (response.statusCode >= 400 && response.statusCode <= 599) { 7398 var cause = responseBody; 7399 if (decodeResponseBody) { 7400 if (window_1.TextDecoder) { 7401 var charset = getCharset(response.headers && response.headers['content-type']); 7402 try { 7403 cause = new TextDecoder(charset).decode(responseBody); 7404 } catch (e) {} 7405 } else { 7406 cause = String.fromCharCode.apply(null, new Uint8Array(responseBody)); 7407 } 7408 } 7409 callback({ 7410 cause: cause 7411 }); 7412 return; 7413 } // otherwise, request succeeded 7414 7415 callback(null, responseBody); 7416 }; 7417 }; 7418 function getCharset(contentTypeHeader) { 7419 if (contentTypeHeader === void 0) { 7420 contentTypeHeader = ''; 7421 } 7422 return contentTypeHeader.toLowerCase().split(';').reduce(function (charset, contentType) { 7423 var _contentType$split = contentType.split('='), 7424 type = _contentType$split[0], 7425 value = _contentType$split[1]; 7426 if (type.trim() === 'charset') { 7427 return value.trim(); 7428 } 7429 return charset; 7430 }, 'utf-8'); 7431 } 7432 var httpHandler = httpResponseHandler; 7433 7434 createXHR.httpHandler = httpHandler; 7435 /** 7436 * @license 7437 * slighly modified parse-headers 2.0.2 <https://github.com/kesla/parse-headers/> 7438 * Copyright (c) 2014 David Björklund 7439 * Available under the MIT license 7440 * <https://github.com/kesla/parse-headers/blob/master/LICENCE> 7441 */ 7442 7443 var parseHeaders = function parseHeaders(headers) { 7444 var result = {}; 7445 if (!headers) { 7446 return result; 7447 } 7448 headers.trim().split('\n').forEach(function (row) { 7449 var index = row.indexOf(':'); 7450 var key = row.slice(0, index).trim().toLowerCase(); 7451 var value = row.slice(index + 1).trim(); 7452 if (typeof result[key] === 'undefined') { 7453 result[key] = value; 7454 } else if (Array.isArray(result[key])) { 7455 result[key].push(value); 7456 } else { 7457 result[key] = [result[key], value]; 7458 } 7459 }); 7460 return result; 7461 }; 7462 var lib = createXHR; // Allow use of default import syntax in TypeScript 7463 7464 var default_1 = createXHR; 7465 createXHR.XMLHttpRequest = window_1.XMLHttpRequest || noop$1; 7466 createXHR.XDomainRequest = "withCredentials" in new createXHR.XMLHttpRequest() ? createXHR.XMLHttpRequest : window_1.XDomainRequest; 7467 forEachArray(["get", "put", "post", "patch", "head", "delete"], function (method) { 7468 createXHR[method === "delete" ? "del" : method] = function (uri, options, callback) { 7469 options = initParams(uri, options, callback); 7470 options.method = method.toUpperCase(); 7471 return _createXHR(options); 7472 }; 7473 }); 7474 function forEachArray(array, iterator) { 7475 for (var i = 0; i < array.length; i++) { 7476 iterator(array[i]); 7477 } 7478 } 7479 function isEmpty(obj) { 7480 for (var i in obj) { 7481 if (obj.hasOwnProperty(i)) return false; 7482 } 7483 return true; 7484 } 7485 function initParams(uri, options, callback) { 7486 var params = uri; 7487 if (isFunction_1(options)) { 7488 callback = options; 7489 if (typeof uri === "string") { 7490 params = { 7491 uri: uri 7492 }; 7493 } 7494 } else { 7495 params = _extends_1({}, options, { 7496 uri: uri 7497 }); 7498 } 7499 params.callback = callback; 7500 return params; 7501 } 7502 function createXHR(uri, options, callback) { 7503 options = initParams(uri, options, callback); 7504 return _createXHR(options); 7505 } 7506 function _createXHR(options) { 7507 if (typeof options.callback === "undefined") { 7508 throw new Error("callback argument missing"); 7509 } 7510 var called = false;
vendor: 4,654 bytes, lines 7511-7651
7511 var callback = function cbOnce(err, response, body) { 7512 if (!called) { 7513 called = true; 7514 options.callback(err, response, body); 7515 } 7516 }; 7517 function readystatechange() { 7518 if (xhr.readyState === 4) { 7519 setTimeout(loadFunc, 0); 7520 } 7521 } 7522 function getBody() { 7523 // Chrome with requestType=blob throws errors arround when even testing access to responseText 7524 var body = undefined; 7525 if (xhr.response) { 7526 body = xhr.response; 7527 } else { 7528 body = xhr.responseText || getXml(xhr); 7529 } 7530 if (isJson) { 7531 try { 7532 body = JSON.parse(body); 7533 } catch (e) {} 7534 } 7535 return body; 7536 } 7537 function errorFunc(evt) { 7538 clearTimeout(timeoutTimer); 7539 if (!(evt instanceof Error)) { 7540 evt = new Error("" + (evt || "Unknown XMLHttpRequest Error")); 7541 } 7542 evt.statusCode = 0; 7543 return callback(evt, failureResponse); 7544 } // will load the data & process the response in a special response object 7545 7546 function loadFunc() { 7547 if (aborted) return; 7548 var status; 7549 clearTimeout(timeoutTimer); 7550 if (options.useXDR && xhr.status === undefined) { 7551 //IE8 CORS GET successful response doesn't have a status field, but body is fine 7552 status = 200; 7553 } else { 7554 status = xhr.status === 1223 ? 204 : xhr.status; 7555 } 7556 var response = failureResponse; 7557 var err = null; 7558 if (status !== 0) { 7559 response = { 7560 body: getBody(), 7561 statusCode: status, 7562 method: method, 7563 headers: {}, 7564 url: uri, 7565 rawRequest: xhr 7566 }; 7567 if (xhr.getAllResponseHeaders) { 7568 //remember xhr can in fact be XDR for CORS in IE 7569 response.headers = parseHeaders(xhr.getAllResponseHeaders()); 7570 } 7571 } else { 7572 err = new Error("Internal XMLHttpRequest Error"); 7573 } 7574 return callback(err, response, response.body); 7575 } 7576 var xhr = options.xhr || null; 7577 if (!xhr) { 7578 if (options.cors || options.useXDR) { 7579 xhr = new createXHR.XDomainRequest(); 7580 } else { 7581 xhr = new createXHR.XMLHttpRequest(); 7582 } 7583 } 7584 var key; 7585 var aborted; 7586 var uri = xhr.url = options.uri || options.url; 7587 var method = xhr.method = options.method || "GET"; 7588 var body = options.body || options.data; 7589 var headers = xhr.headers = options.headers || {}; 7590 var sync = !!options.sync; 7591 var isJson = false; 7592 var timeoutTimer; 7593 var failureResponse = { 7594 body: undefined, 7595 headers: {}, 7596 statusCode: 0, 7597 method: method, 7598 url: uri, 7599 rawRequest: xhr 7600 }; 7601 if ("json" in options && options.json !== false) { 7602 isJson = true; 7603 headers["accept"] || headers["Accept"] || (headers["Accept"] = "application/json"); //Don't override existing accept header declared by user 7604 7605 if (method !== "GET" && method !== "HEAD") { 7606 headers["content-type"] || headers["Content-Type"] || (headers["Content-Type"] = "application/json"); //Don't override existing accept header declared by user 7607 7608 body = JSON.stringify(options.json === true ? body : options.json); 7609 } 7610 } 7611 xhr.onreadystatechange = readystatechange; 7612 xhr.onload = loadFunc; 7613 xhr.onerror = errorFunc; // IE9 must have onprogress be set to a unique function. 7614 7615 xhr.onprogress = function () {// IE must die 7616 }; 7617 xhr.onabort = function () { 7618 aborted = true; 7619 }; 7620 xhr.ontimeout = errorFunc; 7621 xhr.open(method, uri, !sync, options.username, options.password); //has to be after open 7622 7623 if (!sync) { 7624 xhr.withCredentials = !!options.withCredentials; 7625 } // Cannot set timeout with sync request 7626 // not setting timeout on the xhr object, because of old webkits etc. not handling that correctly 7627 // both npm's request and jquery 1.x use this kind of timeout, so this is being consistent 7628 7629 if (!sync && options.timeout > 0) { 7630 timeoutTimer = setTimeout(function () { 7631 if (aborted) return; 7632 aborted = true; //IE9 may still call readystatechange 7633 7634 xhr.abort("timeout"); 7635 var e = new Error("XMLHttpRequest timeout"); 7636 e.code = "ETIMEDOUT"; 7637 errorFunc(e); 7638 }, options.timeout); 7639 } 7640 if (xhr.setRequestHeader) { 7641 for (key in headers) { 7642 if (headers.hasOwnProperty(key)) { 7643 xhr.setRequestHeader(key, headers[key]); 7644 } 7645 } 7646 } else if (options.headers && !isEmpty(options.headers)) { 7647 throw new Error("Headers cannot be set on an XDomainRequest object"); 7648 } 7649 if ("responseType" in options) { 7650 xhr.responseType = options.responseType; 7651 }
vendor: 5,048 bytes, lines 7652-7812
7652 if ("beforeSend" in options && typeof options.beforeSend === "function") { 7653 options.beforeSend(xhr); 7654 } // Microsoft Edge browser sends "undefined" when send is called with undefined value. 7655 // XMLHttpRequest spec says to pass null as body to indicate no body 7656 // See https://github.com/naugtur/xhr/issues/100. 7657 7658 xhr.send(body || null); 7659 return xhr; 7660 } 7661 function getXml(xhr) { 7662 // xhr.responseXML will throw Exception "InvalidStateError" or "DOMException" 7663 // See https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseXML. 7664 try { 7665 if (xhr.responseType === "document") { 7666 return xhr.responseXML; 7667 } 7668 var firefoxBugTakenEffect = xhr.responseXML && xhr.responseXML.documentElement.nodeName === "parsererror"; 7669 if (xhr.responseType === "" && !firefoxBugTakenEffect) { 7670 return xhr.responseXML; 7671 } 7672 } catch (e) {} 7673 return null; 7674 } 7675 function noop$1() {} 7676 lib.default = default_1; 7677 7678 /** 7679 * @file text-track.js 7680 */ 7681 7682 /** 7683 * Takes a webvtt file contents and parses it into cues 7684 * 7685 * @param {string} srcContent 7686 * webVTT file contents 7687 * 7688 * @param {TextTrack} track 7689 * TextTrack to add cues to. Cues come from the srcContent. 7690 * 7691 * @private 7692 */ 7693 const parseCues = function (srcContent, track) { 7694 const parser = new window.WebVTT.Parser(window, window.vttjs, window.WebVTT.StringDecoder()); 7695 const errors = []; 7696 parser.oncue = function (cue) { 7697 track.addCue(cue); 7698 }; 7699 parser.onparsingerror = function (error) { 7700 errors.push(error); 7701 }; 7702 parser.onflush = function () { 7703 track.trigger({ 7704 type: 'loadeddata', 7705 target: track 7706 }); 7707 }; 7708 parser.parse(srcContent); 7709 if (errors.length > 0) { 7710 if (window.console && window.console.groupCollapsed) { 7711 window.console.groupCollapsed(`Text Track parsing errors for ${track.src}`); 7712 } 7713 errors.forEach(error => log$1.error(error)); 7714 if (window.console && window.console.groupEnd) { 7715 window.console.groupEnd(); 7716 } 7717 } 7718 parser.flush(); 7719 }; 7720 7721 /** 7722 * Load a `TextTrack` from a specified url. 7723 * 7724 * @param {string} src 7725 * Url to load track from. 7726 * 7727 * @param {TextTrack} track 7728 * Track to add cues to. Comes from the content at the end of `url`. 7729 * 7730 * @private 7731 */ 7732 const loadTrack = function (src, track) { 7733 const opts = { 7734 uri: src 7735 }; 7736 const crossOrigin = isCrossOrigin(src); 7737 if (crossOrigin) { 7738 opts.cors = crossOrigin; 7739 } 7740 const withCredentials = track.tech_.crossOrigin() === 'use-credentials'; 7741 if (withCredentials) { 7742 opts.withCredentials = withCredentials; 7743 } 7744 lib(opts, bind_(this, function (err, response, responseBody) { 7745 if (err) { 7746 return log$1.error(err, response); 7747 } 7748 track.loaded_ = true; 7749 7750 // Make sure that vttjs has loaded, otherwise, wait till it finished loading 7751 // NOTE: this is only used for the alt/video.novtt.js build 7752 if (typeof window.WebVTT !== 'function') { 7753 if (track.tech_) { 7754 // to prevent use before define eslint error, we define loadHandler 7755 // as a let here 7756 track.tech_.any(['vttjsloaded', 'vttjserror'], event => { 7757 if (event.type === 'vttjserror') { 7758 log$1.error(`vttjs failed to load, stopping trying to process ${track.src}`); 7759 return; 7760 } 7761 return parseCues(responseBody, track); 7762 }); 7763 } 7764 } else { 7765 parseCues(responseBody, track); 7766 } 7767 })); 7768 }; 7769 7770 /** 7771 * A representation of a single `TextTrack`. 7772 * 7773 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrack} 7774 * @extends Track 7775 */ 7776 class TextTrack extends Track { 7777 /** 7778 * Create an instance of this class. 7779 * 7780 * @param {Object} options={} 7781 * Object of option names and values 7782 * 7783 * @param { import('../tech/tech').default } options.tech 7784 * A reference to the tech that owns this TextTrack. 7785 * 7786 * @param {TextTrack~Kind} [options.kind='subtitles'] 7787 * A valid text track kind. 7788 * 7789 * @param {TextTrack~Mode} [options.mode='disabled'] 7790 * A valid text track mode. 7791 * 7792 * @param {string} [options.id='vjs_track_' + Guid.newGUID()] 7793 * A unique id for this TextTrack. 7794 * 7795 * @param {string} [options.label=''] 7796 * The menu label for this track. 7797 * 7798 * @param {string} [options.language=''] 7799 * A valid two character language code. 7800 * 7801 * @param {string} [options.srclang=''] 7802 * A valid two character language code. An alternative, but deprioritized 7803 * version of `options.language` 7804 * 7805 * @param {string} [options.src] 7806 * A url to TextTrack cues. 7807 * 7808 * @param {boolean} [options.default] 7809 * If this track should default to on or off. 7810 */ 7811 constructor(options = {}) { 7812 if (!options.tech) {
vendor: 13,413 bytes, lines 7813-8240
7813 throw new Error('A tech was not provided.'); 7814 } 7815 const settings = merge$2(options, { 7816 kind: TextTrackKind[options.kind] || 'subtitles', 7817 language: options.language || options.srclang || '' 7818 }); 7819 let mode = TextTrackMode[settings.mode] || 'disabled'; 7820 const default_ = settings.default; 7821 if (settings.kind === 'metadata' || settings.kind === 'chapters') { 7822 mode = 'hidden'; 7823 } 7824 super(settings); 7825 this.tech_ = settings.tech; 7826 this.cues_ = []; 7827 this.activeCues_ = []; 7828 this.preload_ = this.tech_.preloadTextTracks !== false; 7829 const cues = new TextTrackCueList(this.cues_); 7830 const activeCues = new TextTrackCueList(this.activeCues_); 7831 let changed = false; 7832 this.timeupdateHandler = bind_(this, function (event = {}) { 7833 if (this.tech_.isDisposed()) { 7834 return; 7835 } 7836 if (!this.tech_.isReady_) { 7837 if (event.type !== 'timeupdate') { 7838 this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler); 7839 } 7840 return; 7841 } 7842 7843 // Accessing this.activeCues for the side-effects of updating itself 7844 // due to its nature as a getter function. Do not remove or cues will 7845 // stop updating! 7846 // Use the setter to prevent deletion from uglify (pure_getters rule) 7847 this.activeCues = this.activeCues; 7848 if (changed) { 7849 this.trigger('cuechange'); 7850 changed = false; 7851 } 7852 if (event.type !== 'timeupdate') { 7853 this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler); 7854 } 7855 }); 7856 const disposeHandler = () => { 7857 this.stopTracking(); 7858 }; 7859 this.tech_.one('dispose', disposeHandler); 7860 if (mode !== 'disabled') { 7861 this.startTracking(); 7862 } 7863 Object.defineProperties(this, { 7864 /** 7865 * @memberof TextTrack 7866 * @member {boolean} default 7867 * If this track was set to be on or off by default. Cannot be changed after 7868 * creation. 7869 * @instance 7870 * 7871 * @readonly 7872 */ 7873 default: { 7874 get() { 7875 return default_; 7876 }, 7877 set() {} 7878 }, 7879 /** 7880 * @memberof TextTrack 7881 * @member {string} mode 7882 * Set the mode of this TextTrack to a valid {@link TextTrack~Mode}. Will 7883 * not be set if setting to an invalid mode. 7884 * @instance 7885 * 7886 * @fires TextTrack#modechange 7887 */ 7888 mode: { 7889 get() { 7890 return mode; 7891 }, 7892 set(newMode) { 7893 if (!TextTrackMode[newMode]) { 7894 return; 7895 } 7896 if (mode === newMode) { 7897 return; 7898 } 7899 mode = newMode; 7900 if (!this.preload_ && mode !== 'disabled' && this.cues.length === 0) { 7901 // On-demand load. 7902 loadTrack(this.src, this); 7903 } 7904 this.stopTracking(); 7905 if (mode !== 'disabled') { 7906 this.startTracking(); 7907 } 7908 /** 7909 * An event that fires when mode changes on this track. This allows 7910 * the TextTrackList that holds this track to act accordingly. 7911 * 7912 * > Note: This is not part of the spec! 7913 * 7914 * @event TextTrack#modechange 7915 * @type {Event} 7916 */ 7917 this.trigger('modechange'); 7918 } 7919 }, 7920 /** 7921 * @memberof TextTrack 7922 * @member {TextTrackCueList} cues 7923 * The text track cue list for this TextTrack. 7924 * @instance 7925 */ 7926 cues: { 7927 get() { 7928 if (!this.loaded_) { 7929 return null; 7930 } 7931 return cues; 7932 }, 7933 set() {} 7934 }, 7935 /** 7936 * @memberof TextTrack 7937 * @member {TextTrackCueList} activeCues 7938 * The list text track cues that are currently active for this TextTrack. 7939 * @instance 7940 */ 7941 activeCues: { 7942 get() { 7943 if (!this.loaded_) { 7944 return null; 7945 } 7946 7947 // nothing to do 7948 if (this.cues.length === 0) { 7949 return activeCues; 7950 } 7951 const ct = this.tech_.currentTime(); 7952 const active = []; 7953 for (let i = 0, l = this.cues.length; i < l; i++) { 7954 const cue = this.cues[i]; 7955 if (cue.startTime <= ct && cue.endTime >= ct) { 7956 active.push(cue); 7957 } 7958 } 7959 changed = false; 7960 if (active.length !== this.activeCues_.length) { 7961 changed = true; 7962 } else { 7963 for (let i = 0; i < active.length; i++) { 7964 if (this.activeCues_.indexOf(active[i]) === -1) { 7965 changed = true; 7966 } 7967 } 7968 } 7969 this.activeCues_ = active; 7970 activeCues.setCues_(this.activeCues_); 7971 return activeCues; 7972 }, 7973 // /!\ Keep this setter empty (see the timeupdate handler above) 7974 set() {} 7975 } 7976 }); 7977 if (settings.src) { 7978 this.src = settings.src; 7979 if (!this.preload_) { 7980 // Tracks will load on-demand. 7981 // Act like we're loaded for other purposes. 7982 this.loaded_ = true; 7983 } 7984 if (this.preload_ || settings.kind !== 'subtitles' && settings.kind !== 'captions') { 7985 loadTrack(this.src, this); 7986 } 7987 } else { 7988 this.loaded_ = true; 7989 } 7990 } 7991 startTracking() { 7992 // More precise cues based on requestVideoFrameCallback with a requestAnimationFram fallback 7993 this.rvf_ = this.tech_.requestVideoFrameCallback(this.timeupdateHandler); 7994 // Also listen to timeupdate in case rVFC/rAF stops (window in background, audio in video el) 7995 this.tech_.on('timeupdate', this.timeupdateHandler); 7996 } 7997 stopTracking() { 7998 if (this.rvf_) { 7999 this.tech_.cancelVideoFrameCallback(this.rvf_); 8000 this.rvf_ = undefined; 8001 } 8002 this.tech_.off('timeupdate', this.timeupdateHandler); 8003 } 8004 8005 /** 8006 * Add a cue to the internal list of cues. 8007 * 8008 * @param {TextTrack~Cue} cue 8009 * The cue to add to our internal list 8010 */ 8011 addCue(originalCue) { 8012 let cue = originalCue; 8013 8014 // Testing if the cue is a VTTCue in a way that survives minification 8015 if (!('getCueAsHTML' in cue)) { 8016 cue = new window.vttjs.VTTCue(originalCue.startTime, originalCue.endTime, originalCue.text); 8017 for (const prop in originalCue) { 8018 if (!(prop in cue)) { 8019 cue[prop] = originalCue[prop]; 8020 } 8021 } 8022 8023 // make sure that `id` is copied over 8024 cue.id = originalCue.id; 8025 cue.originalCue_ = originalCue; 8026 } 8027 const tracks = this.tech_.textTracks(); 8028 for (let i = 0; i < tracks.length; i++) { 8029 if (tracks[i] !== this) { 8030 tracks[i].removeCue(cue); 8031 } 8032 } 8033 this.cues_.push(cue); 8034 this.cues.setCues_(this.cues_); 8035 } 8036 8037 /** 8038 * Remove a cue from our internal list 8039 * 8040 * @param {TextTrack~Cue} removeCue 8041 * The cue to remove from our internal list 8042 */ 8043 removeCue(removeCue) { 8044 let i = this.cues_.length; 8045 while (i--) { 8046 const cue = this.cues_[i]; 8047 if (cue === removeCue || cue.originalCue_ && cue.originalCue_ === removeCue) { 8048 this.cues_.splice(i, 1); 8049 this.cues.setCues_(this.cues_); 8050 break; 8051 } 8052 } 8053 } 8054 } 8055 8056 /** 8057 * cuechange - One or more cues in the track have become active or stopped being active. 8058 * @protected 8059 */ 8060 TextTrack.prototype.allowedEvents_ = { 8061 cuechange: 'cuechange' 8062 }; 8063 8064 /** 8065 * A representation of a single `AudioTrack`. If it is part of an {@link AudioTrackList} 8066 * only one `AudioTrack` in the list will be enabled at a time. 8067 * 8068 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotrack} 8069 * @extends Track 8070 */ 8071 class AudioTrack extends Track { 8072 /** 8073 * Create an instance of this class. 8074 * 8075 * @param {Object} [options={}] 8076 * Object of option names and values 8077 * 8078 * @param {AudioTrack~Kind} [options.kind=''] 8079 * A valid audio track kind 8080 * 8081 * @param {string} [options.id='vjs_track_' + Guid.newGUID()] 8082 * A unique id for this AudioTrack. 8083 * 8084 * @param {string} [options.label=''] 8085 * The menu label for this track. 8086 * 8087 * @param {string} [options.language=''] 8088 * A valid two character language code. 8089 * 8090 * @param {boolean} [options.enabled] 8091 * If this track is the one that is currently playing. If this track is part of 8092 * an {@link AudioTrackList}, only one {@link AudioTrack} will be enabled. 8093 */ 8094 constructor(options = {}) { 8095 const settings = merge$2(options, { 8096 kind: AudioTrackKind[options.kind] || '' 8097 }); 8098 super(settings); 8099 let enabled = false; 8100 8101 /** 8102 * @memberof AudioTrack 8103 * @member {boolean} enabled 8104 * If this `AudioTrack` is enabled or not. When setting this will 8105 * fire {@link AudioTrack#enabledchange} if the state of enabled is changed. 8106 * @instance 8107 * 8108 * @fires VideoTrack#selectedchange 8109 */ 8110 Object.defineProperty(this, 'enabled', { 8111 get() { 8112 return enabled; 8113 }, 8114 set(newEnabled) { 8115 // an invalid or unchanged value 8116 if (typeof newEnabled !== 'boolean' || newEnabled === enabled) { 8117 return; 8118 } 8119 enabled = newEnabled; 8120 8121 /** 8122 * An event that fires when enabled changes on this track. This allows 8123 * the AudioTrackList that holds this track to act accordingly. 8124 * 8125 * > Note: This is not part of the spec! Native tracks will do 8126 * this internally without an event. 8127 * 8128 * @event AudioTrack#enabledchange 8129 * @type {Event} 8130 */ 8131 this.trigger('enabledchange'); 8132 } 8133 }); 8134 8135 // if the user sets this track to selected then 8136 // set selected to that true value otherwise 8137 // we keep it false 8138 if (settings.enabled) { 8139 this.enabled = settings.enabled; 8140 } 8141 this.loaded_ = true; 8142 } 8143 } 8144 8145 /** 8146 * A representation of a single `VideoTrack`. 8147 * 8148 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#videotrack} 8149 * @extends Track 8150 */ 8151 class VideoTrack extends Track { 8152 /** 8153 * Create an instance of this class. 8154 * 8155 * @param {Object} [options={}] 8156 * Object of option names and values 8157 * 8158 * @param {string} [options.kind=''] 8159 * A valid {@link VideoTrack~Kind} 8160 * 8161 * @param {string} [options.id='vjs_track_' + Guid.newGUID()] 8162 * A unique id for this AudioTrack. 8163 * 8164 * @param {string} [options.label=''] 8165 * The menu label for this track. 8166 * 8167 * @param {string} [options.language=''] 8168 * A valid two character language code. 8169 * 8170 * @param {boolean} [options.selected] 8171 * If this track is the one that is currently playing. 8172 */ 8173 constructor(options = {}) { 8174 const settings = merge$2(options, { 8175 kind: VideoTrackKind[options.kind] || '' 8176 }); 8177 super(settings); 8178 let selected = false; 8179 8180 /** 8181 * @memberof VideoTrack 8182 * @member {boolean} selected 8183 * If this `VideoTrack` is selected or not. When setting this will 8184 * fire {@link VideoTrack#selectedchange} if the state of selected changed. 8185 * @instance 8186 * 8187 * @fires VideoTrack#selectedchange 8188 */ 8189 Object.defineProperty(this, 'selected', { 8190 get() { 8191 return selected; 8192 }, 8193 set(newSelected) { 8194 // an invalid or unchanged value 8195 if (typeof newSelected !== 'boolean' || newSelected === selected) { 8196 return; 8197 } 8198 selected = newSelected; 8199 8200 /** 8201 * An event that fires when selected changes on this track. This allows 8202 * the VideoTrackList that holds this track to act accordingly. 8203 * 8204 * > Note: This is not part of the spec! Native tracks will do 8205 * this internally without an event. 8206 * 8207 * @event VideoTrack#selectedchange 8208 * @type {Event} 8209 */ 8210 this.trigger('selectedchange'); 8211 } 8212 }); 8213 8214 // if the user sets this track to selected then 8215 // set selected to that true value otherwise 8216 // we keep it false 8217 if (settings.selected) { 8218 this.selected = settings.selected; 8219 } 8220 } 8221 } 8222 8223 /** 8224 * @file html-track-element.js 8225 */ 8226 8227 /** 8228 * A single track represented in the DOM. 8229 * 8230 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#htmltrackelement} 8231 * @extends EventTarget 8232 */ 8233 class HTMLTrackElement extends EventTarget$2 { 8234 /** 8235 * Create an instance of this class. 8236 * 8237 * @param {Object} options={} 8238 * Object of option names and values 8239 * 8240 * @param { import('../tech/tech').default
vendor: 62,173 bytes, lines 8240-10112
8240} options.tech 8241 * A reference to the tech that owns this HTMLTrackElement. 8242 * 8243 * @param {TextTrack~Kind} [options.kind='subtitles'] 8244 * A valid text track kind. 8245 * 8246 * @param {TextTrack~Mode} [options.mode='disabled'] 8247 * A valid text track mode. 8248 * 8249 * @param {string} [options.id='vjs_track_' + Guid.newGUID()] 8250 * A unique id for this TextTrack. 8251 * 8252 * @param {string} [options.label=''] 8253 * The menu label for this track. 8254 * 8255 * @param {string} [options.language=''] 8256 * A valid two character language code. 8257 * 8258 * @param {string} [options.srclang=''] 8259 * A valid two character language code. An alternative, but deprioritized 8260 * version of `options.language` 8261 * 8262 * @param {string} [options.src] 8263 * A url to TextTrack cues. 8264 * 8265 * @param {boolean} [options.default] 8266 * If this track should default to on or off. 8267 */ 8268 constructor(options = {}) { 8269 super(); 8270 let readyState; 8271 const track = new TextTrack(options); 8272 this.kind = track.kind; 8273 this.src = track.src; 8274 this.srclang = track.language; 8275 this.label = track.label; 8276 this.default = track.default; 8277 Object.defineProperties(this, { 8278 /** 8279 * @memberof HTMLTrackElement 8280 * @member {HTMLTrackElement~ReadyState} readyState 8281 * The current ready state of the track element. 8282 * @instance 8283 */ 8284 readyState: { 8285 get() { 8286 return readyState; 8287 } 8288 }, 8289 /** 8290 * @memberof HTMLTrackElement 8291 * @member {TextTrack} track 8292 * The underlying TextTrack object. 8293 * @instance 8294 * 8295 */ 8296 track: { 8297 get() { 8298 return track; 8299 } 8300 } 8301 }); 8302 readyState = HTMLTrackElement.NONE; 8303 8304 /** 8305 * @listens TextTrack#loadeddata 8306 * @fires HTMLTrackElement#load 8307 */ 8308 track.addEventListener('loadeddata', () => { 8309 readyState = HTMLTrackElement.LOADED; 8310 this.trigger({ 8311 type: 'load', 8312 target: this 8313 }); 8314 }); 8315 } 8316 } 8317 8318 /** 8319 * @protected 8320 */ 8321 HTMLTrackElement.prototype.allowedEvents_ = { 8322 load: 'load' 8323 }; 8324 8325 /** 8326 * The text track not loaded state. 8327 * 8328 * @type {number} 8329 * @static 8330 */ 8331 HTMLTrackElement.NONE = 0; 8332 8333 /** 8334 * The text track loading state. 8335 * 8336 * @type {number} 8337 * @static 8338 */ 8339 HTMLTrackElement.LOADING = 1; 8340 8341 /** 8342 * The text track loaded state. 8343 * 8344 * @type {number} 8345 * @static 8346 */ 8347 HTMLTrackElement.LOADED = 2; 8348 8349 /** 8350 * The text track failed to load state. 8351 * 8352 * @type {number} 8353 * @static 8354 */ 8355 HTMLTrackElement.ERROR = 3; 8356 8357 /* 8358 * This file contains all track properties that are used in 8359 * player.js, tech.js, html5.js and possibly other techs in the future. 8360 */ 8361 8362 const NORMAL = { 8363 audio: { 8364 ListClass: AudioTrackList, 8365 TrackClass: AudioTrack, 8366 capitalName: 'Audio' 8367 }, 8368 video: { 8369 ListClass: VideoTrackList, 8370 TrackClass: VideoTrack, 8371 capitalName: 'Video' 8372 }, 8373 text: { 8374 ListClass: TextTrackList, 8375 TrackClass: TextTrack, 8376 capitalName: 'Text' 8377 } 8378 }; 8379 Object.keys(NORMAL).forEach(function (type) { 8380 NORMAL[type].getterName = `${type}Tracks`; 8381 NORMAL[type].privateName = `${type}Tracks_`; 8382 }); 8383 const REMOTE = { 8384 remoteText: { 8385 ListClass: TextTrackList, 8386 TrackClass: TextTrack, 8387 capitalName: 'RemoteText', 8388 getterName: 'remoteTextTracks', 8389 privateName: 'remoteTextTracks_' 8390 }, 8391 remoteTextEl: { 8392 ListClass: HtmlTrackElementList, 8393 TrackClass: HTMLTrackElement, 8394 capitalName: 'RemoteTextTrackEls', 8395 getterName: 'remoteTextTrackEls', 8396 privateName: 'remoteTextTrackEls_' 8397 } 8398 }; 8399 const ALL = Object.assign({}, NORMAL, REMOTE); 8400 REMOTE.names = Object.keys(REMOTE); 8401 NORMAL.names = Object.keys(NORMAL); 8402 ALL.names = [].concat(REMOTE.names).concat(NORMAL.names); 8403 8404 var minDoc = {}; 8405 8406 var topLevel = typeof commonjsGlobal !== 'undefined' ? commonjsGlobal : typeof window !== 'undefined' ? window : {}; 8407 var doccy; 8408 if (typeof document !== 'undefined') { 8409 doccy = document; 8410 } else { 8411 doccy = topLevel['__GLOBAL_DOCUMENT_CACHE@4']; 8412 if (!doccy) { 8413 doccy = topLevel['__GLOBAL_DOCUMENT_CACHE@4'] = minDoc; 8414 } 8415 } 8416 var document_1 = doccy; 8417 8418 /** 8419 * Copyright 2013 vtt.js Contributors 8420 * 8421 * Licensed under the Apache License, Version 2.0 (the "License"); 8422 * you may not use this file except in compliance with the License. 8423 * You may obtain a copy of the License at 8424 * 8425 * http://www.apache.org/licenses/LICENSE-2.0 8426 * 8427 * Unless required by applicable law or agreed to in writing, software 8428 * distributed under the License is distributed on an "AS IS" BASIS, 8429 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8430 * See the License for the specific language governing permissions and 8431 * limitations under the License. 8432 */ 8433 8434 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 8435 /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ 8436 8437 var _objCreate = Object.create || function () { 8438 function F() {} 8439 return function (o) { 8440 if (arguments.length !== 1) { 8441 throw new Error('Object.create shim only accepts one parameter.'); 8442 } 8443 F.prototype = o; 8444 return new F(); 8445 }; 8446 }(); 8447 8448 // Creates a new ParserError object from an errorData object. The errorData 8449 // object should have default code and message properties. The default message 8450 // property can be overriden by passing in a message parameter. 8451 // See ParsingError.Errors below for acceptable errors. 8452 function ParsingError(errorData, message) { 8453 this.name = "ParsingError"; 8454 this.code = errorData.code; 8455 this.message = message || errorData.message; 8456 } 8457 ParsingError.prototype = _objCreate(Error.prototype); 8458 ParsingError.prototype.constructor = ParsingError; 8459 8460 // ParsingError metadata for acceptable ParsingErrors. 8461 ParsingError.Errors = { 8462 BadSignature: { 8463 code: 0, 8464 message: "Malformed WebVTT signature." 8465 }, 8466 BadTimeStamp: { 8467 code: 1, 8468 message: "Malformed time stamp." 8469 } 8470 }; 8471 8472 // Try to parse input as a time stamp. 8473 function parseTimeStamp(input) { 8474 function computeSeconds(h, m, s, f) { 8475 return (h | 0) * 3600 + (m | 0) * 60 + (s | 0) + (f | 0) / 1000; 8476 } 8477 var m = input.match(/^(\d+):(\d{1,2})(:\d{1,2})?\.(\d{3})/); 8478 if (!m) { 8479 return null; 8480 } 8481 if (m[3]) { 8482 // Timestamp takes the form of [hours]:[minutes]:[seconds].[milliseconds] 8483 return computeSeconds(m[1], m[2], m[3].replace(":", ""), m[4]); 8484 } else if (m[1] > 59) { 8485 // Timestamp takes the form of [hours]:[minutes].[milliseconds] 8486 // First position is hours as it's over 59. 8487 return computeSeconds(m[1], m[2], 0, m[4]); 8488 } else { 8489 // Timestamp takes the form of [minutes]:[seconds].[milliseconds] 8490 return computeSeconds(0, m[1], m[2], m[4]); 8491 } 8492 } 8493 8494 // A settings object holds key/value pairs and will ignore anything but the first 8495 // assignment to a specific key. 8496 function Settings() { 8497 this.values = _objCreate(null); 8498 } 8499 Settings.prototype = { 8500 // Only accept the first assignment to any key. 8501 set: function (k, v) { 8502 if (!this.get(k) && v !== "") { 8503 this.values[k] = v; 8504 } 8505 }, 8506 // Return the value for a key, or a default value. 8507 // If 'defaultKey' is passed then 'dflt' is assumed to be an object with 8508 // a number of possible default values as properties where 'defaultKey' is 8509 // the key of the property that will be chosen; otherwise it's assumed to be 8510 // a single value. 8511 get: function (k, dflt, defaultKey) { 8512 if (defaultKey) { 8513 return this.has(k) ? this.values[k] : dflt[defaultKey]; 8514 } 8515 return this.has(k) ? this.values[k] : dflt; 8516 }, 8517 // Check whether we have a value for a key. 8518 has: function (k) { 8519 return k in this.values; 8520 }, 8521 // Accept a setting if its one of the given alternatives. 8522 alt: function (k, v, a) { 8523 for (var n = 0; n < a.length; ++n) { 8524 if (v === a[n]) { 8525 this.set(k, v); 8526 break; 8527 } 8528 } 8529 }, 8530 // Accept a setting if its a valid (signed) integer. 8531 integer: function (k, v) { 8532 if (/^-?\d+$/.test(v)) { 8533 // integer 8534 this.set(k, parseInt(v, 10)); 8535 } 8536 }, 8537 // Accept a setting if its a valid percentage. 8538 percent: function (k, v) { 8539 if (v.match(/^([\d]{1,3})(\.[\d]*)?%$/)) { 8540 v = parseFloat(v); 8541 if (v >= 0 && v <= 100) { 8542 this.set(k, v); 8543 return true; 8544 } 8545 } 8546 return false; 8547 } 8548 }; 8549 8550 // Helper function to parse input into groups separated by 'groupDelim', and 8551 // interprete each group as a key/value pair separated by 'keyValueDelim'. 8552 function parseOptions(input, callback, keyValueDelim, groupDelim) { 8553 var groups = groupDelim ? input.split(groupDelim) : [input]; 8554 for (var i in groups) { 8555 if (typeof groups[i] !== "string") { 8556 continue; 8557 } 8558 var kv = groups[i].split(keyValueDelim); 8559 if (kv.length !== 2) { 8560 continue; 8561 } 8562 var k = kv[0].trim(); 8563 var v = kv[1].trim(); 8564 callback(k, v); 8565 } 8566 } 8567 function parseCue(input, cue, regionList) { 8568 // Remember the original input if we need to throw an error. 8569 var oInput = input; 8570 // 4.1 WebVTT timestamp 8571 function consumeTimeStamp() { 8572 var ts = parseTimeStamp(input); 8573 if (ts === null) { 8574 throw new ParsingError(ParsingError.Errors.BadTimeStamp, "Malformed timestamp: " + oInput); 8575 } 8576 // Remove time stamp from input. 8577 input = input.replace(/^[^\sa-zA-Z-]+/, ""); 8578 return ts; 8579 } 8580 8581 // 4.4.2 WebVTT cue settings 8582 function consumeCueSettings(input, cue) { 8583 var settings = new Settings(); 8584 parseOptions(input, function (k, v) { 8585 switch (k) { 8586 case "region": 8587 // Find the last region we parsed with the same region id. 8588 for (var i = regionList.length - 1; i >= 0; i--) { 8589 if (regionList[i].id === v) { 8590 settings.set(k, regionList[i].region); 8591 break; 8592 } 8593 } 8594 break; 8595 case "vertical": 8596 settings.alt(k, v, ["rl", "lr"]); 8597 break; 8598 case "line": 8599 var vals = v.split(","), 8600 vals0 = vals[0]; 8601 settings.integer(k, vals0); 8602 settings.percent(k, vals0) ? settings.set("snapToLines", false) : null; 8603 settings.alt(k, vals0, ["auto"]); 8604 if (vals.length === 2) { 8605 settings.alt("lineAlign", vals[1], ["start", "center", "end"]); 8606 } 8607 break; 8608 case "position": 8609 vals = v.split(","); 8610 settings.percent(k, vals[0]); 8611 if (vals.length === 2) { 8612 settings.alt("positionAlign", vals[1], ["start", "center", "end"]); 8613 } 8614 break; 8615 case "size": 8616 settings.percent(k, v); 8617 break; 8618 case "align": 8619 settings.alt(k, v, ["start", "center", "end", "left", "right"]); 8620 break; 8621 } 8622 }, /:/, /\s/); 8623 8624 // Apply default values for any missing fields. 8625 cue.region = settings.get("region", null); 8626 cue.vertical = settings.get("vertical", ""); 8627 try { 8628 cue.line = settings.get("line", "auto"); 8629 } catch (e) {} 8630 cue.lineAlign = settings.get("lineAlign", "start"); 8631 cue.snapToLines = settings.get("snapToLines", true); 8632 cue.size = settings.get("size", 100); 8633 // Safari still uses the old middle value and won't accept center 8634 try { 8635 cue.align = settings.get("align", "center"); 8636 } catch (e) { 8637 cue.align = settings.get("align", "middle"); 8638 } 8639 try { 8640 cue.position = settings.get("position", "auto"); 8641 } catch (e) { 8642 cue.position = settings.get("position", { 8643 start: 0, 8644 left: 0, 8645 center: 50, 8646 middle: 50, 8647 end: 100, 8648 right: 100 8649 }, cue.align); 8650 } 8651 cue.positionAlign = settings.get("positionAlign", { 8652 start: "start", 8653 left: "start", 8654 center: "center", 8655 middle: "center", 8656 end: "end", 8657 right: "end" 8658 }, cue.align); 8659 } 8660 function skipWhitespace() { 8661 input = input.replace(/^\s+/, ""); 8662 } 8663 8664 // 4.1 WebVTT cue timings. 8665 skipWhitespace(); 8666 cue.startTime = consumeTimeStamp(); // (1) collect cue start time 8667 skipWhitespace(); 8668 if (input.substr(0, 3) !== "-->") { 8669 // (3) next characters must match "-->" 8670 throw new ParsingError(ParsingError.Errors.BadTimeStamp, "Malformed time stamp (time stamps must be separated by '-->'): " + oInput); 8671 } 8672 input = input.substr(3); 8673 skipWhitespace(); 8674 cue.endTime = consumeTimeStamp(); // (5) collect cue end time 8675 8676 // 4.1 WebVTT cue settings list. 8677 skipWhitespace(); 8678 consumeCueSettings(input, cue); 8679 } 8680 8681 // When evaluating this file as part of a Webpack bundle for server 8682 // side rendering, `document` is an empty object. 8683 var TEXTAREA_ELEMENT = document_1.createElement && document_1.createElement("textarea"); 8684 var TAG_NAME = { 8685 c: "span", 8686 i: "i", 8687 b: "b", 8688 u: "u", 8689 ruby: "ruby", 8690 rt: "rt", 8691 v: "span", 8692 lang: "span" 8693 }; 8694 8695 // 5.1 default text color 8696 // 5.2 default text background color is equivalent to text color with bg_ prefix 8697 var DEFAULT_COLOR_CLASS = { 8698 white: 'rgba(255,255,255,1)', 8699 lime: 'rgba(0,255,0,1)', 8700 cyan: 'rgba(0,255,255,1)', 8701 red: 'rgba(255,0,0,1)', 8702 yellow: 'rgba(255,255,0,1)', 8703 magenta: 'rgba(255,0,255,1)', 8704 blue: 'rgba(0,0,255,1)', 8705 black: 'rgba(0,0,0,1)' 8706 }; 8707 var TAG_ANNOTATION = { 8708 v: "title", 8709 lang: "lang" 8710 }; 8711 var NEEDS_PARENT = { 8712 rt: "ruby" 8713 }; 8714 8715 // Parse content into a document fragment. 8716 function parseContent(window, input) { 8717 function nextToken() { 8718 // Check for end-of-string. 8719 if (!input) { 8720 return null; 8721 } 8722 8723 // Consume 'n' characters from the input. 8724 function consume(result) { 8725 input = input.substr(result.length); 8726 return result; 8727 } 8728 var m = input.match(/^([^<]*)(<[^>]*>?)?/); 8729 // If there is some text before the next tag, return it, otherwise return 8730 // the tag. 8731 return consume(m[1] ? m[1] : m[2]); 8732 } 8733 function unescape(s) { 8734 TEXTAREA_ELEMENT.innerHTML = s; 8735 s = TEXTAREA_ELEMENT.textContent; 8736 TEXTAREA_ELEMENT.textContent = ""; 8737 return s; 8738 } 8739 function shouldAdd(current, element) { 8740 return !NEEDS_PARENT[element.localName] || NEEDS_PARENT[element.localName] === current.localName; 8741 } 8742 8743 // Create an element for this tag. 8744 function createElement(type, annotation) { 8745 var tagName = TAG_NAME[type]; 8746 if (!tagName) { 8747 return null; 8748 } 8749 var element = window.document.createElement(tagName); 8750 var name = TAG_ANNOTATION[type]; 8751 if (name && annotation) { 8752 element[name] = annotation.trim(); 8753 } 8754 return element; 8755 } 8756 var rootDiv = window.document.createElement("div"), 8757 current = rootDiv, 8758 t, 8759 tagStack = []; 8760 while ((t = nextToken()) !== null) { 8761 if (t[0] === '<') { 8762 if (t[1] === "/") { 8763 // If the closing tag matches, move back up to the parent node. 8764 if (tagStack.length && tagStack[tagStack.length - 1] === t.substr(2).replace(">", "")) { 8765 tagStack.pop(); 8766 current = current.parentNode; 8767 } 8768 // Otherwise just ignore the end tag. 8769 continue; 8770 } 8771 var ts = parseTimeStamp(t.substr(1, t.length - 2)); 8772 var node; 8773 if (ts) { 8774 // Timestamps are lead nodes as well. 8775 node = window.document.createProcessingInstruction("timestamp", ts); 8776 current.appendChild(node); 8777 continue; 8778 } 8779 var m = t.match(/^<([^.\s/0-9>]+)(\.[^\s\\>]+)?([^>\\]+)?(\\?)>?$/); 8780 // If we can't parse the tag, skip to the next tag. 8781 if (!m) { 8782 continue; 8783 } 8784 // Try to construct an element, and ignore the tag if we couldn't. 8785 node = createElement(m[1], m[3]); 8786 if (!node) { 8787 continue; 8788 } 8789 // Determine if the tag should be added based on the context of where it 8790 // is placed in the cuetext. 8791 if (!shouldAdd(current, node)) { 8792 continue; 8793 } 8794 // Set the class list (as a list of classes, separated by space). 8795 if (m[2]) { 8796 var classes = m[2].split('.'); 8797 classes.forEach(function (cl) { 8798 var bgColor = /^bg_/.test(cl); 8799 // slice out `bg_` if it's a background color 8800 var colorName = bgColor ? cl.slice(3) : cl; 8801 if (DEFAULT_COLOR_CLASS.hasOwnProperty(colorName)) { 8802 var propName = bgColor ? 'background-color' : 'color'; 8803 var propValue = DEFAULT_COLOR_CLASS[colorName]; 8804 node.style[propName] = propValue; 8805 } 8806 }); 8807 node.className = classes.join(' '); 8808 } 8809 // Append the node to the current node, and enter the scope of the new 8810 // node. 8811 tagStack.push(m[1]); 8812 current.appendChild(node); 8813 current = node; 8814 continue; 8815 } 8816 8817 // Text nodes are leaf nodes. 8818 current.appendChild(window.document.createTextNode(unescape(t))); 8819 } 8820 return rootDiv; 8821 } 8822 8823 // This is a list of all the Unicode characters that have a strong 8824 // right-to-left category. What this means is that these characters are 8825 // written right-to-left for sure. It was generated by pulling all the strong 8826 // right-to-left characters out of the Unicode data table. That table can 8827 // found at: http://www.unicode.org/Public/UNIDATA/UnicodeData.txt 8828 var strongRTLRanges = [[0x5be, 0x5be], [0x5c0, 0x5c0], [0x5c3, 0x5c3], [0x5c6, 0x5c6], [0x5d0, 0x5ea], [0x5f0, 0x5f4], [0x608, 0x608], [0x60b, 0x60b], [0x60d, 0x60d], [0x61b, 0x61b], [0x61e, 0x64a], [0x66d, 0x66f], [0x671, 0x6d5], [0x6e5, 0x6e6], [0x6ee, 0x6ef], [0x6fa, 0x70d], [0x70f, 0x710], [0x712, 0x72f], [0x74d, 0x7a5], [0x7b1, 0x7b1], [0x7c0, 0x7ea], [0x7f4, 0x7f5], [0x7fa, 0x7fa], [0x800, 0x815], [0x81a, 0x81a], [0x824, 0x824], [0x828, 0x828], [0x830, 0x83e], [0x840, 0x858], [0x85e, 0x85e], [0x8a0, 0x8a0], [0x8a2, 0x8ac], [0x200f, 0x200f], [0xfb1d, 0xfb1d], [0xfb1f, 0xfb28], [0xfb2a, 0xfb36], [0xfb38, 0xfb3c], [0xfb3e, 0xfb3e], [0xfb40, 0xfb41], [0xfb43, 0xfb44], [0xfb46, 0xfbc1], [0xfbd3, 0xfd3d], [0xfd50, 0xfd8f], [0xfd92, 0xfdc7], [0xfdf0, 0xfdfc], [0xfe70, 0xfe74], [0xfe76, 0xfefc], [0x10800, 0x10805], [0x10808, 0x10808], [0x1080a, 0x10835], [0x10837, 0x10838], [0x1083c, 0x1083c], [0x1083f, 0x10855], [0x10857, 0x1085f], [0x10900, 0x1091b], [0x10920, 0x10939], [0x1093f, 0x1093f], [0x10980, 0x109b7], [0x109be, 0x109bf], [0x10a00, 0x10a00], [0x10a10, 0x10a13], [0x10a15, 0x10a17], [0x10a19, 0x10a33], [0x10a40, 0x10a47], [0x10a50, 0x10a58], [0x10a60, 0x10a7f], [0x10b00, 0x10b35], [0x10b40, 0x10b55], [0x10b58, 0x10b72], [0x10b78, 0x10b7f], [0x10c00, 0x10c48], [0x1ee00, 0x1ee03], [0x1ee05, 0x1ee1f], [0x1ee21, 0x1ee22], [0x1ee24, 0x1ee24], [0x1ee27, 0x1ee27], [0x1ee29, 0x1ee32], [0x1ee34, 0x1ee37], [0x1ee39, 0x1ee39], [0x1ee3b, 0x1ee3b], [0x1ee42, 0x1ee42], [0x1ee47, 0x1ee47], [0x1ee49, 0x1ee49], [0x1ee4b, 0x1ee4b], [0x1ee4d, 0x1ee4f], [0x1ee51, 0x1ee52], [0x1ee54, 0x1ee54], [0x1ee57, 0x1ee57], [0x1ee59, 0x1ee59], [0x1ee5b, 0x1ee5b], [0x1ee5d, 0x1ee5d], [0x1ee5f, 0x1ee5f], [0x1ee61, 0x1ee62], [0x1ee64, 0x1ee64], [0x1ee67, 0x1ee6a], [0x1ee6c, 0x1ee72], [0x1ee74, 0x1ee77], [0x1ee79, 0x1ee7c], [0x1ee7e, 0x1ee7e], [0x1ee80, 0x1ee89], [0x1ee8b, 0x1ee9b], [0x1eea1, 0x1eea3], [0x1eea5, 0x1eea9], [0x1eeab, 0x1eebb], [0x10fffd, 0x10fffd]]; 8829 function isStrongRTLChar(charCode) { 8830 for (var i = 0; i < strongRTLRanges.length; i++) { 8831 var currentRange = strongRTLRanges[i]; 8832 if (charCode >= currentRange[0] && charCode <= currentRange[1]) { 8833 return true; 8834 } 8835 } 8836 return false; 8837 } 8838 function determineBidi(cueDiv) { 8839 var nodeStack = [], 8840 text = "", 8841 charCode; 8842 if (!cueDiv || !cueDiv.childNodes) { 8843 return "ltr"; 8844 } 8845 function pushNodes(nodeStack, node) { 8846 for (var i = node.childNodes.length - 1; i >= 0; i--) { 8847 nodeStack.push(node.childNodes[i]); 8848 } 8849 } 8850 function nextTextNode(nodeStack) { 8851 if (!nodeStack || !nodeStack.length) { 8852 return null; 8853 } 8854 var node = nodeStack.pop(), 8855 text = node.textContent || node.innerText; 8856 if (text) { 8857 // TODO: This should match all unicode type B characters (paragraph 8858 // separator characters). See issue #115. 8859 var m = text.match(/^.*(\n|\r)/); 8860 if (m) { 8861 nodeStack.length = 0; 8862 return m[0]; 8863 } 8864 return text; 8865 } 8866 if (node.tagName === "ruby") { 8867 return nextTextNode(nodeStack); 8868 } 8869 if (node.childNodes) { 8870 pushNodes(nodeStack, node); 8871 return nextTextNode(nodeStack); 8872 } 8873 } 8874 pushNodes(nodeStack, cueDiv); 8875 while (text = nextTextNode(nodeStack)) { 8876 for (var i = 0; i < text.length; i++) { 8877 charCode = text.charCodeAt(i); 8878 if (isStrongRTLChar(charCode)) { 8879 return "rtl"; 8880 } 8881 } 8882 } 8883 return "ltr"; 8884 } 8885 function computeLinePos(cue) { 8886 if (typeof cue.line === "number" && (cue.snapToLines || cue.line >= 0 && cue.line <= 100)) { 8887 return cue.line; 8888 } 8889 if (!cue.track || !cue.track.textTrackList || !cue.track.textTrackList.mediaElement) { 8890 return -1; 8891 } 8892 var track = cue.track, 8893 trackList = track.textTrackList, 8894 count = 0; 8895 for (var i = 0; i < trackList.length && trackList[i] !== track; i++) { 8896 if (trackList[i].mode === "showing") { 8897 count++; 8898 } 8899 } 8900 return ++count * -1; 8901 } 8902 function StyleBox() {} 8903 8904 // Apply styles to a div. If there is no div passed then it defaults to the 8905 // div on 'this'. 8906 StyleBox.prototype.applyStyles = function (styles, div) { 8907 div = div || this.div; 8908 for (var prop in styles) { 8909 if (styles.hasOwnProperty(prop)) { 8910 div.style[prop] = styles[prop]; 8911 } 8912 } 8913 }; 8914 StyleBox.prototype.formatStyle = function (val, unit) { 8915 return val === 0 ? 0 : val + unit; 8916 }; 8917 8918 // Constructs the computed display state of the cue (a div). Places the div 8919 // into the overlay which should be a block level element (usually a div). 8920 function CueStyleBox(window, cue, styleOptions) { 8921 StyleBox.call(this); 8922 this.cue = cue; 8923 8924 // Parse our cue's text into a DOM tree rooted at 'cueDiv'. This div will 8925 // have inline positioning and will function as the cue background box. 8926 this.cueDiv = parseContent(window, cue.text); 8927 var styles = { 8928 color: "rgba(255, 255, 255, 1)", 8929 backgroundColor: "rgba(0, 0, 0, 0.8)", 8930 position: "relative", 8931 left: 0, 8932 right: 0, 8933 top: 0, 8934 bottom: 0, 8935 display: "inline", 8936 writingMode: cue.vertical === "" ? "horizontal-tb" : cue.vertical === "lr" ? "vertical-lr" : "vertical-rl", 8937 unicodeBidi: "plaintext" 8938 }; 8939 this.applyStyles(styles, this.cueDiv); 8940 8941 // Create an absolutely positioned div that will be used to position the cue 8942 // div. Note, all WebVTT cue-setting alignments are equivalent to the CSS 8943 // mirrors of them except middle instead of center on Safari. 8944 this.div = window.document.createElement("div"); 8945 styles = { 8946 direction: determineBidi(this.cueDiv), 8947 writingMode: cue.vertical === "" ? "horizontal-tb" : cue.vertical === "lr" ? "vertical-lr" : "vertical-rl", 8948 unicodeBidi: "plaintext", 8949 textAlign: cue.align === "middle" ? "center" : cue.align, 8950 font: styleOptions.font, 8951 whiteSpace: "pre-line", 8952 position: "absolute" 8953 }; 8954 this.applyStyles(styles); 8955 this.div.appendChild(this.cueDiv); 8956 8957 // Calculate the distance from the reference edge of the viewport to the text 8958 // position of the cue box. The reference edge will be resolved later when 8959 // the box orientation styles are applied. 8960 var textPos = 0; 8961 switch (cue.positionAlign) { 8962 case "start": 8963 case "line-left": 8964 textPos = cue.position; 8965 break; 8966 case "center": 8967 textPos = cue.position - cue.size / 2; 8968 break; 8969 case "end": 8970 case "line-right": 8971 textPos = cue.position - cue.size; 8972 break; 8973 } 8974 8975 // Horizontal box orientation; textPos is the distance from the left edge of the 8976 // area to the left edge of the box and cue.size is the distance extending to 8977 // the right from there. 8978 if (cue.vertical === "") { 8979 this.applyStyles({ 8980 left: this.formatStyle(textPos, "%"), 8981 width: this.formatStyle(cue.size, "%") 8982 }); 8983 // Vertical box orientation; textPos is the distance from the top edge of the 8984 // area to the top edge of the box and cue.size is the height extending 8985 // downwards from there. 8986 } else { 8987 this.applyStyles({ 8988 top: this.formatStyle(textPos, "%"), 8989 height: this.formatStyle(cue.size, "%") 8990 }); 8991 } 8992 this.move = function (box) { 8993 this.applyStyles({ 8994 top: this.formatStyle(box.top, "px"), 8995 bottom: this.formatStyle(box.bottom, "px"), 8996 left: this.formatStyle(box.left, "px"), 8997 right: this.formatStyle(box.right, "px"), 8998 height: this.formatStyle(box.height, "px"), 8999 width: this.formatStyle(box.width, "px") 9000 }); 9001 }; 9002 } 9003 CueStyleBox.prototype = _objCreate(StyleBox.prototype); 9004 CueStyleBox.prototype.constructor = CueStyleBox; 9005 9006 // Represents the co-ordinates of an Element in a way that we can easily 9007 // compute things with such as if it overlaps or intersects with another Element. 9008 // Can initialize it with either a StyleBox or another BoxPosition. 9009 function BoxPosition(obj) { 9010 // Either a BoxPosition was passed in and we need to copy it, or a StyleBox 9011 // was passed in and we need to copy the results of 'getBoundingClientRect' 9012 // as the object returned is readonly. All co-ordinate values are in reference 9013 // to the viewport origin (top left). 9014 var lh, height, width, top; 9015 if (obj.div) { 9016 height = obj.div.offsetHeight; 9017 width = obj.div.offsetWidth; 9018 top = obj.div.offsetTop; 9019 var rects = (rects = obj.div.childNodes) && (rects = rects[0]) && rects.getClientRects && rects.getClientRects(); 9020 obj = obj.div.getBoundingClientRect(); 9021 // In certain cases the outter div will be slightly larger then the sum of 9022 // the inner div's lines. This could be due to bold text, etc, on some platforms. 9023 // In this case we should get the average line height and use that. This will 9024 // result in the desired behaviour. 9025 lh = rects ? Math.max(rects[0] && rects[0].height || 0, obj.height / rects.length) : 0; 9026 } 9027 this.left = obj.left; 9028 this.right = obj.right; 9029 this.top = obj.top || top; 9030 this.height = obj.height || height; 9031 this.bottom = obj.bottom || top + (obj.height || height); 9032 this.width = obj.width || width; 9033 this.lineHeight = lh !== undefined ? lh : obj.lineHeight; 9034 } 9035 9036 // Move the box along a particular axis. Optionally pass in an amount to move 9037 // the box. If no amount is passed then the default is the line height of the 9038 // box. 9039 BoxPosition.prototype.move = function (axis, toMove) { 9040 toMove = toMove !== undefined ? toMove : this.lineHeight; 9041 switch (axis) { 9042 case "+x": 9043 this.left += toMove; 9044 this.right += toMove; 9045 break; 9046 case "-x": 9047 this.left -= toMove; 9048 this.right -= toMove; 9049 break; 9050 case "+y": 9051 this.top += toMove; 9052 this.bottom += toMove; 9053 break; 9054 case "-y": 9055 this.top -= toMove; 9056 this.bottom -= toMove; 9057 break; 9058 } 9059 }; 9060 9061 // Check if this box overlaps another box, b2. 9062 BoxPosition.prototype.overlaps = function (b2) { 9063 return this.left < b2.right && this.right > b2.left && this.top < b2.bottom && this.bottom > b2.top; 9064 }; 9065 9066 // Check if this box overlaps any other boxes in boxes. 9067 BoxPosition.prototype.overlapsAny = function (boxes) { 9068 for (var i = 0; i < boxes.length; i++) { 9069 if (this.overlaps(boxes[i])) { 9070 return true; 9071 } 9072 } 9073 return false; 9074 }; 9075 9076 // Check if this box is within another box. 9077 BoxPosition.prototype.within = function (container) { 9078 return this.top >= container.top && this.bottom <= container.bottom && this.left >= container.left && this.right <= container.right; 9079 }; 9080 9081 // Check if this box is entirely within the container or it is overlapping 9082 // on the edge opposite of the axis direction passed. For example, if "+x" is 9083 // passed and the box is overlapping on the left edge of the container, then 9084 // return true. 9085 BoxPosition.prototype.overlapsOppositeAxis = function (container, axis) { 9086 switch (axis) { 9087 case "+x": 9088 return this.left < container.left; 9089 case "-x": 9090 return this.right > container.right; 9091 case "+y": 9092 return this.top < container.top; 9093 case "-y": 9094 return this.bottom > container.bottom; 9095 } 9096 }; 9097 9098 // Find the percentage of the area that this box is overlapping with another 9099 // box. 9100 BoxPosition.prototype.intersectPercentage = function (b2) { 9101 var x = Math.max(0, Math.min(this.right, b2.right) - Math.max(this.left, b2.left)), 9102 y = Math.max(0, Math.min(this.bottom, b2.bottom) - Math.max(this.top, b2.top)), 9103 intersectArea = x * y; 9104 return intersectArea / (this.height * this.width); 9105 }; 9106 9107 // Convert the positions from this box to CSS compatible positions using 9108 // the reference container's positions. This has to be done because this 9109 // box's positions are in reference to the viewport origin, whereas, CSS 9110 // values are in referecne to their respective edges. 9111 BoxPosition.prototype.toCSSCompatValues = function (reference) { 9112 return { 9113 top: this.top - reference.top, 9114 bottom: reference.bottom - this.bottom, 9115 left: this.left - reference.left, 9116 right: reference.right - this.right, 9117 height: this.height, 9118 width: this.width 9119 }; 9120 }; 9121 9122 // Get an object that represents the box's position without anything extra. 9123 // Can pass a StyleBox, HTMLElement, or another BoxPositon. 9124 BoxPosition.getSimpleBoxPosition = function (obj) { 9125 var height = obj.div ? obj.div.offsetHeight : obj.tagName ? obj.offsetHeight : 0; 9126 var width = obj.div ? obj.div.offsetWidth : obj.tagName ? obj.offsetWidth : 0; 9127 var top = obj.div ? obj.div.offsetTop : obj.tagName ? obj.offsetTop : 0; 9128 obj = obj.div ? obj.div.getBoundingClientRect() : obj.tagName ? obj.getBoundingClientRect() : obj; 9129 var ret = { 9130 left: obj.left, 9131 right: obj.right, 9132 top: obj.top || top, 9133 height: obj.height || height, 9134 bottom: obj.bottom || top + (obj.height || height), 9135 width: obj.width || width 9136 }; 9137 return ret; 9138 }; 9139 9140 // Move a StyleBox to its specified, or next best, position. The containerBox 9141 // is the box that contains the StyleBox, such as a div. boxPositions are 9142 // a list of other boxes that the styleBox can't overlap with. 9143 function moveBoxToLinePosition(window, styleBox, containerBox, boxPositions) { 9144 // Find the best position for a cue box, b, on the video. The axis parameter 9145 // is a list of axis, the order of which, it will move the box along. For example: 9146 // Passing ["+x", "-x"] will move the box first along the x axis in the positive 9147 // direction. If it doesn't find a good position for it there it will then move 9148 // it along the x axis in the negative direction. 9149 function findBestPosition(b, axis) { 9150 var bestPosition, 9151 specifiedPosition = new BoxPosition(b), 9152 percentage = 1; // Highest possible so the first thing we get is better. 9153 9154 for (var i = 0; i < axis.length; i++) { 9155 while (b.overlapsOppositeAxis(containerBox, axis[i]) || b.within(containerBox) && b.overlapsAny(boxPositions)) { 9156 b.move(axis[i]); 9157 } 9158 // We found a spot where we aren't overlapping anything. This is our 9159 // best position. 9160 if (b.within(containerBox)) { 9161 return b; 9162 } 9163 var p = b.intersectPercentage(containerBox); 9164 // If we're outside the container box less then we were on our last try 9165 // then remember this position as the best position. 9166 if (percentage > p) { 9167 bestPosition = new BoxPosition(b); 9168 percentage = p; 9169 } 9170 // Reset the box position to the specified position. 9171 b = new BoxPosition(specifiedPosition); 9172 } 9173 return bestPosition || specifiedPosition; 9174 } 9175 var boxPosition = new BoxPosition(styleBox), 9176 cue = styleBox.cue, 9177 linePos = computeLinePos(cue), 9178 axis = []; 9179 9180 // If we have a line number to align the cue to. 9181 if (cue.snapToLines) { 9182 var size; 9183 switch (cue.vertical) { 9184 case "": 9185 axis = ["+y", "-y"]; 9186 size = "height"; 9187 break; 9188 case "rl": 9189 axis = ["+x", "-x"]; 9190 size = "width"; 9191 break; 9192 case "lr": 9193 axis = ["-x", "+x"]; 9194 size = "width"; 9195 break; 9196 } 9197 var step = boxPosition.lineHeight, 9198 position = step * Math.round(linePos), 9199 maxPosition = containerBox[size] + step, 9200 initialAxis = axis[0]; 9201 9202 // If the specified intial position is greater then the max position then 9203 // clamp the box to the amount of steps it would take for the box to 9204 // reach the max position. 9205 if (Math.abs(position) > maxPosition) { 9206 position = position < 0 ? -1 : 1; 9207 position *= Math.ceil(maxPosition / step) * step; 9208 } 9209 9210 // If computed line position returns negative then line numbers are 9211 // relative to the bottom of the video instead of the top. Therefore, we 9212 // need to increase our initial position by the length or width of the 9213 // video, depending on the writing direction, and reverse our axis directions. 9214 if (linePos < 0) { 9215 position += cue.vertical === "" ? containerBox.height : containerBox.width; 9216 axis = axis.reverse(); 9217 } 9218 9219 // Move the box to the specified position. This may not be its best 9220 // position. 9221 boxPosition.move(initialAxis, position); 9222 } else { 9223 // If we have a percentage line value for the cue. 9224 var calculatedPercentage = boxPosition.lineHeight / containerBox.height * 100; 9225 switch (cue.lineAlign) { 9226 case "center": 9227 linePos -= calculatedPercentage / 2; 9228 break; 9229 case "end": 9230 linePos -= calculatedPercentage; 9231 break; 9232 } 9233 9234 // Apply initial line position to the cue box. 9235 switch (cue.vertical) { 9236 case "": 9237 styleBox.applyStyles({ 9238 top: styleBox.formatStyle(linePos, "%") 9239 }); 9240 break; 9241 case "rl": 9242 styleBox.applyStyles({ 9243 left: styleBox.formatStyle(linePos, "%") 9244 }); 9245 break; 9246 case "lr": 9247 styleBox.applyStyles({ 9248 right: styleBox.formatStyle(linePos, "%") 9249 }); 9250 break; 9251 } 9252 axis = ["+y", "-x", "+x", "-y"]; 9253 9254 // Get the box position again after we've applied the specified positioning 9255 // to it. 9256 boxPosition = new BoxPosition(styleBox); 9257 } 9258 var bestPosition = findBestPosition(boxPosition, axis); 9259 styleBox.move(bestPosition.toCSSCompatValues(containerBox)); 9260 } 9261 function WebVTT$1() { 9262 // Nothing 9263 } 9264 9265 // Helper to allow strings to be decoded instead of the default binary utf8 data. 9266 WebVTT$1.StringDecoder = function () { 9267 return { 9268 decode: function (data) { 9269 if (!data) { 9270 return ""; 9271 } 9272 if (typeof data !== "string") { 9273 throw new Error("Error - expected string data."); 9274 } 9275 return decodeURIComponent(encodeURIComponent(data)); 9276 } 9277 }; 9278 }; 9279 WebVTT$1.convertCueToDOMTree = function (window, cuetext) { 9280 if (!window || !cuetext) { 9281 return null; 9282 } 9283 return parseContent(window, cuetext); 9284 }; 9285 var FONT_SIZE_PERCENT = 0.05; 9286 var FONT_STYLE = "sans-serif"; 9287 var CUE_BACKGROUND_PADDING = "1.5%"; 9288 9289 // Runs the processing model over the cues and regions passed to it. 9290 // @param overlay A block level element (usually a div) that the computed cues 9291 // and regions will be placed into. 9292 WebVTT$1.processCues = function (window, cues, overlay) { 9293 if (!window || !cues || !overlay) { 9294 return null; 9295 } 9296 9297 // Remove all previous children. 9298 while (overlay.firstChild) { 9299 overlay.removeChild(overlay.firstChild); 9300 } 9301 var paddedOverlay = window.document.createElement("div"); 9302 paddedOverlay.style.position = "absolute"; 9303 paddedOverlay.style.left = "0"; 9304 paddedOverlay.style.right = "0"; 9305 paddedOverlay.style.top = "0"; 9306 paddedOverlay.style.bottom = "0"; 9307 paddedOverlay.style.margin = CUE_BACKGROUND_PADDING; 9308 overlay.appendChild(paddedOverlay); 9309 9310 // Determine if we need to compute the display states of the cues. This could 9311 // be the case if a cue's state has been changed since the last computation or 9312 // if it has not been computed yet. 9313 function shouldCompute(cues) { 9314 for (var i = 0; i < cues.length; i++) { 9315 if (cues[i].hasBeenReset || !cues[i].displayState) { 9316 return true; 9317 } 9318 } 9319 return false; 9320 } 9321 9322 // We don't need to recompute the cues' display states. Just reuse them. 9323 if (!shouldCompute(cues)) { 9324 for (var i = 0; i < cues.length; i++) { 9325 paddedOverlay.appendChild(cues[i].displayState); 9326 } 9327 return; 9328 } 9329 var boxPositions = [], 9330 containerBox = BoxPosition.getSimpleBoxPosition(paddedOverlay), 9331 fontSize = Math.round(containerBox.height * FONT_SIZE_PERCENT * 100) / 100; 9332 var styleOptions = { 9333 font: fontSize + "px " + FONT_STYLE 9334 }; 9335 (function () { 9336 var styleBox, cue; 9337 for (var i = 0; i < cues.length; i++) { 9338 cue = cues[i]; 9339 9340 // Compute the intial position and styles of the cue div. 9341 styleBox = new CueStyleBox(window, cue, styleOptions); 9342 paddedOverlay.appendChild(styleBox.div); 9343 9344 // Move the cue div to it's correct line position. 9345 moveBoxToLinePosition(window, styleBox, containerBox, boxPositions); 9346 9347 // Remember the computed div so that we don't have to recompute it later 9348 // if we don't have too. 9349 cue.displayState = styleBox.div; 9350 boxPositions.push(BoxPosition.getSimpleBoxPosition(styleBox)); 9351 } 9352 })(); 9353 }; 9354 WebVTT$1.Parser = function (window, vttjs, decoder) { 9355 if (!decoder) { 9356 decoder = vttjs; 9357 vttjs = {}; 9358 } 9359 if (!vttjs) { 9360 vttjs = {}; 9361 } 9362 this.window = window; 9363 this.vttjs = vttjs; 9364 this.state = "INITIAL"; 9365 this.buffer = ""; 9366 this.decoder = decoder || new TextDecoder("utf8"); 9367 this.regionList = []; 9368 }; 9369 WebVTT$1.Parser.prototype = { 9370 // If the error is a ParsingError then report it to the consumer if 9371 // possible. If it's not a ParsingError then throw it like normal. 9372 reportOrThrowError: function (e) { 9373 if (e instanceof ParsingError) { 9374 this.onparsingerror && this.onparsingerror(e); 9375 } else { 9376 throw e; 9377 } 9378 }, 9379 parse: function (data) { 9380 var self = this; 9381 9382 // If there is no data then we won't decode it, but will just try to parse 9383 // whatever is in buffer already. This may occur in circumstances, for 9384 // example when flush() is called. 9385 if (data) { 9386 // Try to decode the data that we received. 9387 self.buffer += self.decoder.decode(data, { 9388 stream: true 9389 }); 9390 } 9391 function collectNextLine() { 9392 var buffer = self.buffer; 9393 var pos = 0; 9394 while (pos < buffer.length && buffer[pos] !== '\r' && buffer[pos] !== '\n') { 9395 ++pos; 9396 } 9397 var line = buffer.substr(0, pos); 9398 // Advance the buffer early in case we fail below. 9399 if (buffer[pos] === '\r') { 9400 ++pos; 9401 } 9402 if (buffer[pos] === '\n') { 9403 ++pos; 9404 } 9405 self.buffer = buffer.substr(pos); 9406 return line; 9407 } 9408 9409 // 3.4 WebVTT region and WebVTT region settings syntax 9410 function parseRegion(input) { 9411 var settings = new Settings(); 9412 parseOptions(input, function (k, v) { 9413 switch (k) { 9414 case "id": 9415 settings.set(k, v); 9416 break; 9417 case "width": 9418 settings.percent(k, v); 9419 break; 9420 case "lines": 9421 settings.integer(k, v); 9422 break; 9423 case "regionanchor": 9424 case "viewportanchor": 9425 var xy = v.split(','); 9426 if (xy.length !== 2) { 9427 break; 9428 } 9429 // We have to make sure both x and y parse, so use a temporary 9430 // settings object here. 9431 var anchor = new Settings(); 9432 anchor.percent("x", xy[0]); 9433 anchor.percent("y", xy[1]); 9434 if (!anchor.has("x") || !anchor.has("y")) { 9435 break; 9436 } 9437 settings.set(k + "X", anchor.get("x")); 9438 settings.set(k + "Y", anchor.get("y")); 9439 break; 9440 case "scroll": 9441 settings.alt(k, v, ["up"]); 9442 break; 9443 } 9444 }, /=/, /\s/); 9445 9446 // Create the region, using default values for any values that were not 9447 // specified. 9448 if (settings.has("id")) { 9449 var region = new (self.vttjs.VTTRegion || self.window.VTTRegion)(); 9450 region.width = settings.get("width", 100); 9451 region.lines = settings.get("lines", 3); 9452 region.regionAnchorX = settings.get("regionanchorX", 0); 9453 region.regionAnchorY = settings.get("regionanchorY", 100); 9454 region.viewportAnchorX = settings.get("viewportanchorX", 0); 9455 region.viewportAnchorY = settings.get("viewportanchorY", 100); 9456 region.scroll = settings.get("scroll", ""); 9457 // Register the region. 9458 self.onregion && self.onregion(region); 9459 // Remember the VTTRegion for later in case we parse any VTTCues that 9460 // reference it. 9461 self.regionList.push({ 9462 id: settings.get("id"), 9463 region: region 9464 }); 9465 } 9466 } 9467 9468 // draft-pantos-http-live-streaming-20 9469 // https://tools.ietf.org/html/draft-pantos-http-live-streaming-20#section-3.5 9470 // 3.5 WebVTT 9471 function parseTimestampMap(input) { 9472 var settings = new Settings(); 9473 parseOptions(input, function (k, v) { 9474 switch (k) { 9475 case "MPEGT": 9476 settings.integer(k + 'S', v); 9477 break; 9478 case "LOCA": 9479 settings.set(k + 'L', parseTimeStamp(v)); 9480 break; 9481 } 9482 }, /[^\d]:/, /,/); 9483 self.ontimestampmap && self.ontimestampmap({ 9484 "MPEGTS": settings.get("MPEGTS"), 9485 "LOCAL": settings.get("LOCAL") 9486 }); 9487 } 9488 9489 // 3.2 WebVTT metadata header syntax 9490 function parseHeader(input) { 9491 if (input.match(/X-TIMESTAMP-MAP/)) { 9492 // This line contains HLS X-TIMESTAMP-MAP metadata 9493 parseOptions(input, function (k, v) { 9494 switch (k) { 9495 case "X-TIMESTAMP-MAP": 9496 parseTimestampMap(v); 9497 break; 9498 } 9499 }, /=/); 9500 } else { 9501 parseOptions(input, function (k, v) { 9502 switch (k) { 9503 case "Region": 9504 // 3.3 WebVTT region metadata header syntax 9505 parseRegion(v); 9506 break; 9507 } 9508 }, /:/); 9509 } 9510 } 9511 9512 // 5.1 WebVTT file parsing. 9513 try { 9514 var line; 9515 if (self.state === "INITIAL") { 9516 // We can't start parsing until we have the first line. 9517 if (!/\r\n|\n/.test(self.buffer)) { 9518 return this; 9519 } 9520 line = collectNextLine(); 9521 var m = line.match(/^WEBVTT([ \t].*)?$/); 9522 if (!m || !m[0]) { 9523 throw new ParsingError(ParsingError.Errors.BadSignature); 9524 } 9525 self.state = "HEADER"; 9526 } 9527 var alreadyCollectedLine = false; 9528 while (self.buffer) { 9529 // We can't parse a line until we have the full line. 9530 if (!/\r\n|\n/.test(self.buffer)) { 9531 return this; 9532 } 9533 if (!alreadyCollectedLine) { 9534 line = collectNextLine(); 9535 } else { 9536 alreadyCollectedLine = false; 9537 } 9538 switch (self.state) { 9539 case "HEADER": 9540 // 13-18 - Allow a header (metadata) under the WEBVTT line. 9541 if (/:/.test(line)) { 9542 parseHeader(line); 9543 } else if (!line) { 9544 // An empty line terminates the header and starts the body (cues). 9545 self.state = "ID"; 9546 } 9547 continue; 9548 case "NOTE": 9549 // Ignore NOTE blocks. 9550 if (!line) { 9551 self.state = "ID"; 9552 } 9553 continue; 9554 case "ID": 9555 // Check for the start of NOTE blocks. 9556 if (/^NOTE($|[ \t])/.test(line)) { 9557 self.state = "NOTE"; 9558 break; 9559 } 9560 // 19-29 - Allow any number of line terminators, then initialize new cue values. 9561 if (!line) { 9562 continue; 9563 } 9564 self.cue = new (self.vttjs.VTTCue || self.window.VTTCue)(0, 0, ""); 9565 // Safari still uses the old middle value and won't accept center 9566 try { 9567 self.cue.align = "center"; 9568 } catch (e) { 9569 self.cue.align = "middle"; 9570 } 9571 self.state = "CUE"; 9572 // 30-39 - Check if self line contains an optional identifier or timing data. 9573 if (line.indexOf("-->") === -1) { 9574 self.cue.id = line; 9575 continue; 9576 } 9577 // Process line as start of a cue. 9578 /*falls through*/ 9579 case "CUE": 9580 // 40 - Collect cue timings and settings. 9581 try { 9582 parseCue(line, self.cue, self.regionList); 9583 } catch (e) { 9584 self.reportOrThrowError(e); 9585 // In case of an error ignore rest of the cue. 9586 self.cue = null; 9587 self.state = "BADCUE"; 9588 continue; 9589 } 9590 self.state = "CUETEXT"; 9591 continue; 9592 case "CUETEXT": 9593 var hasSubstring = line.indexOf("-->") !== -1; 9594 // 34 - If we have an empty line then report the cue. 9595 // 35 - If we have the special substring '-->' then report the cue, 9596 // but do not collect the line as we need to process the current 9597 // one as a new cue. 9598 if (!line || hasSubstring && (alreadyCollectedLine = true)) { 9599 // We are done parsing self cue. 9600 self.oncue && self.oncue(self.cue); 9601 self.cue = null; 9602 self.state = "ID"; 9603 continue; 9604 } 9605 if (self.cue.text) { 9606 self.cue.text += "\n"; 9607 } 9608 self.cue.text += line.replace(/\u2028/g, '\n').replace(/u2029/g, '\n'); 9609 continue; 9610 case "BADCUE": 9611 // BADCUE 9612 // 54-62 - Collect and discard the remaining cue. 9613 if (!line) { 9614 self.state = "ID"; 9615 } 9616 continue; 9617 } 9618 } 9619 } catch (e) { 9620 self.reportOrThrowError(e); 9621 9622 // If we are currently parsing a cue, report what we have. 9623 if (self.state === "CUETEXT" && self.cue && self.oncue) { 9624 self.oncue(self.cue); 9625 } 9626 self.cue = null; 9627 // Enter BADWEBVTT state if header was not parsed correctly otherwise 9628 // another exception occurred so enter BADCUE state. 9629 self.state = self.state === "INITIAL" ? "BADWEBVTT" : "BADCUE"; 9630 } 9631 return this; 9632 }, 9633 flush: function () { 9634 var self = this; 9635 try { 9636 // Finish decoding the stream. 9637 self.buffer += self.decoder.decode(); 9638 // Synthesize the end of the current cue or region. 9639 if (self.cue || self.state === "HEADER") { 9640 self.buffer += "\n\n"; 9641 self.parse(); 9642 } 9643 // If we've flushed, parsed, and we're still on the INITIAL state then 9644 // that means we don't have enough of the stream to parse the first 9645 // line. 9646 if (self.state === "INITIAL") { 9647 throw new ParsingError(ParsingError.Errors.BadSignature); 9648 } 9649 } catch (e) { 9650 self.reportOrThrowError(e); 9651 } 9652 self.onflush && self.onflush(); 9653 return this; 9654 } 9655 }; 9656 var vtt = WebVTT$1; 9657 9658 /** 9659 * Copyright 2013 vtt.js Contributors 9660 * 9661 * Licensed under the Apache License, Version 2.0 (the "License"); 9662 * you may not use this file except in compliance with the License. 9663 * You may obtain a copy of the License at 9664 * 9665 * http://www.apache.org/licenses/LICENSE-2.0 9666 * 9667 * Unless required by applicable law or agreed to in writing, software 9668 * distributed under the License is distributed on an "AS IS" BASIS, 9669 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9670 * See the License for the specific language governing permissions and 9671 * limitations under the License. 9672 */ 9673 9674 var autoKeyword = "auto"; 9675 var directionSetting = { 9676 "": 1, 9677 "lr": 1, 9678 "rl": 1 9679 }; 9680 var alignSetting = { 9681 "start": 1, 9682 "center": 1, 9683 "end": 1, 9684 "left": 1, 9685 "right": 1, 9686 "auto": 1, 9687 "line-left": 1, 9688 "line-right": 1 9689 }; 9690 function findDirectionSetting(value) { 9691 if (typeof value !== "string") { 9692 return false; 9693 } 9694 var dir = directionSetting[value.toLowerCase()]; 9695 return dir ? value.toLowerCase() : false; 9696 } 9697 function findAlignSetting(value) { 9698 if (typeof value !== "string") { 9699 return false; 9700 } 9701 var align = alignSetting[value.toLowerCase()]; 9702 return align ? value.toLowerCase() : false; 9703 } 9704 function VTTCue(startTime, endTime, text) { 9705 /** 9706 * Shim implementation specific properties. These properties are not in 9707 * the spec. 9708 */ 9709 9710 // Lets us know when the VTTCue's data has changed in such a way that we need 9711 // to recompute its display state. This lets us compute its display state 9712 // lazily. 9713 this.hasBeenReset = false; 9714 9715 /** 9716 * VTTCue and TextTrackCue properties 9717 * http://dev.w3.org/html5/webvtt/#vttcue-interface 9718 */ 9719 9720 var _id = ""; 9721 var _pauseOnExit = false; 9722 var _startTime = startTime; 9723 var _endTime = endTime; 9724 var _text = text; 9725 var _region = null; 9726 var _vertical = ""; 9727 var _snapToLines = true; 9728 var _line = "auto"; 9729 var _lineAlign = "start"; 9730 var _position = "auto"; 9731 var _positionAlign = "auto"; 9732 var _size = 100; 9733 var _align = "center"; 9734 Object.defineProperties(this, { 9735 "id": { 9736 enumerable: true, 9737 get: function () { 9738 return _id; 9739 }, 9740 set: function (value) { 9741 _id = "" + value; 9742 } 9743 }, 9744 "pauseOnExit": { 9745 enumerable: true, 9746 get: function () { 9747 return _pauseOnExit; 9748 }, 9749 set: function (value) { 9750 _pauseOnExit = !!value; 9751 } 9752 }, 9753 "startTime": { 9754 enumerable: true, 9755 get: function () { 9756 return _startTime; 9757 }, 9758 set: function (value) { 9759 if (typeof value !== "number") { 9760 throw new TypeError("Start time must be set to a number."); 9761 } 9762 _startTime = value; 9763 this.hasBeenReset = true; 9764 } 9765 }, 9766 "endTime": { 9767 enumerable: true, 9768 get: function () { 9769 return _endTime; 9770 }, 9771 set: function (value) { 9772 if (typeof value !== "number") { 9773 throw new TypeError("End time must be set to a number."); 9774 } 9775 _endTime = value; 9776 this.hasBeenReset = true; 9777 } 9778 }, 9779 "text": { 9780 enumerable: true, 9781 get: function () { 9782 return _text; 9783 }, 9784 set: function (value) { 9785 _text = "" + value; 9786 this.hasBeenReset = true; 9787 } 9788 }, 9789 "region": { 9790 enumerable: true, 9791 get: function () { 9792 return _region; 9793 }, 9794 set: function (value) { 9795 _region = value; 9796 this.hasBeenReset = true; 9797 } 9798 }, 9799 "vertical": { 9800 enumerable: true, 9801 get: function () { 9802 return _vertical; 9803 }, 9804 set: function (value) { 9805 var setting = findDirectionSetting(value); 9806 // Have to check for false because the setting an be an empty string. 9807 if (setting === false) { 9808 throw new SyntaxError("Vertical: an invalid or illegal direction string was specified."); 9809 } 9810 _vertical = setting; 9811 this.hasBeenReset = true; 9812 } 9813 }, 9814 "snapToLines": { 9815 enumerable: true, 9816 get: function () { 9817 return _snapToLines; 9818 }, 9819 set: function (value) { 9820 _snapToLines = !!value; 9821 this.hasBeenReset = true; 9822 } 9823 }, 9824 "line": { 9825 enumerable: true, 9826 get: function () { 9827 return _line; 9828 }, 9829 set: function (value) { 9830 if (typeof value !== "number" && value !== autoKeyword) { 9831 throw new SyntaxError("Line: an invalid number or illegal string was specified."); 9832 } 9833 _line = value; 9834 this.hasBeenReset = true; 9835 } 9836 }, 9837 "lineAlign": { 9838 enumerable: true, 9839 get: function () { 9840 return _lineAlign; 9841 }, 9842 set: function (value) { 9843 var setting = findAlignSetting(value); 9844 if (!setting) { 9845 console.warn("lineAlign: an invalid or illegal string was specified."); 9846 } else { 9847 _lineAlign = setting; 9848 this.hasBeenReset = true; 9849 } 9850 } 9851 }, 9852 "position": { 9853 enumerable: true, 9854 get: function () { 9855 return _position; 9856 }, 9857 set: function (value) { 9858 if (value < 0 || value > 100) { 9859 throw new Error("Position must be between 0 and 100."); 9860 } 9861 _position = value; 9862 this.hasBeenReset = true; 9863 } 9864 }, 9865 "positionAlign": { 9866 enumerable: true, 9867 get: function () { 9868 return _positionAlign; 9869 }, 9870 set: function (value) { 9871 var setting = findAlignSetting(value); 9872 if (!setting) { 9873 console.warn("positionAlign: an invalid or illegal string was specified."); 9874 } else { 9875 _positionAlign = setting; 9876 this.hasBeenReset = true; 9877 } 9878 } 9879 }, 9880 "size": { 9881 enumerable: true, 9882 get: function () { 9883 return _size; 9884 }, 9885 set: function (value) { 9886 if (value < 0 || value > 100) { 9887 throw new Error("Size must be between 0 and 100."); 9888 } 9889 _size = value; 9890 this.hasBeenReset = true; 9891 } 9892 }, 9893 "align": { 9894 enumerable: true, 9895 get: function () { 9896 return _align; 9897 }, 9898 set: function (value) { 9899 var setting = findAlignSetting(value); 9900 if (!setting) { 9901 throw new SyntaxError("align: an invalid or illegal alignment string was specified."); 9902 } 9903 _align = setting; 9904 this.hasBeenReset = true; 9905 } 9906 } 9907 }); 9908 9909 /** 9910 * Other <track> spec defined properties 9911 */ 9912 9913 // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-video-element.html#text-track-cue-display-state 9914 this.displayState = undefined; 9915 } 9916 9917 /** 9918 * VTTCue methods 9919 */ 9920 9921 VTTCue.prototype.getCueAsHTML = function () { 9922 // Assume WebVTT.convertCueToDOMTree is on the global. 9923 return WebVTT.convertCueToDOMTree(window, this.text); 9924 }; 9925 var vttcue = VTTCue; 9926 9927 /** 9928 * Copyright 2013 vtt.js Contributors 9929 * 9930 * Licensed under the Apache License, Version 2.0 (the "License"); 9931 * you may not use this file except in compliance with the License. 9932 * You may obtain a copy of the License at 9933 * 9934 * http://www.apache.org/licenses/LICENSE-2.0 9935 * 9936 * Unless required by applicable law or agreed to in writing, software 9937 * distributed under the License is distributed on an "AS IS" BASIS, 9938 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9939 * See the License for the specific language governing permissions and 9940 * limitations under the License. 9941 */ 9942 9943 var scrollSetting = { 9944 "": true, 9945 "up": true 9946 }; 9947 function findScrollSetting(value) { 9948 if (typeof value !== "string") { 9949 return false; 9950 } 9951 var scroll = scrollSetting[value.toLowerCase()]; 9952 return scroll ? value.toLowerCase() : false; 9953 } 9954 function isValidPercentValue(value) { 9955 return typeof value === "number" && value >= 0 && value <= 100; 9956 } 9957 9958 // VTTRegion shim http://dev.w3.org/html5/webvtt/#vttregion-interface 9959 function VTTRegion() { 9960 var _width = 100; 9961 var _lines = 3; 9962 var _regionAnchorX = 0; 9963 var _regionAnchorY = 100; 9964 var _viewportAnchorX = 0; 9965 var _viewportAnchorY = 100; 9966 var _scroll = ""; 9967 Object.defineProperties(this, { 9968 "width": { 9969 enumerable: true, 9970 get: function () { 9971 return _width; 9972 }, 9973 set: function (value) { 9974 if (!isValidPercentValue(value)) { 9975 throw new Error("Width must be between 0 and 100."); 9976 } 9977 _width = value; 9978 } 9979 }, 9980 "lines": { 9981 enumerable: true, 9982 get: function () { 9983 return _lines; 9984 }, 9985 set: function (value) { 9986 if (typeof value !== "number") { 9987 throw new TypeError("Lines must be set to a number."); 9988 } 9989 _lines = value; 9990 } 9991 }, 9992 "regionAnchorY": { 9993 enumerable: true, 9994 get: function () { 9995 return _regionAnchorY; 9996 }, 9997 set: function (value) { 9998 if (!isValidPercentValue(value)) { 9999 throw new Error("RegionAnchorX must be between 0 and 100."); 10000 } 10001 _regionAnchorY = value; 10002 } 10003 }, 10004 "regionAnchorX": { 10005 enumerable: true, 10006 get: function () { 10007 return _regionAnchorX; 10008 }, 10009 set: function (value) { 10010 if (!isValidPercentValue(value)) { 10011 throw new Error("RegionAnchorY must be between 0 and 100."); 10012 } 10013 _regionAnchorX = value; 10014 } 10015 }, 10016 "viewportAnchorY": { 10017 enumerable: true, 10018 get: function () { 10019 return _viewportAnchorY; 10020 }, 10021 set: function (value) { 10022 if (!isValidPercentValue(value)) { 10023 throw new Error("ViewportAnchorY must be between 0 and 100."); 10024 } 10025 _viewportAnchorY = value; 10026 } 10027 }, 10028 "viewportAnchorX": { 10029 enumerable: true, 10030 get: function () { 10031 return _viewportAnchorX; 10032 }, 10033 set: function (value) { 10034 if (!isValidPercentValue(value)) { 10035 throw new Error("ViewportAnchorX must be between 0 and 100."); 10036 } 10037 _viewportAnchorX = value; 10038 } 10039 }, 10040 "scroll": { 10041 enumerable: true, 10042 get: function () { 10043 return _scroll; 10044 }, 10045 set: function (value) { 10046 var setting = findScrollSetting(value); 10047 // Have to check for false as an empty string is a legal value. 10048 if (setting === false) { 10049 console.warn("Scroll: an invalid or illegal string was specified."); 10050 } else { 10051 _scroll = setting; 10052 } 10053 } 10054 } 10055 }); 10056 } 10057 var vttregion = VTTRegion; 10058 10059 var browserIndex = createCommonjsModule(function (module) { 10060 /** 10061 * Copyright 2013 vtt.js Contributors 10062 * 10063 * Licensed under the Apache License, Version 2.0 (the "License"); 10064 * you may not use this file except in compliance with the License. 10065 * You may obtain a copy of the License at 10066 * 10067 * http://www.apache.org/licenses/LICENSE-2.0 10068 * 10069 * Unless required by applicable law or agreed to in writing, software 10070 * distributed under the License is distributed on an "AS IS" BASIS, 10071 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10072 * See the License for the specific language governing permissions and 10073 * limitations under the License. 10074 */ 10075 10076 // Default exports for Node. Export the extended versions of VTTCue and 10077 // VTTRegion in Node since we likely want the capability to convert back and 10078 // forth between JSON. If we don't then it's not that big of a deal since we're 10079 // off browser. 10080 10081 var vttjs = module.exports = { 10082 WebVTT: vtt, 10083 VTTCue: vttcue, 10084 VTTRegion: vttregion 10085 }; 10086 window_1.vttjs = vttjs; 10087 window_1.WebVTT = vttjs.WebVTT; 10088 var cueShim = vttjs.VTTCue; 10089 var regionShim = vttjs.VTTRegion; 10090 var nativeVTTCue = window_1.VTTCue; 10091 var nativeVTTRegion = window_1.VTTRegion; 10092 vttjs.shim = function () { 10093 window_1.VTTCue = cueShim; 10094 window_1.VTTRegion = regionShim; 10095 }; 10096 vttjs.restore = function () { 10097 window_1.VTTCue = nativeVTTCue; 10098 window_1.VTTRegion = nativeVTTRegion; 10099 }; 10100 if (!window_1.VTTCue) { 10101 vttjs.shim(); 10102 } 10103 }); 10104 browserIndex.WebVTT; 10105 browserIndex.VTTCue; 10106 browserIndex.VTTRegion; 10107 10108 /** 10109 * @file tech.js 10110 */ 10111 10112 /**
10112 10113 * An Object containing a structure like: `{src: 'url', type: 'mimetype'}` or string 10114 * that just contains the src url alone. 10115 * * `var SourceObject = {src: 'http://ex.com/video.mp4', type: 'video/mp4'};` 10116 * `var SourceString = 'http://example.com/some-video.mp4';` 10117 * 10118 * @typedef {Object|string} SourceObject 10119 * 10120 * @property {string} src 10121 * The url to the source 10122 * 10123 * @property {string} type 10124 * The mime type of the source 10125 */ 10126 10127 /** 10128 * A function used by {@link Tech} to create a new {@link TextTrack}. 10129 * 10130 * @private 10131 * 10132 * @param {Tech} self 10133 * An instance of the Tech class. 10134 * 10135 * @param {string} kind 10136 * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata) 10137 * 10138 * @param {string} [label] 10139 * Label to identify the text track 10140 * 10141 * @param {string} [language] 10142 * Two letter language abbreviation 10143 * 10144 * @param {Object} [options={}] 10145 * An object with additional text track options 10146 * 10147 * @return {TextTrack} 10148 * The text track that was created. 10149 */ 10150 function createTrackHelper(self, kind, label, language, options = {}) { 10151 const tracks = self.textTracks(); 10152 options.kind = kind; 10153 if (label) { 10154 options.label = label; 10155 } 10156 if (language) { 10157 options.language = language; 10158 } 10159 options.tech = self; 10160 const track = new ALL.text.TrackClass(options); 10161 tracks.addTrack(track); 10162 return track; 10163 } 10164 10165 /** 10166 * This is the base class for media playback technology controllers, such as 10167 * {@link HTML5} 10168 * 10169 * @extends Component 10170 */ 10171 class Tech extends Component$1 { 10172 /** 10173 * Create an instance of this Tech. 10174 * 10175 * @param {Object} [options] 10176 * The key/value store of player options. 10177 * 10178 * @param {Function} [ready] 10179 * Callback function to call when the `HTML5` Tech is ready. 10180 */ 10181 constructor(options = {}, ready = function () {}) { 10182 // we don't want the tech to report user activity automatically.
vendor: 11,419 bytes, lines 10183-10554
10183 // This is done manually in addControlsListeners 10184 options.reportTouchActivity = false; 10185 super(null, options, ready); 10186 this.onDurationChange_ = e => this.onDurationChange(e); 10187 this.trackProgress_ = e => this.trackProgress(e); 10188 this.trackCurrentTime_ = e => this.trackCurrentTime(e); 10189 this.stopTrackingCurrentTime_ = e => this.stopTrackingCurrentTime(e); 10190 this.disposeSourceHandler_ = e => this.disposeSourceHandler(e); 10191 this.queuedHanders_ = new Set(); 10192 10193 // keep track of whether the current source has played at all to 10194 // implement a very limited played() 10195 this.hasStarted_ = false; 10196 this.on('playing', function () { 10197 this.hasStarted_ = true; 10198 }); 10199 this.on('loadstart', function () { 10200 this.hasStarted_ = false; 10201 }); 10202 ALL.names.forEach(name => { 10203 const props = ALL[name]; 10204 if (options && options[props.getterName]) { 10205 this[props.privateName] = options[props.getterName]; 10206 } 10207 }); 10208 10209 // Manually track progress in cases where the browser/tech doesn't report it. 10210 if (!this.featuresProgressEvents) { 10211 this.manualProgressOn(); 10212 } 10213 10214 // Manually track timeupdates in cases where the browser/tech doesn't report it. 10215 if (!this.featuresTimeupdateEvents) { 10216 this.manualTimeUpdatesOn(); 10217 } 10218 ['Text', 'Audio', 'Video'].forEach(track => { 10219 if (options[`native${track}Tracks`] === false) { 10220 this[`featuresNative${track}Tracks`] = false; 10221 } 10222 }); 10223 if (options.nativeCaptions === false || options.nativeTextTracks === false) { 10224 this.featuresNativeTextTracks = false; 10225 } else if (options.nativeCaptions === true || options.nativeTextTracks === true) { 10226 this.featuresNativeTextTracks = true; 10227 } 10228 if (!this.featuresNativeTextTracks) { 10229 this.emulateTextTracks(); 10230 } 10231 this.preloadTextTracks = options.preloadTextTracks !== false; 10232 this.autoRemoteTextTracks_ = new ALL.text.ListClass(); 10233 this.initTrackListeners(); 10234 10235 // Turn on component tap events only if not using native controls 10236 if (!options.nativeControlsForTouch) { 10237 this.emitTapEvents(); 10238 } 10239 if (this.constructor) { 10240 this.name_ = this.constructor.name || 'Unknown Tech'; 10241 } 10242 } 10243 10244 /** 10245 * A special function to trigger source set in a way that will allow player 10246 * to re-trigger if the player or tech are not ready yet. 10247 * 10248 * @fires Tech#sourceset 10249 * @param {string} src The source string at the time of the source changing. 10250 */ 10251 triggerSourceset(src) { 10252 if (!this.isReady_) { 10253 // on initial ready we have to trigger source set 10254 // 1ms after ready so that player can watch for it. 10255 this.one('ready', () => this.setTimeout(() => this.triggerSourceset(src), 1)); 10256 } 10257 10258 /** 10259 * Fired when the source is set on the tech causing the media element 10260 * to reload. 10261 * 10262 * @see {@link Player#event:sourceset} 10263 * @event Tech#sourceset 10264 * @type {Event} 10265 */ 10266 this.trigger({ 10267 src, 10268 type: 'sourceset' 10269 }); 10270 } 10271 10272 /* Fallbacks for unsupported event types 10273 ================================================================================ */ 10274 10275 /** 10276 * Polyfill the `progress` event for browsers that don't support it natively. 10277 * 10278 * @see {@link Tech#trackProgress} 10279 */ 10280 manualProgressOn() { 10281 this.on('durationchange', this.onDurationChange_); 10282 this.manualProgress = true; 10283 10284 // Trigger progress watching when a source begins loading 10285 this.one('ready', this.trackProgress_); 10286 } 10287 10288 /** 10289 * Turn off the polyfill for `progress` events that was created in 10290 * {@link Tech#manualProgressOn} 10291 */ 10292 manualProgressOff() { 10293 this.manualProgress = false; 10294 this.stopTrackingProgress(); 10295 this.off('durationchange', this.onDurationChange_); 10296 } 10297 10298 /** 10299 * This is used to trigger a `progress` event when the buffered percent changes. It 10300 * sets an interval function that will be called every 500 milliseconds to check if the 10301 * buffer end percent has changed. 10302 * 10303 * > This function is called by {@link Tech#manualProgressOn} 10304 * 10305 * @param {Event} event 10306 * The `ready` event that caused this to run. 10307 * 10308 * @listens Tech#ready 10309 * @fires Tech#progress 10310 */ 10311 trackProgress(event) { 10312 this.stopTrackingProgress(); 10313 this.progressInterval = this.setInterval(bind_(this, function () { 10314 // Don't trigger unless buffered amount is greater than last time 10315 10316 const numBufferedPercent = this.bufferedPercent(); 10317 if (this.bufferedPercent_ !== numBufferedPercent) { 10318 /** 10319 * See {@link Player#progress} 10320 * 10321 * @event Tech#progress 10322 * @type {Event} 10323 */ 10324 this.trigger('progress'); 10325 } 10326 this.bufferedPercent_ = numBufferedPercent; 10327 if (numBufferedPercent === 1) { 10328 this.stopTrackingProgress(); 10329 } 10330 }), 500); 10331 } 10332 10333 /** 10334 * Update our internal duration on a `durationchange` event by calling 10335 * {@link Tech#duration}. 10336 * 10337 * @param {Event} event 10338 * The `durationchange` event that caused this to run. 10339 * 10340 * @listens Tech#durationchange 10341 */ 10342 onDurationChange(event) { 10343 this.duration_ = this.duration(); 10344 } 10345 10346 /** 10347 * Get and create a `TimeRange` object for buffering. 10348 * 10349 * @return { import('../utils/time').TimeRange } 10350 * The time range object that was created. 10351 */ 10352 buffered() { 10353 return createTimeRanges$1(0, 0); 10354 } 10355 10356 /** 10357 * Get the percentage of the current video that is currently buffered. 10358 * 10359 * @return {number} 10360 * A number from 0 to 1 that represents the decimal percentage of the 10361 * video that is buffered. 10362 * 10363 */ 10364 bufferedPercent() { 10365 return bufferedPercent(this.buffered(), this.duration_); 10366 } 10367 10368 /** 10369 * Turn off the polyfill for `progress` events that was created in 10370 * {@link Tech#manualProgressOn} 10371 * Stop manually tracking progress events by clearing the interval that was set in 10372 * {@link Tech#trackProgress}. 10373 */ 10374 stopTrackingProgress() { 10375 this.clearInterval(this.progressInterval); 10376 } 10377 10378 /** 10379 * Polyfill the `timeupdate` event for browsers that don't support it. 10380 * 10381 * @see {@link Tech#trackCurrentTime} 10382 */ 10383 manualTimeUpdatesOn() { 10384 this.manualTimeUpdates = true; 10385 this.on('play', this.trackCurrentTime_); 10386 this.on('pause', this.stopTrackingCurrentTime_); 10387 } 10388 10389 /** 10390 * Turn off the polyfill for `timeupdate` events that was created in 10391 * {@link Tech#manualTimeUpdatesOn} 10392 */ 10393 manualTimeUpdatesOff() { 10394 this.manualTimeUpdates = false; 10395 this.stopTrackingCurrentTime(); 10396 this.off('play', this.trackCurrentTime_); 10397 this.off('pause', this.stopTrackingCurrentTime_); 10398 } 10399 10400 /** 10401 * Sets up an interval function to track current time and trigger `timeupdate` every 10402 * 250 milliseconds. 10403 * 10404 * @listens Tech#play 10405 * @triggers Tech#timeupdate 10406 */ 10407 trackCurrentTime() { 10408 if (this.currentTimeInterval) { 10409 this.stopTrackingCurrentTime(); 10410 } 10411 this.currentTimeInterval = this.setInterval(function () { 10412 /** 10413 * Triggered at an interval of 250ms to indicated that time is passing in the video. 10414 * 10415 * @event Tech#timeupdate 10416 * @type {Event} 10417 */ 10418 this.trigger({ 10419 type: 'timeupdate', 10420 target: this, 10421 manuallyTriggered: true 10422 }); 10423 10424 // 42 = 24 fps // 250 is what Webkit uses // FF uses 15 10425 }, 250); 10426 } 10427 10428 /** 10429 * Stop the interval function created in {@link Tech#trackCurrentTime} so that the 10430 * `timeupdate` event is no longer triggered. 10431 * 10432 * @listens {Tech#pause} 10433 */ 10434 stopTrackingCurrentTime() { 10435 this.clearInterval(this.currentTimeInterval); 10436 10437 // #1002 - if the video ends right before the next timeupdate would happen, 10438 // the progress bar won't make it all the way to the end 10439 this.trigger({ 10440 type: 'timeupdate', 10441 target: this, 10442 manuallyTriggered: true 10443 }); 10444 } 10445 10446 /** 10447 * Turn off all event polyfills, clear the `Tech`s {@link AudioTrackList}, 10448 * {@link VideoTrackList}, and {@link TextTrackList}, and dispose of this Tech. 10449 * 10450 * @fires Component#dispose 10451 */ 10452 dispose() { 10453 // clear out all tracks because we can't reuse them between techs 10454 this.clearTracks(NORMAL.names); 10455 10456 // Turn off any manual progress or timeupdate tracking 10457 if (this.manualProgress) { 10458 this.manualProgressOff(); 10459 } 10460 if (this.manualTimeUpdates) { 10461 this.manualTimeUpdatesOff(); 10462 } 10463 super.dispose(); 10464 } 10465 10466 /** 10467 * Clear out a single `TrackList` or an array of `TrackLists` given their names. 10468 * 10469 * > Note: Techs without source handlers should call this between sources for `video` 10470 * & `audio` tracks. You don't want to use them between tracks! 10471 * 10472 * @param {string[]|string} types 10473 * TrackList names to clear, valid names are `video`, `audio`, and 10474 * `text`. 10475 */ 10476 clearTracks(types) { 10477 types = [].concat(types); 10478 // clear out all tracks because we can't reuse them between techs 10479 types.forEach(type => { 10480 const list = this[`${type}Tracks`]() || []; 10481 let i = list.length; 10482 while (i--) { 10483 const track = list[i]; 10484 if (type === 'text') { 10485 this.removeRemoteTextTrack(track); 10486 } 10487 list.removeTrack(track); 10488 } 10489 }); 10490 } 10491 10492 /** 10493 * Remove any TextTracks added via addRemoteTextTrack that are 10494 * flagged for automatic garbage collection 10495 */ 10496 cleanupAutoTextTracks() { 10497 const list = this.autoRemoteTextTracks_ || []; 10498 let i = list.length; 10499 while (i--) { 10500 const track = list[i]; 10501 this.removeRemoteTextTrack(track); 10502 } 10503 } 10504 10505 /** 10506 * Reset the tech, which will removes all sources and reset the internal readyState. 10507 * 10508 * @abstract 10509 */ 10510 reset() {} 10511 10512 /** 10513 * Get the value of `crossOrigin` from the tech. 10514 * 10515 * @abstract 10516 * 10517 * @see {Html5#crossOrigin} 10518 */ 10519 crossOrigin() {} 10520 10521 /** 10522 * Set the value of `crossOrigin` on the tech. 10523 * 10524 * @abstract 10525 * 10526 * @param {string} crossOrigin the crossOrigin value 10527 * @see {Html5#setCrossOrigin} 10528 */ 10529 setCrossOrigin() {} 10530 10531 /** 10532 * Get or set an error on the Tech. 10533 * 10534 * @param {MediaError} [err] 10535 * Error to set on the Tech 10536 * 10537 * @return {MediaError|null} 10538 * The current error object on the tech, or null if there isn't one. 10539 */ 10540 error(err) { 10541 if (err !== undefined) { 10542 this.error_ = new MediaError(err); 10543 this.trigger('error'); 10544 } 10545 return this.error_; 10546 } 10547 10548 /** 10549 * Returns the `TimeRange`s that have been played through for the current source. 10550 * 10551 * > NOTE: This implementation is incomplete. It does not track the played `TimeRange`. 10552 * It only checks whether the source has played at all or not. 10553 * 10554 * @return { import('../utils/time').TimeRange
vendor: 9,042 bytes, lines 10554-10834
10554} 10555 * - A single time range if this video has played 10556 * - An empty set of ranges if not. 10557 */ 10558 played() { 10559 if (this.hasStarted_) { 10560 return createTimeRanges$1(0, 0); 10561 } 10562 return createTimeRanges$1(); 10563 } 10564 10565 /** 10566 * Start playback 10567 * 10568 * @abstract 10569 * 10570 * @see {Html5#play} 10571 */ 10572 play() {} 10573 10574 /** 10575 * Set whether we are scrubbing or not 10576 * 10577 * @abstract 10578 * @param {boolean} _isScrubbing 10579 * - true for we are currently scrubbing 10580 * - false for we are no longer scrubbing 10581 * 10582 * @see {Html5#setScrubbing} 10583 */ 10584 setScrubbing(_isScrubbing) {} 10585 10586 /** 10587 * Get whether we are scrubbing or not 10588 * 10589 * @abstract 10590 * 10591 * @see {Html5#scrubbing} 10592 */ 10593 scrubbing() {} 10594 10595 /** 10596 * Causes a manual time update to occur if {@link Tech#manualTimeUpdatesOn} was 10597 * previously called. 10598 * 10599 * @param {number} _seconds 10600 * Set the current time of the media to this. 10601 * @fires Tech#timeupdate 10602 */ 10603 setCurrentTime(_seconds) { 10604 // improve the accuracy of manual timeupdates 10605 if (this.manualTimeUpdates) { 10606 /** 10607 * A manual `timeupdate` event. 10608 * 10609 * @event Tech#timeupdate 10610 * @type {Event} 10611 */ 10612 this.trigger({ 10613 type: 'timeupdate', 10614 target: this, 10615 manuallyTriggered: true 10616 }); 10617 } 10618 } 10619 10620 /** 10621 * Turn on listeners for {@link VideoTrackList}, {@link {AudioTrackList}, and 10622 * {@link TextTrackList} events. 10623 * 10624 * This adds {@link EventTarget~EventListeners} for `addtrack`, and `removetrack`. 10625 * 10626 * @fires Tech#audiotrackchange 10627 * @fires Tech#videotrackchange 10628 * @fires Tech#texttrackchange 10629 */ 10630 initTrackListeners() { 10631 /** 10632 * Triggered when tracks are added or removed on the Tech {@link AudioTrackList} 10633 * 10634 * @event Tech#audiotrackchange 10635 * @type {Event} 10636 */ 10637 10638 /** 10639 * Triggered when tracks are added or removed on the Tech {@link VideoTrackList} 10640 * 10641 * @event Tech#videotrackchange 10642 * @type {Event} 10643 */ 10644 10645 /** 10646 * Triggered when tracks are added or removed on the Tech {@link TextTrackList} 10647 * 10648 * @event Tech#texttrackchange 10649 * @type {Event} 10650 */ 10651 NORMAL.names.forEach(name => { 10652 const props = NORMAL[name]; 10653 const trackListChanges = () => { 10654 this.trigger(`${name}trackchange`); 10655 }; 10656 const tracks = this[props.getterName](); 10657 tracks.addEventListener('removetrack', trackListChanges); 10658 tracks.addEventListener('addtrack', trackListChanges); 10659 this.on('dispose', () => { 10660 tracks.removeEventListener('removetrack', trackListChanges); 10661 tracks.removeEventListener('addtrack', trackListChanges); 10662 }); 10663 }); 10664 } 10665 10666 /** 10667 * Emulate TextTracks using vtt.js if necessary 10668 * 10669 * @fires Tech#vttjsloaded 10670 * @fires Tech#vttjserror 10671 */ 10672 addWebVttScript_() { 10673 if (window.WebVTT) { 10674 return; 10675 } 10676 10677 // Initially, Tech.el_ is a child of a dummy-div wait until the Component system 10678 // signals that the Tech is ready at which point Tech.el_ is part of the DOM 10679 // before inserting the WebVTT script 10680 if (document.body.contains(this.el())) { 10681 // load via require if available and vtt.js script location was not passed in 10682 // as an option. novtt builds will turn the above require call into an empty object 10683 // which will cause this if check to always fail. 10684 if (!this.options_['vtt.js'] && isPlain(browserIndex) && Object.keys(browserIndex).length > 0) { 10685 this.trigger('vttjsloaded'); 10686 return; 10687 } 10688 10689 // load vtt.js via the script location option or the cdn of no location was 10690 // passed in 10691 const script = document.createElement('script'); 10692 script.src = this.options_['vtt.js'] || 'https://vjs.zencdn.net/vttjs/0.14.1/vtt.min.js'; 10693 script.onload = () => { 10694 /** 10695 * Fired when vtt.js is loaded. 10696 * 10697 * @event Tech#vttjsloaded 10698 * @type {Event} 10699 */ 10700 this.trigger('vttjsloaded'); 10701 }; 10702 script.onerror = () => { 10703 /** 10704 * Fired when vtt.js was not loaded due to an error 10705 * 10706 * @event Tech#vttjsloaded 10707 * @type {Event} 10708 */ 10709 this.trigger('vttjserror'); 10710 }; 10711 this.on('dispose', () => { 10712 script.onload = null; 10713 script.onerror = null; 10714 }); 10715 // but have not loaded yet and we set it to true before the inject so that 10716 // we don't overwrite the injected window.WebVTT if it loads right away 10717 window.WebVTT = true; 10718 this.el().parentNode.appendChild(script); 10719 } else { 10720 this.ready(this.addWebVttScript_); 10721 } 10722 } 10723 10724 /** 10725 * Emulate texttracks 10726 * 10727 */ 10728 emulateTextTracks() { 10729 const tracks = this.textTracks(); 10730 const remoteTracks = this.remoteTextTracks(); 10731 const handleAddTrack = e => tracks.addTrack(e.track); 10732 const handleRemoveTrack = e => tracks.removeTrack(e.track); 10733 remoteTracks.on('addtrack', handleAddTrack); 10734 remoteTracks.on('removetrack', handleRemoveTrack); 10735 this.addWebVttScript_(); 10736 const updateDisplay = () => this.trigger('texttrackchange'); 10737 const textTracksChanges = () => { 10738 updateDisplay(); 10739 for (let i = 0; i < tracks.length; i++) { 10740 const track = tracks[i]; 10741 track.removeEventListener('cuechange', updateDisplay); 10742 if (track.mode === 'showing') { 10743 track.addEventListener('cuechange', updateDisplay); 10744 } 10745 } 10746 }; 10747 textTracksChanges(); 10748 tracks.addEventListener('change', textTracksChanges); 10749 tracks.addEventListener('addtrack', textTracksChanges); 10750 tracks.addEventListener('removetrack', textTracksChanges); 10751 this.on('dispose', function () { 10752 remoteTracks.off('addtrack', handleAddTrack); 10753 remoteTracks.off('removetrack', handleRemoveTrack); 10754 tracks.removeEventListener('change', textTracksChanges); 10755 tracks.removeEventListener('addtrack', textTracksChanges); 10756 tracks.removeEventListener('removetrack', textTracksChanges); 10757 for (let i = 0; i < tracks.length; i++) { 10758 const track = tracks[i]; 10759 track.removeEventListener('cuechange', updateDisplay); 10760 } 10761 }); 10762 } 10763 10764 /** 10765 * Create and returns a remote {@link TextTrack} object. 10766 * 10767 * @param {string} kind 10768 * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata) 10769 * 10770 * @param {string} [label] 10771 * Label to identify the text track 10772 * 10773 * @param {string} [language] 10774 * Two letter language abbreviation 10775 * 10776 * @return {TextTrack} 10777 * The TextTrack that gets created. 10778 */ 10779 addTextTrack(kind, label, language) { 10780 if (!kind) { 10781 throw new Error('TextTrack kind is required but was not provided'); 10782 } 10783 return createTrackHelper(this, kind, label, language); 10784 } 10785 10786 /** 10787 * Create an emulated TextTrack for use by addRemoteTextTrack 10788 * 10789 * This is intended to be overridden by classes that inherit from 10790 * Tech in order to create native or custom TextTracks. 10791 * 10792 * @param {Object} options 10793 * The object should contain the options to initialize the TextTrack with. 10794 * 10795 * @param {string} [options.kind] 10796 * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata). 10797 * 10798 * @param {string} [options.label]. 10799 * Label to identify the text track 10800 * 10801 * @param {string} [options.language] 10802 * Two letter language abbreviation. 10803 * 10804 * @return {HTMLTrackElement} 10805 * The track element that gets created. 10806 */ 10807 createRemoteTextTrack(options) { 10808 const track = merge$2(options, { 10809 tech: this 10810 }); 10811 return new REMOTE.remoteTextEl.TrackClass(track); 10812 } 10813 10814 /** 10815 * Creates a remote text track object and returns an html track element. 10816 * 10817 * > Note: This can be an emulated {@link HTMLTrackElement} or a native one. 10818 * 10819 * @param {Object} options 10820 * See {@link Tech#createRemoteTextTrack} for more detailed properties. 10821 * 10822 * @param {boolean} [manualCleanup=false] 10823 * - When false: the TextTrack will be automatically removed from the video 10824 * element whenever the source changes 10825 * - When True: The TextTrack will have to be cleaned up manually 10826 * 10827 * @return {HTMLTrackElement} 10828 * An Html Track Element. 10829 * 10830 */ 10831 addRemoteTextTrack(options = {}, manualCleanup) { 10832 const htmlTrackElement = this.createRemoteTextTrack(options); 10833 if (typeof manualCleanup !== 'boolean') { 10834 manualCleanup = false;
vendor: 7,402 bytes, lines 10835-11095
10835 } 10836 10837 // store HTMLTrackElement and TextTrack to remote list 10838 this.remoteTextTrackEls().addTrackElement_(htmlTrackElement); 10839 this.remoteTextTracks().addTrack(htmlTrackElement.track); 10840 if (manualCleanup === false) { 10841 // create the TextTrackList if it doesn't exist 10842 this.ready(() => this.autoRemoteTextTracks_.addTrack(htmlTrackElement.track)); 10843 } 10844 return htmlTrackElement; 10845 } 10846 10847 /** 10848 * Remove a remote text track from the remote `TextTrackList`. 10849 * 10850 * @param {TextTrack} track 10851 * `TextTrack` to remove from the `TextTrackList` 10852 */ 10853 removeRemoteTextTrack(track) { 10854 const trackElement = this.remoteTextTrackEls().getTrackElementByTrack_(track); 10855 10856 // remove HTMLTrackElement and TextTrack from remote list 10857 this.remoteTextTrackEls().removeTrackElement_(trackElement); 10858 this.remoteTextTracks().removeTrack(track); 10859 this.autoRemoteTextTracks_.removeTrack(track); 10860 } 10861 10862 /** 10863 * Gets available media playback quality metrics as specified by the W3C's Media 10864 * Playback Quality API. 10865 * 10866 * @see [Spec]{@link https://wicg.github.io/media-playback-quality} 10867 * 10868 * @return {Object} 10869 * An object with supported media playback quality metrics 10870 * 10871 * @abstract 10872 */ 10873 getVideoPlaybackQuality() { 10874 return {}; 10875 } 10876 10877 /** 10878 * Attempt to create a floating video window always on top of other windows 10879 * so that users may continue consuming media while they interact with other 10880 * content sites, or applications on their device. 10881 * 10882 * @see [Spec]{@link https://wicg.github.io/picture-in-picture} 10883 * 10884 * @return {Promise|undefined} 10885 * A promise with a Picture-in-Picture window if the browser supports 10886 * Promises (or one was passed in as an option). It returns undefined 10887 * otherwise. 10888 * 10889 * @abstract 10890 */ 10891 requestPictureInPicture() { 10892 return Promise.reject(); 10893 } 10894 10895 /** 10896 * A method to check for the value of the 'disablePictureInPicture' <video> property. 10897 * Defaults to true, as it should be considered disabled if the tech does not support pip 10898 * 10899 * @abstract 10900 */ 10901 disablePictureInPicture() { 10902 return true; 10903 } 10904 10905 /** 10906 * A method to set or unset the 'disablePictureInPicture' <video> property. 10907 * 10908 * @abstract 10909 */ 10910 setDisablePictureInPicture() {} 10911 10912 /** 10913 * A fallback implementation of requestVideoFrameCallback using requestAnimationFrame 10914 * 10915 * @param {function} cb 10916 * @return {number} request id 10917 */ 10918 requestVideoFrameCallback(cb) { 10919 const id = newGUID(); 10920 if (!this.isReady_ || this.paused()) { 10921 this.queuedHanders_.add(id); 10922 this.one('playing', () => { 10923 if (this.queuedHanders_.has(id)) { 10924 this.queuedHanders_.delete(id); 10925 cb(); 10926 } 10927 }); 10928 } else { 10929 this.requestNamedAnimationFrame(id, cb); 10930 } 10931 return id; 10932 } 10933 10934 /** 10935 * A fallback implementation of cancelVideoFrameCallback 10936 * 10937 * @param {number} id id of callback to be cancelled 10938 */ 10939 cancelVideoFrameCallback(id) { 10940 if (this.queuedHanders_.has(id)) { 10941 this.queuedHanders_.delete(id); 10942 } else { 10943 this.cancelNamedAnimationFrame(id); 10944 } 10945 } 10946 10947 /** 10948 * A method to set a poster from a `Tech`. 10949 * 10950 * @abstract 10951 */ 10952 setPoster() {} 10953 10954 /** 10955 * A method to check for the presence of the 'playsinline' <video> attribute. 10956 * 10957 * @abstract 10958 */ 10959 playsinline() {} 10960 10961 /** 10962 * A method to set or unset the 'playsinline' <video> attribute. 10963 * 10964 * @abstract 10965 */ 10966 setPlaysinline() {} 10967 10968 /** 10969 * Attempt to force override of native audio tracks. 10970 * 10971 * @param {boolean} override - If set to true native audio will be overridden, 10972 * otherwise native audio will potentially be used. 10973 * 10974 * @abstract 10975 */ 10976 overrideNativeAudioTracks(override) {} 10977 10978 /** 10979 * Attempt to force override of native video tracks. 10980 * 10981 * @param {boolean} override - If set to true native video will be overridden, 10982 * otherwise native video will potentially be used. 10983 * 10984 * @abstract 10985 */ 10986 overrideNativeVideoTracks(override) {} 10987 10988 /** 10989 * Check if the tech can support the given mime-type. 10990 * 10991 * The base tech does not support any type, but source handlers might 10992 * overwrite this. 10993 * 10994 * @param {string} _type 10995 * The mimetype to check for support 10996 * 10997 * @return {string} 10998 * 'probably', 'maybe', or empty string 10999 * 11000 * @see [Spec]{@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/canPlayType} 11001 * 11002 * @abstract 11003 */ 11004 canPlayType(_type) { 11005 return ''; 11006 } 11007 11008 /** 11009 * Check if the type is supported by this tech. 11010 * 11011 * The base tech does not support any type, but source handlers might 11012 * overwrite this. 11013 * 11014 * @param {string} _type 11015 * The media type to check 11016 * @return {string} Returns the native video element's response 11017 */ 11018 static canPlayType(_type) { 11019 return ''; 11020 } 11021 11022 /** 11023 * Check if the tech can support the given source 11024 * 11025 * @param {Object} srcObj 11026 * The source object 11027 * @param {Object} options 11028 * The options passed to the tech 11029 * @return {string} 'probably', 'maybe', or '' (empty string) 11030 */ 11031 static canPlaySource(srcObj, options) { 11032 return Tech.canPlayType(srcObj.type); 11033 } 11034 11035 /* 11036 * Return whether the argument is a Tech or not. 11037 * Can be passed either a Class like `Html5` or a instance like `player.tech_` 11038 * 11039 * @param {Object} component 11040 * The item to check 11041 * 11042 * @return {boolean} 11043 * Whether it is a tech or not 11044 * - True if it is a tech 11045 * - False if it is not 11046 */ 11047 static isTech(component) { 11048 return component.prototype instanceof Tech || component instanceof Tech || component === Tech; 11049 } 11050 11051 /** 11052 * Registers a `Tech` into a shared list for videojs. 11053 * 11054 * @param {string} name 11055 * Name of the `Tech` to register. 11056 * 11057 * @param {Object} tech 11058 * The `Tech` class to register. 11059 */ 11060 static registerTech(name, tech) { 11061 if (!Tech.techs_) { 11062 Tech.techs_ = {}; 11063 } 11064 if (!Tech.isTech(tech)) { 11065 throw new Error(`Tech ${name} must be a Tech`); 11066 } 11067 if (!Tech.canPlayType) { 11068 throw new Error('Techs must have a static canPlayType method on them'); 11069 } 11070 if (!Tech.canPlaySource) { 11071 throw new Error('Techs must have a static canPlaySource method on them'); 11072 } 11073 name = toTitleCase$1(name); 11074 Tech.techs_[name] = tech; 11075 Tech.techs_[toLowerCase(name)] = tech; 11076 if (name !== 'Tech') { 11077 // camel case the techName for use in techOrder 11078 Tech.defaultTechOrder_.push(name); 11079 } 11080 return tech; 11081 } 11082 11083 /** 11084 * Get a `Tech` from the shared list by name. 11085 * 11086 * @param {string} name 11087 * `camelCase` or `TitleCase` name of the Tech to get 11088 * 11089 * @return {Tech|undefined} 11090 * The `Tech` or undefined if there was no tech with the name requested. 11091 */ 11092 static getTech(name) { 11093 if (!name) { 11094 return; 11095 }
vendor: 10,237 bytes, lines 11096-11463
11096 if (Tech.techs_ && Tech.techs_[name]) { 11097 return Tech.techs_[name]; 11098 } 11099 name = toTitleCase$1(name); 11100 if (window && window.videojs && window.videojs[name]) { 11101 log$1.warn(`The ${name} tech was added to the videojs object when it should be registered using videojs.registerTech(name, tech)`); 11102 return window.videojs[name]; 11103 } 11104 } 11105 } 11106 11107 /** 11108 * Get the {@link VideoTrackList} 11109 * 11110 * @returns {VideoTrackList} 11111 * @method Tech.prototype.videoTracks 11112 */ 11113 11114 /** 11115 * Get the {@link AudioTrackList} 11116 * 11117 * @returns {AudioTrackList} 11118 * @method Tech.prototype.audioTracks 11119 */ 11120 11121 /** 11122 * Get the {@link TextTrackList} 11123 * 11124 * @returns {TextTrackList} 11125 * @method Tech.prototype.textTracks 11126 */ 11127 11128 /** 11129 * Get the remote element {@link TextTrackList} 11130 * 11131 * @returns {TextTrackList} 11132 * @method Tech.prototype.remoteTextTracks 11133 */ 11134 11135 /** 11136 * Get the remote element {@link HtmlTrackElementList} 11137 * 11138 * @returns {HtmlTrackElementList} 11139 * @method Tech.prototype.remoteTextTrackEls 11140 */ 11141 11142 ALL.names.forEach(function (name) { 11143 const props = ALL[name]; 11144 Tech.prototype[props.getterName] = function () { 11145 this[props.privateName] = this[props.privateName] || new props.ListClass(); 11146 return this[props.privateName]; 11147 }; 11148 }); 11149 11150 /** 11151 * List of associated text tracks 11152 * 11153 * @type {TextTrackList} 11154 * @private 11155 * @property Tech#textTracks_ 11156 */ 11157 11158 /** 11159 * List of associated audio tracks. 11160 * 11161 * @type {AudioTrackList} 11162 * @private 11163 * @property Tech#audioTracks_ 11164 */ 11165 11166 /** 11167 * List of associated video tracks. 11168 * 11169 * @type {VideoTrackList} 11170 * @private 11171 * @property Tech#videoTracks_ 11172 */ 11173 11174 /** 11175 * Boolean indicating whether the `Tech` supports volume control. 11176 * 11177 * @type {boolean} 11178 * @default 11179 */ 11180 Tech.prototype.featuresVolumeControl = true; 11181 11182 /** 11183 * Boolean indicating whether the `Tech` supports muting volume. 11184 * 11185 * @type {boolean} 11186 * @default 11187 */ 11188 Tech.prototype.featuresMuteControl = true; 11189 11190 /** 11191 * Boolean indicating whether the `Tech` supports fullscreen resize control. 11192 * Resizing plugins using request fullscreen reloads the plugin 11193 * 11194 * @type {boolean} 11195 * @default 11196 */ 11197 Tech.prototype.featuresFullscreenResize = false; 11198 11199 /** 11200 * Boolean indicating whether the `Tech` supports changing the speed at which the video 11201 * plays. Examples: 11202 * - Set player to play 2x (twice) as fast 11203 * - Set player to play 0.5x (half) as fast 11204 * 11205 * @type {boolean} 11206 * @default 11207 */ 11208 Tech.prototype.featuresPlaybackRate = false; 11209 11210 /** 11211 * Boolean indicating whether the `Tech` supports the `progress` event. 11212 * This will be used to determine if {@link Tech#manualProgressOn} should be called. 11213 * 11214 * @type {boolean} 11215 * @default 11216 */ 11217 Tech.prototype.featuresProgressEvents = false; 11218 11219 /** 11220 * Boolean indicating whether the `Tech` supports the `sourceset` event. 11221 * 11222 * A tech should set this to `true` and then use {@link Tech#triggerSourceset} 11223 * to trigger a {@link Tech#event:sourceset} at the earliest time after getting 11224 * a new source. 11225 * 11226 * @type {boolean} 11227 * @default 11228 */ 11229 Tech.prototype.featuresSourceset = false; 11230 11231 /** 11232 * Boolean indicating whether the `Tech` supports the `timeupdate` event. 11233 * This will be used to determine if {@link Tech#manualTimeUpdates} should be called. 11234 * 11235 * @type {boolean} 11236 * @default 11237 */ 11238 Tech.prototype.featuresTimeupdateEvents = false; 11239 11240 /** 11241 * Boolean indicating whether the `Tech` supports the native `TextTrack`s. 11242 * This will help us integrate with native `TextTrack`s if the browser supports them. 11243 * 11244 * @type {boolean} 11245 * @default 11246 */ 11247 Tech.prototype.featuresNativeTextTracks = false; 11248 11249 /** 11250 * Boolean indicating whether the `Tech` supports `requestVideoFrameCallback`. 11251 * 11252 * @type {boolean} 11253 * @default 11254 */ 11255 Tech.prototype.featuresVideoFrameCallback = false; 11256 11257 /** 11258 * A functional mixin for techs that want to use the Source Handler pattern. 11259 * Source handlers are scripts for handling specific formats. 11260 * The source handler pattern is used for adaptive formats (HLS, DASH) that 11261 * manually load video data and feed it into a Source Buffer (Media Source Extensions) 11262 * Example: `Tech.withSourceHandlers.call(MyTech);` 11263 * 11264 * @param {Tech} _Tech 11265 * The tech to add source handler functions to. 11266 * 11267 * @mixes Tech~SourceHandlerAdditions 11268 */ 11269 Tech.withSourceHandlers = function (_Tech) { 11270 /** 11271 * Register a source handler 11272 * 11273 * @param {Function} handler 11274 * The source handler class 11275 * 11276 * @param {number} [index] 11277 * Register it at the following index 11278 */ 11279 _Tech.registerSourceHandler = function (handler, index) { 11280 let handlers = _Tech.sourceHandlers; 11281 if (!handlers) { 11282 handlers = _Tech.sourceHandlers = []; 11283 } 11284 if (index === undefined) { 11285 // add to the end of the list 11286 index = handlers.length; 11287 } 11288 handlers.splice(index, 0, handler); 11289 }; 11290 11291 /** 11292 * Check if the tech can support the given type. Also checks the 11293 * Techs sourceHandlers. 11294 * 11295 * @param {string} type 11296 * The mimetype to check. 11297 * 11298 * @return {string} 11299 * 'probably', 'maybe', or '' (empty string) 11300 */ 11301 _Tech.canPlayType = function (type) { 11302 const handlers = _Tech.sourceHandlers || []; 11303 let can; 11304 for (let i = 0; i < handlers.length; i++) { 11305 can = handlers[i].canPlayType(type); 11306 if (can) { 11307 return can; 11308 } 11309 } 11310 return ''; 11311 }; 11312 11313 /** 11314 * Returns the first source handler that supports the source. 11315 * 11316 * TODO: Answer question: should 'probably' be prioritized over 'maybe' 11317 * 11318 * @param {SourceObject} source 11319 * The source object 11320 * 11321 * @param {Object} options 11322 * The options passed to the tech 11323 * 11324 * @return {SourceHandler|null} 11325 * The first source handler that supports the source or null if 11326 * no SourceHandler supports the source 11327 */ 11328 _Tech.selectSourceHandler = function (source, options) { 11329 const handlers = _Tech.sourceHandlers || []; 11330 let can; 11331 for (let i = 0; i < handlers.length; i++) { 11332 can = handlers[i].canHandleSource(source, options); 11333 if (can) { 11334 return handlers[i]; 11335 } 11336 } 11337 return null; 11338 }; 11339 11340 /** 11341 * Check if the tech can support the given source. 11342 * 11343 * @param {SourceObject} srcObj 11344 * The source object 11345 * 11346 * @param {Object} options 11347 * The options passed to the tech 11348 * 11349 * @return {string} 11350 * 'probably', 'maybe', or '' (empty string) 11351 */ 11352 _Tech.canPlaySource = function (srcObj, options) { 11353 const sh = _Tech.selectSourceHandler(srcObj, options); 11354 if (sh) { 11355 return sh.canHandleSource(srcObj, options); 11356 } 11357 return ''; 11358 }; 11359 11360 /** 11361 * When using a source handler, prefer its implementation of 11362 * any function normally provided by the tech. 11363 */ 11364 const deferrable = ['seekable', 'seeking', 'duration']; 11365 11366 /** 11367 * A wrapper around {@link Tech#seekable} that will call a `SourceHandler`s seekable 11368 * function if it exists, with a fallback to the Techs seekable function. 11369 * 11370 * @method _Tech.seekable 11371 */ 11372 11373 /** 11374 * A wrapper around {@link Tech#duration} that will call a `SourceHandler`s duration 11375 * function if it exists, otherwise it will fallback to the techs duration function. 11376 * 11377 * @method _Tech.duration 11378 */ 11379 11380 deferrable.forEach(function (fnName) { 11381 const originalFn = this[fnName]; 11382 if (typeof originalFn !== 'function') { 11383 return; 11384 } 11385 this[fnName] = function () { 11386 if (this.sourceHandler_ && this.sourceHandler_[fnName]) { 11387 return this.sourceHandler_[fnName].apply(this.sourceHandler_, arguments); 11388 } 11389 return originalFn.apply(this, arguments); 11390 }; 11391 }, _Tech.prototype); 11392 11393 /** 11394 * Create a function for setting the source using a source object 11395 * and source handlers. 11396 * Should never be called unless a source handler was found. 11397 * 11398 * @param {SourceObject} source 11399 * A source object with src and type keys 11400 */ 11401 _Tech.prototype.setSource = function (source) { 11402 let sh = _Tech.selectSourceHandler(source, this.options_); 11403 if (!sh) { 11404 // Fall back to a native source handler when unsupported sources are 11405 // deliberately set 11406 if (_Tech.nativeSourceHandler) { 11407 sh = _Tech.nativeSourceHandler; 11408 } else { 11409 log$1.error('No source handler found for the current source.'); 11410 } 11411 } 11412 11413 // Dispose any existing source handler 11414 this.disposeSourceHandler(); 11415 this.off('dispose', this.disposeSourceHandler_); 11416 if (sh !== _Tech.nativeSourceHandler) { 11417 this.currentSource_ = source; 11418 } 11419 this.sourceHandler_ = sh.handleSource(source, this, this.options_); 11420 this.one('dispose', this.disposeSourceHandler_); 11421 }; 11422 11423 /** 11424 * Clean up any existing SourceHandlers and listeners when the Tech is disposed. 11425 * 11426 * @listens Tech#dispose 11427 */ 11428 _Tech.prototype.disposeSourceHandler = function () { 11429 // if we have a source and get another one 11430 // then we are loading something new 11431 // than clear all of our current tracks 11432 if (this.currentSource_) { 11433 this.clearTracks(['audio', 'video']); 11434 this.currentSource_ = null; 11435 } 11436 11437 // always clean up auto-text tracks 11438 this.cleanupAutoTextTracks(); 11439 if (this.sourceHandler_) { 11440 if (this.sourceHandler_.dispose) { 11441 this.sourceHandler_.dispose(); 11442 } 11443 this.sourceHandler_ = null; 11444 } 11445 }; 11446 }; 11447 11448 // The base Tech class needs to be registered as a Component. It is the only 11449 // Tech that can be registered as a Component. 11450 Component$1.registerComponent('Tech', Tech); 11451 Tech.registerTech('Tech', Tech); 11452 11453 /** 11454 * A list of techs that should be added to techOrder on Players 11455 * 11456 * @private 11457 */ 11458 Tech.defaultTechOrder_ = []; 11459 11460 /** 11461 * @file middleware.js 11462 * @module middleware 11463 */
vendor: 11,759 bytes, lines 11464-11873
11464 const middlewares = {}; 11465 const middlewareInstances = {}; 11466 const TERMINATOR = {}; 11467 11468 /** 11469 * A middleware object is a plain JavaScript object that has methods that 11470 * match the {@link Tech} methods found in the lists of allowed 11471 * {@link module:middleware.allowedGetters|getters}, 11472 * {@link module:middleware.allowedSetters|setters}, and 11473 * {@link module:middleware.allowedMediators|mediators}. 11474 * 11475 * @typedef {Object} MiddlewareObject 11476 */ 11477 11478 /** 11479 * A middleware factory function that should return a 11480 * {@link module:middleware~MiddlewareObject|MiddlewareObject}. 11481 * 11482 * This factory will be called for each player when needed, with the player 11483 * passed in as an argument. 11484 * 11485 * @callback MiddlewareFactory 11486 * @param { import('../player').default } player 11487 * A Video.js player. 11488 */ 11489 11490 /** 11491 * Define a middleware that the player should use by way of a factory function 11492 * that returns a middleware object. 11493 * 11494 * @param {string} type 11495 * The MIME type to match or `"*"` for all MIME types. 11496 * 11497 * @param {MiddlewareFactory} middleware 11498 * A middleware factory function that will be executed for 11499 * matching types. 11500 */ 11501 function use(type, middleware) { 11502 middlewares[type] = middlewares[type] || []; 11503 middlewares[type].push(middleware); 11504 } 11505 11506 /** 11507 * Asynchronously sets a source using middleware by recursing through any 11508 * matching middlewares and calling `setSource` on each, passing along the 11509 * previous returned value each time. 11510 * 11511 * @param { import('../player').default } player 11512 * A {@link Player} instance. 11513 * 11514 * @param {Tech~SourceObject} src 11515 * A source object. 11516 * 11517 * @param {Function} 11518 * The next middleware to run. 11519 */ 11520 function setSource(player, src, next) { 11521 player.setTimeout(() => setSourceHelper(src, middlewares[src.type], next, player), 1); 11522 } 11523 11524 /** 11525 * When the tech is set, passes the tech to each middleware's `setTech` method. 11526 * 11527 * @param {Object[]} middleware 11528 * An array of middleware instances. 11529 * 11530 * @param { import('../tech/tech').default } tech 11531 * A Video.js tech. 11532 */ 11533 function setTech(middleware, tech) { 11534 middleware.forEach(mw => mw.setTech && mw.setTech(tech)); 11535 } 11536 11537 /** 11538 * Calls a getter on the tech first, through each middleware 11539 * from right to left to the player. 11540 * 11541 * @param {Object[]} middleware 11542 * An array of middleware instances. 11543 * 11544 * @param { import('../tech/tech').default } tech 11545 * The current tech. 11546 * 11547 * @param {string} method 11548 * A method name. 11549 * 11550 * @return {*} 11551 * The final value from the tech after middleware has intercepted it. 11552 */ 11553 function get(middleware, tech, method) { 11554 return middleware.reduceRight(middlewareIterator(method), tech[method]()); 11555 } 11556 11557 /** 11558 * Takes the argument given to the player and calls the setter method on each 11559 * middleware from left to right to the tech. 11560 * 11561 * @param {Object[]} middleware 11562 * An array of middleware instances. 11563 * 11564 * @param { import('../tech/tech').default } tech 11565 * The current tech. 11566 * 11567 * @param {string} method 11568 * A method name. 11569 * 11570 * @param {*} arg 11571 * The value to set on the tech. 11572 * 11573 * @return {*} 11574 * The return value of the `method` of the `tech`. 11575 */ 11576 function set(middleware, tech, method, arg) { 11577 return tech[method](middleware.reduce(middlewareIterator(method), arg)); 11578 } 11579 11580 /** 11581 * Takes the argument given to the player and calls the `call` version of the 11582 * method on each middleware from left to right. 11583 * 11584 * Then, call the passed in method on the tech and return the result unchanged 11585 * back to the player, through middleware, this time from right to left. 11586 * 11587 * @param {Object[]} middleware 11588 * An array of middleware instances. 11589 * 11590 * @param { import('../tech/tech').default } tech 11591 * The current tech. 11592 * 11593 * @param {string} method 11594 * A method name. 11595 * 11596 * @param {*} arg 11597 * The value to set on the tech. 11598 * 11599 * @return {*} 11600 * The return value of the `method` of the `tech`, regardless of the 11601 * return values of middlewares. 11602 */ 11603 function mediate(middleware, tech, method, arg = null) { 11604 const callMethod = 'call' + toTitleCase$1(method); 11605 const middlewareValue = middleware.reduce(middlewareIterator(callMethod), arg); 11606 const terminated = middlewareValue === TERMINATOR; 11607 // deprecated. The `null` return value should instead return TERMINATOR to 11608 // prevent confusion if a techs method actually returns null. 11609 const returnValue = terminated ? null : tech[method](middlewareValue); 11610 executeRight(middleware, method, returnValue, terminated); 11611 return returnValue; 11612 } 11613 11614 /** 11615 * Enumeration of allowed getters where the keys are method names. 11616 * 11617 * @type {Object} 11618 */ 11619 const allowedGetters = { 11620 buffered: 1, 11621 currentTime: 1, 11622 duration: 1, 11623 muted: 1, 11624 played: 1, 11625 paused: 1, 11626 seekable: 1, 11627 volume: 1, 11628 ended: 1 11629 }; 11630 11631 /** 11632 * Enumeration of allowed setters where the keys are method names. 11633 * 11634 * @type {Object} 11635 */ 11636 const allowedSetters = { 11637 setCurrentTime: 1, 11638 setMuted: 1, 11639 setVolume: 1 11640 }; 11641 11642 /** 11643 * Enumeration of allowed mediators where the keys are method names. 11644 * 11645 * @type {Object} 11646 */ 11647 const allowedMediators = { 11648 play: 1, 11649 pause: 1 11650 }; 11651 function middlewareIterator(method) { 11652 return (value, mw) => { 11653 // if the previous middleware terminated, pass along the termination 11654 if (value === TERMINATOR) { 11655 return TERMINATOR; 11656 } 11657 if (mw[method]) { 11658 return mw[method](value); 11659 } 11660 return value; 11661 }; 11662 } 11663 function executeRight(mws, method, value, terminated) { 11664 for (let i = mws.length - 1; i >= 0; i--) { 11665 const mw = mws[i]; 11666 if (mw[method]) { 11667 mw[method](terminated, value); 11668 } 11669 } 11670 } 11671 11672 /** 11673 * Clear the middleware cache for a player. 11674 * 11675 * @param { import('../player').default } player 11676 * A {@link Player} instance. 11677 */ 11678 function clearCacheForPlayer(player) { 11679 middlewareInstances[player.id()] = null; 11680 } 11681 11682 /** 11683 * { 11684 * [playerId]: [[mwFactory, mwInstance], ...] 11685 * } 11686 * 11687 * @private 11688 */ 11689 function getOrCreateFactory(player, mwFactory) { 11690 const mws = middlewareInstances[player.id()]; 11691 let mw = null; 11692 if (mws === undefined || mws === null) { 11693 mw = mwFactory(player); 11694 middlewareInstances[player.id()] = [[mwFactory, mw]]; 11695 return mw; 11696 } 11697 for (let i = 0; i < mws.length; i++) { 11698 const [mwf, mwi] = mws[i]; 11699 if (mwf !== mwFactory) { 11700 continue; 11701 } 11702 mw = mwi; 11703 } 11704 if (mw === null) { 11705 mw = mwFactory(player); 11706 mws.push([mwFactory, mw]); 11707 } 11708 return mw; 11709 } 11710 function setSourceHelper(src = {}, middleware = [], next, player, acc = [], lastRun = false) { 11711 const [mwFactory, ...mwrest] = middleware; 11712 11713 // if mwFactory is a string, then we're at a fork in the road 11714 if (typeof mwFactory === 'string') { 11715 setSourceHelper(src, middlewares[mwFactory], next, player, acc, lastRun); 11716 11717 // if we have an mwFactory, call it with the player to get the mw, 11718 // then call the mw's setSource method 11719 } else if (mwFactory) { 11720 const mw = getOrCreateFactory(player, mwFactory); 11721 11722 // if setSource isn't present, implicitly select this middleware 11723 if (!mw.setSource) { 11724 acc.push(mw); 11725 return setSourceHelper(src, mwrest, next, player, acc, lastRun); 11726 } 11727 mw.setSource(Object.assign({}, src), function (err, _src) { 11728 // something happened, try the next middleware on the current level 11729 // make sure to use the old src 11730 if (err) { 11731 return setSourceHelper(src, mwrest, next, player, acc, lastRun); 11732 } 11733 11734 // we've succeeded, now we need to go deeper 11735 acc.push(mw); 11736 11737 // if it's the same type, continue down the current chain 11738 // otherwise, we want to go down the new chain 11739 setSourceHelper(_src, src.type === _src.type ? mwrest : middlewares[_src.type], next, player, acc, lastRun); 11740 }); 11741 } else if (mwrest.length) { 11742 setSourceHelper(src, mwrest, next, player, acc, lastRun); 11743 } else if (lastRun) { 11744 next(src, acc); 11745 } else { 11746 setSourceHelper(src, middlewares['*'], next, player, acc, true); 11747 } 11748 } 11749 11750 /** 11751 * Mimetypes 11752 * 11753 * @see https://www.iana.org/assignments/media-types/media-types.xhtml 11754 * @typedef Mimetypes~Kind 11755 * @enum 11756 */ 11757 const MimetypesKind = { 11758 opus: 'video/ogg', 11759 ogv: 'video/ogg', 11760 mp4: 'video/mp4', 11761 mov: 'video/mp4', 11762 m4v: 'video/mp4', 11763 mkv: 'video/x-matroska', 11764 m4a: 'audio/mp4', 11765 mp3: 'audio/mpeg', 11766 aac: 'audio/aac', 11767 caf: 'audio/x-caf', 11768 flac: 'audio/flac', 11769 oga: 'audio/ogg', 11770 wav: 'audio/wav', 11771 m3u8: 'application/x-mpegURL', 11772 mpd: 'application/dash+xml', 11773 jpg: 'image/jpeg', 11774 jpeg: 'image/jpeg', 11775 gif: 'image/gif', 11776 png: 'image/png', 11777 svg: 'image/svg+xml', 11778 webp: 'image/webp' 11779 }; 11780 11781 /** 11782 * Get the mimetype of a given src url if possible 11783 * 11784 * @param {string} src 11785 * The url to the src 11786 * 11787 * @return {string} 11788 * return the mimetype if it was known or empty string otherwise 11789 */ 11790 const getMimetype = function (src = '') { 11791 const ext = getFileExtension(src); 11792 const mimetype = MimetypesKind[ext.toLowerCase()]; 11793 return mimetype || ''; 11794 }; 11795 11796 /** 11797 * Find the mime type of a given source string if possible. Uses the player 11798 * source cache. 11799 * 11800 * @param { import('../player').default } player 11801 * The player object 11802 * 11803 * @param {string} src 11804 * The source string 11805 * 11806 * @return {string} 11807 * The type that was found 11808 */ 11809 const findMimetype = (player, src) => { 11810 if (!src) { 11811 return ''; 11812 } 11813 11814 // 1. check for the type in the `source` cache 11815 if (player.cache_.source.src === src && player.cache_.source.type) { 11816 return player.cache_.source.type; 11817 } 11818 11819 // 2. see if we have this source in our `currentSources` cache 11820 const matchingSources = player.cache_.sources.filter(s => s.src === src); 11821 if (matchingSources.length) { 11822 return matchingSources[0].type; 11823 } 11824 11825 // 3. look for the src url in source elements and use the type there 11826 const sources = player.$$('source'); 11827 for (let i = 0; i < sources.length; i++) { 11828 const s = sources[i]; 11829 if (s.type && s.src && s.src === src) { 11830 return s.type; 11831 } 11832 } 11833 11834 // 4. finally fallback to our list of mime types based on src url extension 11835 return getMimetype(src); 11836 }; 11837 11838 /** 11839 * @module filter-source 11840 */ 11841 11842 /** 11843 * Filter out single bad source objects or multiple source objects in an 11844 * array. Also flattens nested source object arrays into a 1 dimensional 11845 * array of source objects. 11846 * 11847 * @param {Tech~SourceObject|Tech~SourceObject[]} src 11848 * The src object to filter 11849 * 11850 * @return {Tech~SourceObject[]} 11851 * An array of sourceobjects containing only valid sources 11852 * 11853 * @private 11854 */ 11855 const filterSource = function (src) { 11856 // traverse array 11857 if (Array.isArray(src)) { 11858 let newsrc = []; 11859 src.forEach(function (srcobj) { 11860 srcobj = filterSource(srcobj); 11861 if (Array.isArray(srcobj)) { 11862 newsrc = newsrc.concat(srcobj); 11863 } else if (isObject$1(srcobj)) { 11864 newsrc.push(srcobj); 11865 } 11866 }); 11867 src = newsrc; 11868 } else if (typeof src === 'string' && src.trim()) { 11869 // convert string into object 11870 src = [fixSource({ 11871 src 11872 })]; 11873 } else if (isObject$1
vendor: 17,264 bytes, lines 11873-11901
11873(src) && typeof src.src === 'string' && src.src && src.src.trim()) { 11874 // src is already valid 11875 src = [fixSource(src)]; 11876 } else { 11877 // invalid source, turn it into an empty array 11878 src = []; 11879 } 11880 return src; 11881 }; 11882 11883 /** 11884 * Checks src mimetype, adding it when possible 11885 * 11886 * @param {Tech~SourceObject} src 11887 * The src object to check 11888 * @return {Tech~SourceObject} 11889 * src Object with known type 11890 */ 11891 function fixSource(src) { 11892 if (!src.type) { 11893 const mimetype = getMimetype(src.src); 11894 if (mimetype) { 11895 src.type = mimetype; 11896 } 11897 } 11898 return src; 11899 } 11900 11901 var icons = "<svg xmlns=\"http://www.w3.org/2000/svg\">\n <defs>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-play\">\n <path d=\"M16 10v28l22-14z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-pause\">\n <path d=\"M12 38h8V10h-8v28zm16-28v28h8V10h-8z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-audio\">\n <path d=\"M24 2C14.06 2 6 10.06 6 20v14c0 3.31 2.69 6 6 6h6V24h-8v-4c0-7.73 6.27-14 14-14s14 6.27 14 14v4h-8v16h6c3.31 0 6-2.69 6-6V20c0-9.94-8.06-18-18-18z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-captions\">\n <path d=\"M38 8H10c-2.21 0-4 1.79-4 4v24c0 2.21 1.79 4 4 4h28c2.21 0 4-1.79 4-4V12c0-2.21-1.79-4-4-4zM22 22h-3v-1h-4v6h4v-1h3v2a2 2 0 0 1-2 2h-6a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v2zm14 0h-3v-1h-4v6h4v-1h3v2a2 2 0 0 1-2 2h-6a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v2z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-subtitles\">\n <path d=\"M40 8H8c-2.21 0-4 1.79-4 4v24c0 2.21 1.79 4 4 4h32c2.21 0 4-1.79 4-4V12c0-2.21-1.79-4-4-4zM8 24h8v4H8v-4zm20 12H8v-4h20v4zm12 0h-8v-4h8v4zm0-8H20v-4h20v4z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-fullscreen-enter\">\n <path d=\"M14 28h-4v10h10v-4h-6v-6zm-4-8h4v-6h6v-4H10v10zm24 14h-6v4h10V28h-4v6zm-6-24v4h6v6h4V10H28z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-fullscreen-exit\">\n <path d=\"M10 32h6v6h4V28H10v4zm6-16h-6v4h10V10h-4v6zm12 22h4v-6h6v-4H28v10zm4-22v-6h-4v10h10v-4h-6z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-play-circle\">\n <path d=\"M20 33l12-9-12-9v18zm4-29C12.95 4 4 12.95 4 24s8.95 20 20 20 20-8.95 20-20S35.05 4 24 4zm0 36c-8.82 0-16-7.18-16-16S15.18 8 24 8s16 7.18 16 16-7.18 16-16 16z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-volume-mute\">\n <path d=\"M33 24c0-3.53-2.04-6.58-5-8.05v4.42l4.91 4.91c.06-.42.09-.85.09-1.28zm5 0c0 1.88-.41 3.65-1.08 5.28l3.03 3.03C41.25 29.82 42 27 42 24c0-8.56-5.99-15.72-14-17.54v4.13c5.78 1.72 10 7.07 10 13.41zM8.55 6L6 8.55 15.45 18H6v12h8l10 10V26.55l8.51 8.51c-1.34 1.03-2.85 1.86-4.51 2.36v4.13a17.94 17.94 0 0 0 7.37-3.62L39.45 42 42 39.45l-18-18L8.55 6zM24 8l-4.18 4.18L24 16.36V8z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-volume-low\">\n <path d=\"M14 18v12h8l10 10V8L22 18h-8z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-volume-medium\">\n <path d=\"M37 24c0-3.53-2.04-6.58-5-8.05v16.11c2.96-1.48 5-4.53 5-8.06zm-27-6v12h8l10 10V8L18 18h-8z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-volume-high\">\n <path d=\"M6 18v12h8l10 10V8L14 18H6zm27 6c0-3.53-2.04-6.58-5-8.05v16.11c2.96-1.48 5-4.53 5-8.06zM28 6.46v4.13c5.78 1.72 10 7.07 10 13.41s-4.22 11.69-10 13.41v4.13c8.01-1.82 14-8.97 14-17.54S36.01 8.28 28 6.46z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-spinner\">\n <path d=\"M18.8 21l9.53-16.51C26.94 4.18 25.49 4 24 4c-4.8 0-9.19 1.69-12.64 4.51l7.33 12.69.11-.2zm24.28-3c-1.84-5.85-6.3-10.52-11.99-12.68L23.77 18h19.31zm.52 2H28.62l.58 1 9.53 16.5C41.99 33.94 44 29.21 44 24c0-1.37-.14-2.71-.4-4zm-26.53 4l-7.8-13.5C6.01 14.06 4 18.79 4 24c0 1.37.14 2.71.4 4h14.98l-2.31-4zM4.92 30c1.84 5.85 6.3 10.52 11.99 12.68L24.23 30H4.92zm22.54 0l-7.8 13.51c1.4.31 2.85.49 4.34.49 4.8 0 9.19-1.69 12.64-4.51L29.31 26.8 27.46 30z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 24 24\" id=\"vjs-icon-hd\">\n <path d=\"M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-8 12H9.5v-2h-2v2H6V9h1.5v2.5h2V9H11v6zm2-6h4c.55 0 1 .45 1 1v4c0 .55-.45 1-1 1h-4V9zm1.5 4.5h2v-3h-2v3z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-chapters\">\n <path d=\"M6 26h4v-4H6v4zm0 8h4v-4H6v4zm0-16h4v-4H6v4zm8 8h28v-4H14v4zm0 8h28v-4H14v4zm0-20v4h28v-4H14z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 40 40\" id=\"vjs-icon-downloading\">\n <path d=\"M18.208 36.875q-3.208-.292-5.979-1.729-2.771-1.438-4.812-3.729-2.042-2.292-3.188-5.229-1.146-2.938-1.146-6.23 0-6.583 4.334-11.416 4.333-4.834 10.833-5.5v3.166q-5.167.75-8.583 4.646Q6.25 14.75 6.25 19.958q0 5.209 3.396 9.104 3.396 3.896 8.562 4.646zM20 28.417L11.542 20l2.083-2.083 4.917 4.916v-11.25h2.916v11.25l4.875-4.916L28.417 20zm1.792 8.458v-3.167q1.833-.25 3.541-.958 1.709-.708 3.167-1.875l2.333 2.292q-1.958 1.583-4.25 2.541-2.291.959-4.791 1.167zm6.791-27.792q-1.541-1.125-3.25-1.854-1.708-.729-3.541-1.021V3.042q2.5.25 4.77 1.208 2.271.958 4.271 2.5zm4.584 21.584l-2.25-2.25q1.166-1.5 1.854-3.209.687-1.708.937-3.541h3.209q-.292 2.5-1.229 4.791-.938 2.292-2.521 4.209zm.541-12.417q-.291-1.833-.958-3.562-.667-1.73-1.833-3.188l2.375-2.208q1.541 1.916 2.458 4.208.917 2.292 1.167 4.75z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-file-download\">\n <path d=\"M10.8 40.55q-1.35 0-2.375-1T7.4 37.15v-7.7h3.4v7.7h26.35v-7.7h3.4v7.7q0 1.4-1 2.4t-2.4 1zM24 32.1L13.9 22.05l2.45-2.45 5.95 5.95V7.15h3.4v18.4l5.95-5.95 2.45 2.45z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-file-download-done\">\n <path d=\"M9.8 40.5v-3.45h28.4v3.45zm9.2-9.05L7.4 19.85l2.45-2.35L19 26.65l19.2-19.2 2.4 2.4z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-file-download-off\">\n <path d=\"M4.9 4.75L43.25 43.1 41 45.3l-4.75-4.75q-.05.05-.075.025-.025-.025-.075-.025H10.8q-1.35 0-2.375-1T7.4 37.15v-7.7h3.4v7.7h22.05l-7-7-1.85 1.8L13.9 21.9l1.85-1.85L2.7 7zm26.75 14.7l2.45 2.45-3.75 3.8-2.45-2.5zM25.7 7.15V21.1l-3.4-3.45V7.15z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-share\">\n <path d=\"M36 32.17c-1.52 0-2.89.59-3.93 1.54L17.82 25.4c.11-.45.18-.92.18-1.4s-.07-.95-.18-1.4l14.1-8.23c1.07 1 2.5 1.62 4.08 1.62 3.31 0 6-2.69 6-6s-2.69-6-6-6-6 2.69-6 6c0 .48.07.95.18 1.4l-14.1 8.23c-1.07-1-2.5-1.62-4.08-1.62-3.31 0-6 2.69-6 6s2.69 6 6 6c1.58 0 3.01-.62 4.08-1.62l14.25 8.31c-.1.42-.16.86-.16 1.31A5.83 5.83 0 1 0 36 32.17z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-cog\">\n <path d=\"M38.86 25.95c.08-.64.14-1.29.14-1.95s-.06-1.31-.14-1.95l4.23-3.31c.38-.3.49-.84.24-1.28l-4-6.93c-.25-.43-.77-.61-1.22-.43l-4.98 2.01c-1.03-.79-2.16-1.46-3.38-1.97L29 4.84c-.09-.47-.5-.84-1-.84h-8c-.5 0-.91.37-.99.84l-.75 5.3a14.8 14.8 0 0 0-3.38 1.97L9.9 10.1a1 1 0 0 0-1.22.43l-4 6.93c-.25.43-.14.97.24 1.28l4.22 3.31C9.06 22.69 9 23.34 9 24s.06 1.31.14 1.95l-4.22 3.31c-.38.3-.49.84-.24 1.28l4 6.93c.25.43.77.61 1.22.43l4.98-2.01c1.03.79 2.16 1.46 3.38 1.97l.75 5.3c.08.47.49.84.99.84h8c.5 0 .91-.37.99-.84l.75-5.3a14.8 14.8 0 0 0 3.38-1.97l4.98 2.01a1 1 0 0 0 1.22-.43l4-6.93c.25-.43.14-.97-.24-1.28l-4.22-3.31zM24 31c-3.87 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-square\">\n <path d=\"M36 8H12c-2.21 0-4 1.79-4 4v24c0 2.21 1.79 4 4 4h24c2.21 0 4-1.79 4-4V12c0-2.21-1.79-4-4-4zm0 28H12V12h24v24z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-circle\">\n <circle cx=\"24\" cy=\"24\" r=\"20\"></circle>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-circle-outline\">\n <path d=\"M24 4C12.95 4 4 12.95 4 24s8.95 20 20 20 20-8.95 20-20S35.05 4 24 4zm0 36c-8.82 0-16-7.18-16-16S15.18 8 24 8s16 7.18 16 16-7.18 16-16 16z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-circle-inner-circle\">\n <path d=\"M24 4C12.97 4 4 12.97 4 24s8.97 20 20 20 20-8.97 20-20S35.03 4 24 4zm0 36c-8.82 0-16-7.18-16-16S15.18 8 24 8s16 7.18 16 16-7.18 16-16 16zm6-16c0 3.31-2.69 6-6 6s-6-2.69-6-6 2.69-6 6-6 6 2.69 6 6z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-cancel\">\n <path d=\"M24 4C12.95 4 4 12.95 4 24s8.95 20 20 20 20-8.95 20-20S35.05 4 24 4zm10 27.17L31.17 34 24 26.83 16.83 34 14 31.17 21.17 24 14 16.83 16.83 14 24 21.17 31.17 14 34 16.83 26.83 24 34 31.17z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-replay\">\n <path d=\"M24 10V2L14 12l10 10v-8c6.63 0 12 5.37 12 12s-5.37 12-12 12-12-5.37-12-12H8c0 8.84 7.16 16 16 16s16-7.16 16-16-7.16-16-16-16z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-repeat\">\n <path d=\"M14 14h20v6l8-8-8-8v6H10v12h4v-8zm20 20H14v-6l-8 8 8 8v-6h24V26h-4v8z\"></path>\n </symbol>\n <symbol viewBox=\"0 96 48 48\" id=\"vjs-icon-replay-5\">\n <path d=\"M17.689 98l-8.697 8.696 8.697 8.697 2.486-2.485-4.32-4.319h1.302c4.93 0 9.071 1.722 12.424 5.165 3.352 3.443 5.029 7.638 5.029 12.584h3.55c0-2.958-.553-5.73-1.658-8.313-1.104-2.583-2.622-4.841-4.555-6.774-1.932-1.932-4.19-3.45-6.773-4.555-2.584-1.104-5.355-1.657-8.313-1.657H15.5l4.615-4.615zm-8.08 21.659v13.861h11.357v5.008H9.609V143h12.7c.834 0 1.55-.298 2.146-.894.596-.597.895-1.31.895-2.145v-7.781c0-.835-.299-1.55-.895-2.147a2.929 2.929 0 0 0-2.147-.894h-8.227v-5.096H25.35v-4.384z\"></path>\n </symbol>\n <symbol viewBox=\"0 96 48 48\" id=\"vjs-icon-replay-10\">\n <path d=\"M42.315 125.63c0-4.997-1.694-9.235-5.08-12.713-3.388-3.479-7.571-5.218-12.552-5.218h-1.315l4.363 4.363-2.51 2.51-8.787-8.786L25.221 97l2.45 2.45-4.662 4.663h1.375c2.988 0 5.788.557 8.397 1.673 2.61 1.116 4.892 2.65 6.844 4.602 1.953 1.953 3.487 4.234 4.602 6.844 1.116 2.61 1.674 5.41 1.674 8.398zM8.183 142v-19.657H3.176V117.8h9.643V142zm13.63 0c-1.156 0-2.127-.393-2.912-1.178-.778-.778-1.168-1.746-1.168-2.902v-16.04c0-1.156.393-2.127 1.178-2.912.779-.779 1.746-1.168 2.902-1.168h7.696c1.156 0 2.126.392 2.911 1.177.779.78 1.168 1.747 1.168 2.903v16.04c0 1.156-.392 2.127-1.177 2.912-.779.779-1.746 1.168-2.902 1.168zm.556-4.636h6.583v-15.02H22.37z\"></path>\n </symbol>\n <symbol viewBox=\"0 96 48 48\" id=\"vjs-icon-replay-30\">\n <path d=\"M26.047 97l-8.733 8.732 8.733 8.733 2.496-2.494-4.336-4.338h1.307c4.95 0 9.108 1.73 12.474 5.187 3.367 3.458 5.051 7.668 5.051 12.635h3.565c0-2.97-.556-5.751-1.665-8.346-1.109-2.594-2.633-4.862-4.574-6.802-1.94-1.941-4.208-3.466-6.803-4.575-2.594-1.109-5.375-1.664-8.345-1.664H23.85l4.634-4.634zM2.555 117.531v4.688h10.297v5.25H5.873v4.687h6.979v5.156H2.555V142H13.36c1.061 0 1.95-.395 2.668-1.186.718-.79 1.076-1.772 1.076-2.94v-16.218c0-1.168-.358-2.149-1.076-2.94-.717-.79-1.607-1.185-2.668-1.185zm22.482.14c-1.149 0-2.11.39-2.885 1.165-.78.78-1.172 1.744-1.172 2.893v15.943c0 1.149.388 2.11 1.163 2.885.78.78 1.745 1.172 2.894 1.172h7.649c1.148 0 2.11-.388 2.884-1.163.78-.78 1.17-1.745 1.17-2.894v-15.943c0-1.15-.386-2.111-1.16-2.885-.78-.78-1.746-1.172-2.894-1.172zm.553 4.518h6.545v14.93H25.59z\"></path>\n </symbol>\n <symbol viewBox=\"0 96 48 48\" id=\"vjs-icon-forward-5\">\n <path d=\"M29.508 97l-2.431 2.43 4.625 4.625h-1.364c-2.965 0-5.742.554-8.332 1.66-2.589 1.107-4.851 2.629-6.788 4.566-1.937 1.937-3.458 4.2-4.565 6.788-1.107 2.59-1.66 5.367-1.66 8.331h3.557c0-4.957 1.68-9.16 5.04-12.611 3.36-3.45 7.51-5.177 12.451-5.177h1.304l-4.326 4.33 2.49 2.49 8.715-8.716zm-9.783 21.61v13.89h11.382v5.018H19.725V142h12.727a2.93 2.93 0 0 0 2.15-.896 2.93 2.93 0 0 0 .896-2.15v-7.798c0-.837-.299-1.554-.896-2.152a2.93 2.93 0 0 0-2.15-.896h-8.245V123h11.29v-4.392z\"></path>\n </symbol>\n <symbol viewBox=\"0 96 48 48\" id=\"vjs-icon-forward-10\">\n <path d=\"M23.119 97l-2.386 2.383 4.538 4.538h-1.339c-2.908 0-5.633.543-8.173 1.63-2.54 1.085-4.76 2.577-6.66 4.478-1.9 1.9-3.392 4.12-4.478 6.66-1.085 2.54-1.629 5.264-1.629 8.172h3.49c0-4.863 1.648-8.986 4.944-12.372 3.297-3.385 7.368-5.078 12.216-5.078h1.279l-4.245 4.247 2.443 2.442 8.55-8.55zm-9.52 21.45v4.42h4.871V142h4.513v-23.55zm18.136 0c-1.125 0-2.066.377-2.824 1.135-.764.764-1.148 1.709-1.148 2.834v15.612c0 1.124.38 2.066 1.139 2.824.764.764 1.708 1.145 2.833 1.145h7.489c1.125 0 2.066-.378 2.824-1.136.764-.764 1.145-1.709 1.145-2.833v-15.612c0-1.125-.378-2.067-1.136-2.825-.764-.764-1.708-1.145-2.833-1.145zm.54 4.42h6.408v14.617h-6.407z\"></path>\n </symbol>\n <symbol viewBox=\"0 96 48 48\" id=\"vjs-icon-forward-30\">\n <path d=\"M25.549 97l-2.437 2.434 4.634 4.635H26.38c-2.97 0-5.753.555-8.347 1.664-2.594 1.109-4.861 2.633-6.802 4.574-1.94 1.94-3.465 4.207-4.574 6.802-1.109 2.594-1.664 5.377-1.664 8.347h3.565c0-4.967 1.683-9.178 5.05-12.636 3.366-3.458 7.525-5.187 12.475-5.187h1.307l-4.335 4.338 2.495 2.494 8.732-8.732zm-11.553 20.53v4.689h10.297v5.249h-6.978v4.688h6.978v5.156H13.996V142h10.808c1.06 0 1.948-.395 2.666-1.186.718-.79 1.077-1.771 1.077-2.94v-16.217c0-1.169-.36-2.15-1.077-2.94-.718-.79-1.605-1.186-2.666-1.186zm21.174.168c-1.149 0-2.11.389-2.884 1.163-.78.78-1.172 1.745-1.172 2.894v15.942c0 1.15.388 2.11 1.162 2.885.78.78 1.745 1.17 2.894 1.17h7.649c1.149 0 2.11-.386 2.885-1.16.78-.78 1.17-1.746 1.17-2.895v-15.942c0-1.15-.387-2.11-1.161-2.885-.78-.78-1.745-1.172-2.894-1.172zm.552 4.516h6.542v14.931h-6.542z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 512 512\" id=\"vjs-icon-audio-description\">\n <g fill-rule=\"evenodd\"><path d=\"M227.29 381.351V162.993c50.38-1.017 89.108-3.028 117.631 17.126 27.374 19.342 48.734 56.965 44.89 105.325-4.067 51.155-41.335 94.139-89.776 98.475-24.085 2.155-71.972 0-71.972 0s-.84-1.352-.773-2.568m48.755-54.804c31.43 1.26 53.208-16.633 56.495-45.386 4.403-38.51-21.188-63.552-58.041-60.796v103.612c-.036 1.466.575 2.22 1.546 2.57\"></path><path d=\"M383.78 381.328c13.336 3.71 17.387-11.06 23.215-21.408 12.722-22.571 22.294-51.594 22.445-84.774.221-47.594-18.343-82.517-35.6-106.182h-8.51c-.587 3.874 2.226 7.315 3.865 10.276 13.166 23.762 25.367 56.553 25.54 94.194.2 43.176-14.162 79.278-30.955 107.894\"></path><path d=\"M425.154 381.328c13.336 3.71 17.384-11.061 23.215-21.408 12.721-22.571 22.291-51.594 22.445-84.774.221-47.594-18.343-82.517-35.6-106.182h-8.511c-.586 3.874 2.226 7.315 3.866 10.276 13.166 23.762 25.367 56.553 25.54 94.194.2 43.176-14.162 79.278-30.955 107.894\"></path><path d=\"M466.26 381.328c13.337 3.71 17.385-11.061 23.216-21.408 12.722-22.571 22.292-51.594 22.445-84.774.221-47.594-18.343-82.517-35.6-106.182h-8.51c-.587 3.874 2.225 7.315 3.865 10.276 13.166 23.762 25.367 56.553 25.54 94.194.2 43.176-14.162 79.278-30.955 107.894M4.477 383.005H72.58l18.573-28.484 64.169-.135s.065 19.413.065 28.62h48.756V160.307h-58.816c-5.653 9.537-140.85 222.697-140.85 222.697zm152.667-145.282v71.158l-40.453-.27 40.453-70.888z\"></path></g>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-next-item\">\n <path d=\"M12 36l17-12-17-12v24zm20-24v24h4V12h-4z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-previous-item\">\n <path d=\"M12 12h4v24h-4zm7 12l17 12V12z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-shuffle\">\n <path d=\"M21.17 18.34L10.83 8 8 10.83l10.34 10.34 2.83-2.83zM29 8l4.09 4.09L8 37.17 10.83 40l25.09-25.09L40 19V8H29zm.66 18.83l-2.83 2.83 6.26 6.26L29 40h11V29l-4.09 4.09-6.25-6.26z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-cast\">\n <path d=\"M42 6H6c-2.21 0-4 1.79-4 4v6h4v-6h36v28H28v4h14c2.21 0 4-1.79 4-4V10c0-2.21-1.79-4-4-4zM2 36v6h6c0-3.31-2.69-6-6-6zm0-8v4c5.52 0 10 4.48 10 10h4c0-7.73-6.27-14-14-14zm0-8v4c9.94 0 18 8.06 18 18h4c0-12.15-9.85-22-22-22z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 48 48\" id=\"vjs-icon-picture-in-picture-enter\">\n <path d=\"M38 22H22v11.99h16V22zm8 16V9.96C46 7.76 44.2 6 42 6H6C3.8 6 2 7.76 2 9.96V38c0 2.2 1.8 4 4 4h36c2.2 0 4-1.8 4-4zm-4 .04H6V9.94h36v28.1z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 22 18\" id=\"vjs-icon-picture-in-picture-exit\">\n <path d=\"M18 4H4v10h14V4zm4 12V1.98C22 .88 21.1 0 20 0H2C.9 0 0 .88 0 1.98V16c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2zm-2 .02H2V1.97h18v14.05z\"></path>\n <path fill=\"none\" d=\"M-1-3h24v24H-1z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 1792 1792\" id=\"vjs-icon-facebook\">\n <path d=\"M1343 12v264h-157q-86 0-116 36t-30 108v189h293l-39 296h-254v759H734V905H479V609h255V391q0-186 104-288.5T1115 0q147 0 228 12z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 1792 1792\" id=\"vjs-icon-linkedin\">\n <path d=\"M477 625v991H147V625h330zm21-306q1 73-50.5 122T312 490h-2q-82 0-132-49t-50-122q0-74 51.5-122.5T314 148t133 48.5T498 319zm1166 729v568h-329v-530q0-105-40.5-164.5T1168 862q-63 0-105.5 34.5T999 982q-11 30-11 81v553H659q2-399 2-647t-1-296l-1-48h329v144h-2q20-32 41-56t56.5-52 87-43.5T1285 602q171 0 275 113.5t104 332.5z\"></path>\n </symbol>\n <symbol viewBox=\"0 0 1
11901792 1792\" id=\"vjs-icon-twitter\">\n <path d=\"M1684 408q-67 98-162 167 1 14 1 42 0 130-38 259.5T1369.5 1125 1185 1335.5t-258 146-323 54.5q-271 0-496-145 35 4 78 4 225 0 401-138-105-2-188-64.5T285 1033q33 5 61 5 43 0 85-11-112-23-185.5-111.5T172 710v-4q68 38 146 41-66-44-105-115t-39-154q0-88 44-163 121 149 294.5 238.5T884 653q-8-38-8-74 0-134 94.5-228.5T1199 256q140 0 236 102 109-21 205-78-37 115-142 178 93-10 186-50z\"></path>\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>"; 11902 11903 /** 11904 * @file loader.js 11905 */ 11906 11907 /** 11908 * The `MediaLoader` is the `Component` that decides which playback technology to load 11909 * when a player is initialized. 11910 * 11911 * @extends Component 11912 */ 11913 class MediaLoader extends Component$1 { 11914 /** 11915 * Create an instance of this class. 11916 * 11917 * @param { import('../player').default } player 11918 * The `Player` that this class should attach to. 11919 * 11920 * @param {Object} [options] 11921 * The key/value store of player options. 11922 * 11923 * @param {Function} [ready] 11924 * The function that is run when this component is ready. 11925 */ 11926 constructor(player, options, ready) { 11927 // MediaLoader has no element 11928 const options_ = merge$2({ 11929 createEl: false 11930 }, options); 11931 super(player, options_, ready); 11932 11933 // If there are no sources when the player is initialized, 11934 // load the first supported playback technology. 11935 11936 if (!options.playerOptions.sources || options.playerOptions.sources.length === 0) { 11937 for (let i = 0, j = options.playerOptions.techOrder; i < j.length; i++) { 11938 const techName = toTitleCase$1(j[i]); 11939 let tech = Tech.getTech(techName); 11940
vendor: 5,851 bytes, lines 11941-12133
11941 // Support old behavior of techs being registered as components. 11942 // Remove once that deprecated behavior is removed. 11943 if (!techName) { 11944 tech = Component$1.getComponent(techName); 11945 } 11946 11947 // Check if the browser supports this technology 11948 if (tech && tech.isSupported()) { 11949 player.loadTech_(techName); 11950 break; 11951 } 11952 } 11953 } else { 11954 // Loop through playback technologies (e.g. HTML5) and check for support. 11955 // Then load the best source. 11956 // A few assumptions here: 11957 // All playback technologies respect preload false. 11958 player.src(options.playerOptions.sources); 11959 } 11960 } 11961 } 11962 Component$1.registerComponent('MediaLoader', MediaLoader); 11963 11964 /** 11965 * @file clickable-component.js 11966 */ 11967 11968 /** 11969 * Component which is clickable or keyboard actionable, but is not a 11970 * native HTML button. 11971 * 11972 * @extends Component 11973 */ 11974 class ClickableComponent extends Component$1 { 11975 /** 11976 * Creates an instance of this class. 11977 * 11978 * @param { import('./player').default } player 11979 * The `Player` that this class should be attached to. 11980 * 11981 * @param {Object} [options] 11982 * The key/value store of component options. 11983 * 11984 * @param {function} [options.clickHandler] 11985 * The function to call when the button is clicked / activated 11986 * 11987 * @param {string} [options.controlText] 11988 * The text to set on the button 11989 * 11990 * @param {string} [options.className] 11991 * A class or space separated list of classes to add the component 11992 * 11993 */ 11994 constructor(player, options) { 11995 super(player, options); 11996 if (this.options_.controlText) { 11997 this.controlText(this.options_.controlText); 11998 } 11999 this.handleMouseOver_ = e => this.handleMouseOver(e); 12000 this.handleMouseOut_ = e => this.handleMouseOut(e); 12001 this.handleClick_ = e => this.handleClick(e); 12002 this.handleKeyDown_ = e => this.handleKeyDown(e); 12003 this.emitTapEvents(); 12004 this.enable(); 12005 } 12006 12007 /** 12008 * Create the `ClickableComponent`s DOM element. 12009 * 12010 * @param {string} [tag=div] 12011 * The element's node type. 12012 * 12013 * @param {Object} [props={}] 12014 * An object of properties that should be set on the element. 12015 * 12016 * @param {Object} [attributes={}] 12017 * An object of attributes that should be set on the element. 12018 * 12019 * @return {Element} 12020 * The element that gets created. 12021 */ 12022 createEl(tag = 'div', props = {}, attributes = {}) { 12023 props = Object.assign({ 12024 className: this.buildCSSClass(), 12025 tabIndex: 0 12026 }, props); 12027 if (tag === 'button') { 12028 log$1.error(`Creating a ClickableComponent with an HTML element of ${tag} is not supported; use a Button instead.`); 12029 } 12030 12031 // Add ARIA attributes for clickable element which is not a native HTML button 12032 attributes = Object.assign({ 12033 role: 'button' 12034 }, attributes); 12035 this.tabIndex_ = props.tabIndex; 12036 const el = createEl(tag, props, attributes); 12037 if (!this.player_.options_.experimentalSvgIcons) { 12038 el.appendChild(createEl('span', { 12039 className: 'vjs-icon-placeholder' 12040 }, { 12041 'aria-hidden': true 12042 })); 12043 } 12044 this.createControlTextEl(el); 12045 return el; 12046 } 12047 dispose() { 12048 // remove controlTextEl_ on dispose 12049 this.controlTextEl_ = null; 12050 super.dispose(); 12051 } 12052 12053 /** 12054 * Create a control text element on this `ClickableComponent` 12055 * 12056 * @param {Element} [el] 12057 * Parent element for the control text. 12058 * 12059 * @return {Element} 12060 * The control text element that gets created. 12061 */ 12062 createControlTextEl(el) { 12063 this.controlTextEl_ = createEl('span', { 12064 className: 'vjs-control-text' 12065 }, { 12066 // let the screen reader user know that the text of the element may change 12067 'aria-live': 'polite' 12068 }); 12069 if (el) { 12070 el.appendChild(this.controlTextEl_); 12071 } 12072 this.controlText(this.controlText_, el); 12073 return this.controlTextEl_; 12074 } 12075 12076 /** 12077 * Get or set the localize text to use for the controls on the `ClickableComponent`. 12078 * 12079 * @param {string} [text] 12080 * Control text for element. 12081 * 12082 * @param {Element} [el=this.el()] 12083 * Element to set the title on. 12084 * 12085 * @return {string} 12086 * - The control text when getting 12087 */ 12088 controlText(text, el = this.el()) { 12089 if (text === undefined) { 12090 return this.controlText_ || 'Need Text'; 12091 } 12092 const localizedText = this.localize(text); 12093 12094 /** @protected */ 12095 this.controlText_ = text; 12096 textContent(this.controlTextEl_, localizedText); 12097 if (!this.nonIconControl && !this.player_.options_.noUITitleAttributes) { 12098 // Set title attribute if only an icon is shown 12099 el.setAttribute('title', localizedText); 12100 } 12101 } 12102 12103 /** 12104 * Builds the default DOM `className`. 12105 * 12106 * @return {string} 12107 * The DOM `className` for this object. 12108 */ 12109 buildCSSClass() { 12110 return `vjs-control vjs-button ${super.buildCSSClass()}`; 12111 } 12112 12113 /** 12114 * Enable this `ClickableComponent` 12115 */ 12116 enable() { 12117 if (!this.enabled_) { 12118 this.enabled_ = true; 12119 this.removeClass('vjs-disabled'); 12120 this.el_.setAttribute('aria-disabled', 'false'); 12121 if (typeof this.tabIndex_ !== 'undefined') { 12122 this.el_.setAttribute('tabIndex', this.tabIndex_); 12123 } 12124 this.on(['tap', 'click'], this.handleClick_); 12125 this.on('keydown', this.handleKeyDown_); 12126 } 12127 } 12128 12129 /** 12130 * Disable this `ClickableComponent` 12131 */ 12132 disable() { 12133 this.enabled_ = false;
vendor: 7,818 bytes, lines 12134-12385
12134 this.addClass('vjs-disabled'); 12135 this.el_.setAttribute('aria-disabled', 'true'); 12136 if (typeof this.tabIndex_ !== 'undefined') { 12137 this.el_.removeAttribute('tabIndex'); 12138 } 12139 this.off('mouseover', this.handleMouseOver_); 12140 this.off('mouseout', this.handleMouseOut_); 12141 this.off(['tap', 'click'], this.handleClick_); 12142 this.off('keydown', this.handleKeyDown_); 12143 } 12144 12145 /** 12146 * Handles language change in ClickableComponent for the player in components 12147 * 12148 * 12149 */ 12150 handleLanguagechange() { 12151 this.controlText(this.controlText_); 12152 } 12153 12154 /** 12155 * Event handler that is called when a `ClickableComponent` receives a 12156 * `click` or `tap` event. 12157 * 12158 * @param {Event} event 12159 * The `tap` or `click` event that caused this function to be called. 12160 * 12161 * @listens tap 12162 * @listens click 12163 * @abstract 12164 */ 12165 handleClick(event) { 12166 if (this.options_.clickHandler) { 12167 this.options_.clickHandler.call(this, arguments); 12168 } 12169 } 12170 12171 /** 12172 * Event handler that is called when a `ClickableComponent` receives a 12173 * `keydown` event. 12174 * 12175 * By default, if the key is Space or Enter, it will trigger a `click` event. 12176 * 12177 * @param {KeyboardEvent} event 12178 * The `keydown` event that caused this function to be called. 12179 * 12180 * @listens keydown 12181 */ 12182 handleKeyDown(event) { 12183 // Support Space or Enter key operation to fire a click event. Also, 12184 // prevent the event from propagating through the DOM and triggering 12185 // Player hotkeys. 12186 if (keycode.isEventKey(event, 'Space') || keycode.isEventKey(event, 'Enter')) { 12187 event.preventDefault(); 12188 event.stopPropagation(); 12189 this.trigger('click'); 12190 } else { 12191 // Pass keypress handling up for unsupported keys 12192 super.handleKeyDown(event); 12193 } 12194 } 12195 } 12196 Component$1.registerComponent('ClickableComponent', ClickableComponent); 12197 12198 /** 12199 * @file poster-image.js 12200 */ 12201 12202 /** 12203 * A `ClickableComponent` that handles showing the poster image for the player. 12204 * 12205 * @extends ClickableComponent 12206 */ 12207 class PosterImage extends ClickableComponent { 12208 /** 12209 * Create an instance of this class. 12210 * 12211 * @param { import('./player').default } player 12212 * The `Player` that this class should attach to. 12213 * 12214 * @param {Object} [options] 12215 * The key/value store of player options. 12216 */ 12217 constructor(player, options) { 12218 super(player, options); 12219 this.update(); 12220 this.update_ = e => this.update(e); 12221 player.on('posterchange', this.update_); 12222 } 12223 12224 /** 12225 * Clean up and dispose of the `PosterImage`. 12226 */ 12227 dispose() { 12228 this.player().off('posterchange', this.update_); 12229 super.dispose(); 12230 } 12231 12232 /** 12233 * Create the `PosterImage`s DOM element. 12234 * 12235 * @return {Element} 12236 * The element that gets created. 12237 */ 12238 createEl() { 12239 // The el is an empty div to keep position in the DOM 12240 // A picture and img el will be inserted when a source is set 12241 return createEl('div', { 12242 className: 'vjs-poster' 12243 }); 12244 } 12245 12246 /** 12247 * Get or set the `PosterImage`'s crossOrigin option. 12248 * 12249 * @param {string|null} [value] 12250 * The value to set the crossOrigin to. If an argument is 12251 * given, must be one of `'anonymous'` or `'use-credentials'`, or 'null'. 12252 * 12253 * @return {string|null} 12254 * - The current crossOrigin value of the `Player` when getting. 12255 * - undefined when setting 12256 */ 12257 crossOrigin(value) { 12258 // `null` can be set to unset a value 12259 if (typeof value === 'undefined') { 12260 if (this.$('img')) { 12261 // If the poster's element exists, give its value 12262 return this.$('img').crossOrigin; 12263 } else if (this.player_.tech_ && this.player_.tech_.isReady_) { 12264 // If not but the tech is ready, query the tech 12265 return this.player_.crossOrigin(); 12266 } 12267 // Otherwise check options as the poster is usually set before the state of crossorigin 12268 // can be retrieved by the getter 12269 return this.player_.options_.crossOrigin || this.player_.options_.crossorigin || null; 12270 } 12271 if (value !== null && value !== 'anonymous' && value !== 'use-credentials') { 12272 this.player_.log.warn(`crossOrigin must be null, "anonymous" or "use-credentials", given "${value}"`); 12273 return; 12274 } 12275 if (this.$('img')) { 12276 this.$('img').crossOrigin = value; 12277 } 12278 return; 12279 } 12280 12281 /** 12282 * An {@link EventTarget~EventListener} for {@link Player#posterchange} events. 12283 * 12284 * @listens Player#posterchange 12285 * 12286 * @param {Event} [event] 12287 * The `Player#posterchange` event that triggered this function. 12288 */ 12289 update(event) { 12290 const url = this.player().poster(); 12291 this.setSrc(url); 12292 12293 // If there's no poster source we should display:none on this component 12294 // so it's not still clickable or right-clickable 12295 if (url) { 12296 this.show(); 12297 } else { 12298 this.hide(); 12299 } 12300 } 12301 12302 /** 12303 * Set the source of the `PosterImage` depending on the display method. (Re)creates 12304 * the inner picture and img elementss when needed. 12305 * 12306 * @param {string} [url] 12307 * The URL to the source for the `PosterImage`. If not specified or falsy, 12308 * any source and ant inner picture/img are removed. 12309 */ 12310 setSrc(url) { 12311 if (!url) { 12312 this.el_.textContent = ''; 12313 return; 12314 } 12315 if (!this.$('img')) { 12316 this.el_.appendChild(createEl('picture', { 12317 className: 'vjs-poster', 12318 // Don't want poster to be tabbable. 12319 tabIndex: -1 12320 }, {}, createEl('img', { 12321 loading: 'lazy', 12322 crossOrigin: this.crossOrigin() 12323 }, { 12324 alt: '' 12325 }))); 12326 } 12327 this.$('img').src = url; 12328 } 12329 12330 /** 12331 * An {@link EventTarget~EventListener} for clicks on the `PosterImage`. See 12332 * {@link ClickableComponent#handleClick} for instances where this will be triggered. 12333 * 12334 * @listens tap 12335 * @listens click 12336 * @listens keydown 12337 * 12338 * @param {Event} event 12339 + The `click`, `tap` or `keydown` event that caused this function to be called. 12340 */ 12341 handleClick(event) { 12342 // We don't want a click to trigger playback when controls are disabled 12343 if (!this.player_.controls()) { 12344 return; 12345 } 12346 if (this.player_.tech(true)) { 12347 this.player_.tech(true).focus(); 12348 } 12349 if (this.player_.paused()) { 12350 silencePromise(this.player_.play()); 12351 } else { 12352 this.player_.pause(); 12353 } 12354 } 12355 } 12356 12357 /** 12358 * Get or set the `PosterImage`'s crossorigin option. For the HTML5 player, this 12359 * sets the `crossOrigin` property on the `<img>` tag to control the CORS 12360 * behavior. 12361 * 12362 * @param {string|null} [value] 12363 * The value to set the `PosterImages`'s crossorigin to. If an argument is 12364 * given, must be one of `anonymous` or `use-credentials`. 12365 * 12366 * @return {string|null|undefined} 12367 * - The current crossorigin value of the `Player` when getting. 12368 * - undefined when setting 12369 */ 12370 PosterImage.prototype.crossorigin = PosterImage.prototype.crossOrigin; 12371 Component$1.registerComponent('PosterImage', PosterImage); 12372 12373 /** 12374 * @file text-track-display.js 12375 */ 12376 const darkGray = '#222'; 12377 const lightGray = '#ccc'; 12378 const fontMap = { 12379 monospace: 'monospace', 12380 sansSerif: 'sans-serif', 12381 serif: 'serif', 12382 monospaceSansSerif: '"Andale Mono", "Lucida Console", monospace', 12383 monospaceSerif: '"Courier New", monospace', 12384 proportionalSansSerif: 'sans-serif', 12385 proportionalSerif: 'serif',
vendor: 20,012 bytes, lines 12386-12968
12386 casual: '"Comic Sans MS", Impact, fantasy', 12387 script: '"Monotype Corsiva", cursive', 12388 smallcaps: '"Andale Mono", "Lucida Console", monospace, sans-serif' 12389 }; 12390 12391 /** 12392 * Construct an rgba color from a given hex color code. 12393 * 12394 * @param {number} color 12395 * Hex number for color, like #f0e or #f604e2. 12396 * 12397 * @param {number} opacity 12398 * Value for opacity, 0.0 - 1.0. 12399 * 12400 * @return {string} 12401 * The rgba color that was created, like 'rgba(255, 0, 0, 0.3)'. 12402 */ 12403 function constructColor(color, opacity) { 12404 let hex; 12405 if (color.length === 4) { 12406 // color looks like "#f0e" 12407 hex = color[1] + color[1] + color[2] + color[2] + color[3] + color[3]; 12408 } else if (color.length === 7) { 12409 // color looks like "#f604e2" 12410 hex = color.slice(1); 12411 } else { 12412 throw new Error('Invalid color code provided, ' + color + '; must be formatted as e.g. #f0e or #f604e2.'); 12413 } 12414 return 'rgba(' + parseInt(hex.slice(0, 2), 16) + ',' + parseInt(hex.slice(2, 4), 16) + ',' + parseInt(hex.slice(4, 6), 16) + ',' + opacity + ')'; 12415 } 12416 12417 /** 12418 * Try to update the style of a DOM element. Some style changes will throw an error, 12419 * particularly in IE8. Those should be noops. 12420 * 12421 * @param {Element} el 12422 * The DOM element to be styled. 12423 * 12424 * @param {string} style 12425 * The CSS property on the element that should be styled. 12426 * 12427 * @param {string} rule 12428 * The style rule that should be applied to the property. 12429 * 12430 * @private 12431 */ 12432 function tryUpdateStyle(el, style, rule) { 12433 try { 12434 el.style[style] = rule; 12435 } catch (e) { 12436 // Satisfies linter. 12437 return; 12438 } 12439 } 12440 12441 /** 12442 * Converts the CSS top/right/bottom/left property numeric value to string in pixels. 12443 * 12444 * @param {number} position 12445 * The CSS top/right/bottom/left property value. 12446 * 12447 * @return {string} 12448 * The CSS property value that was created, like '10px'. 12449 * 12450 * @private 12451 */ 12452 function getCSSPositionValue(position) { 12453 return position ? `${position}px` : ''; 12454 } 12455 12456 /** 12457 * The component for displaying text track cues. 12458 * 12459 * @extends Component 12460 */ 12461 class TextTrackDisplay extends Component$1 { 12462 /** 12463 * Creates an instance of this class. 12464 * 12465 * @param { import('../player').default } player 12466 * The `Player` that this class should be attached to. 12467 * 12468 * @param {Object} [options] 12469 * The key/value store of player options. 12470 * 12471 * @param {Function} [ready] 12472 * The function to call when `TextTrackDisplay` is ready. 12473 */ 12474 constructor(player, options, ready) { 12475 super(player, options, ready); 12476 const updateDisplayTextHandler = e => this.updateDisplay(e); 12477 const updateDisplayHandler = e => { 12478 this.updateDisplayOverlay(); 12479 this.updateDisplay(e); 12480 }; 12481 player.on('loadstart', e => this.toggleDisplay(e)); 12482 player.on('texttrackchange', updateDisplayTextHandler); 12483 player.on('loadedmetadata', e => { 12484 this.updateDisplayOverlay(); 12485 this.preselectTrack(e); 12486 }); 12487 12488 // This used to be called during player init, but was causing an error 12489 // if a track should show by default and the display hadn't loaded yet. 12490 // Should probably be moved to an external track loader when we support 12491 // tracks that don't need a display. 12492 player.ready(bind_(this, function () { 12493 if (player.tech_ && player.tech_.featuresNativeTextTracks) { 12494 this.hide(); 12495 return; 12496 } 12497 player.on('fullscreenchange', updateDisplayHandler); 12498 player.on('playerresize', updateDisplayHandler); 12499 const screenOrientation = window.screen.orientation || window; 12500 const changeOrientationEvent = window.screen.orientation ? 'change' : 'orientationchange'; 12501 screenOrientation.addEventListener(changeOrientationEvent, updateDisplayHandler); 12502 player.on('dispose', () => screenOrientation.removeEventListener(changeOrientationEvent, updateDisplayHandler)); 12503 const tracks = this.options_.playerOptions.tracks || []; 12504 for (let i = 0; i < tracks.length; i++) { 12505 this.player_.addRemoteTextTrack(tracks[i], true); 12506 } 12507 this.preselectTrack(); 12508 })); 12509 } 12510 12511 /** 12512 * Preselect a track following this precedence: 12513 * - matches the previously selected {@link TextTrack}'s language and kind 12514 * - matches the previously selected {@link TextTrack}'s language only 12515 * - is the first default captions track 12516 * - is the first default descriptions track 12517 * 12518 * @listens Player#loadstart 12519 */ 12520 preselectTrack() { 12521 const modes = { 12522 captions: 1, 12523 subtitles: 1 12524 }; 12525 const trackList = this.player_.textTracks(); 12526 const userPref = this.player_.cache_.selectedLanguage; 12527 let firstDesc; 12528 let firstCaptions; 12529 let preferredTrack; 12530 for (let i = 0; i < trackList.length; i++) { 12531 const track = trackList[i]; 12532 if (userPref && userPref.enabled && userPref.language && userPref.language === track.language && track.kind in modes) { 12533 // Always choose the track that matches both language and kind 12534 if (track.kind === userPref.kind) { 12535 preferredTrack = track; 12536 // or choose the first track that matches language 12537 } else if (!preferredTrack) { 12538 preferredTrack = track; 12539 } 12540 12541 // clear everything if offTextTrackMenuItem was clicked 12542 } else if (userPref && !userPref.enabled) { 12543 preferredTrack = null; 12544 firstDesc = null; 12545 firstCaptions = null; 12546 } else if (track.default) { 12547 if (track.kind === 'descriptions' && !firstDesc) { 12548 firstDesc = track; 12549 } else if (track.kind in modes && !firstCaptions) { 12550 firstCaptions = track; 12551 } 12552 } 12553 } 12554 12555 // The preferredTrack matches the user preference and takes 12556 // precedence over all the other tracks. 12557 // So, display the preferredTrack before the first default track 12558 // and the subtitles/captions track before the descriptions track 12559 if (preferredTrack) { 12560 preferredTrack.mode = 'showing'; 12561 } else if (firstCaptions) { 12562 firstCaptions.mode = 'showing'; 12563 } else if (firstDesc) { 12564 firstDesc.mode = 'showing'; 12565 } 12566 } 12567 12568 /** 12569 * Turn display of {@link TextTrack}'s from the current state into the other state. 12570 * There are only two states: 12571 * - 'shown' 12572 * - 'hidden' 12573 * 12574 * @listens Player#loadstart 12575 */ 12576 toggleDisplay() { 12577 if (this.player_.tech_ && this.player_.tech_.featuresNativeTextTracks) { 12578 this.hide(); 12579 } else { 12580 this.show(); 12581 } 12582 } 12583 12584 /** 12585 * Create the {@link Component}'s DOM element. 12586 * 12587 * @return {Element} 12588 * The element that was created. 12589 */ 12590 createEl() { 12591 return super.createEl('div', { 12592 className: 'vjs-text-track-display' 12593 }, { 12594 'translate': 'yes', 12595 'aria-live': 'off', 12596 'aria-atomic': 'true' 12597 }); 12598 } 12599 12600 /** 12601 * Clear all displayed {@link TextTrack}s. 12602 */ 12603 clearDisplay() { 12604 if (typeof window.WebVTT === 'function') { 12605 window.WebVTT.processCues(window, [], this.el_); 12606 } 12607 } 12608 12609 /** 12610 * Update the displayed TextTrack when a either a {@link Player#texttrackchange} or 12611 * a {@link Player#fullscreenchange} is fired. 12612 * 12613 * @listens Player#texttrackchange 12614 * @listens Player#fullscreenchange 12615 */ 12616 updateDisplay() { 12617 const tracks = this.player_.textTracks(); 12618 const allowMultipleShowingTracks = this.options_.allowMultipleShowingTracks; 12619 this.clearDisplay(); 12620 if (allowMultipleShowingTracks) { 12621 const showingTracks = []; 12622 for (let i = 0; i < tracks.length; ++i) { 12623 const track = tracks[i]; 12624 if (track.mode !== 'showing') { 12625 continue; 12626 } 12627 showingTracks.push(track); 12628 } 12629 this.updateForTrack(showingTracks); 12630 return; 12631 } 12632 12633 // Track display prioritization model: if multiple tracks are 'showing', 12634 // display the first 'subtitles' or 'captions' track which is 'showing', 12635 // otherwise display the first 'descriptions' track which is 'showing' 12636 12637 let descriptionsTrack = null; 12638 let captionsSubtitlesTrack = null; 12639 let i = tracks.length; 12640 while (i--) { 12641 const track = tracks[i]; 12642 if (track.mode === 'showing') { 12643 if (track.kind === 'descriptions') { 12644 descriptionsTrack = track; 12645 } else { 12646 captionsSubtitlesTrack = track; 12647 } 12648 } 12649 } 12650 if (captionsSubtitlesTrack) { 12651 if (this.getAttribute('aria-live') !== 'off') { 12652 this.setAttribute('aria-live', 'off'); 12653 } 12654 this.updateForTrack(captionsSubtitlesTrack); 12655 } else if (descriptionsTrack) { 12656 if (this.getAttribute('aria-live') !== 'assertive') { 12657 this.setAttribute('aria-live', 'assertive'); 12658 } 12659 this.updateForTrack(descriptionsTrack); 12660 } 12661 } 12662 12663 /** 12664 * Updates the displayed TextTrack to be sure it overlays the video when a either 12665 * a {@link Player#texttrackchange} or a {@link Player#fullscreenchange} is fired. 12666 */ 12667 updateDisplayOverlay() { 12668 // inset-inline and inset-block are not supprted on old chrome, but these are 12669 // only likely to be used on TV devices 12670 if (!this.player_.videoHeight() || !window.CSS.supports('inset-inline: 10px')) { 12671 return; 12672 } 12673 const playerWidth = this.player_.currentWidth(); 12674 const playerHeight = this.player_.currentHeight(); 12675 const playerAspectRatio = playerWidth / playerHeight; 12676 const videoAspectRatio = this.player_.videoWidth() / this.player_.videoHeight(); 12677 let insetInlineMatch = 0; 12678 let insetBlockMatch = 0; 12679 if (Math.abs(playerAspectRatio - videoAspectRatio) > 0.1) { 12680 if (playerAspectRatio > videoAspectRatio) { 12681 insetInlineMatch = Math.round((playerWidth - playerHeight * videoAspectRatio) / 2); 12682 } else { 12683 insetBlockMatch = Math.round((playerHeight - playerWidth / videoAspectRatio) / 2); 12684 } 12685 } 12686 tryUpdateStyle(this.el_, 'insetInline', getCSSPositionValue(insetInlineMatch)); 12687 tryUpdateStyle(this.el_, 'insetBlock', getCSSPositionValue(insetBlockMatch)); 12688 } 12689 12690 /** 12691 * Style {@Link TextTrack} activeCues according to {@Link TextTrackSettings}. 12692 * 12693 * @param {TextTrack} track 12694 * Text track object containing active cues to style. 12695 */ 12696 updateDisplayState(track) { 12697 const overrides = this.player_.textTrackSettings.getValues(); 12698 const cues = track.activeCues; 12699 let i = cues.length; 12700 while (i--) { 12701 const cue = cues[i]; 12702 if (!cue) { 12703 continue; 12704 } 12705 const cueDiv = cue.displayState; 12706 if (overrides.color) { 12707 cueDiv.firstChild.style.color = overrides.color; 12708 } 12709 if (overrides.textOpacity) { 12710 tryUpdateStyle(cueDiv.firstChild, 'color', constructColor(overrides.color || '#fff', overrides.textOpacity)); 12711 } 12712 if (overrides.backgroundColor) { 12713 cueDiv.firstChild.style.backgroundColor = overrides.backgroundColor; 12714 } 12715 if (overrides.backgroundOpacity) { 12716 tryUpdateStyle(cueDiv.firstChild, 'backgroundColor', constructColor(overrides.backgroundColor || '#000', overrides.backgroundOpacity)); 12717 } 12718 if (overrides.windowColor) { 12719 if (overrides.windowOpacity) { 12720 tryUpdateStyle(cueDiv, 'backgroundColor', constructColor(overrides.windowColor, overrides.windowOpacity)); 12721 } else { 12722 cueDiv.style.backgroundColor = overrides.windowColor; 12723 } 12724 } 12725 if (overrides.edgeStyle) { 12726 if (overrides.edgeStyle === 'dropshadow') { 12727 cueDiv.firstChild.style.textShadow = `2px 2px 3px ${darkGray}, 2px 2px 4px ${darkGray}, 2px 2px 5px ${darkGray}`; 12728 } else if (overrides.edgeStyle === 'raised') { 12729 cueDiv.firstChild.style.textShadow = `1px 1px ${darkGray}, 2px 2px ${darkGray}, 3px 3px ${darkGray}`; 12730 } else if (overrides.edgeStyle === 'depressed') { 12731 cueDiv.firstChild.style.textShadow = `1px 1px ${lightGray}, 0 1px ${lightGray}, -1px -1px ${darkGray}, 0 -1px ${darkGray}`; 12732 } else if (overrides.edgeStyle === 'uniform') { 12733 cueDiv.firstChild.style.textShadow = `0 0 4px ${darkGray}, 0 0 4px ${darkGray}, 0 0 4px ${darkGray}, 0 0 4px ${darkGray}`; 12734 } 12735 } 12736 if (overrides.fontPercent && overrides.fontPercent !== 1) { 12737 const fontSize = window.parseFloat(cueDiv.style.fontSize); 12738 cueDiv.style.fontSize = fontSize * overrides.fontPercent + 'px'; 12739 cueDiv.style.height = 'auto'; 12740 cueDiv.style.top = 'auto'; 12741 } 12742 if (overrides.fontFamily && overrides.fontFamily !== 'default') { 12743 if (overrides.fontFamily === 'small-caps') { 12744 cueDiv.firstChild.style.fontVariant = 'small-caps'; 12745 } else { 12746 cueDiv.firstChild.style.fontFamily = fontMap[overrides.fontFamily]; 12747 } 12748 } 12749 } 12750 } 12751 12752 /** 12753 * Add an {@link TextTrack} to to the {@link Tech}s {@link TextTrackList}. 12754 * 12755 * @param {TextTrack|TextTrack[]} tracks 12756 * Text track object or text track array to be added to the list. 12757 */ 12758 updateForTrack(tracks) { 12759 if (!Array.isArray(tracks)) { 12760 tracks = [tracks]; 12761 } 12762 if (typeof window.WebVTT !== 'function' || tracks.every(track => { 12763 return !track.activeCues; 12764 })) { 12765 return; 12766 } 12767 const cues = []; 12768 12769 // push all active track cues 12770 for (let i = 0; i < tracks.length; ++i) { 12771 const track = tracks[i]; 12772 for (let j = 0; j < track.activeCues.length; ++j) { 12773 cues.push(track.activeCues[j]); 12774 } 12775 } 12776 12777 // removes all cues before it processes new ones 12778 window.WebVTT.processCues(window, cues, this.el_); 12779 12780 // add unique class to each language text track & add settings styling if necessary 12781 for (let i = 0; i < tracks.length; ++i) { 12782 const track = tracks[i]; 12783 for (let j = 0; j < track.activeCues.length; ++j) { 12784 const cueEl = track.activeCues[j].displayState; 12785 addClass(cueEl, 'vjs-text-track-cue', 'vjs-text-track-cue-' + (track.language ? track.language : i)); 12786 if (track.language) { 12787 setAttribute(cueEl, 'lang', track.language); 12788 } 12789 } 12790 if (this.player_.textTrackSettings) { 12791 this.updateDisplayState(track); 12792 } 12793 } 12794 } 12795 } 12796 Component$1.registerComponent('TextTrackDisplay', TextTrackDisplay); 12797 12798 /** 12799 * @file loading-spinner.js 12800 */ 12801 12802 /** 12803 * A loading spinner for use during waiting/loading events. 12804 * 12805 * @extends Component 12806 */ 12807 class LoadingSpinner extends Component$1 { 12808 /** 12809 * Create the `LoadingSpinner`s DOM element. 12810 * 12811 * @return {Element} 12812 * The dom element that gets created. 12813 */ 12814 createEl() { 12815 const isAudio = this.player_.isAudio(); 12816 const playerType = this.localize(isAudio ? 'Audio Player' : 'Video Player'); 12817 const controlText = createEl('span', { 12818 className: 'vjs-control-text', 12819 textContent: this.localize('{1} is loading.', [playerType]) 12820 }); 12821 const el = super.createEl('div', { 12822 className: 'vjs-loading-spinner', 12823 dir: 'ltr' 12824 }); 12825 el.appendChild(controlText); 12826 return el; 12827 } 12828 12829 /** 12830 * Update control text on languagechange 12831 */ 12832 handleLanguagechange() { 12833 this.$('.vjs-control-text').textContent = this.localize('{1} is loading.', [this.player_.isAudio() ? 'Audio Player' : 'Video Player']); 12834 } 12835 } 12836 Component$1.registerComponent('LoadingSpinner', LoadingSpinner); 12837 12838 /** 12839 * @file button.js 12840 */ 12841 12842 /** 12843 * Base class for all buttons. 12844 * 12845 * @extends ClickableComponent 12846 */ 12847 class Button extends ClickableComponent { 12848 /** 12849 * Create the `Button`s DOM element. 12850 * 12851 * @param {string} [tag="button"] 12852 * The element's node type. This argument is IGNORED: no matter what 12853 * is passed, it will always create a `button` element. 12854 * 12855 * @param {Object} [props={}] 12856 * An object of properties that should be set on the element. 12857 * 12858 * @param {Object} [attributes={}] 12859 * An object of attributes that should be set on the element. 12860 * 12861 * @return {Element} 12862 * The element that gets created. 12863 */ 12864 createEl(tag, props = {}, attributes = {}) { 12865 tag = 'button'; 12866 props = Object.assign({ 12867 className: this.buildCSSClass() 12868 }, props); 12869 12870 // Add attributes for button element 12871 attributes = Object.assign({ 12872 // Necessary since the default button type is "submit" 12873 type: 'button' 12874 }, attributes); 12875 const el = createEl(tag, props, attributes); 12876 if (!this.player_.options_.experimentalSvgIcons) { 12877 el.appendChild(createEl('span', { 12878 className: 'vjs-icon-placeholder' 12879 }, { 12880 'aria-hidden': true 12881 })); 12882 } 12883 this.createControlTextEl(el); 12884 return el; 12885 } 12886 12887 /** 12888 * Add a child `Component` inside of this `Button`. 12889 * 12890 * @param {string|Component} child 12891 * The name or instance of a child to add. 12892 * 12893 * @param {Object} [options={}] 12894 * The key/value store of options that will get passed to children of 12895 * the child. 12896 * 12897 * @return {Component} 12898 * The `Component` that gets added as a child. When using a string the 12899 * `Component` will get created by this process. 12900 * 12901 * @deprecated since version 5 12902 */ 12903 addChild(child, options = {}) { 12904 const className = this.constructor.name; 12905 log$1.warn(`Adding an actionable (user controllable) child to a Button (${className}) is not supported; use a ClickableComponent instead.`); 12906 12907 // Avoid the error message generated by ClickableComponent's addChild method 12908 return Component$1.prototype.addChild.call(this, child, options); 12909 } 12910 12911 /** 12912 * Enable the `Button` element so that it can be activated or clicked. Use this with 12913 * {@link Button#disable}. 12914 */ 12915 enable() { 12916 super.enable(); 12917 this.el_.removeAttribute('disabled'); 12918 } 12919 12920 /** 12921 * Disable the `Button` element so that it cannot be activated or clicked. Use this with 12922 * {@link Button#enable}. 12923 */ 12924 disable() { 12925 super.disable(); 12926 this.el_.setAttribute('disabled', 'disabled'); 12927 } 12928 12929 /** 12930 * This gets called when a `Button` has focus and `keydown` is triggered via a key 12931 * press. 12932 * 12933 * @param {KeyboardEvent} event 12934 * The event that caused this function to get called. 12935 * 12936 * @listens keydown 12937 */ 12938 handleKeyDown(event) { 12939 // Ignore Space or Enter key operation, which is handled by the browser for 12940 // a button - though not for its super class, ClickableComponent. Also, 12941 // prevent the event from propagating through the DOM and triggering Player 12942 // hotkeys. We do not preventDefault here because we _want_ the browser to 12943 // handle it. 12944 if (keycode.isEventKey(event, 'Space') || keycode.isEventKey(event, 'Enter')) { 12945 event.stopPropagation(); 12946 return; 12947 } 12948 12949 // Pass keypress handling up for unsupported keys 12950 super.handleKeyDown(event); 12951 } 12952 } 12953 Component$1.registerComponent('Button', Button); 12954 12955 /** 12956 * @file big-play-button.js 12957 */ 12958 12959 /** 12960 * The initial play button that shows before the video has played. The hiding of the 12961 * `BigPlayButton` get done via CSS and `Player` states. 12962 * 12963 * @extends Button 12964 */ 12965 class BigPlayButton extends Button { 12966 constructor(player, options) { 12967 super(player, options); 12968 this.mouseused_ = false;
vendor: 4,353 bytes, lines 12969-13126
12969 this.setIcon('play'); 12970 this.on('mousedown', e => this.handleMouseDown(e)); 12971 } 12972 12973 /** 12974 * Builds the default DOM `className`. 12975 * 12976 * @return {string} 12977 * The DOM `className` for this object. Always returns 'vjs-big-play-button'. 12978 */ 12979 buildCSSClass() { 12980 return 'vjs-big-play-button'; 12981 } 12982 12983 /** 12984 * This gets called when a `BigPlayButton` "clicked". See {@link ClickableComponent} 12985 * for more detailed information on what a click can be. 12986 * 12987 * @param {KeyboardEvent|MouseEvent|TouchEvent} event 12988 * The `keydown`, `tap`, or `click` event that caused this function to be 12989 * called. 12990 * 12991 * @listens tap 12992 * @listens click 12993 */ 12994 handleClick(event) { 12995 const playPromise = this.player_.play(); 12996 12997 // exit early if clicked via the mouse 12998 if (this.mouseused_ && 'clientX' in event && 'clientY' in event) { 12999 silencePromise(playPromise); 13000 if (this.player_.tech(true)) { 13001 this.player_.tech(true).focus(); 13002 } 13003 return; 13004 } 13005 const cb = this.player_.getChild('controlBar'); 13006 const playToggle = cb && cb.getChild('playToggle'); 13007 if (!playToggle) { 13008 this.player_.tech(true).focus(); 13009 return; 13010 } 13011 const playFocus = () => playToggle.focus(); 13012 if (isPromise(playPromise)) { 13013 playPromise.then(playFocus, () => {}); 13014 } else { 13015 this.setTimeout(playFocus, 1); 13016 } 13017 } 13018 13019 /** 13020 * Event handler that is called when a `BigPlayButton` receives a 13021 * `keydown` event. 13022 * 13023 * @param {KeyboardEvent} event 13024 * The `keydown` event that caused this function to be called. 13025 * 13026 * @listens keydown 13027 */ 13028 handleKeyDown(event) { 13029 this.mouseused_ = false; 13030 super.handleKeyDown(event); 13031 } 13032 13033 /** 13034 * Handle `mousedown` events on the `BigPlayButton`. 13035 * 13036 * @param {MouseEvent} event 13037 * `mousedown` or `touchstart` event that triggered this function 13038 * 13039 * @listens mousedown 13040 */ 13041 handleMouseDown(event) { 13042 this.mouseused_ = true; 13043 } 13044 } 13045 13046 /** 13047 * The text that should display over the `BigPlayButton`s controls. Added to for localization. 13048 * 13049 * @type {string} 13050 * @protected 13051 */ 13052 BigPlayButton.prototype.controlText_ = 'Play Video'; 13053 Component$1.registerComponent('BigPlayButton', BigPlayButton); 13054 13055 /** 13056 * @file close-button.js 13057 */ 13058 13059 /** 13060 * The `CloseButton` is a `{@link Button}` that fires a `close` event when 13061 * it gets clicked. 13062 * 13063 * @extends Button 13064 */ 13065 class CloseButton extends Button { 13066 /** 13067 * Creates an instance of the this class. 13068 * 13069 * @param { import('./player').default } player 13070 * The `Player` that this class should be attached to. 13071 * 13072 * @param {Object} [options] 13073 * The key/value store of player options. 13074 */ 13075 constructor(player, options) { 13076 super(player, options); 13077 this.setIcon('cancel'); 13078 this.controlText(options && options.controlText || this.localize('Close')); 13079 } 13080 13081 /** 13082 * Builds the default DOM `className`. 13083 * 13084 * @return {string} 13085 * The DOM `className` for this object. 13086 */ 13087 buildCSSClass() { 13088 return `vjs-close-button ${super.buildCSSClass()}`; 13089 } 13090 13091 /** 13092 * This gets called when a `CloseButton` gets clicked. See 13093 * {@link ClickableComponent#handleClick} for more information on when 13094 * this will be triggered 13095 * 13096 * @param {Event} event 13097 * The `keydown`, `tap`, or `click` event that caused this function to be 13098 * called. 13099 * 13100 * @listens tap 13101 * @listens click 13102 * @fires CloseButton#close 13103 */ 13104 handleClick(event) { 13105 /** 13106 * Triggered when the a `CloseButton` is clicked. 13107 * 13108 * @event CloseButton#close 13109 * @type {Event} 13110 * 13111 * @property {boolean} [bubbles=false] 13112 * set to false so that the close event does not 13113 * bubble up to parents if there is no listener 13114 */ 13115 this.trigger({ 13116 type: 'close', 13117 bubbles: false 13118 }); 13119 } 13120 /** 13121 * Event handler that is called when a `CloseButton` receives a 13122 * `keydown` event. 13123 * 13124 * By default, if the key is Esc, it will trigger a `click` event. 13125 * 13126 * @param {KeyboardEvent} event
vendor: 20,330 bytes, lines 13127-13851
13127 * The `keydown` event that caused this function to be called. 13128 * 13129 * @listens keydown 13130 */ 13131 handleKeyDown(event) { 13132 // Esc button will trigger `click` event 13133 if (keycode.isEventKey(event, 'Esc')) { 13134 event.preventDefault(); 13135 event.stopPropagation(); 13136 this.trigger('click'); 13137 } else { 13138 // Pass keypress handling up for unsupported keys 13139 super.handleKeyDown(event); 13140 } 13141 } 13142 } 13143 Component$1.registerComponent('CloseButton', CloseButton); 13144 13145 /** 13146 * @file play-toggle.js 13147 */ 13148 13149 /** 13150 * Button to toggle between play and pause. 13151 * 13152 * @extends Button 13153 */ 13154 class PlayToggle extends Button { 13155 /** 13156 * Creates an instance of this class. 13157 * 13158 * @param { import('./player').default } player 13159 * The `Player` that this class should be attached to. 13160 * 13161 * @param {Object} [options={}] 13162 * The key/value store of player options. 13163 */ 13164 constructor(player, options = {}) { 13165 super(player, options); 13166 13167 // show or hide replay icon 13168 options.replay = options.replay === undefined || options.replay; 13169 this.setIcon('play'); 13170 this.on(player, 'play', e => this.handlePlay(e)); 13171 this.on(player, 'pause', e => this.handlePause(e)); 13172 if (options.replay) { 13173 this.on(player, 'ended', e => this.handleEnded(e)); 13174 } 13175 } 13176 13177 /** 13178 * Builds the default DOM `className`. 13179 * 13180 * @return {string} 13181 * The DOM `className` for this object. 13182 */ 13183 buildCSSClass() { 13184 return `vjs-play-control ${super.buildCSSClass()}`; 13185 } 13186 13187 /** 13188 * This gets called when an `PlayToggle` is "clicked". See 13189 * {@link ClickableComponent} for more detailed information on what a click can be. 13190 * 13191 * @param {Event} [event] 13192 * The `keydown`, `tap`, or `click` event that caused this function to be 13193 * called. 13194 * 13195 * @listens tap 13196 * @listens click 13197 */ 13198 handleClick(event) { 13199 if (this.player_.paused()) { 13200 silencePromise(this.player_.play()); 13201 } else { 13202 this.player_.pause(); 13203 } 13204 } 13205 13206 /** 13207 * This gets called once after the video has ended and the user seeks so that 13208 * we can change the replay button back to a play button. 13209 * 13210 * @param {Event} [event] 13211 * The event that caused this function to run. 13212 * 13213 * @listens Player#seeked 13214 */ 13215 handleSeeked(event) { 13216 this.removeClass('vjs-ended'); 13217 if (this.player_.paused()) { 13218 this.handlePause(event); 13219 } else { 13220 this.handlePlay(event); 13221 } 13222 } 13223 13224 /** 13225 * Add the vjs-playing class to the element so it can change appearance. 13226 * 13227 * @param {Event} [event] 13228 * The event that caused this function to run. 13229 * 13230 * @listens Player#play 13231 */ 13232 handlePlay(event) { 13233 this.removeClass('vjs-ended', 'vjs-paused'); 13234 this.addClass('vjs-playing'); 13235 // change the button text to "Pause" 13236 this.setIcon('pause'); 13237 this.controlText('Pause'); 13238 } 13239 13240 /** 13241 * Add the vjs-paused class to the element so it can change appearance. 13242 * 13243 * @param {Event} [event] 13244 * The event that caused this function to run. 13245 * 13246 * @listens Player#pause 13247 */ 13248 handlePause(event) { 13249 this.removeClass('vjs-playing'); 13250 this.addClass('vjs-paused'); 13251 // change the button text to "Play" 13252 this.setIcon('play'); 13253 this.controlText('Play'); 13254 } 13255 13256 /** 13257 * Add the vjs-ended class to the element so it can change appearance 13258 * 13259 * @param {Event} [event] 13260 * The event that caused this function to run. 13261 * 13262 * @listens Player#ended 13263 */ 13264 handleEnded(event) { 13265 this.removeClass('vjs-playing'); 13266 this.addClass('vjs-ended'); 13267 // change the button text to "Replay" 13268 this.setIcon('replay'); 13269 this.controlText('Replay'); 13270 13271 // on the next seek remove the replay button 13272 this.one(this.player_, 'seeked', e => this.handleSeeked(e)); 13273 } 13274 } 13275 13276 /** 13277 * The text that should display over the `PlayToggle`s controls. Added for localization. 13278 * 13279 * @type {string} 13280 * @protected 13281 */ 13282 PlayToggle.prototype.controlText_ = 'Play'; 13283 Component$1.registerComponent('PlayToggle', PlayToggle); 13284 13285 /** 13286 * @file time-display.js 13287 */ 13288 13289 /** 13290 * Displays time information about the video 13291 * 13292 * @extends Component 13293 */ 13294 class TimeDisplay extends Component$1 { 13295 /** 13296 * Creates an instance of this class. 13297 * 13298 * @param { import('../../player').default } player 13299 * The `Player` that this class should be attached to. 13300 * 13301 * @param {Object} [options] 13302 * The key/value store of player options. 13303 */ 13304 constructor(player, options) { 13305 super(player, options); 13306 this.on(player, ['timeupdate', 'ended', 'seeking'], e => this.update(e)); 13307 this.updateTextNode_(); 13308 } 13309 13310 /** 13311 * Create the `Component`'s DOM element 13312 * 13313 * @return {Element} 13314 * The element that was created. 13315 */ 13316 createEl() { 13317 const className = this.buildCSSClass(); 13318 const el = super.createEl('div', { 13319 className: `${className} vjs-time-control vjs-control` 13320 }); 13321 const span = createEl('span', { 13322 className: 'vjs-control-text', 13323 textContent: `${this.localize(this.labelText_)}\u00a0` 13324 }, { 13325 role: 'presentation' 13326 }); 13327 el.appendChild(span); 13328 this.contentEl_ = createEl('span', { 13329 className: `${className}-display` 13330 }, { 13331 // span elements have no implicit role, but some screen readers (notably VoiceOver) 13332 // treat them as a break between items in the DOM when using arrow keys 13333 // (or left-to-right swipes on iOS) to read contents of a page. Using 13334 // role='presentation' causes VoiceOver to NOT treat this span as a break. 13335 role: 'presentation' 13336 }); 13337 el.appendChild(this.contentEl_); 13338 return el; 13339 } 13340 dispose() { 13341 this.contentEl_ = null; 13342 this.textNode_ = null; 13343 super.dispose(); 13344 } 13345 13346 /** 13347 * Updates the displayed time according to the `updateContent` function which is defined in the child class. 13348 * 13349 * @param {Event} [event] 13350 * The `timeupdate`, `ended` or `seeking` (if enableSmoothSeeking is true) event that caused this function to be called. 13351 */ 13352 update(event) { 13353 if (!this.player_.options_.enableSmoothSeeking && event.type === 'seeking') { 13354 return; 13355 } 13356 this.updateContent(event); 13357 } 13358 13359 /** 13360 * Updates the time display text node with a new time 13361 * 13362 * @param {number} [time=0] the time to update to 13363 * 13364 * @private 13365 */ 13366 updateTextNode_(time = 0) { 13367 time = formatTime(time); 13368 if (this.formattedTime_ === time) { 13369 return; 13370 } 13371 this.formattedTime_ = time; 13372 this.requestNamedAnimationFrame('TimeDisplay#updateTextNode_', () => { 13373 if (!this.contentEl_) { 13374 return; 13375 } 13376 let oldNode = this.textNode_; 13377 if (oldNode && this.contentEl_.firstChild !== oldNode) { 13378 oldNode = null; 13379 log$1.warn('TimeDisplay#updateTextnode_: Prevented replacement of text node element since it was no longer a child of this node. Appending a new node instead.'); 13380 } 13381 this.textNode_ = document.createTextNode(this.formattedTime_); 13382 if (!this.textNode_) { 13383 return; 13384 } 13385 if (oldNode) { 13386 this.contentEl_.replaceChild(this.textNode_, oldNode); 13387 } else { 13388 this.contentEl_.appendChild(this.textNode_); 13389 } 13390 }); 13391 } 13392 13393 /** 13394 * To be filled out in the child class, should update the displayed time 13395 * in accordance with the fact that the current time has changed. 13396 * 13397 * @param {Event} [event] 13398 * The `timeupdate` event that caused this to run. 13399 * 13400 * @listens Player#timeupdate 13401 */ 13402 updateContent(event) {} 13403 } 13404 13405 /** 13406 * The text that is added to the `TimeDisplay` for screen reader users. 13407 * 13408 * @type {string} 13409 * @private 13410 */ 13411 TimeDisplay.prototype.labelText_ = 'Time'; 13412 13413 /** 13414 * The text that should display over the `TimeDisplay`s controls. Added to for localization. 13415 * 13416 * @type {string} 13417 * @protected 13418 * 13419 * @deprecated in v7; controlText_ is not used in non-active display Components 13420 */ 13421 TimeDisplay.prototype.controlText_ = 'Time'; 13422 Component$1.registerComponent('TimeDisplay', TimeDisplay); 13423 13424 /** 13425 * @file current-time-display.js 13426 */ 13427 13428 /** 13429 * Displays the current time 13430 * 13431 * @extends Component 13432 */ 13433 class CurrentTimeDisplay extends TimeDisplay { 13434 /** 13435 * Builds the default DOM `className`. 13436 * 13437 * @return {string} 13438 * The DOM `className` for this object. 13439 */ 13440 buildCSSClass() { 13441 return 'vjs-current-time'; 13442 } 13443 13444 /** 13445 * Update current time display 13446 * 13447 * @param {Event} [event] 13448 * The `timeupdate` event that caused this function to run. 13449 * 13450 * @listens Player#timeupdate 13451 */ 13452 updateContent(event) { 13453 // Allows for smooth scrubbing, when player can't keep up. 13454 let time; 13455 if (this.player_.ended()) { 13456 time = this.player_.duration(); 13457 } else { 13458 time = this.player_.scrubbing() ? this.player_.getCache().currentTime : this.player_.currentTime(); 13459 } 13460 this.updateTextNode_(time); 13461 } 13462 } 13463 13464 /** 13465 * The text that is added to the `CurrentTimeDisplay` for screen reader users. 13466 * 13467 * @type {string} 13468 * @private 13469 */ 13470 CurrentTimeDisplay.prototype.labelText_ = 'Current Time'; 13471 13472 /** 13473 * The text that should display over the `CurrentTimeDisplay`s controls. Added to for localization. 13474 * 13475 * @type {string} 13476 * @protected 13477 * 13478 * @deprecated in v7; controlText_ is not used in non-active display Components 13479 */ 13480 CurrentTimeDisplay.prototype.controlText_ = 'Current Time'; 13481 Component$1.registerComponent('CurrentTimeDisplay', CurrentTimeDisplay); 13482 13483 /** 13484 * @file duration-display.js 13485 */ 13486 13487 /** 13488 * Displays the duration 13489 * 13490 * @extends Component 13491 */ 13492 class DurationDisplay extends TimeDisplay { 13493 /** 13494 * Creates an instance of this class. 13495 * 13496 * @param { import('../../player').default } player 13497 * The `Player` that this class should be attached to. 13498 * 13499 * @param {Object} [options] 13500 * The key/value store of player options. 13501 */ 13502 constructor(player, options) { 13503 super(player, options); 13504 const updateContent = e => this.updateContent(e); 13505 13506 // we do not want to/need to throttle duration changes, 13507 // as they should always display the changed duration as 13508 // it has changed 13509 this.on(player, 'durationchange', updateContent); 13510 13511 // Listen to loadstart because the player duration is reset when a new media element is loaded, 13512 // but the durationchange on the user agent will not fire. 13513 // @see [Spec]{@link https://www.w3.org/TR/2011/WD-html5-20110113/video.html#media-element-load-algorithm} 13514 this.on(player, 'loadstart', updateContent); 13515 13516 // Also listen for timeupdate (in the parent) and loadedmetadata because removing those 13517 // listeners could have broken dependent applications/libraries. These 13518 // can likely be removed for 7.0. 13519 this.on(player, 'loadedmetadata', updateContent); 13520 } 13521 13522 /** 13523 * Builds the default DOM `className`. 13524 * 13525 * @return {string} 13526 * The DOM `className` for this object. 13527 */ 13528 buildCSSClass() { 13529 return 'vjs-duration'; 13530 } 13531 13532 /** 13533 * Update duration time display. 13534 * 13535 * @param {Event} [event] 13536 * The `durationchange`, `timeupdate`, or `loadedmetadata` event that caused 13537 * this function to be called. 13538 * 13539 * @listens Player#durationchange 13540 * @listens Player#timeupdate 13541 * @listens Player#loadedmetadata 13542 */ 13543 updateContent(event) { 13544 const duration = this.player_.duration(); 13545 this.updateTextNode_(duration); 13546 } 13547 } 13548 13549 /** 13550 * The text that is added to the `DurationDisplay` for screen reader users. 13551 * 13552 * @type {string} 13553 * @private 13554 */ 13555 DurationDisplay.prototype.labelText_ = 'Duration'; 13556 13557 /** 13558 * The text that should display over the `DurationDisplay`s controls. Added to for localization. 13559 * 13560 * @type {string} 13561 * @protected 13562 * 13563 * @deprecated in v7; controlText_ is not used in non-active display Components 13564 */ 13565 DurationDisplay.prototype.controlText_ = 'Duration'; 13566 Component$1.registerComponent('DurationDisplay', DurationDisplay); 13567 13568 /** 13569 * @file time-divider.js 13570 */ 13571 13572 /** 13573 * The separator between the current time and duration. 13574 * Can be hidden if it's not needed in the design. 13575 * 13576 * @extends Component 13577 */ 13578 class TimeDivider extends Component$1 { 13579 /** 13580 * Create the component's DOM element 13581 * 13582 * @return {Element} 13583 * The element that was created. 13584 */ 13585 createEl() { 13586 const el = super.createEl('div', { 13587 className: 'vjs-time-control vjs-time-divider' 13588 }, { 13589 // this element and its contents can be hidden from assistive techs since 13590 // it is made extraneous by the announcement of the control text 13591 // for the current time and duration displays 13592 'aria-hidden': true 13593 }); 13594 const div = super.createEl('div'); 13595 const span = super.createEl('span', { 13596 textContent: '/' 13597 }); 13598 div.appendChild(span); 13599 el.appendChild(div); 13600 return el; 13601 } 13602 } 13603 Component$1.registerComponent('TimeDivider', TimeDivider); 13604 13605 /** 13606 * @file remaining-time-display.js 13607 */ 13608 13609 /** 13610 * Displays the time left in the video 13611 * 13612 * @extends Component 13613 */ 13614 class RemainingTimeDisplay extends TimeDisplay { 13615 /** 13616 * Creates an instance of this class. 13617 * 13618 * @param { import('../../player').default } player 13619 * The `Player` that this class should be attached to. 13620 * 13621 * @param {Object} [options] 13622 * The key/value store of player options. 13623 */ 13624 constructor(player, options) { 13625 super(player, options); 13626 this.on(player, 'durationchange', e => this.updateContent(e)); 13627 } 13628 13629 /** 13630 * Builds the default DOM `className`. 13631 * 13632 * @return {string} 13633 * The DOM `className` for this object. 13634 */ 13635 buildCSSClass() { 13636 return 'vjs-remaining-time'; 13637 } 13638 13639 /** 13640 * Create the `Component`'s DOM element with the "minus" character prepend to the time 13641 * 13642 * @return {Element} 13643 * The element that was created. 13644 */ 13645 createEl() { 13646 const el = super.createEl(); 13647 if (this.options_.displayNegative !== false) { 13648 el.insertBefore(createEl('span', {}, { 13649 'aria-hidden': true 13650 }, '-'), this.contentEl_); 13651 } 13652 return el; 13653 } 13654 13655 /** 13656 * Update remaining time display. 13657 * 13658 * @param {Event} [event] 13659 * The `timeupdate` or `durationchange` event that caused this to run. 13660 * 13661 * @listens Player#timeupdate 13662 * @listens Player#durationchange 13663 */ 13664 updateContent(event) { 13665 if (typeof this.player_.duration() !== 'number') { 13666 return; 13667 } 13668 let time; 13669 13670 // @deprecated We should only use remainingTimeDisplay 13671 // as of video.js 7 13672 if (this.player_.ended()) { 13673 time = 0; 13674 } else if (this.player_.remainingTimeDisplay) { 13675 time = this.player_.remainingTimeDisplay(); 13676 } else { 13677 time = this.player_.remainingTime(); 13678 } 13679 this.updateTextNode_(time); 13680 } 13681 } 13682 13683 /** 13684 * The text that is added to the `RemainingTimeDisplay` for screen reader users. 13685 * 13686 * @type {string} 13687 * @private 13688 */ 13689 RemainingTimeDisplay.prototype.labelText_ = 'Remaining Time'; 13690 13691 /** 13692 * The text that should display over the `RemainingTimeDisplay`s controls. Added to for localization. 13693 * 13694 * @type {string} 13695 * @protected 13696 * 13697 * @deprecated in v7; controlText_ is not used in non-active display Components 13698 */ 13699 RemainingTimeDisplay.prototype.controlText_ = 'Remaining Time'; 13700 Component$1.registerComponent('RemainingTimeDisplay', RemainingTimeDisplay); 13701 13702 /** 13703 * @file live-display.js 13704 */ 13705 13706 // TODO - Future make it click to snap to live 13707 13708 /** 13709 * Displays the live indicator when duration is Infinity. 13710 * 13711 * @extends Component 13712 */ 13713 class LiveDisplay extends Component$1 { 13714 /** 13715 * Creates an instance of this class. 13716 * 13717 * @param { import('./player').default } player 13718 * The `Player` that this class should be attached to. 13719 * 13720 * @param {Object} [options] 13721 * The key/value store of player options. 13722 */ 13723 constructor(player, options) { 13724 super(player, options); 13725 this.updateShowing(); 13726 this.on(this.player(), 'durationchange', e => this.updateShowing(e)); 13727 } 13728 13729 /** 13730 * Create the `Component`'s DOM element 13731 * 13732 * @return {Element} 13733 * The element that was created. 13734 */ 13735 createEl() { 13736 const el = super.createEl('div', { 13737 className: 'vjs-live-control vjs-control' 13738 }); 13739 this.contentEl_ = createEl('div', { 13740 className: 'vjs-live-display' 13741 }, { 13742 'aria-live': 'off' 13743 }); 13744 this.contentEl_.appendChild(createEl('span', { 13745 className: 'vjs-control-text', 13746 textContent: `${this.localize('Stream Type')}\u00a0` 13747 })); 13748 this.contentEl_.appendChild(document.createTextNode(this.localize('LIVE'))); 13749 el.appendChild(this.contentEl_); 13750 return el; 13751 } 13752 dispose() { 13753 this.contentEl_ = null; 13754 super.dispose(); 13755 } 13756 13757 /** 13758 * Check the duration to see if the LiveDisplay should be showing or not. Then show/hide 13759 * it accordingly 13760 * 13761 * @param {Event} [event] 13762 * The {@link Player#durationchange} event that caused this function to run. 13763 * 13764 * @listens Player#durationchange 13765 */ 13766 updateShowing(event) { 13767 if (this.player().duration() === Infinity) { 13768 this.show(); 13769 } else { 13770 this.hide(); 13771 } 13772 } 13773 } 13774 Component$1.registerComponent('LiveDisplay', LiveDisplay); 13775 13776 /** 13777 * @file seek-to-live.js 13778 */ 13779 13780 /** 13781 * Displays the live indicator when duration is Infinity. 13782 * 13783 * @extends Component 13784 */ 13785 class SeekToLive extends Button { 13786 /** 13787 * Creates an instance of this class. 13788 * 13789 * @param { import('./player').default } player 13790 * The `Player` that this class should be attached to. 13791 * 13792 * @param {Object} [options] 13793 * The key/value store of player options. 13794 */ 13795 constructor(player, options) { 13796 super(player, options); 13797 this.updateLiveEdgeStatus(); 13798 if (this.player_.liveTracker) { 13799 this.updateLiveEdgeStatusHandler_ = e => this.updateLiveEdgeStatus(e); 13800 this.on(this.player_.liveTracker, 'liveedgechange', this.updateLiveEdgeStatusHandler_); 13801 } 13802 } 13803 13804 /** 13805 * Create the `Component`'s DOM element 13806 * 13807 * @return {Element} 13808 * The element that was created. 13809 */ 13810 createEl() { 13811 const el = super.createEl('button', { 13812 className: 'vjs-seek-to-live-control vjs-control' 13813 }); 13814 this.setIcon('circle', el); 13815 this.textEl_ = createEl('span', { 13816 className: 'vjs-seek-to-live-text', 13817 textContent: this.localize('LIVE') 13818 }, { 13819 'aria-hidden': 'true' 13820 }); 13821 el.appendChild(this.textEl_); 13822 return el; 13823 } 13824 13825 /** 13826 * Update the state of this button if we are at the live edge 13827 * or not 13828 */ 13829 updateLiveEdgeStatus() { 13830 // default to live edge 13831 if (!this.player_.liveTracker || this.player_.liveTracker.atLiveEdge()) { 13832 this.setAttribute('aria-disabled', true); 13833 this.addClass('vjs-at-live-edge'); 13834 this.controlText('Seek to live, currently playing live'); 13835 } else { 13836 this.setAttribute('aria-disabled', false); 13837 this.removeClass('vjs-at-live-edge'); 13838 this.controlText('Seek to live, currently behind live'); 13839 } 13840 } 13841 13842 /** 13843 * On click bring us as near to the live point as possible. 13844 * This requires that we wait for the next `live-seekable-change` 13845 * event which will happen every segment length seconds. 13846 */ 13847 handleClick() { 13848 this.player_.liveTracker.seekToLiveEdge(); 13849 } 13850 13851 /**
13852 * Dispose of the element and stop tracking 13853 */ 13854 dispose() { 13855 if (this.player_.liveTracker) { 13856 this.off(this.player_.liveTracker, 'liveedgechange', this.updateLiveEdgeStatusHandler_); 13857 } 13858 this.textEl_ = null; 13859 super.dispose(); 13860 } 13861 } 13862 /** 13863 * The text that should display over the `SeekToLive`s control. Added for localization. 13864 * 13865 * @type {string} 13866 * @protected 13867 */ 13868 SeekToLive.prototype.controlText_ = 'Seek to live, currently playing live'; 13869 Component$1.registerComponent('SeekToLive', SeekToLive); 13870 13871 /** 13872 * @file num.js 13873 * @module num 13874 */ 13875 13876 /** 13877 * Keep a number between a min and a max value 13878 * 13879 * @param {number} number 13880 * The number to clamp 13881 * 13882 * @param {number} min 13883 * The minimum value 13884 * @param {number} max 13885 * The maximum value 13886 * 13887 * @return {number} 13888 * the clamped number 13889 */ 13890 function clamp(number, min, max) { 13891 number = Number(number); 13892 return Math.min(max, Math.max(min, isNaN(number) ? min : number)); 13893 } 13894 13895 var Num = /*#__PURE__*/Object.freeze({ 13896 __proto__: null, 13897 clamp: clamp 13898 }); 13899 13900 /** 13901 * @file slider.js 13902 */ 13903 13904 /** 13905 * The base functionality for a slider. Can be vertical or horizontal. 13906 * For instance the volume bar or the seek bar on a video is a slider. 13907 * 13908 * @extends Component 13909 */ 13910 class Slider extends Component$1 { 13911 /** 13912 * Create an instance of this class 13913 * 13914 * @param { import('../player').default
vendor: 8,858 bytes, lines 13914-14180
13914} player 13915 * The `Player` that this class should be attached to. 13916 * 13917 * @param {Object} [options] 13918 * The key/value store of player options. 13919 */ 13920 constructor(player, options) { 13921 super(player, options); 13922 this.handleMouseDown_ = e => this.handleMouseDown(e); 13923 this.handleMouseUp_ = e => this.handleMouseUp(e); 13924 this.handleKeyDown_ = e => this.handleKeyDown(e); 13925 this.handleClick_ = e => this.handleClick(e); 13926 this.handleMouseMove_ = e => this.handleMouseMove(e); 13927 this.update_ = e => this.update(e); 13928 13929 // Set property names to bar to match with the child Slider class is looking for 13930 this.bar = this.getChild(this.options_.barName); 13931 13932 // Set a horizontal or vertical class on the slider depending on the slider type 13933 this.vertical(!!this.options_.vertical); 13934 this.enable(); 13935 } 13936 13937 /** 13938 * Are controls are currently enabled for this slider or not. 13939 * 13940 * @return {boolean} 13941 * true if controls are enabled, false otherwise 13942 */ 13943 enabled() { 13944 return this.enabled_; 13945 } 13946 13947 /** 13948 * Enable controls for this slider if they are disabled 13949 */ 13950 enable() { 13951 if (this.enabled()) { 13952 return; 13953 } 13954 this.on('mousedown', this.handleMouseDown_); 13955 this.on('touchstart', this.handleMouseDown_); 13956 this.on('keydown', this.handleKeyDown_); 13957 this.on('click', this.handleClick_); 13958 13959 // TODO: deprecated, controlsvisible does not seem to be fired 13960 this.on(this.player_, 'controlsvisible', this.update); 13961 if (this.playerEvent) { 13962 this.on(this.player_, this.playerEvent, this.update); 13963 } 13964 this.removeClass('disabled'); 13965 this.setAttribute('tabindex', 0); 13966 this.enabled_ = true; 13967 } 13968 13969 /** 13970 * Disable controls for this slider if they are enabled 13971 */ 13972 disable() { 13973 if (!this.enabled()) { 13974 return; 13975 } 13976 const doc = this.bar.el_.ownerDocument; 13977 this.off('mousedown', this.handleMouseDown_); 13978 this.off('touchstart', this.handleMouseDown_); 13979 this.off('keydown', this.handleKeyDown_); 13980 this.off('click', this.handleClick_); 13981 this.off(this.player_, 'controlsvisible', this.update_); 13982 this.off(doc, 'mousemove', this.handleMouseMove_); 13983 this.off(doc, 'mouseup', this.handleMouseUp_); 13984 this.off(doc, 'touchmove', this.handleMouseMove_); 13985 this.off(doc, 'touchend', this.handleMouseUp_); 13986 this.removeAttribute('tabindex'); 13987 this.addClass('disabled'); 13988 if (this.playerEvent) { 13989 this.off(this.player_, this.playerEvent, this.update); 13990 } 13991 this.enabled_ = false; 13992 } 13993 13994 /** 13995 * Create the `Slider`s DOM element. 13996 * 13997 * @param {string} type 13998 * Type of element to create. 13999 * 14000 * @param {Object} [props={}] 14001 * List of properties in Object form. 14002 * 14003 * @param {Object} [attributes={}] 14004 * list of attributes in Object form. 14005 * 14006 * @return {Element} 14007 * The element that gets created. 14008 */ 14009 createEl(type, props = {}, attributes = {}) { 14010 // Add the slider element class to all sub classes 14011 props.className = props.className + ' vjs-slider'; 14012 props = Object.assign({ 14013 tabIndex: 0 14014 }, props); 14015 attributes = Object.assign({ 14016 'role': 'slider', 14017 'aria-valuenow': 0, 14018 'aria-valuemin': 0, 14019 'aria-valuemax': 100 14020 }, attributes); 14021 return super.createEl(type, props, attributes); 14022 } 14023 14024 /** 14025 * Handle `mousedown` or `touchstart` events on the `Slider`. 14026 * 14027 * @param {MouseEvent} event 14028 * `mousedown` or `touchstart` event that triggered this function 14029 * 14030 * @listens mousedown 14031 * @listens touchstart 14032 * @fires Slider#slideractive 14033 */ 14034 handleMouseDown(event) { 14035 const doc = this.bar.el_.ownerDocument; 14036 if (event.type === 'mousedown') { 14037 event.preventDefault(); 14038 } 14039 // Do not call preventDefault() on touchstart in Chrome 14040 // to avoid console warnings. Use a 'touch-action: none' style 14041 // instead to prevent unintended scrolling. 14042 // https://developers.google.com/web/updates/2017/01/scrolling-intervention 14043 if (event.type === 'touchstart' && !IS_CHROME) { 14044 event.preventDefault(); 14045 } 14046 blockTextSelection(); 14047 this.addClass('vjs-sliding'); 14048 /** 14049 * Triggered when the slider is in an active state 14050 * 14051 * @event Slider#slideractive 14052 * @type {MouseEvent} 14053 */ 14054 this.trigger('slideractive'); 14055 this.on(doc, 'mousemove', this.handleMouseMove_); 14056 this.on(doc, 'mouseup', this.handleMouseUp_); 14057 this.on(doc, 'touchmove', this.handleMouseMove_); 14058 this.on(doc, 'touchend', this.handleMouseUp_); 14059 this.handleMouseMove(event, true); 14060 } 14061 14062 /** 14063 * Handle the `mousemove`, `touchmove`, and `mousedown` events on this `Slider`. 14064 * The `mousemove` and `touchmove` events will only only trigger this function during 14065 * `mousedown` and `touchstart`. This is due to {@link Slider#handleMouseDown} and 14066 * {@link Slider#handleMouseUp}. 14067 * 14068 * @param {MouseEvent} event 14069 * `mousedown`, `mousemove`, `touchstart`, or `touchmove` event that triggered 14070 * this function 14071 * @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. 14072 * 14073 * @listens mousemove 14074 * @listens touchmove 14075 */ 14076 handleMouseMove(event) {} 14077 14078 /** 14079 * Handle `mouseup` or `touchend` events on the `Slider`. 14080 * 14081 * @param {MouseEvent} event 14082 * `mouseup` or `touchend` event that triggered this function. 14083 * 14084 * @listens touchend 14085 * @listens mouseup 14086 * @fires Slider#sliderinactive 14087 */ 14088 handleMouseUp(event) { 14089 const doc = this.bar.el_.ownerDocument; 14090 unblockTextSelection(); 14091 this.removeClass('vjs-sliding'); 14092 /** 14093 * Triggered when the slider is no longer in an active state. 14094 * 14095 * @event Slider#sliderinactive 14096 * @type {Event} 14097 */ 14098 this.trigger('sliderinactive'); 14099 this.off(doc, 'mousemove', this.handleMouseMove_); 14100 this.off(doc, 'mouseup', this.handleMouseUp_); 14101 this.off(doc, 'touchmove', this.handleMouseMove_); 14102 this.off(doc, 'touchend', this.handleMouseUp_); 14103 this.update(); 14104 } 14105 14106 /** 14107 * Update the progress bar of the `Slider`. 14108 * 14109 * @return {number} 14110 * The percentage of progress the progress bar represents as a 14111 * number from 0 to 1. 14112 */ 14113 update() { 14114 // In VolumeBar init we have a setTimeout for update that pops and update 14115 // to the end of the execution stack. The player is destroyed before then 14116 // update will cause an error 14117 // If there's no bar... 14118 if (!this.el_ || !this.bar) { 14119 return; 14120 } 14121 14122 // clamp progress between 0 and 1 14123 // and only round to four decimal places, as we round to two below 14124 const progress = this.getProgress(); 14125 if (progress === this.progress_) { 14126 return progress; 14127 } 14128 this.progress_ = progress; 14129 this.requestNamedAnimationFrame('Slider#update', () => { 14130 // Set the new bar width or height 14131 const sizeKey = this.vertical() ? 'height' : 'width'; 14132 14133 // Convert to a percentage for css value 14134 this.bar.el().style[sizeKey] = (progress * 100).toFixed(2) + '%'; 14135 }); 14136 return progress; 14137 } 14138 14139 /** 14140 * Get the percentage of the bar that should be filled 14141 * but clamped and rounded. 14142 * 14143 * @return {number} 14144 * percentage filled that the slider is 14145 */ 14146 getProgress() { 14147 return Number(clamp(this.getPercent(), 0, 1).toFixed(4)); 14148 } 14149 14150 /** 14151 * Calculate distance for slider 14152 * 14153 * @param {Event} event 14154 * The event that caused this function to run. 14155 * 14156 * @return {number} 14157 * The current position of the Slider. 14158 * - position.x for vertical `Slider`s 14159 * - position.y for horizontal `Slider`s 14160 */ 14161 calculateDistance(event) { 14162 const position = getPointerPosition(this.el_, event); 14163 if (this.vertical()) { 14164 return position.y; 14165 } 14166 return position.x; 14167 } 14168 14169 /** 14170 * Handle a `keydown` event on the `Slider`. Watches for left, right, up, and down 14171 * arrow keys. This function will only be called when the slider has focus. See 14172 * {@link Slider#handleFocus} and {@link Slider#handleBlur}. 14173 * 14174 * @param {KeyboardEvent} event 14175 * the `keydown` event that caused this function to run. 14176 * 14177 * @listens keydown 14178 */ 14179 handleKeyDown(event) { 14180
vendor: 88,768 bytes, lines 14180-17042
14180// Left and Down Arrows 14181 if (keycode.isEventKey(event, 'Left') || keycode.isEventKey(event, 'Down')) { 14182 event.preventDefault(); 14183 event.stopPropagation(); 14184 this.stepBack(); 14185 14186 // Up and Right Arrows 14187 } else if (keycode.isEventKey(event, 'Right') || keycode.isEventKey(event, 'Up')) { 14188 event.preventDefault(); 14189 event.stopPropagation(); 14190 this.stepForward(); 14191 } else { 14192 // Pass keydown handling up for unsupported keys 14193 super.handleKeyDown(event); 14194 } 14195 } 14196 14197 /** 14198 * Listener for click events on slider, used to prevent clicks 14199 * from bubbling up to parent elements like button menus. 14200 * 14201 * @param {Object} event 14202 * Event that caused this object to run 14203 */ 14204 handleClick(event) { 14205 event.stopPropagation(); 14206 event.preventDefault(); 14207 } 14208 14209 /** 14210 * Get/set if slider is horizontal for vertical 14211 * 14212 * @param {boolean} [bool] 14213 * - true if slider is vertical, 14214 * - false is horizontal 14215 * 14216 * @return {boolean} 14217 * - true if slider is vertical, and getting 14218 * - false if the slider is horizontal, and getting 14219 */ 14220 vertical(bool) { 14221 if (bool === undefined) { 14222 return this.vertical_ || false; 14223 } 14224 this.vertical_ = !!bool; 14225 if (this.vertical_) { 14226 this.addClass('vjs-slider-vertical'); 14227 } else { 14228 this.addClass('vjs-slider-horizontal'); 14229 } 14230 } 14231 } 14232 Component$1.registerComponent('Slider', Slider); 14233 14234 /** 14235 * @file load-progress-bar.js 14236 */ 14237 14238 // get the percent width of a time compared to the total end 14239 const percentify = (time, end) => clamp(time / end * 100, 0, 100).toFixed(2) + '%'; 14240 14241 /** 14242 * Shows loading progress 14243 * 14244 * @extends Component 14245 */ 14246 class LoadProgressBar extends Component$1 { 14247 /** 14248 * Creates an instance of this class. 14249 * 14250 * @param { import('../../player').default } player 14251 * The `Player` that this class should be attached to. 14252 * 14253 * @param {Object} [options] 14254 * The key/value store of player options. 14255 */ 14256 constructor(player, options) { 14257 super(player, options); 14258 this.partEls_ = []; 14259 this.on(player, 'progress', e => this.update(e)); 14260 } 14261 14262 /** 14263 * Create the `Component`'s DOM element 14264 * 14265 * @return {Element} 14266 * The element that was created. 14267 */ 14268 createEl() { 14269 const el = super.createEl('div', { 14270 className: 'vjs-load-progress' 14271 }); 14272 const wrapper = createEl('span', { 14273 className: 'vjs-control-text' 14274 }); 14275 const loadedText = createEl('span', { 14276 textContent: this.localize('Loaded') 14277 }); 14278 const separator = document.createTextNode(': '); 14279 this.percentageEl_ = createEl('span', { 14280 className: 'vjs-control-text-loaded-percentage', 14281 textContent: '0%' 14282 }); 14283 el.appendChild(wrapper); 14284 wrapper.appendChild(loadedText); 14285 wrapper.appendChild(separator); 14286 wrapper.appendChild(this.percentageEl_); 14287 return el; 14288 } 14289 dispose() { 14290 this.partEls_ = null; 14291 this.percentageEl_ = null; 14292 super.dispose(); 14293 } 14294 14295 /** 14296 * Update progress bar 14297 * 14298 * @param {Event} [event] 14299 * The `progress` event that caused this function to run. 14300 * 14301 * @listens Player#progress 14302 */ 14303 update(event) { 14304 this.requestNamedAnimationFrame('LoadProgressBar#update', () => { 14305 const liveTracker = this.player_.liveTracker; 14306 const buffered = this.player_.buffered(); 14307 const duration = liveTracker && liveTracker.isLive() ? liveTracker.seekableEnd() : this.player_.duration(); 14308 const bufferedEnd = this.player_.bufferedEnd(); 14309 const children = this.partEls_; 14310 const percent = percentify(bufferedEnd, duration); 14311 if (this.percent_ !== percent) { 14312 // update the width of the progress bar 14313 this.el_.style.width = percent; 14314 // update the control-text 14315 textContent(this.percentageEl_, percent); 14316 this.percent_ = percent; 14317 } 14318 14319 // add child elements to represent the individual buffered time ranges 14320 for (let i = 0; i < buffered.length; i++) { 14321 const start = buffered.start(i); 14322 const end = buffered.end(i); 14323 let part = children[i]; 14324 if (!part) { 14325 part = this.el_.appendChild(createEl()); 14326 children[i] = part; 14327 } 14328 14329 // only update if changed 14330 if (part.dataset.start === start && part.dataset.end === end) { 14331 continue; 14332 } 14333 part.dataset.start = start; 14334 part.dataset.end = end; 14335 14336 // set the percent based on the width of the progress bar (bufferedEnd) 14337 part.style.left = percentify(start, bufferedEnd); 14338 part.style.width = percentify(end - start, bufferedEnd); 14339 } 14340 14341 // remove unused buffered range elements 14342 for (let i = children.length; i > buffered.length; i--) { 14343 this.el_.removeChild(children[i - 1]); 14344 } 14345 children.length = buffered.length; 14346 }); 14347 } 14348 } 14349 Component$1.registerComponent('LoadProgressBar', LoadProgressBar); 14350 14351 /** 14352 * @file time-tooltip.js 14353 */ 14354 14355 /** 14356 * Time tooltips display a time above the progress bar. 14357 * 14358 * @extends Component 14359 */ 14360 class TimeTooltip extends Component$1 { 14361 /** 14362 * Creates an instance of this class. 14363 * 14364 * @param { import('../../player').default } player 14365 * The {@link Player} that this class should be attached to. 14366 * 14367 * @param {Object} [options] 14368 * The key/value store of player options. 14369 */ 14370 constructor(player, options) { 14371 super(player, options); 14372 this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL); 14373 } 14374 14375 /** 14376 * Create the time tooltip DOM element 14377 * 14378 * @return {Element} 14379 * The element that was created. 14380 */ 14381 createEl() { 14382 return super.createEl('div', { 14383 className: 'vjs-time-tooltip' 14384 }, { 14385 'aria-hidden': 'true' 14386 }); 14387 } 14388 14389 /** 14390 * Updates the position of the time tooltip relative to the `SeekBar`. 14391 * 14392 * @param {Object} seekBarRect 14393 * The `ClientRect` for the {@link SeekBar} element. 14394 * 14395 * @param {number} seekBarPoint 14396 * A number from 0 to 1, representing a horizontal reference point 14397 * from the left edge of the {@link SeekBar} 14398 */ 14399 update(seekBarRect, seekBarPoint, content) { 14400 const tooltipRect = findPosition(this.el_); 14401 const playerRect = getBoundingClientRect(this.player_.el()); 14402 const seekBarPointPx = seekBarRect.width * seekBarPoint; 14403 14404 // do nothing if either rect isn't available 14405 // for example, if the player isn't in the DOM for testing 14406 if (!playerRect || !tooltipRect) { 14407 return; 14408 } 14409 14410 // This is the space left of the `seekBarPoint` available within the bounds 14411 // of the player. We calculate any gap between the left edge of the player 14412 // and the left edge of the `SeekBar` and add the number of pixels in the 14413 // `SeekBar` before hitting the `seekBarPoint` 14414 const spaceLeftOfPoint = seekBarRect.left - playerRect.left + seekBarPointPx; 14415 14416 // This is the space right of the `seekBarPoint` available within the bounds 14417 // of the player. We calculate the number of pixels from the `seekBarPoint` 14418 // to the right edge of the `SeekBar` and add to that any gap between the 14419 // right edge of the `SeekBar` and the player. 14420 const spaceRightOfPoint = seekBarRect.width - seekBarPointPx + (playerRect.right - seekBarRect.right); 14421 14422 // This is the number of pixels by which the tooltip will need to be pulled 14423 // further to the right to center it over the `seekBarPoint`. 14424 let pullTooltipBy = tooltipRect.width / 2; 14425 14426 // Adjust the `pullTooltipBy` distance to the left or right depending on 14427 // the results of the space calculations above. 14428 if (spaceLeftOfPoint < pullTooltipBy) { 14429 pullTooltipBy += pullTooltipBy - spaceLeftOfPoint; 14430 } else if (spaceRightOfPoint < pullTooltipBy) { 14431 pullTooltipBy = spaceRightOfPoint; 14432 } 14433 14434 // Due to the imprecision of decimal/ratio based calculations and varying 14435 // rounding behaviors, there are cases where the spacing adjustment is off 14436 // by a pixel or two. This adds insurance to these calculations. 14437 if (pullTooltipBy < 0) { 14438 pullTooltipBy = 0; 14439 } else if (pullTooltipBy > tooltipRect.width) { 14440 pullTooltipBy = tooltipRect.width; 14441 } 14442 14443 // prevent small width fluctuations within 0.4px from 14444 // changing the value below. 14445 // This really helps for live to prevent the play 14446 // progress time tooltip from jittering 14447 pullTooltipBy = Math.round(pullTooltipBy); 14448 this.el_.style.right = `-${pullTooltipBy}px`; 14449 this.write(content); 14450 } 14451 14452 /** 14453 * Write the time to the tooltip DOM element. 14454 * 14455 * @param {string} content 14456 * The formatted time for the tooltip. 14457 */ 14458 write(content) { 14459 textContent(this.el_, content); 14460 } 14461 14462 /** 14463 * Updates the position of the time tooltip relative to the `SeekBar`. 14464 * 14465 * @param {Object} seekBarRect 14466 * The `ClientRect` for the {@link SeekBar} element. 14467 * 14468 * @param {number} seekBarPoint 14469 * A number from 0 to 1, representing a horizontal reference point 14470 * from the left edge of the {@link SeekBar} 14471 * 14472 * @param {number} time 14473 * The time to update the tooltip to, not used during live playback 14474 * 14475 * @param {Function} cb 14476 * A function that will be called during the request animation frame 14477 * for tooltips that need to do additional animations from the default 14478 */ 14479 updateTime(seekBarRect, seekBarPoint, time, cb) { 14480 this.requestNamedAnimationFrame('TimeTooltip#updateTime', () => { 14481 let content; 14482 const duration = this.player_.duration(); 14483 if (this.player_.liveTracker && this.player_.liveTracker.isLive()) { 14484 const liveWindow = this.player_.liveTracker.liveWindow(); 14485 const secondsBehind = liveWindow - seekBarPoint * liveWindow; 14486 content = (secondsBehind < 1 ? '' : '-') + formatTime(secondsBehind, liveWindow); 14487 } else { 14488 content = formatTime(time, duration); 14489 } 14490 this.update(seekBarRect, seekBarPoint, content); 14491 if (cb) { 14492 cb(); 14493 } 14494 }); 14495 } 14496 } 14497 Component$1.registerComponent('TimeTooltip', TimeTooltip); 14498 14499 /** 14500 * @file play-progress-bar.js 14501 */ 14502 14503 /** 14504 * Used by {@link SeekBar} to display media playback progress as part of the 14505 * {@link ProgressControl}. 14506 * 14507 * @extends Component 14508 */ 14509 class PlayProgressBar extends Component$1 { 14510 /** 14511 * Creates an instance of this class. 14512 * 14513 * @param { import('../../player').default } player 14514 * The {@link Player} that this class should be attached to. 14515 * 14516 * @param {Object} [options] 14517 * The key/value store of player options. 14518 */ 14519 constructor(player, options) { 14520 super(player, options); 14521 this.setIcon('circle'); 14522 this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL); 14523 } 14524 14525 /** 14526 * Create the the DOM element for this class. 14527 * 14528 * @return {Element} 14529 * The element that was created. 14530 */ 14531 createEl() { 14532 return super.createEl('div', { 14533 className: 'vjs-play-progress vjs-slider-bar' 14534 }, { 14535 'aria-hidden': 'true' 14536 }); 14537 } 14538 14539 /** 14540 * Enqueues updates to its own DOM as well as the DOM of its 14541 * {@link TimeTooltip} child. 14542 * 14543 * @param {Object} seekBarRect 14544 * The `ClientRect` for the {@link SeekBar} element. 14545 * 14546 * @param {number} seekBarPoint 14547 * A number from 0 to 1, representing a horizontal reference point 14548 * from the left edge of the {@link SeekBar} 14549 */ 14550 update(seekBarRect, seekBarPoint) { 14551 const timeTooltip = this.getChild('timeTooltip'); 14552 if (!timeTooltip) { 14553 return; 14554 } 14555 const time = this.player_.scrubbing() ? this.player_.getCache().currentTime : this.player_.currentTime(); 14556 timeTooltip.updateTime(seekBarRect, seekBarPoint, time); 14557 } 14558 } 14559 14560 /** 14561 * Default options for {@link PlayProgressBar}. 14562 * 14563 * @type {Object} 14564 * @private 14565 */ 14566 PlayProgressBar.prototype.options_ = { 14567 children: [] 14568 }; 14569 14570 // Time tooltips should not be added to a player on mobile devices 14571 if (!IS_IOS && !IS_ANDROID) { 14572 PlayProgressBar.prototype.options_.children.push('timeTooltip'); 14573 } 14574 Component$1.registerComponent('PlayProgressBar', PlayProgressBar); 14575 14576 /** 14577 * @file mouse-time-display.js 14578 */ 14579 14580 /** 14581 * The {@link MouseTimeDisplay} component tracks mouse movement over the 14582 * {@link ProgressControl}. It displays an indicator and a {@link TimeTooltip} 14583 * indicating the time which is represented by a given point in the 14584 * {@link ProgressControl}. 14585 * 14586 * @extends Component 14587 */ 14588 class MouseTimeDisplay extends Component$1 { 14589 /** 14590 * Creates an instance of this class. 14591 * 14592 * @param { import('../../player').default } player 14593 * The {@link Player} that this class should be attached to. 14594 * 14595 * @param {Object} [options] 14596 * The key/value store of player options. 14597 */ 14598 constructor(player, options) { 14599 super(player, options); 14600 this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL); 14601 } 14602 14603 /** 14604 * Create the DOM element for this class. 14605 * 14606 * @return {Element} 14607 * The element that was created. 14608 */ 14609 createEl() { 14610 return super.createEl('div', { 14611 className: 'vjs-mouse-display' 14612 }); 14613 } 14614 14615 /** 14616 * Enqueues updates to its own DOM as well as the DOM of its 14617 * {@link TimeTooltip} child. 14618 * 14619 * @param {Object} seekBarRect 14620 * The `ClientRect` for the {@link SeekBar} element. 14621 * 14622 * @param {number} seekBarPoint 14623 * A number from 0 to 1, representing a horizontal reference point 14624 * from the left edge of the {@link SeekBar} 14625 */ 14626 update(seekBarRect, seekBarPoint) { 14627 const time = seekBarPoint * this.player_.duration(); 14628 this.getChild('timeTooltip').updateTime(seekBarRect, seekBarPoint, time, () => { 14629 this.el_.style.left = `${seekBarRect.width * seekBarPoint}px`; 14630 }); 14631 } 14632 } 14633 14634 /** 14635 * Default options for `MouseTimeDisplay` 14636 * 14637 * @type {Object} 14638 * @private 14639 */ 14640 MouseTimeDisplay.prototype.options_ = { 14641 children: ['timeTooltip'] 14642 }; 14643 Component$1.registerComponent('MouseTimeDisplay', MouseTimeDisplay); 14644 14645 /** 14646 * @file seek-bar.js 14647 */ 14648 14649 // The number of seconds the `step*` functions move the timeline. 14650 const STEP_SECONDS = 5; 14651 14652 // The multiplier of STEP_SECONDS that PgUp/PgDown move the timeline. 14653 const PAGE_KEY_MULTIPLIER = 12; 14654 14655 /** 14656 * Seek bar and container for the progress bars. Uses {@link PlayProgressBar} 14657 * as its `bar`. 14658 * 14659 * @extends Slider 14660 */ 14661 class SeekBar extends Slider { 14662 /** 14663 * Creates an instance of this class. 14664 * 14665 * @param { import('../../player').default } player 14666 * The `Player` that this class should be attached to. 14667 * 14668 * @param {Object} [options] 14669 * The key/value store of player options. 14670 */ 14671 constructor(player, options) { 14672 super(player, options); 14673 this.setEventHandlers_(); 14674 } 14675 14676 /** 14677 * Sets the event handlers 14678 * 14679 * @private 14680 */ 14681 setEventHandlers_() { 14682 this.update_ = bind_(this, this.update); 14683 this.update = throttle(this.update_, UPDATE_REFRESH_INTERVAL); 14684 this.on(this.player_, ['ended', 'durationchange', 'timeupdate'], this.update); 14685 if (this.player_.liveTracker) { 14686 this.on(this.player_.liveTracker, 'liveedgechange', this.update); 14687 } 14688 14689 // when playing, let's ensure we smoothly update the play progress bar 14690 // via an interval 14691 this.updateInterval = null; 14692 this.enableIntervalHandler_ = e => this.enableInterval_(e); 14693 this.disableIntervalHandler_ = e => this.disableInterval_(e); 14694 this.on(this.player_, ['playing'], this.enableIntervalHandler_); 14695 this.on(this.player_, ['ended', 'pause', 'waiting'], this.disableIntervalHandler_); 14696 14697 // we don't need to update the play progress if the document is hidden, 14698 // also, this causes the CPU to spike and eventually crash the page on IE11. 14699 if ('hidden' in document && 'visibilityState' in document) { 14700 this.on(document, 'visibilitychange', this.toggleVisibility_); 14701 } 14702 } 14703 toggleVisibility_(e) { 14704 if (document.visibilityState === 'hidden') { 14705 this.cancelNamedAnimationFrame('SeekBar#update'); 14706 this.cancelNamedAnimationFrame('Slider#update'); 14707 this.disableInterval_(e); 14708 } else { 14709 if (!this.player_.ended() && !this.player_.paused()) { 14710 this.enableInterval_(); 14711 } 14712 14713 // we just switched back to the page and someone may be looking, so, update ASAP 14714 this.update(); 14715 } 14716 } 14717 enableInterval_() { 14718 if (this.updateInterval) { 14719 return; 14720 } 14721 this.updateInterval = this.setInterval(this.update, UPDATE_REFRESH_INTERVAL); 14722 } 14723 disableInterval_(e) { 14724 if (this.player_.liveTracker && this.player_.liveTracker.isLive() && e && e.type !== 'ended') { 14725 return; 14726 } 14727 if (!this.updateInterval) { 14728 return; 14729 } 14730 this.clearInterval(this.updateInterval); 14731 this.updateInterval = null; 14732 } 14733 14734 /** 14735 * Create the `Component`'s DOM element 14736 * 14737 * @return {Element} 14738 * The element that was created. 14739 */ 14740 createEl() { 14741 return super.createEl('div', { 14742 className: 'vjs-progress-holder' 14743 }, { 14744 'aria-label': this.localize('Progress Bar') 14745 }); 14746 } 14747 14748 /** 14749 * This function updates the play progress bar and accessibility 14750 * attributes to whatever is passed in. 14751 * 14752 * @param {Event} [event] 14753 * The `timeupdate` or `ended` event that caused this to run. 14754 * 14755 * @listens Player#timeupdate 14756 * 14757 * @return {number} 14758 * The current percent at a number from 0-1 14759 */ 14760 update(event) { 14761 // ignore updates while the tab is hidden 14762 if (document.visibilityState === 'hidden') { 14763 return; 14764 } 14765 const percent = super.update(); 14766 this.requestNamedAnimationFrame('SeekBar#update', () => { 14767 const currentTime = this.player_.ended() ? this.player_.duration() : this.getCurrentTime_(); 14768 const liveTracker = this.player_.liveTracker; 14769 let duration = this.player_.duration(); 14770 if (liveTracker && liveTracker.isLive()) { 14771 duration = this.player_.liveTracker.liveCurrentTime(); 14772 } 14773 if (this.percent_ !== percent) { 14774 // machine readable value of progress bar (percentage complete) 14775 this.el_.setAttribute('aria-valuenow', (percent * 100).toFixed(2)); 14776 this.percent_ = percent; 14777 } 14778 if (this.currentTime_ !== currentTime || this.duration_ !== duration) { 14779 // human readable value of progress bar (time complete) 14780 this.el_.setAttribute('aria-valuetext', this.localize('progress bar timing: currentTime={1} duration={2}', [formatTime(currentTime, duration), formatTime(duration, duration)], '{1} of {2}')); 14781 this.currentTime_ = currentTime; 14782 this.duration_ = duration; 14783 } 14784 14785 // update the progress bar time tooltip with the current time 14786 if (this.bar) { 14787 this.bar.update(getBoundingClientRect(this.el()), this.getProgress()); 14788 } 14789 }); 14790 return percent; 14791 } 14792 14793 /** 14794 * Prevent liveThreshold from causing seeks to seem like they 14795 * are not happening from a user perspective. 14796 * 14797 * @param {number} ct 14798 * current time to seek to 14799 */ 14800 userSeek_(ct) { 14801 if (this.player_.liveTracker && this.player_.liveTracker.isLive()) { 14802 this.player_.liveTracker.nextSeekedFromUser(); 14803 } 14804 this.player_.currentTime(ct); 14805 } 14806 14807 /** 14808 * Get the value of current time but allows for smooth scrubbing, 14809 * when player can't keep up. 14810 * 14811 * @return {number} 14812 * The current time value to display 14813 * 14814 * @private 14815 */ 14816 getCurrentTime_() { 14817 return this.player_.scrubbing() ? this.player_.getCache().currentTime : this.player_.currentTime(); 14818 } 14819 14820 /** 14821 * Get the percentage of media played so far. 14822 * 14823 * @return {number} 14824 * The percentage of media played so far (0 to 1). 14825 */ 14826 getPercent() { 14827 const currentTime = this.getCurrentTime_(); 14828 let percent; 14829 const liveTracker = this.player_.liveTracker; 14830 if (liveTracker && liveTracker.isLive()) { 14831 percent = (currentTime - liveTracker.seekableStart()) / liveTracker.liveWindow(); 14832 14833 // prevent the percent from changing at the live edge 14834 if (liveTracker.atLiveEdge()) { 14835 percent = 1; 14836 } 14837 } else { 14838 percent = currentTime / this.player_.duration(); 14839 } 14840 return percent; 14841 } 14842 14843 /** 14844 * Handle mouse down on seek bar 14845 * 14846 * @param {MouseEvent} event 14847 * The `mousedown` event that caused this to run. 14848 * 14849 * @listens mousedown 14850 */ 14851 handleMouseDown(event) { 14852 if (!isSingleLeftClick(event)) { 14853 return; 14854 } 14855 14856 // Stop event propagation to prevent double fire in progress-control.js 14857 event.stopPropagation(); 14858 this.videoWasPlaying = !this.player_.paused(); 14859 this.player_.pause(); 14860 super.handleMouseDown(event); 14861 } 14862 14863 /** 14864 * Handle mouse move on seek bar 14865 * 14866 * @param {MouseEvent} event 14867 * The `mousemove` event that caused this to run. 14868 * @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 14869 * 14870 * @listens mousemove 14871 */ 14872 handleMouseMove(event, mouseDown = false) { 14873 if (!isSingleLeftClick(event) || isNaN(this.player_.duration())) { 14874 return; 14875 } 14876 if (!mouseDown && !this.player_.scrubbing()) { 14877 this.player_.scrubbing(true); 14878 } 14879 let newTime; 14880 const distance = this.calculateDistance(event); 14881 const liveTracker = this.player_.liveTracker; 14882 if (!liveTracker || !liveTracker.isLive()) { 14883 newTime = distance * this.player_.duration(); 14884 14885 // Don't let video end while scrubbing. 14886 if (newTime === this.player_.duration()) { 14887 newTime = newTime - 0.1; 14888 } 14889 } else { 14890 if (distance >= 0.99) { 14891 liveTracker.seekToLiveEdge(); 14892 return; 14893 } 14894 const seekableStart = liveTracker.seekableStart(); 14895 const seekableEnd = liveTracker.liveCurrentTime(); 14896 newTime = seekableStart + distance * liveTracker.liveWindow(); 14897 14898 // Don't let video end while scrubbing. 14899 if (newTime >= seekableEnd) { 14900 newTime = seekableEnd; 14901 } 14902 14903 // Compensate for precision differences so that currentTime is not less 14904 // than seekable start 14905 if (newTime <= seekableStart) { 14906 newTime = seekableStart + 0.1; 14907 } 14908 14909 // On android seekableEnd can be Infinity sometimes, 14910 // this will cause newTime to be Infinity, which is 14911 // not a valid currentTime. 14912 if (newTime === Infinity) { 14913 return; 14914 } 14915 } 14916 14917 // Set new time (tell player to seek to new time) 14918 this.userSeek_(newTime); 14919 if (this.player_.options_.enableSmoothSeeking) { 14920 this.update(); 14921 } 14922 } 14923 enable() { 14924 super.enable(); 14925 const mouseTimeDisplay = this.getChild('mouseTimeDisplay'); 14926 if (!mouseTimeDisplay) { 14927 return; 14928 } 14929 mouseTimeDisplay.show(); 14930 } 14931 disable() { 14932 super.disable(); 14933 const mouseTimeDisplay = this.getChild('mouseTimeDisplay'); 14934 if (!mouseTimeDisplay) { 14935 return; 14936 } 14937 mouseTimeDisplay.hide(); 14938 } 14939 14940 /** 14941 * Handle mouse up on seek bar 14942 * 14943 * @param {MouseEvent} event 14944 * The `mouseup` event that caused this to run. 14945 * 14946 * @listens mouseup 14947 */ 14948 handleMouseUp(event) { 14949 super.handleMouseUp(event); 14950 14951 // Stop event propagation to prevent double fire in progress-control.js 14952 if (event) { 14953 event.stopPropagation(); 14954 } 14955 this.player_.scrubbing(false); 14956 14957 /** 14958 * Trigger timeupdate because we're done seeking and the time has changed. 14959 * This is particularly useful for if the player is paused to time the time displays. 14960 * 14961 * @event Tech#timeupdate 14962 * @type {Event} 14963 */ 14964 this.player_.trigger({ 14965 type: 'timeupdate', 14966 target: this, 14967 manuallyTriggered: true 14968 }); 14969 if (this.videoWasPlaying) { 14970 silencePromise(this.player_.play()); 14971 } else { 14972 // We're done seeking and the time has changed. 14973 // If the player is paused, make sure we display the correct time on the seek bar. 14974 this.update_(); 14975 } 14976 } 14977 14978 /** 14979 * Move more quickly fast forward for keyboard-only users 14980 */ 14981 stepForward() { 14982 this.userSeek_(this.player_.currentTime() + STEP_SECONDS); 14983 } 14984 14985 /** 14986 * Move more quickly rewind for keyboard-only users 14987 */ 14988 stepBack() { 14989 this.userSeek_(this.player_.currentTime() - STEP_SECONDS); 14990 } 14991 14992 /** 14993 * Toggles the playback state of the player 14994 * This gets called when enter or space is used on the seekbar 14995 * 14996 * @param {KeyboardEvent} event 14997 * The `keydown` event that caused this function to be called 14998 * 14999 */ 15000 handleAction(event) { 15001 if (this.player_.paused()) { 15002 this.player_.play(); 15003 } else { 15004 this.player_.pause(); 15005 } 15006 } 15007 15008 /** 15009 * Called when this SeekBar has focus and a key gets pressed down. 15010 * Supports the following keys: 15011 * 15012 * Space or Enter key fire a click event 15013 * Home key moves to start of the timeline 15014 * End key moves to end of the timeline 15015 * Digit "0" through "9" keys move to 0%, 10% ... 80%, 90% of the timeline 15016 * PageDown key moves back a larger step than ArrowDown 15017 * PageUp key moves forward a large step 15018 * 15019 * @param {KeyboardEvent} event 15020 * The `keydown` event that caused this function to be called. 15021 * 15022 * @listens keydown 15023 */ 15024 handleKeyDown(event) { 15025 const liveTracker = this.player_.liveTracker; 15026 if (keycode.isEventKey(event, 'Space') || keycode.isEventKey(event, 'Enter')) { 15027 event.preventDefault(); 15028 event.stopPropagation(); 15029 this.handleAction(event); 15030 } else if (keycode.isEventKey(event, 'Home')) { 15031 event.preventDefault(); 15032 event.stopPropagation(); 15033 this.userSeek_(0); 15034 } else if (keycode.isEventKey(event, 'End')) { 15035 event.preventDefault(); 15036 event.stopPropagation(); 15037 if (liveTracker && liveTracker.isLive()) { 15038 this.userSeek_(liveTracker.liveCurrentTime()); 15039 } else { 15040 this.userSeek_(this.player_.duration()); 15041 } 15042 } else if (/^[0-9]$/.test(keycode(event))) { 15043 event.preventDefault(); 15044 event.stopPropagation(); 15045 const gotoFraction = (keycode.codes[keycode(event)] - keycode.codes['0']) * 10.0 / 100.0; 15046 if (liveTracker && liveTracker.isLive()) { 15047 this.userSeek_(liveTracker.seekableStart() + liveTracker.liveWindow() * gotoFraction); 15048 } else { 15049 this.userSeek_(this.player_.duration() * gotoFraction); 15050 } 15051 } else if (keycode.isEventKey(event, 'PgDn')) { 15052 event.preventDefault(); 15053 event.stopPropagation(); 15054 this.userSeek_(this.player_.currentTime() - STEP_SECONDS * PAGE_KEY_MULTIPLIER); 15055 } else if (keycode.isEventKey(event, 'PgUp')) { 15056 event.preventDefault(); 15057 event.stopPropagation(); 15058 this.userSeek_(this.player_.currentTime() + STEP_SECONDS * PAGE_KEY_MULTIPLIER); 15059 } else { 15060 // Pass keydown handling up for unsupported keys 15061 super.handleKeyDown(event); 15062 } 15063 } 15064 dispose() { 15065 this.disableInterval_(); 15066 this.off(this.player_, ['ended', 'durationchange', 'timeupdate'], this.update); 15067 if (this.player_.liveTracker) { 15068 this.off(this.player_.liveTracker, 'liveedgechange', this.update); 15069 } 15070 this.off(this.player_, ['playing'], this.enableIntervalHandler_); 15071 this.off(this.player_, ['ended', 'pause', 'waiting'], this.disableIntervalHandler_); 15072 15073 // we don't need to update the play progress if the document is hidden, 15074 // also, this causes the CPU to spike and eventually crash the page on IE11. 15075 if ('hidden' in document && 'visibilityState' in document) { 15076 this.off(document, 'visibilitychange', this.toggleVisibility_); 15077 } 15078 super.dispose(); 15079 } 15080 } 15081 15082 /** 15083 * Default options for the `SeekBar` 15084 * 15085 * @type {Object} 15086 * @private 15087 */ 15088 SeekBar.prototype.options_ = { 15089 children: ['loadProgressBar', 'playProgressBar'], 15090 barName: 'playProgressBar' 15091 }; 15092 15093 // MouseTimeDisplay tooltips should not be added to a player on mobile devices 15094 if (!IS_IOS && !IS_ANDROID) { 15095 SeekBar.prototype.options_.children.splice(1, 0, 'mouseTimeDisplay'); 15096 } 15097 Component$1.registerComponent('SeekBar', SeekBar); 15098 15099 /** 15100 * @file progress-control.js 15101 */ 15102 15103 /** 15104 * The Progress Control component contains the seek bar, load progress, 15105 * and play progress. 15106 * 15107 * @extends Component 15108 */ 15109 class ProgressControl extends Component$1 { 15110 /** 15111 * Creates an instance of this class. 15112 * 15113 * @param { import('../../player').default } player 15114 * The `Player` that this class should be attached to. 15115 * 15116 * @param {Object} [options] 15117 * The key/value store of player options. 15118 */ 15119 constructor(player, options) { 15120 super(player, options); 15121 this.handleMouseMove = throttle(bind_(this, this.handleMouseMove), UPDATE_REFRESH_INTERVAL); 15122 this.throttledHandleMouseSeek = throttle(bind_(this, this.handleMouseSeek), UPDATE_REFRESH_INTERVAL); 15123 this.handleMouseUpHandler_ = e => this.handleMouseUp(e); 15124 this.handleMouseDownHandler_ = e => this.handleMouseDown(e); 15125 this.enable(); 15126 } 15127 15128 /** 15129 * Create the `Component`'s DOM element 15130 * 15131 * @return {Element} 15132 * The element that was created. 15133 */ 15134 createEl() { 15135 return super.createEl('div', { 15136 className: 'vjs-progress-control vjs-control' 15137 }); 15138 } 15139 15140 /** 15141 * When the mouse moves over the `ProgressControl`, the pointer position 15142 * gets passed down to the `MouseTimeDisplay` component. 15143 * 15144 * @param {Event} event 15145 * The `mousemove` event that caused this function to run. 15146 * 15147 * @listen mousemove 15148 */ 15149 handleMouseMove(event) { 15150 const seekBar = this.getChild('seekBar'); 15151 if (!seekBar) { 15152 return; 15153 } 15154 const playProgressBar = seekBar.getChild('playProgressBar'); 15155 const mouseTimeDisplay = seekBar.getChild('mouseTimeDisplay'); 15156 if (!playProgressBar && !mouseTimeDisplay) { 15157 return; 15158 } 15159 const seekBarEl = seekBar.el(); 15160 const seekBarRect = findPosition(seekBarEl); 15161 let seekBarPoint = getPointerPosition(seekBarEl, event).x; 15162 15163 // The default skin has a gap on either side of the `SeekBar`. This means 15164 // that it's possible to trigger this behavior outside the boundaries of 15165 // the `SeekBar`. This ensures we stay within it at all times. 15166 seekBarPoint = clamp(seekBarPoint, 0, 1); 15167 if (mouseTimeDisplay) { 15168 mouseTimeDisplay.update(seekBarRect, seekBarPoint); 15169 } 15170 if (playProgressBar) { 15171 playProgressBar.update(seekBarRect, seekBar.getProgress()); 15172 } 15173 } 15174 15175 /** 15176 * A throttled version of the {@link ProgressControl#handleMouseSeek} listener. 15177 * 15178 * @method ProgressControl#throttledHandleMouseSeek 15179 * @param {Event} event 15180 * The `mousemove` event that caused this function to run. 15181 * 15182 * @listen mousemove 15183 * @listen touchmove 15184 */ 15185 15186 /** 15187 * Handle `mousemove` or `touchmove` events on the `ProgressControl`. 15188 * 15189 * @param {Event} event 15190 * `mousedown` or `touchstart` event that triggered this function 15191 * 15192 * @listens mousemove 15193 * @listens touchmove 15194 */ 15195 handleMouseSeek(event) { 15196 const seekBar = this.getChild('seekBar'); 15197 if (seekBar) { 15198 seekBar.handleMouseMove(event); 15199 } 15200 } 15201 15202 /** 15203 * Are controls are currently enabled for this progress control. 15204 * 15205 * @return {boolean} 15206 * true if controls are enabled, false otherwise 15207 */ 15208 enabled() { 15209 return this.enabled_; 15210 } 15211 15212 /** 15213 * Disable all controls on the progress control and its children 15214 */ 15215 disable() { 15216 this.children().forEach(child => child.disable && child.disable()); 15217 if (!this.enabled()) { 15218 return; 15219 } 15220 this.off(['mousedown', 'touchstart'], this.handleMouseDownHandler_); 15221 this.off(this.el_, 'mousemove', this.handleMouseMove); 15222 this.removeListenersAddedOnMousedownAndTouchstart(); 15223 this.addClass('disabled'); 15224 this.enabled_ = false; 15225 15226 // Restore normal playback state if controls are disabled while scrubbing 15227 if (this.player_.scrubbing()) { 15228 const seekBar = this.getChild('seekBar'); 15229 this.player_.scrubbing(false); 15230 if (seekBar.videoWasPlaying) { 15231 silencePromise(this.player_.play()); 15232 } 15233 } 15234 } 15235 15236 /** 15237 * Enable all controls on the progress control and its children 15238 */ 15239 enable() { 15240 this.children().forEach(child => child.enable && child.enable()); 15241 if (this.enabled()) { 15242 return; 15243 } 15244 this.on(['mousedown', 'touchstart'], this.handleMouseDownHandler_); 15245 this.on(this.el_, 'mousemove', this.handleMouseMove); 15246 this.removeClass('disabled'); 15247 this.enabled_ = true; 15248 } 15249 15250 /** 15251 * Cleanup listeners after the user finishes interacting with the progress controls 15252 */ 15253 removeListenersAddedOnMousedownAndTouchstart() { 15254 const doc = this.el_.ownerDocument; 15255 this.off(doc, 'mousemove', this.throttledHandleMouseSeek); 15256 this.off(doc, 'touchmove', this.throttledHandleMouseSeek); 15257 this.off(doc, 'mouseup', this.handleMouseUpHandler_); 15258 this.off(doc, 'touchend', this.handleMouseUpHandler_); 15259 } 15260 15261 /** 15262 * Handle `mousedown` or `touchstart` events on the `ProgressControl`. 15263 * 15264 * @param {Event} event 15265 * `mousedown` or `touchstart` event that triggered this function 15266 * 15267 * @listens mousedown 15268 * @listens touchstart 15269 */ 15270 handleMouseDown(event) { 15271 const doc = this.el_.ownerDocument; 15272 const seekBar = this.getChild('seekBar'); 15273 if (seekBar) { 15274 seekBar.handleMouseDown(event); 15275 } 15276 this.on(doc, 'mousemove', this.throttledHandleMouseSeek); 15277 this.on(doc, 'touchmove', this.throttledHandleMouseSeek); 15278 this.on(doc, 'mouseup', this.handleMouseUpHandler_); 15279 this.on(doc, 'touchend', this.handleMouseUpHandler_); 15280 } 15281 15282 /** 15283 * Handle `mouseup` or `touchend` events on the `ProgressControl`. 15284 * 15285 * @param {Event} event 15286 * `mouseup` or `touchend` event that triggered this function. 15287 * 15288 * @listens touchend 15289 * @listens mouseup 15290 */ 15291 handleMouseUp(event) { 15292 const seekBar = this.getChild('seekBar'); 15293 if (seekBar) { 15294 seekBar.handleMouseUp(event); 15295 } 15296 this.removeListenersAddedOnMousedownAndTouchstart(); 15297 } 15298 } 15299 15300 /** 15301 * Default options for `ProgressControl` 15302 * 15303 * @type {Object} 15304 * @private 15305 */ 15306 ProgressControl.prototype.options_ = { 15307 children: ['seekBar'] 15308 }; 15309 Component$1.registerComponent('ProgressControl', ProgressControl); 15310 15311 /** 15312 * @file picture-in-picture-toggle.js 15313 */ 15314 15315 /** 15316 * Toggle Picture-in-Picture mode 15317 * 15318 * @extends Button 15319 */ 15320 class PictureInPictureToggle extends Button { 15321 /** 15322 * Creates an instance of this class. 15323 * 15324 * @param { import('./player').default } player 15325 * The `Player` that this class should be attached to. 15326 * 15327 * @param {Object} [options] 15328 * The key/value store of player options. 15329 * 15330 * @listens Player#enterpictureinpicture 15331 * @listens Player#leavepictureinpicture 15332 */ 15333 constructor(player, options) { 15334 super(player, options); 15335 this.setIcon('picture-in-picture-enter'); 15336 this.on(player, ['enterpictureinpicture', 'leavepictureinpicture'], e => this.handlePictureInPictureChange(e)); 15337 this.on(player, ['disablepictureinpicturechanged', 'loadedmetadata'], e => this.handlePictureInPictureEnabledChange(e)); 15338 this.on(player, ['loadedmetadata', 'audioonlymodechange', 'audiopostermodechange'], () => this.handlePictureInPictureAudioModeChange()); 15339 15340 // TODO: Deactivate button on player emptied event. 15341 this.disable(); 15342 } 15343 15344 /** 15345 * Builds the default DOM `className`. 15346 * 15347 * @return {string} 15348 * The DOM `className` for this object. 15349 */ 15350 buildCSSClass() { 15351 return `vjs-picture-in-picture-control vjs-hidden ${super.buildCSSClass()}`; 15352 } 15353 15354 /** 15355 * Displays or hides the button depending on the audio mode detection. 15356 * Exits picture-in-picture if it is enabled when switching to audio mode. 15357 */ 15358 handlePictureInPictureAudioModeChange() { 15359 // This audio detection will not detect HLS or DASH audio-only streams because there was no reliable way to detect them at the time 15360 const isSourceAudio = this.player_.currentType().substring(0, 5) === 'audio'; 15361 const isAudioMode = isSourceAudio || this.player_.audioPosterMode() || this.player_.audioOnlyMode(); 15362 if (!isAudioMode) { 15363 this.show(); 15364 return; 15365 } 15366 if (this.player_.isInPictureInPicture()) { 15367 this.player_.exitPictureInPicture(); 15368 } 15369 this.hide(); 15370 } 15371 15372 /** 15373 * Enables or disables button based on availability of a Picture-In-Picture mode. 15374 * 15375 * Enabled if 15376 * - `player.options().enableDocumentPictureInPicture` is true and 15377 * window.documentPictureInPicture is available; or 15378 * - `player.disablePictureInPicture()` is false and 15379 * element.requestPictureInPicture is available 15380 */ 15381 handlePictureInPictureEnabledChange() { 15382 if (document.pictureInPictureEnabled && this.player_.disablePictureInPicture() === false || this.player_.options_.enableDocumentPictureInPicture && 'documentPictureInPicture' in window) { 15383 this.enable(); 15384 } else { 15385 this.disable(); 15386 } 15387 } 15388 15389 /** 15390 * Handles enterpictureinpicture and leavepictureinpicture on the player and change control text accordingly. 15391 * 15392 * @param {Event} [event] 15393 * The {@link Player#enterpictureinpicture} or {@link Player#leavepictureinpicture} event that caused this function to be 15394 * called. 15395 * 15396 * @listens Player#enterpictureinpicture 15397 * @listens Player#leavepictureinpicture 15398 */ 15399 handlePictureInPictureChange(event) { 15400 if (this.player_.isInPictureInPicture()) { 15401 this.setIcon('picture-in-picture-exit'); 15402 this.controlText('Exit Picture-in-Picture'); 15403 } else { 15404 this.setIcon('picture-in-picture-enter'); 15405 this.controlText('Picture-in-Picture'); 15406 } 15407 this.handlePictureInPictureEnabledChange(); 15408 } 15409 15410 /** 15411 * This gets called when an `PictureInPictureToggle` is "clicked". See 15412 * {@link ClickableComponent} for more detailed information on what a click can be. 15413 * 15414 * @param {Event} [event] 15415 * The `keydown`, `tap`, or `click` event that caused this function to be 15416 * called. 15417 * 15418 * @listens tap 15419 * @listens click 15420 */ 15421 handleClick(event) { 15422 if (!this.player_.isInPictureInPicture()) { 15423 this.player_.requestPictureInPicture(); 15424 } else { 15425 this.player_.exitPictureInPicture(); 15426 } 15427 } 15428 15429 /** 15430 * Show the `Component`s element if it is hidden by removing the 15431 * 'vjs-hidden' class name from it only in browsers that support the Picture-in-Picture API. 15432 */ 15433 show() { 15434 // Does not allow to display the pictureInPictureToggle in browsers that do not support the Picture-in-Picture API, e.g. Firefox. 15435 if (typeof document.exitPictureInPicture !== 'function') { 15436 return; 15437 } 15438 super.show(); 15439 } 15440 } 15441 15442 /** 15443 * The text that should display over the `PictureInPictureToggle`s controls. Added for localization. 15444 * 15445 * @type {string} 15446 * @protected 15447 */ 15448 PictureInPictureToggle.prototype.controlText_ = 'Picture-in-Picture'; 15449 Component$1.registerComponent('PictureInPictureToggle', PictureInPictureToggle); 15450 15451 /** 15452 * @file fullscreen-toggle.js 15453 */ 15454 15455 /** 15456 * Toggle fullscreen video 15457 * 15458 * @extends Button 15459 */ 15460 class FullscreenToggle extends Button { 15461 /** 15462 * Creates an instance of this class. 15463 * 15464 * @param { import('./player').default } player 15465 * The `Player` that this class should be attached to. 15466 * 15467 * @param {Object} [options] 15468 * The key/value store of player options. 15469 */ 15470 constructor(player, options) { 15471 super(player, options); 15472 this.setIcon('fullscreen-enter'); 15473 this.on(player, 'fullscreenchange', e => this.handleFullscreenChange(e)); 15474 if (document[player.fsApi_.fullscreenEnabled] === false) { 15475 this.disable(); 15476 } 15477 } 15478 15479 /** 15480 * Builds the default DOM `className`. 15481 * 15482 * @return {string} 15483 * The DOM `className` for this object. 15484 */ 15485 buildCSSClass() { 15486 return `vjs-fullscreen-control ${super.buildCSSClass()}`; 15487 } 15488 15489 /** 15490 * Handles fullscreenchange on the player and change control text accordingly. 15491 * 15492 * @param {Event} [event] 15493 * The {@link Player#fullscreenchange} event that caused this function to be 15494 * called. 15495 * 15496 * @listens Player#fullscreenchange 15497 */ 15498 handleFullscreenChange(event) { 15499 if (this.player_.isFullscreen()) { 15500 this.controlText('Exit Fullscreen'); 15501 this.setIcon('fullscreen-exit'); 15502 } else { 15503 this.controlText('Fullscreen'); 15504 this.setIcon('fullscreen-enter'); 15505 } 15506 } 15507 15508 /** 15509 * This gets called when an `FullscreenToggle` is "clicked". See 15510 * {@link ClickableComponent} for more detailed information on what a click can be. 15511 * 15512 * @param {Event} [event] 15513 * The `keydown`, `tap`, or `click` event that caused this function to be 15514 * called. 15515 * 15516 * @listens tap 15517 * @listens click 15518 */ 15519 handleClick(event) { 15520 if (!this.player_.isFullscreen()) { 15521 this.player_.requestFullscreen(); 15522 } else { 15523 this.player_.exitFullscreen(); 15524 } 15525 } 15526 } 15527 15528 /** 15529 * The text that should display over the `FullscreenToggle`s controls. Added for localization. 15530 * 15531 * @type {string} 15532 * @protected 15533 */ 15534 FullscreenToggle.prototype.controlText_ = 'Fullscreen'; 15535 Component$1.registerComponent('FullscreenToggle', FullscreenToggle); 15536 15537 /** 15538 * Check if volume control is supported and if it isn't hide the 15539 * `Component` that was passed using the `vjs-hidden` class. 15540 * 15541 * @param { import('../../component').default } self 15542 * The component that should be hidden if volume is unsupported 15543 * 15544 * @param { import('../../player').default } player 15545 * A reference to the player 15546 * 15547 * @private 15548 */ 15549 const checkVolumeSupport = function (self, player) { 15550 // hide volume controls when they're not supported by the current tech 15551 if (player.tech_ && !player.tech_.featuresVolumeControl) { 15552 self.addClass('vjs-hidden'); 15553 } 15554 self.on(player, 'loadstart', function () { 15555 if (!player.tech_.featuresVolumeControl) { 15556 self.addClass('vjs-hidden'); 15557 } else { 15558 self.removeClass('vjs-hidden'); 15559 } 15560 }); 15561 }; 15562 15563 /** 15564 * @file volume-level.js 15565 */ 15566 15567 /** 15568 * Shows volume level 15569 * 15570 * @extends Component 15571 */ 15572 class VolumeLevel extends Component$1 { 15573 /** 15574 * Create the `Component`'s DOM element 15575 * 15576 * @return {Element} 15577 * The element that was created. 15578 */ 15579 createEl() { 15580 const el = super.createEl('div', { 15581 className: 'vjs-volume-level' 15582 }); 15583 this.setIcon('circle', el); 15584 el.appendChild(super.createEl('span', { 15585 className: 'vjs-control-text' 15586 })); 15587 return el; 15588 } 15589 } 15590 Component$1.registerComponent('VolumeLevel', VolumeLevel); 15591 15592 /** 15593 * @file volume-level-tooltip.js 15594 */ 15595 15596 /** 15597 * Volume level tooltips display a volume above or side by side the volume bar. 15598 * 15599 * @extends Component 15600 */ 15601 class VolumeLevelTooltip extends Component$1 { 15602 /** 15603 * Creates an instance of this class. 15604 * 15605 * @param { import('../../player').default } player 15606 * The {@link Player} that this class should be attached to. 15607 * 15608 * @param {Object} [options] 15609 * The key/value store of player options. 15610 */ 15611 constructor(player, options) { 15612 super(player, options); 15613 this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL); 15614 } 15615 15616 /** 15617 * Create the volume tooltip DOM element 15618 * 15619 * @return {Element} 15620 * The element that was created. 15621 */ 15622 createEl() { 15623 return super.createEl('div', { 15624 className: 'vjs-volume-tooltip' 15625 }, { 15626 'aria-hidden': 'true' 15627 }); 15628 } 15629 15630 /** 15631 * Updates the position of the tooltip relative to the `VolumeBar` and 15632 * its content text. 15633 * 15634 * @param {Object} rangeBarRect 15635 * The `ClientRect` for the {@link VolumeBar} element. 15636 * 15637 * @param {number} rangeBarPoint 15638 * A number from 0 to 1, representing a horizontal/vertical reference point 15639 * from the left edge of the {@link VolumeBar} 15640 * 15641 * @param {boolean} vertical 15642 * Referees to the Volume control position 15643 * in the control bar{@link VolumeControl} 15644 * 15645 */ 15646 update(rangeBarRect, rangeBarPoint, vertical, content) { 15647 if (!vertical) { 15648 const tooltipRect = getBoundingClientRect(this.el_); 15649 const playerRect = getBoundingClientRect(this.player_.el()); 15650 const volumeBarPointPx = rangeBarRect.width * rangeBarPoint; 15651 if (!playerRect || !tooltipRect) { 15652 return; 15653 } 15654 const spaceLeftOfPoint = rangeBarRect.left - playerRect.left + volumeBarPointPx; 15655 const spaceRightOfPoint = rangeBarRect.width - volumeBarPointPx + (playerRect.right - rangeBarRect.right); 15656 let pullTooltipBy = tooltipRect.width / 2; 15657 if (spaceLeftOfPoint < pullTooltipBy) { 15658 pullTooltipBy += pullTooltipBy - spaceLeftOfPoint; 15659 } else if (spaceRightOfPoint < pullTooltipBy) { 15660 pullTooltipBy = spaceRightOfPoint; 15661 } 15662 if (pullTooltipBy < 0) { 15663 pullTooltipBy = 0; 15664 } else if (pullTooltipBy > tooltipRect.width) { 15665 pullTooltipBy = tooltipRect.width; 15666 } 15667 this.el_.style.right = `-${pullTooltipBy}px`; 15668 } 15669 this.write(`${content}%`); 15670 } 15671 15672 /** 15673 * Write the volume to the tooltip DOM element. 15674 * 15675 * @param {string} content 15676 * The formatted volume for the tooltip. 15677 */ 15678 write(content) { 15679 textContent(this.el_, content); 15680 } 15681 15682 /** 15683 * Updates the position of the volume tooltip relative to the `VolumeBar`. 15684 * 15685 * @param {Object} rangeBarRect 15686 * The `ClientRect` for the {@link VolumeBar} element. 15687 * 15688 * @param {number} rangeBarPoint 15689 * A number from 0 to 1, representing a horizontal/vertical reference point 15690 * from the left edge of the {@link VolumeBar} 15691 * 15692 * @param {boolean} vertical 15693 * Referees to the Volume control position 15694 * in the control bar{@link VolumeControl} 15695 * 15696 * @param {number} volume 15697 * The volume level to update the tooltip to 15698 * 15699 * @param {Function} cb 15700 * A function that will be called during the request animation frame 15701 * for tooltips that need to do additional animations from the default 15702 */ 15703 updateVolume(rangeBarRect, rangeBarPoint, vertical, volume, cb) { 15704 this.requestNamedAnimationFrame('VolumeLevelTooltip#updateVolume', () => { 15705 this.update(rangeBarRect, rangeBarPoint, vertical, volume.toFixed(0)); 15706 if (cb) { 15707 cb(); 15708 } 15709 }); 15710 } 15711 } 15712 Component$1.registerComponent('VolumeLevelTooltip', VolumeLevelTooltip); 15713 15714 /** 15715 * @file mouse-volume-level-display.js 15716 */ 15717 15718 /** 15719 * The {@link MouseVolumeLevelDisplay} component tracks mouse movement over the 15720 * {@link VolumeControl}. It displays an indicator and a {@link VolumeLevelTooltip} 15721 * indicating the volume level which is represented by a given point in the 15722 * {@link VolumeBar}. 15723 * 15724 * @extends Component 15725 */ 15726 class MouseVolumeLevelDisplay extends Component$1 { 15727 /** 15728 * Creates an instance of this class. 15729 * 15730 * @param { import('../../player').default } player 15731 * The {@link Player} that this class should be attached to. 15732 * 15733 * @param {Object} [options] 15734 * The key/value store of player options. 15735 */ 15736 constructor(player, options) { 15737 super(player, options); 15738 this.update = throttle(bind_(this, this.update), UPDATE_REFRESH_INTERVAL); 15739 } 15740 15741 /** 15742 * Create the DOM element for this class. 15743 * 15744 * @return {Element} 15745 * The element that was created. 15746 */ 15747 createEl() { 15748 return super.createEl('div', { 15749 className: 'vjs-mouse-display' 15750 }); 15751 } 15752 15753 /** 15754 * Enquires updates to its own DOM as well as the DOM of its 15755 * {@link VolumeLevelTooltip} child. 15756 * 15757 * @param {Object} rangeBarRect 15758 * The `ClientRect` for the {@link VolumeBar} element. 15759 * 15760 * @param {number} rangeBarPoint 15761 * A number from 0 to 1, representing a horizontal/vertical reference point 15762 * from the left edge of the {@link VolumeBar} 15763 * 15764 * @param {boolean} vertical 15765 * Referees to the Volume control position 15766 * in the control bar{@link VolumeControl} 15767 * 15768 */ 15769 update(rangeBarRect, rangeBarPoint, vertical) { 15770 const volume = 100 * rangeBarPoint; 15771 this.getChild('volumeLevelTooltip').updateVolume(rangeBarRect, rangeBarPoint, vertical, volume, () => { 15772 if (vertical) { 15773 this.el_.style.bottom = `${rangeBarRect.height * rangeBarPoint}px`; 15774 } else { 15775 this.el_.style.left = `${rangeBarRect.width * rangeBarPoint}px`; 15776 } 15777 }); 15778 } 15779 } 15780 15781 /** 15782 * Default options for `MouseVolumeLevelDisplay` 15783 * 15784 * @type {Object} 15785 * @private 15786 */ 15787 MouseVolumeLevelDisplay.prototype.options_ = { 15788 children: ['volumeLevelTooltip'] 15789 }; 15790 Component$1.registerComponent('MouseVolumeLevelDisplay', MouseVolumeLevelDisplay); 15791 15792 /** 15793 * @file volume-bar.js 15794 */ 15795 15796 /** 15797 * The bar that contains the volume level and can be clicked on to adjust the level 15798 * 15799 * @extends Slider 15800 */ 15801 class VolumeBar extends Slider { 15802 /** 15803 * Creates an instance of this class. 15804 * 15805 * @param { import('../../player').default } player 15806 * The `Player` that this class should be attached to. 15807 * 15808 * @param {Object} [options] 15809 * The key/value store of player options. 15810 */ 15811 constructor(player, options) { 15812 super(player, options); 15813 this.on('slideractive', e => this.updateLastVolume_(e)); 15814 this.on(player, 'volumechange', e => this.updateARIAAttributes(e)); 15815 player.ready(() => this.updateARIAAttributes()); 15816 } 15817 15818 /** 15819 * Create the `Component`'s DOM element 15820 * 15821 * @return {Element} 15822 * The element that was created. 15823 */ 15824 createEl() { 15825 return super.createEl('div', { 15826 className: 'vjs-volume-bar vjs-slider-bar' 15827 }, { 15828 'aria-label': this.localize('Volume Level'), 15829 'aria-live': 'polite' 15830 }); 15831 } 15832 15833 /** 15834 * Handle mouse down on volume bar 15835 * 15836 * @param {Event} event 15837 * The `mousedown` event that caused this to run. 15838 * 15839 * @listens mousedown 15840 */ 15841 handleMouseDown(event) { 15842 if (!isSingleLeftClick(event)) { 15843 return; 15844 } 15845 super.handleMouseDown(event); 15846 } 15847 15848 /** 15849 * Handle movement events on the {@link VolumeMenuButton}. 15850 * 15851 * @param {Event} event 15852 * The event that caused this function to run. 15853 * 15854 * @listens mousemove 15855 */ 15856 handleMouseMove(event) { 15857 const mouseVolumeLevelDisplay = this.getChild('mouseVolumeLevelDisplay'); 15858 if (mouseVolumeLevelDisplay) { 15859 const volumeBarEl = this.el(); 15860 const volumeBarRect = getBoundingClientRect(volumeBarEl); 15861 const vertical = this.vertical(); 15862 let volumeBarPoint = getPointerPosition(volumeBarEl, event); 15863 volumeBarPoint = vertical ? volumeBarPoint.y : volumeBarPoint.x; 15864 // The default skin has a gap on either side of the `VolumeBar`. This means 15865 // that it's possible to trigger this behavior outside the boundaries of 15866 // the `VolumeBar`. This ensures we stay within it at all times. 15867 volumeBarPoint = clamp(volumeBarPoint, 0, 1); 15868 mouseVolumeLevelDisplay.update(volumeBarRect, volumeBarPoint, vertical); 15869 } 15870 if (!isSingleLeftClick(event)) { 15871 return; 15872 } 15873 this.checkMuted(); 15874 this.player_.volume(this.calculateDistance(event)); 15875 } 15876 15877 /** 15878 * If the player is muted unmute it. 15879 */ 15880 checkMuted() { 15881 if (this.player_.muted()) { 15882 this.player_.muted(false); 15883 } 15884 } 15885 15886 /** 15887 * Get percent of volume level 15888 * 15889 * @return {number} 15890 * Volume level percent as a decimal number. 15891 */ 15892 getPercent() { 15893 if (this.player_.muted()) { 15894 return 0; 15895 } 15896 return this.player_.volume(); 15897 } 15898 15899 /** 15900 * Increase volume level for keyboard users 15901 */ 15902 stepForward() { 15903 this.checkMuted(); 15904 this.player_.volume(this.player_.volume() + 0.1); 15905 } 15906 15907 /** 15908 * Decrease volume level for keyboard users 15909 */ 15910 stepBack() { 15911 this.checkMuted(); 15912 this.player_.volume(this.player_.volume() - 0.1); 15913 } 15914 15915 /** 15916 * Update ARIA accessibility attributes 15917 * 15918 * @param {Event} [event] 15919 * The `volumechange` event that caused this function to run. 15920 * 15921 * @listens Player#volumechange 15922 */ 15923 updateARIAAttributes(event) { 15924 const ariaValue = this.player_.muted() ? 0 : this.volumeAsPercentage_(); 15925 this.el_.setAttribute('aria-valuenow', ariaValue); 15926 this.el_.setAttribute('aria-valuetext', ariaValue + '%'); 15927 } 15928 15929 /** 15930 * Returns the current value of the player volume as a percentage 15931 * 15932 * @private 15933 */ 15934 volumeAsPercentage_() { 15935 return Math.round(this.player_.volume() * 100); 15936 } 15937 15938 /** 15939 * When user starts dragging the VolumeBar, store the volume and listen for 15940 * the end of the drag. When the drag ends, if the volume was set to zero, 15941 * set lastVolume to the stored volume. 15942 * 15943 * @listens slideractive 15944 * @private 15945 */ 15946 updateLastVolume_() { 15947 const volumeBeforeDrag = this.player_.volume(); 15948 this.one('sliderinactive', () => { 15949 if (this.player_.volume() === 0) { 15950 this.player_.lastVolume_(volumeBeforeDrag); 15951 } 15952 }); 15953 } 15954 } 15955 15956 /** 15957 * Default options for the `VolumeBar` 15958 * 15959 * @type {Object} 15960 * @private 15961 */ 15962 VolumeBar.prototype.options_ = { 15963 children: ['volumeLevel'], 15964 barName: 'volumeLevel' 15965 }; 15966 15967 // MouseVolumeLevelDisplay tooltip should not be added to a player on mobile devices 15968 if (!IS_IOS && !IS_ANDROID) { 15969 VolumeBar.prototype.options_.children.splice(0, 0, 'mouseVolumeLevelDisplay'); 15970 } 15971 15972 /** 15973 * Call the update event for this Slider when this event happens on the player. 15974 * 15975 * @type {string} 15976 */ 15977 VolumeBar.prototype.playerEvent = 'volumechange'; 15978 Component$1.registerComponent('VolumeBar', VolumeBar); 15979 15980 /** 15981 * @file volume-control.js 15982 */ 15983 15984 /** 15985 * The component for controlling the volume level 15986 * 15987 * @extends Component 15988 */ 15989 class VolumeControl extends Component$1 { 15990 /** 15991 * Creates an instance of this class. 15992 * 15993 * @param { import('../../player').default } player 15994 * The `Player` that this class should be attached to. 15995 * 15996 * @param {Object} [options={}] 15997 * The key/value store of player options. 15998 */ 15999 constructor(player, options = {}) { 16000 options.vertical = options.vertical || false; 16001 16002 // Pass the vertical option down to the VolumeBar if 16003 // the VolumeBar is turned on. 16004 if (typeof options.volumeBar === 'undefined' || isPlain(options.volumeBar)) { 16005 options.volumeBar = options.volumeBar || {}; 16006 options.volumeBar.vertical = options.vertical; 16007 } 16008 super(player, options); 16009 16010 // hide this control if volume support is missing 16011 checkVolumeSupport(this, player); 16012 this.throttledHandleMouseMove = throttle(bind_(this, this.handleMouseMove), UPDATE_REFRESH_INTERVAL); 16013 this.handleMouseUpHandler_ = e => this.handleMouseUp(e); 16014 this.on('mousedown', e => this.handleMouseDown(e)); 16015 this.on('touchstart', e => this.handleMouseDown(e)); 16016 this.on('mousemove', e => this.handleMouseMove(e)); 16017 16018 // while the slider is active (the mouse has been pressed down and 16019 // is dragging) or in focus we do not want to hide the VolumeBar 16020 this.on(this.volumeBar, ['focus', 'slideractive'], () => { 16021 this.volumeBar.addClass('vjs-slider-active'); 16022 this.addClass('vjs-slider-active'); 16023 this.trigger('slideractive'); 16024 }); 16025 this.on(this.volumeBar, ['blur', 'sliderinactive'], () => { 16026 this.volumeBar.removeClass('vjs-slider-active'); 16027 this.removeClass('vjs-slider-active'); 16028 this.trigger('sliderinactive'); 16029 }); 16030 } 16031 16032 /** 16033 * Create the `Component`'s DOM element 16034 * 16035 * @return {Element} 16036 * The element that was created. 16037 */ 16038 createEl() { 16039 let orientationClass = 'vjs-volume-horizontal'; 16040 if (this.options_.vertical) { 16041 orientationClass = 'vjs-volume-vertical'; 16042 } 16043 return super.createEl('div', { 16044 className: `vjs-volume-control vjs-control ${orientationClass}` 16045 }); 16046 } 16047 16048 /** 16049 * Handle `mousedown` or `touchstart` events on the `VolumeControl`. 16050 * 16051 * @param {Event} event 16052 * `mousedown` or `touchstart` event that triggered this function 16053 * 16054 * @listens mousedown 16055 * @listens touchstart 16056 */ 16057 handleMouseDown(event) { 16058 const doc = this.el_.ownerDocument; 16059 this.on(doc, 'mousemove', this.throttledHandleMouseMove); 16060 this.on(doc, 'touchmove', this.throttledHandleMouseMove); 16061 this.on(doc, 'mouseup', this.handleMouseUpHandler_); 16062 this.on(doc, 'touchend', this.handleMouseUpHandler_); 16063 } 16064 16065 /** 16066 * Handle `mouseup` or `touchend` events on the `VolumeControl`. 16067 * 16068 * @param {Event} event 16069 * `mouseup` or `touchend` event that triggered this function. 16070 * 16071 * @listens touchend 16072 * @listens mouseup 16073 */ 16074 handleMouseUp(event) { 16075 const doc = this.el_.ownerDocument; 16076 this.off(doc, 'mousemove', this.throttledHandleMouseMove); 16077 this.off(doc, 'touchmove', this.throttledHandleMouseMove); 16078 this.off(doc, 'mouseup', this.handleMouseUpHandler_); 16079 this.off(doc, 'touchend', this.handleMouseUpHandler_); 16080 } 16081 16082 /** 16083 * Handle `mousedown` or `touchstart` events on the `VolumeControl`. 16084 * 16085 * @param {Event} event 16086 * `mousedown` or `touchstart` event that triggered this function 16087 * 16088 * @listens mousedown 16089 * @listens touchstart 16090 */ 16091 handleMouseMove(event) { 16092 this.volumeBar.handleMouseMove(event); 16093 } 16094 } 16095 16096 /** 16097 * Default options for the `VolumeControl` 16098 * 16099 * @type {Object} 16100 * @private 16101 */ 16102 VolumeControl.prototype.options_ = { 16103 children: ['volumeBar'] 16104 }; 16105 Component$1.registerComponent('VolumeControl', VolumeControl); 16106 16107 /** 16108 * Check if muting volume is supported and if it isn't hide the mute toggle 16109 * button. 16110 * 16111 * @param { import('../../component').default } self 16112 * A reference to the mute toggle button 16113 * 16114 * @param { import('../../player').default } player 16115 * A reference to the player 16116 * 16117 * @private 16118 */ 16119 const checkMuteSupport = function (self, player) { 16120 // hide mute toggle button if it's not supported by the current tech 16121 if (player.tech_ && !player.tech_.featuresMuteControl) { 16122 self.addClass('vjs-hidden'); 16123 } 16124 self.on(player, 'loadstart', function () { 16125 if (!player.tech_.featuresMuteControl) { 16126 self.addClass('vjs-hidden'); 16127 } else { 16128 self.removeClass('vjs-hidden'); 16129 } 16130 }); 16131 }; 16132 16133 /** 16134 * @file mute-toggle.js 16135 */ 16136 16137 /** 16138 * A button component for muting the audio. 16139 * 16140 * @extends Button 16141 */ 16142 class MuteToggle extends Button { 16143 /** 16144 * Creates an instance of this class. 16145 * 16146 * @param { import('./player').default } player 16147 * The `Player` that this class should be attached to. 16148 * 16149 * @param {Object} [options] 16150 * The key/value store of player options. 16151 */ 16152 constructor(player, options) { 16153 super(player, options); 16154 16155 // hide this control if volume support is missing 16156 checkMuteSupport(this, player); 16157 this.on(player, ['loadstart', 'volumechange'], e => this.update(e)); 16158 } 16159 16160 /** 16161 * Builds the default DOM `className`. 16162 * 16163 * @return {string} 16164 * The DOM `className` for this object. 16165 */ 16166 buildCSSClass() { 16167 return `vjs-mute-control ${super.buildCSSClass()}`; 16168 } 16169 16170 /** 16171 * This gets called when an `MuteToggle` is "clicked". See 16172 * {@link ClickableComponent} for more detailed information on what a click can be. 16173 * 16174 * @param {Event} [event] 16175 * The `keydown`, `tap`, or `click` event that caused this function to be 16176 * called. 16177 * 16178 * @listens tap 16179 * @listens click 16180 */ 16181 handleClick(event) { 16182 const vol = this.player_.volume(); 16183 const lastVolume = this.player_.lastVolume_(); 16184 if (vol === 0) { 16185 const volumeToSet = lastVolume < 0.1 ? 0.1 : lastVolume; 16186 this.player_.volume(volumeToSet); 16187 this.player_.muted(false); 16188 } else { 16189 this.player_.muted(this.player_.muted() ? false : true); 16190 } 16191 } 16192 16193 /** 16194 * Update the `MuteToggle` button based on the state of `volume` and `muted` 16195 * on the player. 16196 * 16197 * @param {Event} [event] 16198 * The {@link Player#loadstart} event if this function was called 16199 * through an event. 16200 * 16201 * @listens Player#loadstart 16202 * @listens Player#volumechange 16203 */ 16204 update(event) { 16205 this.updateIcon_(); 16206 this.updateControlText_(); 16207 } 16208 16209 /** 16210 * Update the appearance of the `MuteToggle` icon. 16211 * 16212 * Possible states (given `level` variable below): 16213 * - 0: crossed out 16214 * - 1: zero bars of volume 16215 * - 2: one bar of volume 16216 * - 3: two bars of volume 16217 * 16218 * @private 16219 */ 16220 updateIcon_() { 16221 const vol = this.player_.volume(); 16222 let level = 3; 16223 this.setIcon('volume-high'); 16224 16225 // in iOS when a player is loaded with muted attribute 16226 // and volume is changed with a native mute button 16227 // we want to make sure muted state is updated 16228 if (IS_IOS && this.player_.tech_ && this.player_.tech_.el_) { 16229 this.player_.muted(this.player_.tech_.el_.muted); 16230 } 16231 if (vol === 0 || this.player_.muted()) { 16232 this.setIcon('volume-mute'); 16233 level = 0; 16234 } else if (vol < 0.33) { 16235 this.setIcon('volume-low'); 16236 level = 1; 16237 } else if (vol < 0.67) { 16238 this.setIcon('volume-medium'); 16239 level = 2; 16240 } 16241 removeClass(this.el_, [0, 1, 2, 3].reduce((str, i) => str + `${i ? ' ' : ''}vjs-vol-${i}`, '')); 16242 addClass(this.el_, `vjs-vol-${level}`); 16243 } 16244 16245 /** 16246 * If `muted` has changed on the player, update the control text 16247 * (`title` attribute on `vjs-mute-control` element and content of 16248 * `vjs-control-text` element). 16249 * 16250 * @private 16251 */ 16252 updateControlText_() { 16253 const soundOff = this.player_.muted() || this.player_.volume() === 0; 16254 const text = soundOff ? 'Unmute' : 'Mute'; 16255 if (this.controlText() !== text) { 16256 this.controlText(text); 16257 } 16258 } 16259 } 16260 16261 /** 16262 * The text that should display over the `MuteToggle`s controls. Added for localization. 16263 * 16264 * @type {string} 16265 * @protected 16266 */ 16267 MuteToggle.prototype.controlText_ = 'Mute'; 16268 Component$1.registerComponent('MuteToggle', MuteToggle); 16269 16270 /** 16271 * @file volume-control.js 16272 */ 16273 16274 /** 16275 * A Component to contain the MuteToggle and VolumeControl so that 16276 * they can work together. 16277 * 16278 * @extends Component 16279 */ 16280 class VolumePanel extends Component$1 { 16281 /** 16282 * Creates an instance of this class. 16283 * 16284 * @param { import('./player').default } player 16285 * The `Player` that this class should be attached to. 16286 * 16287 * @param {Object} [options={}] 16288 * The key/value store of player options. 16289 */ 16290 constructor(player, options = {}) { 16291 if (typeof options.inline !== 'undefined') { 16292 options.inline = options.inline; 16293 } else { 16294 options.inline = true; 16295 } 16296 16297 // pass the inline option down to the VolumeControl as vertical if 16298 // the VolumeControl is on. 16299 if (typeof options.volumeControl === 'undefined' || isPlain(options.volumeControl)) { 16300 options.volumeControl = options.volumeControl || {}; 16301 options.volumeControl.vertical = !options.inline; 16302 } 16303 super(player, options); 16304 16305 // this handler is used by mouse handler methods below 16306 this.handleKeyPressHandler_ = e => this.handleKeyPress(e); 16307 this.on(player, ['loadstart'], e => this.volumePanelState_(e)); 16308 this.on(this.muteToggle, 'keyup', e => this.handleKeyPress(e)); 16309 this.on(this.volumeControl, 'keyup', e => this.handleVolumeControlKeyUp(e)); 16310 this.on('keydown', e => this.handleKeyPress(e)); 16311 this.on('mouseover', e => this.handleMouseOver(e)); 16312 this.on('mouseout', e => this.handleMouseOut(e)); 16313 16314 // while the slider is active (the mouse has been pressed down and 16315 // is dragging) we do not want to hide the VolumeBar 16316 this.on(this.volumeControl, ['slideractive'], this.sliderActive_); 16317 this.on(this.volumeControl, ['sliderinactive'], this.sliderInactive_); 16318 } 16319 16320 /** 16321 * Add vjs-slider-active class to the VolumePanel 16322 * 16323 * @listens VolumeControl#slideractive 16324 * @private 16325 */ 16326 sliderActive_() { 16327 this.addClass('vjs-slider-active'); 16328 } 16329 16330 /** 16331 * Removes vjs-slider-active class to the VolumePanel 16332 * 16333 * @listens VolumeControl#sliderinactive 16334 * @private 16335 */ 16336 sliderInactive_() { 16337 this.removeClass('vjs-slider-active'); 16338 } 16339 16340 /** 16341 * Adds vjs-hidden or vjs-mute-toggle-only to the VolumePanel 16342 * depending on MuteToggle and VolumeControl state 16343 * 16344 * @listens Player#loadstart 16345 * @private 16346 */ 16347 volumePanelState_() { 16348 // hide volume panel if neither volume control or mute toggle 16349 // are displayed 16350 if (this.volumeControl.hasClass('vjs-hidden') && this.muteToggle.hasClass('vjs-hidden')) { 16351 this.addClass('vjs-hidden'); 16352 } 16353 16354 // if only mute toggle is visible we don't want 16355 // volume panel expanding when hovered or active 16356 if (this.volumeControl.hasClass('vjs-hidden') && !this.muteToggle.hasClass('vjs-hidden')) { 16357 this.addClass('vjs-mute-toggle-only'); 16358 } 16359 } 16360 16361 /** 16362 * Create the `Component`'s DOM element 16363 * 16364 * @return {Element} 16365 * The element that was created. 16366 */ 16367 createEl() { 16368 let orientationClass = 'vjs-volume-panel-horizontal'; 16369 if (!this.options_.inline) { 16370 orientationClass = 'vjs-volume-panel-vertical'; 16371 } 16372 return super.createEl('div', { 16373 className: `vjs-volume-panel vjs-control ${orientationClass}` 16374 }); 16375 } 16376 16377 /** 16378 * Dispose of the `volume-panel` and all child components. 16379 */ 16380 dispose() { 16381 this.handleMouseOut(); 16382 super.dispose(); 16383 } 16384 16385 /** 16386 * Handles `keyup` events on the `VolumeControl`, looking for ESC, which closes 16387 * the volume panel and sets focus on `MuteToggle`. 16388 * 16389 * @param {Event} event 16390 * The `keyup` event that caused this function to be called. 16391 * 16392 * @listens keyup 16393 */ 16394 handleVolumeControlKeyUp(event) { 16395 if (keycode.isEventKey(event, 'Esc')) { 16396 this.muteToggle.focus(); 16397 } 16398 } 16399 16400 /** 16401 * This gets called when a `VolumePanel` gains hover via a `mouseover` event. 16402 * Turns on listening for `mouseover` event. When they happen it 16403 * calls `this.handleMouseOver`. 16404 * 16405 * @param {Event} event 16406 * The `mouseover` event that caused this function to be called. 16407 * 16408 * @listens mouseover 16409 */ 16410 handleMouseOver(event) { 16411 this.addClass('vjs-hover'); 16412 on(document, 'keyup', this.handleKeyPressHandler_); 16413 } 16414 16415 /** 16416 * This gets called when a `VolumePanel` gains hover via a `mouseout` event. 16417 * Turns on listening for `mouseout` event. When they happen it 16418 * calls `this.handleMouseOut`. 16419 * 16420 * @param {Event} event 16421 * The `mouseout` event that caused this function to be called. 16422 * 16423 * @listens mouseout 16424 */ 16425 handleMouseOut(event) { 16426 this.removeClass('vjs-hover'); 16427 off(document, 'keyup', this.handleKeyPressHandler_); 16428 } 16429 16430 /** 16431 * Handles `keyup` event on the document or `keydown` event on the `VolumePanel`, 16432 * looking for ESC, which hides the `VolumeControl`. 16433 * 16434 * @param {Event} event 16435 * The keypress that triggered this event. 16436 * 16437 * @listens keydown | keyup 16438 */ 16439 handleKeyPress(event) { 16440 if (keycode.isEventKey(event, 'Esc')) { 16441 this.handleMouseOut(); 16442 } 16443 } 16444 } 16445 16446 /** 16447 * Default options for the `VolumeControl` 16448 * 16449 * @type {Object} 16450 * @private 16451 */ 16452 VolumePanel.prototype.options_ = { 16453 children: ['muteToggle', 'volumeControl'] 16454 }; 16455 Component$1.registerComponent('VolumePanel', VolumePanel); 16456 16457 /** 16458 * Button to skip forward a configurable amount of time 16459 * through a video. Renders in the control bar. 16460 * 16461 * e.g. options: {controlBar: {skipButtons: forward: 5}} 16462 * 16463 * @extends Button 16464 */ 16465 class SkipForward extends Button { 16466 constructor(player, options) { 16467 super(player, options); 16468 this.validOptions = [5, 10, 30]; 16469 this.skipTime = this.getSkipForwardTime(); 16470 if (this.skipTime && this.validOptions.includes(this.skipTime)) { 16471 this.setIcon(`forward-${this.skipTime}`); 16472 this.controlText(this.localize('Skip forward {1} seconds', [this.skipTime])); 16473 this.show(); 16474 } else { 16475 this.hide(); 16476 } 16477 } 16478 getSkipForwardTime() { 16479 const playerOptions = this.options_.playerOptions; 16480 return playerOptions.controlBar && playerOptions.controlBar.skipButtons && playerOptions.controlBar.skipButtons.forward; 16481 } 16482 buildCSSClass() { 16483 return `vjs-skip-forward-${this.getSkipForwardTime()} ${super.buildCSSClass()}`; 16484 } 16485 16486 /** 16487 * On click, skips forward in the duration/seekable range by a configurable amount of seconds. 16488 * If the time left in the duration/seekable range is less than the configured 'skip forward' time, 16489 * skips to end of duration/seekable range. 16490 * 16491 * Handle a click on a `SkipForward` button 16492 * 16493 * @param {EventTarget~Event} event 16494 * The `click` event that caused this function 16495 * to be called 16496 */ 16497 handleClick(event) { 16498 if (isNaN(this.player_.duration())) { 16499 return; 16500 } 16501 const currentVideoTime = this.player_.currentTime(); 16502 const liveTracker = this.player_.liveTracker; 16503 const duration = liveTracker && liveTracker.isLive() ? liveTracker.seekableEnd() : this.player_.duration(); 16504 let newTime; 16505 if (currentVideoTime + this.skipTime <= duration) { 16506 newTime = currentVideoTime + this.skipTime; 16507 } else { 16508 newTime = duration; 16509 } 16510 this.player_.currentTime(newTime); 16511 } 16512 16513 /** 16514 * Update control text on languagechange 16515 */ 16516 handleLanguagechange() { 16517 this.controlText(this.localize('Skip forward {1} seconds', [this.skipTime])); 16518 } 16519 } 16520 SkipForward.prototype.controlText_ = 'Skip Forward'; 16521 Component$1.registerComponent('SkipForward', SkipForward); 16522 16523 /** 16524 * Button to skip backward a configurable amount of time 16525 * through a video. Renders in the control bar. 16526 * 16527 * * e.g. options: {controlBar: {skipButtons: backward: 5}} 16528 * 16529 * @extends Button 16530 */ 16531 class SkipBackward extends Button { 16532 constructor(player, options) { 16533 super(player, options); 16534 this.validOptions = [5, 10, 30]; 16535 this.skipTime = this.getSkipBackwardTime(); 16536 if (this.skipTime && this.validOptions.includes(this.skipTime)) { 16537 this.setIcon(`replay-${this.skipTime}`); 16538 this.controlText(this.localize('Skip backward {1} seconds', [this.skipTime])); 16539 this.show(); 16540 } else { 16541 this.hide(); 16542 } 16543 } 16544 getSkipBackwardTime() { 16545 const playerOptions = this.options_.playerOptions; 16546 return playerOptions.controlBar && playerOptions.controlBar.skipButtons && playerOptions.controlBar.skipButtons.backward; 16547 } 16548 buildCSSClass() { 16549 return `vjs-skip-backward-${this.getSkipBackwardTime()} ${super.buildCSSClass()}`; 16550 } 16551 16552 /** 16553 * On click, skips backward in the video by a configurable amount of seconds. 16554 * If the current time in the video is less than the configured 'skip backward' time, 16555 * skips to beginning of video or seekable range. 16556 * 16557 * Handle a click on a `SkipBackward` button 16558 * 16559 * @param {EventTarget~Event} event 16560 * The `click` event that caused this function 16561 * to be called 16562 */ 16563 handleClick(event) { 16564 const currentVideoTime = this.player_.currentTime(); 16565 const liveTracker = this.player_.liveTracker; 16566 const seekableStart = liveTracker && liveTracker.isLive() && liveTracker.seekableStart(); 16567 let newTime; 16568 if (seekableStart && currentVideoTime - this.skipTime <= seekableStart) { 16569 newTime = seekableStart; 16570 } else if (currentVideoTime >= this.skipTime) { 16571 newTime = currentVideoTime - this.skipTime; 16572 } else { 16573 newTime = 0; 16574 } 16575 this.player_.currentTime(newTime); 16576 } 16577 16578 /** 16579 * Update control text on languagechange 16580 */ 16581 handleLanguagechange() { 16582 this.controlText(this.localize('Skip backward {1} seconds', [this.skipTime])); 16583 } 16584 } 16585 SkipBackward.prototype.controlText_ = 'Skip Backward'; 16586 Component$1.registerComponent('SkipBackward', SkipBackward); 16587 16588 /** 16589 * @file menu.js 16590 */ 16591 16592 /** 16593 * The Menu component is used to build popup menus, including subtitle and 16594 * captions selection menus. 16595 * 16596 * @extends Component 16597 */ 16598 class Menu extends Component$1 { 16599 /** 16600 * Create an instance of this class. 16601 * 16602 * @param { import('../player').default } player 16603 * the player that this component should attach to 16604 * 16605 * @param {Object} [options] 16606 * Object of option names and values 16607 * 16608 */ 16609 constructor(player, options) { 16610 super(player, options); 16611 if (options) { 16612 this.menuButton_ = options.menuButton; 16613 } 16614 this.focusedChild_ = -1; 16615 this.on('keydown', e => this.handleKeyDown(e)); 16616 16617 // All the menu item instances share the same blur handler provided by the menu container. 16618 this.boundHandleBlur_ = e => this.handleBlur(e); 16619 this.boundHandleTapClick_ = e => this.handleTapClick(e); 16620 } 16621 16622 /** 16623 * Add event listeners to the {@link MenuItem}. 16624 * 16625 * @param {Object} component 16626 * The instance of the `MenuItem` to add listeners to. 16627 * 16628 */ 16629 addEventListenerForItem(component) { 16630 if (!(component instanceof Component$1)) { 16631 return; 16632 } 16633 this.on(component, 'blur', this.boundHandleBlur_); 16634 this.on(component, ['tap', 'click'], this.boundHandleTapClick_); 16635 } 16636 16637 /** 16638 * Remove event listeners from the {@link MenuItem}. 16639 * 16640 * @param {Object} component 16641 * The instance of the `MenuItem` to remove listeners. 16642 * 16643 */ 16644 removeEventListenerForItem(component) { 16645 if (!(component instanceof Component$1)) { 16646 return; 16647 } 16648 this.off(component, 'blur', this.boundHandleBlur_); 16649 this.off(component, ['tap', 'click'], this.boundHandleTapClick_); 16650 } 16651 16652 /** 16653 * This method will be called indirectly when the component has been added 16654 * before the component adds to the new menu instance by `addItem`. 16655 * In this case, the original menu instance will remove the component 16656 * by calling `removeChild`. 16657 * 16658 * @param {Object} component 16659 * The instance of the `MenuItem` 16660 */ 16661 removeChild(component) { 16662 if (typeof component === 'string') { 16663 component = this.getChild(component); 16664 } 16665 this.removeEventListenerForItem(component); 16666 super.removeChild(component); 16667 } 16668 16669 /** 16670 * Add a {@link MenuItem} to the menu. 16671 * 16672 * @param {Object|string} component 16673 * The name or instance of the `MenuItem` to add. 16674 * 16675 */ 16676 addItem(component) { 16677 const childComponent = this.addChild(component); 16678 if (childComponent) { 16679 this.addEventListenerForItem(childComponent); 16680 } 16681 } 16682 16683 /** 16684 * Create the `Menu`s DOM element. 16685 * 16686 * @return {Element} 16687 * the element that was created 16688 */ 16689 createEl() { 16690 const contentElType = this.options_.contentElType || 'ul'; 16691 this.contentEl_ = createEl(contentElType, { 16692 className: 'vjs-menu-content' 16693 }); 16694 this.contentEl_.setAttribute('role', 'menu'); 16695 const el = super.createEl('div', { 16696 append: this.contentEl_, 16697 className: 'vjs-menu' 16698 }); 16699 el.appendChild(this.contentEl_); 16700 16701 // Prevent clicks from bubbling up. Needed for Menu Buttons, 16702 // where a click on the parent is significant 16703 on(el, 'click', function (event) { 16704 event.preventDefault(); 16705 event.stopImmediatePropagation(); 16706 }); 16707 return el; 16708 } 16709 dispose() { 16710 this.contentEl_ = null; 16711 this.boundHandleBlur_ = null; 16712 this.boundHandleTapClick_ = null; 16713 super.dispose(); 16714 } 16715 16716 /** 16717 * Called when a `MenuItem` loses focus. 16718 * 16719 * @param {Event} event 16720 * The `blur` event that caused this function to be called. 16721 * 16722 * @listens blur 16723 */ 16724 handleBlur(event) { 16725 const relatedTarget = event.relatedTarget || document.activeElement; 16726 16727 // Close menu popup when a user clicks outside the menu 16728 if (!this.children().some(element => { 16729 return element.el() === relatedTarget; 16730 })) { 16731 const btn = this.menuButton_; 16732 if (btn && btn.buttonPressed_ && relatedTarget !== btn.el().firstChild) { 16733 btn.unpressButton(); 16734 } 16735 } 16736 } 16737 16738 /** 16739 * Called when a `MenuItem` gets clicked or tapped. 16740 * 16741 * @param {Event} event 16742 * The `click` or `tap` event that caused this function to be called. 16743 * 16744 * @listens click,tap 16745 */ 16746 handleTapClick(event) { 16747 // Unpress the associated MenuButton, and move focus back to it 16748 if (this.menuButton_) { 16749 this.menuButton_.unpressButton(); 16750 const childComponents = this.children(); 16751 if (!Array.isArray(childComponents)) { 16752 return; 16753 } 16754 const foundComponent = childComponents.filter(component => component.el() === event.target)[0]; 16755 if (!foundComponent) { 16756 return; 16757 } 16758 16759 // don't focus menu button if item is a caption settings item 16760 // because focus will move elsewhere 16761 if (foundComponent.name() !== 'CaptionSettingsMenuItem') { 16762 this.menuButton_.focus(); 16763 } 16764 } 16765 } 16766 16767 /** 16768 * Handle a `keydown` event on this menu. This listener is added in the constructor. 16769 * 16770 * @param {KeyboardEvent} event 16771 * A `keydown` event that happened on the menu. 16772 * 16773 * @listens keydown 16774 */ 16775 handleKeyDown(event) { 16776 // Left and Down Arrows 16777 if (keycode.isEventKey(event, 'Left') || keycode.isEventKey(event, 'Down')) { 16778 event.preventDefault(); 16779 event.stopPropagation(); 16780 this.stepForward(); 16781 16782 // Up and Right Arrows 16783 } else if (keycode.isEventKey(event, 'Right') || keycode.isEventKey(event, 'Up')) { 16784 event.preventDefault(); 16785 event.stopPropagation(); 16786 this.stepBack(); 16787 } 16788 } 16789 16790 /** 16791 * Move to next (lower) menu item for keyboard users. 16792 */ 16793 stepForward() { 16794 let stepChild = 0; 16795 if (this.focusedChild_ !== undefined) { 16796 stepChild = this.focusedChild_ + 1; 16797 } 16798 this.focus(stepChild); 16799 } 16800 16801 /** 16802 * Move to previous (higher) menu item for keyboard users. 16803 */ 16804 stepBack() { 16805 let stepChild = 0; 16806 if (this.focusedChild_ !== undefined) { 16807 stepChild = this.focusedChild_ - 1; 16808 } 16809 this.focus(stepChild); 16810 } 16811 16812 /** 16813 * Set focus on a {@link MenuItem} in the `Menu`. 16814 * 16815 * @param {Object|string} [item=0] 16816 * Index of child item set focus on. 16817 */ 16818 focus(item = 0) { 16819 const children = this.children().slice(); 16820 const haveTitle = children.length && children[0].hasClass('vjs-menu-title'); 16821 if (haveTitle) { 16822 children.shift(); 16823 } 16824 if (children.length > 0) { 16825 if (item < 0) { 16826 item = 0; 16827 } else if (item >= children.length) { 16828 item = children.length - 1; 16829 } 16830 this.focusedChild_ = item; 16831 children[item].el_.focus(); 16832 } 16833 } 16834 } 16835 Component$1.registerComponent('Menu', Menu); 16836 16837 /** 16838 * @file menu-button.js 16839 */ 16840 16841 /** 16842 * A `MenuButton` class for any popup {@link Menu}. 16843 * 16844 * @extends Component 16845 */ 16846 class MenuButton extends Component$1 { 16847 /** 16848 * Creates an instance of this class. 16849 * 16850 * @param { import('../player').default } player 16851 * The `Player` that this class should be attached to. 16852 * 16853 * @param {Object} [options={}] 16854 * The key/value store of player options. 16855 */ 16856 constructor(player, options = {}) { 16857 super(player, options); 16858 this.menuButton_ = new Button(player, options); 16859 this.menuButton_.controlText(this.controlText_); 16860 this.menuButton_.el_.setAttribute('aria-haspopup', 'true'); 16861 16862 // Add buildCSSClass values to the button, not the wrapper 16863 const buttonClass = Button.prototype.buildCSSClass(); 16864 this.menuButton_.el_.className = this.buildCSSClass() + ' ' + buttonClass; 16865 this.menuButton_.removeClass('vjs-control'); 16866 this.addChild(this.menuButton_); 16867 this.update(); 16868 this.enabled_ = true; 16869 const handleClick = e => this.handleClick(e); 16870 this.handleMenuKeyUp_ = e => this.handleMenuKeyUp(e); 16871 this.on(this.menuButton_, 'tap', handleClick); 16872 this.on(this.menuButton_, 'click', handleClick); 16873 this.on(this.menuButton_, 'keydown', e => this.handleKeyDown(e)); 16874 this.on(this.menuButton_, 'mouseenter', () => { 16875 this.addClass('vjs-hover'); 16876 this.menu.show(); 16877 on(document, 'keyup', this.handleMenuKeyUp_); 16878 }); 16879 this.on('mouseleave', e => this.handleMouseLeave(e)); 16880 this.on('keydown', e => this.handleSubmenuKeyDown(e)); 16881 } 16882 16883 /** 16884 * Update the menu based on the current state of its items. 16885 */ 16886 update() { 16887 const menu = this.createMenu(); 16888 if (this.menu) { 16889 this.menu.dispose(); 16890 this.removeChild(this.menu); 16891 } 16892 this.menu = menu; 16893 this.addChild(menu); 16894 16895 /** 16896 * Track the state of the menu button 16897 * 16898 * @type {Boolean} 16899 * @private 16900 */ 16901 this.buttonPressed_ = false; 16902 this.menuButton_.el_.setAttribute('aria-expanded', 'false'); 16903 if (this.items && this.items.length <= this.hideThreshold_) { 16904 this.hide(); 16905 this.menu.contentEl_.removeAttribute('role'); 16906 } else { 16907 this.show(); 16908 this.menu.contentEl_.setAttribute('role', 'menu'); 16909 } 16910 } 16911 16912 /** 16913 * Create the menu and add all items to it. 16914 * 16915 * @return {Menu} 16916 * The constructed menu 16917 */ 16918 createMenu() { 16919 const menu = new Menu(this.player_, { 16920 menuButton: this 16921 }); 16922 16923 /** 16924 * Hide the menu if the number of items is less than or equal to this threshold. This defaults 16925 * to 0 and whenever we add items which can be hidden to the menu we'll increment it. We list 16926 * it here because every time we run `createMenu` we need to reset the value. 16927 * 16928 * @protected 16929 * @type {Number} 16930 */ 16931 this.hideThreshold_ = 0; 16932 16933 // Add a title list item to the top 16934 if (this.options_.title) { 16935 const titleEl = createEl('li', { 16936 className: 'vjs-menu-title', 16937 textContent: toTitleCase$1(this.options_.title), 16938 tabIndex: -1 16939 }); 16940 const titleComponent = new Component$1(this.player_, { 16941 el: titleEl 16942 }); 16943 menu.addItem(titleComponent); 16944 } 16945 this.items = this.createItems(); 16946 if (this.items) { 16947 // Add menu items to the menu 16948 for (let i = 0; i < this.items.length; i++) { 16949 menu.addItem(this.items[i]); 16950 } 16951 } 16952 return menu; 16953 } 16954 16955 /** 16956 * Create the list of menu items. Specific to each subclass. 16957 * 16958 * @abstract 16959 */ 16960 createItems() {} 16961 16962 /** 16963 * Create the `MenuButtons`s DOM element. 16964 * 16965 * @return {Element} 16966 * The element that gets created. 16967 */ 16968 createEl() { 16969 return super.createEl('div', { 16970 className: this.buildWrapperCSSClass() 16971 }, {}); 16972 } 16973 16974 /** 16975 * Overwrites the `setIcon` method from `Component`. 16976 * In this case, we want the icon to be appended to the menuButton. 16977 * 16978 * @param {string} name 16979 * The icon name to be added. 16980 */ 16981 setIcon(name) { 16982 super.setIcon(name, this.menuButton_.el_); 16983 } 16984 16985 /** 16986 * Allow sub components to stack CSS class names for the wrapper element 16987 * 16988 * @return {string} 16989 * The constructed wrapper DOM `className` 16990 */ 16991 buildWrapperCSSClass() { 16992 let menuButtonClass = 'vjs-menu-button'; 16993 16994 // If the inline option is passed, we want to use different styles altogether. 16995 if (this.options_.inline === true) { 16996 menuButtonClass += '-inline'; 16997 } else { 16998 menuButtonClass += '-popup'; 16999 } 17000 17001 // TODO: Fix the CSS so that this isn't necessary 17002 const buttonClass = Button.prototype.buildCSSClass(); 17003 return `vjs-menu-button ${menuButtonClass} ${buttonClass} ${super.buildCSSClass()}`; 17004 } 17005 17006 /** 17007 * Builds the default DOM `className`. 17008 * 17009 * @return {string} 17010 * The DOM `className` for this object. 17011 */ 17012 buildCSSClass() { 17013 let menuButtonClass = 'vjs-menu-button'; 17014 17015 // If the inline option is passed, we want to use different styles altogether. 17016 if (this.options_.inline === true) { 17017 menuButtonClass += '-inline'; 17018 } else { 17019 menuButtonClass += '-popup'; 17020 } 17021 return `vjs-menu-button ${menuButtonClass} ${super.buildCSSClass()}`; 17022 } 17023 17024 /** 17025 * Get or set the localized control text that will be used for accessibility. 17026 * 17027 * > NOTE: This will come from the internal `menuButton_` element. 17028 * 17029 * @param {string} [text] 17030 * Control text for element. 17031 * 17032 * @param {Element} [el=this.menuButton_.el()] 17033 * Element to set the title on. 17034 * 17035 * @return {string} 17036 * - The control text when getting 17037 */ 17038 controlText(text, el = this.menuButton_.el()) { 17039 return this.menuButton_.controlText(text, el); 17040 } 17041 17042 /**
17043 * Dispose of the `menu-button` and all child components. 17044 */ 17045 dispose() { 17046 this.handleMouseLeave(); 17047 super.dispose(); 17048 } 17049 17050 /** 17051 * Handle a click on a `MenuButton`. 17052 * See {@link ClickableComponent#handleClick} for instances where this is called. 17053 * 17054 * @param {Event} event 17055 * The `keydown`, `tap`, or `click` event that caused this function to be 17056 * called. 17057 * 17058 * @listens tap 17059 * @listens click 17060 */ 17061 handleClick(event) { 17062 if (this.buttonPressed_) { 17063 this.unpressButton(); 17064 } else { 17065 this.pressButton(); 17066 } 17067 } 17068 17069 /** 17070 * Handle `mouseleave` for `MenuButton`. 17071 * 17072 * @param {Event} event 17073 * The `mouseleave` event that caused this function to be called. 17074 * 17075 * @listens mouseleave 17076 */ 17077 handleMouseLeave(event) { 17078 this.removeClass('vjs-hover'); 17079 off(document, 'keyup', this.handleMenuKeyUp_); 17080 } 17081 17082 /** 17083 * Set the focus to the actual button, not to this element 17084 */ 17085 focus() { 17086 this.menuButton_.focus(); 17087 } 17088 17089 /** 17090 * Remove the focus from the actual button, not this element 17091 */ 17092 blur() { 17093 this.menuButton_.blur(); 17094 } 17095 17096 /** 17097 * Handle tab, escape, down arrow, and up arrow keys for `MenuButton`. See 17098 * {@link ClickableComponent#handleKeyDown} for instances where this is called. 17099 * 17100 * @param {Event} event 17101 * The `keydown` event that caused this function to be called. 17102 * 17103 * @listens keydown 17104 */ 17105 handleKeyDown(event) { 17106 // Escape or Tab unpress the 'button' 17107 if (keycode.isEventKey(event, 'Esc') || keycode.isEventKey(event, 'Tab')) { 17108 if (this.buttonPressed_) { 17109 this.unpressButton(); 17110 } 17111 17112 // Don't preventDefault for Tab key - we still want to lose focus 17113 if (!keycode.isEventKey(event, 'Tab')) { 17114 event.preventDefault(); 17115 // Set focus back to the menu button's button 17116 this.menuButton_.focus(); 17117 } 17118 // Up Arrow or Down Arrow also 'press' the button to open the menu 17119 } else if (keycode.isEventKey(event, 'Up') || keycode.isEventKey(event, 'Down')) { 17120 if (!this.buttonPressed_) { 17121 event.preventDefault(); 17122 this.pressButton(); 17123 } 17124 } 17125 } 17126 17127 /** 17128 * Handle a `keyup` event on a `MenuButton`. The listener for this is added in 17129 * the constructor. 17130 * 17131 * @param {Event} event 17132 * Key press event 17133 * 17134 * @listens keyup 17135 */ 17136 handleMenuKeyUp(event) { 17137 // Escape hides popup menu 17138 if (keycode.isEventKey(event, 'Esc') || keycode.isEventKey(event, 'Tab')) { 17139 this.removeClass('vjs-hover'); 17140 } 17141 } 17142 17143 /** 17144 * This method name now delegates to `handleSubmenuKeyDown`. This means 17145 * anyone calling `handleSubmenuKeyPress` will not see their method calls 17146 * stop working. 17147 * 17148 * @param {Event} event 17149 * The event that caused this function to be called. 17150 */ 17151 handleSubmenuKeyPress(event) { 17152 this.handleSubmenuKeyDown(event); 17153 } 17154 17155 /** 17156 * Handle a `keydown` event on a sub-menu. The listener for this is added in 17157 * the constructor. 17158 * 17159 * @param {Event} event 17160 * Key press event 17161 * 17162 * @listens keydown 17163 */ 17164 handleSubmenuKeyDown(event) { 17165 // Escape or Tab unpress the 'button' 17166 if (keycode.isEventKey(event, 'Esc') || keycode.isEventKey(event, 'Tab')) { 17167 if (this.buttonPressed_) { 17168 this.unpressButton(); 17169 } 17170 // Don't preventDefault for Tab key - we still want to lose focus 17171 if (!keycode.isEventKey(event, 'Tab')) { 17172 event.preventDefault(); 17173 // Set focus back to the menu button's button 17174 this.menuButton_.focus(); 17175 } 17176 } 17177 } 17178 17179 /** 17180 * Put the current `MenuButton` into a pressed state. 17181 */ 17182 pressButton() { 17183 if (this.enabled_) { 17184 this.buttonPressed_ = true; 17185 this.menu.show(); 17186 this.menu.lockShowing(); 17187 this.menuButton_.el_.setAttribute('aria-expanded', 'true'); 17188 17189 // set the focus into the submenu, except on iOS where it is resulting in 17190 // undesired scrolling behavior when the player is in an iframe 17191 if (IS_IOS && isInFrame()) { 17192 // Return early so that the menu isn't focused 17193 return; 17194 } 17195 this.menu.focus(); 17196 } 17197 } 17198 17199 /** 17200 * Take the current `MenuButton` out of a pressed state. 17201 */ 17202 unpressButton() { 17203 if (this.enabled_) { 17204 this.buttonPressed_ = false;
vendor: 4,795 bytes, lines 17205-17367
17205 this.menu.unlockShowing(); 17206 this.menu.hide(); 17207 this.menuButton_.el_.setAttribute('aria-expanded', 'false'); 17208 } 17209 } 17210 17211 /** 17212 * Disable the `MenuButton`. Don't allow it to be clicked. 17213 */ 17214 disable() { 17215 this.unpressButton(); 17216 this.enabled_ = false; 17217 this.addClass('vjs-disabled'); 17218 this.menuButton_.disable(); 17219 } 17220 17221 /** 17222 * Enable the `MenuButton`. Allow it to be clicked. 17223 */ 17224 enable() { 17225 this.enabled_ = true; 17226 this.removeClass('vjs-disabled'); 17227 this.menuButton_.enable(); 17228 } 17229 } 17230 Component$1.registerComponent('MenuButton', MenuButton); 17231 17232 /** 17233 * @file track-button.js 17234 */ 17235 17236 /** 17237 * The base class for buttons that toggle specific track types (e.g. subtitles). 17238 * 17239 * @extends MenuButton 17240 */ 17241 class TrackButton extends MenuButton { 17242 /** 17243 * Creates an instance of this class. 17244 * 17245 * @param { import('./player').default } player 17246 * The `Player` that this class should be attached to. 17247 * 17248 * @param {Object} [options] 17249 * The key/value store of player options. 17250 */ 17251 constructor(player, options) { 17252 const tracks = options.tracks; 17253 super(player, options); 17254 if (this.items.length <= 1) { 17255 this.hide(); 17256 } 17257 if (!tracks) { 17258 return; 17259 } 17260 const updateHandler = bind_(this, this.update); 17261 tracks.addEventListener('removetrack', updateHandler); 17262 tracks.addEventListener('addtrack', updateHandler); 17263 tracks.addEventListener('labelchange', updateHandler); 17264 this.player_.on('ready', updateHandler); 17265 this.player_.on('dispose', function () { 17266 tracks.removeEventListener('removetrack', updateHandler); 17267 tracks.removeEventListener('addtrack', updateHandler); 17268 tracks.removeEventListener('labelchange', updateHandler); 17269 }); 17270 } 17271 } 17272 Component$1.registerComponent('TrackButton', TrackButton); 17273 17274 /** 17275 * @file menu-keys.js 17276 */ 17277 17278 /** 17279 * All keys used for operation of a menu (`MenuButton`, `Menu`, and `MenuItem`) 17280 * Note that 'Enter' and 'Space' are not included here (otherwise they would 17281 * prevent the `MenuButton` and `MenuItem` from being keyboard-clickable) 17282 * 17283 * @typedef MenuKeys 17284 * @array 17285 */ 17286 const MenuKeys = ['Tab', 'Esc', 'Up', 'Down', 'Right', 'Left']; 17287 17288 /** 17289 * @file menu-item.js 17290 */ 17291 17292 /** 17293 * The component for a menu item. `<li>` 17294 * 17295 * @extends ClickableComponent 17296 */ 17297 class MenuItem extends ClickableComponent { 17298 /** 17299 * Creates an instance of the this class. 17300 * 17301 * @param { import('../player').default } player 17302 * The `Player` that this class should be attached to. 17303 * 17304 * @param {Object} [options={}] 17305 * The key/value store of player options. 17306 * 17307 */ 17308 constructor(player, options) { 17309 super(player, options); 17310 this.selectable = options.selectable; 17311 this.isSelected_ = options.selected || false; 17312 this.multiSelectable = options.multiSelectable; 17313 this.selected(this.isSelected_); 17314 if (this.selectable) { 17315 if (this.multiSelectable) { 17316 this.el_.setAttribute('role', 'menuitemcheckbox'); 17317 } else { 17318 this.el_.setAttribute('role', 'menuitemradio'); 17319 } 17320 } else { 17321 this.el_.setAttribute('role', 'menuitem'); 17322 } 17323 } 17324 17325 /** 17326 * Create the `MenuItem's DOM element 17327 * 17328 * @param {string} [type=li] 17329 * Element's node type, not actually used, always set to `li`. 17330 * 17331 * @param {Object} [props={}] 17332 * An object of properties that should be set on the element 17333 * 17334 * @param {Object} [attrs={}] 17335 * An object of attributes that should be set on the element 17336 * 17337 * @return {Element} 17338 * The element that gets created. 17339 */ 17340 createEl(type, props, attrs) { 17341 // The control is textual, not just an icon 17342 this.nonIconControl = true; 17343 const el = super.createEl('li', Object.assign({ 17344 className: 'vjs-menu-item', 17345 tabIndex: -1 17346 }, props), attrs); 17347 17348 // swap icon with menu item text. 17349 const menuItemEl = createEl('span', { 17350 className: 'vjs-menu-item-text', 17351 textContent: this.localize(this.options_.label) 17352 }); 17353 17354 // If using SVG icons, the element with vjs-icon-placeholder will be added separately. 17355 if (this.player_.options_.experimentalSvgIcons) { 17356 el.appendChild(menuItemEl); 17357 } else { 17358 el.replaceChild(menuItemEl, el.querySelector('.vjs-icon-placeholder')); 17359 } 17360 return el; 17361 } 17362 17363 /** 17364 * Ignore keys which are used by the menu, but pass any other ones up. See 17365 * {@link ClickableComponent#handleKeyDown} for instances where this is called. 17366 * 17367 * @param {KeyboardEvent} event
vendor: 24,562 bytes, lines 17368-18177
17368 * The `keydown` event that caused this function to be called. 17369 * 17370 * @listens keydown 17371 */ 17372 handleKeyDown(event) { 17373 if (!MenuKeys.some(key => keycode.isEventKey(event, key))) { 17374 // Pass keydown handling up for unused keys 17375 super.handleKeyDown(event); 17376 } 17377 } 17378 17379 /** 17380 * Any click on a `MenuItem` puts it into the selected state. 17381 * See {@link ClickableComponent#handleClick} for instances where this is called. 17382 * 17383 * @param {Event} event 17384 * The `keydown`, `tap`, or `click` event that caused this function to be 17385 * called. 17386 * 17387 * @listens tap 17388 * @listens click 17389 */ 17390 handleClick(event) { 17391 this.selected(true); 17392 } 17393 17394 /** 17395 * Set the state for this menu item as selected or not. 17396 * 17397 * @param {boolean} selected 17398 * if the menu item is selected or not 17399 */ 17400 selected(selected) { 17401 if (this.selectable) { 17402 if (selected) { 17403 this.addClass('vjs-selected'); 17404 this.el_.setAttribute('aria-checked', 'true'); 17405 // aria-checked isn't fully supported by browsers/screen readers, 17406 // so indicate selected state to screen reader in the control text. 17407 this.controlText(', selected'); 17408 this.isSelected_ = true; 17409 } else { 17410 this.removeClass('vjs-selected'); 17411 this.el_.setAttribute('aria-checked', 'false'); 17412 // Indicate un-selected state to screen reader 17413 this.controlText(''); 17414 this.isSelected_ = false; 17415 } 17416 } 17417 } 17418 } 17419 Component$1.registerComponent('MenuItem', MenuItem); 17420 17421 /** 17422 * @file text-track-menu-item.js 17423 */ 17424 17425 /** 17426 * The specific menu item type for selecting a language within a text track kind 17427 * 17428 * @extends MenuItem 17429 */ 17430 class TextTrackMenuItem extends MenuItem { 17431 /** 17432 * Creates an instance of this class. 17433 * 17434 * @param { import('../../player').default } player 17435 * The `Player` that this class should be attached to. 17436 * 17437 * @param {Object} [options] 17438 * The key/value store of player options. 17439 */ 17440 constructor(player, options) { 17441 const track = options.track; 17442 const tracks = player.textTracks(); 17443 17444 // Modify options for parent MenuItem class's init. 17445 options.label = track.label || track.language || 'Unknown'; 17446 options.selected = track.mode === 'showing'; 17447 super(player, options); 17448 this.track = track; 17449 // Determine the relevant kind(s) of tracks for this component and filter 17450 // out empty kinds. 17451 this.kinds = (options.kinds || [options.kind || this.track.kind]).filter(Boolean); 17452 const changeHandler = (...args) => { 17453 this.handleTracksChange.apply(this, args); 17454 }; 17455 const selectedLanguageChangeHandler = (...args) => { 17456 this.handleSelectedLanguageChange.apply(this, args); 17457 }; 17458 player.on(['loadstart', 'texttrackchange'], changeHandler); 17459 tracks.addEventListener('change', changeHandler); 17460 tracks.addEventListener('selectedlanguagechange', selectedLanguageChangeHandler); 17461 this.on('dispose', function () { 17462 player.off(['loadstart', 'texttrackchange'], changeHandler); 17463 tracks.removeEventListener('change', changeHandler); 17464 tracks.removeEventListener('selectedlanguagechange', selectedLanguageChangeHandler); 17465 }); 17466 17467 // iOS7 doesn't dispatch change events to TextTrackLists when an 17468 // associated track's mode changes. Without something like 17469 // Object.observe() (also not present on iOS7), it's not 17470 // possible to detect changes to the mode attribute and polyfill 17471 // the change event. As a poor substitute, we manually dispatch 17472 // change events whenever the controls modify the mode. 17473 if (tracks.onchange === undefined) { 17474 let event; 17475 this.on(['tap', 'click'], function () { 17476 if (typeof window.Event !== 'object') { 17477 // Android 2.3 throws an Illegal Constructor error for window.Event 17478 try { 17479 event = new window.Event('change'); 17480 } catch (err) { 17481 // continue regardless of error 17482 } 17483 } 17484 if (!event) { 17485 event = document.createEvent('Event'); 17486 event.initEvent('change', true, true); 17487 } 17488 tracks.dispatchEvent(event); 17489 }); 17490 } 17491 17492 // set the default state based on current tracks 17493 this.handleTracksChange(); 17494 } 17495 17496 /** 17497 * This gets called when an `TextTrackMenuItem` is "clicked". See 17498 * {@link ClickableComponent} for more detailed information on what a click can be. 17499 * 17500 * @param {Event} event 17501 * The `keydown`, `tap`, or `click` event that caused this function to be 17502 * called. 17503 * 17504 * @listens tap 17505 * @listens click 17506 */ 17507 handleClick(event) { 17508 const referenceTrack = this.track; 17509 const tracks = this.player_.textTracks(); 17510 super.handleClick(event); 17511 if (!tracks) { 17512 return; 17513 } 17514 for (let i = 0; i < tracks.length; i++) { 17515 const track = tracks[i]; 17516 17517 // If the track from the text tracks list is not of the right kind, 17518 // skip it. We do not want to affect tracks of incompatible kind(s). 17519 if (this.kinds.indexOf(track.kind) === -1) { 17520 continue; 17521 } 17522 17523 // If this text track is the component's track and it is not showing, 17524 // set it to showing. 17525 if (track === referenceTrack) { 17526 if (track.mode !== 'showing') { 17527 track.mode = 'showing'; 17528 } 17529 17530 // If this text track is not the component's track and it is not 17531 // disabled, set it to disabled. 17532 } else if (track.mode !== 'disabled') { 17533 track.mode = 'disabled'; 17534 } 17535 } 17536 } 17537 17538 /** 17539 * Handle text track list change 17540 * 17541 * @param {Event} event 17542 * The `change` event that caused this function to be called. 17543 * 17544 * @listens TextTrackList#change 17545 */ 17546 handleTracksChange(event) { 17547 const shouldBeSelected = this.track.mode === 'showing'; 17548 17549 // Prevent redundant selected() calls because they may cause 17550 // screen readers to read the appended control text unnecessarily 17551 if (shouldBeSelected !== this.isSelected_) { 17552 this.selected(shouldBeSelected); 17553 } 17554 } 17555 handleSelectedLanguageChange(event) { 17556 if (this.track.mode === 'showing') { 17557 const selectedLanguage = this.player_.cache_.selectedLanguage; 17558 17559 // Don't replace the kind of track across the same language 17560 if (selectedLanguage && selectedLanguage.enabled && selectedLanguage.language === this.track.language && selectedLanguage.kind !== this.track.kind) { 17561 return; 17562 } 17563 this.player_.cache_.selectedLanguage = { 17564 enabled: true, 17565 language: this.track.language, 17566 kind: this.track.kind 17567 }; 17568 } 17569 } 17570 dispose() { 17571 // remove reference to track object on dispose 17572 this.track = null; 17573 super.dispose(); 17574 } 17575 } 17576 Component$1.registerComponent('TextTrackMenuItem', TextTrackMenuItem); 17577 17578 /** 17579 * @file off-text-track-menu-item.js 17580 */ 17581 17582 /** 17583 * A special menu item for turning off a specific type of text track 17584 * 17585 * @extends TextTrackMenuItem 17586 */ 17587 class OffTextTrackMenuItem extends TextTrackMenuItem { 17588 /** 17589 * Creates an instance of this class. 17590 * 17591 * @param { import('../../player').default } player 17592 * The `Player` that this class should be attached to. 17593 * 17594 * @param {Object} [options] 17595 * The key/value store of player options. 17596 */ 17597 constructor(player, options) { 17598 // Create pseudo track info 17599 // Requires options['kind'] 17600 options.track = { 17601 player, 17602 // it is no longer necessary to store `kind` or `kinds` on the track itself 17603 // since they are now stored in the `kinds` property of all instances of 17604 // TextTrackMenuItem, but this will remain for backwards compatibility 17605 kind: options.kind, 17606 kinds: options.kinds, 17607 default: false, 17608 mode: 'disabled' 17609 }; 17610 if (!options.kinds) { 17611 options.kinds = [options.kind]; 17612 } 17613 if (options.label) { 17614 options.track.label = options.label; 17615 } else { 17616 options.track.label = options.kinds.join(' and ') + ' off'; 17617 } 17618 17619 // MenuItem is selectable 17620 options.selectable = true; 17621 // MenuItem is NOT multiSelectable (i.e. only one can be marked "selected" at a time) 17622 options.multiSelectable = false; 17623 super(player, options); 17624 } 17625 17626 /** 17627 * Handle text track change 17628 * 17629 * @param {Event} event 17630 * The event that caused this function to run 17631 */ 17632 handleTracksChange(event) { 17633 const tracks = this.player().textTracks(); 17634 let shouldBeSelected = true; 17635 for (let i = 0, l = tracks.length; i < l; i++) { 17636 const track = tracks[i]; 17637 if (this.options_.kinds.indexOf(track.kind) > -1 && track.mode === 'showing') { 17638 shouldBeSelected = false; 17639 break; 17640 } 17641 } 17642 17643 // Prevent redundant selected() calls because they may cause 17644 // screen readers to read the appended control text unnecessarily 17645 if (shouldBeSelected !== this.isSelected_) { 17646 this.selected(shouldBeSelected); 17647 } 17648 } 17649 handleSelectedLanguageChange(event) { 17650 const tracks = this.player().textTracks(); 17651 let allHidden = true; 17652 for (let i = 0, l = tracks.length; i < l; i++) { 17653 const track = tracks[i]; 17654 if (['captions', 'descriptions', 'subtitles'].indexOf(track.kind) > -1 && track.mode === 'showing') { 17655 allHidden = false; 17656 break; 17657 } 17658 } 17659 if (allHidden) { 17660 this.player_.cache_.selectedLanguage = { 17661 enabled: false 17662 }; 17663 } 17664 } 17665 17666 /** 17667 * Update control text and label on languagechange 17668 */ 17669 handleLanguagechange() { 17670 this.$('.vjs-menu-item-text').textContent = this.player_.localize(this.options_.label); 17671 super.handleLanguagechange(); 17672 } 17673 } 17674 Component$1.registerComponent('OffTextTrackMenuItem', OffTextTrackMenuItem); 17675 17676 /** 17677 * @file text-track-button.js 17678 */ 17679 17680 /** 17681 * The base class for buttons that toggle specific text track types (e.g. subtitles) 17682 * 17683 * @extends MenuButton 17684 */ 17685 class TextTrackButton extends TrackButton { 17686 /** 17687 * Creates an instance of this class. 17688 * 17689 * @param { import('../../player').default } player 17690 * The `Player` that this class should be attached to. 17691 * 17692 * @param {Object} [options={}] 17693 * The key/value store of player options. 17694 */ 17695 constructor(player, options = {}) { 17696 options.tracks = player.textTracks(); 17697 super(player, options); 17698 } 17699 17700 /** 17701 * Create a menu item for each text track 17702 * 17703 * @param {TextTrackMenuItem[]} [items=[]] 17704 * Existing array of items to use during creation 17705 * 17706 * @return {TextTrackMenuItem[]} 17707 * Array of menu items that were created 17708 */ 17709 createItems(items = [], TrackMenuItem = TextTrackMenuItem) { 17710 // Label is an override for the [track] off label 17711 // USed to localise captions/subtitles 17712 let label; 17713 if (this.label_) { 17714 label = `${this.label_} off`; 17715 } 17716 // Add an OFF menu item to turn all tracks off 17717 items.push(new OffTextTrackMenuItem(this.player_, { 17718 kinds: this.kinds_, 17719 kind: this.kind_, 17720 label 17721 })); 17722 this.hideThreshold_ += 1; 17723 const tracks = this.player_.textTracks(); 17724 if (!Array.isArray(this.kinds_)) { 17725 this.kinds_ = [this.kind_]; 17726 } 17727 for (let i = 0; i < tracks.length; i++) { 17728 const track = tracks[i]; 17729 17730 // only add tracks that are of an appropriate kind and have a label 17731 if (this.kinds_.indexOf(track.kind) > -1) { 17732 const item = new TrackMenuItem(this.player_, { 17733 track, 17734 kinds: this.kinds_, 17735 kind: this.kind_, 17736 // MenuItem is selectable 17737 selectable: true, 17738 // MenuItem is NOT multiSelectable (i.e. only one can be marked "selected" at a time) 17739 multiSelectable: false 17740 }); 17741 item.addClass(`vjs-${track.kind}-menu-item`); 17742 items.push(item); 17743 } 17744 } 17745 return items; 17746 } 17747 } 17748 Component$1.registerComponent('TextTrackButton', TextTrackButton); 17749 17750 /** 17751 * @file chapters-track-menu-item.js 17752 */ 17753 17754 /** 17755 * The chapter track menu item 17756 * 17757 * @extends MenuItem 17758 */ 17759 class ChaptersTrackMenuItem extends MenuItem { 17760 /** 17761 * Creates an instance of this class. 17762 * 17763 * @param { import('../../player').default } player 17764 * The `Player` that this class should be attached to. 17765 * 17766 * @param {Object} [options] 17767 * The key/value store of player options. 17768 */ 17769 constructor(player, options) { 17770 const track = options.track; 17771 const cue = options.cue; 17772 const currentTime = player.currentTime(); 17773 17774 // Modify options for parent MenuItem class's init. 17775 options.selectable = true; 17776 options.multiSelectable = false; 17777 options.label = cue.text; 17778 options.selected = cue.startTime <= currentTime && currentTime < cue.endTime; 17779 super(player, options); 17780 this.track = track; 17781 this.cue = cue; 17782 } 17783 17784 /** 17785 * This gets called when an `ChaptersTrackMenuItem` is "clicked". See 17786 * {@link ClickableComponent} for more detailed information on what a click can be. 17787 * 17788 * @param {Event} [event] 17789 * The `keydown`, `tap`, or `click` event that caused this function to be 17790 * called. 17791 * 17792 * @listens tap 17793 * @listens click 17794 */ 17795 handleClick(event) { 17796 super.handleClick(); 17797 this.player_.currentTime(this.cue.startTime); 17798 } 17799 } 17800 Component$1.registerComponent('ChaptersTrackMenuItem', ChaptersTrackMenuItem); 17801 17802 /** 17803 * @file chapters-button.js 17804 */ 17805 17806 /** 17807 * The button component for toggling and selecting chapters 17808 * Chapters act much differently than other text tracks 17809 * Cues are navigation vs. other tracks of alternative languages 17810 * 17811 * @extends TextTrackButton 17812 */ 17813 class ChaptersButton extends TextTrackButton { 17814 /** 17815 * Creates an instance of this class. 17816 * 17817 * @param { import('../../player').default } player 17818 * The `Player` that this class should be attached to. 17819 * 17820 * @param {Object} [options] 17821 * The key/value store of player options. 17822 * 17823 * @param {Function} [ready] 17824 * The function to call when this function is ready. 17825 */ 17826 constructor(player, options, ready) { 17827 super(player, options, ready); 17828 this.setIcon('chapters'); 17829 this.selectCurrentItem_ = () => { 17830 this.items.forEach(item => { 17831 item.selected(this.track_.activeCues[0] === item.cue); 17832 }); 17833 }; 17834 } 17835 17836 /** 17837 * Builds the default DOM `className`. 17838 * 17839 * @return {string} 17840 * The DOM `className` for this object. 17841 */ 17842 buildCSSClass() { 17843 return `vjs-chapters-button ${super.buildCSSClass()}`; 17844 } 17845 buildWrapperCSSClass() { 17846 return `vjs-chapters-button ${super.buildWrapperCSSClass()}`; 17847 } 17848 17849 /** 17850 * Update the menu based on the current state of its items. 17851 * 17852 * @param {Event} [event] 17853 * An event that triggered this function to run. 17854 * 17855 * @listens TextTrackList#addtrack 17856 * @listens TextTrackList#removetrack 17857 * @listens TextTrackList#change 17858 */ 17859 update(event) { 17860 if (event && event.track && event.track.kind !== 'chapters') { 17861 return; 17862 } 17863 const track = this.findChaptersTrack(); 17864 if (track !== this.track_) { 17865 this.setTrack(track); 17866 super.update(); 17867 } else if (!this.items || track && track.cues && track.cues.length !== this.items.length) { 17868 // Update the menu initially or if the number of cues has changed since set 17869 super.update(); 17870 } 17871 } 17872 17873 /** 17874 * Set the currently selected track for the chapters button. 17875 * 17876 * @param {TextTrack} track 17877 * The new track to select. Nothing will change if this is the currently selected 17878 * track. 17879 */ 17880 setTrack(track) { 17881 if (this.track_ === track) { 17882 return; 17883 } 17884 if (!this.updateHandler_) { 17885 this.updateHandler_ = this.update.bind(this); 17886 } 17887 17888 // here this.track_ refers to the old track instance 17889 if (this.track_) { 17890 const remoteTextTrackEl = this.player_.remoteTextTrackEls().getTrackElementByTrack_(this.track_); 17891 if (remoteTextTrackEl) { 17892 remoteTextTrackEl.removeEventListener('load', this.updateHandler_); 17893 } 17894 this.track_.removeEventListener('cuechange', this.selectCurrentItem_); 17895 this.track_ = null; 17896 } 17897 this.track_ = track; 17898 17899 // here this.track_ refers to the new track instance 17900 if (this.track_) { 17901 this.track_.mode = 'hidden'; 17902 const remoteTextTrackEl = this.player_.remoteTextTrackEls().getTrackElementByTrack_(this.track_); 17903 if (remoteTextTrackEl) { 17904 remoteTextTrackEl.addEventListener('load', this.updateHandler_); 17905 } 17906 this.track_.addEventListener('cuechange', this.selectCurrentItem_); 17907 } 17908 } 17909 17910 /** 17911 * Find the track object that is currently in use by this ChaptersButton 17912 * 17913 * @return {TextTrack|undefined} 17914 * The current track or undefined if none was found. 17915 */ 17916 findChaptersTrack() { 17917 const tracks = this.player_.textTracks() || []; 17918 for (let i = tracks.length - 1; i >= 0; i--) { 17919 // We will always choose the last track as our chaptersTrack 17920 const track = tracks[i]; 17921 if (track.kind === this.kind_) { 17922 return track; 17923 } 17924 } 17925 } 17926 17927 /** 17928 * Get the caption for the ChaptersButton based on the track label. This will also 17929 * use the current tracks localized kind as a fallback if a label does not exist. 17930 * 17931 * @return {string} 17932 * The tracks current label or the localized track kind. 17933 */ 17934 getMenuCaption() { 17935 if (this.track_ && this.track_.label) { 17936 return this.track_.label; 17937 } 17938 return this.localize(toTitleCase$1(this.kind_)); 17939 } 17940 17941 /** 17942 * Create menu from chapter track 17943 * 17944 * @return { import('../../menu/menu').default } 17945 * New menu for the chapter buttons 17946 */ 17947 createMenu() { 17948 this.options_.title = this.getMenuCaption(); 17949 return super.createMenu(); 17950 } 17951 17952 /** 17953 * Create a menu item for each text track 17954 * 17955 * @return { import('./text-track-menu-item').default[] } 17956 * Array of menu items 17957 */ 17958 createItems() { 17959 const items = []; 17960 if (!this.track_) { 17961 return items; 17962 } 17963 const cues = this.track_.cues; 17964 if (!cues) { 17965 return items; 17966 } 17967 for (let i = 0, l = cues.length; i < l; i++) { 17968 const cue = cues[i]; 17969 const mi = new ChaptersTrackMenuItem(this.player_, { 17970 track: this.track_, 17971 cue 17972 }); 17973 items.push(mi); 17974 } 17975 return items; 17976 } 17977 } 17978 17979 /** 17980 * `kind` of TextTrack to look for to associate it with this menu. 17981 * 17982 * @type {string} 17983 * @private 17984 */ 17985 ChaptersButton.prototype.kind_ = 'chapters'; 17986 17987 /** 17988 * The text that should display over the `ChaptersButton`s controls. Added for localization. 17989 * 17990 * @type {string} 17991 * @protected 17992 */ 17993 ChaptersButton.prototype.controlText_ = 'Chapters'; 17994 Component$1.registerComponent('ChaptersButton', ChaptersButton); 17995 17996 /** 17997 * @file descriptions-button.js 17998 */ 17999 18000 /** 18001 * The button component for toggling and selecting descriptions 18002 * 18003 * @extends TextTrackButton 18004 */ 18005 class DescriptionsButton extends TextTrackButton { 18006 /** 18007 * Creates an instance of this class. 18008 * 18009 * @param { import('../../player').default } player 18010 * The `Player` that this class should be attached to. 18011 * 18012 * @param {Object} [options] 18013 * The key/value store of player options. 18014 * 18015 * @param {Function} [ready] 18016 * The function to call when this component is ready. 18017 */ 18018 constructor(player, options, ready) { 18019 super(player, options, ready); 18020 this.setIcon('audio-description'); 18021 const tracks = player.textTracks(); 18022 const changeHandler = bind_(this, this.handleTracksChange); 18023 tracks.addEventListener('change', changeHandler); 18024 this.on('dispose', function () { 18025 tracks.removeEventListener('change', changeHandler); 18026 }); 18027 } 18028 18029 /** 18030 * Handle text track change 18031 * 18032 * @param {Event} event 18033 * The event that caused this function to run 18034 * 18035 * @listens TextTrackList#change 18036 */ 18037 handleTracksChange(event) { 18038 const tracks = this.player().textTracks(); 18039 let disabled = false; 18040 18041 // Check whether a track of a different kind is showing 18042 for (let i = 0, l = tracks.length; i < l; i++) { 18043 const track = tracks[i]; 18044 if (track.kind !== this.kind_ && track.mode === 'showing') { 18045 disabled = true; 18046 break; 18047 } 18048 } 18049 18050 // If another track is showing, disable this menu button 18051 if (disabled) { 18052 this.disable(); 18053 } else { 18054 this.enable(); 18055 } 18056 } 18057 18058 /** 18059 * Builds the default DOM `className`. 18060 * 18061 * @return {string} 18062 * The DOM `className` for this object. 18063 */ 18064 buildCSSClass() { 18065 return `vjs-descriptions-button ${super.buildCSSClass()}`; 18066 } 18067 buildWrapperCSSClass() { 18068 return `vjs-descriptions-button ${super.buildWrapperCSSClass()}`; 18069 } 18070 } 18071 18072 /** 18073 * `kind` of TextTrack to look for to associate it with this menu. 18074 * 18075 * @type {string} 18076 * @private 18077 */ 18078 DescriptionsButton.prototype.kind_ = 'descriptions'; 18079 18080 /** 18081 * The text that should display over the `DescriptionsButton`s controls. Added for localization. 18082 * 18083 * @type {string} 18084 * @protected 18085 */ 18086 DescriptionsButton.prototype.controlText_ = 'Descriptions'; 18087 Component$1.registerComponent('DescriptionsButton', DescriptionsButton); 18088 18089 /** 18090 * @file subtitles-button.js 18091 */ 18092 18093 /** 18094 * The button component for toggling and selecting subtitles 18095 * 18096 * @extends TextTrackButton 18097 */ 18098 class SubtitlesButton extends TextTrackButton { 18099 /** 18100 * Creates an instance of this class. 18101 * 18102 * @param { import('../../player').default } player 18103 * The `Player` that this class should be attached to. 18104 * 18105 * @param {Object} [options] 18106 * The key/value store of player options. 18107 * 18108 * @param {Function} [ready] 18109 * The function to call when this component is ready. 18110 */ 18111 constructor(player, options, ready) { 18112 super(player, options, ready); 18113 this.setIcon('subtitles'); 18114 } 18115 18116 /** 18117 * Builds the default DOM `className`. 18118 * 18119 * @return {string} 18120 * The DOM `className` for this object. 18121 */ 18122 buildCSSClass() { 18123 return `vjs-subtitles-button ${super.buildCSSClass()}`; 18124 } 18125 buildWrapperCSSClass() { 18126 return `vjs-subtitles-button ${super.buildWrapperCSSClass()}`; 18127 } 18128 } 18129 18130 /** 18131 * `kind` of TextTrack to look for to associate it with this menu. 18132 * 18133 * @type {string} 18134 * @private 18135 */ 18136 SubtitlesButton.prototype.kind_ = 'subtitles'; 18137 18138 /** 18139 * The text that should display over the `SubtitlesButton`s controls. Added for localization. 18140 * 18141 * @type {string} 18142 * @protected 18143 */ 18144 SubtitlesButton.prototype.controlText_ = 'Subtitles'; 18145 Component$1.registerComponent('SubtitlesButton', SubtitlesButton); 18146 18147 /** 18148 * @file caption-settings-menu-item.js 18149 */ 18150 18151 /** 18152 * The menu item for caption track settings menu 18153 * 18154 * @extends TextTrackMenuItem 18155 */ 18156 class CaptionSettingsMenuItem extends TextTrackMenuItem { 18157 /** 18158 * Creates an instance of this class. 18159 * 18160 * @param { import('../../player').default } player 18161 * The `Player` that this class should be attached to. 18162 * 18163 * @param {Object} [options] 18164 * The key/value store of player options. 18165 */ 18166 constructor(player, options) { 18167 options.track = { 18168 player, 18169 kind: options.kind, 18170 label: options.kind + ' settings', 18171 selectable: false, 18172 default: false, 18173 mode: 'disabled' 18174 }; 18175 18176 // CaptionSettingsMenuItem has no concept of 'selected' 18177 options.selectable = false;
vendor: 8,431 bytes, lines 18178-18464
18178 options.name = 'CaptionSettingsMenuItem'; 18179 super(player, options); 18180 this.addClass('vjs-texttrack-settings'); 18181 this.controlText(', opens ' + options.kind + ' settings dialog'); 18182 } 18183 18184 /** 18185 * This gets called when an `CaptionSettingsMenuItem` is "clicked". See 18186 * {@link ClickableComponent} for more detailed information on what a click can be. 18187 * 18188 * @param {Event} [event] 18189 * The `keydown`, `tap`, or `click` event that caused this function to be 18190 * called. 18191 * 18192 * @listens tap 18193 * @listens click 18194 */ 18195 handleClick(event) { 18196 this.player().getChild('textTrackSettings').open(); 18197 } 18198 18199 /** 18200 * Update control text and label on languagechange 18201 */ 18202 handleLanguagechange() { 18203 this.$('.vjs-menu-item-text').textContent = this.player_.localize(this.options_.kind + ' settings'); 18204 super.handleLanguagechange(); 18205 } 18206 } 18207 Component$1.registerComponent('CaptionSettingsMenuItem', CaptionSettingsMenuItem); 18208 18209 /** 18210 * @file captions-button.js 18211 */ 18212 18213 /** 18214 * The button component for toggling and selecting captions 18215 * 18216 * @extends TextTrackButton 18217 */ 18218 class CaptionsButton extends TextTrackButton { 18219 /** 18220 * Creates an instance of this class. 18221 * 18222 * @param { import('../../player').default } player 18223 * The `Player` that this class should be attached to. 18224 * 18225 * @param {Object} [options] 18226 * The key/value store of player options. 18227 * 18228 * @param {Function} [ready] 18229 * The function to call when this component is ready. 18230 */ 18231 constructor(player, options, ready) { 18232 super(player, options, ready); 18233 this.setIcon('captions'); 18234 } 18235 18236 /** 18237 * Builds the default DOM `className`. 18238 * 18239 * @return {string} 18240 * The DOM `className` for this object. 18241 */ 18242 buildCSSClass() { 18243 return `vjs-captions-button ${super.buildCSSClass()}`; 18244 } 18245 buildWrapperCSSClass() { 18246 return `vjs-captions-button ${super.buildWrapperCSSClass()}`; 18247 } 18248 18249 /** 18250 * Create caption menu items 18251 * 18252 * @return {CaptionSettingsMenuItem[]} 18253 * The array of current menu items. 18254 */ 18255 createItems() { 18256 const items = []; 18257 if (!(this.player().tech_ && this.player().tech_.featuresNativeTextTracks) && this.player().getChild('textTrackSettings')) { 18258 items.push(new CaptionSettingsMenuItem(this.player_, { 18259 kind: this.kind_ 18260 })); 18261 this.hideThreshold_ += 1; 18262 } 18263 return super.createItems(items); 18264 } 18265 } 18266 18267 /** 18268 * `kind` of TextTrack to look for to associate it with this menu. 18269 * 18270 * @type {string} 18271 * @private 18272 */ 18273 CaptionsButton.prototype.kind_ = 'captions'; 18274 18275 /** 18276 * The text that should display over the `CaptionsButton`s controls. Added for localization. 18277 * 18278 * @type {string} 18279 * @protected 18280 */ 18281 CaptionsButton.prototype.controlText_ = 'Captions'; 18282 Component$1.registerComponent('CaptionsButton', CaptionsButton); 18283 18284 /** 18285 * @file subs-caps-menu-item.js 18286 */ 18287 18288 /** 18289 * SubsCapsMenuItem has an [cc] icon to distinguish captions from subtitles 18290 * in the SubsCapsMenu. 18291 * 18292 * @extends TextTrackMenuItem 18293 */ 18294 class SubsCapsMenuItem extends TextTrackMenuItem { 18295 createEl(type, props, attrs) { 18296 const el = super.createEl(type, props, attrs); 18297 const parentSpan = el.querySelector('.vjs-menu-item-text'); 18298 if (this.options_.track.kind === 'captions') { 18299 if (this.player_.options_.experimentalSvgIcons) { 18300 this.setIcon('captions', el); 18301 } else { 18302 parentSpan.appendChild(createEl('span', { 18303 className: 'vjs-icon-placeholder' 18304 }, { 18305 'aria-hidden': true 18306 })); 18307 } 18308 parentSpan.appendChild(createEl('span', { 18309 className: 'vjs-control-text', 18310 // space added as the text will visually flow with the 18311 // label 18312 textContent: ` ${this.localize('Captions')}` 18313 })); 18314 } 18315 return el; 18316 } 18317 } 18318 Component$1.registerComponent('SubsCapsMenuItem', SubsCapsMenuItem); 18319 18320 /** 18321 * @file sub-caps-button.js 18322 */ 18323 18324 /** 18325 * The button component for toggling and selecting captions and/or subtitles 18326 * 18327 * @extends TextTrackButton 18328 */ 18329 class SubsCapsButton extends TextTrackButton { 18330 /** 18331 * Creates an instance of this class. 18332 * 18333 * @param { import('../../player').default } player 18334 * The `Player` that this class should be attached to. 18335 * 18336 * @param {Object} [options] 18337 * The key/value store of player options. 18338 * 18339 * @param {Function} [ready] 18340 * The function to call when this component is ready. 18341 */ 18342 constructor(player, options = {}) { 18343 super(player, options); 18344 18345 // Although North America uses "captions" in most cases for 18346 // "captions and subtitles" other locales use "subtitles" 18347 this.label_ = 'subtitles'; 18348 this.setIcon('subtitles'); 18349 if (['en', 'en-us', 'en-ca', 'fr-ca'].indexOf(this.player_.language_) > -1) { 18350 this.label_ = 'captions'; 18351 this.setIcon('captions'); 18352 } 18353 this.menuButton_.controlText(toTitleCase$1(this.label_)); 18354 } 18355 18356 /** 18357 * Builds the default DOM `className`. 18358 * 18359 * @return {string} 18360 * The DOM `className` for this object. 18361 */ 18362 buildCSSClass() { 18363 return `vjs-subs-caps-button ${super.buildCSSClass()}`; 18364 } 18365 buildWrapperCSSClass() { 18366 return `vjs-subs-caps-button ${super.buildWrapperCSSClass()}`; 18367 } 18368 18369 /** 18370 * Create caption/subtitles menu items 18371 * 18372 * @return {CaptionSettingsMenuItem[]} 18373 * The array of current menu items. 18374 */ 18375 createItems() { 18376 let items = []; 18377 if (!(this.player().tech_ && this.player().tech_.featuresNativeTextTracks) && this.player().getChild('textTrackSettings')) { 18378 items.push(new CaptionSettingsMenuItem(this.player_, { 18379 kind: this.label_ 18380 })); 18381 this.hideThreshold_ += 1; 18382 } 18383 items = super.createItems(items, SubsCapsMenuItem); 18384 return items; 18385 } 18386 } 18387 18388 /** 18389 * `kind`s of TextTrack to look for to associate it with this menu. 18390 * 18391 * @type {array} 18392 * @private 18393 */ 18394 SubsCapsButton.prototype.kinds_ = ['captions', 'subtitles']; 18395 18396 /** 18397 * The text that should display over the `SubsCapsButton`s controls. 18398 * 18399 * 18400 * @type {string} 18401 * @protected 18402 */ 18403 SubsCapsButton.prototype.controlText_ = 'Subtitles'; 18404 Component$1.registerComponent('SubsCapsButton', SubsCapsButton); 18405 18406 /** 18407 * @file audio-track-menu-item.js 18408 */ 18409 18410 /** 18411 * An {@link AudioTrack} {@link MenuItem} 18412 * 18413 * @extends MenuItem 18414 */ 18415 class AudioTrackMenuItem extends MenuItem { 18416 /** 18417 * Creates an instance of this class. 18418 * 18419 * @param { import('../../player').default } player 18420 * The `Player` that this class should be attached to. 18421 * 18422 * @param {Object} [options] 18423 * The key/value store of player options. 18424 */ 18425 constructor(player, options) { 18426 const track = options.track; 18427 const tracks = player.audioTracks(); 18428 18429 // Modify options for parent MenuItem class's init. 18430 options.label = track.label || track.language || 'Unknown'; 18431 options.selected = track.enabled; 18432 super(player, options); 18433 this.track = track; 18434 this.addClass(`vjs-${track.kind}-menu-item`); 18435 const changeHandler = (...args) => { 18436 this.handleTracksChange.apply(this, args); 18437 }; 18438 tracks.addEventListener('change', changeHandler); 18439 this.on('dispose', () => { 18440 tracks.removeEventListener('change', changeHandler); 18441 }); 18442 } 18443 createEl(type, props, attrs) { 18444 const el = super.createEl(type, props, attrs); 18445 const parentSpan = el.querySelector('.vjs-menu-item-text'); 18446 if (['main-desc', 'description'].indexOf(this.options_.track.kind) >= 0) { 18447 parentSpan.appendChild(createEl('span', { 18448 className: 'vjs-icon-placeholder' 18449 }, { 18450 'aria-hidden': true 18451 })); 18452 parentSpan.appendChild(createEl('span', { 18453 className: 'vjs-control-text', 18454 textContent: ' ' + this.localize('Descriptions') 18455 })); 18456 } 18457 return el; 18458 } 18459 18460 /** 18461 * This gets called when an `AudioTrackMenuItem is "clicked". See {@link ClickableComponent} 18462 * for more detailed information on what a click can be. 18463 * 18464 * @param {Event} [event]
vendor: 19,608 bytes, lines 18465-19155
18465 * The `keydown`, `tap`, or `click` event that caused this function to be 18466 * called. 18467 * 18468 * @listens tap 18469 * @listens click 18470 */ 18471 handleClick(event) { 18472 super.handleClick(event); 18473 18474 // the audio track list will automatically toggle other tracks 18475 // off for us. 18476 this.track.enabled = true; 18477 18478 // when native audio tracks are used, we want to make sure that other tracks are turned off 18479 if (this.player_.tech_.featuresNativeAudioTracks) { 18480 const tracks = this.player_.audioTracks(); 18481 for (let i = 0; i < tracks.length; i++) { 18482 const track = tracks[i]; 18483 18484 // skip the current track since we enabled it above 18485 if (track === this.track) { 18486 continue; 18487 } 18488 track.enabled = track === this.track; 18489 } 18490 } 18491 } 18492 18493 /** 18494 * Handle any {@link AudioTrack} change. 18495 * 18496 * @param {Event} [event] 18497 * The {@link AudioTrackList#change} event that caused this to run. 18498 * 18499 * @listens AudioTrackList#change 18500 */ 18501 handleTracksChange(event) { 18502 this.selected(this.track.enabled); 18503 } 18504 } 18505 Component$1.registerComponent('AudioTrackMenuItem', AudioTrackMenuItem); 18506 18507 /** 18508 * @file audio-track-button.js 18509 */ 18510 18511 /** 18512 * The base class for buttons that toggle specific {@link AudioTrack} types. 18513 * 18514 * @extends TrackButton 18515 */ 18516 class AudioTrackButton extends TrackButton { 18517 /** 18518 * Creates an instance of this class. 18519 * 18520 * @param {Player} player 18521 * The `Player` that this class should be attached to. 18522 * 18523 * @param {Object} [options={}] 18524 * The key/value store of player options. 18525 */ 18526 constructor(player, options = {}) { 18527 options.tracks = player.audioTracks(); 18528 super(player, options); 18529 this.setIcon('audio'); 18530 } 18531 18532 /** 18533 * Builds the default DOM `className`. 18534 * 18535 * @return {string} 18536 * The DOM `className` for this object. 18537 */ 18538 buildCSSClass() { 18539 return `vjs-audio-button ${super.buildCSSClass()}`; 18540 } 18541 buildWrapperCSSClass() { 18542 return `vjs-audio-button ${super.buildWrapperCSSClass()}`; 18543 } 18544 18545 /** 18546 * Create a menu item for each audio track 18547 * 18548 * @param {AudioTrackMenuItem[]} [items=[]] 18549 * An array of existing menu items to use. 18550 * 18551 * @return {AudioTrackMenuItem[]} 18552 * An array of menu items 18553 */ 18554 createItems(items = []) { 18555 // if there's only one audio track, there no point in showing it 18556 this.hideThreshold_ = 1; 18557 const tracks = this.player_.audioTracks(); 18558 for (let i = 0; i < tracks.length; i++) { 18559 const track = tracks[i]; 18560 items.push(new AudioTrackMenuItem(this.player_, { 18561 track, 18562 // MenuItem is selectable 18563 selectable: true, 18564 // MenuItem is NOT multiSelectable (i.e. only one can be marked "selected" at a time) 18565 multiSelectable: false 18566 })); 18567 } 18568 return items; 18569 } 18570 } 18571 18572 /** 18573 * The text that should display over the `AudioTrackButton`s controls. Added for localization. 18574 * 18575 * @type {string} 18576 * @protected 18577 */ 18578 AudioTrackButton.prototype.controlText_ = 'Audio Track'; 18579 Component$1.registerComponent('AudioTrackButton', AudioTrackButton); 18580 18581 /** 18582 * @file playback-rate-menu-item.js 18583 */ 18584 18585 /** 18586 * The specific menu item type for selecting a playback rate. 18587 * 18588 * @extends MenuItem 18589 */ 18590 class PlaybackRateMenuItem extends MenuItem { 18591 /** 18592 * Creates an instance of this class. 18593 * 18594 * @param { import('../../player').default } player 18595 * The `Player` that this class should be attached to. 18596 * 18597 * @param {Object} [options] 18598 * The key/value store of player options. 18599 */ 18600 constructor(player, options) { 18601 const label = options.rate; 18602 const rate = parseFloat(label, 10); 18603 18604 // Modify options for parent MenuItem class's init. 18605 options.label = label; 18606 options.selected = rate === player.playbackRate(); 18607 options.selectable = true; 18608 options.multiSelectable = false; 18609 super(player, options); 18610 this.label = label; 18611 this.rate = rate; 18612 this.on(player, 'ratechange', e => this.update(e)); 18613 } 18614 18615 /** 18616 * This gets called when an `PlaybackRateMenuItem` is "clicked". See 18617 * {@link ClickableComponent} for more detailed information on what a click can be. 18618 * 18619 * @param {Event} [event] 18620 * The `keydown`, `tap`, or `click` event that caused this function to be 18621 * called. 18622 * 18623 * @listens tap 18624 * @listens click 18625 */ 18626 handleClick(event) { 18627 super.handleClick(); 18628 this.player().playbackRate(this.rate); 18629 } 18630 18631 /** 18632 * Update the PlaybackRateMenuItem when the playbackrate changes. 18633 * 18634 * @param {Event} [event] 18635 * The `ratechange` event that caused this function to run. 18636 * 18637 * @listens Player#ratechange 18638 */ 18639 update(event) { 18640 this.selected(this.player().playbackRate() === this.rate); 18641 } 18642 } 18643 18644 /** 18645 * The text that should display over the `PlaybackRateMenuItem`s controls. Added for localization. 18646 * 18647 * @type {string} 18648 * @private 18649 */ 18650 PlaybackRateMenuItem.prototype.contentElType = 'button'; 18651 Component$1.registerComponent('PlaybackRateMenuItem', PlaybackRateMenuItem); 18652 18653 /** 18654 * @file playback-rate-menu-button.js 18655 */ 18656 18657 /** 18658 * The component for controlling the playback rate. 18659 * 18660 * @extends MenuButton 18661 */ 18662 class PlaybackRateMenuButton extends MenuButton { 18663 /** 18664 * Creates an instance of this class. 18665 * 18666 * @param { import('../../player').default } player 18667 * The `Player` that this class should be attached to. 18668 * 18669 * @param {Object} [options] 18670 * The key/value store of player options. 18671 */ 18672 constructor(player, options) { 18673 super(player, options); 18674 this.menuButton_.el_.setAttribute('aria-describedby', this.labelElId_); 18675 this.updateVisibility(); 18676 this.updateLabel(); 18677 this.on(player, 'loadstart', e => this.updateVisibility(e)); 18678 this.on(player, 'ratechange', e => this.updateLabel(e)); 18679 this.on(player, 'playbackrateschange', e => this.handlePlaybackRateschange(e)); 18680 } 18681 18682 /** 18683 * Create the `Component`'s DOM element 18684 * 18685 * @return {Element} 18686 * The element that was created. 18687 */ 18688 createEl() { 18689 const el = super.createEl(); 18690 this.labelElId_ = 'vjs-playback-rate-value-label-' + this.id_; 18691 this.labelEl_ = createEl('div', { 18692 className: 'vjs-playback-rate-value', 18693 id: this.labelElId_, 18694 textContent: '1x' 18695 }); 18696 el.appendChild(this.labelEl_); 18697 return el; 18698 } 18699 dispose() { 18700 this.labelEl_ = null; 18701 super.dispose(); 18702 } 18703 18704 /** 18705 * Builds the default DOM `className`. 18706 * 18707 * @return {string} 18708 * The DOM `className` for this object. 18709 */ 18710 buildCSSClass() { 18711 return `vjs-playback-rate ${super.buildCSSClass()}`; 18712 } 18713 buildWrapperCSSClass() { 18714 return `vjs-playback-rate ${super.buildWrapperCSSClass()}`; 18715 } 18716 18717 /** 18718 * Create the list of menu items. Specific to each subclass. 18719 * 18720 */ 18721 createItems() { 18722 const rates = this.playbackRates(); 18723 const items = []; 18724 for (let i = rates.length - 1; i >= 0; i--) { 18725 items.push(new PlaybackRateMenuItem(this.player(), { 18726 rate: rates[i] + 'x' 18727 })); 18728 } 18729 return items; 18730 } 18731 18732 /** 18733 * On playbackrateschange, update the menu to account for the new items. 18734 * 18735 * @listens Player#playbackrateschange 18736 */ 18737 handlePlaybackRateschange(event) { 18738 this.update(); 18739 } 18740 18741 /** 18742 * Get possible playback rates 18743 * 18744 * @return {Array} 18745 * All possible playback rates 18746 */ 18747 playbackRates() { 18748 const player = this.player(); 18749 return player.playbackRates && player.playbackRates() || []; 18750 } 18751 18752 /** 18753 * Get whether playback rates is supported by the tech 18754 * and an array of playback rates exists 18755 * 18756 * @return {boolean} 18757 * Whether changing playback rate is supported 18758 */ 18759 playbackRateSupported() { 18760 return this.player().tech_ && this.player().tech_.featuresPlaybackRate && this.playbackRates() && this.playbackRates().length > 0; 18761 } 18762 18763 /** 18764 * Hide playback rate controls when they're no playback rate options to select 18765 * 18766 * @param {Event} [event] 18767 * The event that caused this function to run. 18768 * 18769 * @listens Player#loadstart 18770 */ 18771 updateVisibility(event) { 18772 if (this.playbackRateSupported()) { 18773 this.removeClass('vjs-hidden'); 18774 } else { 18775 this.addClass('vjs-hidden'); 18776 } 18777 } 18778 18779 /** 18780 * Update button label when rate changed 18781 * 18782 * @param {Event} [event] 18783 * The event that caused this function to run. 18784 * 18785 * @listens Player#ratechange 18786 */ 18787 updateLabel(event) { 18788 if (this.playbackRateSupported()) { 18789 this.labelEl_.textContent = this.player().playbackRate() + 'x'; 18790 } 18791 } 18792 } 18793 18794 /** 18795 * The text that should display over the `PlaybackRateMenuButton`s controls. 18796 * 18797 * Added for localization. 18798 * 18799 * @type {string} 18800 * @protected 18801 */ 18802 PlaybackRateMenuButton.prototype.controlText_ = 'Playback Rate'; 18803 Component$1.registerComponent('PlaybackRateMenuButton', PlaybackRateMenuButton); 18804 18805 /** 18806 * @file spacer.js 18807 */ 18808 18809 /** 18810 * Just an empty spacer element that can be used as an append point for plugins, etc. 18811 * Also can be used to create space between elements when necessary. 18812 * 18813 * @extends Component 18814 */ 18815 class Spacer extends Component$1 { 18816 /** 18817 * Builds the default DOM `className`. 18818 * 18819 * @return {string} 18820 * The DOM `className` for this object. 18821 */ 18822 buildCSSClass() { 18823 return `vjs-spacer ${super.buildCSSClass()}`; 18824 } 18825 18826 /** 18827 * Create the `Component`'s DOM element 18828 * 18829 * @return {Element} 18830 * The element that was created. 18831 */ 18832 createEl(tag = 'div', props = {}, attributes = {}) { 18833 if (!props.className) { 18834 props.className = this.buildCSSClass(); 18835 } 18836 return super.createEl(tag, props, attributes); 18837 } 18838 } 18839 Component$1.registerComponent('Spacer', Spacer); 18840 18841 /** 18842 * @file custom-control-spacer.js 18843 */ 18844 18845 /** 18846 * Spacer specifically meant to be used as an insertion point for new plugins, etc. 18847 * 18848 * @extends Spacer 18849 */ 18850 class CustomControlSpacer extends Spacer { 18851 /** 18852 * Builds the default DOM `className`. 18853 * 18854 * @return {string} 18855 * The DOM `className` for this object. 18856 */ 18857 buildCSSClass() { 18858 return `vjs-custom-control-spacer ${super.buildCSSClass()}`; 18859 } 18860 18861 /** 18862 * Create the `Component`'s DOM element 18863 * 18864 * @return {Element} 18865 * The element that was created. 18866 */ 18867 createEl() { 18868 return super.createEl('div', { 18869 className: this.buildCSSClass(), 18870 // No-flex/table-cell mode requires there be some content 18871 // in the cell to fill the remaining space of the table. 18872 textContent: '\u00a0' 18873 }); 18874 } 18875 } 18876 Component$1.registerComponent('CustomControlSpacer', CustomControlSpacer); 18877 18878 /** 18879 * @file control-bar.js 18880 */ 18881 18882 /** 18883 * Container of main controls. 18884 * 18885 * @extends Component 18886 */ 18887 class ControlBar extends Component$1 { 18888 /** 18889 * Create the `Component`'s DOM element 18890 * 18891 * @return {Element} 18892 * The element that was created. 18893 */ 18894 createEl() { 18895 return super.createEl('div', { 18896 className: 'vjs-control-bar', 18897 dir: 'ltr' 18898 }); 18899 } 18900 } 18901 18902 /** 18903 * Default options for `ControlBar` 18904 * 18905 * @type {Object} 18906 * @private 18907 */ 18908 ControlBar.prototype.options_ = { 18909 children: ['playToggle', 'skipBackward', 'skipForward', 'volumePanel', 'currentTimeDisplay', 'timeDivider', 'durationDisplay', 'progressControl', 'liveDisplay', 'seekToLive', 'remainingTimeDisplay', 'customControlSpacer', 'playbackRateMenuButton', 'chaptersButton', 'descriptionsButton', 'subsCapsButton', 'audioTrackButton', 'pictureInPictureToggle', 'fullscreenToggle'] 18910 }; 18911 Component$1.registerComponent('ControlBar', ControlBar); 18912 18913 /** 18914 * @file error-display.js 18915 */ 18916 18917 /** 18918 * A display that indicates an error has occurred. This means that the video 18919 * is unplayable. 18920 * 18921 * @extends ModalDialog 18922 */ 18923 class ErrorDisplay extends ModalDialog { 18924 /** 18925 * Creates an instance of this class. 18926 * 18927 * @param { import('./player').default } player 18928 * The `Player` that this class should be attached to. 18929 * 18930 * @param {Object} [options] 18931 * The key/value store of player options. 18932 */ 18933 constructor(player, options) { 18934 super(player, options); 18935 this.on(player, 'error', e => { 18936 this.close(); 18937 this.open(e); 18938 }); 18939 } 18940 18941 /** 18942 * Builds the default DOM `className`. 18943 * 18944 * @return {string} 18945 * The DOM `className` for this object. 18946 * 18947 * @deprecated Since version 5. 18948 */ 18949 buildCSSClass() { 18950 return `vjs-error-display ${super.buildCSSClass()}`; 18951 } 18952 18953 /** 18954 * Gets the localized error message based on the `Player`s error. 18955 * 18956 * @return {string} 18957 * The `Player`s error message localized or an empty string. 18958 */ 18959 content() { 18960 const error = this.player().error(); 18961 return error ? this.localize(error.message) : ''; 18962 } 18963 } 18964 18965 /** 18966 * The default options for an `ErrorDisplay`. 18967 * 18968 * @private 18969 */ 18970 ErrorDisplay.prototype.options_ = Object.assign({}, ModalDialog.prototype.options_, { 18971 pauseOnOpen: false, 18972 fillAlways: true, 18973 temporary: false, 18974 uncloseable: true 18975 }); 18976 Component$1.registerComponent('ErrorDisplay', ErrorDisplay); 18977 18978 /** 18979 * @file text-track-settings.js 18980 */ 18981 const LOCAL_STORAGE_KEY$1 = 'vjs-text-track-settings'; 18982 const COLOR_BLACK = ['#000', 'Black']; 18983 const COLOR_BLUE = ['#00F', 'Blue']; 18984 const COLOR_CYAN = ['#0FF', 'Cyan']; 18985 const COLOR_GREEN = ['#0F0', 'Green']; 18986 const COLOR_MAGENTA = ['#F0F', 'Magenta']; 18987 const COLOR_RED = ['#F00', 'Red']; 18988 const COLOR_WHITE = ['#FFF', 'White']; 18989 const COLOR_YELLOW = ['#FF0', 'Yellow']; 18990 const OPACITY_OPAQUE = ['1', 'Opaque']; 18991 const OPACITY_SEMI = ['0.5', 'Semi-Transparent']; 18992 const OPACITY_TRANS = ['0', 'Transparent']; 18993 18994 // Configuration for the various <select> elements in the DOM of this component. 18995 // 18996 // Possible keys include: 18997 // 18998 // `default`: 18999 // The default option index. Only needs to be provided if not zero. 19000 // `parser`: 19001 // A function which is used to parse the value from the selected option in 19002 // a customized way. 19003 // `selector`: 19004 // The selector used to find the associated <select> element. 19005 const selectConfigs = { 19006 backgroundColor: { 19007 selector: '.vjs-bg-color > select', 19008 id: 'captions-background-color-%s', 19009 label: 'Color', 19010 options: [COLOR_BLACK, COLOR_WHITE, COLOR_RED, COLOR_GREEN, COLOR_BLUE, COLOR_YELLOW, COLOR_MAGENTA, COLOR_CYAN] 19011 }, 19012 backgroundOpacity: { 19013 selector: '.vjs-bg-opacity > select', 19014 id: 'captions-background-opacity-%s', 19015 label: 'Opacity', 19016 options: [OPACITY_OPAQUE, OPACITY_SEMI, OPACITY_TRANS] 19017 }, 19018 color: { 19019 selector: '.vjs-text-color > select', 19020 id: 'captions-foreground-color-%s', 19021 label: 'Color', 19022 options: [COLOR_WHITE, COLOR_BLACK, COLOR_RED, COLOR_GREEN, COLOR_BLUE, COLOR_YELLOW, COLOR_MAGENTA, COLOR_CYAN] 19023 }, 19024 edgeStyle: { 19025 selector: '.vjs-edge-style > select', 19026 id: '%s', 19027 label: 'Text Edge Style', 19028 options: [['none', 'None'], ['raised', 'Raised'], ['depressed', 'Depressed'], ['uniform', 'Uniform'], ['dropshadow', 'Drop shadow']] 19029 }, 19030 fontFamily: { 19031 selector: '.vjs-font-family > select', 19032 id: 'captions-font-family-%s', 19033 label: 'Font Family', 19034 options: [['proportionalSansSerif', 'Proportional Sans-Serif'], ['monospaceSansSerif', 'Monospace Sans-Serif'], ['proportionalSerif', 'Proportional Serif'], ['monospaceSerif', 'Monospace Serif'], ['casual', 'Casual'], ['script', 'Script'], ['small-caps', 'Small Caps']] 19035 }, 19036 fontPercent: { 19037 selector: '.vjs-font-percent > select', 19038 id: 'captions-font-size-%s', 19039 label: 'Font Size', 19040 options: [['0.50', '50%'], ['0.75', '75%'], ['1.00', '100%'], ['1.25', '125%'], ['1.50', '150%'], ['1.75', '175%'], ['2.00', '200%'], ['3.00', '300%'], ['4.00', '400%']], 19041 default: 2, 19042 parser: v => v === '1.00' ? null : Number(v) 19043 }, 19044 textOpacity: { 19045 selector: '.vjs-text-opacity > select', 19046 id: 'captions-foreground-opacity-%s', 19047 label: 'Opacity', 19048 options: [OPACITY_OPAQUE, OPACITY_SEMI] 19049 }, 19050 // Options for this object are defined below. 19051 windowColor: { 19052 selector: '.vjs-window-color > select', 19053 id: 'captions-window-color-%s', 19054 label: 'Color' 19055 }, 19056 // Options for this object are defined below. 19057 windowOpacity: { 19058 selector: '.vjs-window-opacity > select', 19059 id: 'captions-window-opacity-%s', 19060 label: 'Opacity', 19061 options: [OPACITY_TRANS, OPACITY_SEMI, OPACITY_OPAQUE] 19062 } 19063 }; 19064 selectConfigs.windowColor.options = selectConfigs.backgroundColor.options; 19065 19066 /** 19067 * Get the actual value of an option. 19068 * 19069 * @param {string} value 19070 * The value to get 19071 * 19072 * @param {Function} [parser] 19073 * Optional function to adjust the value. 19074 * 19075 * @return {*} 19076 * - Will be `undefined` if no value exists 19077 * - Will be `undefined` if the given value is "none". 19078 * - Will be the actual value otherwise. 19079 * 19080 * @private 19081 */ 19082 function parseOptionValue(value, parser) { 19083 if (parser) { 19084 value = parser(value); 19085 } 19086 if (value && value !== 'none') { 19087 return value; 19088 } 19089 } 19090 19091 /** 19092 * Gets the value of the selected <option> element within a <select> element. 19093 * 19094 * @param {Element} el 19095 * the element to look in 19096 * 19097 * @param {Function} [parser] 19098 * Optional function to adjust the value. 19099 * 19100 * @return {*} 19101 * - Will be `undefined` if no value exists 19102 * - Will be `undefined` if the given value is "none". 19103 * - Will be the actual value otherwise. 19104 * 19105 * @private 19106 */ 19107 function getSelectedOptionValue(el, parser) { 19108 const value = el.options[el.options.selectedIndex].value; 19109 return parseOptionValue(value, parser); 19110 } 19111 19112 /** 19113 * Sets the selected <option> element within a <select> element based on a 19114 * given value. 19115 * 19116 * @param {Element} el 19117 * The element to look in. 19118 * 19119 * @param {string} value 19120 * the property to look on. 19121 * 19122 * @param {Function} [parser] 19123 * Optional function to adjust the value before comparing. 19124 * 19125 * @private 19126 */ 19127 function setSelectedOption(el, value, parser) { 19128 if (!value) { 19129 return; 19130 } 19131 for (let i = 0; i < el.options.length; i++) { 19132 if (parseOptionValue(el.options[i].value, parser) === value) { 19133 el.selectedIndex = i; 19134 break; 19135 } 19136 } 19137 } 19138 19139 /** 19140 * Manipulate Text Tracks settings. 19141 * 19142 * @extends ModalDialog 19143 */ 19144 class TextTrackSettings extends ModalDialog { 19145 /** 19146 * Creates an instance of this class. 19147 * 19148 * @param { import('../player').default } player 19149 * The `Player` that this class should be attached to. 19150 * 19151 * @param {Object} [options] 19152 * The key/value store of player options. 19153 */ 19154 constructor(player, options) { 19155 options.temporary = false;
19156 super(player, options); 19157 this.updateDisplay = this.updateDisplay.bind(this); 19158 19159 // fill the modal and pretend we have opened it 19160 this.fill(); 19161 this.hasBeenOpened_ = this.hasBeenFilled_ = true; 19162 this.endDialog = createEl('p', { 19163 className: 'vjs-control-text', 19164 textContent: this.localize('End of dialog window.') 19165 }); 19166 this.el().appendChild(this.endDialog); 19167 this.setDefaults(); 19168 19169 // Grab `persistTextTrackSettings` from the player options if not passed in child options 19170 if (options.persistTextTrackSettings === undefined) { 19171 this.options_.persistTextTrackSettings = this.options_.playerOptions.persistTextTrackSettings; 19172 } 19173 this.on(this.$('.vjs-done-button'), 'click', () => { 19174 this.saveSettings(); 19175 this.close(); 19176 }); 19177 this.on(this.$('.vjs-default-button'), 'click', () => { 19178 this.setDefaults(); 19179 this.updateDisplay(); 19180 }); 19181 each(selectConfigs, config => { 19182 this.on(this.$(config.selector), 'change', this.updateDisplay); 19183 }); 19184 if (this.options_.persistTextTrackSettings) { 19185 this.restoreSettings(); 19186 } 19187 } 19188 dispose() { 19189 this.endDialog = null; 19190 super.dispose(); 19191 } 19192 19193 /** 19194 * Create a <select> element with configured options. 19195 * 19196 * @param {string} key 19197 * Configuration key to use during creation. 19198 * 19199 * @return {string} 19200 * An HTML string. 19201 * 19202 * @private 19203 */ 19204 createElSelect_(key, legendId = '', type = 'label') { 19205 const config = selectConfigs[key]; 19206 const id = config.id.replace('%s', this.id_); 19207 const selectLabelledbyIds = [legendId, id].join(' ').trim(); 19208 return [`<${type} id="${id}" class="${type === 'label' ? 'vjs-label' : ''}">`, this.localize(config.label), `</${type}>`, `<select aria-labelledby="${selectLabelledbyIds}">`].concat(config.options.map(o => { 19209 const optionId = id + '-' + o[1].replace(/\W+/g, ''); 19210 return [`<option id="${optionId}" value="${o[0]}" `, `aria-labelledby="${selectLabelledbyIds} ${optionId}">`, this.localize(o[1]), '</option>'].join(''); 19211 })).concat('</select>').join(''); 19212 } 19213 19214 /** 19215 * Create foreground color element for the component 19216 * 19217 * @return {string} 19218 * An HTML string. 19219 * 19220 * @private 19221 */ 19222 createElFgColor_() { 19223 const legendId = `captions-text-legend-${this.id_}`; 19224 return ['<fieldset class="vjs-fg vjs-track-setting">', `<legend id="${legendId}">`, this.localize('Text'), '</legend>', '<span class="vjs-text-color">', this.createElSelect_('color', legendId), '</span>', '<span class="vjs-text-opacity vjs-opacity">', this.createElSelect_('textOpacity', legendId), '</span>', '</fieldset>'].join(''); 19225 } 19226 19227 /** 19228 * Create background color element for the component 19229 * 19230 * @return {string} 19231 * An HTML string. 19232 * 19233 * @private 19234 */ 19235 createElBgColor_() { 19236 const legendId = `captions-background-${this.id_}`; 19237 return ['<fieldset class="vjs-bg vjs-track-setting">', `<legend id="${legendId}">`, this.localize('Text Background'), '</legend>', '<span class="vjs-bg-color">', this.createElSelect_('backgroundColor', legendId), '</span>', '<span class="vjs-bg-opacity vjs-opacity">', this.createElSelect_('backgroundOpacity', legendId), '</span>', '</fieldset>'].join(''); 19238 } 19239 19240 /** 19241 * Create window color element for the component 19242 * 19243 * @return {string} 19244 * An HTML string. 19245 * 19246 * @private 19247 */ 19248 createElWinColor_() { 19249 const legendId = `captions-window-${this.id_}`; 19250 return ['<fieldset class="vjs-window vjs-track-setting">', `<legend id="${legendId}">`, this.localize('Caption Area Background'), '</legend>', '<span class="vjs-window-color">', this.createElSelect_('windowColor', legendId), '</span>', '<span class="vjs-window-opacity vjs-opacity">', this.createElSelect_('windowOpacity', legendId), '</span>', '</fieldset>'].join(''); 19251 } 19252 19253 /** 19254 * Create color elements for the component 19255 * 19256 * @return {Element} 19257 * The element that was created 19258 * 19259 * @private 19260 */ 19261 createElColors_() { 19262 return createEl('div', { 19263 className: 'vjs-track-settings-colors', 19264 innerHTML: [this.createElFgColor_(), this.createElBgColor_(), this.createElWinColor_()].join('') 19265 }); 19266 } 19267 19268 /** 19269 * Create font elements for the component 19270 * 19271 * @return {Element} 19272 * The element that was created. 19273 * 19274 * @private 19275 */ 19276 createElFont_() { 19277 return createEl('div', { 19278 className: 'vjs-track-settings-font', 19279 innerHTML: ['<fieldset class="vjs-font-percent vjs-track-setting">', this.createElSelect_('fontPercent', '', 'legend'), '</fieldset>', '<fieldset class="vjs-edge-style vjs-track-setting">', this.createElSelect_('edgeStyle', '', 'legend'), '</fieldset>', '<fieldset class="vjs-font-family vjs-track-setting">', this.createElSelect_('fontFamily', '', 'legend'), '</fieldset>'].join('') 19280 }); 19281 } 19282 19283 /** 19284 * Create controls for the component 19285 * 19286 * @return {Element} 19287 * The element that was created. 19288 * 19289 * @private 19290 */ 19291 createElControls_() { 19292 const defaultsDescription = this.localize('restore all settings to the default values'); 19293 return createEl('div', { 19294 className: 'vjs-track-settings-controls', 19295 innerHTML: [`<button type="button" class="vjs-default-button" title="${defaultsDescription}">`, this.localize('Reset'), `<span class="vjs-control-text"> ${defaultsDescription}</span>`, '</button>', `<button type="button" class="vjs-done-button">${this.localize('Done')}</button>`].join('') 19296 }); 19297 } 19298 content() { 19299 return [this.createElColors_(), this.createElFont_(), this.createElControls_()]; 19300 } 19301 label() { 19302 return this.localize('Caption Settings Dialog'); 19303 } 19304 description() { 19305 return this.localize('Beginning of dialog window. Escape will cancel and close the window.'); 19306 } 19307 buildCSSClass() { 19308 return super.buildCSSClass() + ' vjs-text-track-settings'; 19309 } 19310 19311 /** 19312 * Gets an object of text track settings (or null). 19313 * 19314 * @return {Object} 19315 * An object with config values parsed from the DOM or localStorage. 19316 */ 19317 getValues() { 19318 return reduce(selectConfigs, (accum, config, key) => { 19319 const value = getSelectedOptionValue(this.$(config.selector), config.parser); 19320 if (value !== undefined) { 19321 accum[key] = value; 19322 } 19323 return accum; 19324 }, {}); 19325 } 19326 19327 /** 19328 * Sets text track settings from an object of values. 19329 * 19330 * @param {Object} values 19331 * An object with config values parsed from the DOM or localStorage. 19332 */ 19333 setValues(values) { 19334 each(selectConfigs, (config, key) => { 19335 setSelectedOption(this.$(config.selector), values[key], config.parser); 19336 }); 19337 } 19338 19339 /** 19340 * Sets all `<select>` elements to their default values. 19341 */ 19342 setDefaults() { 19343 each(selectConfigs, config => { 19344 const index = config.hasOwnProperty('default') ? config.default : 0; 19345 this.$(config.selector).selectedIndex = index; 19346 }); 19347 } 19348 19349 /** 19350 * Restore texttrack settings from localStorage 19351 */ 19352 restoreSettings() { 19353 let values;
vendor: 8,527 bytes, lines 19354-19612
19354 try { 19355 values = JSON.parse(window.localStorage.getItem(LOCAL_STORAGE_KEY$1)); 19356 } catch (err) { 19357 log$1.warn(err); 19358 } 19359 if (values) { 19360 this.setValues(values); 19361 } 19362 } 19363 19364 /** 19365 * Save text track settings to localStorage 19366 */ 19367 saveSettings() { 19368 if (!this.options_.persistTextTrackSettings) { 19369 return; 19370 } 19371 const values = this.getValues(); 19372 try { 19373 if (Object.keys(values).length) { 19374 window.localStorage.setItem(LOCAL_STORAGE_KEY$1, JSON.stringify(values)); 19375 } else { 19376 window.localStorage.removeItem(LOCAL_STORAGE_KEY$1); 19377 } 19378 } catch (err) { 19379 log$1.warn(err); 19380 } 19381 } 19382 19383 /** 19384 * Update display of text track settings 19385 */ 19386 updateDisplay() { 19387 const ttDisplay = this.player_.getChild('textTrackDisplay'); 19388 if (ttDisplay) { 19389 ttDisplay.updateDisplay(); 19390 } 19391 } 19392 19393 /** 19394 * conditionally blur the element and refocus the captions button 19395 * 19396 * @private 19397 */ 19398 conditionalBlur_() { 19399 this.previouslyActiveEl_ = null; 19400 const cb = this.player_.controlBar; 19401 const subsCapsBtn = cb && cb.subsCapsButton; 19402 const ccBtn = cb && cb.captionsButton; 19403 if (subsCapsBtn) { 19404 subsCapsBtn.focus(); 19405 } else if (ccBtn) { 19406 ccBtn.focus(); 19407 } 19408 } 19409 19410 /** 19411 * Repopulate dialog with new localizations on languagechange 19412 */ 19413 handleLanguagechange() { 19414 this.fill(); 19415 } 19416 } 19417 Component$1.registerComponent('TextTrackSettings', TextTrackSettings); 19418 19419 /** 19420 * @file resize-manager.js 19421 */ 19422 19423 /** 19424 * A Resize Manager. It is in charge of triggering `playerresize` on the player in the right conditions. 19425 * 19426 * 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}. 19427 * 19428 * If the ResizeObserver is available natively, it will be used. A polyfill can be passed in as an option. 19429 * If a `playerresize` event is not needed, the ResizeManager component can be removed from the player, see the example below. 19430 * 19431 * @example <caption>How to disable the resize manager</caption> 19432 * const player = videojs('#vid', { 19433 * resizeManager: false 19434 * }); 19435 * 19436 * @see {@link https://wicg.github.io/ResizeObserver/|ResizeObserver specification} 19437 * 19438 * @extends Component 19439 */ 19440 class ResizeManager extends Component$1 { 19441 /** 19442 * Create the ResizeManager. 19443 * 19444 * @param {Object} player 19445 * The `Player` that this class should be attached to. 19446 * 19447 * @param {Object} [options] 19448 * The key/value store of ResizeManager options. 19449 * 19450 * @param {Object} [options.ResizeObserver] 19451 * A polyfill for ResizeObserver can be passed in here. 19452 * If this is set to null it will ignore the native ResizeObserver and fall back to the iframe fallback. 19453 */ 19454 constructor(player, options) { 19455 let RESIZE_OBSERVER_AVAILABLE = options.ResizeObserver || window.ResizeObserver; 19456 19457 // if `null` was passed, we want to disable the ResizeObserver 19458 if (options.ResizeObserver === null) { 19459 RESIZE_OBSERVER_AVAILABLE = false; 19460 } 19461 19462 // Only create an element when ResizeObserver isn't available 19463 const options_ = merge$2({ 19464 createEl: !RESIZE_OBSERVER_AVAILABLE, 19465 reportTouchActivity: false 19466 }, options); 19467 super(player, options_); 19468 this.ResizeObserver = options.ResizeObserver || window.ResizeObserver; 19469 this.loadListener_ = null; 19470 this.resizeObserver_ = null; 19471 this.debouncedHandler_ = debounce(() => { 19472 this.resizeHandler(); 19473 }, 100, false, this); 19474 if (RESIZE_OBSERVER_AVAILABLE) { 19475 this.resizeObserver_ = new this.ResizeObserver(this.debouncedHandler_); 19476 this.resizeObserver_.observe(player.el()); 19477 } else { 19478 this.loadListener_ = () => { 19479 if (!this.el_ || !this.el_.contentWindow) { 19480 return; 19481 } 19482 const debouncedHandler_ = this.debouncedHandler_; 19483 let unloadListener_ = this.unloadListener_ = function () { 19484 off(this, 'resize', debouncedHandler_); 19485 off(this, 'unload', unloadListener_); 19486 unloadListener_ = null; 19487 }; 19488 19489 // safari and edge can unload the iframe before resizemanager dispose 19490 // we have to dispose of event handlers correctly before that happens 19491 on(this.el_.contentWindow, 'unload', unloadListener_); 19492 on(this.el_.contentWindow, 'resize', debouncedHandler_); 19493 }; 19494 this.one('load', this.loadListener_); 19495 } 19496 } 19497 createEl() { 19498 return super.createEl('iframe', { 19499 className: 'vjs-resize-manager', 19500 tabIndex: -1, 19501 title: this.localize('No content') 19502 }, { 19503 'aria-hidden': 'true' 19504 }); 19505 } 19506 19507 /** 19508 * Called when a resize is triggered on the iframe or a resize is observed via the ResizeObserver 19509 * 19510 * @fires Player#playerresize 19511 */ 19512 resizeHandler() { 19513 /** 19514 * Called when the player size has changed 19515 * 19516 * @event Player#playerresize 19517 * @type {Event} 19518 */ 19519 // make sure player is still around to trigger 19520 // prevents this from causing an error after dispose 19521 if (!this.player_ || !this.player_.trigger) { 19522 return; 19523 } 19524 this.player_.trigger('playerresize'); 19525 } 19526 dispose() { 19527 if (this.debouncedHandler_) { 19528 this.debouncedHandler_.cancel(); 19529 } 19530 if (this.resizeObserver_) { 19531 if (this.player_.el()) { 19532 this.resizeObserver_.unobserve(this.player_.el()); 19533 } 19534 this.resizeObserver_.disconnect(); 19535 } 19536 if (this.loadListener_) { 19537 this.off('load', this.loadListener_); 19538 } 19539 if (this.el_ && this.el_.contentWindow && this.unloadListener_) { 19540 this.unloadListener_.call(this.el_.contentWindow); 19541 } 19542 this.ResizeObserver = null; 19543 this.resizeObserver = null; 19544 this.debouncedHandler_ = null; 19545 this.loadListener_ = null; 19546 super.dispose(); 19547 } 19548 } 19549 Component$1.registerComponent('ResizeManager', ResizeManager); 19550 19551 const defaults = { 19552 trackingThreshold: 20, 19553 liveTolerance: 15 19554 }; 19555 19556 /* 19557 track when we are at the live edge, and other helpers for live playback */ 19558 19559 /** 19560 * A class for checking live current time and determining when the player 19561 * is at or behind the live edge. 19562 */ 19563 class LiveTracker extends Component$1 { 19564 /** 19565 * Creates an instance of this class. 19566 * 19567 * @param { import('./player').default } player 19568 * The `Player` that this class should be attached to. 19569 * 19570 * @param {Object} [options] 19571 * The key/value store of player options. 19572 * 19573 * @param {number} [options.trackingThreshold=20] 19574 * Number of seconds of live window (seekableEnd - seekableStart) that 19575 * media needs to have before the liveui will be shown. 19576 * 19577 * @param {number} [options.liveTolerance=15] 19578 * Number of seconds behind live that we have to be 19579 * before we will be considered non-live. Note that this will only 19580 * be used when playing at the live edge. This allows large seekable end 19581 * changes to not effect whether we are live or not. 19582 */ 19583 constructor(player, options) { 19584 // LiveTracker does not need an element 19585 const options_ = merge$2(defaults, options, { 19586 createEl: false 19587 }); 19588 super(player, options_); 19589 this.trackLiveHandler_ = () => this.trackLive_(); 19590 this.handlePlay_ = e => this.handlePlay(e); 19591 this.handleFirstTimeupdate_ = e => this.handleFirstTimeupdate(e); 19592 this.handleSeeked_ = e => this.handleSeeked(e); 19593 this.seekToLiveEdge_ = e => this.seekToLiveEdge(e); 19594 this.reset_(); 19595 this.on(this.player_, 'durationchange', e => this.handleDurationchange(e)); 19596 // we should try to toggle tracking on canplay as native playback engines, like Safari 19597 // may not have the proper values for things like seekableEnd until then 19598 this.on(this.player_, 'canplay', () => this.toggleTracking()); 19599 } 19600 19601 /** 19602 * all the functionality for tracking when seek end changes 19603 * and for tracking how far past seek end we should be 19604 */ 19605 trackLive_() { 19606 const seekable = this.player_.seekable(); 19607 19608 // skip undefined seekable 19609 if (!seekable || !seekable.length) { 19610 return; 19611 } 19612 const newTime = Number(window
vendor: 9,638 bytes, lines 19612-19937
19612.performance.now().toFixed(4)); 19613 const deltaTime = this.lastTime_ === -1 ? 0 : (newTime - this.lastTime_) / 1000; 19614 this.lastTime_ = newTime; 19615 this.pastSeekEnd_ = this.pastSeekEnd() + deltaTime; 19616 const liveCurrentTime = this.liveCurrentTime(); 19617 const currentTime = this.player_.currentTime(); 19618 19619 // we are behind live if any are true 19620 // 1. the player is paused 19621 // 2. the user seeked to a location 2 seconds away from live 19622 // 3. the difference between live and current time is greater 19623 // liveTolerance which defaults to 15s 19624 let isBehind = this.player_.paused() || this.seekedBehindLive_ || Math.abs(liveCurrentTime - currentTime) > this.options_.liveTolerance; 19625 19626 // we cannot be behind if 19627 // 1. until we have not seen a timeupdate yet 19628 // 2. liveCurrentTime is Infinity, which happens on Android and Native Safari 19629 if (!this.timeupdateSeen_ || liveCurrentTime === Infinity) { 19630 isBehind = false; 19631 } 19632 if (isBehind !== this.behindLiveEdge_) { 19633 this.behindLiveEdge_ = isBehind; 19634 this.trigger('liveedgechange'); 19635 } 19636 } 19637 19638 /** 19639 * handle a durationchange event on the player 19640 * and start/stop tracking accordingly. 19641 */ 19642 handleDurationchange() { 19643 this.toggleTracking(); 19644 } 19645 19646 /** 19647 * start/stop tracking 19648 */ 19649 toggleTracking() { 19650 if (this.player_.duration() === Infinity && this.liveWindow() >= this.options_.trackingThreshold) { 19651 if (this.player_.options_.liveui) { 19652 this.player_.addClass('vjs-liveui'); 19653 } 19654 this.startTracking(); 19655 } else { 19656 this.player_.removeClass('vjs-liveui'); 19657 this.stopTracking(); 19658 } 19659 } 19660 19661 /** 19662 * start tracking live playback 19663 */ 19664 startTracking() { 19665 if (this.isTracking()) { 19666 return; 19667 } 19668 19669 // If we haven't seen a timeupdate, we need to check whether playback 19670 // began before this component started tracking. This can happen commonly 19671 // when using autoplay. 19672 if (!this.timeupdateSeen_) { 19673 this.timeupdateSeen_ = this.player_.hasStarted(); 19674 } 19675 this.trackingInterval_ = this.setInterval(this.trackLiveHandler_, UPDATE_REFRESH_INTERVAL); 19676 this.trackLive_(); 19677 this.on(this.player_, ['play', 'pause'], this.trackLiveHandler_); 19678 if (!this.timeupdateSeen_) { 19679 this.one(this.player_, 'play', this.handlePlay_); 19680 this.one(this.player_, 'timeupdate', this.handleFirstTimeupdate_); 19681 } else { 19682 this.on(this.player_, 'seeked', this.handleSeeked_); 19683 } 19684 } 19685 19686 /** 19687 * handle the first timeupdate on the player if it wasn't already playing 19688 * when live tracker started tracking. 19689 */ 19690 handleFirstTimeupdate() { 19691 this.timeupdateSeen_ = true; 19692 this.on(this.player_, 'seeked', this.handleSeeked_); 19693 } 19694 19695 /** 19696 * Keep track of what time a seek starts, and listen for seeked 19697 * to find where a seek ends. 19698 */ 19699 handleSeeked() { 19700 const timeDiff = Math.abs(this.liveCurrentTime() - this.player_.currentTime()); 19701 this.seekedBehindLive_ = this.nextSeekedFromUser_ && timeDiff > 2; 19702 this.nextSeekedFromUser_ = false; 19703 this.trackLive_(); 19704 } 19705 19706 /** 19707 * handle the first play on the player, and make sure that we seek 19708 * right to the live edge. 19709 */ 19710 handlePlay() { 19711 this.one(this.player_, 'timeupdate', this.seekToLiveEdge_); 19712 } 19713 19714 /** 19715 * Stop tracking, and set all internal variables to 19716 * their initial value. 19717 */ 19718 reset_() { 19719 this.lastTime_ = -1; 19720 this.pastSeekEnd_ = 0; 19721 this.lastSeekEnd_ = -1; 19722 this.behindLiveEdge_ = true; 19723 this.timeupdateSeen_ = false; 19724 this.seekedBehindLive_ = false; 19725 this.nextSeekedFromUser_ = false; 19726 this.clearInterval(this.trackingInterval_); 19727 this.trackingInterval_ = null; 19728 this.off(this.player_, ['play', 'pause'], this.trackLiveHandler_); 19729 this.off(this.player_, 'seeked', this.handleSeeked_); 19730 this.off(this.player_, 'play', this.handlePlay_); 19731 this.off(this.player_, 'timeupdate', this.handleFirstTimeupdate_); 19732 this.off(this.player_, 'timeupdate', this.seekToLiveEdge_); 19733 } 19734 19735 /** 19736 * The next seeked event is from the user. Meaning that any seek 19737 * > 2s behind live will be considered behind live for real and 19738 * liveTolerance will be ignored. 19739 */ 19740 nextSeekedFromUser() { 19741 this.nextSeekedFromUser_ = true; 19742 } 19743 19744 /** 19745 * stop tracking live playback 19746 */ 19747 stopTracking() { 19748 if (!this.isTracking()) { 19749 return; 19750 } 19751 this.reset_(); 19752 this.trigger('liveedgechange'); 19753 } 19754 19755 /** 19756 * A helper to get the player seekable end 19757 * so that we don't have to null check everywhere 19758 * 19759 * @return {number} 19760 * The furthest seekable end or Infinity. 19761 */ 19762 seekableEnd() { 19763 const seekable = this.player_.seekable(); 19764 const seekableEnds = []; 19765 let i = seekable ? seekable.length : 0; 19766 while (i--) { 19767 seekableEnds.push(seekable.end(i)); 19768 } 19769 19770 // grab the furthest seekable end after sorting, or if there are none 19771 // default to Infinity 19772 return seekableEnds.length ? seekableEnds.sort()[seekableEnds.length - 1] : Infinity; 19773 } 19774 19775 /** 19776 * A helper to get the player seekable start 19777 * so that we don't have to null check everywhere 19778 * 19779 * @return {number} 19780 * The earliest seekable start or 0. 19781 */ 19782 seekableStart() { 19783 const seekable = this.player_.seekable(); 19784 const seekableStarts = []; 19785 let i = seekable ? seekable.length : 0; 19786 while (i--) { 19787 seekableStarts.push(seekable.start(i)); 19788 } 19789 19790 // grab the first seekable start after sorting, or if there are none 19791 // default to 0 19792 return seekableStarts.length ? seekableStarts.sort()[0] : 0; 19793 } 19794 19795 /** 19796 * Get the live time window aka 19797 * the amount of time between seekable start and 19798 * live current time. 19799 * 19800 * @return {number} 19801 * The amount of seconds that are seekable in 19802 * the live video. 19803 */ 19804 liveWindow() { 19805 const liveCurrentTime = this.liveCurrentTime(); 19806 19807 // if liveCurrenTime is Infinity then we don't have a liveWindow at all 19808 if (liveCurrentTime === Infinity) { 19809 return 0; 19810 } 19811 return liveCurrentTime - this.seekableStart(); 19812 } 19813 19814 /** 19815 * Determines if the player is live, only checks if this component 19816 * is tracking live playback or not 19817 * 19818 * @return {boolean} 19819 * Whether liveTracker is tracking 19820 */ 19821 isLive() { 19822 return this.isTracking(); 19823 } 19824 19825 /** 19826 * Determines if currentTime is at the live edge and won't fall behind 19827 * on each seekableendchange 19828 * 19829 * @return {boolean} 19830 * Whether playback is at the live edge 19831 */ 19832 atLiveEdge() { 19833 return !this.behindLiveEdge(); 19834 } 19835 19836 /** 19837 * get what we expect the live current time to be 19838 * 19839 * @return {number} 19840 * The expected live current time 19841 */ 19842 liveCurrentTime() { 19843 return this.pastSeekEnd() + this.seekableEnd(); 19844 } 19845 19846 /** 19847 * The number of seconds that have occurred after seekable end 19848 * changed. This will be reset to 0 once seekable end changes. 19849 * 19850 * @return {number} 19851 * Seconds past the current seekable end 19852 */ 19853 pastSeekEnd() { 19854 const seekableEnd = this.seekableEnd(); 19855 if (this.lastSeekEnd_ !== -1 && seekableEnd !== this.lastSeekEnd_) { 19856 this.pastSeekEnd_ = 0; 19857 } 19858 this.lastSeekEnd_ = seekableEnd; 19859 return this.pastSeekEnd_; 19860 } 19861 19862 /** 19863 * If we are currently behind the live edge, aka currentTime will be 19864 * behind on a seekableendchange 19865 * 19866 * @return {boolean} 19867 * If we are behind the live edge 19868 */ 19869 behindLiveEdge() { 19870 return this.behindLiveEdge_; 19871 } 19872 19873 /** 19874 * Whether live tracker is currently tracking or not. 19875 */ 19876 isTracking() { 19877 return typeof this.trackingInterval_ === 'number'; 19878 } 19879 19880 /** 19881 * Seek to the live edge if we are behind the live edge 19882 */ 19883 seekToLiveEdge() { 19884 this.seekedBehindLive_ = false; 19885 if (this.atLiveEdge()) { 19886 return; 19887 } 19888 this.nextSeekedFromUser_ = false; 19889 this.player_.currentTime(this.liveCurrentTime()); 19890 } 19891 19892 /** 19893 * Dispose of liveTracker 19894 */ 19895 dispose() { 19896 this.stopTracking(); 19897 super.dispose(); 19898 } 19899 } 19900 Component$1.registerComponent('LiveTracker', LiveTracker); 19901 19902 /** 19903 * Displays an element over the player which contains an optional title and 19904 * description for the current content. 19905 * 19906 * Much of the code for this component originated in the now obsolete 19907 * videojs-dock plugin: https://github.com/brightcove/videojs-dock/ 19908 * 19909 * @extends Component 19910 */ 19911 class TitleBar extends Component$1 { 19912 constructor(player, options) { 19913 super(player, options); 19914 this.on('statechanged', e => this.updateDom_()); 19915 this.updateDom_(); 19916 } 19917 19918 /** 19919 * Create the `TitleBar`'s DOM element 19920 * 19921 * @return {Element} 19922 * The element that was created. 19923 */ 19924 createEl() { 19925 this.els = { 19926 title: createEl('div', { 19927 className: 'vjs-title-bar-title', 19928 id: `vjs-title-bar-title-${newGUID()}` 19929 }), 19930 description: createEl('div', { 19931 className: 'vjs-title-bar-description', 19932 id: `vjs-title-bar-description-${newGUID()}` 19933 }) 19934 }; 19935 return createEl('div', { 19936 className: 'vjs-title-bar' 19937 }, {}, values
vendor: 10,251 bytes, lines 19937-20248
19937$1(this.els)); 19938 } 19939 19940 /** 19941 * Updates the DOM based on the component's state object. 19942 */ 19943 updateDom_() { 19944 const tech = this.player_.tech_; 19945 const techEl = tech && tech.el_; 19946 const techAriaAttrs = { 19947 title: 'aria-labelledby', 19948 description: 'aria-describedby' 19949 }; 19950 ['title', 'description'].forEach(k => { 19951 const value = this.state[k]; 19952 const el = this.els[k]; 19953 const techAriaAttr = techAriaAttrs[k]; 19954 emptyEl(el); 19955 if (value) { 19956 textContent(el, value); 19957 } 19958 19959 // If there is a tech element available, update its ARIA attributes 19960 // according to whether a title and/or description have been provided. 19961 if (techEl) { 19962 techEl.removeAttribute(techAriaAttr); 19963 if (value) { 19964 techEl.setAttribute(techAriaAttr, el.id); 19965 } 19966 } 19967 }); 19968 if (this.state.title || this.state.description) { 19969 this.show(); 19970 } else { 19971 this.hide(); 19972 } 19973 } 19974 19975 /** 19976 * Update the contents of the title bar component with new title and 19977 * description text. 19978 * 19979 * If both title and description are missing, the title bar will be hidden. 19980 * 19981 * If either title or description are present, the title bar will be visible. 19982 * 19983 * NOTE: Any previously set value will be preserved. To unset a previously 19984 * set value, you must pass an empty string or null. 19985 * 19986 * For example: 19987 * 19988 * ``` 19989 * update({title: 'foo', description: 'bar'}) // title: 'foo', description: 'bar' 19990 * update({description: 'bar2'}) // title: 'foo', description: 'bar2' 19991 * update({title: ''}) // title: '', description: 'bar2' 19992 * update({title: 'foo', description: null}) // title: 'foo', description: null 19993 * ``` 19994 * 19995 * @param {Object} [options={}] 19996 * An options object. When empty, the title bar will be hidden. 19997 * 19998 * @param {string} [options.title] 19999 * A title to display in the title bar. 20000 * 20001 * @param {string} [options.description] 20002 * A description to display in the title bar. 20003 */ 20004 update(options) { 20005 this.setState(options); 20006 } 20007 20008 /** 20009 * Dispose the component. 20010 */ 20011 dispose() { 20012 const tech = this.player_.tech_; 20013 const techEl = tech && tech.el_; 20014 if (techEl) { 20015 techEl.removeAttribute('aria-labelledby'); 20016 techEl.removeAttribute('aria-describedby'); 20017 } 20018 super.dispose(); 20019 this.els = null; 20020 } 20021 } 20022 Component$1.registerComponent('TitleBar', TitleBar); 20023 20024 /** 20025 * This function is used to fire a sourceset when there is something 20026 * similar to `mediaEl.load()` being called. It will try to find the source via 20027 * the `src` attribute and then the `<source>` elements. It will then fire `sourceset` 20028 * with the source that was found or empty string if we cannot know. If it cannot 20029 * find a source then `sourceset` will not be fired. 20030 * 20031 * @param { import('./html5').default } tech 20032 * The tech object that sourceset was setup on 20033 * 20034 * @return {boolean} 20035 * returns false if the sourceset was not fired and true otherwise. 20036 */ 20037 const sourcesetLoad = tech => { 20038 const el = tech.el(); 20039 20040 // if `el.src` is set, that source will be loaded. 20041 if (el.hasAttribute('src')) { 20042 tech.triggerSourceset(el.src); 20043 return true; 20044 } 20045 20046 /** 20047 * Since there isn't a src property on the media element, source elements will be used for 20048 * implementing the source selection algorithm. This happens asynchronously and 20049 * for most cases were there is more than one source we cannot tell what source will 20050 * be loaded, without re-implementing the source selection algorithm. At this time we are not 20051 * going to do that. There are three special cases that we do handle here though: 20052 * 20053 * 1. If there are no sources, do not fire `sourceset`. 20054 * 2. If there is only one `<source>` with a `src` property/attribute that is our `src` 20055 * 3. If there is more than one `<source>` but all of them have the same `src` url. 20056 * That will be our src. 20057 */ 20058 const sources = tech.$$('source'); 20059 const srcUrls = []; 20060 let src = ''; 20061 20062 // if there are no sources, do not fire sourceset 20063 if (!sources.length) { 20064 return false; 20065 } 20066 20067 // only count valid/non-duplicate source elements 20068 for (let i = 0; i < sources.length; i++) { 20069 const url = sources[i].src; 20070 if (url && srcUrls.indexOf(url) === -1) { 20071 srcUrls.push(url); 20072 } 20073 } 20074 20075 // there were no valid sources 20076 if (!srcUrls.length) { 20077 return false; 20078 } 20079 20080 // there is only one valid source element url 20081 // use that 20082 if (srcUrls.length === 1) { 20083 src = srcUrls[0]; 20084 } 20085 tech.triggerSourceset(src); 20086 return true; 20087 }; 20088 20089 /** 20090 * our implementation of an `innerHTML` descriptor for browsers 20091 * that do not have one. 20092 */ 20093 const innerHTMLDescriptorPolyfill = Object.defineProperty({}, 'innerHTML', { 20094 get() { 20095 return this.cloneNode(true).innerHTML; 20096 }, 20097 set(v) { 20098 // make a dummy node to use innerHTML on 20099 const dummy = document.createElement(this.nodeName.toLowerCase()); 20100 20101 // set innerHTML to the value provided 20102 dummy.innerHTML = v; 20103 20104 // make a document fragment to hold the nodes from dummy 20105 const docFrag = document.createDocumentFragment(); 20106 20107 // copy all of the nodes created by the innerHTML on dummy 20108 // to the document fragment 20109 while (dummy.childNodes.length) { 20110 docFrag.appendChild(dummy.childNodes[0]); 20111 } 20112 20113 // remove content 20114 this.innerText = ''; 20115 20116 // now we add all of that html in one by appending the 20117 // document fragment. This is how innerHTML does it. 20118 window.Element.prototype.appendChild.call(this, docFrag); 20119 20120 // then return the result that innerHTML's setter would 20121 return this.innerHTML; 20122 } 20123 }); 20124 20125 /** 20126 * Get a property descriptor given a list of priorities and the 20127 * property to get. 20128 */ 20129 const getDescriptor = (priority, prop) => { 20130 let descriptor = {}; 20131 for (let i = 0; i < priority.length; i++) { 20132 descriptor = Object.getOwnPropertyDescriptor(priority[i], prop); 20133 if (descriptor && descriptor.set && descriptor.get) { 20134 break; 20135 } 20136 } 20137 descriptor.enumerable = true; 20138 descriptor.configurable = true; 20139 return descriptor; 20140 }; 20141 const getInnerHTMLDescriptor = tech => getDescriptor([tech.el(), window.HTMLMediaElement.prototype, window.Element.prototype, innerHTMLDescriptorPolyfill], 'innerHTML'); 20142 20143 /** 20144 * Patches browser internal functions so that we can tell synchronously 20145 * if a `<source>` was appended to the media element. For some reason this 20146 * causes a `sourceset` if the the media element is ready and has no source. 20147 * This happens when: 20148 * - The page has just loaded and the media element does not have a source. 20149 * - The media element was emptied of all sources, then `load()` was called. 20150 * 20151 * It does this by patching the following functions/properties when they are supported: 20152 * 20153 * - `append()` - can be used to add a `<source>` element to the media element 20154 * - `appendChild()` - can be used to add a `<source>` element to the media element 20155 * - `insertAdjacentHTML()` - can be used to add a `<source>` element to the media element 20156 * - `innerHTML` - can be used to add a `<source>` element to the media element 20157 * 20158 * @param {Html5} tech 20159 * The tech object that sourceset is being setup on. 20160 */ 20161 const firstSourceWatch = function (tech) { 20162 const el = tech.el(); 20163 20164 // make sure firstSourceWatch isn't setup twice. 20165 if (el.resetSourceWatch_) { 20166 return; 20167 } 20168 const old = {}; 20169 const innerDescriptor = getInnerHTMLDescriptor(tech); 20170 const appendWrapper = appendFn => (...args) => { 20171 const retval = appendFn.apply(el, args); 20172 sourcesetLoad(tech); 20173 return retval; 20174 }; 20175 ['append', 'appendChild', 'insertAdjacentHTML'].forEach(k => { 20176 if (!el[k]) { 20177 return; 20178 } 20179 20180 // store the old function 20181 old[k] = el[k]; 20182 20183 // call the old function with a sourceset if a source 20184 // was loaded 20185 el[k] = appendWrapper(old[k]); 20186 }); 20187 Object.defineProperty(el, 'innerHTML', merge$2(innerDescriptor, { 20188 set: appendWrapper(innerDescriptor.set) 20189 })); 20190 el.resetSourceWatch_ = () => { 20191 el.resetSourceWatch_ = null; 20192 Object.keys(old).forEach(k => { 20193 el[k] = old[k]; 20194 }); 20195 Object.defineProperty(el, 'innerHTML', innerDescriptor); 20196 }; 20197 20198 // on the first sourceset, we need to revert our changes 20199 tech.one('sourceset', el.resetSourceWatch_); 20200 }; 20201 20202 /** 20203 * our implementation of a `src` descriptor for browsers 20204 * that do not have one 20205 */ 20206 const srcDescriptorPolyfill = Object.defineProperty({}, 'src', { 20207 get() { 20208 if (this.hasAttribute('src')) { 20209 return getAbsoluteURL(window.Element.prototype.getAttribute.call(this, 'src')); 20210 } 20211 return ''; 20212 }, 20213 set(v) { 20214 window.Element.prototype.setAttribute.call(this, 'src', v); 20215 return v; 20216 } 20217 }); 20218 const getSrcDescriptor = tech => getDescriptor([tech.el(), window.HTMLMediaElement.prototype, srcDescriptorPolyfill], 'src'); 20219 20220 /** 20221 * setup `sourceset` handling on the `Html5` tech. This function 20222 * patches the following element properties/functions: 20223 * 20224 * - `src` - to determine when `src` is set 20225 * - `setAttribute()` - to determine when `src` is set 20226 * - `load()` - this re-triggers the source selection algorithm, and can 20227 * cause a sourceset. 20228 * 20229 * If there is no source when we are adding `sourceset` support or during a `load()` 20230 * we also patch the functions listed in `firstSourceWatch`. 20231 * 20232 * @param {Html5} tech 20233 * The tech to patch 20234 */ 20235 const setupSourceset = function (tech) { 20236 if (!tech.featuresSourceset) { 20237 return; 20238 } 20239 const el = tech.el(); 20240 20241 // make sure sourceset isn't setup twice. 20242 if (el.resetSourceset_) { 20243 return; 20244 } 20245 const srcDescriptor = getSrcDescriptor(tech); 20246 const oldSetAttribute = el.setAttribute; 20247 const oldLoad = el.load; 20248 Object.defineProperty(el, 'src', merge$2
vendor: 9,111 bytes, lines 20248-20486
20248(srcDescriptor, { 20249 set: v => { 20250 const retval = srcDescriptor.set.call(el, v); 20251 20252 // we use the getter here to get the actual value set on src 20253 tech.triggerSourceset(el.src); 20254 return retval; 20255 } 20256 })); 20257 el.setAttribute = (n, v) => { 20258 const retval = oldSetAttribute.call(el, n, v); 20259 if (/src/i.test(n)) { 20260 tech.triggerSourceset(el.src); 20261 } 20262 return retval; 20263 }; 20264 el.load = () => { 20265 const retval = oldLoad.call(el); 20266 20267 // if load was called, but there was no source to fire 20268 // sourceset on. We have to watch for a source append 20269 // as that can trigger a `sourceset` when the media element 20270 // has no source 20271 if (!sourcesetLoad(tech)) { 20272 tech.triggerSourceset(''); 20273 firstSourceWatch(tech); 20274 } 20275 return retval; 20276 }; 20277 if (el.currentSrc) { 20278 tech.triggerSourceset(el.currentSrc); 20279 } else if (!sourcesetLoad(tech)) { 20280 firstSourceWatch(tech); 20281 } 20282 el.resetSourceset_ = () => { 20283 el.resetSourceset_ = null; 20284 el.load = oldLoad; 20285 el.setAttribute = oldSetAttribute; 20286 Object.defineProperty(el, 'src', srcDescriptor); 20287 if (el.resetSourceWatch_) { 20288 el.resetSourceWatch_(); 20289 } 20290 }; 20291 }; 20292 20293 /** 20294 * @file html5.js 20295 */ 20296 20297 /** 20298 * HTML5 Media Controller - Wrapper for HTML5 Media API 20299 * 20300 * @mixes Tech~SourceHandlerAdditions 20301 * @extends Tech 20302 */ 20303 class Html5 extends Tech { 20304 /** 20305 * Create an instance of this Tech. 20306 * 20307 * @param {Object} [options] 20308 * The key/value store of player options. 20309 * 20310 * @param {Function} [ready] 20311 * Callback function to call when the `HTML5` Tech is ready. 20312 */ 20313 constructor(options, ready) { 20314 super(options, ready); 20315 const source = options.source; 20316 let crossoriginTracks = false; 20317 this.featuresVideoFrameCallback = this.featuresVideoFrameCallback && this.el_.tagName === 'VIDEO'; 20318 20319 // Set the source if one is provided 20320 // 1) Check if the source is new (if not, we want to keep the original so playback isn't interrupted) 20321 // 2) Check to see if the network state of the tag was failed at init, and if so, reset the source 20322 // anyway so the error gets fired. 20323 if (source && (this.el_.currentSrc !== source.src || options.tag && options.tag.initNetworkState_ === 3)) { 20324 this.setSource(source); 20325 } else { 20326 this.handleLateInit_(this.el_); 20327 } 20328 20329 // setup sourceset after late sourceset/init 20330 if (options.enableSourceset) { 20331 this.setupSourcesetHandling_(); 20332 } 20333 this.isScrubbing_ = false; 20334 if (this.el_.hasChildNodes()) { 20335 const nodes = this.el_.childNodes; 20336 let nodesLength = nodes.length; 20337 const removeNodes = []; 20338 while (nodesLength--) { 20339 const node = nodes[nodesLength]; 20340 const nodeName = node.nodeName.toLowerCase(); 20341 if (nodeName === 'track') { 20342 if (!this.featuresNativeTextTracks) { 20343 // Empty video tag tracks so the built-in player doesn't use them also. 20344 // This may not be fast enough to stop HTML5 browsers from reading the tags 20345 // so we'll need to turn off any default tracks if we're manually doing 20346 // captions and subtitles. videoElement.textTracks 20347 removeNodes.push(node); 20348 } else { 20349 // store HTMLTrackElement and TextTrack to remote list 20350 this.remoteTextTrackEls().addTrackElement_(node); 20351 this.remoteTextTracks().addTrack(node.track); 20352 this.textTracks().addTrack(node.track); 20353 if (!crossoriginTracks && !this.el_.hasAttribute('crossorigin') && isCrossOrigin(node.src)) { 20354 crossoriginTracks = true; 20355 } 20356 } 20357 } 20358 } 20359 for (let i = 0; i < removeNodes.length; i++) { 20360 this.el_.removeChild(removeNodes[i]); 20361 } 20362 } 20363 this.proxyNativeTracks_(); 20364 if (this.featuresNativeTextTracks && crossoriginTracks) { 20365 log$1.warn('Text Tracks are being loaded from another origin but the crossorigin attribute isn\'t used.\n' + 'This may prevent text tracks from loading.'); 20366 } 20367 20368 // prevent iOS Safari from disabling metadata text tracks during native playback 20369 this.restoreMetadataTracksInIOSNativePlayer_(); 20370 20371 // Determine if native controls should be used 20372 // Our goal should be to get the custom controls on mobile solid everywhere 20373 // so we can remove this all together. Right now this will block custom 20374 // controls on touch enabled laptops like the Chrome Pixel 20375 if ((TOUCH_ENABLED || IS_IPHONE) && options.nativeControlsForTouch === true) { 20376 this.setControls(true); 20377 } 20378 20379 // on iOS, we want to proxy `webkitbeginfullscreen` and `webkitendfullscreen` 20380 // into a `fullscreenchange` event 20381 this.proxyWebkitFullscreen_(); 20382 this.triggerReady(); 20383 } 20384 20385 /** 20386 * Dispose of `HTML5` media element and remove all tracks. 20387 */ 20388 dispose() { 20389 if (this.el_ && this.el_.resetSourceset_) { 20390 this.el_.resetSourceset_(); 20391 } 20392 Html5.disposeMediaElement(this.el_); 20393 this.options_ = null; 20394 20395 // tech will handle clearing of the emulated track list 20396 super.dispose(); 20397 } 20398 20399 /** 20400 * Modify the media element so that we can detect when 20401 * the source is changed. Fires `sourceset` just after the source has changed 20402 */ 20403 setupSourcesetHandling_() { 20404 setupSourceset(this); 20405 } 20406 20407 /** 20408 * When a captions track is enabled in the iOS Safari native player, all other 20409 * tracks are disabled (including metadata tracks), which nulls all of their 20410 * associated cue points. This will restore metadata tracks to their pre-fullscreen 20411 * state in those cases so that cue points are not needlessly lost. 20412 * 20413 * @private 20414 */ 20415 restoreMetadataTracksInIOSNativePlayer_() { 20416 const textTracks = this.textTracks(); 20417 let metadataTracksPreFullscreenState; 20418 20419 // captures a snapshot of every metadata track's current state 20420 const takeMetadataTrackSnapshot = () => { 20421 metadataTracksPreFullscreenState = []; 20422 for (let i = 0; i < textTracks.length; i++) { 20423 const track = textTracks[i]; 20424 if (track.kind === 'metadata') { 20425 metadataTracksPreFullscreenState.push({ 20426 track, 20427 storedMode: track.mode 20428 }); 20429 } 20430 } 20431 }; 20432 20433 // snapshot each metadata track's initial state, and update the snapshot 20434 // each time there is a track 'change' event 20435 takeMetadataTrackSnapshot(); 20436 textTracks.addEventListener('change', takeMetadataTrackSnapshot); 20437 this.on('dispose', () => textTracks.removeEventListener('change', takeMetadataTrackSnapshot)); 20438 const restoreTrackMode = () => { 20439 for (let i = 0; i < metadataTracksPreFullscreenState.length; i++) { 20440 const storedTrack = metadataTracksPreFullscreenState[i]; 20441 if (storedTrack.track.mode === 'disabled' && storedTrack.track.mode !== storedTrack.storedMode) { 20442 storedTrack.track.mode = storedTrack.storedMode; 20443 } 20444 } 20445 // we only want this handler to be executed on the first 'change' event 20446 textTracks.removeEventListener('change', restoreTrackMode); 20447 }; 20448 20449 // when we enter fullscreen playback, stop updating the snapshot and 20450 // restore all track modes to their pre-fullscreen state 20451 this.on('webkitbeginfullscreen', () => { 20452 textTracks.removeEventListener('change', takeMetadataTrackSnapshot); 20453 20454 // remove the listener before adding it just in case it wasn't previously removed 20455 textTracks.removeEventListener('change', restoreTrackMode); 20456 textTracks.addEventListener('change', restoreTrackMode); 20457 }); 20458 20459 // start updating the snapshot again after leaving fullscreen 20460 this.on('webkitendfullscreen', () => { 20461 // remove the listener before adding it just in case it wasn't previously removed 20462 textTracks.removeEventListener('change', takeMetadataTrackSnapshot); 20463 textTracks.addEventListener('change', takeMetadataTrackSnapshot); 20464 20465 // remove the restoreTrackMode handler in case it wasn't triggered during fullscreen playback 20466 textTracks.removeEventListener('change', restoreTrackMode); 20467 }); 20468 } 20469 20470 /** 20471 * Attempt to force override of tracks for the given type 20472 * 20473 * @param {string} type - Track type to override, possible values include 'Audio', 20474 * 'Video', and 'Text'. 20475 * @param {boolean} override - If set to true native audio/video will be overridden, 20476 * otherwise native audio/video will potentially be used. 20477 * @private 20478 */ 20479 overrideNative_(type, override) { 20480 // If there is no behavioral change don't add/remove listeners 20481 if (override !== this[`featuresNative${type}Tracks`]) { 20482 return; 20483 } 20484 const lowerCaseType = type.toLowerCase(); 20485 if (this[`${lowerCaseType}TracksListeners_`]) { 20486 Object.keys(this[`${lowerCaseType}
vendor: 5,859 bytes, lines 20486-20646
20486TracksListeners_`]).forEach(eventName => { 20487 const elTracks = this.el()[`${lowerCaseType}Tracks`]; 20488 elTracks.removeEventListener(eventName, this[`${lowerCaseType}TracksListeners_`][eventName]); 20489 }); 20490 } 20491 this[`featuresNative${type}Tracks`] = !override; 20492 this[`${lowerCaseType}TracksListeners_`] = null; 20493 this.proxyNativeTracksForType_(lowerCaseType); 20494 } 20495 20496 /** 20497 * Attempt to force override of native audio tracks. 20498 * 20499 * @param {boolean} override - If set to true native audio will be overridden, 20500 * otherwise native audio will potentially be used. 20501 */ 20502 overrideNativeAudioTracks(override) { 20503 this.overrideNative_('Audio', override); 20504 } 20505 20506 /** 20507 * Attempt to force override of native video tracks. 20508 * 20509 * @param {boolean} override - If set to true native video will be overridden, 20510 * otherwise native video will potentially be used. 20511 */ 20512 overrideNativeVideoTracks(override) { 20513 this.overrideNative_('Video', override); 20514 } 20515 20516 /** 20517 * Proxy native track list events for the given type to our track 20518 * lists if the browser we are playing in supports that type of track list. 20519 * 20520 * @param {string} name - Track type; values include 'audio', 'video', and 'text' 20521 * @private 20522 */ 20523 proxyNativeTracksForType_(name) { 20524 const props = NORMAL[name]; 20525 const elTracks = this.el()[props.getterName]; 20526 const techTracks = this[props.getterName](); 20527 if (!this[`featuresNative${props.capitalName}Tracks`] || !elTracks || !elTracks.addEventListener) { 20528 return; 20529 } 20530 const listeners = { 20531 change: e => { 20532 const event = { 20533 type: 'change', 20534 target: techTracks, 20535 currentTarget: techTracks, 20536 srcElement: techTracks 20537 }; 20538 techTracks.trigger(event); 20539 20540 // if we are a text track change event, we should also notify the 20541 // remote text track list. This can potentially cause a false positive 20542 // if we were to get a change event on a non-remote track and 20543 // we triggered the event on the remote text track list which doesn't 20544 // contain that track. However, best practices mean looping through the 20545 // list of tracks and searching for the appropriate mode value, so, 20546 // this shouldn't pose an issue 20547 if (name === 'text') { 20548 this[REMOTE.remoteText.getterName]().trigger(event); 20549 } 20550 }, 20551 addtrack(e) { 20552 techTracks.addTrack(e.track); 20553 }, 20554 removetrack(e) { 20555 techTracks.removeTrack(e.track); 20556 } 20557 }; 20558 const removeOldTracks = function () { 20559 const removeTracks = []; 20560 for (let i = 0; i < techTracks.length; i++) { 20561 let found = false; 20562 for (let j = 0; j < elTracks.length; j++) { 20563 if (elTracks[j] === techTracks[i]) { 20564 found = true; 20565 break; 20566 } 20567 } 20568 if (!found) { 20569 removeTracks.push(techTracks[i]); 20570 } 20571 } 20572 while (removeTracks.length) { 20573 techTracks.removeTrack(removeTracks.shift()); 20574 } 20575 }; 20576 this[props.getterName + 'Listeners_'] = listeners; 20577 Object.keys(listeners).forEach(eventName => { 20578 const listener = listeners[eventName]; 20579 elTracks.addEventListener(eventName, listener); 20580 this.on('dispose', e => elTracks.removeEventListener(eventName, listener)); 20581 }); 20582 20583 // Remove (native) tracks that are not used anymore 20584 this.on('loadstart', removeOldTracks); 20585 this.on('dispose', e => this.off('loadstart', removeOldTracks)); 20586 } 20587 20588 /** 20589 * Proxy all native track list events to our track lists if the browser we are playing 20590 * in supports that type of track list. 20591 * 20592 * @private 20593 */ 20594 proxyNativeTracks_() { 20595 NORMAL.names.forEach(name => { 20596 this.proxyNativeTracksForType_(name); 20597 }); 20598 } 20599 20600 /** 20601 * Create the `Html5` Tech's DOM element. 20602 * 20603 * @return {Element} 20604 * The element that gets created. 20605 */ 20606 createEl() { 20607 let el = this.options_.tag; 20608 20609 // Check if this browser supports moving the element into the box. 20610 // On the iPhone video will break if you move the element, 20611 // So we have to create a brand new element. 20612 // If we ingested the player div, we do not need to move the media element. 20613 if (!el || !(this.options_.playerElIngest || this.movingMediaElementInDOM)) { 20614 // If the original tag is still there, clone and remove it. 20615 if (el) { 20616 const clone = el.cloneNode(true); 20617 if (el.parentNode) { 20618 el.parentNode.insertBefore(clone, el); 20619 } 20620 Html5.disposeMediaElement(el); 20621 el = clone; 20622 } else { 20623 el = document.createElement('video'); 20624 20625 // determine if native controls should be used 20626 const tagAttributes = this.options_.tag && getAttributes(this.options_.tag); 20627 const attributes = merge$2({}, tagAttributes); 20628 if (!TOUCH_ENABLED || this.options_.nativeControlsForTouch !== true) { 20629 delete attributes.controls; 20630 } 20631 setAttributes(el, Object.assign(attributes, { 20632 id: this.options_.techId, 20633 class: 'vjs-tech' 20634 })); 20635 } 20636 el.playerId = this.options_.playerId; 20637 } 20638 if (typeof this.options_.preload !== 'undefined') { 20639 setAttribute(el, 'preload', this.options_.preload); 20640 } 20641 if (this.options_.disablePictureInPicture !== undefined) { 20642 el.disablePictureInPicture = this.options_.disablePictureInPicture; 20643 } 20644 20645 // Update specific tag settings, in case they were overridden 20646 // `autoplay` has to be *last* so that `muted` and `playsinline` are present
vendor: 11,693 bytes, lines 20647-20999
20647 // when iOS/Safari or other browsers attempt to autoplay. 20648 const settingsAttrs = ['loop', 'muted', 'playsinline', 'autoplay']; 20649 for (let i = 0; i < settingsAttrs.length; i++) { 20650 const attr = settingsAttrs[i]; 20651 const value = this.options_[attr]; 20652 if (typeof value !== 'undefined') { 20653 if (value) { 20654 setAttribute(el, attr, attr); 20655 } else { 20656 removeAttribute(el, attr); 20657 } 20658 el[attr] = value; 20659 } 20660 } 20661 return el; 20662 } 20663 20664 /** 20665 * This will be triggered if the loadstart event has already fired, before videojs was 20666 * ready. Two known examples of when this can happen are: 20667 * 1. If we're loading the playback object after it has started loading 20668 * 2. The media is already playing the (often with autoplay on) then 20669 * 20670 * This function will fire another loadstart so that videojs can catchup. 20671 * 20672 * @fires Tech#loadstart 20673 * 20674 * @return {undefined} 20675 * returns nothing. 20676 */ 20677 handleLateInit_(el) { 20678 if (el.networkState === 0 || el.networkState === 3) { 20679 // The video element hasn't started loading the source yet 20680 // or didn't find a source 20681 return; 20682 } 20683 if (el.readyState === 0) { 20684 // NetworkState is set synchronously BUT loadstart is fired at the 20685 // end of the current stack, usually before setInterval(fn, 0). 20686 // So at this point we know loadstart may have already fired or is 20687 // about to fire, and either way the player hasn't seen it yet. 20688 // We don't want to fire loadstart prematurely here and cause a 20689 // double loadstart so we'll wait and see if it happens between now 20690 // and the next loop, and fire it if not. 20691 // HOWEVER, we also want to make sure it fires before loadedmetadata 20692 // which could also happen between now and the next loop, so we'll 20693 // watch for that also. 20694 let loadstartFired = false; 20695 const setLoadstartFired = function () { 20696 loadstartFired = true; 20697 }; 20698 this.on('loadstart', setLoadstartFired); 20699 const triggerLoadstart = function () { 20700 // We did miss the original loadstart. Make sure the player 20701 // sees loadstart before loadedmetadata 20702 if (!loadstartFired) { 20703 this.trigger('loadstart'); 20704 } 20705 }; 20706 this.on('loadedmetadata', triggerLoadstart); 20707 this.ready(function () { 20708 this.off('loadstart', setLoadstartFired); 20709 this.off('loadedmetadata', triggerLoadstart); 20710 if (!loadstartFired) { 20711 // We did miss the original native loadstart. Fire it now. 20712 this.trigger('loadstart'); 20713 } 20714 }); 20715 return; 20716 } 20717 20718 // From here on we know that loadstart already fired and we missed it. 20719 // The other readyState events aren't as much of a problem if we double 20720 // them, so not going to go to as much trouble as loadstart to prevent 20721 // that unless we find reason to. 20722 const eventsToTrigger = ['loadstart']; 20723 20724 // loadedmetadata: newly equal to HAVE_METADATA (1) or greater 20725 eventsToTrigger.push('loadedmetadata'); 20726 20727 // loadeddata: newly increased to HAVE_CURRENT_DATA (2) or greater 20728 if (el.readyState >= 2) { 20729 eventsToTrigger.push('loadeddata'); 20730 } 20731 20732 // canplay: newly increased to HAVE_FUTURE_DATA (3) or greater 20733 if (el.readyState >= 3) { 20734 eventsToTrigger.push('canplay'); 20735 } 20736 20737 // canplaythrough: newly equal to HAVE_ENOUGH_DATA (4) 20738 if (el.readyState >= 4) { 20739 eventsToTrigger.push('canplaythrough'); 20740 } 20741 20742 // We still need to give the player time to add event listeners 20743 this.ready(function () { 20744 eventsToTrigger.forEach(function (type) { 20745 this.trigger(type); 20746 }, this); 20747 }); 20748 } 20749 20750 /** 20751 * Set whether we are scrubbing or not. 20752 * This is used to decide whether we should use `fastSeek` or not. 20753 * `fastSeek` is used to provide trick play on Safari browsers. 20754 * 20755 * @param {boolean} isScrubbing 20756 * - true for we are currently scrubbing 20757 * - false for we are no longer scrubbing 20758 */ 20759 setScrubbing(isScrubbing) { 20760 this.isScrubbing_ = isScrubbing; 20761 } 20762 20763 /** 20764 * Get whether we are scrubbing or not. 20765 * 20766 * @return {boolean} isScrubbing 20767 * - true for we are currently scrubbing 20768 * - false for we are no longer scrubbing 20769 */ 20770 scrubbing() { 20771 return this.isScrubbing_; 20772 } 20773 20774 /** 20775 * Set current time for the `HTML5` tech. 20776 * 20777 * @param {number} seconds 20778 * Set the current time of the media to this. 20779 */ 20780 setCurrentTime(seconds) { 20781 try { 20782 if (this.isScrubbing_ && this.el_.fastSeek && IS_ANY_SAFARI) { 20783 this.el_.fastSeek(seconds); 20784 } else { 20785 this.el_.currentTime = seconds; 20786 } 20787 } catch (e) { 20788 log$1(e, 'Video is not ready. (Video.js)'); 20789 // this.warning(VideoJS.warnings.videoNotReady); 20790 } 20791 } 20792 20793 /** 20794 * Get the current duration of the HTML5 media element. 20795 * 20796 * @return {number} 20797 * The duration of the media or 0 if there is no duration. 20798 */ 20799 duration() { 20800 // Android Chrome will report duration as Infinity for VOD HLS until after 20801 // playback has started, which triggers the live display erroneously. 20802 // Return NaN if playback has not started and trigger a durationupdate once 20803 // the duration can be reliably known. 20804 if (this.el_.duration === Infinity && IS_ANDROID && IS_CHROME && this.el_.currentTime === 0) { 20805 // Wait for the first `timeupdate` with currentTime > 0 - there may be 20806 // several with 0 20807 const checkProgress = () => { 20808 if (this.el_.currentTime > 0) { 20809 // Trigger durationchange for genuinely live video 20810 if (this.el_.duration === Infinity) { 20811 this.trigger('durationchange'); 20812 } 20813 this.off('timeupdate', checkProgress); 20814 } 20815 }; 20816 this.on('timeupdate', checkProgress); 20817 return NaN; 20818 } 20819 return this.el_.duration || NaN; 20820 } 20821 20822 /** 20823 * Get the current width of the HTML5 media element. 20824 * 20825 * @return {number} 20826 * The width of the HTML5 media element. 20827 */ 20828 width() { 20829 return this.el_.offsetWidth; 20830 } 20831 20832 /** 20833 * Get the current height of the HTML5 media element. 20834 * 20835 * @return {number} 20836 * The height of the HTML5 media element. 20837 */ 20838 height() { 20839 return this.el_.offsetHeight; 20840 } 20841 20842 /** 20843 * Proxy iOS `webkitbeginfullscreen` and `webkitendfullscreen` into 20844 * `fullscreenchange` event. 20845 * 20846 * @private 20847 * @fires fullscreenchange 20848 * @listens webkitendfullscreen 20849 * @listens webkitbeginfullscreen 20850 * @listens webkitbeginfullscreen 20851 */ 20852 proxyWebkitFullscreen_() { 20853 if (!('webkitDisplayingFullscreen' in this.el_)) { 20854 return; 20855 } 20856 const endFn = function () { 20857 this.trigger('fullscreenchange', { 20858 isFullscreen: false 20859 }); 20860 // Safari will sometimes set controls on the videoelement when existing fullscreen. 20861 if (this.el_.controls && !this.options_.nativeControlsForTouch && this.controls()) { 20862 this.el_.controls = false; 20863 } 20864 }; 20865 const beginFn = function () { 20866 if ('webkitPresentationMode' in this.el_ && this.el_.webkitPresentationMode !== 'picture-in-picture') { 20867 this.one('webkitendfullscreen', endFn); 20868 this.trigger('fullscreenchange', { 20869 isFullscreen: true, 20870 // set a flag in case another tech triggers fullscreenchange 20871 nativeIOSFullscreen: true 20872 }); 20873 } 20874 }; 20875 this.on('webkitbeginfullscreen', beginFn); 20876 this.on('dispose', () => { 20877 this.off('webkitbeginfullscreen', beginFn); 20878 this.off('webkitendfullscreen', endFn); 20879 }); 20880 } 20881 20882 /** 20883 * Check if fullscreen is supported on the video el. 20884 * 20885 * @return {boolean} 20886 * - True if fullscreen is supported. 20887 * - False if fullscreen is not supported. 20888 */ 20889 supportsFullScreen() { 20890 return typeof this.el_.webkitEnterFullScreen === 'function'; 20891 } 20892 20893 /** 20894 * Request that the `HTML5` Tech enter fullscreen. 20895 */ 20896 enterFullScreen() { 20897 const video = this.el_; 20898 if (video.paused && video.networkState <= video.HAVE_METADATA) { 20899 // attempt to prime the video element for programmatic access 20900 // this isn't necessary on the desktop but shouldn't hurt 20901 silencePromise(this.el_.play()); 20902 20903 // playing and pausing synchronously during the transition to fullscreen 20904 // can get iOS ~6.1 devices into a play/pause loop 20905 this.setTimeout(function () { 20906 video.pause(); 20907 try { 20908 video.webkitEnterFullScreen(); 20909 } catch (e) { 20910 this.trigger('fullscreenerror', e); 20911 } 20912 }, 0); 20913 } else { 20914 try { 20915 video.webkitEnterFullScreen(); 20916 } catch (e) { 20917 this.trigger('fullscreenerror', e); 20918 } 20919 } 20920 } 20921 20922 /** 20923 * Request that the `HTML5` Tech exit fullscreen. 20924 */ 20925 exitFullScreen() { 20926 if (!this.el_.webkitDisplayingFullscreen) { 20927 this.trigger('fullscreenerror', new Error('The video is not fullscreen')); 20928 return; 20929 } 20930 this.el_.webkitExitFullScreen(); 20931 } 20932 20933 /** 20934 * Create a floating video window always on top of other windows so that users may 20935 * continue consuming media while they interact with other content sites, or 20936 * applications on their device. 20937 * 20938 * @see [Spec]{@link https://wicg.github.io/picture-in-picture} 20939 * 20940 * @return {Promise} 20941 * A promise with a Picture-in-Picture window. 20942 */ 20943 requestPictureInPicture() { 20944 return this.el_.requestPictureInPicture(); 20945 } 20946 20947 /** 20948 * Native requestVideoFrameCallback if supported by browser/tech, or fallback 20949 * Don't use rVCF on Safari when DRM is playing, as it doesn't fire 20950 * Needs to be checked later than the constructor 20951 * This will be a false positive for clear sources loaded after a Fairplay source 20952 * 20953 * @param {function} cb function to call 20954 * @return {number} id of request 20955 */ 20956 requestVideoFrameCallback(cb) { 20957 if (this.featuresVideoFrameCallback && !this.el_.webkitKeys) { 20958 return this.el_.requestVideoFrameCallback(cb); 20959 } 20960 return super.requestVideoFrameCallback(cb); 20961 } 20962 20963 /** 20964 * Native or fallback requestVideoFrameCallback 20965 * 20966 * @param {number} id request id to cancel 20967 */ 20968 cancelVideoFrameCallback(id) { 20969 if (this.featuresVideoFrameCallback && !this.el_.webkitKeys) { 20970 this.el_.cancelVideoFrameCallback(id); 20971 } else { 20972 super.cancelVideoFrameCallback(id); 20973 } 20974 } 20975 20976 /** 20977 * A getter/setter for the `Html5` Tech's source object. 20978 * > Note: Please use {@link Html5#setSource} 20979 * 20980 * @param {Tech~SourceObject} [src] 20981 * The source object you want to set on the `HTML5` techs element. 20982 * 20983 * @return {Tech~SourceObject|undefined} 20984 * - The current source object when a source is not passed in. 20985 * - undefined when setting 20986 * 20987 * @deprecated Since version 5. 20988 */ 20989 src(src) { 20990 if (src === undefined) { 20991 return this.el_.src; 20992 } 20993 20994 // Setting src through `src` instead of `setSrc` will be deprecated 20995 this.setSrc(src); 20996 } 20997 20998 /** 20999 *
vendor: 11,056 bytes, lines 20999-21351
20999Reset the tech by removing all sources and then calling 21000 * {@link Html5.resetMediaElement}. 21001 */ 21002 reset() { 21003 Html5.resetMediaElement(this.el_); 21004 } 21005 21006 /** 21007 * Get the current source on the `HTML5` Tech. Falls back to returning the source from 21008 * the HTML5 media element. 21009 * 21010 * @return {Tech~SourceObject} 21011 * The current source object from the HTML5 tech. With a fallback to the 21012 * elements source. 21013 */ 21014 currentSrc() { 21015 if (this.currentSource_) { 21016 return this.currentSource_.src; 21017 } 21018 return this.el_.currentSrc; 21019 } 21020 21021 /** 21022 * Set controls attribute for the HTML5 media Element. 21023 * 21024 * @param {string} val 21025 * Value to set the controls attribute to 21026 */ 21027 setControls(val) { 21028 this.el_.controls = !!val; 21029 } 21030 21031 /** 21032 * Create and returns a remote {@link TextTrack} object. 21033 * 21034 * @param {string} kind 21035 * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata) 21036 * 21037 * @param {string} [label] 21038 * Label to identify the text track 21039 * 21040 * @param {string} [language] 21041 * Two letter language abbreviation 21042 * 21043 * @return {TextTrack} 21044 * The TextTrack that gets created. 21045 */ 21046 addTextTrack(kind, label, language) { 21047 if (!this.featuresNativeTextTracks) { 21048 return super.addTextTrack(kind, label, language); 21049 } 21050 return this.el_.addTextTrack(kind, label, language); 21051 } 21052 21053 /** 21054 * Creates either native TextTrack or an emulated TextTrack depending 21055 * on the value of `featuresNativeTextTracks` 21056 * 21057 * @param {Object} options 21058 * The object should contain the options to initialize the TextTrack with. 21059 * 21060 * @param {string} [options.kind] 21061 * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata). 21062 * 21063 * @param {string} [options.label] 21064 * Label to identify the text track 21065 * 21066 * @param {string} [options.language] 21067 * Two letter language abbreviation. 21068 * 21069 * @param {boolean} [options.default] 21070 * Default this track to on. 21071 * 21072 * @param {string} [options.id] 21073 * The internal id to assign this track. 21074 * 21075 * @param {string} [options.src] 21076 * A source url for the track. 21077 * 21078 * @return {HTMLTrackElement} 21079 * The track element that gets created. 21080 */ 21081 createRemoteTextTrack(options) { 21082 if (!this.featuresNativeTextTracks) { 21083 return super.createRemoteTextTrack(options); 21084 } 21085 const htmlTrackElement = document.createElement('track'); 21086 if (options.kind) { 21087 htmlTrackElement.kind = options.kind; 21088 } 21089 if (options.label) { 21090 htmlTrackElement.label = options.label; 21091 } 21092 if (options.language || options.srclang) { 21093 htmlTrackElement.srclang = options.language || options.srclang; 21094 } 21095 if (options.default) { 21096 htmlTrackElement.default = options.default; 21097 } 21098 if (options.id) { 21099 htmlTrackElement.id = options.id; 21100 } 21101 if (options.src) { 21102 htmlTrackElement.src = options.src; 21103 } 21104 return htmlTrackElement; 21105 } 21106 21107 /** 21108 * Creates a remote text track object and returns an html track element. 21109 * 21110 * @param {Object} options The object should contain values for 21111 * kind, language, label, and src (location of the WebVTT file) 21112 * @param {boolean} [manualCleanup=false] if set to true, the TextTrack 21113 * will not be removed from the TextTrackList and HtmlTrackElementList 21114 * after a source change 21115 * @return {HTMLTrackElement} An Html Track Element. 21116 * This can be an emulated {@link HTMLTrackElement} or a native one. 21117 * 21118 */ 21119 addRemoteTextTrack(options, manualCleanup) { 21120 const htmlTrackElement = super.addRemoteTextTrack(options, manualCleanup); 21121 if (this.featuresNativeTextTracks) { 21122 this.el().appendChild(htmlTrackElement); 21123 } 21124 return htmlTrackElement; 21125 } 21126 21127 /** 21128 * Remove remote `TextTrack` from `TextTrackList` object 21129 * 21130 * @param {TextTrack} track 21131 * `TextTrack` object to remove 21132 */ 21133 removeRemoteTextTrack(track) { 21134 super.removeRemoteTextTrack(track); 21135 if (this.featuresNativeTextTracks) { 21136 const tracks = this.$$('track'); 21137 let i = tracks.length; 21138 while (i--) { 21139 if (track === tracks[i] || track === tracks[i].track) { 21140 this.el().removeChild(tracks[i]); 21141 } 21142 } 21143 } 21144 } 21145 21146 /** 21147 * Gets available media playback quality metrics as specified by the W3C's Media 21148 * Playback Quality API. 21149 * 21150 * @see [Spec]{@link https://wicg.github.io/media-playback-quality} 21151 * 21152 * @return {Object} 21153 * An object with supported media playback quality metrics 21154 */ 21155 getVideoPlaybackQuality() { 21156 if (typeof this.el().getVideoPlaybackQuality === 'function') { 21157 return this.el().getVideoPlaybackQuality(); 21158 } 21159 const videoPlaybackQuality = {}; 21160 if (typeof this.el().webkitDroppedFrameCount !== 'undefined' && typeof this.el().webkitDecodedFrameCount !== 'undefined') { 21161 videoPlaybackQuality.droppedVideoFrames = this.el().webkitDroppedFrameCount; 21162 videoPlaybackQuality.totalVideoFrames = this.el().webkitDecodedFrameCount; 21163 } 21164 if (window.performance) { 21165 videoPlaybackQuality.creationTime = window.performance.now(); 21166 } 21167 return videoPlaybackQuality; 21168 } 21169 } 21170 21171 /* HTML5 Support Testing ---------------------------------------------------- */ 21172 21173 /** 21174 * Element for testing browser HTML5 media capabilities 21175 * 21176 * @type {Element} 21177 * @constant 21178 * @private 21179 */ 21180 defineLazyProperty(Html5, 'TEST_VID', function () { 21181 if (!isReal()) { 21182 return; 21183 } 21184 const video = document.createElement('video'); 21185 const track = document.createElement('track'); 21186 track.kind = 'captions'; 21187 track.srclang = 'en'; 21188 track.label = 'English'; 21189 video.appendChild(track); 21190 return video; 21191 }); 21192 21193 /** 21194 * Check if HTML5 media is supported by this browser/device. 21195 * 21196 * @return {boolean} 21197 * - True if HTML5 media is supported. 21198 * - False if HTML5 media is not supported. 21199 */ 21200 Html5.isSupported = function () { 21201 // IE with no Media Player is a LIAR! (#984) 21202 try { 21203 Html5.TEST_VID.volume = 0.5; 21204 } catch (e) { 21205 return false; 21206 } 21207 return !!(Html5.TEST_VID && Html5.TEST_VID.canPlayType); 21208 }; 21209 21210 /** 21211 * Check if the tech can support the given type 21212 * 21213 * @param {string} type 21214 * The mimetype to check 21215 * @return {string} 'probably', 'maybe', or '' (empty string) 21216 */ 21217 Html5.canPlayType = function (type) { 21218 return Html5.TEST_VID.canPlayType(type); 21219 }; 21220 21221 /** 21222 * Check if the tech can support the given source 21223 * 21224 * @param {Object} srcObj 21225 * The source object 21226 * @param {Object} options 21227 * The options passed to the tech 21228 * @return {string} 'probably', 'maybe', or '' (empty string) 21229 */ 21230 Html5.canPlaySource = function (srcObj, options) { 21231 return Html5.canPlayType(srcObj.type); 21232 }; 21233 21234 /** 21235 * Check if the volume can be changed in this browser/device. 21236 * Volume cannot be changed in a lot of mobile devices. 21237 * Specifically, it can't be changed from 1 on iOS. 21238 * 21239 * @return {boolean} 21240 * - True if volume can be controlled 21241 * - False otherwise 21242 */ 21243 Html5.canControlVolume = function () { 21244 // IE will error if Windows Media Player not installed #3315 21245 try { 21246 const volume = Html5.TEST_VID.volume; 21247 Html5.TEST_VID.volume = volume / 2 + 0.1; 21248 const canControl = volume !== Html5.TEST_VID.volume; 21249 21250 // With the introduction of iOS 15, there are cases where the volume is read as 21251 // changed but reverts back to its original state at the start of the next tick. 21252 // To determine whether volume can be controlled on iOS, 21253 // a timeout is set and the volume is checked asynchronously. 21254 // Since `features` doesn't currently work asynchronously, the value is manually set. 21255 if (canControl && IS_IOS) { 21256 window.setTimeout(() => { 21257 if (Html5 && Html5.prototype) { 21258 Html5.prototype.featuresVolumeControl = volume !== Html5.TEST_VID.volume; 21259 } 21260 }); 21261 21262 // default iOS to false, which will be updated in the timeout above. 21263 return false; 21264 } 21265 return canControl; 21266 } catch (e) { 21267 return false; 21268 } 21269 }; 21270 21271 /** 21272 * Check if the volume can be muted in this browser/device. 21273 * Some devices, e.g. iOS, don't allow changing volume 21274 * but permits muting/unmuting. 21275 * 21276 * @return {boolean} 21277 * - True if volume can be muted 21278 * - False otherwise 21279 */ 21280 Html5.canMuteVolume = function () { 21281 try { 21282 const muted = Html5.TEST_VID.muted; 21283 21284 // in some versions of iOS muted property doesn't always 21285 // work, so we want to set both property and attribute 21286 Html5.TEST_VID.muted = !muted; 21287 if (Html5.TEST_VID.muted) { 21288 setAttribute(Html5.TEST_VID, 'muted', 'muted'); 21289 } else { 21290 removeAttribute(Html5.TEST_VID, 'muted', 'muted'); 21291 } 21292 return muted !== Html5.TEST_VID.muted; 21293 } catch (e) { 21294 return false; 21295 } 21296 }; 21297 21298 /** 21299 * Check if the playback rate can be changed in this browser/device. 21300 * 21301 * @return {boolean} 21302 * - True if playback rate can be controlled 21303 * - False otherwise 21304 */ 21305 Html5.canControlPlaybackRate = function () { 21306 // Playback rate API is implemented in Android Chrome, but doesn't do anything 21307 // https://github.com/videojs/video.js/issues/3180 21308 if (IS_ANDROID && IS_CHROME && CHROME_VERSION < 58) { 21309 return false; 21310 } 21311 // IE will error if Windows Media Player not installed #3315 21312 try { 21313 const playbackRate = Html5.TEST_VID.playbackRate; 21314 Html5.TEST_VID.playbackRate = playbackRate / 2 + 0.1; 21315 return playbackRate !== Html5.TEST_VID.playbackRate; 21316 } catch (e) { 21317 return false; 21318 } 21319 }; 21320 21321 /** 21322 * Check if we can override a video/audio elements attributes, with 21323 * Object.defineProperty. 21324 * 21325 * @return {boolean} 21326 * - True if builtin attributes can be overridden 21327 * - False otherwise 21328 */ 21329 Html5.canOverrideAttributes = function () { 21330 // if we cannot overwrite the src/innerHTML property, there is no support 21331 // iOS 7 safari for instance cannot do this. 21332 try { 21333 const noop = () => {}; 21334 Object.defineProperty(document.createElement('video'), 'src', { 21335 get: noop, 21336 set: noop 21337 }); 21338 Object.defineProperty(document.createElement('audio'), 'src', { 21339 get: noop, 21340 set: noop 21341 }); 21342 Object.defineProperty(document.createElement('video'), 'innerHTML', { 21343 get: noop, 21344 set: noop 21345 }); 21346 Object.defineProperty(document.createElement('audio'), 'innerHTML', { 21347 get: noop, 21348 set: noop 21349 }); 21350 } catch (e) { 21351 return false;
vendor: 31,613 bytes, lines 21352-22221
21352 } 21353 return true; 21354 }; 21355 21356 /** 21357 * Check to see if native `TextTrack`s are supported by this browser/device. 21358 * 21359 * @return {boolean} 21360 * - True if native `TextTrack`s are supported. 21361 * - False otherwise 21362 */ 21363 Html5.supportsNativeTextTracks = function () { 21364 return IS_ANY_SAFARI || IS_IOS && IS_CHROME; 21365 }; 21366 21367 /** 21368 * Check to see if native `VideoTrack`s are supported by this browser/device 21369 * 21370 * @return {boolean} 21371 * - True if native `VideoTrack`s are supported. 21372 * - False otherwise 21373 */ 21374 Html5.supportsNativeVideoTracks = function () { 21375 return !!(Html5.TEST_VID && Html5.TEST_VID.videoTracks); 21376 }; 21377 21378 /** 21379 * Check to see if native `AudioTrack`s are supported by this browser/device 21380 * 21381 * @return {boolean} 21382 * - True if native `AudioTrack`s are supported. 21383 * - False otherwise 21384 */ 21385 Html5.supportsNativeAudioTracks = function () { 21386 return !!(Html5.TEST_VID && Html5.TEST_VID.audioTracks); 21387 }; 21388 21389 /** 21390 * An array of events available on the Html5 tech. 21391 * 21392 * @private 21393 * @type {Array} 21394 */ 21395 Html5.Events = ['loadstart', 'suspend', 'abort', 'error', 'emptied', 'stalled', 'loadedmetadata', 'loadeddata', 'canplay', 'canplaythrough', 'playing', 'waiting', 'seeking', 'seeked', 'ended', 'durationchange', 'timeupdate', 'progress', 'play', 'pause', 'ratechange', 'resize', 'volumechange']; 21396 21397 /** 21398 * Boolean indicating whether the `Tech` supports volume control. 21399 * 21400 * @type {boolean} 21401 * @default {@link Html5.canControlVolume} 21402 */ 21403 /** 21404 * Boolean indicating whether the `Tech` supports muting volume. 21405 * 21406 * @type {boolean} 21407 * @default {@link Html5.canMuteVolume} 21408 */ 21409 21410 /** 21411 * Boolean indicating whether the `Tech` supports changing the speed at which the media 21412 * plays. Examples: 21413 * - Set player to play 2x (twice) as fast 21414 * - Set player to play 0.5x (half) as fast 21415 * 21416 * @type {boolean} 21417 * @default {@link Html5.canControlPlaybackRate} 21418 */ 21419 21420 /** 21421 * Boolean indicating whether the `Tech` supports the `sourceset` event. 21422 * 21423 * @type {boolean} 21424 * @default 21425 */ 21426 /** 21427 * Boolean indicating whether the `HTML5` tech currently supports native `TextTrack`s. 21428 * 21429 * @type {boolean} 21430 * @default {@link Html5.supportsNativeTextTracks} 21431 */ 21432 /** 21433 * Boolean indicating whether the `HTML5` tech currently supports native `VideoTrack`s. 21434 * 21435 * @type {boolean} 21436 * @default {@link Html5.supportsNativeVideoTracks} 21437 */ 21438 /** 21439 * Boolean indicating whether the `HTML5` tech currently supports native `AudioTrack`s. 21440 * 21441 * @type {boolean} 21442 * @default {@link Html5.supportsNativeAudioTracks} 21443 */ 21444 [['featuresMuteControl', 'canMuteVolume'], ['featuresPlaybackRate', 'canControlPlaybackRate'], ['featuresSourceset', 'canOverrideAttributes'], ['featuresNativeTextTracks', 'supportsNativeTextTracks'], ['featuresNativeVideoTracks', 'supportsNativeVideoTracks'], ['featuresNativeAudioTracks', 'supportsNativeAudioTracks']].forEach(function ([key, fn]) { 21445 defineLazyProperty(Html5.prototype, key, () => Html5[fn](), true); 21446 }); 21447 Html5.prototype.featuresVolumeControl = Html5.canControlVolume(); 21448 21449 /** 21450 * Boolean indicating whether the `HTML5` tech currently supports the media element 21451 * moving in the DOM. iOS breaks if you move the media element, so this is set this to 21452 * false there. Everywhere else this should be true. 21453 * 21454 * @type {boolean} 21455 * @default 21456 */ 21457 Html5.prototype.movingMediaElementInDOM = !IS_IOS; 21458 21459 // TODO: Previous comment: No longer appears to be used. Can probably be removed. 21460 // Is this true? 21461 /** 21462 * Boolean indicating whether the `HTML5` tech currently supports automatic media resize 21463 * when going into fullscreen. 21464 * 21465 * @type {boolean} 21466 * @default 21467 */ 21468 Html5.prototype.featuresFullscreenResize = true; 21469 21470 /** 21471 * Boolean indicating whether the `HTML5` tech currently supports the progress event. 21472 * If this is false, manual `progress` events will be triggered instead. 21473 * 21474 * @type {boolean} 21475 * @default 21476 */ 21477 Html5.prototype.featuresProgressEvents = true; 21478 21479 /** 21480 * Boolean indicating whether the `HTML5` tech currently supports the timeupdate event. 21481 * If this is false, manual `timeupdate` events will be triggered instead. 21482 * 21483 * @default 21484 */ 21485 Html5.prototype.featuresTimeupdateEvents = true; 21486 21487 /** 21488 * Whether the HTML5 el supports `requestVideoFrameCallback` 21489 * 21490 * @type {boolean} 21491 */ 21492 Html5.prototype.featuresVideoFrameCallback = !!(Html5.TEST_VID && Html5.TEST_VID.requestVideoFrameCallback); 21493 Html5.disposeMediaElement = function (el) { 21494 if (!el) { 21495 return; 21496 } 21497 if (el.parentNode) { 21498 el.parentNode.removeChild(el); 21499 } 21500 21501 // remove any child track or source nodes to prevent their loading 21502 while (el.hasChildNodes()) { 21503 el.removeChild(el.firstChild); 21504 } 21505 21506 // remove any src reference. not setting `src=''` because that causes a warning 21507 // in firefox 21508 el.removeAttribute('src'); 21509 21510 // force the media element to update its loading state by calling load() 21511 // however IE on Windows 7N has a bug that throws an error so need a try/catch (#793) 21512 if (typeof el.load === 'function') { 21513 // wrapping in an iife so it's not deoptimized (#1060#discussion_r10324473) 21514 (function () { 21515 try { 21516 el.load(); 21517 } catch (e) { 21518 // not supported 21519 } 21520 })(); 21521 } 21522 }; 21523 Html5.resetMediaElement = function (el) { 21524 if (!el) { 21525 return; 21526 } 21527 const sources = el.querySelectorAll('source'); 21528 let i = sources.length; 21529 while (i--) { 21530 el.removeChild(sources[i]); 21531 } 21532 21533 // remove any src reference. 21534 // not setting `src=''` because that throws an error 21535 el.removeAttribute('src'); 21536 if (typeof el.load === 'function') { 21537 // wrapping in an iife so it's not deoptimized (#1060#discussion_r10324473) 21538 (function () { 21539 try { 21540 el.load(); 21541 } catch (e) { 21542 // satisfy linter 21543 } 21544 })(); 21545 } 21546 }; 21547 21548 /* Native HTML5 element property wrapping ----------------------------------- */ 21549 // Wrap native boolean attributes with getters that check both property and attribute 21550 // The list is as followed: 21551 // muted, defaultMuted, autoplay, controls, loop, playsinline 21552 [ 21553 /** 21554 * Get the value of `muted` from the media element. `muted` indicates 21555 * that the volume for the media should be set to silent. This does not actually change 21556 * the `volume` attribute. 21557 * 21558 * @method Html5#muted 21559 * @return {boolean} 21560 * - True if the value of `volume` should be ignored and the audio set to silent. 21561 * - False if the value of `volume` should be used. 21562 * 21563 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-muted} 21564 */ 21565 'muted', 21566 /** 21567 * Get the value of `defaultMuted` from the media element. `defaultMuted` indicates 21568 * whether the media should start muted or not. Only changes the default state of the 21569 * media. `muted` and `defaultMuted` can have different values. {@link Html5#muted} indicates the 21570 * current state. 21571 * 21572 * @method Html5#defaultMuted 21573 * @return {boolean} 21574 * - The value of `defaultMuted` from the media element. 21575 * - True indicates that the media should start muted. 21576 * - False indicates that the media should not start muted 21577 * 21578 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultmuted} 21579 */ 21580 'defaultMuted', 21581 /** 21582 * Get the value of `autoplay` from the media element. `autoplay` indicates 21583 * that the media should start to play as soon as the page is ready. 21584 * 21585 * @method Html5#autoplay 21586 * @return {boolean} 21587 * - The value of `autoplay` from the media element. 21588 * - True indicates that the media should start as soon as the page loads. 21589 * - False indicates that the media should not start as soon as the page loads. 21590 * 21591 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-autoplay} 21592 */ 21593 'autoplay', 21594 /** 21595 * Get the value of `controls` from the media element. `controls` indicates 21596 * whether the native media controls should be shown or hidden. 21597 * 21598 * @method Html5#controls 21599 * @return {boolean} 21600 * - The value of `controls` from the media element. 21601 * - True indicates that native controls should be showing. 21602 * - False indicates that native controls should be hidden. 21603 * 21604 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-controls} 21605 */ 21606 'controls', 21607 /** 21608 * Get the value of `loop` from the media element. `loop` indicates 21609 * that the media should return to the start of the media and continue playing once 21610 * it reaches the end. 21611 * 21612 * @method Html5#loop 21613 * @return {boolean} 21614 * - The value of `loop` from the media element. 21615 * - True indicates that playback should seek back to start once 21616 * the end of a media is reached. 21617 * - False indicates that playback should not loop back to the start when the 21618 * end of the media is reached. 21619 * 21620 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-loop} 21621 */ 21622 'loop', 21623 /** 21624 * Get the value of `playsinline` from the media element. `playsinline` indicates 21625 * to the browser that non-fullscreen playback is preferred when fullscreen 21626 * playback is the native default, such as in iOS Safari. 21627 * 21628 * @method Html5#playsinline 21629 * @return {boolean} 21630 * - The value of `playsinline` from the media element. 21631 * - True indicates that the media should play inline. 21632 * - False indicates that the media should not play inline. 21633 * 21634 * @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline} 21635 */ 21636 'playsinline'].forEach(function (prop) { 21637 Html5.prototype[prop] = function () { 21638 return this.el_[prop] || this.el_.hasAttribute(prop); 21639 }; 21640 }); 21641 21642 // Wrap native boolean attributes with setters that set both property and attribute 21643 // The list is as followed: 21644 // setMuted, setDefaultMuted, setAutoplay, setLoop, setPlaysinline 21645 // setControls is special-cased above 21646 [ 21647 /** 21648 * Set the value of `muted` on the media element. `muted` indicates that the current 21649 * audio level should be silent. 21650 * 21651 * @method Html5#setMuted 21652 * @param {boolean} muted 21653 * - True if the audio should be set to silent 21654 * - False otherwise 21655 * 21656 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-muted} 21657 */ 21658 'muted', 21659 /** 21660 * Set the value of `defaultMuted` on the media element. `defaultMuted` indicates that the current 21661 * audio level should be silent, but will only effect the muted level on initial playback.. 21662 * 21663 * @method Html5.prototype.setDefaultMuted 21664 * @param {boolean} defaultMuted 21665 * - True if the audio should be set to silent 21666 * - False otherwise 21667 * 21668 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultmuted} 21669 */ 21670 'defaultMuted', 21671 /** 21672 * Set the value of `autoplay` on the media element. `autoplay` indicates 21673 * that the media should start to play as soon as the page is ready. 21674 * 21675 * @method Html5#setAutoplay 21676 * @param {boolean} autoplay 21677 * - True indicates that the media should start as soon as the page loads. 21678 * - False indicates that the media should not start as soon as the page loads. 21679 * 21680 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-autoplay} 21681 */ 21682 'autoplay', 21683 /** 21684 * Set the value of `loop` on the media element. `loop` indicates 21685 * that the media should return to the start of the media and continue playing once 21686 * it reaches the end. 21687 * 21688 * @method Html5#setLoop 21689 * @param {boolean} loop 21690 * - True indicates that playback should seek back to start once 21691 * the end of a media is reached. 21692 * - False indicates that playback should not loop back to the start when the 21693 * end of the media is reached. 21694 * 21695 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-loop} 21696 */ 21697 'loop', 21698 /** 21699 * Set the value of `playsinline` from the media element. `playsinline` indicates 21700 * to the browser that non-fullscreen playback is preferred when fullscreen 21701 * playback is the native default, such as in iOS Safari. 21702 * 21703 * @method Html5#setPlaysinline 21704 * @param {boolean} playsinline 21705 * - True indicates that the media should play inline. 21706 * - False indicates that the media should not play inline. 21707 * 21708 * @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline} 21709 */ 21710 'playsinline'].forEach(function (prop) { 21711 Html5.prototype['set' + toTitleCase$1(prop)] = function (v) { 21712 this.el_[prop] = v; 21713 if (v) { 21714 this.el_.setAttribute(prop, prop); 21715 } else { 21716 this.el_.removeAttribute(prop); 21717 } 21718 }; 21719 }); 21720 21721 // Wrap native properties with a getter 21722 // The list is as followed 21723 // paused, currentTime, buffered, volume, poster, preload, error, seeking 21724 // seekable, ended, playbackRate, defaultPlaybackRate, disablePictureInPicture 21725 // played, networkState, readyState, videoWidth, videoHeight, crossOrigin 21726 [ 21727 /** 21728 * Get the value of `paused` from the media element. `paused` indicates whether the media element 21729 * is currently paused or not. 21730 * 21731 * @method Html5#paused 21732 * @return {boolean} 21733 * The value of `paused` from the media element. 21734 * 21735 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-paused} 21736 */ 21737 'paused', 21738 /** 21739 * Get the value of `currentTime` from the media element. `currentTime` indicates 21740 * the current second that the media is at in playback. 21741 * 21742 * @method Html5#currentTime 21743 * @return {number} 21744 * The value of `currentTime` from the media element. 21745 * 21746 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-currenttime} 21747 */ 21748 'currentTime', 21749 /** 21750 * Get the value of `buffered` from the media element. `buffered` is a `TimeRange` 21751 * object that represents the parts of the media that are already downloaded and 21752 * available for playback. 21753 * 21754 * @method Html5#buffered 21755 * @return {TimeRange} 21756 * The value of `buffered` from the media element. 21757 * 21758 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-buffered} 21759 */ 21760 'buffered', 21761 /** 21762 * Get the value of `volume` from the media element. `volume` indicates 21763 * the current playback volume of audio for a media. `volume` will be a value from 0 21764 * (silent) to 1 (loudest and default). 21765 * 21766 * @method Html5#volume 21767 * @return {number} 21768 * The value of `volume` from the media element. Value will be between 0-1. 21769 * 21770 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-a-volume} 21771 */ 21772 'volume', 21773 /** 21774 * Get the value of `poster` from the media element. `poster` indicates 21775 * that the url of an image file that can/will be shown when no media data is available. 21776 * 21777 * @method Html5#poster 21778 * @return {string} 21779 * The value of `poster` from the media element. Value will be a url to an 21780 * image. 21781 * 21782 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-video-poster} 21783 */ 21784 'poster', 21785 /** 21786 * Get the value of `preload` from the media element. `preload` indicates 21787 * what should download before the media is interacted with. It can have the following 21788 * values: 21789 * - none: nothing should be downloaded 21790 * - metadata: poster and the first few frames of the media may be downloaded to get 21791 * media dimensions and other metadata 21792 * - auto: allow the media and metadata for the media to be downloaded before 21793 * interaction 21794 * 21795 * @method Html5#preload 21796 * @return {string} 21797 * The value of `preload` from the media element. Will be 'none', 'metadata', 21798 * or 'auto'. 21799 * 21800 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-preload} 21801 */ 21802 'preload', 21803 /** 21804 * Get the value of the `error` from the media element. `error` indicates any 21805 * MediaError that may have occurred during playback. If error returns null there is no 21806 * current error. 21807 * 21808 * @method Html5#error 21809 * @return {MediaError|null} 21810 * The value of `error` from the media element. Will be `MediaError` if there 21811 * is a current error and null otherwise. 21812 * 21813 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-error} 21814 */ 21815 'error', 21816 /** 21817 * Get the value of `seeking` from the media element. `seeking` indicates whether the 21818 * media is currently seeking to a new position or not. 21819 * 21820 * @method Html5#seeking 21821 * @return {boolean} 21822 * - The value of `seeking` from the media element. 21823 * - True indicates that the media is currently seeking to a new position. 21824 * - False indicates that the media is not seeking to a new position at this time. 21825 * 21826 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-seeking} 21827 */ 21828 'seeking', 21829 /** 21830 * Get the value of `seekable` from the media element. `seekable` returns a 21831 * `TimeRange` object indicating ranges of time that can currently be `seeked` to. 21832 * 21833 * @method Html5#seekable 21834 * @return {TimeRange} 21835 * The value of `seekable` from the media element. A `TimeRange` object 21836 * indicating the current ranges of time that can be seeked to. 21837 * 21838 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-seekable} 21839 */ 21840 'seekable', 21841 /** 21842 * Get the value of `ended` from the media element. `ended` indicates whether 21843 * the media has reached the end or not. 21844 * 21845 * @method Html5#ended 21846 * @return {boolean} 21847 * - The value of `ended` from the media element. 21848 * - True indicates that the media has ended. 21849 * - False indicates that the media has not ended. 21850 * 21851 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-ended} 21852 */ 21853 'ended', 21854 /** 21855 * Get the value of `playbackRate` from the media element. `playbackRate` indicates 21856 * the rate at which the media is currently playing back. Examples: 21857 * - if playbackRate is set to 2, media will play twice as fast. 21858 * - if playbackRate is set to 0.5, media will play half as fast. 21859 * 21860 * @method Html5#playbackRate 21861 * @return {number} 21862 * The value of `playbackRate` from the media element. A number indicating 21863 * the current playback speed of the media, where 1 is normal speed. 21864 * 21865 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate} 21866 */ 21867 'playbackRate', 21868 /** 21869 * Get the value of `defaultPlaybackRate` from the media element. `defaultPlaybackRate` indicates 21870 * the rate at which the media is currently playing back. This value will not indicate the current 21871 * `playbackRate` after playback has started, use {@link Html5#playbackRate} for that. 21872 * 21873 * Examples: 21874 * - if defaultPlaybackRate is set to 2, media will play twice as fast. 21875 * - if defaultPlaybackRate is set to 0.5, media will play half as fast. 21876 * 21877 * @method Html5.prototype.defaultPlaybackRate 21878 * @return {number} 21879 * The value of `defaultPlaybackRate` from the media element. A number indicating 21880 * the current playback speed of the media, where 1 is normal speed. 21881 * 21882 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate} 21883 */ 21884 'defaultPlaybackRate', 21885 /** 21886 * Get the value of 'disablePictureInPicture' from the video element. 21887 * 21888 * @method Html5#disablePictureInPicture 21889 * @return {boolean} value 21890 * - The value of `disablePictureInPicture` from the video element. 21891 * - True indicates that the video can't be played in Picture-In-Picture mode 21892 * - False indicates that the video can be played in Picture-In-Picture mode 21893 * 21894 * @see [Spec]{@link https://w3c.github.io/picture-in-picture/#disable-pip} 21895 */ 21896 'disablePictureInPicture', 21897 /** 21898 * Get the value of `played` from the media element. `played` returns a `TimeRange` 21899 * object representing points in the media timeline that have been played. 21900 * 21901 * @method Html5#played 21902 * @return {TimeRange} 21903 * The value of `played` from the media element. A `TimeRange` object indicating 21904 * the ranges of time that have been played. 21905 * 21906 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-played} 21907 */ 21908 'played', 21909 /** 21910 * Get the value of `networkState` from the media element. `networkState` indicates 21911 * the current network state. It returns an enumeration from the following list: 21912 * - 0: NETWORK_EMPTY 21913 * - 1: NETWORK_IDLE 21914 * - 2: NETWORK_LOADING 21915 * - 3: NETWORK_NO_SOURCE 21916 * 21917 * @method Html5#networkState 21918 * @return {number} 21919 * The value of `networkState` from the media element. This will be a number 21920 * from the list in the description. 21921 * 21922 * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-networkstate} 21923 */ 21924 'networkState', 21925 /** 21926 * Get the value of `readyState` from the media element. `readyState` indicates 21927 * the current state of the media element. It returns an enumeration from the 21928 * following list: 21929 * - 0: HAVE_NOTHING 21930 * - 1: HAVE_METADATA 21931 * - 2: HAVE_CURRENT_DATA 21932 * - 3: HAVE_FUTURE_DATA 21933 * - 4: HAVE_ENOUGH_DATA 21934 * 21935 * @method Html5#readyState 21936 * @return {number} 21937 * The value of `readyState` from the media element. This will be a number 21938 * from the list in the description. 21939 * 21940 * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#ready-states} 21941 */ 21942 'readyState', 21943 /** 21944 * Get the value of `videoWidth` from the video element. `videoWidth` indicates 21945 * the current width of the video in css pixels. 21946 * 21947 * @method Html5#videoWidth 21948 * @return {number} 21949 * The value of `videoWidth` from the video element. This will be a number 21950 * in css pixels. 21951 * 21952 * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-video-videowidth} 21953 */ 21954 'videoWidth', 21955 /** 21956 * Get the value of `videoHeight` from the video element. `videoHeight` indicates 21957 * the current height of the video in css pixels. 21958 * 21959 * @method Html5#videoHeight 21960 * @return {number} 21961 * The value of `videoHeight` from the video element. This will be a number 21962 * in css pixels. 21963 * 21964 * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-video-videowidth} 21965 */ 21966 'videoHeight', 21967 /** 21968 * Get the value of `crossOrigin` from the media element. `crossOrigin` indicates 21969 * to the browser that should sent the cookies along with the requests for the 21970 * different assets/playlists 21971 * 21972 * @method Html5#crossOrigin 21973 * @return {string} 21974 * - anonymous indicates that the media should not sent cookies. 21975 * - use-credentials indicates that the media should sent cookies along the requests. 21976 * 21977 * @see [Spec]{@link https://html.spec.whatwg.org/#attr-media-crossorigin} 21978 */ 21979 'crossOrigin'].forEach(function (prop) { 21980 Html5.prototype[prop] = function () { 21981 return this.el_[prop]; 21982 }; 21983 }); 21984 21985 // Wrap native properties with a setter in this format: 21986 // set + toTitleCase(name) 21987 // The list is as follows: 21988 // setVolume, setSrc, setPoster, setPreload, setPlaybackRate, setDefaultPlaybackRate, 21989 // setDisablePictureInPicture, setCrossOrigin 21990 [ 21991 /** 21992 * Set the value of `volume` on the media element. `volume` indicates the current 21993 * audio level as a percentage in decimal form. This means that 1 is 100%, 0.5 is 50%, and 21994 * so on. 21995 * 21996 * @method Html5#setVolume 21997 * @param {number} percentAsDecimal 21998 * The volume percent as a decimal. Valid range is from 0-1. 21999 * 22000 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-a-volume} 22001 */ 22002 'volume', 22003 /** 22004 * Set the value of `src` on the media element. `src` indicates the current 22005 * {@link Tech~SourceObject} for the media. 22006 * 22007 * @method Html5#setSrc 22008 * @param {Tech~SourceObject} src 22009 * The source object to set as the current source. 22010 * 22011 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-src} 22012 */ 22013 'src', 22014 /** 22015 * Set the value of `poster` on the media element. `poster` is the url to 22016 * an image file that can/will be shown when no media data is available. 22017 * 22018 * @method Html5#setPoster 22019 * @param {string} poster 22020 * The url to an image that should be used as the `poster` for the media 22021 * element. 22022 * 22023 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-poster} 22024 */ 22025 'poster', 22026 /** 22027 * Set the value of `preload` on the media element. `preload` indicates 22028 * what should download before the media is interacted with. It can have the following 22029 * values: 22030 * - none: nothing should be downloaded 22031 * - metadata: poster and the first few frames of the media may be downloaded to get 22032 * media dimensions and other metadata 22033 * - auto: allow the media and metadata for the media to be downloaded before 22034 * interaction 22035 * 22036 * @method Html5#setPreload 22037 * @param {string} preload 22038 * The value of `preload` to set on the media element. Must be 'none', 'metadata', 22039 * or 'auto'. 22040 * 22041 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-preload} 22042 */ 22043 'preload', 22044 /** 22045 * Set the value of `playbackRate` on the media element. `playbackRate` indicates 22046 * the rate at which the media should play back. Examples: 22047 * - if playbackRate is set to 2, media will play twice as fast. 22048 * - if playbackRate is set to 0.5, media will play half as fast. 22049 * 22050 * @method Html5#setPlaybackRate 22051 * @return {number} 22052 * The value of `playbackRate` from the media element. A number indicating 22053 * the current playback speed of the media, where 1 is normal speed. 22054 * 22055 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate} 22056 */ 22057 'playbackRate', 22058 /** 22059 * Set the value of `defaultPlaybackRate` on the media element. `defaultPlaybackRate` indicates 22060 * the rate at which the media should play back upon initial startup. Changing this value 22061 * after a video has started will do nothing. Instead you should used {@link Html5#setPlaybackRate}. 22062 * 22063 * Example Values: 22064 * - if playbackRate is set to 2, media will play twice as fast. 22065 * - if playbackRate is set to 0.5, media will play half as fast. 22066 * 22067 * @method Html5.prototype.setDefaultPlaybackRate 22068 * @return {number} 22069 * The value of `defaultPlaybackRate` from the media element. A number indicating 22070 * the current playback speed of the media, where 1 is normal speed. 22071 * 22072 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultplaybackrate} 22073 */ 22074 'defaultPlaybackRate', 22075 /** 22076 * Prevents the browser from suggesting a Picture-in-Picture context menu 22077 * or to request Picture-in-Picture automatically in some cases. 22078 * 22079 * @method Html5#setDisablePictureInPicture 22080 * @param {boolean} value 22081 * The true value will disable Picture-in-Picture mode. 22082 * 22083 * @see [Spec]{@link https://w3c.github.io/picture-in-picture/#disable-pip} 22084 */ 22085 'disablePictureInPicture', 22086 /** 22087 * Set the value of `crossOrigin` from the media element. `crossOrigin` indicates 22088 * to the browser that should sent the cookies along with the requests for the 22089 * different assets/playlists 22090 * 22091 * @method Html5#setCrossOrigin 22092 * @param {string} crossOrigin 22093 * - anonymous indicates that the media should not sent cookies. 22094 * - use-credentials indicates that the media should sent cookies along the requests. 22095 * 22096 * @see [Spec]{@link https://html.spec.whatwg.org/#attr-media-crossorigin} 22097 */ 22098 'crossOrigin'].forEach(function (prop) { 22099 Html5.prototype['set' + toTitleCase$1(prop)] = function (v) { 22100 this.el_[prop] = v; 22101 }; 22102 }); 22103 22104 // wrap native functions with a function 22105 // The list is as follows: 22106 // pause, load, play 22107 [ 22108 /** 22109 * A wrapper around the media elements `pause` function. This will call the `HTML5` 22110 * media elements `pause` function. 22111 * 22112 * @method Html5#pause 22113 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-pause} 22114 */ 22115 'pause', 22116 /** 22117 * A wrapper around the media elements `load` function. This will call the `HTML5`s 22118 * media element `load` function. 22119 * 22120 * @method Html5#load 22121 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-load} 22122 */ 22123 'load', 22124 /** 22125 * A wrapper around the media elements `play` function. This will call the `HTML5`s 22126 * media element `play` function. 22127 * 22128 * @method Html5#play 22129 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-play} 22130 */ 22131 'play'].forEach(function (prop) { 22132 Html5.prototype[prop] = function () { 22133 return this.el_[prop](); 22134 }; 22135 }); 22136 Tech.withSourceHandlers(Html5); 22137 22138 /** 22139 * Native source handler for Html5, simply passes the source to the media element. 22140 * 22141 * @property {Tech~SourceObject} source 22142 * The source object 22143 * 22144 * @property {Html5} tech 22145 * The instance of the HTML5 tech. 22146 */ 22147 Html5.nativeSourceHandler = {}; 22148 22149 /** 22150 * Check if the media element can play the given mime type. 22151 * 22152 * @param {string} type 22153 * The mimetype to check 22154 * 22155 * @return {string} 22156 * 'probably', 'maybe', or '' (empty string) 22157 */ 22158 Html5.nativeSourceHandler.canPlayType = function (type) { 22159 // IE without MediaPlayer throws an error (#519) 22160 try { 22161 return Html5.TEST_VID.canPlayType(type); 22162 } catch (e) { 22163 return ''; 22164 } 22165 }; 22166 22167 /** 22168 * Check if the media element can handle a source natively. 22169 * 22170 * @param {Tech~SourceObject} source 22171 * The source object 22172 * 22173 * @param {Object} [options] 22174 * Options to be passed to the tech. 22175 * 22176 * @return {string} 22177 * 'probably', 'maybe', or '' (empty string). 22178 */ 22179 Html5.nativeSourceHandler.canHandleSource = function (source, options) { 22180 // If a type was provided we should rely on that 22181 if (source.type) { 22182 return Html5.nativeSourceHandler.canPlayType(source.type); 22183 22184 // If no type, fall back to checking 'video/[EXTENSION]' 22185 } else if (source.src) { 22186 const ext = getFileExtension(source.src); 22187 return Html5.nativeSourceHandler.canPlayType(`video/${ext}`); 22188 } 22189 return ''; 22190 }; 22191 22192 /** 22193 * Pass the source to the native media element. 22194 * 22195 * @param {Tech~SourceObject} source 22196 * The source object 22197 * 22198 * @param {Html5} tech 22199 * The instance of the Html5 tech 22200 * 22201 * @param {Object} [options] 22202 * The options to pass to the source 22203 */ 22204 Html5.nativeSourceHandler.handleSource = function (source, tech, options) { 22205 tech.setSrc(source.src); 22206 }; 22207 22208 /** 22209 * A noop for the native dispose function, as cleanup is not needed. 22210 */ 22211 Html5.nativeSourceHandler.dispose = function () {}; 22212 22213 // Register the native source handler 22214 Html5.registerSourceHandler(Html5.nativeSourceHandler); 22215 Tech.registerTech('Html5', Html5); 22216 22217 /** 22218 * @file player.js 22219 */ 22220 22221 /
22221/ The following tech events are simply re-triggered 22222 // on the player when they happen 22223 const TECH_EVENTS_RETRIGGER = [ 22224 /** 22225 * Fired while the user agent is downloading media data. 22226 * 22227 * @event Player#progress 22228 * @type {Event} 22229 */ 22230 /** 22231 * Retrigger the `progress` event that was triggered by the {@link Tech}. 22232 * 22233 * @private 22234 * @method Player#handleTechProgress_ 22235 * @fires Player#progress 22236 * @listens Tech#progress 22237 */ 22238 'progress', 22239 /** 22240 * Fires when the loading of an audio/video is aborted. 22241 * 22242 * @event Player#abort 22243 * @type {Event} 22244 */ 22245 /** 22246 * Retrigger the `abort` event that was triggered by the {@link Tech}. 22247 * 22248 * @private 22249 * @method Player#handleTechAbort_ 22250 * @fires Player#abort 22251 * @listens Tech#abort 22252 */ 22253 'abort', 22254 /** 22255 * Fires when the browser is intentionally not getting media data. 22256 * 22257 * @event Player#suspend 22258 * @type {Event} 22259 */ 22260 /** 22261 * Retrigger the `suspend` event that was triggered by the {@link Tech}. 22262 * 22263 * @private 22264 * @method Player#handleTechSuspend_ 22265 * @fires Player#suspend 22266 * @listens Tech#suspend 22267 */ 22268 'suspend', 22269 /** 22270 * Fires when the current playlist is empty. 22271 * 22272 * @event Player#emptied 22273 * @type {Event} 22274 */ 22275 /** 22276 * Retrigger the `emptied` event that was triggered by the {@link Tech}. 22277 * 22278 * @private 22279 * @method Player#handleTechEmptied_ 22280 * @fires Player#emptied 22281 * @listens Tech#emptied 22282 */ 22283 'emptied', 22284 /** 22285 * Fires when the browser is trying to get media data, but data is not available. 22286 * 22287 * @event Player#stalled 22288 * @type {Event} 22289 */ 22290 /** 22291 * Retrigger the `stalled` event that was triggered by the {@link Tech}. 22292 * 22293 * @private 22294 * @method Player#handleTechStalled_ 22295 * @fires Player#stalled 22296 * @listens Tech#stalled 22297 */ 22298 'stalled', 22299 /** 22300 * Fires when the browser has loaded meta data for the audio/video. 22301 * 22302 * @event Player#loadedmetadata 22303 * @type {Event} 22304 */ 22305 /** 22306 * Retrigger the `loadedmetadata` event that was triggered by the {@link Tech}. 22307 * 22308 * @private 22309 * @method Player#handleTechLoadedmetadata_ 22310 * @fires Player#loadedmetadata 22311 * @listens Tech#loadedmetadata 22312 */ 22313 'loadedmetadata', 22314 /** 22315 * Fires when the browser has loaded the current frame of the audio/video. 22316 * 22317 * @event Player#loadeddata 22318 * @type {event} 22319 */ 22320 /** 22321 * Retrigger the `loadeddata` event that was triggered by the {@link Tech}. 22322 * 22323 * @private 22324 * @method Player#handleTechLoaddeddata_ 22325 * @fires Player#loadeddata 22326 * @listens Tech#loadeddata 22327 */ 22328 'loadeddata', 22329 /** 22330 * Fires when the current playback position has changed. 22331 * 22332 * @event Player#timeupdate 22333 * @type {event} 22334 */ 22335 /** 22336 * Retrigger the `timeupdate` event that was triggered by the {@link Tech}. 22337 * 22338 * @private 22339 * @method Player#handleTechTimeUpdate_ 22340 * @fires Player#timeupdate 22341 * @listens Tech#timeupdate 22342 */ 22343 'timeupdate', 22344 /** 22345 * Fires when the video's intrinsic dimensions change 22346 * 22347 * @event Player#resize 22348 * @type {event} 22349 */ 22350 /** 22351 * Retrigger the `resize` event that was triggered by the {@link Tech}. 22352 * 22353 * @private 22354 * @method Player#handleTechResize_ 22355 * @fires Player#resize 22356 * @listens Tech#resize 22357 */ 22358 'resize', 22359 /** 22360 * Fires when the volume has been changed 22361 * 22362 * @event Player#volumechange 22363 * @type {event} 22364 */ 22365 /** 22366 * Retrigger the `volumechange` event that was triggered by the {@link Tech}. 22367 * 22368 * @private 22369 * @method Player#handleTechVolumechange_ 22370 * @fires Player#volumechange 22371 * @listens Tech#volumechange 22372 */ 22373 'volumechange', 22374 /** 22375 * Fires when the text track has been changed 22376 * 22377 * @event Player#texttrackchange 22378 * @type {event} 22379 */ 22380 /** 22381 * Retrigger the `texttrackchange` event that was triggered by the {@link Tech}. 22382 * 22383 * @private 22384 * @method Player#handleTechTexttrackchange_ 22385 * @fires Player#texttrackchange 22386 * @listens Tech#texttrackchange 22387 */ 22388 'texttrackchange']; 22389 22390 // events to queue when playback rate is zero 22391 // this is a hash for the sole purpose of mapping non-camel-cased event names 22392 // to camel-cased function names 22393 const TECH_EVENTS_QUEUE = { 22394 canplay: 'CanPlay', 22395 canplaythrough: 'CanPlayThrough', 22396 playing: 'Playing', 22397 seeked: 'Seeked' 22398 }; 22399 const BREAKPOINT_ORDER = ['tiny', 'xsmall', 'small', 'medium', 'large', 'xlarge', 'huge']; 22400 const BREAKPOINT_CLASSES = {}; 22401 22402 // grep: vjs-layout-tiny 22403 // grep: vjs-layout-x-small 22404 // grep: vjs-layout-small 22405 // grep: vjs-layout-medium 22406 // grep: vjs-layout-large 22407 // grep: vjs-layout-x-large 22408 // grep: vjs-layout-huge
22409 BREAKPOINT_ORDER.forEach(k => { 22410 const v = k.charAt(0) === 'x' ? `x-${k.substring(1)}` : k; 22411 BREAKPOINT_CLASSES[k] = `vjs-layout-${v}`; 22412 }); 22413 const DEFAULT_BREAKPOINTS = { 22414 tiny: 210, 22415 xsmall: 320, 22416 small: 425, 22417 medium: 768, 22418 large: 1440, 22419 xlarge: 2560, 22420 huge: Infinity 22421 }; 22422 22423 /** 22424 * An instance of the `Player` class is created when any of the Video.js setup methods 22425 * are used to initialize a video. 22426 * 22427 * After an instance has been created it can be accessed globally in three ways: 22428 * 1. By calling `videojs.getPlayer('example_video_1');` 22429 * 2. By calling `videojs('example_video_1');` (not recommended) 22430 * 2. By using it directly via `videojs.players.example_video_1;` 22431 * 22432 * @extends Component 22433 * @global 22434 */ 22435 class Player extends Component$1 { 22436 /** 22437 * Create an instance of this class. 22438 * 22439 * @param {Element} tag 22440 * The original video DOM element used for configuring options. 22441 * 22442 * @param {Object} [options] 22443 * Object of option names and values. 22444 * 22445 * @param {Function} [ready] 22446 * Ready callback function. 22447 */ 22448 constructor(tag, options, ready) { 22449 // Make sure tag ID exists 22450 // also here.. probably better 22451 tag.id = tag.id || options.id || `vjs_video_${newGUID()}`; 22452 22453 // Set Options 22454 // The options argument overrides options set in the video tag 22455 // which overrides globally set options. 22456 // This latter part coincides with the load order 22457 // (tag must exist before Player) 22458 options = Object.assign(Player.getTagSettings(tag), options); 22459 22460 // Delay the initialization of children because we need to set up 22461 // player properties first, and can't use `this` before `super()` 22462 options.initChildren = false; 22463 22464 // Same with creating the element 22465 options.createEl = false;
22466 22467 // don't auto mixin the evented mixin 22468 options.evented = false; 22469 22470 // we don't want the player to report touch activity on itself 22471 // see enableTouchActivity in Component 22472 options.reportTouchActivity = false; 22473 22474 // If language is not set, get the closest lang attribute 22475 if (!options.language) { 22476 const closest = tag.closest('[lang]'); 22477 if (closest) { 22478 options.language = closest.getAttribute('lang'); 22479 } 22480 } 22481 22482 // Run base component initializing with new options 22483 super(null, options, ready); 22484 22485 // Create bound methods for document listeners. 22486 this.boundDocumentFullscreenChange_ = e => this.documentFullscreenChange_(e); 22487 this.boundFullWindowOnEscKey_ = e => this.fullWindowOnEscKey(e); 22488 this.boundUpdateStyleEl_ = e => this.updateStyleEl_(e); 22489 this.boundApplyInitTime_ = e => this.applyInitTime_(e); 22490 this.boundUpdateCurrentBreakpoint_ = e => this.updateCurrentBreakpoint_(e); 22491 this.boundHandleTechClick_ = e => this.handleTechClick_(e); 22492 this.boundHandleTechDoubleClick_ = e => this.handleTechDoubleClick_(e); 22493 this.boundHandleTechTouchStart_ = e => this.handleTechTouchStart_(e); 22494 this.boundHandleTechTouchMove_ = e => this.handleTechTouchMove_(e); 22495 this.boundHandleTechTouchEnd_ = e => this.handleTechTouchEnd_(e); 22496 this.boundHandleTechTap_ = e => this.handleTechTap_(e); 22497 22498 // default isFullscreen_ to false 22499 this.isFullscreen_ = false;
vendor: 13,558 bytes, lines 22500-22881
22500 22501 // create logger 22502 this.log = createLogger(this.id_); 22503 22504 // Hold our own reference to fullscreen api so it can be mocked in tests 22505 this.fsApi_ = FullscreenApi; 22506 22507 // Tracks when a tech changes the poster 22508 this.isPosterFromTech_ = false; 22509 22510 // Holds callback info that gets queued when playback rate is zero 22511 // and a seek is happening 22512 this.queuedCallbacks_ = []; 22513 22514 // Turn off API access because we're loading a new tech that might load asynchronously 22515 this.isReady_ = false; 22516 22517 // Init state hasStarted_ 22518 this.hasStarted_ = false; 22519 22520 // Init state userActive_ 22521 this.userActive_ = false; 22522 22523 // Init debugEnabled_ 22524 this.debugEnabled_ = false; 22525 22526 // Init state audioOnlyMode_ 22527 this.audioOnlyMode_ = false; 22528 22529 // Init state audioPosterMode_ 22530 this.audioPosterMode_ = false; 22531 22532 // Init state audioOnlyCache_ 22533 this.audioOnlyCache_ = { 22534 playerHeight: null, 22535 hiddenChildren: [] 22536 }; 22537 22538 // if the global option object was accidentally blown away by 22539 // someone, bail early with an informative error 22540 if (!this.options_ || !this.options_.techOrder || !this.options_.techOrder.length) { 22541 throw new Error('No techOrder specified. Did you overwrite ' + 'videojs.options instead of just changing the ' + 'properties you want to override?'); 22542 } 22543 22544 // Store the original tag used to set options 22545 this.tag = tag; 22546 22547 // Store the tag attributes used to restore html5 element 22548 this.tagAttributes = tag && getAttributes(tag); 22549 22550 // Update current language 22551 this.language(this.options_.language); 22552 22553 // Update Supported Languages 22554 if (options.languages) { 22555 // Normalise player option languages to lowercase 22556 const languagesToLower = {}; 22557 Object.getOwnPropertyNames(options.languages).forEach(function (name) { 22558 languagesToLower[name.toLowerCase()] = options.languages[name]; 22559 }); 22560 this.languages_ = languagesToLower; 22561 } else { 22562 this.languages_ = Player.prototype.options_.languages; 22563 } 22564 this.resetCache_(); 22565 22566 // Set poster 22567 /** @type string */ 22568 this.poster_ = options.poster || ''; 22569 22570 // Set controls 22571 /** @type {boolean} */ 22572 this.controls_ = !!options.controls; 22573 22574 // Original tag settings stored in options 22575 // now remove immediately so native controls don't flash. 22576 // May be turned back on by HTML5 tech if nativeControlsForTouch is true 22577 tag.controls = false; 22578 tag.removeAttribute('controls'); 22579 this.changingSrc_ = false; 22580 this.playCallbacks_ = []; 22581 this.playTerminatedQueue_ = []; 22582 22583 // the attribute overrides the option 22584 if (tag.hasAttribute('autoplay')) { 22585 this.autoplay(true); 22586 } else { 22587 // otherwise use the setter to validate and 22588 // set the correct value. 22589 this.autoplay(this.options_.autoplay); 22590 } 22591 22592 // check plugins 22593 if (options.plugins) { 22594 Object.keys(options.plugins).forEach(name => { 22595 if (typeof this[name] !== 'function') { 22596 throw new Error(`plugin "${name}" does not exist`); 22597 } 22598 }); 22599 } 22600 22601 /* 22602 * Store the internal state of scrubbing 22603 * 22604 * @private 22605 * @return {Boolean} True if the user is scrubbing 22606 */ 22607 this.scrubbing_ = false; 22608 this.el_ = this.createEl(); 22609 22610 // Make this an evented object and use `el_` as its event bus. 22611 evented(this, { 22612 eventBusKey: 'el_' 22613 }); 22614 22615 // listen to document and player fullscreenchange handlers so we receive those events 22616 // before a user can receive them so we can update isFullscreen appropriately. 22617 // make sure that we listen to fullscreenchange events before everything else to make sure that 22618 // our isFullscreen method is updated properly for internal components as well as external. 22619 if (this.fsApi_.requestFullscreen) { 22620 on(document, this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_); 22621 this.on(this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_); 22622 } 22623 if (this.fluid_) { 22624 this.on(['playerreset', 'resize'], this.boundUpdateStyleEl_); 22625 } 22626 // We also want to pass the original player options to each component and plugin 22627 // as well so they don't need to reach back into the player for options later. 22628 // We also need to do another copy of this.options_ so we don't end up with 22629 // an infinite loop. 22630 const playerOptionsCopy = merge$2(this.options_); 22631 22632 // Load plugins 22633 if (options.plugins) { 22634 Object.keys(options.plugins).forEach(name => { 22635 this[name](options.plugins[name]); 22636 }); 22637 } 22638 22639 // Enable debug mode to fire debugon event for all plugins. 22640 if (options.debug) { 22641 this.debug(true); 22642 } 22643 this.options_.playerOptions = playerOptionsCopy; 22644 this.middleware_ = []; 22645 this.playbackRates(options.playbackRates); 22646 if (options.experimentalSvgIcons) { 22647 // Add SVG Sprite to the DOM 22648 const parser = new window.DOMParser(); 22649 const parsedSVG = parser.parseFromString(icons, 'image/svg+xml'); 22650 const errorNode = parsedSVG.querySelector('parsererror'); 22651 if (errorNode) { 22652 log$1.warn('Failed to load SVG Icons. Falling back to Font Icons.'); 22653 this.options_.experimentalSvgIcons = null; 22654 } else { 22655 const sprite = parsedSVG.documentElement; 22656 sprite.style.display = 'none'; 22657 this.el_.appendChild(sprite); 22658 this.addClass('vjs-svg-icons-enabled'); 22659 } 22660 } 22661 this.initChildren(); 22662 22663 // Set isAudio based on whether or not an audio tag was used 22664 this.isAudio(tag.nodeName.toLowerCase() === 'audio'); 22665 22666 // Update controls className. Can't do this when the controls are initially 22667 // set because the element doesn't exist yet. 22668 if (this.controls()) { 22669 this.addClass('vjs-controls-enabled'); 22670 } else { 22671 this.addClass('vjs-controls-disabled'); 22672 } 22673 22674 // Set ARIA label and region role depending on player type 22675 this.el_.setAttribute('role', 'region'); 22676 if (this.isAudio()) { 22677 this.el_.setAttribute('aria-label', this.localize('Audio Player')); 22678 } else { 22679 this.el_.setAttribute('aria-label', this.localize('Video Player')); 22680 } 22681 if (this.isAudio()) { 22682 this.addClass('vjs-audio'); 22683 } 22684 22685 // TODO: Make this smarter. Toggle user state between touching/mousing 22686 // using events, since devices can have both touch and mouse events. 22687 // TODO: Make this check be performed again when the window switches between monitors 22688 // (See https://github.com/videojs/video.js/issues/5683) 22689 if (TOUCH_ENABLED) { 22690 this.addClass('vjs-touch-enabled'); 22691 } 22692 22693 // iOS Safari has broken hover handling 22694 if (!IS_IOS) { 22695 this.addClass('vjs-workinghover'); 22696 } 22697 22698 // Make player easily findable by ID 22699 Player.players[this.id_] = this; 22700 22701 // Add a major version class to aid css in plugins 22702 const majorVersion = version$5.split('.')[0]; 22703 this.addClass(`vjs-v${majorVersion}`); 22704 22705 // When the player is first initialized, trigger activity so components 22706 // like the control bar show themselves if needed 22707 this.userActive(true); 22708 this.reportUserActivity(); 22709 this.one('play', e => this.listenForUserActivity_(e)); 22710 this.on('keydown', e => this.handleKeyDown(e)); 22711 this.on('languagechange', e => this.handleLanguagechange(e)); 22712 this.breakpoints(this.options_.breakpoints); 22713 this.responsive(this.options_.responsive); 22714 22715 // Calling both the audio mode methods after the player is fully 22716 // setup to be able to listen to the events triggered by them 22717 this.on('ready', () => { 22718 // Calling the audioPosterMode method first so that 22719 // the audioOnlyMode can take precedence when both options are set to true 22720 this.audioPosterMode(this.options_.audioPosterMode); 22721 this.audioOnlyMode(this.options_.audioOnlyMode); 22722 }); 22723 } 22724 22725 /** 22726 * Destroys the video player and does any necessary cleanup. 22727 * 22728 * This is especially helpful if you are dynamically adding and removing videos 22729 * to/from the DOM. 22730 * 22731 * @fires Player#dispose 22732 */ 22733 dispose() { 22734 /** 22735 * Called when the player is being disposed of. 22736 * 22737 * @event Player#dispose 22738 * @type {Event} 22739 */ 22740 this.trigger('dispose'); 22741 // prevent dispose from being called twice 22742 this.off('dispose'); 22743 22744 // Make sure all player-specific document listeners are unbound. This is 22745 off(document, this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_); 22746 off(document, 'keydown', this.boundFullWindowOnEscKey_); 22747 if (this.styleEl_ && this.styleEl_.parentNode) { 22748 this.styleEl_.parentNode.removeChild(this.styleEl_); 22749 this.styleEl_ = null; 22750 } 22751 22752 // Kill reference to this player 22753 Player.players[this.id_] = null; 22754 if (this.tag && this.tag.player) { 22755 this.tag.player = null; 22756 } 22757 if (this.el_ && this.el_.player) { 22758 this.el_.player = null; 22759 } 22760 if (this.tech_) { 22761 this.tech_.dispose(); 22762 this.isPosterFromTech_ = false; 22763 this.poster_ = ''; 22764 } 22765 if (this.playerElIngest_) { 22766 this.playerElIngest_ = null; 22767 } 22768 if (this.tag) { 22769 this.tag = null; 22770 } 22771 clearCacheForPlayer(this); 22772 22773 // remove all event handlers for track lists 22774 // all tracks and track listeners are removed on 22775 // tech dispose 22776 ALL.names.forEach(name => { 22777 const props = ALL[name]; 22778 const list = this[props.getterName](); 22779 22780 // if it is not a native list 22781 // we have to manually remove event listeners 22782 if (list && list.off) { 22783 list.off(); 22784 } 22785 }); 22786 22787 // the actual .el_ is removed here, or replaced if 22788 super.dispose({ 22789 restoreEl: this.options_.restoreEl 22790 }); 22791 } 22792 22793 /** 22794 * Create the `Player`'s DOM element. 22795 * 22796 * @return {Element} 22797 * The DOM element that gets created. 22798 */ 22799 createEl() { 22800 let tag = this.tag; 22801 let el; 22802 let playerElIngest = this.playerElIngest_ = tag.parentNode && tag.parentNode.hasAttribute && tag.parentNode.hasAttribute('data-vjs-player'); 22803 const divEmbed = this.tag.tagName.toLowerCase() === 'video-js'; 22804 if (playerElIngest) { 22805 el = this.el_ = tag.parentNode; 22806 } else if (!divEmbed) { 22807 el = this.el_ = super.createEl('div'); 22808 } 22809 22810 // Copy over all the attributes from the tag, including ID and class 22811 // ID will now reference player box, not the video tag 22812 const attrs = getAttributes(tag); 22813 if (divEmbed) { 22814 el = this.el_ = tag; 22815 tag = this.tag = document.createElement('video'); 22816 while (el.children.length) { 22817 tag.appendChild(el.firstChild); 22818 } 22819 if (!hasClass(el, 'video-js')) { 22820 addClass(el, 'video-js'); 22821 } 22822 el.appendChild(tag); 22823 playerElIngest = this.playerElIngest_ = el; 22824 // move properties over from our custom `video-js` element 22825 // to our new `video` element. This will move things like 22826 // `src` or `controls` that were set via js before the player 22827 // was initialized. 22828 Object.keys(el).forEach(k => { 22829 try { 22830 tag[k] = el[k]; 22831 } catch (e) { 22832 // we got a a property like outerHTML which we can't actually copy, ignore it 22833 } 22834 }); 22835 } 22836 22837 // set tabindex to -1 to remove the video element from the focus order 22838 tag.setAttribute('tabindex', '-1'); 22839 attrs.tabindex = '-1'; 22840 22841 // Workaround for #4583 on Chrome (on Windows) with JAWS. 22842 // See https://github.com/FreedomScientific/VFO-standards-support/issues/78 22843 // Note that we can't detect if JAWS is being used, but this ARIA attribute 22844 // doesn't change behavior of Chrome if JAWS is not being used 22845 if (IS_CHROME && IS_WINDOWS) { 22846 tag.setAttribute('role', 'application'); 22847 attrs.role = 'application'; 22848 } 22849 22850 // Remove width/height attrs from tag so CSS can make it 100% width/height 22851 tag.removeAttribute('width'); 22852 tag.removeAttribute('height'); 22853 if ('width' in attrs) { 22854 delete attrs.width; 22855 } 22856 if ('height' in attrs) { 22857 delete attrs.height; 22858 } 22859 Object.getOwnPropertyNames(attrs).forEach(function (attr) { 22860 // don't copy over the class attribute to the player element when we're in a div embed 22861 // the class is already set up properly in the divEmbed case 22862 // and we want to make sure that the `video-js` class doesn't get lost 22863 if (!(divEmbed && attr === 'class')) { 22864 el.setAttribute(attr, attrs[attr]); 22865 } 22866 if (divEmbed) { 22867 tag.setAttribute(attr, attrs[attr]); 22868 } 22869 }); 22870 22871 // Update tag id/class for use as HTML5 playback tech 22872 // Might think we should do this after embedding in container so .vjs-tech class 22873 // doesn't flash 100% width/height, but class only applies with .video-js parent 22874 tag.playerId = tag.id; 22875 tag.id += '_html5_api'; 22876 tag.className = 'vjs-tech'; 22877 22878 // Make player findable on elements 22879 tag.player = el.player = this; 22880 // Default state of video is paused 22881 this.addClass('vjs-paused'
vendor: 9,008 bytes, lines 22881-23146
22881); 22882 22883 // Add a style element in the player that we'll use to set the width/height 22884 // of the player in a way that's still overridable by CSS, just like the 22885 // video element 22886 if (window.VIDEOJS_NO_DYNAMIC_STYLE !== true) { 22887 this.styleEl_ = createStyleElement('vjs-styles-dimensions'); 22888 const defaultsStyleEl = $('.vjs-styles-defaults'); 22889 const head = $('head'); 22890 head.insertBefore(this.styleEl_, defaultsStyleEl ? defaultsStyleEl.nextSibling : head.firstChild); 22891 } 22892 this.fill_ = false; 22893 this.fluid_ = false; 22894 22895 // Pass in the width/height/aspectRatio options which will update the style el 22896 this.width(this.options_.width); 22897 this.height(this.options_.height); 22898 this.fill(this.options_.fill); 22899 this.fluid(this.options_.fluid); 22900 this.aspectRatio(this.options_.aspectRatio); 22901 // support both crossOrigin and crossorigin to reduce confusion and issues around the name 22902 this.crossOrigin(this.options_.crossOrigin || this.options_.crossorigin); 22903 22904 // Hide any links within the video/audio tag, 22905 // because IE doesn't hide them completely from screen readers. 22906 const links = tag.getElementsByTagName('a'); 22907 for (let i = 0; i < links.length; i++) { 22908 const linkEl = links.item(i); 22909 addClass(linkEl, 'vjs-hidden'); 22910 linkEl.setAttribute('hidden', 'hidden'); 22911 } 22912 22913 // insertElFirst seems to cause the networkState to flicker from 3 to 2, so 22914 // keep track of the original for later so we can know if the source originally failed 22915 tag.initNetworkState_ = tag.networkState; 22916 22917 // Wrap video tag in div (el/box) container 22918 if (tag.parentNode && !playerElIngest) { 22919 tag.parentNode.insertBefore(el, tag); 22920 } 22921 22922 // insert the tag as the first child of the player element 22923 // then manually add it to the children array so that this.addChild 22924 // will work properly for other components 22925 // 22926 // Breaks iPhone, fixed in HTML5 setup. 22927 prependTo(tag, el); 22928 this.children_.unshift(tag); 22929 22930 // Set lang attr on player to ensure CSS :lang() in consistent with player 22931 // if it's been set to something different to the doc 22932 this.el_.setAttribute('lang', this.language_); 22933 this.el_.setAttribute('translate', 'no'); 22934 this.el_ = el; 22935 return el; 22936 } 22937 22938 /** 22939 * Get or set the `Player`'s crossOrigin option. For the HTML5 player, this 22940 * sets the `crossOrigin` property on the `<video>` tag to control the CORS 22941 * behavior. 22942 * 22943 * @see [Video Element Attributes]{@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#attr-crossorigin} 22944 * 22945 * @param {string|null} [value] 22946 * The value to set the `Player`'s crossOrigin to. If an argument is 22947 * given, must be one of `'anonymous'` or `'use-credentials'`, or 'null'. 22948 * 22949 * @return {string|null|undefined} 22950 * - The current crossOrigin value of the `Player` when getting. 22951 * - undefined when setting 22952 */ 22953 crossOrigin(value) { 22954 // `null` can be set to unset a value 22955 if (typeof value === 'undefined') { 22956 return this.techGet_('crossOrigin'); 22957 } 22958 if (value !== null && value !== 'anonymous' && value !== 'use-credentials') { 22959 log$1.warn(`crossOrigin must be null, "anonymous" or "use-credentials", given "${value}"`); 22960 return; 22961 } 22962 this.techCall_('setCrossOrigin', value); 22963 if (this.posterImage) { 22964 this.posterImage.crossOrigin(value); 22965 } 22966 return; 22967 } 22968 22969 /** 22970 * A getter/setter for the `Player`'s width. Returns the player's configured value. 22971 * To get the current width use `currentWidth()`. 22972 * 22973 * @param {number|string} [value] 22974 * CSS value to set the `Player`'s width to. 22975 * 22976 * @return {number|undefined} 22977 * - The current width of the `Player` when getting. 22978 * - Nothing when setting 22979 */ 22980 width(value) { 22981 return this.dimension('width', value); 22982 } 22983 22984 /** 22985 * A getter/setter for the `Player`'s height. Returns the player's configured value. 22986 * To get the current height use `currentheight()`. 22987 * 22988 * @param {number|string} [value] 22989 * CSS value to set the `Player`'s height to. 22990 * 22991 * @return {number|undefined} 22992 * - The current height of the `Player` when getting. 22993 * - Nothing when setting 22994 */ 22995 height(value) { 22996 return this.dimension('height', value); 22997 } 22998 22999 /** 23000 * A getter/setter for the `Player`'s width & height. 23001 * 23002 * @param {string} dimension 23003 * This string can be: 23004 * - 'width' 23005 * - 'height' 23006 * 23007 * @param {number|string} [value] 23008 * Value for dimension specified in the first argument. 23009 * 23010 * @return {number} 23011 * The dimension arguments value when getting (width/height). 23012 */ 23013 dimension(dimension, value) { 23014 const privDimension = dimension + '_'; 23015 if (value === undefined) { 23016 return this[privDimension] || 0; 23017 } 23018 if (value === '' || value === 'auto') { 23019 // If an empty string is given, reset the dimension to be automatic 23020 this[privDimension] = undefined; 23021 this.updateStyleEl_(); 23022 return; 23023 } 23024 const parsedVal = parseFloat(value); 23025 if (isNaN(parsedVal)) { 23026 log$1.error(`Improper value "${value}" supplied for for ${dimension}`); 23027 return; 23028 } 23029 this[privDimension] = parsedVal; 23030 this.updateStyleEl_(); 23031 } 23032 23033 /** 23034 * A getter/setter/toggler for the vjs-fluid `className` on the `Player`. 23035 * 23036 * Turning this on will turn off fill mode. 23037 * 23038 * @param {boolean} [bool] 23039 * - A value of true adds the class. 23040 * - A value of false removes the class. 23041 * - No value will be a getter. 23042 * 23043 * @return {boolean|undefined} 23044 * - The value of fluid when getting. 23045 * - `undefined` when setting. 23046 */ 23047 fluid(bool) { 23048 if (bool === undefined) { 23049 return !!this.fluid_; 23050 } 23051 this.fluid_ = !!bool; 23052 if (isEvented(this)) { 23053 this.off(['playerreset', 'resize'], this.boundUpdateStyleEl_); 23054 } 23055 if (bool) { 23056 this.addClass('vjs-fluid'); 23057 this.fill(false); 23058 addEventedCallback(this, () => { 23059 this.on(['playerreset', 'resize'], this.boundUpdateStyleEl_); 23060 }); 23061 } else { 23062 this.removeClass('vjs-fluid'); 23063 } 23064 this.updateStyleEl_(); 23065 } 23066 23067 /** 23068 * A getter/setter/toggler for the vjs-fill `className` on the `Player`. 23069 * 23070 * Turning this on will turn off fluid mode. 23071 * 23072 * @param {boolean} [bool] 23073 * - A value of true adds the class. 23074 * - A value of false removes the class. 23075 * - No value will be a getter. 23076 * 23077 * @return {boolean|undefined} 23078 * - The value of fluid when getting. 23079 * - `undefined` when setting. 23080 */ 23081 fill(bool) { 23082 if (bool === undefined) { 23083 return !!this.fill_; 23084 } 23085 this.fill_ = !!bool; 23086 if (bool) { 23087 this.addClass('vjs-fill'); 23088 this.fluid(false); 23089 } else { 23090 this.removeClass('vjs-fill'); 23091 } 23092 } 23093 23094 /** 23095 * Get/Set the aspect ratio 23096 * 23097 * @param {string} [ratio] 23098 * Aspect ratio for player 23099 * 23100 * @return {string|undefined} 23101 * returns the current aspect ratio when getting 23102 */ 23103 23104 /** 23105 * A getter/setter for the `Player`'s aspect ratio. 23106 * 23107 * @param {string} [ratio] 23108 * The value to set the `Player`'s aspect ratio to. 23109 * 23110 * @return {string|undefined} 23111 * - The current aspect ratio of the `Player` when getting. 23112 * - undefined when setting 23113 */ 23114 aspectRatio(ratio) { 23115 if (ratio === undefined) { 23116 return this.aspectRatio_; 23117 } 23118 23119 // Check for width:height format 23120 if (!/^\d+\:\d+$/.test(ratio)) { 23121 throw new Error('Improper value supplied for aspect ratio. The format should be width:height, for example 16:9.'); 23122 } 23123 this.aspectRatio_ = ratio; 23124 23125 // We're assuming if you set an aspect ratio you want fluid mode, 23126 // because in fixed mode you could calculate width and height yourself. 23127 this.fluid(true); 23128 this.updateStyleEl_(); 23129 } 23130 23131 /** 23132 * Update styles of the `Player` element (height, width and aspect ratio). 23133 * 23134 * @private 23135 * @listens Tech#loadedmetadata 23136 */ 23137 updateStyleEl_() { 23138 if (window.VIDEOJS_NO_DYNAMIC_STYLE === true) { 23139 const width = typeof this.width_ === 'number' ? this.width_ : this.options_.width; 23140 const height = typeof this.height_ === 'number' ? this.height_ : this.options_.height; 23141 const techEl = this.tech_ && this.tech_.el(); 23142 if (techEl) { 23143 if (width >= 0) { 23144 techEl.width = width; 23145 } 23146 if (height >= 0) {
vendor: 9,567 bytes, lines 23147-23385
23147 techEl.height = height; 23148 } 23149 } 23150 return; 23151 } 23152 let width; 23153 let height; 23154 let aspectRatio; 23155 let idClass; 23156 23157 // The aspect ratio is either used directly or to calculate width and height. 23158 if (this.aspectRatio_ !== undefined && this.aspectRatio_ !== 'auto') { 23159 // Use any aspectRatio that's been specifically set 23160 aspectRatio = this.aspectRatio_; 23161 } else if (this.videoWidth() > 0) { 23162 // Otherwise try to get the aspect ratio from the video metadata 23163 aspectRatio = this.videoWidth() + ':' + this.videoHeight(); 23164 } else { 23165 // Or use a default. The video element's is 2:1, but 16:9 is more common. 23166 aspectRatio = '16:9'; 23167 } 23168 23169 // Get the ratio as a decimal we can use to calculate dimensions 23170 const ratioParts = aspectRatio.split(':'); 23171 const ratioMultiplier = ratioParts[1] / ratioParts[0]; 23172 if (this.width_ !== undefined) { 23173 // Use any width that's been specifically set 23174 width = this.width_; 23175 } else if (this.height_ !== undefined) { 23176 // Or calculate the width from the aspect ratio if a height has been set 23177 width = this.height_ / ratioMultiplier; 23178 } else { 23179 // Or use the video's metadata, or use the video el's default of 300 23180 width = this.videoWidth() || 300; 23181 } 23182 if (this.height_ !== undefined) { 23183 // Use any height that's been specifically set 23184 height = this.height_; 23185 } else { 23186 // Otherwise calculate the height from the ratio and the width 23187 height = width * ratioMultiplier; 23188 } 23189 23190 // Ensure the CSS class is valid by starting with an alpha character 23191 if (/^[^a-zA-Z]/.test(this.id())) { 23192 idClass = 'dimensions-' + this.id(); 23193 } else { 23194 idClass = this.id() + '-dimensions'; 23195 } 23196 23197 // Ensure the right class is still on the player for the style element 23198 this.addClass(idClass); 23199 setTextContent(this.styleEl_, ` 23200 .${idClass} { 23201 width: ${width}px; 23202 height: ${height}px; 23203 } 23204 23205 .${idClass}.vjs-fluid:not(.vjs-audio-only-mode) { 23206 padding-top: ${ratioMultiplier * 100}%; 23207 } 23208 `); 23209 } 23210 23211 /** 23212 * Load/Create an instance of playback {@link Tech} including element 23213 * and API methods. Then append the `Tech` element in `Player` as a child. 23214 * 23215 * @param {string} techName 23216 * name of the playback technology 23217 * 23218 * @param {string} source 23219 * video source 23220 * 23221 * @private 23222 */ 23223 loadTech_(techName, source) { 23224 // Pause and remove current playback technology 23225 if (this.tech_) { 23226 this.unloadTech_(); 23227 } 23228 const titleTechName = toTitleCase$1(techName); 23229 const camelTechName = techName.charAt(0).toLowerCase() + techName.slice(1); 23230 23231 // get rid of the HTML5 video tag as soon as we are using another tech 23232 if (titleTechName !== 'Html5' && this.tag) { 23233 Tech.getTech('Html5').disposeMediaElement(this.tag); 23234 this.tag.player = null; 23235 this.tag = null; 23236 } 23237 this.techName_ = titleTechName; 23238 23239 // Turn off API access because we're loading a new tech that might load asynchronously 23240 this.isReady_ = false; 23241 let autoplay = this.autoplay(); 23242 23243 // if autoplay is a string (or `true` with normalizeAutoplay: true) we pass false to the tech 23244 // because the player is going to handle autoplay on `loadstart` 23245 if (typeof this.autoplay() === 'string' || this.autoplay() === true && this.options_.normalizeAutoplay) { 23246 autoplay = false; 23247 } 23248 23249 // Grab tech-specific options from player options and add source and parent element to use. 23250 const techOptions = { 23251 source, 23252 autoplay, 23253 'nativeControlsForTouch': this.options_.nativeControlsForTouch, 23254 'playerId': this.id(), 23255 'techId': `${this.id()}_${camelTechName}_api`, 23256 'playsinline': this.options_.playsinline, 23257 'preload': this.options_.preload, 23258 'loop': this.options_.loop, 23259 'disablePictureInPicture': this.options_.disablePictureInPicture, 23260 'muted': this.options_.muted, 23261 'poster': this.poster(), 23262 'language': this.language(), 23263 'playerElIngest': this.playerElIngest_ || false, 23264 'vtt.js': this.options_['vtt.js'], 23265 'canOverridePoster': !!this.options_.techCanOverridePoster, 23266 'enableSourceset': this.options_.enableSourceset 23267 }; 23268 ALL.names.forEach(name => { 23269 const props = ALL[name]; 23270 techOptions[props.getterName] = this[props.privateName]; 23271 }); 23272 Object.assign(techOptions, this.options_[titleTechName]); 23273 Object.assign(techOptions, this.options_[camelTechName]); 23274 Object.assign(techOptions, this.options_[techName.toLowerCase()]); 23275 if (this.tag) { 23276 techOptions.tag = this.tag; 23277 } 23278 if (source && source.src === this.cache_.src && this.cache_.currentTime > 0) { 23279 techOptions.startTime = this.cache_.currentTime; 23280 } 23281 23282 // Initialize tech instance 23283 const TechClass = Tech.getTech(techName); 23284 if (!TechClass) { 23285 throw new Error(`No Tech named '${titleTechName}' exists! '${titleTechName}' should be registered using videojs.registerTech()'`); 23286 } 23287 this.tech_ = new TechClass(techOptions); 23288 23289 // player.triggerReady is always async, so don't need this to be async 23290 this.tech_.ready(bind_(this, this.handleTechReady_), true); 23291 textTrackConverter.jsonToTextTracks(this.textTracksJson_ || [], this.tech_); 23292 23293 // Listen to all HTML5-defined events and trigger them on the player 23294 TECH_EVENTS_RETRIGGER.forEach(event => { 23295 this.on(this.tech_, event, e => this[`handleTech${toTitleCase$1(event)}_`](e)); 23296 }); 23297 Object.keys(TECH_EVENTS_QUEUE).forEach(event => { 23298 this.on(this.tech_, event, eventObj => { 23299 if (this.tech_.playbackRate() === 0 && this.tech_.seeking()) { 23300 this.queuedCallbacks_.push({ 23301 callback: this[`handleTech${TECH_EVENTS_QUEUE[event]}_`].bind(this), 23302 event: eventObj 23303 }); 23304 return; 23305 } 23306 this[`handleTech${TECH_EVENTS_QUEUE[event]}_`](eventObj); 23307 }); 23308 }); 23309 this.on(this.tech_, 'loadstart', e => this.handleTechLoadStart_(e)); 23310 this.on(this.tech_, 'sourceset', e => this.handleTechSourceset_(e)); 23311 this.on(this.tech_, 'waiting', e => this.handleTechWaiting_(e)); 23312 this.on(this.tech_, 'ended', e => this.handleTechEnded_(e)); 23313 this.on(this.tech_, 'seeking', e => this.handleTechSeeking_(e)); 23314 this.on(this.tech_, 'play', e => this.handleTechPlay_(e)); 23315 this.on(this.tech_, 'pause', e => this.handleTechPause_(e)); 23316 this.on(this.tech_, 'durationchange', e => this.handleTechDurationChange_(e)); 23317 this.on(this.tech_, 'fullscreenchange', (e, data) => this.handleTechFullscreenChange_(e, data)); 23318 this.on(this.tech_, 'fullscreenerror', (e, err) => this.handleTechFullscreenError_(e, err)); 23319 this.on(this.tech_, 'enterpictureinpicture', e => this.handleTechEnterPictureInPicture_(e)); 23320 this.on(this.tech_, 'leavepictureinpicture', e => this.handleTechLeavePictureInPicture_(e)); 23321 this.on(this.tech_, 'error', e => this.handleTechError_(e)); 23322 this.on(this.tech_, 'posterchange', e => this.handleTechPosterChange_(e)); 23323 this.on(this.tech_, 'textdata', e => this.handleTechTextData_(e)); 23324 this.on(this.tech_, 'ratechange', e => this.handleTechRateChange_(e)); 23325 this.on(this.tech_, 'loadedmetadata', this.boundUpdateStyleEl_); 23326 this.usingNativeControls(this.techGet_('controls')); 23327 if (this.controls() && !this.usingNativeControls()) { 23328 this.addTechControlsListeners_(); 23329 } 23330 23331 // Add the tech element in the DOM if it was not already there 23332 // Make sure to not insert the original video element if using Html5 23333 if (this.tech_.el().parentNode !== this.el() && (titleTechName !== 'Html5' || !this.tag)) { 23334 prependTo(this.tech_.el(), this.el()); 23335 } 23336 23337 // Get rid of the original video tag reference after the first tech is loaded 23338 if (this.tag) { 23339 this.tag.player = null; 23340 this.tag = null; 23341 } 23342 } 23343 23344 /** 23345 * Unload and dispose of the current playback {@link Tech}. 23346 * 23347 * @private 23348 */ 23349 unloadTech_() { 23350 // Save the current text tracks so that we can reuse the same text tracks with the next tech 23351 ALL.names.forEach(name => { 23352 const props = ALL[name]; 23353 this[props.privateName] = this[props.getterName](); 23354 }); 23355 this.textTracksJson_ = textTrackConverter.textTracksToJson(this.tech_); 23356 this.isReady_ = false; 23357 this.tech_.dispose(); 23358 this.tech_ = false; 23359 if (this.isPosterFromTech_) { 23360 this.poster_ = ''; 23361 this.trigger('posterchange'); 23362 } 23363 this.isPosterFromTech_ = false; 23364 } 23365 23366 /** 23367 * Return a reference to the current {@link Tech}. 23368 * It will print a warning by default about the danger of using the tech directly 23369 * but any argument that is passed in will silence the warning. 23370 * 23371 * @param {*} [safety] 23372 * Anything passed in to silence the warning 23373 * 23374 * @return {Tech} 23375 * The Tech 23376 */ 23377 tech(safety) { 23378 if (safety === undefined) { 23379 log$1.warn('Using the tech directly can be dangerous. I hope you know what you\'re doing.\n' + 'See https://github.com/videojs/video.js/issues/2617 for more info.\n'); 23380 } 23381 return this.tech_; 23382 } 23383 23384 /** 23385 *
23385Set up click and touch listeners for the playback element 23386 * 23387 * - On desktops: a click on the video itself will toggle playback 23388 * - On mobile devices: a click on the video toggles controls 23389 * which is done by toggling the user state between active and 23390 * inactive 23391 * - A tap can signal that a user has become active or has become inactive 23392 * e.g. a quick tap on an iPhone movie should reveal the controls. Another 23393 * quick tap should hide them again (signaling the user is in an inactive 23394 * viewing state) 23395 * - In addition to this, we still want the user to be considered inactive after 23396 * a few seconds of inactivity. 23397 * 23398 * > Note: the only part of iOS interaction we can't mimic with this setup 23399 * is a touch and hold on the video element counting as activity in order to 23400 * keep the controls showing, but that shouldn't be an issue. A touch and hold 23401 * on any controls will still keep the user active 23402 * 23403 * @private 23404 */ 23405 addTechControlsListeners_() { 23406 // Make sure to remove all the previous listeners in case we are called multiple times. 23407 this.removeTechControlsListeners_(); 23408 this.on(this.tech_, 'click', this.boundHandleTechClick_); 23409 this.on(this.tech_, 'dblclick', this.boundHandleTechDoubleClick_); 23410 23411 // If the controls were hidden we don't want that to change without a tap event 23412 // so we'll check if the controls were already showing before reporting user 23413 // activity 23414 this.on(this.tech_, 'touchstart', this.boundHandleTechTouchStart_); 23415 this.on(this.tech_, 'touchmove', this.boundHandleTechTouchMove_); 23416 this.on(this.tech_, 'touchend', this.boundHandleTechTouchEnd_); 23417 23418 // The tap listener needs to come after the touchend listener because the tap 23419 // listener cancels out any reportedUserActivity when setting userActive(false) 23420 this.on(this.tech_, 'tap', this.boundHandleTechTap_); 23421 } 23422 23423 /** 23424 * Remove the listeners used for click and tap controls. This is needed for 23425 * toggling to controls disabled, where a tap/touch should do nothing. 23426 * 23427 * @private 23428 */ 23429 removeTechControlsListeners_() { 23430 // We don't want to just use `this.off()` because there might be other needed 23431 // listeners added by techs that extend this. 23432 this.off(this.tech_, 'tap', this.boundHandleTechTap_); 23433 this.off(this.tech_, 'touchstart', this.boundHandleTechTouchStart_); 23434 this.off(this.tech_, 'touchmove', this.boundHandleTechTouchMove_); 23435 this.off(this.tech_, 'touchend', this.boundHandleTechTouchEnd_); 23436 this.off(this.tech_, 'click', this.boundHandleTechClick_); 23437 this.off(this.tech_, 'dblclick', this.boundHandleTechDoubleClick_); 23438 } 23439 23440 /** 23441 * Player waits for the tech to be ready 23442 * 23443 * @private 23444 */ 23445 handleTechReady_() { 23446 this.triggerReady(); 23447 23448 // Keep the same volume as before 23449 if (this.cache_.volume) { 23450 this.techCall_('setVolume', this.cache_.volume); 23451 } 23452 23453 // Look if the tech found a higher resolution poster while loading 23454 this.handleTechPosterChange_(); 23455 23456 // Update the duration if available 23457 this.handleTechDurationChange_(); 23458 } 23459 23460 /** 23461 * Retrigger the `loadstart` event that was triggered by the {@link Tech}. 23462 * 23463 * @fires Player#loadstart 23464 * @listens Tech#loadstart 23465 * @private 23466 */ 23467 handleTechLoadStart_() { 23468 // TODO: Update to use `emptied` event instead. See #1277. 23469 23470 this.removeClass('vjs-ended', 'vjs-seeking'); 23471 23472 // reset the error state 23473 this.error(null); 23474 23475 // Update the duration 23476 this.handleTechDurationChange_(); 23477 if (!this.paused()) { 23478 /** 23479 * Fired when the user agent begins looking for media data 23480 * 23481 * @event Player#loadstart 23482 * @type {Event} 23483 */ 23484 this.trigger('loadstart'); 23485 } else { 23486 // reset the hasStarted state 23487 this.hasStarted(false); 23488 this.trigger('loadstart'); 23489 } 23490 23491 // autoplay happens after loadstart for the browser, 23492 // so we mimic that behavior 23493 this.manualAutoplay_(this.autoplay() === true && this.options_.normalizeAutoplay ? 'play' : this.autoplay()); 23494 } 23495 23496 /** 23497 * Handle autoplay string values, rather than the typical boolean 23498 * values that should be handled by the tech. Note that this is not 23499 * part of any specification. Valid values and what they do can be 23500 * found on the autoplay getter at Player#autoplay() 23501 */ 23502 manualAutoplay_(type) { 23503 if (!this.tech_ || typeof type !== 'string') { 23504 return; 23505 } 23506 23507 // Save original muted() value, set muted to true, and attempt to play(). 23508 // On promise rejection, restore muted from saved value 23509 const resolveMuted = () => { 23510 const previouslyMuted = this.muted(); 23511 this.muted(true); 23512 const restoreMuted = () => { 23513 this.muted(previouslyMuted); 23514 }; 23515 23516 // restore muted on play terminatation 23517 this.playTerminatedQueue_.push(restoreMuted); 23518 const mutedPromise = this.play(); 23519 if (!isPromise(mutedPromise)) { 23520 return; 23521 } 23522 return mutedPromise.catch(err => { 23523 restoreMuted(); 23524 throw new Error(`Rejection at manualAutoplay. Restoring muted value. ${err ? err : ''}`); 23525 }); 23526 }; 23527 let promise; 23528 23529 // if muted defaults to true 23530 // the only thing we can do is call play 23531 if (type === 'any' && !this.muted()) { 23532 promise = this.play();
vendor: 17,112 bytes, lines 23533-24089
23533 if (isPromise(promise)) { 23534 promise = promise.catch(resolveMuted); 23535 } 23536 } else if (type === 'muted' && !this.muted()) { 23537 promise = resolveMuted(); 23538 } else { 23539 promise = this.play(); 23540 } 23541 if (!isPromise(promise)) { 23542 return; 23543 } 23544 return promise.then(() => { 23545 this.trigger({ 23546 type: 'autoplay-success', 23547 autoplay: type 23548 }); 23549 }).catch(() => { 23550 this.trigger({ 23551 type: 'autoplay-failure', 23552 autoplay: type 23553 }); 23554 }); 23555 } 23556 23557 /** 23558 * Update the internal source caches so that we return the correct source from 23559 * `src()`, `currentSource()`, and `currentSources()`. 23560 * 23561 * > Note: `currentSources` will not be updated if the source that is passed in exists 23562 * in the current `currentSources` cache. 23563 * 23564 * 23565 * @param {Tech~SourceObject} srcObj 23566 * A string or object source to update our caches to. 23567 */ 23568 updateSourceCaches_(srcObj = '') { 23569 let src = srcObj; 23570 let type = ''; 23571 if (typeof src !== 'string') { 23572 src = srcObj.src; 23573 type = srcObj.type; 23574 } 23575 23576 // make sure all the caches are set to default values 23577 // to prevent null checking 23578 this.cache_.source = this.cache_.source || {}; 23579 this.cache_.sources = this.cache_.sources || []; 23580 23581 // try to get the type of the src that was passed in 23582 if (src && !type) { 23583 type = findMimetype(this, src); 23584 } 23585 23586 // update `currentSource` cache always 23587 this.cache_.source = merge$2({}, srcObj, { 23588 src, 23589 type 23590 }); 23591 const matchingSources = this.cache_.sources.filter(s => s.src && s.src === src); 23592 const sourceElSources = []; 23593 const sourceEls = this.$$('source'); 23594 const matchingSourceEls = []; 23595 for (let i = 0; i < sourceEls.length; i++) { 23596 const sourceObj = getAttributes(sourceEls[i]); 23597 sourceElSources.push(sourceObj); 23598 if (sourceObj.src && sourceObj.src === src) { 23599 matchingSourceEls.push(sourceObj.src); 23600 } 23601 } 23602 23603 // if we have matching source els but not matching sources 23604 // the current source cache is not up to date 23605 if (matchingSourceEls.length && !matchingSources.length) { 23606 this.cache_.sources = sourceElSources; 23607 // if we don't have matching source or source els set the 23608 // sources cache to the `currentSource` cache 23609 } else if (!matchingSources.length) { 23610 this.cache_.sources = [this.cache_.source]; 23611 } 23612 23613 // update the tech `src` cache 23614 this.cache_.src = src; 23615 } 23616 23617 /** 23618 * *EXPERIMENTAL* Fired when the source is set or changed on the {@link Tech} 23619 * causing the media element to reload. 23620 * 23621 * It will fire for the initial source and each subsequent source. 23622 * This event is a custom event from Video.js and is triggered by the {@link Tech}. 23623 * 23624 * The event object for this event contains a `src` property that will contain the source 23625 * that was available when the event was triggered. This is generally only necessary if Video.js 23626 * is switching techs while the source was being changed. 23627 * 23628 * It is also fired when `load` is called on the player (or media element) 23629 * because the {@link https://html.spec.whatwg.org/multipage/media.html#dom-media-load|specification for `load`} 23630 * says that the resource selection algorithm needs to be aborted and restarted. 23631 * In this case, it is very likely that the `src` property will be set to the 23632 * empty string `""` to indicate we do not know what the source will be but 23633 * that it is changing. 23634 * 23635 * *This event is currently still experimental and may change in minor releases.* 23636 * __To use this, pass `enableSourceset` option to the player.__ 23637 * 23638 * @event Player#sourceset 23639 * @type {Event} 23640 * @prop {string} src 23641 * The source url available when the `sourceset` was triggered. 23642 * It will be an empty string if we cannot know what the source is 23643 * but know that the source will change. 23644 */ 23645 /** 23646 * Retrigger the `sourceset` event that was triggered by the {@link Tech}. 23647 * 23648 * @fires Player#sourceset 23649 * @listens Tech#sourceset 23650 * @private 23651 */ 23652 handleTechSourceset_(event) { 23653 // only update the source cache when the source 23654 // was not updated using the player api 23655 if (!this.changingSrc_) { 23656 let updateSourceCaches = src => this.updateSourceCaches_(src); 23657 const playerSrc = this.currentSource().src; 23658 const eventSrc = event.src; 23659 23660 // if we have a playerSrc that is not a blob, and a tech src that is a blob 23661 if (playerSrc && !/^blob:/.test(playerSrc) && /^blob:/.test(eventSrc)) { 23662 // if both the tech source and the player source were updated we assume 23663 // something like @videojs/http-streaming did the sourceset and skip updating the source cache. 23664 if (!this.lastSource_ || this.lastSource_.tech !== eventSrc && this.lastSource_.player !== playerSrc) { 23665 updateSourceCaches = () => {}; 23666 } 23667 } 23668 23669 // update the source to the initial source right away 23670 // in some cases this will be empty string 23671 updateSourceCaches(eventSrc); 23672 23673 // if the `sourceset` `src` was an empty string 23674 // wait for a `loadstart` to update the cache to `currentSrc`. 23675 // If a sourceset happens before a `loadstart`, we reset the state 23676 if (!event.src) { 23677 this.tech_.any(['sourceset', 'loadstart'], e => { 23678 // if a sourceset happens before a `loadstart` there 23679 // is nothing to do as this `handleTechSourceset_` 23680 // will be called again and this will be handled there. 23681 if (e.type === 'sourceset') { 23682 return; 23683 } 23684 const techSrc = this.techGet_('currentSrc'); 23685 this.lastSource_.tech = techSrc; 23686 this.updateSourceCaches_(techSrc); 23687 }); 23688 } 23689 } 23690 this.lastSource_ = { 23691 player: this.currentSource().src, 23692 tech: event.src 23693 }; 23694 this.trigger({ 23695 src: event.src, 23696 type: 'sourceset' 23697 }); 23698 } 23699 23700 /** 23701 * Add/remove the vjs-has-started class 23702 * 23703 * 23704 * @param {boolean} request 23705 * - true: adds the class 23706 * - false: remove the class 23707 * 23708 * @return {boolean} 23709 * the boolean value of hasStarted_ 23710 */ 23711 hasStarted(request) { 23712 if (request === undefined) { 23713 // act as getter, if we have no request to change 23714 return this.hasStarted_; 23715 } 23716 if (request === this.hasStarted_) { 23717 return; 23718 } 23719 this.hasStarted_ = request; 23720 if (this.hasStarted_) { 23721 this.addClass('vjs-has-started'); 23722 } else { 23723 this.removeClass('vjs-has-started'); 23724 } 23725 } 23726 23727 /** 23728 * Fired whenever the media begins or resumes playback 23729 * 23730 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-play} 23731 * @fires Player#play 23732 * @listens Tech#play 23733 * @private 23734 */ 23735 handleTechPlay_() { 23736 this.removeClass('vjs-ended', 'vjs-paused'); 23737 this.addClass('vjs-playing'); 23738 23739 // hide the poster when the user hits play 23740 this.hasStarted(true); 23741 /** 23742 * Triggered whenever an {@link Tech#play} event happens. Indicates that 23743 * playback has started or resumed. 23744 * 23745 * @event Player#play 23746 * @type {Event} 23747 */ 23748 this.trigger('play'); 23749 } 23750 23751 /** 23752 * Retrigger the `ratechange` event that was triggered by the {@link Tech}. 23753 * 23754 * If there were any events queued while the playback rate was zero, fire 23755 * those events now. 23756 * 23757 * @private 23758 * @method Player#handleTechRateChange_ 23759 * @fires Player#ratechange 23760 * @listens Tech#ratechange 23761 */ 23762 handleTechRateChange_() { 23763 if (this.tech_.playbackRate() > 0 && this.cache_.lastPlaybackRate === 0) { 23764 this.queuedCallbacks_.forEach(queued => queued.callback(queued.event)); 23765 this.queuedCallbacks_ = []; 23766 } 23767 this.cache_.lastPlaybackRate = this.tech_.playbackRate(); 23768 /** 23769 * Fires when the playing speed of the audio/video is changed 23770 * 23771 * @event Player#ratechange 23772 * @type {event} 23773 */ 23774 this.trigger('ratechange'); 23775 } 23776 23777 /** 23778 * Retrigger the `waiting` event that was triggered by the {@link Tech}. 23779 * 23780 * @fires Player#waiting 23781 * @listens Tech#waiting 23782 * @private 23783 */ 23784 handleTechWaiting_() { 23785 this.addClass('vjs-waiting'); 23786 /** 23787 * A readyState change on the DOM element has caused playback to stop. 23788 * 23789 * @event Player#waiting 23790 * @type {Event} 23791 */ 23792 this.trigger('waiting'); 23793 23794 // Browsers may emit a timeupdate event after a waiting event. In order to prevent 23795 // premature removal of the waiting class, wait for the time to change. 23796 const timeWhenWaiting = this.currentTime(); 23797 const timeUpdateListener = () => { 23798 if (timeWhenWaiting !== this.currentTime()) { 23799 this.removeClass('vjs-waiting'); 23800 this.off('timeupdate', timeUpdateListener); 23801 } 23802 }; 23803 this.on('timeupdate', timeUpdateListener); 23804 } 23805 23806 /** 23807 * Retrigger the `canplay` event that was triggered by the {@link Tech}. 23808 * > Note: This is not consistent between browsers. See #1351 23809 * 23810 * @fires Player#canplay 23811 * @listens Tech#canplay 23812 * @private 23813 */ 23814 handleTechCanPlay_() { 23815 this.removeClass('vjs-waiting'); 23816 /** 23817 * The media has a readyState of HAVE_FUTURE_DATA or greater. 23818 * 23819 * @event Player#canplay 23820 * @type {Event} 23821 */ 23822 this.trigger('canplay'); 23823 } 23824 23825 /** 23826 * Retrigger the `canplaythrough` event that was triggered by the {@link Tech}. 23827 * 23828 * @fires Player#canplaythrough 23829 * @listens Tech#canplaythrough 23830 * @private 23831 */ 23832 handleTechCanPlayThrough_() { 23833 this.removeClass('vjs-waiting'); 23834 /** 23835 * The media has a readyState of HAVE_ENOUGH_DATA or greater. This means that the 23836 * entire media file can be played without buffering. 23837 * 23838 * @event Player#canplaythrough 23839 * @type {Event} 23840 */ 23841 this.trigger('canplaythrough'); 23842 } 23843 23844 /** 23845 * Retrigger the `playing` event that was triggered by the {@link Tech}. 23846 * 23847 * @fires Player#playing 23848 * @listens Tech#playing 23849 * @private 23850 */ 23851 handleTechPlaying_() { 23852 this.removeClass('vjs-waiting'); 23853 /** 23854 * The media is no longer blocked from playback, and has started playing. 23855 * 23856 * @event Player#playing 23857 * @type {Event} 23858 */ 23859 this.trigger('playing'); 23860 } 23861 23862 /** 23863 * Retrigger the `seeking` event that was triggered by the {@link Tech}. 23864 * 23865 * @fires Player#seeking 23866 * @listens Tech#seeking 23867 * @private 23868 */ 23869 handleTechSeeking_() { 23870 this.addClass('vjs-seeking'); 23871 /** 23872 * Fired whenever the player is jumping to a new time 23873 * 23874 * @event Player#seeking 23875 * @type {Event} 23876 */ 23877 this.trigger('seeking'); 23878 } 23879 23880 /** 23881 * Retrigger the `seeked` event that was triggered by the {@link Tech}. 23882 * 23883 * @fires Player#seeked 23884 * @listens Tech#seeked 23885 * @private 23886 */ 23887 handleTechSeeked_() { 23888 this.removeClass('vjs-seeking', 'vjs-ended'); 23889 /** 23890 * Fired when the player has finished jumping to a new time 23891 * 23892 * @event Player#seeked 23893 * @type {Event} 23894 */ 23895 this.trigger('seeked'); 23896 } 23897 23898 /** 23899 * Retrigger the `pause` event that was triggered by the {@link Tech}. 23900 * 23901 * @fires Player#pause 23902 * @listens Tech#pause 23903 * @private 23904 */ 23905 handleTechPause_() { 23906 this.removeClass('vjs-playing'); 23907 this.addClass('vjs-paused'); 23908 /** 23909 * Fired whenever the media has been paused 23910 * 23911 * @event Player#pause 23912 * @type {Event} 23913 */ 23914 this.trigger('pause'); 23915 } 23916 23917 /** 23918 * Retrigger the `ended` event that was triggered by the {@link Tech}. 23919 * 23920 * @fires Player#ended 23921 * @listens Tech#ended 23922 * @private 23923 */ 23924 handleTechEnded_() { 23925 this.addClass('vjs-ended'); 23926 this.removeClass('vjs-waiting'); 23927 if (this.options_.loop) { 23928 this.currentTime(0); 23929 this.play(); 23930 } else if (!this.paused()) { 23931 this.pause(); 23932 } 23933 23934 /** 23935 * Fired when the end of the media resource is reached (currentTime == duration) 23936 * 23937 * @event Player#ended 23938 * @type {Event} 23939 */ 23940 this.trigger('ended'); 23941 } 23942 23943 /** 23944 * Fired when the duration of the media resource is first known or changed 23945 * 23946 * @listens Tech#durationchange 23947 * @private 23948 */ 23949 handleTechDurationChange_() { 23950 this.duration(this.techGet_('duration')); 23951 } 23952 23953 /** 23954 * Handle a click on the media element to play/pause 23955 * 23956 * @param {Event} event 23957 * the event that caused this function to trigger 23958 * 23959 * @listens Tech#click 23960 * @private 23961 */ 23962 handleTechClick_(event) { 23963 // When controls are disabled a click should not toggle playback because 23964 // the click is considered a control 23965 if (!this.controls_) { 23966 return; 23967 } 23968 if (this.options_ === undefined || this.options_.userActions === undefined || this.options_.userActions.click === undefined || this.options_.userActions.click !== false) { 23969 if (this.options_ !== undefined && this.options_.userActions !== undefined && typeof this.options_.userActions.click === 'function') { 23970 this.options_.userActions.click.call(this, event); 23971 } else if (this.paused()) { 23972 silencePromise(this.play()); 23973 } else { 23974 this.pause(); 23975 } 23976 } 23977 } 23978 23979 /** 23980 * Handle a double-click on the media element to enter/exit fullscreen 23981 * 23982 * @param {Event} event 23983 * the event that caused this function to trigger 23984 * 23985 * @listens Tech#dblclick 23986 * @private 23987 */ 23988 handleTechDoubleClick_(event) { 23989 if (!this.controls_) { 23990 return; 23991 } 23992 23993 // we do not want to toggle fullscreen state 23994 // when double-clicking inside a control bar or a modal 23995 const inAllowedEls = Array.prototype.some.call(this.$$('.vjs-control-bar, .vjs-modal-dialog'), el => el.contains(event.target)); 23996 if (!inAllowedEls) { 23997 /* 23998 * options.userActions.doubleClick 23999 * 24000 * If `undefined` or `true`, double-click toggles fullscreen if controls are present 24001 * Set to `false` to disable double-click handling 24002 * Set to a function to substitute an external double-click handler 24003 */ 24004 if (this.options_ === undefined || this.options_.userActions === undefined || this.options_.userActions.doubleClick === undefined || this.options_.userActions.doubleClick !== false) { 24005 if (this.options_ !== undefined && this.options_.userActions !== undefined && typeof this.options_.userActions.doubleClick === 'function') { 24006 this.options_.userActions.doubleClick.call(this, event); 24007 } else if (this.isFullscreen()) { 24008 this.exitFullscreen(); 24009 } else { 24010 this.requestFullscreen(); 24011 } 24012 } 24013 } 24014 } 24015 24016 /** 24017 * Handle a tap on the media element. It will toggle the user 24018 * activity state, which hides and shows the controls. 24019 * 24020 * @listens Tech#tap 24021 * @private 24022 */ 24023 handleTechTap_() { 24024 this.userActive(!this.userActive()); 24025 } 24026 24027 /** 24028 * Handle touch to start 24029 * 24030 * @listens Tech#touchstart 24031 * @private 24032 */ 24033 handleTechTouchStart_() { 24034 this.userWasActive = this.userActive(); 24035 } 24036 24037 /** 24038 * Handle touch to move 24039 * 24040 * @listens Tech#touchmove 24041 * @private 24042 */ 24043 handleTechTouchMove_() { 24044 if (this.userWasActive) { 24045 this.reportUserActivity(); 24046 } 24047 } 24048 24049 /** 24050 * Handle touch to end 24051 * 24052 * @param {Event} event 24053 * the touchend event that triggered 24054 * this function 24055 * 24056 * @listens Tech#touchend 24057 * @private 24058 */ 24059 handleTechTouchEnd_(event) { 24060 // Stop the mouse events from also happening 24061 if (event.cancelable) { 24062 event.preventDefault(); 24063 } 24064 } 24065 24066 /** 24067 * @private 24068 */ 24069 toggleFullscreenClass_() { 24070 if (this.isFullscreen()) { 24071 this.addClass('vjs-fullscreen'); 24072 } else { 24073 this.removeClass('vjs-fullscreen'); 24074 } 24075 } 24076 24077 /** 24078 * when the document fschange event triggers it calls this 24079 */ 24080 documentFullscreenChange_(e) { 24081 const targetPlayer = e.target.player; 24082 24083 // if another player was fullscreen 24084 // do a null check for targetPlayer because older firefox's would put document as e.target 24085 if (targetPlayer && targetPlayer !== this) { 24086 return; 24087 } 24088 const el = this.el(); 24089 let isFs = document
vendor: 10,170 bytes, lines 24089-24436
24089[this.fsApi_.fullscreenElement] === el; 24090 if (!isFs && el.matches) { 24091 isFs = el.matches(':' + this.fsApi_.fullscreen); 24092 } 24093 this.isFullscreen(isFs); 24094 } 24095 24096 /** 24097 * Handle Tech Fullscreen Change 24098 * 24099 * @param {Event} event 24100 * the fullscreenchange event that triggered this function 24101 * 24102 * @param {Object} data 24103 * the data that was sent with the event 24104 * 24105 * @private 24106 * @listens Tech#fullscreenchange 24107 * @fires Player#fullscreenchange 24108 */ 24109 handleTechFullscreenChange_(event, data) { 24110 if (data) { 24111 if (data.nativeIOSFullscreen) { 24112 this.addClass('vjs-ios-native-fs'); 24113 this.tech_.one('webkitendfullscreen', () => { 24114 this.removeClass('vjs-ios-native-fs'); 24115 }); 24116 } 24117 this.isFullscreen(data.isFullscreen); 24118 } 24119 } 24120 handleTechFullscreenError_(event, err) { 24121 this.trigger('fullscreenerror', err); 24122 } 24123 24124 /** 24125 * @private 24126 */ 24127 togglePictureInPictureClass_() { 24128 if (this.isInPictureInPicture()) { 24129 this.addClass('vjs-picture-in-picture'); 24130 } else { 24131 this.removeClass('vjs-picture-in-picture'); 24132 } 24133 } 24134 24135 /** 24136 * Handle Tech Enter Picture-in-Picture. 24137 * 24138 * @param {Event} event 24139 * the enterpictureinpicture event that triggered this function 24140 * 24141 * @private 24142 * @listens Tech#enterpictureinpicture 24143 */ 24144 handleTechEnterPictureInPicture_(event) { 24145 this.isInPictureInPicture(true); 24146 } 24147 24148 /** 24149 * Handle Tech Leave Picture-in-Picture. 24150 * 24151 * @param {Event} event 24152 * the leavepictureinpicture event that triggered this function 24153 * 24154 * @private 24155 * @listens Tech#leavepictureinpicture 24156 */ 24157 handleTechLeavePictureInPicture_(event) { 24158 this.isInPictureInPicture(false); 24159 } 24160 24161 /** 24162 * Fires when an error occurred during the loading of an audio/video. 24163 * 24164 * @private 24165 * @listens Tech#error 24166 */ 24167 handleTechError_() { 24168 const error = this.tech_.error(); 24169 if (error) { 24170 this.error(error); 24171 } 24172 } 24173 24174 /** 24175 * Retrigger the `textdata` event that was triggered by the {@link Tech}. 24176 * 24177 * @fires Player#textdata 24178 * @listens Tech#textdata 24179 * @private 24180 */ 24181 handleTechTextData_() { 24182 let data = null; 24183 if (arguments.length > 1) { 24184 data = arguments[1]; 24185 } 24186 24187 /** 24188 * Fires when we get a textdata event from tech 24189 * 24190 * @event Player#textdata 24191 * @type {Event} 24192 */ 24193 this.trigger('textdata', data); 24194 } 24195 24196 /** 24197 * Get object for cached values. 24198 * 24199 * @return {Object} 24200 * get the current object cache 24201 */ 24202 getCache() { 24203 return this.cache_; 24204 } 24205 24206 /** 24207 * Resets the internal cache object. 24208 * 24209 * Using this function outside the player constructor or reset method may 24210 * have unintended side-effects. 24211 * 24212 * @private 24213 */ 24214 resetCache_() { 24215 this.cache_ = { 24216 // Right now, the currentTime is not _really_ cached because it is always 24217 // retrieved from the tech (see: currentTime). However, for completeness, 24218 // we set it to zero here to ensure that if we do start actually caching 24219 // it, we reset it along with everything else. 24220 currentTime: 0, 24221 initTime: 0, 24222 inactivityTimeout: this.options_.inactivityTimeout, 24223 duration: NaN, 24224 lastVolume: 1, 24225 lastPlaybackRate: this.defaultPlaybackRate(), 24226 media: null, 24227 src: '', 24228 source: {}, 24229 sources: [], 24230 playbackRates: [], 24231 volume: 1 24232 }; 24233 } 24234 24235 /** 24236 * Pass values to the playback tech 24237 * 24238 * @param {string} [method] 24239 * the method to call 24240 * 24241 * @param {Object} [arg] 24242 * the argument to pass 24243 * 24244 * @private 24245 */ 24246 techCall_(method, arg) { 24247 // If it's not ready yet, call method when it is 24248 24249 this.ready(function () { 24250 if (method in allowedSetters) { 24251 return set(this.middleware_, this.tech_, method, arg); 24252 } else if (method in allowedMediators) { 24253 return mediate(this.middleware_, this.tech_, method, arg); 24254 } 24255 try { 24256 if (this.tech_) { 24257 this.tech_[method](arg); 24258 } 24259 } catch (e) { 24260 log$1(e); 24261 throw e; 24262 } 24263 }, true); 24264 } 24265 24266 /** 24267 * Mediate attempt to call playback tech method 24268 * and return the value of the method called. 24269 * 24270 * @param {string} method 24271 * Tech method 24272 * 24273 * @return {*} 24274 * Value returned by the tech method called, undefined if tech 24275 * is not ready or tech method is not present 24276 * 24277 * @private 24278 */ 24279 techGet_(method) { 24280 if (!this.tech_ || !this.tech_.isReady_) { 24281 return; 24282 } 24283 if (method in allowedGetters) { 24284 return get(this.middleware_, this.tech_, method); 24285 } else if (method in allowedMediators) { 24286 return mediate(this.middleware_, this.tech_, method); 24287 } 24288 24289 // Log error when playback tech object is present but method 24290 // is undefined or unavailable 24291 try { 24292 return this.tech_[method](); 24293 } catch (e) { 24294 // When building additional tech libs, an expected method may not be defined yet 24295 if (this.tech_[method] === undefined) { 24296 log$1(`Video.js: ${method} method not defined for ${this.techName_} playback technology.`, e); 24297 throw e; 24298 } 24299 24300 // When a method isn't available on the object it throws a TypeError 24301 if (e.name === 'TypeError') { 24302 log$1(`Video.js: ${method} unavailable on ${this.techName_} playback technology element.`, e); 24303 this.tech_.isReady_ = false; 24304 throw e; 24305 } 24306 24307 // If error unknown, just log and throw 24308 log$1(e); 24309 throw e; 24310 } 24311 } 24312 24313 /** 24314 * Attempt to begin playback at the first opportunity. 24315 * 24316 * @return {Promise|undefined} 24317 * Returns a promise if the browser supports Promises (or one 24318 * was passed in as an option). This promise will be resolved on 24319 * the return value of play. If this is undefined it will fulfill the 24320 * promise chain otherwise the promise chain will be fulfilled when 24321 * the promise from play is fulfilled. 24322 */ 24323 play() { 24324 return new Promise(resolve => { 24325 this.play_(resolve); 24326 }); 24327 } 24328 24329 /** 24330 * The actual logic for play, takes a callback that will be resolved on the 24331 * return value of play. This allows us to resolve to the play promise if there 24332 * is one on modern browsers. 24333 * 24334 * @private 24335 * @param {Function} [callback] 24336 * The callback that should be called when the techs play is actually called 24337 */ 24338 play_(callback = silencePromise) { 24339 this.playCallbacks_.push(callback); 24340 const isSrcReady = Boolean(!this.changingSrc_ && (this.src() || this.currentSrc())); 24341 const isSafariOrIOS = Boolean(IS_ANY_SAFARI || IS_IOS); 24342 24343 // treat calls to play_ somewhat like the `one` event function 24344 if (this.waitToPlay_) { 24345 this.off(['ready', 'loadstart'], this.waitToPlay_); 24346 this.waitToPlay_ = null; 24347 } 24348 24349 // if the player/tech is not ready or the src itself is not ready 24350 // queue up a call to play on `ready` or `loadstart` 24351 if (!this.isReady_ || !isSrcReady) { 24352 this.waitToPlay_ = e => { 24353 this.play_(); 24354 }; 24355 this.one(['ready', 'loadstart'], this.waitToPlay_); 24356 24357 // if we are in Safari, there is a high chance that loadstart will trigger after the gesture timeperiod 24358 // in that case, we need to prime the video element by calling load so it'll be ready in time 24359 if (!isSrcReady && isSafariOrIOS) { 24360 this.load(); 24361 } 24362 return; 24363 } 24364 24365 // If the player/tech is ready and we have a source, we can attempt playback. 24366 const val = this.techGet_('play'); 24367 24368 // For native playback, reset the progress bar if we get a play call from a replay. 24369 const isNativeReplay = isSafariOrIOS && this.hasClass('vjs-ended'); 24370 if (isNativeReplay) { 24371 this.resetProgressBar_(); 24372 } 24373 // play was terminated if the returned value is null 24374 if (val === null) { 24375 this.runPlayTerminatedQueue_(); 24376 } else { 24377 this.runPlayCallbacks_(val); 24378 } 24379 } 24380 24381 /** 24382 * These functions will be run when if play is terminated. If play 24383 * runPlayCallbacks_ is run these function will not be run. This allows us 24384 * to differentiate between a terminated play and an actual call to play. 24385 */ 24386 runPlayTerminatedQueue_() { 24387 const queue = this.playTerminatedQueue_.slice(0); 24388 this.playTerminatedQueue_ = []; 24389 queue.forEach(function (q) { 24390 q(); 24391 }); 24392 } 24393 24394 /** 24395 * When a callback to play is delayed we have to run these 24396 * callbacks when play is actually called on the tech. This function 24397 * runs the callbacks that were delayed and accepts the return value 24398 * from the tech. 24399 * 24400 * @param {undefined|Promise} val 24401 * The return value from the tech. 24402 */ 24403 runPlayCallbacks_(val) { 24404 const callbacks = this.playCallbacks_.slice(0); 24405 this.playCallbacks_ = []; 24406 // clear play terminatedQueue since we finished a real play 24407 this.playTerminatedQueue_ = []; 24408 callbacks.forEach(function (cb) { 24409 cb(val); 24410 }); 24411 } 24412 24413 /** 24414 * Pause the video playback 24415 */ 24416 pause() { 24417 this.techCall_('pause'); 24418 } 24419 24420 /** 24421 * Check if the player is paused or has yet to play 24422 * 24423 * @return {boolean} 24424 * - false: if the media is currently playing 24425 * - true: if media is not currently playing 24426 */ 24427 paused() { 24428 // The initial state of paused should be true (in Safari it's actually false) 24429 return this.techGet_('paused') === false ? false : true; 24430 } 24431 24432 /** 24433 * Get a TimeRange object representing the current ranges of time that the user 24434 * has played. 24435 * 24436 * @return {
24436 import('./utils/time').TimeRange } 24437 * A time range object that represents all the increments of time that have 24438 * been played. 24439 */ 24440 played() { 24441 return this.techGet_('played') || createTimeRanges$1(0, 0); 24442 } 24443 24444 /** 24445 * Sets or returns whether or not the user is "scrubbing". Scrubbing is 24446 * when the user has clicked the progress bar handle and is 24447 * dragging it along the progress bar. 24448 * 24449 * @param {boolean} [isScrubbing] 24450 * whether the user is or is not scrubbing 24451 * 24452 * @return {boolean|undefined} 24453 * - The value of scrubbing when getting 24454 * - Nothing when setting 24455 */ 24456 scrubbing(isScrubbing) { 24457 if (typeof isScrubbing === 'undefined') { 24458 return this.scrubbing_; 24459 } 24460 this.scrubbing_ = !!isScrubbing; 24461 this.techCall_('setScrubbing', this.scrubbing_); 24462 if (isScrubbing) { 24463 this.addClass('vjs-scrubbing'); 24464 } else { 24465 this.removeClass('vjs-scrubbing'); 24466 } 24467 } 24468 24469 /** 24470 * Get or set the current time (in seconds) 24471 * 24472 * @param {number|string} [seconds] 24473 * The time to seek to in seconds 24474 * 24475 * @return {number|undefined} 24476 * - the current time in seconds when getting 24477 * - Nothing when setting 24478 */ 24479 currentTime(seconds) { 24480 if (seconds === undefined) { 24481 // cache last currentTime and return. default to 0 seconds 24482 // 24483 // Caching the currentTime is meant to prevent a massive amount of reads on the tech's
vendor: 4,287 bytes, lines 24484-24613
24484 // currentTime when scrubbing, but may not provide much performance benefit after all. 24485 // Should be tested. Also something has to read the actual current time or the cache will 24486 // never get updated. 24487 this.cache_.currentTime = this.techGet_('currentTime') || 0; 24488 return this.cache_.currentTime; 24489 } 24490 if (seconds < 0) { 24491 seconds = 0; 24492 } 24493 if (!this.isReady_ || this.changingSrc_ || !this.tech_ || !this.tech_.isReady_) { 24494 this.cache_.initTime = seconds; 24495 this.off('canplay', this.boundApplyInitTime_); 24496 this.one('canplay', this.boundApplyInitTime_); 24497 return; 24498 } 24499 this.techCall_('setCurrentTime', seconds); 24500 this.cache_.initTime = 0; 24501 if (isFinite(seconds)) { 24502 this.cache_.currentTime = Number(seconds); 24503 } 24504 } 24505 24506 /** 24507 * Apply the value of initTime stored in cache as currentTime. 24508 * 24509 * @private 24510 */ 24511 applyInitTime_() { 24512 this.currentTime(this.cache_.initTime); 24513 } 24514 24515 /** 24516 * Normally gets the length in time of the video in seconds; 24517 * in all but the rarest use cases an argument will NOT be passed to the method 24518 * 24519 * > **NOTE**: The video must have started loading before the duration can be 24520 * known, and depending on preload behaviour may not be known until the video starts 24521 * playing. 24522 * 24523 * @fires Player#durationchange 24524 * 24525 * @param {number} [seconds] 24526 * The duration of the video to set in seconds 24527 * 24528 * @return {number|undefined} 24529 * - The duration of the video in seconds when getting 24530 * - Nothing when setting 24531 */ 24532 duration(seconds) { 24533 if (seconds === undefined) { 24534 // return NaN if the duration is not known 24535 return this.cache_.duration !== undefined ? this.cache_.duration : NaN; 24536 } 24537 seconds = parseFloat(seconds); 24538 24539 // Standardize on Infinity for signaling video is live 24540 if (seconds < 0) { 24541 seconds = Infinity; 24542 } 24543 if (seconds !== this.cache_.duration) { 24544 // Cache the last set value for optimized scrubbing 24545 this.cache_.duration = seconds; 24546 if (seconds === Infinity) { 24547 this.addClass('vjs-live'); 24548 } else { 24549 this.removeClass('vjs-live'); 24550 } 24551 if (!isNaN(seconds)) { 24552 // Do not fire durationchange unless the duration value is known. 24553 // @see [Spec]{@link https://www.w3.org/TR/2011/WD-html5-20110113/video.html#media-element-load-algorithm} 24554 24555 /** 24556 * @event Player#durationchange 24557 * @type {Event} 24558 */ 24559 this.trigger('durationchange'); 24560 } 24561 } 24562 } 24563 24564 /** 24565 * Calculates how much time is left in the video. Not part 24566 * of the native video API. 24567 * 24568 * @return {number} 24569 * The time remaining in seconds 24570 */ 24571 remainingTime() { 24572 return this.duration() - this.currentTime(); 24573 } 24574 24575 /** 24576 * A remaining time function that is intended to be used when 24577 * the time is to be displayed directly to the user. 24578 * 24579 * @return {number} 24580 * The rounded time remaining in seconds 24581 */ 24582 remainingTimeDisplay() { 24583 return Math.floor(this.duration()) - Math.floor(this.currentTime()); 24584 } 24585 24586 // 24587 // Kind of like an array of portions of the video that have been downloaded. 24588 24589 /** 24590 * Get a TimeRange object with an array of the times of the video 24591 * that have been downloaded. If you just want the percent of the 24592 * video that's been downloaded, use bufferedPercent. 24593 * 24594 * @see [Buffered Spec]{@link http://dev.w3.org/html5/spec/video.html#dom-media-buffered} 24595 * 24596 * @return { import('./utils/time').TimeRange } 24597 * A mock {@link TimeRanges} object (following HTML spec) 24598 */ 24599 buffered() { 24600 let buffered = this.techGet_('buffered'); 24601 if (!buffered || !buffered.length) { 24602 buffered = createTimeRanges$1(0, 0); 24603 } 24604 return buffered; 24605 } 24606 24607 /** 24608 * Get the TimeRanges of the media that are currently available 24609 * for seeking to. 24610 * 24611 * @see [Seekable Spec]{@link https://html.spec.whatwg.org/multipage/media.html#dom-media-seekable} 24612 * 24613 * @return { import('./utils/time').TimeRange
vendor: 12,814 bytes, lines 24613-24985
24613} 24614 * A mock {@link TimeRanges} object (following HTML spec) 24615 */ 24616 seekable() { 24617 let seekable = this.techGet_('seekable'); 24618 if (!seekable || !seekable.length) { 24619 seekable = createTimeRanges$1(0, 0); 24620 } 24621 return seekable; 24622 } 24623 24624 /** 24625 * Returns whether the player is in the "seeking" state. 24626 * 24627 * @return {boolean} True if the player is in the seeking state, false if not. 24628 */ 24629 seeking() { 24630 return this.techGet_('seeking'); 24631 } 24632 24633 /** 24634 * Returns whether the player is in the "ended" state. 24635 * 24636 * @return {boolean} True if the player is in the ended state, false if not. 24637 */ 24638 ended() { 24639 return this.techGet_('ended'); 24640 } 24641 24642 /** 24643 * Returns the current state of network activity for the element, from 24644 * the codes in the list below. 24645 * - NETWORK_EMPTY (numeric value 0) 24646 * The element has not yet been initialised. All attributes are in 24647 * their initial states. 24648 * - NETWORK_IDLE (numeric value 1) 24649 * The element's resource selection algorithm is active and has 24650 * selected a resource, but it is not actually using the network at 24651 * this time. 24652 * - NETWORK_LOADING (numeric value 2) 24653 * The user agent is actively trying to download data. 24654 * - NETWORK_NO_SOURCE (numeric value 3) 24655 * The element's resource selection algorithm is active, but it has 24656 * not yet found a resource to use. 24657 * 24658 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#network-states 24659 * @return {number} the current network activity state 24660 */ 24661 networkState() { 24662 return this.techGet_('networkState'); 24663 } 24664 24665 /** 24666 * Returns a value that expresses the current state of the element 24667 * with respect to rendering the current playback position, from the 24668 * codes in the list below. 24669 * - HAVE_NOTHING (numeric value 0) 24670 * No information regarding the media resource is available. 24671 * - HAVE_METADATA (numeric value 1) 24672 * Enough of the resource has been obtained that the duration of the 24673 * resource is available. 24674 * - HAVE_CURRENT_DATA (numeric value 2) 24675 * Data for the immediate current playback position is available. 24676 * - HAVE_FUTURE_DATA (numeric value 3) 24677 * Data for the immediate current playback position is available, as 24678 * well as enough data for the user agent to advance the current 24679 * playback position in the direction of playback. 24680 * - HAVE_ENOUGH_DATA (numeric value 4) 24681 * The user agent estimates that enough data is available for 24682 * playback to proceed uninterrupted. 24683 * 24684 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-readystate 24685 * @return {number} the current playback rendering state 24686 */ 24687 readyState() { 24688 return this.techGet_('readyState'); 24689 } 24690 24691 /** 24692 * Get the percent (as a decimal) of the video that's been downloaded. 24693 * This method is not a part of the native HTML video API. 24694 * 24695 * @return {number} 24696 * A decimal between 0 and 1 representing the percent 24697 * that is buffered 0 being 0% and 1 being 100% 24698 */ 24699 bufferedPercent() { 24700 return bufferedPercent(this.buffered(), this.duration()); 24701 } 24702 24703 /** 24704 * Get the ending time of the last buffered time range 24705 * This is used in the progress bar to encapsulate all time ranges. 24706 * 24707 * @return {number} 24708 * The end of the last buffered time range 24709 */ 24710 bufferedEnd() { 24711 const buffered = this.buffered(); 24712 const duration = this.duration(); 24713 let end = buffered.end(buffered.length - 1); 24714 if (end > duration) { 24715 end = duration; 24716 } 24717 return end; 24718 } 24719 24720 /** 24721 * Get or set the current volume of the media 24722 * 24723 * @param {number} [percentAsDecimal] 24724 * The new volume as a decimal percent: 24725 * - 0 is muted/0%/off 24726 * - 1.0 is 100%/full 24727 * - 0.5 is half volume or 50% 24728 * 24729 * @return {number|undefined} 24730 * The current volume as a percent when getting 24731 */ 24732 volume(percentAsDecimal) { 24733 let vol; 24734 if (percentAsDecimal !== undefined) { 24735 // Force value to between 0 and 1 24736 vol = Math.max(0, Math.min(1, percentAsDecimal)); 24737 this.cache_.volume = vol; 24738 this.techCall_('setVolume', vol); 24739 if (vol > 0) { 24740 this.lastVolume_(vol); 24741 } 24742 return; 24743 } 24744 24745 // Default to 1 when returning current volume. 24746 vol = parseFloat(this.techGet_('volume')); 24747 return isNaN(vol) ? 1 : vol; 24748 } 24749 24750 /** 24751 * Get the current muted state, or turn mute on or off 24752 * 24753 * @param {boolean} [muted] 24754 * - true to mute 24755 * - false to unmute 24756 * 24757 * @return {boolean|undefined} 24758 * - true if mute is on and getting 24759 * - false if mute is off and getting 24760 * - nothing if setting 24761 */ 24762 muted(muted) { 24763 if (muted !== undefined) { 24764 this.techCall_('setMuted', muted); 24765 return; 24766 } 24767 return this.techGet_('muted') || false; 24768 } 24769 24770 /** 24771 * Get the current defaultMuted state, or turn defaultMuted on or off. defaultMuted 24772 * indicates the state of muted on initial playback. 24773 * 24774 * ```js 24775 * var myPlayer = videojs('some-player-id'); 24776 * 24777 * myPlayer.src("http://www.example.com/path/to/video.mp4"); 24778 * 24779 * // get, should be false 24780 * console.log(myPlayer.defaultMuted()); 24781 * // set to true 24782 * myPlayer.defaultMuted(true); 24783 * // get should be true 24784 * console.log(myPlayer.defaultMuted()); 24785 * ``` 24786 * 24787 * @param {boolean} [defaultMuted] 24788 * - true to mute 24789 * - false to unmute 24790 * 24791 * @return {boolean|undefined} 24792 * - true if defaultMuted is on and getting 24793 * - false if defaultMuted is off and getting 24794 * - Nothing when setting 24795 */ 24796 defaultMuted(defaultMuted) { 24797 if (defaultMuted !== undefined) { 24798 this.techCall_('setDefaultMuted', defaultMuted); 24799 } 24800 return this.techGet_('defaultMuted') || false; 24801 } 24802 24803 /** 24804 * Get the last volume, or set it 24805 * 24806 * @param {number} [percentAsDecimal] 24807 * The new last volume as a decimal percent: 24808 * - 0 is muted/0%/off 24809 * - 1.0 is 100%/full 24810 * - 0.5 is half volume or 50% 24811 * 24812 * @return {number|undefined} 24813 * - The current value of lastVolume as a percent when getting 24814 * - Nothing when setting 24815 * 24816 * @private 24817 */ 24818 lastVolume_(percentAsDecimal) { 24819 if (percentAsDecimal !== undefined && percentAsDecimal !== 0) { 24820 this.cache_.lastVolume = percentAsDecimal; 24821 return; 24822 } 24823 return this.cache_.lastVolume; 24824 } 24825 24826 /** 24827 * Check if current tech can support native fullscreen 24828 * (e.g. with built in controls like iOS) 24829 * 24830 * @return {boolean} 24831 * if native fullscreen is supported 24832 */ 24833 supportsFullScreen() { 24834 return this.techGet_('supportsFullScreen') || false; 24835 } 24836 24837 /** 24838 * Check if the player is in fullscreen mode or tell the player that it 24839 * is or is not in fullscreen mode. 24840 * 24841 * > NOTE: As of the latest HTML5 spec, isFullscreen is no longer an official 24842 * property and instead document.fullscreenElement is used. But isFullscreen is 24843 * still a valuable property for internal player workings. 24844 * 24845 * @param {boolean} [isFS] 24846 * Set the players current fullscreen state 24847 * 24848 * @return {boolean|undefined} 24849 * - true if fullscreen is on and getting 24850 * - false if fullscreen is off and getting 24851 * - Nothing when setting 24852 */ 24853 isFullscreen(isFS) { 24854 if (isFS !== undefined) { 24855 const oldValue = this.isFullscreen_; 24856 this.isFullscreen_ = Boolean(isFS); 24857 24858 // if we changed fullscreen state and we're in prefixed mode, trigger fullscreenchange 24859 // this is the only place where we trigger fullscreenchange events for older browsers 24860 // fullWindow mode is treated as a prefixed event and will get a fullscreenchange event as well 24861 if (this.isFullscreen_ !== oldValue && this.fsApi_.prefixed) { 24862 /** 24863 * @event Player#fullscreenchange 24864 * @type {Event} 24865 */ 24866 this.trigger('fullscreenchange'); 24867 } 24868 this.toggleFullscreenClass_(); 24869 return; 24870 } 24871 return this.isFullscreen_; 24872 } 24873 24874 /** 24875 * Increase the size of the video to full screen 24876 * In some browsers, full screen is not supported natively, so it enters 24877 * "full window mode", where the video fills the browser window. 24878 * In browsers and devices that support native full screen, sometimes the 24879 * browser's default controls will be shown, and not the Video.js custom skin. 24880 * This includes most mobile devices (iOS, Android) and older versions of 24881 * Safari. 24882 * 24883 * @param {Object} [fullscreenOptions] 24884 * Override the player fullscreen options 24885 * 24886 * @fires Player#fullscreenchange 24887 */ 24888 requestFullscreen(fullscreenOptions) { 24889 if (this.isInPictureInPicture()) { 24890 this.exitPictureInPicture(); 24891 } 24892 const self = this; 24893 return new Promise((resolve, reject) => { 24894 function offHandler() { 24895 self.off('fullscreenerror', errorHandler); 24896 self.off('fullscreenchange', changeHandler); 24897 } 24898 function changeHandler() { 24899 offHandler(); 24900 resolve(); 24901 } 24902 function errorHandler(e, err) { 24903 offHandler(); 24904 reject(err); 24905 } 24906 self.one('fullscreenchange', changeHandler); 24907 self.one('fullscreenerror', errorHandler); 24908 const promise = self.requestFullscreenHelper_(fullscreenOptions); 24909 if (promise) { 24910 promise.then(offHandler, offHandler); 24911 promise.then(resolve, reject); 24912 } 24913 }); 24914 } 24915 requestFullscreenHelper_(fullscreenOptions) { 24916 let fsOptions; 24917 24918 // Only pass fullscreen options to requestFullscreen in spec-compliant browsers. 24919 // Use defaults or player configured option unless passed directly to this method. 24920 if (!this.fsApi_.prefixed) { 24921 fsOptions = this.options_.fullscreen && this.options_.fullscreen.options || {}; 24922 if (fullscreenOptions !== undefined) { 24923 fsOptions = fullscreenOptions; 24924 } 24925 } 24926 24927 // This method works as follows: 24928 // 1. if a fullscreen api is available, use it 24929 // 1. call requestFullscreen with potential options 24930 // 2. if we got a promise from above, use it to update isFullscreen() 24931 // 2. otherwise, if the tech supports fullscreen, call `enterFullScreen` on it. 24932 // This is particularly used for iPhone, older iPads, and non-safari browser on iOS. 24933 // 3. otherwise, use "fullWindow" mode 24934 if (this.fsApi_.requestFullscreen) { 24935 const promise = this.el_[this.fsApi_.requestFullscreen](fsOptions); 24936 24937 // Even on browsers with promise support this may not return a promise 24938 if (promise) { 24939 promise.then(() => this.isFullscreen(true), () => this.isFullscreen(false)); 24940 } 24941 return promise; 24942 } else if (this.tech_.supportsFullScreen() && !this.options_.preferFullWindow === true) { 24943 // we can't take the video.js controls fullscreen but we can go fullscreen 24944 // with native controls 24945 this.techCall_('enterFullScreen'); 24946 } else { 24947 // fullscreen isn't supported so we'll just stretch the video element to 24948 // fill the viewport 24949 this.enterFullWindow(); 24950 } 24951 } 24952 24953 /** 24954 * Return the video to its normal size after having been in full screen mode 24955 * 24956 * @fires Player#fullscreenchange 24957 */ 24958 exitFullscreen() { 24959 const self = this; 24960 return new Promise((resolve, reject) => { 24961 function offHandler() { 24962 self.off('fullscreenerror', errorHandler); 24963 self.off('fullscreenchange', changeHandler); 24964 } 24965 function changeHandler() { 24966 offHandler(); 24967 resolve(); 24968 } 24969 function errorHandler(e, err) { 24970 offHandler(); 24971 reject(err); 24972 } 24973 self.one('fullscreenchange', changeHandler); 24974 self.one('fullscreenerror', errorHandler); 24975 const promise = self.exitFullscreenHelper_(); 24976 if (promise) { 24977 promise.then(offHandler, offHandler); 24978 // map the promise to our resolve/reject methods 24979 promise.then(resolve, reject); 24980 } 24981 }); 24982 } 24983 exitFullscreenHelper_() { 24984 if (this.fsApi_.requestFullscreen) { 24985 const promise = document
vendor: 10,438 bytes, lines 24985-25283
24985[this.fsApi_.exitFullscreen](); 24986 24987 // Even on browsers with promise support this may not return a promise 24988 if (promise) { 24989 // we're splitting the promise here, so, we want to catch the 24990 // potential error so that this chain doesn't have unhandled errors 24991 silencePromise(promise.then(() => this.isFullscreen(false))); 24992 } 24993 return promise; 24994 } else if (this.tech_.supportsFullScreen() && !this.options_.preferFullWindow === true) { 24995 this.techCall_('exitFullScreen'); 24996 } else { 24997 this.exitFullWindow(); 24998 } 24999 } 25000 25001 /** 25002 * When fullscreen isn't supported we can stretch the 25003 * video container to as wide as the browser will let us. 25004 * 25005 * @fires Player#enterFullWindow 25006 */ 25007 enterFullWindow() { 25008 this.isFullscreen(true); 25009 this.isFullWindow = true; 25010 25011 // Storing original doc overflow value to return to when fullscreen is off 25012 this.docOrigOverflow = document.documentElement.style.overflow; 25013 25014 // Add listener for esc key to exit fullscreen 25015 on(document, 'keydown', this.boundFullWindowOnEscKey_); 25016 25017 // Hide any scroll bars 25018 document.documentElement.style.overflow = 'hidden'; 25019 25020 // Apply fullscreen styles 25021 addClass(document.body, 'vjs-full-window'); 25022 25023 /** 25024 * @event Player#enterFullWindow 25025 * @type {Event} 25026 */ 25027 this.trigger('enterFullWindow'); 25028 } 25029 25030 /** 25031 * Check for call to either exit full window or 25032 * full screen on ESC key 25033 * 25034 * @param {string} event 25035 * Event to check for key press 25036 */ 25037 fullWindowOnEscKey(event) { 25038 if (keycode.isEventKey(event, 'Esc')) { 25039 if (this.isFullscreen() === true) { 25040 if (!this.isFullWindow) { 25041 this.exitFullscreen(); 25042 } else { 25043 this.exitFullWindow(); 25044 } 25045 } 25046 } 25047 } 25048 25049 /** 25050 * Exit full window 25051 * 25052 * @fires Player#exitFullWindow 25053 */ 25054 exitFullWindow() { 25055 this.isFullscreen(false); 25056 this.isFullWindow = false; 25057 off(document, 'keydown', this.boundFullWindowOnEscKey_); 25058 25059 // Unhide scroll bars. 25060 document.documentElement.style.overflow = this.docOrigOverflow; 25061 25062 // Remove fullscreen styles 25063 removeClass(document.body, 'vjs-full-window'); 25064 25065 // Resize the box, controller, and poster to original sizes 25066 // this.positionAll(); 25067 /** 25068 * @event Player#exitFullWindow 25069 * @type {Event} 25070 */ 25071 this.trigger('exitFullWindow'); 25072 } 25073 25074 /** 25075 * Get or set disable Picture-in-Picture mode. 25076 * 25077 * @param {boolean} [value] 25078 * - true will disable Picture-in-Picture mode 25079 * - false will enable Picture-in-Picture mode 25080 */ 25081 disablePictureInPicture(value) { 25082 if (value === undefined) { 25083 return this.techGet_('disablePictureInPicture'); 25084 } 25085 this.techCall_('setDisablePictureInPicture', value); 25086 this.options_.disablePictureInPicture = value; 25087 this.trigger('disablepictureinpicturechanged'); 25088 } 25089 25090 /** 25091 * Check if the player is in Picture-in-Picture mode or tell the player that it 25092 * is or is not in Picture-in-Picture mode. 25093 * 25094 * @param {boolean} [isPiP] 25095 * Set the players current Picture-in-Picture state 25096 * 25097 * @return {boolean|undefined} 25098 * - true if Picture-in-Picture is on and getting 25099 * - false if Picture-in-Picture is off and getting 25100 * - nothing if setting 25101 */ 25102 isInPictureInPicture(isPiP) { 25103 if (isPiP !== undefined) { 25104 this.isInPictureInPicture_ = !!isPiP; 25105 this.togglePictureInPictureClass_(); 25106 return; 25107 } 25108 return !!this.isInPictureInPicture_; 25109 } 25110 25111 /** 25112 * Create a floating video window always on top of other windows so that users may 25113 * continue consuming media while they interact with other content sites, or 25114 * applications on their device. 25115 * 25116 * This can use document picture-in-picture or element picture in picture 25117 * 25118 * Set `enableDocumentPictureInPicture` to `true` to use docPiP on a supported browser 25119 * Else set `disablePictureInPicture` to `false` to disable elPiP on a supported browser 25120 * 25121 * 25122 * @see [Spec]{@link https://w3c.github.io/picture-in-picture/} 25123 * @see [Spec]{@link https://wicg.github.io/document-picture-in-picture/} 25124 * 25125 * @fires Player#enterpictureinpicture 25126 * 25127 * @return {Promise} 25128 * A promise with a Picture-in-Picture window. 25129 */ 25130 requestPictureInPicture() { 25131 if (this.options_.enableDocumentPictureInPicture && window.documentPictureInPicture) { 25132 const pipContainer = document.createElement(this.el().tagName); 25133 pipContainer.classList = this.el().classList; 25134 pipContainer.classList.add('vjs-pip-container'); 25135 if (this.posterImage) { 25136 pipContainer.appendChild(this.posterImage.el().cloneNode(true)); 25137 } 25138 if (this.titleBar) { 25139 pipContainer.appendChild(this.titleBar.el().cloneNode(true)); 25140 } 25141 pipContainer.appendChild(createEl('p', { 25142 className: 'vjs-pip-text' 25143 }, {}, this.localize('Playing in picture-in-picture'))); 25144 return window.documentPictureInPicture.requestWindow({ 25145 // The aspect ratio won't be correct, Chrome bug https://crbug.com/1407629 25146 width: this.videoWidth(), 25147 height: this.videoHeight() 25148 }).then(pipWindow => { 25149 copyStyleSheetsToWindow(pipWindow); 25150 this.el_.parentNode.insertBefore(pipContainer, this.el_); 25151 pipWindow.document.body.appendChild(this.el_); 25152 pipWindow.document.body.classList.add('vjs-pip-window'); 25153 this.player_.isInPictureInPicture(true); 25154 this.player_.trigger('enterpictureinpicture'); 25155 25156 // Listen for the PiP closing event to move the video back. 25157 pipWindow.addEventListener('pagehide', event => { 25158 const pipVideo = event.target.querySelector('.video-js'); 25159 pipContainer.parentNode.replaceChild(pipVideo, pipContainer); 25160 this.player_.isInPictureInPicture(false); 25161 this.player_.trigger('leavepictureinpicture'); 25162 }); 25163 return pipWindow; 25164 }); 25165 } 25166 if ('pictureInPictureEnabled' in document && this.disablePictureInPicture() === false) { 25167 /** 25168 * This event fires when the player enters picture in picture mode 25169 * 25170 * @event Player#enterpictureinpicture 25171 * @type {Event} 25172 */ 25173 return this.techGet_('requestPictureInPicture'); 25174 } 25175 return Promise.reject('No PiP mode is available'); 25176 } 25177 25178 /** 25179 * Exit Picture-in-Picture mode. 25180 * 25181 * @see [Spec]{@link https://wicg.github.io/picture-in-picture} 25182 * 25183 * @fires Player#leavepictureinpicture 25184 * 25185 * @return {Promise} 25186 * A promise. 25187 */ 25188 exitPictureInPicture() { 25189 if (window.documentPictureInPicture && window.documentPictureInPicture.window) { 25190 // With documentPictureInPicture, Player#leavepictureinpicture is fired in the pagehide handler 25191 window.documentPictureInPicture.window.close(); 25192 return Promise.resolve(); 25193 } 25194 if ('pictureInPictureEnabled' in document) { 25195 /** 25196 * This event fires when the player leaves picture in picture mode 25197 * 25198 * @event Player#leavepictureinpicture 25199 * @type {Event} 25200 */ 25201 return document.exitPictureInPicture(); 25202 } 25203 } 25204 25205 /** 25206 * Called when this Player has focus and a key gets pressed down, or when 25207 * any Component of this player receives a key press that it doesn't handle. 25208 * This allows player-wide hotkeys (either as defined below, or optionally 25209 * by an external function). 25210 * 25211 * @param {KeyboardEvent} event 25212 * The `keydown` event that caused this function to be called. 25213 * 25214 * @listens keydown 25215 */ 25216 handleKeyDown(event) { 25217 const { 25218 userActions 25219 } = this.options_; 25220 25221 // Bail out if hotkeys are not configured. 25222 if (!userActions || !userActions.hotkeys) { 25223 return; 25224 } 25225 25226 // Function that determines whether or not to exclude an element from 25227 // hotkeys handling. 25228 const excludeElement = el => { 25229 const tagName = el.tagName.toLowerCase(); 25230 25231 // The first and easiest test is for `contenteditable` elements. 25232 if (el.isContentEditable) { 25233 return true; 25234 } 25235 25236 // Inputs matching these types will still trigger hotkey handling as 25237 // they are not text inputs. 25238 const allowedInputTypes = ['button', 'checkbox', 'hidden', 'radio', 'reset', 'submit']; 25239 if (tagName === 'input') { 25240 return allowedInputTypes.indexOf(el.type) === -1; 25241 } 25242 25243 // The final test is by tag name. These tags will be excluded entirely. 25244 const excludedTags = ['textarea']; 25245 return excludedTags.indexOf(tagName) !== -1; 25246 }; 25247 25248 // Bail out if the user is focused on an interactive form element. 25249 if (excludeElement(this.el_.ownerDocument.activeElement)) { 25250 return; 25251 } 25252 if (typeof userActions.hotkeys === 'function') { 25253 userActions.hotkeys.call(this, event); 25254 } else { 25255 this.handleHotkeys(event); 25256 } 25257 } 25258 25259 /** 25260 * Called when this Player receives a hotkey keydown event. 25261 * Supported player-wide hotkeys are: 25262 * 25263 * f - toggle fullscreen 25264 * m - toggle mute 25265 * k or Space - toggle play/pause 25266 * 25267 * @param {Event} event 25268 * The `keydown` event that caused this function to be called. 25269 */ 25270 handleHotkeys(event) { 25271 const hotkeys = this.options_.userActions ? this.options_.userActions.hotkeys : {}; 25272 25273 // set fullscreenKey, muteKey, playPauseKey from `hotkeys`, use defaults if not set 25274 const { 25275 fullscreenKey = keydownEvent => keycode.isEventKey(keydownEvent, 'f'), 25276 muteKey = keydownEvent => keycode.isEventKey(keydownEvent, 'm'), 25277 playPauseKey = keydownEvent => keycode.isEventKey(keydownEvent, 'k') || keycode.isEventKey(keydownEvent, 'Space') 25278 } = hotkeys; 25279 if (fullscreenKey.call(this, event)) { 25280 event.preventDefault(); 25281 event.stopPropagation(); 25282 const FSToggle = Component$1.getComponent('FullscreenToggle'); 25283 if (document
vendor: 23,977 bytes, lines 25283-26038
25283[this.fsApi_.fullscreenEnabled] !== false) { 25284 FSToggle.prototype.handleClick.call(this, event); 25285 } 25286 } else if (muteKey.call(this, event)) { 25287 event.preventDefault(); 25288 event.stopPropagation(); 25289 const MuteToggle = Component$1.getComponent('MuteToggle'); 25290 MuteToggle.prototype.handleClick.call(this, event); 25291 } else if (playPauseKey.call(this, event)) { 25292 event.preventDefault(); 25293 event.stopPropagation(); 25294 const PlayToggle = Component$1.getComponent('PlayToggle'); 25295 PlayToggle.prototype.handleClick.call(this, event); 25296 } 25297 } 25298 25299 /** 25300 * Check whether the player can play a given mimetype 25301 * 25302 * @see https://www.w3.org/TR/2011/WD-html5-20110113/video.html#dom-navigator-canplaytype 25303 * 25304 * @param {string} type 25305 * The mimetype to check 25306 * 25307 * @return {string} 25308 * 'probably', 'maybe', or '' (empty string) 25309 */ 25310 canPlayType(type) { 25311 let can; 25312 25313 // Loop through each playback technology in the options order 25314 for (let i = 0, j = this.options_.techOrder; i < j.length; i++) { 25315 const techName = j[i]; 25316 let tech = Tech.getTech(techName); 25317 25318 // Support old behavior of techs being registered as components. 25319 // Remove once that deprecated behavior is removed. 25320 if (!tech) { 25321 tech = Component$1.getComponent(techName); 25322 } 25323 25324 // Check if the current tech is defined before continuing 25325 if (!tech) { 25326 log$1.error(`The "${techName}" tech is undefined. Skipped browser support check for that tech.`); 25327 continue; 25328 } 25329 25330 // Check if the browser supports this technology 25331 if (tech.isSupported()) { 25332 can = tech.canPlayType(type); 25333 if (can) { 25334 return can; 25335 } 25336 } 25337 } 25338 return ''; 25339 } 25340 25341 /** 25342 * Select source based on tech-order or source-order 25343 * Uses source-order selection if `options.sourceOrder` is truthy. Otherwise, 25344 * defaults to tech-order selection 25345 * 25346 * @param {Array} sources 25347 * The sources for a media asset 25348 * 25349 * @return {Object|boolean} 25350 * Object of source and tech order or false 25351 */ 25352 selectSource(sources) { 25353 // Get only the techs specified in `techOrder` that exist and are supported by the 25354 // current platform 25355 const techs = this.options_.techOrder.map(techName => { 25356 return [techName, Tech.getTech(techName)]; 25357 }).filter(([techName, tech]) => { 25358 // Check if the current tech is defined before continuing 25359 if (tech) { 25360 // Check if the browser supports this technology 25361 return tech.isSupported(); 25362 } 25363 log$1.error(`The "${techName}" tech is undefined. Skipped browser support check for that tech.`); 25364 return false; 25365 }); 25366 25367 // Iterate over each `innerArray` element once per `outerArray` element and execute 25368 // `tester` with both. If `tester` returns a non-falsy value, exit early and return 25369 // that value. 25370 const findFirstPassingTechSourcePair = function (outerArray, innerArray, tester) { 25371 let found; 25372 outerArray.some(outerChoice => { 25373 return innerArray.some(innerChoice => { 25374 found = tester(outerChoice, innerChoice); 25375 if (found) { 25376 return true; 25377 } 25378 }); 25379 }); 25380 return found; 25381 }; 25382 let foundSourceAndTech; 25383 const flip = fn => (a, b) => fn(b, a); 25384 const finder = ([techName, tech], source) => { 25385 if (tech.canPlaySource(source, this.options_[techName.toLowerCase()])) { 25386 return { 25387 source, 25388 tech: techName 25389 }; 25390 } 25391 }; 25392 25393 // Depending on the truthiness of `options.sourceOrder`, we swap the order of techs and sources 25394 // to select from them based on their priority. 25395 if (this.options_.sourceOrder) { 25396 // Source-first ordering 25397 foundSourceAndTech = findFirstPassingTechSourcePair(sources, techs, flip(finder)); 25398 } else { 25399 // Tech-first ordering 25400 foundSourceAndTech = findFirstPassingTechSourcePair(techs, sources, finder); 25401 } 25402 return foundSourceAndTech || false; 25403 } 25404 25405 /** 25406 * Executes source setting and getting logic 25407 * 25408 * @param {Tech~SourceObject|Tech~SourceObject[]|string} [source] 25409 * A SourceObject, an array of SourceObjects, or a string referencing 25410 * a URL to a media source. It is _highly recommended_ that an object 25411 * or array of objects is used here, so that source selection 25412 * algorithms can take the `type` into account. 25413 * 25414 * If not provided, this method acts as a getter. 25415 * @param {boolean} [isRetry] 25416 * Indicates whether this is being called internally as a result of a retry 25417 * 25418 * @return {string|undefined} 25419 * If the `source` argument is missing, returns the current source 25420 * URL. Otherwise, returns nothing/undefined. 25421 */ 25422 handleSrc_(source, isRetry) { 25423 // getter usage 25424 if (typeof source === 'undefined') { 25425 return this.cache_.src || ''; 25426 } 25427 25428 // Reset retry behavior for new source 25429 if (this.resetRetryOnError_) { 25430 this.resetRetryOnError_(); 25431 } 25432 25433 // filter out invalid sources and turn our source into 25434 // an array of source objects 25435 const sources = filterSource(source); 25436 25437 // if a source was passed in then it is invalid because 25438 // it was filtered to a zero length Array. So we have to 25439 // show an error 25440 if (!sources.length) { 25441 this.setTimeout(function () { 25442 this.error({ 25443 code: 4, 25444 message: this.options_.notSupportedMessage 25445 }); 25446 }, 0); 25447 return; 25448 } 25449 25450 // initial sources 25451 this.changingSrc_ = true; 25452 25453 // Only update the cached source list if we are not retrying a new source after error, 25454 // since in that case we want to include the failed source(s) in the cache 25455 if (!isRetry) { 25456 this.cache_.sources = sources; 25457 } 25458 this.updateSourceCaches_(sources[0]); 25459 25460 // middlewareSource is the source after it has been changed by middleware 25461 setSource(this, sources[0], (middlewareSource, mws) => { 25462 this.middleware_ = mws; 25463 25464 // since sourceSet is async we have to update the cache again after we select a source since 25465 // the source that is selected could be out of order from the cache update above this callback. 25466 if (!isRetry) { 25467 this.cache_.sources = sources; 25468 } 25469 this.updateSourceCaches_(middlewareSource); 25470 const err = this.src_(middlewareSource); 25471 if (err) { 25472 if (sources.length > 1) { 25473 return this.handleSrc_(sources.slice(1)); 25474 } 25475 this.changingSrc_ = false; 25476 25477 // We need to wrap this in a timeout to give folks a chance to add error event handlers 25478 this.setTimeout(function () { 25479 this.error({ 25480 code: 4, 25481 message: this.options_.notSupportedMessage 25482 }); 25483 }, 0); 25484 25485 // we could not find an appropriate tech, but let's still notify the delegate that this is it 25486 // this needs a better comment about why this is needed 25487 this.triggerReady(); 25488 return; 25489 } 25490 setTech(mws, this.tech_); 25491 }); 25492 25493 // Try another available source if this one fails before playback. 25494 if (sources.length > 1) { 25495 const retry = () => { 25496 // Remove the error modal 25497 this.error(null); 25498 this.handleSrc_(sources.slice(1), true); 25499 }; 25500 const stopListeningForErrors = () => { 25501 this.off('error', retry); 25502 }; 25503 this.one('error', retry); 25504 this.one('playing', stopListeningForErrors); 25505 this.resetRetryOnError_ = () => { 25506 this.off('error', retry); 25507 this.off('playing', stopListeningForErrors); 25508 }; 25509 } 25510 } 25511 25512 /** 25513 * Get or set the video source. 25514 * 25515 * @param {Tech~SourceObject|Tech~SourceObject[]|string} [source] 25516 * A SourceObject, an array of SourceObjects, or a string referencing 25517 * a URL to a media source. It is _highly recommended_ that an object 25518 * or array of objects is used here, so that source selection 25519 * algorithms can take the `type` into account. 25520 * 25521 * If not provided, this method acts as a getter. 25522 * 25523 * @return {string|undefined} 25524 * If the `source` argument is missing, returns the current source 25525 * URL. Otherwise, returns nothing/undefined. 25526 */ 25527 src(source) { 25528 return this.handleSrc_(source, false); 25529 } 25530 25531 /** 25532 * Set the source object on the tech, returns a boolean that indicates whether 25533 * there is a tech that can play the source or not 25534 * 25535 * @param {Tech~SourceObject} source 25536 * The source object to set on the Tech 25537 * 25538 * @return {boolean} 25539 * - True if there is no Tech to playback this source 25540 * - False otherwise 25541 * 25542 * @private 25543 */ 25544 src_(source) { 25545 const sourceTech = this.selectSource([source]); 25546 if (!sourceTech) { 25547 return true; 25548 } 25549 if (!titleCaseEquals(sourceTech.tech, this.techName_)) { 25550 this.changingSrc_ = true; 25551 // load this technology with the chosen source 25552 this.loadTech_(sourceTech.tech, sourceTech.source); 25553 this.tech_.ready(() => { 25554 this.changingSrc_ = false; 25555 }); 25556 return false; 25557 } 25558 25559 // wait until the tech is ready to set the source 25560 // and set it synchronously if possible (#2326) 25561 this.ready(function () { 25562 // The setSource tech method was added with source handlers 25563 // so older techs won't support it 25564 // We need to check the direct prototype for the case where subclasses 25565 // of the tech do not support source handlers 25566 if (this.tech_.constructor.prototype.hasOwnProperty('setSource')) { 25567 this.techCall_('setSource', source); 25568 } else { 25569 this.techCall_('src', source.src); 25570 } 25571 this.changingSrc_ = false; 25572 }, true); 25573 return false; 25574 } 25575 25576 /** 25577 * Begin loading the src data. 25578 */ 25579 load() { 25580 // Workaround to use the load method with the VHS. 25581 // Does not cover the case when the load method is called directly from the mediaElement. 25582 if (this.tech_ && this.tech_.vhs) { 25583 this.src(this.currentSource()); 25584 return; 25585 } 25586 this.techCall_('load'); 25587 } 25588 25589 /** 25590 * Reset the player. Loads the first tech in the techOrder, 25591 * removes all the text tracks in the existing `tech`, 25592 * and calls `reset` on the `tech`. 25593 */ 25594 reset() { 25595 if (this.paused()) { 25596 this.doReset_(); 25597 } else { 25598 const playPromise = this.play(); 25599 silencePromise(playPromise.then(() => this.doReset_())); 25600 } 25601 } 25602 doReset_() { 25603 if (this.tech_) { 25604 this.tech_.clearTracks('text'); 25605 } 25606 this.removeClass('vjs-playing'); 25607 this.addClass('vjs-paused'); 25608 this.resetCache_(); 25609 this.poster(''); 25610 this.loadTech_(this.options_.techOrder[0], null); 25611 this.techCall_('reset'); 25612 this.resetControlBarUI_(); 25613 this.error(null); 25614 if (this.titleBar) { 25615 this.titleBar.update({ 25616 title: undefined, 25617 description: undefined 25618 }); 25619 } 25620 if (isEvented(this)) { 25621 this.trigger('playerreset'); 25622 } 25623 } 25624 25625 /** 25626 * Reset Control Bar's UI by calling sub-methods that reset 25627 * all of Control Bar's components 25628 */ 25629 resetControlBarUI_() { 25630 this.resetProgressBar_(); 25631 this.resetPlaybackRate_(); 25632 this.resetVolumeBar_(); 25633 } 25634 25635 /** 25636 * Reset tech's progress so progress bar is reset in the UI 25637 */ 25638 resetProgressBar_() { 25639 this.currentTime(0); 25640 const { 25641 currentTimeDisplay, 25642 durationDisplay, 25643 progressControl, 25644 remainingTimeDisplay 25645 } = this.controlBar || {}; 25646 const { 25647 seekBar 25648 } = progressControl || {}; 25649 if (currentTimeDisplay) { 25650 currentTimeDisplay.updateContent(); 25651 } 25652 if (durationDisplay) { 25653 durationDisplay.updateContent(); 25654 } 25655 if (remainingTimeDisplay) { 25656 remainingTimeDisplay.updateContent(); 25657 } 25658 if (seekBar) { 25659 seekBar.update(); 25660 if (seekBar.loadProgressBar) { 25661 seekBar.loadProgressBar.update(); 25662 } 25663 } 25664 } 25665 25666 /** 25667 * Reset Playback ratio 25668 */ 25669 resetPlaybackRate_() { 25670 this.playbackRate(this.defaultPlaybackRate()); 25671 this.handleTechRateChange_(); 25672 } 25673 25674 /** 25675 * Reset Volume bar 25676 */ 25677 resetVolumeBar_() { 25678 this.volume(1.0); 25679 this.trigger('volumechange'); 25680 } 25681 25682 /** 25683 * Returns all of the current source objects. 25684 * 25685 * @return {Tech~SourceObject[]} 25686 * The current source objects 25687 */ 25688 currentSources() { 25689 const source = this.currentSource(); 25690 const sources = []; 25691 25692 // assume `{}` or `{ src }` 25693 if (Object.keys(source).length !== 0) { 25694 sources.push(source); 25695 } 25696 return this.cache_.sources || sources; 25697 } 25698 25699 /** 25700 * Returns the current source object. 25701 * 25702 * @return {Tech~SourceObject} 25703 * The current source object 25704 */ 25705 currentSource() { 25706 return this.cache_.source || {}; 25707 } 25708 25709 /** 25710 * Returns the fully qualified URL of the current source value e.g. http://mysite.com/video.mp4 25711 * Can be used in conjunction with `currentType` to assist in rebuilding the current source object. 25712 * 25713 * @return {string} 25714 * The current source 25715 */ 25716 currentSrc() { 25717 return this.currentSource() && this.currentSource().src || ''; 25718 } 25719 25720 /** 25721 * Get the current source type e.g. video/mp4 25722 * This can allow you rebuild the current source object so that you could load the same 25723 * source and tech later 25724 * 25725 * @return {string} 25726 * The source MIME type 25727 */ 25728 currentType() { 25729 return this.currentSource() && this.currentSource().type || ''; 25730 } 25731 25732 /** 25733 * Get or set the preload attribute 25734 * 25735 * @param {'none'|'auto'|'metadata'} [value] 25736 * Preload mode to pass to tech 25737 * 25738 * @return {string|undefined} 25739 * - The preload attribute value when getting 25740 * - Nothing when setting 25741 */ 25742 preload(value) { 25743 if (value !== undefined) { 25744 this.techCall_('setPreload', value); 25745 this.options_.preload = value; 25746 return; 25747 } 25748 return this.techGet_('preload'); 25749 } 25750 25751 /** 25752 * Get or set the autoplay option. When this is a boolean it will 25753 * modify the attribute on the tech. When this is a string the attribute on 25754 * the tech will be removed and `Player` will handle autoplay on loadstarts. 25755 * 25756 * @param {boolean|'play'|'muted'|'any'} [value] 25757 * - true: autoplay using the browser behavior 25758 * - false: do not autoplay 25759 * - 'play': call play() on every loadstart 25760 * - 'muted': call muted() then play() on every loadstart 25761 * - 'any': call play() on every loadstart. if that fails call muted() then play(). 25762 * - *: values other than those listed here will be set `autoplay` to true 25763 * 25764 * @return {boolean|string|undefined} 25765 * - The current value of autoplay when getting 25766 * - Nothing when setting 25767 */ 25768 autoplay(value) { 25769 // getter usage 25770 if (value === undefined) { 25771 return this.options_.autoplay || false; 25772 } 25773 let techAutoplay; 25774 25775 // if the value is a valid string set it to that, or normalize `true` to 'play', if need be 25776 if (typeof value === 'string' && /(any|play|muted)/.test(value) || value === true && this.options_.normalizeAutoplay) { 25777 this.options_.autoplay = value; 25778 this.manualAutoplay_(typeof value === 'string' ? value : 'play'); 25779 techAutoplay = false; 25780 25781 // any falsy value sets autoplay to false in the browser, 25782 // lets do the same 25783 } else if (!value) { 25784 this.options_.autoplay = false; 25785 25786 // any other value (ie truthy) sets autoplay to true 25787 } else { 25788 this.options_.autoplay = true; 25789 } 25790 techAutoplay = typeof techAutoplay === 'undefined' ? this.options_.autoplay : techAutoplay; 25791 25792 // if we don't have a tech then we do not queue up 25793 // a setAutoplay call on tech ready. We do this because the 25794 // autoplay option will be passed in the constructor and we 25795 // do not need to set it twice 25796 if (this.tech_) { 25797 this.techCall_('setAutoplay', techAutoplay); 25798 } 25799 } 25800 25801 /** 25802 * Set or unset the playsinline attribute. 25803 * Playsinline tells the browser that non-fullscreen playback is preferred. 25804 * 25805 * @param {boolean} [value] 25806 * - true means that we should try to play inline by default 25807 * - false means that we should use the browser's default playback mode, 25808 * which in most cases is inline. iOS Safari is a notable exception 25809 * and plays fullscreen by default. 25810 * 25811 * @return {string|undefined} 25812 * - the current value of playsinline 25813 * - Nothing when setting 25814 * 25815 * @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline} 25816 */ 25817 playsinline(value) { 25818 if (value !== undefined) { 25819 this.techCall_('setPlaysinline', value); 25820 this.options_.playsinline = value; 25821 } 25822 return this.techGet_('playsinline'); 25823 } 25824 25825 /** 25826 * Get or set the loop attribute on the video element. 25827 * 25828 * @param {boolean} [value] 25829 * - true means that we should loop the video 25830 * - false means that we should not loop the video 25831 * 25832 * @return {boolean|undefined} 25833 * - The current value of loop when getting 25834 * - Nothing when setting 25835 */ 25836 loop(value) { 25837 if (value !== undefined) { 25838 this.techCall_('setLoop', value); 25839 this.options_.loop = value; 25840 return; 25841 } 25842 return this.techGet_('loop'); 25843 } 25844 25845 /** 25846 * Get or set the poster image source url 25847 * 25848 * @fires Player#posterchange 25849 * 25850 * @param {string} [src] 25851 * Poster image source URL 25852 * 25853 * @return {string|undefined} 25854 * - The current value of poster when getting 25855 * - Nothing when setting 25856 */ 25857 poster(src) { 25858 if (src === undefined) { 25859 return this.poster_; 25860 } 25861 25862 // The correct way to remove a poster is to set as an empty string 25863 // other falsey values will throw errors 25864 if (!src) { 25865 src = ''; 25866 } 25867 if (src === this.poster_) { 25868 return; 25869 } 25870 25871 // update the internal poster variable 25872 this.poster_ = src; 25873 25874 // update the tech's poster 25875 this.techCall_('setPoster', src); 25876 this.isPosterFromTech_ = false; 25877 25878 // alert components that the poster has been set 25879 /** 25880 * This event fires when the poster image is changed on the player. 25881 * 25882 * @event Player#posterchange 25883 * @type {Event} 25884 */ 25885 this.trigger('posterchange'); 25886 } 25887 25888 /** 25889 * Some techs (e.g. YouTube) can provide a poster source in an 25890 * asynchronous way. We want the poster component to use this 25891 * poster source so that it covers up the tech's controls. 25892 * (YouTube's play button). However we only want to use this 25893 * source if the player user hasn't set a poster through 25894 * the normal APIs. 25895 * 25896 * @fires Player#posterchange 25897 * @listens Tech#posterchange 25898 * @private 25899 */ 25900 handleTechPosterChange_() { 25901 if ((!this.poster_ || this.options_.techCanOverridePoster) && this.tech_ && this.tech_.poster) { 25902 const newPoster = this.tech_.poster() || ''; 25903 if (newPoster !== this.poster_) { 25904 this.poster_ = newPoster; 25905 this.isPosterFromTech_ = true; 25906 25907 // Let components know the poster has changed 25908 this.trigger('posterchange'); 25909 } 25910 } 25911 } 25912 25913 /** 25914 * Get or set whether or not the controls are showing. 25915 * 25916 * @fires Player#controlsenabled 25917 * 25918 * @param {boolean} [bool] 25919 * - true to turn controls on 25920 * - false to turn controls off 25921 * 25922 * @return {boolean|undefined} 25923 * - The current value of controls when getting 25924 * - Nothing when setting 25925 */ 25926 controls(bool) { 25927 if (bool === undefined) { 25928 return !!this.controls_; 25929 } 25930 bool = !!bool; 25931 25932 // Don't trigger a change event unless it actually changed 25933 if (this.controls_ === bool) { 25934 return; 25935 } 25936 this.controls_ = bool; 25937 if (this.usingNativeControls()) { 25938 this.techCall_('setControls', bool); 25939 } 25940 if (this.controls_) { 25941 this.removeClass('vjs-controls-disabled'); 25942 this.addClass('vjs-controls-enabled'); 25943 /** 25944 * @event Player#controlsenabled 25945 * @type {Event} 25946 */ 25947 this.trigger('controlsenabled'); 25948 if (!this.usingNativeControls()) { 25949 this.addTechControlsListeners_(); 25950 } 25951 } else { 25952 this.removeClass('vjs-controls-enabled'); 25953 this.addClass('vjs-controls-disabled'); 25954 /** 25955 * @event Player#controlsdisabled 25956 * @type {Event} 25957 */ 25958 this.trigger('controlsdisabled'); 25959 if (!this.usingNativeControls()) { 25960 this.removeTechControlsListeners_(); 25961 } 25962 } 25963 } 25964 25965 /** 25966 * Toggle native controls on/off. Native controls are the controls built into 25967 * devices (e.g. default iPhone controls) or other techs 25968 * (e.g. Vimeo Controls) 25969 * **This should only be set by the current tech, because only the tech knows 25970 * if it can support native controls** 25971 * 25972 * @fires Player#usingnativecontrols 25973 * @fires Player#usingcustomcontrols 25974 * 25975 * @param {boolean} [bool] 25976 * - true to turn native controls on 25977 * - false to turn native controls off 25978 * 25979 * @return {boolean|undefined} 25980 * - The current value of native controls when getting 25981 * - Nothing when setting 25982 */ 25983 usingNativeControls(bool) { 25984 if (bool === undefined) { 25985 return !!this.usingNativeControls_; 25986 } 25987 bool = !!bool; 25988 25989 // Don't trigger a change event unless it actually changed 25990 if (this.usingNativeControls_ === bool) { 25991 return; 25992 } 25993 this.usingNativeControls_ = bool; 25994 if (this.usingNativeControls_) { 25995 this.addClass('vjs-using-native-controls'); 25996 25997 /** 25998 * player is using the native device controls 25999 * 26000 * @event Player#usingnativecontrols 26001 * @type {Event} 26002 */ 26003 this.trigger('usingnativecontrols'); 26004 } else { 26005 this.removeClass('vjs-using-native-controls'); 26006 26007 /** 26008 * player is using the custom HTML controls 26009 * 26010 * @event Player#usingcustomcontrols 26011 * @type {Event} 26012 */ 26013 this.trigger('usingcustomcontrols'); 26014 } 26015 } 26016 26017 /** 26018 * Set or get the current MediaError 26019 * 26020 * @fires Player#error 26021 * 26022 * @param {MediaError|string|number} [err] 26023 * A MediaError or a string/number to be turned 26024 * into a MediaError 26025 * 26026 * @return {MediaError|null|undefined} 26027 * - The current MediaError when getting (or null) 26028 * - Nothing when setting 26029 */ 26030 error(err) { 26031 if (err === undefined) { 26032 return this.error_ || null; 26033 } 26034 26035 // allow hooks to modify error object 26036 hooks('beforeerror').forEach(hookFunction => { 26037 const newErr = hookFunction(this, err); 26038 if (!(isObject
vendor: 3,495 bytes, lines 26038-26147
26038$1(newErr) && !Array.isArray(newErr) || typeof newErr === 'string' || typeof newErr === 'number' || newErr === null)) { 26039 this.log.error('please return a value that MediaError expects in beforeerror hooks'); 26040 return; 26041 } 26042 err = newErr; 26043 }); 26044 26045 // Suppress the first error message for no compatible source until 26046 // user interaction 26047 if (this.options_.suppressNotSupportedError && err && err.code === 4) { 26048 const triggerSuppressedError = function () { 26049 this.error(err); 26050 }; 26051 this.options_.suppressNotSupportedError = false; 26052 this.any(['click', 'touchstart'], triggerSuppressedError); 26053 this.one('loadstart', function () { 26054 this.off(['click', 'touchstart'], triggerSuppressedError); 26055 }); 26056 return; 26057 } 26058 26059 // restoring to default 26060 if (err === null) { 26061 this.error_ = null; 26062 this.removeClass('vjs-error'); 26063 if (this.errorDisplay) { 26064 this.errorDisplay.close(); 26065 } 26066 return; 26067 } 26068 this.error_ = new MediaError(err); 26069 26070 // add the vjs-error classname to the player 26071 this.addClass('vjs-error'); 26072 26073 // log the name of the error type and any message 26074 // IE11 logs "[object object]" and required you to expand message to see error object 26075 log$1.error(`(CODE:${this.error_.code} ${MediaError.errorTypes[this.error_.code]})`, this.error_.message, this.error_); 26076 26077 /** 26078 * @event Player#error 26079 * @type {Event} 26080 */ 26081 this.trigger('error'); 26082 26083 // notify hooks of the per player error 26084 hooks('error').forEach(hookFunction => hookFunction(this, this.error_)); 26085 return; 26086 } 26087 26088 /** 26089 * Report user activity 26090 * 26091 * @param {Object} event 26092 * Event object 26093 */ 26094 reportUserActivity(event) { 26095 this.userActivity_ = true; 26096 } 26097 26098 /** 26099 * Get/set if user is active 26100 * 26101 * @fires Player#useractive 26102 * @fires Player#userinactive 26103 * 26104 * @param {boolean} [bool] 26105 * - true if the user is active 26106 * - false if the user is inactive 26107 * 26108 * @return {boolean|undefined} 26109 * - The current value of userActive when getting 26110 * - Nothing when setting 26111 */ 26112 userActive(bool) { 26113 if (bool === undefined) { 26114 return this.userActive_; 26115 } 26116 bool = !!bool; 26117 if (bool === this.userActive_) { 26118 return; 26119 } 26120 this.userActive_ = bool; 26121 if (this.userActive_) { 26122 this.userActivity_ = true; 26123 this.removeClass('vjs-user-inactive'); 26124 this.addClass('vjs-user-active'); 26125 /** 26126 * @event Player#useractive 26127 * @type {Event} 26128 */ 26129 this.trigger('useractive'); 26130 return; 26131 } 26132 26133 // Chrome/Safari/IE have bugs where when you change the cursor it can 26134 // trigger a mousemove event. This causes an issue when you're hiding 26135 // the cursor when the user is inactive, and a mousemove signals user 26136 // activity. Making it impossible to go into inactive mode. Specifically 26137 // this happens in fullscreen when we really need to hide the cursor. 26138 // 26139 // When this gets resolved in ALL browsers it can be removed 26140 // https://code.google.com/p/chromium/issues/detail?id=103041 26141 if (this.tech_) { 26142 this.tech_.one('mousemove', function (e) { 26143 e.stopPropagation(); 26144 e.preventDefault(); 26145 }); 26146 } 26147 this.userActivity_ = false;
vendor: 6,717 bytes, lines 26148-26330
26148 this.removeClass('vjs-user-active'); 26149 this.addClass('vjs-user-inactive'); 26150 /** 26151 * @event Player#userinactive 26152 * @type {Event} 26153 */ 26154 this.trigger('userinactive'); 26155 } 26156 26157 /** 26158 * Listen for user activity based on timeout value 26159 * 26160 * @private 26161 */ 26162 listenForUserActivity_() { 26163 let mouseInProgress; 26164 let lastMoveX; 26165 let lastMoveY; 26166 const handleActivity = bind_(this, this.reportUserActivity); 26167 const handleMouseMove = function (e) { 26168 // #1068 - Prevent mousemove spamming 26169 // Chrome Bug: https://code.google.com/p/chromium/issues/detail?id=366970 26170 if (e.screenX !== lastMoveX || e.screenY !== lastMoveY) { 26171 lastMoveX = e.screenX; 26172 lastMoveY = e.screenY; 26173 handleActivity(); 26174 } 26175 }; 26176 const handleMouseDown = function () { 26177 handleActivity(); 26178 // For as long as the they are touching the device or have their mouse down, 26179 // we consider them active even if they're not moving their finger or mouse. 26180 // So we want to continue to update that they are active 26181 this.clearInterval(mouseInProgress); 26182 // Setting userActivity=true now and setting the interval to the same time 26183 // as the activityCheck interval (250) should ensure we never miss the 26184 // next activityCheck 26185 mouseInProgress = this.setInterval(handleActivity, 250); 26186 }; 26187 const handleMouseUpAndMouseLeave = function (event) { 26188 handleActivity(); 26189 // Stop the interval that maintains activity if the mouse/touch is down 26190 this.clearInterval(mouseInProgress); 26191 }; 26192 26193 // Any mouse movement will be considered user activity 26194 this.on('mousedown', handleMouseDown); 26195 this.on('mousemove', handleMouseMove); 26196 this.on('mouseup', handleMouseUpAndMouseLeave); 26197 this.on('mouseleave', handleMouseUpAndMouseLeave); 26198 const controlBar = this.getChild('controlBar'); 26199 26200 // Fixes bug on Android & iOS where when tapping progressBar (when control bar is displayed) 26201 // controlBar would no longer be hidden by default timeout. 26202 if (controlBar && !IS_IOS && !IS_ANDROID) { 26203 controlBar.on('mouseenter', function (event) { 26204 if (this.player().options_.inactivityTimeout !== 0) { 26205 this.player().cache_.inactivityTimeout = this.player().options_.inactivityTimeout; 26206 } 26207 this.player().options_.inactivityTimeout = 0; 26208 }); 26209 controlBar.on('mouseleave', function (event) { 26210 this.player().options_.inactivityTimeout = this.player().cache_.inactivityTimeout; 26211 }); 26212 } 26213 26214 // Listen for keyboard navigation 26215 // Shouldn't need to use inProgress interval because of key repeat 26216 this.on('keydown', handleActivity); 26217 this.on('keyup', handleActivity); 26218 26219 // Run an interval every 250 milliseconds instead of stuffing everything into 26220 // the mousemove/touchmove function itself, to prevent performance degradation. 26221 // `this.reportUserActivity` simply sets this.userActivity_ to true, which 26222 // then gets picked up by this loop 26223 // http://ejohn.org/blog/learning-from-twitter/ 26224 let inactivityTimeout; 26225 26226 /** @this Player */ 26227 const activityCheck = function () { 26228 // Check to see if mouse/touch activity has happened 26229 if (!this.userActivity_) { 26230 return; 26231 } 26232 26233 // Reset the activity tracker 26234 this.userActivity_ = false; 26235 26236 // If the user state was inactive, set the state to active 26237 this.userActive(true); 26238 26239 // Clear any existing inactivity timeout to start the timer over 26240 this.clearTimeout(inactivityTimeout); 26241 const timeout = this.options_.inactivityTimeout; 26242 if (timeout <= 0) { 26243 return; 26244 } 26245 26246 // In <timeout> milliseconds, if no more activity has occurred the 26247 // user will be considered inactive 26248 inactivityTimeout = this.setTimeout(function () { 26249 // Protect against the case where the inactivityTimeout can trigger just 26250 // before the next user activity is picked up by the activity check loop 26251 // causing a flicker 26252 if (!this.userActivity_) { 26253 this.userActive(false); 26254 } 26255 }, timeout); 26256 }; 26257 this.setInterval(activityCheck, 250); 26258 } 26259 26260 /** 26261 * Gets or sets the current playback rate. A playback rate of 26262 * 1.0 represents normal speed and 0.5 would indicate half-speed 26263 * playback, for instance. 26264 * 26265 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-playbackrate 26266 * 26267 * @param {number} [rate] 26268 * New playback rate to set. 26269 * 26270 * @return {number|undefined} 26271 * - The current playback rate when getting or 1.0 26272 * - Nothing when setting 26273 */ 26274 playbackRate(rate) { 26275 if (rate !== undefined) { 26276 // NOTE: this.cache_.lastPlaybackRate is set from the tech handler 26277 // that is registered above 26278 this.techCall_('setPlaybackRate', rate); 26279 return; 26280 } 26281 if (this.tech_ && this.tech_.featuresPlaybackRate) { 26282 return this.cache_.lastPlaybackRate || this.techGet_('playbackRate'); 26283 } 26284 return 1.0; 26285 } 26286 26287 /** 26288 * Gets or sets the current default playback rate. A default playback rate of 26289 * 1.0 represents normal speed and 0.5 would indicate half-speed playback, for instance. 26290 * defaultPlaybackRate will only represent what the initial playbackRate of a video was, not 26291 * not the current playbackRate. 26292 * 26293 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-defaultplaybackrate 26294 * 26295 * @param {number} [rate] 26296 * New default playback rate to set. 26297 * 26298 * @return {number|undefined} 26299 * - The default playback rate when getting or 1.0 26300 * - Nothing when setting 26301 */ 26302 defaultPlaybackRate(rate) { 26303 if (rate !== undefined) { 26304 return this.techCall_('setDefaultPlaybackRate', rate); 26305 } 26306 if (this.tech_ && this.tech_.featuresPlaybackRate) { 26307 return this.techGet_('defaultPlaybackRate'); 26308 } 26309 return 1.0; 26310 } 26311 26312 /** 26313 * Gets or sets the audio flag 26314 * 26315 * @param {boolean} [bool] 26316 * - true signals that this is an audio player 26317 * - false signals that this is not an audio player 26318 * 26319 * @return {boolean|undefined} 26320 * - The current value of isAudio when getting 26321 * - Nothing when setting 26322 */ 26323 isAudio(bool) { 26324 if (bool !== undefined) { 26325 this.isAudio_ = !!bool; 26326 return; 26327 } 26328 return !!this.isAudio_; 26329 } 26330
26330enableAudioOnlyUI_() { 26331 // Update styling immediately to show the control bar so we can get its height 26332 this.addClass('vjs-audio-only-mode'); 26333 const playerChildren = this.children(); 26334 const controlBar = this.getChild('ControlBar'); 26335 const controlBarHeight = controlBar && controlBar.currentHeight(); 26336 26337 // Hide all player components except the control bar. Control bar components 26338 // needed only for video are hidden with CSS 26339 playerChildren.forEach(child => { 26340 if (child === controlBar) { 26341 return; 26342 } 26343 if (child.el_ && !child.hasClass('vjs-hidden')) { 26344 child.hide(); 26345 this.audioOnlyCache_.hiddenChildren.push(child); 26346 } 26347 }); 26348 this.audioOnlyCache_.playerHeight = this.currentHeight(); 26349 26350 // Set the player height the same as the control bar 26351 this.height(controlBarHeight); 26352 this.trigger('audioonlymodechange'); 26353 } 26354 disableAudioOnlyUI_() { 26355 this.removeClass('vjs-audio-only-mode'); 26356 26357 // Show player components that were previously hidden
vendor: 4,729 bytes, lines 26358-26491
26358 this.audioOnlyCache_.hiddenChildren.forEach(child => child.show()); 26359 26360 // Reset player height 26361 this.height(this.audioOnlyCache_.playerHeight); 26362 this.trigger('audioonlymodechange'); 26363 } 26364 26365 /** 26366 * Get the current audioOnlyMode state or set audioOnlyMode to true or false. 26367 * 26368 * Setting this to `true` will hide all player components except the control bar, 26369 * as well as control bar components needed only for video. 26370 * 26371 * @param {boolean} [value] 26372 * The value to set audioOnlyMode to. 26373 * 26374 * @return {Promise|boolean} 26375 * A Promise is returned when setting the state, and a boolean when getting 26376 * the present state 26377 */ 26378 audioOnlyMode(value) { 26379 if (typeof value !== 'boolean' || value === this.audioOnlyMode_) { 26380 return this.audioOnlyMode_; 26381 } 26382 this.audioOnlyMode_ = value; 26383 26384 // Enable Audio Only Mode 26385 if (value) { 26386 const exitPromises = []; 26387 26388 // Fullscreen and PiP are not supported in audioOnlyMode, so exit if we need to. 26389 if (this.isInPictureInPicture()) { 26390 exitPromises.push(this.exitPictureInPicture()); 26391 } 26392 if (this.isFullscreen()) { 26393 exitPromises.push(this.exitFullscreen()); 26394 } 26395 if (this.audioPosterMode()) { 26396 exitPromises.push(this.audioPosterMode(false)); 26397 } 26398 return Promise.all(exitPromises).then(() => this.enableAudioOnlyUI_()); 26399 } 26400 26401 // Disable Audio Only Mode 26402 return Promise.resolve().then(() => this.disableAudioOnlyUI_()); 26403 } 26404 enablePosterModeUI_() { 26405 // Hide the video element and show the poster image to enable posterModeUI 26406 const tech = this.tech_ && this.tech_; 26407 tech.hide(); 26408 this.addClass('vjs-audio-poster-mode'); 26409 this.trigger('audiopostermodechange'); 26410 } 26411 disablePosterModeUI_() { 26412 // Show the video element and hide the poster image to disable posterModeUI 26413 const tech = this.tech_ && this.tech_; 26414 tech.show(); 26415 this.removeClass('vjs-audio-poster-mode'); 26416 this.trigger('audiopostermodechange'); 26417 } 26418 26419 /** 26420 * Get the current audioPosterMode state or set audioPosterMode to true or false 26421 * 26422 * @param {boolean} [value] 26423 * The value to set audioPosterMode to. 26424 * 26425 * @return {Promise|boolean} 26426 * A Promise is returned when setting the state, and a boolean when getting 26427 * the present state 26428 */ 26429 audioPosterMode(value) { 26430 if (typeof value !== 'boolean' || value === this.audioPosterMode_) { 26431 return this.audioPosterMode_; 26432 } 26433 this.audioPosterMode_ = value; 26434 if (value) { 26435 if (this.audioOnlyMode()) { 26436 const audioOnlyModePromise = this.audioOnlyMode(false); 26437 return audioOnlyModePromise.then(() => { 26438 // enable audio poster mode after audio only mode is disabled 26439 this.enablePosterModeUI_(); 26440 }); 26441 } 26442 return Promise.resolve().then(() => { 26443 // enable audio poster mode 26444 this.enablePosterModeUI_(); 26445 }); 26446 } 26447 return Promise.resolve().then(() => { 26448 // disable audio poster mode 26449 this.disablePosterModeUI_(); 26450 }); 26451 } 26452 26453 /** 26454 * A helper method for adding a {@link TextTrack} to our 26455 * {@link TextTrackList}. 26456 * 26457 * In addition to the W3C settings we allow adding additional info through options. 26458 * 26459 * @see http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-addtexttrack 26460 * 26461 * @param {string} [kind] 26462 * the kind of TextTrack you are adding 26463 * 26464 * @param {string} [label] 26465 * the label to give the TextTrack label 26466 * 26467 * @param {string} [language] 26468 * the language to set on the TextTrack 26469 * 26470 * @return {TextTrack|undefined} 26471 * the TextTrack that was added or undefined 26472 * if there is no tech 26473 */ 26474 addTextTrack(kind, label, language) { 26475 if (this.tech_) { 26476 return this.tech_.addTextTrack(kind, label, language); 26477 } 26478 } 26479 26480 /** 26481 * Create a remote {@link TextTrack} and an {@link HTMLTrackElement}. 26482 * 26483 * @param {Object} options 26484 * Options to pass to {@link HTMLTrackElement} during creation. See 26485 * {@link HTMLTrackElement} for object properties that you should use. 26486 * 26487 * @param {boolean} [manualCleanup=false] if set to true, the TextTrack will not be removed 26488 * from the TextTrackList and HtmlTrackElementList 26489 * after a source change 26490 * 26491 * @return { import('./tracks/html-track-element').default
vendor: 5,649 bytes, lines 26491-26677
26491} 26492 * the HTMLTrackElement that was created and added 26493 * to the HtmlTrackElementList and the remote 26494 * TextTrackList 26495 * 26496 */ 26497 addRemoteTextTrack(options, manualCleanup) { 26498 if (this.tech_) { 26499 return this.tech_.addRemoteTextTrack(options, manualCleanup); 26500 } 26501 } 26502 26503 /** 26504 * Remove a remote {@link TextTrack} from the respective 26505 * {@link TextTrackList} and {@link HtmlTrackElementList}. 26506 * 26507 * @param {Object} track 26508 * Remote {@link TextTrack} to remove 26509 * 26510 * @return {undefined} 26511 * does not return anything 26512 */ 26513 removeRemoteTextTrack(obj = {}) { 26514 let { 26515 track 26516 } = obj; 26517 if (!track) { 26518 track = obj; 26519 } 26520 26521 // destructure the input into an object with a track argument, defaulting to arguments[0] 26522 // default the whole argument to an empty object if nothing was passed in 26523 26524 if (this.tech_) { 26525 return this.tech_.removeRemoteTextTrack(track); 26526 } 26527 } 26528 26529 /** 26530 * Gets available media playback quality metrics as specified by the W3C's Media 26531 * Playback Quality API. 26532 * 26533 * @see [Spec]{@link https://wicg.github.io/media-playback-quality} 26534 * 26535 * @return {Object|undefined} 26536 * An object with supported media playback quality metrics or undefined if there 26537 * is no tech or the tech does not support it. 26538 */ 26539 getVideoPlaybackQuality() { 26540 return this.techGet_('getVideoPlaybackQuality'); 26541 } 26542 26543 /** 26544 * Get video width 26545 * 26546 * @return {number} 26547 * current video width 26548 */ 26549 videoWidth() { 26550 return this.tech_ && this.tech_.videoWidth && this.tech_.videoWidth() || 0; 26551 } 26552 26553 /** 26554 * Get video height 26555 * 26556 * @return {number} 26557 * current video height 26558 */ 26559 videoHeight() { 26560 return this.tech_ && this.tech_.videoHeight && this.tech_.videoHeight() || 0; 26561 } 26562 26563 /** 26564 * Set or get the player's language code. 26565 * 26566 * Changing the language will trigger 26567 * [languagechange]{@link Player#event:languagechange} 26568 * which Components can use to update control text. 26569 * ClickableComponent will update its control text by default on 26570 * [languagechange]{@link Player#event:languagechange}. 26571 * 26572 * @fires Player#languagechange 26573 * 26574 * @param {string} [code] 26575 * the language code to set the player to 26576 * 26577 * @return {string|undefined} 26578 * - The current language code when getting 26579 * - Nothing when setting 26580 */ 26581 language(code) { 26582 if (code === undefined) { 26583 return this.language_; 26584 } 26585 if (this.language_ !== String(code).toLowerCase()) { 26586 this.language_ = String(code).toLowerCase(); 26587 26588 // during first init, it's possible some things won't be evented 26589 if (isEvented(this)) { 26590 /** 26591 * fires when the player language change 26592 * 26593 * @event Player#languagechange 26594 * @type {Event} 26595 */ 26596 this.trigger('languagechange'); 26597 } 26598 } 26599 } 26600 26601 /** 26602 * Get the player's language dictionary 26603 * Merge every time, because a newly added plugin might call videojs.addLanguage() at any time 26604 * Languages specified directly in the player options have precedence 26605 * 26606 * @return {Array} 26607 * An array of of supported languages 26608 */ 26609 languages() { 26610 return merge$2(Player.prototype.options_.languages, this.languages_); 26611 } 26612 26613 /** 26614 * returns a JavaScript object representing the current track 26615 * information. **DOES not return it as JSON** 26616 * 26617 * @return {Object} 26618 * Object representing the current of track info 26619 */ 26620 toJSON() { 26621 const options = merge$2(this.options_); 26622 const tracks = options.tracks; 26623 options.tracks = []; 26624 for (let i = 0; i < tracks.length; i++) { 26625 let track = tracks[i]; 26626 26627 // deep merge tracks and null out player so no circular references 26628 track = merge$2(track); 26629 track.player = undefined; 26630 options.tracks[i] = track; 26631 } 26632 return options; 26633 } 26634 26635 /** 26636 * Creates a simple modal dialog (an instance of the {@link ModalDialog} 26637 * component) that immediately overlays the player with arbitrary 26638 * content and removes itself when closed. 26639 * 26640 * @param {string|Function|Element|Array|null} content 26641 * Same as {@link ModalDialog#content}'s param of the same name. 26642 * The most straight-forward usage is to provide a string or DOM 26643 * element. 26644 * 26645 * @param {Object} [options] 26646 * Extra options which will be passed on to the {@link ModalDialog}. 26647 * 26648 * @return {ModalDialog} 26649 * the {@link ModalDialog} that was created 26650 */ 26651 createModal(content, options) { 26652 options = options || {}; 26653 options.content = content || ''; 26654 const modal = new ModalDialog(this, options); 26655 this.addChild(modal); 26656 modal.on('dispose', () => { 26657 this.removeChild(modal); 26658 }); 26659 modal.open(); 26660 return modal; 26661 } 26662 26663 /** 26664 * Change breakpoint classes when the player resizes. 26665 * 26666 * @private 26667 */ 26668 updateCurrentBreakpoint_() { 26669 if (!this.responsive()) { 26670 return; 26671 } 26672 const currentBreakpoint = this.currentBreakpoint(); 26673 const currentWidth = this.currentWidth(); 26674 for (let i = 0; i < BREAKPOINT_ORDER.length; i++) { 26675 const candidateBreakpoint = BREAKPOINT_ORDER[i]; 26676 const maxWidth = this.breakpoints_[candidateBreakpoint]; 26677 if (currentWidth <= maxWidth) {
vendor: 7,457 bytes, lines 26678-26887
26678 // The current breakpoint did not change, nothing to do. 26679 if (currentBreakpoint === candidateBreakpoint) { 26680 return; 26681 } 26682 26683 // Only remove a class if there is a current breakpoint. 26684 if (currentBreakpoint) { 26685 this.removeClass(BREAKPOINT_CLASSES[currentBreakpoint]); 26686 } 26687 this.addClass(BREAKPOINT_CLASSES[candidateBreakpoint]); 26688 this.breakpoint_ = candidateBreakpoint; 26689 break; 26690 } 26691 } 26692 } 26693 26694 /** 26695 * Removes the current breakpoint. 26696 * 26697 * @private 26698 */ 26699 removeCurrentBreakpoint_() { 26700 const className = this.currentBreakpointClass(); 26701 this.breakpoint_ = ''; 26702 if (className) { 26703 this.removeClass(className); 26704 } 26705 } 26706 26707 /** 26708 * Get or set breakpoints on the player. 26709 * 26710 * Calling this method with an object or `true` will remove any previous 26711 * custom breakpoints and start from the defaults again. 26712 * 26713 * @param {Object|boolean} [breakpoints] 26714 * If an object is given, it can be used to provide custom 26715 * breakpoints. If `true` is given, will set default breakpoints. 26716 * If this argument is not given, will simply return the current 26717 * breakpoints. 26718 * 26719 * @param {number} [breakpoints.tiny] 26720 * The maximum width for the "vjs-layout-tiny" class. 26721 * 26722 * @param {number} [breakpoints.xsmall] 26723 * The maximum width for the "vjs-layout-x-small" class. 26724 * 26725 * @param {number} [breakpoints.small] 26726 * The maximum width for the "vjs-layout-small" class. 26727 * 26728 * @param {number} [breakpoints.medium] 26729 * The maximum width for the "vjs-layout-medium" class. 26730 * 26731 * @param {number} [breakpoints.large] 26732 * The maximum width for the "vjs-layout-large" class. 26733 * 26734 * @param {number} [breakpoints.xlarge] 26735 * The maximum width for the "vjs-layout-x-large" class. 26736 * 26737 * @param {number} [breakpoints.huge] 26738 * The maximum width for the "vjs-layout-huge" class. 26739 * 26740 * @return {Object} 26741 * An object mapping breakpoint names to maximum width values. 26742 */ 26743 breakpoints(breakpoints) { 26744 // Used as a getter. 26745 if (breakpoints === undefined) { 26746 return Object.assign(this.breakpoints_); 26747 } 26748 this.breakpoint_ = ''; 26749 this.breakpoints_ = Object.assign({}, DEFAULT_BREAKPOINTS, breakpoints); 26750 26751 // When breakpoint definitions change, we need to update the currently 26752 // selected breakpoint. 26753 this.updateCurrentBreakpoint_(); 26754 26755 // Clone the breakpoints before returning. 26756 return Object.assign(this.breakpoints_); 26757 } 26758 26759 /** 26760 * Get or set a flag indicating whether or not this player should adjust 26761 * its UI based on its dimensions. 26762 * 26763 * @param {boolean} [value] 26764 * Should be `true` if the player should adjust its UI based on its 26765 * dimensions; otherwise, should be `false`. 26766 * 26767 * @return {boolean|undefined} 26768 * Will be `true` if this player should adjust its UI based on its 26769 * dimensions; otherwise, will be `false`. 26770 * Nothing if setting 26771 */ 26772 responsive(value) { 26773 // Used as a getter. 26774 if (value === undefined) { 26775 return this.responsive_; 26776 } 26777 value = Boolean(value); 26778 const current = this.responsive_; 26779 26780 // Nothing changed. 26781 if (value === current) { 26782 return; 26783 } 26784 26785 // The value actually changed, set it. 26786 this.responsive_ = value; 26787 26788 // Start listening for breakpoints and set the initial breakpoint if the 26789 // player is now responsive. 26790 if (value) { 26791 this.on('playerresize', this.boundUpdateCurrentBreakpoint_); 26792 this.updateCurrentBreakpoint_(); 26793 26794 // Stop listening for breakpoints if the player is no longer responsive. 26795 } else { 26796 this.off('playerresize', this.boundUpdateCurrentBreakpoint_); 26797 this.removeCurrentBreakpoint_(); 26798 } 26799 return value; 26800 } 26801 26802 /** 26803 * Get current breakpoint name, if any. 26804 * 26805 * @return {string} 26806 * If there is currently a breakpoint set, returns a the key from the 26807 * breakpoints object matching it. Otherwise, returns an empty string. 26808 */ 26809 currentBreakpoint() { 26810 return this.breakpoint_; 26811 } 26812 26813 /** 26814 * Get the current breakpoint class name. 26815 * 26816 * @return {string} 26817 * The matching class name (e.g. `"vjs-layout-tiny"` or 26818 * `"vjs-layout-large"`) for the current breakpoint. Empty string if 26819 * there is no current breakpoint. 26820 */ 26821 currentBreakpointClass() { 26822 return BREAKPOINT_CLASSES[this.breakpoint_] || ''; 26823 } 26824 26825 /** 26826 * An object that describes a single piece of media. 26827 * 26828 * Properties that are not part of this type description will be retained; so, 26829 * this can be viewed as a generic metadata storage mechanism as well. 26830 * 26831 * @see {@link https://wicg.github.io/mediasession/#the-mediametadata-interface} 26832 * @typedef {Object} Player~MediaObject 26833 * 26834 * @property {string} [album] 26835 * Unused, except if this object is passed to the `MediaSession` 26836 * API. 26837 * 26838 * @property {string} [artist] 26839 * Unused, except if this object is passed to the `MediaSession` 26840 * API. 26841 * 26842 * @property {Object[]} [artwork] 26843 * Unused, except if this object is passed to the `MediaSession` 26844 * API. If not specified, will be populated via the `poster`, if 26845 * available. 26846 * 26847 * @property {string} [poster] 26848 * URL to an image that will display before playback. 26849 * 26850 * @property {Tech~SourceObject|Tech~SourceObject[]|string} [src] 26851 * A single source object, an array of source objects, or a string 26852 * referencing a URL to a media source. It is _highly recommended_ 26853 * that an object or array of objects is used here, so that source 26854 * selection algorithms can take the `type` into account. 26855 * 26856 * @property {string} [title] 26857 * Unused, except if this object is passed to the `MediaSession` 26858 * API. 26859 * 26860 * @property {Object[]} [textTracks] 26861 * An array of objects to be used to create text tracks, following 26862 * the {@link https://www.w3.org/TR/html50/embedded-content-0.html#the-track-element|native track element format}. 26863 * For ease of removal, these will be created as "remote" text 26864 * tracks and set to automatically clean up on source changes. 26865 * 26866 * These objects may have properties like `src`, `kind`, `label`, 26867 * and `language`, see {@link Tech#createRemoteTextTrack}. 26868 */ 26869 26870 /** 26871 * Populate the player using a {@link Player~MediaObject|MediaObject}. 26872 * 26873 * @param {Player~MediaObject} media 26874 * A media object. 26875 * 26876 * @param {Function} ready 26877 * A callback to be called when the player is ready. 26878 */ 26879 loadMedia(media, ready) { 26880 if (!media || typeof media !== 'object') { 26881 return; 26882 } 26883 const crossOrigin = this.crossOrigin(); 26884 this.reset(); 26885 26886 // Clone the media object so it cannot be mutated from outside. 26887 this.cache_.media = merge$
vendor: 8,471 bytes, lines 26887-27188
268872(media); 26888 const { 26889 artist, 26890 artwork, 26891 description, 26892 poster, 26893 src, 26894 textTracks, 26895 title 26896 } = this.cache_.media; 26897 26898 // If `artwork` is not given, create it using `poster`. 26899 if (!artwork && poster) { 26900 this.cache_.media.artwork = [{ 26901 src: poster, 26902 type: getMimetype(poster) 26903 }]; 26904 } 26905 if (crossOrigin) { 26906 this.crossOrigin(crossOrigin); 26907 } 26908 if (src) { 26909 this.src(src); 26910 } 26911 if (poster) { 26912 this.poster(poster); 26913 } 26914 if (Array.isArray(textTracks)) { 26915 textTracks.forEach(tt => this.addRemoteTextTrack(tt, false)); 26916 } 26917 if (this.titleBar) { 26918 this.titleBar.update({ 26919 title, 26920 description: description || artist || '' 26921 }); 26922 } 26923 this.ready(ready); 26924 } 26925 26926 /** 26927 * Get a clone of the current {@link Player~MediaObject} for this player. 26928 * 26929 * If the `loadMedia` method has not been used, will attempt to return a 26930 * {@link Player~MediaObject} based on the current state of the player. 26931 * 26932 * @return {Player~MediaObject} 26933 */ 26934 getMedia() { 26935 if (!this.cache_.media) { 26936 const poster = this.poster(); 26937 const src = this.currentSources(); 26938 const textTracks = Array.prototype.map.call(this.remoteTextTracks(), tt => ({ 26939 kind: tt.kind, 26940 label: tt.label, 26941 language: tt.language, 26942 src: tt.src 26943 })); 26944 const media = { 26945 src, 26946 textTracks 26947 }; 26948 if (poster) { 26949 media.poster = poster; 26950 media.artwork = [{ 26951 src: media.poster, 26952 type: getMimetype(media.poster) 26953 }]; 26954 } 26955 return media; 26956 } 26957 return merge$2(this.cache_.media); 26958 } 26959 26960 /** 26961 * Gets tag settings 26962 * 26963 * @param {Element} tag 26964 * The player tag 26965 * 26966 * @return {Object} 26967 * An object containing all of the settings 26968 * for a player tag 26969 */ 26970 static getTagSettings(tag) { 26971 const baseOptions = { 26972 sources: [], 26973 tracks: [] 26974 }; 26975 const tagOptions = getAttributes(tag); 26976 const dataSetup = tagOptions['data-setup']; 26977 if (hasClass(tag, 'vjs-fill')) { 26978 tagOptions.fill = true; 26979 } 26980 if (hasClass(tag, 'vjs-fluid')) { 26981 tagOptions.fluid = true; 26982 } 26983 26984 // Check if data-setup attr exists. 26985 if (dataSetup !== null) { 26986 // Parse options JSON 26987 // If empty string, make it a parsable json object. 26988 const [err, data] = tuple(dataSetup || '{}'); 26989 if (err) { 26990 log$1.error(err); 26991 } 26992 Object.assign(tagOptions, data); 26993 } 26994 Object.assign(baseOptions, tagOptions); 26995 26996 // Get tag children settings 26997 if (tag.hasChildNodes()) { 26998 const children = tag.childNodes; 26999 for (let i = 0, j = children.length; i < j; i++) { 27000 const child = children[i]; 27001 // Change case needed: http://ejohn.org/blog/nodename-case-sensitivity/ 27002 const childName = child.nodeName.toLowerCase(); 27003 if (childName === 'source') { 27004 baseOptions.sources.push(getAttributes(child)); 27005 } else if (childName === 'track') { 27006 baseOptions.tracks.push(getAttributes(child)); 27007 } 27008 } 27009 } 27010 return baseOptions; 27011 } 27012 27013 /** 27014 * Set debug mode to enable/disable logs at info level. 27015 * 27016 * @param {boolean} enabled 27017 * @fires Player#debugon 27018 * @fires Player#debugoff 27019 * @return {boolean|undefined} 27020 */ 27021 debug(enabled) { 27022 if (enabled === undefined) { 27023 return this.debugEnabled_; 27024 } 27025 if (enabled) { 27026 this.trigger('debugon'); 27027 this.previousLogLevel_ = this.log.level; 27028 this.log.level('debug'); 27029 this.debugEnabled_ = true; 27030 } else { 27031 this.trigger('debugoff'); 27032 this.log.level(this.previousLogLevel_); 27033 this.previousLogLevel_ = undefined; 27034 this.debugEnabled_ = false; 27035 } 27036 } 27037 27038 /** 27039 * Set or get current playback rates. 27040 * Takes an array and updates the playback rates menu with the new items. 27041 * Pass in an empty array to hide the menu. 27042 * Values other than arrays are ignored. 27043 * 27044 * @fires Player#playbackrateschange 27045 * @param {number[]} newRates 27046 * The new rates that the playback rates menu should update to. 27047 * An empty array will hide the menu 27048 * @return {number[]} When used as a getter will return the current playback rates 27049 */ 27050 playbackRates(newRates) { 27051 if (newRates === undefined) { 27052 return this.cache_.playbackRates; 27053 } 27054 27055 // ignore any value that isn't an array 27056 if (!Array.isArray(newRates)) { 27057 return; 27058 } 27059 27060 // ignore any arrays that don't only contain numbers 27061 if (!newRates.every(rate => typeof rate === 'number')) { 27062 return; 27063 } 27064 this.cache_.playbackRates = newRates; 27065 27066 /** 27067 * fires when the playback rates in a player are changed 27068 * 27069 * @event Player#playbackrateschange 27070 * @type {Event} 27071 */ 27072 this.trigger('playbackrateschange'); 27073 } 27074 } 27075 27076 /** 27077 * Get the {@link VideoTrackList} 27078 * 27079 * @link https://html.spec.whatwg.org/multipage/embedded-content.html#videotracklist 27080 * 27081 * @return {VideoTrackList} 27082 * the current video track list 27083 * 27084 * @method Player.prototype.videoTracks 27085 */ 27086 27087 /** 27088 * Get the {@link AudioTrackList} 27089 * 27090 * @link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotracklist 27091 * 27092 * @return {AudioTrackList} 27093 * the current audio track list 27094 * 27095 * @method Player.prototype.audioTracks 27096 */ 27097 27098 /** 27099 * Get the {@link TextTrackList} 27100 * 27101 * @link http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-texttracks 27102 * 27103 * @return {TextTrackList} 27104 * the current text track list 27105 * 27106 * @method Player.prototype.textTracks 27107 */ 27108 27109 /** 27110 * Get the remote {@link TextTrackList} 27111 * 27112 * @return {TextTrackList} 27113 * The current remote text track list 27114 * 27115 * @method Player.prototype.remoteTextTracks 27116 */ 27117 27118 /** 27119 * Get the remote {@link HtmlTrackElementList} tracks. 27120 * 27121 * @return {HtmlTrackElementList} 27122 * The current remote text track element list 27123 * 27124 * @method Player.prototype.remoteTextTrackEls 27125 */ 27126 27127 ALL.names.forEach(function (name) { 27128 const props = ALL[name]; 27129 Player.prototype[props.getterName] = function () { 27130 if (this.tech_) { 27131 return this.tech_[props.getterName](); 27132 } 27133 27134 // if we have not yet loadTech_, we create {video,audio,text}Tracks_ 27135 // these will be passed to the tech during loading 27136 this[props.privateName] = this[props.privateName] || new props.ListClass(); 27137 return this[props.privateName]; 27138 }; 27139 }); 27140 27141 /** 27142 * Get or set the `Player`'s crossorigin option. For the HTML5 player, this 27143 * sets the `crossOrigin` property on the `<video>` tag to control the CORS 27144 * behavior. 27145 * 27146 * @see [Video Element Attributes]{@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#attr-crossorigin} 27147 * 27148 * @param {string} [value] 27149 * The value to set the `Player`'s crossorigin to. If an argument is 27150 * given, must be one of `anonymous` or `use-credentials`. 27151 * 27152 * @return {string|undefined} 27153 * - The current crossorigin value of the `Player` when getting. 27154 * - undefined when setting 27155 */ 27156 Player.prototype.crossorigin = Player.prototype.crossOrigin; 27157 27158 /** 27159 * Global enumeration of players. 27160 * 27161 * The keys are the player IDs and the values are either the {@link Player} 27162 * instance or `null` for disposed players. 27163 * 27164 * @type {Object} 27165 */ 27166 Player.players = {}; 27167 const navigator = window.navigator; 27168 27169 /* 27170 * Player instance options, surfaced using options 27171 * options = Player.prototype.options_ 27172 * Make changes in options, not here. 27173 * 27174 * @type {Object} 27175 * @private 27176 */ 27177 Player.prototype.options_ = { 27178 // Default order of fallback technology 27179 techOrder: Tech.defaultTechOrder_, 27180 html5: {}, 27181 // enable sourceset by default 27182 enableSourceset: true, 27183 // default inactivity timeout 27184 inactivityTimeout: 2000, 27185 // default playback rates 27186 playbackRates: [], 27187 // Add playback rate selection by adding rates 27188 // 'playbackRates': [0.5, 1, 1.5, 2],
27189 liveui: false, 27190 // Included control sets 27191 children: ['mediaLoader', 'posterImage', 'titleBar', 'textTrackDisplay', 'loadingSpinner', 'bigPlayButton', 'liveTracker', 'controlBar', 'errorDisplay', 'textTrackSettings', 'resizeManager'], 27192 language: navigator && (navigator.languages && navigator.languages[0] || navigator.userLanguage || navigator.language) || 'en', 27193 // locales and their language translations 27194 languages: {}, 27195 // Default message to show when a video cannot be played. 27196 notSupportedMessage: 'No compatible source was found for this media.', 27197 normalizeAutoplay: false, 27198 fullscreen: { 27199 options: { 27200 navigationUI: 'hide' 27201 } 27202 }, 27203 breakpoints: {}, 27204 responsive: false, 27205 audioOnlyMode: false, 27206 audioPosterMode: false, 27207 // Default smooth seeking to false 27208 enableSmoothSeeking: false 27209 }; 27210 TECH_EVENTS_RETRIGGER.forEach(function (event) { 27211 Player.prototype[`handleTech${toTitleCase$1(event)}_`] = function () { 27212 return this.trigger(event); 27213 }; 27214 }); 27215 27216 /** 27217 * Fired when the player has initial duration and dimension information 27218 * 27219 * @event Player#loadedmetadata 27220 * @type {Event} 27221 */ 27222 27223 /** 27224 * Fired when the player has downloaded data at the current playback position 27225 * 27226 * @event Player#loadeddata 27227 * @type {Event} 27228 */ 27229 27230 /** 27231 * Fired when the current playback position has changed * 27232 * During playback this is fired every 15-250 milliseconds, depending on the 27233 * playback technology in use. 27234 * 27235 * @event Player#timeupdate 27236 * @type {Event} 27237 */ 27238 27239 /** 27240 * Fired when the volume changes 27241 * 27242 * @event Player#volumechange 27243 * @type {Event} 27244 */ 27245 27246 /** 27247 * Reports whether or not a player has a plugin available. 27248 * 27249 * This does not report whether or not the plugin has ever been initialized 27250 * on this player. For that, [usingPlugin]{@link Player#usingPlugin}. 27251 * 27252 * @method Player#hasPlugin 27253 * @param {string} name 27254 * The name of a plugin. 27255 * 27256 * @return {boolean} 27257 * Whether or not this player has the requested plugin available. 27258 */ 27259 27260 /** 27261 * Reports whether or not a player is using a plugin by name. 27262 * 27263 * For basic plugins, this only reports whether the plugin has _ever_ been 27264 * initialized on this player. 27265 * 27266 * @method Player#usingPlugin 27267 * @param {string} name 27268 * The name of a plugin. 27269 * 27270 * @return {boolean} 27271 * Whether or not this player is using the requested plugin. 27272 */ 27273 27274 Component$1.registerComponent('Player', Player
vendor: 7,826 bytes, lines 27274-27549
27274); 27275 27276 /** 27277 * @file plugin.js 27278 */ 27279 27280 /** 27281 * The base plugin name. 27282 * 27283 * @private 27284 * @constant 27285 * @type {string} 27286 */ 27287 const BASE_PLUGIN_NAME = 'plugin'; 27288 27289 /** 27290 * The key on which a player's active plugins cache is stored. 27291 * 27292 * @private 27293 * @constant 27294 * @type {string} 27295 */ 27296 const PLUGIN_CACHE_KEY = 'activePlugins_'; 27297 27298 /** 27299 * Stores registered plugins in a private space. 27300 * 27301 * @private 27302 * @type {Object} 27303 */ 27304 const pluginStorage = {}; 27305 27306 /** 27307 * Reports whether or not a plugin has been registered. 27308 * 27309 * @private 27310 * @param {string} name 27311 * The name of a plugin. 27312 * 27313 * @return {boolean} 27314 * Whether or not the plugin has been registered. 27315 */ 27316 const pluginExists = name => pluginStorage.hasOwnProperty(name); 27317 27318 /** 27319 * Get a single registered plugin by name. 27320 * 27321 * @private 27322 * @param {string} name 27323 * The name of a plugin. 27324 * 27325 * @return {typeof Plugin|Function|undefined} 27326 * The plugin (or undefined). 27327 */ 27328 const getPlugin = name => pluginExists(name) ? pluginStorage[name] : undefined; 27329 27330 /** 27331 * Marks a plugin as "active" on a player. 27332 * 27333 * Also, ensures that the player has an object for tracking active plugins. 27334 * 27335 * @private 27336 * @param {Player} player 27337 * A Video.js player instance. 27338 * 27339 * @param {string} name 27340 * The name of a plugin. 27341 */ 27342 const markPluginAsActive = (player, name) => { 27343 player[PLUGIN_CACHE_KEY] = player[PLUGIN_CACHE_KEY] || {}; 27344 player[PLUGIN_CACHE_KEY][name] = true; 27345 }; 27346 27347 /** 27348 * Triggers a pair of plugin setup events. 27349 * 27350 * @private 27351 * @param {Player} player 27352 * A Video.js player instance. 27353 * 27354 * @param {PluginEventHash} hash 27355 * A plugin event hash. 27356 * 27357 * @param {boolean} [before] 27358 * If true, prefixes the event name with "before". In other words, 27359 * use this to trigger "beforepluginsetup" instead of "pluginsetup". 27360 */ 27361 const triggerSetupEvent = (player, hash, before) => { 27362 const eventName = (before ? 'before' : '') + 'pluginsetup'; 27363 player.trigger(eventName, hash); 27364 player.trigger(eventName + ':' + hash.name, hash); 27365 }; 27366 27367 /** 27368 * Takes a basic plugin function and returns a wrapper function which marks 27369 * on the player that the plugin has been activated. 27370 * 27371 * @private 27372 * @param {string} name 27373 * The name of the plugin. 27374 * 27375 * @param {Function} plugin 27376 * The basic plugin. 27377 * 27378 * @return {Function} 27379 * A wrapper function for the given plugin. 27380 */ 27381 const createBasicPlugin = function (name, plugin) { 27382 const basicPluginWrapper = function () { 27383 // We trigger the "beforepluginsetup" and "pluginsetup" events on the player 27384 // regardless, but we want the hash to be consistent with the hash provided 27385 // for advanced plugins. 27386 // 27387 // The only potentially counter-intuitive thing here is the `instance` in 27388 // the "pluginsetup" event is the value returned by the `plugin` function. 27389 triggerSetupEvent(this, { 27390 name, 27391 plugin, 27392 instance: null 27393 }, true); 27394 const instance = plugin.apply(this, arguments); 27395 markPluginAsActive(this, name); 27396 triggerSetupEvent(this, { 27397 name, 27398 plugin, 27399 instance 27400 }); 27401 return instance; 27402 }; 27403 Object.keys(plugin).forEach(function (prop) { 27404 basicPluginWrapper[prop] = plugin[prop]; 27405 }); 27406 return basicPluginWrapper; 27407 }; 27408 27409 /** 27410 * Takes a plugin sub-class and returns a factory function for generating 27411 * instances of it. 27412 * 27413 * This factory function will replace itself with an instance of the requested 27414 * sub-class of Plugin. 27415 * 27416 * @private 27417 * @param {string} name 27418 * The name of the plugin. 27419 * 27420 * @param {Plugin} PluginSubClass 27421 * The advanced plugin. 27422 * 27423 * @return {Function} 27424 */ 27425 const createPluginFactory = (name, PluginSubClass) => { 27426 // Add a `name` property to the plugin prototype so that each plugin can 27427 // refer to itself by name. 27428 PluginSubClass.prototype.name = name; 27429 return function (...args) { 27430 triggerSetupEvent(this, { 27431 name, 27432 plugin: PluginSubClass, 27433 instance: null 27434 }, true); 27435 const instance = new PluginSubClass(...[this, ...args]); 27436 27437 // The plugin is replaced by a function that returns the current instance. 27438 this[name] = () => instance; 27439 triggerSetupEvent(this, instance.getEventHash()); 27440 return instance; 27441 }; 27442 }; 27443 27444 /** 27445 * Parent class for all advanced plugins. 27446 * 27447 * @mixes module:evented~EventedMixin 27448 * @mixes module:stateful~StatefulMixin 27449 * @fires Player#beforepluginsetup 27450 * @fires Player#beforepluginsetup:$name 27451 * @fires Player#pluginsetup 27452 * @fires Player#pluginsetup:$name 27453 * @listens Player#dispose 27454 * @throws {Error} 27455 * If attempting to instantiate the base {@link Plugin} class 27456 * directly instead of via a sub-class. 27457 */ 27458 class Plugin { 27459 /** 27460 * Creates an instance of this class. 27461 * 27462 * Sub-classes should call `super` to ensure plugins are properly initialized. 27463 * 27464 * @param {Player} player 27465 * A Video.js player instance. 27466 */ 27467 constructor(player) { 27468 if (this.constructor === Plugin) { 27469 throw new Error('Plugin must be sub-classed; not directly instantiated.'); 27470 } 27471 this.player = player; 27472 if (!this.log) { 27473 this.log = this.player.log.createLogger(this.name); 27474 } 27475 27476 // Make this object evented, but remove the added `trigger` method so we 27477 // use the prototype version instead. 27478 evented(this); 27479 delete this.trigger; 27480 stateful(this, this.constructor.defaultState); 27481 markPluginAsActive(player, this.name); 27482 27483 // Auto-bind the dispose method so we can use it as a listener and unbind 27484 // it later easily. 27485 this.dispose = this.dispose.bind(this); 27486 27487 // If the player is disposed, dispose the plugin. 27488 player.on('dispose', this.dispose); 27489 } 27490 27491 /** 27492 * Get the version of the plugin that was set on <pluginName>.VERSION 27493 */ 27494 version() { 27495 return this.constructor.VERSION; 27496 } 27497 27498 /** 27499 * Each event triggered by plugins includes a hash of additional data with 27500 * conventional properties. 27501 * 27502 * This returns that object or mutates an existing hash. 27503 * 27504 * @param {Object} [hash={}] 27505 * An object to be used as event an event hash. 27506 * 27507 * @return {PluginEventHash} 27508 * An event hash object with provided properties mixed-in. 27509 */ 27510 getEventHash(hash = {}) { 27511 hash.name = this.name; 27512 hash.plugin = this.constructor; 27513 hash.instance = this; 27514 return hash; 27515 } 27516 27517 /** 27518 * Triggers an event on the plugin object and overrides 27519 * {@link module:evented~EventedMixin.trigger|EventedMixin.trigger}. 27520 * 27521 * @param {string|Object} event 27522 * An event type or an object with a type property. 27523 * 27524 * @param {Object} [hash={}] 27525 * Additional data hash to merge with a 27526 * {@link PluginEventHash|PluginEventHash}. 27527 * 27528 * @return {boolean} 27529 * Whether or not default was prevented. 27530 */ 27531 trigger(event, hash = {}) { 27532 return trigger(this.eventBusEl_, event, this.getEventHash(hash)); 27533 } 27534 27535 /** 27536 * Handles "statechanged" events on the plugin. No-op by default, override by 27537 * subclassing. 27538 * 27539 * @abstract 27540 * @param {Event} e 27541 * An event object provided by a "statechanged" event. 27542 * 27543 * @param {Object} e.changes 27544 * An object describing changes that occurred with the "statechanged" 27545 * event. 27546 */ 27547 handleStateChanged(e) {} 27548 27549 /**
vendor: 8,400 bytes, lines 27550-27828
27550 * Disposes a plugin. 27551 * 27552 * Subclasses can override this if they want, but for the sake of safety, 27553 * it's probably best to subscribe the "dispose" event. 27554 * 27555 * @fires Plugin#dispose 27556 */ 27557 dispose() { 27558 const { 27559 name, 27560 player 27561 } = this; 27562 27563 /** 27564 * Signals that a advanced plugin is about to be disposed. 27565 * 27566 * @event Plugin#dispose 27567 * @type {Event} 27568 */ 27569 this.trigger('dispose'); 27570 this.off(); 27571 player.off('dispose', this.dispose); 27572 27573 // Eliminate any possible sources of leaking memory by clearing up 27574 // references between the player and the plugin instance and nulling out 27575 // the plugin's state and replacing methods with a function that throws. 27576 player[PLUGIN_CACHE_KEY][name] = false; 27577 this.player = this.state = null; 27578 27579 // Finally, replace the plugin name on the player with a new factory 27580 // function, so that the plugin is ready to be set up again. 27581 player[name] = createPluginFactory(name, pluginStorage[name]); 27582 } 27583 27584 /** 27585 * Determines if a plugin is a basic plugin (i.e. not a sub-class of `Plugin`). 27586 * 27587 * @param {string|Function} plugin 27588 * If a string, matches the name of a plugin. If a function, will be 27589 * tested directly. 27590 * 27591 * @return {boolean} 27592 * Whether or not a plugin is a basic plugin. 27593 */ 27594 static isBasic(plugin) { 27595 const p = typeof plugin === 'string' ? getPlugin(plugin) : plugin; 27596 return typeof p === 'function' && !Plugin.prototype.isPrototypeOf(p.prototype); 27597 } 27598 27599 /** 27600 * Register a Video.js plugin. 27601 * 27602 * @param {string} name 27603 * The name of the plugin to be registered. Must be a string and 27604 * must not match an existing plugin or a method on the `Player` 27605 * prototype. 27606 * 27607 * @param {typeof Plugin|Function} plugin 27608 * A sub-class of `Plugin` or a function for basic plugins. 27609 * 27610 * @return {typeof Plugin|Function} 27611 * For advanced plugins, a factory function for that plugin. For 27612 * basic plugins, a wrapper function that initializes the plugin. 27613 */ 27614 static registerPlugin(name, plugin) { 27615 if (typeof name !== 'string') { 27616 throw new Error(`Illegal plugin name, "${name}", must be a string, was ${typeof name}.`); 27617 } 27618 if (pluginExists(name)) { 27619 log$1.warn(`A plugin named "${name}" already exists. You may want to avoid re-registering plugins!`); 27620 } else if (Player.prototype.hasOwnProperty(name)) { 27621 throw new Error(`Illegal plugin name, "${name}", cannot share a name with an existing player method!`); 27622 } 27623 if (typeof plugin !== 'function') { 27624 throw new Error(`Illegal plugin for "${name}", must be a function, was ${typeof plugin}.`); 27625 } 27626 pluginStorage[name] = plugin; 27627 27628 // Add a player prototype method for all sub-classed plugins (but not for 27629 // the base Plugin class). 27630 if (name !== BASE_PLUGIN_NAME) { 27631 if (Plugin.isBasic(plugin)) { 27632 Player.prototype[name] = createBasicPlugin(name, plugin); 27633 } else { 27634 Player.prototype[name] = createPluginFactory(name, plugin); 27635 } 27636 } 27637 return plugin; 27638 } 27639 27640 /** 27641 * De-register a Video.js plugin. 27642 * 27643 * @param {string} name 27644 * The name of the plugin to be de-registered. Must be a string that 27645 * matches an existing plugin. 27646 * 27647 * @throws {Error} 27648 * If an attempt is made to de-register the base plugin. 27649 */ 27650 static deregisterPlugin(name) { 27651 if (name === BASE_PLUGIN_NAME) { 27652 throw new Error('Cannot de-register base plugin.'); 27653 } 27654 if (pluginExists(name)) { 27655 delete pluginStorage[name]; 27656 delete Player.prototype[name]; 27657 } 27658 } 27659 27660 /** 27661 * Gets an object containing multiple Video.js plugins. 27662 * 27663 * @param {Array} [names] 27664 * If provided, should be an array of plugin names. Defaults to _all_ 27665 * plugin names. 27666 * 27667 * @return {Object|undefined} 27668 * An object containing plugin(s) associated with their name(s) or 27669 * `undefined` if no matching plugins exist). 27670 */ 27671 static getPlugins(names = Object.keys(pluginStorage)) { 27672 let result; 27673 names.forEach(name => { 27674 const plugin = getPlugin(name); 27675 if (plugin) { 27676 result = result || {}; 27677 result[name] = plugin; 27678 } 27679 }); 27680 return result; 27681 } 27682 27683 /** 27684 * Gets a plugin's version, if available 27685 * 27686 * @param {string} name 27687 * The name of a plugin. 27688 * 27689 * @return {string} 27690 * The plugin's version or an empty string. 27691 */ 27692 static getPluginVersion(name) { 27693 const plugin = getPlugin(name); 27694 return plugin && plugin.VERSION || ''; 27695 } 27696 } 27697 27698 /** 27699 * Gets a plugin by name if it exists. 27700 * 27701 * @static 27702 * @method getPlugin 27703 * @memberOf Plugin 27704 * @param {string} name 27705 * The name of a plugin. 27706 * 27707 * @returns {typeof Plugin|Function|undefined} 27708 * The plugin (or `undefined`). 27709 */ 27710 Plugin.getPlugin = getPlugin; 27711 27712 /** 27713 * The name of the base plugin class as it is registered. 27714 * 27715 * @type {string} 27716 */ 27717 Plugin.BASE_PLUGIN_NAME = BASE_PLUGIN_NAME; 27718 Plugin.registerPlugin(BASE_PLUGIN_NAME, Plugin); 27719 27720 /** 27721 * Documented in player.js 27722 * 27723 * @ignore 27724 */ 27725 Player.prototype.usingPlugin = function (name) { 27726 return !!this[PLUGIN_CACHE_KEY] && this[PLUGIN_CACHE_KEY][name] === true; 27727 }; 27728 27729 /** 27730 * Documented in player.js 27731 * 27732 * @ignore 27733 */ 27734 Player.prototype.hasPlugin = function (name) { 27735 return !!pluginExists(name); 27736 }; 27737 27738 /** 27739 * Signals that a plugin is about to be set up on a player. 27740 * 27741 * @event Player#beforepluginsetup 27742 * @type {PluginEventHash} 27743 */ 27744 27745 /** 27746 * Signals that a plugin is about to be set up on a player - by name. The name 27747 * is the name of the plugin. 27748 * 27749 * @event Player#beforepluginsetup:$name 27750 * @type {PluginEventHash} 27751 */ 27752 27753 /** 27754 * Signals that a plugin has just been set up on a player. 27755 * 27756 * @event Player#pluginsetup 27757 * @type {PluginEventHash} 27758 */ 27759 27760 /** 27761 * Signals that a plugin has just been set up on a player - by name. The name 27762 * is the name of the plugin. 27763 * 27764 * @event Player#pluginsetup:$name 27765 * @type {PluginEventHash} 27766 */ 27767 27768 /** 27769 * @typedef {Object} PluginEventHash 27770 * 27771 * @property {string} instance 27772 * For basic plugins, the return value of the plugin function. For 27773 * advanced plugins, the plugin instance on which the event is fired. 27774 * 27775 * @property {string} name 27776 * The name of the plugin. 27777 * 27778 * @property {string} plugin 27779 * For basic plugins, the plugin function. For advanced plugins, the 27780 * plugin class/constructor. 27781 */ 27782 27783 /** 27784 * @file deprecate.js 27785 * @module deprecate 27786 */ 27787 27788 /** 27789 * Decorate a function with a deprecation message the first time it is called. 27790 * 27791 * @param {string} message 27792 * A deprecation message to log the first time the returned function 27793 * is called. 27794 * 27795 * @param {Function} fn 27796 * The function to be deprecated. 27797 * 27798 * @return {Function} 27799 * A wrapper function that will log a deprecation warning the first 27800 * time it is called. The return value will be the return value of 27801 * the wrapped function. 27802 */ 27803 function deprecate(message, fn) { 27804 let warned = false; 27805 return function (...args) { 27806 if (!warned) { 27807 log$1.warn(message); 27808 } 27809 warned = true; 27810 return fn.apply(this, args); 27811 }; 27812 } 27813 27814 /** 27815 * Internal function used to mark a function as deprecated in the next major 27816 * version with consistent messaging. 27817 * 27818 * @param {number} major The major version where it will be removed 27819 * @param {string} oldName The old function name 27820 * @param {string} newName The new function name 27821 * @param {Function} fn The function to deprecate 27822 * @return {Function} The decorated function 27823 */ 27824 function deprecateForMajor(major, oldName, newName, fn) { 27825 return deprecate(`${oldName} is deprecated and will be removed in ${major}.0; please use ${newName} instead.`, fn); 27826 } 27827 27828
27828/** 27829 * @file video.js 27830 * @module videojs 27831 */ 27832 27833 /** 27834 * Normalize an `id` value by trimming off a leading `#` 27835 * 27836 * @private 27837 * @param {string} id 27838 * A string, maybe with a leading `#`. 27839 * 27840 * @return {string} 27841 * The string, without any leading `#`. 27842 */ 27843 const normalizeId = id => id.indexOf('#') === 0 ? id.slice(1) : id; 27844 27845 /** 27846 * A callback that is called when a component is ready. Does not have any 27847 * parameters and any callback value will be ignored. See: {@link Component~ReadyCallback} 27848 * 27849 * @callback ReadyCallback 27850 */ 27851 27852 /** 27853 * The `videojs()` function doubles as the main function for users to create a 27854 * {@link Player} instance as well as the main library namespace. 27855 * 27856 * It can also be used as a getter for a pre-existing {@link Player} instance. 27857 * However, we _strongly_ recommend using `videojs.getPlayer()` for this 27858 * purpose because it avoids any potential for unintended initialization. 27859 * 27860 * Due to [limitations](https://github.com/jsdoc3/jsdoc/issues/955#issuecomment-313829149) 27861 * of our JSDoc template, we cannot properly document this as both a function 27862 * and a namespace, so its function signature is documented here. 27863 * 27864 * #### Arguments 27865 * ##### id 27866 * string|Element, **required** 27867 * 27868 * Video element or video element ID. 27869 * 27870 * ##### options 27871 * Object, optional 27872 * 27873 * Options object for providing settings. 27874 * See: [Options Guide](https://docs.videojs.com/tutorial-options.html). 27875 * 27876 * ##### ready 27877 * {@link Component~ReadyCallback}, optional 27878 * 27879 * A function to be called when the {@link Player} and {@link Tech} are ready. 27880 * 27881 * #### Return Value 27882 * 27883 * The `videojs()` function returns a {@link Player} instance. 27884 * 27885 * @namespace 27886 * 27887 * @borrows AudioTrack as AudioTrack 27888 * @borrows Component.getComponent as getComponent 27889 * @borrows module:events.on as on 27890 * @borrows module:events.one as one 27891 * @borrows module:events.off as off 27892 * @borrows module:events.trigger as trigger 27893 * @borrows EventTarget as EventTarget 27894 * @borrows module:middleware.use as use 27895 * @borrows Player.players as players 27896 * @borrows Plugin.registerPlugin as registerPlugin 27897 * @borrows Plugin.deregisterPlugin as deregisterPlugin 27898 * @borrows Plugin.getPlugins as getPlugins 27899 * @borrows Plugin.getPlugin as getPlugin 27900 * @borrows Plugin.getPluginVersion as getPluginVersion 27901 * @borrows Tech.getTech as getTech 27902 * @borrows Tech.registerTech as registerTech 27903 * @borrows TextTrack as TextTrack 27904 * @borrows VideoTrack as VideoTrack 27905 * 27906 * @param {string|Element}
vendor: 15,493 bytes, lines 27906-28366
27906 id 27907 * Video element or video element ID. 27908 * 27909 * @param {Object} [options] 27910 * Options object for providing settings. 27911 * See: [Options Guide](https://docs.videojs.com/tutorial-options.html). 27912 * 27913 * @param {ReadyCallback} [ready] 27914 * A function to be called when the {@link Player} and {@link Tech} are 27915 * ready. 27916 * 27917 * @return {Player} 27918 * The `videojs()` function returns a {@link Player|Player} instance. 27919 */ 27920 function videojs(id, options, ready) { 27921 let player = videojs.getPlayer(id); 27922 if (player) { 27923 if (options) { 27924 log$1.warn(`Player "${id}" is already initialised. Options will not be applied.`); 27925 } 27926 if (ready) { 27927 player.ready(ready); 27928 } 27929 return player; 27930 } 27931 const el = typeof id === 'string' ? $('#' + normalizeId(id)) : id; 27932 if (!isEl(el)) { 27933 throw new TypeError('The element or ID supplied is not valid. (videojs)'); 27934 } 27935 27936 // document.body.contains(el) will only check if el is contained within that one document. 27937 // This causes problems for elements in iframes. 27938 // Instead, use the element's ownerDocument instead of the global document. 27939 // This will make sure that the element is indeed in the dom of that document. 27940 // Additionally, check that the document in question has a default view. 27941 // If the document is no longer attached to the dom, the defaultView of the document will be null. 27942 // If element is inside Shadow DOM (e.g. is part of a Custom element), ownerDocument.body 27943 // always returns false. Instead, use the Shadow DOM root. 27944 const inShadowDom = 'getRootNode' in el ? el.getRootNode() instanceof window.ShadowRoot : false; 27945 const rootNode = inShadowDom ? el.getRootNode() : el.ownerDocument.body; 27946 if (!el.ownerDocument.defaultView || !rootNode.contains(el)) { 27947 log$1.warn('The element supplied is not included in the DOM'); 27948 } 27949 options = options || {}; 27950 27951 // Store a copy of the el before modification, if it is to be restored in destroy() 27952 // If div ingest, store the parent div 27953 if (options.restoreEl === true) { 27954 options.restoreEl = (el.parentNode && el.parentNode.hasAttribute('data-vjs-player') ? el.parentNode : el).cloneNode(true); 27955 } 27956 hooks('beforesetup').forEach(hookFunction => { 27957 const opts = hookFunction(el, merge$2(options)); 27958 if (!isObject$1(opts) || Array.isArray(opts)) { 27959 log$1.error('please return an object in beforesetup hooks'); 27960 return; 27961 } 27962 options = merge$2(options, opts); 27963 }); 27964 27965 // We get the current "Player" component here in case an integration has 27966 // replaced it with a custom player. 27967 const PlayerComponent = Component$1.getComponent('Player'); 27968 player = new PlayerComponent(el, options, ready); 27969 hooks('setup').forEach(hookFunction => hookFunction(player)); 27970 return player; 27971 } 27972 videojs.hooks_ = hooks_; 27973 videojs.hooks = hooks; 27974 videojs.hook = hook; 27975 videojs.hookOnce = hookOnce; 27976 videojs.removeHook = removeHook; 27977 27978 // Add default styles 27979 if (window.VIDEOJS_NO_DYNAMIC_STYLE !== true && isReal()) { 27980 let style = $('.vjs-styles-defaults'); 27981 if (!style) { 27982 style = createStyleElement('vjs-styles-defaults'); 27983 const head = $('head'); 27984 if (head) { 27985 head.insertBefore(style, head.firstChild); 27986 } 27987 setTextContent(style, ` 27988 .video-js { 27989 width: 300px; 27990 height: 150px; 27991 } 27992 27993 .vjs-fluid:not(.vjs-audio-only-mode) { 27994 padding-top: 56.25% 27995 } 27996 `); 27997 } 27998 } 27999 28000 // Run Auto-load players 28001 // You have to wait at least once in case this script is loaded after your 28002 // video in the DOM (weird behavior only with minified version) 28003 autoSetupTimeout(1, videojs); 28004 28005 /** 28006 * Current Video.js version. Follows [semantic versioning](https://semver.org/). 28007 * 28008 * @type {string} 28009 */ 28010 videojs.VERSION = version$5; 28011 28012 /** 28013 * The global options object. These are the settings that take effect 28014 * if no overrides are specified when the player is created. 28015 * 28016 * @type {Object} 28017 */ 28018 videojs.options = Player.prototype.options_; 28019 28020 /** 28021 * Get an object with the currently created players, keyed by player ID 28022 * 28023 * @return {Object} 28024 * The created players 28025 */ 28026 videojs.getPlayers = () => Player.players; 28027 28028 /** 28029 * Get a single player based on an ID or DOM element. 28030 * 28031 * This is useful if you want to check if an element or ID has an associated 28032 * Video.js player, but not create one if it doesn't. 28033 * 28034 * @param {string|Element} id 28035 * An HTML element - `<video>`, `<audio>`, or `<video-js>` - 28036 * or a string matching the `id` of such an element. 28037 * 28038 * @return {Player|undefined} 28039 * A player instance or `undefined` if there is no player instance 28040 * matching the argument. 28041 */ 28042 videojs.getPlayer = id => { 28043 const players = Player.players; 28044 let tag; 28045 if (typeof id === 'string') { 28046 const nId = normalizeId(id); 28047 const player = players[nId]; 28048 if (player) { 28049 return player; 28050 } 28051 tag = $('#' + nId); 28052 } else { 28053 tag = id; 28054 } 28055 if (isEl(tag)) { 28056 const { 28057 player, 28058 playerId 28059 } = tag; 28060 28061 // Element may have a `player` property referring to an already created 28062 // player instance. If so, return that. 28063 if (player || players[playerId]) { 28064 return player || players[playerId]; 28065 } 28066 } 28067 }; 28068 28069 /** 28070 * Returns an array of all current players. 28071 * 28072 * @return {Array} 28073 * An array of all players. The array will be in the order that 28074 * `Object.keys` provides, which could potentially vary between 28075 * JavaScript engines. 28076 * 28077 */ 28078 videojs.getAllPlayers = () => 28079 // Disposed players leave a key with a `null` value, so we need to make sure 28080 // we filter those out. 28081 Object.keys(Player.players).map(k => Player.players[k]).filter(Boolean); 28082 videojs.players = Player.players; 28083 videojs.getComponent = Component$1.getComponent; 28084 28085 /** 28086 * Register a component so it can referred to by name. Used when adding to other 28087 * components, either through addChild `component.addChild('myComponent')` or through 28088 * default children options `{ children: ['myComponent'] }`. 28089 * 28090 * > NOTE: You could also just initialize the component before adding. 28091 * `component.addChild(new MyComponent());` 28092 * 28093 * @param {string} name 28094 * The class name of the component 28095 * 28096 * @param {typeof Component} comp 28097 * The component class 28098 * 28099 * @return {typeof Component} 28100 * The newly registered component 28101 */ 28102 videojs.registerComponent = (name, comp) => { 28103 if (Tech.isTech(comp)) { 28104 log$1.warn(`The ${name} tech was registered as a component. It should instead be registered using videojs.registerTech(name, tech)`); 28105 } 28106 return Component$1.registerComponent.call(Component$1, name, comp); 28107 }; 28108 videojs.getTech = Tech.getTech; 28109 videojs.registerTech = Tech.registerTech; 28110 videojs.use = use; 28111 28112 /** 28113 * An object that can be returned by a middleware to signify 28114 * that the middleware is being terminated. 28115 * 28116 * @type {object} 28117 * @property {object} middleware.TERMINATOR 28118 */ 28119 Object.defineProperty(videojs, 'middleware', { 28120 value: {}, 28121 writeable: false, 28122 enumerable: true 28123 }); 28124 Object.defineProperty(videojs.middleware, 'TERMINATOR', { 28125 value: TERMINATOR, 28126 writeable: false, 28127 enumerable: true 28128 }); 28129 28130 /** 28131 * A reference to the {@link module:browser|browser utility module} as an object. 28132 * 28133 * @type {Object} 28134 * @see {@link module:browser|browser} 28135 */ 28136 videojs.browser = browser; 28137 28138 /** 28139 * A reference to the {@link module:obj|obj utility module} as an object. 28140 * 28141 * @type {Object} 28142 * @see {@link module:obj|obj} 28143 */ 28144 videojs.obj = Obj; 28145 28146 /** 28147 * Deprecated reference to the {@link module:obj.merge|merge function} 28148 * 28149 * @type {Function} 28150 * @see {@link module:obj.merge|merge} 28151 * @deprecated Deprecated and will be removed in 9.0. Please use videojs.obj.merge instead. 28152 */ 28153 videojs.mergeOptions = deprecateForMajor(9, 'videojs.mergeOptions', 'videojs.obj.merge', merge$2); 28154 28155 /** 28156 * Deprecated reference to the {@link module:obj.defineLazyProperty|defineLazyProperty function} 28157 * 28158 * @type {Function} 28159 * @see {@link module:obj.defineLazyProperty|defineLazyProperty} 28160 * @deprecated Deprecated and will be removed in 9.0. Please use videojs.obj.defineLazyProperty instead. 28161 */ 28162 videojs.defineLazyProperty = deprecateForMajor(9, 'videojs.defineLazyProperty', 'videojs.obj.defineLazyProperty', defineLazyProperty); 28163 28164 /** 28165 * Deprecated reference to the {@link module:fn.bind_|fn.bind_ function} 28166 * 28167 * @type {Function} 28168 * @see {@link module:fn.bind_|fn.bind_} 28169 * @deprecated Deprecated and will be removed in 9.0. Please use native Function.prototype.bind instead. 28170 */ 28171 videojs.bind = deprecateForMajor(9, 'videojs.bind', 'native Function.prototype.bind', bind_); 28172 videojs.registerPlugin = Plugin.registerPlugin; 28173 videojs.deregisterPlugin = Plugin.deregisterPlugin; 28174 28175 /** 28176 * Deprecated method to register a plugin with Video.js 28177 * 28178 * @deprecated Deprecated and will be removed in 9.0. Use videojs.registerPlugin() instead. 28179 * 28180 * @param {string} name 28181 * The plugin name 28182 * 28183 * @param {typeof Plugin|Function} plugin 28184 * The plugin sub-class or function 28185 * 28186 * @return {typeof Plugin|Function} 28187 */ 28188 videojs.plugin = (name, plugin) => { 28189 log$1.warn('videojs.plugin() is deprecated; use videojs.registerPlugin() instead'); 28190 return Plugin.registerPlugin(name, plugin); 28191 }; 28192 videojs.getPlugins = Plugin.getPlugins; 28193 videojs.getPlugin = Plugin.getPlugin; 28194 videojs.getPluginVersion = Plugin.getPluginVersion; 28195 28196 /** 28197 * Adding languages so that they're available to all players. 28198 * Example: `videojs.addLanguage('es', { 'Hello': 'Hola' });` 28199 * 28200 * @param {string} code 28201 * The language code or dictionary property 28202 * 28203 * @param {Object} data 28204 * The data values to be translated 28205 * 28206 * @return {Object} 28207 * The resulting language dictionary object 28208 */ 28209 videojs.addLanguage = function (code, data) { 28210 code = ('' + code).toLowerCase(); 28211 videojs.options.languages = merge$2(videojs.options.languages, { 28212 [code]: data 28213 }); 28214 return videojs.options.languages[code]; 28215 }; 28216 28217 /** 28218 * A reference to the {@link module:log|log utility module} as an object. 28219 * 28220 * @type {Function} 28221 * @see {@link module:log|log} 28222 */ 28223 videojs.log = log$1; 28224 videojs.createLogger = createLogger; 28225 28226 /** 28227 * A reference to the {@link module:time|time utility module} as an object. 28228 * 28229 * @type {Object} 28230 * @see {@link module:time|time} 28231 */ 28232 videojs.time = Time; 28233 28234 /** 28235 * Deprecated reference to the {@link module:time.createTimeRanges|createTimeRanges function} 28236 * 28237 * @type {Function} 28238 * @see {@link module:time.createTimeRanges|createTimeRanges} 28239 * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.createTimeRanges instead. 28240 */ 28241 videojs.createTimeRange = deprecateForMajor(9, 'videojs.createTimeRange', 'videojs.time.createTimeRanges', createTimeRanges$1); 28242 28243 /** 28244 * Deprecated reference to the {@link module:time.createTimeRanges|createTimeRanges function} 28245 * 28246 * @type {Function} 28247 * @see {@link module:time.createTimeRanges|createTimeRanges} 28248 * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.createTimeRanges instead. 28249 */ 28250 videojs.createTimeRanges = deprecateForMajor(9, 'videojs.createTimeRanges', 'videojs.time.createTimeRanges', createTimeRanges$1); 28251 28252 /** 28253 * Deprecated reference to the {@link module:time.formatTime|formatTime function} 28254 * 28255 * @type {Function} 28256 * @see {@link module:time.formatTime|formatTime} 28257 * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.format instead. 28258 */ 28259 videojs.formatTime = deprecateForMajor(9, 'videojs.formatTime', 'videojs.time.formatTime', formatTime); 28260 28261 /** 28262 * Deprecated reference to the {@link module:time.setFormatTime|setFormatTime function} 28263 * 28264 * @type {Function} 28265 * @see {@link module:time.setFormatTime|setFormatTime} 28266 * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.setFormat instead. 28267 */ 28268 videojs.setFormatTime = deprecateForMajor(9, 'videojs.setFormatTime', 'videojs.time.setFormatTime', setFormatTime); 28269 28270 /** 28271 * Deprecated reference to the {@link module:time.resetFormatTime|resetFormatTime function} 28272 * 28273 * @type {Function} 28274 * @see {@link module:time.resetFormatTime|resetFormatTime} 28275 * @deprecated Deprecated and will be removed in 9.0. Please use videojs.time.resetFormat instead. 28276 */ 28277 videojs.resetFormatTime = deprecateForMajor(9, 'videojs.resetFormatTime', 'videojs.time.resetFormatTime', resetFormatTime); 28278 28279 /** 28280 * Deprecated reference to the {@link module:url.parseUrl|Url.parseUrl function} 28281 * 28282 * @type {Function} 28283 * @see {@link module:url.parseUrl|parseUrl} 28284 * @deprecated Deprecated and will be removed in 9.0. Please use videojs.url.parseUrl instead. 28285 */ 28286 videojs.parseUrl = deprecateForMajor(9, 'videojs.parseUrl', 'videojs.url.parseUrl', parseUrl); 28287 28288 /** 28289 * Deprecated reference to the {@link module:url.isCrossOrigin|Url.isCrossOrigin function} 28290 * 28291 * @type {Function} 28292 * @see {@link module:url.isCrossOrigin|isCrossOrigin} 28293 * @deprecated Deprecated and will be removed in 9.0. Please use videojs.url.isCrossOrigin instead. 28294 */ 28295 videojs.isCrossOrigin = deprecateForMajor(9, 'videojs.isCrossOrigin', 'videojs.url.isCrossOrigin', isCrossOrigin); 28296 videojs.EventTarget = EventTarget$2; 28297 videojs.any = any; 28298 videojs.on = on; 28299 videojs.one = one; 28300 videojs.off = off; 28301 videojs.trigger = trigger; 28302 28303 /** 28304 * A cross-browser XMLHttpRequest wrapper. 28305 * 28306 * @function 28307 * @param {Object} options 28308 * Settings for the request. 28309 * 28310 * @return {XMLHttpRequest|XDomainRequest} 28311 * The request object. 28312 * 28313 * @see https://github.com/Raynos/xhr 28314 */ 28315 videojs.xhr = lib; 28316 videojs.TextTrack = TextTrack; 28317 videojs.AudioTrack = AudioTrack; 28318 videojs.VideoTrack = VideoTrack; 28319 ['isEl', 'isTextNode', 'createEl', 'hasClass', 'addClass', 'removeClass', 'toggleClass', 'setAttributes', 'getAttributes', 'emptyEl', 'appendContent', 'insertContent'].forEach(k => { 28320 videojs[k] = function () { 28321 log$1.warn(`videojs.${k}() is deprecated; use videojs.dom.${k}() instead`); 28322 return Dom[k].apply(null, arguments); 28323 }; 28324 }); 28325 videojs.computedStyle = deprecateForMajor(9, 'videojs.computedStyle', 'videojs.dom.computedStyle', computedStyle); 28326 28327 /** 28328 * A reference to the {@link module:dom|DOM utility module} as an object. 28329 * 28330 * @type {Object} 28331 * @see {@link module:dom|dom} 28332 */ 28333 videojs.dom = Dom; 28334 28335 /** 28336 * A reference to the {@link module:fn|fn utility module} as an object. 28337 * 28338 * @type {Object} 28339 * @see {@link module:fn|fn} 28340 */ 28341 videojs.fn = Fn; 28342 28343 /** 28344 * A reference to the {@link module:num|num utility module} as an object. 28345 * 28346 * @type {Object} 28347 * @see {@link module:num|num} 28348 */ 28349 videojs.num = Num; 28350 28351 /** 28352 * A reference to the {@link module:str|str utility module} as an object. 28353 * 28354 * @type {Object} 28355 * @see {@link module:str|str} 28356 */ 28357 videojs.str = Str; 28358 28359 /** 28360 * A reference to the {@link module:url|URL utility module} as an object. 28361 * 28362 * @type {Object} 28363 * @see {@link module:url|url} 28364 */ 28365 videojs.url = Url; 28366
vendor: 10,142 bytes, lines 28367-28651
28367 createCommonjsModule(function (module, exports) { 28368 /*! @name videojs-contrib-quality-levels @version 4.0.0 @license Apache-2.0 */ 28369 (function (global, factory) { 28370 module.exports = factory(videojs) ; 28371 })(commonjsGlobal, function (videojs) { 28372 28373 function _interopDefaultLegacy(e) { 28374 return e && typeof e === 'object' && 'default' in e ? e : { 28375 'default': e 28376 }; 28377 } 28378 var videojs__default = /*#__PURE__*/_interopDefaultLegacy(videojs); 28379 28380 /** 28381 * A single QualityLevel. 28382 * 28383 * interface QualityLevel { 28384 * readonly attribute DOMString id; 28385 * attribute DOMString label; 28386 * readonly attribute long width; 28387 * readonly attribute long height; 28388 * readonly attribute long bitrate; 28389 * attribute boolean enabled; 28390 * }; 28391 * 28392 * @class QualityLevel 28393 */ 28394 class QualityLevel { 28395 /** 28396 * Creates a QualityLevel 28397 * 28398 * @param {Representation|Object} representation The representation of the quality level 28399 * @param {string} representation.id Unique id of the QualityLevel 28400 * @param {number=} representation.width Resolution width of the QualityLevel 28401 * @param {number=} representation.height Resolution height of the QualityLevel 28402 * @param {number} representation.bandwidth Bitrate of the QualityLevel 28403 * @param {number=} representation.frameRate Frame-rate of the QualityLevel 28404 * @param {Function} representation.enabled Callback to enable/disable QualityLevel 28405 */ 28406 constructor(representation) { 28407 let level = this; // eslint-disable-line 28408 28409 level.id = representation.id; 28410 level.label = level.id; 28411 level.width = representation.width; 28412 level.height = representation.height; 28413 level.bitrate = representation.bandwidth; 28414 level.frameRate = representation.frameRate; 28415 level.enabled_ = representation.enabled; 28416 Object.defineProperty(level, 'enabled', { 28417 /** 28418 * Get whether the QualityLevel is enabled. 28419 * 28420 * @return {boolean} True if the QualityLevel is enabled. 28421 */ 28422 get() { 28423 return level.enabled_(); 28424 }, 28425 /** 28426 * Enable or disable the QualityLevel. 28427 * 28428 * @param {boolean} enable true to enable QualityLevel, false to disable. 28429 */ 28430 set(enable) { 28431 level.enabled_(enable); 28432 } 28433 }); 28434 return level; 28435 } 28436 } 28437 28438 /** 28439 * A list of QualityLevels. 28440 * 28441 * interface QualityLevelList : EventTarget { 28442 * getter QualityLevel (unsigned long index); 28443 * readonly attribute unsigned long length; 28444 * readonly attribute long selectedIndex; 28445 * 28446 * void addQualityLevel(QualityLevel qualityLevel) 28447 * void removeQualityLevel(QualityLevel remove) 28448 * QualityLevel? getQualityLevelById(DOMString id); 28449 * 28450 * attribute EventHandler onchange; 28451 * attribute EventHandler onaddqualitylevel; 28452 * attribute EventHandler onremovequalitylevel; 28453 * }; 28454 * 28455 * @extends videojs.EventTarget 28456 * @class QualityLevelList 28457 */ 28458 28459 class QualityLevelList extends videojs__default['default'].EventTarget { 28460 /** 28461 * Creates a QualityLevelList. 28462 */ 28463 constructor() { 28464 super(); 28465 let list = this; // eslint-disable-line 28466 28467 list.levels_ = []; 28468 list.selectedIndex_ = -1; 28469 /** 28470 * Get the index of the currently selected QualityLevel. 28471 * 28472 * @returns {number} The index of the selected QualityLevel. -1 if none selected. 28473 * @readonly 28474 */ 28475 28476 Object.defineProperty(list, 'selectedIndex', { 28477 get() { 28478 return list.selectedIndex_; 28479 } 28480 }); 28481 /** 28482 * Get the length of the list of QualityLevels. 28483 * 28484 * @returns {number} The length of the list. 28485 * @readonly 28486 */ 28487 28488 Object.defineProperty(list, 'length', { 28489 get() { 28490 return list.levels_.length; 28491 } 28492 }); 28493 list[Symbol.iterator] = () => list.levels_.values(); 28494 return list; 28495 } 28496 /** 28497 * Adds a quality level to the list. 28498 * 28499 * @param {Representation|Object} representation The representation of the quality level 28500 * @param {string} representation.id Unique id of the QualityLevel 28501 * @param {number=} representation.width Resolution width of the QualityLevel 28502 * @param {number=} representation.height Resolution height of the QualityLevel 28503 * @param {number} representation.bandwidth Bitrate of the QualityLevel 28504 * @param {number=} representation.frameRate Frame-rate of the QualityLevel 28505 * @param {Function} representation.enabled Callback to enable/disable QualityLevel 28506 * @return {QualityLevel} the QualityLevel added to the list 28507 * @method addQualityLevel 28508 */ 28509 28510 addQualityLevel(representation) { 28511 let qualityLevel = this.getQualityLevelById(representation.id); // Do not add duplicate quality levels 28512 28513 if (qualityLevel) { 28514 return qualityLevel; 28515 } 28516 const index = this.levels_.length; 28517 qualityLevel = new QualityLevel(representation); 28518 if (!('' + index in this)) { 28519 Object.defineProperty(this, index, { 28520 get() { 28521 return this.levels_[index]; 28522 } 28523 }); 28524 } 28525 this.levels_.push(qualityLevel); 28526 this.trigger({ 28527 qualityLevel, 28528 type: 'addqualitylevel' 28529 }); 28530 return qualityLevel; 28531 } 28532 /** 28533 * Removes a quality level from the list. 28534 * 28535 * @param {QualityLevel} qualityLevel The QualityLevel to remove from the list. 28536 * @return {QualityLevel|null} the QualityLevel removed or null if nothing removed 28537 * @method removeQualityLevel 28538 */ 28539 28540 removeQualityLevel(qualityLevel) { 28541 let removed = null; 28542 for (let i = 0, l = this.length; i < l; i++) { 28543 if (this[i] === qualityLevel) { 28544 removed = this.levels_.splice(i, 1)[0]; 28545 if (this.selectedIndex_ === i) { 28546 this.selectedIndex_ = -1; 28547 } else if (this.selectedIndex_ > i) { 28548 this.selectedIndex_--; 28549 } 28550 break; 28551 } 28552 } 28553 if (removed) { 28554 this.trigger({ 28555 qualityLevel, 28556 type: 'removequalitylevel' 28557 }); 28558 } 28559 return removed; 28560 } 28561 /** 28562 * Searches for a QualityLevel with the given id. 28563 * 28564 * @param {string} id The id of the QualityLevel to find. 28565 * @return {QualityLevel|null} The QualityLevel with id, or null if not found. 28566 * @method getQualityLevelById 28567 */ 28568 28569 getQualityLevelById(id) { 28570 for (let i = 0, l = this.length; i < l; i++) { 28571 const level = this[i]; 28572 if (level.id === id) { 28573 return level; 28574 } 28575 } 28576 return null; 28577 } 28578 /** 28579 * Resets the list of QualityLevels to empty 28580 * 28581 * @method dispose 28582 */ 28583 28584 dispose() { 28585 this.selectedIndex_ = -1; 28586 this.levels_.length = 0; 28587 } 28588 } 28589 /** 28590 * change - The selected QualityLevel has changed. 28591 * addqualitylevel - A QualityLevel has been added to the QualityLevelList. 28592 * removequalitylevel - A QualityLevel has been removed from the QualityLevelList. 28593 */ 28594 28595 QualityLevelList.prototype.allowedEvents_ = { 28596 change: 'change', 28597 addqualitylevel: 'addqualitylevel', 28598 removequalitylevel: 'removequalitylevel' 28599 }; // emulate attribute EventHandler support to allow for feature detection 28600 28601 for (const event in QualityLevelList.prototype.allowedEvents_) { 28602 QualityLevelList.prototype['on' + event] = null; 28603 } 28604 var version = "4.0.0"; 28605 28606 /** 28607 * Initialization function for the qualityLevels plugin. Sets up the QualityLevelList and 28608 * event handlers. 28609 * 28610 * @param {Player} player Player object. 28611 * @param {Object} options Plugin options object. 28612 * @return {QualityLevelList} a list of QualityLevels 28613 */ 28614 28615 const initPlugin = function (player, options) { 28616 const originalPluginFn = player.qualityLevels; 28617 const qualityLevelList = new QualityLevelList(); 28618 const disposeHandler = function () { 28619 qualityLevelList.dispose(); 28620 player.qualityLevels = originalPluginFn; 28621 player.off('dispose', disposeHandler); 28622 }; 28623 player.on('dispose', disposeHandler); 28624 player.qualityLevels = () => qualityLevelList; 28625 player.qualityLevels.VERSION = version; 28626 return qualityLevelList; 28627 }; 28628 /** 28629 * A video.js plugin. 28630 * 28631 * In the plugin function, the value of `this` is a video.js `Player` 28632 * instance. You cannot rely on the player being in a "ready" state here, 28633 * depending on how the plugin is invoked. This may or may not be important 28634 * to you; if not, remove the wait for "ready"! 28635 * 28636 * @param {Object} options Plugin options object 28637 * @return {QualityLevelList} a list of QualityLevels 28638 */ 28639 28640 const qualityLevels = function (options) { 28641 return initPlugin(this, videojs__default['default'].obj.merge({}, options)); 28642 }; // Register the plugin with video.js. 28643 28644 videojs__default['default'].registerPlugin('qualityLevels', qualityLevels); // Include the version number. 28645 28646 qualityLevels.VERSION = version; 28647 return qualityLevels; 28648 }); 28649 }); 28650 28651 var urlToolkit = createCommonjsModule(function (module, exports
vendor: 4,625 bytes, lines 28651-28741
28651) { 28652 // see https://tools.ietf.org/html/rfc1808 28653 28654 (function (root) { 28655 var URL_REGEX = /^(?=((?:[a-zA-Z0-9+\-.]+:)?))\1(?=((?:\/\/[^\/?#]*)?))\2(?=((?:(?:[^?#\/]*\/)*[^;?#\/]*)?))\3((?:;[^?#]*)?)(\?[^#]*)?(#[^]*)?$/; 28656 var FIRST_SEGMENT_REGEX = /^(?=([^\/?#]*))\1([^]*)$/; 28657 var SLASH_DOT_REGEX = /(?:\/|^)\.(?=\/)/g; 28658 var SLASH_DOT_DOT_REGEX = /(?:\/|^)\.\.\/(?!\.\.\/)[^\/]*(?=\/)/g; 28659 var URLToolkit = { 28660 // If opts.alwaysNormalize is true then the path will always be normalized even when it starts with / or // 28661 // E.g 28662 // With opts.alwaysNormalize = false (default, spec compliant) 28663 // http://a.com/b/cd + /e/f/../g => http://a.com/e/f/../g 28664 // With opts.alwaysNormalize = true (not spec compliant) 28665 // http://a.com/b/cd + /e/f/../g => http://a.com/e/g 28666 buildAbsoluteURL: function (baseURL, relativeURL, opts) { 28667 opts = opts || {}; 28668 // remove any remaining space and CRLF 28669 baseURL = baseURL.trim(); 28670 relativeURL = relativeURL.trim(); 28671 if (!relativeURL) { 28672 // 2a) If the embedded URL is entirely empty, it inherits the 28673 // entire base URL (i.e., is set equal to the base URL) 28674 // and we are done. 28675 if (!opts.alwaysNormalize) { 28676 return baseURL; 28677 } 28678 var basePartsForNormalise = URLToolkit.parseURL(baseURL); 28679 if (!basePartsForNormalise) { 28680 throw new Error('Error trying to parse base URL.'); 28681 } 28682 basePartsForNormalise.path = URLToolkit.normalizePath(basePartsForNormalise.path); 28683 return URLToolkit.buildURLFromParts(basePartsForNormalise); 28684 } 28685 var relativeParts = URLToolkit.parseURL(relativeURL); 28686 if (!relativeParts) { 28687 throw new Error('Error trying to parse relative URL.'); 28688 } 28689 if (relativeParts.scheme) { 28690 // 2b) If the embedded URL starts with a scheme name, it is 28691 // interpreted as an absolute URL and we are done. 28692 if (!opts.alwaysNormalize) { 28693 return relativeURL; 28694 } 28695 relativeParts.path = URLToolkit.normalizePath(relativeParts.path); 28696 return URLToolkit.buildURLFromParts(relativeParts); 28697 } 28698 var baseParts = URLToolkit.parseURL(baseURL); 28699 if (!baseParts) { 28700 throw new Error('Error trying to parse base URL.'); 28701 } 28702 if (!baseParts.netLoc && baseParts.path && baseParts.path[0] !== '/') { 28703 // If netLoc missing and path doesn't start with '/', assume everthing before the first '/' is the netLoc 28704 // This causes 'example.com/a' to be handled as '//example.com/a' instead of '/example.com/a' 28705 var pathParts = FIRST_SEGMENT_REGEX.exec(baseParts.path); 28706 baseParts.netLoc = pathParts[1]; 28707 baseParts.path = pathParts[2]; 28708 } 28709 if (baseParts.netLoc && !baseParts.path) { 28710 baseParts.path = '/'; 28711 } 28712 var builtParts = { 28713 // 2c) Otherwise, the embedded URL inherits the scheme of 28714 // the base URL. 28715 scheme: baseParts.scheme, 28716 netLoc: relativeParts.netLoc, 28717 path: null, 28718 params: relativeParts.params, 28719 query: relativeParts.query, 28720 fragment: relativeParts.fragment 28721 }; 28722 if (!relativeParts.netLoc) { 28723 // 3) If the embedded URL's <net_loc> is non-empty, we skip to 28724 // Step 7. Otherwise, the embedded URL inherits the <net_loc> 28725 // (if any) of the base URL. 28726 builtParts.netLoc = baseParts.netLoc; 28727 // 4) If the embedded URL path is preceded by a slash "/", the 28728 // path is not relative and we skip to Step 7. 28729 if (relativeParts.path[0] !== '/') { 28730 if (!relativeParts.path) { 28731 // 5) If the embedded URL path is empty (and not preceded by a 28732 // slash), then the embedded URL inherits the base URL path 28733 builtParts.path = baseParts.path; 28734 // 5a) if the embedded URL's <params> is non-empty, we skip to 28735 // step 7; otherwise, it inherits the <params> of the base 28736 // URL (if any) and 28737 if (!relativeParts.params) { 28738 builtParts.params = baseParts.params; 28739 // 5b) if the embedded URL's <query> is non-empty, we skip to 28740 // step 7; otherwise, it inherits the <query> of the base 28741 // URL (if any) and we skip to step 7.
vendor: 5,376 bytes, lines 28742-28880
28742 if (!relativeParts.query) { 28743 builtParts.query = baseParts.query; 28744 } 28745 } 28746 } else { 28747 // 6) The last segment of the base URL's path (anything 28748 // following the rightmost slash "/", or the entire path if no 28749 // slash is present) is removed and the embedded URL's path is 28750 // appended in its place. 28751 var baseURLPath = baseParts.path; 28752 var newPath = baseURLPath.substring(0, baseURLPath.lastIndexOf('/') + 1) + relativeParts.path; 28753 builtParts.path = URLToolkit.normalizePath(newPath); 28754 } 28755 } 28756 } 28757 if (builtParts.path === null) { 28758 builtParts.path = opts.alwaysNormalize ? URLToolkit.normalizePath(relativeParts.path) : relativeParts.path; 28759 } 28760 return URLToolkit.buildURLFromParts(builtParts); 28761 }, 28762 parseURL: function (url) { 28763 var parts = URL_REGEX.exec(url); 28764 if (!parts) { 28765 return null; 28766 } 28767 return { 28768 scheme: parts[1] || '', 28769 netLoc: parts[2] || '', 28770 path: parts[3] || '', 28771 params: parts[4] || '', 28772 query: parts[5] || '', 28773 fragment: parts[6] || '' 28774 }; 28775 }, 28776 normalizePath: function (path) { 28777 // The following operations are 28778 // then applied, in order, to the new path: 28779 // 6a) All occurrences of "./", where "." is a complete path 28780 // segment, are removed. 28781 // 6b) If the path ends with "." as a complete path segment, 28782 // that "." is removed. 28783 path = path.split('').reverse().join('').replace(SLASH_DOT_REGEX, ''); 28784 // 6c) All occurrences of "<segment>/../", where <segment> is a 28785 // complete path segment not equal to "..", are removed. 28786 // Removal of these path segments is performed iteratively, 28787 // removing the leftmost matching pattern on each iteration, 28788 // until no matching pattern remains. 28789 // 6d) If the path ends with "<segment>/..", where <segment> is a 28790 // complete path segment not equal to "..", that 28791 // "<segment>/.." is removed. 28792 while (path.length !== (path = path.replace(SLASH_DOT_DOT_REGEX, '')).length) {} 28793 return path.split('').reverse().join(''); 28794 }, 28795 buildURLFromParts: function (parts) { 28796 return parts.scheme + parts.netLoc + parts.path + parts.params + parts.query + parts.fragment; 28797 } 28798 }; 28799 module.exports = URLToolkit; 28800 })(); 28801 }); 28802 28803 var DEFAULT_LOCATION = 'http://example.com'; 28804 var resolveUrl$1 = function resolveUrl(baseUrl, relativeUrl) { 28805 // return early if we don't need to resolve 28806 if (/^[a-z]+:/i.test(relativeUrl)) { 28807 return relativeUrl; 28808 } // if baseUrl is a data URI, ignore it and resolve everything relative to window.location 28809 28810 if (/^data:/.test(baseUrl)) { 28811 baseUrl = window.location && window.location.href || ''; 28812 } // IE11 supports URL but not the URL constructor 28813 // feature detect the behavior we want 28814 28815 var nativeURL = typeof window.URL === 'function'; 28816 var protocolLess = /^\/\//.test(baseUrl); // remove location if window.location isn't available (i.e. we're in node) 28817 // and if baseUrl isn't an absolute url 28818 28819 var removeLocation = !window.location && !/\/\//i.test(baseUrl); // if the base URL is relative then combine with the current location 28820 28821 if (nativeURL) { 28822 baseUrl = new window.URL(baseUrl, window.location || DEFAULT_LOCATION); 28823 } else if (!/\/\//i.test(baseUrl)) { 28824 baseUrl = urlToolkit.buildAbsoluteURL(window.location && window.location.href || '', baseUrl); 28825 } 28826 if (nativeURL) { 28827 var newUrl = new URL(relativeUrl, baseUrl); // if we're a protocol-less url, remove the protocol 28828 // and if we're location-less, remove the location 28829 // otherwise, return the url unmodified 28830 28831 if (removeLocation) { 28832 return newUrl.href.slice(DEFAULT_LOCATION.length); 28833 } else if (protocolLess) { 28834 return newUrl.href.slice(newUrl.protocol.length); 28835 } 28836 return newUrl.href; 28837 } 28838 return urlToolkit.buildAbsoluteURL(baseUrl, relativeUrl); 28839 }; 28840 28841 /** 28842 * @file stream.js 28843 */ 28844 28845 /** 28846 * A lightweight readable stream implemention that handles event dispatching. 28847 * 28848 * @class Stream 28849 */ 28850 var Stream = /*#__PURE__*/function () { 28851 function Stream() { 28852 this.listeners = {}; 28853 } 28854 /** 28855 * Add a listener for a specified event type. 28856 * 28857 * @param {string} type the event name 28858 * @param {Function} listener the callback to be invoked when an event of 28859 * the specified type occurs 28860 */ 28861 28862 var _proto = Stream.prototype; 28863 _proto.on = function on(type, listener) { 28864 if (!this.listeners[type]) { 28865 this.listeners[type] = []; 28866 } 28867 this.listeners[type].push(listener); 28868 } 28869 /** 28870 * Remove a listener for a specified event type. 28871 * 28872 * @param {string} type the event name 28873 * @param {Function} listener a function previously registered for this 28874 * type of event through `on` 28875 * @return {boolean} if we could turn it off or not 28876 */; 28877 28878 _proto.off = function off(type, listener) { 28879 if (!this.listeners[type]) { 28880 return false;
vendor: 36,599 bytes, lines 28881-29924
28881 } 28882 var index = this.listeners[type].indexOf(listener); // TODO: which is better? 28883 // In Video.js we slice listener functions 28884 // on trigger so that it does not mess up the order 28885 // while we loop through. 28886 // 28887 // Here we slice on off so that the loop in trigger 28888 // can continue using it's old reference to loop without 28889 // messing up the order. 28890 28891 this.listeners[type] = this.listeners[type].slice(0); 28892 this.listeners[type].splice(index, 1); 28893 return index > -1; 28894 } 28895 /** 28896 * Trigger an event of the specified type on this stream. Any additional 28897 * arguments to this function are passed as parameters to event listeners. 28898 * 28899 * @param {string} type the event name 28900 */; 28901 28902 _proto.trigger = function trigger(type) { 28903 var callbacks = this.listeners[type]; 28904 if (!callbacks) { 28905 return; 28906 } // Slicing the arguments on every invocation of this method 28907 // can add a significant amount of overhead. Avoid the 28908 // intermediate object creation for the common case of a 28909 // single callback argument 28910 28911 if (arguments.length === 2) { 28912 var length = callbacks.length; 28913 for (var i = 0; i < length; ++i) { 28914 callbacks[i].call(this, arguments[1]); 28915 } 28916 } else { 28917 var args = Array.prototype.slice.call(arguments, 1); 28918 var _length = callbacks.length; 28919 for (var _i = 0; _i < _length; ++_i) { 28920 callbacks[_i].apply(this, args); 28921 } 28922 } 28923 } 28924 /** 28925 * Destroys the stream and cleans up. 28926 */; 28927 28928 _proto.dispose = function dispose() { 28929 this.listeners = {}; 28930 } 28931 /** 28932 * Forwards all `data` events on this stream to the destination stream. The 28933 * destination stream should provide a method `push` to receive the data 28934 * events as they arrive. 28935 * 28936 * @param {Stream} destination the stream that will receive all `data` events 28937 * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options 28938 */; 28939 28940 _proto.pipe = function pipe(destination) { 28941 this.on('data', function (data) { 28942 destination.push(data); 28943 }); 28944 }; 28945 return Stream; 28946 }(); 28947 28948 var atob$1 = function atob(s) { 28949 return window.atob ? window.atob(s) : Buffer.from(s, 'base64').toString('binary'); 28950 }; 28951 function decodeB64ToUint8Array$1(b64Text) { 28952 var decodedString = atob$1(b64Text); 28953 var array = new Uint8Array(decodedString.length); 28954 for (var i = 0; i < decodedString.length; i++) { 28955 array[i] = decodedString.charCodeAt(i); 28956 } 28957 return array; 28958 } 28959 28960 /*! @name m3u8-parser @version 7.1.0 @license Apache-2.0 */ 28961 28962 /** 28963 * @file m3u8/line-stream.js 28964 */ 28965 /** 28966 * A stream that buffers string input and generates a `data` event for each 28967 * line. 28968 * 28969 * @class LineStream 28970 * @extends Stream 28971 */ 28972 28973 class LineStream extends Stream { 28974 constructor() { 28975 super(); 28976 this.buffer = ''; 28977 } 28978 /** 28979 * Add new data to be parsed. 28980 * 28981 * @param {string} data the text to process 28982 */ 28983 28984 push(data) { 28985 let nextNewline; 28986 this.buffer += data; 28987 nextNewline = this.buffer.indexOf('\n'); 28988 for (; nextNewline > -1; nextNewline = this.buffer.indexOf('\n')) { 28989 this.trigger('data', this.buffer.substring(0, nextNewline)); 28990 this.buffer = this.buffer.substring(nextNewline + 1); 28991 } 28992 } 28993 } 28994 const TAB = String.fromCharCode(0x09); 28995 const parseByterange = function (byterangeString) { 28996 // optionally match and capture 0+ digits before `@` 28997 // optionally match and capture 0+ digits after `@` 28998 const match = /([0-9.]*)?@?([0-9.]*)?/.exec(byterangeString || ''); 28999 const result = {}; 29000 if (match[1]) { 29001 result.length = parseInt(match[1], 10); 29002 } 29003 if (match[2]) { 29004 result.offset = parseInt(match[2], 10); 29005 } 29006 return result; 29007 }; 29008 /** 29009 * "forgiving" attribute list psuedo-grammar: 29010 * attributes -> keyvalue (',' keyvalue)* 29011 * keyvalue -> key '=' value 29012 * key -> [^=]* 29013 * value -> '"' [^"]* '"' | [^,]* 29014 */ 29015 29016 const attributeSeparator = function () { 29017 const key = '[^=]*'; 29018 const value = '"[^"]*"|[^,]*'; 29019 const keyvalue = '(?:' + key + ')=(?:' + value + ')'; 29020 return new RegExp('(?:^|,)(' + keyvalue + ')'); 29021 }; 29022 /** 29023 * Parse attributes from a line given the separator 29024 * 29025 * @param {string} attributes the attribute line to parse 29026 */ 29027 29028 const parseAttributes$1 = function (attributes) { 29029 const result = {}; 29030 if (!attributes) { 29031 return result; 29032 } // split the string using attributes as the separator 29033 29034 const attrs = attributes.split(attributeSeparator()); 29035 let i = attrs.length; 29036 let attr; 29037 while (i--) { 29038 // filter out unmatched portions of the string 29039 if (attrs[i] === '') { 29040 continue; 29041 } // split the key and value 29042 29043 attr = /([^=]*)=(.*)/.exec(attrs[i]).slice(1); // trim whitespace and remove optional quotes around the value 29044 29045 attr[0] = attr[0].replace(/^\s+|\s+$/g, ''); 29046 attr[1] = attr[1].replace(/^\s+|\s+$/g, ''); 29047 attr[1] = attr[1].replace(/^['"](.*)['"]$/g, '$1'); 29048 result[attr[0]] = attr[1]; 29049 } 29050 return result; 29051 }; 29052 /** 29053 * A line-level M3U8 parser event stream. It expects to receive input one 29054 * line at a time and performs a context-free parse of its contents. A stream 29055 * interpretation of a manifest can be useful if the manifest is expected to 29056 * be too large to fit comfortably into memory or the entirety of the input 29057 * is not immediately available. Otherwise, it's probably much easier to work 29058 * with a regular `Parser` object. 29059 * 29060 * Produces `data` events with an object that captures the parser's 29061 * interpretation of the input. That object has a property `tag` that is one 29062 * of `uri`, `comment`, or `tag`. URIs only have a single additional 29063 * property, `line`, which captures the entirety of the input without 29064 * interpretation. Comments similarly have a single additional property 29065 * `text` which is the input without the leading `#`. 29066 * 29067 * Tags always have a property `tagType` which is the lower-cased version of 29068 * the M3U8 directive without the `#EXT` or `#EXT-X-` prefix. For instance, 29069 * `#EXT-X-MEDIA-SEQUENCE` becomes `media-sequence` when parsed. Unrecognized 29070 * tags are given the tag type `unknown` and a single additional property 29071 * `data` with the remainder of the input. 29072 * 29073 * @class ParseStream 29074 * @extends Stream 29075 */ 29076 29077 class ParseStream extends Stream { 29078 constructor() { 29079 super(); 29080 this.customParsers = []; 29081 this.tagMappers = []; 29082 } 29083 /** 29084 * Parses an additional line of input. 29085 * 29086 * @param {string} line a single line of an M3U8 file to parse 29087 */ 29088 29089 push(line) { 29090 let match; 29091 let event; // strip whitespace 29092 29093 line = line.trim(); 29094 if (line.length === 0) { 29095 // ignore empty lines 29096 return; 29097 } // URIs 29098 29099 if (line[0] !== '#') { 29100 this.trigger('data', { 29101 type: 'uri', 29102 uri: line 29103 }); 29104 return; 29105 } // map tags 29106 29107 const newLines = this.tagMappers.reduce((acc, mapper) => { 29108 const mappedLine = mapper(line); // skip if unchanged 29109 29110 if (mappedLine === line) { 29111 return acc; 29112 } 29113 return acc.concat([mappedLine]); 29114 }, [line]); 29115 newLines.forEach(newLine => { 29116 for (let i = 0; i < this.customParsers.length; i++) { 29117 if (this.customParsers[i].call(this, newLine)) { 29118 return; 29119 } 29120 } // Comments 29121 29122 if (newLine.indexOf('#EXT') !== 0) { 29123 this.trigger('data', { 29124 type: 'comment', 29125 text: newLine.slice(1) 29126 }); 29127 return; 29128 } // strip off any carriage returns here so the regex matching 29129 // doesn't have to account for them. 29130 29131 newLine = newLine.replace('\r', ''); // Tags 29132 29133 match = /^#EXTM3U/.exec(newLine); 29134 if (match) { 29135 this.trigger('data', { 29136 type: 'tag', 29137 tagType: 'm3u' 29138 }); 29139 return; 29140 } 29141 match = /^#EXTINF:([0-9\.]*)?,?(.*)?$/.exec(newLine); 29142 if (match) { 29143 event = { 29144 type: 'tag', 29145 tagType: 'inf' 29146 }; 29147 if (match[1]) { 29148 event.duration = parseFloat(match[1]); 29149 } 29150 if (match[2]) { 29151 event.title = match[2]; 29152 } 29153 this.trigger('data', event); 29154 return; 29155 } 29156 match = /^#EXT-X-TARGETDURATION:([0-9.]*)?/.exec(newLine); 29157 if (match) { 29158 event = { 29159 type: 'tag', 29160 tagType: 'targetduration' 29161 }; 29162 if (match[1]) { 29163 event.duration = parseInt(match[1], 10); 29164 } 29165 this.trigger('data', event); 29166 return; 29167 } 29168 match = /^#EXT-X-VERSION:([0-9.]*)?/.exec(newLine); 29169 if (match) { 29170 event = { 29171 type: 'tag', 29172 tagType: 'version' 29173 }; 29174 if (match[1]) { 29175 event.version = parseInt(match[1], 10); 29176 } 29177 this.trigger('data', event); 29178 return; 29179 } 29180 match = /^#EXT-X-MEDIA-SEQUENCE:(\-?[0-9.]*)?/.exec(newLine); 29181 if (match) { 29182 event = { 29183 type: 'tag', 29184 tagType: 'media-sequence' 29185 }; 29186 if (match[1]) { 29187 event.number = parseInt(match[1], 10); 29188 } 29189 this.trigger('data', event); 29190 return; 29191 } 29192 match = /^#EXT-X-DISCONTINUITY-SEQUENCE:(\-?[0-9.]*)?/.exec(newLine); 29193 if (match) { 29194 event = { 29195 type: 'tag', 29196 tagType: 'discontinuity-sequence' 29197 }; 29198 if (match[1]) { 29199 event.number = parseInt(match[1], 10); 29200 } 29201 this.trigger('data', event); 29202 return; 29203 } 29204 match = /^#EXT-X-PLAYLIST-TYPE:(.*)?$/.exec(newLine); 29205 if (match) { 29206 event = { 29207 type: 'tag', 29208 tagType: 'playlist-type' 29209 }; 29210 if (match[1]) { 29211 event.playlistType = match[1]; 29212 } 29213 this.trigger('data', event); 29214 return; 29215 } 29216 match = /^#EXT-X-BYTERANGE:(.*)?$/.exec(newLine); 29217 if (match) { 29218 event = _extends$1(parseByterange(match[1]), { 29219 type: 'tag', 29220 tagType: 'byterange' 29221 }); 29222 this.trigger('data', event); 29223 return; 29224 } 29225 match = /^#EXT-X-ALLOW-CACHE:(YES|NO)?/.exec(newLine); 29226 if (match) { 29227 event = { 29228 type: 'tag', 29229 tagType: 'allow-cache' 29230 }; 29231 if (match[1]) { 29232 event.allowed = !/NO/.test(match[1]); 29233 } 29234 this.trigger('data', event); 29235 return; 29236 } 29237 match = /^#EXT-X-MAP:(.*)$/.exec(newLine); 29238 if (match) { 29239 event = { 29240 type: 'tag', 29241 tagType: 'map' 29242 }; 29243 if (match[1]) { 29244 const attributes = parseAttributes$1(match[1]); 29245 if (attributes.URI) { 29246 event.uri = attributes.URI; 29247 } 29248 if (attributes.BYTERANGE) { 29249 event.byterange = parseByterange(attributes.BYTERANGE); 29250 } 29251 } 29252 this.trigger('data', event); 29253 return; 29254 } 29255 match = /^#EXT-X-STREAM-INF:(.*)$/.exec(newLine); 29256 if (match) { 29257 event = { 29258 type: 'tag', 29259 tagType: 'stream-inf' 29260 }; 29261 if (match[1]) { 29262 event.attributes = parseAttributes$1(match[1]); 29263 if (event.attributes.RESOLUTION) { 29264 const split = event.attributes.RESOLUTION.split('x'); 29265 const resolution = {}; 29266 if (split[0]) { 29267 resolution.width = parseInt(split[0], 10); 29268 } 29269 if (split[1]) { 29270 resolution.height = parseInt(split[1], 10); 29271 } 29272 event.attributes.RESOLUTION = resolution; 29273 } 29274 if (event.attributes.BANDWIDTH) { 29275 event.attributes.BANDWIDTH = parseInt(event.attributes.BANDWIDTH, 10); 29276 } 29277 if (event.attributes['FRAME-RATE']) { 29278 event.attributes['FRAME-RATE'] = parseFloat(event.attributes['FRAME-RATE']); 29279 } 29280 if (event.attributes['PROGRAM-ID']) { 29281 event.attributes['PROGRAM-ID'] = parseInt(event.attributes['PROGRAM-ID'], 10); 29282 } 29283 } 29284 this.trigger('data', event); 29285 return; 29286 } 29287 match = /^#EXT-X-MEDIA:(.*)$/.exec(newLine); 29288 if (match) { 29289 event = { 29290 type: 'tag', 29291 tagType: 'media' 29292 }; 29293 if (match[1]) { 29294 event.attributes = parseAttributes$1(match[1]); 29295 } 29296 this.trigger('data', event); 29297 return; 29298 } 29299 match = /^#EXT-X-ENDLIST/.exec(newLine); 29300 if (match) { 29301 this.trigger('data', { 29302 type: 'tag', 29303 tagType: 'endlist' 29304 }); 29305 return; 29306 } 29307 match = /^#EXT-X-DISCONTINUITY/.exec(newLine); 29308 if (match) { 29309 this.trigger('data', { 29310 type: 'tag', 29311 tagType: 'discontinuity' 29312 }); 29313 return; 29314 } 29315 match = /^#EXT-X-PROGRAM-DATE-TIME:(.*)$/.exec(newLine); 29316 if (match) { 29317 event = { 29318 type: 'tag', 29319 tagType: 'program-date-time' 29320 }; 29321 if (match[1]) { 29322 event.dateTimeString = match[1]; 29323 event.dateTimeObject = new Date(match[1]); 29324 } 29325 this.trigger('data', event); 29326 return; 29327 } 29328 match = /^#EXT-X-KEY:(.*)$/.exec(newLine); 29329 if (match) { 29330 event = { 29331 type: 'tag', 29332 tagType: 'key' 29333 }; 29334 if (match[1]) { 29335 event.attributes = parseAttributes$1(match[1]); // parse the IV string into a Uint32Array 29336 29337 if (event.attributes.IV) { 29338 if (event.attributes.IV.substring(0, 2).toLowerCase() === '0x') { 29339 event.attributes.IV = event.attributes.IV.substring(2); 29340 } 29341 event.attributes.IV = event.attributes.IV.match(/.{8}/g); 29342 event.attributes.IV[0] = parseInt(event.attributes.IV[0], 16); 29343 event.attributes.IV[1] = parseInt(event.attributes.IV[1], 16); 29344 event.attributes.IV[2] = parseInt(event.attributes.IV[2], 16); 29345 event.attributes.IV[3] = parseInt(event.attributes.IV[3], 16); 29346 event.attributes.IV = new Uint32Array(event.attributes.IV); 29347 } 29348 } 29349 this.trigger('data', event); 29350 return; 29351 } 29352 match = /^#EXT-X-START:(.*)$/.exec(newLine); 29353 if (match) { 29354 event = { 29355 type: 'tag', 29356 tagType: 'start' 29357 }; 29358 if (match[1]) { 29359 event.attributes = parseAttributes$1(match[1]); 29360 event.attributes['TIME-OFFSET'] = parseFloat(event.attributes['TIME-OFFSET']); 29361 event.attributes.PRECISE = /YES/.test(event.attributes.PRECISE); 29362 } 29363 this.trigger('data', event); 29364 return; 29365 } 29366 match = /^#EXT-X-CUE-OUT-CONT:(.*)?$/.exec(newLine); 29367 if (match) { 29368 event = { 29369 type: 'tag', 29370 tagType: 'cue-out-cont' 29371 }; 29372 if (match[1]) { 29373 event.data = match[1]; 29374 } else { 29375 event.data = ''; 29376 } 29377 this.trigger('data', event); 29378 return; 29379 } 29380 match = /^#EXT-X-CUE-OUT:(.*)?$/.exec(newLine); 29381 if (match) { 29382 event = { 29383 type: 'tag', 29384 tagType: 'cue-out' 29385 }; 29386 if (match[1]) { 29387 event.data = match[1]; 29388 } else { 29389 event.data = ''; 29390 } 29391 this.trigger('data', event); 29392 return; 29393 } 29394 match = /^#EXT-X-CUE-IN:(.*)?$/.exec(newLine); 29395 if (match) { 29396 event = { 29397 type: 'tag', 29398 tagType: 'cue-in' 29399 }; 29400 if (match[1]) { 29401 event.data = match[1]; 29402 } else { 29403 event.data = ''; 29404 } 29405 this.trigger('data', event); 29406 return; 29407 } 29408 match = /^#EXT-X-SKIP:(.*)$/.exec(newLine); 29409 if (match && match[1]) { 29410 event = { 29411 type: 'tag', 29412 tagType: 'skip' 29413 }; 29414 event.attributes = parseAttributes$1(match[1]); 29415 if (event.attributes.hasOwnProperty('SKIPPED-SEGMENTS')) { 29416 event.attributes['SKIPPED-SEGMENTS'] = parseInt(event.attributes['SKIPPED-SEGMENTS'], 10); 29417 } 29418 if (event.attributes.hasOwnProperty('RECENTLY-REMOVED-DATERANGES')) { 29419 event.attributes['RECENTLY-REMOVED-DATERANGES'] = event.attributes['RECENTLY-REMOVED-DATERANGES'].split(TAB); 29420 } 29421 this.trigger('data', event); 29422 return; 29423 } 29424 match = /^#EXT-X-PART:(.*)$/.exec(newLine); 29425 if (match && match[1]) { 29426 event = { 29427 type: 'tag', 29428 tagType: 'part' 29429 }; 29430 event.attributes = parseAttributes$1(match[1]); 29431 ['DURATION'].forEach(function (key) { 29432 if (event.attributes.hasOwnProperty(key)) { 29433 event.attributes[key] = parseFloat(event.attributes[key]); 29434 } 29435 }); 29436 ['INDEPENDENT', 'GAP'].forEach(function (key) { 29437 if (event.attributes.hasOwnProperty(key)) { 29438 event.attributes[key] = /YES/.test(event.attributes[key]); 29439 } 29440 }); 29441 if (event.attributes.hasOwnProperty('BYTERANGE')) { 29442 event.attributes.byterange = parseByterange(event.attributes.BYTERANGE); 29443 } 29444 this.trigger('data', event); 29445 return; 29446 } 29447 match = /^#EXT-X-SERVER-CONTROL:(.*)$/.exec(newLine); 29448 if (match && match[1]) { 29449 event = { 29450 type: 'tag', 29451 tagType: 'server-control' 29452 }; 29453 event.attributes = parseAttributes$1(match[1]); 29454 ['CAN-SKIP-UNTIL', 'PART-HOLD-BACK', 'HOLD-BACK'].forEach(function (key) { 29455 if (event.attributes.hasOwnProperty(key)) { 29456 event.attributes[key] = parseFloat(event.attributes[key]); 29457 } 29458 }); 29459 ['CAN-SKIP-DATERANGES', 'CAN-BLOCK-RELOAD'].forEach(function (key) { 29460 if (event.attributes.hasOwnProperty(key)) { 29461 event.attributes[key] = /YES/.test(event.attributes[key]); 29462 } 29463 }); 29464 this.trigger('data', event); 29465 return; 29466 } 29467 match = /^#EXT-X-PART-INF:(.*)$/.exec(newLine); 29468 if (match && match[1]) { 29469 event = { 29470 type: 'tag', 29471 tagType: 'part-inf' 29472 }; 29473 event.attributes = parseAttributes$1(match[1]); 29474 ['PART-TARGET'].forEach(function (key) { 29475 if (event.attributes.hasOwnProperty(key)) { 29476 event.attributes[key] = parseFloat(event.attributes[key]); 29477 } 29478 }); 29479 this.trigger('data', event); 29480 return; 29481 } 29482 match = /^#EXT-X-PRELOAD-HINT:(.*)$/.exec(newLine); 29483 if (match && match[1]) { 29484 event = { 29485 type: 'tag', 29486 tagType: 'preload-hint' 29487 }; 29488 event.attributes = parseAttributes$1(match[1]); 29489 ['BYTERANGE-START', 'BYTERANGE-LENGTH'].forEach(function (key) { 29490 if (event.attributes.hasOwnProperty(key)) { 29491 event.attributes[key] = parseInt(event.attributes[key], 10); 29492 const subkey = key === 'BYTERANGE-LENGTH' ? 'length' : 'offset'; 29493 event.attributes.byterange = event.attributes.byterange || {}; 29494 event.attributes.byterange[subkey] = event.attributes[key]; // only keep the parsed byterange object. 29495 29496 delete event.attributes[key]; 29497 } 29498 }); 29499 this.trigger('data', event); 29500 return; 29501 } 29502 match = /^#EXT-X-RENDITION-REPORT:(.*)$/.exec(newLine); 29503 if (match && match[1]) { 29504 event = { 29505 type: 'tag', 29506 tagType: 'rendition-report' 29507 }; 29508 event.attributes = parseAttributes$1(match[1]); 29509 ['LAST-MSN', 'LAST-PART'].forEach(function (key) { 29510 if (event.attributes.hasOwnProperty(key)) { 29511 event.attributes[key] = parseInt(event.attributes[key], 10); 29512 } 29513 }); 29514 this.trigger('data', event); 29515 return; 29516 } 29517 match = /^#EXT-X-DATERANGE:(.*)$/.exec(newLine); 29518 if (match && match[1]) { 29519 event = { 29520 type: 'tag', 29521 tagType: 'daterange' 29522 }; 29523 event.attributes = parseAttributes$1(match[1]); 29524 ['ID', 'CLASS'].forEach(function (key) { 29525 if (event.attributes.hasOwnProperty(key)) { 29526 event.attributes[key] = String(event.attributes[key]); 29527 } 29528 }); 29529 ['START-DATE', 'END-DATE'].forEach(function (key) { 29530 if (event.attributes.hasOwnProperty(key)) { 29531 event.attributes[key] = new Date(event.attributes[key]); 29532 } 29533 }); 29534 ['DURATION', 'PLANNED-DURATION'].forEach(function (key) { 29535 if (event.attributes.hasOwnProperty(key)) { 29536 event.attributes[key] = parseFloat(event.attributes[key]); 29537 } 29538 }); 29539 ['END-ON-NEXT'].forEach(function (key) { 29540 if (event.attributes.hasOwnProperty(key)) { 29541 event.attributes[key] = /YES/i.test(event.attributes[key]); 29542 } 29543 }); 29544 ['SCTE35-CMD', ' SCTE35-OUT', 'SCTE35-IN'].forEach(function (key) { 29545 if (event.attributes.hasOwnProperty(key)) { 29546 event.attributes[key] = event.attributes[key].toString(16); 29547 } 29548 }); 29549 const clientAttributePattern = /^X-([A-Z]+-)+[A-Z]+$/; 29550 for (const key in event.attributes) { 29551 if (!clientAttributePattern.test(key)) { 29552 continue; 29553 } 29554 const isHexaDecimal = /[0-9A-Fa-f]{6}/g.test(event.attributes[key]); 29555 const isDecimalFloating = /^\d+(\.\d+)?$/.test(event.attributes[key]); 29556 event.attributes[key] = isHexaDecimal ? event.attributes[key].toString(16) : isDecimalFloating ? parseFloat(event.attributes[key]) : String(event.attributes[key]); 29557 } 29558 this.trigger('data', event); 29559 return; 29560 } 29561 match = /^#EXT-X-INDEPENDENT-SEGMENTS/.exec(newLine); 29562 if (match) { 29563 this.trigger('data', { 29564 type: 'tag', 29565 tagType: 'independent-segments' 29566 }); 29567 return; 29568 } 29569 match = /^#EXT-X-CONTENT-STEERING:(.*)$/.exec(newLine); 29570 if (match) { 29571 event = { 29572 type: 'tag', 29573 tagType: 'content-steering' 29574 }; 29575 event.attributes = parseAttributes$1(match[1]); 29576 this.trigger('data', event); 29577 return; 29578 } // unknown tag type 29579 29580 this.trigger('data', { 29581 type: 'tag', 29582 data: newLine.slice(4) 29583 }); 29584 }); 29585 } 29586 /** 29587 * Add a parser for custom headers 29588 * 29589 * @param {Object} options a map of options for the added parser 29590 * @param {RegExp} options.expression a regular expression to match the custom header 29591 * @param {string} options.customType the custom type to register to the output 29592 * @param {Function} [options.dataParser] function to parse the line into an object 29593 * @param {boolean} [options.segment] should tag data be attached to the segment object 29594 */ 29595 29596 addParser({ 29597 expression, 29598 customType, 29599 dataParser, 29600 segment 29601 }) { 29602 if (typeof dataParser !== 'function') { 29603 dataParser = line => line; 29604 } 29605 this.customParsers.push(line => { 29606 const match = expression.exec(line); 29607 if (match) { 29608 this.trigger('data', { 29609 type: 'custom', 29610 data: dataParser(line), 29611 customType, 29612 segment 29613 }); 29614 return true; 29615 } 29616 }); 29617 } 29618 /** 29619 * Add a custom header mapper 29620 * 29621 * @param {Object} options 29622 * @param {RegExp} options.expression a regular expression to match the custom header 29623 * @param {Function} options.map function to translate tag into a different tag 29624 */ 29625 29626 addTagMapper({ 29627 expression, 29628 map 29629 }) { 29630 const mapFn = line => { 29631 if (expression.test(line)) { 29632 return map(line); 29633 } 29634 return line; 29635 }; 29636 this.tagMappers.push(mapFn); 29637 } 29638 } 29639 const camelCase = str => str.toLowerCase().replace(/-(\w)/g, a => a[1].toUpperCase()); 29640 const camelCaseKeys = function (attributes) { 29641 const result = {}; 29642 Object.keys(attributes).forEach(function (key) { 29643 result[camelCase(key)] = attributes[key]; 29644 }); 29645 return result; 29646 }; // set SERVER-CONTROL hold back based upon targetDuration and partTargetDuration 29647 // we need this helper because defaults are based upon targetDuration and 29648 // partTargetDuration being set, but they may not be if SERVER-CONTROL appears before 29649 // target durations are set. 29650 29651 const setHoldBack = function (manifest) { 29652 const { 29653 serverControl, 29654 targetDuration, 29655 partTargetDuration 29656 } = manifest; 29657 if (!serverControl) { 29658 return; 29659 } 29660 const tag = '#EXT-X-SERVER-CONTROL'; 29661 const hb = 'holdBack'; 29662 const phb = 'partHoldBack'; 29663 const minTargetDuration = targetDuration && targetDuration * 3; 29664 const minPartDuration = partTargetDuration && partTargetDuration * 2; 29665 if (targetDuration && !serverControl.hasOwnProperty(hb)) { 29666 serverControl[hb] = minTargetDuration; 29667 this.trigger('info', { 29668 message: `${tag} defaulting HOLD-BACK to targetDuration * 3 (${minTargetDuration}).` 29669 }); 29670 } 29671 if (minTargetDuration && serverControl[hb] < minTargetDuration) { 29672 this.trigger('warn', { 29673 message: `${tag} clamping HOLD-BACK (${serverControl[hb]}) to targetDuration * 3 (${minTargetDuration})` 29674 }); 29675 serverControl[hb] = minTargetDuration; 29676 } // default no part hold back to part target duration * 3 29677 29678 if (partTargetDuration && !serverControl.hasOwnProperty(phb)) { 29679 serverControl[phb] = partTargetDuration * 3; 29680 this.trigger('info', { 29681 message: `${tag} defaulting PART-HOLD-BACK to partTargetDuration * 3 (${serverControl[phb]}).` 29682 }); 29683 } // if part hold back is too small default it to part target duration * 2 29684 29685 if (partTargetDuration && serverControl[phb] < minPartDuration) { 29686 this.trigger('warn', { 29687 message: `${tag} clamping PART-HOLD-BACK (${serverControl[phb]}) to partTargetDuration * 2 (${minPartDuration}).` 29688 }); 29689 serverControl[phb] = minPartDuration; 29690 } 29691 }; 29692 /** 29693 * A parser for M3U8 files. The current interpretation of the input is 29694 * exposed as a property `manifest` on parser objects. It's just two lines to 29695 * create and parse a manifest once you have the contents available as a string: 29696 * 29697 * ```js 29698 * var parser = new m3u8.Parser(); 29699 * parser.push(xhr.responseText); 29700 * ``` 29701 * 29702 * New input can later be applied to update the manifest object by calling 29703 * `push` again. 29704 * 29705 * The parser attempts to create a usable manifest object even if the 29706 * underlying input is somewhat nonsensical. It emits `info` and `warning` 29707 * events during the parse if it encounters input that seems invalid or 29708 * requires some property of the manifest object to be defaulted. 29709 * 29710 * @class Parser 29711 * @extends Stream 29712 */ 29713 29714 class Parser extends Stream { 29715 constructor() { 29716 super(); 29717 this.lineStream = new LineStream(); 29718 this.parseStream = new ParseStream(); 29719 this.lineStream.pipe(this.parseStream); 29720 this.lastProgramDateTime = null; 29721 /* eslint-disable consistent-this */ 29722 29723 const self = this; 29724 /* eslint-enable consistent-this */ 29725 29726 const uris = []; 29727 let currentUri = {}; // if specified, the active EXT-X-MAP definition 29728 29729 let currentMap; // if specified, the active decryption key 29730 29731 let key; 29732 let hasParts = false; 29733 const noop = function () {}; 29734 const defaultMediaGroups = { 29735 'AUDIO': {}, 29736 'VIDEO': {}, 29737 'CLOSED-CAPTIONS': {}, 29738 'SUBTITLES': {} 29739 }; // This is the Widevine UUID from DASH IF IOP. The same exact string is 29740 // used in MPDs with Widevine encrypted streams. 29741 29742 const widevineUuid = 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed'; // group segments into numbered timelines delineated by discontinuities 29743 29744 let currentTimeline = 0; // the manifest is empty until the parse stream begins delivering data 29745 29746 this.manifest = { 29747 allowCache: true, 29748 discontinuityStarts: [], 29749 dateRanges: [], 29750 segments: [] 29751 }; // keep track of the last seen segment's byte range end, as segments are not required 29752 // to provide the offset, in which case it defaults to the next byte after the 29753 // previous segment 29754 29755 let lastByterangeEnd = 0; // keep track of the last seen part's byte range end. 29756 29757 let lastPartByterangeEnd = 0; 29758 const dateRangeTags = {}; 29759 this.on('end', () => { 29760 // only add preloadSegment if we don't yet have a uri for it. 29761 // and we actually have parts/preloadHints 29762 if (currentUri.uri || !currentUri.parts && !currentUri.preloadHints) { 29763 return; 29764 } 29765 if (!currentUri.map && currentMap) { 29766 currentUri.map = currentMap; 29767 } 29768 if (!currentUri.key && key) { 29769 currentUri.key = key; 29770 } 29771 if (!currentUri.timeline && typeof currentTimeline === 'number') { 29772 currentUri.timeline = currentTimeline; 29773 } 29774 this.manifest.preloadSegment = currentUri; 29775 }); // update the manifest with the m3u8 entry from the parse stream 29776 29777 this.parseStream.on('data', function (entry) { 29778 let mediaGroup; 29779 let rendition; 29780 ({ 29781 tag() { 29782 // switch based on the tag type 29783 (({ 29784 version() { 29785 if (entry.version) { 29786 this.manifest.version = entry.version; 29787 } 29788 }, 29789 'allow-cache'() { 29790 this.manifest.allowCache = entry.allowed; 29791 if (!('allowed' in entry)) { 29792 this.trigger('info', { 29793 message: 'defaulting allowCache to YES' 29794 }); 29795 this.manifest.allowCache = true; 29796 } 29797 }, 29798 byterange() { 29799 const byterange = {}; 29800 if ('length' in entry) { 29801 currentUri.byterange = byterange; 29802 byterange.length = entry.length; 29803 if (!('offset' in entry)) { 29804 /* 29805 * From the latest spec (as of this writing): 29806 * https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.2 29807 * 29808 * Same text since EXT-X-BYTERANGE's introduction in draft 7: 29809 * https://tools.ietf.org/html/draft-pantos-http-live-streaming-07#section-3.3.1) 29810 * 29811 * "If o [offset] is not present, the sub-range begins at the next byte 29812 * following the sub-range of the previous media segment." 29813 */ 29814 entry.offset = lastByterangeEnd; 29815 } 29816 } 29817 if ('offset' in entry) { 29818 currentUri.byterange = byterange; 29819 byterange.offset = entry.offset; 29820 } 29821 lastByterangeEnd = byterange.offset + byterange.length; 29822 }, 29823 endlist() { 29824 this.manifest.endList = true; 29825 }, 29826 inf() { 29827 if (!('mediaSequence' in this.manifest)) { 29828 this.manifest.mediaSequence = 0; 29829 this.trigger('info', { 29830 message: 'defaulting media sequence to zero' 29831 }); 29832 } 29833 if (!('discontinuitySequence' in this.manifest)) { 29834 this.manifest.discontinuitySequence = 0; 29835 this.trigger('info', { 29836 message: 'defaulting discontinuity sequence to zero' 29837 }); 29838 } 29839 if (entry.title) { 29840 currentUri.title = entry.title; 29841 } 29842 if (entry.duration > 0) { 29843 currentUri.duration = entry.duration; 29844 } 29845 if (entry.duration === 0) { 29846 currentUri.duration = 0.01; 29847 this.trigger('info', { 29848 message: 'updating zero segment duration to a small value' 29849 }); 29850 } 29851 this.manifest.segments = uris; 29852 }, 29853 key() { 29854 if (!entry.attributes) { 29855 this.trigger('warn', { 29856 message: 'ignoring key declaration without attribute list' 29857 }); 29858 return; 29859 } // clear the active encryption key 29860 29861 if (entry.attributes.METHOD === 'NONE') { 29862 key = null; 29863 return; 29864 } 29865 if (!entry.attributes.URI) { 29866 this.trigger('warn', { 29867 message: 'ignoring key declaration without URI' 29868 }); 29869 return; 29870 } 29871 if (entry.attributes.KEYFORMAT === 'com.apple.streamingkeydelivery') { 29872 this.manifest.contentProtection = this.manifest.contentProtection || {}; // TODO: add full support for this. 29873 29874 this.manifest.contentProtection['com.apple.fps.1_0'] = { 29875 attributes: entry.attributes 29876 }; 29877 return; 29878 } 29879 if (entry.attributes.KEYFORMAT === 'com.microsoft.playready') { 29880 this.manifest.contentProtection = this.manifest.contentProtection || {}; // TODO: add full support for this. 29881 29882 this.manifest.contentProtection['com.microsoft.playready'] = { 29883 uri: entry.attributes.URI 29884 }; 29885 return; 29886 } // check if the content is encrypted for Widevine 29887 // Widevine/HLS spec: https://storage.googleapis.com/wvdocs/Widevine_DRM_HLS.pdf 29888 29889 if (entry.attributes.KEYFORMAT === widevineUuid) { 29890 const VALID_METHODS = ['SAMPLE-AES', 'SAMPLE-AES-CTR', 'SAMPLE-AES-CENC']; 29891 if (VALID_METHODS.indexOf(entry.attributes.METHOD) === -1) { 29892 this.trigger('warn', { 29893 message: 'invalid key method provided for Widevine' 29894 }); 29895 return; 29896 } 29897 if (entry.attributes.METHOD === 'SAMPLE-AES-CENC') { 29898 this.trigger('warn', { 29899 message: 'SAMPLE-AES-CENC is deprecated, please use SAMPLE-AES-CTR instead' 29900 }); 29901 } 29902 if (entry.attributes.URI.substring(0, 23) !== 'data:text/plain;base64,') { 29903 this.trigger('warn', { 29904 message: 'invalid key URI provided for Widevine' 29905 }); 29906 return; 29907 } 29908 if (!(entry.attributes.KEYID && entry.attributes.KEYID.substring(0, 2) === '0x')) { 29909 this.trigger('warn', { 29910 message: 'invalid key ID provided for Widevine' 29911 }); 29912 return; 29913 } // if Widevine key attributes are valid, store them as `contentProtection` 29914 // on the manifest to emulate Widevine tag structure in a DASH mpd 29915 29916 this.manifest.contentProtection = this.manifest.contentProtection || {}; 29917 this.manifest.contentProtection['com.widevine.alpha'] = { 29918 attributes: { 29919 schemeIdUri: entry.attributes.KEYFORMAT, 29920 // remove '0x' from the key id string 29921 keyId: entry.attributes.KEYID.substring(2) 29922 }, 29923 // decode the base64-encoded PSSH box 29924 pssh: decodeB64ToUint8Array$1
vendor: 16,332 bytes, lines 29924-30254
29924(entry.attributes.URI.split(',')[1]) 29925 }; 29926 return; 29927 } 29928 if (!entry.attributes.METHOD) { 29929 this.trigger('warn', { 29930 message: 'defaulting key method to AES-128' 29931 }); 29932 } // setup an encryption key for upcoming segments 29933 29934 key = { 29935 method: entry.attributes.METHOD || 'AES-128', 29936 uri: entry.attributes.URI 29937 }; 29938 if (typeof entry.attributes.IV !== 'undefined') { 29939 key.iv = entry.attributes.IV; 29940 } 29941 }, 29942 'media-sequence'() { 29943 if (!isFinite(entry.number)) { 29944 this.trigger('warn', { 29945 message: 'ignoring invalid media sequence: ' + entry.number 29946 }); 29947 return; 29948 } 29949 this.manifest.mediaSequence = entry.number; 29950 }, 29951 'discontinuity-sequence'() { 29952 if (!isFinite(entry.number)) { 29953 this.trigger('warn', { 29954 message: 'ignoring invalid discontinuity sequence: ' + entry.number 29955 }); 29956 return; 29957 } 29958 this.manifest.discontinuitySequence = entry.number; 29959 currentTimeline = entry.number; 29960 }, 29961 'playlist-type'() { 29962 if (!/VOD|EVENT/.test(entry.playlistType)) { 29963 this.trigger('warn', { 29964 message: 'ignoring unknown playlist type: ' + entry.playlist 29965 }); 29966 return; 29967 } 29968 this.manifest.playlistType = entry.playlistType; 29969 }, 29970 map() { 29971 currentMap = {}; 29972 if (entry.uri) { 29973 currentMap.uri = entry.uri; 29974 } 29975 if (entry.byterange) { 29976 currentMap.byterange = entry.byterange; 29977 } 29978 if (key) { 29979 currentMap.key = key; 29980 } 29981 }, 29982 'stream-inf'() { 29983 this.manifest.playlists = uris; 29984 this.manifest.mediaGroups = this.manifest.mediaGroups || defaultMediaGroups; 29985 if (!entry.attributes) { 29986 this.trigger('warn', { 29987 message: 'ignoring empty stream-inf attributes' 29988 }); 29989 return; 29990 } 29991 if (!currentUri.attributes) { 29992 currentUri.attributes = {}; 29993 } 29994 _extends$1(currentUri.attributes, entry.attributes); 29995 }, 29996 media() { 29997 this.manifest.mediaGroups = this.manifest.mediaGroups || defaultMediaGroups; 29998 if (!(entry.attributes && entry.attributes.TYPE && entry.attributes['GROUP-ID'] && entry.attributes.NAME)) { 29999 this.trigger('warn', { 30000 message: 'ignoring incomplete or missing media group' 30001 }); 30002 return; 30003 } // find the media group, creating defaults as necessary 30004 30005 const mediaGroupType = this.manifest.mediaGroups[entry.attributes.TYPE]; 30006 mediaGroupType[entry.attributes['GROUP-ID']] = mediaGroupType[entry.attributes['GROUP-ID']] || {}; 30007 mediaGroup = mediaGroupType[entry.attributes['GROUP-ID']]; // collect the rendition metadata 30008 30009 rendition = { 30010 default: /yes/i.test(entry.attributes.DEFAULT) 30011 }; 30012 if (rendition.default) { 30013 rendition.autoselect = true; 30014 } else { 30015 rendition.autoselect = /yes/i.test(entry.attributes.AUTOSELECT); 30016 } 30017 if (entry.attributes.LANGUAGE) { 30018 rendition.language = entry.attributes.LANGUAGE; 30019 } 30020 if (entry.attributes.URI) { 30021 rendition.uri = entry.attributes.URI; 30022 } 30023 if (entry.attributes['INSTREAM-ID']) { 30024 rendition.instreamId = entry.attributes['INSTREAM-ID']; 30025 } 30026 if (entry.attributes.CHARACTERISTICS) { 30027 rendition.characteristics = entry.attributes.CHARACTERISTICS; 30028 } 30029 if (entry.attributes.FORCED) { 30030 rendition.forced = /yes/i.test(entry.attributes.FORCED); 30031 } // insert the new rendition 30032 30033 mediaGroup[entry.attributes.NAME] = rendition; 30034 }, 30035 discontinuity() { 30036 currentTimeline += 1; 30037 currentUri.discontinuity = true; 30038 this.manifest.discontinuityStarts.push(uris.length); 30039 }, 30040 'program-date-time'() { 30041 if (typeof this.manifest.dateTimeString === 'undefined') { 30042 // PROGRAM-DATE-TIME is a media-segment tag, but for backwards 30043 // compatibility, we add the first occurence of the PROGRAM-DATE-TIME tag 30044 // to the manifest object 30045 // TODO: Consider removing this in future major version 30046 this.manifest.dateTimeString = entry.dateTimeString; 30047 this.manifest.dateTimeObject = entry.dateTimeObject; 30048 } 30049 currentUri.dateTimeString = entry.dateTimeString; 30050 currentUri.dateTimeObject = entry.dateTimeObject; 30051 const { 30052 lastProgramDateTime 30053 } = this; 30054 this.lastProgramDateTime = new Date(entry.dateTimeString).getTime(); // We should extrapolate Program Date Time backward only during first program date time occurrence. 30055 // Once we have at least one program date time point, we can always extrapolate it forward using lastProgramDateTime reference. 30056 30057 if (lastProgramDateTime === null) { 30058 // Extrapolate Program Date Time backward 30059 // Since it is first program date time occurrence we're assuming that 30060 // all this.manifest.segments have no program date time info 30061 this.manifest.segments.reduceRight((programDateTime, segment) => { 30062 segment.programDateTime = programDateTime - segment.duration * 1000; 30063 return segment.programDateTime; 30064 }, this.lastProgramDateTime); 30065 } 30066 }, 30067 targetduration() { 30068 if (!isFinite(entry.duration) || entry.duration < 0) { 30069 this.trigger('warn', { 30070 message: 'ignoring invalid target duration: ' + entry.duration 30071 }); 30072 return; 30073 } 30074 this.manifest.targetDuration = entry.duration; 30075 setHoldBack.call(this, this.manifest); 30076 }, 30077 start() { 30078 if (!entry.attributes || isNaN(entry.attributes['TIME-OFFSET'])) { 30079 this.trigger('warn', { 30080 message: 'ignoring start declaration without appropriate attribute list' 30081 }); 30082 return; 30083 } 30084 this.manifest.start = { 30085 timeOffset: entry.attributes['TIME-OFFSET'], 30086 precise: entry.attributes.PRECISE 30087 }; 30088 }, 30089 'cue-out'() { 30090 currentUri.cueOut = entry.data; 30091 }, 30092 'cue-out-cont'() { 30093 currentUri.cueOutCont = entry.data; 30094 }, 30095 'cue-in'() { 30096 currentUri.cueIn = entry.data; 30097 }, 30098 'skip'() { 30099 this.manifest.skip = camelCaseKeys(entry.attributes); 30100 this.warnOnMissingAttributes_('#EXT-X-SKIP', entry.attributes, ['SKIPPED-SEGMENTS']); 30101 }, 30102 'part'() { 30103 hasParts = true; // parts are always specifed before a segment 30104 30105 const segmentIndex = this.manifest.segments.length; 30106 const part = camelCaseKeys(entry.attributes); 30107 currentUri.parts = currentUri.parts || []; 30108 currentUri.parts.push(part); 30109 if (part.byterange) { 30110 if (!part.byterange.hasOwnProperty('offset')) { 30111 part.byterange.offset = lastPartByterangeEnd; 30112 } 30113 lastPartByterangeEnd = part.byterange.offset + part.byterange.length; 30114 } 30115 const partIndex = currentUri.parts.length - 1; 30116 this.warnOnMissingAttributes_(`#EXT-X-PART #${partIndex} for segment #${segmentIndex}`, entry.attributes, ['URI', 'DURATION']); 30117 if (this.manifest.renditionReports) { 30118 this.manifest.renditionReports.forEach((r, i) => { 30119 if (!r.hasOwnProperty('lastPart')) { 30120 this.trigger('warn', { 30121 message: `#EXT-X-RENDITION-REPORT #${i} lacks required attribute(s): LAST-PART` 30122 }); 30123 } 30124 }); 30125 } 30126 }, 30127 'server-control'() { 30128 const attrs = this.manifest.serverControl = camelCaseKeys(entry.attributes); 30129 if (!attrs.hasOwnProperty('canBlockReload')) { 30130 attrs.canBlockReload = false; 30131 this.trigger('info', { 30132 message: '#EXT-X-SERVER-CONTROL defaulting CAN-BLOCK-RELOAD to false' 30133 }); 30134 } 30135 setHoldBack.call(this, this.manifest); 30136 if (attrs.canSkipDateranges && !attrs.hasOwnProperty('canSkipUntil')) { 30137 this.trigger('warn', { 30138 message: '#EXT-X-SERVER-CONTROL lacks required attribute CAN-SKIP-UNTIL which is required when CAN-SKIP-DATERANGES is set' 30139 }); 30140 } 30141 }, 30142 'preload-hint'() { 30143 // parts are always specifed before a segment 30144 const segmentIndex = this.manifest.segments.length; 30145 const hint = camelCaseKeys(entry.attributes); 30146 const isPart = hint.type && hint.type === 'PART'; 30147 currentUri.preloadHints = currentUri.preloadHints || []; 30148 currentUri.preloadHints.push(hint); 30149 if (hint.byterange) { 30150 if (!hint.byterange.hasOwnProperty('offset')) { 30151 // use last part byterange end or zero if not a part. 30152 hint.byterange.offset = isPart ? lastPartByterangeEnd : 0; 30153 if (isPart) { 30154 lastPartByterangeEnd = hint.byterange.offset + hint.byterange.length; 30155 } 30156 } 30157 } 30158 const index = currentUri.preloadHints.length - 1; 30159 this.warnOnMissingAttributes_(`#EXT-X-PRELOAD-HINT #${index} for segment #${segmentIndex}`, entry.attributes, ['TYPE', 'URI']); 30160 if (!hint.type) { 30161 return; 30162 } // search through all preload hints except for the current one for 30163 // a duplicate type. 30164 30165 for (let i = 0; i < currentUri.preloadHints.length - 1; i++) { 30166 const otherHint = currentUri.preloadHints[i]; 30167 if (!otherHint.type) { 30168 continue; 30169 } 30170 if (otherHint.type === hint.type) { 30171 this.trigger('warn', { 30172 message: `#EXT-X-PRELOAD-HINT #${index} for segment #${segmentIndex} has the same TYPE ${hint.type} as preload hint #${i}` 30173 }); 30174 } 30175 } 30176 }, 30177 'rendition-report'() { 30178 const report = camelCaseKeys(entry.attributes); 30179 this.manifest.renditionReports = this.manifest.renditionReports || []; 30180 this.manifest.renditionReports.push(report); 30181 const index = this.manifest.renditionReports.length - 1; 30182 const required = ['LAST-MSN', 'URI']; 30183 if (hasParts) { 30184 required.push('LAST-PART'); 30185 } 30186 this.warnOnMissingAttributes_(`#EXT-X-RENDITION-REPORT #${index}`, entry.attributes, required); 30187 }, 30188 'part-inf'() { 30189 this.manifest.partInf = camelCaseKeys(entry.attributes); 30190 this.warnOnMissingAttributes_('#EXT-X-PART-INF', entry.attributes, ['PART-TARGET']); 30191 if (this.manifest.partInf.partTarget) { 30192 this.manifest.partTargetDuration = this.manifest.partInf.partTarget; 30193 } 30194 setHoldBack.call(this, this.manifest); 30195 }, 30196 'daterange'() { 30197 this.manifest.dateRanges.push(camelCaseKeys(entry.attributes)); 30198 const index = this.manifest.dateRanges.length - 1; 30199 this.warnOnMissingAttributes_(`#EXT-X-DATERANGE #${index}`, entry.attributes, ['ID', 'START-DATE']); 30200 const dateRange = this.manifest.dateRanges[index]; 30201 if (dateRange.endDate && dateRange.startDate && new Date(dateRange.endDate) < new Date(dateRange.startDate)) { 30202 this.trigger('warn', { 30203 message: 'EXT-X-DATERANGE END-DATE must be equal to or later than the value of the START-DATE' 30204 }); 30205 } 30206 if (dateRange.duration && dateRange.duration < 0) { 30207 this.trigger('warn', { 30208 message: 'EXT-X-DATERANGE DURATION must not be negative' 30209 }); 30210 } 30211 if (dateRange.plannedDuration && dateRange.plannedDuration < 0) { 30212 this.trigger('warn', { 30213 message: 'EXT-X-DATERANGE PLANNED-DURATION must not be negative' 30214 }); 30215 } 30216 const endOnNextYes = !!dateRange.endOnNext; 30217 if (endOnNextYes && !dateRange.class) { 30218 this.trigger('warn', { 30219 message: 'EXT-X-DATERANGE with an END-ON-NEXT=YES attribute must have a CLASS attribute' 30220 }); 30221 } 30222 if (endOnNextYes && (dateRange.duration || dateRange.endDate)) { 30223 this.trigger('warn', { 30224 message: 'EXT-X-DATERANGE with an END-ON-NEXT=YES attribute must not contain DURATION or END-DATE attributes' 30225 }); 30226 } 30227 if (dateRange.duration && dateRange.endDate) { 30228 const startDate = dateRange.startDate; 30229 const newDateInSeconds = startDate.getTime() + dateRange.duration * 1000; 30230 this.manifest.dateRanges[index].endDate = new Date(newDateInSeconds); 30231 } 30232 if (!dateRangeTags[dateRange.id]) { 30233 dateRangeTags[dateRange.id] = dateRange; 30234 } else { 30235 for (const attribute in dateRangeTags[dateRange.id]) { 30236 if (!!dateRange[attribute] && JSON.stringify(dateRangeTags[dateRange.id][attribute]) !== JSON.stringify(dateRange[attribute])) { 30237 this.trigger('warn', { 30238 message: 'EXT-X-DATERANGE tags with the same ID in a playlist must have the same attributes values' 30239 }); 30240 break; 30241 } 30242 } // if tags with the same ID do not have conflicting attributes, merge them 30243 30244 const dateRangeWithSameId = this.manifest.dateRanges.findIndex(dateRangeToFind => dateRangeToFind.id === dateRange.id); 30245 this.manifest.dateRanges[dateRangeWithSameId] = _extends$1(this.manifest.dateRanges[dateRangeWithSameId], dateRange); 30246 dateRangeTags[dateRange.id] = _extends$1(dateRangeTags[dateRange.id], dateRange); // after merging, delete the duplicate dateRange that was added last 30247 30248 this.manifest.dateRanges.pop(); 30249 } 30250 }, 30251 'independent-segments'() { 30252 this.manifest.independentSegments = true; 30253 }, 30254 '
vendor: 6,772 bytes, lines 30254-30438
30254content-steering'() { 30255 this.manifest.contentSteering = camelCaseKeys(entry.attributes); 30256 this.warnOnMissingAttributes_('#EXT-X-CONTENT-STEERING', entry.attributes, ['SERVER-URI']); 30257 } 30258 })[entry.tagType] || noop).call(self); 30259 }, 30260 uri() { 30261 currentUri.uri = entry.uri; 30262 uris.push(currentUri); // if no explicit duration was declared, use the target duration 30263 30264 if (this.manifest.targetDuration && !('duration' in currentUri)) { 30265 this.trigger('warn', { 30266 message: 'defaulting segment duration to the target duration' 30267 }); 30268 currentUri.duration = this.manifest.targetDuration; 30269 } // annotate with encryption information, if necessary 30270 30271 if (key) { 30272 currentUri.key = key; 30273 } 30274 currentUri.timeline = currentTimeline; // annotate with initialization segment information, if necessary 30275 30276 if (currentMap) { 30277 currentUri.map = currentMap; 30278 } // reset the last byterange end as it needs to be 0 between parts 30279 30280 lastPartByterangeEnd = 0; // Once we have at least one program date time we can always extrapolate it forward 30281 30282 if (this.lastProgramDateTime !== null) { 30283 currentUri.programDateTime = this.lastProgramDateTime; 30284 this.lastProgramDateTime += currentUri.duration * 1000; 30285 } // prepare for the next URI 30286 30287 currentUri = {}; 30288 }, 30289 comment() {// comments are not important for playback 30290 }, 30291 custom() { 30292 // if this is segment-level data attach the output to the segment 30293 if (entry.segment) { 30294 currentUri.custom = currentUri.custom || {}; 30295 currentUri.custom[entry.customType] = entry.data; // if this is manifest-level data attach to the top level manifest object 30296 } else { 30297 this.manifest.custom = this.manifest.custom || {}; 30298 this.manifest.custom[entry.customType] = entry.data; 30299 } 30300 } 30301 })[entry.type].call(self); 30302 }); 30303 } 30304 warnOnMissingAttributes_(identifier, attributes, required) { 30305 const missing = []; 30306 required.forEach(function (key) { 30307 if (!attributes.hasOwnProperty(key)) { 30308 missing.push(key); 30309 } 30310 }); 30311 if (missing.length) { 30312 this.trigger('warn', { 30313 message: `${identifier} lacks required attribute(s): ${missing.join(', ')}` 30314 }); 30315 } 30316 } 30317 /** 30318 * Parse the input string and update the manifest object. 30319 * 30320 * @param {string} chunk a potentially incomplete portion of the manifest 30321 */ 30322 30323 push(chunk) { 30324 this.lineStream.push(chunk); 30325 } 30326 /** 30327 * Flush any remaining input. This can be handy if the last line of an M3U8 30328 * manifest did not contain a trailing newline but the file has been 30329 * completely received. 30330 */ 30331 30332 end() { 30333 // flush any buffered input 30334 this.lineStream.push('\n'); 30335 if (this.manifest.dateRanges.length && this.lastProgramDateTime === null) { 30336 this.trigger('warn', { 30337 message: 'A playlist with EXT-X-DATERANGE tag must contain atleast one EXT-X-PROGRAM-DATE-TIME tag' 30338 }); 30339 } 30340 this.lastProgramDateTime = null; 30341 this.trigger('end'); 30342 } 30343 /** 30344 * Add an additional parser for non-standard tags 30345 * 30346 * @param {Object} options a map of options for the added parser 30347 * @param {RegExp} options.expression a regular expression to match the custom header 30348 * @param {string} options.customType the custom type to register to the output 30349 * @param {Function} [options.dataParser] function to parse the line into an object 30350 * @param {boolean} [options.segment] should tag data be attached to the segment object 30351 */ 30352 30353 addParser(options) { 30354 this.parseStream.addParser(options); 30355 } 30356 /** 30357 * Add a custom header mapper 30358 * 30359 * @param {Object} options 30360 * @param {RegExp} options.expression a regular expression to match the custom header 30361 * @param {Function} options.map function to translate tag into a different tag 30362 */ 30363 30364 addTagMapper(options) { 30365 this.parseStream.addTagMapper(options); 30366 } 30367 } 30368 30369 var regexs = { 30370 // to determine mime types 30371 mp4: /^(av0?1|avc0?[1234]|vp0?9|flac|opus|mp3|mp4a|mp4v|stpp.ttml.im1t)/, 30372 webm: /^(vp0?[89]|av0?1|opus|vorbis)/, 30373 ogg: /^(vp0?[89]|theora|flac|opus|vorbis)/, 30374 // to determine if a codec is audio or video 30375 video: /^(av0?1|avc0?[1234]|vp0?[89]|hvc1|hev1|theora|mp4v)/, 30376 audio: /^(mp4a|flac|vorbis|opus|ac-[34]|ec-3|alac|mp3|speex|aac)/, 30377 text: /^(stpp.ttml.im1t)/, 30378 // mux.js support regex 30379 muxerVideo: /^(avc0?1)/, 30380 muxerAudio: /^(mp4a)/, 30381 // match nothing as muxer does not support text right now. 30382 // there cannot never be a character before the start of a string 30383 // so this matches nothing. 30384 muxerText: /a^/ 30385 }; 30386 var mediaTypes = ['video', 'audio', 'text']; 30387 var upperMediaTypes = ['Video', 'Audio', 'Text']; 30388 /** 30389 * Replace the old apple-style `avc1.<dd>.<dd>` codec string with the standard 30390 * `avc1.<hhhhhh>` 30391 * 30392 * @param {string} codec 30393 * Codec string to translate 30394 * @return {string} 30395 * The translated codec string 30396 */ 30397 30398 var translateLegacyCodec = function translateLegacyCodec(codec) { 30399 if (!codec) { 30400 return codec; 30401 } 30402 return codec.replace(/avc1\.(\d+)\.(\d+)/i, function (orig, profile, avcLevel) { 30403 var profileHex = ('00' + Number(profile).toString(16)).slice(-2); 30404 var avcLevelHex = ('00' + Number(avcLevel).toString(16)).slice(-2); 30405 return 'avc1.' + profileHex + '00' + avcLevelHex; 30406 }); 30407 }; 30408 /** 30409 * @typedef {Object} ParsedCodecInfo 30410 * @property {number} codecCount 30411 * Number of codecs parsed 30412 * @property {string} [videoCodec] 30413 * Parsed video codec (if found) 30414 * @property {string} [videoObjectTypeIndicator] 30415 * Video object type indicator (if found) 30416 * @property {string|null} audioProfile 30417 * Audio profile 30418 */ 30419 30420 /** 30421 * Parses a codec string to retrieve the number of codecs specified, the video codec and 30422 * object type indicator, and the audio profile. 30423 * 30424 * @param {string} [codecString] 30425 * The codec string to parse 30426 * @return {ParsedCodecInfo} 30427 * Parsed codec info 30428 */ 30429 30430 var parseCodecs = function parseCodecs(codecString) { 30431 if (codecString === void 0) { 30432 codecString = ''; 30433 } 30434 var codecs = codecString.split(','); 30435 var result = []; 30436 codecs.forEach(function (codec) { 30437 codec = codec.trim(); 30438 var codecType;
vendor: 3,347 bytes, lines 30439-30545
30439 mediaTypes.forEach(function (name) { 30440 var match = regexs[name].exec(codec.toLowerCase()); 30441 if (!match || match.length <= 1) { 30442 return; 30443 } 30444 codecType = name; // maintain codec case 30445 30446 var type = codec.substring(0, match[1].length); 30447 var details = codec.replace(type, ''); 30448 result.push({ 30449 type: type, 30450 details: details, 30451 mediaType: name 30452 }); 30453 }); 30454 if (!codecType) { 30455 result.push({ 30456 type: codec, 30457 details: '', 30458 mediaType: 'unknown' 30459 }); 30460 } 30461 }); 30462 return result; 30463 }; 30464 /** 30465 * Returns a ParsedCodecInfo object for the default alternate audio playlist if there is 30466 * a default alternate audio playlist for the provided audio group. 30467 * 30468 * @param {Object} master 30469 * The master playlist 30470 * @param {string} audioGroupId 30471 * ID of the audio group for which to find the default codec info 30472 * @return {ParsedCodecInfo} 30473 * Parsed codec info 30474 */ 30475 30476 var codecsFromDefault = function codecsFromDefault(master, audioGroupId) { 30477 if (!master.mediaGroups.AUDIO || !audioGroupId) { 30478 return null; 30479 } 30480 var audioGroup = master.mediaGroups.AUDIO[audioGroupId]; 30481 if (!audioGroup) { 30482 return null; 30483 } 30484 for (var name in audioGroup) { 30485 var audioType = audioGroup[name]; 30486 if (audioType.default && audioType.playlists) { 30487 // codec should be the same for all playlists within the audio type 30488 return parseCodecs(audioType.playlists[0].attributes.CODECS); 30489 } 30490 } 30491 return null; 30492 }; 30493 var isAudioCodec = function isAudioCodec(codec) { 30494 if (codec === void 0) { 30495 codec = ''; 30496 } 30497 return regexs.audio.test(codec.trim().toLowerCase()); 30498 }; 30499 var isTextCodec = function isTextCodec(codec) { 30500 if (codec === void 0) { 30501 codec = ''; 30502 } 30503 return regexs.text.test(codec.trim().toLowerCase()); 30504 }; 30505 var getMimeForCodec = function getMimeForCodec(codecString) { 30506 if (!codecString || typeof codecString !== 'string') { 30507 return; 30508 } 30509 var codecs = codecString.toLowerCase().split(',').map(function (c) { 30510 return translateLegacyCodec(c.trim()); 30511 }); // default to video type 30512 30513 var type = 'video'; // only change to audio type if the only codec we have is 30514 // audio 30515 30516 if (codecs.length === 1 && isAudioCodec(codecs[0])) { 30517 type = 'audio'; 30518 } else if (codecs.length === 1 && isTextCodec(codecs[0])) { 30519 // text uses application/<container> for now 30520 type = 'application'; 30521 } // default the container to mp4 30522 30523 var container = 'mp4'; // every codec must be able to go into the container 30524 // for that container to be the correct one 30525 30526 if (codecs.every(function (c) { 30527 return regexs.mp4.test(c); 30528 })) { 30529 container = 'mp4'; 30530 } else if (codecs.every(function (c) { 30531 return regexs.webm.test(c); 30532 })) { 30533 container = 'webm'; 30534 } else if (codecs.every(function (c) { 30535 return regexs.ogg.test(c); 30536 })) { 30537 container = 'ogg'; 30538 } 30539 return type + "/" + container + ";codecs=\"" + codecString + "\""; 30540 }; 30541 var browserSupportsCodec = function browserSupportsCodec(codecString) { 30542 if (codecString === void 0) { 30543 codecString = ''; 30544 } 30545 return window.MediaSource && window.MediaSource.isTypeSupported && window.
vendor: 8,931 bytes, lines 30545-30800
30545MediaSource.isTypeSupported(getMimeForCodec(codecString)) || false; 30546 }; 30547 var muxerSupportsCodec = function muxerSupportsCodec(codecString) { 30548 if (codecString === void 0) { 30549 codecString = ''; 30550 } 30551 return codecString.toLowerCase().split(',').every(function (codec) { 30552 codec = codec.trim(); // any match is supported. 30553 30554 for (var i = 0; i < upperMediaTypes.length; i++) { 30555 var type = upperMediaTypes[i]; 30556 if (regexs["muxer" + type].test(codec)) { 30557 return true; 30558 } 30559 } 30560 return false; 30561 }); 30562 }; 30563 var DEFAULT_AUDIO_CODEC = 'mp4a.40.2'; 30564 var DEFAULT_VIDEO_CODEC = 'avc1.4d400d'; 30565 30566 var MPEGURL_REGEX = /^(audio|video|application)\/(x-|vnd\.apple\.)?mpegurl/i; 30567 var DASH_REGEX = /^application\/dash\+xml/i; 30568 /** 30569 * Returns a string that describes the type of source based on a video source object's 30570 * media type. 30571 * 30572 * @see {@link https://dev.w3.org/html5/pf-summary/video.html#dom-source-type|Source Type} 30573 * 30574 * @param {string} type 30575 * Video source object media type 30576 * @return {('hls'|'dash'|'vhs-json'|null)} 30577 * VHS source type string 30578 */ 30579 30580 var simpleTypeFromSourceType = function simpleTypeFromSourceType(type) { 30581 if (MPEGURL_REGEX.test(type)) { 30582 return 'hls'; 30583 } 30584 if (DASH_REGEX.test(type)) { 30585 return 'dash'; 30586 } // Denotes the special case of a manifest object passed to http-streaming instead of a 30587 // source URL. 30588 // 30589 // See https://en.wikipedia.org/wiki/Media_type for details on specifying media types. 30590 // 30591 // In this case, vnd stands for vendor, video.js for the organization, VHS for this 30592 // project, and the +json suffix identifies the structure of the media type. 30593 30594 if (type === 'application/vnd.videojs.vhs+json') { 30595 return 'vhs-json'; 30596 } 30597 return null; 30598 }; 30599 30600 // const log2 = Math.log2 ? Math.log2 : (x) => (Math.log(x) / Math.log(2)); 30601 // we used to do this with log2 but BigInt does not support builtin math 30602 // Math.ceil(log2(x)); 30603 30604 var countBits = function countBits(x) { 30605 return x.toString(2).length; 30606 }; // count the number of whole bytes it would take to represent a number 30607 30608 var countBytes = function countBytes(x) { 30609 return Math.ceil(countBits(x) / 8); 30610 }; 30611 var isArrayBufferView = function isArrayBufferView(obj) { 30612 if (ArrayBuffer.isView === 'function') { 30613 return ArrayBuffer.isView(obj); 30614 } 30615 return obj && obj.buffer instanceof ArrayBuffer; 30616 }; 30617 var isTypedArray = function isTypedArray(obj) { 30618 return isArrayBufferView(obj); 30619 }; 30620 var toUint8 = function toUint8(bytes) { 30621 if (bytes instanceof Uint8Array) { 30622 return bytes; 30623 } 30624 if (!Array.isArray(bytes) && !isTypedArray(bytes) && !(bytes instanceof ArrayBuffer)) { 30625 // any non-number or NaN leads to empty uint8array 30626 // eslint-disable-next-line 30627 if (typeof bytes !== 'number' || typeof bytes === 'number' && bytes !== bytes) { 30628 bytes = 0; 30629 } else { 30630 bytes = [bytes]; 30631 } 30632 } 30633 return new Uint8Array(bytes && bytes.buffer || bytes, bytes && bytes.byteOffset || 0, bytes && bytes.byteLength || 0); 30634 }; 30635 var BigInt = window.BigInt || Number; 30636 var BYTE_TABLE = [BigInt('0x1'), BigInt('0x100'), BigInt('0x10000'), BigInt('0x1000000'), BigInt('0x100000000'), BigInt('0x10000000000'), BigInt('0x1000000000000'), BigInt('0x100000000000000'), BigInt('0x10000000000000000')]; 30637 (function () { 30638 var a = new Uint16Array([0xFFCC]); 30639 var b = new Uint8Array(a.buffer, a.byteOffset, a.byteLength); 30640 if (b[0] === 0xFF) { 30641 return 'big'; 30642 } 30643 if (b[0] === 0xCC) { 30644 return 'little'; 30645 } 30646 return 'unknown'; 30647 })(); 30648 var bytesToNumber = function bytesToNumber(bytes, _temp) { 30649 var _ref = _temp === void 0 ? {} : _temp, 30650 _ref$signed = _ref.signed, 30651 signed = _ref$signed === void 0 ? false : _ref$signed, 30652 _ref$le = _ref.le, 30653 le = _ref$le === void 0 ? false : _ref$le; 30654 bytes = toUint8(bytes); 30655 var fn = le ? 'reduce' : 'reduceRight'; 30656 var obj = bytes[fn] ? bytes[fn] : Array.prototype[fn]; 30657 var number = obj.call(bytes, function (total, byte, i) { 30658 var exponent = le ? i : Math.abs(i + 1 - bytes.length); 30659 return total + BigInt(byte) * BYTE_TABLE[exponent]; 30660 }, BigInt(0)); 30661 if (signed) { 30662 var max = BYTE_TABLE[bytes.length] / BigInt(2) - BigInt(1); 30663 number = BigInt(number); 30664 if (number > max) { 30665 number -= max; 30666 number -= max; 30667 number -= BigInt(2); 30668 } 30669 } 30670 return Number(number); 30671 }; 30672 var numberToBytes = function numberToBytes(number, _temp2) { 30673 var _ref2 = _temp2 === void 0 ? {} : _temp2, 30674 _ref2$le = _ref2.le, 30675 le = _ref2$le === void 0 ? false : _ref2$le; 30676 30677 // eslint-disable-next-line 30678 if (typeof number !== 'bigint' && typeof number !== 'number' || typeof number === 'number' && number !== number) { 30679 number = 0; 30680 } 30681 number = BigInt(number); 30682 var byteCount = countBytes(number); 30683 var bytes = new Uint8Array(new ArrayBuffer(byteCount)); 30684 for (var i = 0; i < byteCount; i++) { 30685 var byteIndex = le ? i : Math.abs(i + 1 - bytes.length); 30686 bytes[byteIndex] = Number(number / BYTE_TABLE[i] & BigInt(0xFF)); 30687 if (number < 0) { 30688 bytes[byteIndex] = Math.abs(~bytes[byteIndex]); 30689 bytes[byteIndex] -= i === 0 ? 1 : 2; 30690 } 30691 } 30692 return bytes; 30693 }; 30694 var stringToBytes = function stringToBytes(string, stringIsBytes) { 30695 if (typeof string !== 'string' && string && typeof string.toString === 'function') { 30696 string = string.toString(); 30697 } 30698 if (typeof string !== 'string') { 30699 return new Uint8Array(); 30700 } // If the string already is bytes, we don't have to do this 30701 // otherwise we do this so that we split multi length characters 30702 // into individual bytes 30703 30704 if (!stringIsBytes) { 30705 string = unescape(encodeURIComponent(string)); 30706 } 30707 var view = new Uint8Array(string.length); 30708 for (var i = 0; i < string.length; i++) { 30709 view[i] = string.charCodeAt(i); 30710 } 30711 return view; 30712 }; 30713 var concatTypedArrays = function concatTypedArrays() { 30714 for (var _len = arguments.length, buffers = new Array(_len), _key = 0; _key < _len; _key++) { 30715 buffers[_key] = arguments[_key]; 30716 } 30717 buffers = buffers.filter(function (b) { 30718 return b && (b.byteLength || b.length) && typeof b !== 'string'; 30719 }); 30720 if (buffers.length <= 1) { 30721 // for 0 length we will return empty uint8 30722 // for 1 length we return the first uint8 30723 return toUint8(buffers[0]); 30724 } 30725 var totalLen = buffers.reduce(function (total, buf, i) { 30726 return total + (buf.byteLength || buf.length); 30727 }, 0); 30728 var tempBuffer = new Uint8Array(totalLen); 30729 var offset = 0; 30730 buffers.forEach(function (buf) { 30731 buf = toUint8(buf); 30732 tempBuffer.set(buf, offset); 30733 offset += buf.byteLength; 30734 }); 30735 return tempBuffer; 30736 }; 30737 /** 30738 * Check if the bytes "b" are contained within bytes "a". 30739 * 30740 * @param {Uint8Array|Array} a 30741 * Bytes to check in 30742 * 30743 * @param {Uint8Array|Array} b 30744 * Bytes to check for 30745 * 30746 * @param {Object} options 30747 * options 30748 * 30749 * @param {Array|Uint8Array} [offset=0] 30750 * offset to use when looking at bytes in a 30751 * 30752 * @param {Array|Uint8Array} [mask=[]] 30753 * mask to use on bytes before comparison. 30754 * 30755 * @return {boolean} 30756 * If all bytes in b are inside of a, taking into account 30757 * bit masks. 30758 */ 30759 30760 var bytesMatch = function bytesMatch(a, b, _temp3) { 30761 var _ref3 = _temp3 === void 0 ? {} : _temp3, 30762 _ref3$offset = _ref3.offset, 30763 offset = _ref3$offset === void 0 ? 0 : _ref3$offset, 30764 _ref3$mask = _ref3.mask, 30765 mask = _ref3$mask === void 0 ? [] : _ref3$mask; 30766 a = toUint8(a); 30767 b = toUint8(b); // ie 11 does not support uint8 every 30768 30769 var fn = b.every ? b.every : Array.prototype.every; 30770 return b.length && a.length - offset >= b.length && 30771 // ie 11 doesn't support every on uin8 30772 fn.call(b, function (bByte, i) { 30773 var aByte = mask[i] ? mask[i] & a[offset + i] : a[offset + i]; 30774 return bByte === aByte; 30775 }); 30776 }; 30777 30778 /** 30779 * Loops through all supported media groups in master and calls the provided 30780 * callback for each group 30781 * 30782 * @param {Object} master 30783 * The parsed master manifest object 30784 * @param {string[]} groups 30785 * The media groups to call the callback for 30786 * @param {Function} callback 30787 * Callback to call for each media group 30788 */ 30789 var forEachMediaGroup$1 = function forEachMediaGroup(master, groups, callback) { 30790 groups.forEach(function (mediaType) { 30791 for (var groupKey in master.mediaGroups[mediaType]) { 30792 for (var labelKey in master.mediaGroups[mediaType][groupKey]) { 30793 var mediaProperties = master.mediaGroups[mediaType][groupKey][labelKey]; 30794 callback(mediaProperties, mediaType, groupKey, labelKey); 30795 } 30796 } 30797 }); 30798 }; 30799 30800
30800var atob = function atob(s) { 30801 return window.atob ? window.atob(s) : Buffer.from(s, 'base64').toString('binary'); 30802 }; 30803 function decodeB64ToUint8Array(b64Text) { 30804 var decodedString = atob(b64Text); 30805 var array = new Uint8Array(decodedString.length); 30806 for (var i = 0; i < decodedString.length; i++) { 30807 array[i] = decodedString.charCodeAt(i); 30808 } 30809 return array; 30810 }
vendor: 6,685 bytes, lines 30810-30997
30810 30811 30812 /** 30813 * Ponyfill for `Array.prototype.find` which is only available in ES6 runtimes. 30814 * 30815 * Works with anything that has a `length` property and index access properties, including NodeList. 30816 * 30817 * @template {unknown} T 30818 * @param {Array<T> | ({length:number, [number]: T})} list 30819 * @param {function (item: T, index: number, list:Array<T> | ({length:number, [number]: T})):boolean} predicate 30820 * @param {Partial<Pick<ArrayConstructor['prototype'], 'find'>>?} ac `Array.prototype` by default, 30821 * allows injecting a custom implementation in tests 30822 * @returns {T | undefined} 30823 * 30824 * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find 30825 * @see https://tc39.es/ecma262/multipage/indexed-collections.html#sec-array.prototype.find 30826 */ 30827 function find$1(list, predicate, ac) { 30828 if (ac === undefined) { 30829 ac = Array.prototype; 30830 } 30831 if (list && typeof ac.find === 'function') { 30832 return ac.find.call(list, predicate); 30833 } 30834 for (var i = 0; i < list.length; i++) { 30835 if (Object.prototype.hasOwnProperty.call(list, i)) { 30836 var item = list[i]; 30837 if (predicate.call(undefined, item, i, list)) { 30838 return item; 30839 } 30840 } 30841 } 30842 } 30843 30844 /** 30845 * "Shallow freezes" an object to render it immutable. 30846 * Uses `Object.freeze` if available, 30847 * otherwise the immutability is only in the type. 30848 * 30849 * Is used to create "enum like" objects. 30850 * 30851 * @template T 30852 * @param {T} object the object to freeze 30853 * @param {Pick<ObjectConstructor, 'freeze'> = Object} oc `Object` by default, 30854 * allows to inject custom object constructor for tests 30855 * @returns {Readonly<T>} 30856 * 30857 * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze 30858 */ 30859 function freeze(object, oc) { 30860 if (oc === undefined) { 30861 oc = Object; 30862 } 30863 return oc && typeof oc.freeze === 'function' ? oc.freeze(object) : object; 30864 } 30865 30866 /** 30867 * Since we can not rely on `Object.assign` we provide a simplified version 30868 * that is sufficient for our needs. 30869 * 30870 * @param {Object} target 30871 * @param {Object | null | undefined} source 30872 * 30873 * @returns {Object} target 30874 * @throws TypeError if target is not an object 30875 * 30876 * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign 30877 * @see https://tc39.es/ecma262/multipage/fundamental-objects.html#sec-object.assign 30878 */ 30879 function assign(target, source) { 30880 if (target === null || typeof target !== 'object') { 30881 throw new TypeError('target is not an object'); 30882 } 30883 for (var key in source) { 30884 if (Object.prototype.hasOwnProperty.call(source, key)) { 30885 target[key] = source[key]; 30886 } 30887 } 30888 return target; 30889 } 30890 30891 /** 30892 * All mime types that are allowed as input to `DOMParser.parseFromString` 30893 * 30894 * @see https://developer.mozilla.org/en-US/docs/Web/API/DOMParser/parseFromString#Argument02 MDN 30895 * @see https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#domparsersupportedtype WHATWG HTML Spec 30896 * @see DOMParser.prototype.parseFromString 30897 */ 30898 var MIME_TYPE = freeze({ 30899 /** 30900 * `text/html`, the only mime type that triggers treating an XML document as HTML. 30901 * 30902 * @see DOMParser.SupportedType.isHTML 30903 * @see https://www.iana.org/assignments/media-types/text/html IANA MimeType registration 30904 * @see https://en.wikipedia.org/wiki/HTML Wikipedia 30905 * @see https://developer.mozilla.org/en-US/docs/Web/API/DOMParser/parseFromString MDN 30906 * @see https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-domparser-parsefromstring WHATWG HTML Spec 30907 */ 30908 HTML: 'text/html', 30909 /** 30910 * Helper method to check a mime type if it indicates an HTML document 30911 * 30912 * @param {string} [value] 30913 * @returns {boolean} 30914 * 30915 * @see https://www.iana.org/assignments/media-types/text/html IANA MimeType registration 30916 * @see https://en.wikipedia.org/wiki/HTML Wikipedia 30917 * @see https://developer.mozilla.org/en-US/docs/Web/API/DOMParser/parseFromString MDN 30918 * @see https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-domparser-parsefromstring */ 30919 isHTML: function (value) { 30920 return value === MIME_TYPE.HTML; 30921 }, 30922 /** 30923 * `application/xml`, the standard mime type for XML documents. 30924 * 30925 * @see https://www.iana.org/assignments/media-types/application/xml IANA MimeType registration 30926 * @see https://tools.ietf.org/html/rfc7303#section-9.1 RFC 7303 30927 * @see https://en.wikipedia.org/wiki/XML_and_MIME Wikipedia 30928 */ 30929 XML_APPLICATION: 'application/xml', 30930 /** 30931 * `text/html`, an alias for `application/xml`. 30932 * 30933 * @see https://tools.ietf.org/html/rfc7303#section-9.2 RFC 7303 30934 * @see https://www.iana.org/assignments/media-types/text/xml IANA MimeType registration 30935 * @see https://en.wikipedia.org/wiki/XML_and_MIME Wikipedia 30936 */ 30937 XML_TEXT: 'text/xml', 30938 /** 30939 * `application/xhtml+xml`, indicates an XML document that has the default HTML namespace, 30940 * but is parsed as an XML document. 30941 * 30942 * @see https://www.iana.org/assignments/media-types/application/xhtml+xml IANA MimeType registration 30943 * @see https://dom.spec.whatwg.org/#dom-domimplementation-createdocument WHATWG DOM Spec 30944 * @see https://en.wikipedia.org/wiki/XHTML Wikipedia 30945 */ 30946 XML_XHTML_APPLICATION: 'application/xhtml+xml', 30947 /** 30948 * `image/svg+xml`, 30949 * 30950 * @see https://www.iana.org/assignments/media-types/image/svg+xml IANA MimeType registration 30951 * @see https://www.w3.org/TR/SVG11/ W3C SVG 1.1 30952 * @see https://en.wikipedia.org/wiki/Scalable_Vector_Graphics Wikipedia 30953 */ 30954 XML_SVG_IMAGE: 'image/svg+xml' 30955 }); 30956 30957 /** 30958 * Namespaces that are used in this code base. 30959 * 30960 * @see http://www.w3.org/TR/REC-xml-names 30961 */ 30962 var NAMESPACE$3 = freeze({ 30963 /** 30964 * The XHTML namespace. 30965 * 30966 * @see http://www.w3.org/1999/xhtml 30967 */ 30968 HTML: 'http://www.w3.org/1999/xhtml', 30969 /** 30970 * Checks if `uri` equals `NAMESPACE.HTML`. 30971 * 30972 * @param {string} [uri] 30973 * 30974 * @see NAMESPACE.HTML 30975 */ 30976 isHTML: function (uri) { 30977 return uri === NAMESPACE$3.HTML; 30978 }, 30979 /** 30980 * The SVG namespace. 30981 * 30982 * @see http://www.w3.org/2000/svg 30983 */ 30984 SVG: 'http://www.w3.org/2000/svg', 30985 /** 30986 * The `xml:` namespace. 30987 * 30988 * @see http://www.w3.org/XML/1998/namespace 30989 */ 30990 XML: 'http://www.w3.org/XML/1998/namespace', 30991 /** 30992 * The `xmlns:` namespace 30993 * 30994 * @see https://www.w3.org/2000/xmlns/ 30995 */ 30996 XMLNS: 'http://www.w3.org/2000/xmlns/' 30997 });
vendor: 15,318 bytes, lines 30998-31423
30998 var assign_1 = assign; 30999 var find_1 = find$1; 31000 var freeze_1 = freeze; 31001 var MIME_TYPE_1 = MIME_TYPE; 31002 var NAMESPACE_1 = NAMESPACE$3; 31003 var conventions = { 31004 assign: assign_1, 31005 find: find_1, 31006 freeze: freeze_1, 31007 MIME_TYPE: MIME_TYPE_1, 31008 NAMESPACE: NAMESPACE_1 31009 }; 31010 31011 var find = conventions.find; 31012 var NAMESPACE$2 = conventions.NAMESPACE; 31013 31014 /** 31015 * A prerequisite for `[].filter`, to drop elements that are empty 31016 * @param {string} input 31017 * @returns {boolean} 31018 */ 31019 function notEmptyString(input) { 31020 return input !== ''; 31021 } 31022 /** 31023 * @see https://infra.spec.whatwg.org/#split-on-ascii-whitespace 31024 * @see https://infra.spec.whatwg.org/#ascii-whitespace 31025 * 31026 * @param {string} input 31027 * @returns {string[]} (can be empty) 31028 */ 31029 function splitOnASCIIWhitespace(input) { 31030 // U+0009 TAB, U+000A LF, U+000C FF, U+000D CR, U+0020 SPACE 31031 return input ? input.split(/[\t\n\f\r ]+/).filter(notEmptyString) : []; 31032 } 31033 31034 /** 31035 * Adds element as a key to current if it is not already present. 31036 * 31037 * @param {Record<string, boolean | undefined>} current 31038 * @param {string} element 31039 * @returns {Record<string, boolean | undefined>} 31040 */ 31041 function orderedSetReducer(current, element) { 31042 if (!current.hasOwnProperty(element)) { 31043 current[element] = true; 31044 } 31045 return current; 31046 } 31047 31048 /** 31049 * @see https://infra.spec.whatwg.org/#ordered-set 31050 * @param {string} input 31051 * @returns {string[]} 31052 */ 31053 function toOrderedSet(input) { 31054 if (!input) return []; 31055 var list = splitOnASCIIWhitespace(input); 31056 return Object.keys(list.reduce(orderedSetReducer, {})); 31057 } 31058 31059 /** 31060 * Uses `list.indexOf` to implement something like `Array.prototype.includes`, 31061 * which we can not rely on being available. 31062 * 31063 * @param {any[]} list 31064 * @returns {function(any): boolean} 31065 */ 31066 function arrayIncludes(list) { 31067 return function (element) { 31068 return list && list.indexOf(element) !== -1; 31069 }; 31070 } 31071 function copy(src, dest) { 31072 for (var p in src) { 31073 if (Object.prototype.hasOwnProperty.call(src, p)) { 31074 dest[p] = src[p]; 31075 } 31076 } 31077 } 31078 31079 /** 31080 ^\w+\.prototype\.([_\w]+)\s*=\s*((?:.*\{\s*?[\r\n][\s\S]*?^})|\S.*?(?=[;\r\n]));? 31081 ^\w+\.prototype\.([_\w]+)\s*=\s*(\S.*?(?=[;\r\n]));? 31082 */ 31083 function _extends(Class, Super) { 31084 var pt = Class.prototype; 31085 if (!(pt instanceof Super)) { 31086 function t() {} 31087 t.prototype = Super.prototype; 31088 t = new t(); 31089 copy(pt, t); 31090 Class.prototype = pt = t; 31091 } 31092 if (pt.constructor != Class) { 31093 if (typeof Class != 'function') { 31094 console.error("unknown Class:" + Class); 31095 } 31096 pt.constructor = Class; 31097 } 31098 } 31099 31100 // Node Types 31101 var NodeType = {}; 31102 var ELEMENT_NODE = NodeType.ELEMENT_NODE = 1; 31103 var ATTRIBUTE_NODE = NodeType.ATTRIBUTE_NODE = 2; 31104 var TEXT_NODE = NodeType.TEXT_NODE = 3; 31105 var CDATA_SECTION_NODE = NodeType.CDATA_SECTION_NODE = 4; 31106 var ENTITY_REFERENCE_NODE = NodeType.ENTITY_REFERENCE_NODE = 5; 31107 var ENTITY_NODE = NodeType.ENTITY_NODE = 6; 31108 var PROCESSING_INSTRUCTION_NODE = NodeType.PROCESSING_INSTRUCTION_NODE = 7; 31109 var COMMENT_NODE = NodeType.COMMENT_NODE = 8; 31110 var DOCUMENT_NODE = NodeType.DOCUMENT_NODE = 9; 31111 var DOCUMENT_TYPE_NODE = NodeType.DOCUMENT_TYPE_NODE = 10; 31112 var DOCUMENT_FRAGMENT_NODE = NodeType.DOCUMENT_FRAGMENT_NODE = 11; 31113 var NOTATION_NODE = NodeType.NOTATION_NODE = 12; 31114 31115 // ExceptionCode 31116 var ExceptionCode = {}; 31117 var ExceptionMessage = {}; 31118 ExceptionCode.INDEX_SIZE_ERR = (ExceptionMessage[1] = "Index size error", 1); 31119 ExceptionCode.DOMSTRING_SIZE_ERR = (ExceptionMessage[2] = "DOMString size error", 2); 31120 var HIERARCHY_REQUEST_ERR = ExceptionCode.HIERARCHY_REQUEST_ERR = (ExceptionMessage[3] = "Hierarchy request error", 3); 31121 ExceptionCode.WRONG_DOCUMENT_ERR = (ExceptionMessage[4] = "Wrong document", 4); 31122 ExceptionCode.INVALID_CHARACTER_ERR = (ExceptionMessage[5] = "Invalid character", 5); 31123 ExceptionCode.NO_DATA_ALLOWED_ERR = (ExceptionMessage[6] = "No data allowed", 6); 31124 ExceptionCode.NO_MODIFICATION_ALLOWED_ERR = (ExceptionMessage[7] = "No modification allowed", 7); 31125 var NOT_FOUND_ERR = ExceptionCode.NOT_FOUND_ERR = (ExceptionMessage[8] = "Not found", 8); 31126 ExceptionCode.NOT_SUPPORTED_ERR = (ExceptionMessage[9] = "Not supported", 9); 31127 var INUSE_ATTRIBUTE_ERR = ExceptionCode.INUSE_ATTRIBUTE_ERR = (ExceptionMessage[10] = "Attribute in use", 10); 31128 //level2 31129 ExceptionCode.INVALID_STATE_ERR = (ExceptionMessage[11] = "Invalid state", 11); 31130 ExceptionCode.SYNTAX_ERR = (ExceptionMessage[12] = "Syntax error", 12); 31131 ExceptionCode.INVALID_MODIFICATION_ERR = (ExceptionMessage[13] = "Invalid modification", 13); 31132 ExceptionCode.NAMESPACE_ERR = (ExceptionMessage[14] = "Invalid namespace", 14); 31133 ExceptionCode.INVALID_ACCESS_ERR = (ExceptionMessage[15] = "Invalid access", 15); 31134 31135 /** 31136 * DOM Level 2 31137 * Object DOMException 31138 * @see http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/ecma-script-binding.html 31139 * @see http://www.w3.org/TR/REC-DOM-Level-1/ecma-script-language-binding.html 31140 */ 31141 function DOMException(code, message) { 31142 if (message instanceof Error) { 31143 var error = message; 31144 } else { 31145 error = this; 31146 Error.call(this, ExceptionMessage[code]); 31147 this.message = ExceptionMessage[code]; 31148 if (Error.captureStackTrace) Error.captureStackTrace(this, DOMException); 31149 } 31150 error.code = code; 31151 if (message) this.message = this.message + ": " + message; 31152 return error; 31153 } 31154 DOMException.prototype = Error.prototype; 31155 copy(ExceptionCode, DOMException); 31156 31157 /** 31158 * @see http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/core.html#ID-536297177 31159 * The NodeList interface provides the abstraction of an ordered collection of nodes, without defining or constraining how this collection is implemented. NodeList objects in the DOM are live. 31160 * The items in the NodeList are accessible via an integral index, starting from 0. 31161 */ 31162 function NodeList() {} 31163 NodeList.prototype = { 31164 /** 31165 * The number of nodes in the list. The range of valid child node indices is 0 to length-1 inclusive. 31166 * @standard level1 31167 */ 31168 length: 0, 31169 /** 31170 * Returns the indexth item in the collection. If index is greater than or equal to the number of nodes in the list, this returns null. 31171 * @standard level1 31172 * @param index unsigned long 31173 * Index into the collection. 31174 * @return Node 31175 * The node at the indexth position in the NodeList, or null if that is not a valid index. 31176 */ 31177 item: function (index) { 31178 return index >= 0 && index < this.length ? this[index] : null; 31179 }, 31180 toString: function (isHTML, nodeFilter) { 31181 for (var buf = [], i = 0; i < this.length; i++) { 31182 serializeToString(this[i], buf, isHTML, nodeFilter); 31183 } 31184 return buf.join(''); 31185 }, 31186 /** 31187 * @private 31188 * @param {function (Node):boolean} predicate 31189 * @returns {Node[]} 31190 */ 31191 filter: function (predicate) { 31192 return Array.prototype.filter.call(this, predicate); 31193 }, 31194 /** 31195 * @private 31196 * @param {Node} item 31197 * @returns {number} 31198 */ 31199 indexOf: function (item) { 31200 return Array.prototype.indexOf.call(this, item); 31201 } 31202 }; 31203 function LiveNodeList(node, refresh) { 31204 this._node = node; 31205 this._refresh = refresh; 31206 _updateLiveList(this); 31207 } 31208 function _updateLiveList(list) { 31209 var inc = list._node._inc || list._node.ownerDocument._inc; 31210 if (list._inc !== inc) { 31211 var ls = list._refresh(list._node); 31212 __set__(list, 'length', ls.length); 31213 if (!list.$$length || ls.length < list.$$length) { 31214 for (var i = ls.length; (i in list); i++) { 31215 if (Object.prototype.hasOwnProperty.call(list, i)) { 31216 delete list[i]; 31217 } 31218 } 31219 } 31220 copy(ls, list); 31221 list._inc = inc; 31222 } 31223 } 31224 LiveNodeList.prototype.item = function (i) { 31225 _updateLiveList(this); 31226 return this[i] || null; 31227 }; 31228 _extends(LiveNodeList, NodeList); 31229 31230 /** 31231 * Objects implementing the NamedNodeMap interface are used 31232 * to represent collections of nodes that can be accessed by name. 31233 * Note that NamedNodeMap does not inherit from NodeList; 31234 * NamedNodeMaps are not maintained in any particular order. 31235 * Objects contained in an object implementing NamedNodeMap may also be accessed by an ordinal index, 31236 * but this is simply to allow convenient enumeration of the contents of a NamedNodeMap, 31237 * and does not imply that the DOM specifies an order to these Nodes. 31238 * NamedNodeMap objects in the DOM are live. 31239 * used for attributes or DocumentType entities 31240 */ 31241 function NamedNodeMap() {} 31242 function _findNodeIndex(list, node) { 31243 var i = list.length; 31244 while (i--) { 31245 if (list[i] === node) { 31246 return i; 31247 } 31248 } 31249 } 31250 function _addNamedNode(el, list, newAttr, oldAttr) { 31251 if (oldAttr) { 31252 list[_findNodeIndex(list, oldAttr)] = newAttr; 31253 } else { 31254 list[list.length++] = newAttr; 31255 } 31256 if (el) { 31257 newAttr.ownerElement = el; 31258 var doc = el.ownerDocument; 31259 if (doc) { 31260 oldAttr && _onRemoveAttribute(doc, el, oldAttr); 31261 _onAddAttribute(doc, el, newAttr); 31262 } 31263 } 31264 } 31265 function _removeNamedNode(el, list, attr) { 31266 //console.log('remove attr:'+attr) 31267 var i = _findNodeIndex(list, attr); 31268 if (i >= 0) { 31269 var lastIndex = list.length - 1; 31270 while (i < lastIndex) { 31271 list[i] = list[++i]; 31272 } 31273 list.length = lastIndex; 31274 if (el) { 31275 var doc = el.ownerDocument; 31276 if (doc) { 31277 _onRemoveAttribute(doc, el, attr); 31278 attr.ownerElement = null; 31279 } 31280 } 31281 } else { 31282 throw new DOMException(NOT_FOUND_ERR, new Error(el.tagName + '@' + attr)); 31283 } 31284 } 31285 NamedNodeMap.prototype = { 31286 length: 0, 31287 item: NodeList.prototype.item, 31288 getNamedItem: function (key) { 31289 // if(key.indexOf(':')>0 || key == 'xmlns'){ 31290 // return null; 31291 // } 31292 //console.log() 31293 var i = this.length; 31294 while (i--) { 31295 var attr = this[i]; 31296 //console.log(attr.nodeName,key) 31297 if (attr.nodeName == key) { 31298 return attr; 31299 } 31300 } 31301 }, 31302 setNamedItem: function (attr) { 31303 var el = attr.ownerElement; 31304 if (el && el != this._ownerElement) { 31305 throw new DOMException(INUSE_ATTRIBUTE_ERR); 31306 } 31307 var oldAttr = this.getNamedItem(attr.nodeName); 31308 _addNamedNode(this._ownerElement, this, attr, oldAttr); 31309 return oldAttr; 31310 }, 31311 /* returns Node */ 31312 setNamedItemNS: function (attr) { 31313 // raises: WRONG_DOCUMENT_ERR,NO_MODIFICATION_ALLOWED_ERR,INUSE_ATTRIBUTE_ERR 31314 var el = attr.ownerElement, 31315 oldAttr; 31316 if (el && el != this._ownerElement) { 31317 throw new DOMException(INUSE_ATTRIBUTE_ERR); 31318 } 31319 oldAttr = this.getNamedItemNS(attr.namespaceURI, attr.localName); 31320 _addNamedNode(this._ownerElement, this, attr, oldAttr); 31321 return oldAttr; 31322 }, 31323 /* returns Node */ 31324 removeNamedItem: function (key) { 31325 var attr = this.getNamedItem(key); 31326 _removeNamedNode(this._ownerElement, this, attr); 31327 return attr; 31328 }, 31329 // raises: NOT_FOUND_ERR,NO_MODIFICATION_ALLOWED_ERR 31330 31331 //for level2 31332 removeNamedItemNS: function (namespaceURI, localName) { 31333 var attr = this.getNamedItemNS(namespaceURI, localName); 31334 _removeNamedNode(this._ownerElement, this, attr); 31335 return attr; 31336 }, 31337 getNamedItemNS: function (namespaceURI, localName) { 31338 var i = this.length; 31339 while (i--) { 31340 var node = this[i]; 31341 if (node.localName == localName && node.namespaceURI == namespaceURI) { 31342 return node; 31343 } 31344 } 31345 return null; 31346 } 31347 }; 31348 31349 /** 31350 * The DOMImplementation interface represents an object providing methods 31351 * which are not dependent on any particular document. 31352 * Such an object is returned by the `Document.implementation` property. 31353 * 31354 * __The individual methods describe the differences compared to the specs.__ 31355 * 31356 * @constructor 31357 * 31358 * @see https://developer.mozilla.org/en-US/docs/Web/API/DOMImplementation MDN 31359 * @see https://www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#ID-102161490 DOM Level 1 Core (Initial) 31360 * @see https://www.w3.org/TR/DOM-Level-2-Core/core.html#ID-102161490 DOM Level 2 Core 31361 * @see https://www.w3.org/TR/DOM-Level-3-Core/core.html#ID-102161490 DOM Level 3 Core 31362 * @see https://dom.spec.whatwg.org/#domimplementation DOM Living Standard 31363 */ 31364 function DOMImplementation$1() {} 31365 DOMImplementation$1.prototype = { 31366 /** 31367 * The DOMImplementation.hasFeature() method returns a Boolean flag indicating if a given feature is supported. 31368 * The different implementations fairly diverged in what kind of features were reported. 31369 * The latest version of the spec settled to force this method to always return true, where the functionality was accurate and in use. 31370 * 31371 * @deprecated It is deprecated and modern browsers return true in all cases. 31372 * 31373 * @param {string} feature 31374 * @param {string} [version] 31375 * @returns {boolean} always true 31376 * 31377 * @see https://developer.mozilla.org/en-US/docs/Web/API/DOMImplementation/hasFeature MDN 31378 * @see https://www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#ID-5CED94D7 DOM Level 1 Core 31379 * @see https://dom.spec.whatwg.org/#dom-domimplementation-hasfeature DOM Living Standard 31380 */ 31381 hasFeature: function (feature, version) { 31382 return true; 31383 }, 31384 /** 31385 * Creates an XML Document object of the specified type with its document element. 31386 * 31387 * __It behaves slightly different from the description in the living standard__: 31388 * - There is no interface/class `XMLDocument`, it returns a `Document` instance. 31389 * - `contentType`, `encoding`, `mode`, `origin`, `url` fields are currently not declared. 31390 * - this implementation is not validating names or qualified names 31391 * (when parsing XML strings, the SAX parser takes care of that) 31392 * 31393 * @param {string|null} namespaceURI 31394 * @param {string} qualifiedName 31395 * @param {DocumentType=null} doctype 31396 * @returns {Document} 31397 * 31398 * @see https://developer.mozilla.org/en-US/docs/Web/API/DOMImplementation/createDocument MDN 31399 * @see https://www.w3.org/TR/DOM-Level-2-Core/core.html#Level-2-Core-DOM-createDocument DOM Level 2 Core (initial) 31400 * @see https://dom.spec.whatwg.org/#dom-domimplementation-createdocument DOM Level 2 Core 31401 * 31402 * @see https://dom.spec.whatwg.org/#validate-and-extract DOM: Validate and extract 31403 * @see https://www.w3.org/TR/xml/#NT-NameStartChar XML Spec: Names 31404 * @see https://www.w3.org/TR/xml-names/#ns-qualnames XML Namespaces: Qualified names 31405 */ 31406 createDocument: function (namespaceURI, qualifiedName, doctype) { 31407 var doc = new Document(); 31408 doc.implementation = this; 31409 doc.childNodes = new NodeList(); 31410 doc.doctype = doctype || null; 31411 if (doctype) { 31412 doc.appendChild(doctype); 31413 } 31414 if (qualifiedName) { 31415 var root = doc.createElementNS(namespaceURI, qualifiedName); 31416 doc.appendChild(root); 31417 } 31418 return doc; 31419 }, 31420 /** 31421 * Returns a doctype, with the given `qualifiedName`, `publicId`, and `systemId`. 31422 * 31423 * __This behavior is slightly different from the in the specs__:
vendor: 5,159 bytes, lines 31424-31567
31424 * - this implementation is not validating names or qualified names 31425 * (when parsing XML strings, the SAX parser takes care of that) 31426 * 31427 * @param {string} qualifiedName 31428 * @param {string} [publicId] 31429 * @param {string} [systemId] 31430 * @returns {DocumentType} which can either be used with `DOMImplementation.createDocument` upon document creation 31431 * or can be put into the document via methods like `Node.insertBefore()` or `Node.replaceChild()` 31432 * 31433 * @see https://developer.mozilla.org/en-US/docs/Web/API/DOMImplementation/createDocumentType MDN 31434 * @see https://www.w3.org/TR/DOM-Level-2-Core/core.html#Level-2-Core-DOM-createDocType DOM Level 2 Core 31435 * @see https://dom.spec.whatwg.org/#dom-domimplementation-createdocumenttype DOM Living Standard 31436 * 31437 * @see https://dom.spec.whatwg.org/#validate-and-extract DOM: Validate and extract 31438 * @see https://www.w3.org/TR/xml/#NT-NameStartChar XML Spec: Names 31439 * @see https://www.w3.org/TR/xml-names/#ns-qualnames XML Namespaces: Qualified names 31440 */ 31441 createDocumentType: function (qualifiedName, publicId, systemId) { 31442 var node = new DocumentType(); 31443 node.name = qualifiedName; 31444 node.nodeName = qualifiedName; 31445 node.publicId = publicId || ''; 31446 node.systemId = systemId || ''; 31447 return node; 31448 } 31449 }; 31450 31451 /** 31452 * @see http://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/core.html#ID-1950641247 31453 */ 31454 31455 function Node() {} 31456 Node.prototype = { 31457 firstChild: null, 31458 lastChild: null, 31459 previousSibling: null, 31460 nextSibling: null, 31461 attributes: null, 31462 parentNode: null, 31463 childNodes: null, 31464 ownerDocument: null, 31465 nodeValue: null, 31466 namespaceURI: null, 31467 prefix: null, 31468 localName: null, 31469 // Modified in DOM Level 2: 31470 insertBefore: function (newChild, refChild) { 31471 //raises 31472 return _insertBefore(this, newChild, refChild); 31473 }, 31474 replaceChild: function (newChild, oldChild) { 31475 //raises 31476 _insertBefore(this, newChild, oldChild, assertPreReplacementValidityInDocument); 31477 if (oldChild) { 31478 this.removeChild(oldChild); 31479 } 31480 }, 31481 removeChild: function (oldChild) { 31482 return _removeChild(this, oldChild); 31483 }, 31484 appendChild: function (newChild) { 31485 return this.insertBefore(newChild, null); 31486 }, 31487 hasChildNodes: function () { 31488 return this.firstChild != null; 31489 }, 31490 cloneNode: function (deep) { 31491 return cloneNode(this.ownerDocument || this, this, deep); 31492 }, 31493 // Modified in DOM Level 2: 31494 normalize: function () { 31495 var child = this.firstChild; 31496 while (child) { 31497 var next = child.nextSibling; 31498 if (next && next.nodeType == TEXT_NODE && child.nodeType == TEXT_NODE) { 31499 this.removeChild(next); 31500 child.appendData(next.data); 31501 } else { 31502 child.normalize(); 31503 child = next; 31504 } 31505 } 31506 }, 31507 // Introduced in DOM Level 2: 31508 isSupported: function (feature, version) { 31509 return this.ownerDocument.implementation.hasFeature(feature, version); 31510 }, 31511 // Introduced in DOM Level 2: 31512 hasAttributes: function () { 31513 return this.attributes.length > 0; 31514 }, 31515 /** 31516 * Look up the prefix associated to the given namespace URI, starting from this node. 31517 * **The default namespace declarations are ignored by this method.** 31518 * See Namespace Prefix Lookup for details on the algorithm used by this method. 31519 * 31520 * _Note: The implementation seems to be incomplete when compared to the algorithm described in the specs._ 31521 * 31522 * @param {string | null} namespaceURI 31523 * @returns {string | null} 31524 * @see https://www.w3.org/TR/DOM-Level-3-Core/core.html#Node3-lookupNamespacePrefix 31525 * @see https://www.w3.org/TR/DOM-Level-3-Core/namespaces-algorithms.html#lookupNamespacePrefixAlgo 31526 * @see https://dom.spec.whatwg.org/#dom-node-lookupprefix 31527 * @see https://github.com/xmldom/xmldom/issues/322 31528 */ 31529 lookupPrefix: function (namespaceURI) { 31530 var el = this; 31531 while (el) { 31532 var map = el._nsMap; 31533 //console.dir(map) 31534 if (map) { 31535 for (var n in map) { 31536 if (Object.prototype.hasOwnProperty.call(map, n) && map[n] === namespaceURI) { 31537 return n; 31538 } 31539 } 31540 } 31541 el = el.nodeType == ATTRIBUTE_NODE ? el.ownerDocument : el.parentNode; 31542 } 31543 return null; 31544 }, 31545 // Introduced in DOM Level 3: 31546 lookupNamespaceURI: function (prefix) { 31547 var el = this; 31548 while (el) { 31549 var map = el._nsMap; 31550 //console.dir(map) 31551 if (map) { 31552 if (Object.prototype.hasOwnProperty.call(map, prefix)) { 31553 return map[prefix]; 31554 } 31555 } 31556 el = el.nodeType == ATTRIBUTE_NODE ? el.ownerDocument : el.parentNode; 31557 } 31558 return null; 31559 }, 31560 // Introduced in DOM Level 3: 31561 isDefaultNamespace: function (namespaceURI) { 31562 var prefix = this.lookupPrefix(namespaceURI); 31563 return prefix == null; 31564 } 31565 }; 31566 function _xmlEncoder(c) { 31567 return c == '<' && '<' || c == '>' && '>' || c == '&' && '&' || c == '"' && '"
vendor: 4,793 bytes, lines 31567-31729
31567' || '&#' + c.charCodeAt() + ';'; 31568 } 31569 copy(NodeType, Node); 31570 copy(NodeType, Node.prototype); 31571 31572 /** 31573 * @param callback return true for continue,false for break 31574 * @return boolean true: break visit; 31575 */ 31576 function _visitNode(node, callback) { 31577 if (callback(node)) { 31578 return true; 31579 } 31580 if (node = node.firstChild) { 31581 do { 31582 if (_visitNode(node, callback)) { 31583 return true; 31584 } 31585 } while (node = node.nextSibling); 31586 } 31587 } 31588 function Document() { 31589 this.ownerDocument = this; 31590 } 31591 function _onAddAttribute(doc, el, newAttr) { 31592 doc && doc._inc++; 31593 var ns = newAttr.namespaceURI; 31594 if (ns === NAMESPACE$2.XMLNS) { 31595 //update namespace 31596 el._nsMap[newAttr.prefix ? newAttr.localName : ''] = newAttr.value; 31597 } 31598 } 31599 function _onRemoveAttribute(doc, el, newAttr, remove) { 31600 doc && doc._inc++; 31601 var ns = newAttr.namespaceURI; 31602 if (ns === NAMESPACE$2.XMLNS) { 31603 //update namespace 31604 delete el._nsMap[newAttr.prefix ? newAttr.localName : '']; 31605 } 31606 } 31607 31608 /** 31609 * Updates `el.childNodes`, updating the indexed items and it's `length`. 31610 * Passing `newChild` means it will be appended. 31611 * Otherwise it's assumed that an item has been removed, 31612 * and `el.firstNode` and it's `.nextSibling` are used 31613 * to walk the current list of child nodes. 31614 * 31615 * @param {Document} doc 31616 * @param {Node} el 31617 * @param {Node} [newChild] 31618 * @private 31619 */ 31620 function _onUpdateChild(doc, el, newChild) { 31621 if (doc && doc._inc) { 31622 doc._inc++; 31623 //update childNodes 31624 var cs = el.childNodes; 31625 if (newChild) { 31626 cs[cs.length++] = newChild; 31627 } else { 31628 var child = el.firstChild; 31629 var i = 0; 31630 while (child) { 31631 cs[i++] = child; 31632 child = child.nextSibling; 31633 } 31634 cs.length = i; 31635 delete cs[cs.length]; 31636 } 31637 } 31638 } 31639 31640 /** 31641 * Removes the connections between `parentNode` and `child` 31642 * and any existing `child.previousSibling` or `child.nextSibling`. 31643 * 31644 * @see https://github.com/xmldom/xmldom/issues/135 31645 * @see https://github.com/xmldom/xmldom/issues/145 31646 * 31647 * @param {Node} parentNode 31648 * @param {Node} child 31649 * @returns {Node} the child that was removed. 31650 * @private 31651 */ 31652 function _removeChild(parentNode, child) { 31653 var previous = child.previousSibling; 31654 var next = child.nextSibling; 31655 if (previous) { 31656 previous.nextSibling = next; 31657 } else { 31658 parentNode.firstChild = next; 31659 } 31660 if (next) { 31661 next.previousSibling = previous; 31662 } else { 31663 parentNode.lastChild = previous; 31664 } 31665 child.parentNode = null; 31666 child.previousSibling = null; 31667 child.nextSibling = null; 31668 _onUpdateChild(parentNode.ownerDocument, parentNode); 31669 return child; 31670 } 31671 31672 /** 31673 * Returns `true` if `node` can be a parent for insertion. 31674 * @param {Node} node 31675 * @returns {boolean} 31676 */ 31677 function hasValidParentNodeType(node) { 31678 return node && (node.nodeType === Node.DOCUMENT_NODE || node.nodeType === Node.DOCUMENT_FRAGMENT_NODE || node.nodeType === Node.ELEMENT_NODE); 31679 } 31680 31681 /** 31682 * Returns `true` if `node` can be inserted according to it's `nodeType`. 31683 * @param {Node} node 31684 * @returns {boolean} 31685 */ 31686 function hasInsertableNodeType(node) { 31687 return node && (isElementNode(node) || isTextNode(node) || isDocTypeNode(node) || node.nodeType === Node.DOCUMENT_FRAGMENT_NODE || node.nodeType === Node.COMMENT_NODE || node.nodeType === Node.PROCESSING_INSTRUCTION_NODE); 31688 } 31689 31690 /** 31691 * Returns true if `node` is a DOCTYPE node 31692 * @param {Node} node 31693 * @returns {boolean} 31694 */ 31695 function isDocTypeNode(node) { 31696 return node && node.nodeType === Node.DOCUMENT_TYPE_NODE; 31697 } 31698 31699 /** 31700 * Returns true if the node is an element 31701 * @param {Node} node 31702 * @returns {boolean} 31703 */ 31704 function isElementNode(node) { 31705 return node && node.nodeType === Node.ELEMENT_NODE; 31706 } 31707 /** 31708 * Returns true if `node` is a text node 31709 * @param {Node} node 31710 * @returns {boolean} 31711 */ 31712 function isTextNode(node) { 31713 return node && node.nodeType === Node.TEXT_NODE; 31714 } 31715 31716 /** 31717 * Check if en element node can be inserted before `child`, or at the end if child is falsy, 31718 * according to the presence and position of a doctype node on the same level. 31719 * 31720 * @param {Document} doc The document node 31721 * @param {Node} child the node that would become the nextSibling if the element would be inserted 31722 * @returns {boolean} `true` if an element can be inserted before child 31723 * @private 31724 * https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity 31725 */ 31726 function isElementInsertionPossible(doc, child) { 31727 var parentChildNodes = doc.childNodes || []; 31728 if (find(parentChildNodes, isElementNode) || isDocTypeNode(child)) { 31729 return false;
vendor: 13,855 bytes, lines 31730-32054
31730 } 31731 var docTypeNode = find(parentChildNodes, isDocTypeNode); 31732 return !(child && docTypeNode && parentChildNodes.indexOf(docTypeNode) > parentChildNodes.indexOf(child)); 31733 } 31734 31735 /** 31736 * Check if en element node can be inserted before `child`, or at the end if child is falsy, 31737 * according to the presence and position of a doctype node on the same level. 31738 * 31739 * @param {Node} doc The document node 31740 * @param {Node} child the node that would become the nextSibling if the element would be inserted 31741 * @returns {boolean} `true` if an element can be inserted before child 31742 * @private 31743 * https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity 31744 */ 31745 function isElementReplacementPossible(doc, child) { 31746 var parentChildNodes = doc.childNodes || []; 31747 function hasElementChildThatIsNotChild(node) { 31748 return isElementNode(node) && node !== child; 31749 } 31750 if (find(parentChildNodes, hasElementChildThatIsNotChild)) { 31751 return false; 31752 } 31753 var docTypeNode = find(parentChildNodes, isDocTypeNode); 31754 return !(child && docTypeNode && parentChildNodes.indexOf(docTypeNode) > parentChildNodes.indexOf(child)); 31755 } 31756 31757 /** 31758 * @private 31759 * Steps 1-5 of the checks before inserting and before replacing a child are the same. 31760 * 31761 * @param {Node} parent the parent node to insert `node` into 31762 * @param {Node} node the node to insert 31763 * @param {Node=} child the node that should become the `nextSibling` of `node` 31764 * @returns {Node} 31765 * @throws DOMException for several node combinations that would create a DOM that is not well-formed. 31766 * @throws DOMException if `child` is provided but is not a child of `parent`. 31767 * @see https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity 31768 * @see https://dom.spec.whatwg.org/#concept-node-replace 31769 */ 31770 function assertPreInsertionValidity1to5(parent, node, child) { 31771 // 1. If `parent` is not a Document, DocumentFragment, or Element node, then throw a "HierarchyRequestError" DOMException. 31772 if (!hasValidParentNodeType(parent)) { 31773 throw new DOMException(HIERARCHY_REQUEST_ERR, 'Unexpected parent node type ' + parent.nodeType); 31774 } 31775 // 2. If `node` is a host-including inclusive ancestor of `parent`, then throw a "HierarchyRequestError" DOMException. 31776 // not implemented! 31777 // 3. If `child` is non-null and its parent is not `parent`, then throw a "NotFoundError" DOMException. 31778 if (child && child.parentNode !== parent) { 31779 throw new DOMException(NOT_FOUND_ERR, 'child not in parent'); 31780 } 31781 if ( 31782 // 4. If `node` is not a DocumentFragment, DocumentType, Element, or CharacterData node, then throw a "HierarchyRequestError" DOMException. 31783 !hasInsertableNodeType(node) || 31784 // 5. If either `node` is a Text node and `parent` is a document, 31785 // the sax parser currently adds top level text nodes, this will be fixed in 0.9.0 31786 // || (node.nodeType === Node.TEXT_NODE && parent.nodeType === Node.DOCUMENT_NODE) 31787 // or `node` is a doctype and `parent` is not a document, then throw a "HierarchyRequestError" DOMException. 31788 isDocTypeNode(node) && parent.nodeType !== Node.DOCUMENT_NODE) { 31789 throw new DOMException(HIERARCHY_REQUEST_ERR, 'Unexpected node type ' + node.nodeType + ' for parent node type ' + parent.nodeType); 31790 } 31791 } 31792 31793 /** 31794 * @private 31795 * Step 6 of the checks before inserting and before replacing a child are different. 31796 * 31797 * @param {Document} parent the parent node to insert `node` into 31798 * @param {Node} node the node to insert 31799 * @param {Node | undefined} child the node that should become the `nextSibling` of `node` 31800 * @returns {Node} 31801 * @throws DOMException for several node combinations that would create a DOM that is not well-formed. 31802 * @throws DOMException if `child` is provided but is not a child of `parent`. 31803 * @see https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity 31804 * @see https://dom.spec.whatwg.org/#concept-node-replace 31805 */ 31806 function assertPreInsertionValidityInDocument(parent, node, child) { 31807 var parentChildNodes = parent.childNodes || []; 31808 var nodeChildNodes = node.childNodes || []; 31809 31810 // DocumentFragment 31811 if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { 31812 var nodeChildElements = nodeChildNodes.filter(isElementNode); 31813 // If node has more than one element child or has a Text node child. 31814 if (nodeChildElements.length > 1 || find(nodeChildNodes, isTextNode)) { 31815 throw new DOMException(HIERARCHY_REQUEST_ERR, 'More than one element or text in fragment'); 31816 } 31817 // Otherwise, if `node` has one element child and either `parent` has an element child, 31818 // `child` is a doctype, or `child` is non-null and a doctype is following `child`. 31819 if (nodeChildElements.length === 1 && !isElementInsertionPossible(parent, child)) { 31820 throw new DOMException(HIERARCHY_REQUEST_ERR, 'Element in fragment can not be inserted before doctype'); 31821 } 31822 } 31823 // Element 31824 if (isElementNode(node)) { 31825 // `parent` has an element child, `child` is a doctype, 31826 // or `child` is non-null and a doctype is following `child`. 31827 if (!isElementInsertionPossible(parent, child)) { 31828 throw new DOMException(HIERARCHY_REQUEST_ERR, 'Only one element can be added and only after doctype'); 31829 } 31830 } 31831 // DocumentType 31832 if (isDocTypeNode(node)) { 31833 // `parent` has a doctype child, 31834 if (find(parentChildNodes, isDocTypeNode)) { 31835 throw new DOMException(HIERARCHY_REQUEST_ERR, 'Only one doctype is allowed'); 31836 } 31837 var parentElementChild = find(parentChildNodes, isElementNode); 31838 // `child` is non-null and an element is preceding `child`, 31839 if (child && parentChildNodes.indexOf(parentElementChild) < parentChildNodes.indexOf(child)) { 31840 throw new DOMException(HIERARCHY_REQUEST_ERR, 'Doctype can only be inserted before an element'); 31841 } 31842 // or `child` is null and `parent` has an element child. 31843 if (!child && parentElementChild) { 31844 throw new DOMException(HIERARCHY_REQUEST_ERR, 'Doctype can not be appended since element is present'); 31845 } 31846 } 31847 } 31848 31849 /** 31850 * @private 31851 * Step 6 of the checks before inserting and before replacing a child are different. 31852 * 31853 * @param {Document} parent the parent node to insert `node` into 31854 * @param {Node} node the node to insert 31855 * @param {Node | undefined} child the node that should become the `nextSibling` of `node` 31856 * @returns {Node} 31857 * @throws DOMException for several node combinations that would create a DOM that is not well-formed. 31858 * @throws DOMException if `child` is provided but is not a child of `parent`. 31859 * @see https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity 31860 * @see https://dom.spec.whatwg.org/#concept-node-replace 31861 */ 31862 function assertPreReplacementValidityInDocument(parent, node, child) { 31863 var parentChildNodes = parent.childNodes || []; 31864 var nodeChildNodes = node.childNodes || []; 31865 31866 // DocumentFragment 31867 if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { 31868 var nodeChildElements = nodeChildNodes.filter(isElementNode); 31869 // If `node` has more than one element child or has a Text node child. 31870 if (nodeChildElements.length > 1 || find(nodeChildNodes, isTextNode)) { 31871 throw new DOMException(HIERARCHY_REQUEST_ERR, 'More than one element or text in fragment'); 31872 } 31873 // Otherwise, if `node` has one element child and either `parent` has an element child that is not `child` or a doctype is following `child`. 31874 if (nodeChildElements.length === 1 && !isElementReplacementPossible(parent, child)) { 31875 throw new DOMException(HIERARCHY_REQUEST_ERR, 'Element in fragment can not be inserted before doctype'); 31876 } 31877 } 31878 // Element 31879 if (isElementNode(node)) { 31880 // `parent` has an element child that is not `child` or a doctype is following `child`. 31881 if (!isElementReplacementPossible(parent, child)) { 31882 throw new DOMException(HIERARCHY_REQUEST_ERR, 'Only one element can be added and only after doctype'); 31883 } 31884 } 31885 // DocumentType 31886 if (isDocTypeNode(node)) { 31887 function hasDoctypeChildThatIsNotChild(node) { 31888 return isDocTypeNode(node) && node !== child; 31889 } 31890 31891 // `parent` has a doctype child that is not `child`, 31892 if (find(parentChildNodes, hasDoctypeChildThatIsNotChild)) { 31893 throw new DOMException(HIERARCHY_REQUEST_ERR, 'Only one doctype is allowed'); 31894 } 31895 var parentElementChild = find(parentChildNodes, isElementNode); 31896 // or an element is preceding `child`. 31897 if (child && parentChildNodes.indexOf(parentElementChild) < parentChildNodes.indexOf(child)) { 31898 throw new DOMException(HIERARCHY_REQUEST_ERR, 'Doctype can only be inserted before an element'); 31899 } 31900 } 31901 } 31902 31903 /** 31904 * @private 31905 * @param {Node} parent the parent node to insert `node` into 31906 * @param {Node} node the node to insert 31907 * @param {Node=} child the node that should become the `nextSibling` of `node` 31908 * @returns {Node} 31909 * @throws DOMException for several node combinations that would create a DOM that is not well-formed. 31910 * @throws DOMException if `child` is provided but is not a child of `parent`. 31911 * @see https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity 31912 */ 31913 function _insertBefore(parent, node, child, _inDocumentAssertion) { 31914 // To ensure pre-insertion validity of a node into a parent before a child, run these steps: 31915 assertPreInsertionValidity1to5(parent, node, child); 31916 31917 // If parent is a document, and any of the statements below, switched on the interface node implements, 31918 // are true, then throw a "HierarchyRequestError" DOMException. 31919 if (parent.nodeType === Node.DOCUMENT_NODE) { 31920 (_inDocumentAssertion || assertPreInsertionValidityInDocument)(parent, node, child); 31921 } 31922 var cp = node.parentNode; 31923 if (cp) { 31924 cp.removeChild(node); //remove and update 31925 } 31926 31927 if (node.nodeType === DOCUMENT_FRAGMENT_NODE) { 31928 var newFirst = node.firstChild; 31929 if (newFirst == null) { 31930 return node; 31931 } 31932 var newLast = node.lastChild; 31933 } else { 31934 newFirst = newLast = node; 31935 } 31936 var pre = child ? child.previousSibling : parent.lastChild; 31937 newFirst.previousSibling = pre; 31938 newLast.nextSibling = child; 31939 if (pre) { 31940 pre.nextSibling = newFirst; 31941 } else { 31942 parent.firstChild = newFirst; 31943 } 31944 if (child == null) { 31945 parent.lastChild = newLast; 31946 } else { 31947 child.previousSibling = newLast; 31948 } 31949 do { 31950 newFirst.parentNode = parent; 31951 } while (newFirst !== newLast && (newFirst = newFirst.nextSibling)); 31952 _onUpdateChild(parent.ownerDocument || parent, parent); 31953 //console.log(parent.lastChild.nextSibling == null) 31954 if (node.nodeType == DOCUMENT_FRAGMENT_NODE) { 31955 node.firstChild = node.lastChild = null; 31956 } 31957 return node; 31958 } 31959 31960 /** 31961 * Appends `newChild` to `parentNode`. 31962 * If `newChild` is already connected to a `parentNode` it is first removed from it. 31963 * 31964 * @see https://github.com/xmldom/xmldom/issues/135 31965 * @see https://github.com/xmldom/xmldom/issues/145 31966 * @param {Node} parentNode 31967 * @param {Node} newChild 31968 * @returns {Node} 31969 * @private 31970 */ 31971 function _appendSingleChild(parentNode, newChild) { 31972 if (newChild.parentNode) { 31973 newChild.parentNode.removeChild(newChild); 31974 } 31975 newChild.parentNode = parentNode; 31976 newChild.previousSibling = parentNode.lastChild; 31977 newChild.nextSibling = null; 31978 if (newChild.previousSibling) { 31979 newChild.previousSibling.nextSibling = newChild; 31980 } else { 31981 parentNode.firstChild = newChild; 31982 } 31983 parentNode.lastChild = newChild; 31984 _onUpdateChild(parentNode.ownerDocument, parentNode, newChild); 31985 return newChild; 31986 } 31987 Document.prototype = { 31988 //implementation : null, 31989 nodeName: '#document', 31990 nodeType: DOCUMENT_NODE, 31991 /** 31992 * The DocumentType node of the document. 31993 * 31994 * @readonly 31995 * @type DocumentType 31996 */ 31997 doctype: null, 31998 documentElement: null, 31999 _inc: 1, 32000 insertBefore: function (newChild, refChild) { 32001 //raises 32002 if (newChild.nodeType == DOCUMENT_FRAGMENT_NODE) { 32003 var child = newChild.firstChild; 32004 while (child) { 32005 var next = child.nextSibling; 32006 this.insertBefore(child, refChild); 32007 child = next; 32008 } 32009 return newChild; 32010 } 32011 _insertBefore(this, newChild, refChild); 32012 newChild.ownerDocument = this; 32013 if (this.documentElement === null && newChild.nodeType === ELEMENT_NODE) { 32014 this.documentElement = newChild; 32015 } 32016 return newChild; 32017 }, 32018 removeChild: function (oldChild) { 32019 if (this.documentElement == oldChild) { 32020 this.documentElement = null; 32021 } 32022 return _removeChild(this, oldChild); 32023 }, 32024 replaceChild: function (newChild, oldChild) { 32025 //raises 32026 _insertBefore(this, newChild, oldChild, assertPreReplacementValidityInDocument); 32027 newChild.ownerDocument = this; 32028 if (oldChild) { 32029 this.removeChild(oldChild); 32030 } 32031 if (isElementNode(newChild)) { 32032 this.documentElement = newChild; 32033 } 32034 }, 32035 // Introduced in DOM Level 2: 32036 importNode: function (importedNode, deep) { 32037 return importNode(this, importedNode, deep); 32038 }, 32039 // Introduced in DOM Level 2: 32040 getElementById: function (id) { 32041 var rtv = null; 32042 _visitNode(this.documentElement, function (node) { 32043 if (node.nodeType == ELEMENT_NODE) { 32044 if (node.getAttribute('id') == id) { 32045 rtv = node; 32046 return true; 32047 } 32048 } 32049 }); 32050 return rtv; 32051 }, 32052 /** 32053 * The `getElementsByClassName` method of `Document` interface returns an array-like object 32054 * of all child elements which have **all** of the given class
vendor: 8,940 bytes, lines 32054-32306
32054name(s). 32055 * 32056 * Returns an empty list if `classeNames` is an empty string or only contains HTML white space characters. 32057 * 32058 * 32059 * Warning: This is a live LiveNodeList. 32060 * Changes in the DOM will reflect in the array as the changes occur. 32061 * If an element selected by this array no longer qualifies for the selector, 32062 * it will automatically be removed. Be aware of this for iteration purposes. 32063 * 32064 * @param {string} classNames is a string representing the class name(s) to match; multiple class names are separated by (ASCII-)whitespace 32065 * 32066 * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementsByClassName 32067 * @see https://dom.spec.whatwg.org/#concept-getelementsbyclassname 32068 */ 32069 getElementsByClassName: function (classNames) { 32070 var classNamesSet = toOrderedSet(classNames); 32071 return new LiveNodeList(this, function (base) { 32072 var ls = []; 32073 if (classNamesSet.length > 0) { 32074 _visitNode(base.documentElement, function (node) { 32075 if (node !== base && node.nodeType === ELEMENT_NODE) { 32076 var nodeClassNames = node.getAttribute('class'); 32077 // can be null if the attribute does not exist 32078 if (nodeClassNames) { 32079 // before splitting and iterating just compare them for the most common case 32080 var matches = classNames === nodeClassNames; 32081 if (!matches) { 32082 var nodeClassNamesSet = toOrderedSet(nodeClassNames); 32083 matches = classNamesSet.every(arrayIncludes(nodeClassNamesSet)); 32084 } 32085 if (matches) { 32086 ls.push(node); 32087 } 32088 } 32089 } 32090 }); 32091 } 32092 return ls; 32093 }); 32094 }, 32095 //document factory method: 32096 createElement: function (tagName) { 32097 var node = new Element(); 32098 node.ownerDocument = this; 32099 node.nodeName = tagName; 32100 node.tagName = tagName; 32101 node.localName = tagName; 32102 node.childNodes = new NodeList(); 32103 var attrs = node.attributes = new NamedNodeMap(); 32104 attrs._ownerElement = node; 32105 return node; 32106 }, 32107 createDocumentFragment: function () { 32108 var node = new DocumentFragment(); 32109 node.ownerDocument = this; 32110 node.childNodes = new NodeList(); 32111 return node; 32112 }, 32113 createTextNode: function (data) { 32114 var node = new Text(); 32115 node.ownerDocument = this; 32116 node.appendData(data); 32117 return node; 32118 }, 32119 createComment: function (data) { 32120 var node = new Comment(); 32121 node.ownerDocument = this; 32122 node.appendData(data); 32123 return node; 32124 }, 32125 createCDATASection: function (data) { 32126 var node = new CDATASection(); 32127 node.ownerDocument = this; 32128 node.appendData(data); 32129 return node; 32130 }, 32131 createProcessingInstruction: function (target, data) { 32132 var node = new ProcessingInstruction(); 32133 node.ownerDocument = this; 32134 node.tagName = node.nodeName = node.target = target; 32135 node.nodeValue = node.data = data; 32136 return node; 32137 }, 32138 createAttribute: function (name) { 32139 var node = new Attr(); 32140 node.ownerDocument = this; 32141 node.name = name; 32142 node.nodeName = name; 32143 node.localName = name; 32144 node.specified = true; 32145 return node; 32146 }, 32147 createEntityReference: function (name) { 32148 var node = new EntityReference(); 32149 node.ownerDocument = this; 32150 node.nodeName = name; 32151 return node; 32152 }, 32153 // Introduced in DOM Level 2: 32154 createElementNS: function (namespaceURI, qualifiedName) { 32155 var node = new Element(); 32156 var pl = qualifiedName.split(':'); 32157 var attrs = node.attributes = new NamedNodeMap(); 32158 node.childNodes = new NodeList(); 32159 node.ownerDocument = this; 32160 node.nodeName = qualifiedName; 32161 node.tagName = qualifiedName; 32162 node.namespaceURI = namespaceURI; 32163 if (pl.length == 2) { 32164 node.prefix = pl[0]; 32165 node.localName = pl[1]; 32166 } else { 32167 //el.prefix = null; 32168 node.localName = qualifiedName; 32169 } 32170 attrs._ownerElement = node; 32171 return node; 32172 }, 32173 // Introduced in DOM Level 2: 32174 createAttributeNS: function (namespaceURI, qualifiedName) { 32175 var node = new Attr(); 32176 var pl = qualifiedName.split(':'); 32177 node.ownerDocument = this; 32178 node.nodeName = qualifiedName; 32179 node.name = qualifiedName; 32180 node.namespaceURI = namespaceURI; 32181 node.specified = true; 32182 if (pl.length == 2) { 32183 node.prefix = pl[0]; 32184 node.localName = pl[1]; 32185 } else { 32186 //el.prefix = null; 32187 node.localName = qualifiedName; 32188 } 32189 return node; 32190 } 32191 }; 32192 _extends(Document, Node); 32193 function Element() { 32194 this._nsMap = {}; 32195 } 32196 Element.prototype = { 32197 nodeType: ELEMENT_NODE, 32198 hasAttribute: function (name) { 32199 return this.getAttributeNode(name) != null; 32200 }, 32201 getAttribute: function (name) { 32202 var attr = this.getAttributeNode(name); 32203 return attr && attr.value || ''; 32204 }, 32205 getAttributeNode: function (name) { 32206 return this.attributes.getNamedItem(name); 32207 }, 32208 setAttribute: function (name, value) { 32209 var attr = this.ownerDocument.createAttribute(name); 32210 attr.value = attr.nodeValue = "" + value; 32211 this.setAttributeNode(attr); 32212 }, 32213 removeAttribute: function (name) { 32214 var attr = this.getAttributeNode(name); 32215 attr && this.removeAttributeNode(attr); 32216 }, 32217 //four real opeartion method 32218 appendChild: function (newChild) { 32219 if (newChild.nodeType === DOCUMENT_FRAGMENT_NODE) { 32220 return this.insertBefore(newChild, null); 32221 } else { 32222 return _appendSingleChild(this, newChild); 32223 } 32224 }, 32225 setAttributeNode: function (newAttr) { 32226 return this.attributes.setNamedItem(newAttr); 32227 }, 32228 setAttributeNodeNS: function (newAttr) { 32229 return this.attributes.setNamedItemNS(newAttr); 32230 }, 32231 removeAttributeNode: function (oldAttr) { 32232 //console.log(this == oldAttr.ownerElement) 32233 return this.attributes.removeNamedItem(oldAttr.nodeName); 32234 }, 32235 //get real attribute name,and remove it by removeAttributeNode 32236 removeAttributeNS: function (namespaceURI, localName) { 32237 var old = this.getAttributeNodeNS(namespaceURI, localName); 32238 old && this.removeAttributeNode(old); 32239 }, 32240 hasAttributeNS: function (namespaceURI, localName) { 32241 return this.getAttributeNodeNS(namespaceURI, localName) != null; 32242 }, 32243 getAttributeNS: function (namespaceURI, localName) { 32244 var attr = this.getAttributeNodeNS(namespaceURI, localName); 32245 return attr && attr.value || ''; 32246 }, 32247 setAttributeNS: function (namespaceURI, qualifiedName, value) { 32248 var attr = this.ownerDocument.createAttributeNS(namespaceURI, qualifiedName); 32249 attr.value = attr.nodeValue = "" + value; 32250 this.setAttributeNode(attr); 32251 }, 32252 getAttributeNodeNS: function (namespaceURI, localName) { 32253 return this.attributes.getNamedItemNS(namespaceURI, localName); 32254 }, 32255 getElementsByTagName: function (tagName) { 32256 return new LiveNodeList(this, function (base) { 32257 var ls = []; 32258 _visitNode(base, function (node) { 32259 if (node !== base && node.nodeType == ELEMENT_NODE && (tagName === '*' || node.tagName == tagName)) { 32260 ls.push(node); 32261 } 32262 }); 32263 return ls; 32264 }); 32265 }, 32266 getElementsByTagNameNS: function (namespaceURI, localName) { 32267 return new LiveNodeList(this, function (base) { 32268 var ls = []; 32269 _visitNode(base, function (node) { 32270 if (node !== base && node.nodeType === ELEMENT_NODE && (namespaceURI === '*' || node.namespaceURI === namespaceURI) && (localName === '*' || node.localName == localName)) { 32271 ls.push(node); 32272 } 32273 }); 32274 return ls; 32275 }); 32276 } 32277 }; 32278 Document.prototype.getElementsByTagName = Element.prototype.getElementsByTagName; 32279 Document.prototype.getElementsByTagNameNS = Element.prototype.getElementsByTagNameNS; 32280 _extends(Element, Node); 32281 function Attr() {} 32282 Attr.prototype.nodeType = ATTRIBUTE_NODE; 32283 _extends(Attr, Node); 32284 function CharacterData() {} 32285 CharacterData.prototype = { 32286 data: '', 32287 substringData: function (offset, count) { 32288 return this.data.substring(offset, offset + count); 32289 }, 32290 appendData: function (text) { 32291 text = this.data + text; 32292 this.nodeValue = this.data = text; 32293 this.length = text.length; 32294 }, 32295 insertData: function (offset, text) { 32296 this.replaceData(offset, 0, text); 32297 }, 32298 appendChild: function (newChild) { 32299 throw new Error(ExceptionMessage[HIERARCHY_REQUEST_ERR]); 32300 }, 32301 deleteData: function (offset, count) { 32302 this.replaceData(offset, count, ""); 32303 }, 32304 replaceData: function (offset, count, text) { 32305 var start = this.data.substring(0, offset); 32306 var end = this.data.substring(offset + count);
vendor: 4,539 bytes, lines 32307-32428
32307 text = start + text + end; 32308 this.nodeValue = this.data = text; 32309 this.length = text.length; 32310 } 32311 }; 32312 _extends(CharacterData, Node); 32313 function Text() {} 32314 Text.prototype = { 32315 nodeName: "#text", 32316 nodeType: TEXT_NODE, 32317 splitText: function (offset) { 32318 var text = this.data; 32319 var newText = text.substring(offset); 32320 text = text.substring(0, offset); 32321 this.data = this.nodeValue = text; 32322 this.length = text.length; 32323 var newNode = this.ownerDocument.createTextNode(newText); 32324 if (this.parentNode) { 32325 this.parentNode.insertBefore(newNode, this.nextSibling); 32326 } 32327 return newNode; 32328 } 32329 }; 32330 _extends(Text, CharacterData); 32331 function Comment() {} 32332 Comment.prototype = { 32333 nodeName: "#comment", 32334 nodeType: COMMENT_NODE 32335 }; 32336 _extends(Comment, CharacterData); 32337 function CDATASection() {} 32338 CDATASection.prototype = { 32339 nodeName: "#cdata-section", 32340 nodeType: CDATA_SECTION_NODE 32341 }; 32342 _extends(CDATASection, CharacterData); 32343 function DocumentType() {} 32344 DocumentType.prototype.nodeType = DOCUMENT_TYPE_NODE; 32345 _extends(DocumentType, Node); 32346 function Notation() {} 32347 Notation.prototype.nodeType = NOTATION_NODE; 32348 _extends(Notation, Node); 32349 function Entity() {} 32350 Entity.prototype.nodeType = ENTITY_NODE; 32351 _extends(Entity, Node); 32352 function EntityReference() {} 32353 EntityReference.prototype.nodeType = ENTITY_REFERENCE_NODE; 32354 _extends(EntityReference, Node); 32355 function DocumentFragment() {} 32356 DocumentFragment.prototype.nodeName = "#document-fragment"; 32357 DocumentFragment.prototype.nodeType = DOCUMENT_FRAGMENT_NODE; 32358 _extends(DocumentFragment, Node); 32359 function ProcessingInstruction() {} 32360 ProcessingInstruction.prototype.nodeType = PROCESSING_INSTRUCTION_NODE; 32361 _extends(ProcessingInstruction, Node); 32362 function XMLSerializer() {} 32363 XMLSerializer.prototype.serializeToString = function (node, isHtml, nodeFilter) { 32364 return nodeSerializeToString.call(node, isHtml, nodeFilter); 32365 }; 32366 Node.prototype.toString = nodeSerializeToString; 32367 function nodeSerializeToString(isHtml, nodeFilter) { 32368 var buf = []; 32369 var refNode = this.nodeType == 9 && this.documentElement || this; 32370 var prefix = refNode.prefix; 32371 var uri = refNode.namespaceURI; 32372 if (uri && prefix == null) { 32373 //console.log(prefix) 32374 var prefix = refNode.lookupPrefix(uri); 32375 if (prefix == null) { 32376 //isHTML = true; 32377 var visibleNamespaces = [{ 32378 namespace: uri, 32379 prefix: null 32380 } 32381 //{namespace:uri,prefix:''} 32382 ]; 32383 } 32384 } 32385 32386 serializeToString(this, buf, isHtml, nodeFilter, visibleNamespaces); 32387 //console.log('###',this.nodeType,uri,prefix,buf.join('')) 32388 return buf.join(''); 32389 } 32390 function needNamespaceDefine(node, isHTML, visibleNamespaces) { 32391 var prefix = node.prefix || ''; 32392 var uri = node.namespaceURI; 32393 // According to [Namespaces in XML 1.0](https://www.w3.org/TR/REC-xml-names/#ns-using) , 32394 // and more specifically https://www.w3.org/TR/REC-xml-names/#nsc-NoPrefixUndecl : 32395 // > In a namespace declaration for a prefix [...], the attribute value MUST NOT be empty. 32396 // in a similar manner [Namespaces in XML 1.1](https://www.w3.org/TR/xml-names11/#ns-using) 32397 // and more specifically https://www.w3.org/TR/xml-names11/#nsc-NSDeclared : 32398 // > [...] Furthermore, the attribute value [...] must not be an empty string. 32399 // so serializing empty namespace value like xmlns:ds="" would produce an invalid XML document. 32400 if (!uri) { 32401 return false; 32402 } 32403 if (prefix === "xml" && uri === NAMESPACE$2.XML || uri === NAMESPACE$2.XMLNS) { 32404 return false; 32405 } 32406 var i = visibleNamespaces.length; 32407 while (i--) { 32408 var ns = visibleNamespaces[i]; 32409 // get namespace prefix 32410 if (ns.prefix === prefix) { 32411 return ns.namespace !== uri; 32412 } 32413 } 32414 return true; 32415 } 32416 /** 32417 * Well-formed constraint: No < in Attribute Values 32418 * > The replacement text of any entity referred to directly or indirectly 32419 * > in an attribute value must not contain a <. 32420 * @see https://www.w3.org/TR/xml11/#CleanAttrVals 32421 * @see https://www.w3.org/TR/xml11/#NT-AttValue 32422 * 32423 * Literal whitespace other than space that appear in attribute values 32424 * are serialized as their entity references, so they will be preserved. 32425 * (In contrast to whitespace literals in the input which are normalized to spaces) 32426 * @see https://www.w3.org/TR/xml11/#AVNormalize 32427 * @see https://w3c.github.io/DOM-Parsing/#serializing-an-element-s-attributes 32428 */
vendor: 5,249 bytes, lines 32429-32568
32429 function addSerializedAttribute(buf, qualifiedName, value) { 32430 buf.push(' ', qualifiedName, '="', value.replace(/[<>&"\t\n\r]/g, _xmlEncoder), '"'); 32431 } 32432 function serializeToString(node, buf, isHTML, nodeFilter, visibleNamespaces) { 32433 if (!visibleNamespaces) { 32434 visibleNamespaces = []; 32435 } 32436 if (nodeFilter) { 32437 node = nodeFilter(node); 32438 if (node) { 32439 if (typeof node == 'string') { 32440 buf.push(node); 32441 return; 32442 } 32443 } else { 32444 return; 32445 } 32446 //buf.sort.apply(attrs, attributeSorter); 32447 } 32448 32449 switch (node.nodeType) { 32450 case ELEMENT_NODE: 32451 var attrs = node.attributes; 32452 var len = attrs.length; 32453 var child = node.firstChild; 32454 var nodeName = node.tagName; 32455 isHTML = NAMESPACE$2.isHTML(node.namespaceURI) || isHTML; 32456 var prefixedNodeName = nodeName; 32457 if (!isHTML && !node.prefix && node.namespaceURI) { 32458 var defaultNS; 32459 // lookup current default ns from `xmlns` attribute 32460 for (var ai = 0; ai < attrs.length; ai++) { 32461 if (attrs.item(ai).name === 'xmlns') { 32462 defaultNS = attrs.item(ai).value; 32463 break; 32464 } 32465 } 32466 if (!defaultNS) { 32467 // lookup current default ns in visibleNamespaces 32468 for (var nsi = visibleNamespaces.length - 1; nsi >= 0; nsi--) { 32469 var namespace = visibleNamespaces[nsi]; 32470 if (namespace.prefix === '' && namespace.namespace === node.namespaceURI) { 32471 defaultNS = namespace.namespace; 32472 break; 32473 } 32474 } 32475 } 32476 if (defaultNS !== node.namespaceURI) { 32477 for (var nsi = visibleNamespaces.length - 1; nsi >= 0; nsi--) { 32478 var namespace = visibleNamespaces[nsi]; 32479 if (namespace.namespace === node.namespaceURI) { 32480 if (namespace.prefix) { 32481 prefixedNodeName = namespace.prefix + ':' + nodeName; 32482 } 32483 break; 32484 } 32485 } 32486 } 32487 } 32488 buf.push('<', prefixedNodeName); 32489 for (var i = 0; i < len; i++) { 32490 // add namespaces for attributes 32491 var attr = attrs.item(i); 32492 if (attr.prefix == 'xmlns') { 32493 visibleNamespaces.push({ 32494 prefix: attr.localName, 32495 namespace: attr.value 32496 }); 32497 } else if (attr.nodeName == 'xmlns') { 32498 visibleNamespaces.push({ 32499 prefix: '', 32500 namespace: attr.value 32501 }); 32502 } 32503 } 32504 for (var i = 0; i < len; i++) { 32505 var attr = attrs.item(i); 32506 if (needNamespaceDefine(attr, isHTML, visibleNamespaces)) { 32507 var prefix = attr.prefix || ''; 32508 var uri = attr.namespaceURI; 32509 addSerializedAttribute(buf, prefix ? 'xmlns:' + prefix : "xmlns", uri); 32510 visibleNamespaces.push({ 32511 prefix: prefix, 32512 namespace: uri 32513 }); 32514 } 32515 serializeToString(attr, buf, isHTML, nodeFilter, visibleNamespaces); 32516 } 32517 32518 // add namespace for current node 32519 if (nodeName === prefixedNodeName && needNamespaceDefine(node, isHTML, visibleNamespaces)) { 32520 var prefix = node.prefix || ''; 32521 var uri = node.namespaceURI; 32522 addSerializedAttribute(buf, prefix ? 'xmlns:' + prefix : "xmlns", uri); 32523 visibleNamespaces.push({ 32524 prefix: prefix, 32525 namespace: uri 32526 }); 32527 } 32528 if (child || isHTML && !/^(?:meta|link|img|br|hr|input)$/i.test(nodeName)) { 32529 buf.push('>'); 32530 //if is cdata child node 32531 if (isHTML && /^script$/i.test(nodeName)) { 32532 while (child) { 32533 if (child.data) { 32534 buf.push(child.data); 32535 } else { 32536 serializeToString(child, buf, isHTML, nodeFilter, visibleNamespaces.slice()); 32537 } 32538 child = child.nextSibling; 32539 } 32540 } else { 32541 while (child) { 32542 serializeToString(child, buf, isHTML, nodeFilter, visibleNamespaces.slice()); 32543 child = child.nextSibling; 32544 } 32545 } 32546 buf.push('</', prefixedNodeName, '>'); 32547 } else { 32548 buf.push('/>'); 32549 } 32550 // remove added visible namespaces 32551 //visibleNamespaces.length = startVisibleNamespaces; 32552 return; 32553 case DOCUMENT_NODE: 32554 case DOCUMENT_FRAGMENT_NODE: 32555 var child = node.firstChild; 32556 while (child) { 32557 serializeToString(child, buf, isHTML, nodeFilter, visibleNamespaces.slice()); 32558 child = child.nextSibling; 32559 } 32560 return; 32561 case ATTRIBUTE_NODE: 32562 return addSerializedAttribute(buf, node.name, node.value); 32563 case TEXT_NODE: 32564 /** 32565 * The ampersand character (&) and the left angle bracket (<) must not appear in their literal form, 32566 * except when used as markup delimiters, or within a comment, a processing instruction, or a CDATA section. 32567 * If they are needed elsewhere, they must be escaped using either numeric character references or the strings 32568 * `&` and `<` respectively.
vendor: 6,368 bytes, lines 32569-32778
32569 * The right angle bracket (>) may be represented using the string " > ", and must, for compatibility, 32570 * be escaped using either `>` or a character reference when it appears in the string `]]>` in content, 32571 * when that string is not marking the end of a CDATA section. 32572 * 32573 * In the content of elements, character data is any string of characters 32574 * which does not contain the start-delimiter of any markup 32575 * and does not include the CDATA-section-close delimiter, `]]>`. 32576 * 32577 * @see https://www.w3.org/TR/xml/#NT-CharData 32578 * @see https://w3c.github.io/DOM-Parsing/#xml-serializing-a-text-node 32579 */ 32580 return buf.push(node.data.replace(/[<&>]/g, _xmlEncoder)); 32581 case CDATA_SECTION_NODE: 32582 return buf.push('<![CDATA[', node.data, ']]>'); 32583 case COMMENT_NODE: 32584 return buf.push("<!--", node.data, "-->"); 32585 case DOCUMENT_TYPE_NODE: 32586 var pubid = node.publicId; 32587 var sysid = node.systemId; 32588 buf.push('<!DOCTYPE ', node.name); 32589 if (pubid) { 32590 buf.push(' PUBLIC ', pubid); 32591 if (sysid && sysid != '.') { 32592 buf.push(' ', sysid); 32593 } 32594 buf.push('>'); 32595 } else if (sysid && sysid != '.') { 32596 buf.push(' SYSTEM ', sysid, '>'); 32597 } else { 32598 var sub = node.internalSubset; 32599 if (sub) { 32600 buf.push(" [", sub, "]"); 32601 } 32602 buf.push(">"); 32603 } 32604 return; 32605 case PROCESSING_INSTRUCTION_NODE: 32606 return buf.push("<?", node.target, " ", node.data, "?>"); 32607 case ENTITY_REFERENCE_NODE: 32608 return buf.push('&', node.nodeName, ';'); 32609 //case ENTITY_NODE: 32610 //case NOTATION_NODE: 32611 default: 32612 buf.push('??', node.nodeName); 32613 } 32614 } 32615 function importNode(doc, node, deep) { 32616 var node2; 32617 switch (node.nodeType) { 32618 case ELEMENT_NODE: 32619 node2 = node.cloneNode(false); 32620 node2.ownerDocument = doc; 32621 //var attrs = node2.attributes; 32622 //var len = attrs.length; 32623 //for(var i=0;i<len;i++){ 32624 //node2.setAttributeNodeNS(importNode(doc,attrs.item(i),deep)); 32625 //} 32626 case DOCUMENT_FRAGMENT_NODE: 32627 break; 32628 case ATTRIBUTE_NODE: 32629 deep = true; 32630 break; 32631 //case ENTITY_REFERENCE_NODE: 32632 //case PROCESSING_INSTRUCTION_NODE: 32633 ////case TEXT_NODE: 32634 //case CDATA_SECTION_NODE: 32635 //case COMMENT_NODE: 32636 // deep = false; 32637 // break; 32638 //case DOCUMENT_NODE: 32639 //case DOCUMENT_TYPE_NODE: 32640 //cannot be imported. 32641 //case ENTITY_NODE: 32642 //case NOTATION_NODEï¼ 32643 //can not hit in level3 32644 //default:throw e; 32645 } 32646 32647 if (!node2) { 32648 node2 = node.cloneNode(false); //false 32649 } 32650 32651 node2.ownerDocument = doc; 32652 node2.parentNode = null; 32653 if (deep) { 32654 var child = node.firstChild; 32655 while (child) { 32656 node2.appendChild(importNode(doc, child, deep)); 32657 child = child.nextSibling; 32658 } 32659 } 32660 return node2; 32661 } 32662 // 32663 //var _relationMap = {firstChild:1,lastChild:1,previousSibling:1,nextSibling:1, 32664 // attributes:1,childNodes:1,parentNode:1,documentElement:1,doctype,}; 32665 function cloneNode(doc, node, deep) { 32666 var node2 = new node.constructor(); 32667 for (var n in node) { 32668 if (Object.prototype.hasOwnProperty.call(node, n)) { 32669 var v = node[n]; 32670 if (typeof v != "object") { 32671 if (v != node2[n]) { 32672 node2[n] = v; 32673 } 32674 } 32675 } 32676 } 32677 if (node.childNodes) { 32678 node2.childNodes = new NodeList(); 32679 } 32680 node2.ownerDocument = doc; 32681 switch (node2.nodeType) { 32682 case ELEMENT_NODE: 32683 var attrs = node.attributes; 32684 var attrs2 = node2.attributes = new NamedNodeMap(); 32685 var len = attrs.length; 32686 attrs2._ownerElement = node2; 32687 for (var i = 0; i < len; i++) { 32688 node2.setAttributeNode(cloneNode(doc, attrs.item(i), true)); 32689 } 32690 break; 32691 case ATTRIBUTE_NODE: 32692 deep = true; 32693 } 32694 if (deep) { 32695 var child = node.firstChild; 32696 while (child) { 32697 node2.appendChild(cloneNode(doc, child, deep)); 32698 child = child.nextSibling; 32699 } 32700 } 32701 return node2; 32702 } 32703 function __set__(object, key, value) { 32704 object[key] = value; 32705 } 32706 //do dynamic 32707 try { 32708 if (Object.defineProperty) { 32709 Object.defineProperty(LiveNodeList.prototype, 'length', { 32710 get: function () { 32711 _updateLiveList(this); 32712 return this.$$length; 32713 } 32714 }); 32715 Object.defineProperty(Node.prototype, 'textContent', { 32716 get: function () { 32717 return getTextContent(this); 32718 }, 32719 set: function (data) { 32720 switch (this.nodeType) { 32721 case ELEMENT_NODE: 32722 case DOCUMENT_FRAGMENT_NODE: 32723 while (this.firstChild) { 32724 this.removeChild(this.firstChild); 32725 } 32726 if (data || String(data)) { 32727 this.appendChild(this.ownerDocument.createTextNode(data)); 32728 } 32729 break; 32730 default: 32731 this.data = data; 32732 this.value = data; 32733 this.nodeValue = data; 32734 } 32735 } 32736 }); 32737 function getTextContent(node) { 32738 switch (node.nodeType) { 32739 case ELEMENT_NODE: 32740 case DOCUMENT_FRAGMENT_NODE: 32741 var buf = []; 32742 node = node.firstChild; 32743 while (node) { 32744 if (node.nodeType !== 7 && node.nodeType !== 8) { 32745 buf.push(getTextContent(node)); 32746 } 32747 node = node.nextSibling; 32748 } 32749 return buf.join(''); 32750 default: 32751 return node.nodeValue; 32752 } 32753 } 32754 __set__ = function (object, key, value) { 32755 //console.log(value) 32756 object['$$' + key] = value; 32757 }; 32758 } 32759 } catch (e) {//ie8 32760 } 32761 32762 //if(typeof require == 'function'){ 32763 var DocumentType_1 = DocumentType; 32764 var DOMException_1 = DOMException; 32765 var DOMImplementation_1 = DOMImplementation$1; 32766 var Element_1 = Element; 32767 var Node_1 = Node; 32768 var NodeList_1 = NodeList; 32769 var XMLSerializer_1 = XMLSerializer; 32770 //} 32771 32772 var dom = { 32773 DocumentType: DocumentType_1, 32774 DOMException: DOMException_1, 32775 DOMImplementation: DOMImplementation_1, 32776 Element: Element_1, 32777 Node: Node_1, 32778 NodeList: NodeList
vendor: 55,445 bytes, lines 32778-34953
32778_1, 32779 XMLSerializer: XMLSerializer_1 32780 }; 32781 32782 var entities = createCommonjsModule(function (module, exports) { 32783 32784 var freeze = conventions.freeze; 32785 32786 /** 32787 * The entities that are predefined in every XML document. 32788 * 32789 * @see https://www.w3.org/TR/2006/REC-xml11-20060816/#sec-predefined-ent W3C XML 1.1 32790 * @see https://www.w3.org/TR/2008/REC-xml-20081126/#sec-predefined-ent W3C XML 1.0 32791 * @see https://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references#Predefined_entities_in_XML Wikipedia 32792 */ 32793 exports.XML_ENTITIES = freeze({ 32794 amp: '&', 32795 apos: "'", 32796 gt: '>', 32797 lt: '<', 32798 quot: '"' 32799 }); 32800 32801 /** 32802 * A map of all entities that are detected in an HTML document. 32803 * They contain all entries from `XML_ENTITIES`. 32804 * 32805 * @see XML_ENTITIES 32806 * @see DOMParser.parseFromString 32807 * @see DOMImplementation.prototype.createHTMLDocument 32808 * @see https://html.spec.whatwg.org/#named-character-references WHATWG HTML(5) Spec 32809 * @see https://html.spec.whatwg.org/entities.json JSON 32810 * @see https://www.w3.org/TR/xml-entity-names/ W3C XML Entity Names 32811 * @see https://www.w3.org/TR/html4/sgml/entities.html W3C HTML4/SGML 32812 * @see https://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references#Character_entity_references_in_HTML Wikipedia (HTML) 32813 * @see https://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references#Entities_representing_special_characters_in_XHTML Wikpedia (XHTML) 32814 */ 32815 exports.HTML_ENTITIES = freeze({ 32816 Aacute: '\u00C1', 32817 aacute: '\u00E1', 32818 Abreve: '\u0102', 32819 abreve: '\u0103', 32820 ac: '\u223E', 32821 acd: '\u223F', 32822 acE: '\u223E\u0333', 32823 Acirc: '\u00C2', 32824 acirc: '\u00E2', 32825 acute: '\u00B4', 32826 Acy: '\u0410', 32827 acy: '\u0430', 32828 AElig: '\u00C6', 32829 aelig: '\u00E6', 32830 af: '\u2061', 32831 Afr: '\uD835\uDD04', 32832 afr: '\uD835\uDD1E', 32833 Agrave: '\u00C0', 32834 agrave: '\u00E0', 32835 alefsym: '\u2135', 32836 aleph: '\u2135', 32837 Alpha: '\u0391', 32838 alpha: '\u03B1', 32839 Amacr: '\u0100', 32840 amacr: '\u0101', 32841 amalg: '\u2A3F', 32842 AMP: '\u0026', 32843 amp: '\u0026', 32844 And: '\u2A53', 32845 and: '\u2227', 32846 andand: '\u2A55', 32847 andd: '\u2A5C', 32848 andslope: '\u2A58', 32849 andv: '\u2A5A', 32850 ang: '\u2220', 32851 ange: '\u29A4', 32852 angle: '\u2220', 32853 angmsd: '\u2221', 32854 angmsdaa: '\u29A8', 32855 angmsdab: '\u29A9', 32856 angmsdac: '\u29AA', 32857 angmsdad: '\u29AB', 32858 angmsdae: '\u29AC', 32859 angmsdaf: '\u29AD', 32860 angmsdag: '\u29AE', 32861 angmsdah: '\u29AF', 32862 angrt: '\u221F', 32863 angrtvb: '\u22BE', 32864 angrtvbd: '\u299D', 32865 angsph: '\u2222', 32866 angst: '\u00C5', 32867 angzarr: '\u237C', 32868 Aogon: '\u0104', 32869 aogon: '\u0105', 32870 Aopf: '\uD835\uDD38', 32871 aopf: '\uD835\uDD52', 32872 ap: '\u2248', 32873 apacir: '\u2A6F', 32874 apE: '\u2A70', 32875 ape: '\u224A', 32876 apid: '\u224B', 32877 apos: '\u0027', 32878 ApplyFunction: '\u2061', 32879 approx: '\u2248', 32880 approxeq: '\u224A', 32881 Aring: '\u00C5', 32882 aring: '\u00E5', 32883 Ascr: '\uD835\uDC9C', 32884 ascr: '\uD835\uDCB6', 32885 Assign: '\u2254', 32886 ast: '\u002A', 32887 asymp: '\u2248', 32888 asympeq: '\u224D', 32889 Atilde: '\u00C3', 32890 atilde: '\u00E3', 32891 Auml: '\u00C4', 32892 auml: '\u00E4', 32893 awconint: '\u2233', 32894 awint: '\u2A11', 32895 backcong: '\u224C', 32896 backepsilon: '\u03F6', 32897 backprime: '\u2035', 32898 backsim: '\u223D', 32899 backsimeq: '\u22CD', 32900 Backslash: '\u2216', 32901 Barv: '\u2AE7', 32902 barvee: '\u22BD', 32903 Barwed: '\u2306', 32904 barwed: '\u2305', 32905 barwedge: '\u2305', 32906 bbrk: '\u23B5', 32907 bbrktbrk: '\u23B6', 32908 bcong: '\u224C', 32909 Bcy: '\u0411', 32910 bcy: '\u0431', 32911 bdquo: '\u201E', 32912 becaus: '\u2235', 32913 Because: '\u2235', 32914 because: '\u2235', 32915 bemptyv: '\u29B0', 32916 bepsi: '\u03F6', 32917 bernou: '\u212C', 32918 Bernoullis: '\u212C', 32919 Beta: '\u0392', 32920 beta: '\u03B2', 32921 beth: '\u2136', 32922 between: '\u226C', 32923 Bfr: '\uD835\uDD05', 32924 bfr: '\uD835\uDD1F', 32925 bigcap: '\u22C2', 32926 bigcirc: '\u25EF', 32927 bigcup: '\u22C3', 32928 bigodot: '\u2A00', 32929 bigoplus: '\u2A01', 32930 bigotimes: '\u2A02', 32931 bigsqcup: '\u2A06', 32932 bigstar: '\u2605', 32933 bigtriangledown: '\u25BD', 32934 bigtriangleup: '\u25B3', 32935 biguplus: '\u2A04', 32936 bigvee: '\u22C1', 32937 bigwedge: '\u22C0', 32938 bkarow: '\u290D', 32939 blacklozenge: '\u29EB', 32940 blacksquare: '\u25AA', 32941 blacktriangle: '\u25B4', 32942 blacktriangledown: '\u25BE', 32943 blacktriangleleft: '\u25C2', 32944 blacktriangleright: '\u25B8', 32945 blank: '\u2423', 32946 blk12: '\u2592', 32947 blk14: '\u2591', 32948 blk34: '\u2593', 32949 block: '\u2588', 32950 bne: '\u003D\u20E5', 32951 bnequiv: '\u2261\u20E5', 32952 bNot: '\u2AED', 32953 bnot: '\u2310', 32954 Bopf: '\uD835\uDD39', 32955 bopf: '\uD835\uDD53', 32956 bot: '\u22A5', 32957 bottom: '\u22A5', 32958 bowtie: '\u22C8', 32959 boxbox: '\u29C9', 32960 boxDL: '\u2557', 32961 boxDl: '\u2556', 32962 boxdL: '\u2555', 32963 boxdl: '\u2510', 32964 boxDR: '\u2554', 32965 boxDr: '\u2553', 32966 boxdR: '\u2552', 32967 boxdr: '\u250C', 32968 boxH: '\u2550', 32969 boxh: '\u2500', 32970 boxHD: '\u2566', 32971 boxHd: '\u2564', 32972 boxhD: '\u2565', 32973 boxhd: '\u252C', 32974 boxHU: '\u2569', 32975 boxHu: '\u2567', 32976 boxhU: '\u2568', 32977 boxhu: '\u2534', 32978 boxminus: '\u229F', 32979 boxplus: '\u229E', 32980 boxtimes: '\u22A0', 32981 boxUL: '\u255D', 32982 boxUl: '\u255C', 32983 boxuL: '\u255B', 32984 boxul: '\u2518', 32985 boxUR: '\u255A', 32986 boxUr: '\u2559', 32987 boxuR: '\u2558', 32988 boxur: '\u2514', 32989 boxV: '\u2551', 32990 boxv: '\u2502', 32991 boxVH: '\u256C', 32992 boxVh: '\u256B', 32993 boxvH: '\u256A', 32994 boxvh: '\u253C', 32995 boxVL: '\u2563', 32996 boxVl: '\u2562', 32997 boxvL: '\u2561', 32998 boxvl: '\u2524', 32999 boxVR: '\u2560', 33000 boxVr: '\u255F', 33001 boxvR: '\u255E', 33002 boxvr: '\u251C', 33003 bprime: '\u2035', 33004 Breve: '\u02D8', 33005 breve: '\u02D8', 33006 brvbar: '\u00A6', 33007 Bscr: '\u212C', 33008 bscr: '\uD835\uDCB7', 33009 bsemi: '\u204F', 33010 bsim: '\u223D', 33011 bsime: '\u22CD', 33012 bsol: '\u005C', 33013 bsolb: '\u29C5', 33014 bsolhsub: '\u27C8', 33015 bull: '\u2022', 33016 bullet: '\u2022', 33017 bump: '\u224E', 33018 bumpE: '\u2AAE', 33019 bumpe: '\u224F', 33020 Bumpeq: '\u224E', 33021 bumpeq: '\u224F', 33022 Cacute: '\u0106', 33023 cacute: '\u0107', 33024 Cap: '\u22D2', 33025 cap: '\u2229', 33026 capand: '\u2A44', 33027 capbrcup: '\u2A49', 33028 capcap: '\u2A4B', 33029 capcup: '\u2A47', 33030 capdot: '\u2A40', 33031 CapitalDifferentialD: '\u2145', 33032 caps: '\u2229\uFE00', 33033 caret: '\u2041', 33034 caron: '\u02C7', 33035 Cayleys: '\u212D', 33036 ccaps: '\u2A4D', 33037 Ccaron: '\u010C', 33038 ccaron: '\u010D', 33039 Ccedil: '\u00C7', 33040 ccedil: '\u00E7', 33041 Ccirc: '\u0108', 33042 ccirc: '\u0109', 33043 Cconint: '\u2230', 33044 ccups: '\u2A4C', 33045 ccupssm: '\u2A50', 33046 Cdot: '\u010A', 33047 cdot: '\u010B', 33048 cedil: '\u00B8', 33049 Cedilla: '\u00B8', 33050 cemptyv: '\u29B2', 33051 cent: '\u00A2', 33052 CenterDot: '\u00B7', 33053 centerdot: '\u00B7', 33054 Cfr: '\u212D', 33055 cfr: '\uD835\uDD20', 33056 CHcy: '\u0427', 33057 chcy: '\u0447', 33058 check: '\u2713', 33059 checkmark: '\u2713', 33060 Chi: '\u03A7', 33061 chi: '\u03C7', 33062 cir: '\u25CB', 33063 circ: '\u02C6', 33064 circeq: '\u2257', 33065 circlearrowleft: '\u21BA', 33066 circlearrowright: '\u21BB', 33067 circledast: '\u229B', 33068 circledcirc: '\u229A', 33069 circleddash: '\u229D', 33070 CircleDot: '\u2299', 33071 circledR: '\u00AE', 33072 circledS: '\u24C8', 33073 CircleMinus: '\u2296', 33074 CirclePlus: '\u2295', 33075 CircleTimes: '\u2297', 33076 cirE: '\u29C3', 33077 cire: '\u2257', 33078 cirfnint: '\u2A10', 33079 cirmid: '\u2AEF', 33080 cirscir: '\u29C2', 33081 ClockwiseContourIntegral: '\u2232', 33082 CloseCurlyDoubleQuote: '\u201D', 33083 CloseCurlyQuote: '\u2019', 33084 clubs: '\u2663', 33085 clubsuit: '\u2663', 33086 Colon: '\u2237', 33087 colon: '\u003A', 33088 Colone: '\u2A74', 33089 colone: '\u2254', 33090 coloneq: '\u2254', 33091 comma: '\u002C', 33092 commat: '\u0040', 33093 comp: '\u2201', 33094 compfn: '\u2218', 33095 complement: '\u2201', 33096 complexes: '\u2102', 33097 cong: '\u2245', 33098 congdot: '\u2A6D', 33099 Congruent: '\u2261', 33100 Conint: '\u222F', 33101 conint: '\u222E', 33102 ContourIntegral: '\u222E', 33103 Copf: '\u2102', 33104 copf: '\uD835\uDD54', 33105 coprod: '\u2210', 33106 Coproduct: '\u2210', 33107 COPY: '\u00A9', 33108 copy: '\u00A9', 33109 copysr: '\u2117', 33110 CounterClockwiseContourIntegral: '\u2233', 33111 crarr: '\u21B5', 33112 Cross: '\u2A2F', 33113 cross: '\u2717', 33114 Cscr: '\uD835\uDC9E', 33115 cscr: '\uD835\uDCB8', 33116 csub: '\u2ACF', 33117 csube: '\u2AD1', 33118 csup: '\u2AD0', 33119 csupe: '\u2AD2', 33120 ctdot: '\u22EF', 33121 cudarrl: '\u2938', 33122 cudarrr: '\u2935', 33123 cuepr: '\u22DE', 33124 cuesc: '\u22DF', 33125 cularr: '\u21B6', 33126 cularrp: '\u293D', 33127 Cup: '\u22D3', 33128 cup: '\u222A', 33129 cupbrcap: '\u2A48', 33130 CupCap: '\u224D', 33131 cupcap: '\u2A46', 33132 cupcup: '\u2A4A', 33133 cupdot: '\u228D', 33134 cupor: '\u2A45', 33135 cups: '\u222A\uFE00', 33136 curarr: '\u21B7', 33137 curarrm: '\u293C', 33138 curlyeqprec: '\u22DE', 33139 curlyeqsucc: '\u22DF', 33140 curlyvee: '\u22CE', 33141 curlywedge: '\u22CF', 33142 curren: '\u00A4', 33143 curvearrowleft: '\u21B6', 33144 curvearrowright: '\u21B7', 33145 cuvee: '\u22CE', 33146 cuwed: '\u22CF', 33147 cwconint: '\u2232', 33148 cwint: '\u2231', 33149 cylcty: '\u232D', 33150 Dagger: '\u2021', 33151 dagger: '\u2020', 33152 daleth: '\u2138', 33153 Darr: '\u21A1', 33154 dArr: '\u21D3', 33155 darr: '\u2193', 33156 dash: '\u2010', 33157 Dashv: '\u2AE4', 33158 dashv: '\u22A3', 33159 dbkarow: '\u290F', 33160 dblac: '\u02DD', 33161 Dcaron: '\u010E', 33162 dcaron: '\u010F', 33163 Dcy: '\u0414', 33164 dcy: '\u0434', 33165 DD: '\u2145', 33166 dd: '\u2146', 33167 ddagger: '\u2021', 33168 ddarr: '\u21CA', 33169 DDotrahd: '\u2911', 33170 ddotseq: '\u2A77', 33171 deg: '\u00B0', 33172 Del: '\u2207', 33173 Delta: '\u0394', 33174 delta: '\u03B4', 33175 demptyv: '\u29B1', 33176 dfisht: '\u297F', 33177 Dfr: '\uD835\uDD07', 33178 dfr: '\uD835\uDD21', 33179 dHar: '\u2965', 33180 dharl: '\u21C3', 33181 dharr: '\u21C2', 33182 DiacriticalAcute: '\u00B4', 33183 DiacriticalDot: '\u02D9', 33184 DiacriticalDoubleAcute: '\u02DD', 33185 DiacriticalGrave: '\u0060', 33186 DiacriticalTilde: '\u02DC', 33187 diam: '\u22C4', 33188 Diamond: '\u22C4', 33189 diamond: '\u22C4', 33190 diamondsuit: '\u2666', 33191 diams: '\u2666', 33192 die: '\u00A8', 33193 DifferentialD: '\u2146', 33194 digamma: '\u03DD', 33195 disin: '\u22F2', 33196 div: '\u00F7', 33197 divide: '\u00F7', 33198 divideontimes: '\u22C7', 33199 divonx: '\u22C7', 33200 DJcy: '\u0402', 33201 djcy: '\u0452', 33202 dlcorn: '\u231E', 33203 dlcrop: '\u230D', 33204 dollar: '\u0024', 33205 Dopf: '\uD835\uDD3B', 33206 dopf: '\uD835\uDD55', 33207 Dot: '\u00A8', 33208 dot: '\u02D9', 33209 DotDot: '\u20DC', 33210 doteq: '\u2250', 33211 doteqdot: '\u2251', 33212 DotEqual: '\u2250', 33213 dotminus: '\u2238', 33214 dotplus: '\u2214', 33215 dotsquare: '\u22A1', 33216 doublebarwedge: '\u2306', 33217 DoubleContourIntegral: '\u222F', 33218 DoubleDot: '\u00A8', 33219 DoubleDownArrow: '\u21D3', 33220 DoubleLeftArrow: '\u21D0', 33221 DoubleLeftRightArrow: '\u21D4', 33222 DoubleLeftTee: '\u2AE4', 33223 DoubleLongLeftArrow: '\u27F8', 33224 DoubleLongLeftRightArrow: '\u27FA', 33225 DoubleLongRightArrow: '\u27F9', 33226 DoubleRightArrow: '\u21D2', 33227 DoubleRightTee: '\u22A8', 33228 DoubleUpArrow: '\u21D1', 33229 DoubleUpDownArrow: '\u21D5', 33230 DoubleVerticalBar: '\u2225', 33231 DownArrow: '\u2193', 33232 Downarrow: '\u21D3', 33233 downarrow: '\u2193', 33234 DownArrowBar: '\u2913', 33235 DownArrowUpArrow: '\u21F5', 33236 DownBreve: '\u0311', 33237 downdownarrows: '\u21CA', 33238 downharpoonleft: '\u21C3', 33239 downharpoonright: '\u21C2', 33240 DownLeftRightVector: '\u2950', 33241 DownLeftTeeVector: '\u295E', 33242 DownLeftVector: '\u21BD', 33243 DownLeftVectorBar: '\u2956', 33244 DownRightTeeVector: '\u295F', 33245 DownRightVector: '\u21C1', 33246 DownRightVectorBar: '\u2957', 33247 DownTee: '\u22A4', 33248 DownTeeArrow: '\u21A7', 33249 drbkarow: '\u2910', 33250 drcorn: '\u231F', 33251 drcrop: '\u230C', 33252 Dscr: '\uD835\uDC9F', 33253 dscr: '\uD835\uDCB9', 33254 DScy: '\u0405', 33255 dscy: '\u0455', 33256 dsol: '\u29F6', 33257 Dstrok: '\u0110', 33258 dstrok: '\u0111', 33259 dtdot: '\u22F1', 33260 dtri: '\u25BF', 33261 dtrif: '\u25BE', 33262 duarr: '\u21F5', 33263 duhar: '\u296F', 33264 dwangle: '\u29A6', 33265 DZcy: '\u040F', 33266 dzcy: '\u045F', 33267 dzigrarr: '\u27FF', 33268 Eacute: '\u00C9', 33269 eacute: '\u00E9', 33270 easter: '\u2A6E', 33271 Ecaron: '\u011A', 33272 ecaron: '\u011B', 33273 ecir: '\u2256', 33274 Ecirc: '\u00CA', 33275 ecirc: '\u00EA', 33276 ecolon: '\u2255', 33277 Ecy: '\u042D', 33278 ecy: '\u044D', 33279 eDDot: '\u2A77', 33280 Edot: '\u0116', 33281 eDot: '\u2251', 33282 edot: '\u0117', 33283 ee: '\u2147', 33284 efDot: '\u2252', 33285 Efr: '\uD835\uDD08', 33286 efr: '\uD835\uDD22', 33287 eg: '\u2A9A', 33288 Egrave: '\u00C8', 33289 egrave: '\u00E8', 33290 egs: '\u2A96', 33291 egsdot: '\u2A98', 33292 el: '\u2A99', 33293 Element: '\u2208', 33294 elinters: '\u23E7', 33295 ell: '\u2113', 33296 els: '\u2A95', 33297 elsdot: '\u2A97', 33298 Emacr: '\u0112', 33299 emacr: '\u0113', 33300 empty: '\u2205', 33301 emptyset: '\u2205', 33302 EmptySmallSquare: '\u25FB', 33303 emptyv: '\u2205', 33304 EmptyVerySmallSquare: '\u25AB', 33305 emsp: '\u2003', 33306 emsp13: '\u2004', 33307 emsp14: '\u2005', 33308 ENG: '\u014A', 33309 eng: '\u014B', 33310 ensp: '\u2002', 33311 Eogon: '\u0118', 33312 eogon: '\u0119', 33313 Eopf: '\uD835\uDD3C', 33314 eopf: '\uD835\uDD56', 33315 epar: '\u22D5', 33316 eparsl: '\u29E3', 33317 eplus: '\u2A71', 33318 epsi: '\u03B5', 33319 Epsilon: '\u0395', 33320 epsilon: '\u03B5', 33321 epsiv: '\u03F5', 33322 eqcirc: '\u2256', 33323 eqcolon: '\u2255', 33324 eqsim: '\u2242', 33325 eqslantgtr: '\u2A96', 33326 eqslantless: '\u2A95', 33327 Equal: '\u2A75', 33328 equals: '\u003D', 33329 EqualTilde: '\u2242', 33330 equest: '\u225F', 33331 Equilibrium: '\u21CC', 33332 equiv: '\u2261', 33333 equivDD: '\u2A78', 33334 eqvparsl: '\u29E5', 33335 erarr: '\u2971', 33336 erDot: '\u2253', 33337 Escr: '\u2130', 33338 escr: '\u212F', 33339 esdot: '\u2250', 33340 Esim: '\u2A73', 33341 esim: '\u2242', 33342 Eta: '\u0397', 33343 eta: '\u03B7', 33344 ETH: '\u00D0', 33345 eth: '\u00F0', 33346 Euml: '\u00CB', 33347 euml: '\u00EB', 33348 euro: '\u20AC', 33349 excl: '\u0021', 33350 exist: '\u2203', 33351 Exists: '\u2203', 33352 expectation: '\u2130', 33353 ExponentialE: '\u2147', 33354 exponentiale: '\u2147', 33355 fallingdotseq: '\u2252', 33356 Fcy: '\u0424', 33357 fcy: '\u0444', 33358 female: '\u2640', 33359 ffilig: '\uFB03', 33360 fflig: '\uFB00', 33361 ffllig: '\uFB04', 33362 Ffr: '\uD835\uDD09', 33363 ffr: '\uD835\uDD23', 33364 filig: '\uFB01', 33365 FilledSmallSquare: '\u25FC', 33366 FilledVerySmallSquare: '\u25AA', 33367 fjlig: '\u0066\u006A', 33368 flat: '\u266D', 33369 fllig: '\uFB02', 33370 fltns: '\u25B1', 33371 fnof: '\u0192', 33372 Fopf: '\uD835\uDD3D', 33373 fopf: '\uD835\uDD57', 33374 ForAll: '\u2200', 33375 forall: '\u2200', 33376 fork: '\u22D4', 33377 forkv: '\u2AD9', 33378 Fouriertrf: '\u2131', 33379 fpartint: '\u2A0D', 33380 frac12: '\u00BD', 33381 frac13: '\u2153', 33382 frac14: '\u00BC', 33383 frac15: '\u2155', 33384 frac16: '\u2159', 33385 frac18: '\u215B', 33386 frac23: '\u2154', 33387 frac25: '\u2156', 33388 frac34: '\u00BE', 33389 frac35: '\u2157', 33390 frac38: '\u215C', 33391 frac45: '\u2158', 33392 frac56: '\u215A', 33393 frac58: '\u215D', 33394 frac78: '\u215E', 33395 frasl: '\u2044', 33396 frown: '\u2322', 33397 Fscr: '\u2131', 33398 fscr: '\uD835\uDCBB', 33399 gacute: '\u01F5', 33400 Gamma: '\u0393', 33401 gamma: '\u03B3', 33402 Gammad: '\u03DC', 33403 gammad: '\u03DD', 33404 gap: '\u2A86', 33405 Gbreve: '\u011E', 33406 gbreve: '\u011F', 33407 Gcedil: '\u0122', 33408 Gcirc: '\u011C', 33409 gcirc: '\u011D', 33410 Gcy: '\u0413', 33411 gcy: '\u0433', 33412 Gdot: '\u0120', 33413 gdot: '\u0121', 33414 gE: '\u2267', 33415 ge: '\u2265', 33416 gEl: '\u2A8C', 33417 gel: '\u22DB', 33418 geq: '\u2265', 33419 geqq: '\u2267', 33420 geqslant: '\u2A7E', 33421 ges: '\u2A7E', 33422 gescc: '\u2AA9', 33423 gesdot: '\u2A80', 33424 gesdoto: '\u2A82', 33425 gesdotol: '\u2A84', 33426 gesl: '\u22DB\uFE00', 33427 gesles: '\u2A94', 33428 Gfr: '\uD835\uDD0A', 33429 gfr: '\uD835\uDD24', 33430 Gg: '\u22D9', 33431 gg: '\u226B', 33432 ggg: '\u22D9', 33433 gimel: '\u2137', 33434 GJcy: '\u0403', 33435 gjcy: '\u0453', 33436 gl: '\u2277', 33437 gla: '\u2AA5', 33438 glE: '\u2A92', 33439 glj: '\u2AA4', 33440 gnap: '\u2A8A', 33441 gnapprox: '\u2A8A', 33442 gnE: '\u2269', 33443 gne: '\u2A88', 33444 gneq: '\u2A88', 33445 gneqq: '\u2269', 33446 gnsim: '\u22E7', 33447 Gopf: '\uD835\uDD3E', 33448 gopf: '\uD835\uDD58', 33449 grave: '\u0060', 33450 GreaterEqual: '\u2265', 33451 GreaterEqualLess: '\u22DB', 33452 GreaterFullEqual: '\u2267', 33453 GreaterGreater: '\u2AA2', 33454 GreaterLess: '\u2277', 33455 GreaterSlantEqual: '\u2A7E', 33456 GreaterTilde: '\u2273', 33457 Gscr: '\uD835\uDCA2', 33458 gscr: '\u210A', 33459 gsim: '\u2273', 33460 gsime: '\u2A8E', 33461 gsiml: '\u2A90', 33462 Gt: '\u226B', 33463 GT: '\u003E', 33464 gt: '\u003E', 33465 gtcc: '\u2AA7', 33466 gtcir: '\u2A7A', 33467 gtdot: '\u22D7', 33468 gtlPar: '\u2995', 33469 gtquest: '\u2A7C', 33470 gtrapprox: '\u2A86', 33471 gtrarr: '\u2978', 33472 gtrdot: '\u22D7', 33473 gtreqless: '\u22DB', 33474 gtreqqless: '\u2A8C', 33475 gtrless: '\u2277', 33476 gtrsim: '\u2273', 33477 gvertneqq: '\u2269\uFE00', 33478 gvnE: '\u2269\uFE00', 33479 Hacek: '\u02C7', 33480 hairsp: '\u200A', 33481 half: '\u00BD', 33482 hamilt: '\u210B', 33483 HARDcy: '\u042A', 33484 hardcy: '\u044A', 33485 hArr: '\u21D4', 33486 harr: '\u2194', 33487 harrcir: '\u2948', 33488 harrw: '\u21AD', 33489 Hat: '\u005E', 33490 hbar: '\u210F', 33491 Hcirc: '\u0124', 33492 hcirc: '\u0125', 33493 hearts: '\u2665', 33494 heartsuit: '\u2665', 33495 hellip: '\u2026', 33496 hercon: '\u22B9', 33497 Hfr: '\u210C', 33498 hfr: '\uD835\uDD25', 33499 HilbertSpace: '\u210B', 33500 hksearow: '\u2925', 33501 hkswarow: '\u2926', 33502 hoarr: '\u21FF', 33503 homtht: '\u223B', 33504 hookleftarrow: '\u21A9', 33505 hookrightarrow: '\u21AA', 33506 Hopf: '\u210D', 33507 hopf: '\uD835\uDD59', 33508 horbar: '\u2015', 33509 HorizontalLine: '\u2500', 33510 Hscr: '\u210B', 33511 hscr: '\uD835\uDCBD', 33512 hslash: '\u210F', 33513 Hstrok: '\u0126', 33514 hstrok: '\u0127', 33515 HumpDownHump: '\u224E', 33516 HumpEqual: '\u224F', 33517 hybull: '\u2043', 33518 hyphen: '\u2010', 33519 Iacute: '\u00CD', 33520 iacute: '\u00ED', 33521 ic: '\u2063', 33522 Icirc: '\u00CE', 33523 icirc: '\u00EE', 33524 Icy: '\u0418', 33525 icy: '\u0438', 33526 Idot: '\u0130', 33527 IEcy: '\u0415', 33528 iecy: '\u0435', 33529 iexcl: '\u00A1', 33530 iff: '\u21D4', 33531 Ifr: '\u2111', 33532 ifr: '\uD835\uDD26', 33533 Igrave: '\u00CC', 33534 igrave: '\u00EC', 33535 ii: '\u2148', 33536 iiiint: '\u2A0C', 33537 iiint: '\u222D', 33538 iinfin: '\u29DC', 33539 iiota: '\u2129', 33540 IJlig: '\u0132', 33541 ijlig: '\u0133', 33542 Im: '\u2111', 33543 Imacr: '\u012A', 33544 imacr: '\u012B', 33545 image: '\u2111', 33546 ImaginaryI: '\u2148', 33547 imagline: '\u2110', 33548 imagpart: '\u2111', 33549 imath: '\u0131', 33550 imof: '\u22B7', 33551 imped: '\u01B5', 33552 Implies: '\u21D2', 33553 in: '\u2208', 33554 incare: '\u2105', 33555 infin: '\u221E', 33556 infintie: '\u29DD', 33557 inodot: '\u0131', 33558 Int: '\u222C', 33559 int: '\u222B', 33560 intcal: '\u22BA', 33561 integers: '\u2124', 33562 Integral: '\u222B', 33563 intercal: '\u22BA', 33564 Intersection: '\u22C2', 33565 intlarhk: '\u2A17', 33566 intprod: '\u2A3C', 33567 InvisibleComma: '\u2063', 33568 InvisibleTimes: '\u2062', 33569 IOcy: '\u0401', 33570 iocy: '\u0451', 33571 Iogon: '\u012E', 33572 iogon: '\u012F', 33573 Iopf: '\uD835\uDD40', 33574 iopf: '\uD835\uDD5A', 33575 Iota: '\u0399', 33576 iota: '\u03B9', 33577 iprod: '\u2A3C', 33578 iquest: '\u00BF', 33579 Iscr: '\u2110', 33580 iscr: '\uD835\uDCBE', 33581 isin: '\u2208', 33582 isindot: '\u22F5', 33583 isinE: '\u22F9', 33584 isins: '\u22F4', 33585 isinsv: '\u22F3', 33586 isinv: '\u2208', 33587 it: '\u2062', 33588 Itilde: '\u0128', 33589 itilde: '\u0129', 33590 Iukcy: '\u0406', 33591 iukcy: '\u0456', 33592 Iuml: '\u00CF', 33593 iuml: '\u00EF', 33594 Jcirc: '\u0134', 33595 jcirc: '\u0135', 33596 Jcy: '\u0419', 33597 jcy: '\u0439', 33598 Jfr: '\uD835\uDD0D', 33599 jfr: '\uD835\uDD27', 33600 jmath: '\u0237', 33601 Jopf: '\uD835\uDD41', 33602 jopf: '\uD835\uDD5B', 33603 Jscr: '\uD835\uDCA5', 33604 jscr: '\uD835\uDCBF', 33605 Jsercy: '\u0408', 33606 jsercy: '\u0458', 33607 Jukcy: '\u0404', 33608 jukcy: '\u0454', 33609 Kappa: '\u039A', 33610 kappa: '\u03BA', 33611 kappav: '\u03F0', 33612 Kcedil: '\u0136', 33613 kcedil: '\u0137', 33614 Kcy: '\u041A', 33615 kcy: '\u043A', 33616 Kfr: '\uD835\uDD0E', 33617 kfr: '\uD835\uDD28', 33618 kgreen: '\u0138', 33619 KHcy: '\u0425', 33620 khcy: '\u0445', 33621 KJcy: '\u040C', 33622 kjcy: '\u045C', 33623 Kopf: '\uD835\uDD42', 33624 kopf: '\uD835\uDD5C', 33625 Kscr: '\uD835\uDCA6', 33626 kscr: '\uD835\uDCC0', 33627 lAarr: '\u21DA', 33628 Lacute: '\u0139', 33629 lacute: '\u013A', 33630 laemptyv: '\u29B4', 33631 lagran: '\u2112', 33632 Lambda: '\u039B', 33633 lambda: '\u03BB', 33634 Lang: '\u27EA', 33635 lang: '\u27E8', 33636 langd: '\u2991', 33637 langle: '\u27E8', 33638 lap: '\u2A85', 33639 Laplacetrf: '\u2112', 33640 laquo: '\u00AB', 33641 Larr: '\u219E', 33642 lArr: '\u21D0', 33643 larr: '\u2190', 33644 larrb: '\u21E4', 33645 larrbfs: '\u291F', 33646 larrfs: '\u291D', 33647 larrhk: '\u21A9', 33648 larrlp: '\u21AB', 33649 larrpl: '\u2939', 33650 larrsim: '\u2973', 33651 larrtl: '\u21A2', 33652 lat: '\u2AAB', 33653 lAtail: '\u291B', 33654 latail: '\u2919', 33655 late: '\u2AAD', 33656 lates: '\u2AAD\uFE00', 33657 lBarr: '\u290E', 33658 lbarr: '\u290C', 33659 lbbrk: '\u2772', 33660 lbrace: '\u007B', 33661 lbrack: '\u005B', 33662 lbrke: '\u298B', 33663 lbrksld: '\u298F', 33664 lbrkslu: '\u298D', 33665 Lcaron: '\u013D', 33666 lcaron: '\u013E', 33667 Lcedil: '\u013B', 33668 lcedil: '\u013C', 33669 lceil: '\u2308', 33670 lcub: '\u007B', 33671 Lcy: '\u041B', 33672 lcy: '\u043B', 33673 ldca: '\u2936', 33674 ldquo: '\u201C', 33675 ldquor: '\u201E', 33676 ldrdhar: '\u2967', 33677 ldrushar: '\u294B', 33678 ldsh: '\u21B2', 33679 lE: '\u2266', 33680 le: '\u2264', 33681 LeftAngleBracket: '\u27E8', 33682 LeftArrow: '\u2190', 33683 Leftarrow: '\u21D0', 33684 leftarrow: '\u2190', 33685 LeftArrowBar: '\u21E4', 33686 LeftArrowRightArrow: '\u21C6', 33687 leftarrowtail: '\u21A2', 33688 LeftCeiling: '\u2308', 33689 LeftDoubleBracket: '\u27E6', 33690 LeftDownTeeVector: '\u2961', 33691 LeftDownVector: '\u21C3', 33692 LeftDownVectorBar: '\u2959', 33693 LeftFloor: '\u230A', 33694 leftharpoondown: '\u21BD', 33695 leftharpoonup: '\u21BC', 33696 leftleftarrows: '\u21C7', 33697 LeftRightArrow: '\u2194', 33698 Leftrightarrow: '\u21D4', 33699 leftrightarrow: '\u2194', 33700 leftrightarrows: '\u21C6', 33701 leftrightharpoons: '\u21CB', 33702 leftrightsquigarrow: '\u21AD', 33703 LeftRightVector: '\u294E', 33704 LeftTee: '\u22A3', 33705 LeftTeeArrow: '\u21A4', 33706 LeftTeeVector: '\u295A', 33707 leftthreetimes: '\u22CB', 33708 LeftTriangle: '\u22B2', 33709 LeftTriangleBar: '\u29CF', 33710 LeftTriangleEqual: '\u22B4', 33711 LeftUpDownVector: '\u2951', 33712 LeftUpTeeVector: '\u2960', 33713 LeftUpVector: '\u21BF', 33714 LeftUpVectorBar: '\u2958', 33715 LeftVector: '\u21BC', 33716 LeftVectorBar: '\u2952', 33717 lEg: '\u2A8B', 33718 leg: '\u22DA', 33719 leq: '\u2264', 33720 leqq: '\u2266', 33721 leqslant: '\u2A7D', 33722 les: '\u2A7D', 33723 lescc: '\u2AA8', 33724 lesdot: '\u2A7F', 33725 lesdoto: '\u2A81', 33726 lesdotor: '\u2A83', 33727 lesg: '\u22DA\uFE00', 33728 lesges: '\u2A93', 33729 lessapprox: '\u2A85', 33730 lessdot: '\u22D6', 33731 lesseqgtr: '\u22DA', 33732 lesseqqgtr: '\u2A8B', 33733 LessEqualGreater: '\u22DA', 33734 LessFullEqual: '\u2266', 33735 LessGreater: '\u2276', 33736 lessgtr: '\u2276', 33737 LessLess: '\u2AA1', 33738 lesssim: '\u2272', 33739 LessSlantEqual: '\u2A7D', 33740 LessTilde: '\u2272', 33741 lfisht: '\u297C', 33742 lfloor: '\u230A', 33743 Lfr: '\uD835\uDD0F', 33744 lfr: '\uD835\uDD29', 33745 lg: '\u2276', 33746 lgE: '\u2A91', 33747 lHar: '\u2962', 33748 lhard: '\u21BD', 33749 lharu: '\u21BC', 33750 lharul: '\u296A', 33751 lhblk: '\u2584', 33752 LJcy: '\u0409', 33753 ljcy: '\u0459', 33754 Ll: '\u22D8', 33755 ll: '\u226A', 33756 llarr: '\u21C7', 33757 llcorner: '\u231E', 33758 Lleftarrow: '\u21DA', 33759 llhard: '\u296B', 33760 lltri: '\u25FA', 33761 Lmidot: '\u013F', 33762 lmidot: '\u0140', 33763 lmoust: '\u23B0', 33764 lmoustache: '\u23B0', 33765 lnap: '\u2A89', 33766 lnapprox: '\u2A89', 33767 lnE: '\u2268', 33768 lne: '\u2A87', 33769 lneq: '\u2A87', 33770 lneqq: '\u2268', 33771 lnsim: '\u22E6', 33772 loang: '\u27EC', 33773 loarr: '\u21FD', 33774 lobrk: '\u27E6', 33775 LongLeftArrow: '\u27F5', 33776 Longleftarrow: '\u27F8', 33777 longleftarrow: '\u27F5', 33778 LongLeftRightArrow: '\u27F7', 33779 Longleftrightarrow: '\u27FA', 33780 longleftrightarrow: '\u27F7', 33781 longmapsto: '\u27FC', 33782 LongRightArrow: '\u27F6', 33783 Longrightarrow: '\u27F9', 33784 longrightarrow: '\u27F6', 33785 looparrowleft: '\u21AB', 33786 looparrowright: '\u21AC', 33787 lopar: '\u2985', 33788 Lopf: '\uD835\uDD43', 33789 lopf: '\uD835\uDD5D', 33790 loplus: '\u2A2D', 33791 lotimes: '\u2A34', 33792 lowast: '\u2217', 33793 lowbar: '\u005F', 33794 LowerLeftArrow: '\u2199', 33795 LowerRightArrow: '\u2198', 33796 loz: '\u25CA', 33797 lozenge: '\u25CA', 33798 lozf: '\u29EB', 33799 lpar: '\u0028', 33800 lparlt: '\u2993', 33801 lrarr: '\u21C6', 33802 lrcorner: '\u231F', 33803 lrhar: '\u21CB', 33804 lrhard: '\u296D', 33805 lrm: '\u200E', 33806 lrtri: '\u22BF', 33807 lsaquo: '\u2039', 33808 Lscr: '\u2112', 33809 lscr: '\uD835\uDCC1', 33810 Lsh: '\u21B0', 33811 lsh: '\u21B0', 33812 lsim: '\u2272', 33813 lsime: '\u2A8D', 33814 lsimg: '\u2A8F', 33815 lsqb: '\u005B', 33816 lsquo: '\u2018', 33817 lsquor: '\u201A', 33818 Lstrok: '\u0141', 33819 lstrok: '\u0142', 33820 Lt: '\u226A', 33821 LT: '\u003C', 33822 lt: '\u003C', 33823 ltcc: '\u2AA6', 33824 ltcir: '\u2A79', 33825 ltdot: '\u22D6', 33826 lthree: '\u22CB', 33827 ltimes: '\u22C9', 33828 ltlarr: '\u2976', 33829 ltquest: '\u2A7B', 33830 ltri: '\u25C3', 33831 ltrie: '\u22B4', 33832 ltrif: '\u25C2', 33833 ltrPar: '\u2996', 33834 lurdshar: '\u294A', 33835 luruhar: '\u2966', 33836 lvertneqq: '\u2268\uFE00', 33837 lvnE: '\u2268\uFE00', 33838 macr: '\u00AF', 33839 male: '\u2642', 33840 malt: '\u2720', 33841 maltese: '\u2720', 33842 Map: '\u2905', 33843 map: '\u21A6', 33844 mapsto: '\u21A6', 33845 mapstodown: '\u21A7', 33846 mapstoleft: '\u21A4', 33847 mapstoup: '\u21A5', 33848 marker: '\u25AE', 33849 mcomma: '\u2A29', 33850 Mcy: '\u041C', 33851 mcy: '\u043C', 33852 mdash: '\u2014', 33853 mDDot: '\u223A', 33854 measuredangle: '\u2221', 33855 MediumSpace: '\u205F', 33856 Mellintrf: '\u2133', 33857 Mfr: '\uD835\uDD10', 33858 mfr: '\uD835\uDD2A', 33859 mho: '\u2127', 33860 micro: '\u00B5', 33861 mid: '\u2223', 33862 midast: '\u002A', 33863 midcir: '\u2AF0', 33864 middot: '\u00B7', 33865 minus: '\u2212', 33866 minusb: '\u229F', 33867 minusd: '\u2238', 33868 minusdu: '\u2A2A', 33869 MinusPlus: '\u2213', 33870 mlcp: '\u2ADB', 33871 mldr: '\u2026', 33872 mnplus: '\u2213', 33873 models: '\u22A7', 33874 Mopf: '\uD835\uDD44', 33875 mopf: '\uD835\uDD5E', 33876 mp: '\u2213', 33877 Mscr: '\u2133', 33878 mscr: '\uD835\uDCC2', 33879 mstpos: '\u223E', 33880 Mu: '\u039C', 33881 mu: '\u03BC', 33882 multimap: '\u22B8', 33883 mumap: '\u22B8', 33884 nabla: '\u2207', 33885 Nacute: '\u0143', 33886 nacute: '\u0144', 33887 nang: '\u2220\u20D2', 33888 nap: '\u2249', 33889 napE: '\u2A70\u0338', 33890 napid: '\u224B\u0338', 33891 napos: '\u0149', 33892 napprox: '\u2249', 33893 natur: '\u266E', 33894 natural: '\u266E', 33895 naturals: '\u2115', 33896 nbsp: '\u00A0', 33897 nbump: '\u224E\u0338', 33898 nbumpe: '\u224F\u0338', 33899 ncap: '\u2A43', 33900 Ncaron: '\u0147', 33901 ncaron: '\u0148', 33902 Ncedil: '\u0145', 33903 ncedil: '\u0146', 33904 ncong: '\u2247', 33905 ncongdot: '\u2A6D\u0338', 33906 ncup: '\u2A42', 33907 Ncy: '\u041D', 33908 ncy: '\u043D', 33909 ndash: '\u2013', 33910 ne: '\u2260', 33911 nearhk: '\u2924', 33912 neArr: '\u21D7', 33913 nearr: '\u2197', 33914 nearrow: '\u2197', 33915 nedot: '\u2250\u0338', 33916 NegativeMediumSpace: '\u200B', 33917 NegativeThickSpace: '\u200B', 33918 NegativeThinSpace: '\u200B', 33919 NegativeVeryThinSpace: '\u200B', 33920 nequiv: '\u2262', 33921 nesear: '\u2928', 33922 nesim: '\u2242\u0338', 33923 NestedGreaterGreater: '\u226B', 33924 NestedLessLess: '\u226A', 33925 NewLine: '\u000A', 33926 nexist: '\u2204', 33927 nexists: '\u2204', 33928 Nfr: '\uD835\uDD11', 33929 nfr: '\uD835\uDD2B', 33930 ngE: '\u2267\u0338', 33931 nge: '\u2271', 33932 ngeq: '\u2271', 33933 ngeqq: '\u2267\u0338', 33934 ngeqslant: '\u2A7E\u0338', 33935 nges: '\u2A7E\u0338', 33936 nGg: '\u22D9\u0338', 33937 ngsim: '\u2275', 33938 nGt: '\u226B\u20D2', 33939 ngt: '\u226F', 33940 ngtr: '\u226F', 33941 nGtv: '\u226B\u0338', 33942 nhArr: '\u21CE', 33943 nharr: '\u21AE', 33944 nhpar: '\u2AF2', 33945 ni: '\u220B', 33946 nis: '\u22FC', 33947 nisd: '\u22FA', 33948 niv: '\u220B', 33949 NJcy: '\u040A', 33950 njcy: '\u045A', 33951 nlArr: '\u21CD', 33952 nlarr: '\u219A', 33953 nldr: '\u2025', 33954 nlE: '\u2266\u0338', 33955 nle: '\u2270', 33956 nLeftarrow: '\u21CD', 33957 nleftarrow: '\u219A', 33958 nLeftrightarrow: '\u21CE', 33959 nleftrightarrow: '\u21AE', 33960 nleq: '\u2270', 33961 nleqq: '\u2266\u0338', 33962 nleqslant: '\u2A7D\u0338', 33963 nles: '\u2A7D\u0338', 33964 nless: '\u226E', 33965 nLl: '\u22D8\u0338', 33966 nlsim: '\u2274', 33967 nLt: '\u226A\u20D2', 33968 nlt: '\u226E', 33969 nltri: '\u22EA', 33970 nltrie: '\u22EC', 33971 nLtv: '\u226A\u0338', 33972 nmid: '\u2224', 33973 NoBreak: '\u2060', 33974 NonBreakingSpace: '\u00A0', 33975 Nopf: '\u2115', 33976 nopf: '\uD835\uDD5F', 33977 Not: '\u2AEC', 33978 not: '\u00AC', 33979 NotCongruent: '\u2262', 33980 NotCupCap: '\u226D', 33981 NotDoubleVerticalBar: '\u2226', 33982 NotElement: '\u2209', 33983 NotEqual: '\u2260', 33984 NotEqualTilde: '\u2242\u0338', 33985 NotExists: '\u2204', 33986 NotGreater: '\u226F', 33987 NotGreaterEqual: '\u2271', 33988 NotGreaterFullEqual: '\u2267\u0338', 33989 NotGreaterGreater: '\u226B\u0338', 33990 NotGreaterLess: '\u2279', 33991 NotGreaterSlantEqual: '\u2A7E\u0338', 33992 NotGreaterTilde: '\u2275', 33993 NotHumpDownHump: '\u224E\u0338', 33994 NotHumpEqual: '\u224F\u0338', 33995 notin: '\u2209', 33996 notindot: '\u22F5\u0338', 33997 notinE: '\u22F9\u0338', 33998 notinva: '\u2209', 33999 notinvb: '\u22F7', 34000 notinvc: '\u22F6', 34001 NotLeftTriangle: '\u22EA', 34002 NotLeftTriangleBar: '\u29CF\u0338', 34003 NotLeftTriangleEqual: '\u22EC', 34004 NotLess: '\u226E', 34005 NotLessEqual: '\u2270', 34006 NotLessGreater: '\u2278', 34007 NotLessLess: '\u226A\u0338', 34008 NotLessSlantEqual: '\u2A7D\u0338', 34009 NotLessTilde: '\u2274', 34010 NotNestedGreaterGreater: '\u2AA2\u0338', 34011 NotNestedLessLess: '\u2AA1\u0338', 34012 notni: '\u220C', 34013 notniva: '\u220C', 34014 notnivb: '\u22FE', 34015 notnivc: '\u22FD', 34016 NotPrecedes: '\u2280', 34017 NotPrecedesEqual: '\u2AAF\u0338', 34018 NotPrecedesSlantEqual: '\u22E0', 34019 NotReverseElement: '\u220C', 34020 NotRightTriangle: '\u22EB', 34021 NotRightTriangleBar: '\u29D0\u0338', 34022 NotRightTriangleEqual: '\u22ED', 34023 NotSquareSubset: '\u228F\u0338', 34024 NotSquareSubsetEqual: '\u22E2', 34025 NotSquareSuperset: '\u2290\u0338', 34026 NotSquareSupersetEqual: '\u22E3', 34027 NotSubset: '\u2282\u20D2', 34028 NotSubsetEqual: '\u2288', 34029 NotSucceeds: '\u2281', 34030 NotSucceedsEqual: '\u2AB0\u0338', 34031 NotSucceedsSlantEqual: '\u22E1', 34032 NotSucceedsTilde: '\u227F\u0338', 34033 NotSuperset: '\u2283\u20D2', 34034 NotSupersetEqual: '\u2289', 34035 NotTilde: '\u2241', 34036 NotTildeEqual: '\u2244', 34037 NotTildeFullEqual: '\u2247', 34038 NotTildeTilde: '\u2249', 34039 NotVerticalBar: '\u2224', 34040 npar: '\u2226', 34041 nparallel: '\u2226', 34042 nparsl: '\u2AFD\u20E5', 34043 npart: '\u2202\u0338', 34044 npolint: '\u2A14', 34045 npr: '\u2280', 34046 nprcue: '\u22E0', 34047 npre: '\u2AAF\u0338', 34048 nprec: '\u2280', 34049 npreceq: '\u2AAF\u0338', 34050 nrArr: '\u21CF', 34051 nrarr: '\u219B', 34052 nrarrc: '\u2933\u0338', 34053 nrarrw: '\u219D\u0338', 34054 nRightarrow: '\u21CF', 34055 nrightarrow: '\u219B', 34056 nrtri: '\u22EB', 34057 nrtrie: '\u22ED', 34058 nsc: '\u2281', 34059 nsccue: '\u22E1', 34060 nsce: '\u2AB0\u0338', 34061 Nscr: '\uD835\uDCA9', 34062 nscr: '\uD835\uDCC3', 34063 nshortmid: '\u2224', 34064 nshortparallel: '\u2226', 34065 nsim: '\u2241', 34066 nsime: '\u2244', 34067 nsimeq: '\u2244', 34068 nsmid: '\u2224', 34069 nspar: '\u2226', 34070 nsqsube: '\u22E2', 34071 nsqsupe: '\u22E3', 34072 nsub: '\u2284', 34073 nsubE: '\u2AC5\u0338', 34074 nsube: '\u2288', 34075 nsubset: '\u2282\u20D2', 34076 nsubseteq: '\u2288', 34077 nsubseteqq: '\u2AC5\u0338', 34078 nsucc: '\u2281', 34079 nsucceq: '\u2AB0\u0338', 34080 nsup: '\u2285', 34081 nsupE: '\u2AC6\u0338', 34082 nsupe: '\u2289', 34083 nsupset: '\u2283\u20D2', 34084 nsupseteq: '\u2289', 34085 nsupseteqq: '\u2AC6\u0338', 34086 ntgl: '\u2279', 34087 Ntilde: '\u00D1', 34088 ntilde: '\u00F1', 34089 ntlg: '\u2278', 34090 ntriangleleft: '\u22EA', 34091 ntrianglelefteq: '\u22EC', 34092 ntriangleright: '\u22EB', 34093 ntrianglerighteq: '\u22ED', 34094 Nu: '\u039D', 34095 nu: '\u03BD', 34096 num: '\u0023', 34097 numero: '\u2116', 34098 numsp: '\u2007', 34099 nvap: '\u224D\u20D2', 34100 nVDash: '\u22AF', 34101 nVdash: '\u22AE', 34102 nvDash: '\u22AD', 34103 nvdash: '\u22AC', 34104 nvge: '\u2265\u20D2', 34105 nvgt: '\u003E\u20D2', 34106 nvHarr: '\u2904', 34107 nvinfin: '\u29DE', 34108 nvlArr: '\u2902', 34109 nvle: '\u2264\u20D2', 34110 nvlt: '\u003C\u20D2', 34111 nvltrie: '\u22B4\u20D2', 34112 nvrArr: '\u2903', 34113 nvrtrie: '\u22B5\u20D2', 34114 nvsim: '\u223C\u20D2', 34115 nwarhk: '\u2923', 34116 nwArr: '\u21D6', 34117 nwarr: '\u2196', 34118 nwarrow: '\u2196', 34119 nwnear: '\u2927', 34120 Oacute: '\u00D3', 34121 oacute: '\u00F3', 34122 oast: '\u229B', 34123 ocir: '\u229A', 34124 Ocirc: '\u00D4', 34125 ocirc: '\u00F4', 34126 Ocy: '\u041E', 34127 ocy: '\u043E', 34128 odash: '\u229D', 34129 Odblac: '\u0150', 34130 odblac: '\u0151', 34131 odiv: '\u2A38', 34132 odot: '\u2299', 34133 odsold: '\u29BC', 34134 OElig: '\u0152', 34135 oelig: '\u0153', 34136 ofcir: '\u29BF', 34137 Ofr: '\uD835\uDD12', 34138 ofr: '\uD835\uDD2C', 34139 ogon: '\u02DB', 34140 Ograve: '\u00D2', 34141 ograve: '\u00F2', 34142 ogt: '\u29C1', 34143 ohbar: '\u29B5', 34144 ohm: '\u03A9', 34145 oint: '\u222E', 34146 olarr: '\u21BA', 34147 olcir: '\u29BE', 34148 olcross: '\u29BB', 34149 oline: '\u203E', 34150 olt: '\u29C0', 34151 Omacr: '\u014C', 34152 omacr: '\u014D', 34153 Omega: '\u03A9', 34154 omega: '\u03C9', 34155 Omicron: '\u039F', 34156 omicron: '\u03BF', 34157 omid: '\u29B6', 34158 ominus: '\u2296', 34159 Oopf: '\uD835\uDD46', 34160 oopf: '\uD835\uDD60', 34161 opar: '\u29B7', 34162 OpenCurlyDoubleQuote: '\u201C', 34163 OpenCurlyQuote: '\u2018', 34164 operp: '\u29B9', 34165 oplus: '\u2295', 34166 Or: '\u2A54', 34167 or: '\u2228', 34168 orarr: '\u21BB', 34169 ord: '\u2A5D', 34170 order: '\u2134', 34171 orderof: '\u2134', 34172 ordf: '\u00AA', 34173 ordm: '\u00BA', 34174 origof: '\u22B6', 34175 oror: '\u2A56', 34176 orslope: '\u2A57', 34177 orv: '\u2A5B', 34178 oS: '\u24C8', 34179 Oscr: '\uD835\uDCAA', 34180 oscr: '\u2134', 34181 Oslash: '\u00D8', 34182 oslash: '\u00F8', 34183 osol: '\u2298', 34184 Otilde: '\u00D5', 34185 otilde: '\u00F5', 34186 Otimes: '\u2A37', 34187 otimes: '\u2297', 34188 otimesas: '\u2A36', 34189 Ouml: '\u00D6', 34190 ouml: '\u00F6', 34191 ovbar: '\u233D', 34192 OverBar: '\u203E', 34193 OverBrace: '\u23DE', 34194 OverBracket: '\u23B4', 34195 OverParenthesis: '\u23DC', 34196 par: '\u2225', 34197 para: '\u00B6', 34198 parallel: '\u2225', 34199 parsim: '\u2AF3', 34200 parsl: '\u2AFD', 34201 part: '\u2202', 34202 PartialD: '\u2202', 34203 Pcy: '\u041F', 34204 pcy: '\u043F', 34205 percnt: '\u0025', 34206 period: '\u002E', 34207 permil: '\u2030', 34208 perp: '\u22A5', 34209 pertenk: '\u2031', 34210 Pfr: '\uD835\uDD13', 34211 pfr: '\uD835\uDD2D', 34212 Phi: '\u03A6', 34213 phi: '\u03C6', 34214 phiv: '\u03D5', 34215 phmmat: '\u2133', 34216 phone: '\u260E', 34217 Pi: '\u03A0', 34218 pi: '\u03C0', 34219 pitchfork: '\u22D4', 34220 piv: '\u03D6', 34221 planck: '\u210F', 34222 planckh: '\u210E', 34223 plankv: '\u210F', 34224 plus: '\u002B', 34225 plusacir: '\u2A23', 34226 plusb: '\u229E', 34227 pluscir: '\u2A22', 34228 plusdo: '\u2214', 34229 plusdu: '\u2A25', 34230 pluse: '\u2A72', 34231 PlusMinus: '\u00B1', 34232 plusmn: '\u00B1', 34233 plussim: '\u2A26', 34234 plustwo: '\u2A27', 34235 pm: '\u00B1', 34236 Poincareplane: '\u210C', 34237 pointint: '\u2A15', 34238 Popf: '\u2119', 34239 popf: '\uD835\uDD61', 34240 pound: '\u00A3', 34241 Pr: '\u2ABB', 34242 pr: '\u227A', 34243 prap: '\u2AB7', 34244 prcue: '\u227C', 34245 prE: '\u2AB3', 34246 pre: '\u2AAF', 34247 prec: '\u227A', 34248 precapprox: '\u2AB7', 34249 preccurlyeq: '\u227C', 34250 Precedes: '\u227A', 34251 PrecedesEqual: '\u2AAF', 34252 PrecedesSlantEqual: '\u227C', 34253 PrecedesTilde: '\u227E', 34254 preceq: '\u2AAF', 34255 precnapprox: '\u2AB9', 34256 precneqq: '\u2AB5', 34257 precnsim: '\u22E8', 34258 precsim: '\u227E', 34259 Prime: '\u2033', 34260 prime: '\u2032', 34261 primes: '\u2119', 34262 prnap: '\u2AB9', 34263 prnE: '\u2AB5', 34264 prnsim: '\u22E8', 34265 prod: '\u220F', 34266 Product: '\u220F', 34267 profalar: '\u232E', 34268 profline: '\u2312', 34269 profsurf: '\u2313', 34270 prop: '\u221D', 34271 Proportion: '\u2237', 34272 Proportional: '\u221D', 34273 propto: '\u221D', 34274 prsim: '\u227E', 34275 prurel: '\u22B0', 34276 Pscr: '\uD835\uDCAB', 34277 pscr: '\uD835\uDCC5', 34278 Psi: '\u03A8', 34279 psi: '\u03C8', 34280 puncsp: '\u2008', 34281 Qfr: '\uD835\uDD14', 34282 qfr: '\uD835\uDD2E', 34283 qint: '\u2A0C', 34284 Qopf: '\u211A', 34285 qopf: '\uD835\uDD62', 34286 qprime: '\u2057', 34287 Qscr: '\uD835\uDCAC', 34288 qscr: '\uD835\uDCC6', 34289 quaternions: '\u210D', 34290 quatint: '\u2A16', 34291 quest: '\u003F', 34292 questeq: '\u225F', 34293 QUOT: '\u0022', 34294 quot: '\u0022', 34295 rAarr: '\u21DB', 34296 race: '\u223D\u0331', 34297 Racute: '\u0154', 34298 racute: '\u0155', 34299 radic: '\u221A', 34300 raemptyv: '\u29B3', 34301 Rang: '\u27EB', 34302 rang: '\u27E9', 34303 rangd: '\u2992', 34304 range: '\u29A5', 34305 rangle: '\u27E9', 34306 raquo: '\u00BB', 34307 Rarr: '\u21A0', 34308 rArr: '\u21D2', 34309 rarr: '\u2192', 34310 rarrap: '\u2975', 34311 rarrb: '\u21E5', 34312 rarrbfs: '\u2920', 34313 rarrc: '\u2933', 34314 rarrfs: '\u291E', 34315 rarrhk: '\u21AA', 34316 rarrlp: '\u21AC', 34317 rarrpl: '\u2945', 34318 rarrsim: '\u2974', 34319 Rarrtl: '\u2916', 34320 rarrtl: '\u21A3', 34321 rarrw: '\u219D', 34322 rAtail: '\u291C', 34323 ratail: '\u291A', 34324 ratio: '\u2236', 34325 rationals: '\u211A', 34326 RBarr: '\u2910', 34327 rBarr: '\u290F', 34328 rbarr: '\u290D', 34329 rbbrk: '\u2773', 34330 rbrace: '\u007D', 34331 rbrack: '\u005D', 34332 rbrke: '\u298C', 34333 rbrksld: '\u298E', 34334 rbrkslu: '\u2990', 34335 Rcaron: '\u0158', 34336 rcaron: '\u0159', 34337 Rcedil: '\u0156', 34338 rcedil: '\u0157', 34339 rceil: '\u2309', 34340 rcub: '\u007D', 34341 Rcy: '\u0420', 34342 rcy: '\u0440', 34343 rdca: '\u2937', 34344 rdldhar: '\u2969', 34345 rdquo: '\u201D', 34346 rdquor: '\u201D', 34347 rdsh: '\u21B3', 34348 Re: '\u211C', 34349 real: '\u211C', 34350 realine: '\u211B', 34351 realpart: '\u211C', 34352 reals: '\u211D', 34353 rect: '\u25AD', 34354 REG: '\u00AE', 34355 reg: '\u00AE', 34356 ReverseElement: '\u220B', 34357 ReverseEquilibrium: '\u21CB', 34358 ReverseUpEquilibrium: '\u296F', 34359 rfisht: '\u297D', 34360 rfloor: '\u230B', 34361 Rfr: '\u211C', 34362 rfr: '\uD835\uDD2F', 34363 rHar: '\u2964', 34364 rhard: '\u21C1', 34365 rharu: '\u21C0', 34366 rharul: '\u296C', 34367 Rho: '\u03A1', 34368 rho: '\u03C1', 34369 rhov: '\u03F1', 34370 RightAngleBracket: '\u27E9', 34371 RightArrow: '\u2192', 34372 Rightarrow: '\u21D2', 34373 rightarrow: '\u2192', 34374 RightArrowBar: '\u21E5', 34375 RightArrowLeftArrow: '\u21C4', 34376 rightarrowtail: '\u21A3', 34377 RightCeiling: '\u2309', 34378 RightDoubleBracket: '\u27E7', 34379 RightDownTeeVector: '\u295D', 34380 RightDownVector: '\u21C2', 34381 RightDownVectorBar: '\u2955', 34382 RightFloor: '\u230B', 34383 rightharpoondown: '\u21C1', 34384 rightharpoonup: '\u21C0', 34385 rightleftarrows: '\u21C4', 34386 rightleftharpoons: '\u21CC', 34387 rightrightarrows: '\u21C9', 34388 rightsquigarrow: '\u219D', 34389 RightTee: '\u22A2', 34390 RightTeeArrow: '\u21A6', 34391 RightTeeVector: '\u295B', 34392 rightthreetimes: '\u22CC', 34393 RightTriangle: '\u22B3', 34394 RightTriangleBar: '\u29D0', 34395 RightTriangleEqual: '\u22B5', 34396 RightUpDownVector: '\u294F', 34397 RightUpTeeVector: '\u295C', 34398 RightUpVector: '\u21BE', 34399 RightUpVectorBar: '\u2954', 34400 RightVector: '\u21C0', 34401 RightVectorBar: '\u2953', 34402 ring: '\u02DA', 34403 risingdotseq: '\u2253', 34404 rlarr: '\u21C4', 34405 rlhar: '\u21CC', 34406 rlm: '\u200F', 34407 rmoust: '\u23B1', 34408 rmoustache: '\u23B1', 34409 rnmid: '\u2AEE', 34410 roang: '\u27ED', 34411 roarr: '\u21FE', 34412 robrk: '\u27E7', 34413 ropar: '\u2986', 34414 Ropf: '\u211D', 34415 ropf: '\uD835\uDD63', 34416 roplus: '\u2A2E', 34417 rotimes: '\u2A35', 34418 RoundImplies: '\u2970', 34419 rpar: '\u0029', 34420 rpargt: '\u2994', 34421 rppolint: '\u2A12', 34422 rrarr: '\u21C9', 34423 Rrightarrow: '\u21DB', 34424 rsaquo: '\u203A', 34425 Rscr: '\u211B', 34426 rscr: '\uD835\uDCC7', 34427 Rsh: '\u21B1', 34428 rsh: '\u21B1', 34429 rsqb: '\u005D', 34430 rsquo: '\u2019', 34431 rsquor: '\u2019', 34432 rthree: '\u22CC', 34433 rtimes: '\u22CA', 34434 rtri: '\u25B9', 34435 rtrie: '\u22B5', 34436 rtrif: '\u25B8', 34437 rtriltri: '\u29CE', 34438 RuleDelayed: '\u29F4', 34439 ruluhar: '\u2968', 34440 rx: '\u211E', 34441 Sacute: '\u015A', 34442 sacute: '\u015B', 34443 sbquo: '\u201A', 34444 Sc: '\u2ABC', 34445 sc: '\u227B', 34446 scap: '\u2AB8', 34447 Scaron: '\u0160', 34448 scaron: '\u0161', 34449 sccue: '\u227D', 34450 scE: '\u2AB4', 34451 sce: '\u2AB0', 34452 Scedil: '\u015E', 34453 scedil: '\u015F', 34454 Scirc: '\u015C', 34455 scirc: '\u015D', 34456 scnap: '\u2ABA', 34457 scnE: '\u2AB6', 34458 scnsim: '\u22E9', 34459 scpolint: '\u2A13', 34460 scsim: '\u227F', 34461 Scy: '\u0421', 34462 scy: '\u0441', 34463 sdot: '\u22C5', 34464 sdotb: '\u22A1', 34465 sdote: '\u2A66', 34466 searhk: '\u2925', 34467 seArr: '\u21D8', 34468 searr: '\u2198', 34469 searrow: '\u2198', 34470 sect: '\u00A7', 34471 semi: '\u003B', 34472 seswar: '\u2929', 34473 setminus: '\u2216', 34474 setmn: '\u2216', 34475 sext: '\u2736', 34476 Sfr: '\uD835\uDD16', 34477 sfr: '\uD835\uDD30', 34478 sfrown: '\u2322', 34479 sharp: '\u266F', 34480 SHCHcy: '\u0429', 34481 shchcy: '\u0449', 34482 SHcy: '\u0428', 34483 shcy: '\u0448', 34484 ShortDownArrow: '\u2193', 34485 ShortLeftArrow: '\u2190', 34486 shortmid: '\u2223', 34487 shortparallel: '\u2225', 34488 ShortRightArrow: '\u2192', 34489 ShortUpArrow: '\u2191', 34490 shy: '\u00AD', 34491 Sigma: '\u03A3', 34492 sigma: '\u03C3', 34493 sigmaf: '\u03C2', 34494 sigmav: '\u03C2', 34495 sim: '\u223C', 34496 simdot: '\u2A6A', 34497 sime: '\u2243', 34498 simeq: '\u2243', 34499 simg: '\u2A9E', 34500 simgE: '\u2AA0', 34501 siml: '\u2A9D', 34502 simlE: '\u2A9F', 34503 simne: '\u2246', 34504 simplus: '\u2A24', 34505 simrarr: '\u2972', 34506 slarr: '\u2190', 34507 SmallCircle: '\u2218', 34508 smallsetminus: '\u2216', 34509 smashp: '\u2A33', 34510 smeparsl: '\u29E4', 34511 smid: '\u2223', 34512 smile: '\u2323', 34513 smt: '\u2AAA', 34514 smte: '\u2AAC', 34515 smtes: '\u2AAC\uFE00', 34516 SOFTcy: '\u042C', 34517 softcy: '\u044C', 34518 sol: '\u002F', 34519 solb: '\u29C4', 34520 solbar: '\u233F', 34521 Sopf: '\uD835\uDD4A', 34522 sopf: '\uD835\uDD64', 34523 spades: '\u2660', 34524 spadesuit: '\u2660', 34525 spar: '\u2225', 34526 sqcap: '\u2293', 34527 sqcaps: '\u2293\uFE00', 34528 sqcup: '\u2294', 34529 sqcups: '\u2294\uFE00', 34530 Sqrt: '\u221A', 34531 sqsub: '\u228F', 34532 sqsube: '\u2291', 34533 sqsubset: '\u228F', 34534 sqsubseteq: '\u2291', 34535 sqsup: '\u2290', 34536 sqsupe: '\u2292', 34537 sqsupset: '\u2290', 34538 sqsupseteq: '\u2292', 34539 squ: '\u25A1', 34540 Square: '\u25A1', 34541 square: '\u25A1', 34542 SquareIntersection: '\u2293', 34543 SquareSubset: '\u228F', 34544 SquareSubsetEqual: '\u2291', 34545 SquareSuperset: '\u2290', 34546 SquareSupersetEqual: '\u2292', 34547 SquareUnion: '\u2294', 34548 squarf: '\u25AA', 34549 squf: '\u25AA', 34550 srarr: '\u2192', 34551 Sscr: '\uD835\uDCAE', 34552 sscr: '\uD835\uDCC8', 34553 ssetmn: '\u2216', 34554 ssmile: '\u2323', 34555 sstarf: '\u22C6', 34556 Star: '\u22C6', 34557 star: '\u2606', 34558 starf: '\u2605', 34559 straightepsilon: '\u03F5', 34560 straightphi: '\u03D5', 34561 strns: '\u00AF', 34562 Sub: '\u22D0', 34563 sub: '\u2282', 34564 subdot: '\u2ABD', 34565 subE: '\u2AC5', 34566 sube: '\u2286', 34567 subedot: '\u2AC3', 34568 submult: '\u2AC1', 34569 subnE: '\u2ACB', 34570 subne: '\u228A', 34571 subplus: '\u2ABF', 34572 subrarr: '\u2979', 34573 Subset: '\u22D0', 34574 subset: '\u2282', 34575 subseteq: '\u2286', 34576 subseteqq: '\u2AC5', 34577 SubsetEqual: '\u2286', 34578 subsetneq: '\u228A', 34579 subsetneqq: '\u2ACB', 34580 subsim: '\u2AC7', 34581 subsub: '\u2AD5', 34582 subsup: '\u2AD3', 34583 succ: '\u227B', 34584 succapprox: '\u2AB8', 34585 succcurlyeq: '\u227D', 34586 Succeeds: '\u227B', 34587 SucceedsEqual: '\u2AB0', 34588 SucceedsSlantEqual: '\u227D', 34589 SucceedsTilde: '\u227F', 34590 succeq: '\u2AB0', 34591 succnapprox: '\u2ABA', 34592 succneqq: '\u2AB6', 34593 succnsim: '\u22E9', 34594 succsim: '\u227F', 34595 SuchThat: '\u220B', 34596 Sum: '\u2211', 34597 sum: '\u2211', 34598 sung: '\u266A', 34599 Sup: '\u22D1', 34600 sup: '\u2283', 34601 sup1: '\u00B9', 34602 sup2: '\u00B2', 34603 sup3: '\u00B3', 34604 supdot: '\u2ABE', 34605 supdsub: '\u2AD8', 34606 supE: '\u2AC6', 34607 supe: '\u2287', 34608 supedot: '\u2AC4', 34609 Superset: '\u2283', 34610 SupersetEqual: '\u2287', 34611 suphsol: '\u27C9', 34612 suphsub: '\u2AD7', 34613 suplarr: '\u297B', 34614 supmult: '\u2AC2', 34615 supnE: '\u2ACC', 34616 supne: '\u228B', 34617 supplus: '\u2AC0', 34618 Supset: '\u22D1', 34619 supset: '\u2283', 34620 supseteq: '\u2287', 34621 supseteqq: '\u2AC6', 34622 supsetneq: '\u228B', 34623 supsetneqq: '\u2ACC', 34624 supsim: '\u2AC8', 34625 supsub: '\u2AD4', 34626 supsup: '\u2AD6', 34627 swarhk: '\u2926', 34628 swArr: '\u21D9', 34629 swarr: '\u2199', 34630 swarrow: '\u2199', 34631 swnwar: '\u292A', 34632 szlig: '\u00DF', 34633 Tab: '\u0009', 34634 target: '\u2316', 34635 Tau: '\u03A4', 34636 tau: '\u03C4', 34637 tbrk: '\u23B4', 34638 Tcaron: '\u0164', 34639 tcaron: '\u0165', 34640 Tcedil: '\u0162', 34641 tcedil: '\u0163', 34642 Tcy: '\u0422', 34643 tcy: '\u0442', 34644 tdot: '\u20DB', 34645 telrec: '\u2315', 34646 Tfr: '\uD835\uDD17', 34647 tfr: '\uD835\uDD31', 34648 there4: '\u2234', 34649 Therefore: '\u2234', 34650 therefore: '\u2234', 34651 Theta: '\u0398', 34652 theta: '\u03B8', 34653 thetasym: '\u03D1', 34654 thetav: '\u03D1', 34655 thickapprox: '\u2248', 34656 thicksim: '\u223C', 34657 ThickSpace: '\u205F\u200A', 34658 thinsp: '\u2009', 34659 ThinSpace: '\u2009', 34660 thkap: '\u2248', 34661 thksim: '\u223C', 34662 THORN: '\u00DE', 34663 thorn: '\u00FE', 34664 Tilde: '\u223C', 34665 tilde: '\u02DC', 34666 TildeEqual: '\u2243', 34667 TildeFullEqual: '\u2245', 34668 TildeTilde: '\u2248', 34669 times: '\u00D7', 34670 timesb: '\u22A0', 34671 timesbar: '\u2A31', 34672 timesd: '\u2A30', 34673 tint: '\u222D', 34674 toea: '\u2928', 34675 top: '\u22A4', 34676 topbot: '\u2336', 34677 topcir: '\u2AF1', 34678 Topf: '\uD835\uDD4B', 34679 topf: '\uD835\uDD65', 34680 topfork: '\u2ADA', 34681 tosa: '\u2929', 34682 tprime: '\u2034', 34683 TRADE: '\u2122', 34684 trade: '\u2122', 34685 triangle: '\u25B5', 34686 triangledown: '\u25BF', 34687 triangleleft: '\u25C3', 34688 trianglelefteq: '\u22B4', 34689 triangleq: '\u225C', 34690 triangleright: '\u25B9', 34691 trianglerighteq: '\u22B5', 34692 tridot: '\u25EC', 34693 trie: '\u225C', 34694 triminus: '\u2A3A', 34695 TripleDot: '\u20DB', 34696 triplus: '\u2A39', 34697 trisb: '\u29CD', 34698 tritime: '\u2A3B', 34699 trpezium: '\u23E2', 34700 Tscr: '\uD835\uDCAF', 34701 tscr: '\uD835\uDCC9', 34702 TScy: '\u0426', 34703 tscy: '\u0446', 34704 TSHcy: '\u040B', 34705 tshcy: '\u045B', 34706 Tstrok: '\u0166', 34707 tstrok: '\u0167', 34708 twixt: '\u226C', 34709 twoheadleftarrow: '\u219E', 34710 twoheadrightarrow: '\u21A0', 34711 Uacute: '\u00DA', 34712 uacute: '\u00FA', 34713 Uarr: '\u219F', 34714 uArr: '\u21D1', 34715 uarr: '\u2191', 34716 Uarrocir: '\u2949', 34717 Ubrcy: '\u040E', 34718 ubrcy: '\u045E', 34719 Ubreve: '\u016C', 34720 ubreve: '\u016D', 34721 Ucirc: '\u00DB', 34722 ucirc: '\u00FB', 34723 Ucy: '\u0423', 34724 ucy: '\u0443', 34725 udarr: '\u21C5', 34726 Udblac: '\u0170', 34727 udblac: '\u0171', 34728 udhar: '\u296E', 34729 ufisht: '\u297E', 34730 Ufr: '\uD835\uDD18', 34731 ufr: '\uD835\uDD32', 34732 Ugrave: '\u00D9', 34733 ugrave: '\u00F9', 34734 uHar: '\u2963', 34735 uharl: '\u21BF', 34736 uharr: '\u21BE', 34737 uhblk: '\u2580', 34738 ulcorn: '\u231C', 34739 ulcorner: '\u231C', 34740 ulcrop: '\u230F', 34741 ultri: '\u25F8', 34742 Umacr: '\u016A', 34743 umacr: '\u016B', 34744 uml: '\u00A8', 34745 UnderBar: '\u005F', 34746 UnderBrace: '\u23DF', 34747 UnderBracket: '\u23B5', 34748 UnderParenthesis: '\u23DD', 34749 Union: '\u22C3', 34750 UnionPlus: '\u228E', 34751 Uogon: '\u0172', 34752 uogon: '\u0173', 34753 Uopf: '\uD835\uDD4C', 34754 uopf: '\uD835\uDD66', 34755 UpArrow: '\u2191', 34756 Uparrow: '\u21D1', 34757 uparrow: '\u2191', 34758 UpArrowBar: '\u2912', 34759 UpArrowDownArrow: '\u21C5', 34760 UpDownArrow: '\u2195', 34761 Updownarrow: '\u21D5', 34762 updownarrow: '\u2195', 34763 UpEquilibrium: '\u296E', 34764 upharpoonleft: '\u21BF', 34765 upharpoonright: '\u21BE', 34766 uplus: '\u228E', 34767 UpperLeftArrow: '\u2196', 34768 UpperRightArrow: '\u2197', 34769 Upsi: '\u03D2', 34770 upsi: '\u03C5', 34771 upsih: '\u03D2', 34772 Upsilon: '\u03A5', 34773 upsilon: '\u03C5', 34774 UpTee: '\u22A5', 34775 UpTeeArrow: '\u21A5', 34776 upuparrows: '\u21C8', 34777 urcorn: '\u231D', 34778 urcorner: '\u231D', 34779 urcrop: '\u230E', 34780 Uring: '\u016E', 34781 uring: '\u016F', 34782 urtri: '\u25F9', 34783 Uscr: '\uD835\uDCB0', 34784 uscr: '\uD835\uDCCA', 34785 utdot: '\u22F0', 34786 Utilde: '\u0168', 34787 utilde: '\u0169', 34788 utri: '\u25B5', 34789 utrif: '\u25B4', 34790 uuarr: '\u21C8', 34791 Uuml: '\u00DC', 34792 uuml: '\u00FC', 34793 uwangle: '\u29A7', 34794 vangrt: '\u299C', 34795 varepsilon: '\u03F5', 34796 varkappa: '\u03F0', 34797 varnothing: '\u2205', 34798 varphi: '\u03D5', 34799 varpi: '\u03D6', 34800 varpropto: '\u221D', 34801 vArr: '\u21D5', 34802 varr: '\u2195', 34803 varrho: '\u03F1', 34804 varsigma: '\u03C2', 34805 varsubsetneq: '\u228A\uFE00', 34806 varsubsetneqq: '\u2ACB\uFE00', 34807 varsupsetneq: '\u228B\uFE00', 34808 varsupsetneqq: '\u2ACC\uFE00', 34809 vartheta: '\u03D1', 34810 vartriangleleft: '\u22B2', 34811 vartriangleright: '\u22B3', 34812 Vbar: '\u2AEB', 34813 vBar: '\u2AE8', 34814 vBarv: '\u2AE9', 34815 Vcy: '\u0412', 34816 vcy: '\u0432', 34817 VDash: '\u22AB', 34818 Vdash: '\u22A9', 34819 vDash: '\u22A8', 34820 vdash: '\u22A2', 34821 Vdashl: '\u2AE6', 34822 Vee: '\u22C1', 34823 vee: '\u2228', 34824 veebar: '\u22BB', 34825 veeeq: '\u225A', 34826 vellip: '\u22EE', 34827 Verbar: '\u2016', 34828 verbar: '\u007C', 34829 Vert: '\u2016', 34830 vert: '\u007C', 34831 VerticalBar: '\u2223', 34832 VerticalLine: '\u007C', 34833 VerticalSeparator: '\u2758', 34834 VerticalTilde: '\u2240', 34835 VeryThinSpace: '\u200A', 34836 Vfr: '\uD835\uDD19', 34837 vfr: '\uD835\uDD33', 34838 vltri: '\u22B2', 34839 vnsub: '\u2282\u20D2', 34840 vnsup: '\u2283\u20D2', 34841 Vopf: '\uD835\uDD4D', 34842 vopf: '\uD835\uDD67', 34843 vprop: '\u221D', 34844 vrtri: '\u22B3', 34845 Vscr: '\uD835\uDCB1', 34846 vscr: '\uD835\uDCCB', 34847 vsubnE: '\u2ACB\uFE00', 34848 vsubne: '\u228A\uFE00', 34849 vsupnE: '\u2ACC\uFE00', 34850 vsupne: '\u228B\uFE00', 34851 Vvdash: '\u22AA', 34852 vzigzag: '\u299A', 34853 Wcirc: '\u0174', 34854 wcirc: '\u0175', 34855 wedbar: '\u2A5F', 34856 Wedge: '\u22C0', 34857 wedge: '\u2227', 34858 wedgeq: '\u2259', 34859 weierp: '\u2118', 34860 Wfr: '\uD835\uDD1A', 34861 wfr: '\uD835\uDD34', 34862 Wopf: '\uD835\uDD4E', 34863 wopf: '\uD835\uDD68', 34864 wp: '\u2118', 34865 wr: '\u2240', 34866 wreath: '\u2240', 34867 Wscr: '\uD835\uDCB2', 34868 wscr: '\uD835\uDCCC', 34869 xcap: '\u22C2', 34870 xcirc: '\u25EF', 34871 xcup: '\u22C3', 34872 xdtri: '\u25BD', 34873 Xfr: '\uD835\uDD1B', 34874 xfr: '\uD835\uDD35', 34875 xhArr: '\u27FA', 34876 xharr: '\u27F7', 34877 Xi: '\u039E', 34878 xi: '\u03BE', 34879 xlArr: '\u27F8', 34880 xlarr: '\u27F5', 34881 xmap: '\u27FC', 34882 xnis: '\u22FB', 34883 xodot: '\u2A00', 34884 Xopf: '\uD835\uDD4F', 34885 xopf: '\uD835\uDD69', 34886 xoplus: '\u2A01', 34887 xotime: '\u2A02', 34888 xrArr: '\u27F9', 34889 xrarr: '\u27F6', 34890 Xscr: '\uD835\uDCB3', 34891 xscr: '\uD835\uDCCD', 34892 xsqcup: '\u2A06', 34893 xuplus: '\u2A04', 34894 xutri: '\u25B3', 34895 xvee: '\u22C1', 34896 xwedge: '\u22C0', 34897 Yacute: '\u00DD', 34898 yacute: '\u00FD', 34899 YAcy: '\u042F', 34900 yacy: '\u044F', 34901 Ycirc: '\u0176', 34902 ycirc: '\u0177', 34903 Ycy: '\u042B', 34904 ycy: '\u044B', 34905 yen: '\u00A5', 34906 Yfr: '\uD835\uDD1C', 34907 yfr: '\uD835\uDD36', 34908 YIcy: '\u0407', 34909 yicy: '\u0457', 34910 Yopf: '\uD835\uDD50', 34911 yopf: '\uD835\uDD6A', 34912 Yscr: '\uD835\uDCB4', 34913 yscr: '\uD835\uDCCE', 34914 YUcy: '\u042E', 34915 yucy: '\u044E', 34916 Yuml: '\u0178', 34917 yuml: '\u00FF', 34918 Zacute: '\u0179', 34919 zacute: '\u017A', 34920 Zcaron: '\u017D', 34921 zcaron: '\u017E', 34922 Zcy: '\u0417', 34923 zcy: '\u0437', 34924 Zdot: '\u017B', 34925 zdot: '\u017C', 34926 zeetrf: '\u2128', 34927 ZeroWidthSpace: '\u200B', 34928 Zeta: '\u0396', 34929 zeta: '\u03B6', 34930 Zfr: '\u2128', 34931 zfr: '\uD835\uDD37', 34932 ZHcy: '\u0416', 34933 zhcy: '\u0436', 34934 zigrarr: '\u21DD', 34935 Zopf: '\u2124', 34936 zopf: '\uD835\uDD6B', 34937 Zscr: '\uD835\uDCB5', 34938 zscr: '\uD835\uDCCF', 34939 zwj: '\u200D', 34940 zwnj: '\u200C' 34941 }); 34942 34943 /** 34944 * @deprecated use `HTML_ENTITIES` instead 34945 * @see HTML_ENTITIES 34946 */ 34947 exports.entityMap = exports.HTML_ENTITIES; 34948 }); 34949 entities.XML_ENTITIES; 34950 entities.HTML_ENTITIES; 34951 entities.entityMap; 34952 34953 var NAMESPACE$1 = conventions.NAMESPACE;
vendor: 4,452 bytes, lines 34954-35051
34954 34955 //[4] NameStartChar ::= ":" | [A-Z] | "_" | [a-z] | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF] | [#x370-#x37D] | [#x37F-#x1FFF] | [#x200C-#x200D] | [#x2070-#x218F] | [#x2C00-#x2FEF] | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] | [#x10000-#xEFFFF] 34956 //[4a] NameChar ::= NameStartChar | "-" | "." | [0-9] | #xB7 | [#x0300-#x036F] | [#x203F-#x2040] 34957 //[5] Name ::= NameStartChar (NameChar)* 34958 var nameStartChar = /[A-Z_a-z\xC0-\xD6\xD8-\xF6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]/; //\u10000-\uEFFFF 34959 var nameChar = new RegExp("[\\-\\.0-9" + nameStartChar.source.slice(1, -1) + "\\u00B7\\u0300-\\u036F\\u203F-\\u2040]"); 34960 var tagNamePattern = new RegExp('^' + nameStartChar.source + nameChar.source + '*(?:\:' + nameStartChar.source + nameChar.source + '*)?$'); 34961 //var tagNamePattern = /^[a-zA-Z_][\w\-\.]*(?:\:[a-zA-Z_][\w\-\.]*)?$/ 34962 //var handlers = 'resolveEntity,getExternalSubset,characters,endDocument,endElement,endPrefixMapping,ignorableWhitespace,processingInstruction,setDocumentLocator,skippedEntity,startDocument,startElement,startPrefixMapping,notationDecl,unparsedEntityDecl,error,fatalError,warning,attributeDecl,elementDecl,externalEntityDecl,internalEntityDecl,comment,endCDATA,endDTD,endEntity,startCDATA,startDTD,startEntity'.split(',') 34963 34964 //S_TAG, S_ATTR, S_EQ, S_ATTR_NOQUOT_VALUE 34965 //S_ATTR_SPACE, S_ATTR_END, S_TAG_SPACE, S_TAG_CLOSE 34966 var S_TAG = 0; //tag name offerring 34967 var S_ATTR = 1; //attr name offerring 34968 var S_ATTR_SPACE = 2; //attr name end and space offer 34969 var S_EQ = 3; //=space? 34970 var S_ATTR_NOQUOT_VALUE = 4; //attr value(no quot value only) 34971 var S_ATTR_END = 5; //attr value end and no space(quot end) 34972 var S_TAG_SPACE = 6; //(attr value end || tag end ) && (space offer) 34973 var S_TAG_CLOSE = 7; //closed el<el /> 34974 34975 /** 34976 * Creates an error that will not be caught by XMLReader aka the SAX parser. 34977 * 34978 * @param {string} message 34979 * @param {any?} locator Optional, can provide details about the location in the source 34980 * @constructor 34981 */ 34982 function ParseError$1(message, locator) { 34983 this.message = message; 34984 this.locator = locator; 34985 if (Error.captureStackTrace) Error.captureStackTrace(this, ParseError$1); 34986 } 34987 ParseError$1.prototype = new Error(); 34988 ParseError$1.prototype.name = ParseError$1.name; 34989 function XMLReader$1() {} 34990 XMLReader$1.prototype = { 34991 parse: function (source, defaultNSMap, entityMap) { 34992 var domBuilder = this.domBuilder; 34993 domBuilder.startDocument(); 34994 _copy(defaultNSMap, defaultNSMap = {}); 34995 parse$1(source, defaultNSMap, entityMap, domBuilder, this.errorHandler); 34996 domBuilder.endDocument(); 34997 } 34998 }; 34999 function parse$1(source, defaultNSMapCopy, entityMap, domBuilder, errorHandler) { 35000 function fixedFromCharCode(code) { 35001 // String.prototype.fromCharCode does not supports 35002 // > 2 bytes unicode chars directly 35003 if (code > 0xffff) { 35004 code -= 0x10000; 35005 var surrogate1 = 0xd800 + (code >> 10), 35006 surrogate2 = 0xdc00 + (code & 0x3ff); 35007 return String.fromCharCode(surrogate1, surrogate2); 35008 } else { 35009 return String.fromCharCode(code); 35010 } 35011 } 35012 function entityReplacer(a) { 35013 var k = a.slice(1, -1); 35014 if (Object.hasOwnProperty.call(entityMap, k)) { 35015 return entityMap[k]; 35016 } else if (k.charAt(0) === '#') { 35017 return fixedFromCharCode(parseInt(k.substr(1).replace('x', '0x'))); 35018 } else { 35019 errorHandler.error('entity not found:' + a); 35020 return a; 35021 } 35022 } 35023 function appendText(end) { 35024 //has some bugs 35025 if (end > start) { 35026 var xt = source.substring(start, end).replace(/&#?\w+;/g, entityReplacer); 35027 locator && position(start); 35028 domBuilder.characters(xt, 0, end - start); 35029 start = end; 35030 } 35031 } 35032 function position(p, m) { 35033 while (p >= lineEnd && (m = linePattern.exec(source))) { 35034 lineStart = m.index; 35035 lineEnd = lineStart + m[0].length; 35036 locator.lineNumber++; 35037 //console.log('line++:',locator,startPos,endPos) 35038 } 35039 35040 locator.columnNumber = p - lineStart + 1; 35041 } 35042 var lineStart = 0; 35043 var lineEnd = 0; 35044 var linePattern = /.*(?:\r\n?|\n)|.*$/g; 35045 var locator = domBuilder.locator; 35046 var parseStack = [{ 35047 currentNSMap: defaultNSMapCopy 35048 }]; 35049 var closeMap = {}; 35050 var start = 0; 35051 while (true) {
vendor: 16,142 bytes, lines 35052-35508
35052 try { 35053 var tagStart = source.indexOf('<', start); 35054 if (tagStart < 0) { 35055 if (!source.substr(start).match(/^\s*$/)) { 35056 var doc = domBuilder.doc; 35057 var text = doc.createTextNode(source.substr(start)); 35058 doc.appendChild(text); 35059 domBuilder.currentElement = text; 35060 } 35061 return; 35062 } 35063 if (tagStart > start) { 35064 appendText(tagStart); 35065 } 35066 switch (source.charAt(tagStart + 1)) { 35067 case '/': 35068 var end = source.indexOf('>', tagStart + 3); 35069 var tagName = source.substring(tagStart + 2, end).replace(/[ \t\n\r]+$/g, ''); 35070 var config = parseStack.pop(); 35071 if (end < 0) { 35072 tagName = source.substring(tagStart + 2).replace(/[\s<].*/, ''); 35073 errorHandler.error("end tag name: " + tagName + ' is not complete:' + config.tagName); 35074 end = tagStart + 1 + tagName.length; 35075 } else if (tagName.match(/\s</)) { 35076 tagName = tagName.replace(/[\s<].*/, ''); 35077 errorHandler.error("end tag name: " + tagName + ' maybe not complete'); 35078 end = tagStart + 1 + tagName.length; 35079 } 35080 var localNSMap = config.localNSMap; 35081 var endMatch = config.tagName == tagName; 35082 var endIgnoreCaseMach = endMatch || config.tagName && config.tagName.toLowerCase() == tagName.toLowerCase(); 35083 if (endIgnoreCaseMach) { 35084 domBuilder.endElement(config.uri, config.localName, tagName); 35085 if (localNSMap) { 35086 for (var prefix in localNSMap) { 35087 if (Object.prototype.hasOwnProperty.call(localNSMap, prefix)) { 35088 domBuilder.endPrefixMapping(prefix); 35089 } 35090 } 35091 } 35092 if (!endMatch) { 35093 errorHandler.fatalError("end tag name: " + tagName + ' is not match the current start tagName:' + config.tagName); // No known test case 35094 } 35095 } else { 35096 parseStack.push(config); 35097 } 35098 end++; 35099 break; 35100 // end elment 35101 case '?': 35102 // <?...?> 35103 locator && position(tagStart); 35104 end = parseInstruction(source, tagStart, domBuilder); 35105 break; 35106 case '!': 35107 // <!doctype,<![CDATA,<!-- 35108 locator && position(tagStart); 35109 end = parseDCC(source, tagStart, domBuilder, errorHandler); 35110 break; 35111 default: 35112 locator && position(tagStart); 35113 var el = new ElementAttributes(); 35114 var currentNSMap = parseStack[parseStack.length - 1].currentNSMap; 35115 //elStartEnd 35116 var end = parseElementStartPart(source, tagStart, el, currentNSMap, entityReplacer, errorHandler); 35117 var len = el.length; 35118 if (!el.closed && fixSelfClosed(source, end, el.tagName, closeMap)) { 35119 el.closed = true; 35120 if (!entityMap.nbsp) { 35121 errorHandler.warning('unclosed xml attribute'); 35122 } 35123 } 35124 if (locator && len) { 35125 var locator2 = copyLocator(locator, {}); 35126 //try{//attribute position fixed 35127 for (var i = 0; i < len; i++) { 35128 var a = el[i]; 35129 position(a.offset); 35130 a.locator = copyLocator(locator, {}); 35131 } 35132 domBuilder.locator = locator2; 35133 if (appendElement$1(el, domBuilder, currentNSMap)) { 35134 parseStack.push(el); 35135 } 35136 domBuilder.locator = locator; 35137 } else { 35138 if (appendElement$1(el, domBuilder, currentNSMap)) { 35139 parseStack.push(el); 35140 } 35141 } 35142 if (NAMESPACE$1.isHTML(el.uri) && !el.closed) { 35143 end = parseHtmlSpecialContent(source, end, el.tagName, entityReplacer, domBuilder); 35144 } else { 35145 end++; 35146 } 35147 } 35148 } catch (e) { 35149 if (e instanceof ParseError$1) { 35150 throw e; 35151 } 35152 errorHandler.error('element parse error: ' + e); 35153 end = -1; 35154 } 35155 if (end > start) { 35156 start = end; 35157 } else { 35158 //TODO: è¿éæå¯è½saxåéï¼æä½ç½®é误é£é© 35159 appendText(Math.max(tagStart, start) + 1); 35160 } 35161 } 35162 } 35163 function copyLocator(f, t) { 35164 t.lineNumber = f.lineNumber; 35165 t.columnNumber = f.columnNumber; 35166 return t; 35167 } 35168 35169 /** 35170 * @see #appendElement(source,elStartEnd,el,selfClosed,entityReplacer,domBuilder,parseStack); 35171 * @return end of the elementStartPart(end of elementEndPart for selfClosed el) 35172 */ 35173 function parseElementStartPart(source, start, el, currentNSMap, entityReplacer, errorHandler) { 35174 /** 35175 * @param {string} qname 35176 * @param {string} value 35177 * @param {number} startIndex 35178 */ 35179 function addAttribute(qname, value, startIndex) { 35180 if (el.attributeNames.hasOwnProperty(qname)) { 35181 errorHandler.fatalError('Attribute ' + qname + ' redefined'); 35182 } 35183 el.addValue(qname, 35184 // @see https://www.w3.org/TR/xml/#AVNormalize 35185 // since the xmldom sax parser does not "interpret" DTD the following is not implemented: 35186 // - recursive replacement of (DTD) entity references 35187 // - trimming and collapsing multiple spaces into a single one for attributes that are not of type CDATA 35188 value.replace(/[\t\n\r]/g, ' ').replace(/&#?\w+;/g, entityReplacer), startIndex); 35189 } 35190 var attrName; 35191 var value; 35192 var p = ++start; 35193 var s = S_TAG; //status 35194 while (true) { 35195 var c = source.charAt(p); 35196 switch (c) { 35197 case '=': 35198 if (s === S_ATTR) { 35199 //attrName 35200 attrName = source.slice(start, p); 35201 s = S_EQ; 35202 } else if (s === S_ATTR_SPACE) { 35203 s = S_EQ; 35204 } else { 35205 //fatalError: equal must after attrName or space after attrName 35206 throw new Error('attribute equal must after attrName'); // No known test case 35207 } 35208 35209 break; 35210 case '\'': 35211 case '"': 35212 if (s === S_EQ || s === S_ATTR //|| s == S_ATTR_SPACE 35213 ) { 35214 //equal 35215 if (s === S_ATTR) { 35216 errorHandler.warning('attribute value must after "="'); 35217 attrName = source.slice(start, p); 35218 } 35219 start = p + 1; 35220 p = source.indexOf(c, start); 35221 if (p > 0) { 35222 value = source.slice(start, p); 35223 addAttribute(attrName, value, start - 1); 35224 s = S_ATTR_END; 35225 } else { 35226 //fatalError: no end quot match 35227 throw new Error('attribute value no end \'' + c + '\' match'); 35228 } 35229 } else if (s == S_ATTR_NOQUOT_VALUE) { 35230 value = source.slice(start, p); 35231 addAttribute(attrName, value, start); 35232 errorHandler.warning('attribute "' + attrName + '" missed start quot(' + c + ')!!'); 35233 start = p + 1; 35234 s = S_ATTR_END; 35235 } else { 35236 //fatalError: no equal before 35237 throw new Error('attribute value must after "="'); // No known test case 35238 } 35239 35240 break; 35241 case '/': 35242 switch (s) { 35243 case S_TAG: 35244 el.setTagName(source.slice(start, p)); 35245 case S_ATTR_END: 35246 case S_TAG_SPACE: 35247 case S_TAG_CLOSE: 35248 s = S_TAG_CLOSE; 35249 el.closed = true; 35250 case S_ATTR_NOQUOT_VALUE: 35251 case S_ATTR: 35252 break; 35253 case S_ATTR_SPACE: 35254 el.closed = true; 35255 break; 35256 //case S_EQ: 35257 default: 35258 throw new Error("attribute invalid close char('/')"); 35259 // No known test case 35260 } 35261 35262 break; 35263 case '': 35264 //end document 35265 errorHandler.error('unexpected end of input'); 35266 if (s == S_TAG) { 35267 el.setTagName(source.slice(start, p)); 35268 } 35269 return p; 35270 case '>': 35271 switch (s) { 35272 case S_TAG: 35273 el.setTagName(source.slice(start, p)); 35274 case S_ATTR_END: 35275 case S_TAG_SPACE: 35276 case S_TAG_CLOSE: 35277 break; 35278 //normal 35279 case S_ATTR_NOQUOT_VALUE: //Compatible state 35280 case S_ATTR: 35281 value = source.slice(start, p); 35282 if (value.slice(-1) === '/') { 35283 el.closed = true; 35284 value = value.slice(0, -1); 35285 } 35286 case S_ATTR_SPACE: 35287 if (s === S_ATTR_SPACE) { 35288 value = attrName; 35289 } 35290 if (s == S_ATTR_NOQUOT_VALUE) { 35291 errorHandler.warning('attribute "' + value + '" missed quot(")!'); 35292 addAttribute(attrName, value, start); 35293 } else { 35294 if (!NAMESPACE$1.isHTML(currentNSMap['']) || !value.match(/^(?:disabled|checked|selected)$/i)) { 35295 errorHandler.warning('attribute "' + value + '" missed value!! "' + value + '" instead!!'); 35296 } 35297 addAttribute(value, value, start); 35298 } 35299 break; 35300 case S_EQ: 35301 throw new Error('attribute value missed!!'); 35302 } 35303 // console.log(tagName,tagNamePattern,tagNamePattern.test(tagName)) 35304 return p; 35305 /*xml space '\x20' | #x9 | #xD | #xA; */ 35306 case '\u0080': 35307 c = ' '; 35308 default: 35309 if (c <= ' ') { 35310 //space 35311 switch (s) { 35312 case S_TAG: 35313 el.setTagName(source.slice(start, p)); //tagName 35314 s = S_TAG_SPACE; 35315 break; 35316 case S_ATTR: 35317 attrName = source.slice(start, p); 35318 s = S_ATTR_SPACE; 35319 break; 35320 case S_ATTR_NOQUOT_VALUE: 35321 var value = source.slice(start, p); 35322 errorHandler.warning('attribute "' + value + '" missed quot(")!!'); 35323 addAttribute(attrName, value, start); 35324 case S_ATTR_END: 35325 s = S_TAG_SPACE; 35326 break; 35327 //case S_TAG_SPACE: 35328 //case S_EQ: 35329 //case S_ATTR_SPACE: 35330 // void();break; 35331 //case S_TAG_CLOSE: 35332 //ignore warning 35333 } 35334 } else { 35335 //not space 35336 //S_TAG, S_ATTR, S_EQ, S_ATTR_NOQUOT_VALUE 35337 //S_ATTR_SPACE, S_ATTR_END, S_TAG_SPACE, S_TAG_CLOSE 35338 switch (s) { 35339 //case S_TAG:void();break; 35340 //case S_ATTR:void();break; 35341 //case S_ATTR_NOQUOT_VALUE:void();break; 35342 case S_ATTR_SPACE: 35343 el.tagName; 35344 if (!NAMESPACE$1.isHTML(currentNSMap['']) || !attrName.match(/^(?:disabled|checked|selected)$/i)) { 35345 errorHandler.warning('attribute "' + attrName + '" missed value!! "' + attrName + '" instead2!!'); 35346 } 35347 addAttribute(attrName, attrName, start); 35348 start = p; 35349 s = S_ATTR; 35350 break; 35351 case S_ATTR_END: 35352 errorHandler.warning('attribute space is required"' + attrName + '"!!'); 35353 case S_TAG_SPACE: 35354 s = S_ATTR; 35355 start = p; 35356 break; 35357 case S_EQ: 35358 s = S_ATTR_NOQUOT_VALUE; 35359 start = p; 35360 break; 35361 case S_TAG_CLOSE: 35362 throw new Error("elements closed character '/' and '>' must be connected to"); 35363 } 35364 } 35365 } //end outer switch 35366 //console.log('p++',p) 35367 p++; 35368 } 35369 } 35370 /** 35371 * @return true if has new namespace define 35372 */ 35373 function appendElement$1(el, domBuilder, currentNSMap) { 35374 var tagName = el.tagName; 35375 var localNSMap = null; 35376 //var currentNSMap = parseStack[parseStack.length-1].currentNSMap; 35377 var i = el.length; 35378 while (i--) { 35379 var a = el[i]; 35380 var qName = a.qName; 35381 var value = a.value; 35382 var nsp = qName.indexOf(':'); 35383 if (nsp > 0) { 35384 var prefix = a.prefix = qName.slice(0, nsp); 35385 var localName = qName.slice(nsp + 1); 35386 var nsPrefix = prefix === 'xmlns' && localName; 35387 } else { 35388 localName = qName; 35389 prefix = null; 35390 nsPrefix = qName === 'xmlns' && ''; 35391 } 35392 //can not set prefix,because prefix !== '' 35393 a.localName = localName; 35394 //prefix == null for no ns prefix attribute 35395 if (nsPrefix !== false) { 35396 //hack!! 35397 if (localNSMap == null) { 35398 localNSMap = {}; 35399 //console.log(currentNSMap,0) 35400 _copy(currentNSMap, currentNSMap = {}); 35401 //console.log(currentNSMap,1) 35402 } 35403 35404 currentNSMap[nsPrefix] = localNSMap[nsPrefix] = value; 35405 a.uri = NAMESPACE$1.XMLNS; 35406 domBuilder.startPrefixMapping(nsPrefix, value); 35407 } 35408 } 35409 var i = el.length; 35410 while (i--) { 35411 a = el[i]; 35412 var prefix = a.prefix; 35413 if (prefix) { 35414 //no prefix attribute has no namespace 35415 if (prefix === 'xml') { 35416 a.uri = NAMESPACE$1.XML; 35417 } 35418 if (prefix !== 'xmlns') { 35419 a.uri = currentNSMap[prefix || '']; 35420 35421 //{console.log('###'+a.qName,domBuilder.locator.systemId+'',currentNSMap,a.uri)} 35422 } 35423 } 35424 } 35425 35426 var nsp = tagName.indexOf(':'); 35427 if (nsp > 0) { 35428 prefix = el.prefix = tagName.slice(0, nsp); 35429 localName = el.localName = tagName.slice(nsp + 1); 35430 } else { 35431 prefix = null; //important!! 35432 localName = el.localName = tagName; 35433 } 35434 //no prefix element has default namespace 35435 var ns = el.uri = currentNSMap[prefix || '']; 35436 domBuilder.startElement(ns, localName, tagName, el); 35437 //endPrefixMapping and startPrefixMapping have not any help for dom builder 35438 //localNSMap = null 35439 if (el.closed) { 35440 domBuilder.endElement(ns, localName, tagName); 35441 if (localNSMap) { 35442 for (prefix in localNSMap) { 35443 if (Object.prototype.hasOwnProperty.call(localNSMap, prefix)) { 35444 domBuilder.endPrefixMapping(prefix); 35445 } 35446 } 35447 } 35448 } else { 35449 el.currentNSMap = currentNSMap; 35450 el.localNSMap = localNSMap; 35451 //parseStack.push(el); 35452 return true; 35453 } 35454 } 35455 function parseHtmlSpecialContent(source, elStartEnd, tagName, entityReplacer, domBuilder) { 35456 if (/^(?:script|textarea)$/i.test(tagName)) { 35457 var elEndStart = source.indexOf('</' + tagName + '>', elStartEnd); 35458 var text = source.substring(elStartEnd + 1, elEndStart); 35459 if (/[&<]/.test(text)) { 35460 if (/^script$/i.test(tagName)) { 35461 //if(!/\]\]>/.test(text)){ 35462 //lexHandler.startCDATA(); 35463 domBuilder.characters(text, 0, text.length); 35464 //lexHandler.endCDATA(); 35465 return elEndStart; 35466 //} 35467 } //}else{//text area 35468 text = text.replace(/&#?\w+;/g, entityReplacer); 35469 domBuilder.characters(text, 0, text.length); 35470 return elEndStart; 35471 //} 35472 } 35473 } 35474 35475 return elStartEnd + 1; 35476 } 35477 function fixSelfClosed(source, elStartEnd, tagName, closeMap) { 35478 //if(tagName in closeMap){ 35479 var pos = closeMap[tagName]; 35480 if (pos == null) { 35481 //console.log(tagName) 35482 pos = source.lastIndexOf('</' + tagName + '>'); 35483 if (pos < elStartEnd) { 35484 //å¿è®°éå 35485 pos = source.lastIndexOf('</' + tagName); 35486 } 35487 closeMap[tagName] = pos; 35488 } 35489 return pos < elStartEnd; 35490 //} 35491 } 35492 35493 function _copy(source, target) { 35494 for (var n in source) { 35495 if (Object.prototype.hasOwnProperty.call(source, n)) { 35496 target[n] = source[n]; 35497 } 35498 } 35499 } 35500 function parseDCC(source, start, domBuilder, errorHandler) { 35501 //sure start with '<!' 35502 var next = source.charAt(start + 2); 35503 switch (next) { 35504 case '-': 35505 if (source.charAt(start + 3) === '-') { 35506 var end = source.indexOf('-->', start + 4); 35507 //append comment source.substring(4,end)//<!-- 35508 if (end > start) {
vendor: 14,774 bytes, lines 35509-35950
35509 domBuilder.comment(source, start + 4, end - start - 4); 35510 return end + 3; 35511 } else { 35512 errorHandler.error("Unclosed comment"); 35513 return -1; 35514 } 35515 } else { 35516 //error 35517 return -1; 35518 } 35519 default: 35520 if (source.substr(start + 3, 6) == 'CDATA[') { 35521 var end = source.indexOf(']]>', start + 9); 35522 domBuilder.startCDATA(); 35523 domBuilder.characters(source, start + 9, end - start - 9); 35524 domBuilder.endCDATA(); 35525 return end + 3; 35526 } 35527 //<!DOCTYPE 35528 //startDTD(java.lang.String name, java.lang.String publicId, java.lang.String systemId) 35529 var matchs = split(source, start); 35530 var len = matchs.length; 35531 if (len > 1 && /!doctype/i.test(matchs[0][0])) { 35532 var name = matchs[1][0]; 35533 var pubid = false; 35534 var sysid = false; 35535 if (len > 3) { 35536 if (/^public$/i.test(matchs[2][0])) { 35537 pubid = matchs[3][0]; 35538 sysid = len > 4 && matchs[4][0]; 35539 } else if (/^system$/i.test(matchs[2][0])) { 35540 sysid = matchs[3][0]; 35541 } 35542 } 35543 var lastMatch = matchs[len - 1]; 35544 domBuilder.startDTD(name, pubid, sysid); 35545 domBuilder.endDTD(); 35546 return lastMatch.index + lastMatch[0].length; 35547 } 35548 } 35549 return -1; 35550 } 35551 function parseInstruction(source, start, domBuilder) { 35552 var end = source.indexOf('?>', start); 35553 if (end) { 35554 var match = source.substring(start, end).match(/^<\?(\S*)\s*([\s\S]*?)\s*$/); 35555 if (match) { 35556 match[0].length; 35557 domBuilder.processingInstruction(match[1], match[2]); 35558 return end + 2; 35559 } else { 35560 //error 35561 return -1; 35562 } 35563 } 35564 return -1; 35565 } 35566 function ElementAttributes() { 35567 this.attributeNames = {}; 35568 } 35569 ElementAttributes.prototype = { 35570 setTagName: function (tagName) { 35571 if (!tagNamePattern.test(tagName)) { 35572 throw new Error('invalid tagName:' + tagName); 35573 } 35574 this.tagName = tagName; 35575 }, 35576 addValue: function (qName, value, offset) { 35577 if (!tagNamePattern.test(qName)) { 35578 throw new Error('invalid attribute:' + qName); 35579 } 35580 this.attributeNames[qName] = this.length; 35581 this[this.length++] = { 35582 qName: qName, 35583 value: value, 35584 offset: offset 35585 }; 35586 }, 35587 length: 0, 35588 getLocalName: function (i) { 35589 return this[i].localName; 35590 }, 35591 getLocator: function (i) { 35592 return this[i].locator; 35593 }, 35594 getQName: function (i) { 35595 return this[i].qName; 35596 }, 35597 getURI: function (i) { 35598 return this[i].uri; 35599 }, 35600 getValue: function (i) { 35601 return this[i].value; 35602 } 35603 // ,getIndex:function(uri, localName)){ 35604 // if(localName){ 35605 // 35606 // }else{ 35607 // var qName = uri 35608 // } 35609 // }, 35610 // getValue:function(){return this.getValue(this.getIndex.apply(this,arguments))}, 35611 // getType:function(uri,localName){} 35612 // getType:function(i){}, 35613 }; 35614 35615 function split(source, start) { 35616 var match; 35617 var buf = []; 35618 var reg = /'[^']+'|"[^"]+"|[^\s<>\/=]+=?|(\/?\s*>|<)/g; 35619 reg.lastIndex = start; 35620 reg.exec(source); //skip < 35621 while (match = reg.exec(source)) { 35622 buf.push(match); 35623 if (match[1]) return buf; 35624 } 35625 } 35626 var XMLReader_1 = XMLReader$1; 35627 var ParseError_1 = ParseError$1; 35628 var sax = { 35629 XMLReader: XMLReader_1, 35630 ParseError: ParseError_1 35631 }; 35632 35633 var DOMImplementation = dom.DOMImplementation; 35634 var NAMESPACE = conventions.NAMESPACE; 35635 var ParseError = sax.ParseError; 35636 var XMLReader = sax.XMLReader; 35637 35638 /** 35639 * Normalizes line ending according to https://www.w3.org/TR/xml11/#sec-line-ends: 35640 * 35641 * > XML parsed entities are often stored in computer files which, 35642 * > for editing convenience, are organized into lines. 35643 * > These lines are typically separated by some combination 35644 * > of the characters CARRIAGE RETURN (#xD) and LINE FEED (#xA). 35645 * > 35646 * > To simplify the tasks of applications, the XML processor must behave 35647 * > as if it normalized all line breaks in external parsed entities (including the document entity) 35648 * > on input, before parsing, by translating all of the following to a single #xA character: 35649 * > 35650 * > 1. the two-character sequence #xD #xA 35651 * > 2. the two-character sequence #xD #x85 35652 * > 3. the single character #x85 35653 * > 4. the single character #x2028 35654 * > 5. any #xD character that is not immediately followed by #xA or #x85. 35655 * 35656 * @param {string} input 35657 * @returns {string} 35658 */ 35659 function normalizeLineEndings(input) { 35660 return input.replace(/\r[\n\u0085]/g, '\n').replace(/[\r\u0085\u2028]/g, '\n'); 35661 } 35662 35663 /** 35664 * @typedef Locator 35665 * @property {number} [columnNumber] 35666 * @property {number} [lineNumber] 35667 */ 35668 35669 /** 35670 * @typedef DOMParserOptions 35671 * @property {DOMHandler} [domBuilder] 35672 * @property {Function} [errorHandler] 35673 * @property {(string) => string} [normalizeLineEndings] used to replace line endings before parsing 35674 * defaults to `normalizeLineEndings` 35675 * @property {Locator} [locator] 35676 * @property {Record<string, string>} [xmlns] 35677 * 35678 * @see normalizeLineEndings 35679 */ 35680 35681 /** 35682 * The DOMParser interface provides the ability to parse XML or HTML source code 35683 * from a string into a DOM `Document`. 35684 * 35685 * _xmldom is different from the spec in that it allows an `options` parameter, 35686 * to override the default behavior._ 35687 * 35688 * @param {DOMParserOptions} [options] 35689 * @constructor 35690 * 35691 * @see https://developer.mozilla.org/en-US/docs/Web/API/DOMParser 35692 * @see https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-parsing-and-serialization 35693 */ 35694 function DOMParser$1(options) { 35695 this.options = options || { 35696 locator: {} 35697 }; 35698 } 35699 DOMParser$1.prototype.parseFromString = function (source, mimeType) { 35700 var options = this.options; 35701 var sax = new XMLReader(); 35702 var domBuilder = options.domBuilder || new DOMHandler(); //contentHandler and LexicalHandler 35703 var errorHandler = options.errorHandler; 35704 var locator = options.locator; 35705 var defaultNSMap = options.xmlns || {}; 35706 var isHTML = /\/x?html?$/.test(mimeType); //mimeType.toLowerCase().indexOf('html') > -1; 35707 var entityMap = isHTML ? entities.HTML_ENTITIES : entities.XML_ENTITIES; 35708 if (locator) { 35709 domBuilder.setDocumentLocator(locator); 35710 } 35711 sax.errorHandler = buildErrorHandler(errorHandler, domBuilder, locator); 35712 sax.domBuilder = options.domBuilder || domBuilder; 35713 if (isHTML) { 35714 defaultNSMap[''] = NAMESPACE.HTML; 35715 } 35716 defaultNSMap.xml = defaultNSMap.xml || NAMESPACE.XML; 35717 var normalize = options.normalizeLineEndings || normalizeLineEndings; 35718 if (source && typeof source === 'string') { 35719 sax.parse(normalize(source), defaultNSMap, entityMap); 35720 } else { 35721 sax.errorHandler.error('invalid doc source'); 35722 } 35723 return domBuilder.doc; 35724 }; 35725 function buildErrorHandler(errorImpl, domBuilder, locator) { 35726 if (!errorImpl) { 35727 if (domBuilder instanceof DOMHandler) { 35728 return domBuilder; 35729 } 35730 errorImpl = domBuilder; 35731 } 35732 var errorHandler = {}; 35733 var isCallback = errorImpl instanceof Function; 35734 locator = locator || {}; 35735 function build(key) { 35736 var fn = errorImpl[key]; 35737 if (!fn && isCallback) { 35738 fn = errorImpl.length == 2 ? function (msg) { 35739 errorImpl(key, msg); 35740 } : errorImpl; 35741 } 35742 errorHandler[key] = fn && function (msg) { 35743 fn('[xmldom ' + key + ']\t' + msg + _locator(locator)); 35744 } || function () {}; 35745 } 35746 build('warning'); 35747 build('error'); 35748 build('fatalError'); 35749 return errorHandler; 35750 } 35751 35752 //console.log('#\n\n\n\n\n\n\n####') 35753 /** 35754 * +ContentHandler+ErrorHandler 35755 * +LexicalHandler+EntityResolver2 35756 * -DeclHandler-DTDHandler 35757 * 35758 * DefaultHandler:EntityResolver, DTDHandler, ContentHandler, ErrorHandler 35759 * DefaultHandler2:DefaultHandler,LexicalHandler, DeclHandler, EntityResolver2 35760 * @link http://www.saxproject.org/apidoc/org/xml/sax/helpers/DefaultHandler.html 35761 */ 35762 function DOMHandler() { 35763 this.cdata = false; 35764 } 35765 function position(locator, node) { 35766 node.lineNumber = locator.lineNumber; 35767 node.columnNumber = locator.columnNumber; 35768 } 35769 /** 35770 * @see org.xml.sax.ContentHandler#startDocument 35771 * @link http://www.saxproject.org/apidoc/org/xml/sax/ContentHandler.html 35772 */ 35773 DOMHandler.prototype = { 35774 startDocument: function () { 35775 this.doc = new DOMImplementation().createDocument(null, null, null); 35776 if (this.locator) { 35777 this.doc.documentURI = this.locator.systemId; 35778 } 35779 }, 35780 startElement: function (namespaceURI, localName, qName, attrs) { 35781 var doc = this.doc; 35782 var el = doc.createElementNS(namespaceURI, qName || localName); 35783 var len = attrs.length; 35784 appendElement(this, el); 35785 this.currentElement = el; 35786 this.locator && position(this.locator, el); 35787 for (var i = 0; i < len; i++) { 35788 var namespaceURI = attrs.getURI(i); 35789 var value = attrs.getValue(i); 35790 var qName = attrs.getQName(i); 35791 var attr = doc.createAttributeNS(namespaceURI, qName); 35792 this.locator && position(attrs.getLocator(i), attr); 35793 attr.value = attr.nodeValue = value; 35794 el.setAttributeNode(attr); 35795 } 35796 }, 35797 endElement: function (namespaceURI, localName, qName) { 35798 var current = this.currentElement; 35799 current.tagName; 35800 this.currentElement = current.parentNode; 35801 }, 35802 startPrefixMapping: function (prefix, uri) {}, 35803 endPrefixMapping: function (prefix) {}, 35804 processingInstruction: function (target, data) { 35805 var ins = this.doc.createProcessingInstruction(target, data); 35806 this.locator && position(this.locator, ins); 35807 appendElement(this, ins); 35808 }, 35809 ignorableWhitespace: function (ch, start, length) {}, 35810 characters: function (chars, start, length) { 35811 chars = _toString.apply(this, arguments); 35812 //console.log(chars) 35813 if (chars) { 35814 if (this.cdata) { 35815 var charNode = this.doc.createCDATASection(chars); 35816 } else { 35817 var charNode = this.doc.createTextNode(chars); 35818 } 35819 if (this.currentElement) { 35820 this.currentElement.appendChild(charNode); 35821 } else if (/^\s*$/.test(chars)) { 35822 this.doc.appendChild(charNode); 35823 //process xml 35824 } 35825 35826 this.locator && position(this.locator, charNode); 35827 } 35828 }, 35829 skippedEntity: function (name) {}, 35830 endDocument: function () { 35831 this.doc.normalize(); 35832 }, 35833 setDocumentLocator: function (locator) { 35834 if (this.locator = locator) { 35835 // && !('lineNumber' in locator)){ 35836 locator.lineNumber = 0; 35837 } 35838 }, 35839 //LexicalHandler 35840 comment: function (chars, start, length) { 35841 chars = _toString.apply(this, arguments); 35842 var comm = this.doc.createComment(chars); 35843 this.locator && position(this.locator, comm); 35844 appendElement(this, comm); 35845 }, 35846 startCDATA: function () { 35847 //used in characters() methods 35848 this.cdata = true; 35849 }, 35850 endCDATA: function () { 35851 this.cdata = false; 35852 }, 35853 startDTD: function (name, publicId, systemId) { 35854 var impl = this.doc.implementation; 35855 if (impl && impl.createDocumentType) { 35856 var dt = impl.createDocumentType(name, publicId, systemId); 35857 this.locator && position(this.locator, dt); 35858 appendElement(this, dt); 35859 this.doc.doctype = dt; 35860 } 35861 }, 35862 /** 35863 * @see org.xml.sax.ErrorHandler 35864 * @link http://www.saxproject.org/apidoc/org/xml/sax/ErrorHandler.html 35865 */ 35866 warning: function (error) { 35867 console.warn('[xmldom warning]\t' + error, _locator(this.locator)); 35868 }, 35869 error: function (error) { 35870 console.error('[xmldom error]\t' + error, _locator(this.locator)); 35871 }, 35872 fatalError: function (error) { 35873 throw new ParseError(error, this.locator); 35874 } 35875 }; 35876 function _locator(l) { 35877 if (l) { 35878 return '\n@' + (l.systemId || '') + '#[line:' + l.lineNumber + ',col:' + l.columnNumber + ']'; 35879 } 35880 } 35881 function _toString(chars, start, length) { 35882 if (typeof chars == 'string') { 35883 return chars.substr(start, length); 35884 } else { 35885 //java sax connect width xmldom on rhino(what about: "? && !(chars instanceof String)") 35886 if (chars.length >= start + length || start) { 35887 return new java.lang.String(chars, start, length) + ''; 35888 } 35889 return chars; 35890 } 35891 } 35892 35893 /* 35894 * @link http://www.saxproject.org/apidoc/org/xml/sax/ext/LexicalHandler.html 35895 * used method of org.xml.sax.ext.LexicalHandler: 35896 * #comment(chars, start, length) 35897 * #startCDATA() 35898 * #endCDATA() 35899 * #startDTD(name, publicId, systemId) 35900 * 35901 * 35902 * IGNORED method of org.xml.sax.ext.LexicalHandler: 35903 * #endDTD() 35904 * #startEntity(name) 35905 * #endEntity(name) 35906 * 35907 * 35908 * @link http://www.saxproject.org/apidoc/org/xml/sax/ext/DeclHandler.html 35909 * IGNORED method of org.xml.sax.ext.DeclHandler 35910 * #attributeDecl(eName, aName, type, mode, value) 35911 * #elementDecl(name, model) 35912 * #externalEntityDecl(name, publicId, systemId) 35913 * #internalEntityDecl(name, value) 35914 * @link http://www.saxproject.org/apidoc/org/xml/sax/ext/EntityResolver2.html 35915 * IGNORED method of org.xml.sax.EntityResolver2 35916 * #resolveEntity(String name,String publicId,String baseURI,String systemId) 35917 * #resolveEntity(publicId, systemId) 35918 * #getExternalSubset(name, baseURI) 35919 * @link http://www.saxproject.org/apidoc/org/xml/sax/DTDHandler.html 35920 * IGNORED method of org.xml.sax.DTDHandler 35921 * #notationDecl(name, publicId, systemId) {}; 35922 * #unparsedEntityDecl(name, publicId, systemId, notationName) {}; 35923 */ 35924 "endDTD,startEntity,endEntity,attributeDecl,elementDecl,externalEntityDecl,internalEntityDecl,resolveEntity,getExternalSubset,notationDecl,unparsedEntityDecl".replace(/\w+/g, function (key) { 35925 DOMHandler.prototype[key] = function () { 35926 return null; 35927 }; 35928 }); 35929 35930 /* Private static helpers treated below as private instance methods, so don't need to add these to the public API; we might use a Relator to also get rid of non-standard public properties */ 35931 function appendElement(hander, node) { 35932 if (!hander.currentElement) { 35933 hander.doc.appendChild(node); 35934 } else { 35935 hander.currentElement.appendChild(node); 35936 } 35937 } //appendChild and setAttributeNS are preformance key 35938 35939 var __DOMHandler = DOMHandler; 35940 var normalizeLineEndings_1 = normalizeLineEndings; 35941 var DOMParser_1 = DOMParser$1; 35942 var domParser = { 35943 __DOMHandler: __DOMHandler, 35944 normalizeLineEndings: normalizeLineEndings_1, 35945 DOMParser: DOMParser_1 35946 }; 35947 35948 var DOMParser = domParser.DOMParser; 35949 35950 /*! @name mpd-parser @version 1.3
vendor: 16,826 bytes, lines 35950-36454
35950.0 @license Apache-2.0 */ 35951 const isObject = obj => { 35952 return !!obj && typeof obj === 'object'; 35953 }; 35954 const merge$1 = (...objects) => { 35955 return objects.reduce((result, source) => { 35956 if (typeof source !== 'object') { 35957 return result; 35958 } 35959 Object.keys(source).forEach(key => { 35960 if (Array.isArray(result[key]) && Array.isArray(source[key])) { 35961 result[key] = result[key].concat(source[key]); 35962 } else if (isObject(result[key]) && isObject(source[key])) { 35963 result[key] = merge$1(result[key], source[key]); 35964 } else { 35965 result[key] = source[key]; 35966 } 35967 }); 35968 return result; 35969 }, {}); 35970 }; 35971 const values = o => Object.keys(o).map(k => o[k]); 35972 const range = (start, end) => { 35973 const result = []; 35974 for (let i = start; i < end; i++) { 35975 result.push(i); 35976 } 35977 return result; 35978 }; 35979 const flatten = lists => lists.reduce((x, y) => x.concat(y), []); 35980 const from = list => { 35981 if (!list.length) { 35982 return []; 35983 } 35984 const result = []; 35985 for (let i = 0; i < list.length; i++) { 35986 result.push(list[i]); 35987 } 35988 return result; 35989 }; 35990 const findIndexes = (l, key) => l.reduce((a, e, i) => { 35991 if (e[key]) { 35992 a.push(i); 35993 } 35994 return a; 35995 }, []); 35996 /** 35997 * Returns a union of the included lists provided each element can be identified by a key. 35998 * 35999 * @param {Array} list - list of lists to get the union of 36000 * @param {Function} keyFunction - the function to use as a key for each element 36001 * 36002 * @return {Array} the union of the arrays 36003 */ 36004 36005 const union = (lists, keyFunction) => { 36006 return values(lists.reduce((acc, list) => { 36007 list.forEach(el => { 36008 acc[keyFunction(el)] = el; 36009 }); 36010 return acc; 36011 }, {})); 36012 }; 36013 var errors = { 36014 INVALID_NUMBER_OF_PERIOD: 'INVALID_NUMBER_OF_PERIOD', 36015 INVALID_NUMBER_OF_CONTENT_STEERING: 'INVALID_NUMBER_OF_CONTENT_STEERING', 36016 DASH_EMPTY_MANIFEST: 'DASH_EMPTY_MANIFEST', 36017 DASH_INVALID_XML: 'DASH_INVALID_XML', 36018 NO_BASE_URL: 'NO_BASE_URL', 36019 MISSING_SEGMENT_INFORMATION: 'MISSING_SEGMENT_INFORMATION', 36020 SEGMENT_TIME_UNSPECIFIED: 'SEGMENT_TIME_UNSPECIFIED', 36021 UNSUPPORTED_UTC_TIMING_SCHEME: 'UNSUPPORTED_UTC_TIMING_SCHEME' 36022 }; 36023 36024 /** 36025 * @typedef {Object} SingleUri 36026 * @property {string} uri - relative location of segment 36027 * @property {string} resolvedUri - resolved location of segment 36028 * @property {Object} byterange - Object containing information on how to make byte range 36029 * requests following byte-range-spec per RFC2616. 36030 * @property {String} byterange.length - length of range request 36031 * @property {String} byterange.offset - byte offset of range request 36032 * 36033 * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.1 36034 */ 36035 36036 /** 36037 * Converts a URLType node (5.3.9.2.3 Table 13) to a segment object 36038 * that conforms to how m3u8-parser is structured 36039 * 36040 * @see https://github.com/videojs/m3u8-parser 36041 * 36042 * @param {string} baseUrl - baseUrl provided by <BaseUrl> nodes 36043 * @param {string} source - source url for segment 36044 * @param {string} range - optional range used for range calls, 36045 * follows RFC 2616, Clause 14.35.1 36046 * @return {SingleUri} full segment information transformed into a format similar 36047 * to m3u8-parser 36048 */ 36049 36050 const urlTypeToSegment = ({ 36051 baseUrl = '', 36052 source = '', 36053 range = '', 36054 indexRange = '' 36055 }) => { 36056 const segment = { 36057 uri: source, 36058 resolvedUri: resolveUrl$1(baseUrl || '', source) 36059 }; 36060 if (range || indexRange) { 36061 const rangeStr = range ? range : indexRange; 36062 const ranges = rangeStr.split('-'); // default to parsing this as a BigInt if possible 36063 36064 let startRange = window.BigInt ? window.BigInt(ranges[0]) : parseInt(ranges[0], 10); 36065 let endRange = window.BigInt ? window.BigInt(ranges[1]) : parseInt(ranges[1], 10); // convert back to a number if less than MAX_SAFE_INTEGER 36066 36067 if (startRange < Number.MAX_SAFE_INTEGER && typeof startRange === 'bigint') { 36068 startRange = Number(startRange); 36069 } 36070 if (endRange < Number.MAX_SAFE_INTEGER && typeof endRange === 'bigint') { 36071 endRange = Number(endRange); 36072 } 36073 let length; 36074 if (typeof endRange === 'bigint' || typeof startRange === 'bigint') { 36075 length = window.BigInt(endRange) - window.BigInt(startRange) + window.BigInt(1); 36076 } else { 36077 length = endRange - startRange + 1; 36078 } 36079 if (typeof length === 'bigint' && length < Number.MAX_SAFE_INTEGER) { 36080 length = Number(length); 36081 } // byterange should be inclusive according to 36082 // RFC 2616, Clause 14.35.1 36083 36084 segment.byterange = { 36085 length, 36086 offset: startRange 36087 }; 36088 } 36089 return segment; 36090 }; 36091 const byteRangeToString = byterange => { 36092 // `endRange` is one less than `offset + length` because the HTTP range 36093 // header uses inclusive ranges 36094 let endRange; 36095 if (typeof byterange.offset === 'bigint' || typeof byterange.length === 'bigint') { 36096 endRange = window.BigInt(byterange.offset) + window.BigInt(byterange.length) - window.BigInt(1); 36097 } else { 36098 endRange = byterange.offset + byterange.length - 1; 36099 } 36100 return `${byterange.offset}-${endRange}`; 36101 }; 36102 36103 /** 36104 * parse the end number attribue that can be a string 36105 * number, or undefined. 36106 * 36107 * @param {string|number|undefined} endNumber 36108 * The end number attribute. 36109 * 36110 * @return {number|null} 36111 * The result of parsing the end number. 36112 */ 36113 36114 const parseEndNumber = endNumber => { 36115 if (endNumber && typeof endNumber !== 'number') { 36116 endNumber = parseInt(endNumber, 10); 36117 } 36118 if (isNaN(endNumber)) { 36119 return null; 36120 } 36121 return endNumber; 36122 }; 36123 /** 36124 * Functions for calculating the range of available segments in static and dynamic 36125 * manifests. 36126 */ 36127 36128 const segmentRange = { 36129 /** 36130 * Returns the entire range of available segments for a static MPD 36131 * 36132 * @param {Object} attributes 36133 * Inheritied MPD attributes 36134 * @return {{ start: number, end: number }} 36135 * The start and end numbers for available segments 36136 */ 36137 static(attributes) { 36138 const { 36139 duration, 36140 timescale = 1, 36141 sourceDuration, 36142 periodDuration 36143 } = attributes; 36144 const endNumber = parseEndNumber(attributes.endNumber); 36145 const segmentDuration = duration / timescale; 36146 if (typeof endNumber === 'number') { 36147 return { 36148 start: 0, 36149 end: endNumber 36150 }; 36151 } 36152 if (typeof periodDuration === 'number') { 36153 return { 36154 start: 0, 36155 end: periodDuration / segmentDuration 36156 }; 36157 } 36158 return { 36159 start: 0, 36160 end: sourceDuration / segmentDuration 36161 }; 36162 }, 36163 /** 36164 * Returns the current live window range of available segments for a dynamic MPD 36165 * 36166 * @param {Object} attributes 36167 * Inheritied MPD attributes 36168 * @return {{ start: number, end: number }} 36169 * The start and end numbers for available segments 36170 */ 36171 dynamic(attributes) { 36172 const { 36173 NOW, 36174 clientOffset, 36175 availabilityStartTime, 36176 timescale = 1, 36177 duration, 36178 periodStart = 0, 36179 minimumUpdatePeriod = 0, 36180 timeShiftBufferDepth = Infinity 36181 } = attributes; 36182 const endNumber = parseEndNumber(attributes.endNumber); // clientOffset is passed in at the top level of mpd-parser and is an offset calculated 36183 // after retrieving UTC server time. 36184 36185 const now = (NOW + clientOffset) / 1000; // WC stands for Wall Clock. 36186 // Convert the period start time to EPOCH. 36187 36188 const periodStartWC = availabilityStartTime + periodStart; // Period end in EPOCH is manifest's retrieval time + time until next update. 36189 36190 const periodEndWC = now + minimumUpdatePeriod; 36191 const periodDuration = periodEndWC - periodStartWC; 36192 const segmentCount = Math.ceil(periodDuration * timescale / duration); 36193 const availableStart = Math.floor((now - periodStartWC - timeShiftBufferDepth) * timescale / duration); 36194 const availableEnd = Math.floor((now - periodStartWC) * timescale / duration); 36195 return { 36196 start: Math.max(0, availableStart), 36197 end: typeof endNumber === 'number' ? endNumber : Math.min(segmentCount, availableEnd) 36198 }; 36199 } 36200 }; 36201 /** 36202 * Maps a range of numbers to objects with information needed to build the corresponding 36203 * segment list 36204 * 36205 * @name toSegmentsCallback 36206 * @function 36207 * @param {number} number 36208 * Number of the segment 36209 * @param {number} index 36210 * Index of the number in the range list 36211 * @return {{ number: Number, duration: Number, timeline: Number, time: Number }} 36212 * Object with segment timing and duration info 36213 */ 36214 36215 /** 36216 * Returns a callback for Array.prototype.map for mapping a range of numbers to 36217 * information needed to build the segment list. 36218 * 36219 * @param {Object} attributes 36220 * Inherited MPD attributes 36221 * @return {toSegmentsCallback} 36222 * Callback map function 36223 */ 36224 36225 const toSegments = attributes => number => { 36226 const { 36227 duration, 36228 timescale = 1, 36229 periodStart, 36230 startNumber = 1 36231 } = attributes; 36232 return { 36233 number: startNumber + number, 36234 duration: duration / timescale, 36235 timeline: periodStart, 36236 time: number * duration 36237 }; 36238 }; 36239 /** 36240 * Returns a list of objects containing segment timing and duration info used for 36241 * building the list of segments. This uses the @duration attribute specified 36242 * in the MPD manifest to derive the range of segments. 36243 * 36244 * @param {Object} attributes 36245 * Inherited MPD attributes 36246 * @return {{number: number, duration: number, time: number, timeline: number}[]} 36247 * List of Objects with segment timing and duration info 36248 */ 36249 36250 const parseByDuration = attributes => { 36251 const { 36252 type, 36253 duration, 36254 timescale = 1, 36255 periodDuration, 36256 sourceDuration 36257 } = attributes; 36258 const { 36259 start, 36260 end 36261 } = segmentRange[type](attributes); 36262 const segments = range(start, end).map(toSegments(attributes)); 36263 if (type === 'static') { 36264 const index = segments.length - 1; // section is either a period or the full source 36265 36266 const sectionDuration = typeof periodDuration === 'number' ? periodDuration : sourceDuration; // final segment may be less than full segment duration 36267 36268 segments[index].duration = sectionDuration - duration / timescale * index; 36269 } 36270 return segments; 36271 }; 36272 36273 /** 36274 * Translates SegmentBase into a set of segments. 36275 * (DASH SPEC Section 5.3.9.3.2) contains a set of <SegmentURL> nodes. Each 36276 * node should be translated into segment. 36277 * 36278 * @param {Object} attributes 36279 * Object containing all inherited attributes from parent elements with attribute 36280 * names as keys 36281 * @return {Object.<Array>} list of segments 36282 */ 36283 36284 const segmentsFromBase = attributes => { 36285 const { 36286 baseUrl, 36287 initialization = {}, 36288 sourceDuration, 36289 indexRange = '', 36290 periodStart, 36291 presentationTime, 36292 number = 0, 36293 duration 36294 } = attributes; // base url is required for SegmentBase to work, per spec (Section 5.3.9.2.1) 36295 36296 if (!baseUrl) { 36297 throw new Error(errors.NO_BASE_URL); 36298 } 36299 const initSegment = urlTypeToSegment({ 36300 baseUrl, 36301 source: initialization.sourceURL, 36302 range: initialization.range 36303 }); 36304 const segment = urlTypeToSegment({ 36305 baseUrl, 36306 source: baseUrl, 36307 indexRange 36308 }); 36309 segment.map = initSegment; // If there is a duration, use it, otherwise use the given duration of the source 36310 // (since SegmentBase is only for one total segment) 36311 36312 if (duration) { 36313 const segmentTimeInfo = parseByDuration(attributes); 36314 if (segmentTimeInfo.length) { 36315 segment.duration = segmentTimeInfo[0].duration; 36316 segment.timeline = segmentTimeInfo[0].timeline; 36317 } 36318 } else if (sourceDuration) { 36319 segment.duration = sourceDuration; 36320 segment.timeline = periodStart; 36321 } // If presentation time is provided, these segments are being generated by SIDX 36322 // references, and should use the time provided. For the general case of SegmentBase, 36323 // there should only be one segment in the period, so its presentation time is the same 36324 // as its period start. 36325 36326 segment.presentationTime = presentationTime || periodStart; 36327 segment.number = number; 36328 return [segment]; 36329 }; 36330 /** 36331 * Given a playlist, a sidx box, and a baseUrl, update the segment list of the playlist 36332 * according to the sidx information given. 36333 * 36334 * playlist.sidx has metadadata about the sidx where-as the sidx param 36335 * is the parsed sidx box itself. 36336 * 36337 * @param {Object} playlist the playlist to update the sidx information for 36338 * @param {Object} sidx the parsed sidx box 36339 * @return {Object} the playlist object with the updated sidx information 36340 */ 36341 36342 const addSidxSegmentsToPlaylist$1 = (playlist, sidx, baseUrl) => { 36343 // Retain init segment information 36344 const initSegment = playlist.sidx.map ? playlist.sidx.map : null; // Retain source duration from initial main manifest parsing 36345 36346 const sourceDuration = playlist.sidx.duration; // Retain source timeline 36347 36348 const timeline = playlist.timeline || 0; 36349 const sidxByteRange = playlist.sidx.byterange; 36350 const sidxEnd = sidxByteRange.offset + sidxByteRange.length; // Retain timescale of the parsed sidx 36351 36352 const timescale = sidx.timescale; // referenceType 1 refers to other sidx boxes 36353 36354 const mediaReferences = sidx.references.filter(r => r.referenceType !== 1); 36355 const segments = []; 36356 const type = playlist.endList ? 'static' : 'dynamic'; 36357 const periodStart = playlist.sidx.timeline; 36358 let presentationTime = periodStart; 36359 let number = playlist.mediaSequence || 0; // firstOffset is the offset from the end of the sidx box 36360 36361 let startIndex; // eslint-disable-next-line 36362 36363 if (typeof sidx.firstOffset === 'bigint') { 36364 startIndex = window.BigInt(sidxEnd) + sidx.firstOffset; 36365 } else { 36366 startIndex = sidxEnd + sidx.firstOffset; 36367 } 36368 for (let i = 0; i < mediaReferences.length; i++) { 36369 const reference = sidx.references[i]; // size of the referenced (sub)segment 36370 36371 const size = reference.referencedSize; // duration of the referenced (sub)segment, in the timescale 36372 // this will be converted to seconds when generating segments 36373 36374 const duration = reference.subsegmentDuration; // should be an inclusive range 36375 36376 let endIndex; // eslint-disable-next-line 36377 36378 if (typeof startIndex === 'bigint') { 36379 endIndex = startIndex + window.BigInt(size) - window.BigInt(1); 36380 } else { 36381 endIndex = startIndex + size - 1; 36382 } 36383 const indexRange = `${startIndex}-${endIndex}`; 36384 const attributes = { 36385 baseUrl, 36386 timescale, 36387 timeline, 36388 periodStart, 36389 presentationTime, 36390 number, 36391 duration, 36392 sourceDuration, 36393 indexRange, 36394 type 36395 }; 36396 const segment = segmentsFromBase(attributes)[0]; 36397 if (initSegment) { 36398 segment.map = initSegment; 36399 } 36400 segments.push(segment); 36401 if (typeof startIndex === 'bigint') { 36402 startIndex += window.BigInt(size); 36403 } else { 36404 startIndex += size; 36405 } 36406 presentationTime += duration / timescale; 36407 number++; 36408 } 36409 playlist.segments = segments; 36410 return playlist; 36411 }; 36412 const SUPPORTED_MEDIA_TYPES = ['AUDIO', 'SUBTITLES']; // allow one 60fps frame as leniency (arbitrarily chosen) 36413 36414 const TIME_FUDGE = 1 / 60; 36415 /** 36416 * Given a list of timelineStarts, combines, dedupes, and sorts them. 36417 * 36418 * @param {TimelineStart[]} timelineStarts - list of timeline starts 36419 * 36420 * @return {TimelineStart[]} the combined and deduped timeline starts 36421 */ 36422 36423 const getUniqueTimelineStarts = timelineStarts => { 36424 return union(timelineStarts, ({ 36425 timeline 36426 }) => timeline).sort((a, b) => a.timeline > b.timeline ? 1 : -1); 36427 }; 36428 /** 36429 * Finds the playlist with the matching NAME attribute. 36430 * 36431 * @param {Array} playlists - playlists to search through 36432 * @param {string} name - the NAME attribute to search for 36433 * 36434 * @return {Object|null} the matching playlist object, or null 36435 */ 36436 36437 const findPlaylistWithName = (playlists, name) => { 36438 for (let i = 0; i < playlists.length; i++) { 36439 if (playlists[i].attributes.NAME === name) { 36440 return playlists[i]; 36441 } 36442 } 36443 return null; 36444 }; 36445 /** 36446 * Gets a flattened array of media group playlists. 36447 * 36448 * @param {Object} manifest - the main manifest object 36449 * 36450 * @return {Array} the media group playlists 36451 */ 36452 36453 const getMediaGroupPlaylists = manifest => { 36454 let mediaGroupPlaylists = [];
vendor: 1,489 bytes, lines 36455-36496
36455 forEachMediaGroup$1(manifest, SUPPORTED_MEDIA_TYPES, (properties, type, group, label) => { 36456 mediaGroupPlaylists = mediaGroupPlaylists.concat(properties.playlists || []); 36457 }); 36458 return mediaGroupPlaylists; 36459 }; 36460 /** 36461 * Updates the playlist's media sequence numbers. 36462 * 36463 * @param {Object} config - options object 36464 * @param {Object} config.playlist - the playlist to update 36465 * @param {number} config.mediaSequence - the mediaSequence number to start with 36466 */ 36467 36468 const updateMediaSequenceForPlaylist = ({ 36469 playlist, 36470 mediaSequence 36471 }) => { 36472 playlist.mediaSequence = mediaSequence; 36473 playlist.segments.forEach((segment, index) => { 36474 segment.number = playlist.mediaSequence + index; 36475 }); 36476 }; 36477 /** 36478 * Updates the media and discontinuity sequence numbers of newPlaylists given oldPlaylists 36479 * and a complete list of timeline starts. 36480 * 36481 * If no matching playlist is found, only the discontinuity sequence number of the playlist 36482 * will be updated. 36483 * 36484 * Since early available timelines are not supported, at least one segment must be present. 36485 * 36486 * @param {Object} config - options object 36487 * @param {Object[]} oldPlaylists - the old playlists to use as a reference 36488 * @param {Object[]} newPlaylists - the new playlists to update 36489 * @param {Object} timelineStarts - all timelineStarts seen in the stream to this point 36490 */ 36491 36492 const updateSequenceNumbers = ({ 36493 oldPlaylists, 36494 newPlaylists, 36495 timelineStarts 36496 }) => {
vendor: 14,798 bytes, lines 36497-36831
36497 newPlaylists.forEach(playlist => { 36498 playlist.discontinuitySequence = timelineStarts.findIndex(function ({ 36499 timeline 36500 }) { 36501 return timeline === playlist.timeline; 36502 }); // Playlists NAMEs come from DASH Representation IDs, which are mandatory 36503 // (see ISO_23009-1-2012 5.3.5.2). 36504 // 36505 // If the same Representation existed in a prior Period, it will retain the same NAME. 36506 36507 const oldPlaylist = findPlaylistWithName(oldPlaylists, playlist.attributes.NAME); 36508 if (!oldPlaylist) { 36509 // Since this is a new playlist, the media sequence values can start from 0 without 36510 // consequence. 36511 return; 36512 } // TODO better support for live SIDX 36513 // 36514 // As of this writing, mpd-parser does not support multiperiod SIDX (in live or VOD). 36515 // This is evident by a playlist only having a single SIDX reference. In a multiperiod 36516 // playlist there would need to be multiple SIDX references. In addition, live SIDX is 36517 // not supported when the SIDX properties change on refreshes. 36518 // 36519 // In the future, if support needs to be added, the merging logic here can be called 36520 // after SIDX references are resolved. For now, exit early to prevent exceptions being 36521 // thrown due to undefined references. 36522 36523 if (playlist.sidx) { 36524 return; 36525 } // Since we don't yet support early available timelines, we don't need to support 36526 // playlists with no segments. 36527 36528 const firstNewSegment = playlist.segments[0]; 36529 const oldMatchingSegmentIndex = oldPlaylist.segments.findIndex(function (oldSegment) { 36530 return Math.abs(oldSegment.presentationTime - firstNewSegment.presentationTime) < TIME_FUDGE; 36531 }); // No matching segment from the old playlist means the entire playlist was refreshed. 36532 // In this case the media sequence should account for this update, and the new segments 36533 // should be marked as discontinuous from the prior content, since the last prior 36534 // timeline was removed. 36535 36536 if (oldMatchingSegmentIndex === -1) { 36537 updateMediaSequenceForPlaylist({ 36538 playlist, 36539 mediaSequence: oldPlaylist.mediaSequence + oldPlaylist.segments.length 36540 }); 36541 playlist.segments[0].discontinuity = true; 36542 playlist.discontinuityStarts.unshift(0); // No matching segment does not necessarily mean there's missing content. 36543 // 36544 // If the new playlist's timeline is the same as the last seen segment's timeline, 36545 // then a discontinuity can be added to identify that there's potentially missing 36546 // content. If there's no missing content, the discontinuity should still be rather 36547 // harmless. It's possible that if segment durations are accurate enough, that the 36548 // existence of a gap can be determined using the presentation times and durations, 36549 // but if the segment timing info is off, it may introduce more problems than simply 36550 // adding the discontinuity. 36551 // 36552 // If the new playlist's timeline is different from the last seen segment's timeline, 36553 // then a discontinuity can be added to identify that this is the first seen segment 36554 // of a new timeline. However, the logic at the start of this function that 36555 // determined the disconinuity sequence by timeline index is now off by one (the 36556 // discontinuity of the newest timeline hasn't yet fallen off the manifest...since 36557 // we added it), so the disconinuity sequence must be decremented. 36558 // 36559 // A period may also have a duration of zero, so the case of no segments is handled 36560 // here even though we don't yet support early available periods. 36561 36562 if (!oldPlaylist.segments.length && playlist.timeline > oldPlaylist.timeline || oldPlaylist.segments.length && playlist.timeline > oldPlaylist.segments[oldPlaylist.segments.length - 1].timeline) { 36563 playlist.discontinuitySequence--; 36564 } 36565 return; 36566 } // If the first segment matched with a prior segment on a discontinuity (it's matching 36567 // on the first segment of a period), then the discontinuitySequence shouldn't be the 36568 // timeline's matching one, but instead should be the one prior, and the first segment 36569 // of the new manifest should be marked with a discontinuity. 36570 // 36571 // The reason for this special case is that discontinuity sequence shows how many 36572 // discontinuities have fallen off of the playlist, and discontinuities are marked on 36573 // the first segment of a new "timeline." Because of this, while DASH will retain that 36574 // Period while the "timeline" exists, HLS keeps track of it via the discontinuity 36575 // sequence, and that first segment is an indicator, but can be removed before that 36576 // timeline is gone. 36577 36578 const oldMatchingSegment = oldPlaylist.segments[oldMatchingSegmentIndex]; 36579 if (oldMatchingSegment.discontinuity && !firstNewSegment.discontinuity) { 36580 firstNewSegment.discontinuity = true; 36581 playlist.discontinuityStarts.unshift(0); 36582 playlist.discontinuitySequence--; 36583 } 36584 updateMediaSequenceForPlaylist({ 36585 playlist, 36586 mediaSequence: oldPlaylist.segments[oldMatchingSegmentIndex].number 36587 }); 36588 }); 36589 }; 36590 /** 36591 * Given an old parsed manifest object and a new parsed manifest object, updates the 36592 * sequence and timing values within the new manifest to ensure that it lines up with the 36593 * old. 36594 * 36595 * @param {Array} oldManifest - the old main manifest object 36596 * @param {Array} newManifest - the new main manifest object 36597 * 36598 * @return {Object} the updated new manifest object 36599 */ 36600 36601 const positionManifestOnTimeline = ({ 36602 oldManifest, 36603 newManifest 36604 }) => { 36605 // Starting from v4.1.2 of the IOP, section 4.4.3.3 states: 36606 // 36607 // "MPD@availabilityStartTime and Period@start shall not be changed over MPD updates." 36608 // 36609 // This was added from https://github.com/Dash-Industry-Forum/DASH-IF-IOP/issues/160 36610 // 36611 // Because of this change, and the difficulty of supporting periods with changing start 36612 // times, periods with changing start times are not supported. This makes the logic much 36613 // simpler, since periods with the same start time can be considerred the same period 36614 // across refreshes. 36615 // 36616 // To give an example as to the difficulty of handling periods where the start time may 36617 // change, if a single period manifest is refreshed with another manifest with a single 36618 // period, and both the start and end times are increased, then the only way to determine 36619 // if it's a new period or an old one that has changed is to look through the segments of 36620 // each playlist and determine the presentation time bounds to find a match. In addition, 36621 // if the period start changed to exceed the old period end, then there would be no 36622 // match, and it would not be possible to determine whether the refreshed period is a new 36623 // one or the old one. 36624 const oldPlaylists = oldManifest.playlists.concat(getMediaGroupPlaylists(oldManifest)); 36625 const newPlaylists = newManifest.playlists.concat(getMediaGroupPlaylists(newManifest)); // Save all seen timelineStarts to the new manifest. Although this potentially means that 36626 // there's a "memory leak" in that it will never stop growing, in reality, only a couple 36627 // of properties are saved for each seen Period. Even long running live streams won't 36628 // generate too many Periods, unless the stream is watched for decades. In the future, 36629 // this can be optimized by mapping to discontinuity sequence numbers for each timeline, 36630 // but it may not become an issue, and the additional info can be useful for debugging. 36631 36632 newManifest.timelineStarts = getUniqueTimelineStarts([oldManifest.timelineStarts, newManifest.timelineStarts]); 36633 updateSequenceNumbers({ 36634 oldPlaylists, 36635 newPlaylists, 36636 timelineStarts: newManifest.timelineStarts 36637 }); 36638 return newManifest; 36639 }; 36640 const generateSidxKey = sidx => sidx && sidx.uri + '-' + byteRangeToString(sidx.byterange); 36641 const mergeDiscontiguousPlaylists = playlists => { 36642 // Break out playlists into groups based on their baseUrl 36643 const playlistsByBaseUrl = playlists.reduce(function (acc, cur) { 36644 if (!acc[cur.attributes.baseUrl]) { 36645 acc[cur.attributes.baseUrl] = []; 36646 } 36647 acc[cur.attributes.baseUrl].push(cur); 36648 return acc; 36649 }, {}); 36650 let allPlaylists = []; 36651 Object.values(playlistsByBaseUrl).forEach(playlistGroup => { 36652 const mergedPlaylists = values(playlistGroup.reduce((acc, playlist) => { 36653 // assuming playlist IDs are the same across periods 36654 // TODO: handle multiperiod where representation sets are not the same 36655 // across periods 36656 const name = playlist.attributes.id + (playlist.attributes.lang || ''); 36657 if (!acc[name]) { 36658 // First Period 36659 acc[name] = playlist; 36660 acc[name].attributes.timelineStarts = []; 36661 } else { 36662 // Subsequent Periods 36663 if (playlist.segments) { 36664 // first segment of subsequent periods signal a discontinuity 36665 if (playlist.segments[0]) { 36666 playlist.segments[0].discontinuity = true; 36667 } 36668 acc[name].segments.push(...playlist.segments); 36669 } // bubble up contentProtection, this assumes all DRM content 36670 // has the same contentProtection 36671 36672 if (playlist.attributes.contentProtection) { 36673 acc[name].attributes.contentProtection = playlist.attributes.contentProtection; 36674 } 36675 } 36676 acc[name].attributes.timelineStarts.push({ 36677 // Although they represent the same number, it's important to have both to make it 36678 // compatible with HLS potentially having a similar attribute. 36679 start: playlist.attributes.periodStart, 36680 timeline: playlist.attributes.periodStart 36681 }); 36682 return acc; 36683 }, {})); 36684 allPlaylists = allPlaylists.concat(mergedPlaylists); 36685 }); 36686 return allPlaylists.map(playlist => { 36687 playlist.discontinuityStarts = findIndexes(playlist.segments || [], 'discontinuity'); 36688 return playlist; 36689 }); 36690 }; 36691 const addSidxSegmentsToPlaylist = (playlist, sidxMapping) => { 36692 const sidxKey = generateSidxKey(playlist.sidx); 36693 const sidxMatch = sidxKey && sidxMapping[sidxKey] && sidxMapping[sidxKey].sidx; 36694 if (sidxMatch) { 36695 addSidxSegmentsToPlaylist$1(playlist, sidxMatch, playlist.sidx.resolvedUri); 36696 } 36697 return playlist; 36698 }; 36699 const addSidxSegmentsToPlaylists = (playlists, sidxMapping = {}) => { 36700 if (!Object.keys(sidxMapping).length) { 36701 return playlists; 36702 } 36703 for (const i in playlists) { 36704 playlists[i] = addSidxSegmentsToPlaylist(playlists[i], sidxMapping); 36705 } 36706 return playlists; 36707 }; 36708 const formatAudioPlaylist = ({ 36709 attributes, 36710 segments, 36711 sidx, 36712 mediaSequence, 36713 discontinuitySequence, 36714 discontinuityStarts 36715 }, isAudioOnly) => { 36716 const playlist = { 36717 attributes: { 36718 NAME: attributes.id, 36719 BANDWIDTH: attributes.bandwidth, 36720 CODECS: attributes.codecs, 36721 ['PROGRAM-ID']: 1 36722 }, 36723 uri: '', 36724 endList: attributes.type === 'static', 36725 timeline: attributes.periodStart, 36726 resolvedUri: attributes.baseUrl || '', 36727 targetDuration: attributes.duration, 36728 discontinuitySequence, 36729 discontinuityStarts, 36730 timelineStarts: attributes.timelineStarts, 36731 mediaSequence, 36732 segments 36733 }; 36734 if (attributes.contentProtection) { 36735 playlist.contentProtection = attributes.contentProtection; 36736 } 36737 if (attributes.serviceLocation) { 36738 playlist.attributes.serviceLocation = attributes.serviceLocation; 36739 } 36740 if (sidx) { 36741 playlist.sidx = sidx; 36742 } 36743 if (isAudioOnly) { 36744 playlist.attributes.AUDIO = 'audio'; 36745 playlist.attributes.SUBTITLES = 'subs'; 36746 } 36747 return playlist; 36748 }; 36749 const formatVttPlaylist = ({ 36750 attributes, 36751 segments, 36752 mediaSequence, 36753 discontinuityStarts, 36754 discontinuitySequence 36755 }) => { 36756 if (typeof segments === 'undefined') { 36757 // vtt tracks may use single file in BaseURL 36758 segments = [{ 36759 uri: attributes.baseUrl, 36760 timeline: attributes.periodStart, 36761 resolvedUri: attributes.baseUrl || '', 36762 duration: attributes.sourceDuration, 36763 number: 0 36764 }]; // targetDuration should be the same duration as the only segment 36765 36766 attributes.duration = attributes.sourceDuration; 36767 } 36768 const m3u8Attributes = { 36769 NAME: attributes.id, 36770 BANDWIDTH: attributes.bandwidth, 36771 ['PROGRAM-ID']: 1 36772 }; 36773 if (attributes.codecs) { 36774 m3u8Attributes.CODECS = attributes.codecs; 36775 } 36776 const vttPlaylist = { 36777 attributes: m3u8Attributes, 36778 uri: '', 36779 endList: attributes.type === 'static', 36780 timeline: attributes.periodStart, 36781 resolvedUri: attributes.baseUrl || '', 36782 targetDuration: attributes.duration, 36783 timelineStarts: attributes.timelineStarts, 36784 discontinuityStarts, 36785 discontinuitySequence, 36786 mediaSequence, 36787 segments 36788 }; 36789 if (attributes.serviceLocation) { 36790 vttPlaylist.attributes.serviceLocation = attributes.serviceLocation; 36791 } 36792 return vttPlaylist; 36793 }; 36794 const organizeAudioPlaylists = (playlists, sidxMapping = {}, isAudioOnly = false) => { 36795 let mainPlaylist; 36796 const formattedPlaylists = playlists.reduce((a, playlist) => { 36797 const role = playlist.attributes.role && playlist.attributes.role.value || ''; 36798 const language = playlist.attributes.lang || ''; 36799 let label = playlist.attributes.label || 'main'; 36800 if (language && !playlist.attributes.label) { 36801 const roleLabel = role ? ` (${role})` : ''; 36802 label = `${playlist.attributes.lang}${roleLabel}`; 36803 } 36804 if (!a[label]) { 36805 a[label] = { 36806 language, 36807 autoselect: true, 36808 default: role === 'main', 36809 playlists: [], 36810 uri: '' 36811 }; 36812 } 36813 const formatted = addSidxSegmentsToPlaylist(formatAudioPlaylist(playlist, isAudioOnly), sidxMapping); 36814 a[label].playlists.push(formatted); 36815 if (typeof mainPlaylist === 'undefined' && role === 'main') { 36816 mainPlaylist = playlist; 36817 mainPlaylist.default = true; 36818 } 36819 return a; 36820 }, {}); // if no playlists have role "main", mark the first as main 36821 36822 if (!mainPlaylist) { 36823 const firstLabel = Object.keys(formattedPlaylists)[0]; 36824 formattedPlaylists[firstLabel].default = true; 36825 } 36826 return formattedPlaylists; 36827 }; 36828 const organizeVttPlaylists = (playlists, sidxMapping = {}) => { 36829 return playlists.reduce((a, playlist) => { 36830 const label = playlist.attributes.label || playlist.attributes.lang || 'text'; 36831
36831if (!a[label]) { 36832 a[label] = { 36833 language: label, 36834 default: false, 36835 autoselect: false, 36836 playlists: [], 36837 uri: '' 36838 }; 36839 } 36840 a[label].playlists.push(addSidxSegmentsToPlaylist(formatVttPlaylist(playlist), sidxMapping)); 36841 return a; 36842 }, {}); 36843 }; 36844 const organizeCaptionServices = captionServices => captionServices.reduce((svcObj, svc) => { 36845 if (!svc) { 36846 return svcObj; 36847 }
vendor: 1,424 bytes, lines 36848-36900
36848 svc.forEach(service => { 36849 const { 36850 channel, 36851 language 36852 } = service; 36853 svcObj[language] = { 36854 autoselect: false, 36855 default: false, 36856 instreamId: channel, 36857 language 36858 }; 36859 if (service.hasOwnProperty('aspectRatio')) { 36860 svcObj[language].aspectRatio = service.aspectRatio; 36861 } 36862 if (service.hasOwnProperty('easyReader')) { 36863 svcObj[language].easyReader = service.easyReader; 36864 } 36865 if (service.hasOwnProperty('3D')) { 36866 svcObj[language]['3D'] = service['3D']; 36867 } 36868 }); 36869 return svcObj; 36870 }, {}); 36871 const formatVideoPlaylist = ({ 36872 attributes, 36873 segments, 36874 sidx, 36875 discontinuityStarts 36876 }) => { 36877 const playlist = { 36878 attributes: { 36879 NAME: attributes.id, 36880 AUDIO: 'audio', 36881 SUBTITLES: 'subs', 36882 RESOLUTION: { 36883 width: attributes.width, 36884 height: attributes.height 36885 }, 36886 CODECS: attributes.codecs, 36887 BANDWIDTH: attributes.bandwidth, 36888 ['PROGRAM-ID']: 1 36889 }, 36890 uri: '', 36891 endList: attributes.type === 'static', 36892 timeline: attributes.periodStart, 36893 resolvedUri: attributes.baseUrl || '', 36894 targetDuration: attributes.duration, 36895 discontinuityStarts, 36896 timelineStarts: attributes.timelineStarts, 36897 segments 36898 }; 36899 if (attributes.frameRate) { 36900 playlist.attributes['FRAME-RATE'] = attributes.frameRate
vendor: 30,136 bytes, lines 36900-37760
36900; 36901 } 36902 if (attributes.contentProtection) { 36903 playlist.contentProtection = attributes.contentProtection; 36904 } 36905 if (attributes.serviceLocation) { 36906 playlist.attributes.serviceLocation = attributes.serviceLocation; 36907 } 36908 if (sidx) { 36909 playlist.sidx = sidx; 36910 } 36911 return playlist; 36912 }; 36913 const videoOnly = ({ 36914 attributes 36915 }) => attributes.mimeType === 'video/mp4' || attributes.mimeType === 'video/webm' || attributes.contentType === 'video'; 36916 const audioOnly = ({ 36917 attributes 36918 }) => attributes.mimeType === 'audio/mp4' || attributes.mimeType === 'audio/webm' || attributes.contentType === 'audio'; 36919 const vttOnly = ({ 36920 attributes 36921 }) => attributes.mimeType === 'text/vtt' || attributes.contentType === 'text'; 36922 /** 36923 * Contains start and timeline properties denoting a timeline start. For DASH, these will 36924 * be the same number. 36925 * 36926 * @typedef {Object} TimelineStart 36927 * @property {number} start - the start time of the timeline 36928 * @property {number} timeline - the timeline number 36929 */ 36930 36931 /** 36932 * Adds appropriate media and discontinuity sequence values to the segments and playlists. 36933 * 36934 * Throughout mpd-parser, the `number` attribute is used in relation to `startNumber`, a 36935 * DASH specific attribute used in constructing segment URI's from templates. However, from 36936 * an HLS perspective, the `number` attribute on a segment would be its `mediaSequence` 36937 * value, which should start at the original media sequence value (or 0) and increment by 1 36938 * for each segment thereafter. Since DASH's `startNumber` values are independent per 36939 * period, it doesn't make sense to use it for `number`. Instead, assume everything starts 36940 * from a 0 mediaSequence value and increment from there. 36941 * 36942 * Note that VHS currently doesn't use the `number` property, but it can be helpful for 36943 * debugging and making sense of the manifest. 36944 * 36945 * For live playlists, to account for values increasing in manifests when periods are 36946 * removed on refreshes, merging logic should be used to update the numbers to their 36947 * appropriate values (to ensure they're sequential and increasing). 36948 * 36949 * @param {Object[]} playlists - the playlists to update 36950 * @param {TimelineStart[]} timelineStarts - the timeline starts for the manifest 36951 */ 36952 36953 const addMediaSequenceValues = (playlists, timelineStarts) => { 36954 // increment all segments sequentially 36955 playlists.forEach(playlist => { 36956 playlist.mediaSequence = 0; 36957 playlist.discontinuitySequence = timelineStarts.findIndex(function ({ 36958 timeline 36959 }) { 36960 return timeline === playlist.timeline; 36961 }); 36962 if (!playlist.segments) { 36963 return; 36964 } 36965 playlist.segments.forEach((segment, index) => { 36966 segment.number = index; 36967 }); 36968 }); 36969 }; 36970 /** 36971 * Given a media group object, flattens all playlists within the media group into a single 36972 * array. 36973 * 36974 * @param {Object} mediaGroupObject - the media group object 36975 * 36976 * @return {Object[]} 36977 * The media group playlists 36978 */ 36979 36980 const flattenMediaGroupPlaylists = mediaGroupObject => { 36981 if (!mediaGroupObject) { 36982 return []; 36983 } 36984 return Object.keys(mediaGroupObject).reduce((acc, label) => { 36985 const labelContents = mediaGroupObject[label]; 36986 return acc.concat(labelContents.playlists); 36987 }, []); 36988 }; 36989 const toM3u8 = ({ 36990 dashPlaylists, 36991 locations, 36992 contentSteering, 36993 sidxMapping = {}, 36994 previousManifest, 36995 eventStream 36996 }) => { 36997 if (!dashPlaylists.length) { 36998 return {}; 36999 } // grab all main manifest attributes 37000 37001 const { 37002 sourceDuration: duration, 37003 type, 37004 suggestedPresentationDelay, 37005 minimumUpdatePeriod 37006 } = dashPlaylists[0].attributes; 37007 const videoPlaylists = mergeDiscontiguousPlaylists(dashPlaylists.filter(videoOnly)).map(formatVideoPlaylist); 37008 const audioPlaylists = mergeDiscontiguousPlaylists(dashPlaylists.filter(audioOnly)); 37009 const vttPlaylists = mergeDiscontiguousPlaylists(dashPlaylists.filter(vttOnly)); 37010 const captions = dashPlaylists.map(playlist => playlist.attributes.captionServices).filter(Boolean); 37011 const manifest = { 37012 allowCache: true, 37013 discontinuityStarts: [], 37014 segments: [], 37015 endList: true, 37016 mediaGroups: { 37017 AUDIO: {}, 37018 VIDEO: {}, 37019 ['CLOSED-CAPTIONS']: {}, 37020 SUBTITLES: {} 37021 }, 37022 uri: '', 37023 duration, 37024 playlists: addSidxSegmentsToPlaylists(videoPlaylists, sidxMapping) 37025 }; 37026 if (minimumUpdatePeriod >= 0) { 37027 manifest.minimumUpdatePeriod = minimumUpdatePeriod * 1000; 37028 } 37029 if (locations) { 37030 manifest.locations = locations; 37031 } 37032 if (contentSteering) { 37033 manifest.contentSteering = contentSteering; 37034 } 37035 if (type === 'dynamic') { 37036 manifest.suggestedPresentationDelay = suggestedPresentationDelay; 37037 } 37038 if (eventStream && eventStream.length > 0) { 37039 manifest.eventStream = eventStream; 37040 } 37041 const isAudioOnly = manifest.playlists.length === 0; 37042 const organizedAudioGroup = audioPlaylists.length ? organizeAudioPlaylists(audioPlaylists, sidxMapping, isAudioOnly) : null; 37043 const organizedVttGroup = vttPlaylists.length ? organizeVttPlaylists(vttPlaylists, sidxMapping) : null; 37044 const formattedPlaylists = videoPlaylists.concat(flattenMediaGroupPlaylists(organizedAudioGroup), flattenMediaGroupPlaylists(organizedVttGroup)); 37045 const playlistTimelineStarts = formattedPlaylists.map(({ 37046 timelineStarts 37047 }) => timelineStarts); 37048 manifest.timelineStarts = getUniqueTimelineStarts(playlistTimelineStarts); 37049 addMediaSequenceValues(formattedPlaylists, manifest.timelineStarts); 37050 if (organizedAudioGroup) { 37051 manifest.mediaGroups.AUDIO.audio = organizedAudioGroup; 37052 } 37053 if (organizedVttGroup) { 37054 manifest.mediaGroups.SUBTITLES.subs = organizedVttGroup; 37055 } 37056 if (captions.length) { 37057 manifest.mediaGroups['CLOSED-CAPTIONS'].cc = organizeCaptionServices(captions); 37058 } 37059 if (previousManifest) { 37060 return positionManifestOnTimeline({ 37061 oldManifest: previousManifest, 37062 newManifest: manifest 37063 }); 37064 } 37065 return manifest; 37066 }; 37067 37068 /** 37069 * Calculates the R (repetition) value for a live stream (for the final segment 37070 * in a manifest where the r value is negative 1) 37071 * 37072 * @param {Object} attributes 37073 * Object containing all inherited attributes from parent elements with attribute 37074 * names as keys 37075 * @param {number} time 37076 * current time (typically the total time up until the final segment) 37077 * @param {number} duration 37078 * duration property for the given <S /> 37079 * 37080 * @return {number} 37081 * R value to reach the end of the given period 37082 */ 37083 const getLiveRValue = (attributes, time, duration) => { 37084 const { 37085 NOW, 37086 clientOffset, 37087 availabilityStartTime, 37088 timescale = 1, 37089 periodStart = 0, 37090 minimumUpdatePeriod = 0 37091 } = attributes; 37092 const now = (NOW + clientOffset) / 1000; 37093 const periodStartWC = availabilityStartTime + periodStart; 37094 const periodEndWC = now + minimumUpdatePeriod; 37095 const periodDuration = periodEndWC - periodStartWC; 37096 return Math.ceil((periodDuration * timescale - time) / duration); 37097 }; 37098 /** 37099 * Uses information provided by SegmentTemplate.SegmentTimeline to determine segment 37100 * timing and duration 37101 * 37102 * @param {Object} attributes 37103 * Object containing all inherited attributes from parent elements with attribute 37104 * names as keys 37105 * @param {Object[]} segmentTimeline 37106 * List of objects representing the attributes of each S element contained within 37107 * 37108 * @return {{number: number, duration: number, time: number, timeline: number}[]} 37109 * List of Objects with segment timing and duration info 37110 */ 37111 37112 const parseByTimeline = (attributes, segmentTimeline) => { 37113 const { 37114 type, 37115 minimumUpdatePeriod = 0, 37116 media = '', 37117 sourceDuration, 37118 timescale = 1, 37119 startNumber = 1, 37120 periodStart: timeline 37121 } = attributes; 37122 const segments = []; 37123 let time = -1; 37124 for (let sIndex = 0; sIndex < segmentTimeline.length; sIndex++) { 37125 const S = segmentTimeline[sIndex]; 37126 const duration = S.d; 37127 const repeat = S.r || 0; 37128 const segmentTime = S.t || 0; 37129 if (time < 0) { 37130 // first segment 37131 time = segmentTime; 37132 } 37133 if (segmentTime && segmentTime > time) { 37134 // discontinuity 37135 // TODO: How to handle this type of discontinuity 37136 // timeline++ here would treat it like HLS discontuity and content would 37137 // get appended without gap 37138 // E.G. 37139 // <S t="0" d="1" /> 37140 // <S d="1" /> 37141 // <S d="1" /> 37142 // <S t="5" d="1" /> 37143 // would have $Time$ values of [0, 1, 2, 5] 37144 // should this be appened at time positions [0, 1, 2, 3],(#EXT-X-DISCONTINUITY) 37145 // or [0, 1, 2, gap, gap, 5]? (#EXT-X-GAP) 37146 // does the value of sourceDuration consider this when calculating arbitrary 37147 // negative @r repeat value? 37148 // E.G. Same elements as above with this added at the end 37149 // <S d="1" r="-1" /> 37150 // with a sourceDuration of 10 37151 // Would the 2 gaps be included in the time duration calculations resulting in 37152 // 8 segments with $Time$ values of [0, 1, 2, 5, 6, 7, 8, 9] or 10 segments 37153 // with $Time$ values of [0, 1, 2, 5, 6, 7, 8, 9, 10, 11] ? 37154 time = segmentTime; 37155 } 37156 let count; 37157 if (repeat < 0) { 37158 const nextS = sIndex + 1; 37159 if (nextS === segmentTimeline.length) { 37160 // last segment 37161 if (type === 'dynamic' && minimumUpdatePeriod > 0 && media.indexOf('$Number$') > 0) { 37162 count = getLiveRValue(attributes, time, duration); 37163 } else { 37164 // TODO: This may be incorrect depending on conclusion of TODO above 37165 count = (sourceDuration * timescale - time) / duration; 37166 } 37167 } else { 37168 count = (segmentTimeline[nextS].t - time) / duration; 37169 } 37170 } else { 37171 count = repeat + 1; 37172 } 37173 const end = startNumber + segments.length + count; 37174 let number = startNumber + segments.length; 37175 while (number < end) { 37176 segments.push({ 37177 number, 37178 duration: duration / timescale, 37179 time, 37180 timeline 37181 }); 37182 time += duration; 37183 number++; 37184 } 37185 } 37186 return segments; 37187 }; 37188 const identifierPattern = /\$([A-z]*)(?:(%0)([0-9]+)d)?\$/g; 37189 /** 37190 * Replaces template identifiers with corresponding values. To be used as the callback 37191 * for String.prototype.replace 37192 * 37193 * @name replaceCallback 37194 * @function 37195 * @param {string} match 37196 * Entire match of identifier 37197 * @param {string} identifier 37198 * Name of matched identifier 37199 * @param {string} format 37200 * Format tag string. Its presence indicates that padding is expected 37201 * @param {string} width 37202 * Desired length of the replaced value. Values less than this width shall be left 37203 * zero padded 37204 * @return {string} 37205 * Replacement for the matched identifier 37206 */ 37207 37208 /** 37209 * Returns a function to be used as a callback for String.prototype.replace to replace 37210 * template identifiers 37211 * 37212 * @param {Obect} values 37213 * Object containing values that shall be used to replace known identifiers 37214 * @param {number} values.RepresentationID 37215 * Value of the Representation@id attribute 37216 * @param {number} values.Number 37217 * Number of the corresponding segment 37218 * @param {number} values.Bandwidth 37219 * Value of the Representation@bandwidth attribute. 37220 * @param {number} values.Time 37221 * Timestamp value of the corresponding segment 37222 * @return {replaceCallback} 37223 * Callback to be used with String.prototype.replace to replace identifiers 37224 */ 37225 37226 const identifierReplacement = values => (match, identifier, format, width) => { 37227 if (match === '$$') { 37228 // escape sequence 37229 return '$'; 37230 } 37231 if (typeof values[identifier] === 'undefined') { 37232 return match; 37233 } 37234 const value = '' + values[identifier]; 37235 if (identifier === 'RepresentationID') { 37236 // Format tag shall not be present with RepresentationID 37237 return value; 37238 } 37239 if (!format) { 37240 width = 1; 37241 } else { 37242 width = parseInt(width, 10); 37243 } 37244 if (value.length >= width) { 37245 return value; 37246 } 37247 return `${new Array(width - value.length + 1).join('0')}${value}`; 37248 }; 37249 /** 37250 * Constructs a segment url from a template string 37251 * 37252 * @param {string} url 37253 * Template string to construct url from 37254 * @param {Obect} values 37255 * Object containing values that shall be used to replace known identifiers 37256 * @param {number} values.RepresentationID 37257 * Value of the Representation@id attribute 37258 * @param {number} values.Number 37259 * Number of the corresponding segment 37260 * @param {number} values.Bandwidth 37261 * Value of the Representation@bandwidth attribute. 37262 * @param {number} values.Time 37263 * Timestamp value of the corresponding segment 37264 * @return {string} 37265 * Segment url with identifiers replaced 37266 */ 37267 37268 const constructTemplateUrl = (url, values) => url.replace(identifierPattern, identifierReplacement(values)); 37269 /** 37270 * Generates a list of objects containing timing and duration information about each 37271 * segment needed to generate segment uris and the complete segment object 37272 * 37273 * @param {Object} attributes 37274 * Object containing all inherited attributes from parent elements with attribute 37275 * names as keys 37276 * @param {Object[]|undefined} segmentTimeline 37277 * List of objects representing the attributes of each S element contained within 37278 * the SegmentTimeline element 37279 * @return {{number: number, duration: number, time: number, timeline: number}[]} 37280 * List of Objects with segment timing and duration info 37281 */ 37282 37283 const parseTemplateInfo = (attributes, segmentTimeline) => { 37284 if (!attributes.duration && !segmentTimeline) { 37285 // if neither @duration or SegmentTimeline are present, then there shall be exactly 37286 // one media segment 37287 return [{ 37288 number: attributes.startNumber || 1, 37289 duration: attributes.sourceDuration, 37290 time: 0, 37291 timeline: attributes.periodStart 37292 }]; 37293 } 37294 if (attributes.duration) { 37295 return parseByDuration(attributes); 37296 } 37297 return parseByTimeline(attributes, segmentTimeline); 37298 }; 37299 /** 37300 * Generates a list of segments using information provided by the SegmentTemplate element 37301 * 37302 * @param {Object} attributes 37303 * Object containing all inherited attributes from parent elements with attribute 37304 * names as keys 37305 * @param {Object[]|undefined} segmentTimeline 37306 * List of objects representing the attributes of each S element contained within 37307 * the SegmentTimeline element 37308 * @return {Object[]} 37309 * List of segment objects 37310 */ 37311 37312 const segmentsFromTemplate = (attributes, segmentTimeline) => { 37313 const templateValues = { 37314 RepresentationID: attributes.id, 37315 Bandwidth: attributes.bandwidth || 0 37316 }; 37317 const { 37318 initialization = { 37319 sourceURL: '', 37320 range: '' 37321 } 37322 } = attributes; 37323 const mapSegment = urlTypeToSegment({ 37324 baseUrl: attributes.baseUrl, 37325 source: constructTemplateUrl(initialization.sourceURL, templateValues), 37326 range: initialization.range 37327 }); 37328 const segments = parseTemplateInfo(attributes, segmentTimeline); 37329 return segments.map(segment => { 37330 templateValues.Number = segment.number; 37331 templateValues.Time = segment.time; 37332 const uri = constructTemplateUrl(attributes.media || '', templateValues); // See DASH spec section 5.3.9.2.2 37333 // - if timescale isn't present on any level, default to 1. 37334 37335 const timescale = attributes.timescale || 1; // - if presentationTimeOffset isn't present on any level, default to 0 37336 37337 const presentationTimeOffset = attributes.presentationTimeOffset || 0; 37338 const presentationTime = 37339 // Even if the @t attribute is not specified for the segment, segment.time is 37340 // calculated in mpd-parser prior to this, so it's assumed to be available. 37341 attributes.periodStart + (segment.time - presentationTimeOffset) / timescale; 37342 const map = { 37343 uri, 37344 timeline: segment.timeline, 37345 duration: segment.duration, 37346 resolvedUri: resolveUrl$1(attributes.baseUrl || '', uri), 37347 map: mapSegment, 37348 number: segment.number, 37349 presentationTime 37350 }; 37351 return map; 37352 }); 37353 }; 37354 37355 /** 37356 * Converts a <SegmentUrl> (of type URLType from the DASH spec 5.3.9.2 Table 14) 37357 * to an object that matches the output of a segment in videojs/mpd-parser 37358 * 37359 * @param {Object} attributes 37360 * Object containing all inherited attributes from parent elements with attribute 37361 * names as keys 37362 * @param {Object} segmentUrl 37363 * <SegmentURL> node to translate into a segment object 37364 * @return {Object} translated segment object 37365 */ 37366 37367 const SegmentURLToSegmentObject = (attributes, segmentUrl) => { 37368 const { 37369 baseUrl, 37370 initialization = {} 37371 } = attributes; 37372 const initSegment = urlTypeToSegment({ 37373 baseUrl, 37374 source: initialization.sourceURL, 37375 range: initialization.range 37376 }); 37377 const segment = urlTypeToSegment({ 37378 baseUrl, 37379 source: segmentUrl.media, 37380 range: segmentUrl.mediaRange 37381 }); 37382 segment.map = initSegment; 37383 return segment; 37384 }; 37385 /** 37386 * Generates a list of segments using information provided by the SegmentList element 37387 * SegmentList (DASH SPEC Section 5.3.9.3.2) contains a set of <SegmentURL> nodes. Each 37388 * node should be translated into segment. 37389 * 37390 * @param {Object} attributes 37391 * Object containing all inherited attributes from parent elements with attribute 37392 * names as keys 37393 * @param {Object[]|undefined} segmentTimeline 37394 * List of objects representing the attributes of each S element contained within 37395 * the SegmentTimeline element 37396 * @return {Object.<Array>} list of segments 37397 */ 37398 37399 const segmentsFromList = (attributes, segmentTimeline) => { 37400 const { 37401 duration, 37402 segmentUrls = [], 37403 periodStart 37404 } = attributes; // Per spec (5.3.9.2.1) no way to determine segment duration OR 37405 // if both SegmentTimeline and @duration are defined, it is outside of spec. 37406 37407 if (!duration && !segmentTimeline || duration && segmentTimeline) { 37408 throw new Error(errors.SEGMENT_TIME_UNSPECIFIED); 37409 } 37410 const segmentUrlMap = segmentUrls.map(segmentUrlObject => SegmentURLToSegmentObject(attributes, segmentUrlObject)); 37411 let segmentTimeInfo; 37412 if (duration) { 37413 segmentTimeInfo = parseByDuration(attributes); 37414 } 37415 if (segmentTimeline) { 37416 segmentTimeInfo = parseByTimeline(attributes, segmentTimeline); 37417 } 37418 const segments = segmentTimeInfo.map((segmentTime, index) => { 37419 if (segmentUrlMap[index]) { 37420 const segment = segmentUrlMap[index]; // See DASH spec section 5.3.9.2.2 37421 // - if timescale isn't present on any level, default to 1. 37422 37423 const timescale = attributes.timescale || 1; // - if presentationTimeOffset isn't present on any level, default to 0 37424 37425 const presentationTimeOffset = attributes.presentationTimeOffset || 0; 37426 segment.timeline = segmentTime.timeline; 37427 segment.duration = segmentTime.duration; 37428 segment.number = segmentTime.number; 37429 segment.presentationTime = periodStart + (segmentTime.time - presentationTimeOffset) / timescale; 37430 return segment; 37431 } // Since we're mapping we should get rid of any blank segments (in case 37432 // the given SegmentTimeline is handling for more elements than we have 37433 // SegmentURLs for). 37434 }).filter(segment => segment); 37435 return segments; 37436 }; 37437 const generateSegments = ({ 37438 attributes, 37439 segmentInfo 37440 }) => { 37441 let segmentAttributes; 37442 let segmentsFn; 37443 if (segmentInfo.template) { 37444 segmentsFn = segmentsFromTemplate; 37445 segmentAttributes = merge$1(attributes, segmentInfo.template); 37446 } else if (segmentInfo.base) { 37447 segmentsFn = segmentsFromBase; 37448 segmentAttributes = merge$1(attributes, segmentInfo.base); 37449 } else if (segmentInfo.list) { 37450 segmentsFn = segmentsFromList; 37451 segmentAttributes = merge$1(attributes, segmentInfo.list); 37452 } 37453 const segmentsInfo = { 37454 attributes 37455 }; 37456 if (!segmentsFn) { 37457 return segmentsInfo; 37458 } 37459 const segments = segmentsFn(segmentAttributes, segmentInfo.segmentTimeline); // The @duration attribute will be used to determin the playlist's targetDuration which 37460 // must be in seconds. Since we've generated the segment list, we no longer need 37461 // @duration to be in @timescale units, so we can convert it here. 37462 37463 if (segmentAttributes.duration) { 37464 const { 37465 duration, 37466 timescale = 1 37467 } = segmentAttributes; 37468 segmentAttributes.duration = duration / timescale; 37469 } else if (segments.length) { 37470 // if there is no @duration attribute, use the largest segment duration as 37471 // as target duration 37472 segmentAttributes.duration = segments.reduce((max, segment) => { 37473 return Math.max(max, Math.ceil(segment.duration)); 37474 }, 0); 37475 } else { 37476 segmentAttributes.duration = 0; 37477 } 37478 segmentsInfo.attributes = segmentAttributes; 37479 segmentsInfo.segments = segments; // This is a sidx box without actual segment information 37480 37481 if (segmentInfo.base && segmentAttributes.indexRange) { 37482 segmentsInfo.sidx = segments[0]; 37483 segmentsInfo.segments = []; 37484 } 37485 return segmentsInfo; 37486 }; 37487 const toPlaylists = representations => representations.map(generateSegments); 37488 const findChildren = (element, name) => from(element.childNodes).filter(({ 37489 tagName 37490 }) => tagName === name); 37491 const getContent = element => element.textContent.trim(); 37492 37493 /** 37494 * Converts the provided string that may contain a division operation to a number. 37495 * 37496 * @param {string} value - the provided string value 37497 * 37498 * @return {number} the parsed string value 37499 */ 37500 const parseDivisionValue = value => { 37501 return parseFloat(value.split('/').reduce((prev, current) => prev / current)); 37502 }; 37503 const parseDuration = str => { 37504 const SECONDS_IN_YEAR = 365 * 24 * 60 * 60; 37505 const SECONDS_IN_MONTH = 30 * 24 * 60 * 60; 37506 const SECONDS_IN_DAY = 24 * 60 * 60; 37507 const SECONDS_IN_HOUR = 60 * 60; 37508 const SECONDS_IN_MIN = 60; // P10Y10M10DT10H10M10.1S 37509 37510 const durationRegex = /P(?:(\d*)Y)?(?:(\d*)M)?(?:(\d*)D)?(?:T(?:(\d*)H)?(?:(\d*)M)?(?:([\d.]*)S)?)?/; 37511 const match = durationRegex.exec(str); 37512 if (!match) { 37513 return 0; 37514 } 37515 const [year, month, day, hour, minute, second] = match.slice(1); 37516 return parseFloat(year || 0) * SECONDS_IN_YEAR + parseFloat(month || 0) * SECONDS_IN_MONTH + parseFloat(day || 0) * SECONDS_IN_DAY + parseFloat(hour || 0) * SECONDS_IN_HOUR + parseFloat(minute || 0) * SECONDS_IN_MIN + parseFloat(second || 0); 37517 }; 37518 const parseDate = str => { 37519 // Date format without timezone according to ISO 8601 37520 // YYY-MM-DDThh:mm:ss.ssssss 37521 const dateRegex = /^\d+-\d+-\d+T\d+:\d+:\d+(\.\d+)?$/; // If the date string does not specifiy a timezone, we must specifiy UTC. This is 37522 // expressed by ending with 'Z' 37523 37524 if (dateRegex.test(str)) { 37525 str += 'Z'; 37526 } 37527 return Date.parse(str); 37528 }; 37529 const parsers = { 37530 /** 37531 * Specifies the duration of the entire Media Presentation. Format is a duration string 37532 * as specified in ISO 8601 37533 * 37534 * @param {string} value 37535 * value of attribute as a string 37536 * @return {number} 37537 * The duration in seconds 37538 */ 37539 mediaPresentationDuration(value) { 37540 return parseDuration(value); 37541 }, 37542 /** 37543 * Specifies the Segment availability start time for all Segments referred to in this 37544 * MPD. For a dynamic manifest, it specifies the anchor for the earliest availability 37545 * time. Format is a date string as specified in ISO 8601 37546 * 37547 * @param {string} value 37548 * value of attribute as a string 37549 * @return {number} 37550 * The date as seconds from unix epoch 37551 */ 37552 availabilityStartTime(value) { 37553 return parseDate(value) / 1000; 37554 }, 37555 /** 37556 * Specifies the smallest period between potential changes to the MPD. Format is a 37557 * duration string as specified in ISO 8601 37558 * 37559 * @param {string} value 37560 * value of attribute as a string 37561 * @return {number} 37562 * The duration in seconds 37563 */ 37564 minimumUpdatePeriod(value) { 37565 return parseDuration(value); 37566 }, 37567 /** 37568 * Specifies the suggested presentation delay. Format is a 37569 * duration string as specified in ISO 8601 37570 * 37571 * @param {string} value 37572 * value of attribute as a string 37573 * @return {number} 37574 * The duration in seconds 37575 */ 37576 suggestedPresentationDelay(value) { 37577 return parseDuration(value); 37578 }, 37579 /** 37580 * specifices the type of mpd. Can be either "static" or "dynamic" 37581 * 37582 * @param {string} value 37583 * value of attribute as a string 37584 * 37585 * @return {string} 37586 * The type as a string 37587 */ 37588 type(value) { 37589 return value; 37590 }, 37591 /** 37592 * Specifies the duration of the smallest time shifting buffer for any Representation 37593 * in the MPD. Format is a duration string as specified in ISO 8601 37594 * 37595 * @param {string} value 37596 * value of attribute as a string 37597 * @return {number} 37598 * The duration in seconds 37599 */ 37600 timeShiftBufferDepth(value) { 37601 return parseDuration(value); 37602 }, 37603 /** 37604 * Specifies the PeriodStart time of the Period relative to the availabilityStarttime. 37605 * Format is a duration string as specified in ISO 8601 37606 * 37607 * @param {string} value 37608 * value of attribute as a string 37609 * @return {number} 37610 * The duration in seconds 37611 */ 37612 start(value) { 37613 return parseDuration(value); 37614 }, 37615 /** 37616 * Specifies the width of the visual presentation 37617 * 37618 * @param {string} value 37619 * value of attribute as a string 37620 * @return {number} 37621 * The parsed width 37622 */ 37623 width(value) { 37624 return parseInt(value, 10); 37625 }, 37626 /** 37627 * Specifies the height of the visual presentation 37628 * 37629 * @param {string} value 37630 * value of attribute as a string 37631 * @return {number} 37632 * The parsed height 37633 */ 37634 height(value) { 37635 return parseInt(value, 10); 37636 }, 37637 /** 37638 * Specifies the bitrate of the representation 37639 * 37640 * @param {string} value 37641 * value of attribute as a string 37642 * @return {number} 37643 * The parsed bandwidth 37644 */ 37645 bandwidth(value) { 37646 return parseInt(value, 10); 37647 }, 37648 /** 37649 * Specifies the frame rate of the representation 37650 * 37651 * @param {string} value 37652 * value of attribute as a string 37653 * @return {number} 37654 * The parsed frame rate 37655 */ 37656 frameRate(value) { 37657 return parseDivisionValue(value); 37658 }, 37659 /** 37660 * Specifies the number of the first Media Segment in this Representation in the Period 37661 * 37662 * @param {string} value 37663 * value of attribute as a string 37664 * @return {number} 37665 * The parsed number 37666 */ 37667 startNumber(value) { 37668 return parseInt(value, 10); 37669 }, 37670 /** 37671 * Specifies the timescale in units per seconds 37672 * 37673 * @param {string} value 37674 * value of attribute as a string 37675 * @return {number} 37676 * The parsed timescale 37677 */ 37678 timescale(value) { 37679 return parseInt(value, 10); 37680 }, 37681 /** 37682 * Specifies the presentationTimeOffset. 37683 * 37684 * @param {string} value 37685 * value of the attribute as a string 37686 * 37687 * @return {number} 37688 * The parsed presentationTimeOffset 37689 */ 37690 presentationTimeOffset(value) { 37691 return parseInt(value, 10); 37692 }, 37693 /** 37694 * Specifies the constant approximate Segment duration 37695 * NOTE: The <Period> element also contains an @duration attribute. This duration 37696 * specifies the duration of the Period. This attribute is currently not 37697 * supported by the rest of the parser, however we still check for it to prevent 37698 * errors. 37699 * 37700 * @param {string} value 37701 * value of attribute as a string 37702 * @return {number} 37703 * The parsed duration 37704 */ 37705 duration(value) { 37706 const parsedValue = parseInt(value, 10); 37707 if (isNaN(parsedValue)) { 37708 return parseDuration(value); 37709 } 37710 return parsedValue; 37711 }, 37712 /** 37713 * Specifies the Segment duration, in units of the value of the @timescale. 37714 * 37715 * @param {string} value 37716 * value of attribute as a string 37717 * @return {number} 37718 * The parsed duration 37719 */ 37720 d(value) { 37721 return parseInt(value, 10); 37722 }, 37723 /** 37724 * Specifies the MPD start time, in @timescale units, the first Segment in the series 37725 * starts relative to the beginning of the Period 37726 * 37727 * @param {string} value 37728 * value of attribute as a string 37729 * @return {number} 37730 * The parsed time 37731 */ 37732 t(value) { 37733 return parseInt(value, 10); 37734 }, 37735 /** 37736 * Specifies the repeat count of the number of following contiguous Segments with the 37737 * same duration expressed by the value of @d 37738 * 37739 * @param {string} value 37740 * value of attribute as a string 37741 * @return {number} 37742 * The parsed number 37743 */ 37744 r(value) { 37745 return parseInt(value, 10); 37746 }, 37747 /** 37748 * Specifies the presentationTime. 37749 * 37750 * @param {string} value 37751 * value of the attribute as a string 37752 * 37753 * @return {number} 37754 * The parsed presentationTime 37755 */ 37756 presentationTime(value) { 37757 return parseInt(value, 10); 37758 }, 37759 /** 37760 *
37760Default parser for all other attributes. Acts as a no-op and just returns the value 37761 * as a string 37762 * 37763 * @param {string} value 37764 * value of attribute as a string 37765 * @return {string} 37766 * Unparsed value 37767 */ 37768 DEFAULT(value) { 37769 return value; 37770 } 37771 }; 37772 /** 37773 * Gets all the attributes and values of the provided node, parses attributes with known 37774 * types, and returns an object with attribute names mapped to values. 37775 * 37776 * @param {Node} el 37777 * The node to parse attributes from 37778 * @return {Object} 37779 * Object with all attributes of el parsed 37780 */ 37781 37782 const parseAttributes = el => { 37783 if (!(el && el.attributes)) { 37784 return {}; 37785 } 37786 return from(el.attributes).reduce((a, e) => { 37787 const parseFn = parsers[e.name] || parsers.DEFAULT; 37788 a[e.n
vendor: 43,831 bytes, lines 37788-38923
37788ame] = parseFn(e.value); 37789 return a; 37790 }, {}); 37791 }; 37792 const keySystemsMap = { 37793 'urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b': 'org.w3.clearkey', 37794 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed': 'com.widevine.alpha', 37795 'urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95': 'com.microsoft.playready', 37796 'urn:uuid:f239e769-efa3-4850-9c16-a903c6932efb': 'com.adobe.primetime', 37797 // ISO_IEC 23009-1_2022 5.8.5.2.2 The mp4 Protection Scheme 37798 'urn:mpeg:dash:mp4protection:2011': 'mp4protection' 37799 }; 37800 /** 37801 * Builds a list of urls that is the product of the reference urls and BaseURL values 37802 * 37803 * @param {Object[]} references 37804 * List of objects containing the reference URL as well as its attributes 37805 * @param {Node[]} baseUrlElements 37806 * List of BaseURL nodes from the mpd 37807 * @return {Object[]} 37808 * List of objects with resolved urls and attributes 37809 */ 37810 37811 const buildBaseUrls = (references, baseUrlElements) => { 37812 if (!baseUrlElements.length) { 37813 return references; 37814 } 37815 return flatten(references.map(function (reference) { 37816 return baseUrlElements.map(function (baseUrlElement) { 37817 const initialBaseUrl = getContent(baseUrlElement); 37818 const resolvedBaseUrl = resolveUrl$1(reference.baseUrl, initialBaseUrl); 37819 const finalBaseUrl = merge$1(parseAttributes(baseUrlElement), { 37820 baseUrl: resolvedBaseUrl 37821 }); // If the URL is resolved, we want to get the serviceLocation from the reference 37822 // assuming there is no serviceLocation on the initialBaseUrl 37823 37824 if (resolvedBaseUrl !== initialBaseUrl && !finalBaseUrl.serviceLocation && reference.serviceLocation) { 37825 finalBaseUrl.serviceLocation = reference.serviceLocation; 37826 } 37827 return finalBaseUrl; 37828 }); 37829 })); 37830 }; 37831 /** 37832 * Contains all Segment information for its containing AdaptationSet 37833 * 37834 * @typedef {Object} SegmentInformation 37835 * @property {Object|undefined} template 37836 * Contains the attributes for the SegmentTemplate node 37837 * @property {Object[]|undefined} segmentTimeline 37838 * Contains a list of atrributes for each S node within the SegmentTimeline node 37839 * @property {Object|undefined} list 37840 * Contains the attributes for the SegmentList node 37841 * @property {Object|undefined} base 37842 * Contains the attributes for the SegmentBase node 37843 */ 37844 37845 /** 37846 * Returns all available Segment information contained within the AdaptationSet node 37847 * 37848 * @param {Node} adaptationSet 37849 * The AdaptationSet node to get Segment information from 37850 * @return {SegmentInformation} 37851 * The Segment information contained within the provided AdaptationSet 37852 */ 37853 37854 const getSegmentInformation = adaptationSet => { 37855 const segmentTemplate = findChildren(adaptationSet, 'SegmentTemplate')[0]; 37856 const segmentList = findChildren(adaptationSet, 'SegmentList')[0]; 37857 const segmentUrls = segmentList && findChildren(segmentList, 'SegmentURL').map(s => merge$1({ 37858 tag: 'SegmentURL' 37859 }, parseAttributes(s))); 37860 const segmentBase = findChildren(adaptationSet, 'SegmentBase')[0]; 37861 const segmentTimelineParentNode = segmentList || segmentTemplate; 37862 const segmentTimeline = segmentTimelineParentNode && findChildren(segmentTimelineParentNode, 'SegmentTimeline')[0]; 37863 const segmentInitializationParentNode = segmentList || segmentBase || segmentTemplate; 37864 const segmentInitialization = segmentInitializationParentNode && findChildren(segmentInitializationParentNode, 'Initialization')[0]; // SegmentTemplate is handled slightly differently, since it can have both 37865 // @initialization and an <Initialization> node. @initialization can be templated, 37866 // while the node can have a url and range specified. If the <SegmentTemplate> has 37867 // both @initialization and an <Initialization> subelement we opt to override with 37868 // the node, as this interaction is not defined in the spec. 37869 37870 const template = segmentTemplate && parseAttributes(segmentTemplate); 37871 if (template && segmentInitialization) { 37872 template.initialization = segmentInitialization && parseAttributes(segmentInitialization); 37873 } else if (template && template.initialization) { 37874 // If it is @initialization we convert it to an object since this is the format that 37875 // later functions will rely on for the initialization segment. This is only valid 37876 // for <SegmentTemplate> 37877 template.initialization = { 37878 sourceURL: template.initialization 37879 }; 37880 } 37881 const segmentInfo = { 37882 template, 37883 segmentTimeline: segmentTimeline && findChildren(segmentTimeline, 'S').map(s => parseAttributes(s)), 37884 list: segmentList && merge$1(parseAttributes(segmentList), { 37885 segmentUrls, 37886 initialization: parseAttributes(segmentInitialization) 37887 }), 37888 base: segmentBase && merge$1(parseAttributes(segmentBase), { 37889 initialization: parseAttributes(segmentInitialization) 37890 }) 37891 }; 37892 Object.keys(segmentInfo).forEach(key => { 37893 if (!segmentInfo[key]) { 37894 delete segmentInfo[key]; 37895 } 37896 }); 37897 return segmentInfo; 37898 }; 37899 /** 37900 * Contains Segment information and attributes needed to construct a Playlist object 37901 * from a Representation 37902 * 37903 * @typedef {Object} RepresentationInformation 37904 * @property {SegmentInformation} segmentInfo 37905 * Segment information for this Representation 37906 * @property {Object} attributes 37907 * Inherited attributes for this Representation 37908 */ 37909 37910 /** 37911 * Maps a Representation node to an object containing Segment information and attributes 37912 * 37913 * @name inheritBaseUrlsCallback 37914 * @function 37915 * @param {Node} representation 37916 * Representation node from the mpd 37917 * @return {RepresentationInformation} 37918 * Representation information needed to construct a Playlist object 37919 */ 37920 37921 /** 37922 * Returns a callback for Array.prototype.map for mapping Representation nodes to 37923 * Segment information and attributes using inherited BaseURL nodes. 37924 * 37925 * @param {Object} adaptationSetAttributes 37926 * Contains attributes inherited by the AdaptationSet 37927 * @param {Object[]} adaptationSetBaseUrls 37928 * List of objects containing resolved base URLs and attributes 37929 * inherited by the AdaptationSet 37930 * @param {SegmentInformation} adaptationSetSegmentInfo 37931 * Contains Segment information for the AdaptationSet 37932 * @return {inheritBaseUrlsCallback} 37933 * Callback map function 37934 */ 37935 37936 const inheritBaseUrls = (adaptationSetAttributes, adaptationSetBaseUrls, adaptationSetSegmentInfo) => representation => { 37937 const repBaseUrlElements = findChildren(representation, 'BaseURL'); 37938 const repBaseUrls = buildBaseUrls(adaptationSetBaseUrls, repBaseUrlElements); 37939 const attributes = merge$1(adaptationSetAttributes, parseAttributes(representation)); 37940 const representationSegmentInfo = getSegmentInformation(representation); 37941 return repBaseUrls.map(baseUrl => { 37942 return { 37943 segmentInfo: merge$1(adaptationSetSegmentInfo, representationSegmentInfo), 37944 attributes: merge$1(attributes, baseUrl) 37945 }; 37946 }); 37947 }; 37948 /** 37949 * Tranforms a series of content protection nodes to 37950 * an object containing pssh data by key system 37951 * 37952 * @param {Node[]} contentProtectionNodes 37953 * Content protection nodes 37954 * @return {Object} 37955 * Object containing pssh data by key system 37956 */ 37957 37958 const generateKeySystemInformation = contentProtectionNodes => { 37959 return contentProtectionNodes.reduce((acc, node) => { 37960 const attributes = parseAttributes(node); // Although it could be argued that according to the UUID RFC spec the UUID string (a-f chars) should be generated 37961 // as a lowercase string it also mentions it should be treated as case-insensitive on input. Since the key system 37962 // UUIDs in the keySystemsMap are hardcoded as lowercase in the codebase there isn't any reason not to do 37963 // .toLowerCase() on the input UUID string from the manifest (at least I could not think of one). 37964 37965 if (attributes.schemeIdUri) { 37966 attributes.schemeIdUri = attributes.schemeIdUri.toLowerCase(); 37967 } 37968 const keySystem = keySystemsMap[attributes.schemeIdUri]; 37969 if (keySystem) { 37970 acc[keySystem] = { 37971 attributes 37972 }; 37973 const psshNode = findChildren(node, 'cenc:pssh')[0]; 37974 if (psshNode) { 37975 const pssh = getContent(psshNode); 37976 acc[keySystem].pssh = pssh && decodeB64ToUint8Array(pssh); 37977 } 37978 } 37979 return acc; 37980 }, {}); 37981 }; // defined in ANSI_SCTE 214-1 2016 37982 37983 const parseCaptionServiceMetadata = service => { 37984 // 608 captions 37985 if (service.schemeIdUri === 'urn:scte:dash:cc:cea-608:2015') { 37986 const values = typeof service.value !== 'string' ? [] : service.value.split(';'); 37987 return values.map(value => { 37988 let channel; 37989 let language; // default language to value 37990 37991 language = value; 37992 if (/^CC\d=/.test(value)) { 37993 [channel, language] = value.split('='); 37994 } else if (/^CC\d$/.test(value)) { 37995 channel = value; 37996 } 37997 return { 37998 channel, 37999 language 38000 }; 38001 }); 38002 } else if (service.schemeIdUri === 'urn:scte:dash:cc:cea-708:2015') { 38003 const values = typeof service.value !== 'string' ? [] : service.value.split(';'); 38004 return values.map(value => { 38005 const flags = { 38006 // service or channel number 1-63 38007 'channel': undefined, 38008 // language is a 3ALPHA per ISO 639.2/B 38009 // field is required 38010 'language': undefined, 38011 // BIT 1/0 or ? 38012 // default value is 1, meaning 16:9 aspect ratio, 0 is 4:3, ? is unknown 38013 'aspectRatio': 1, 38014 // BIT 1/0 38015 // easy reader flag indicated the text is tailed to the needs of beginning readers 38016 // default 0, or off 38017 'easyReader': 0, 38018 // BIT 1/0 38019 // If 3d metadata is present (CEA-708.1) then 1 38020 // default 0 38021 '3D': 0 38022 }; 38023 if (/=/.test(value)) { 38024 const [channel, opts = ''] = value.split('='); 38025 flags.channel = channel; 38026 flags.language = value; 38027 opts.split(',').forEach(opt => { 38028 const [name, val] = opt.split(':'); 38029 if (name === 'lang') { 38030 flags.language = val; // er for easyReadery 38031 } else if (name === 'er') { 38032 flags.easyReader = Number(val); // war for wide aspect ratio 38033 } else if (name === 'war') { 38034 flags.aspectRatio = Number(val); 38035 } else if (name === '3D') { 38036 flags['3D'] = Number(val); 38037 } 38038 }); 38039 } else { 38040 flags.language = value; 38041 } 38042 if (flags.channel) { 38043 flags.channel = 'SERVICE' + flags.channel; 38044 } 38045 return flags; 38046 }); 38047 } 38048 }; 38049 /** 38050 * A map callback that will parse all event stream data for a collection of periods 38051 * DASH ISO_IEC_23009 5.10.2.2 38052 * https://dashif-documents.azurewebsites.net/Events/master/event.html#mpd-event-timing 38053 * 38054 * @param {PeriodInformation} period object containing necessary period information 38055 * @return a collection of parsed eventstream event objects 38056 */ 38057 38058 const toEventStream = period => { 38059 // get and flatten all EventStreams tags and parse attributes and children 38060 return flatten(findChildren(period.node, 'EventStream').map(eventStream => { 38061 const eventStreamAttributes = parseAttributes(eventStream); 38062 const schemeIdUri = eventStreamAttributes.schemeIdUri; // find all Events per EventStream tag and map to return objects 38063 38064 return findChildren(eventStream, 'Event').map(event => { 38065 const eventAttributes = parseAttributes(event); 38066 const presentationTime = eventAttributes.presentationTime || 0; 38067 const timescale = eventStreamAttributes.timescale || 1; 38068 const duration = eventAttributes.duration || 0; 38069 const start = presentationTime / timescale + period.attributes.start; 38070 return { 38071 schemeIdUri, 38072 value: eventStreamAttributes.value, 38073 id: eventAttributes.id, 38074 start, 38075 end: start + duration / timescale, 38076 messageData: getContent(event) || eventAttributes.messageData, 38077 contentEncoding: eventStreamAttributes.contentEncoding, 38078 presentationTimeOffset: eventStreamAttributes.presentationTimeOffset || 0 38079 }; 38080 }); 38081 })); 38082 }; 38083 /** 38084 * Maps an AdaptationSet node to a list of Representation information objects 38085 * 38086 * @name toRepresentationsCallback 38087 * @function 38088 * @param {Node} adaptationSet 38089 * AdaptationSet node from the mpd 38090 * @return {RepresentationInformation[]} 38091 * List of objects containing Representaion information 38092 */ 38093 38094 /** 38095 * Returns a callback for Array.prototype.map for mapping AdaptationSet nodes to a list of 38096 * Representation information objects 38097 * 38098 * @param {Object} periodAttributes 38099 * Contains attributes inherited by the Period 38100 * @param {Object[]} periodBaseUrls 38101 * Contains list of objects with resolved base urls and attributes 38102 * inherited by the Period 38103 * @param {string[]} periodSegmentInfo 38104 * Contains Segment Information at the period level 38105 * @return {toRepresentationsCallback} 38106 * Callback map function 38107 */ 38108 38109 const toRepresentations = (periodAttributes, periodBaseUrls, periodSegmentInfo) => adaptationSet => { 38110 const adaptationSetAttributes = parseAttributes(adaptationSet); 38111 const adaptationSetBaseUrls = buildBaseUrls(periodBaseUrls, findChildren(adaptationSet, 'BaseURL')); 38112 const role = findChildren(adaptationSet, 'Role')[0]; 38113 const roleAttributes = { 38114 role: parseAttributes(role) 38115 }; 38116 let attrs = merge$1(periodAttributes, adaptationSetAttributes, roleAttributes); 38117 const accessibility = findChildren(adaptationSet, 'Accessibility')[0]; 38118 const captionServices = parseCaptionServiceMetadata(parseAttributes(accessibility)); 38119 if (captionServices) { 38120 attrs = merge$1(attrs, { 38121 captionServices 38122 }); 38123 } 38124 const label = findChildren(adaptationSet, 'Label')[0]; 38125 if (label && label.childNodes.length) { 38126 const labelVal = label.childNodes[0].nodeValue.trim(); 38127 attrs = merge$1(attrs, { 38128 label: labelVal 38129 }); 38130 } 38131 const contentProtection = generateKeySystemInformation(findChildren(adaptationSet, 'ContentProtection')); 38132 if (Object.keys(contentProtection).length) { 38133 attrs = merge$1(attrs, { 38134 contentProtection 38135 }); 38136 } 38137 const segmentInfo = getSegmentInformation(adaptationSet); 38138 const representations = findChildren(adaptationSet, 'Representation'); 38139 const adaptationSetSegmentInfo = merge$1(periodSegmentInfo, segmentInfo); 38140 return flatten(representations.map(inheritBaseUrls(attrs, adaptationSetBaseUrls, adaptationSetSegmentInfo))); 38141 }; 38142 /** 38143 * Contains all period information for mapping nodes onto adaptation sets. 38144 * 38145 * @typedef {Object} PeriodInformation 38146 * @property {Node} period.node 38147 * Period node from the mpd 38148 * @property {Object} period.attributes 38149 * Parsed period attributes from node plus any added 38150 */ 38151 38152 /** 38153 * Maps a PeriodInformation object to a list of Representation information objects for all 38154 * AdaptationSet nodes contained within the Period. 38155 * 38156 * @name toAdaptationSetsCallback 38157 * @function 38158 * @param {PeriodInformation} period 38159 * Period object containing necessary period information 38160 * @param {number} periodStart 38161 * Start time of the Period within the mpd 38162 * @return {RepresentationInformation[]} 38163 * List of objects containing Representaion information 38164 */ 38165 38166 /** 38167 * Returns a callback for Array.prototype.map for mapping Period nodes to a list of 38168 * Representation information objects 38169 * 38170 * @param {Object} mpdAttributes 38171 * Contains attributes inherited by the mpd 38172 * @param {Object[]} mpdBaseUrls 38173 * Contains list of objects with resolved base urls and attributes 38174 * inherited by the mpd 38175 * @return {toAdaptationSetsCallback} 38176 * Callback map function 38177 */ 38178 38179 const toAdaptationSets = (mpdAttributes, mpdBaseUrls) => (period, index) => { 38180 const periodBaseUrls = buildBaseUrls(mpdBaseUrls, findChildren(period.node, 'BaseURL')); 38181 const periodAttributes = merge$1(mpdAttributes, { 38182 periodStart: period.attributes.start 38183 }); 38184 if (typeof period.attributes.duration === 'number') { 38185 periodAttributes.periodDuration = period.attributes.duration; 38186 } 38187 const adaptationSets = findChildren(period.node, 'AdaptationSet'); 38188 const periodSegmentInfo = getSegmentInformation(period.node); 38189 return flatten(adaptationSets.map(toRepresentations(periodAttributes, periodBaseUrls, periodSegmentInfo))); 38190 }; 38191 /** 38192 * Tranforms an array of content steering nodes into an object 38193 * containing CDN content steering information from the MPD manifest. 38194 * 38195 * For more information on the DASH spec for Content Steering parsing, see: 38196 * https://dashif.org/docs/DASH-IF-CTS-00XX-Content-Steering-Community-Review.pdf 38197 * 38198 * @param {Node[]} contentSteeringNodes 38199 * Content steering nodes 38200 * @param {Function} eventHandler 38201 * The event handler passed into the parser options to handle warnings 38202 * @return {Object} 38203 * Object containing content steering data 38204 */ 38205 38206 const generateContentSteeringInformation = (contentSteeringNodes, eventHandler) => { 38207 // If there are more than one ContentSteering tags, throw an error 38208 if (contentSteeringNodes.length > 1) { 38209 eventHandler({ 38210 type: 'warn', 38211 message: 'The MPD manifest should contain no more than one ContentSteering tag' 38212 }); 38213 } // Return a null value if there are no ContentSteering tags 38214 38215 if (!contentSteeringNodes.length) { 38216 return null; 38217 } 38218 const infoFromContentSteeringTag = merge$1({ 38219 serverURL: getContent(contentSteeringNodes[0]) 38220 }, parseAttributes(contentSteeringNodes[0])); // Converts `queryBeforeStart` to a boolean, as well as setting the default value 38221 // to `false` if it doesn't exist 38222 38223 infoFromContentSteeringTag.queryBeforeStart = infoFromContentSteeringTag.queryBeforeStart === 'true'; 38224 return infoFromContentSteeringTag; 38225 }; 38226 /** 38227 * Gets Period@start property for a given period. 38228 * 38229 * @param {Object} options 38230 * Options object 38231 * @param {Object} options.attributes 38232 * Period attributes 38233 * @param {Object} [options.priorPeriodAttributes] 38234 * Prior period attributes (if prior period is available) 38235 * @param {string} options.mpdType 38236 * The MPD@type these periods came from 38237 * @return {number|null} 38238 * The period start, or null if it's an early available period or error 38239 */ 38240 38241 const getPeriodStart = ({ 38242 attributes, 38243 priorPeriodAttributes, 38244 mpdType 38245 }) => { 38246 // Summary of period start time calculation from DASH spec section 5.3.2.1 38247 // 38248 // A period's start is the first period's start + time elapsed after playing all 38249 // prior periods to this one. Periods continue one after the other in time (without 38250 // gaps) until the end of the presentation. 38251 // 38252 // The value of Period@start should be: 38253 // 1. if Period@start is present: value of Period@start 38254 // 2. if previous period exists and it has @duration: previous Period@start + 38255 // previous Period@duration 38256 // 3. if this is first period and MPD@type is 'static': 0 38257 // 4. in all other cases, consider the period an "early available period" (note: not 38258 // currently supported) 38259 // (1) 38260 if (typeof attributes.start === 'number') { 38261 return attributes.start; 38262 } // (2) 38263 38264 if (priorPeriodAttributes && typeof priorPeriodAttributes.start === 'number' && typeof priorPeriodAttributes.duration === 'number') { 38265 return priorPeriodAttributes.start + priorPeriodAttributes.duration; 38266 } // (3) 38267 38268 if (!priorPeriodAttributes && mpdType === 'static') { 38269 return 0; 38270 } // (4) 38271 // There is currently no logic for calculating the Period@start value if there is 38272 // no Period@start or prior Period@start and Period@duration available. This is not made 38273 // explicit by the DASH interop guidelines or the DASH spec, however, since there's 38274 // nothing about any other resolution strategies, it's implied. Thus, this case should 38275 // be considered an early available period, or error, and null should suffice for both 38276 // of those cases. 38277 38278 return null; 38279 }; 38280 /** 38281 * Traverses the mpd xml tree to generate a list of Representation information objects 38282 * that have inherited attributes from parent nodes 38283 * 38284 * @param {Node} mpd 38285 * The root node of the mpd 38286 * @param {Object} options 38287 * Available options for inheritAttributes 38288 * @param {string} options.manifestUri 38289 * The uri source of the mpd 38290 * @param {number} options.NOW 38291 * Current time per DASH IOP. Default is current time in ms since epoch 38292 * @param {number} options.clientOffset 38293 * Client time difference from NOW (in milliseconds) 38294 * @return {RepresentationInformation[]} 38295 * List of objects containing Representation information 38296 */ 38297 38298 const inheritAttributes = (mpd, options = {}) => { 38299 const { 38300 manifestUri = '', 38301 NOW = Date.now(), 38302 clientOffset = 0, 38303 // TODO: For now, we are expecting an eventHandler callback function 38304 // to be passed into the mpd parser as an option. 38305 // In the future, we should enable stream parsing by using the Stream class from vhs-utils. 38306 // This will support new features including a standardized event handler. 38307 // See the m3u8 parser for examples of how stream parsing is currently used for HLS parsing. 38308 // https://github.com/videojs/vhs-utils/blob/88d6e10c631e57a5af02c5a62bc7376cd456b4f5/src/stream.js#L9 38309 eventHandler = function () {} 38310 } = options; 38311 const periodNodes = findChildren(mpd, 'Period'); 38312 if (!periodNodes.length) { 38313 throw new Error(errors.INVALID_NUMBER_OF_PERIOD); 38314 } 38315 const locations = findChildren(mpd, 'Location'); 38316 const mpdAttributes = parseAttributes(mpd); 38317 const mpdBaseUrls = buildBaseUrls([{ 38318 baseUrl: manifestUri 38319 }], findChildren(mpd, 'BaseURL')); 38320 const contentSteeringNodes = findChildren(mpd, 'ContentSteering'); // See DASH spec section 5.3.1.2, Semantics of MPD element. Default type to 'static'. 38321 38322 mpdAttributes.type = mpdAttributes.type || 'static'; 38323 mpdAttributes.sourceDuration = mpdAttributes.mediaPresentationDuration || 0; 38324 mpdAttributes.NOW = NOW; 38325 mpdAttributes.clientOffset = clientOffset; 38326 if (locations.length) { 38327 mpdAttributes.locations = locations.map(getContent); 38328 } 38329 const periods = []; // Since toAdaptationSets acts on individual periods right now, the simplest approach to 38330 // adding properties that require looking at prior periods is to parse attributes and add 38331 // missing ones before toAdaptationSets is called. If more such properties are added, it 38332 // may be better to refactor toAdaptationSets. 38333 38334 periodNodes.forEach((node, index) => { 38335 const attributes = parseAttributes(node); // Use the last modified prior period, as it may contain added information necessary 38336 // for this period. 38337 38338 const priorPeriod = periods[index - 1]; 38339 attributes.start = getPeriodStart({ 38340 attributes, 38341 priorPeriodAttributes: priorPeriod ? priorPeriod.attributes : null, 38342 mpdType: mpdAttributes.type 38343 }); 38344 periods.push({ 38345 node, 38346 attributes 38347 }); 38348 }); 38349 return { 38350 locations: mpdAttributes.locations, 38351 contentSteeringInfo: generateContentSteeringInformation(contentSteeringNodes, eventHandler), 38352 // TODO: There are occurences where this `representationInfo` array contains undesired 38353 // duplicates. This generally occurs when there are multiple BaseURL nodes that are 38354 // direct children of the MPD node. When we attempt to resolve URLs from a combination of the 38355 // parent BaseURL and a child BaseURL, and the value does not resolve, 38356 // we end up returning the child BaseURL multiple times. 38357 // We need to determine a way to remove these duplicates in a safe way. 38358 // See: https://github.com/videojs/mpd-parser/pull/17#discussion_r162750527 38359 representationInfo: flatten(periods.map(toAdaptationSets(mpdAttributes, mpdBaseUrls))), 38360 eventStream: flatten(periods.map(toEventStream)) 38361 }; 38362 }; 38363 const stringToMpdXml = manifestString => { 38364 if (manifestString === '') { 38365 throw new Error(errors.DASH_EMPTY_MANIFEST); 38366 } 38367 const parser = new DOMParser(); 38368 let xml; 38369 let mpd; 38370 try { 38371 xml = parser.parseFromString(manifestString, 'application/xml'); 38372 mpd = xml && xml.documentElement.tagName === 'MPD' ? xml.documentElement : null; 38373 } catch (e) {// ie 11 throws on invalid xml 38374 } 38375 if (!mpd || mpd && mpd.getElementsByTagName('parsererror').length > 0) { 38376 throw new Error(errors.DASH_INVALID_XML); 38377 } 38378 return mpd; 38379 }; 38380 38381 /** 38382 * Parses the manifest for a UTCTiming node, returning the nodes attributes if found 38383 * 38384 * @param {string} mpd 38385 * XML string of the MPD manifest 38386 * @return {Object|null} 38387 * Attributes of UTCTiming node specified in the manifest. Null if none found 38388 */ 38389 38390 const parseUTCTimingScheme = mpd => { 38391 const UTCTimingNode = findChildren(mpd, 'UTCTiming')[0]; 38392 if (!UTCTimingNode) { 38393 return null; 38394 } 38395 const attributes = parseAttributes(UTCTimingNode); 38396 switch (attributes.schemeIdUri) { 38397 case 'urn:mpeg:dash:utc:http-head:2014': 38398 case 'urn:mpeg:dash:utc:http-head:2012': 38399 attributes.method = 'HEAD'; 38400 break; 38401 case 'urn:mpeg:dash:utc:http-xsdate:2014': 38402 case 'urn:mpeg:dash:utc:http-iso:2014': 38403 case 'urn:mpeg:dash:utc:http-xsdate:2012': 38404 case 'urn:mpeg:dash:utc:http-iso:2012': 38405 attributes.method = 'GET'; 38406 break; 38407 case 'urn:mpeg:dash:utc:direct:2014': 38408 case 'urn:mpeg:dash:utc:direct:2012': 38409 attributes.method = 'DIRECT'; 38410 attributes.value = Date.parse(attributes.value); 38411 break; 38412 case 'urn:mpeg:dash:utc:http-ntp:2014': 38413 case 'urn:mpeg:dash:utc:ntp:2014': 38414 case 'urn:mpeg:dash:utc:sntp:2014': 38415 default: 38416 throw new Error(errors.UNSUPPORTED_UTC_TIMING_SCHEME); 38417 } 38418 return attributes; 38419 }; 38420 /* 38421 * Given a DASH manifest string and options, parses the DASH manifest into an object in the 38422 * form outputed by m3u8-parser and accepted by videojs/http-streaming. 38423 * 38424 * For live DASH manifests, if `previousManifest` is provided in options, then the newly 38425 * parsed DASH manifest will have its media sequence and discontinuity sequence values 38426 * updated to reflect its position relative to the prior manifest. 38427 * 38428 * @param {string} manifestString - the DASH manifest as a string 38429 * @param {options} [options] - any options 38430 * 38431 * @return {Object} the manifest object 38432 */ 38433 38434 const parse = (manifestString, options = {}) => { 38435 const parsedManifestInfo = inheritAttributes(stringToMpdXml(manifestString), options); 38436 const playlists = toPlaylists(parsedManifestInfo.representationInfo); 38437 return toM3u8({ 38438 dashPlaylists: playlists, 38439 locations: parsedManifestInfo.locations, 38440 contentSteering: parsedManifestInfo.contentSteeringInfo, 38441 sidxMapping: options.sidxMapping, 38442 previousManifest: options.previousManifest, 38443 eventStream: parsedManifestInfo.eventStream 38444 }); 38445 }; 38446 /** 38447 * Parses the manifest for a UTCTiming node, returning the nodes attributes if found 38448 * 38449 * @param {string} manifestString 38450 * XML string of the MPD manifest 38451 * @return {Object|null} 38452 * Attributes of UTCTiming node specified in the manifest. Null if none found 38453 */ 38454 38455 const parseUTCTiming = manifestString => parseUTCTimingScheme(stringToMpdXml(manifestString)); 38456 38457 var MAX_UINT32 = Math.pow(2, 32); 38458 var getUint64$1 = function (uint8) { 38459 var dv = new DataView(uint8.buffer, uint8.byteOffset, uint8.byteLength); 38460 var value; 38461 if (dv.getBigUint64) { 38462 value = dv.getBigUint64(0); 38463 if (value < Number.MAX_SAFE_INTEGER) { 38464 return Number(value); 38465 } 38466 return value; 38467 } 38468 return dv.getUint32(0) * MAX_UINT32 + dv.getUint32(4); 38469 }; 38470 var numbers = { 38471 getUint64: getUint64$1, 38472 MAX_UINT32: MAX_UINT32 38473 }; 38474 38475 var getUint64 = numbers.getUint64; 38476 var parseSidx = function (data) { 38477 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 38478 result = { 38479 version: data[0], 38480 flags: new Uint8Array(data.subarray(1, 4)), 38481 references: [], 38482 referenceId: view.getUint32(4), 38483 timescale: view.getUint32(8) 38484 }, 38485 i = 12; 38486 if (result.version === 0) { 38487 result.earliestPresentationTime = view.getUint32(i); 38488 result.firstOffset = view.getUint32(i + 4); 38489 i += 8; 38490 } else { 38491 // read 64 bits 38492 result.earliestPresentationTime = getUint64(data.subarray(i)); 38493 result.firstOffset = getUint64(data.subarray(i + 8)); 38494 i += 16; 38495 } 38496 i += 2; // reserved 38497 38498 var referenceCount = view.getUint16(i); 38499 i += 2; // start of references 38500 38501 for (; referenceCount > 0; i += 12, referenceCount--) { 38502 result.references.push({ 38503 referenceType: (data[i] & 0x80) >>> 7, 38504 referencedSize: view.getUint32(i) & 0x7FFFFFFF, 38505 subsegmentDuration: view.getUint32(i + 4), 38506 startsWithSap: !!(data[i + 8] & 0x80), 38507 sapType: (data[i + 8] & 0x70) >>> 4, 38508 sapDeltaTime: view.getUint32(i + 8) & 0x0FFFFFFF 38509 }); 38510 } 38511 return result; 38512 }; 38513 var parseSidx_1 = parseSidx; 38514 38515 var ID3 = toUint8([0x49, 0x44, 0x33]); 38516 var getId3Size = function getId3Size(bytes, offset) { 38517 if (offset === void 0) { 38518 offset = 0; 38519 } 38520 bytes = toUint8(bytes); 38521 var flags = bytes[offset + 5]; 38522 var returnSize = bytes[offset + 6] << 21 | bytes[offset + 7] << 14 | bytes[offset + 8] << 7 | bytes[offset + 9]; 38523 var footerPresent = (flags & 16) >> 4; 38524 if (footerPresent) { 38525 return returnSize + 20; 38526 } 38527 return returnSize + 10; 38528 }; 38529 var getId3Offset = function getId3Offset(bytes, offset) { 38530 if (offset === void 0) { 38531 offset = 0; 38532 } 38533 bytes = toUint8(bytes); 38534 if (bytes.length - offset < 10 || !bytesMatch(bytes, ID3, { 38535 offset: offset 38536 })) { 38537 return offset; 38538 } 38539 offset += getId3Size(bytes, offset); // recursive check for id3 tags as some files 38540 // have multiple ID3 tag sections even though 38541 // they should not. 38542 38543 return getId3Offset(bytes, offset); 38544 }; 38545 38546 var normalizePath$1 = function normalizePath(path) { 38547 if (typeof path === 'string') { 38548 return stringToBytes(path); 38549 } 38550 if (typeof path === 'number') { 38551 return path; 38552 } 38553 return path; 38554 }; 38555 var normalizePaths$1 = function normalizePaths(paths) { 38556 if (!Array.isArray(paths)) { 38557 return [normalizePath$1(paths)]; 38558 } 38559 return paths.map(function (p) { 38560 return normalizePath$1(p); 38561 }); 38562 }; 38563 /** 38564 * find any number of boxes by name given a path to it in an iso bmff 38565 * such as mp4. 38566 * 38567 * @param {TypedArray} bytes 38568 * bytes for the iso bmff to search for boxes in 38569 * 38570 * @param {Uint8Array[]|string[]|string|Uint8Array} name 38571 * An array of paths or a single path representing the name 38572 * of boxes to search through in bytes. Paths may be 38573 * uint8 (character codes) or strings. 38574 * 38575 * @param {boolean} [complete=false] 38576 * Should we search only for complete boxes on the final path. 38577 * This is very useful when you do not want to get back partial boxes 38578 * in the case of streaming files. 38579 * 38580 * @return {Uint8Array[]} 38581 * An array of the end paths that we found. 38582 */ 38583 38584 var findBox = function findBox(bytes, paths, complete) { 38585 if (complete === void 0) { 38586 complete = false; 38587 } 38588 paths = normalizePaths$1(paths); 38589 bytes = toUint8(bytes); 38590 var results = []; 38591 if (!paths.length) { 38592 // short-circuit the search for empty paths 38593 return results; 38594 } 38595 var i = 0; 38596 while (i < bytes.length) { 38597 var size = (bytes[i] << 24 | bytes[i + 1] << 16 | bytes[i + 2] << 8 | bytes[i + 3]) >>> 0; 38598 var type = bytes.subarray(i + 4, i + 8); // invalid box format. 38599 38600 if (size === 0) { 38601 break; 38602 } 38603 var end = i + size; 38604 if (end > bytes.length) { 38605 // this box is bigger than the number of bytes we have 38606 // and complete is set, we cannot find any more boxes. 38607 if (complete) { 38608 break; 38609 } 38610 end = bytes.length; 38611 } 38612 var data = bytes.subarray(i + 8, end); 38613 if (bytesMatch(type, paths[0])) { 38614 if (paths.length === 1) { 38615 // this is the end of the path and we've found the box we were 38616 // looking for 38617 results.push(data); 38618 } else { 38619 // recursively search for the next box along the path 38620 results.push.apply(results, findBox(data, paths.slice(1), complete)); 38621 } 38622 } 38623 i = end; 38624 } // we've finished searching all of bytes 38625 38626 return results; 38627 }; 38628 38629 // https://matroska-org.github.io/libebml/specs.html 38630 // https://www.matroska.org/technical/elements.html 38631 // https://www.webmproject.org/docs/container/ 38632 38633 var EBML_TAGS = { 38634 EBML: toUint8([0x1A, 0x45, 0xDF, 0xA3]), 38635 DocType: toUint8([0x42, 0x82]), 38636 Segment: toUint8([0x18, 0x53, 0x80, 0x67]), 38637 SegmentInfo: toUint8([0x15, 0x49, 0xA9, 0x66]), 38638 Tracks: toUint8([0x16, 0x54, 0xAE, 0x6B]), 38639 Track: toUint8([0xAE]), 38640 TrackNumber: toUint8([0xd7]), 38641 DefaultDuration: toUint8([0x23, 0xe3, 0x83]), 38642 TrackEntry: toUint8([0xAE]), 38643 TrackType: toUint8([0x83]), 38644 FlagDefault: toUint8([0x88]), 38645 CodecID: toUint8([0x86]), 38646 CodecPrivate: toUint8([0x63, 0xA2]), 38647 VideoTrack: toUint8([0xe0]), 38648 AudioTrack: toUint8([0xe1]), 38649 // Not used yet, but will be used for live webm/mkv 38650 // see https://www.matroska.org/technical/basics.html#block-structure 38651 // see https://www.matroska.org/technical/basics.html#simpleblock-structure 38652 Cluster: toUint8([0x1F, 0x43, 0xB6, 0x75]), 38653 Timestamp: toUint8([0xE7]), 38654 TimestampScale: toUint8([0x2A, 0xD7, 0xB1]), 38655 BlockGroup: toUint8([0xA0]), 38656 BlockDuration: toUint8([0x9B]), 38657 Block: toUint8([0xA1]), 38658 SimpleBlock: toUint8([0xA3]) 38659 }; 38660 /** 38661 * This is a simple table to determine the length 38662 * of things in ebml. The length is one based (starts at 1, 38663 * rather than zero) and for every zero bit before a one bit 38664 * we add one to length. We also need this table because in some 38665 * case we have to xor all the length bits from another value. 38666 */ 38667 38668 var LENGTH_TABLE = [128, 64, 32, 16, 8, 4, 2, 1]; 38669 var getLength = function getLength(byte) { 38670 var len = 1; 38671 for (var i = 0; i < LENGTH_TABLE.length; i++) { 38672 if (byte & LENGTH_TABLE[i]) { 38673 break; 38674 } 38675 len++; 38676 } 38677 return len; 38678 }; // length in ebml is stored in the first 4 to 8 bits 38679 // of the first byte. 4 for the id length and 8 for the 38680 // data size length. Length is measured by converting the number to binary 38681 // then 1 + the number of zeros before a 1 is encountered starting 38682 // from the left. 38683 38684 var getvint = function getvint(bytes, offset, removeLength, signed) { 38685 if (removeLength === void 0) { 38686 removeLength = true; 38687 } 38688 if (signed === void 0) { 38689 signed = false; 38690 } 38691 var length = getLength(bytes[offset]); 38692 var valueBytes = bytes.subarray(offset, offset + length); // NOTE that we do **not** subarray here because we need to copy these bytes 38693 // as they will be modified below to remove the dataSizeLen bits and we do not 38694 // want to modify the original data. normally we could just call slice on 38695 // uint8array but ie 11 does not support that... 38696 38697 if (removeLength) { 38698 valueBytes = Array.prototype.slice.call(bytes, offset, offset + length); 38699 valueBytes[0] ^= LENGTH_TABLE[length - 1]; 38700 } 38701 return { 38702 length: length, 38703 value: bytesToNumber(valueBytes, { 38704 signed: signed 38705 }), 38706 bytes: valueBytes 38707 }; 38708 }; 38709 var normalizePath = function normalizePath(path) { 38710 if (typeof path === 'string') { 38711 return path.match(/.{1,2}/g).map(function (p) { 38712 return normalizePath(p); 38713 }); 38714 } 38715 if (typeof path === 'number') { 38716 return numberToBytes(path); 38717 } 38718 return path; 38719 }; 38720 var normalizePaths = function normalizePaths(paths) { 38721 if (!Array.isArray(paths)) { 38722 return [normalizePath(paths)]; 38723 } 38724 return paths.map(function (p) { 38725 return normalizePath(p); 38726 }); 38727 }; 38728 var getInfinityDataSize = function getInfinityDataSize(id, bytes, offset) { 38729 if (offset >= bytes.length) { 38730 return bytes.length; 38731 } 38732 var innerid = getvint(bytes, offset, false); 38733 if (bytesMatch(id.bytes, innerid.bytes)) { 38734 return offset; 38735 } 38736 var dataHeader = getvint(bytes, offset + innerid.length); 38737 return getInfinityDataSize(id, bytes, offset + dataHeader.length + dataHeader.value + innerid.length); 38738 }; 38739 /** 38740 * Notes on the EBLM format. 38741 * 38742 * EBLM uses "vints" tags. Every vint tag contains 38743 * two parts 38744 * 38745 * 1. The length from the first byte. You get this by 38746 * converting the byte to binary and counting the zeros 38747 * before a 1. Then you add 1 to that. Examples 38748 * 00011111 = length 4 because there are 3 zeros before a 1. 38749 * 00100000 = length 3 because there are 2 zeros before a 1. 38750 * 00000011 = length 7 because there are 6 zeros before a 1. 38751 * 38752 * 2. The bits used for length are removed from the first byte 38753 * Then all the bytes are merged into a value. NOTE: this 38754 * is not the case for id ebml tags as there id includes 38755 * length bits. 38756 * 38757 */ 38758 38759 var findEbml = function findEbml(bytes, paths) { 38760 paths = normalizePaths(paths); 38761 bytes = toUint8(bytes); 38762 var results = []; 38763 if (!paths.length) { 38764 return results; 38765 } 38766 var i = 0; 38767 while (i < bytes.length) { 38768 var id = getvint(bytes, i, false); 38769 var dataHeader = getvint(bytes, i + id.length); 38770 var dataStart = i + id.length + dataHeader.length; // dataSize is unknown or this is a live stream 38771 38772 if (dataHeader.value === 0x7f) { 38773 dataHeader.value = getInfinityDataSize(id, bytes, dataStart); 38774 if (dataHeader.value !== bytes.length) { 38775 dataHeader.value -= dataStart; 38776 } 38777 } 38778 var dataEnd = dataStart + dataHeader.value > bytes.length ? bytes.length : dataStart + dataHeader.value; 38779 var data = bytes.subarray(dataStart, dataEnd); 38780 if (bytesMatch(paths[0], id.bytes)) { 38781 if (paths.length === 1) { 38782 // this is the end of the paths and we've found the tag we were 38783 // looking for 38784 results.push(data); 38785 } else { 38786 // recursively search for the next tag inside of the data 38787 // of this one 38788 results = results.concat(findEbml(data, paths.slice(1))); 38789 } 38790 } 38791 var totalLength = id.length + dataHeader.length + data.length; // move past this tag entirely, we are not looking for it 38792 38793 i += totalLength; 38794 } 38795 return results; 38796 }; // see https://www.matroska.org/technical/basics.html#block-structure 38797 38798 var NAL_TYPE_ONE = toUint8([0x00, 0x00, 0x00, 0x01]); 38799 var NAL_TYPE_TWO = toUint8([0x00, 0x00, 0x01]); 38800 var EMULATION_PREVENTION = toUint8([0x00, 0x00, 0x03]); 38801 /** 38802 * Expunge any "Emulation Prevention" bytes from a "Raw Byte 38803 * Sequence Payload" 38804 * 38805 * @param data {Uint8Array} the bytes of a RBSP from a NAL 38806 * unit 38807 * @return {Uint8Array} the RBSP without any Emulation 38808 * Prevention Bytes 38809 */ 38810 38811 var discardEmulationPreventionBytes = function discardEmulationPreventionBytes(bytes) { 38812 var positions = []; 38813 var i = 1; // Find all `Emulation Prevention Bytes` 38814 38815 while (i < bytes.length - 2) { 38816 if (bytesMatch(bytes.subarray(i, i + 3), EMULATION_PREVENTION)) { 38817 positions.push(i + 2); 38818 i++; 38819 } 38820 i++; 38821 } // If no Emulation Prevention Bytes were found just return the original 38822 // array 38823 38824 if (positions.length === 0) { 38825 return bytes; 38826 } // Create a new array to hold the NAL unit data 38827 38828 var newLength = bytes.length - positions.length; 38829 var newData = new Uint8Array(newLength); 38830 var sourceIndex = 0; 38831 for (i = 0; i < newLength; sourceIndex++, i++) { 38832 if (sourceIndex === positions[0]) { 38833 // Skip this byte 38834 sourceIndex++; // Remove this position index 38835 38836 positions.shift(); 38837 } 38838 newData[i] = bytes[sourceIndex]; 38839 } 38840 return newData; 38841 }; 38842 var findNal = function findNal(bytes, dataType, types, nalLimit) { 38843 if (nalLimit === void 0) { 38844 nalLimit = Infinity; 38845 } 38846 bytes = toUint8(bytes); 38847 types = [].concat(types); 38848 var i = 0; 38849 var nalStart; 38850 var nalsFound = 0; // keep searching until: 38851 // we reach the end of bytes 38852 // we reach the maximum number of nals they want to seach 38853 // NOTE: that we disregard nalLimit when we have found the start 38854 // of the nal we want so that we can find the end of the nal we want. 38855 38856 while (i < bytes.length && (nalsFound < nalLimit || nalStart)) { 38857 var nalOffset = void 0; 38858 if (bytesMatch(bytes.subarray(i), NAL_TYPE_ONE)) { 38859 nalOffset = 4; 38860 } else if (bytesMatch(bytes.subarray(i), NAL_TYPE_TWO)) { 38861 nalOffset = 3; 38862 } // we are unsynced, 38863 // find the next nal unit 38864 38865 if (!nalOffset) { 38866 i++; 38867 continue; 38868 } 38869 nalsFound++; 38870 if (nalStart) { 38871 return discardEmulationPreventionBytes(bytes.subarray(nalStart, i)); 38872 } 38873 var nalType = void 0; 38874 if (dataType === 'h264') { 38875 nalType = bytes[i + nalOffset] & 0x1f; 38876 } else if (dataType === 'h265') { 38877 nalType = bytes[i + nalOffset] >> 1 & 0x3f; 38878 } 38879 if (types.indexOf(nalType) !== -1) { 38880 nalStart = i + nalOffset; 38881 } // nal header is 1 length for h264, and 2 for h265 38882 38883 i += nalOffset + (dataType === 'h264' ? 1 : 2); 38884 } 38885 return bytes.subarray(0, 0); 38886 }; 38887 var findH264Nal = function findH264Nal(bytes, type, nalLimit) { 38888 return findNal(bytes, 'h264', type, nalLimit); 38889 }; 38890 var findH265Nal = function findH265Nal(bytes, type, nalLimit) { 38891 return findNal(bytes, 'h265', type, nalLimit); 38892 }; 38893 38894 var CONSTANTS = { 38895 // "webm" string literal in hex 38896 'webm': toUint8([0x77, 0x65, 0x62, 0x6d]), 38897 // "matroska" string literal in hex 38898 'matroska': toUint8([0x6d, 0x61, 0x74, 0x72, 0x6f, 0x73, 0x6b, 0x61]), 38899 // "fLaC" string literal in hex 38900 'flac': toUint8([0x66, 0x4c, 0x61, 0x43]), 38901 // "OggS" string literal in hex 38902 'ogg': toUint8([0x4f, 0x67, 0x67, 0x53]), 38903 // ac-3 sync byte, also works for ec-3 as that is simply a codec 38904 // of ac-3 38905 'ac3': toUint8([0x0b, 0x77]), 38906 // "RIFF" string literal in hex used for wav and avi 38907 'riff': toUint8([0x52, 0x49, 0x46, 0x46]), 38908 // "AVI" string literal in hex 38909 'avi': toUint8([0x41, 0x56, 0x49]), 38910 // "WAVE" string literal in hex 38911 'wav': toUint8([0x57, 0x41, 0x56, 0x45]), 38912 // "ftyp3g" string literal in hex 38913 '3gp': toUint8([0x66, 0x74, 0x79, 0x70, 0x33, 0x67]), 38914 // "ftyp" string literal in hex 38915 'mp4': toUint8([0x66, 0x74, 0x79, 0x70]), 38916 // "styp" string literal in hex 38917 'fmp4': toUint8([0x73, 0x74, 0x79, 0x70]), 38918 // "ftypqt" string literal in hex 38919 'mov': toUint8([0x66, 0x74, 0x79, 0x70, 0x71, 0x74]), 38920 // moov string literal in hex 38921 'moov': toUint8([0x6D, 0x6F, 0x6F, 0x76]), 38922 // moof string literal in hex 38923 'moof': toUint8([0x6D, 0x6F, 0x6F, 0x66])
38923 38924 }; 38925 var _isLikely = { 38926 aac: function aac(bytes) { 38927 var offset = getId3Offset(bytes); 38928 return bytesMatch(bytes, [0xFF, 0x10], { 38929 offset: offset, 38930 mask: [0xFF, 0x16] 38931 }); 38932 }, 38933 mp3: function mp3(bytes) { 38934 var offset = getId3Offset(bytes); 38935 return bytesMatch(bytes, [0xFF, 0x02], { 38936 offset: offset, 38937 mask: [0xFF, 0x06] 38938 }); 38939 }, 38940 webm: function webm(bytes) {
vendor: 5,962 bytes, lines 38941-39130
38941 var docType = findEbml(bytes, [EBML_TAGS.EBML, EBML_TAGS.DocType])[0]; // check if DocType EBML tag is webm 38942 38943 return bytesMatch(docType, CONSTANTS.webm); 38944 }, 38945 mkv: function mkv(bytes) { 38946 var docType = findEbml(bytes, [EBML_TAGS.EBML, EBML_TAGS.DocType])[0]; // check if DocType EBML tag is matroska 38947 38948 return bytesMatch(docType, CONSTANTS.matroska); 38949 }, 38950 mp4: function mp4(bytes) { 38951 // if this file is another base media file format, it is not mp4 38952 if (_isLikely['3gp'](bytes) || _isLikely.mov(bytes)) { 38953 return false; 38954 } // if this file starts with a ftyp or styp box its mp4 38955 38956 if (bytesMatch(bytes, CONSTANTS.mp4, { 38957 offset: 4 38958 }) || bytesMatch(bytes, CONSTANTS.fmp4, { 38959 offset: 4 38960 })) { 38961 return true; 38962 } // if this file starts with a moof/moov box its mp4 38963 38964 if (bytesMatch(bytes, CONSTANTS.moof, { 38965 offset: 4 38966 }) || bytesMatch(bytes, CONSTANTS.moov, { 38967 offset: 4 38968 })) { 38969 return true; 38970 } 38971 }, 38972 mov: function mov(bytes) { 38973 return bytesMatch(bytes, CONSTANTS.mov, { 38974 offset: 4 38975 }); 38976 }, 38977 '3gp': function gp(bytes) { 38978 return bytesMatch(bytes, CONSTANTS['3gp'], { 38979 offset: 4 38980 }); 38981 }, 38982 ac3: function ac3(bytes) { 38983 var offset = getId3Offset(bytes); 38984 return bytesMatch(bytes, CONSTANTS.ac3, { 38985 offset: offset 38986 }); 38987 }, 38988 ts: function ts(bytes) { 38989 if (bytes.length < 189 && bytes.length >= 1) { 38990 return bytes[0] === 0x47; 38991 } 38992 var i = 0; // check the first 376 bytes for two matching sync bytes 38993 38994 while (i + 188 < bytes.length && i < 188) { 38995 if (bytes[i] === 0x47 && bytes[i + 188] === 0x47) { 38996 return true; 38997 } 38998 i += 1; 38999 } 39000 return false; 39001 }, 39002 flac: function flac(bytes) { 39003 var offset = getId3Offset(bytes); 39004 return bytesMatch(bytes, CONSTANTS.flac, { 39005 offset: offset 39006 }); 39007 }, 39008 ogg: function ogg(bytes) { 39009 return bytesMatch(bytes, CONSTANTS.ogg); 39010 }, 39011 avi: function avi(bytes) { 39012 return bytesMatch(bytes, CONSTANTS.riff) && bytesMatch(bytes, CONSTANTS.avi, { 39013 offset: 8 39014 }); 39015 }, 39016 wav: function wav(bytes) { 39017 return bytesMatch(bytes, CONSTANTS.riff) && bytesMatch(bytes, CONSTANTS.wav, { 39018 offset: 8 39019 }); 39020 }, 39021 'h264': function h264(bytes) { 39022 // find seq_parameter_set_rbsp 39023 return findH264Nal(bytes, 7, 3).length; 39024 }, 39025 'h265': function h265(bytes) { 39026 // find video_parameter_set_rbsp or seq_parameter_set_rbsp 39027 return findH265Nal(bytes, [32, 33], 3).length; 39028 } 39029 }; // get all the isLikely functions 39030 // but make sure 'ts' is above h264 and h265 39031 // but below everything else as it is the least specific 39032 39033 var isLikelyTypes = Object.keys(_isLikely) // remove ts, h264, h265 39034 .filter(function (t) { 39035 return t !== 'ts' && t !== 'h264' && t !== 'h265'; 39036 }) // add it back to the bottom 39037 .concat(['ts', 'h264', 'h265']); // make sure we are dealing with uint8 data. 39038 39039 isLikelyTypes.forEach(function (type) { 39040 var isLikelyFn = _isLikely[type]; 39041 _isLikely[type] = function (bytes) { 39042 return isLikelyFn(toUint8(bytes)); 39043 }; 39044 }); // export after wrapping 39045 39046 var isLikely = _isLikely; // A useful list of file signatures can be found here 39047 // https://en.wikipedia.org/wiki/List_of_file_signatures 39048 39049 var detectContainerForBytes = function detectContainerForBytes(bytes) { 39050 bytes = toUint8(bytes); 39051 for (var i = 0; i < isLikelyTypes.length; i++) { 39052 var type = isLikelyTypes[i]; 39053 if (isLikely[type](bytes)) { 39054 return type; 39055 } 39056 } 39057 return ''; 39058 }; // fmp4 is not a container 39059 39060 var isLikelyFmp4MediaSegment = function isLikelyFmp4MediaSegment(bytes) { 39061 return findBox(bytes, ['moof']).length > 0; 39062 }; 39063 39064 /** 39065 * mux.js 39066 * 39067 * Copyright (c) Brightcove 39068 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 39069 */ 39070 var ONE_SECOND_IN_TS = 90000, 39071 // 90kHz clock 39072 secondsToVideoTs, 39073 secondsToAudioTs, 39074 videoTsToSeconds, 39075 audioTsToSeconds, 39076 audioTsToVideoTs, 39077 videoTsToAudioTs, 39078 metadataTsToSeconds; 39079 secondsToVideoTs = function (seconds) { 39080 return seconds * ONE_SECOND_IN_TS; 39081 }; 39082 secondsToAudioTs = function (seconds, sampleRate) { 39083 return seconds * sampleRate; 39084 }; 39085 videoTsToSeconds = function (timestamp) { 39086 return timestamp / ONE_SECOND_IN_TS; 39087 }; 39088 audioTsToSeconds = function (timestamp, sampleRate) { 39089 return timestamp / sampleRate; 39090 }; 39091 audioTsToVideoTs = function (timestamp, sampleRate) { 39092 return secondsToVideoTs(audioTsToSeconds(timestamp, sampleRate)); 39093 }; 39094 videoTsToAudioTs = function (timestamp, sampleRate) { 39095 return secondsToAudioTs(videoTsToSeconds(timestamp), sampleRate); 39096 }; 39097 39098 /** 39099 * Adjust ID3 tag or caption timing information by the timeline pts values 39100 * (if keepOriginalTimestamps is false) and convert to seconds 39101 */ 39102 metadataTsToSeconds = function (timestamp, timelineStartPts, keepOriginalTimestamps) { 39103 return videoTsToSeconds(keepOriginalTimestamps ? timestamp : timestamp - timelineStartPts); 39104 }; 39105 var clock = { 39106 ONE_SECOND_IN_TS: ONE_SECOND_IN_TS, 39107 secondsToVideoTs: secondsToVideoTs, 39108 secondsToAudioTs: secondsToAudioTs, 39109 videoTsToSeconds: videoTsToSeconds, 39110 audioTsToSeconds: audioTsToSeconds, 39111 audioTsToVideoTs: audioTsToVideoTs, 39112 videoTsToAudioTs: videoTsToAudioTs, 39113 metadataTsToSeconds: metadataTsToSeconds 39114 }; 39115 var clock_1 = clock.ONE_SECOND_IN_TS; 39116 39117 /*! @name @videojs/http-streaming @version 3.9.1 @license Apache-2.0 */ 39118 39119 /** 39120 * @file resolve-url.js - Handling how URLs are resolved and manipulated 39121 */ 39122 const resolveUrl = resolveUrl$1; 39123 /** 39124 * If the xhr request was redirected, return the responseURL, otherwise, 39125 * return the original url. 39126 * 39127 * @api private 39128 * 39129 * @param {string} url - an url being requested 39130 * @param {XMLHttpRequest}
39130 req - xhr request result 39131 * 39132 * @return {string} 39133 */ 39134 39135 const resolveManifestRedirect = (url, req) => { 39136 // To understand how the responseURL below is set and generated: 39137 // - https://fetch.spec.whatwg.org/#concept-response-url 39138 // - https://fetch.spec.whatwg.org/#atomic-http-redirect-handling 39139 if (req && req.responseURL && url !== req.responseURL) { 39140 return req.responseURL; 39141 } 39142 return url; 39143 }; 39144 const logger = source => { 39145 if (videojs.log.debug) { 39146 return videojs.log.debug.bind(videojs, 'VHS:', `${source} >`); 39147 } 39148 return function () {}; 39149 }; 39150 39151 /** 39152 * Provides a compatibility layer between Video.js 7 and 8 API changes for VHS. 39153 */ 39154 /** 39155 * Delegates to videojs.obj.merge (Video.js 8) or 39156 * videojs.mergeOptions (Video.js 7). 39157 */ 39158 39159 function merge(...args) { 39160 const context = videojs.obj || videojs; 39161 const fn = context.merge || context.mergeOptions; 39162 return fn.apply(context, args); 39163 } 39164 /** 39165 * Delegates to videojs.time.createTimeRanges (Video.js 8) or 39166 * videojs.createTimeRanges (Video.js 7). 39167 */ 39168 39169 function createTimeRanges(...args) { 39170 const context = videojs.time || videojs; 39171 const fn = context.createTimeRanges || context.createTimeRanges; 39172 return fn.apply(context, args)
vendor: 14,013 bytes, lines 39172-39654
39172; 39173 } 39174 39175 /** 39176 * ranges 39177 * 39178 * Utilities for working with TimeRanges. 39179 * 39180 */ 39181 39182 const TIME_FUDGE_FACTOR = 1 / 30; // Comparisons between time values such as current time and the end of the buffered range 39183 // can be misleading because of precision differences or when the current media has poorly 39184 // aligned audio and video, which can cause values to be slightly off from what you would 39185 // expect. This value is what we consider to be safe to use in such comparisons to account 39186 // for these scenarios. 39187 39188 const SAFE_TIME_DELTA = TIME_FUDGE_FACTOR * 3; 39189 const filterRanges = function (timeRanges, predicate) { 39190 const results = []; 39191 let i; 39192 if (timeRanges && timeRanges.length) { 39193 // Search for ranges that match the predicate 39194 for (i = 0; i < timeRanges.length; i++) { 39195 if (predicate(timeRanges.start(i), timeRanges.end(i))) { 39196 results.push([timeRanges.start(i), timeRanges.end(i)]); 39197 } 39198 } 39199 } 39200 return createTimeRanges(results); 39201 }; 39202 /** 39203 * Attempts to find the buffered TimeRange that contains the specified 39204 * time. 39205 * 39206 * @param {TimeRanges} buffered - the TimeRanges object to query 39207 * @param {number} time - the time to filter on. 39208 * @return {TimeRanges} a new TimeRanges object 39209 */ 39210 39211 const findRange = function (buffered, time) { 39212 return filterRanges(buffered, function (start, end) { 39213 return start - SAFE_TIME_DELTA <= time && end + SAFE_TIME_DELTA >= time; 39214 }); 39215 }; 39216 /** 39217 * Returns the TimeRanges that begin later than the specified time. 39218 * 39219 * @param {TimeRanges} timeRanges - the TimeRanges object to query 39220 * @param {number} time - the time to filter on. 39221 * @return {TimeRanges} a new TimeRanges object. 39222 */ 39223 39224 const findNextRange = function (timeRanges, time) { 39225 return filterRanges(timeRanges, function (start) { 39226 return start - TIME_FUDGE_FACTOR >= time; 39227 }); 39228 }; 39229 /** 39230 * Returns gaps within a list of TimeRanges 39231 * 39232 * @param {TimeRanges} buffered - the TimeRanges object 39233 * @return {TimeRanges} a TimeRanges object of gaps 39234 */ 39235 39236 const findGaps = function (buffered) { 39237 if (buffered.length < 2) { 39238 return createTimeRanges(); 39239 } 39240 const ranges = []; 39241 for (let i = 1; i < buffered.length; i++) { 39242 const start = buffered.end(i - 1); 39243 const end = buffered.start(i); 39244 ranges.push([start, end]); 39245 } 39246 return createTimeRanges(ranges); 39247 }; 39248 /** 39249 * Calculate the intersection of two TimeRanges 39250 * 39251 * @param {TimeRanges} bufferA 39252 * @param {TimeRanges} bufferB 39253 * @return {TimeRanges} The interesection of `bufferA` with `bufferB` 39254 */ 39255 39256 const bufferIntersection = function (bufferA, bufferB) { 39257 let start = null; 39258 let end = null; 39259 let arity = 0; 39260 const extents = []; 39261 const ranges = []; 39262 if (!bufferA || !bufferA.length || !bufferB || !bufferB.length) { 39263 return createTimeRanges(); 39264 } // Handle the case where we have both buffers and create an 39265 // intersection of the two 39266 39267 let count = bufferA.length; // A) Gather up all start and end times 39268 39269 while (count--) { 39270 extents.push({ 39271 time: bufferA.start(count), 39272 type: 'start' 39273 }); 39274 extents.push({ 39275 time: bufferA.end(count), 39276 type: 'end' 39277 }); 39278 } 39279 count = bufferB.length; 39280 while (count--) { 39281 extents.push({ 39282 time: bufferB.start(count), 39283 type: 'start' 39284 }); 39285 extents.push({ 39286 time: bufferB.end(count), 39287 type: 'end' 39288 }); 39289 } // B) Sort them by time 39290 39291 extents.sort(function (a, b) { 39292 return a.time - b.time; 39293 }); // C) Go along one by one incrementing arity for start and decrementing 39294 // arity for ends 39295 39296 for (count = 0; count < extents.length; count++) { 39297 if (extents[count].type === 'start') { 39298 arity++; // D) If arity is ever incremented to 2 we are entering an 39299 // overlapping range 39300 39301 if (arity === 2) { 39302 start = extents[count].time; 39303 } 39304 } else if (extents[count].type === 'end') { 39305 arity--; // E) If arity is ever decremented to 1 we leaving an 39306 // overlapping range 39307 39308 if (arity === 1) { 39309 end = extents[count].time; 39310 } 39311 } // F) Record overlapping ranges 39312 39313 if (start !== null && end !== null) { 39314 ranges.push([start, end]); 39315 start = null; 39316 end = null; 39317 } 39318 } 39319 return createTimeRanges(ranges); 39320 }; 39321 /** 39322 * Gets a human readable string for a TimeRange 39323 * 39324 * @param {TimeRange} range 39325 * @return {string} a human readable string 39326 */ 39327 39328 const printableRange = range => { 39329 const strArr = []; 39330 if (!range || !range.length) { 39331 return ''; 39332 } 39333 for (let i = 0; i < range.length; i++) { 39334 strArr.push(range.start(i) + ' => ' + range.end(i)); 39335 } 39336 return strArr.join(', '); 39337 }; 39338 /** 39339 * Calculates the amount of time left in seconds until the player hits the end of the 39340 * buffer and causes a rebuffer 39341 * 39342 * @param {TimeRange} buffered 39343 * The state of the buffer 39344 * @param {Numnber} currentTime 39345 * The current time of the player 39346 * @param {number} playbackRate 39347 * The current playback rate of the player. Defaults to 1. 39348 * @return {number} 39349 * Time until the player has to start rebuffering in seconds. 39350 * @function timeUntilRebuffer 39351 */ 39352 39353 const timeUntilRebuffer = function (buffered, currentTime, playbackRate = 1) { 39354 const bufferedEnd = buffered.length ? buffered.end(buffered.length - 1) : 0; 39355 return (bufferedEnd - currentTime) / playbackRate; 39356 }; 39357 /** 39358 * Converts a TimeRanges object into an array representation 39359 * 39360 * @param {TimeRanges} timeRanges 39361 * @return {Array} 39362 */ 39363 39364 const timeRangesToArray = timeRanges => { 39365 const timeRangesList = []; 39366 for (let i = 0; i < timeRanges.length; i++) { 39367 timeRangesList.push({ 39368 start: timeRanges.start(i), 39369 end: timeRanges.end(i) 39370 }); 39371 } 39372 return timeRangesList; 39373 }; 39374 /** 39375 * Determines if two time range objects are different. 39376 * 39377 * @param {TimeRange} a 39378 * the first time range object to check 39379 * 39380 * @param {TimeRange} b 39381 * the second time range object to check 39382 * 39383 * @return {Boolean} 39384 * Whether the time range objects differ 39385 */ 39386 39387 const isRangeDifferent = function (a, b) { 39388 // same object 39389 if (a === b) { 39390 return false; 39391 } // one or the other is undefined 39392 39393 if (!a && b || !b && a) { 39394 return true; 39395 } // length is different 39396 39397 if (a.length !== b.length) { 39398 return true; 39399 } // see if any start/end pair is different 39400 39401 for (let i = 0; i < a.length; i++) { 39402 if (a.start(i) !== b.start(i) || a.end(i) !== b.end(i)) { 39403 return true; 39404 } 39405 } // if the length and every pair is the same 39406 // this is the same time range 39407 39408 return false; 39409 }; 39410 const lastBufferedEnd = function (a) { 39411 if (!a || !a.length || !a.end) { 39412 return; 39413 } 39414 return a.end(a.length - 1); 39415 }; 39416 /** 39417 * A utility function to add up the amount of time in a timeRange 39418 * after a specified startTime. 39419 * ie:[[0, 10], [20, 40], [50, 60]] with a startTime 0 39420 * would return 40 as there are 40s seconds after 0 in the timeRange 39421 * 39422 * @param {TimeRange} range 39423 * The range to check against 39424 * @param {number} startTime 39425 * The time in the time range that you should start counting from 39426 * 39427 * @return {number} 39428 * The number of seconds in the buffer passed the specified time. 39429 */ 39430 39431 const timeAheadOf = function (range, startTime) { 39432 let time = 0; 39433 if (!range || !range.length) { 39434 return time; 39435 } 39436 for (let i = 0; i < range.length; i++) { 39437 const start = range.start(i); 39438 const end = range.end(i); // startTime is after this range entirely 39439 39440 if (startTime > end) { 39441 continue; 39442 } // startTime is within this range 39443 39444 if (startTime > start && startTime <= end) { 39445 time += end - startTime; 39446 continue; 39447 } // startTime is before this range. 39448 39449 time += end - start; 39450 } 39451 return time; 39452 }; 39453 39454 /** 39455 * @file playlist.js 39456 * 39457 * Playlist related utilities. 39458 */ 39459 /** 39460 * Get the duration of a segment, with special cases for 39461 * llhls segments that do not have a duration yet. 39462 * 39463 * @param {Object} playlist 39464 * the playlist that the segment belongs to. 39465 * @param {Object} segment 39466 * the segment to get a duration for. 39467 * 39468 * @return {number} 39469 * the segment duration 39470 */ 39471 39472 const segmentDurationWithParts = (playlist, segment) => { 39473 // if this isn't a preload segment 39474 // then we will have a segment duration that is accurate. 39475 if (!segment.preload) { 39476 return segment.duration; 39477 } // otherwise we have to add up parts and preload hints 39478 // to get an up to date duration. 39479 39480 let result = 0; 39481 (segment.parts || []).forEach(function (p) { 39482 result += p.duration; 39483 }); // for preload hints we have to use partTargetDuration 39484 // as they won't even have a duration yet. 39485 39486 (segment.preloadHints || []).forEach(function (p) { 39487 if (p.type === 'PART') { 39488 result += playlist.partTargetDuration; 39489 } 39490 }); 39491 return result; 39492 }; 39493 /** 39494 * A function to get a combined list of parts and segments with durations 39495 * and indexes. 39496 * 39497 * @param {Playlist} playlist the playlist to get the list for. 39498 * 39499 * @return {Array} The part/segment list. 39500 */ 39501 39502 const getPartsAndSegments = playlist => (playlist.segments || []).reduce((acc, segment, si) => { 39503 if (segment.parts) { 39504 segment.parts.forEach(function (part, pi) { 39505 acc.push({ 39506 duration: part.duration, 39507 segmentIndex: si, 39508 partIndex: pi, 39509 part, 39510 segment 39511 }); 39512 }); 39513 } else { 39514 acc.push({ 39515 duration: segment.duration, 39516 segmentIndex: si, 39517 partIndex: null, 39518 segment, 39519 part: null 39520 }); 39521 } 39522 return acc; 39523 }, []); 39524 const getLastParts = media => { 39525 const lastSegment = media.segments && media.segments.length && media.segments[media.segments.length - 1]; 39526 return lastSegment && lastSegment.parts || []; 39527 }; 39528 const getKnownPartCount = ({ 39529 preloadSegment 39530 }) => { 39531 if (!preloadSegment) { 39532 return; 39533 } 39534 const { 39535 parts, 39536 preloadHints 39537 } = preloadSegment; 39538 let partCount = (preloadHints || []).reduce((count, hint) => count + (hint.type === 'PART' ? 1 : 0), 0); 39539 partCount += parts && parts.length ? parts.length : 0; 39540 return partCount; 39541 }; 39542 /** 39543 * Get the number of seconds to delay from the end of a 39544 * live playlist. 39545 * 39546 * @param {Playlist} main the main playlist 39547 * @param {Playlist} media the media playlist 39548 * @return {number} the hold back in seconds. 39549 */ 39550 39551 const liveEdgeDelay = (main, media) => { 39552 if (media.endList) { 39553 return 0; 39554 } // dash suggestedPresentationDelay trumps everything 39555 39556 if (main && main.suggestedPresentationDelay) { 39557 return main.suggestedPresentationDelay; 39558 } 39559 const hasParts = getLastParts(media).length > 0; // look for "part" delays from ll-hls first 39560 39561 if (hasParts && media.serverControl && media.serverControl.partHoldBack) { 39562 return media.serverControl.partHoldBack; 39563 } else if (hasParts && media.partTargetDuration) { 39564 return media.partTargetDuration * 3; // finally look for full segment delays 39565 } else if (media.serverControl && media.serverControl.holdBack) { 39566 return media.serverControl.holdBack; 39567 } else if (media.targetDuration) { 39568 return media.targetDuration * 3; 39569 } 39570 return 0; 39571 }; 39572 /** 39573 * walk backward until we find a duration we can use 39574 * or return a failure 39575 * 39576 * @param {Playlist} playlist the playlist to walk through 39577 * @param {Number} endSequence the mediaSequence to stop walking on 39578 */ 39579 39580 const backwardDuration = function (playlist, endSequence) { 39581 let result = 0; 39582 let i = endSequence - playlist.mediaSequence; // if a start time is available for segment immediately following 39583 // the interval, use it 39584 39585 let segment = playlist.segments[i]; // Walk backward until we find the latest segment with timeline 39586 // information that is earlier than endSequence 39587 39588 if (segment) { 39589 if (typeof segment.start !== 'undefined') { 39590 return { 39591 result: segment.start, 39592 precise: true 39593 }; 39594 } 39595 if (typeof segment.end !== 'undefined') { 39596 return { 39597 result: segment.end - segment.duration, 39598 precise: true 39599 }; 39600 } 39601 } 39602 while (i--) { 39603 segment = playlist.segments[i]; 39604 if (typeof segment.end !== 'undefined') { 39605 return { 39606 result: result + segment.end, 39607 precise: true 39608 }; 39609 } 39610 result += segmentDurationWithParts(playlist, segment); 39611 if (typeof segment.start !== 'undefined') { 39612 return { 39613 result: result + segment.start, 39614 precise: true 39615 }; 39616 } 39617 } 39618 return { 39619 result, 39620 precise: false 39621 }; 39622 }; 39623 /** 39624 * walk forward until we find a duration we can use 39625 * or return a failure 39626 * 39627 * @param {Playlist} playlist the playlist to walk through 39628 * @param {number} endSequence the mediaSequence to stop walking on 39629 */ 39630 39631 const forwardDuration = function (playlist, endSequence) { 39632 let result = 0; 39633 let segment; 39634 let i = endSequence - playlist.mediaSequence; // Walk forward until we find the earliest segment with timeline 39635 // information 39636 39637 for (; i < playlist.segments.length; i++) { 39638 segment = playlist.segments[i]; 39639 if (typeof segment.start !== 'undefined') { 39640 return { 39641 result: segment.start - result, 39642 precise: true 39643 }; 39644 } 39645 result += segmentDurationWithParts(playlist, segment); 39646 if (typeof segment.end !== 'undefined') { 39647 return { 39648 result: segment.end - result, 39649 precise: true 39650 }; 39651 } 39652 } // indicate we didn't find a useful duration estimate 39653 39654 return {
vendor: 5,055 bytes, lines 39655-39782
39655 result: -1, 39656 precise: false 39657 }; 39658 }; 39659 /** 39660 * Calculate the media duration from the segments associated with a 39661 * playlist. The duration of a subinterval of the available segments 39662 * may be calculated by specifying an end index. 39663 * 39664 * @param {Object} playlist a media playlist object 39665 * @param {number=} endSequence an exclusive upper boundary 39666 * for the playlist. Defaults to playlist length. 39667 * @param {number} expired the amount of time that has dropped 39668 * off the front of the playlist in a live scenario 39669 * @return {number} the duration between the first available segment 39670 * and end index. 39671 */ 39672 39673 const intervalDuration = function (playlist, endSequence, expired) { 39674 if (typeof endSequence === 'undefined') { 39675 endSequence = playlist.mediaSequence + playlist.segments.length; 39676 } 39677 if (endSequence < playlist.mediaSequence) { 39678 return 0; 39679 } // do a backward walk to estimate the duration 39680 39681 const backward = backwardDuration(playlist, endSequence); 39682 if (backward.precise) { 39683 // if we were able to base our duration estimate on timing 39684 // information provided directly from the Media Source, return 39685 // it 39686 return backward.result; 39687 } // walk forward to see if a precise duration estimate can be made 39688 // that way 39689 39690 const forward = forwardDuration(playlist, endSequence); 39691 if (forward.precise) { 39692 // we found a segment that has been buffered and so it's 39693 // position is known precisely 39694 return forward.result; 39695 } // return the less-precise, playlist-based duration estimate 39696 39697 return backward.result + expired; 39698 }; 39699 /** 39700 * Calculates the duration of a playlist. If a start and end index 39701 * are specified, the duration will be for the subset of the media 39702 * timeline between those two indices. The total duration for live 39703 * playlists is always Infinity. 39704 * 39705 * @param {Object} playlist a media playlist object 39706 * @param {number=} endSequence an exclusive upper 39707 * boundary for the playlist. Defaults to the playlist media 39708 * sequence number plus its length. 39709 * @param {number=} expired the amount of time that has 39710 * dropped off the front of the playlist in a live scenario 39711 * @return {number} the duration between the start index and end 39712 * index. 39713 */ 39714 39715 const duration = function (playlist, endSequence, expired) { 39716 if (!playlist) { 39717 return 0; 39718 } 39719 if (typeof expired !== 'number') { 39720 expired = 0; 39721 } // if a slice of the total duration is not requested, use 39722 // playlist-level duration indicators when they're present 39723 39724 if (typeof endSequence === 'undefined') { 39725 // if present, use the duration specified in the playlist 39726 if (playlist.totalDuration) { 39727 return playlist.totalDuration; 39728 } // duration should be Infinity for live playlists 39729 39730 if (!playlist.endList) { 39731 return window.Infinity; 39732 } 39733 } // calculate the total duration based on the segment durations 39734 39735 return intervalDuration(playlist, endSequence, expired); 39736 }; 39737 /** 39738 * Calculate the time between two indexes in the current playlist 39739 * neight the start- nor the end-index need to be within the current 39740 * playlist in which case, the targetDuration of the playlist is used 39741 * to approximate the durations of the segments 39742 * 39743 * @param {Array} options.durationList list to iterate over for durations. 39744 * @param {number} options.defaultDuration duration to use for elements before or after the durationList 39745 * @param {number} options.startIndex partsAndSegments index to start 39746 * @param {number} options.endIndex partsAndSegments index to end. 39747 * @return {number} the number of seconds between startIndex and endIndex 39748 */ 39749 39750 const sumDurations = function ({ 39751 defaultDuration, 39752 durationList, 39753 startIndex, 39754 endIndex 39755 }) { 39756 let durations = 0; 39757 if (startIndex > endIndex) { 39758 [startIndex, endIndex] = [endIndex, startIndex]; 39759 } 39760 if (startIndex < 0) { 39761 for (let i = startIndex; i < Math.min(0, endIndex); i++) { 39762 durations += defaultDuration; 39763 } 39764 startIndex = 0; 39765 } 39766 for (let i = startIndex; i < endIndex; i++) { 39767 durations += durationList[i].duration; 39768 } 39769 return durations; 39770 }; 39771 /** 39772 * Calculates the playlist end time 39773 * 39774 * @param {Object} playlist a media playlist object 39775 * @param {number=} expired the amount of time that has 39776 * dropped off the front of the playlist in a live scenario 39777 * @param {boolean|false} useSafeLiveEnd a boolean value indicating whether or not the 39778 * playlist end calculation should consider the safe live end 39779 * (truncate the playlist end by three segments). This is normally 39780 * used for calculating the end of the playlist's seekable range. 39781 * This takes into account the value of liveEdgePadding. 39782 * Setting liveEdgePadding to 0 is equivalent to setting this to fal
vendor: 19,363 bytes, lines 39782-40330
39782se. 39783 * @param {number} liveEdgePadding a number indicating how far from the end of the playlist we should be in seconds. 39784 * If this is provided, it is used in the safe live end calculation. 39785 * Setting useSafeLiveEnd=false or liveEdgePadding=0 are equivalent. 39786 * Corresponds to suggestedPresentationDelay in DASH manifests. 39787 * @return {number} the end time of playlist 39788 * @function playlistEnd 39789 */ 39790 39791 const playlistEnd = function (playlist, expired, useSafeLiveEnd, liveEdgePadding) { 39792 if (!playlist || !playlist.segments) { 39793 return null; 39794 } 39795 if (playlist.endList) { 39796 return duration(playlist); 39797 } 39798 if (expired === null) { 39799 return null; 39800 } 39801 expired = expired || 0; 39802 let lastSegmentEndTime = intervalDuration(playlist, playlist.mediaSequence + playlist.segments.length, expired); 39803 if (useSafeLiveEnd) { 39804 liveEdgePadding = typeof liveEdgePadding === 'number' ? liveEdgePadding : liveEdgeDelay(null, playlist); 39805 lastSegmentEndTime -= liveEdgePadding; 39806 } // don't return a time less than zero 39807 39808 return Math.max(0, lastSegmentEndTime); 39809 }; 39810 /** 39811 * Calculates the interval of time that is currently seekable in a 39812 * playlist. The returned time ranges are relative to the earliest 39813 * moment in the specified playlist that is still available. A full 39814 * seekable implementation for live streams would need to offset 39815 * these values by the duration of content that has expired from the 39816 * stream. 39817 * 39818 * @param {Object} playlist a media playlist object 39819 * dropped off the front of the playlist in a live scenario 39820 * @param {number=} expired the amount of time that has 39821 * dropped off the front of the playlist in a live scenario 39822 * @param {number} liveEdgePadding how far from the end of the playlist we should be in seconds. 39823 * Corresponds to suggestedPresentationDelay in DASH manifests. 39824 * @return {TimeRanges} the periods of time that are valid targets 39825 * for seeking 39826 */ 39827 39828 const seekable = function (playlist, expired, liveEdgePadding) { 39829 const useSafeLiveEnd = true; 39830 const seekableStart = expired || 0; 39831 let seekableEnd = playlistEnd(playlist, expired, useSafeLiveEnd, liveEdgePadding); 39832 if (seekableEnd === null) { 39833 return createTimeRanges(); 39834 } // Clamp seekable end since it can not be less than the seekable start 39835 39836 if (seekableEnd < seekableStart) { 39837 seekableEnd = seekableStart; 39838 } 39839 return createTimeRanges(seekableStart, seekableEnd); 39840 }; 39841 /** 39842 * Determine the index and estimated starting time of the segment that 39843 * contains a specified playback position in a media playlist. 39844 * 39845 * @param {Object} options.playlist the media playlist to query 39846 * @param {number} options.currentTime The number of seconds since the earliest 39847 * possible position to determine the containing segment for 39848 * @param {number} options.startTime the time when the segment/part starts 39849 * @param {number} options.startingSegmentIndex the segment index to start looking at. 39850 * @param {number?} [options.startingPartIndex] the part index to look at within the segment. 39851 * 39852 * @return {Object} an object with partIndex, segmentIndex, and startTime. 39853 */ 39854 39855 const getMediaInfoForTime = function ({ 39856 playlist, 39857 currentTime, 39858 startingSegmentIndex, 39859 startingPartIndex, 39860 startTime, 39861 exactManifestTimings 39862 }) { 39863 let time = currentTime - startTime; 39864 const partsAndSegments = getPartsAndSegments(playlist); 39865 let startIndex = 0; 39866 for (let i = 0; i < partsAndSegments.length; i++) { 39867 const partAndSegment = partsAndSegments[i]; 39868 if (startingSegmentIndex !== partAndSegment.segmentIndex) { 39869 continue; 39870 } // skip this if part index does not match. 39871 39872 if (typeof startingPartIndex === 'number' && typeof partAndSegment.partIndex === 'number' && startingPartIndex !== partAndSegment.partIndex) { 39873 continue; 39874 } 39875 startIndex = i; 39876 break; 39877 } 39878 if (time < 0) { 39879 // Walk backward from startIndex in the playlist, adding durations 39880 // until we find a segment that contains `time` and return it 39881 if (startIndex > 0) { 39882 for (let i = startIndex - 1; i >= 0; i--) { 39883 const partAndSegment = partsAndSegments[i]; 39884 time += partAndSegment.duration; 39885 if (exactManifestTimings) { 39886 if (time < 0) { 39887 continue; 39888 } 39889 } else if (time + TIME_FUDGE_FACTOR <= 0) { 39890 continue; 39891 } 39892 return { 39893 partIndex: partAndSegment.partIndex, 39894 segmentIndex: partAndSegment.segmentIndex, 39895 startTime: startTime - sumDurations({ 39896 defaultDuration: playlist.targetDuration, 39897 durationList: partsAndSegments, 39898 startIndex, 39899 endIndex: i 39900 }) 39901 }; 39902 } 39903 } // We were unable to find a good segment within the playlist 39904 // so select the first segment 39905 39906 return { 39907 partIndex: partsAndSegments[0] && partsAndSegments[0].partIndex || null, 39908 segmentIndex: partsAndSegments[0] && partsAndSegments[0].segmentIndex || 0, 39909 startTime: currentTime 39910 }; 39911 } // When startIndex is negative, we first walk forward to first segment 39912 // adding target durations. If we "run out of time" before getting to 39913 // the first segment, return the first segment 39914 39915 if (startIndex < 0) { 39916 for (let i = startIndex; i < 0; i++) { 39917 time -= playlist.targetDuration; 39918 if (time < 0) { 39919 return { 39920 partIndex: partsAndSegments[0] && partsAndSegments[0].partIndex || null, 39921 segmentIndex: partsAndSegments[0] && partsAndSegments[0].segmentIndex || 0, 39922 startTime: currentTime 39923 }; 39924 } 39925 } 39926 startIndex = 0; 39927 } // Walk forward from startIndex in the playlist, subtracting durations 39928 // until we find a segment that contains `time` and return it 39929 39930 for (let i = startIndex; i < partsAndSegments.length; i++) { 39931 const partAndSegment = partsAndSegments[i]; 39932 time -= partAndSegment.duration; 39933 const canUseFudgeFactor = partAndSegment.duration > TIME_FUDGE_FACTOR; 39934 const isExactlyAtTheEnd = time === 0; 39935 const isExtremelyCloseToTheEnd = canUseFudgeFactor && time + TIME_FUDGE_FACTOR >= 0; 39936 if (isExactlyAtTheEnd || isExtremelyCloseToTheEnd) { 39937 // 1) We are exactly at the end of the current segment. 39938 // 2) We are extremely close to the end of the current segment (The difference is less than 1 / 30). 39939 // We may encounter this situation when 39940 // we don't have exact match between segment duration info in the manifest and the actual duration of the segment 39941 // For example: 39942 // We appended 3 segments 10 seconds each, meaning we should have 30 sec buffered, 39943 // but we the actual buffered is 29.99999 39944 // 39945 // In both cases: 39946 // if we passed current time -> it means that we already played current segment 39947 // if we passed buffered.end -> it means that this segment is already loaded and buffered 39948 // we should select the next segment if we have one: 39949 if (i !== partsAndSegments.length - 1) { 39950 continue; 39951 } 39952 } 39953 if (exactManifestTimings) { 39954 if (time > 0) { 39955 continue; 39956 } 39957 } else if (time - TIME_FUDGE_FACTOR >= 0) { 39958 continue; 39959 } 39960 return { 39961 partIndex: partAndSegment.partIndex, 39962 segmentIndex: partAndSegment.segmentIndex, 39963 startTime: startTime + sumDurations({ 39964 defaultDuration: playlist.targetDuration, 39965 durationList: partsAndSegments, 39966 startIndex, 39967 endIndex: i 39968 }) 39969 }; 39970 } // We are out of possible candidates so load the last one... 39971 39972 return { 39973 segmentIndex: partsAndSegments[partsAndSegments.length - 1].segmentIndex, 39974 partIndex: partsAndSegments[partsAndSegments.length - 1].partIndex, 39975 startTime: currentTime 39976 }; 39977 }; 39978 /** 39979 * Check whether the playlist is excluded or not. 39980 * 39981 * @param {Object} playlist the media playlist object 39982 * @return {boolean} whether the playlist is excluded or not 39983 * @function isExcluded 39984 */ 39985 39986 const isExcluded = function (playlist) { 39987 return playlist.excludeUntil && playlist.excludeUntil > Date.now(); 39988 }; 39989 /** 39990 * Check whether the playlist is compatible with current playback configuration or has 39991 * been excluded permanently for being incompatible. 39992 * 39993 * @param {Object} playlist the media playlist object 39994 * @return {boolean} whether the playlist is incompatible or not 39995 * @function isIncompatible 39996 */ 39997 39998 const isIncompatible = function (playlist) { 39999 return playlist.excludeUntil && playlist.excludeUntil === Infinity; 40000 }; 40001 /** 40002 * Check whether the playlist is enabled or not. 40003 * 40004 * @param {Object} playlist the media playlist object 40005 * @return {boolean} whether the playlist is enabled or not 40006 * @function isEnabled 40007 */ 40008 40009 const isEnabled = function (playlist) { 40010 const excluded = isExcluded(playlist); 40011 return !playlist.disabled && !excluded; 40012 }; 40013 /** 40014 * Check whether the playlist has been manually disabled through the representations api. 40015 * 40016 * @param {Object} playlist the media playlist object 40017 * @return {boolean} whether the playlist is disabled manually or not 40018 * @function isDisabled 40019 */ 40020 40021 const isDisabled = function (playlist) { 40022 return playlist.disabled; 40023 }; 40024 /** 40025 * Returns whether the current playlist is an AES encrypted HLS stream 40026 * 40027 * @return {boolean} true if it's an AES encrypted HLS stream 40028 */ 40029 40030 const isAes = function (media) { 40031 for (let i = 0; i < media.segments.length; i++) { 40032 if (media.segments[i].key) { 40033 return true; 40034 } 40035 } 40036 return false; 40037 }; 40038 /** 40039 * Checks if the playlist has a value for the specified attribute 40040 * 40041 * @param {string} attr 40042 * Attribute to check for 40043 * @param {Object} playlist 40044 * The media playlist object 40045 * @return {boolean} 40046 * Whether the playlist contains a value for the attribute or not 40047 * @function hasAttribute 40048 */ 40049 40050 const hasAttribute = function (attr, playlist) { 40051 return playlist.attributes && playlist.attributes[attr]; 40052 }; 40053 /** 40054 * Estimates the time required to complete a segment download from the specified playlist 40055 * 40056 * @param {number} segmentDuration 40057 * Duration of requested segment 40058 * @param {number} bandwidth 40059 * Current measured bandwidth of the player 40060 * @param {Object} playlist 40061 * The media playlist object 40062 * @param {number=} bytesReceived 40063 * Number of bytes already received for the request. Defaults to 0 40064 * @return {number|NaN} 40065 * The estimated time to request the segment. NaN if bandwidth information for 40066 * the given playlist is unavailable 40067 * @function estimateSegmentRequestTime 40068 */ 40069 40070 const estimateSegmentRequestTime = function (segmentDuration, bandwidth, playlist, bytesReceived = 0) { 40071 if (!hasAttribute('BANDWIDTH', playlist)) { 40072 return NaN; 40073 } 40074 const size = segmentDuration * playlist.attributes.BANDWIDTH; 40075 return (size - bytesReceived * 8) / bandwidth; 40076 }; 40077 /* 40078 * Returns whether the current playlist is the lowest rendition 40079 * 40080 * @return {Boolean} true if on lowest rendition 40081 */ 40082 40083 const isLowestEnabledRendition = (main, media) => { 40084 if (main.playlists.length === 1) { 40085 return true; 40086 } 40087 const currentBandwidth = media.attributes.BANDWIDTH || Number.MAX_VALUE; 40088 return main.playlists.filter(playlist => { 40089 if (!isEnabled(playlist)) { 40090 return false; 40091 } 40092 return (playlist.attributes.BANDWIDTH || 0) < currentBandwidth; 40093 }).length === 0; 40094 }; 40095 const playlistMatch = (a, b) => { 40096 // both playlits are null 40097 // or only one playlist is non-null 40098 // no match 40099 if (!a && !b || !a && b || a && !b) { 40100 return false; 40101 } // playlist objects are the same, match 40102 40103 if (a === b) { 40104 return true; 40105 } // first try to use id as it should be the most 40106 // accurate 40107 40108 if (a.id && b.id && a.id === b.id) { 40109 return true; 40110 } // next try to use reslovedUri as it should be the 40111 // second most accurate. 40112 40113 if (a.resolvedUri && b.resolvedUri && a.resolvedUri === b.resolvedUri) { 40114 return true; 40115 } // finally try to use uri as it should be accurate 40116 // but might miss a few cases for relative uris 40117 40118 if (a.uri && b.uri && a.uri === b.uri) { 40119 return true; 40120 } 40121 return false; 40122 }; 40123 const someAudioVariant = function (main, callback) { 40124 const AUDIO = main && main.mediaGroups && main.mediaGroups.AUDIO || {}; 40125 let found = false; 40126 for (const groupName in AUDIO) { 40127 for (const label in AUDIO[groupName]) { 40128 found = callback(AUDIO[groupName][label]); 40129 if (found) { 40130 break; 40131 } 40132 } 40133 if (found) { 40134 break; 40135 } 40136 } 40137 return !!found; 40138 }; 40139 const isAudioOnly = main => { 40140 // we are audio only if we have no main playlists but do 40141 // have media group playlists. 40142 if (!main || !main.playlists || !main.playlists.length) { 40143 // without audio variants or playlists this 40144 // is not an audio only main. 40145 const found = someAudioVariant(main, variant => variant.playlists && variant.playlists.length || variant.uri); 40146 return found; 40147 } // if every playlist has only an audio codec it is audio only 40148 40149 for (let i = 0; i < main.playlists.length; i++) { 40150 const playlist = main.playlists[i]; 40151 const CODECS = playlist.attributes && playlist.attributes.CODECS; // all codecs are audio, this is an audio playlist. 40152 40153 if (CODECS && CODECS.split(',').every(c => isAudioCodec(c))) { 40154 continue; 40155 } // playlist is in an audio group it is audio only 40156 40157 const found = someAudioVariant(main, variant => playlistMatch(playlist, variant)); 40158 if (found) { 40159 continue; 40160 } // if we make it here this playlist isn't audio and we 40161 // are not audio only 40162 40163 return false; 40164 } // if we make it past every playlist without returning, then 40165 // this is an audio only playlist. 40166 40167 return true; 40168 }; // exports 40169 40170 var Playlist = { 40171 liveEdgeDelay, 40172 duration, 40173 seekable, 40174 getMediaInfoForTime, 40175 isEnabled, 40176 isDisabled, 40177 isExcluded, 40178 isIncompatible, 40179 playlistEnd, 40180 isAes, 40181 hasAttribute, 40182 estimateSegmentRequestTime, 40183 isLowestEnabledRendition, 40184 isAudioOnly, 40185 playlistMatch, 40186 segmentDurationWithParts 40187 }; 40188 const { 40189 log 40190 } = videojs; 40191 const createPlaylistID = (index, uri) => { 40192 return `${index}-${uri}`; 40193 }; // default function for creating a group id 40194 40195 const groupID = (type, group, label) => { 40196 return `placeholder-uri-${type}-${group}-${label}`; 40197 }; 40198 /** 40199 * Parses a given m3u8 playlist 40200 * 40201 * @param {Function} [onwarn] 40202 * a function to call when the parser triggers a warning event. 40203 * @param {Function} [oninfo] 40204 * a function to call when the parser triggers an info event. 40205 * @param {string} manifestString 40206 * The downloaded manifest string 40207 * @param {Object[]} [customTagParsers] 40208 * An array of custom tag parsers for the m3u8-parser instance 40209 * @param {Object[]} [customTagMappers] 40210 * An array of custom tag mappers for the m3u8-parser instance 40211 * @param {boolean} [llhls] 40212 * Whether to keep ll-hls features in the manifest after parsing. 40213 * @return {Object} 40214 * The manifest object 40215 */ 40216 40217 const parseManifest = ({ 40218 onwarn, 40219 oninfo, 40220 manifestString, 40221 customTagParsers = [], 40222 customTagMappers = [], 40223 llhls 40224 }) => { 40225 const parser = new Parser(); 40226 if (onwarn) { 40227 parser.on('warn', onwarn); 40228 } 40229 if (oninfo) { 40230 parser.on('info', oninfo); 40231 } 40232 customTagParsers.forEach(customParser => parser.addParser(customParser)); 40233 customTagMappers.forEach(mapper => parser.addTagMapper(mapper)); 40234 parser.push(manifestString); 40235 parser.end(); 40236 const manifest = parser.manifest; // remove llhls features from the parsed manifest 40237 // if we don't want llhls support. 40238 40239 if (!llhls) { 40240 ['preloadSegment', 'skip', 'serverControl', 'renditionReports', 'partInf', 'partTargetDuration'].forEach(function (k) { 40241 if (manifest.hasOwnProperty(k)) { 40242 delete manifest[k]; 40243 } 40244 }); 40245 if (manifest.segments) { 40246 manifest.segments.forEach(function (segment) { 40247 ['parts', 'preloadHints'].forEach(function (k) { 40248 if (segment.hasOwnProperty(k)) { 40249 delete segment[k]; 40250 } 40251 }); 40252 }); 40253 } 40254 } 40255 if (!manifest.targetDuration) { 40256 let targetDuration = 10; 40257 if (manifest.segments && manifest.segments.length) { 40258 targetDuration = manifest.segments.reduce((acc, s) => Math.max(acc, s.duration), 0); 40259 } 40260 if (onwarn) { 40261 onwarn({ 40262 message: `manifest has no targetDuration defaulting to ${targetDuration}` 40263 }); 40264 } 40265 manifest.targetDuration = targetDuration; 40266 } 40267 const parts = getLastParts(manifest); 40268 if (parts.length && !manifest.partTargetDuration) { 40269 const partTargetDuration = parts.reduce((acc, p) => Math.max(acc, p.duration), 0); 40270 if (onwarn) { 40271 onwarn({ 40272 message: `manifest has no partTargetDuration defaulting to ${partTargetDuration}` 40273 }); 40274 log.error('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.'); 40275 } 40276 manifest.partTargetDuration = partTargetDuration; 40277 } 40278 return manifest; 40279 }; 40280 /** 40281 * Loops through all supported media groups in main and calls the provided 40282 * callback for each group 40283 * 40284 * @param {Object} main 40285 * The parsed main manifest object 40286 * @param {Function} callback 40287 * Callback to call for each media group 40288 */ 40289 40290 const forEachMediaGroup = (main, callback) => { 40291 if (!main.mediaGroups) { 40292 return; 40293 } 40294 ['AUDIO', 'SUBTITLES'].forEach(mediaType => { 40295 if (!main.mediaGroups[mediaType]) { 40296 return; 40297 } 40298 for (const groupKey in main.mediaGroups[mediaType]) { 40299 for (const labelKey in main.mediaGroups[mediaType][groupKey]) { 40300 const mediaProperties = main.mediaGroups[mediaType][groupKey][labelKey]; 40301 callback(mediaProperties, mediaType, groupKey, labelKey); 40302 } 40303 } 40304 }); 40305 }; 40306 /** 40307 * Adds properties and attributes to the playlist to keep consistent functionality for 40308 * playlists throughout VHS. 40309 * 40310 * @param {Object} config 40311 * Arguments object 40312 * @param {Object} config.playlist 40313 * The media playlist 40314 * @param {string} [config.uri] 40315 * The uri to the media playlist (if media playlist is not from within a main 40316 * playlist) 40317 * @param {string} id 40318 * ID to use for the playlist 40319 */ 40320 40321 const setupMediaPlaylist = ({ 40322 playlist, 40323 uri, 40324 id 40325 }) => { 40326 playlist.id = id; 40327 playlist.playlistErrors_ = 0; 40328 if (uri) { 40329 // For media playlists, m3u8-parser does not have access to a URI, as HLS media 40330 // playlists do not contain their own source URI, but one is needed for c
vendor: 9,212 bytes, lines 40330-40580
40330onsistency in 40331 // VHS. 40332 playlist.uri = uri; 40333 } // For HLS main playlists, even though certain attributes MUST be defined, the 40334 // stream may still be played without them. 40335 // For HLS media playlists, m3u8-parser does not attach an attributes object to the 40336 // manifest. 40337 // 40338 // To avoid undefined reference errors through the project, and make the code easier 40339 // to write/read, add an empty attributes object for these cases. 40340 40341 playlist.attributes = playlist.attributes || {}; 40342 }; 40343 /** 40344 * Adds ID, resolvedUri, and attributes properties to each playlist of the main, where 40345 * necessary. In addition, creates playlist IDs for each playlist and adds playlist ID to 40346 * playlist references to the playlists array. 40347 * 40348 * @param {Object} main 40349 * The main playlist 40350 */ 40351 40352 const setupMediaPlaylists = main => { 40353 let i = main.playlists.length; 40354 while (i--) { 40355 const playlist = main.playlists[i]; 40356 setupMediaPlaylist({ 40357 playlist, 40358 id: createPlaylistID(i, playlist.uri) 40359 }); 40360 playlist.resolvedUri = resolveUrl(main.uri, playlist.uri); 40361 main.playlists[playlist.id] = playlist; // URI reference added for backwards compatibility 40362 40363 main.playlists[playlist.uri] = playlist; // Although the spec states an #EXT-X-STREAM-INF tag MUST have a BANDWIDTH attribute, 40364 // the stream can be played without it. Although an attributes property may have been 40365 // added to the playlist to prevent undefined references, issue a warning to fix the 40366 // manifest. 40367 40368 if (!playlist.attributes.BANDWIDTH) { 40369 log.warn('Invalid playlist STREAM-INF detected. Missing BANDWIDTH attribute.'); 40370 } 40371 } 40372 }; 40373 /** 40374 * Adds resolvedUri properties to each media group. 40375 * 40376 * @param {Object} main 40377 * The main playlist 40378 */ 40379 40380 const resolveMediaGroupUris = main => { 40381 forEachMediaGroup(main, properties => { 40382 if (properties.uri) { 40383 properties.resolvedUri = resolveUrl(main.uri, properties.uri); 40384 } 40385 }); 40386 }; 40387 /** 40388 * Creates a main playlist wrapper to insert a sole media playlist into. 40389 * 40390 * @param {Object} media 40391 * Media playlist 40392 * @param {string} uri 40393 * The media URI 40394 * 40395 * @return {Object} 40396 * main playlist 40397 */ 40398 40399 const mainForMedia = (media, uri) => { 40400 const id = createPlaylistID(0, uri); 40401 const main = { 40402 mediaGroups: { 40403 'AUDIO': {}, 40404 'VIDEO': {}, 40405 'CLOSED-CAPTIONS': {}, 40406 'SUBTITLES': {} 40407 }, 40408 uri: window.location.href, 40409 resolvedUri: window.location.href, 40410 playlists: [{ 40411 uri, 40412 id, 40413 resolvedUri: uri, 40414 // m3u8-parser does not attach an attributes property to media playlists so make 40415 // sure that the property is attached to avoid undefined reference errors 40416 attributes: {} 40417 }] 40418 }; // set up ID reference 40419 40420 main.playlists[id] = main.playlists[0]; // URI reference added for backwards compatibility 40421 40422 main.playlists[uri] = main.playlists[0]; 40423 return main; 40424 }; 40425 /** 40426 * Does an in-place update of the main manifest to add updated playlist URI references 40427 * as well as other properties needed by VHS that aren't included by the parser. 40428 * 40429 * @param {Object} main 40430 * main manifest object 40431 * @param {string} uri 40432 * The source URI 40433 * @param {function} createGroupID 40434 * A function to determine how to create the groupID for mediaGroups 40435 */ 40436 40437 const addPropertiesToMain = (main, uri, createGroupID = groupID) => { 40438 main.uri = uri; 40439 for (let i = 0; i < main.playlists.length; i++) { 40440 if (!main.playlists[i].uri) { 40441 // Set up phony URIs for the playlists since playlists are referenced by their URIs 40442 // throughout VHS, but some formats (e.g., DASH) don't have external URIs 40443 // TODO: consider adding dummy URIs in mpd-parser 40444 const phonyUri = `placeholder-uri-${i}`; 40445 main.playlists[i].uri = phonyUri; 40446 } 40447 } 40448 const audioOnlyMain = isAudioOnly(main); 40449 forEachMediaGroup(main, (properties, mediaType, groupKey, labelKey) => { 40450 // add a playlist array under properties 40451 if (!properties.playlists || !properties.playlists.length) { 40452 // If the manifest is audio only and this media group does not have a uri, check 40453 // if the media group is located in the main list of playlists. If it is, don't add 40454 // placeholder properties as it shouldn't be considered an alternate audio track. 40455 if (audioOnlyMain && mediaType === 'AUDIO' && !properties.uri) { 40456 for (let i = 0; i < main.playlists.length; i++) { 40457 const p = main.playlists[i]; 40458 if (p.attributes && p.attributes.AUDIO && p.attributes.AUDIO === groupKey) { 40459 return; 40460 } 40461 } 40462 } 40463 properties.playlists = [_extends$1({}, properties)]; 40464 } 40465 properties.playlists.forEach(function (p, i) { 40466 const groupId = createGroupID(mediaType, groupKey, labelKey, p); 40467 const id = createPlaylistID(i, groupId); 40468 if (p.uri) { 40469 p.resolvedUri = p.resolvedUri || resolveUrl(main.uri, p.uri); 40470 } else { 40471 // DEPRECATED, this has been added to prevent a breaking change. 40472 // previously we only ever had a single media group playlist, so 40473 // we mark the first playlist uri without prepending the index as we used to 40474 // ideally we would do all of the playlists the same way. 40475 p.uri = i === 0 ? groupId : id; // don't resolve a placeholder uri to an absolute url, just use 40476 // the placeholder again 40477 40478 p.resolvedUri = p.uri; 40479 } 40480 p.id = p.id || id; // add an empty attributes object, all playlists are 40481 // expected to have this. 40482 40483 p.attributes = p.attributes || {}; // setup ID and URI references (URI for backwards compatibility) 40484 40485 main.playlists[p.id] = p; 40486 main.playlists[p.uri] = p; 40487 }); 40488 }); 40489 setupMediaPlaylists(main); 40490 resolveMediaGroupUris(main); 40491 }; 40492 class DateRangesStorage { 40493 constructor() { 40494 this.offset_ = null; 40495 this.pendingDateRanges_ = new Map(); 40496 this.processedDateRanges_ = new Map(); 40497 } 40498 setOffset(segments = []) { 40499 // already set 40500 if (this.offset_ !== null) { 40501 return; 40502 } // no segment to process 40503 40504 if (!segments.length) { 40505 return; 40506 } 40507 const [firstSegment] = segments; // no program date time 40508 40509 if (firstSegment.programDateTime === undefined) { 40510 return; 40511 } // Set offset as ProgramDateTime for the very first segment of the very first playlist load: 40512 40513 this.offset_ = firstSegment.programDateTime / 1000; 40514 } 40515 setPendingDateRanges(dateRanges = []) { 40516 if (!dateRanges.length) { 40517 return; 40518 } 40519 const [dateRange] = dateRanges; 40520 const startTime = dateRange.startDate.getTime(); 40521 this.trimProcessedDateRanges_(startTime); 40522 this.pendingDateRanges_ = dateRanges.reduce((map, pendingDateRange) => { 40523 map.set(pendingDateRange.id, pendingDateRange); 40524 return map; 40525 }, new Map()); 40526 } 40527 processDateRange(dateRange) { 40528 this.pendingDateRanges_.delete(dateRange.id); 40529 this.processedDateRanges_.set(dateRange.id, dateRange); 40530 } 40531 getDateRangesToProcess() { 40532 if (this.offset_ === null) { 40533 return []; 40534 } 40535 const dateRangeClasses = {}; 40536 const dateRangesToProcess = []; 40537 this.pendingDateRanges_.forEach((dateRange, id) => { 40538 if (this.processedDateRanges_.has(id)) { 40539 return; 40540 } 40541 dateRange.startTime = dateRange.startDate.getTime() / 1000 - this.offset_; 40542 dateRange.processDateRange = () => this.processDateRange(dateRange); 40543 dateRangesToProcess.push(dateRange); 40544 if (!dateRange.class) { 40545 return; 40546 } 40547 if (dateRangeClasses[dateRange.class]) { 40548 const length = dateRangeClasses[dateRange.class].push(dateRange); 40549 dateRange.classListIndex = length - 1; 40550 } else { 40551 dateRangeClasses[dateRange.class] = [dateRange]; 40552 dateRange.classListIndex = 0; 40553 } 40554 }); 40555 for (const dateRange of dateRangesToProcess) { 40556 const classList = dateRangeClasses[dateRange.class] || []; 40557 if (dateRange.endDate) { 40558 dateRange.endTime = dateRange.endDate.getTime() / 1000 - this.offset_; 40559 } else if (dateRange.endOnNext && classList[dateRange.classListIndex + 1]) { 40560 dateRange.endTime = classList[dateRange.classListIndex + 1].startTime; 40561 } else if (dateRange.duration) { 40562 dateRange.endTime = dateRange.startTime + dateRange.duration; 40563 } else if (dateRange.plannedDuration) { 40564 dateRange.endTime = dateRange.startTime + dateRange.plannedDuration; 40565 } else { 40566 dateRange.endTime = dateRange.startTime; 40567 } 40568 } 40569 return dateRangesToProcess; 40570 } 40571 trimProcessedDateRanges_(startTime) { 40572 const copy = new Map(this.processedDateRanges_); 40573 copy.forEach((dateRange, id) => { 40574 if (dateRange.startDate.getTime() < startTime) { 40575 this.processedDateRanges_.delete(id); 40576 } 40577 }); 40578 } 40579 } 40580 const
40580{ 40581 EventTarget: EventTarget$1 40582 } = videojs; 40583 const addLLHLSQueryDirectives = (uri, media) => { 40584 if (media.endList || !media.serverControl) { 40585 return uri; 40586 } 40587 const parameters = {}; 40588 if (media.serverControl.canBlockReload) { 40589 const { 40590 preloadSegment 40591 } = media; // next msn is a zero based value, length is not. 40592 40593 let nextMSN = media.mediaSequence + media.segments.length; // If preload segment has parts then it is likely 40594 // that we are going to request a part of that preload segment. 40595 // the logic below is used to determine that. 40596 40597 if (preloadSegment) { 40598 const parts = preloadSegment.parts || []; // _HLS_part is a zero based index 40599 40600 const nextPart = getKnownPartCount(media) - 1; // if nextPart is > -1 and not equal to just the
vendor: 26,146 bytes, lines 40601-41309
40601 // length of parts, then we know we had part preload hints 40602 // and we need to add the _HLS_part= query 40603 40604 if (nextPart > -1 && nextPart !== parts.length - 1) { 40605 // add existing parts to our preload hints 40606 // eslint-disable-next-line 40607 parameters._HLS_part = nextPart; 40608 } // this if statement makes sure that we request the msn 40609 // of the preload segment if: 40610 // 1. the preload segment had parts (and was not yet a full segment) 40611 // but was added to our segments array 40612 // 2. the preload segment had preload hints for parts that are not in 40613 // the manifest yet. 40614 // in all other cases we want the segment after the preload segment 40615 // which will be given by using media.segments.length because it is 1 based 40616 // rather than 0 based. 40617 40618 if (nextPart > -1 || parts.length) { 40619 nextMSN--; 40620 } 40621 } // add _HLS_msn= in front of any _HLS_part query 40622 // eslint-disable-next-line 40623 40624 parameters._HLS_msn = nextMSN; 40625 } 40626 if (media.serverControl && media.serverControl.canSkipUntil) { 40627 // add _HLS_skip= infront of all other queries. 40628 // eslint-disable-next-line 40629 parameters._HLS_skip = media.serverControl.canSkipDateranges ? 'v2' : 'YES'; 40630 } 40631 if (Object.keys(parameters).length) { 40632 const parsedUri = new window.URL(uri); 40633 ['_HLS_skip', '_HLS_msn', '_HLS_part'].forEach(function (name) { 40634 if (!parameters.hasOwnProperty(name)) { 40635 return; 40636 } 40637 parsedUri.searchParams.set(name, parameters[name]); 40638 }); 40639 uri = parsedUri.toString(); 40640 } 40641 return uri; 40642 }; 40643 /** 40644 * Returns a new segment object with properties and 40645 * the parts array merged. 40646 * 40647 * @param {Object} a the old segment 40648 * @param {Object} b the new segment 40649 * 40650 * @return {Object} the merged segment 40651 */ 40652 40653 const updateSegment = (a, b) => { 40654 if (!a) { 40655 return b; 40656 } 40657 const result = merge(a, b); // if only the old segment has preload hints 40658 // and the new one does not, remove preload hints. 40659 40660 if (a.preloadHints && !b.preloadHints) { 40661 delete result.preloadHints; 40662 } // if only the old segment has parts 40663 // then the parts are no longer valid 40664 40665 if (a.parts && !b.parts) { 40666 delete result.parts; // if both segments have parts 40667 // copy part propeties from the old segment 40668 // to the new one. 40669 } else if (a.parts && b.parts) { 40670 for (let i = 0; i < b.parts.length; i++) { 40671 if (a.parts && a.parts[i]) { 40672 result.parts[i] = merge(a.parts[i], b.parts[i]); 40673 } 40674 } 40675 } // set skipped to false for segments that have 40676 // have had information merged from the old segment. 40677 40678 if (!a.skipped && b.skipped) { 40679 result.skipped = false; 40680 } // set preload to false for segments that have 40681 // had information added in the new segment. 40682 40683 if (a.preload && !b.preload) { 40684 result.preload = false; 40685 } 40686 return result; 40687 }; 40688 /** 40689 * Returns a new array of segments that is the result of merging 40690 * properties from an older list of segments onto an updated 40691 * list. No properties on the updated playlist will be ovewritten. 40692 * 40693 * @param {Array} original the outdated list of segments 40694 * @param {Array} update the updated list of segments 40695 * @param {number=} offset the index of the first update 40696 * segment in the original segment list. For non-live playlists, 40697 * this should always be zero and does not need to be 40698 * specified. For live playlists, it should be the difference 40699 * between the media sequence numbers in the original and updated 40700 * playlists. 40701 * @return {Array} a list of merged segment objects 40702 */ 40703 40704 const updateSegments = (original, update, offset) => { 40705 const oldSegments = original.slice(); 40706 const newSegments = update.slice(); 40707 offset = offset || 0; 40708 const result = []; 40709 let currentMap; 40710 for (let newIndex = 0; newIndex < newSegments.length; newIndex++) { 40711 const oldSegment = oldSegments[newIndex + offset]; 40712 const newSegment = newSegments[newIndex]; 40713 if (oldSegment) { 40714 currentMap = oldSegment.map || currentMap; 40715 result.push(updateSegment(oldSegment, newSegment)); 40716 } else { 40717 // carry over map to new segment if it is missing 40718 if (currentMap && !newSegment.map) { 40719 newSegment.map = currentMap; 40720 } 40721 result.push(newSegment); 40722 } 40723 } 40724 return result; 40725 }; 40726 const resolveSegmentUris = (segment, baseUri) => { 40727 // preloadSegment will not have a uri at all 40728 // as the segment isn't actually in the manifest yet, only parts 40729 if (!segment.resolvedUri && segment.uri) { 40730 segment.resolvedUri = resolveUrl(baseUri, segment.uri); 40731 } 40732 if (segment.key && !segment.key.resolvedUri) { 40733 segment.key.resolvedUri = resolveUrl(baseUri, segment.key.uri); 40734 } 40735 if (segment.map && !segment.map.resolvedUri) { 40736 segment.map.resolvedUri = resolveUrl(baseUri, segment.map.uri); 40737 } 40738 if (segment.map && segment.map.key && !segment.map.key.resolvedUri) { 40739 segment.map.key.resolvedUri = resolveUrl(baseUri, segment.map.key.uri); 40740 } 40741 if (segment.parts && segment.parts.length) { 40742 segment.parts.forEach(p => { 40743 if (p.resolvedUri) { 40744 return; 40745 } 40746 p.resolvedUri = resolveUrl(baseUri, p.uri); 40747 }); 40748 } 40749 if (segment.preloadHints && segment.preloadHints.length) { 40750 segment.preloadHints.forEach(p => { 40751 if (p.resolvedUri) { 40752 return; 40753 } 40754 p.resolvedUri = resolveUrl(baseUri, p.uri); 40755 }); 40756 } 40757 }; 40758 const getAllSegments = function (media) { 40759 const segments = media.segments || []; 40760 const preloadSegment = media.preloadSegment; // a preloadSegment with only preloadHints is not currently 40761 // a usable segment, only include a preloadSegment that has 40762 // parts. 40763 40764 if (preloadSegment && preloadSegment.parts && preloadSegment.parts.length) { 40765 // if preloadHints has a MAP that means that the 40766 // init segment is going to change. We cannot use any of the parts 40767 // from this preload segment. 40768 if (preloadSegment.preloadHints) { 40769 for (let i = 0; i < preloadSegment.preloadHints.length; i++) { 40770 if (preloadSegment.preloadHints[i].type === 'MAP') { 40771 return segments; 40772 } 40773 } 40774 } // set the duration for our preload segment to target duration. 40775 40776 preloadSegment.duration = media.targetDuration; 40777 preloadSegment.preload = true; 40778 segments.push(preloadSegment); 40779 } 40780 return segments; 40781 }; // consider the playlist unchanged if the playlist object is the same or 40782 // the number of segments is equal, the media sequence number is unchanged, 40783 // and this playlist hasn't become the end of the playlist 40784 40785 const isPlaylistUnchanged = (a, b) => a === b || a.segments && b.segments && a.segments.length === b.segments.length && a.endList === b.endList && a.mediaSequence === b.mediaSequence && a.preloadSegment === b.preloadSegment; 40786 /** 40787 * Returns a new main playlist that is the result of merging an 40788 * updated media playlist into the original version. If the 40789 * updated media playlist does not match any of the playlist 40790 * entries in the original main playlist, null is returned. 40791 * 40792 * @param {Object} main a parsed main M3U8 object 40793 * @param {Object} media a parsed media M3U8 object 40794 * @return {Object} a new object that represents the original 40795 * main playlist with the updated media playlist merged in, or 40796 * null if the merge produced no change. 40797 */ 40798 40799 const updateMain$1 = (main, newMedia, unchangedCheck = isPlaylistUnchanged) => { 40800 const result = merge(main, {}); 40801 const oldMedia = result.playlists[newMedia.id]; 40802 if (!oldMedia) { 40803 return null; 40804 } 40805 if (unchangedCheck(oldMedia, newMedia)) { 40806 return null; 40807 } 40808 newMedia.segments = getAllSegments(newMedia); 40809 const mergedPlaylist = merge(oldMedia, newMedia); // always use the new media's preload segment 40810 40811 if (mergedPlaylist.preloadSegment && !newMedia.preloadSegment) { 40812 delete mergedPlaylist.preloadSegment; 40813 } // if the update could overlap existing segment information, merge the two segment lists 40814 40815 if (oldMedia.segments) { 40816 if (newMedia.skip) { 40817 newMedia.segments = newMedia.segments || []; // add back in objects for skipped segments, so that we merge 40818 // old properties into the new segments 40819 40820 for (let i = 0; i < newMedia.skip.skippedSegments; i++) { 40821 newMedia.segments.unshift({ 40822 skipped: true 40823 }); 40824 } 40825 } 40826 mergedPlaylist.segments = updateSegments(oldMedia.segments, newMedia.segments, newMedia.mediaSequence - oldMedia.mediaSequence); 40827 } // resolve any segment URIs to prevent us from having to do it later 40828 40829 mergedPlaylist.segments.forEach(segment => { 40830 resolveSegmentUris(segment, mergedPlaylist.resolvedUri); 40831 }); // TODO Right now in the playlists array there are two references to each playlist, one 40832 // that is referenced by index, and one by URI. The index reference may no longer be 40833 // necessary. 40834 40835 for (let i = 0; i < result.playlists.length; i++) { 40836 if (result.playlists[i].id === newMedia.id) { 40837 result.playlists[i] = mergedPlaylist; 40838 } 40839 } 40840 result.playlists[newMedia.id] = mergedPlaylist; // URI reference added for backwards compatibility 40841 40842 result.playlists[newMedia.uri] = mergedPlaylist; // update media group playlist references. 40843 40844 forEachMediaGroup(main, (properties, mediaType, groupKey, labelKey) => { 40845 if (!properties.playlists) { 40846 return; 40847 } 40848 for (let i = 0; i < properties.playlists.length; i++) { 40849 if (newMedia.id === properties.playlists[i].id) { 40850 properties.playlists[i] = mergedPlaylist; 40851 } 40852 } 40853 }); 40854 return result; 40855 }; 40856 /** 40857 * Calculates the time to wait before refreshing a live playlist 40858 * 40859 * @param {Object} media 40860 * The current media 40861 * @param {boolean} update 40862 * True if there were any updates from the last refresh, false otherwise 40863 * @return {number} 40864 * The time in ms to wait before refreshing the live playlist 40865 */ 40866 40867 const refreshDelay = (media, update) => { 40868 const segments = media.segments || []; 40869 const lastSegment = segments[segments.length - 1]; 40870 const lastPart = lastSegment && lastSegment.parts && lastSegment.parts[lastSegment.parts.length - 1]; 40871 const lastDuration = lastPart && lastPart.duration || lastSegment && lastSegment.duration; 40872 if (update && lastDuration) { 40873 return lastDuration * 1000; 40874 } // if the playlist is unchanged since the last reload or last segment duration 40875 // cannot be determined, try again after half the target duration 40876 40877 return (media.partTargetDuration || media.targetDuration || 10) * 500; 40878 }; 40879 /** 40880 * Load a playlist from a remote location 40881 * 40882 * @class PlaylistLoader 40883 * @extends Stream 40884 * @param {string|Object} src url or object of manifest 40885 * @param {boolean} withCredentials the withCredentials xhr option 40886 * @class 40887 */ 40888 40889 class PlaylistLoader extends EventTarget$1 { 40890 constructor(src, vhs, options = {}) { 40891 super(); 40892 if (!src) { 40893 throw new Error('A non-empty playlist URL or object is required'); 40894 } 40895 this.logger_ = logger('PlaylistLoader'); 40896 const { 40897 withCredentials = false 40898 } = options; 40899 this.src = src; 40900 this.vhs_ = vhs; 40901 this.withCredentials = withCredentials; 40902 this.addDateRangesToTextTrack_ = options.addDateRangesToTextTrack; 40903 const vhsOptions = vhs.options_; 40904 this.customTagParsers = vhsOptions && vhsOptions.customTagParsers || []; 40905 this.customTagMappers = vhsOptions && vhsOptions.customTagMappers || []; 40906 this.llhls = vhsOptions && vhsOptions.llhls; 40907 this.dateRangesStorage_ = new DateRangesStorage(); // initialize the loader state 40908 40909 this.state = 'HAVE_NOTHING'; // live playlist staleness timeout 40910 40911 this.handleMediaupdatetimeout_ = this.handleMediaupdatetimeout_.bind(this); 40912 this.on('mediaupdatetimeout', this.handleMediaupdatetimeout_); 40913 this.on('loadedplaylist', this.handleLoadedPlaylist_.bind(this)); 40914 } 40915 handleLoadedPlaylist_() { 40916 const mediaPlaylist = this.media(); 40917 if (!mediaPlaylist) { 40918 return; 40919 } 40920 this.dateRangesStorage_.setOffset(mediaPlaylist.segments); 40921 this.dateRangesStorage_.setPendingDateRanges(mediaPlaylist.dateRanges); 40922 const availableDateRanges = this.dateRangesStorage_.getDateRangesToProcess(); 40923 if (!availableDateRanges.length || !this.addDateRangesToTextTrack_) { 40924 return; 40925 } 40926 this.addDateRangesToTextTrack_(availableDateRanges); 40927 } 40928 handleMediaupdatetimeout_() { 40929 if (this.state !== 'HAVE_METADATA') { 40930 // only refresh the media playlist if no other activity is going on 40931 return; 40932 } 40933 const media = this.media(); 40934 let uri = resolveUrl(this.main.uri, media.uri); 40935 if (this.llhls) { 40936 uri = addLLHLSQueryDirectives(uri, media); 40937 } 40938 this.state = 'HAVE_CURRENT_METADATA'; 40939 this.request = this.vhs_.xhr({ 40940 uri, 40941 withCredentials: this.withCredentials 40942 }, (error, req) => { 40943 // disposed 40944 if (!this.request) { 40945 return; 40946 } 40947 if (error) { 40948 return this.playlistRequestError(this.request, this.media(), 'HAVE_METADATA'); 40949 } 40950 this.haveMetadata({ 40951 playlistString: this.request.responseText, 40952 url: this.media().uri, 40953 id: this.media().id 40954 }); 40955 }); 40956 } 40957 playlistRequestError(xhr, playlist, startingState) { 40958 const { 40959 uri, 40960 id 40961 } = playlist; // any in-flight request is now finished 40962 40963 this.request = null; 40964 if (startingState) { 40965 this.state = startingState; 40966 } 40967 this.error = { 40968 playlist: this.main.playlists[id], 40969 status: xhr.status, 40970 message: `HLS playlist request error at URL: ${uri}.`, 40971 responseText: xhr.responseText, 40972 code: xhr.status >= 500 ? 4 : 2 40973 }; 40974 this.trigger('error'); 40975 } 40976 parseManifest_({ 40977 url, 40978 manifestString 40979 }) { 40980 return parseManifest({ 40981 onwarn: ({ 40982 message 40983 }) => this.logger_(`m3u8-parser warn for ${url}: ${message}`), 40984 oninfo: ({ 40985 message 40986 }) => this.logger_(`m3u8-parser info for ${url}: ${message}`), 40987 manifestString, 40988 customTagParsers: this.customTagParsers, 40989 customTagMappers: this.customTagMappers, 40990 llhls: this.llhls 40991 }); 40992 } 40993 /** 40994 * Update the playlist loader's state in response to a new or updated playlist. 40995 * 40996 * @param {string} [playlistString] 40997 * Playlist string (if playlistObject is not provided) 40998 * @param {Object} [playlistObject] 40999 * Playlist object (if playlistString is not provided) 41000 * @param {string} url 41001 * URL of playlist 41002 * @param {string} id 41003 * ID to use for playlist 41004 */ 41005 41006 haveMetadata({ 41007 playlistString, 41008 playlistObject, 41009 url, 41010 id 41011 }) { 41012 // any in-flight request is now finished 41013 this.request = null; 41014 this.state = 'HAVE_METADATA'; 41015 const playlist = playlistObject || this.parseManifest_({ 41016 url, 41017 manifestString: playlistString 41018 }); 41019 playlist.lastRequest = Date.now(); 41020 setupMediaPlaylist({ 41021 playlist, 41022 uri: url, 41023 id 41024 }); // merge this playlist into the main manifest 41025 41026 const update = updateMain$1(this.main, playlist); 41027 this.targetDuration = playlist.partTargetDuration || playlist.targetDuration; 41028 this.pendingMedia_ = null; 41029 if (update) { 41030 this.main = update; 41031 this.media_ = this.main.playlists[id]; 41032 } else { 41033 this.trigger('playlistunchanged'); 41034 } 41035 this.updateMediaUpdateTimeout_(refreshDelay(this.media(), !!update)); 41036 this.trigger('loadedplaylist'); 41037 } 41038 /** 41039 * Abort any outstanding work and clean up. 41040 */ 41041 41042 dispose() { 41043 this.trigger('dispose'); 41044 this.stopRequest(); 41045 window.clearTimeout(this.mediaUpdateTimeout); 41046 window.clearTimeout(this.finalRenditionTimeout); 41047 this.dateRangesStorage_ = new DateRangesStorage(); 41048 this.off(); 41049 } 41050 stopRequest() { 41051 if (this.request) { 41052 const oldRequest = this.request; 41053 this.request = null; 41054 oldRequest.onreadystatechange = null; 41055 oldRequest.abort(); 41056 } 41057 } 41058 /** 41059 * When called without any arguments, returns the currently 41060 * active media playlist. When called with a single argument, 41061 * triggers the playlist loader to asynchronously switch to the 41062 * specified media playlist. Calling this method while the 41063 * loader is in the HAVE_NOTHING causes an error to be emitted 41064 * but otherwise has no effect. 41065 * 41066 * @param {Object=} playlist the parsed media playlist 41067 * object to switch to 41068 * @param {boolean=} shouldDelay whether we should delay the request by half target duration 41069 * 41070 * @return {Playlist} the current loaded media 41071 */ 41072 41073 media(playlist, shouldDelay) { 41074 // getter 41075 if (!playlist) { 41076 return this.media_; 41077 } // setter 41078 41079 if (this.state === 'HAVE_NOTHING') { 41080 throw new Error('Cannot switch media playlist from ' + this.state); 41081 } // find the playlist object if the target playlist has been 41082 // specified by URI 41083 41084 if (typeof playlist === 'string') { 41085 if (!this.main.playlists[playlist]) { 41086 throw new Error('Unknown playlist URI: ' + playlist); 41087 } 41088 playlist = this.main.playlists[playlist]; 41089 } 41090 window.clearTimeout(this.finalRenditionTimeout); 41091 if (shouldDelay) { 41092 const delay = (playlist.partTargetDuration || playlist.targetDuration) / 2 * 1000 || 5 * 1000; 41093 this.finalRenditionTimeout = window.setTimeout(this.media.bind(this, playlist, false), delay); 41094 return; 41095 } 41096 const startingState = this.state; 41097 const mediaChange = !this.media_ || playlist.id !== this.media_.id; 41098 const mainPlaylistRef = this.main.playlists[playlist.id]; // switch to fully loaded playlists immediately 41099 41100 if (mainPlaylistRef && mainPlaylistRef.endList || 41101 // handle the case of a playlist object (e.g., if using vhs-json with a resolved 41102 // media playlist or, for the case of demuxed audio, a resolved audio media group) 41103 playlist.endList && playlist.segments.length) { 41104 // abort outstanding playlist requests 41105 if (this.request) { 41106 this.request.onreadystatechange = null; 41107 this.request.abort(); 41108 this.request = null; 41109 } 41110 this.state = 'HAVE_METADATA'; 41111 this.media_ = playlist; // trigger media change if the active media has been updated 41112 41113 if (mediaChange) { 41114 this.trigger('mediachanging'); 41115 if (startingState === 'HAVE_MAIN_MANIFEST') { 41116 // The initial playlist was a main manifest, and the first media selected was 41117 // also provided (in the form of a resolved playlist object) as part of the 41118 // source object (rather than just a URL). Therefore, since the media playlist 41119 // doesn't need to be requested, loadedmetadata won't trigger as part of the 41120 // normal flow, and needs an explicit trigger here. 41121 this.trigger('loadedmetadata'); 41122 } else { 41123 this.trigger('mediachange'); 41124 } 41125 } 41126 return; 41127 } // We update/set the timeout here so that live playlists 41128 // that are not a media change will "start" the loader as expected. 41129 // We expect that this function will start the media update timeout 41130 // cycle again. This also prevents a playlist switch failure from 41131 // causing us to stall during live. 41132 41133 this.updateMediaUpdateTimeout_(refreshDelay(playlist, true)); // switching to the active playlist is a no-op 41134 41135 if (!mediaChange) { 41136 return; 41137 } 41138 this.state = 'SWITCHING_MEDIA'; // there is already an outstanding playlist request 41139 41140 if (this.request) { 41141 if (playlist.resolvedUri === this.request.url) { 41142 // requesting to switch to the same playlist multiple times 41143 // has no effect after the first 41144 return; 41145 } 41146 this.request.onreadystatechange = null; 41147 this.request.abort(); 41148 this.request = null; 41149 } // request the new playlist 41150 41151 if (this.media_) { 41152 this.trigger('mediachanging'); 41153 } 41154 this.pendingMedia_ = playlist; 41155 this.request = this.vhs_.xhr({ 41156 uri: playlist.resolvedUri, 41157 withCredentials: this.withCredentials 41158 }, (error, req) => { 41159 // disposed 41160 if (!this.request) { 41161 return; 41162 } 41163 playlist.lastRequest = Date.now(); 41164 playlist.resolvedUri = resolveManifestRedirect(playlist.resolvedUri, req); 41165 if (error) { 41166 return this.playlistRequestError(this.request, playlist, startingState); 41167 } 41168 this.haveMetadata({ 41169 playlistString: req.responseText, 41170 url: playlist.uri, 41171 id: playlist.id 41172 }); // fire loadedmetadata the first time a media playlist is loaded 41173 41174 if (startingState === 'HAVE_MAIN_MANIFEST') { 41175 this.trigger('loadedmetadata'); 41176 } else { 41177 this.trigger('mediachange'); 41178 } 41179 }); 41180 } 41181 /** 41182 * pause loading of the playlist 41183 */ 41184 41185 pause() { 41186 if (this.mediaUpdateTimeout) { 41187 window.clearTimeout(this.mediaUpdateTimeout); 41188 this.mediaUpdateTimeout = null; 41189 } 41190 this.stopRequest(); 41191 if (this.state === 'HAVE_NOTHING') { 41192 // If we pause the loader before any data has been retrieved, its as if we never 41193 // started, so reset to an unstarted state. 41194 this.started = false; 41195 } // Need to restore state now that no activity is happening 41196 41197 if (this.state === 'SWITCHING_MEDIA') { 41198 // if the loader was in the process of switching media, it should either return to 41199 // HAVE_MAIN_MANIFEST or HAVE_METADATA depending on if the loader has loaded a media 41200 // playlist yet. This is determined by the existence of loader.media_ 41201 if (this.media_) { 41202 this.state = 'HAVE_METADATA'; 41203 } else { 41204 this.state = 'HAVE_MAIN_MANIFEST'; 41205 } 41206 } else if (this.state === 'HAVE_CURRENT_METADATA') { 41207 this.state = 'HAVE_METADATA'; 41208 } 41209 } 41210 /** 41211 * start loading of the playlist 41212 */ 41213 41214 load(shouldDelay) { 41215 if (this.mediaUpdateTimeout) { 41216 window.clearTimeout(this.mediaUpdateTimeout); 41217 this.mediaUpdateTimeout = null; 41218 } 41219 const media = this.media(); 41220 if (shouldDelay) { 41221 const delay = media ? (media.partTargetDuration || media.targetDuration) / 2 * 1000 : 5 * 1000; 41222 this.mediaUpdateTimeout = window.setTimeout(() => { 41223 this.mediaUpdateTimeout = null; 41224 this.load(); 41225 }, delay); 41226 return; 41227 } 41228 if (!this.started) { 41229 this.start(); 41230 return; 41231 } 41232 if (media && !media.endList) { 41233 this.trigger('mediaupdatetimeout'); 41234 } else { 41235 this.trigger('loadedplaylist'); 41236 } 41237 } 41238 updateMediaUpdateTimeout_(delay) { 41239 if (this.mediaUpdateTimeout) { 41240 window.clearTimeout(this.mediaUpdateTimeout); 41241 this.mediaUpdateTimeout = null; 41242 } // we only have use mediaupdatetimeout for live playlists. 41243 41244 if (!this.media() || this.media().endList) { 41245 return; 41246 } 41247 this.mediaUpdateTimeout = window.setTimeout(() => { 41248 this.mediaUpdateTimeout = null; 41249 this.trigger('mediaupdatetimeout'); 41250 this.updateMediaUpdateTimeout_(delay); 41251 }, delay); 41252 } 41253 /** 41254 * start loading of the playlist 41255 */ 41256 41257 start() { 41258 this.started = true; 41259 if (typeof this.src === 'object') { 41260 // in the case of an entirely constructed manifest object (meaning there's no actual 41261 // manifest on a server), default the uri to the page's href 41262 if (!this.src.uri) { 41263 this.src.uri = window.location.href; 41264 } // resolvedUri is added on internally after the initial request. Since there's no 41265 // request for pre-resolved manifests, add on resolvedUri here. 41266 41267 this.src.resolvedUri = this.src.uri; // Since a manifest object was passed in as the source (instead of a URL), the first 41268 // request can be skipped (since the top level of the manifest, at a minimum, is 41269 // already available as a parsed manifest object). However, if the manifest object 41270 // represents a main playlist, some media playlists may need to be resolved before 41271 // the starting segment list is available. Therefore, go directly to setup of the 41272 // initial playlist, and let the normal flow continue from there. 41273 // 41274 // Note that the call to setup is asynchronous, as other sections of VHS may assume 41275 // that the first request is asynchronous. 41276 41277 setTimeout(() => { 41278 this.setupInitialPlaylist(this.src); 41279 }, 0); 41280 return; 41281 } // request the specified URL 41282 41283 this.request = this.vhs_.xhr({ 41284 uri: this.src, 41285 withCredentials: this.withCredentials 41286 }, (error, req) => { 41287 // disposed 41288 if (!this.request) { 41289 return; 41290 } // clear the loader's request reference 41291 41292 this.request = null; 41293 if (error) { 41294 this.error = { 41295 status: req.status, 41296 message: `HLS playlist request error at URL: ${this.src}.`, 41297 responseText: req.responseText, 41298 // MEDIA_ERR_NETWORK 41299 code: 2 41300 }; 41301 if (this.state === 'HAVE_NOTHING') { 41302 this.started = false; 41303 } 41304 return this.trigger('error'); 41305 } 41306 this.src = resolveManifestRedirect(this.src, req); 41307 const manifest = this.parseManifest_({ 41308 manifestString: req.responseText, 41309 url: this.src
vendor: 12,224 bytes, lines 41309-41606
41309 41310 }); 41311 this.setupInitialPlaylist(manifest); 41312 }); 41313 } 41314 srcUri() { 41315 return typeof this.src === 'string' ? this.src : this.src.uri; 41316 } 41317 /** 41318 * Given a manifest object that's either a main or media playlist, trigger the proper 41319 * events and set the state of the playlist loader. 41320 * 41321 * If the manifest object represents a main playlist, `loadedplaylist` will be 41322 * triggered to allow listeners to select a playlist. If none is selected, the loader 41323 * will default to the first one in the playlists array. 41324 * 41325 * If the manifest object represents a media playlist, `loadedplaylist` will be 41326 * triggered followed by `loadedmetadata`, as the only available playlist is loaded. 41327 * 41328 * In the case of a media playlist, a main playlist object wrapper with one playlist 41329 * will be created so that all logic can handle playlists in the same fashion (as an 41330 * assumed manifest object schema). 41331 * 41332 * @param {Object} manifest 41333 * The parsed manifest object 41334 */ 41335 41336 setupInitialPlaylist(manifest) { 41337 this.state = 'HAVE_MAIN_MANIFEST'; 41338 if (manifest.playlists) { 41339 this.main = manifest; 41340 addPropertiesToMain(this.main, this.srcUri()); // If the initial main playlist has playlists wtih segments already resolved, 41341 // then resolve URIs in advance, as they are usually done after a playlist request, 41342 // which may not happen if the playlist is resolved. 41343 41344 manifest.playlists.forEach(playlist => { 41345 playlist.segments = getAllSegments(playlist); 41346 playlist.segments.forEach(segment => { 41347 resolveSegmentUris(segment, playlist.resolvedUri); 41348 }); 41349 }); 41350 this.trigger('loadedplaylist'); 41351 if (!this.request) { 41352 // no media playlist was specifically selected so start 41353 // from the first listed one 41354 this.media(this.main.playlists[0]); 41355 } 41356 return; 41357 } // In order to support media playlists passed in as vhs-json, the case where the uri 41358 // is not provided as part of the manifest should be considered, and an appropriate 41359 // default used. 41360 41361 const uri = this.srcUri() || window.location.href; 41362 this.main = mainForMedia(manifest, uri); 41363 this.haveMetadata({ 41364 playlistObject: manifest, 41365 url: uri, 41366 id: this.main.playlists[0].id 41367 }); 41368 this.trigger('loadedmetadata'); 41369 } 41370 /** 41371 * Updates or deletes a preexisting pathway clone. 41372 * Ensures that all playlists related to the old pathway clone are 41373 * either updated or deleted. 41374 * 41375 * @param {Object} clone On update, the pathway clone object for the newly updated pathway clone. 41376 * On delete, the old pathway clone object to be deleted. 41377 * @param {boolean} isUpdate True if the pathway is to be updated, 41378 * false if it is meant to be deleted. 41379 */ 41380 41381 updateOrDeleteClone(clone, isUpdate) { 41382 const main = this.main; 41383 const pathway = clone.ID; 41384 let i = main.playlists.length; // Iterate backwards through the playlist so we can remove playlists if necessary. 41385 41386 while (i--) { 41387 const p = main.playlists[i]; 41388 if (p.attributes['PATHWAY-ID'] === pathway) { 41389 const oldPlaylistUri = p.resolvedUri; 41390 const oldPlaylistId = p.id; // update the indexed playlist and add new playlists by ID and URI 41391 41392 if (isUpdate) { 41393 const newPlaylistUri = this.createCloneURI_(p.resolvedUri, clone); 41394 const newPlaylistId = createPlaylistID(pathway, newPlaylistUri); 41395 const attributes = this.createCloneAttributes_(pathway, p.attributes); 41396 const updatedPlaylist = this.createClonePlaylist_(p, newPlaylistId, clone, attributes); 41397 main.playlists[i] = updatedPlaylist; 41398 main.playlists[newPlaylistId] = updatedPlaylist; 41399 main.playlists[newPlaylistUri] = updatedPlaylist; 41400 } else { 41401 // Remove the indexed playlist. 41402 main.playlists.splice(i, 1); 41403 } // Remove playlists by the old ID and URI. 41404 41405 delete main.playlists[oldPlaylistId]; 41406 delete main.playlists[oldPlaylistUri]; 41407 } 41408 } 41409 this.updateOrDeleteCloneMedia(clone, isUpdate); 41410 } 41411 /** 41412 * Updates or deletes media data based on the pathway clone object. 41413 * Due to the complexity of the media groups and playlists, in all cases 41414 * we remove all of the old media groups and playlists. 41415 * On updates, we then create new media groups and playlists based on the 41416 * new pathway clone object. 41417 * 41418 * @param {Object} clone The pathway clone object for the newly updated pathway clone. 41419 * @param {boolean} isUpdate True if the pathway is to be updated, 41420 * false if it is meant to be deleted. 41421 */ 41422 41423 updateOrDeleteCloneMedia(clone, isUpdate) { 41424 const main = this.main; 41425 const id = clone.ID; 41426 ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach(mediaType => { 41427 if (!main.mediaGroups[mediaType] || !main.mediaGroups[mediaType][id]) { 41428 return; 41429 } 41430 for (const groupKey in main.mediaGroups[mediaType]) { 41431 // Remove all media playlists for the media group for this pathway clone. 41432 if (groupKey === id) { 41433 for (const labelKey in main.mediaGroups[mediaType][groupKey]) { 41434 const oldMedia = main.mediaGroups[mediaType][groupKey][labelKey]; 41435 oldMedia.playlists.forEach((p, i) => { 41436 const oldMediaPlaylist = main.playlists[p.id]; 41437 const oldPlaylistId = oldMediaPlaylist.id; 41438 const oldPlaylistUri = oldMediaPlaylist.resolvedUri; 41439 delete main.playlists[oldPlaylistId]; 41440 delete main.playlists[oldPlaylistUri]; 41441 }); 41442 } // Delete the old media group. 41443 41444 delete main.mediaGroups[mediaType][groupKey]; 41445 } 41446 } 41447 }); // Create the new media groups and playlists if there is an update. 41448 41449 if (isUpdate) { 41450 this.createClonedMediaGroups_(clone); 41451 } 41452 } 41453 /** 41454 * Given a pathway clone object, clones all necessary playlists. 41455 * 41456 * @param {Object} clone The pathway clone object. 41457 * @param {Object} basePlaylist The original playlist to clone from. 41458 */ 41459 41460 addClonePathway(clone, basePlaylist = {}) { 41461 const main = this.main; 41462 const index = main.playlists.length; 41463 const uri = this.createCloneURI_(basePlaylist.resolvedUri, clone); 41464 const playlistId = createPlaylistID(clone.ID, uri); 41465 const attributes = this.createCloneAttributes_(clone.ID, basePlaylist.attributes); 41466 const playlist = this.createClonePlaylist_(basePlaylist, playlistId, clone, attributes); 41467 main.playlists[index] = playlist; // add playlist by ID and URI 41468 41469 main.playlists[playlistId] = playlist; 41470 main.playlists[uri] = playlist; 41471 this.createClonedMediaGroups_(clone); 41472 } 41473 /** 41474 * Given a pathway clone object we create clones of all media. 41475 * In this function, all necessary information and updated playlists 41476 * are added to the `mediaGroup` object. 41477 * Playlists are also added to the `playlists` array so the media groups 41478 * will be properly linked. 41479 * 41480 * @param {Object} clone The pathway clone object. 41481 */ 41482 41483 createClonedMediaGroups_(clone) { 41484 const id = clone.ID; 41485 const baseID = clone['BASE-ID']; 41486 const main = this.main; 41487 ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach(mediaType => { 41488 // If the media type doesn't exist, or there is already a clone, skip 41489 // to the next media type. 41490 if (!main.mediaGroups[mediaType] || main.mediaGroups[mediaType][id]) { 41491 return; 41492 } 41493 for (const groupKey in main.mediaGroups[mediaType]) { 41494 if (groupKey === baseID) { 41495 // Create the group. 41496 main.mediaGroups[mediaType][id] = {}; 41497 } else { 41498 // There is no need to iterate over label keys in this case. 41499 continue; 41500 } 41501 for (const labelKey in main.mediaGroups[mediaType][groupKey]) { 41502 const oldMedia = main.mediaGroups[mediaType][groupKey][labelKey]; 41503 main.mediaGroups[mediaType][id][labelKey] = _extends$1({}, oldMedia); 41504 const newMedia = main.mediaGroups[mediaType][id][labelKey]; // update URIs on the media 41505 41506 const newUri = this.createCloneURI_(oldMedia.resolvedUri, clone); 41507 newMedia.resolvedUri = newUri; 41508 newMedia.uri = newUri; // Reset playlists in the new media group. 41509 41510 newMedia.playlists = []; // Create new playlists in the newly cloned media group. 41511 41512 oldMedia.playlists.forEach((p, i) => { 41513 const oldMediaPlaylist = main.playlists[p.id]; 41514 const group = groupID(mediaType, id, labelKey); 41515 const newPlaylistID = createPlaylistID(id, group); // Check to see if it already exists 41516 41517 if (oldMediaPlaylist && !main.playlists[newPlaylistID]) { 41518 const newMediaPlaylist = this.createClonePlaylist_(oldMediaPlaylist, newPlaylistID, clone); 41519 const newPlaylistUri = newMediaPlaylist.resolvedUri; 41520 main.playlists[newPlaylistID] = newMediaPlaylist; 41521 main.playlists[newPlaylistUri] = newMediaPlaylist; 41522 } 41523 newMedia.playlists[i] = this.createClonePlaylist_(p, newPlaylistID, clone); 41524 }); 41525 } 41526 } 41527 }); 41528 } 41529 /** 41530 * Using the original playlist to be cloned, and the pathway clone object 41531 * information, we create a new playlist. 41532 * 41533 * @param {Object} basePlaylist The original playlist to be cloned from. 41534 * @param {string} id The desired id of the newly cloned playlist. 41535 * @param {Object} clone The pathway clone object. 41536 * @param {Object} attributes An optional object to populate the `attributes` property in the playlist. 41537 * 41538 * @return {Object} The combined cloned playlist. 41539 */ 41540 41541 createClonePlaylist_(basePlaylist, id, clone, attributes) { 41542 const uri = this.createCloneURI_(basePlaylist.resolvedUri, clone); 41543 const newProps = { 41544 resolvedUri: uri, 41545 uri, 41546 id 41547 }; // Remove all segments from previous playlist in the clone. 41548 41549 if (basePlaylist.segments) { 41550 newProps.segments = []; 41551 } 41552 if (attributes) { 41553 newProps.attributes = attributes; 41554 } 41555 return merge(basePlaylist, newProps); 41556 } 41557 /** 41558 * Generates an updated URI for a cloned pathway based on the original 41559 * pathway's URI and the paramaters from the pathway clone object in the 41560 * content steering server response. 41561 * 41562 * @param {string} baseUri URI to be updated in the cloned pathway. 41563 * @param {Object} clone The pathway clone object. 41564 * 41565 * @return {string} The updated URI for the cloned pathway. 41566 */ 41567 41568 createCloneURI_(baseURI, clone) { 41569 const uri = new URL(baseURI); 41570 uri.hostname = clone['URI-REPLACEMENT'].HOST; 41571 const params = clone['URI-REPLACEMENT'].PARAMS; // Add params to the cloned URL. 41572 41573 for (const key of Object.keys(params)) { 41574 uri.searchParams.set(key, params[key]); 41575 } 41576 return uri.href; 41577 } 41578 /** 41579 * Helper function to create the attributes needed for the new clone. 41580 * This mainly adds the necessary media attributes. 41581 * 41582 * @param {string} id The pathway clone object ID. 41583 * @param {Object} oldAttributes The old attributes to compare to. 41584 * @return {Object} The new attributes to add to the playlist. 41585 */ 41586 41587 createCloneAttributes_(id, oldAttributes) { 41588 const attributes = { 41589 ['PATHWAY-ID']: id 41590 }; 41591 ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach(mediaType => { 41592 if (oldAttributes[mediaType]) { 41593 attributes[mediaType] = id; 41594 } 41595 }); 41596 return attributes; 41597 } 41598 /** 41599 * Returns the key ID set from a playlist 41600 * 41601 * @param {playlist} playlist to fetch the key ID set from. 41602 * @return a Set of 32 digit hex strings that represent the unique keyIds for that playlist. 41603 */ 41604 41605 getKeyIdSet(playlist) { 41606
41606if (playlist.contentProtection) { 41607 const keyIds = new Set(); 41608 for (const keysystem in playlist.contentProtection) { 41609 const keyId = playlist.contentProtection[keysystem].attributes.keyId; 41610 if (keyId) { 41611 keyIds.add(keyId.toLowerCase()); 41612 } 41613 } 41614 return keyIds; 41615 } 41616 } 41617 } 41618 41619 /** 41620 * @file xhr.js 41621 */ 41622 const { 41623 xhr: videojsXHR 41624 } = videojs; 41625 const callbackWrapper = function (request, error, response, callback) { 41626 const reqResponse = request.responseType === 'arraybuffer' ? request.response : request.responseText; 41627 if (!error && reqResponse) { 41628 request.responseTime = Date.now(); 41629 request.roundTripTime = request.responseTime - request.requestTime; 41630 request.bytesReceived = reqResponse.byteLength || reqResponse.length; 41631 if (!request.bandwidth) { 41632 request.bandwidth = Math.floor(request.bytesReceived / request.roundTripTime * 8 * 1000); 41633 } 41634 } 41635 if (response.headers) { 41636 request.responseHeaders = response.headers; 41637 } // videojs.xhr now uses a specific code on the error
vendor: 4,803 bytes, lines 41638-41744
41638 // object to signal that a request has timed out instead 41639 // of setting a boolean on the request object 41640 41641 if (error && error.code === 'ETIMEDOUT') { 41642 request.timedout = true; 41643 } // videojs.xhr no longer considers status codes outside of 200 and 0 41644 // (for file uris) to be errors, but the old XHR did, so emulate that 41645 // behavior. Status 206 may be used in response to byterange requests. 41646 41647 if (!error && !request.aborted && response.statusCode !== 200 && response.statusCode !== 206 && response.statusCode !== 0) { 41648 error = new Error('XHR Failed with a response of: ' + (request && (reqResponse || request.responseText))); 41649 } 41650 callback(error, request); 41651 }; 41652 /** 41653 * Iterates over the request hooks Set and calls them in order 41654 * 41655 * @param {Set} hooks the hook Set to iterate over 41656 * @param {Object} options the request options to pass to the xhr wrapper 41657 * @return the callback hook function return value, the modified or new options Object. 41658 */ 41659 41660 const callAllRequestHooks = (requestSet, options) => { 41661 if (!requestSet || !requestSet.size) { 41662 return; 41663 } 41664 let newOptions = options; 41665 requestSet.forEach(requestCallback => { 41666 newOptions = requestCallback(newOptions); 41667 }); 41668 return newOptions; 41669 }; 41670 /** 41671 * Iterates over the response hooks Set and calls them in order. 41672 * 41673 * @param {Set} hooks the hook Set to iterate over 41674 * @param {Object} request the xhr request object 41675 * @param {Object} error the xhr error object 41676 * @param {Object} response the xhr response object 41677 */ 41678 41679 const callAllResponseHooks = (responseSet, request, error, response) => { 41680 if (!responseSet || !responseSet.size) { 41681 return; 41682 } 41683 responseSet.forEach(responseCallback => { 41684 responseCallback(request, error, response); 41685 }); 41686 }; 41687 const xhrFactory = function () { 41688 const xhr = function XhrFunction(options, callback) { 41689 // Add a default timeout 41690 options = merge({ 41691 timeout: 45e3 41692 }, options); // Allow an optional user-specified function to modify the option 41693 // object before we construct the xhr request 41694 // TODO: Remove beforeRequest in the next major release. 41695 41696 const beforeRequest = XhrFunction.beforeRequest || videojs.Vhs.xhr.beforeRequest; // onRequest and onResponse hooks as a Set, at either the player or global level. 41697 // TODO: new Set added here for beforeRequest alias. Remove this when beforeRequest is removed. 41698 41699 const _requestCallbackSet = XhrFunction._requestCallbackSet || videojs.Vhs.xhr._requestCallbackSet || new Set(); 41700 const _responseCallbackSet = XhrFunction._responseCallbackSet || videojs.Vhs.xhr._responseCallbackSet; 41701 if (beforeRequest && typeof beforeRequest === 'function') { 41702 videojs.log.warn('beforeRequest is deprecated, use onRequest instead.'); 41703 _requestCallbackSet.add(beforeRequest); 41704 } // Use the standard videojs.xhr() method unless `videojs.Vhs.xhr` has been overriden 41705 // TODO: switch back to videojs.Vhs.xhr.name === 'XhrFunction' when we drop IE11 41706 41707 const xhrMethod = videojs.Vhs.xhr.original === true ? videojsXHR : videojs.Vhs.xhr; // call all registered onRequest hooks, assign new options. 41708 41709 const beforeRequestOptions = callAllRequestHooks(_requestCallbackSet, options); // Remove the beforeRequest function from the hooks set so stale beforeRequest functions are not called. 41710 41711 _requestCallbackSet.delete(beforeRequest); // xhrMethod will call XMLHttpRequest.open and XMLHttpRequest.send 41712 41713 const request = xhrMethod(beforeRequestOptions || options, function (error, response) { 41714 // call all registered onResponse hooks 41715 callAllResponseHooks(_responseCallbackSet, request, error, response); 41716 return callbackWrapper(request, error, response, callback); 41717 }); 41718 const originalAbort = request.abort; 41719 request.abort = function () { 41720 request.aborted = true; 41721 return originalAbort.apply(request, arguments); 41722 }; 41723 request.uri = options.uri; 41724 request.requestTime = Date.now(); 41725 return request; 41726 }; 41727 xhr.original = true; 41728 return xhr; 41729 }; 41730 /** 41731 * Turns segment byterange into a string suitable for use in 41732 * HTTP Range requests 41733 * 41734 * @param {Object} byterange - an object with two values defining the start and end 41735 * of a byte-range 41736 */ 41737 41738 const byterangeStr = function (byterange) { 41739 // `byterangeEnd` is one less than `offset + length` because the HTTP range 41740 // header uses inclusive ranges 41741 let byterangeEnd; 41742 const byterangeStart = byterange.offset; 41743 if (typeof byterange.offset === 'bigint' || typeof byterange.length === 'bigint') { 41744 byterangeEnd = window.BigInt(byterange.offset) + window.BigInt(byterange.length) - window
vendor: 17,434 bytes, lines 41744-42227
41744.BigInt(1); 41745 } else { 41746 byterangeEnd = byterange.offset + byterange.length - 1; 41747 } 41748 return 'bytes=' + byterangeStart + '-' + byterangeEnd; 41749 }; 41750 /** 41751 * Defines headers for use in the xhr request for a particular segment. 41752 * 41753 * @param {Object} segment - a simplified copy of the segmentInfo object 41754 * from SegmentLoader 41755 */ 41756 41757 const segmentXhrHeaders = function (segment) { 41758 const headers = {}; 41759 if (segment.byterange) { 41760 headers.Range = byterangeStr(segment.byterange); 41761 } 41762 return headers; 41763 }; 41764 41765 /** 41766 * @file bin-utils.js 41767 */ 41768 41769 /** 41770 * convert a TimeRange to text 41771 * 41772 * @param {TimeRange} range the timerange to use for conversion 41773 * @param {number} i the iterator on the range to convert 41774 * @return {string} the range in string format 41775 */ 41776 41777 const textRange = function (range, i) { 41778 return range.start(i) + '-' + range.end(i); 41779 }; 41780 /** 41781 * format a number as hex string 41782 * 41783 * @param {number} e The number 41784 * @param {number} i the iterator 41785 * @return {string} the hex formatted number as a string 41786 */ 41787 41788 const formatHexString = function (e, i) { 41789 const value = e.toString(16); 41790 return '00'.substring(0, 2 - value.length) + value + (i % 2 ? ' ' : ''); 41791 }; 41792 const formatAsciiString = function (e) { 41793 if (e >= 0x20 && e < 0x7e) { 41794 return String.fromCharCode(e); 41795 } 41796 return '.'; 41797 }; 41798 /** 41799 * Creates an object for sending to a web worker modifying properties that are TypedArrays 41800 * into a new object with seperated properties for the buffer, byteOffset, and byteLength. 41801 * 41802 * @param {Object} message 41803 * Object of properties and values to send to the web worker 41804 * @return {Object} 41805 * Modified message with TypedArray values expanded 41806 * @function createTransferableMessage 41807 */ 41808 41809 const createTransferableMessage = function (message) { 41810 const transferable = {}; 41811 Object.keys(message).forEach(key => { 41812 const value = message[key]; 41813 if (isArrayBufferView(value)) { 41814 transferable[key] = { 41815 bytes: value.buffer, 41816 byteOffset: value.byteOffset, 41817 byteLength: value.byteLength 41818 }; 41819 } else { 41820 transferable[key] = value; 41821 } 41822 }); 41823 return transferable; 41824 }; 41825 /** 41826 * Returns a unique string identifier for a media initialization 41827 * segment. 41828 * 41829 * @param {Object} initSegment 41830 * the init segment object. 41831 * 41832 * @return {string} the generated init segment id 41833 */ 41834 41835 const initSegmentId = function (initSegment) { 41836 const byterange = initSegment.byterange || { 41837 length: Infinity, 41838 offset: 0 41839 }; 41840 return [byterange.length, byterange.offset, initSegment.resolvedUri].join(','); 41841 }; 41842 /** 41843 * Returns a unique string identifier for a media segment key. 41844 * 41845 * @param {Object} key the encryption key 41846 * @return {string} the unique id for the media segment key. 41847 */ 41848 41849 const segmentKeyId = function (key) { 41850 return key.resolvedUri; 41851 }; 41852 /** 41853 * utils to help dump binary data to the console 41854 * 41855 * @param {Array|TypedArray} data 41856 * data to dump to a string 41857 * 41858 * @return {string} the data as a hex string. 41859 */ 41860 41861 const hexDump = data => { 41862 const bytes = Array.prototype.slice.call(data); 41863 const step = 16; 41864 let result = ''; 41865 let hex; 41866 let ascii; 41867 for (let j = 0; j < bytes.length / step; j++) { 41868 hex = bytes.slice(j * step, j * step + step).map(formatHexString).join(''); 41869 ascii = bytes.slice(j * step, j * step + step).map(formatAsciiString).join(''); 41870 result += hex + ' ' + ascii + '\n'; 41871 } 41872 return result; 41873 }; 41874 const tagDump = ({ 41875 bytes 41876 }) => hexDump(bytes); 41877 const textRanges = ranges => { 41878 let result = ''; 41879 let i; 41880 for (i = 0; i < ranges.length; i++) { 41881 result += textRange(ranges, i) + ' '; 41882 } 41883 return result; 41884 }; 41885 var utils = /*#__PURE__*/Object.freeze({ 41886 __proto__: null, 41887 createTransferableMessage: createTransferableMessage, 41888 initSegmentId: initSegmentId, 41889 segmentKeyId: segmentKeyId, 41890 hexDump: hexDump, 41891 tagDump: tagDump, 41892 textRanges: textRanges 41893 }); 41894 41895 // TODO handle fmp4 case where the timing info is accurate and doesn't involve transmux 41896 // 25% was arbitrarily chosen, and may need to be refined over time. 41897 41898 const SEGMENT_END_FUDGE_PERCENT = 0.25; 41899 /** 41900 * Converts a player time (any time that can be gotten/set from player.currentTime(), 41901 * e.g., any time within player.seekable().start(0) to player.seekable().end(0)) to a 41902 * program time (any time referencing the real world (e.g., EXT-X-PROGRAM-DATE-TIME)). 41903 * 41904 * The containing segment is required as the EXT-X-PROGRAM-DATE-TIME serves as an "anchor 41905 * point" (a point where we have a mapping from program time to player time, with player 41906 * time being the post transmux start of the segment). 41907 * 41908 * For more details, see [this doc](../../docs/program-time-from-player-time.md). 41909 * 41910 * @param {number} playerTime the player time 41911 * @param {Object} segment the segment which contains the player time 41912 * @return {Date} program time 41913 */ 41914 41915 const playerTimeToProgramTime = (playerTime, segment) => { 41916 if (!segment.dateTimeObject) { 41917 // Can't convert without an "anchor point" for the program time (i.e., a time that can 41918 // be used to map the start of a segment with a real world time). 41919 return null; 41920 } 41921 const transmuxerPrependedSeconds = segment.videoTimingInfo.transmuxerPrependedSeconds; 41922 const transmuxedStart = segment.videoTimingInfo.transmuxedPresentationStart; // get the start of the content from before old content is prepended 41923 41924 const startOfSegment = transmuxedStart + transmuxerPrependedSeconds; 41925 const offsetFromSegmentStart = playerTime - startOfSegment; 41926 return new Date(segment.dateTimeObject.getTime() + offsetFromSegmentStart * 1000); 41927 }; 41928 const originalSegmentVideoDuration = videoTimingInfo => { 41929 return videoTimingInfo.transmuxedPresentationEnd - videoTimingInfo.transmuxedPresentationStart - videoTimingInfo.transmuxerPrependedSeconds; 41930 }; 41931 /** 41932 * Finds a segment that contains the time requested given as an ISO-8601 string. The 41933 * returned segment might be an estimate or an accurate match. 41934 * 41935 * @param {string} programTime The ISO-8601 programTime to find a match for 41936 * @param {Object} playlist A playlist object to search within 41937 */ 41938 41939 const findSegmentForProgramTime = (programTime, playlist) => { 41940 // Assumptions: 41941 // - verifyProgramDateTimeTags has already been run 41942 // - live streams have been started 41943 let dateTimeObject; 41944 try { 41945 dateTimeObject = new Date(programTime); 41946 } catch (e) { 41947 return null; 41948 } 41949 if (!playlist || !playlist.segments || playlist.segments.length === 0) { 41950 return null; 41951 } 41952 let segment = playlist.segments[0]; 41953 if (dateTimeObject < new Date(segment.dateTimeObject)) { 41954 // Requested time is before stream start. 41955 return null; 41956 } 41957 for (let i = 0; i < playlist.segments.length - 1; i++) { 41958 segment = playlist.segments[i]; 41959 const nextSegmentStart = new Date(playlist.segments[i + 1].dateTimeObject); 41960 if (dateTimeObject < nextSegmentStart) { 41961 break; 41962 } 41963 } 41964 const lastSegment = playlist.segments[playlist.segments.length - 1]; 41965 const lastSegmentStart = lastSegment.dateTimeObject; 41966 const lastSegmentDuration = lastSegment.videoTimingInfo ? originalSegmentVideoDuration(lastSegment.videoTimingInfo) : lastSegment.duration + lastSegment.duration * SEGMENT_END_FUDGE_PERCENT; 41967 const lastSegmentEnd = new Date(lastSegmentStart.getTime() + lastSegmentDuration * 1000); 41968 if (dateTimeObject > lastSegmentEnd) { 41969 // Beyond the end of the stream, or our best guess of the end of the stream. 41970 return null; 41971 } 41972 if (dateTimeObject > new Date(lastSegmentStart)) { 41973 segment = lastSegment; 41974 } 41975 return { 41976 segment, 41977 estimatedStart: segment.videoTimingInfo ? segment.videoTimingInfo.transmuxedPresentationStart : Playlist.duration(playlist, playlist.mediaSequence + playlist.segments.indexOf(segment)), 41978 // Although, given that all segments have accurate date time objects, the segment 41979 // selected should be accurate, unless the video has been transmuxed at some point 41980 // (determined by the presence of the videoTimingInfo object), the segment's "player 41981 // time" (the start time in the player) can't be considered accurate. 41982 type: segment.videoTimingInfo ? 'accurate' : 'estimate' 41983 }; 41984 }; 41985 /** 41986 * Finds a segment that contains the given player time(in seconds). 41987 * 41988 * @param {number} time The player time to find a match for 41989 * @param {Object} playlist A playlist object to search within 41990 */ 41991 41992 const findSegmentForPlayerTime = (time, playlist) => { 41993 // Assumptions: 41994 // - there will always be a segment.duration 41995 // - we can start from zero 41996 // - segments are in time order 41997 if (!playlist || !playlist.segments || playlist.segments.length === 0) { 41998 return null; 41999 } 42000 let segmentEnd = 0; 42001 let segment; 42002 for (let i = 0; i < playlist.segments.length; i++) { 42003 segment = playlist.segments[i]; // videoTimingInfo is set after the segment is downloaded and transmuxed, and 42004 // should contain the most accurate values we have for the segment's player times. 42005 // 42006 // Use the accurate transmuxedPresentationEnd value if it is available, otherwise fall 42007 // back to an estimate based on the manifest derived (inaccurate) segment.duration, to 42008 // calculate an end value. 42009 42010 segmentEnd = segment.videoTimingInfo ? segment.videoTimingInfo.transmuxedPresentationEnd : segmentEnd + segment.duration; 42011 if (time <= segmentEnd) { 42012 break; 42013 } 42014 } 42015 const lastSegment = playlist.segments[playlist.segments.length - 1]; 42016 if (lastSegment.videoTimingInfo && lastSegment.videoTimingInfo.transmuxedPresentationEnd < time) { 42017 // The time requested is beyond the stream end. 42018 return null; 42019 } 42020 if (time > segmentEnd) { 42021 // The time is within or beyond the last segment. 42022 // 42023 // Check to see if the time is beyond a reasonable guess of the end of the stream. 42024 if (time > segmentEnd + lastSegment.duration * SEGMENT_END_FUDGE_PERCENT) { 42025 // Technically, because the duration value is only an estimate, the time may still 42026 // exist in the last segment, however, there isn't enough information to make even 42027 // a reasonable estimate. 42028 return null; 42029 } 42030 segment = lastSegment; 42031 } 42032 return { 42033 segment, 42034 estimatedStart: segment.videoTimingInfo ? segment.videoTimingInfo.transmuxedPresentationStart : segmentEnd - segment.duration, 42035 // Because videoTimingInfo is only set after transmux, it is the only way to get 42036 // accurate timing values. 42037 type: segment.videoTimingInfo ? 'accurate' : 'estimate' 42038 }; 42039 }; 42040 /** 42041 * Gives the offset of the comparisonTimestamp from the programTime timestamp in seconds. 42042 * If the offset returned is positive, the programTime occurs after the 42043 * comparisonTimestamp. 42044 * If the offset is negative, the programTime occurs before the comparisonTimestamp. 42045 * 42046 * @param {string} comparisonTimeStamp An ISO-8601 timestamp to compare against 42047 * @param {string} programTime The programTime as an ISO-8601 string 42048 * @return {number} offset 42049 */ 42050 42051 const getOffsetFromTimestamp = (comparisonTimeStamp, programTime) => { 42052 let segmentDateTime; 42053 let programDateTime; 42054 try { 42055 segmentDateTime = new Date(comparisonTimeStamp); 42056 programDateTime = new Date(programTime); 42057 } catch (e) {// TODO handle error 42058 } 42059 const segmentTimeEpoch = segmentDateTime.getTime(); 42060 const programTimeEpoch = programDateTime.getTime(); 42061 return (programTimeEpoch - segmentTimeEpoch) / 1000; 42062 }; 42063 /** 42064 * Checks that all segments in this playlist have programDateTime tags. 42065 * 42066 * @param {Object} playlist A playlist object 42067 */ 42068 42069 const verifyProgramDateTimeTags = playlist => { 42070 if (!playlist.segments || playlist.segments.length === 0) { 42071 return false; 42072 } 42073 for (let i = 0; i < playlist.segments.length; i++) { 42074 const segment = playlist.segments[i]; 42075 if (!segment.dateTimeObject) { 42076 return false; 42077 } 42078 } 42079 return true; 42080 }; 42081 /** 42082 * Returns the programTime of the media given a playlist and a playerTime. 42083 * The playlist must have programDateTime tags for a programDateTime tag to be returned. 42084 * If the segments containing the time requested have not been buffered yet, an estimate 42085 * may be returned to the callback. 42086 * 42087 * @param {Object} args 42088 * @param {Object} args.playlist A playlist object to search within 42089 * @param {number} time A playerTime in seconds 42090 * @param {Function} callback(err, programTime) 42091 * @return {string} err.message A detailed error message 42092 * @return {Object} programTime 42093 * @return {number} programTime.mediaSeconds The streamTime in seconds 42094 * @return {string} programTime.programDateTime The programTime as an ISO-8601 String 42095 */ 42096 42097 const getProgramTime = ({ 42098 playlist, 42099 time = undefined, 42100 callback 42101 }) => { 42102 if (!callback) { 42103 throw new Error('getProgramTime: callback must be provided'); 42104 } 42105 if (!playlist || time === undefined) { 42106 return callback({ 42107 message: 'getProgramTime: playlist and time must be provided' 42108 }); 42109 } 42110 const matchedSegment = findSegmentForPlayerTime(time, playlist); 42111 if (!matchedSegment) { 42112 return callback({ 42113 message: 'valid programTime was not found' 42114 }); 42115 } 42116 if (matchedSegment.type === 'estimate') { 42117 return callback({ 42118 message: 'Accurate programTime could not be determined.' + ' Please seek to e.seekTime and try again', 42119 seekTime: matchedSegment.estimatedStart 42120 }); 42121 } 42122 const programTimeObject = { 42123 mediaSeconds: time 42124 }; 42125 const programTime = playerTimeToProgramTime(time, matchedSegment.segment); 42126 if (programTime) { 42127 programTimeObject.programDateTime = programTime.toISOString(); 42128 } 42129 return callback(null, programTimeObject); 42130 }; 42131 /** 42132 * Seeks in the player to a time that matches the given programTime ISO-8601 string. 42133 * 42134 * @param {Object} args 42135 * @param {string} args.programTime A programTime to seek to as an ISO-8601 String 42136 * @param {Object} args.playlist A playlist to look within 42137 * @param {number} args.retryCount The number of times to try for an accurate seek. Default is 2. 42138 * @param {Function} args.seekTo A method to perform a seek 42139 * @param {boolean} args.pauseAfterSeek Whether to end in a paused state after seeking. Default is true. 42140 * @param {Object} args.tech The tech to seek on 42141 * @param {Function} args.callback(err, newTime) A callback to return the new time to 42142 * @return {string} err.message A detailed error message 42143 * @return {number} newTime The exact time that was seeked to in seconds 42144 */ 42145 42146 const seekToProgramTime = ({ 42147 programTime, 42148 playlist, 42149 retryCount = 2, 42150 seekTo, 42151 pauseAfterSeek = true, 42152 tech, 42153 callback 42154 }) => { 42155 if (!callback) { 42156 throw new Error('seekToProgramTime: callback must be provided'); 42157 } 42158 if (typeof programTime === 'undefined' || !playlist || !seekTo) { 42159 return callback({ 42160 message: 'seekToProgramTime: programTime, seekTo and playlist must be provided' 42161 }); 42162 } 42163 if (!playlist.endList && !tech.hasStarted_) { 42164 return callback({ 42165 message: 'player must be playing a live stream to start buffering' 42166 }); 42167 } 42168 if (!verifyProgramDateTimeTags(playlist)) { 42169 return callback({ 42170 message: 'programDateTime tags must be provided in the manifest ' + playlist.resolvedUri 42171 }); 42172 } 42173 const matchedSegment = findSegmentForProgramTime(programTime, playlist); // no match 42174 42175 if (!matchedSegment) { 42176 return callback({ 42177 message: `${programTime} was not found in the stream` 42178 }); 42179 } 42180 const segment = matchedSegment.segment; 42181 const mediaOffset = getOffsetFromTimestamp(segment.dateTimeObject, programTime); 42182 if (matchedSegment.type === 'estimate') { 42183 // we've run out of retries 42184 if (retryCount === 0) { 42185 return callback({ 42186 message: `${programTime} is not buffered yet. Try again` 42187 }); 42188 } 42189 seekTo(matchedSegment.estimatedStart + mediaOffset); 42190 tech.one('seeked', () => { 42191 seekToProgramTime({ 42192 programTime, 42193 playlist, 42194 retryCount: retryCount - 1, 42195 seekTo, 42196 pauseAfterSeek, 42197 tech, 42198 callback 42199 }); 42200 }); 42201 return; 42202 } // Since the segment.start value is determined from the buffered end or ending time 42203 // of the prior segment, the seekToTime doesn't need to account for any transmuxer 42204 // modifications. 42205 42206 const seekToTime = segment.start + mediaOffset; 42207 const seekedCallback = () => { 42208 return callback(null, tech.currentTime()); 42209 }; // listen for seeked event 42210 42211 tech.one('seeked', seekedCallback); // pause before seeking as video.js will restore this state 42212 42213 if (pauseAfterSeek) { 42214 tech.pause(); 42215 } 42216 seekTo(seekToTime); 42217 }; 42218 42219 // which will only happen if the request is complete. 42220 42221 const callbackOnCompleted = (request, cb) => { 42222 if (request.readyState === 4) { 42223 return cb(); 42224 } 42225 return; 42226 }; 42227 const containerRequest = (uri, xhr, cb
42227) => { 42228 let bytes = []; 42229 let id3Offset; 42230 let finished = false;
vendor: 25,162 bytes, lines 42231-42902
42231 const endRequestAndCallback = function (err, req, type, _bytes) { 42232 req.abort(); 42233 finished = true; 42234 return cb(err, req, type, _bytes); 42235 }; 42236 const progressListener = function (error, request) { 42237 if (finished) { 42238 return; 42239 } 42240 if (error) { 42241 return endRequestAndCallback(error, request, '', bytes); 42242 } // grap the new part of content that was just downloaded 42243 42244 const newPart = request.responseText.substring(bytes && bytes.byteLength || 0, request.responseText.length); // add that onto bytes 42245 42246 bytes = concatTypedArrays(bytes, stringToBytes(newPart, true)); 42247 id3Offset = id3Offset || getId3Offset(bytes); // we need at least 10 bytes to determine a type 42248 // or we need at least two bytes after an id3Offset 42249 42250 if (bytes.length < 10 || id3Offset && bytes.length < id3Offset + 2) { 42251 return callbackOnCompleted(request, () => endRequestAndCallback(error, request, '', bytes)); 42252 } 42253 const type = detectContainerForBytes(bytes); // if this looks like a ts segment but we don't have enough data 42254 // to see the second sync byte, wait until we have enough data 42255 // before declaring it ts 42256 42257 if (type === 'ts' && bytes.length < 188) { 42258 return callbackOnCompleted(request, () => endRequestAndCallback(error, request, '', bytes)); 42259 } // this may be an unsynced ts segment 42260 // wait for 376 bytes before detecting no container 42261 42262 if (!type && bytes.length < 376) { 42263 return callbackOnCompleted(request, () => endRequestAndCallback(error, request, '', bytes)); 42264 } 42265 return endRequestAndCallback(null, request, type, bytes); 42266 }; 42267 const options = { 42268 uri, 42269 beforeSend(request) { 42270 // this forces the browser to pass the bytes to us unprocessed 42271 request.overrideMimeType('text/plain; charset=x-user-defined'); 42272 request.addEventListener('progress', function ({ 42273 total, 42274 loaded 42275 }) { 42276 return callbackWrapper(request, null, { 42277 statusCode: request.status 42278 }, progressListener); 42279 }); 42280 } 42281 }; 42282 const request = xhr(options, function (error, response) { 42283 return callbackWrapper(request, error, response, progressListener); 42284 }); 42285 return request; 42286 }; 42287 const { 42288 EventTarget 42289 } = videojs; 42290 const dashPlaylistUnchanged = function (a, b) { 42291 if (!isPlaylistUnchanged(a, b)) { 42292 return false; 42293 } // for dash the above check will often return true in scenarios where 42294 // the playlist actually has changed because mediaSequence isn't a 42295 // dash thing, and we often set it to 1. So if the playlists have the same amount 42296 // of segments we return true. 42297 // So for dash we need to make sure that the underlying segments are different. 42298 // if sidx changed then the playlists are different. 42299 42300 if (a.sidx && b.sidx && (a.sidx.offset !== b.sidx.offset || a.sidx.length !== b.sidx.length)) { 42301 return false; 42302 } else if (!a.sidx && b.sidx || a.sidx && !b.sidx) { 42303 return false; 42304 } // one or the other does not have segments 42305 // there was a change. 42306 42307 if (a.segments && !b.segments || !a.segments && b.segments) { 42308 return false; 42309 } // neither has segments nothing changed 42310 42311 if (!a.segments && !b.segments) { 42312 return true; 42313 } // check segments themselves 42314 42315 for (let i = 0; i < a.segments.length; i++) { 42316 const aSegment = a.segments[i]; 42317 const bSegment = b.segments[i]; // if uris are different between segments there was a change 42318 42319 if (aSegment.uri !== bSegment.uri) { 42320 return false; 42321 } // neither segment has a byterange, there will be no byterange change. 42322 42323 if (!aSegment.byterange && !bSegment.byterange) { 42324 continue; 42325 } 42326 const aByterange = aSegment.byterange; 42327 const bByterange = bSegment.byterange; // if byterange only exists on one of the segments, there was a change. 42328 42329 if (aByterange && !bByterange || !aByterange && bByterange) { 42330 return false; 42331 } // if both segments have byterange with different offsets, there was a change. 42332 42333 if (aByterange.offset !== bByterange.offset || aByterange.length !== bByterange.length) { 42334 return false; 42335 } 42336 } // if everything was the same with segments, this is the same playlist. 42337 42338 return true; 42339 }; 42340 /** 42341 * Use the representation IDs from the mpd object to create groupIDs, the NAME is set to mandatory representation 42342 * ID in the parser. This allows for continuous playout across periods with the same representation IDs 42343 * (continuous periods as defined in DASH-IF 3.2.12). This is assumed in the mpd-parser as well. If we want to support 42344 * periods without continuous playback this function may need modification as well as the parser. 42345 */ 42346 42347 const dashGroupId = (type, group, label, playlist) => { 42348 // If the manifest somehow does not have an ID (non-dash compliant), use the label. 42349 const playlistId = playlist.attributes.NAME || label; 42350 return `placeholder-uri-${type}-${group}-${playlistId}`; 42351 }; 42352 /** 42353 * Parses the main XML string and updates playlist URI references. 42354 * 42355 * @param {Object} config 42356 * Object of arguments 42357 * @param {string} config.mainXml 42358 * The mpd XML 42359 * @param {string} config.srcUrl 42360 * The mpd URL 42361 * @param {Date} config.clientOffset 42362 * A time difference between server and client 42363 * @param {Object} config.sidxMapping 42364 * SIDX mappings for moof/mdat URIs and byte ranges 42365 * @return {Object} 42366 * The parsed mpd manifest object 42367 */ 42368 42369 const parseMainXml = ({ 42370 mainXml, 42371 srcUrl, 42372 clientOffset, 42373 sidxMapping, 42374 previousManifest 42375 }) => { 42376 const manifest = parse(mainXml, { 42377 manifestUri: srcUrl, 42378 clientOffset, 42379 sidxMapping, 42380 previousManifest 42381 }); 42382 addPropertiesToMain(manifest, srcUrl, dashGroupId); 42383 return manifest; 42384 }; 42385 /** 42386 * Removes any mediaGroup labels that no longer exist in the newMain 42387 * 42388 * @param {Object} update 42389 * The previous mpd object being updated 42390 * @param {Object} newMain 42391 * The new mpd object 42392 */ 42393 42394 const removeOldMediaGroupLabels = (update, newMain) => { 42395 forEachMediaGroup(update, (properties, type, group, label) => { 42396 if (!(label in newMain.mediaGroups[type][group])) { 42397 delete update.mediaGroups[type][group][label]; 42398 } 42399 }); 42400 }; 42401 /** 42402 * Returns a new main manifest that is the result of merging an updated main manifest 42403 * into the original version. 42404 * 42405 * @param {Object} oldMain 42406 * The old parsed mpd object 42407 * @param {Object} newMain 42408 * The updated parsed mpd object 42409 * @return {Object} 42410 * A new object representing the original main manifest with the updated media 42411 * playlists merged in 42412 */ 42413 42414 const updateMain = (oldMain, newMain, sidxMapping) => { 42415 let noChanges = true; 42416 let update = merge(oldMain, { 42417 // These are top level properties that can be updated 42418 duration: newMain.duration, 42419 minimumUpdatePeriod: newMain.minimumUpdatePeriod, 42420 timelineStarts: newMain.timelineStarts 42421 }); // First update the playlists in playlist list 42422 42423 for (let i = 0; i < newMain.playlists.length; i++) { 42424 const playlist = newMain.playlists[i]; 42425 if (playlist.sidx) { 42426 const sidxKey = generateSidxKey(playlist.sidx); // add sidx segments to the playlist if we have all the sidx info already 42427 42428 if (sidxMapping && sidxMapping[sidxKey] && sidxMapping[sidxKey].sidx) { 42429 addSidxSegmentsToPlaylist$1(playlist, sidxMapping[sidxKey].sidx, playlist.sidx.resolvedUri); 42430 } 42431 } 42432 const playlistUpdate = updateMain$1(update, playlist, dashPlaylistUnchanged); 42433 if (playlistUpdate) { 42434 update = playlistUpdate; 42435 noChanges = false; 42436 } 42437 } // Then update media group playlists 42438 42439 forEachMediaGroup(newMain, (properties, type, group, label) => { 42440 if (properties.playlists && properties.playlists.length) { 42441 const id = properties.playlists[0].id; 42442 const playlistUpdate = updateMain$1(update, properties.playlists[0], dashPlaylistUnchanged); 42443 if (playlistUpdate) { 42444 update = playlistUpdate; // add new mediaGroup label if it doesn't exist and assign the new mediaGroup. 42445 42446 if (!(label in update.mediaGroups[type][group])) { 42447 update.mediaGroups[type][group][label] = properties; 42448 } // update the playlist reference within media groups 42449 42450 update.mediaGroups[type][group][label].playlists[0] = update.playlists[id]; 42451 noChanges = false; 42452 } 42453 } 42454 }); // remove mediaGroup labels and references that no longer exist in the newMain 42455 42456 removeOldMediaGroupLabels(update, newMain); 42457 if (newMain.minimumUpdatePeriod !== oldMain.minimumUpdatePeriod) { 42458 noChanges = false; 42459 } 42460 if (noChanges) { 42461 return null; 42462 } 42463 return update; 42464 }; // SIDX should be equivalent if the URI and byteranges of the SIDX match. 42465 // If the SIDXs have maps, the two maps should match, 42466 // both `a` and `b` missing SIDXs is considered matching. 42467 // If `a` or `b` but not both have a map, they aren't matching. 42468 42469 const equivalentSidx = (a, b) => { 42470 const neitherMap = Boolean(!a.map && !b.map); 42471 const equivalentMap = neitherMap || Boolean(a.map && b.map && a.map.byterange.offset === b.map.byterange.offset && a.map.byterange.length === b.map.byterange.length); 42472 return equivalentMap && a.uri === b.uri && a.byterange.offset === b.byterange.offset && a.byterange.length === b.byterange.length; 42473 }; // exported for testing 42474 42475 const compareSidxEntry = (playlists, oldSidxMapping) => { 42476 const newSidxMapping = {}; 42477 for (const id in playlists) { 42478 const playlist = playlists[id]; 42479 const currentSidxInfo = playlist.sidx; 42480 if (currentSidxInfo) { 42481 const key = generateSidxKey(currentSidxInfo); 42482 if (!oldSidxMapping[key]) { 42483 break; 42484 } 42485 const savedSidxInfo = oldSidxMapping[key].sidxInfo; 42486 if (equivalentSidx(savedSidxInfo, currentSidxInfo)) { 42487 newSidxMapping[key] = oldSidxMapping[key]; 42488 } 42489 } 42490 } 42491 return newSidxMapping; 42492 }; 42493 /** 42494 * A function that filters out changed items as they need to be requested separately. 42495 * 42496 * The method is exported for testing 42497 * 42498 * @param {Object} main the parsed mpd XML returned via mpd-parser 42499 * @param {Object} oldSidxMapping the SIDX to compare against 42500 */ 42501 42502 const filterChangedSidxMappings = (main, oldSidxMapping) => { 42503 const videoSidx = compareSidxEntry(main.playlists, oldSidxMapping); 42504 let mediaGroupSidx = videoSidx; 42505 forEachMediaGroup(main, (properties, mediaType, groupKey, labelKey) => { 42506 if (properties.playlists && properties.playlists.length) { 42507 const playlists = properties.playlists; 42508 mediaGroupSidx = merge(mediaGroupSidx, compareSidxEntry(playlists, oldSidxMapping)); 42509 } 42510 }); 42511 return mediaGroupSidx; 42512 }; 42513 class DashPlaylistLoader extends EventTarget { 42514 // DashPlaylistLoader must accept either a src url or a playlist because subsequent 42515 // playlist loader setups from media groups will expect to be able to pass a playlist 42516 // (since there aren't external URLs to media playlists with DASH) 42517 constructor(srcUrlOrPlaylist, vhs, options = {}, mainPlaylistLoader) { 42518 super(); 42519 this.mainPlaylistLoader_ = mainPlaylistLoader || this; 42520 if (!mainPlaylistLoader) { 42521 this.isMain_ = true; 42522 } 42523 const { 42524 withCredentials = false 42525 } = options; 42526 this.vhs_ = vhs; 42527 this.withCredentials = withCredentials; 42528 this.addMetadataToTextTrack = options.addMetadataToTextTrack; 42529 if (!srcUrlOrPlaylist) { 42530 throw new Error('A non-empty playlist URL or object is required'); 42531 } // event naming? 42532 42533 this.on('minimumUpdatePeriod', () => { 42534 this.refreshXml_(); 42535 }); // live playlist staleness timeout 42536 42537 this.on('mediaupdatetimeout', () => { 42538 this.refreshMedia_(this.media().id); 42539 }); 42540 this.state = 'HAVE_NOTHING'; 42541 this.loadedPlaylists_ = {}; 42542 this.logger_ = logger('DashPlaylistLoader'); // initialize the loader state 42543 // The mainPlaylistLoader will be created with a string 42544 42545 if (this.isMain_) { 42546 this.mainPlaylistLoader_.srcUrl = srcUrlOrPlaylist; // TODO: reset sidxMapping between period changes 42547 // once multi-period is refactored 42548 42549 this.mainPlaylistLoader_.sidxMapping_ = {}; 42550 } else { 42551 this.childPlaylist_ = srcUrlOrPlaylist; 42552 } 42553 } 42554 requestErrored_(err, request, startingState) { 42555 // disposed 42556 if (!this.request) { 42557 return true; 42558 } // pending request is cleared 42559 42560 this.request = null; 42561 if (err) { 42562 // use the provided error object or create one 42563 // based on the request/response 42564 this.error = typeof err === 'object' && !(err instanceof Error) ? err : { 42565 status: request.status, 42566 message: 'DASH request error at URL: ' + request.uri, 42567 response: request.response, 42568 // MEDIA_ERR_NETWORK 42569 code: 2 42570 }; 42571 if (startingState) { 42572 this.state = startingState; 42573 } 42574 this.trigger('error'); 42575 return true; 42576 } 42577 } 42578 /** 42579 * Verify that the container of the sidx segment can be parsed 42580 * and if it can, get and parse that segment. 42581 */ 42582 42583 addSidxSegments_(playlist, startingState, cb) { 42584 const sidxKey = playlist.sidx && generateSidxKey(playlist.sidx); // playlist lacks sidx or sidx segments were added to this playlist already. 42585 42586 if (!playlist.sidx || !sidxKey || this.mainPlaylistLoader_.sidxMapping_[sidxKey]) { 42587 // keep this function async 42588 this.mediaRequest_ = window.setTimeout(() => cb(false), 0); 42589 return; 42590 } // resolve the segment URL relative to the playlist 42591 42592 const uri = resolveManifestRedirect(playlist.sidx.resolvedUri); 42593 const fin = (err, request) => { 42594 if (this.requestErrored_(err, request, startingState)) { 42595 return; 42596 } 42597 const sidxMapping = this.mainPlaylistLoader_.sidxMapping_; 42598 let sidx; 42599 try { 42600 sidx = parseSidx_1(toUint8(request.response).subarray(8)); 42601 } catch (e) { 42602 // sidx parsing failed. 42603 this.requestErrored_(e, request, startingState); 42604 return; 42605 } 42606 sidxMapping[sidxKey] = { 42607 sidxInfo: playlist.sidx, 42608 sidx 42609 }; 42610 addSidxSegmentsToPlaylist$1(playlist, sidx, playlist.sidx.resolvedUri); 42611 return cb(true); 42612 }; 42613 this.request = containerRequest(uri, this.vhs_.xhr, (err, request, container, bytes) => { 42614 if (err) { 42615 return fin(err, request); 42616 } 42617 if (!container || container !== 'mp4') { 42618 return fin({ 42619 status: request.status, 42620 message: `Unsupported ${container || 'unknown'} container type for sidx segment at URL: ${uri}`, 42621 // response is just bytes in this case 42622 // but we really don't want to return that. 42623 response: '', 42624 playlist, 42625 internal: true, 42626 playlistExclusionDuration: Infinity, 42627 // MEDIA_ERR_NETWORK 42628 code: 2 42629 }, request); 42630 } // if we already downloaded the sidx bytes in the container request, use them 42631 42632 const { 42633 offset, 42634 length 42635 } = playlist.sidx.byterange; 42636 if (bytes.length >= length + offset) { 42637 return fin(err, { 42638 response: bytes.subarray(offset, offset + length), 42639 status: request.status, 42640 uri: request.uri 42641 }); 42642 } // otherwise request sidx bytes 42643 42644 this.request = this.vhs_.xhr({ 42645 uri, 42646 responseType: 'arraybuffer', 42647 headers: segmentXhrHeaders({ 42648 byterange: playlist.sidx.byterange 42649 }) 42650 }, fin); 42651 }); 42652 } 42653 dispose() { 42654 this.trigger('dispose'); 42655 this.stopRequest(); 42656 this.loadedPlaylists_ = {}; 42657 window.clearTimeout(this.minimumUpdatePeriodTimeout_); 42658 window.clearTimeout(this.mediaRequest_); 42659 window.clearTimeout(this.mediaUpdateTimeout); 42660 this.mediaUpdateTimeout = null; 42661 this.mediaRequest_ = null; 42662 this.minimumUpdatePeriodTimeout_ = null; 42663 if (this.mainPlaylistLoader_.createMupOnMedia_) { 42664 this.off('loadedmetadata', this.mainPlaylistLoader_.createMupOnMedia_); 42665 this.mainPlaylistLoader_.createMupOnMedia_ = null; 42666 } 42667 this.off(); 42668 } 42669 hasPendingRequest() { 42670 return this.request || this.mediaRequest_; 42671 } 42672 stopRequest() { 42673 if (this.request) { 42674 const oldRequest = this.request; 42675 this.request = null; 42676 oldRequest.onreadystatechange = null; 42677 oldRequest.abort(); 42678 } 42679 } 42680 media(playlist) { 42681 // getter 42682 if (!playlist) { 42683 return this.media_; 42684 } // setter 42685 42686 if (this.state === 'HAVE_NOTHING') { 42687 throw new Error('Cannot switch media playlist from ' + this.state); 42688 } 42689 const startingState = this.state; // find the playlist object if the target playlist has been specified by URI 42690 42691 if (typeof playlist === 'string') { 42692 if (!this.mainPlaylistLoader_.main.playlists[playlist]) { 42693 throw new Error('Unknown playlist URI: ' + playlist); 42694 } 42695 playlist = this.mainPlaylistLoader_.main.playlists[playlist]; 42696 } 42697 const mediaChange = !this.media_ || playlist.id !== this.media_.id; // switch to previously loaded playlists immediately 42698 42699 if (mediaChange && this.loadedPlaylists_[playlist.id] && this.loadedPlaylists_[playlist.id].endList) { 42700 this.state = 'HAVE_METADATA'; 42701 this.media_ = playlist; // trigger media change if the active media has been updated 42702 42703 if (mediaChange) { 42704 this.trigger('mediachanging'); 42705 this.trigger('mediachange'); 42706 } 42707 return; 42708 } // switching to the active playlist is a no-op 42709 42710 if (!mediaChange) { 42711 return; 42712 } // switching from an already loaded playlist 42713 42714 if (this.media_) { 42715 this.trigger('mediachanging'); 42716 } 42717 this.addSidxSegments_(playlist, startingState, sidxChanged => { 42718 // everything is ready just continue to haveMetadata 42719 this.haveMetadata({ 42720 startingState, 42721 playlist 42722 }); 42723 }); 42724 } 42725 haveMetadata({ 42726 startingState, 42727 playlist 42728 }) { 42729 this.state = 'HAVE_METADATA'; 42730 this.loadedPlaylists_[playlist.id] = playlist; 42731 this.mediaRequest_ = null; // This will trigger loadedplaylist 42732 42733 this.refreshMedia_(playlist.id); // fire loadedmetadata the first time a media playlist is loaded 42734 // to resolve setup of media groups 42735 42736 if (startingState === 'HAVE_MAIN_MANIFEST') { 42737 this.trigger('loadedmetadata'); 42738 } else { 42739 // trigger media change if the active media has been updated 42740 this.trigger('mediachange'); 42741 } 42742 } 42743 pause() { 42744 if (this.mainPlaylistLoader_.createMupOnMedia_) { 42745 this.off('loadedmetadata', this.mainPlaylistLoader_.createMupOnMedia_); 42746 this.mainPlaylistLoader_.createMupOnMedia_ = null; 42747 } 42748 this.stopRequest(); 42749 window.clearTimeout(this.mediaUpdateTimeout); 42750 this.mediaUpdateTimeout = null; 42751 if (this.isMain_) { 42752 window.clearTimeout(this.mainPlaylistLoader_.minimumUpdatePeriodTimeout_); 42753 this.mainPlaylistLoader_.minimumUpdatePeriodTimeout_ = null; 42754 } 42755 if (this.state === 'HAVE_NOTHING') { 42756 // If we pause the loader before any data has been retrieved, its as if we never 42757 // started, so reset to an unstarted state. 42758 this.started = false; 42759 } 42760 } 42761 load(isFinalRendition) { 42762 window.clearTimeout(this.mediaUpdateTimeout); 42763 this.mediaUpdateTimeout = null; 42764 const media = this.media(); 42765 if (isFinalRendition) { 42766 const delay = media ? media.targetDuration / 2 * 1000 : 5 * 1000; 42767 this.mediaUpdateTimeout = window.setTimeout(() => this.load(), delay); 42768 return; 42769 } // because the playlists are internal to the manifest, load should either load the 42770 // main manifest, or do nothing but trigger an event 42771 42772 if (!this.started) { 42773 this.start(); 42774 return; 42775 } 42776 if (media && !media.endList) { 42777 // Check to see if this is the main loader and the MUP was cleared (this happens 42778 // when the loader was paused). `media` should be set at this point since one is always 42779 // set during `start()`. 42780 if (this.isMain_ && !this.minimumUpdatePeriodTimeout_) { 42781 // Trigger minimumUpdatePeriod to refresh the main manifest 42782 this.trigger('minimumUpdatePeriod'); // Since there was no prior minimumUpdatePeriodTimeout it should be recreated 42783 42784 this.updateMinimumUpdatePeriodTimeout_(); 42785 } 42786 this.trigger('mediaupdatetimeout'); 42787 } else { 42788 this.trigger('loadedplaylist'); 42789 } 42790 } 42791 start() { 42792 this.started = true; // We don't need to request the main manifest again 42793 // Call this asynchronously to match the xhr request behavior below 42794 42795 if (!this.isMain_) { 42796 this.mediaRequest_ = window.setTimeout(() => this.haveMain_(), 0); 42797 return; 42798 } 42799 this.requestMain_((req, mainChanged) => { 42800 this.haveMain_(); 42801 if (!this.hasPendingRequest() && !this.media_) { 42802 this.media(this.mainPlaylistLoader_.main.playlists[0]); 42803 } 42804 }); 42805 } 42806 requestMain_(cb) { 42807 this.request = this.vhs_.xhr({ 42808 uri: this.mainPlaylistLoader_.srcUrl, 42809 withCredentials: this.withCredentials 42810 }, (error, req) => { 42811 if (this.requestErrored_(error, req)) { 42812 if (this.state === 'HAVE_NOTHING') { 42813 this.started = false; 42814 } 42815 return; 42816 } 42817 const mainChanged = req.responseText !== this.mainPlaylistLoader_.mainXml_; 42818 this.mainPlaylistLoader_.mainXml_ = req.responseText; 42819 if (req.responseHeaders && req.responseHeaders.date) { 42820 this.mainLoaded_ = Date.parse(req.responseHeaders.date); 42821 } else { 42822 this.mainLoaded_ = Date.now(); 42823 } 42824 this.mainPlaylistLoader_.srcUrl = resolveManifestRedirect(this.mainPlaylistLoader_.srcUrl, req); 42825 if (mainChanged) { 42826 this.handleMain_(); 42827 this.syncClientServerClock_(() => { 42828 return cb(req, mainChanged); 42829 }); 42830 return; 42831 } 42832 return cb(req, mainChanged); 42833 }); 42834 } 42835 /** 42836 * Parses the main xml for UTCTiming node to sync the client clock to the server 42837 * clock. If the UTCTiming node requires a HEAD or GET request, that request is made. 42838 * 42839 * @param {Function} done 42840 * Function to call when clock sync has completed 42841 */ 42842 42843 syncClientServerClock_(done) { 42844 const utcTiming = parseUTCTiming(this.mainPlaylistLoader_.mainXml_); // No UTCTiming element found in the mpd. Use Date header from mpd request as the 42845 // server clock 42846 42847 if (utcTiming === null) { 42848 this.mainPlaylistLoader_.clientOffset_ = this.mainLoaded_ - Date.now(); 42849 return done(); 42850 } 42851 if (utcTiming.method === 'DIRECT') { 42852 this.mainPlaylistLoader_.clientOffset_ = utcTiming.value - Date.now(); 42853 return done(); 42854 } 42855 this.request = this.vhs_.xhr({ 42856 uri: resolveUrl(this.mainPlaylistLoader_.srcUrl, utcTiming.value), 42857 method: utcTiming.method, 42858 withCredentials: this.withCredentials 42859 }, (error, req) => { 42860 // disposed 42861 if (!this.request) { 42862 return; 42863 } 42864 if (error) { 42865 // sync request failed, fall back to using date header from mpd 42866 // TODO: log warning 42867 this.mainPlaylistLoader_.clientOffset_ = this.mainLoaded_ - Date.now(); 42868 return done(); 42869 } 42870 let serverTime; 42871 if (utcTiming.method === 'HEAD') { 42872 if (!req.responseHeaders || !req.responseHeaders.date) { 42873 // expected date header not preset, fall back to using date header from mpd 42874 // TODO: log warning 42875 serverTime = this.mainLoaded_; 42876 } else { 42877 serverTime = Date.parse(req.responseHeaders.date); 42878 } 42879 } else { 42880 serverTime = Date.parse(req.responseText); 42881 } 42882 this.mainPlaylistLoader_.clientOffset_ = serverTime - Date.now(); 42883 done(); 42884 }); 42885 } 42886 haveMain_() { 42887 this.state = 'HAVE_MAIN_MANIFEST'; 42888 if (this.isMain_) { 42889 // We have the main playlist at this point, so 42890 // trigger this to allow PlaylistController 42891 // to make an initial playlist selection 42892 this.trigger('loadedplaylist'); 42893 } else if (!this.media_) { 42894 // no media playlist was specifically selected so select 42895 // the one the child playlist loader was created with 42896 this.media(this.childPlaylist_); 42897 } 42898 } 42899 handleMain_() { 42900 // clear media request 42901 this.mediaRequest_ = null; 42902 const oldMain = this.mainPlaylistLoader_.main;
vendor: 6,165 bytes, lines 42903-43059
42903 let newMain = parseMainXml({ 42904 mainXml: this.mainPlaylistLoader_.mainXml_, 42905 srcUrl: this.mainPlaylistLoader_.srcUrl, 42906 clientOffset: this.mainPlaylistLoader_.clientOffset_, 42907 sidxMapping: this.mainPlaylistLoader_.sidxMapping_, 42908 previousManifest: oldMain 42909 }); // if we have an old main to compare the new main against 42910 42911 if (oldMain) { 42912 newMain = updateMain(oldMain, newMain, this.mainPlaylistLoader_.sidxMapping_); 42913 } // only update main if we have a new main 42914 42915 this.mainPlaylistLoader_.main = newMain ? newMain : oldMain; 42916 const location = this.mainPlaylistLoader_.main.locations && this.mainPlaylistLoader_.main.locations[0]; 42917 if (location && location !== this.mainPlaylistLoader_.srcUrl) { 42918 this.mainPlaylistLoader_.srcUrl = location; 42919 } 42920 if (!oldMain || newMain && newMain.minimumUpdatePeriod !== oldMain.minimumUpdatePeriod) { 42921 this.updateMinimumUpdatePeriodTimeout_(); 42922 } 42923 this.addEventStreamToMetadataTrack_(newMain); 42924 return Boolean(newMain); 42925 } 42926 updateMinimumUpdatePeriodTimeout_() { 42927 const mpl = this.mainPlaylistLoader_; // cancel any pending creation of mup on media 42928 // a new one will be added if needed. 42929 42930 if (mpl.createMupOnMedia_) { 42931 mpl.off('loadedmetadata', mpl.createMupOnMedia_); 42932 mpl.createMupOnMedia_ = null; 42933 } // clear any pending timeouts 42934 42935 if (mpl.minimumUpdatePeriodTimeout_) { 42936 window.clearTimeout(mpl.minimumUpdatePeriodTimeout_); 42937 mpl.minimumUpdatePeriodTimeout_ = null; 42938 } 42939 let mup = mpl.main && mpl.main.minimumUpdatePeriod; // If the minimumUpdatePeriod has a value of 0, that indicates that the current 42940 // MPD has no future validity, so a new one will need to be acquired when new 42941 // media segments are to be made available. Thus, we use the target duration 42942 // in this case 42943 42944 if (mup === 0) { 42945 if (mpl.media()) { 42946 mup = mpl.media().targetDuration * 1000; 42947 } else { 42948 mpl.createMupOnMedia_ = mpl.updateMinimumUpdatePeriodTimeout_; 42949 mpl.one('loadedmetadata', mpl.createMupOnMedia_); 42950 } 42951 } // if minimumUpdatePeriod is invalid or <= zero, which 42952 // can happen when a live video becomes VOD. skip timeout 42953 // creation. 42954 42955 if (typeof mup !== 'number' || mup <= 0) { 42956 if (mup < 0) { 42957 this.logger_(`found invalid minimumUpdatePeriod of ${mup}, not setting a timeout`); 42958 } 42959 return; 42960 } 42961 this.createMUPTimeout_(mup); 42962 } 42963 createMUPTimeout_(mup) { 42964 const mpl = this.mainPlaylistLoader_; 42965 mpl.minimumUpdatePeriodTimeout_ = window.setTimeout(() => { 42966 mpl.minimumUpdatePeriodTimeout_ = null; 42967 mpl.trigger('minimumUpdatePeriod'); 42968 mpl.createMUPTimeout_(mup); 42969 }, mup); 42970 } 42971 /** 42972 * Sends request to refresh the main xml and updates the parsed main manifest 42973 */ 42974 42975 refreshXml_() { 42976 this.requestMain_((req, mainChanged) => { 42977 if (!mainChanged) { 42978 return; 42979 } 42980 if (this.media_) { 42981 this.media_ = this.mainPlaylistLoader_.main.playlists[this.media_.id]; 42982 } // This will filter out updated sidx info from the mapping 42983 42984 this.mainPlaylistLoader_.sidxMapping_ = filterChangedSidxMappings(this.mainPlaylistLoader_.main, this.mainPlaylistLoader_.sidxMapping_); 42985 this.addSidxSegments_(this.media(), this.state, sidxChanged => { 42986 // TODO: do we need to reload the current playlist? 42987 this.refreshMedia_(this.media().id); 42988 }); 42989 }); 42990 } 42991 /** 42992 * Refreshes the media playlist by re-parsing the main xml and updating playlist 42993 * references. If this is an alternate loader, the updated parsed manifest is retrieved 42994 * from the main loader. 42995 */ 42996 42997 refreshMedia_(mediaID) { 42998 if (!mediaID) { 42999 throw new Error('refreshMedia_ must take a media id'); 43000 } // for main we have to reparse the main xml 43001 // to re-create segments based on current timing values 43002 // which may change media. We only skip updating the main manifest 43003 // if this is the first time this.media_ is being set. 43004 // as main was just parsed in that case. 43005 43006 if (this.media_ && this.isMain_) { 43007 this.handleMain_(); 43008 } 43009 const playlists = this.mainPlaylistLoader_.main.playlists; 43010 const mediaChanged = !this.media_ || this.media_ !== playlists[mediaID]; 43011 if (mediaChanged) { 43012 this.media_ = playlists[mediaID]; 43013 } else { 43014 this.trigger('playlistunchanged'); 43015 } 43016 if (!this.mediaUpdateTimeout) { 43017 const createMediaUpdateTimeout = () => { 43018 if (this.media().endList) { 43019 return; 43020 } 43021 this.mediaUpdateTimeout = window.setTimeout(() => { 43022 this.trigger('mediaupdatetimeout'); 43023 createMediaUpdateTimeout(); 43024 }, refreshDelay(this.media(), Boolean(mediaChanged))); 43025 }; 43026 createMediaUpdateTimeout(); 43027 } 43028 this.trigger('loadedplaylist'); 43029 } 43030 /** 43031 * Takes eventstream data from a parsed DASH manifest and adds it to the metadata text track. 43032 * 43033 * @param {manifest} newMain the newly parsed manifest 43034 */ 43035 43036 addEventStreamToMetadataTrack_(newMain) { 43037 // Only add new event stream metadata if we have a new manifest. 43038 if (newMain && this.mainPlaylistLoader_.main.eventStream) { 43039 // convert EventStream to ID3-like data. 43040 const metadataArray = this.mainPlaylistLoader_.main.eventStream.map(eventStreamNode => { 43041 return { 43042 cueTime: eventStreamNode.start, 43043 frames: [{ 43044 data: eventStreamNode.messageData 43045 }] 43046 }; 43047 }); 43048 this.addMetadataToTextTrack('EventStream', metadataArray, this.mainPlaylistLoader_.main.duration); 43049 } 43050 } 43051 /** 43052 * Returns the key ID set from a playlist 43053 * 43054 * @param {playlist} playlist to fetch the key ID set from. 43055 * @return a Set of 32 digit hex strings that represent the unique keyIds for that playlist. 43056 */ 43057 43058 getKeyIdSet(playlist) { 43059 if (playlist.contentProtection) {
vendor: 32,319 bytes, lines 43060-43971
43060 const keyIds = new Set(); 43061 for (const keysystem in playlist.contentProtection) { 43062 const defaultKID = playlist.contentProtection[keysystem].attributes['cenc:default_KID']; 43063 if (defaultKID) { 43064 // DASH keyIds are separated by dashes. 43065 keyIds.add(defaultKID.replace(/-/g, '').toLowerCase()); 43066 } 43067 } 43068 return keyIds; 43069 } 43070 } 43071 } 43072 var Config = { 43073 GOAL_BUFFER_LENGTH: 30, 43074 MAX_GOAL_BUFFER_LENGTH: 60, 43075 BACK_BUFFER_LENGTH: 30, 43076 GOAL_BUFFER_LENGTH_RATE: 1, 43077 // 0.5 MB/s 43078 INITIAL_BANDWIDTH: 4194304, 43079 // A fudge factor to apply to advertised playlist bitrates to account for 43080 // temporary flucations in client bandwidth 43081 BANDWIDTH_VARIANCE: 1.2, 43082 // How much of the buffer must be filled before we consider upswitching 43083 BUFFER_LOW_WATER_LINE: 0, 43084 MAX_BUFFER_LOW_WATER_LINE: 30, 43085 // TODO: Remove this when experimentalBufferBasedABR is removed 43086 EXPERIMENTAL_MAX_BUFFER_LOW_WATER_LINE: 16, 43087 BUFFER_LOW_WATER_LINE_RATE: 1, 43088 // If the buffer is greater than the high water line, we won't switch down 43089 BUFFER_HIGH_WATER_LINE: 30 43090 }; 43091 const stringToArrayBuffer = string => { 43092 const view = new Uint8Array(new ArrayBuffer(string.length)); 43093 for (let i = 0; i < string.length; i++) { 43094 view[i] = string.charCodeAt(i); 43095 } 43096 return view.buffer; 43097 }; 43098 43099 /* global Blob, BlobBuilder, Worker */ 43100 // unify worker interface 43101 const browserWorkerPolyFill = function (workerObj) { 43102 // node only supports on/off 43103 workerObj.on = workerObj.addEventListener; 43104 workerObj.off = workerObj.removeEventListener; 43105 return workerObj; 43106 }; 43107 const createObjectURL = function (str) { 43108 try { 43109 return URL.createObjectURL(new Blob([str], { 43110 type: 'application/javascript' 43111 })); 43112 } catch (e) { 43113 const blob = new BlobBuilder(); 43114 blob.append(str); 43115 return URL.createObjectURL(blob.getBlob()); 43116 } 43117 }; 43118 const factory = function (code) { 43119 return function () { 43120 const objectUrl = createObjectURL(code); 43121 const worker = browserWorkerPolyFill(new Worker(objectUrl)); 43122 worker.objURL = objectUrl; 43123 const terminate = worker.terminate; 43124 worker.on = worker.addEventListener; 43125 worker.off = worker.removeEventListener; 43126 worker.terminate = function () { 43127 URL.revokeObjectURL(objectUrl); 43128 return terminate.call(this); 43129 }; 43130 return worker; 43131 }; 43132 }; 43133 const transform = function (code) { 43134 return `var browserWorkerPolyFill = ${browserWorkerPolyFill.toString()};\n` + 'browserWorkerPolyFill(self);\n' + code; 43135 }; 43136 const getWorkerString = function (fn) { 43137 return fn.toString().replace(/^function.+?{/, '').slice(0, -1); 43138 }; 43139 43140 /* rollup-plugin-worker-factory start for worker!/home/runner/work/http-streaming/http-streaming/src/transmuxer-worker.js */ 43141 const workerCode$1 = transform(getWorkerString(function () { 43142 var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; 43143 /** 43144 * mux.js 43145 * 43146 * Copyright (c) Brightcove 43147 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 43148 * 43149 * A lightweight readable stream implemention that handles event dispatching. 43150 * Objects that inherit from streams should call init in their constructors. 43151 */ 43152 43153 var Stream$8 = function () { 43154 this.init = function () { 43155 var listeners = {}; 43156 /** 43157 * Add a listener for a specified event type. 43158 * @param type {string} the event name 43159 * @param listener {function} the callback to be invoked when an event of 43160 * the specified type occurs 43161 */ 43162 43163 this.on = function (type, listener) { 43164 if (!listeners[type]) { 43165 listeners[type] = []; 43166 } 43167 listeners[type] = listeners[type].concat(listener); 43168 }; 43169 /** 43170 * Remove a listener for a specified event type. 43171 * @param type {string} the event name 43172 * @param listener {function} a function previously registered for this 43173 * type of event through `on` 43174 */ 43175 43176 this.off = function (type, listener) { 43177 var index; 43178 if (!listeners[type]) { 43179 return false; 43180 } 43181 index = listeners[type].indexOf(listener); 43182 listeners[type] = listeners[type].slice(); 43183 listeners[type].splice(index, 1); 43184 return index > -1; 43185 }; 43186 /** 43187 * Trigger an event of the specified type on this stream. Any additional 43188 * arguments to this function are passed as parameters to event listeners. 43189 * @param type {string} the event name 43190 */ 43191 43192 this.trigger = function (type) { 43193 var callbacks, i, length, args; 43194 callbacks = listeners[type]; 43195 if (!callbacks) { 43196 return; 43197 } // Slicing the arguments on every invocation of this method 43198 // can add a significant amount of overhead. Avoid the 43199 // intermediate object creation for the common case of a 43200 // single callback argument 43201 43202 if (arguments.length === 2) { 43203 length = callbacks.length; 43204 for (i = 0; i < length; ++i) { 43205 callbacks[i].call(this, arguments[1]); 43206 } 43207 } else { 43208 args = []; 43209 i = arguments.length; 43210 for (i = 1; i < arguments.length; ++i) { 43211 args.push(arguments[i]); 43212 } 43213 length = callbacks.length; 43214 for (i = 0; i < length; ++i) { 43215 callbacks[i].apply(this, args); 43216 } 43217 } 43218 }; 43219 /** 43220 * Destroys the stream and cleans up. 43221 */ 43222 43223 this.dispose = function () { 43224 listeners = {}; 43225 }; 43226 }; 43227 }; 43228 /** 43229 * Forwards all `data` events on this stream to the destination stream. The 43230 * destination stream should provide a method `push` to receive the data 43231 * events as they arrive. 43232 * @param destination {stream} the stream that will receive all `data` events 43233 * @param autoFlush {boolean} if false, we will not call `flush` on the destination 43234 * when the current stream emits a 'done' event 43235 * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options 43236 */ 43237 43238 Stream$8.prototype.pipe = function (destination) { 43239 this.on('data', function (data) { 43240 destination.push(data); 43241 }); 43242 this.on('done', function (flushSource) { 43243 destination.flush(flushSource); 43244 }); 43245 this.on('partialdone', function (flushSource) { 43246 destination.partialFlush(flushSource); 43247 }); 43248 this.on('endedtimeline', function (flushSource) { 43249 destination.endTimeline(flushSource); 43250 }); 43251 this.on('reset', function (flushSource) { 43252 destination.reset(flushSource); 43253 }); 43254 return destination; 43255 }; // Default stream functions that are expected to be overridden to perform 43256 // actual work. These are provided by the prototype as a sort of no-op 43257 // implementation so that we don't have to check for their existence in the 43258 // `pipe` function above. 43259 43260 Stream$8.prototype.push = function (data) { 43261 this.trigger('data', data); 43262 }; 43263 Stream$8.prototype.flush = function (flushSource) { 43264 this.trigger('done', flushSource); 43265 }; 43266 Stream$8.prototype.partialFlush = function (flushSource) { 43267 this.trigger('partialdone', flushSource); 43268 }; 43269 Stream$8.prototype.endTimeline = function (flushSource) { 43270 this.trigger('endedtimeline', flushSource); 43271 }; 43272 Stream$8.prototype.reset = function (flushSource) { 43273 this.trigger('reset', flushSource); 43274 }; 43275 var stream = Stream$8; 43276 var MAX_UINT32$1 = Math.pow(2, 32); 43277 var getUint64$3 = function (uint8) { 43278 var dv = new DataView(uint8.buffer, uint8.byteOffset, uint8.byteLength); 43279 var value; 43280 if (dv.getBigUint64) { 43281 value = dv.getBigUint64(0); 43282 if (value < Number.MAX_SAFE_INTEGER) { 43283 return Number(value); 43284 } 43285 return value; 43286 } 43287 return dv.getUint32(0) * MAX_UINT32$1 + dv.getUint32(4); 43288 }; 43289 var numbers = { 43290 getUint64: getUint64$3, 43291 MAX_UINT32: MAX_UINT32$1 43292 }; 43293 /** 43294 * mux.js 43295 * 43296 * Copyright (c) Brightcove 43297 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 43298 * 43299 * Functions that generate fragmented MP4s suitable for use with Media 43300 * Source Extensions. 43301 */ 43302 43303 var MAX_UINT32 = numbers.MAX_UINT32; 43304 var box, dinf, esds, ftyp, mdat, mfhd, minf, moof, moov, mvex, mvhd, trak, tkhd, mdia, mdhd, hdlr, sdtp, stbl, stsd, traf, trex, trun$1, types, MAJOR_BRAND, MINOR_VERSION, AVC1_BRAND, VIDEO_HDLR, AUDIO_HDLR, HDLR_TYPES, VMHD, SMHD, DREF, STCO, STSC, STSZ, STTS; // pre-calculate constants 43305 43306 (function () { 43307 var i; 43308 types = { 43309 avc1: [], 43310 // codingname 43311 avcC: [], 43312 btrt: [], 43313 dinf: [], 43314 dref: [], 43315 esds: [], 43316 ftyp: [], 43317 hdlr: [], 43318 mdat: [], 43319 mdhd: [], 43320 mdia: [], 43321 mfhd: [], 43322 minf: [], 43323 moof: [], 43324 moov: [], 43325 mp4a: [], 43326 // codingname 43327 mvex: [], 43328 mvhd: [], 43329 pasp: [], 43330 sdtp: [], 43331 smhd: [], 43332 stbl: [], 43333 stco: [], 43334 stsc: [], 43335 stsd: [], 43336 stsz: [], 43337 stts: [], 43338 styp: [], 43339 tfdt: [], 43340 tfhd: [], 43341 traf: [], 43342 trak: [], 43343 trun: [], 43344 trex: [], 43345 tkhd: [], 43346 vmhd: [] 43347 }; // In environments where Uint8Array is undefined (e.g., IE8), skip set up so that we 43348 // don't throw an error 43349 43350 if (typeof Uint8Array === 'undefined') { 43351 return; 43352 } 43353 for (i in types) { 43354 if (types.hasOwnProperty(i)) { 43355 types[i] = [i.charCodeAt(0), i.charCodeAt(1), i.charCodeAt(2), i.charCodeAt(3)]; 43356 } 43357 } 43358 MAJOR_BRAND = new Uint8Array(['i'.charCodeAt(0), 's'.charCodeAt(0), 'o'.charCodeAt(0), 'm'.charCodeAt(0)]); 43359 AVC1_BRAND = new Uint8Array(['a'.charCodeAt(0), 'v'.charCodeAt(0), 'c'.charCodeAt(0), '1'.charCodeAt(0)]); 43360 MINOR_VERSION = new Uint8Array([0, 0, 0, 1]); 43361 VIDEO_HDLR = new Uint8Array([0x00, 43362 // version 0 43363 0x00, 0x00, 0x00, 43364 // flags 43365 0x00, 0x00, 0x00, 0x00, 43366 // pre_defined 43367 0x76, 0x69, 0x64, 0x65, 43368 // handler_type: 'vide' 43369 0x00, 0x00, 0x00, 0x00, 43370 // reserved 43371 0x00, 0x00, 0x00, 0x00, 43372 // reserved 43373 0x00, 0x00, 0x00, 0x00, 43374 // reserved 43375 0x56, 0x69, 0x64, 0x65, 0x6f, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'VideoHandler' 43376 ]); 43377 43378 AUDIO_HDLR = new Uint8Array([0x00, 43379 // version 0 43380 0x00, 0x00, 0x00, 43381 // flags 43382 0x00, 0x00, 0x00, 0x00, 43383 // pre_defined 43384 0x73, 0x6f, 0x75, 0x6e, 43385 // handler_type: 'soun' 43386 0x00, 0x00, 0x00, 0x00, 43387 // reserved 43388 0x00, 0x00, 0x00, 0x00, 43389 // reserved 43390 0x00, 0x00, 0x00, 0x00, 43391 // reserved 43392 0x53, 0x6f, 0x75, 0x6e, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'SoundHandler' 43393 ]); 43394 43395 HDLR_TYPES = { 43396 video: VIDEO_HDLR, 43397 audio: AUDIO_HDLR 43398 }; 43399 DREF = new Uint8Array([0x00, 43400 // version 0 43401 0x00, 0x00, 0x00, 43402 // flags 43403 0x00, 0x00, 0x00, 0x01, 43404 // entry_count 43405 0x00, 0x00, 0x00, 0x0c, 43406 // entry_size 43407 0x75, 0x72, 0x6c, 0x20, 43408 // 'url' type 43409 0x00, 43410 // version 0 43411 0x00, 0x00, 0x01 // entry_flags 43412 ]); 43413 43414 SMHD = new Uint8Array([0x00, 43415 // version 43416 0x00, 0x00, 0x00, 43417 // flags 43418 0x00, 0x00, 43419 // balance, 0 means centered 43420 0x00, 0x00 // reserved 43421 ]); 43422 43423 STCO = new Uint8Array([0x00, 43424 // version 43425 0x00, 0x00, 0x00, 43426 // flags 43427 0x00, 0x00, 0x00, 0x00 // entry_count 43428 ]); 43429 43430 STSC = STCO; 43431 STSZ = new Uint8Array([0x00, 43432 // version 43433 0x00, 0x00, 0x00, 43434 // flags 43435 0x00, 0x00, 0x00, 0x00, 43436 // sample_size 43437 0x00, 0x00, 0x00, 0x00 // sample_count 43438 ]); 43439 43440 STTS = STCO; 43441 VMHD = new Uint8Array([0x00, 43442 // version 43443 0x00, 0x00, 0x01, 43444 // flags 43445 0x00, 0x00, 43446 // graphicsmode 43447 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // opcolor 43448 ]); 43449 })(); 43450 43451 box = function (type) { 43452 var payload = [], 43453 size = 0, 43454 i, 43455 result, 43456 view; 43457 for (i = 1; i < arguments.length; i++) { 43458 payload.push(arguments[i]); 43459 } 43460 i = payload.length; // calculate the total size we need to allocate 43461 43462 while (i--) { 43463 size += payload[i].byteLength; 43464 } 43465 result = new Uint8Array(size + 8); 43466 view = new DataView(result.buffer, result.byteOffset, result.byteLength); 43467 view.setUint32(0, result.byteLength); 43468 result.set(type, 4); // copy the payload into the result 43469 43470 for (i = 0, size = 8; i < payload.length; i++) { 43471 result.set(payload[i], size); 43472 size += payload[i].byteLength; 43473 } 43474 return result; 43475 }; 43476 dinf = function () { 43477 return box(types.dinf, box(types.dref, DREF)); 43478 }; 43479 esds = function (track) { 43480 return box(types.esds, new Uint8Array([0x00, 43481 // version 43482 0x00, 0x00, 0x00, 43483 // flags 43484 // ES_Descriptor 43485 0x03, 43486 // tag, ES_DescrTag 43487 0x19, 43488 // length 43489 0x00, 0x00, 43490 // ES_ID 43491 0x00, 43492 // streamDependenceFlag, URL_flag, reserved, streamPriority 43493 // DecoderConfigDescriptor 43494 0x04, 43495 // tag, DecoderConfigDescrTag 43496 0x11, 43497 // length 43498 0x40, 43499 // object type 43500 0x15, 43501 // streamType 43502 0x00, 0x06, 0x00, 43503 // bufferSizeDB 43504 0x00, 0x00, 0xda, 0xc0, 43505 // maxBitrate 43506 0x00, 0x00, 0xda, 0xc0, 43507 // avgBitrate 43508 // DecoderSpecificInfo 43509 0x05, 43510 // tag, DecoderSpecificInfoTag 43511 0x02, 43512 // length 43513 // ISO/IEC 14496-3, AudioSpecificConfig 43514 // for samplingFrequencyIndex see ISO/IEC 13818-7:2006, 8.1.3.2.2, Table 35 43515 track.audioobjecttype << 3 | track.samplingfrequencyindex >>> 1, track.samplingfrequencyindex << 7 | track.channelcount << 3, 0x06, 0x01, 0x02 // GASpecificConfig 43516 ])); 43517 }; 43518 43519 ftyp = function () { 43520 return box(types.ftyp, MAJOR_BRAND, MINOR_VERSION, MAJOR_BRAND, AVC1_BRAND); 43521 }; 43522 hdlr = function (type) { 43523 return box(types.hdlr, HDLR_TYPES[type]); 43524 }; 43525 mdat = function (data) { 43526 return box(types.mdat, data); 43527 }; 43528 mdhd = function (track) { 43529 var result = new Uint8Array([0x00, 43530 // version 0 43531 0x00, 0x00, 0x00, 43532 // flags 43533 0x00, 0x00, 0x00, 0x02, 43534 // creation_time 43535 0x00, 0x00, 0x00, 0x03, 43536 // modification_time 43537 0x00, 0x01, 0x5f, 0x90, 43538 // timescale, 90,000 "ticks" per second 43539 track.duration >>> 24 & 0xFF, track.duration >>> 16 & 0xFF, track.duration >>> 8 & 0xFF, track.duration & 0xFF, 43540 // duration 43541 0x55, 0xc4, 43542 // 'und' language (undetermined) 43543 0x00, 0x00]); // Use the sample rate from the track metadata, when it is 43544 // defined. The sample rate can be parsed out of an ADTS header, for 43545 // instance. 43546 43547 if (track.samplerate) { 43548 result[12] = track.samplerate >>> 24 & 0xFF; 43549 result[13] = track.samplerate >>> 16 & 0xFF; 43550 result[14] = track.samplerate >>> 8 & 0xFF; 43551 result[15] = track.samplerate & 0xFF; 43552 } 43553 return box(types.mdhd, result); 43554 }; 43555 mdia = function (track) { 43556 return box(types.mdia, mdhd(track), hdlr(track.type), minf(track)); 43557 }; 43558 mfhd = function (sequenceNumber) { 43559 return box(types.mfhd, new Uint8Array([0x00, 0x00, 0x00, 0x00, 43560 // flags 43561 (sequenceNumber & 0xFF000000) >> 24, (sequenceNumber & 0xFF0000) >> 16, (sequenceNumber & 0xFF00) >> 8, sequenceNumber & 0xFF // sequence_number 43562 ])); 43563 }; 43564 43565 minf = function (track) { 43566 return box(types.minf, track.type === 'video' ? box(types.vmhd, VMHD) : box(types.smhd, SMHD), dinf(), stbl(track)); 43567 }; 43568 moof = function (sequenceNumber, tracks) { 43569 var trackFragments = [], 43570 i = tracks.length; // build traf boxes for each track fragment 43571 43572 while (i--) { 43573 trackFragments[i] = traf(tracks[i]); 43574 } 43575 return box.apply(null, [types.moof, mfhd(sequenceNumber)].concat(trackFragments)); 43576 }; 43577 /** 43578 * Returns a movie box. 43579 * @param tracks {array} the tracks associated with this movie 43580 * @see ISO/IEC 14496-12:2012(E), section 8.2.1 43581 */ 43582 43583 moov = function (tracks) { 43584 var i = tracks.length, 43585 boxes = []; 43586 while (i--) { 43587 boxes[i] = trak(tracks[i]); 43588 } 43589 return box.apply(null, [types.moov, mvhd(0xffffffff)].concat(boxes).concat(mvex(tracks))); 43590 }; 43591 mvex = function (tracks) { 43592 var i = tracks.length, 43593 boxes = []; 43594 while (i--) { 43595 boxes[i] = trex(tracks[i]); 43596 } 43597 return box.apply(null, [types.mvex].concat(boxes)); 43598 }; 43599 mvhd = function (duration) { 43600 var bytes = new Uint8Array([0x00, 43601 // version 0 43602 0x00, 0x00, 0x00, 43603 // flags 43604 0x00, 0x00, 0x00, 0x01, 43605 // creation_time 43606 0x00, 0x00, 0x00, 0x02, 43607 // modification_time 43608 0x00, 0x01, 0x5f, 0x90, 43609 // timescale, 90,000 "ticks" per second 43610 (duration & 0xFF000000) >> 24, (duration & 0xFF0000) >> 16, (duration & 0xFF00) >> 8, duration & 0xFF, 43611 // duration 43612 0x00, 0x01, 0x00, 0x00, 43613 // 1.0 rate 43614 0x01, 0x00, 43615 // 1.0 volume 43616 0x00, 0x00, 43617 // reserved 43618 0x00, 0x00, 0x00, 0x00, 43619 // reserved 43620 0x00, 0x00, 0x00, 0x00, 43621 // reserved 43622 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 43623 // transformation: unity matrix 43624 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 43625 // pre_defined 43626 0xff, 0xff, 0xff, 0xff // next_track_ID 43627 ]); 43628 43629 return box(types.mvhd, bytes); 43630 }; 43631 sdtp = function (track) { 43632 var samples = track.samples || [], 43633 bytes = new Uint8Array(4 + samples.length), 43634 flags, 43635 i; // leave the full box header (4 bytes) all zero 43636 // write the sample table 43637 43638 for (i = 0; i < samples.length; i++) { 43639 flags = samples[i].flags; 43640 bytes[i + 4] = flags.dependsOn << 4 | flags.isDependedOn << 2 | flags.hasRedundancy; 43641 } 43642 return box(types.sdtp, bytes); 43643 }; 43644 stbl = function (track) { 43645 return box(types.stbl, stsd(track), box(types.stts, STTS), box(types.stsc, STSC), box(types.stsz, STSZ), box(types.stco, STCO)); 43646 }; 43647 (function () { 43648 var videoSample, audioSample; 43649 stsd = function (track) { 43650 return box(types.stsd, new Uint8Array([0x00, 43651 // version 0 43652 0x00, 0x00, 0x00, 43653 // flags 43654 0x00, 0x00, 0x00, 0x01]), track.type === 'video' ? videoSample(track) : audioSample(track)); 43655 }; 43656 videoSample = function (track) { 43657 var sps = track.sps || [], 43658 pps = track.pps || [], 43659 sequenceParameterSets = [], 43660 pictureParameterSets = [], 43661 i, 43662 avc1Box; // assemble the SPSs 43663 43664 for (i = 0; i < sps.length; i++) { 43665 sequenceParameterSets.push((sps[i].byteLength & 0xFF00) >>> 8); 43666 sequenceParameterSets.push(sps[i].byteLength & 0xFF); // sequenceParameterSetLength 43667 43668 sequenceParameterSets = sequenceParameterSets.concat(Array.prototype.slice.call(sps[i])); // SPS 43669 } // assemble the PPSs 43670 43671 for (i = 0; i < pps.length; i++) { 43672 pictureParameterSets.push((pps[i].byteLength & 0xFF00) >>> 8); 43673 pictureParameterSets.push(pps[i].byteLength & 0xFF); 43674 pictureParameterSets = pictureParameterSets.concat(Array.prototype.slice.call(pps[i])); 43675 } 43676 avc1Box = [types.avc1, new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 43677 // reserved 43678 0x00, 0x01, 43679 // data_reference_index 43680 0x00, 0x00, 43681 // pre_defined 43682 0x00, 0x00, 43683 // reserved 43684 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 43685 // pre_defined 43686 (track.width & 0xff00) >> 8, track.width & 0xff, 43687 // width 43688 (track.height & 0xff00) >> 8, track.height & 0xff, 43689 // height 43690 0x00, 0x48, 0x00, 0x00, 43691 // horizresolution 43692 0x00, 0x48, 0x00, 0x00, 43693 // vertresolution 43694 0x00, 0x00, 0x00, 0x00, 43695 // reserved 43696 0x00, 0x01, 43697 // frame_count 43698 0x13, 0x76, 0x69, 0x64, 0x65, 0x6f, 0x6a, 0x73, 0x2d, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x69, 0x62, 0x2d, 0x68, 0x6c, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 43699 // compressorname 43700 0x00, 0x18, 43701 // depth = 24 43702 0x11, 0x11 // pre_defined = -1 43703 ]), box(types.avcC, new Uint8Array([0x01, 43704 // configurationVersion 43705 track.profileIdc, 43706 // AVCProfileIndication 43707 track.profileCompatibility, 43708 // profile_compatibility 43709 track.levelIdc, 43710 // AVCLevelIndication 43711 0xff // lengthSizeMinusOne, hard-coded to 4 bytes 43712 ].concat([sps.length], 43713 // numOfSequenceParameterSets 43714 sequenceParameterSets, 43715 // "SPS" 43716 [pps.length], 43717 // numOfPictureParameterSets 43718 pictureParameterSets // "PPS" 43719 ))), box(types.btrt, new Uint8Array([0x00, 0x1c, 0x9c, 0x80, 43720 // bufferSizeDB 43721 0x00, 0x2d, 0xc6, 0xc0, 43722 // maxBitrate 43723 0x00, 0x2d, 0xc6, 0xc0 // avgBitrate 43724 ]))]; 43725 43726 if (track.sarRatio) { 43727 var hSpacing = track.sarRatio[0], 43728 vSpacing = track.sarRatio[1]; 43729 avc1Box.push(box(types.pasp, new Uint8Array([(hSpacing & 0xFF000000) >> 24, (hSpacing & 0xFF0000) >> 16, (hSpacing & 0xFF00) >> 8, hSpacing & 0xFF, (vSpacing & 0xFF000000) >> 24, (vSpacing & 0xFF0000) >> 16, (vSpacing & 0xFF00) >> 8, vSpacing & 0xFF]))); 43730 } 43731 return box.apply(null, avc1Box); 43732 }; 43733 audioSample = function (track) { 43734 return box(types.mp4a, new Uint8Array([ 43735 // SampleEntry, ISO/IEC 14496-12 43736 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 43737 // reserved 43738 0x00, 0x01, 43739 // data_reference_index 43740 // AudioSampleEntry, ISO/IEC 14496-12 43741 0x00, 0x00, 0x00, 0x00, 43742 // reserved 43743 0x00, 0x00, 0x00, 0x00, 43744 // reserved 43745 (track.channelcount & 0xff00) >> 8, track.channelcount & 0xff, 43746 // channelcount 43747 (track.samplesize & 0xff00) >> 8, track.samplesize & 0xff, 43748 // samplesize 43749 0x00, 0x00, 43750 // pre_defined 43751 0x00, 0x00, 43752 // reserved 43753 (track.samplerate & 0xff00) >> 8, track.samplerate & 0xff, 0x00, 0x00 // samplerate, 16.16 43754 // MP4AudioSampleEntry, ISO/IEC 14496-14 43755 ]), esds(track)); 43756 }; 43757 })(); 43758 tkhd = function (track) { 43759 var result = new Uint8Array([0x00, 43760 // version 0 43761 0x00, 0x00, 0x07, 43762 // flags 43763 0x00, 0x00, 0x00, 0x00, 43764 // creation_time 43765 0x00, 0x00, 0x00, 0x00, 43766 // modification_time 43767 (track.id & 0xFF000000) >> 24, (track.id & 0xFF0000) >> 16, (track.id & 0xFF00) >> 8, track.id & 0xFF, 43768 // track_ID 43769 0x00, 0x00, 0x00, 0x00, 43770 // reserved 43771 (track.duration & 0xFF000000) >> 24, (track.duration & 0xFF0000) >> 16, (track.duration & 0xFF00) >> 8, track.duration & 0xFF, 43772 // duration 43773 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 43774 // reserved 43775 0x00, 0x00, 43776 // layer 43777 0x00, 0x00, 43778 // alternate_group 43779 0x01, 0x00, 43780 // non-audio track volume 43781 0x00, 0x00, 43782 // reserved 43783 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 43784 // transformation: unity matrix 43785 (track.width & 0xFF00) >> 8, track.width & 0xFF, 0x00, 0x00, 43786 // width 43787 (track.height & 0xFF00) >> 8, track.height & 0xFF, 0x00, 0x00 // height 43788 ]); 43789 43790 return box(types.tkhd, result); 43791 }; 43792 /** 43793 * Generate a track fragment (traf) box. A traf box collects metadata 43794 * about tracks in a movie fragment (moof) box. 43795 */ 43796 43797 traf = function (track) { 43798 var trackFragmentHeader, trackFragmentDecodeTime, trackFragmentRun, sampleDependencyTable, dataOffset, upperWordBaseMediaDecodeTime, lowerWordBaseMediaDecodeTime; 43799 trackFragmentHeader = box(types.tfhd, new Uint8Array([0x00, 43800 // version 0 43801 0x00, 0x00, 0x3a, 43802 // flags 43803 (track.id & 0xFF000000) >> 24, (track.id & 0xFF0000) >> 16, (track.id & 0xFF00) >> 8, track.id & 0xFF, 43804 // track_ID 43805 0x00, 0x00, 0x00, 0x01, 43806 // sample_description_index 43807 0x00, 0x00, 0x00, 0x00, 43808 // default_sample_duration 43809 0x00, 0x00, 0x00, 0x00, 43810 // default_sample_size 43811 0x00, 0x00, 0x00, 0x00 // default_sample_flags 43812 ])); 43813 43814 upperWordBaseMediaDecodeTime = Math.floor(track.baseMediaDecodeTime / MAX_UINT32); 43815 lowerWordBaseMediaDecodeTime = Math.floor(track.baseMediaDecodeTime % MAX_UINT32); 43816 trackFragmentDecodeTime = box(types.tfdt, new Uint8Array([0x01, 43817 // version 1 43818 0x00, 0x00, 0x00, 43819 // flags 43820 // baseMediaDecodeTime 43821 upperWordBaseMediaDecodeTime >>> 24 & 0xFF, upperWordBaseMediaDecodeTime >>> 16 & 0xFF, upperWordBaseMediaDecodeTime >>> 8 & 0xFF, upperWordBaseMediaDecodeTime & 0xFF, lowerWordBaseMediaDecodeTime >>> 24 & 0xFF, lowerWordBaseMediaDecodeTime >>> 16 & 0xFF, lowerWordBaseMediaDecodeTime >>> 8 & 0xFF, lowerWordBaseMediaDecodeTime & 0xFF])); // the data offset specifies the number of bytes from the start of 43822 // the containing moof to the first payload byte of the associated 43823 // mdat 43824 43825 dataOffset = 32 + 43826 // tfhd 43827 20 + 43828 // tfdt 43829 8 + 43830 // traf header 43831 16 + 43832 // mfhd 43833 8 + 43834 // moof header 43835 8; // mdat header 43836 // audio tracks require less metadata 43837 43838 if (track.type === 'audio') { 43839 trackFragmentRun = trun$1(track, dataOffset); 43840 return box(types.traf, trackFragmentHeader, trackFragmentDecodeTime, trackFragmentRun); 43841 } // video tracks should contain an independent and disposable samples 43842 // box (sdtp) 43843 // generate one and adjust offsets to match 43844 43845 sampleDependencyTable = sdtp(track); 43846 trackFragmentRun = trun$1(track, sampleDependencyTable.length + dataOffset); 43847 return box(types.traf, trackFragmentHeader, trackFragmentDecodeTime, trackFragmentRun, sampleDependencyTable); 43848 }; 43849 /** 43850 * Generate a track box. 43851 * @param track {object} a track definition 43852 * @return {Uint8Array} the track box 43853 */ 43854 43855 trak = function (track) { 43856 track.duration = track.duration || 0xffffffff; 43857 return box(types.trak, tkhd(track), mdia(track)); 43858 }; 43859 trex = function (track) { 43860 var result = new Uint8Array([0x00, 43861 // version 0 43862 0x00, 0x00, 0x00, 43863 // flags 43864 (track.id & 0xFF000000) >> 24, (track.id & 0xFF0000) >> 16, (track.id & 0xFF00) >> 8, track.id & 0xFF, 43865 // track_ID 43866 0x00, 0x00, 0x00, 0x01, 43867 // default_sample_description_index 43868 0x00, 0x00, 0x00, 0x00, 43869 // default_sample_duration 43870 0x00, 0x00, 0x00, 0x00, 43871 // default_sample_size 43872 0x00, 0x01, 0x00, 0x01 // default_sample_flags 43873 ]); // the last two bytes of default_sample_flags is the sample 43874 // degradation priority, a hint about the importance of this sample 43875 // relative to others. Lower the degradation priority for all sample 43876 // types other than video. 43877 43878 if (track.type !== 'video') { 43879 result[result.length - 1] = 0x00; 43880 } 43881 return box(types.trex, result); 43882 }; 43883 (function () { 43884 var audioTrun, videoTrun, trunHeader; // This method assumes all samples are uniform. That is, if a 43885 // duration is present for the first sample, it will be present for 43886 // all subsequent samples. 43887 // see ISO/IEC 14496-12:2012, Section 8.8.8.1 43888 43889 trunHeader = function (samples, offset) { 43890 var durationPresent = 0, 43891 sizePresent = 0, 43892 flagsPresent = 0, 43893 compositionTimeOffset = 0; // trun flag constants 43894 43895 if (samples.length) { 43896 if (samples[0].duration !== undefined) { 43897 durationPresent = 0x1; 43898 } 43899 if (samples[0].size !== undefined) { 43900 sizePresent = 0x2; 43901 } 43902 if (samples[0].flags !== undefined) { 43903 flagsPresent = 0x4; 43904 } 43905 if (samples[0].compositionTimeOffset !== undefined) { 43906 compositionTimeOffset = 0x8; 43907 } 43908 } 43909 return [0x00, 43910 // version 0 43911 0x00, durationPresent | sizePresent | flagsPresent | compositionTimeOffset, 0x01, 43912 // flags 43913 (samples.length & 0xFF000000) >>> 24, (samples.length & 0xFF0000) >>> 16, (samples.length & 0xFF00) >>> 8, samples.length & 0xFF, 43914 // sample_count 43915 (offset & 0xFF000000) >>> 24, (offset & 0xFF0000) >>> 16, (offset & 0xFF00) >>> 8, offset & 0xFF // data_offset 43916 ]; 43917 }; 43918 43919 videoTrun = function (track, offset) { 43920 var bytesOffest, bytes, header, samples, sample, i; 43921 samples = track.samples || []; 43922 offset += 8 + 12 + 16 * samples.length; 43923 header = trunHeader(samples, offset); 43924 bytes = new Uint8Array(header.length + samples.length * 16); 43925 bytes.set(header); 43926 bytesOffest = header.length; 43927 for (i = 0; i < samples.length; i++) { 43928 sample = samples[i]; 43929 bytes[bytesOffest++] = (sample.duration & 0xFF000000) >>> 24; 43930 bytes[bytesOffest++] = (sample.duration & 0xFF0000) >>> 16; 43931 bytes[bytesOffest++] = (sample.duration & 0xFF00) >>> 8; 43932 bytes[bytesOffest++] = sample.duration & 0xFF; // sample_duration 43933 43934 bytes[bytesOffest++] = (sample.size & 0xFF000000) >>> 24; 43935 bytes[bytesOffest++] = (sample.size & 0xFF0000) >>> 16; 43936 bytes[bytesOffest++] = (sample.size & 0xFF00) >>> 8; 43937 bytes[bytesOffest++] = sample.size & 0xFF; // sample_size 43938 43939 bytes[bytesOffest++] = sample.flags.isLeading << 2 | sample.flags.dependsOn; 43940 bytes[bytesOffest++] = sample.flags.isDependedOn << 6 | sample.flags.hasRedundancy << 4 | sample.flags.paddingValue << 1 | sample.flags.isNonSyncSample; 43941 bytes[bytesOffest++] = sample.flags.degradationPriority & 0xF0 << 8; 43942 bytes[bytesOffest++] = sample.flags.degradationPriority & 0x0F; // sample_flags 43943 43944 bytes[bytesOffest++] = (sample.compositionTimeOffset & 0xFF000000) >>> 24; 43945 bytes[bytesOffest++] = (sample.compositionTimeOffset & 0xFF0000) >>> 16; 43946 bytes[bytesOffest++] = (sample.compositionTimeOffset & 0xFF00) >>> 8; 43947 bytes[bytesOffest++] = sample.compositionTimeOffset & 0xFF; // sample_composition_time_offset 43948 } 43949 43950 return box(types.trun, bytes); 43951 }; 43952 audioTrun = function (track, offset) { 43953 var bytes, bytesOffest, header, samples, sample, i; 43954 samples = track.samples || []; 43955 offset += 8 + 12 + 8 * samples.length; 43956 header = trunHeader(samples, offset); 43957 bytes = new Uint8Array(header.length + samples.length * 8); 43958 bytes.set(header); 43959 bytesOffest = header.length; 43960 for (i = 0; i < samples.length; i++) { 43961 sample = samples[i]; 43962 bytes[bytesOffest++] = (sample.duration & 0xFF000000) >>> 24; 43963 bytes[bytesOffest++] = (sample.duration & 0xFF0000) >>> 16; 43964 bytes[bytesOffest++] = (sample.duration & 0xFF00) >>> 8; 43965 bytes[bytesOffest++] = sample.duration & 0xFF; // sample_duration 43966 43967 bytes[bytesOffest++] = (sample.size & 0xFF000000) >>> 24; 43968 bytes[bytesOffest++] = (sample.size & 0xFF0000) >>> 16; 43969 bytes[bytesOffest++] = (sample.size & 0xFF00) >>> 8; 43970 bytes[bytesOffest++] = sample.size & 0xFF; // sample_size 43971 }
vendor: 18,520 bytes, lines 43972-44470
43972 43973 return box(types.trun, bytes); 43974 }; 43975 trun$1 = function (track, offset) { 43976 if (track.type === 'audio') { 43977 return audioTrun(track, offset); 43978 } 43979 return videoTrun(track, offset); 43980 }; 43981 })(); 43982 var mp4Generator = { 43983 ftyp: ftyp, 43984 mdat: mdat, 43985 moof: moof, 43986 moov: moov, 43987 initSegment: function (tracks) { 43988 var fileType = ftyp(), 43989 movie = moov(tracks), 43990 result; 43991 result = new Uint8Array(fileType.byteLength + movie.byteLength); 43992 result.set(fileType); 43993 result.set(movie, fileType.byteLength); 43994 return result; 43995 } 43996 }; 43997 /** 43998 * mux.js 43999 * 44000 * Copyright (c) Brightcove 44001 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 44002 */ 44003 // composed of the nal units that make up that frame 44004 // Also keep track of cummulative data about the frame from the nal units such 44005 // as the frame duration, starting pts, etc. 44006 44007 var groupNalsIntoFrames = function (nalUnits) { 44008 var i, 44009 currentNal, 44010 currentFrame = [], 44011 frames = []; // TODO added for LHLS, make sure this is OK 44012 44013 frames.byteLength = 0; 44014 frames.nalCount = 0; 44015 frames.duration = 0; 44016 currentFrame.byteLength = 0; 44017 for (i = 0; i < nalUnits.length; i++) { 44018 currentNal = nalUnits[i]; // Split on 'aud'-type nal units 44019 44020 if (currentNal.nalUnitType === 'access_unit_delimiter_rbsp') { 44021 // Since the very first nal unit is expected to be an AUD 44022 // only push to the frames array when currentFrame is not empty 44023 if (currentFrame.length) { 44024 currentFrame.duration = currentNal.dts - currentFrame.dts; // TODO added for LHLS, make sure this is OK 44025 44026 frames.byteLength += currentFrame.byteLength; 44027 frames.nalCount += currentFrame.length; 44028 frames.duration += currentFrame.duration; 44029 frames.push(currentFrame); 44030 } 44031 currentFrame = [currentNal]; 44032 currentFrame.byteLength = currentNal.data.byteLength; 44033 currentFrame.pts = currentNal.pts; 44034 currentFrame.dts = currentNal.dts; 44035 } else { 44036 // Specifically flag key frames for ease of use later 44037 if (currentNal.nalUnitType === 'slice_layer_without_partitioning_rbsp_idr') { 44038 currentFrame.keyFrame = true; 44039 } 44040 currentFrame.duration = currentNal.dts - currentFrame.dts; 44041 currentFrame.byteLength += currentNal.data.byteLength; 44042 currentFrame.push(currentNal); 44043 } 44044 } // For the last frame, use the duration of the previous frame if we 44045 // have nothing better to go on 44046 44047 if (frames.length && (!currentFrame.duration || currentFrame.duration <= 0)) { 44048 currentFrame.duration = frames[frames.length - 1].duration; 44049 } // Push the final frame 44050 // TODO added for LHLS, make sure this is OK 44051 44052 frames.byteLength += currentFrame.byteLength; 44053 frames.nalCount += currentFrame.length; 44054 frames.duration += currentFrame.duration; 44055 frames.push(currentFrame); 44056 return frames; 44057 }; // Convert an array of frames into an array of Gop with each Gop being composed 44058 // of the frames that make up that Gop 44059 // Also keep track of cummulative data about the Gop from the frames such as the 44060 // Gop duration, starting pts, etc. 44061 44062 var groupFramesIntoGops = function (frames) { 44063 var i, 44064 currentFrame, 44065 currentGop = [], 44066 gops = []; // We must pre-set some of the values on the Gop since we 44067 // keep running totals of these values 44068 44069 currentGop.byteLength = 0; 44070 currentGop.nalCount = 0; 44071 currentGop.duration = 0; 44072 currentGop.pts = frames[0].pts; 44073 currentGop.dts = frames[0].dts; // store some metadata about all the Gops 44074 44075 gops.byteLength = 0; 44076 gops.nalCount = 0; 44077 gops.duration = 0; 44078 gops.pts = frames[0].pts; 44079 gops.dts = frames[0].dts; 44080 for (i = 0; i < frames.length; i++) { 44081 currentFrame = frames[i]; 44082 if (currentFrame.keyFrame) { 44083 // Since the very first frame is expected to be an keyframe 44084 // only push to the gops array when currentGop is not empty 44085 if (currentGop.length) { 44086 gops.push(currentGop); 44087 gops.byteLength += currentGop.byteLength; 44088 gops.nalCount += currentGop.nalCount; 44089 gops.duration += currentGop.duration; 44090 } 44091 currentGop = [currentFrame]; 44092 currentGop.nalCount = currentFrame.length; 44093 currentGop.byteLength = currentFrame.byteLength; 44094 currentGop.pts = currentFrame.pts; 44095 currentGop.dts = currentFrame.dts; 44096 currentGop.duration = currentFrame.duration; 44097 } else { 44098 currentGop.duration += currentFrame.duration; 44099 currentGop.nalCount += currentFrame.length; 44100 currentGop.byteLength += currentFrame.byteLength; 44101 currentGop.push(currentFrame); 44102 } 44103 } 44104 if (gops.length && currentGop.duration <= 0) { 44105 currentGop.duration = gops[gops.length - 1].duration; 44106 } 44107 gops.byteLength += currentGop.byteLength; 44108 gops.nalCount += currentGop.nalCount; 44109 gops.duration += currentGop.duration; // push the final Gop 44110 44111 gops.push(currentGop); 44112 return gops; 44113 }; 44114 /* 44115 * Search for the first keyframe in the GOPs and throw away all frames 44116 * until that keyframe. Then extend the duration of the pulled keyframe 44117 * and pull the PTS and DTS of the keyframe so that it covers the time 44118 * range of the frames that were disposed. 44119 * 44120 * @param {Array} gops video GOPs 44121 * @returns {Array} modified video GOPs 44122 */ 44123 44124 var extendFirstKeyFrame = function (gops) { 44125 var currentGop; 44126 if (!gops[0][0].keyFrame && gops.length > 1) { 44127 // Remove the first GOP 44128 currentGop = gops.shift(); 44129 gops.byteLength -= currentGop.byteLength; 44130 gops.nalCount -= currentGop.nalCount; // Extend the first frame of what is now the 44131 // first gop to cover the time period of the 44132 // frames we just removed 44133 44134 gops[0][0].dts = currentGop.dts; 44135 gops[0][0].pts = currentGop.pts; 44136 gops[0][0].duration += currentGop.duration; 44137 } 44138 return gops; 44139 }; 44140 /** 44141 * Default sample object 44142 * see ISO/IEC 14496-12:2012, section 8.6.4.3 44143 */ 44144 44145 var createDefaultSample = function () { 44146 return { 44147 size: 0, 44148 flags: { 44149 isLeading: 0, 44150 dependsOn: 1, 44151 isDependedOn: 0, 44152 hasRedundancy: 0, 44153 degradationPriority: 0, 44154 isNonSyncSample: 1 44155 } 44156 }; 44157 }; 44158 /* 44159 * Collates information from a video frame into an object for eventual 44160 * entry into an MP4 sample table. 44161 * 44162 * @param {Object} frame the video frame 44163 * @param {Number} dataOffset the byte offset to position the sample 44164 * @return {Object} object containing sample table info for a frame 44165 */ 44166 44167 var sampleForFrame = function (frame, dataOffset) { 44168 var sample = createDefaultSample(); 44169 sample.dataOffset = dataOffset; 44170 sample.compositionTimeOffset = frame.pts - frame.dts; 44171 sample.duration = frame.duration; 44172 sample.size = 4 * frame.length; // Space for nal unit size 44173 44174 sample.size += frame.byteLength; 44175 if (frame.keyFrame) { 44176 sample.flags.dependsOn = 2; 44177 sample.flags.isNonSyncSample = 0; 44178 } 44179 return sample; 44180 }; // generate the track's sample table from an array of gops 44181 44182 var generateSampleTable$1 = function (gops, baseDataOffset) { 44183 var h, 44184 i, 44185 sample, 44186 currentGop, 44187 currentFrame, 44188 dataOffset = baseDataOffset || 0, 44189 samples = []; 44190 for (h = 0; h < gops.length; h++) { 44191 currentGop = gops[h]; 44192 for (i = 0; i < currentGop.length; i++) { 44193 currentFrame = currentGop[i]; 44194 sample = sampleForFrame(currentFrame, dataOffset); 44195 dataOffset += sample.size; 44196 samples.push(sample); 44197 } 44198 } 44199 return samples; 44200 }; // generate the track's raw mdat data from an array of gops 44201 44202 var concatenateNalData = function (gops) { 44203 var h, 44204 i, 44205 j, 44206 currentGop, 44207 currentFrame, 44208 currentNal, 44209 dataOffset = 0, 44210 nalsByteLength = gops.byteLength, 44211 numberOfNals = gops.nalCount, 44212 totalByteLength = nalsByteLength + 4 * numberOfNals, 44213 data = new Uint8Array(totalByteLength), 44214 view = new DataView(data.buffer); // For each Gop.. 44215 44216 for (h = 0; h < gops.length; h++) { 44217 currentGop = gops[h]; // For each Frame.. 44218 44219 for (i = 0; i < currentGop.length; i++) { 44220 currentFrame = currentGop[i]; // For each NAL.. 44221 44222 for (j = 0; j < currentFrame.length; j++) { 44223 currentNal = currentFrame[j]; 44224 view.setUint32(dataOffset, currentNal.data.byteLength); 44225 dataOffset += 4; 44226 data.set(currentNal.data, dataOffset); 44227 dataOffset += currentNal.data.byteLength; 44228 } 44229 } 44230 } 44231 return data; 44232 }; // generate the track's sample table from a frame 44233 44234 var generateSampleTableForFrame = function (frame, baseDataOffset) { 44235 var sample, 44236 dataOffset = baseDataOffset || 0, 44237 samples = []; 44238 sample = sampleForFrame(frame, dataOffset); 44239 samples.push(sample); 44240 return samples; 44241 }; // generate the track's raw mdat data from a frame 44242 44243 var concatenateNalDataForFrame = function (frame) { 44244 var i, 44245 currentNal, 44246 dataOffset = 0, 44247 nalsByteLength = frame.byteLength, 44248 numberOfNals = frame.length, 44249 totalByteLength = nalsByteLength + 4 * numberOfNals, 44250 data = new Uint8Array(totalByteLength), 44251 view = new DataView(data.buffer); // For each NAL.. 44252 44253 for (i = 0; i < frame.length; i++) { 44254 currentNal = frame[i]; 44255 view.setUint32(dataOffset, currentNal.data.byteLength); 44256 dataOffset += 4; 44257 data.set(currentNal.data, dataOffset); 44258 dataOffset += currentNal.data.byteLength; 44259 } 44260 return data; 44261 }; 44262 var frameUtils$1 = { 44263 groupNalsIntoFrames: groupNalsIntoFrames, 44264 groupFramesIntoGops: groupFramesIntoGops, 44265 extendFirstKeyFrame: extendFirstKeyFrame, 44266 generateSampleTable: generateSampleTable$1, 44267 concatenateNalData: concatenateNalData, 44268 generateSampleTableForFrame: generateSampleTableForFrame, 44269 concatenateNalDataForFrame: concatenateNalDataForFrame 44270 }; 44271 /** 44272 * mux.js 44273 * 44274 * Copyright (c) Brightcove 44275 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 44276 */ 44277 44278 var highPrefix = [33, 16, 5, 32, 164, 27]; 44279 var lowPrefix = [33, 65, 108, 84, 1, 2, 4, 8, 168, 2, 4, 8, 17, 191, 252]; 44280 var zeroFill = function (count) { 44281 var a = []; 44282 while (count--) { 44283 a.push(0); 44284 } 44285 return a; 44286 }; 44287 var makeTable = function (metaTable) { 44288 return Object.keys(metaTable).reduce(function (obj, key) { 44289 obj[key] = new Uint8Array(metaTable[key].reduce(function (arr, part) { 44290 return arr.concat(part); 44291 }, [])); 44292 return obj; 44293 }, {}); 44294 }; 44295 var silence; 44296 var silence_1 = function () { 44297 if (!silence) { 44298 // Frames-of-silence to use for filling in missing AAC frames 44299 var coneOfSilence = { 44300 96000: [highPrefix, [227, 64], zeroFill(154), [56]], 44301 88200: [highPrefix, [231], zeroFill(170), [56]], 44302 64000: [highPrefix, [248, 192], zeroFill(240), [56]], 44303 48000: [highPrefix, [255, 192], zeroFill(268), [55, 148, 128], zeroFill(54), [112]], 44304 44100: [highPrefix, [255, 192], zeroFill(268), [55, 163, 128], zeroFill(84), [112]], 44305 32000: [highPrefix, [255, 192], zeroFill(268), [55, 234], zeroFill(226), [112]], 44306 24000: [highPrefix, [255, 192], zeroFill(268), [55, 255, 128], zeroFill(268), [111, 112], zeroFill(126), [224]], 44307 16000: [highPrefix, [255, 192], zeroFill(268), [55, 255, 128], zeroFill(268), [111, 255], zeroFill(269), [223, 108], zeroFill(195), [1, 192]], 44308 12000: [lowPrefix, zeroFill(268), [3, 127, 248], zeroFill(268), [6, 255, 240], zeroFill(268), [13, 255, 224], zeroFill(268), [27, 253, 128], zeroFill(259), [56]], 44309 11025: [lowPrefix, zeroFill(268), [3, 127, 248], zeroFill(268), [6, 255, 240], zeroFill(268), [13, 255, 224], zeroFill(268), [27, 255, 192], zeroFill(268), [55, 175, 128], zeroFill(108), [112]], 44310 8000: [lowPrefix, zeroFill(268), [3, 121, 16], zeroFill(47), [7]] 44311 }; 44312 silence = makeTable(coneOfSilence); 44313 } 44314 return silence; 44315 }; 44316 /** 44317 * mux.js 44318 * 44319 * Copyright (c) Brightcove 44320 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 44321 */ 44322 44323 var ONE_SECOND_IN_TS$4 = 90000, 44324 // 90kHz clock 44325 secondsToVideoTs, 44326 secondsToAudioTs, 44327 videoTsToSeconds, 44328 audioTsToSeconds, 44329 audioTsToVideoTs, 44330 videoTsToAudioTs, 44331 metadataTsToSeconds; 44332 secondsToVideoTs = function (seconds) { 44333 return seconds * ONE_SECOND_IN_TS$4; 44334 }; 44335 secondsToAudioTs = function (seconds, sampleRate) { 44336 return seconds * sampleRate; 44337 }; 44338 videoTsToSeconds = function (timestamp) { 44339 return timestamp / ONE_SECOND_IN_TS$4; 44340 }; 44341 audioTsToSeconds = function (timestamp, sampleRate) { 44342 return timestamp / sampleRate; 44343 }; 44344 audioTsToVideoTs = function (timestamp, sampleRate) { 44345 return secondsToVideoTs(audioTsToSeconds(timestamp, sampleRate)); 44346 }; 44347 videoTsToAudioTs = function (timestamp, sampleRate) { 44348 return secondsToAudioTs(videoTsToSeconds(timestamp), sampleRate); 44349 }; 44350 /** 44351 * Adjust ID3 tag or caption timing information by the timeline pts values 44352 * (if keepOriginalTimestamps is false) and convert to seconds 44353 */ 44354 44355 metadataTsToSeconds = function (timestamp, timelineStartPts, keepOriginalTimestamps) { 44356 return videoTsToSeconds(keepOriginalTimestamps ? timestamp : timestamp - timelineStartPts); 44357 }; 44358 var clock$2 = { 44359 ONE_SECOND_IN_TS: ONE_SECOND_IN_TS$4, 44360 secondsToVideoTs: secondsToVideoTs, 44361 secondsToAudioTs: secondsToAudioTs, 44362 videoTsToSeconds: videoTsToSeconds, 44363 audioTsToSeconds: audioTsToSeconds, 44364 audioTsToVideoTs: audioTsToVideoTs, 44365 videoTsToAudioTs: videoTsToAudioTs, 44366 metadataTsToSeconds: metadataTsToSeconds 44367 }; 44368 /** 44369 * mux.js 44370 * 44371 * Copyright (c) Brightcove 44372 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 44373 */ 44374 44375 var coneOfSilence = silence_1; 44376 var clock$1 = clock$2; 44377 /** 44378 * Sum the `byteLength` properties of the data in each AAC frame 44379 */ 44380 44381 var sumFrameByteLengths = function (array) { 44382 var i, 44383 currentObj, 44384 sum = 0; // sum the byteLength's all each nal unit in the frame 44385 44386 for (i = 0; i < array.length; i++) { 44387 currentObj = array[i]; 44388 sum += currentObj.data.byteLength; 44389 } 44390 return sum; 44391 }; // Possibly pad (prefix) the audio track with silence if appending this track 44392 // would lead to the introduction of a gap in the audio buffer 44393 44394 var prefixWithSilence = function (track, frames, audioAppendStartTs, videoBaseMediaDecodeTime) { 44395 var baseMediaDecodeTimeTs, 44396 frameDuration = 0, 44397 audioGapDuration = 0, 44398 audioFillFrameCount = 0, 44399 audioFillDuration = 0, 44400 silentFrame, 44401 i, 44402 firstFrame; 44403 if (!frames.length) { 44404 return; 44405 } 44406 baseMediaDecodeTimeTs = clock$1.audioTsToVideoTs(track.baseMediaDecodeTime, track.samplerate); // determine frame clock duration based on sample rate, round up to avoid overfills 44407 44408 frameDuration = Math.ceil(clock$1.ONE_SECOND_IN_TS / (track.samplerate / 1024)); 44409 if (audioAppendStartTs && videoBaseMediaDecodeTime) { 44410 // insert the shortest possible amount (audio gap or audio to video gap) 44411 audioGapDuration = baseMediaDecodeTimeTs - Math.max(audioAppendStartTs, videoBaseMediaDecodeTime); // number of full frames in the audio gap 44412 44413 audioFillFrameCount = Math.floor(audioGapDuration / frameDuration); 44414 audioFillDuration = audioFillFrameCount * frameDuration; 44415 } // don't attempt to fill gaps smaller than a single frame or larger 44416 // than a half second 44417 44418 if (audioFillFrameCount < 1 || audioFillDuration > clock$1.ONE_SECOND_IN_TS / 2) { 44419 return; 44420 } 44421 silentFrame = coneOfSilence()[track.samplerate]; 44422 if (!silentFrame) { 44423 // we don't have a silent frame pregenerated for the sample rate, so use a frame 44424 // from the content instead 44425 silentFrame = frames[0].data; 44426 } 44427 for (i = 0; i < audioFillFrameCount; i++) { 44428 firstFrame = frames[0]; 44429 frames.splice(0, 0, { 44430 data: silentFrame, 44431 dts: firstFrame.dts - frameDuration, 44432 pts: firstFrame.pts - frameDuration 44433 }); 44434 } 44435 track.baseMediaDecodeTime -= Math.floor(clock$1.videoTsToAudioTs(audioFillDuration, track.samplerate)); 44436 return audioFillDuration; 44437 }; // If the audio segment extends before the earliest allowed dts 44438 // value, remove AAC frames until starts at or after the earliest 44439 // allowed DTS so that we don't end up with a negative baseMedia- 44440 // DecodeTime for the audio track 44441 44442 var trimAdtsFramesByEarliestDts = function (adtsFrames, track, earliestAllowedDts) { 44443 if (track.minSegmentDts >= earliestAllowedDts) { 44444 return adtsFrames; 44445 } // We will need to recalculate the earliest segment Dts 44446 44447 track.minSegmentDts = Infinity; 44448 return adtsFrames.filter(function (currentFrame) { 44449 // If this is an allowed frame, keep it and record it's Dts 44450 if (currentFrame.dts >= earliestAllowedDts) { 44451 track.minSegmentDts = Math.min(track.minSegmentDts, currentFrame.dts); 44452 track.minSegmentPts = track.minSegmentDts; 44453 return true; 44454 } // Otherwise, discard it 44455 44456 return false; 44457 }); 44458 }; // generate the track's raw mdat data from an array of frames 44459 44460 var generateSampleTable = function (frames) { 44461 var i, 44462 currentFrame, 44463 samples = []; 44464 for (i = 0; i < frames.length; i++) { 44465 currentFrame = frames[i]; 44466 samples.push({ 44467 size: currentFrame.data.byteLength, 44468 duration: 1024 // For AAC audio, all samples contain 1024 samples 44469 }); 44470 }
vendor: 11,395 bytes, lines 44471-44790
44471 44472 return samples; 44473 }; // generate the track's sample table from an array of frames 44474 44475 var concatenateFrameData = function (frames) { 44476 var i, 44477 currentFrame, 44478 dataOffset = 0, 44479 data = new Uint8Array(sumFrameByteLengths(frames)); 44480 for (i = 0; i < frames.length; i++) { 44481 currentFrame = frames[i]; 44482 data.set(currentFrame.data, dataOffset); 44483 dataOffset += currentFrame.data.byteLength; 44484 } 44485 return data; 44486 }; 44487 var audioFrameUtils$1 = { 44488 prefixWithSilence: prefixWithSilence, 44489 trimAdtsFramesByEarliestDts: trimAdtsFramesByEarliestDts, 44490 generateSampleTable: generateSampleTable, 44491 concatenateFrameData: concatenateFrameData 44492 }; 44493 /** 44494 * mux.js 44495 * 44496 * Copyright (c) Brightcove 44497 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 44498 */ 44499 44500 var ONE_SECOND_IN_TS$3 = clock$2.ONE_SECOND_IN_TS; 44501 /** 44502 * Store information about the start and end of the track and the 44503 * duration for each frame/sample we process in order to calculate 44504 * the baseMediaDecodeTime 44505 */ 44506 44507 var collectDtsInfo = function (track, data) { 44508 if (typeof data.pts === 'number') { 44509 if (track.timelineStartInfo.pts === undefined) { 44510 track.timelineStartInfo.pts = data.pts; 44511 } 44512 if (track.minSegmentPts === undefined) { 44513 track.minSegmentPts = data.pts; 44514 } else { 44515 track.minSegmentPts = Math.min(track.minSegmentPts, data.pts); 44516 } 44517 if (track.maxSegmentPts === undefined) { 44518 track.maxSegmentPts = data.pts; 44519 } else { 44520 track.maxSegmentPts = Math.max(track.maxSegmentPts, data.pts); 44521 } 44522 } 44523 if (typeof data.dts === 'number') { 44524 if (track.timelineStartInfo.dts === undefined) { 44525 track.timelineStartInfo.dts = data.dts; 44526 } 44527 if (track.minSegmentDts === undefined) { 44528 track.minSegmentDts = data.dts; 44529 } else { 44530 track.minSegmentDts = Math.min(track.minSegmentDts, data.dts); 44531 } 44532 if (track.maxSegmentDts === undefined) { 44533 track.maxSegmentDts = data.dts; 44534 } else { 44535 track.maxSegmentDts = Math.max(track.maxSegmentDts, data.dts); 44536 } 44537 } 44538 }; 44539 /** 44540 * Clear values used to calculate the baseMediaDecodeTime between 44541 * tracks 44542 */ 44543 44544 var clearDtsInfo = function (track) { 44545 delete track.minSegmentDts; 44546 delete track.maxSegmentDts; 44547 delete track.minSegmentPts; 44548 delete track.maxSegmentPts; 44549 }; 44550 /** 44551 * Calculate the track's baseMediaDecodeTime based on the earliest 44552 * DTS the transmuxer has ever seen and the minimum DTS for the 44553 * current track 44554 * @param track {object} track metadata configuration 44555 * @param keepOriginalTimestamps {boolean} If true, keep the timestamps 44556 * in the source; false to adjust the first segment to start at 0. 44557 */ 44558 44559 var calculateTrackBaseMediaDecodeTime = function (track, keepOriginalTimestamps) { 44560 var baseMediaDecodeTime, 44561 scale, 44562 minSegmentDts = track.minSegmentDts; // Optionally adjust the time so the first segment starts at zero. 44563 44564 if (!keepOriginalTimestamps) { 44565 minSegmentDts -= track.timelineStartInfo.dts; 44566 } // track.timelineStartInfo.baseMediaDecodeTime is the location, in time, where 44567 // we want the start of the first segment to be placed 44568 44569 baseMediaDecodeTime = track.timelineStartInfo.baseMediaDecodeTime; // Add to that the distance this segment is from the very first 44570 44571 baseMediaDecodeTime += minSegmentDts; // baseMediaDecodeTime must not become negative 44572 44573 baseMediaDecodeTime = Math.max(0, baseMediaDecodeTime); 44574 if (track.type === 'audio') { 44575 // Audio has a different clock equal to the sampling_rate so we need to 44576 // scale the PTS values into the clock rate of the track 44577 scale = track.samplerate / ONE_SECOND_IN_TS$3; 44578 baseMediaDecodeTime *= scale; 44579 baseMediaDecodeTime = Math.floor(baseMediaDecodeTime); 44580 } 44581 return baseMediaDecodeTime; 44582 }; 44583 var trackDecodeInfo$1 = { 44584 clearDtsInfo: clearDtsInfo, 44585 calculateTrackBaseMediaDecodeTime: calculateTrackBaseMediaDecodeTime, 44586 collectDtsInfo: collectDtsInfo 44587 }; 44588 /** 44589 * mux.js 44590 * 44591 * Copyright (c) Brightcove 44592 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 44593 * 44594 * Reads in-band caption information from a video elementary 44595 * stream. Captions must follow the CEA-708 standard for injection 44596 * into an MPEG-2 transport streams. 44597 * @see https://en.wikipedia.org/wiki/CEA-708 44598 * @see https://www.gpo.gov/fdsys/pkg/CFR-2007-title47-vol1/pdf/CFR-2007-title47-vol1-sec15-119.pdf 44599 */ 44600 // payload type field to indicate how they are to be 44601 // interpreted. CEAS-708 caption content is always transmitted with 44602 // payload type 0x04. 44603 44604 var USER_DATA_REGISTERED_ITU_T_T35 = 4, 44605 RBSP_TRAILING_BITS = 128; 44606 /** 44607 * Parse a supplemental enhancement information (SEI) NAL unit. 44608 * Stops parsing once a message of type ITU T T35 has been found. 44609 * 44610 * @param bytes {Uint8Array} the bytes of a SEI NAL unit 44611 * @return {object} the parsed SEI payload 44612 * @see Rec. ITU-T H.264, 7.3.2.3.1 44613 */ 44614 44615 var parseSei = function (bytes) { 44616 var i = 0, 44617 result = { 44618 payloadType: -1, 44619 payloadSize: 0 44620 }, 44621 payloadType = 0, 44622 payloadSize = 0; // go through the sei_rbsp parsing each each individual sei_message 44623 44624 while (i < bytes.byteLength) { 44625 // stop once we have hit the end of the sei_rbsp 44626 if (bytes[i] === RBSP_TRAILING_BITS) { 44627 break; 44628 } // Parse payload type 44629 44630 while (bytes[i] === 0xFF) { 44631 payloadType += 255; 44632 i++; 44633 } 44634 payloadType += bytes[i++]; // Parse payload size 44635 44636 while (bytes[i] === 0xFF) { 44637 payloadSize += 255; 44638 i++; 44639 } 44640 payloadSize += bytes[i++]; // this sei_message is a 608/708 caption so save it and break 44641 // there can only ever be one caption message in a frame's sei 44642 44643 if (!result.payload && payloadType === USER_DATA_REGISTERED_ITU_T_T35) { 44644 var userIdentifier = String.fromCharCode(bytes[i + 3], bytes[i + 4], bytes[i + 5], bytes[i + 6]); 44645 if (userIdentifier === 'GA94') { 44646 result.payloadType = payloadType; 44647 result.payloadSize = payloadSize; 44648 result.payload = bytes.subarray(i, i + payloadSize); 44649 break; 44650 } else { 44651 result.payload = void 0; 44652 } 44653 } // skip the payload and parse the next message 44654 44655 i += payloadSize; 44656 payloadType = 0; 44657 payloadSize = 0; 44658 } 44659 return result; 44660 }; // see ANSI/SCTE 128-1 (2013), section 8.1 44661 44662 var parseUserData = function (sei) { 44663 // itu_t_t35_contry_code must be 181 (United States) for 44664 // captions 44665 if (sei.payload[0] !== 181) { 44666 return null; 44667 } // itu_t_t35_provider_code should be 49 (ATSC) for captions 44668 44669 if ((sei.payload[1] << 8 | sei.payload[2]) !== 49) { 44670 return null; 44671 } // the user_identifier should be "GA94" to indicate ATSC1 data 44672 44673 if (String.fromCharCode(sei.payload[3], sei.payload[4], sei.payload[5], sei.payload[6]) !== 'GA94') { 44674 return null; 44675 } // finally, user_data_type_code should be 0x03 for caption data 44676 44677 if (sei.payload[7] !== 0x03) { 44678 return null; 44679 } // return the user_data_type_structure and strip the trailing 44680 // marker bits 44681 44682 return sei.payload.subarray(8, sei.payload.length - 1); 44683 }; // see CEA-708-D, section 4.4 44684 44685 var parseCaptionPackets = function (pts, userData) { 44686 var results = [], 44687 i, 44688 count, 44689 offset, 44690 data; // if this is just filler, return immediately 44691 44692 if (!(userData[0] & 0x40)) { 44693 return results; 44694 } // parse out the cc_data_1 and cc_data_2 fields 44695 44696 count = userData[0] & 0x1f; 44697 for (i = 0; i < count; i++) { 44698 offset = i * 3; 44699 data = { 44700 type: userData[offset + 2] & 0x03, 44701 pts: pts 44702 }; // capture cc data when cc_valid is 1 44703 44704 if (userData[offset + 2] & 0x04) { 44705 data.ccData = userData[offset + 3] << 8 | userData[offset + 4]; 44706 results.push(data); 44707 } 44708 } 44709 return results; 44710 }; 44711 var discardEmulationPreventionBytes$1 = function (data) { 44712 var length = data.byteLength, 44713 emulationPreventionBytesPositions = [], 44714 i = 1, 44715 newLength, 44716 newData; // Find all `Emulation Prevention Bytes` 44717 44718 while (i < length - 2) { 44719 if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0x03) { 44720 emulationPreventionBytesPositions.push(i + 2); 44721 i += 2; 44722 } else { 44723 i++; 44724 } 44725 } // If no Emulation Prevention Bytes were found just return the original 44726 // array 44727 44728 if (emulationPreventionBytesPositions.length === 0) { 44729 return data; 44730 } // Create a new array to hold the NAL unit data 44731 44732 newLength = length - emulationPreventionBytesPositions.length; 44733 newData = new Uint8Array(newLength); 44734 var sourceIndex = 0; 44735 for (i = 0; i < newLength; sourceIndex++, i++) { 44736 if (sourceIndex === emulationPreventionBytesPositions[0]) { 44737 // Skip this byte 44738 sourceIndex++; // Remove this position index 44739 44740 emulationPreventionBytesPositions.shift(); 44741 } 44742 newData[i] = data[sourceIndex]; 44743 } 44744 return newData; 44745 }; // exports 44746 44747 var captionPacketParser = { 44748 parseSei: parseSei, 44749 parseUserData: parseUserData, 44750 parseCaptionPackets: parseCaptionPackets, 44751 discardEmulationPreventionBytes: discardEmulationPreventionBytes$1, 44752 USER_DATA_REGISTERED_ITU_T_T35: USER_DATA_REGISTERED_ITU_T_T35 44753 }; 44754 /** 44755 * mux.js 44756 * 44757 * Copyright (c) Brightcove 44758 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 44759 * 44760 * Reads in-band caption information from a video elementary 44761 * stream. Captions must follow the CEA-708 standard for injection 44762 * into an MPEG-2 transport streams. 44763 * @see https://en.wikipedia.org/wiki/CEA-708 44764 * @see https://www.gpo.gov/fdsys/pkg/CFR-2007-title47-vol1/pdf/CFR-2007-title47-vol1-sec15-119.pdf 44765 */ 44766 // Link To Transport 44767 // ----------------- 44768 44769 var Stream$7 = stream; 44770 var cea708Parser = captionPacketParser; 44771 var CaptionStream$2 = function (options) { 44772 options = options || {}; 44773 CaptionStream$2.prototype.init.call(this); // parse708captions flag, default to true 44774 44775 this.parse708captions_ = typeof options.parse708captions === 'boolean' ? options.parse708captions : true; 44776 this.captionPackets_ = []; 44777 this.ccStreams_ = [new Cea608Stream(0, 0), 44778 // eslint-disable-line no-use-before-define 44779 new Cea608Stream(0, 1), 44780 // eslint-disable-line no-use-before-define 44781 new Cea608Stream(1, 0), 44782 // eslint-disable-line no-use-before-define 44783 new Cea608Stream(1, 1) // eslint-disable-line no-use-before-define 44784 ]; 44785 44786 if (this.parse708captions_) { 44787 this.cc708Stream_ = new Cea708Stream({ 44788 captionServices: options.captionServices 44789 }); // eslint-disable-line no-use-before-define 44790 }
vendor: 45,910 bytes, lines 44791-46178
44791 44792 this.reset(); // forward data and done events from CCs to this CaptionStream 44793 44794 this.ccStreams_.forEach(function (cc) { 44795 cc.on('data', this.trigger.bind(this, 'data')); 44796 cc.on('partialdone', this.trigger.bind(this, 'partialdone')); 44797 cc.on('done', this.trigger.bind(this, 'done')); 44798 }, this); 44799 if (this.parse708captions_) { 44800 this.cc708Stream_.on('data', this.trigger.bind(this, 'data')); 44801 this.cc708Stream_.on('partialdone', this.trigger.bind(this, 'partialdone')); 44802 this.cc708Stream_.on('done', this.trigger.bind(this, 'done')); 44803 } 44804 }; 44805 CaptionStream$2.prototype = new Stream$7(); 44806 CaptionStream$2.prototype.push = function (event) { 44807 var sei, userData, newCaptionPackets; // only examine SEI NALs 44808 44809 if (event.nalUnitType !== 'sei_rbsp') { 44810 return; 44811 } // parse the sei 44812 44813 sei = cea708Parser.parseSei(event.escapedRBSP); // no payload data, skip 44814 44815 if (!sei.payload) { 44816 return; 44817 } // ignore everything but user_data_registered_itu_t_t35 44818 44819 if (sei.payloadType !== cea708Parser.USER_DATA_REGISTERED_ITU_T_T35) { 44820 return; 44821 } // parse out the user data payload 44822 44823 userData = cea708Parser.parseUserData(sei); // ignore unrecognized userData 44824 44825 if (!userData) { 44826 return; 44827 } // Sometimes, the same segment # will be downloaded twice. To stop the 44828 // caption data from being processed twice, we track the latest dts we've 44829 // received and ignore everything with a dts before that. However, since 44830 // data for a specific dts can be split across packets on either side of 44831 // a segment boundary, we need to make sure we *don't* ignore the packets 44832 // from the *next* segment that have dts === this.latestDts_. By constantly 44833 // tracking the number of packets received with dts === this.latestDts_, we 44834 // know how many should be ignored once we start receiving duplicates. 44835 44836 if (event.dts < this.latestDts_) { 44837 // We've started getting older data, so set the flag. 44838 this.ignoreNextEqualDts_ = true; 44839 return; 44840 } else if (event.dts === this.latestDts_ && this.ignoreNextEqualDts_) { 44841 this.numSameDts_--; 44842 if (!this.numSameDts_) { 44843 // We've received the last duplicate packet, time to start processing again 44844 this.ignoreNextEqualDts_ = false; 44845 } 44846 return; 44847 } // parse out CC data packets and save them for later 44848 44849 newCaptionPackets = cea708Parser.parseCaptionPackets(event.pts, userData); 44850 this.captionPackets_ = this.captionPackets_.concat(newCaptionPackets); 44851 if (this.latestDts_ !== event.dts) { 44852 this.numSameDts_ = 0; 44853 } 44854 this.numSameDts_++; 44855 this.latestDts_ = event.dts; 44856 }; 44857 CaptionStream$2.prototype.flushCCStreams = function (flushType) { 44858 this.ccStreams_.forEach(function (cc) { 44859 return flushType === 'flush' ? cc.flush() : cc.partialFlush(); 44860 }, this); 44861 }; 44862 CaptionStream$2.prototype.flushStream = function (flushType) { 44863 // make sure we actually parsed captions before proceeding 44864 if (!this.captionPackets_.length) { 44865 this.flushCCStreams(flushType); 44866 return; 44867 } // In Chrome, the Array#sort function is not stable so add a 44868 // presortIndex that we can use to ensure we get a stable-sort 44869 44870 this.captionPackets_.forEach(function (elem, idx) { 44871 elem.presortIndex = idx; 44872 }); // sort caption byte-pairs based on their PTS values 44873 44874 this.captionPackets_.sort(function (a, b) { 44875 if (a.pts === b.pts) { 44876 return a.presortIndex - b.presortIndex; 44877 } 44878 return a.pts - b.pts; 44879 }); 44880 this.captionPackets_.forEach(function (packet) { 44881 if (packet.type < 2) { 44882 // Dispatch packet to the right Cea608Stream 44883 this.dispatchCea608Packet(packet); 44884 } else { 44885 // Dispatch packet to the Cea708Stream 44886 this.dispatchCea708Packet(packet); 44887 } 44888 }, this); 44889 this.captionPackets_.length = 0; 44890 this.flushCCStreams(flushType); 44891 }; 44892 CaptionStream$2.prototype.flush = function () { 44893 return this.flushStream('flush'); 44894 }; // Only called if handling partial data 44895 44896 CaptionStream$2.prototype.partialFlush = function () { 44897 return this.flushStream('partialFlush'); 44898 }; 44899 CaptionStream$2.prototype.reset = function () { 44900 this.latestDts_ = null; 44901 this.ignoreNextEqualDts_ = false; 44902 this.numSameDts_ = 0; 44903 this.activeCea608Channel_ = [null, null]; 44904 this.ccStreams_.forEach(function (ccStream) { 44905 ccStream.reset(); 44906 }); 44907 }; // From the CEA-608 spec: 44908 44909 /* 44910 * When XDS sub-packets are interleaved with other services, the end of each sub-packet shall be followed 44911 * by a control pair to change to a different service. When any of the control codes from 0x10 to 0x1F is 44912 * used to begin a control code pair, it indicates the return to captioning or Text data. The control code pair 44913 * and subsequent data should then be processed according to the FCC rules. It may be necessary for the 44914 * line 21 data encoder to automatically insert a control code pair (i.e. RCL, RU2, RU3, RU4, RDC, or RTD) 44915 * to switch to captioning or Text. 44916 */ 44917 // With that in mind, we ignore any data between an XDS control code and a 44918 // subsequent closed-captioning control code. 44919 44920 CaptionStream$2.prototype.dispatchCea608Packet = function (packet) { 44921 // NOTE: packet.type is the CEA608 field 44922 if (this.setsTextOrXDSActive(packet)) { 44923 this.activeCea608Channel_[packet.type] = null; 44924 } else if (this.setsChannel1Active(packet)) { 44925 this.activeCea608Channel_[packet.type] = 0; 44926 } else if (this.setsChannel2Active(packet)) { 44927 this.activeCea608Channel_[packet.type] = 1; 44928 } 44929 if (this.activeCea608Channel_[packet.type] === null) { 44930 // If we haven't received anything to set the active channel, or the 44931 // packets are Text/XDS data, discard the data; we don't want jumbled 44932 // captions 44933 return; 44934 } 44935 this.ccStreams_[(packet.type << 1) + this.activeCea608Channel_[packet.type]].push(packet); 44936 }; 44937 CaptionStream$2.prototype.setsChannel1Active = function (packet) { 44938 return (packet.ccData & 0x7800) === 0x1000; 44939 }; 44940 CaptionStream$2.prototype.setsChannel2Active = function (packet) { 44941 return (packet.ccData & 0x7800) === 0x1800; 44942 }; 44943 CaptionStream$2.prototype.setsTextOrXDSActive = function (packet) { 44944 return (packet.ccData & 0x7100) === 0x0100 || (packet.ccData & 0x78fe) === 0x102a || (packet.ccData & 0x78fe) === 0x182a; 44945 }; 44946 CaptionStream$2.prototype.dispatchCea708Packet = function (packet) { 44947 if (this.parse708captions_) { 44948 this.cc708Stream_.push(packet); 44949 } 44950 }; // ---------------------- 44951 // Session to Application 44952 // ---------------------- 44953 // This hash maps special and extended character codes to their 44954 // proper Unicode equivalent. The first one-byte key is just a 44955 // non-standard character code. The two-byte keys that follow are 44956 // the extended CEA708 character codes, along with the preceding 44957 // 0x10 extended character byte to distinguish these codes from 44958 // non-extended character codes. Every CEA708 character code that 44959 // is not in this object maps directly to a standard unicode 44960 // character code. 44961 // The transparent space and non-breaking transparent space are 44962 // technically not fully supported since there is no code to 44963 // make them transparent, so they have normal non-transparent 44964 // stand-ins. 44965 // The special closed caption (CC) character isn't a standard 44966 // unicode character, so a fairly similar unicode character was 44967 // chosen in it's place. 44968 44969 var CHARACTER_TRANSLATION_708 = { 44970 0x7f: 0x266a, 44971 // ⪠44972 0x1020: 0x20, 44973 // Transparent Space 44974 0x1021: 0xa0, 44975 // Nob-breaking Transparent Space 44976 0x1025: 0x2026, 44977 // ⦠44978 0x102a: 0x0160, 44979 // Å 44980 0x102c: 0x0152, 44981 // Å 44982 0x1030: 0x2588, 44983 // â 44984 0x1031: 0x2018, 44985 // â 44986 0x1032: 0x2019, 44987 // â 44988 0x1033: 0x201c, 44989 // â 44990 0x1034: 0x201d, 44991 // â 44992 0x1035: 0x2022, 44993 // ⢠44994 0x1039: 0x2122, 44995 // ⢠44996 0x103a: 0x0161, 44997 // Å¡ 44998 0x103c: 0x0153, 44999 // Å 45000 0x103d: 0x2120, 45001 // â 45002 0x103f: 0x0178, 45003 // Ÿ 45004 0x1076: 0x215b, 45005 // â 45006 0x1077: 0x215c, 45007 // â 45008 0x1078: 0x215d, 45009 // â 45010 0x1079: 0x215e, 45011 // â 45012 0x107a: 0x23d0, 45013 // â 45014 0x107b: 0x23a4, 45015 // ⤠45016 0x107c: 0x23a3, 45017 // ⣠45018 0x107d: 0x23af, 45019 // ⯠45020 0x107e: 0x23a6, 45021 // ⦠45022 0x107f: 0x23a1, 45023 // â¡ 45024 0x10a0: 0x3138 // 㸠(CC char) 45025 }; 45026 45027 var get708CharFromCode = function (code) { 45028 var newCode = CHARACTER_TRANSLATION_708[code] || code; 45029 if (code & 0x1000 && code === newCode) { 45030 // Invalid extended code 45031 return ''; 45032 } 45033 return String.fromCharCode(newCode); 45034 }; 45035 var within708TextBlock = function (b) { 45036 return 0x20 <= b && b <= 0x7f || 0xa0 <= b && b <= 0xff; 45037 }; 45038 var Cea708Window = function (windowNum) { 45039 this.windowNum = windowNum; 45040 this.reset(); 45041 }; 45042 Cea708Window.prototype.reset = function () { 45043 this.clearText(); 45044 this.pendingNewLine = false; 45045 this.winAttr = {}; 45046 this.penAttr = {}; 45047 this.penLoc = {}; 45048 this.penColor = {}; // These default values are arbitrary, 45049 // defineWindow will usually override them 45050 45051 this.visible = 0; 45052 this.rowLock = 0; 45053 this.columnLock = 0; 45054 this.priority = 0; 45055 this.relativePositioning = 0; 45056 this.anchorVertical = 0; 45057 this.anchorHorizontal = 0; 45058 this.anchorPoint = 0; 45059 this.rowCount = 1; 45060 this.virtualRowCount = this.rowCount + 1; 45061 this.columnCount = 41; 45062 this.windowStyle = 0; 45063 this.penStyle = 0; 45064 }; 45065 Cea708Window.prototype.getText = function () { 45066 return this.rows.join('\n'); 45067 }; 45068 Cea708Window.prototype.clearText = function () { 45069 this.rows = ['']; 45070 this.rowIdx = 0; 45071 }; 45072 Cea708Window.prototype.newLine = function (pts) { 45073 if (this.rows.length >= this.virtualRowCount && typeof this.beforeRowOverflow === 'function') { 45074 this.beforeRowOverflow(pts); 45075 } 45076 if (this.rows.length > 0) { 45077 this.rows.push(''); 45078 this.rowIdx++; 45079 } // Show all virtual rows since there's no visible scrolling 45080 45081 while (this.rows.length > this.virtualRowCount) { 45082 this.rows.shift(); 45083 this.rowIdx--; 45084 } 45085 }; 45086 Cea708Window.prototype.isEmpty = function () { 45087 if (this.rows.length === 0) { 45088 return true; 45089 } else if (this.rows.length === 1) { 45090 return this.rows[0] === ''; 45091 } 45092 return false; 45093 }; 45094 Cea708Window.prototype.addText = function (text) { 45095 this.rows[this.rowIdx] += text; 45096 }; 45097 Cea708Window.prototype.backspace = function () { 45098 if (!this.isEmpty()) { 45099 var row = this.rows[this.rowIdx]; 45100 this.rows[this.rowIdx] = row.substr(0, row.length - 1); 45101 } 45102 }; 45103 var Cea708Service = function (serviceNum, encoding, stream) { 45104 this.serviceNum = serviceNum; 45105 this.text = ''; 45106 this.currentWindow = new Cea708Window(-1); 45107 this.windows = []; 45108 this.stream = stream; // Try to setup a TextDecoder if an `encoding` value was provided 45109 45110 if (typeof encoding === 'string') { 45111 this.createTextDecoder(encoding); 45112 } 45113 }; 45114 /** 45115 * Initialize service windows 45116 * Must be run before service use 45117 * 45118 * @param {Integer} pts PTS value 45119 * @param {Function} beforeRowOverflow Function to execute before row overflow of a window 45120 */ 45121 45122 Cea708Service.prototype.init = function (pts, beforeRowOverflow) { 45123 this.startPts = pts; 45124 for (var win = 0; win < 8; win++) { 45125 this.windows[win] = new Cea708Window(win); 45126 if (typeof beforeRowOverflow === 'function') { 45127 this.windows[win].beforeRowOverflow = beforeRowOverflow; 45128 } 45129 } 45130 }; 45131 /** 45132 * Set current window of service to be affected by commands 45133 * 45134 * @param {Integer} windowNum Window number 45135 */ 45136 45137 Cea708Service.prototype.setCurrentWindow = function (windowNum) { 45138 this.currentWindow = this.windows[windowNum]; 45139 }; 45140 /** 45141 * Try to create a TextDecoder if it is natively supported 45142 */ 45143 45144 Cea708Service.prototype.createTextDecoder = function (encoding) { 45145 if (typeof TextDecoder === 'undefined') { 45146 this.stream.trigger('log', { 45147 level: 'warn', 45148 message: 'The `encoding` option is unsupported without TextDecoder support' 45149 }); 45150 } else { 45151 try { 45152 this.textDecoder_ = new TextDecoder(encoding); 45153 } catch (error) { 45154 this.stream.trigger('log', { 45155 level: 'warn', 45156 message: 'TextDecoder could not be created with ' + encoding + ' encoding. ' + error 45157 }); 45158 } 45159 } 45160 }; 45161 var Cea708Stream = function (options) { 45162 options = options || {}; 45163 Cea708Stream.prototype.init.call(this); 45164 var self = this; 45165 var captionServices = options.captionServices || {}; 45166 var captionServiceEncodings = {}; 45167 var serviceProps; // Get service encodings from captionServices option block 45168 45169 Object.keys(captionServices).forEach(serviceName => { 45170 serviceProps = captionServices[serviceName]; 45171 if (/^SERVICE/.test(serviceName)) { 45172 captionServiceEncodings[serviceName] = serviceProps.encoding; 45173 } 45174 }); 45175 this.serviceEncodings = captionServiceEncodings; 45176 this.current708Packet = null; 45177 this.services = {}; 45178 this.push = function (packet) { 45179 if (packet.type === 3) { 45180 // 708 packet start 45181 self.new708Packet(); 45182 self.add708Bytes(packet); 45183 } else { 45184 if (self.current708Packet === null) { 45185 // This should only happen at the start of a file if there's no packet start. 45186 self.new708Packet(); 45187 } 45188 self.add708Bytes(packet); 45189 } 45190 }; 45191 }; 45192 Cea708Stream.prototype = new Stream$7(); 45193 /** 45194 * Push current 708 packet, create new 708 packet. 45195 */ 45196 45197 Cea708Stream.prototype.new708Packet = function () { 45198 if (this.current708Packet !== null) { 45199 this.push708Packet(); 45200 } 45201 this.current708Packet = { 45202 data: [], 45203 ptsVals: [] 45204 }; 45205 }; 45206 /** 45207 * Add pts and both bytes from packet into current 708 packet. 45208 */ 45209 45210 Cea708Stream.prototype.add708Bytes = function (packet) { 45211 var data = packet.ccData; 45212 var byte0 = data >>> 8; 45213 var byte1 = data & 0xff; // I would just keep a list of packets instead of bytes, but it isn't clear in the spec 45214 // that service blocks will always line up with byte pairs. 45215 45216 this.current708Packet.ptsVals.push(packet.pts); 45217 this.current708Packet.data.push(byte0); 45218 this.current708Packet.data.push(byte1); 45219 }; 45220 /** 45221 * Parse completed 708 packet into service blocks and push each service block. 45222 */ 45223 45224 Cea708Stream.prototype.push708Packet = function () { 45225 var packet708 = this.current708Packet; 45226 var packetData = packet708.data; 45227 var serviceNum = null; 45228 var blockSize = null; 45229 var i = 0; 45230 var b = packetData[i++]; 45231 packet708.seq = b >> 6; 45232 packet708.sizeCode = b & 0x3f; // 0b00111111; 45233 45234 for (; i < packetData.length; i++) { 45235 b = packetData[i++]; 45236 serviceNum = b >> 5; 45237 blockSize = b & 0x1f; // 0b00011111 45238 45239 if (serviceNum === 7 && blockSize > 0) { 45240 // Extended service num 45241 b = packetData[i++]; 45242 serviceNum = b; 45243 } 45244 this.pushServiceBlock(serviceNum, i, blockSize); 45245 if (blockSize > 0) { 45246 i += blockSize - 1; 45247 } 45248 } 45249 }; 45250 /** 45251 * Parse service block, execute commands, read text. 45252 * 45253 * Note: While many of these commands serve important purposes, 45254 * many others just parse out the parameters or attributes, but 45255 * nothing is done with them because this is not a full and complete 45256 * implementation of the entire 708 spec. 45257 * 45258 * @param {Integer} serviceNum Service number 45259 * @param {Integer} start Start index of the 708 packet data 45260 * @param {Integer} size Block size 45261 */ 45262 45263 Cea708Stream.prototype.pushServiceBlock = function (serviceNum, start, size) { 45264 var b; 45265 var i = start; 45266 var packetData = this.current708Packet.data; 45267 var service = this.services[serviceNum]; 45268 if (!service) { 45269 service = this.initService(serviceNum, i); 45270 } 45271 for (; i < start + size && i < packetData.length; i++) { 45272 b = packetData[i]; 45273 if (within708TextBlock(b)) { 45274 i = this.handleText(i, service); 45275 } else if (b === 0x18) { 45276 i = this.multiByteCharacter(i, service); 45277 } else if (b === 0x10) { 45278 i = this.extendedCommands(i, service); 45279 } else if (0x80 <= b && b <= 0x87) { 45280 i = this.setCurrentWindow(i, service); 45281 } else if (0x98 <= b && b <= 0x9f) { 45282 i = this.defineWindow(i, service); 45283 } else if (b === 0x88) { 45284 i = this.clearWindows(i, service); 45285 } else if (b === 0x8c) { 45286 i = this.deleteWindows(i, service); 45287 } else if (b === 0x89) { 45288 i = this.displayWindows(i, service); 45289 } else if (b === 0x8a) { 45290 i = this.hideWindows(i, service); 45291 } else if (b === 0x8b) { 45292 i = this.toggleWindows(i, service); 45293 } else if (b === 0x97) { 45294 i = this.setWindowAttributes(i, service); 45295 } else if (b === 0x90) { 45296 i = this.setPenAttributes(i, service); 45297 } else if (b === 0x91) { 45298 i = this.setPenColor(i, service); 45299 } else if (b === 0x92) { 45300 i = this.setPenLocation(i, service); 45301 } else if (b === 0x8f) { 45302 service = this.reset(i, service); 45303 } else if (b === 0x08) { 45304 // BS: Backspace 45305 service.currentWindow.backspace(); 45306 } else if (b === 0x0c) { 45307 // FF: Form feed 45308 service.currentWindow.clearText(); 45309 } else if (b === 0x0d) { 45310 // CR: Carriage return 45311 service.currentWindow.pendingNewLine = true; 45312 } else if (b === 0x0e) { 45313 // HCR: Horizontal carriage return 45314 service.currentWindow.clearText(); 45315 } else if (b === 0x8d) { 45316 // DLY: Delay, nothing to do 45317 i++; 45318 } else ; 45319 } 45320 }; 45321 /** 45322 * Execute an extended command 45323 * 45324 * @param {Integer} i Current index in the 708 packet 45325 * @param {Service} service The service object to be affected 45326 * @return {Integer} New index after parsing 45327 */ 45328 45329 Cea708Stream.prototype.extendedCommands = function (i, service) { 45330 var packetData = this.current708Packet.data; 45331 var b = packetData[++i]; 45332 if (within708TextBlock(b)) { 45333 i = this.handleText(i, service, { 45334 isExtended: true 45335 }); 45336 } 45337 return i; 45338 }; 45339 /** 45340 * Get PTS value of a given byte index 45341 * 45342 * @param {Integer} byteIndex Index of the byte 45343 * @return {Integer} PTS 45344 */ 45345 45346 Cea708Stream.prototype.getPts = function (byteIndex) { 45347 // There's 1 pts value per 2 bytes 45348 return this.current708Packet.ptsVals[Math.floor(byteIndex / 2)]; 45349 }; 45350 /** 45351 * Initializes a service 45352 * 45353 * @param {Integer} serviceNum Service number 45354 * @return {Service} Initialized service object 45355 */ 45356 45357 Cea708Stream.prototype.initService = function (serviceNum, i) { 45358 var serviceName = 'SERVICE' + serviceNum; 45359 var self = this; 45360 var serviceName; 45361 var encoding; 45362 if (serviceName in this.serviceEncodings) { 45363 encoding = this.serviceEncodings[serviceName]; 45364 } 45365 this.services[serviceNum] = new Cea708Service(serviceNum, encoding, self); 45366 this.services[serviceNum].init(this.getPts(i), function (pts) { 45367 self.flushDisplayed(pts, self.services[serviceNum]); 45368 }); 45369 return this.services[serviceNum]; 45370 }; 45371 /** 45372 * Execute text writing to current window 45373 * 45374 * @param {Integer} i Current index in the 708 packet 45375 * @param {Service} service The service object to be affected 45376 * @return {Integer} New index after parsing 45377 */ 45378 45379 Cea708Stream.prototype.handleText = function (i, service, options) { 45380 var isExtended = options && options.isExtended; 45381 var isMultiByte = options && options.isMultiByte; 45382 var packetData = this.current708Packet.data; 45383 var extended = isExtended ? 0x1000 : 0x0000; 45384 var currentByte = packetData[i]; 45385 var nextByte = packetData[i + 1]; 45386 var win = service.currentWindow; 45387 var char; 45388 var charCodeArray; // Converts an array of bytes to a unicode hex string. 45389 45390 function toHexString(byteArray) { 45391 return byteArray.map(byte => { 45392 return ('0' + (byte & 0xFF).toString(16)).slice(-2); 45393 }).join(''); 45394 } 45395 if (isMultiByte) { 45396 charCodeArray = [currentByte, nextByte]; 45397 i++; 45398 } else { 45399 charCodeArray = [currentByte]; 45400 } // Use the TextDecoder if one was created for this service 45401 45402 if (service.textDecoder_ && !isExtended) { 45403 char = service.textDecoder_.decode(new Uint8Array(charCodeArray)); 45404 } else { 45405 // We assume any multi-byte char without a decoder is unicode. 45406 if (isMultiByte) { 45407 const unicode = toHexString(charCodeArray); // Takes a unicode hex string and creates a single character. 45408 45409 char = String.fromCharCode(parseInt(unicode, 16)); 45410 } else { 45411 char = get708CharFromCode(extended | currentByte); 45412 } 45413 } 45414 if (win.pendingNewLine && !win.isEmpty()) { 45415 win.newLine(this.getPts(i)); 45416 } 45417 win.pendingNewLine = false; 45418 win.addText(char); 45419 return i; 45420 }; 45421 /** 45422 * Handle decoding of multibyte character 45423 * 45424 * @param {Integer} i Current index in the 708 packet 45425 * @param {Service} service The service object to be affected 45426 * @return {Integer} New index after parsing 45427 */ 45428 45429 Cea708Stream.prototype.multiByteCharacter = function (i, service) { 45430 var packetData = this.current708Packet.data; 45431 var firstByte = packetData[i + 1]; 45432 var secondByte = packetData[i + 2]; 45433 if (within708TextBlock(firstByte) && within708TextBlock(secondByte)) { 45434 i = this.handleText(++i, service, { 45435 isMultiByte: true 45436 }); 45437 } 45438 return i; 45439 }; 45440 /** 45441 * Parse and execute the CW# command. 45442 * 45443 * Set the current window. 45444 * 45445 * @param {Integer} i Current index in the 708 packet 45446 * @param {Service} service The service object to be affected 45447 * @return {Integer} New index after parsing 45448 */ 45449 45450 Cea708Stream.prototype.setCurrentWindow = function (i, service) { 45451 var packetData = this.current708Packet.data; 45452 var b = packetData[i]; 45453 var windowNum = b & 0x07; 45454 service.setCurrentWindow(windowNum); 45455 return i; 45456 }; 45457 /** 45458 * Parse and execute the DF# command. 45459 * 45460 * Define a window and set it as the current window. 45461 * 45462 * @param {Integer} i Current index in the 708 packet 45463 * @param {Service} service The service object to be affected 45464 * @return {Integer} New index after parsing 45465 */ 45466 45467 Cea708Stream.prototype.defineWindow = function (i, service) { 45468 var packetData = this.current708Packet.data; 45469 var b = packetData[i]; 45470 var windowNum = b & 0x07; 45471 service.setCurrentWindow(windowNum); 45472 var win = service.currentWindow; 45473 b = packetData[++i]; 45474 win.visible = (b & 0x20) >> 5; // v 45475 45476 win.rowLock = (b & 0x10) >> 4; // rl 45477 45478 win.columnLock = (b & 0x08) >> 3; // cl 45479 45480 win.priority = b & 0x07; // p 45481 45482 b = packetData[++i]; 45483 win.relativePositioning = (b & 0x80) >> 7; // rp 45484 45485 win.anchorVertical = b & 0x7f; // av 45486 45487 b = packetData[++i]; 45488 win.anchorHorizontal = b; // ah 45489 45490 b = packetData[++i]; 45491 win.anchorPoint = (b & 0xf0) >> 4; // ap 45492 45493 win.rowCount = b & 0x0f; // rc 45494 45495 b = packetData[++i]; 45496 win.columnCount = b & 0x3f; // cc 45497 45498 b = packetData[++i]; 45499 win.windowStyle = (b & 0x38) >> 3; // ws 45500 45501 win.penStyle = b & 0x07; // ps 45502 // The spec says there are (rowCount+1) "virtual rows" 45503 45504 win.virtualRowCount = win.rowCount + 1; 45505 return i; 45506 }; 45507 /** 45508 * Parse and execute the SWA command. 45509 * 45510 * Set attributes of the current window. 45511 * 45512 * @param {Integer} i Current index in the 708 packet 45513 * @param {Service} service The service object to be affected 45514 * @return {Integer} New index after parsing 45515 */ 45516 45517 Cea708Stream.prototype.setWindowAttributes = function (i, service) { 45518 var packetData = this.current708Packet.data; 45519 var b = packetData[i]; 45520 var winAttr = service.currentWindow.winAttr; 45521 b = packetData[++i]; 45522 winAttr.fillOpacity = (b & 0xc0) >> 6; // fo 45523 45524 winAttr.fillRed = (b & 0x30) >> 4; // fr 45525 45526 winAttr.fillGreen = (b & 0x0c) >> 2; // fg 45527 45528 winAttr.fillBlue = b & 0x03; // fb 45529 45530 b = packetData[++i]; 45531 winAttr.borderType = (b & 0xc0) >> 6; // bt 45532 45533 winAttr.borderRed = (b & 0x30) >> 4; // br 45534 45535 winAttr.borderGreen = (b & 0x0c) >> 2; // bg 45536 45537 winAttr.borderBlue = b & 0x03; // bb 45538 45539 b = packetData[++i]; 45540 winAttr.borderType += (b & 0x80) >> 5; // bt 45541 45542 winAttr.wordWrap = (b & 0x40) >> 6; // ww 45543 45544 winAttr.printDirection = (b & 0x30) >> 4; // pd 45545 45546 winAttr.scrollDirection = (b & 0x0c) >> 2; // sd 45547 45548 winAttr.justify = b & 0x03; // j 45549 45550 b = packetData[++i]; 45551 winAttr.effectSpeed = (b & 0xf0) >> 4; // es 45552 45553 winAttr.effectDirection = (b & 0x0c) >> 2; // ed 45554 45555 winAttr.displayEffect = b & 0x03; // de 45556 45557 return i; 45558 }; 45559 /** 45560 * Gather text from all displayed windows and push a caption to output. 45561 * 45562 * @param {Integer} i Current index in the 708 packet 45563 * @param {Service} service The service object to be affected 45564 */ 45565 45566 Cea708Stream.prototype.flushDisplayed = function (pts, service) { 45567 var displayedText = []; // TODO: Positioning not supported, displaying multiple windows will not necessarily 45568 // display text in the correct order, but sample files so far have not shown any issue. 45569 45570 for (var winId = 0; winId < 8; winId++) { 45571 if (service.windows[winId].visible && !service.windows[winId].isEmpty()) { 45572 displayedText.push(service.windows[winId].getText()); 45573 } 45574 } 45575 service.endPts = pts; 45576 service.text = displayedText.join('\n\n'); 45577 this.pushCaption(service); 45578 service.startPts = pts; 45579 }; 45580 /** 45581 * Push a caption to output if the caption contains text. 45582 * 45583 * @param {Service} service The service object to be affected 45584 */ 45585 45586 Cea708Stream.prototype.pushCaption = function (service) { 45587 if (service.text !== '') { 45588 this.trigger('data', { 45589 startPts: service.startPts, 45590 endPts: service.endPts, 45591 text: service.text, 45592 stream: 'cc708_' + service.serviceNum 45593 }); 45594 service.text = ''; 45595 service.startPts = service.endPts; 45596 } 45597 }; 45598 /** 45599 * Parse and execute the DSW command. 45600 * 45601 * Set visible property of windows based on the parsed bitmask. 45602 * 45603 * @param {Integer} i Current index in the 708 packet 45604 * @param {Service} service The service object to be affected 45605 * @return {Integer} New index after parsing 45606 */ 45607 45608 Cea708Stream.prototype.displayWindows = function (i, service) { 45609 var packetData = this.current708Packet.data; 45610 var b = packetData[++i]; 45611 var pts = this.getPts(i); 45612 this.flushDisplayed(pts, service); 45613 for (var winId = 0; winId < 8; winId++) { 45614 if (b & 0x01 << winId) { 45615 service.windows[winId].visible = 1; 45616 } 45617 } 45618 return i; 45619 }; 45620 /** 45621 * Parse and execute the HDW command. 45622 * 45623 * Set visible property of windows based on the parsed bitmask. 45624 * 45625 * @param {Integer} i Current index in the 708 packet 45626 * @param {Service} service The service object to be affected 45627 * @return {Integer} New index after parsing 45628 */ 45629 45630 Cea708Stream.prototype.hideWindows = function (i, service) { 45631 var packetData = this.current708Packet.data; 45632 var b = packetData[++i]; 45633 var pts = this.getPts(i); 45634 this.flushDisplayed(pts, service); 45635 for (var winId = 0; winId < 8; winId++) { 45636 if (b & 0x01 << winId) { 45637 service.windows[winId].visible = 0; 45638 } 45639 } 45640 return i; 45641 }; 45642 /** 45643 * Parse and execute the TGW command. 45644 * 45645 * Set visible property of windows based on the parsed bitmask. 45646 * 45647 * @param {Integer} i Current index in the 708 packet 45648 * @param {Service} service The service object to be affected 45649 * @return {Integer} New index after parsing 45650 */ 45651 45652 Cea708Stream.prototype.toggleWindows = function (i, service) { 45653 var packetData = this.current708Packet.data; 45654 var b = packetData[++i]; 45655 var pts = this.getPts(i); 45656 this.flushDisplayed(pts, service); 45657 for (var winId = 0; winId < 8; winId++) { 45658 if (b & 0x01 << winId) { 45659 service.windows[winId].visible ^= 1; 45660 } 45661 } 45662 return i; 45663 }; 45664 /** 45665 * Parse and execute the CLW command. 45666 * 45667 * Clear text of windows based on the parsed bitmask. 45668 * 45669 * @param {Integer} i Current index in the 708 packet 45670 * @param {Service} service The service object to be affected 45671 * @return {Integer} New index after parsing 45672 */ 45673 45674 Cea708Stream.prototype.clearWindows = function (i, service) { 45675 var packetData = this.current708Packet.data; 45676 var b = packetData[++i]; 45677 var pts = this.getPts(i); 45678 this.flushDisplayed(pts, service); 45679 for (var winId = 0; winId < 8; winId++) { 45680 if (b & 0x01 << winId) { 45681 service.windows[winId].clearText(); 45682 } 45683 } 45684 return i; 45685 }; 45686 /** 45687 * Parse and execute the DLW command. 45688 * 45689 * Re-initialize windows based on the parsed bitmask. 45690 * 45691 * @param {Integer} i Current index in the 708 packet 45692 * @param {Service} service The service object to be affected 45693 * @return {Integer} New index after parsing 45694 */ 45695 45696 Cea708Stream.prototype.deleteWindows = function (i, service) { 45697 var packetData = this.current708Packet.data; 45698 var b = packetData[++i]; 45699 var pts = this.getPts(i); 45700 this.flushDisplayed(pts, service); 45701 for (var winId = 0; winId < 8; winId++) { 45702 if (b & 0x01 << winId) { 45703 service.windows[winId].reset(); 45704 } 45705 } 45706 return i; 45707 }; 45708 /** 45709 * Parse and execute the SPA command. 45710 * 45711 * Set pen attributes of the current window. 45712 * 45713 * @param {Integer} i Current index in the 708 packet 45714 * @param {Service} service The service object to be affected 45715 * @return {Integer} New index after parsing 45716 */ 45717 45718 Cea708Stream.prototype.setPenAttributes = function (i, service) { 45719 var packetData = this.current708Packet.data; 45720 var b = packetData[i]; 45721 var penAttr = service.currentWindow.penAttr; 45722 b = packetData[++i]; 45723 penAttr.textTag = (b & 0xf0) >> 4; // tt 45724 45725 penAttr.offset = (b & 0x0c) >> 2; // o 45726 45727 penAttr.penSize = b & 0x03; // s 45728 45729 b = packetData[++i]; 45730 penAttr.italics = (b & 0x80) >> 7; // i 45731 45732 penAttr.underline = (b & 0x40) >> 6; // u 45733 45734 penAttr.edgeType = (b & 0x38) >> 3; // et 45735 45736 penAttr.fontStyle = b & 0x07; // fs 45737 45738 return i; 45739 }; 45740 /** 45741 * Parse and execute the SPC command. 45742 * 45743 * Set pen color of the current window. 45744 * 45745 * @param {Integer} i Current index in the 708 packet 45746 * @param {Service} service The service object to be affected 45747 * @return {Integer} New index after parsing 45748 */ 45749 45750 Cea708Stream.prototype.setPenColor = function (i, service) { 45751 var packetData = this.current708Packet.data; 45752 var b = packetData[i]; 45753 var penColor = service.currentWindow.penColor; 45754 b = packetData[++i]; 45755 penColor.fgOpacity = (b & 0xc0) >> 6; // fo 45756 45757 penColor.fgRed = (b & 0x30) >> 4; // fr 45758 45759 penColor.fgGreen = (b & 0x0c) >> 2; // fg 45760 45761 penColor.fgBlue = b & 0x03; // fb 45762 45763 b = packetData[++i]; 45764 penColor.bgOpacity = (b & 0xc0) >> 6; // bo 45765 45766 penColor.bgRed = (b & 0x30) >> 4; // br 45767 45768 penColor.bgGreen = (b & 0x0c) >> 2; // bg 45769 45770 penColor.bgBlue = b & 0x03; // bb 45771 45772 b = packetData[++i]; 45773 penColor.edgeRed = (b & 0x30) >> 4; // er 45774 45775 penColor.edgeGreen = (b & 0x0c) >> 2; // eg 45776 45777 penColor.edgeBlue = b & 0x03; // eb 45778 45779 return i; 45780 }; 45781 /** 45782 * Parse and execute the SPL command. 45783 * 45784 * Set pen location of the current window. 45785 * 45786 * @param {Integer} i Current index in the 708 packet 45787 * @param {Service} service The service object to be affected 45788 * @return {Integer} New index after parsing 45789 */ 45790 45791 Cea708Stream.prototype.setPenLocation = function (i, service) { 45792 var packetData = this.current708Packet.data; 45793 var b = packetData[i]; 45794 var penLoc = service.currentWindow.penLoc; // Positioning isn't really supported at the moment, so this essentially just inserts a linebreak 45795 45796 service.currentWindow.pendingNewLine = true; 45797 b = packetData[++i]; 45798 penLoc.row = b & 0x0f; // r 45799 45800 b = packetData[++i]; 45801 penLoc.column = b & 0x3f; // c 45802 45803 return i; 45804 }; 45805 /** 45806 * Execute the RST command. 45807 * 45808 * Reset service to a clean slate. Re-initialize. 45809 * 45810 * @param {Integer} i Current index in the 708 packet 45811 * @param {Service} service The service object to be affected 45812 * @return {Service} Re-initialized service 45813 */ 45814 45815 Cea708Stream.prototype.reset = function (i, service) { 45816 var pts = this.getPts(i); 45817 this.flushDisplayed(pts, service); 45818 return this.initService(service.serviceNum, i); 45819 }; // This hash maps non-ASCII, special, and extended character codes to their 45820 // proper Unicode equivalent. The first keys that are only a single byte 45821 // are the non-standard ASCII characters, which simply map the CEA608 byte 45822 // to the standard ASCII/Unicode. The two-byte keys that follow are the CEA608 45823 // character codes, but have their MSB bitmasked with 0x03 so that a lookup 45824 // can be performed regardless of the field and data channel on which the 45825 // character code was received. 45826 45827 var CHARACTER_TRANSLATION = { 45828 0x2a: 0xe1, 45829 // á 45830 0x5c: 0xe9, 45831 // é 45832 0x5e: 0xed, 45833 // à 45834 0x5f: 0xf3, 45835 // ó 45836 0x60: 0xfa, 45837 // ú 45838 0x7b: 0xe7, 45839 // ç 45840 0x7c: 0xf7, 45841 // ÷ 45842 0x7d: 0xd1, 45843 // à 45844 0x7e: 0xf1, 45845 // ñ 45846 0x7f: 0x2588, 45847 // â 45848 0x0130: 0xae, 45849 // ® 45850 0x0131: 0xb0, 45851 // ° 45852 0x0132: 0xbd, 45853 // ½ 45854 0x0133: 0xbf, 45855 // ¿ 45856 0x0134: 0x2122, 45857 // ⢠45858 0x0135: 0xa2, 45859 // ¢ 45860 0x0136: 0xa3, 45861 // £ 45862 0x0137: 0x266a, 45863 // ⪠45864 0x0138: 0xe0, 45865 // à 45866 0x0139: 0xa0, 45867 // 45868 0x013a: 0xe8, 45869 // è 45870 0x013b: 0xe2, 45871 // â 45872 0x013c: 0xea, 45873 // ê 45874 0x013d: 0xee, 45875 // î 45876 0x013e: 0xf4, 45877 // ô 45878 0x013f: 0xfb, 45879 // û 45880 0x0220: 0xc1, 45881 // à 45882 0x0221: 0xc9, 45883 // à 45884 0x0222: 0xd3, 45885 // à 45886 0x0223: 0xda, 45887 // à 45888 0x0224: 0xdc, 45889 // à 45890 0x0225: 0xfc, 45891 // ü 45892 0x0226: 0x2018, 45893 // â 45894 0x0227: 0xa1, 45895 // ¡ 45896 0x0228: 0x2a, 45897 // * 45898 0x0229: 0x27, 45899 // ' 45900 0x022a: 0x2014, 45901 // â 45902 0x022b: 0xa9, 45903 // © 45904 0x022c: 0x2120, 45905 // â 45906 0x022d: 0x2022, 45907 // ⢠45908 0x022e: 0x201c, 45909 // â 45910 0x022f: 0x201d, 45911 // â 45912 0x0230: 0xc0, 45913 // à 45914 0x0231: 0xc2, 45915 // à 45916 0x0232: 0xc7, 45917 // à 45918 0x0233: 0xc8, 45919 // à 45920 0x0234: 0xca, 45921 // à 45922 0x0235: 0xcb, 45923 // à 45924 0x0236: 0xeb, 45925 // ë 45926 0x0237: 0xce, 45927 // à 45928 0x0238: 0xcf, 45929 // à 45930 0x0239: 0xef, 45931 // ï 45932 0x023a: 0xd4, 45933 // à 45934 0x023b: 0xd9, 45935 // à 45936 0x023c: 0xf9, 45937 // ù 45938 0x023d: 0xdb, 45939 // à 45940 0x023e: 0xab, 45941 // « 45942 0x023f: 0xbb, 45943 // » 45944 0x0320: 0xc3, 45945 // à 45946 0x0321: 0xe3, 45947 // ã 45948 0x0322: 0xcd, 45949 // à 45950 0x0323: 0xcc, 45951 // à 45952 0x0324: 0xec, 45953 // ì 45954 0x0325: 0xd2, 45955 // à 45956 0x0326: 0xf2, 45957 // ò 45958 0x0327: 0xd5, 45959 // à 45960 0x0328: 0xf5, 45961 // õ 45962 0x0329: 0x7b, 45963 // { 45964 0x032a: 0x7d, 45965 // } 45966 0x032b: 0x5c, 45967 // \ 45968 0x032c: 0x5e, 45969 // ^ 45970 0x032d: 0x5f, 45971 // _ 45972 0x032e: 0x7c, 45973 // | 45974 0x032f: 0x7e, 45975 // ~ 45976 0x0330: 0xc4, 45977 // à 45978 0x0331: 0xe4, 45979 // ä 45980 0x0332: 0xd6, 45981 // à 45982 0x0333: 0xf6, 45983 // ö 45984 0x0334: 0xdf, 45985 // à 45986 0x0335: 0xa5, 45987 // Â¥ 45988 0x0336: 0xa4, 45989 // ¤ 45990 0x0337: 0x2502, 45991 // â 45992 0x0338: 0xc5, 45993 // à 45994 0x0339: 0xe5, 45995 // Ã¥ 45996 0x033a: 0xd8, 45997 // à 45998 0x033b: 0xf8, 45999 // ø 46000 0x033c: 0x250c, 46001 // â 46002 0x033d: 0x2510, 46003 // â 46004 0x033e: 0x2514, 46005 // â 46006 0x033f: 0x2518 // â 46007 }; 46008 46009 var getCharFromCode = function (code) { 46010 if (code === null) { 46011 return ''; 46012 } 46013 code = CHARACTER_TRANSLATION[code] || code; 46014 return String.fromCharCode(code); 46015 }; // the index of the last row in a CEA-608 display buffer 46016 46017 var BOTTOM_ROW = 14; // This array is used for mapping PACs -> row #, since there's no way of 46018 // getting it through bit logic. 46019 46020 var ROWS = [0x1100, 0x1120, 0x1200, 0x1220, 0x1500, 0x1520, 0x1600, 0x1620, 0x1700, 0x1720, 0x1000, 0x1300, 0x1320, 0x1400, 0x1420]; // CEA-608 captions are rendered onto a 34x15 matrix of character 46021 // cells. The "bottom" row is the last element in the outer array. 46022 // We keep track of positioning information as we go by storing the 46023 // number of indentations and the tab offset in this buffer. 46024 46025 var createDisplayBuffer = function () { 46026 var result = [], 46027 i = BOTTOM_ROW + 1; 46028 while (i--) { 46029 result.push({ 46030 text: '', 46031 indent: 0, 46032 offset: 0 46033 }); 46034 } 46035 return result; 46036 }; 46037 var Cea608Stream = function (field, dataChannel) { 46038 Cea608Stream.prototype.init.call(this); 46039 this.field_ = field || 0; 46040 this.dataChannel_ = dataChannel || 0; 46041 this.name_ = 'CC' + ((this.field_ << 1 | this.dataChannel_) + 1); 46042 this.setConstants(); 46043 this.reset(); 46044 this.push = function (packet) { 46045 var data, swap, char0, char1, text; // remove the parity bits 46046 46047 data = packet.ccData & 0x7f7f; // ignore duplicate control codes; the spec demands they're sent twice 46048 46049 if (data === this.lastControlCode_) { 46050 this.lastControlCode_ = null; 46051 return; 46052 } // Store control codes 46053 46054 if ((data & 0xf000) === 0x1000) { 46055 this.lastControlCode_ = data; 46056 } else if (data !== this.PADDING_) { 46057 this.lastControlCode_ = null; 46058 } 46059 char0 = data >>> 8; 46060 char1 = data & 0xff; 46061 if (data === this.PADDING_) { 46062 return; 46063 } else if (data === this.RESUME_CAPTION_LOADING_) { 46064 this.mode_ = 'popOn'; 46065 } else if (data === this.END_OF_CAPTION_) { 46066 // If an EOC is received while in paint-on mode, the displayed caption 46067 // text should be swapped to non-displayed memory as if it was a pop-on 46068 // caption. Because of that, we should explicitly switch back to pop-on 46069 // mode 46070 this.mode_ = 'popOn'; 46071 this.clearFormatting(packet.pts); // if a caption was being displayed, it's gone now 46072 46073 this.flushDisplayed(packet.pts); // flip memory 46074 46075 swap = this.displayed_; 46076 this.displayed_ = this.nonDisplayed_; 46077 this.nonDisplayed_ = swap; // start measuring the time to display the caption 46078 46079 this.startPts_ = packet.pts; 46080 } else if (data === this.ROLL_UP_2_ROWS_) { 46081 this.rollUpRows_ = 2; 46082 this.setRollUp(packet.pts); 46083 } else if (data === this.ROLL_UP_3_ROWS_) { 46084 this.rollUpRows_ = 3; 46085 this.setRollUp(packet.pts); 46086 } else if (data === this.ROLL_UP_4_ROWS_) { 46087 this.rollUpRows_ = 4; 46088 this.setRollUp(packet.pts); 46089 } else if (data === this.CARRIAGE_RETURN_) { 46090 this.clearFormatting(packet.pts); 46091 this.flushDisplayed(packet.pts); 46092 this.shiftRowsUp_(); 46093 this.startPts_ = packet.pts; 46094 } else if (data === this.BACKSPACE_) { 46095 if (this.mode_ === 'popOn') { 46096 this.nonDisplayed_[this.row_].text = this.nonDisplayed_[this.row_].text.slice(0, -1); 46097 } else { 46098 this.displayed_[this.row_].text = this.displayed_[this.row_].text.slice(0, -1); 46099 } 46100 } else if (data === this.ERASE_DISPLAYED_MEMORY_) { 46101 this.flushDisplayed(packet.pts); 46102 this.displayed_ = createDisplayBuffer(); 46103 } else if (data === this.ERASE_NON_DISPLAYED_MEMORY_) { 46104 this.nonDisplayed_ = createDisplayBuffer(); 46105 } else if (data === this.RESUME_DIRECT_CAPTIONING_) { 46106 if (this.mode_ !== 'paintOn') { 46107 // NOTE: This should be removed when proper caption positioning is 46108 // implemented 46109 this.flushDisplayed(packet.pts); 46110 this.displayed_ = createDisplayBuffer(); 46111 } 46112 this.mode_ = 'paintOn'; 46113 this.startPts_ = packet.pts; // Append special characters to caption text 46114 } else if (this.isSpecialCharacter(char0, char1)) { 46115 // Bitmask char0 so that we can apply character transformations 46116 // regardless of field and data channel. 46117 // Then byte-shift to the left and OR with char1 so we can pass the 46118 // entire character code to `getCharFromCode`. 46119 char0 = (char0 & 0x03) << 8; 46120 text = getCharFromCode(char0 | char1); 46121 this[this.mode_](packet.pts, text); 46122 this.column_++; // Append extended characters to caption text 46123 } else if (this.isExtCharacter(char0, char1)) { 46124 // Extended characters always follow their "non-extended" equivalents. 46125 // IE if a "è" is desired, you'll always receive "eè"; non-compliant 46126 // decoders are supposed to drop the "è", while compliant decoders 46127 // backspace the "e" and insert "è". 46128 // Delete the previous character 46129 if (this.mode_ === 'popOn') { 46130 this.nonDisplayed_[this.row_].text = this.nonDisplayed_[this.row_].text.slice(0, -1); 46131 } else { 46132 this.displayed_[this.row_].text = this.displayed_[this.row_].text.slice(0, -1); 46133 } // Bitmask char0 so that we can apply character transformations 46134 // regardless of field and data channel. 46135 // Then byte-shift to the left and OR with char1 so we can pass the 46136 // entire character code to `getCharFromCode`. 46137 46138 char0 = (char0 & 0x03) << 8; 46139 text = getCharFromCode(char0 | char1); 46140 this[this.mode_](packet.pts, text); 46141 this.column_++; // Process mid-row codes 46142 } else if (this.isMidRowCode(char0, char1)) { 46143 // Attributes are not additive, so clear all formatting 46144 this.clearFormatting(packet.pts); // According to the standard, mid-row codes 46145 // should be replaced with spaces, so add one now 46146 46147 this[this.mode_](packet.pts, ' '); 46148 this.column_++; 46149 if ((char1 & 0xe) === 0xe) { 46150 this.addFormatting(packet.pts, ['i']); 46151 } 46152 if ((char1 & 0x1) === 0x1) { 46153 this.addFormatting(packet.pts, ['u']); 46154 } // Detect offset control codes and adjust cursor 46155 } else if (this.isOffsetControlCode(char0, char1)) { 46156 // Cursor position is set by indent PAC (see below) in 4-column 46157 // increments, with an additional offset code of 1-3 to reach any 46158 // of the 32 columns specified by CEA-608. So all we need to do 46159 // here is increment the column cursor by the given offset. 46160 const offset = char1 & 0x03; // For an offest value 1-3, set the offset for that caption 46161 // in the non-displayed array. 46162 46163 this.nonDisplayed_[this.row_].offset = offset; 46164 this.column_ += offset; // Detect PACs (Preamble Address Codes) 46165 } else if (this.isPAC(char0, char1)) { 46166 // There's no logic for PAC -> row mapping, so we have to just 46167 // find the row code in an array and use its index :( 46168 var row = ROWS.indexOf(data & 0x1f20); // Configure the caption window if we're in roll-up mode 46169 46170 if (this.mode_ === 'rollUp') { 46171 // This implies that the base row is incorrectly set. 46172 // As per the recommendation in CEA-608(Base Row Implementation), defer to the number 46173 // of roll-up rows set. 46174 if (row - this.rollUpRows_ + 1 < 0) { 46175 row = this.rollUpRows_ - 1; 46176 } 46177 this.setRollUp(packet.pts, row); 46178 }
46178 46179 if (row !== this.row_) { 46180 // formatting is only persistent for current row 46181 this.clearFormatting(packet.pts); 46182 this.row_ = row; 46183 } // All PACs can apply underline, so detect and apply 46184 // (All odd-numbered second bytes set underline) 46185 46186 if (char1 & 0x1 && this.formatting_.indexOf('u') === -1) { 46187 this.addFormatting(packet.pts, ['u']); 46188 } 46189 if ((data & 0x10) === 0x10) { 46190 // We've got an indent level code. Each successive even number
vendor: 1,262 bytes, lines 46191-46218
46191 // increments the column cursor by 4, so we can get the desired 46192 // column position by bit-shifting to the right (to get n/2) 46193 // and multiplying by 4. 46194 const indentations = (data & 0xe) >> 1; 46195 this.column_ = indentations * 4; // add to the number of indentations for positioning 46196 46197 this.nonDisplayed_[this.row_].indent += indentations; 46198 } 46199 if (this.isColorPAC(char1)) { 46200 // it's a color code, though we only support white, which 46201 // can be either normal or italicized. white italics can be 46202 // either 0x4e or 0x6e depending on the row, so we just 46203 // bitwise-and with 0xe to see if italics should be turned on 46204 if ((char1 & 0xe) === 0xe) { 46205 this.addFormatting(packet.pts, ['i']); 46206 } 46207 } // We have a normal character in char0, and possibly one in char1 46208 } else if (this.isNormalChar(char0)) { 46209 if (char1 === 0x00) { 46210 char1 = null; 46211 } 46212 text = getCharFromCode(char0); 46213 text += getCharFromCode(char1); 46214 this[this.mode_](packet.pts, text); 46215 this.column_ += text.length; 46216 } // finish data processing 46217 }; 46218 };
vendor: 15,185 bytes, lines 46219-46614
46219 46220 Cea608Stream.prototype = new Stream$7(); // Trigger a cue point that captures the current state of the 46221 // display buffer 46222 46223 Cea608Stream.prototype.flushDisplayed = function (pts) { 46224 const logWarning = index => { 46225 this.trigger('log', { 46226 level: 'warn', 46227 message: 'Skipping a malformed 608 caption at index ' + index + '.' 46228 }); 46229 }; 46230 const content = []; 46231 this.displayed_.forEach((row, i) => { 46232 if (row && row.text && row.text.length) { 46233 try { 46234 // remove spaces from the start and end of the string 46235 row.text = row.text.trim(); 46236 } catch (e) { 46237 // Ordinarily, this shouldn't happen. However, caption 46238 // parsing errors should not throw exceptions and 46239 // break playback. 46240 logWarning(i); 46241 } // See the below link for more details on the following fields: 46242 // https://dvcs.w3.org/hg/text-tracks/raw-file/default/608toVTT/608toVTT.html#positioning-in-cea-608 46243 46244 if (row.text.length) { 46245 content.push({ 46246 // The text to be displayed in the caption from this specific row, with whitespace removed. 46247 text: row.text, 46248 // Value between 1 and 15 representing the PAC row used to calculate line height. 46249 line: i + 1, 46250 // A number representing the indent position by percentage (CEA-608 PAC indent code). 46251 // The value will be a number between 10 and 80. Offset is used to add an aditional 46252 // value to the position if necessary. 46253 position: 10 + Math.min(70, row.indent * 10) + row.offset * 2.5 46254 }); 46255 } 46256 } else if (row === undefined || row === null) { 46257 logWarning(i); 46258 } 46259 }); 46260 if (content.length) { 46261 this.trigger('data', { 46262 startPts: this.startPts_, 46263 endPts: pts, 46264 content, 46265 stream: this.name_ 46266 }); 46267 } 46268 }; 46269 /** 46270 * Zero out the data, used for startup and on seek 46271 */ 46272 46273 Cea608Stream.prototype.reset = function () { 46274 this.mode_ = 'popOn'; // When in roll-up mode, the index of the last row that will 46275 // actually display captions. If a caption is shifted to a row 46276 // with a lower index than this, it is cleared from the display 46277 // buffer 46278 46279 this.topRow_ = 0; 46280 this.startPts_ = 0; 46281 this.displayed_ = createDisplayBuffer(); 46282 this.nonDisplayed_ = createDisplayBuffer(); 46283 this.lastControlCode_ = null; // Track row and column for proper line-breaking and spacing 46284 46285 this.column_ = 0; 46286 this.row_ = BOTTOM_ROW; 46287 this.rollUpRows_ = 2; // This variable holds currently-applied formatting 46288 46289 this.formatting_ = []; 46290 }; 46291 /** 46292 * Sets up control code and related constants for this instance 46293 */ 46294 46295 Cea608Stream.prototype.setConstants = function () { 46296 // The following attributes have these uses: 46297 // ext_ : char0 for mid-row codes, and the base for extended 46298 // chars (ext_+0, ext_+1, and ext_+2 are char0s for 46299 // extended codes) 46300 // control_: char0 for control codes, except byte-shifted to the 46301 // left so that we can do this.control_ | CONTROL_CODE 46302 // offset_: char0 for tab offset codes 46303 // 46304 // It's also worth noting that control codes, and _only_ control codes, 46305 // differ between field 1 and field2. Field 2 control codes are always 46306 // their field 1 value plus 1. That's why there's the "| field" on the 46307 // control value. 46308 if (this.dataChannel_ === 0) { 46309 this.BASE_ = 0x10; 46310 this.EXT_ = 0x11; 46311 this.CONTROL_ = (0x14 | this.field_) << 8; 46312 this.OFFSET_ = 0x17; 46313 } else if (this.dataChannel_ === 1) { 46314 this.BASE_ = 0x18; 46315 this.EXT_ = 0x19; 46316 this.CONTROL_ = (0x1c | this.field_) << 8; 46317 this.OFFSET_ = 0x1f; 46318 } // Constants for the LSByte command codes recognized by Cea608Stream. This 46319 // list is not exhaustive. For a more comprehensive listing and semantics see 46320 // http://www.gpo.gov/fdsys/pkg/CFR-2010-title47-vol1/pdf/CFR-2010-title47-vol1-sec15-119.pdf 46321 // Padding 46322 46323 this.PADDING_ = 0x0000; // Pop-on Mode 46324 46325 this.RESUME_CAPTION_LOADING_ = this.CONTROL_ | 0x20; 46326 this.END_OF_CAPTION_ = this.CONTROL_ | 0x2f; // Roll-up Mode 46327 46328 this.ROLL_UP_2_ROWS_ = this.CONTROL_ | 0x25; 46329 this.ROLL_UP_3_ROWS_ = this.CONTROL_ | 0x26; 46330 this.ROLL_UP_4_ROWS_ = this.CONTROL_ | 0x27; 46331 this.CARRIAGE_RETURN_ = this.CONTROL_ | 0x2d; // paint-on mode 46332 46333 this.RESUME_DIRECT_CAPTIONING_ = this.CONTROL_ | 0x29; // Erasure 46334 46335 this.BACKSPACE_ = this.CONTROL_ | 0x21; 46336 this.ERASE_DISPLAYED_MEMORY_ = this.CONTROL_ | 0x2c; 46337 this.ERASE_NON_DISPLAYED_MEMORY_ = this.CONTROL_ | 0x2e; 46338 }; 46339 /** 46340 * Detects if the 2-byte packet data is a special character 46341 * 46342 * Special characters have a second byte in the range 0x30 to 0x3f, 46343 * with the first byte being 0x11 (for data channel 1) or 0x19 (for 46344 * data channel 2). 46345 * 46346 * @param {Integer} char0 The first byte 46347 * @param {Integer} char1 The second byte 46348 * @return {Boolean} Whether the 2 bytes are an special character 46349 */ 46350 46351 Cea608Stream.prototype.isSpecialCharacter = function (char0, char1) { 46352 return char0 === this.EXT_ && char1 >= 0x30 && char1 <= 0x3f; 46353 }; 46354 /** 46355 * Detects if the 2-byte packet data is an extended character 46356 * 46357 * Extended characters have a second byte in the range 0x20 to 0x3f, 46358 * with the first byte being 0x12 or 0x13 (for data channel 1) or 46359 * 0x1a or 0x1b (for data channel 2). 46360 * 46361 * @param {Integer} char0 The first byte 46362 * @param {Integer} char1 The second byte 46363 * @return {Boolean} Whether the 2 bytes are an extended character 46364 */ 46365 46366 Cea608Stream.prototype.isExtCharacter = function (char0, char1) { 46367 return (char0 === this.EXT_ + 1 || char0 === this.EXT_ + 2) && char1 >= 0x20 && char1 <= 0x3f; 46368 }; 46369 /** 46370 * Detects if the 2-byte packet is a mid-row code 46371 * 46372 * Mid-row codes have a second byte in the range 0x20 to 0x2f, with 46373 * the first byte being 0x11 (for data channel 1) or 0x19 (for data 46374 * channel 2). 46375 * 46376 * @param {Integer} char0 The first byte 46377 * @param {Integer} char1 The second byte 46378 * @return {Boolean} Whether the 2 bytes are a mid-row code 46379 */ 46380 46381 Cea608Stream.prototype.isMidRowCode = function (char0, char1) { 46382 return char0 === this.EXT_ && char1 >= 0x20 && char1 <= 0x2f; 46383 }; 46384 /** 46385 * Detects if the 2-byte packet is an offset control code 46386 * 46387 * Offset control codes have a second byte in the range 0x21 to 0x23, 46388 * with the first byte being 0x17 (for data channel 1) or 0x1f (for 46389 * data channel 2). 46390 * 46391 * @param {Integer} char0 The first byte 46392 * @param {Integer} char1 The second byte 46393 * @return {Boolean} Whether the 2 bytes are an offset control code 46394 */ 46395 46396 Cea608Stream.prototype.isOffsetControlCode = function (char0, char1) { 46397 return char0 === this.OFFSET_ && char1 >= 0x21 && char1 <= 0x23; 46398 }; 46399 /** 46400 * Detects if the 2-byte packet is a Preamble Address Code 46401 * 46402 * PACs have a first byte in the range 0x10 to 0x17 (for data channel 1) 46403 * or 0x18 to 0x1f (for data channel 2), with the second byte in the 46404 * range 0x40 to 0x7f. 46405 * 46406 * @param {Integer} char0 The first byte 46407 * @param {Integer} char1 The second byte 46408 * @return {Boolean} Whether the 2 bytes are a PAC 46409 */ 46410 46411 Cea608Stream.prototype.isPAC = function (char0, char1) { 46412 return char0 >= this.BASE_ && char0 < this.BASE_ + 8 && char1 >= 0x40 && char1 <= 0x7f; 46413 }; 46414 /** 46415 * Detects if a packet's second byte is in the range of a PAC color code 46416 * 46417 * PAC color codes have the second byte be in the range 0x40 to 0x4f, or 46418 * 0x60 to 0x6f. 46419 * 46420 * @param {Integer} char1 The second byte 46421 * @return {Boolean} Whether the byte is a color PAC 46422 */ 46423 46424 Cea608Stream.prototype.isColorPAC = function (char1) { 46425 return char1 >= 0x40 && char1 <= 0x4f || char1 >= 0x60 && char1 <= 0x7f; 46426 }; 46427 /** 46428 * Detects if a single byte is in the range of a normal character 46429 * 46430 * Normal text bytes are in the range 0x20 to 0x7f. 46431 * 46432 * @param {Integer} char The byte 46433 * @return {Boolean} Whether the byte is a normal character 46434 */ 46435 46436 Cea608Stream.prototype.isNormalChar = function (char) { 46437 return char >= 0x20 && char <= 0x7f; 46438 }; 46439 /** 46440 * Configures roll-up 46441 * 46442 * @param {Integer} pts Current PTS 46443 * @param {Integer} newBaseRow Used by PACs to slide the current window to 46444 * a new position 46445 */ 46446 46447 Cea608Stream.prototype.setRollUp = function (pts, newBaseRow) { 46448 // Reset the base row to the bottom row when switching modes 46449 if (this.mode_ !== 'rollUp') { 46450 this.row_ = BOTTOM_ROW; 46451 this.mode_ = 'rollUp'; // Spec says to wipe memories when switching to roll-up 46452 46453 this.flushDisplayed(pts); 46454 this.nonDisplayed_ = createDisplayBuffer(); 46455 this.displayed_ = createDisplayBuffer(); 46456 } 46457 if (newBaseRow !== undefined && newBaseRow !== this.row_) { 46458 // move currently displayed captions (up or down) to the new base row 46459 for (var i = 0; i < this.rollUpRows_; i++) { 46460 this.displayed_[newBaseRow - i] = this.displayed_[this.row_ - i]; 46461 this.displayed_[this.row_ - i] = { 46462 text: '', 46463 indent: 0, 46464 offset: 0 46465 }; 46466 } 46467 } 46468 if (newBaseRow === undefined) { 46469 newBaseRow = this.row_; 46470 } 46471 this.topRow_ = newBaseRow - this.rollUpRows_ + 1; 46472 }; // Adds the opening HTML tag for the passed character to the caption text, 46473 // and keeps track of it for later closing 46474 46475 Cea608Stream.prototype.addFormatting = function (pts, format) { 46476 this.formatting_ = this.formatting_.concat(format); 46477 var text = format.reduce(function (text, format) { 46478 return text + '<' + format + '>'; 46479 }, ''); 46480 this[this.mode_](pts, text); 46481 }; // Adds HTML closing tags for current formatting to caption text and 46482 // clears remembered formatting 46483 46484 Cea608Stream.prototype.clearFormatting = function (pts) { 46485 if (!this.formatting_.length) { 46486 return; 46487 } 46488 var text = this.formatting_.reverse().reduce(function (text, format) { 46489 return text + '</' + format + '>'; 46490 }, ''); 46491 this.formatting_ = []; 46492 this[this.mode_](pts, text); 46493 }; // Mode Implementations 46494 46495 Cea608Stream.prototype.popOn = function (pts, text) { 46496 var baseRow = this.nonDisplayed_[this.row_].text; // buffer characters 46497 46498 baseRow += text; 46499 this.nonDisplayed_[this.row_].text = baseRow; 46500 }; 46501 Cea608Stream.prototype.rollUp = function (pts, text) { 46502 var baseRow = this.displayed_[this.row_].text; 46503 baseRow += text; 46504 this.displayed_[this.row_].text = baseRow; 46505 }; 46506 Cea608Stream.prototype.shiftRowsUp_ = function () { 46507 var i; // clear out inactive rows 46508 46509 for (i = 0; i < this.topRow_; i++) { 46510 this.displayed_[i] = { 46511 text: '', 46512 indent: 0, 46513 offset: 0 46514 }; 46515 } 46516 for (i = this.row_ + 1; i < BOTTOM_ROW + 1; i++) { 46517 this.displayed_[i] = { 46518 text: '', 46519 indent: 0, 46520 offset: 0 46521 }; 46522 } // shift displayed rows up 46523 46524 for (i = this.topRow_; i < this.row_; i++) { 46525 this.displayed_[i] = this.displayed_[i + 1]; 46526 } // clear out the bottom row 46527 46528 this.displayed_[this.row_] = { 46529 text: '', 46530 indent: 0, 46531 offset: 0 46532 }; 46533 }; 46534 Cea608Stream.prototype.paintOn = function (pts, text) { 46535 var baseRow = this.displayed_[this.row_].text; 46536 baseRow += text; 46537 this.displayed_[this.row_].text = baseRow; 46538 }; // exports 46539 46540 var captionStream = { 46541 CaptionStream: CaptionStream$2, 46542 Cea608Stream: Cea608Stream, 46543 Cea708Stream: Cea708Stream 46544 }; 46545 /** 46546 * mux.js 46547 * 46548 * Copyright (c) Brightcove 46549 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 46550 */ 46551 46552 var streamTypes = { 46553 H264_STREAM_TYPE: 0x1B, 46554 ADTS_STREAM_TYPE: 0x0F, 46555 METADATA_STREAM_TYPE: 0x15 46556 }; 46557 /** 46558 * mux.js 46559 * 46560 * Copyright (c) Brightcove 46561 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 46562 * 46563 * Accepts program elementary stream (PES) data events and corrects 46564 * decode and presentation time stamps to account for a rollover 46565 * of the 33 bit value. 46566 */ 46567 46568 var Stream$6 = stream; 46569 var MAX_TS = 8589934592; 46570 var RO_THRESH = 4294967296; 46571 var TYPE_SHARED = 'shared'; 46572 var handleRollover$1 = function (value, reference) { 46573 var direction = 1; 46574 if (value > reference) { 46575 // If the current timestamp value is greater than our reference timestamp and we detect a 46576 // timestamp rollover, this means the roll over is happening in the opposite direction. 46577 // Example scenario: Enter a long stream/video just after a rollover occurred. The reference 46578 // point will be set to a small number, e.g. 1. The user then seeks backwards over the 46579 // rollover point. In loading this segment, the timestamp values will be very large, 46580 // e.g. 2^33 - 1. Since this comes before the data we loaded previously, we want to adjust 46581 // the time stamp to be `value - 2^33`. 46582 direction = -1; 46583 } // Note: A seek forwards or back that is greater than the RO_THRESH (2^32, ~13 hours) will 46584 // cause an incorrect adjustment. 46585 46586 while (Math.abs(reference - value) > RO_THRESH) { 46587 value += direction * MAX_TS; 46588 } 46589 return value; 46590 }; 46591 var TimestampRolloverStream$1 = function (type) { 46592 var lastDTS, referenceDTS; 46593 TimestampRolloverStream$1.prototype.init.call(this); // The "shared" type is used in cases where a stream will contain muxed 46594 // video and audio. We could use `undefined` here, but having a string 46595 // makes debugging a little clearer. 46596 46597 this.type_ = type || TYPE_SHARED; 46598 this.push = function (data) { 46599 /** 46600 * Rollover stream expects data from elementary stream. 46601 * Elementary stream can push forward 2 types of data 46602 * - Parsed Video/Audio/Timed-metadata PES (packetized elementary stream) packets 46603 * - Tracks metadata from PMT (Program Map Table) 46604 * Rollover stream expects pts/dts info to be available, since it stores lastDTS 46605 * We should ignore non-PES packets since they may override lastDTS to undefined. 46606 * lastDTS is important to signal the next segments 46607 * about rollover from the previous segments. 46608 */ 46609 if (data.type === 'metadata') { 46610 this.trigger('data', data); 46611 return; 46612 } // Any "shared" rollover streams will accept _all_ data. Otherwise, 46613 // streams will only accept data that matches their type. 46614
vendor: 26,567 bytes, lines 46615-47258
46615 if (this.type_ !== TYPE_SHARED && data.type !== this.type_) { 46616 return; 46617 } 46618 if (referenceDTS === undefined) { 46619 referenceDTS = data.dts; 46620 } 46621 data.dts = handleRollover$1(data.dts, referenceDTS); 46622 data.pts = handleRollover$1(data.pts, referenceDTS); 46623 lastDTS = data.dts; 46624 this.trigger('data', data); 46625 }; 46626 this.flush = function () { 46627 referenceDTS = lastDTS; 46628 this.trigger('done'); 46629 }; 46630 this.endTimeline = function () { 46631 this.flush(); 46632 this.trigger('endedtimeline'); 46633 }; 46634 this.discontinuity = function () { 46635 referenceDTS = void 0; 46636 lastDTS = void 0; 46637 }; 46638 this.reset = function () { 46639 this.discontinuity(); 46640 this.trigger('reset'); 46641 }; 46642 }; 46643 TimestampRolloverStream$1.prototype = new Stream$6(); 46644 var timestampRolloverStream = { 46645 TimestampRolloverStream: TimestampRolloverStream$1, 46646 handleRollover: handleRollover$1 46647 }; // Once IE11 support is dropped, this function should be removed. 46648 46649 var typedArrayIndexOf$1 = (typedArray, element, fromIndex) => { 46650 if (!typedArray) { 46651 return -1; 46652 } 46653 var currentIndex = fromIndex; 46654 for (; currentIndex < typedArray.length; currentIndex++) { 46655 if (typedArray[currentIndex] === element) { 46656 return currentIndex; 46657 } 46658 } 46659 return -1; 46660 }; 46661 var typedArray = { 46662 typedArrayIndexOf: typedArrayIndexOf$1 46663 }; 46664 /** 46665 * mux.js 46666 * 46667 * Copyright (c) Brightcove 46668 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 46669 * 46670 * Tools for parsing ID3 frame data 46671 * @see http://id3.org/id3v2.3.0 46672 */ 46673 46674 var typedArrayIndexOf = typedArray.typedArrayIndexOf, 46675 // Frames that allow different types of text encoding contain a text 46676 // encoding description byte [ID3v2.4.0 section 4.] 46677 textEncodingDescriptionByte = { 46678 Iso88591: 0x00, 46679 // ISO-8859-1, terminated with \0. 46680 Utf16: 0x01, 46681 // UTF-16 encoded Unicode BOM, terminated with \0\0 46682 Utf16be: 0x02, 46683 // UTF-16BE encoded Unicode, without BOM, terminated with \0\0 46684 Utf8: 0x03 // UTF-8 encoded Unicode, terminated with \0 46685 }, 46686 // return a percent-encoded representation of the specified byte range 46687 // @see http://en.wikipedia.org/wiki/Percent-encoding 46688 percentEncode$1 = function (bytes, start, end) { 46689 var i, 46690 result = ''; 46691 for (i = start; i < end; i++) { 46692 result += '%' + ('00' + bytes[i].toString(16)).slice(-2); 46693 } 46694 return result; 46695 }, 46696 // return the string representation of the specified byte range, 46697 // interpreted as UTf-8. 46698 parseUtf8 = function (bytes, start, end) { 46699 return decodeURIComponent(percentEncode$1(bytes, start, end)); 46700 }, 46701 // return the string representation of the specified byte range, 46702 // interpreted as ISO-8859-1. 46703 parseIso88591$1 = function (bytes, start, end) { 46704 return unescape(percentEncode$1(bytes, start, end)); // jshint ignore:line 46705 }, 46706 parseSyncSafeInteger$1 = function (data) { 46707 return data[0] << 21 | data[1] << 14 | data[2] << 7 | data[3]; 46708 }, 46709 frameParsers = { 46710 'APIC': function (frame) { 46711 var i = 1, 46712 mimeTypeEndIndex, 46713 descriptionEndIndex, 46714 LINK_MIME_TYPE = '-->'; 46715 if (frame.data[0] !== textEncodingDescriptionByte.Utf8) { 46716 // ignore frames with unrecognized character encodings 46717 return; 46718 } // parsing fields [ID3v2.4.0 section 4.14.] 46719 46720 mimeTypeEndIndex = typedArrayIndexOf(frame.data, 0, i); 46721 if (mimeTypeEndIndex < 0) { 46722 // malformed frame 46723 return; 46724 } // parsing Mime type field (terminated with \0) 46725 46726 frame.mimeType = parseIso88591$1(frame.data, i, mimeTypeEndIndex); 46727 i = mimeTypeEndIndex + 1; // parsing 1-byte Picture Type field 46728 46729 frame.pictureType = frame.data[i]; 46730 i++; 46731 descriptionEndIndex = typedArrayIndexOf(frame.data, 0, i); 46732 if (descriptionEndIndex < 0) { 46733 // malformed frame 46734 return; 46735 } // parsing Description field (terminated with \0) 46736 46737 frame.description = parseUtf8(frame.data, i, descriptionEndIndex); 46738 i = descriptionEndIndex + 1; 46739 if (frame.mimeType === LINK_MIME_TYPE) { 46740 // parsing Picture Data field as URL (always represented as ISO-8859-1 [ID3v2.4.0 section 4.]) 46741 frame.url = parseIso88591$1(frame.data, i, frame.data.length); 46742 } else { 46743 // parsing Picture Data field as binary data 46744 frame.pictureData = frame.data.subarray(i, frame.data.length); 46745 } 46746 }, 46747 'T*': function (frame) { 46748 if (frame.data[0] !== textEncodingDescriptionByte.Utf8) { 46749 // ignore frames with unrecognized character encodings 46750 return; 46751 } // parse text field, do not include null terminator in the frame value 46752 // frames that allow different types of encoding contain terminated text [ID3v2.4.0 section 4.] 46753 46754 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.] 46755 46756 frame.values = frame.value.split('\0'); 46757 }, 46758 'TXXX': function (frame) { 46759 var descriptionEndIndex; 46760 if (frame.data[0] !== textEncodingDescriptionByte.Utf8) { 46761 // ignore frames with unrecognized character encodings 46762 return; 46763 } 46764 descriptionEndIndex = typedArrayIndexOf(frame.data, 0, 1); 46765 if (descriptionEndIndex === -1) { 46766 return; 46767 } // parse the text fields 46768 46769 frame.description = parseUtf8(frame.data, 1, descriptionEndIndex); // do not include the null terminator in the tag value 46770 // frames that allow different types of encoding contain terminated text 46771 // [ID3v2.4.0 section 4.] 46772 46773 frame.value = parseUtf8(frame.data, descriptionEndIndex + 1, frame.data.length).replace(/\0*$/, ''); 46774 frame.data = frame.value; 46775 }, 46776 'W*': function (frame) { 46777 // parse URL field; URL fields are always represented as ISO-8859-1 [ID3v2.4.0 section 4.] 46778 // if the value is followed by a string termination all the following information should be ignored [ID3v2.4.0 section 4.3] 46779 frame.url = parseIso88591$1(frame.data, 0, frame.data.length).replace(/\0.*$/, ''); 46780 }, 46781 'WXXX': function (frame) { 46782 var descriptionEndIndex; 46783 if (frame.data[0] !== textEncodingDescriptionByte.Utf8) { 46784 // ignore frames with unrecognized character encodings 46785 return; 46786 } 46787 descriptionEndIndex = typedArrayIndexOf(frame.data, 0, 1); 46788 if (descriptionEndIndex === -1) { 46789 return; 46790 } // parse the description and URL fields 46791 46792 frame.description = parseUtf8(frame.data, 1, descriptionEndIndex); // URL fields are always represented as ISO-8859-1 [ID3v2.4.0 section 4.] 46793 // if the value is followed by a string termination all the following information 46794 // should be ignored [ID3v2.4.0 section 4.3] 46795 46796 frame.url = parseIso88591$1(frame.data, descriptionEndIndex + 1, frame.data.length).replace(/\0.*$/, ''); 46797 }, 46798 'PRIV': function (frame) { 46799 var i; 46800 for (i = 0; i < frame.data.length; i++) { 46801 if (frame.data[i] === 0) { 46802 // parse the description and URL fields 46803 frame.owner = parseIso88591$1(frame.data, 0, i); 46804 break; 46805 } 46806 } 46807 frame.privateData = frame.data.subarray(i + 1); 46808 frame.data = frame.privateData; 46809 } 46810 }; 46811 var parseId3Frames$1 = function (data) { 46812 var frameSize, 46813 frameHeader, 46814 frameStart = 10, 46815 tagSize = 0, 46816 frames = []; // If we don't have enough data for a header, 10 bytes, 46817 // or 'ID3' in the first 3 bytes this is not a valid ID3 tag. 46818 46819 if (data.length < 10 || data[0] !== 'I'.charCodeAt(0) || data[1] !== 'D'.charCodeAt(0) || data[2] !== '3'.charCodeAt(0)) { 46820 return; 46821 } // the frame size is transmitted as a 28-bit integer in the 46822 // last four bytes of the ID3 header. 46823 // The most significant bit of each byte is dropped and the 46824 // results concatenated to recover the actual value. 46825 46826 tagSize = parseSyncSafeInteger$1(data.subarray(6, 10)); // ID3 reports the tag size excluding the header but it's more 46827 // convenient for our comparisons to include it 46828 46829 tagSize += 10; // check bit 6 of byte 5 for the extended header flag. 46830 46831 var hasExtendedHeader = data[5] & 0x40; 46832 if (hasExtendedHeader) { 46833 // advance the frame start past the extended header 46834 frameStart += 4; // header size field 46835 46836 frameStart += parseSyncSafeInteger$1(data.subarray(10, 14)); 46837 tagSize -= parseSyncSafeInteger$1(data.subarray(16, 20)); // clip any padding off the end 46838 } // parse one or more ID3 frames 46839 // http://id3.org/id3v2.3.0#ID3v2_frame_overview 46840 46841 do { 46842 // determine the number of bytes in this frame 46843 frameSize = parseSyncSafeInteger$1(data.subarray(frameStart + 4, frameStart + 8)); 46844 if (frameSize < 1) { 46845 break; 46846 } 46847 frameHeader = String.fromCharCode(data[frameStart], data[frameStart + 1], data[frameStart + 2], data[frameStart + 3]); 46848 var frame = { 46849 id: frameHeader, 46850 data: data.subarray(frameStart + 10, frameStart + frameSize + 10) 46851 }; 46852 frame.key = frame.id; // parse frame values 46853 46854 if (frameParsers[frame.id]) { 46855 // use frame specific parser 46856 frameParsers[frame.id](frame); 46857 } else if (frame.id[0] === 'T') { 46858 // use text frame generic parser 46859 frameParsers['T*'](frame); 46860 } else if (frame.id[0] === 'W') { 46861 // use URL link frame generic parser 46862 frameParsers['W*'](frame); 46863 } 46864 frames.push(frame); 46865 frameStart += 10; // advance past the frame header 46866 46867 frameStart += frameSize; // advance past the frame body 46868 } while (frameStart < tagSize); 46869 return frames; 46870 }; 46871 var parseId3 = { 46872 parseId3Frames: parseId3Frames$1, 46873 parseSyncSafeInteger: parseSyncSafeInteger$1, 46874 frameParsers: frameParsers 46875 }; 46876 /** 46877 * mux.js 46878 * 46879 * Copyright (c) Brightcove 46880 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 46881 * 46882 * Accepts program elementary stream (PES) data events and parses out 46883 * ID3 metadata from them, if present. 46884 * @see http://id3.org/id3v2.3.0 46885 */ 46886 46887 var Stream$5 = stream, 46888 StreamTypes$3 = streamTypes, 46889 id3 = parseId3, 46890 MetadataStream; 46891 MetadataStream = function (options) { 46892 var settings = { 46893 // the bytes of the program-level descriptor field in MP2T 46894 // see ISO/IEC 13818-1:2013 (E), section 2.6 "Program and 46895 // program element descriptors" 46896 descriptor: options && options.descriptor 46897 }, 46898 // the total size in bytes of the ID3 tag being parsed 46899 tagSize = 0, 46900 // tag data that is not complete enough to be parsed 46901 buffer = [], 46902 // the total number of bytes currently in the buffer 46903 bufferSize = 0, 46904 i; 46905 MetadataStream.prototype.init.call(this); // calculate the text track in-band metadata track dispatch type 46906 // https://html.spec.whatwg.org/multipage/embedded-content.html#steps-to-expose-a-media-resource-specific-text-track 46907 46908 this.dispatchType = StreamTypes$3.METADATA_STREAM_TYPE.toString(16); 46909 if (settings.descriptor) { 46910 for (i = 0; i < settings.descriptor.length; i++) { 46911 this.dispatchType += ('00' + settings.descriptor[i].toString(16)).slice(-2); 46912 } 46913 } 46914 this.push = function (chunk) { 46915 var tag, frameStart, frameSize, frame, i, frameHeader; 46916 if (chunk.type !== 'timed-metadata') { 46917 return; 46918 } // if data_alignment_indicator is set in the PES header, 46919 // we must have the start of a new ID3 tag. Assume anything 46920 // remaining in the buffer was malformed and throw it out 46921 46922 if (chunk.dataAlignmentIndicator) { 46923 bufferSize = 0; 46924 buffer.length = 0; 46925 } // ignore events that don't look like ID3 data 46926 46927 if (buffer.length === 0 && (chunk.data.length < 10 || chunk.data[0] !== 'I'.charCodeAt(0) || chunk.data[1] !== 'D'.charCodeAt(0) || chunk.data[2] !== '3'.charCodeAt(0))) { 46928 this.trigger('log', { 46929 level: 'warn', 46930 message: 'Skipping unrecognized metadata packet' 46931 }); 46932 return; 46933 } // add this chunk to the data we've collected so far 46934 46935 buffer.push(chunk); 46936 bufferSize += chunk.data.byteLength; // grab the size of the entire frame from the ID3 header 46937 46938 if (buffer.length === 1) { 46939 // the frame size is transmitted as a 28-bit integer in the 46940 // last four bytes of the ID3 header. 46941 // The most significant bit of each byte is dropped and the 46942 // results concatenated to recover the actual value. 46943 tagSize = id3.parseSyncSafeInteger(chunk.data.subarray(6, 10)); // ID3 reports the tag size excluding the header but it's more 46944 // convenient for our comparisons to include it 46945 46946 tagSize += 10; 46947 } // if the entire frame has not arrived, wait for more data 46948 46949 if (bufferSize < tagSize) { 46950 return; 46951 } // collect the entire frame so it can be parsed 46952 46953 tag = { 46954 data: new Uint8Array(tagSize), 46955 frames: [], 46956 pts: buffer[0].pts, 46957 dts: buffer[0].dts 46958 }; 46959 for (i = 0; i < tagSize;) { 46960 tag.data.set(buffer[0].data.subarray(0, tagSize - i), i); 46961 i += buffer[0].data.byteLength; 46962 bufferSize -= buffer[0].data.byteLength; 46963 buffer.shift(); 46964 } // find the start of the first frame and the end of the tag 46965 46966 frameStart = 10; 46967 if (tag.data[5] & 0x40) { 46968 // advance the frame start past the extended header 46969 frameStart += 4; // header size field 46970 46971 frameStart += id3.parseSyncSafeInteger(tag.data.subarray(10, 14)); // clip any padding off the end 46972 46973 tagSize -= id3.parseSyncSafeInteger(tag.data.subarray(16, 20)); 46974 } // parse one or more ID3 frames 46975 // http://id3.org/id3v2.3.0#ID3v2_frame_overview 46976 46977 do { 46978 // determine the number of bytes in this frame 46979 frameSize = id3.parseSyncSafeInteger(tag.data.subarray(frameStart + 4, frameStart + 8)); 46980 if (frameSize < 1) { 46981 this.trigger('log', { 46982 level: 'warn', 46983 message: 'Malformed ID3 frame encountered. Skipping remaining metadata parsing.' 46984 }); // If the frame is malformed, don't parse any further frames but allow previous valid parsed frames 46985 // to be sent along. 46986 46987 break; 46988 } 46989 frameHeader = String.fromCharCode(tag.data[frameStart], tag.data[frameStart + 1], tag.data[frameStart + 2], tag.data[frameStart + 3]); 46990 frame = { 46991 id: frameHeader, 46992 data: tag.data.subarray(frameStart + 10, frameStart + frameSize + 10) 46993 }; 46994 frame.key = frame.id; // parse frame values 46995 46996 if (id3.frameParsers[frame.id]) { 46997 // use frame specific parser 46998 id3.frameParsers[frame.id](frame); 46999 } else if (frame.id[0] === 'T') { 47000 // use text frame generic parser 47001 id3.frameParsers['T*'](frame); 47002 } else if (frame.id[0] === 'W') { 47003 // use URL link frame generic parser 47004 id3.frameParsers['W*'](frame); 47005 } // handle the special PRIV frame used to indicate the start 47006 // time for raw AAC data 47007 47008 if (frame.owner === 'com.apple.streaming.transportStreamTimestamp') { 47009 var d = frame.data, 47010 size = (d[3] & 0x01) << 30 | d[4] << 22 | d[5] << 14 | d[6] << 6 | d[7] >>> 2; 47011 size *= 4; 47012 size += d[7] & 0x03; 47013 frame.timeStamp = size; // in raw AAC, all subsequent data will be timestamped based 47014 // on the value of this frame 47015 // we couldn't have known the appropriate pts and dts before 47016 // parsing this ID3 tag so set those values now 47017 47018 if (tag.pts === undefined && tag.dts === undefined) { 47019 tag.pts = frame.timeStamp; 47020 tag.dts = frame.timeStamp; 47021 } 47022 this.trigger('timestamp', frame); 47023 } 47024 tag.frames.push(frame); 47025 frameStart += 10; // advance past the frame header 47026 47027 frameStart += frameSize; // advance past the frame body 47028 } while (frameStart < tagSize); 47029 this.trigger('data', tag); 47030 }; 47031 }; 47032 MetadataStream.prototype = new Stream$5(); 47033 var metadataStream = MetadataStream; 47034 /** 47035 * mux.js 47036 * 47037 * Copyright (c) Brightcove 47038 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 47039 * 47040 * A stream-based mp2t to mp4 converter. This utility can be used to 47041 * deliver mp4s to a SourceBuffer on platforms that support native 47042 * Media Source Extensions. 47043 */ 47044 47045 var Stream$4 = stream, 47046 CaptionStream$1 = captionStream, 47047 StreamTypes$2 = streamTypes, 47048 TimestampRolloverStream = timestampRolloverStream.TimestampRolloverStream; // object types 47049 47050 var TransportPacketStream, TransportParseStream, ElementaryStream; // constants 47051 47052 var MP2T_PACKET_LENGTH$1 = 188, 47053 // bytes 47054 SYNC_BYTE$1 = 0x47; 47055 /** 47056 * Splits an incoming stream of binary data into MPEG-2 Transport 47057 * Stream packets. 47058 */ 47059 47060 TransportPacketStream = function () { 47061 var buffer = new Uint8Array(MP2T_PACKET_LENGTH$1), 47062 bytesInBuffer = 0; 47063 TransportPacketStream.prototype.init.call(this); // Deliver new bytes to the stream. 47064 47065 /** 47066 * Split a stream of data into M2TS packets 47067 **/ 47068 47069 this.push = function (bytes) { 47070 var startIndex = 0, 47071 endIndex = MP2T_PACKET_LENGTH$1, 47072 everything; // If there are bytes remaining from the last segment, prepend them to the 47073 // bytes that were pushed in 47074 47075 if (bytesInBuffer) { 47076 everything = new Uint8Array(bytes.byteLength + bytesInBuffer); 47077 everything.set(buffer.subarray(0, bytesInBuffer)); 47078 everything.set(bytes, bytesInBuffer); 47079 bytesInBuffer = 0; 47080 } else { 47081 everything = bytes; 47082 } // While we have enough data for a packet 47083 47084 while (endIndex < everything.byteLength) { 47085 // Look for a pair of start and end sync bytes in the data.. 47086 if (everything[startIndex] === SYNC_BYTE$1 && everything[endIndex] === SYNC_BYTE$1) { 47087 // We found a packet so emit it and jump one whole packet forward in 47088 // the stream 47089 this.trigger('data', everything.subarray(startIndex, endIndex)); 47090 startIndex += MP2T_PACKET_LENGTH$1; 47091 endIndex += MP2T_PACKET_LENGTH$1; 47092 continue; 47093 } // If we get here, we have somehow become de-synchronized and we need to step 47094 // forward one byte at a time until we find a pair of sync bytes that denote 47095 // a packet 47096 47097 startIndex++; 47098 endIndex++; 47099 } // If there was some data left over at the end of the segment that couldn't 47100 // possibly be a whole packet, keep it because it might be the start of a packet 47101 // that continues in the next segment 47102 47103 if (startIndex < everything.byteLength) { 47104 buffer.set(everything.subarray(startIndex), 0); 47105 bytesInBuffer = everything.byteLength - startIndex; 47106 } 47107 }; 47108 /** 47109 * Passes identified M2TS packets to the TransportParseStream to be parsed 47110 **/ 47111 47112 this.flush = function () { 47113 // If the buffer contains a whole packet when we are being flushed, emit it 47114 // and empty the buffer. Otherwise hold onto the data because it may be 47115 // important for decoding the next segment 47116 if (bytesInBuffer === MP2T_PACKET_LENGTH$1 && buffer[0] === SYNC_BYTE$1) { 47117 this.trigger('data', buffer); 47118 bytesInBuffer = 0; 47119 } 47120 this.trigger('done'); 47121 }; 47122 this.endTimeline = function () { 47123 this.flush(); 47124 this.trigger('endedtimeline'); 47125 }; 47126 this.reset = function () { 47127 bytesInBuffer = 0; 47128 this.trigger('reset'); 47129 }; 47130 }; 47131 TransportPacketStream.prototype = new Stream$4(); 47132 /** 47133 * Accepts an MP2T TransportPacketStream and emits data events with parsed 47134 * forms of the individual transport stream packets. 47135 */ 47136 47137 TransportParseStream = function () { 47138 var parsePsi, parsePat, parsePmt, self; 47139 TransportParseStream.prototype.init.call(this); 47140 self = this; 47141 this.packetsWaitingForPmt = []; 47142 this.programMapTable = undefined; 47143 parsePsi = function (payload, psi) { 47144 var offset = 0; // PSI packets may be split into multiple sections and those 47145 // sections may be split into multiple packets. If a PSI 47146 // section starts in this packet, the payload_unit_start_indicator 47147 // will be true and the first byte of the payload will indicate 47148 // the offset from the current position to the start of the 47149 // section. 47150 47151 if (psi.payloadUnitStartIndicator) { 47152 offset += payload[offset] + 1; 47153 } 47154 if (psi.type === 'pat') { 47155 parsePat(payload.subarray(offset), psi); 47156 } else { 47157 parsePmt(payload.subarray(offset), psi); 47158 } 47159 }; 47160 parsePat = function (payload, pat) { 47161 pat.section_number = payload[7]; // eslint-disable-line camelcase 47162 47163 pat.last_section_number = payload[8]; // eslint-disable-line camelcase 47164 // skip the PSI header and parse the first PMT entry 47165 47166 self.pmtPid = (payload[10] & 0x1F) << 8 | payload[11]; 47167 pat.pmtPid = self.pmtPid; 47168 }; 47169 /** 47170 * Parse out the relevant fields of a Program Map Table (PMT). 47171 * @param payload {Uint8Array} the PMT-specific portion of an MP2T 47172 * packet. The first byte in this array should be the table_id 47173 * field. 47174 * @param pmt {object} the object that should be decorated with 47175 * fields parsed from the PMT. 47176 */ 47177 47178 parsePmt = function (payload, pmt) { 47179 var sectionLength, tableEnd, programInfoLength, offset; // PMTs can be sent ahead of the time when they should actually 47180 // take effect. We don't believe this should ever be the case 47181 // for HLS but we'll ignore "forward" PMT declarations if we see 47182 // them. Future PMT declarations have the current_next_indicator 47183 // set to zero. 47184 47185 if (!(payload[5] & 0x01)) { 47186 return; 47187 } // overwrite any existing program map table 47188 47189 self.programMapTable = { 47190 video: null, 47191 audio: null, 47192 'timed-metadata': {} 47193 }; // the mapping table ends at the end of the current section 47194 47195 sectionLength = (payload[1] & 0x0f) << 8 | payload[2]; 47196 tableEnd = 3 + sectionLength - 4; // to determine where the table is, we have to figure out how 47197 // long the program info descriptors are 47198 47199 programInfoLength = (payload[10] & 0x0f) << 8 | payload[11]; // advance the offset to the first entry in the mapping table 47200 47201 offset = 12 + programInfoLength; 47202 while (offset < tableEnd) { 47203 var streamType = payload[offset]; 47204 var pid = (payload[offset + 1] & 0x1F) << 8 | payload[offset + 2]; // only map a single elementary_pid for audio and video stream types 47205 // TODO: should this be done for metadata too? for now maintain behavior of 47206 // multiple metadata streams 47207 47208 if (streamType === StreamTypes$2.H264_STREAM_TYPE && self.programMapTable.video === null) { 47209 self.programMapTable.video = pid; 47210 } else if (streamType === StreamTypes$2.ADTS_STREAM_TYPE && self.programMapTable.audio === null) { 47211 self.programMapTable.audio = pid; 47212 } else if (streamType === StreamTypes$2.METADATA_STREAM_TYPE) { 47213 // map pid to stream type for metadata streams 47214 self.programMapTable['timed-metadata'][pid] = streamType; 47215 } // move to the next table entry 47216 // skip past the elementary stream descriptors, if present 47217 47218 offset += ((payload[offset + 3] & 0x0F) << 8 | payload[offset + 4]) + 5; 47219 } // record the map on the packet as well 47220 47221 pmt.programMapTable = self.programMapTable; 47222 }; 47223 /** 47224 * Deliver a new MP2T packet to the next stream in the pipeline. 47225 */ 47226 47227 this.push = function (packet) { 47228 var result = {}, 47229 offset = 4; 47230 result.payloadUnitStartIndicator = !!(packet[1] & 0x40); // pid is a 13-bit field starting at the last bit of packet[1] 47231 47232 result.pid = packet[1] & 0x1f; 47233 result.pid <<= 8; 47234 result.pid |= packet[2]; // if an adaption field is present, its length is specified by the 47235 // fifth byte of the TS packet header. The adaptation field is 47236 // used to add stuffing to PES packets that don't fill a complete 47237 // TS packet, and to specify some forms of timing and control data 47238 // that we do not currently use. 47239 47240 if ((packet[3] & 0x30) >>> 4 > 0x01) { 47241 offset += packet[offset] + 1; 47242 } // parse the rest of the packet based on the type 47243 47244 if (result.pid === 0) { 47245 result.type = 'pat'; 47246 parsePsi(packet.subarray(offset), result); 47247 this.trigger('data', result); 47248 } else if (result.pid === this.pmtPid) { 47249 result.type = 'pmt'; 47250 parsePsi(packet.subarray(offset), result); 47251 this.trigger('data', result); // if there are any packets waiting for a PMT to be found, process them now 47252 47253 while (this.packetsWaitingForPmt.length) { 47254 this.processPes_.apply(this, this.packetsWaitingForPmt.shift()); 47255 } 47256 } else if (this.programMapTable === undefined) { 47257 // When we have not seen a PMT yet, defer further processing of 47258 // PES packets until one has been parsed
vendor: 32,290 bytes, lines 47259-48166
47259 this.packetsWaitingForPmt.push([packet, offset, result]); 47260 } else { 47261 this.processPes_(packet, offset, result); 47262 } 47263 }; 47264 this.processPes_ = function (packet, offset, result) { 47265 // set the appropriate stream type 47266 if (result.pid === this.programMapTable.video) { 47267 result.streamType = StreamTypes$2.H264_STREAM_TYPE; 47268 } else if (result.pid === this.programMapTable.audio) { 47269 result.streamType = StreamTypes$2.ADTS_STREAM_TYPE; 47270 } else { 47271 // if not video or audio, it is timed-metadata or unknown 47272 // if unknown, streamType will be undefined 47273 result.streamType = this.programMapTable['timed-metadata'][result.pid]; 47274 } 47275 result.type = 'pes'; 47276 result.data = packet.subarray(offset); 47277 this.trigger('data', result); 47278 }; 47279 }; 47280 TransportParseStream.prototype = new Stream$4(); 47281 TransportParseStream.STREAM_TYPES = { 47282 h264: 0x1b, 47283 adts: 0x0f 47284 }; 47285 /** 47286 * Reconsistutes program elementary stream (PES) packets from parsed 47287 * transport stream packets. That is, if you pipe an 47288 * mp2t.TransportParseStream into a mp2t.ElementaryStream, the output 47289 * events will be events which capture the bytes for individual PES 47290 * packets plus relevant metadata that has been extracted from the 47291 * container. 47292 */ 47293 47294 ElementaryStream = function () { 47295 var self = this, 47296 segmentHadPmt = false, 47297 // PES packet fragments 47298 video = { 47299 data: [], 47300 size: 0 47301 }, 47302 audio = { 47303 data: [], 47304 size: 0 47305 }, 47306 timedMetadata = { 47307 data: [], 47308 size: 0 47309 }, 47310 programMapTable, 47311 parsePes = function (payload, pes) { 47312 var ptsDtsFlags; 47313 const startPrefix = payload[0] << 16 | payload[1] << 8 | payload[2]; // default to an empty array 47314 47315 pes.data = new Uint8Array(); // In certain live streams, the start of a TS fragment has ts packets 47316 // that are frame data that is continuing from the previous fragment. This 47317 // is to check that the pes data is the start of a new pes payload 47318 47319 if (startPrefix !== 1) { 47320 return; 47321 } // get the packet length, this will be 0 for video 47322 47323 pes.packetLength = 6 + (payload[4] << 8 | payload[5]); // find out if this packets starts a new keyframe 47324 47325 pes.dataAlignmentIndicator = (payload[6] & 0x04) !== 0; // PES packets may be annotated with a PTS value, or a PTS value 47326 // and a DTS value. Determine what combination of values is 47327 // available to work with. 47328 47329 ptsDtsFlags = payload[7]; // PTS and DTS are normally stored as a 33-bit number. Javascript 47330 // performs all bitwise operations on 32-bit integers but javascript 47331 // supports a much greater range (52-bits) of integer using standard 47332 // mathematical operations. 47333 // We construct a 31-bit value using bitwise operators over the 31 47334 // most significant bits and then multiply by 4 (equal to a left-shift 47335 // of 2) before we add the final 2 least significant bits of the 47336 // timestamp (equal to an OR.) 47337 47338 if (ptsDtsFlags & 0xC0) { 47339 // the PTS and DTS are not written out directly. For information 47340 // on how they are encoded, see 47341 // http://dvd.sourceforge.net/dvdinfo/pes-hdr.html 47342 pes.pts = (payload[9] & 0x0E) << 27 | (payload[10] & 0xFF) << 20 | (payload[11] & 0xFE) << 12 | (payload[12] & 0xFF) << 5 | (payload[13] & 0xFE) >>> 3; 47343 pes.pts *= 4; // Left shift by 2 47344 47345 pes.pts += (payload[13] & 0x06) >>> 1; // OR by the two LSBs 47346 47347 pes.dts = pes.pts; 47348 if (ptsDtsFlags & 0x40) { 47349 pes.dts = (payload[14] & 0x0E) << 27 | (payload[15] & 0xFF) << 20 | (payload[16] & 0xFE) << 12 | (payload[17] & 0xFF) << 5 | (payload[18] & 0xFE) >>> 3; 47350 pes.dts *= 4; // Left shift by 2 47351 47352 pes.dts += (payload[18] & 0x06) >>> 1; // OR by the two LSBs 47353 } 47354 } // the data section starts immediately after the PES header. 47355 // pes_header_data_length specifies the number of header bytes 47356 // that follow the last byte of the field. 47357 47358 pes.data = payload.subarray(9 + payload[8]); 47359 }, 47360 /** 47361 * Pass completely parsed PES packets to the next stream in the pipeline 47362 **/ 47363 flushStream = function (stream, type, forceFlush) { 47364 var packetData = new Uint8Array(stream.size), 47365 event = { 47366 type: type 47367 }, 47368 i = 0, 47369 offset = 0, 47370 packetFlushable = false, 47371 fragment; // do nothing if there is not enough buffered data for a complete 47372 // PES header 47373 47374 if (!stream.data.length || stream.size < 9) { 47375 return; 47376 } 47377 event.trackId = stream.data[0].pid; // reassemble the packet 47378 47379 for (i = 0; i < stream.data.length; i++) { 47380 fragment = stream.data[i]; 47381 packetData.set(fragment.data, offset); 47382 offset += fragment.data.byteLength; 47383 } // parse assembled packet's PES header 47384 47385 parsePes(packetData, event); // non-video PES packets MUST have a non-zero PES_packet_length 47386 // check that there is enough stream data to fill the packet 47387 47388 packetFlushable = type === 'video' || event.packetLength <= stream.size; // flush pending packets if the conditions are right 47389 47390 if (forceFlush || packetFlushable) { 47391 stream.size = 0; 47392 stream.data.length = 0; 47393 } // only emit packets that are complete. this is to avoid assembling 47394 // incomplete PES packets due to poor segmentation 47395 47396 if (packetFlushable) { 47397 self.trigger('data', event); 47398 } 47399 }; 47400 ElementaryStream.prototype.init.call(this); 47401 /** 47402 * Identifies M2TS packet types and parses PES packets using metadata 47403 * parsed from the PMT 47404 **/ 47405 47406 this.push = function (data) { 47407 ({ 47408 pat: function () {// we have to wait for the PMT to arrive as well before we 47409 // have any meaningful metadata 47410 }, 47411 pes: function () { 47412 var stream, streamType; 47413 switch (data.streamType) { 47414 case StreamTypes$2.H264_STREAM_TYPE: 47415 stream = video; 47416 streamType = 'video'; 47417 break; 47418 case StreamTypes$2.ADTS_STREAM_TYPE: 47419 stream = audio; 47420 streamType = 'audio'; 47421 break; 47422 case StreamTypes$2.METADATA_STREAM_TYPE: 47423 stream = timedMetadata; 47424 streamType = 'timed-metadata'; 47425 break; 47426 default: 47427 // ignore unknown stream types 47428 return; 47429 } // if a new packet is starting, we can flush the completed 47430 // packet 47431 47432 if (data.payloadUnitStartIndicator) { 47433 flushStream(stream, streamType, true); 47434 } // buffer this fragment until we are sure we've received the 47435 // complete payload 47436 47437 stream.data.push(data); 47438 stream.size += data.data.byteLength; 47439 }, 47440 pmt: function () { 47441 var event = { 47442 type: 'metadata', 47443 tracks: [] 47444 }; 47445 programMapTable = data.programMapTable; // translate audio and video streams to tracks 47446 47447 if (programMapTable.video !== null) { 47448 event.tracks.push({ 47449 timelineStartInfo: { 47450 baseMediaDecodeTime: 0 47451 }, 47452 id: +programMapTable.video, 47453 codec: 'avc', 47454 type: 'video' 47455 }); 47456 } 47457 if (programMapTable.audio !== null) { 47458 event.tracks.push({ 47459 timelineStartInfo: { 47460 baseMediaDecodeTime: 0 47461 }, 47462 id: +programMapTable.audio, 47463 codec: 'adts', 47464 type: 'audio' 47465 }); 47466 } 47467 segmentHadPmt = true; 47468 self.trigger('data', event); 47469 } 47470 })[data.type](); 47471 }; 47472 this.reset = function () { 47473 video.size = 0; 47474 video.data.length = 0; 47475 audio.size = 0; 47476 audio.data.length = 0; 47477 this.trigger('reset'); 47478 }; 47479 /** 47480 * Flush any remaining input. Video PES packets may be of variable 47481 * length. Normally, the start of a new video packet can trigger the 47482 * finalization of the previous packet. That is not possible if no 47483 * more video is forthcoming, however. In that case, some other 47484 * mechanism (like the end of the file) has to be employed. When it is 47485 * clear that no additional data is forthcoming, calling this method 47486 * will flush the buffered packets. 47487 */ 47488 47489 this.flushStreams_ = function () { 47490 // !!THIS ORDER IS IMPORTANT!! 47491 // video first then audio 47492 flushStream(video, 'video'); 47493 flushStream(audio, 'audio'); 47494 flushStream(timedMetadata, 'timed-metadata'); 47495 }; 47496 this.flush = function () { 47497 // if on flush we haven't had a pmt emitted 47498 // and we have a pmt to emit. emit the pmt 47499 // so that we trigger a trackinfo downstream. 47500 if (!segmentHadPmt && programMapTable) { 47501 var pmt = { 47502 type: 'metadata', 47503 tracks: [] 47504 }; // translate audio and video streams to tracks 47505 47506 if (programMapTable.video !== null) { 47507 pmt.tracks.push({ 47508 timelineStartInfo: { 47509 baseMediaDecodeTime: 0 47510 }, 47511 id: +programMapTable.video, 47512 codec: 'avc', 47513 type: 'video' 47514 }); 47515 } 47516 if (programMapTable.audio !== null) { 47517 pmt.tracks.push({ 47518 timelineStartInfo: { 47519 baseMediaDecodeTime: 0 47520 }, 47521 id: +programMapTable.audio, 47522 codec: 'adts', 47523 type: 'audio' 47524 }); 47525 } 47526 self.trigger('data', pmt); 47527 } 47528 segmentHadPmt = false; 47529 this.flushStreams_(); 47530 this.trigger('done'); 47531 }; 47532 }; 47533 ElementaryStream.prototype = new Stream$4(); 47534 var m2ts$1 = { 47535 PAT_PID: 0x0000, 47536 MP2T_PACKET_LENGTH: MP2T_PACKET_LENGTH$1, 47537 TransportPacketStream: TransportPacketStream, 47538 TransportParseStream: TransportParseStream, 47539 ElementaryStream: ElementaryStream, 47540 TimestampRolloverStream: TimestampRolloverStream, 47541 CaptionStream: CaptionStream$1.CaptionStream, 47542 Cea608Stream: CaptionStream$1.Cea608Stream, 47543 Cea708Stream: CaptionStream$1.Cea708Stream, 47544 MetadataStream: metadataStream 47545 }; 47546 for (var type in StreamTypes$2) { 47547 if (StreamTypes$2.hasOwnProperty(type)) { 47548 m2ts$1[type] = StreamTypes$2[type]; 47549 } 47550 } 47551 var m2ts_1 = m2ts$1; 47552 /** 47553 * mux.js 47554 * 47555 * Copyright (c) Brightcove 47556 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 47557 */ 47558 47559 var Stream$3 = stream; 47560 var ONE_SECOND_IN_TS$2 = clock$2.ONE_SECOND_IN_TS; 47561 var AdtsStream$1; 47562 var ADTS_SAMPLING_FREQUENCIES$1 = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350]; 47563 /* 47564 * Accepts a ElementaryStream and emits data events with parsed 47565 * AAC Audio Frames of the individual packets. Input audio in ADTS 47566 * format is unpacked and re-emitted as AAC frames. 47567 * 47568 * @see http://wiki.multimedia.cx/index.php?title=ADTS 47569 * @see http://wiki.multimedia.cx/?title=Understanding_AAC 47570 */ 47571 47572 AdtsStream$1 = function (handlePartialSegments) { 47573 var buffer, 47574 frameNum = 0; 47575 AdtsStream$1.prototype.init.call(this); 47576 this.skipWarn_ = function (start, end) { 47577 this.trigger('log', { 47578 level: 'warn', 47579 message: `adts skiping bytes ${start} to ${end} in frame ${frameNum} outside syncword` 47580 }); 47581 }; 47582 this.push = function (packet) { 47583 var i = 0, 47584 frameLength, 47585 protectionSkipBytes, 47586 oldBuffer, 47587 sampleCount, 47588 adtsFrameDuration; 47589 if (!handlePartialSegments) { 47590 frameNum = 0; 47591 } 47592 if (packet.type !== 'audio') { 47593 // ignore non-audio data 47594 return; 47595 } // Prepend any data in the buffer to the input data so that we can parse 47596 // aac frames the cross a PES packet boundary 47597 47598 if (buffer && buffer.length) { 47599 oldBuffer = buffer; 47600 buffer = new Uint8Array(oldBuffer.byteLength + packet.data.byteLength); 47601 buffer.set(oldBuffer); 47602 buffer.set(packet.data, oldBuffer.byteLength); 47603 } else { 47604 buffer = packet.data; 47605 } // unpack any ADTS frames which have been fully received 47606 // for details on the ADTS header, see http://wiki.multimedia.cx/index.php?title=ADTS 47607 47608 var skip; // We use i + 7 here because we want to be able to parse the entire header. 47609 // If we don't have enough bytes to do that, then we definitely won't have a full frame. 47610 47611 while (i + 7 < buffer.length) { 47612 // Look for the start of an ADTS header.. 47613 if (buffer[i] !== 0xFF || (buffer[i + 1] & 0xF6) !== 0xF0) { 47614 if (typeof skip !== 'number') { 47615 skip = i; 47616 } // If a valid header was not found, jump one forward and attempt to 47617 // find a valid ADTS header starting at the next byte 47618 47619 i++; 47620 continue; 47621 } 47622 if (typeof skip === 'number') { 47623 this.skipWarn_(skip, i); 47624 skip = null; 47625 } // The protection skip bit tells us if we have 2 bytes of CRC data at the 47626 // end of the ADTS header 47627 47628 protectionSkipBytes = (~buffer[i + 1] & 0x01) * 2; // Frame length is a 13 bit integer starting 16 bits from the 47629 // end of the sync sequence 47630 // NOTE: frame length includes the size of the header 47631 47632 frameLength = (buffer[i + 3] & 0x03) << 11 | buffer[i + 4] << 3 | (buffer[i + 5] & 0xe0) >> 5; 47633 sampleCount = ((buffer[i + 6] & 0x03) + 1) * 1024; 47634 adtsFrameDuration = sampleCount * ONE_SECOND_IN_TS$2 / ADTS_SAMPLING_FREQUENCIES$1[(buffer[i + 2] & 0x3c) >>> 2]; // If we don't have enough data to actually finish this ADTS frame, 47635 // then we have to wait for more data 47636 47637 if (buffer.byteLength - i < frameLength) { 47638 break; 47639 } // Otherwise, deliver the complete AAC frame 47640 47641 this.trigger('data', { 47642 pts: packet.pts + frameNum * adtsFrameDuration, 47643 dts: packet.dts + frameNum * adtsFrameDuration, 47644 sampleCount: sampleCount, 47645 audioobjecttype: (buffer[i + 2] >>> 6 & 0x03) + 1, 47646 channelcount: (buffer[i + 2] & 1) << 2 | (buffer[i + 3] & 0xc0) >>> 6, 47647 samplerate: ADTS_SAMPLING_FREQUENCIES$1[(buffer[i + 2] & 0x3c) >>> 2], 47648 samplingfrequencyindex: (buffer[i + 2] & 0x3c) >>> 2, 47649 // assume ISO/IEC 14496-12 AudioSampleEntry default of 16 47650 samplesize: 16, 47651 // data is the frame without it's header 47652 data: buffer.subarray(i + 7 + protectionSkipBytes, i + frameLength) 47653 }); 47654 frameNum++; 47655 i += frameLength; 47656 } 47657 if (typeof skip === 'number') { 47658 this.skipWarn_(skip, i); 47659 skip = null; 47660 } // remove processed bytes from the buffer. 47661 47662 buffer = buffer.subarray(i); 47663 }; 47664 this.flush = function () { 47665 frameNum = 0; 47666 this.trigger('done'); 47667 }; 47668 this.reset = function () { 47669 buffer = void 0; 47670 this.trigger('reset'); 47671 }; 47672 this.endTimeline = function () { 47673 buffer = void 0; 47674 this.trigger('endedtimeline'); 47675 }; 47676 }; 47677 AdtsStream$1.prototype = new Stream$3(); 47678 var adts = AdtsStream$1; 47679 /** 47680 * mux.js 47681 * 47682 * Copyright (c) Brightcove 47683 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 47684 */ 47685 47686 var ExpGolomb$1; 47687 /** 47688 * Parser for exponential Golomb codes, a variable-bitwidth number encoding 47689 * scheme used by h264. 47690 */ 47691 47692 ExpGolomb$1 = function (workingData) { 47693 var 47694 // the number of bytes left to examine in workingData 47695 workingBytesAvailable = workingData.byteLength, 47696 // the current word being examined 47697 workingWord = 0, 47698 // :uint 47699 // the number of bits left to examine in the current word 47700 workingBitsAvailable = 0; // :uint; 47701 // ():uint 47702 47703 this.length = function () { 47704 return 8 * workingBytesAvailable; 47705 }; // ():uint 47706 47707 this.bitsAvailable = function () { 47708 return 8 * workingBytesAvailable + workingBitsAvailable; 47709 }; // ():void 47710 47711 this.loadWord = function () { 47712 var position = workingData.byteLength - workingBytesAvailable, 47713 workingBytes = new Uint8Array(4), 47714 availableBytes = Math.min(4, workingBytesAvailable); 47715 if (availableBytes === 0) { 47716 throw new Error('no bytes available'); 47717 } 47718 workingBytes.set(workingData.subarray(position, position + availableBytes)); 47719 workingWord = new DataView(workingBytes.buffer).getUint32(0); // track the amount of workingData that has been processed 47720 47721 workingBitsAvailable = availableBytes * 8; 47722 workingBytesAvailable -= availableBytes; 47723 }; // (count:int):void 47724 47725 this.skipBits = function (count) { 47726 var skipBytes; // :int 47727 47728 if (workingBitsAvailable > count) { 47729 workingWord <<= count; 47730 workingBitsAvailable -= count; 47731 } else { 47732 count -= workingBitsAvailable; 47733 skipBytes = Math.floor(count / 8); 47734 count -= skipBytes * 8; 47735 workingBytesAvailable -= skipBytes; 47736 this.loadWord(); 47737 workingWord <<= count; 47738 workingBitsAvailable -= count; 47739 } 47740 }; // (size:int):uint 47741 47742 this.readBits = function (size) { 47743 var bits = Math.min(workingBitsAvailable, size), 47744 // :uint 47745 valu = workingWord >>> 32 - bits; // :uint 47746 // if size > 31, handle error 47747 47748 workingBitsAvailable -= bits; 47749 if (workingBitsAvailable > 0) { 47750 workingWord <<= bits; 47751 } else if (workingBytesAvailable > 0) { 47752 this.loadWord(); 47753 } 47754 bits = size - bits; 47755 if (bits > 0) { 47756 return valu << bits | this.readBits(bits); 47757 } 47758 return valu; 47759 }; // ():uint 47760 47761 this.skipLeadingZeros = function () { 47762 var leadingZeroCount; // :uint 47763 47764 for (leadingZeroCount = 0; leadingZeroCount < workingBitsAvailable; ++leadingZeroCount) { 47765 if ((workingWord & 0x80000000 >>> leadingZeroCount) !== 0) { 47766 // the first bit of working word is 1 47767 workingWord <<= leadingZeroCount; 47768 workingBitsAvailable -= leadingZeroCount; 47769 return leadingZeroCount; 47770 } 47771 } // we exhausted workingWord and still have not found a 1 47772 47773 this.loadWord(); 47774 return leadingZeroCount + this.skipLeadingZeros(); 47775 }; // ():void 47776 47777 this.skipUnsignedExpGolomb = function () { 47778 this.skipBits(1 + this.skipLeadingZeros()); 47779 }; // ():void 47780 47781 this.skipExpGolomb = function () { 47782 this.skipBits(1 + this.skipLeadingZeros()); 47783 }; // ():uint 47784 47785 this.readUnsignedExpGolomb = function () { 47786 var clz = this.skipLeadingZeros(); // :uint 47787 47788 return this.readBits(clz + 1) - 1; 47789 }; // ():int 47790 47791 this.readExpGolomb = function () { 47792 var valu = this.readUnsignedExpGolomb(); // :int 47793 47794 if (0x01 & valu) { 47795 // the number is odd if the low order bit is set 47796 return 1 + valu >>> 1; // add 1 to make it even, and divide by 2 47797 } 47798 47799 return -1 * (valu >>> 1); // divide by two then make it negative 47800 }; // Some convenience functions 47801 // :Boolean 47802 47803 this.readBoolean = function () { 47804 return this.readBits(1) === 1; 47805 }; // ():int 47806 47807 this.readUnsignedByte = function () { 47808 return this.readBits(8); 47809 }; 47810 this.loadWord(); 47811 }; 47812 var expGolomb = ExpGolomb$1; 47813 /** 47814 * mux.js 47815 * 47816 * Copyright (c) Brightcove 47817 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 47818 */ 47819 47820 var Stream$2 = stream; 47821 var ExpGolomb = expGolomb; 47822 var H264Stream$1, NalByteStream; 47823 var PROFILES_WITH_OPTIONAL_SPS_DATA; 47824 /** 47825 * Accepts a NAL unit byte stream and unpacks the embedded NAL units. 47826 */ 47827 47828 NalByteStream = function () { 47829 var syncPoint = 0, 47830 i, 47831 buffer; 47832 NalByteStream.prototype.init.call(this); 47833 /* 47834 * Scans a byte stream and triggers a data event with the NAL units found. 47835 * @param {Object} data Event received from H264Stream 47836 * @param {Uint8Array} data.data The h264 byte stream to be scanned 47837 * 47838 * @see H264Stream.push 47839 */ 47840 47841 this.push = function (data) { 47842 var swapBuffer; 47843 if (!buffer) { 47844 buffer = data.data; 47845 } else { 47846 swapBuffer = new Uint8Array(buffer.byteLength + data.data.byteLength); 47847 swapBuffer.set(buffer); 47848 swapBuffer.set(data.data, buffer.byteLength); 47849 buffer = swapBuffer; 47850 } 47851 var len = buffer.byteLength; // Rec. ITU-T H.264, Annex B 47852 // scan for NAL unit boundaries 47853 // a match looks like this: 47854 // 0 0 1 .. NAL .. 0 0 1 47855 // ^ sync point ^ i 47856 // or this: 47857 // 0 0 1 .. NAL .. 0 0 0 47858 // ^ sync point ^ i 47859 // advance the sync point to a NAL start, if necessary 47860 47861 for (; syncPoint < len - 3; syncPoint++) { 47862 if (buffer[syncPoint + 2] === 1) { 47863 // the sync point is properly aligned 47864 i = syncPoint + 5; 47865 break; 47866 } 47867 } 47868 while (i < len) { 47869 // look at the current byte to determine if we've hit the end of 47870 // a NAL unit boundary 47871 switch (buffer[i]) { 47872 case 0: 47873 // skip past non-sync sequences 47874 if (buffer[i - 1] !== 0) { 47875 i += 2; 47876 break; 47877 } else if (buffer[i - 2] !== 0) { 47878 i++; 47879 break; 47880 } // deliver the NAL unit if it isn't empty 47881 47882 if (syncPoint + 3 !== i - 2) { 47883 this.trigger('data', buffer.subarray(syncPoint + 3, i - 2)); 47884 } // drop trailing zeroes 47885 47886 do { 47887 i++; 47888 } while (buffer[i] !== 1 && i < len); 47889 syncPoint = i - 2; 47890 i += 3; 47891 break; 47892 case 1: 47893 // skip past non-sync sequences 47894 if (buffer[i - 1] !== 0 || buffer[i - 2] !== 0) { 47895 i += 3; 47896 break; 47897 } // deliver the NAL unit 47898 47899 this.trigger('data', buffer.subarray(syncPoint + 3, i - 2)); 47900 syncPoint = i - 2; 47901 i += 3; 47902 break; 47903 default: 47904 // the current byte isn't a one or zero, so it cannot be part 47905 // of a sync sequence 47906 i += 3; 47907 break; 47908 } 47909 } // filter out the NAL units that were delivered 47910 47911 buffer = buffer.subarray(syncPoint); 47912 i -= syncPoint; 47913 syncPoint = 0; 47914 }; 47915 this.reset = function () { 47916 buffer = null; 47917 syncPoint = 0; 47918 this.trigger('reset'); 47919 }; 47920 this.flush = function () { 47921 // deliver the last buffered NAL unit 47922 if (buffer && buffer.byteLength > 3) { 47923 this.trigger('data', buffer.subarray(syncPoint + 3)); 47924 } // reset the stream state 47925 47926 buffer = null; 47927 syncPoint = 0; 47928 this.trigger('done'); 47929 }; 47930 this.endTimeline = function () { 47931 this.flush(); 47932 this.trigger('endedtimeline'); 47933 }; 47934 }; 47935 NalByteStream.prototype = new Stream$2(); // values of profile_idc that indicate additional fields are included in the SPS 47936 // see Recommendation ITU-T H.264 (4/2013), 47937 // 7.3.2.1.1 Sequence parameter set data syntax 47938 47939 PROFILES_WITH_OPTIONAL_SPS_DATA = { 47940 100: true, 47941 110: true, 47942 122: true, 47943 244: true, 47944 44: true, 47945 83: true, 47946 86: true, 47947 118: true, 47948 128: true, 47949 // TODO: the three profiles below don't 47950 // appear to have sps data in the specificiation anymore? 47951 138: true, 47952 139: true, 47953 134: true 47954 }; 47955 /** 47956 * Accepts input from a ElementaryStream and produces H.264 NAL unit data 47957 * events. 47958 */ 47959 47960 H264Stream$1 = function () { 47961 var nalByteStream = new NalByteStream(), 47962 self, 47963 trackId, 47964 currentPts, 47965 currentDts, 47966 discardEmulationPreventionBytes, 47967 readSequenceParameterSet, 47968 skipScalingList; 47969 H264Stream$1.prototype.init.call(this); 47970 self = this; 47971 /* 47972 * Pushes a packet from a stream onto the NalByteStream 47973 * 47974 * @param {Object} packet - A packet received from a stream 47975 * @param {Uint8Array} packet.data - The raw bytes of the packet 47976 * @param {Number} packet.dts - Decode timestamp of the packet 47977 * @param {Number} packet.pts - Presentation timestamp of the packet 47978 * @param {Number} packet.trackId - The id of the h264 track this packet came from 47979 * @param {('video'|'audio')} packet.type - The type of packet 47980 * 47981 */ 47982 47983 this.push = function (packet) { 47984 if (packet.type !== 'video') { 47985 return; 47986 } 47987 trackId = packet.trackId; 47988 currentPts = packet.pts; 47989 currentDts = packet.dts; 47990 nalByteStream.push(packet); 47991 }; 47992 /* 47993 * Identify NAL unit types and pass on the NALU, trackId, presentation and decode timestamps 47994 * for the NALUs to the next stream component. 47995 * Also, preprocess caption and sequence parameter NALUs. 47996 * 47997 * @param {Uint8Array} data - A NAL unit identified by `NalByteStream.push` 47998 * @see NalByteStream.push 47999 */ 48000 48001 nalByteStream.on('data', function (data) { 48002 var event = { 48003 trackId: trackId, 48004 pts: currentPts, 48005 dts: currentDts, 48006 data: data, 48007 nalUnitTypeCode: data[0] & 0x1f 48008 }; 48009 switch (event.nalUnitTypeCode) { 48010 case 0x05: 48011 event.nalUnitType = 'slice_layer_without_partitioning_rbsp_idr'; 48012 break; 48013 case 0x06: 48014 event.nalUnitType = 'sei_rbsp'; 48015 event.escapedRBSP = discardEmulationPreventionBytes(data.subarray(1)); 48016 break; 48017 case 0x07: 48018 event.nalUnitType = 'seq_parameter_set_rbsp'; 48019 event.escapedRBSP = discardEmulationPreventionBytes(data.subarray(1)); 48020 event.config = readSequenceParameterSet(event.escapedRBSP); 48021 break; 48022 case 0x08: 48023 event.nalUnitType = 'pic_parameter_set_rbsp'; 48024 break; 48025 case 0x09: 48026 event.nalUnitType = 'access_unit_delimiter_rbsp'; 48027 break; 48028 } // This triggers data on the H264Stream 48029 48030 self.trigger('data', event); 48031 }); 48032 nalByteStream.on('done', function () { 48033 self.trigger('done'); 48034 }); 48035 nalByteStream.on('partialdone', function () { 48036 self.trigger('partialdone'); 48037 }); 48038 nalByteStream.on('reset', function () { 48039 self.trigger('reset'); 48040 }); 48041 nalByteStream.on('endedtimeline', function () { 48042 self.trigger('endedtimeline'); 48043 }); 48044 this.flush = function () { 48045 nalByteStream.flush(); 48046 }; 48047 this.partialFlush = function () { 48048 nalByteStream.partialFlush(); 48049 }; 48050 this.reset = function () { 48051 nalByteStream.reset(); 48052 }; 48053 this.endTimeline = function () { 48054 nalByteStream.endTimeline(); 48055 }; 48056 /** 48057 * Advance the ExpGolomb decoder past a scaling list. The scaling 48058 * list is optionally transmitted as part of a sequence parameter 48059 * set and is not relevant to transmuxing. 48060 * @param count {number} the number of entries in this scaling list 48061 * @param expGolombDecoder {object} an ExpGolomb pointed to the 48062 * start of a scaling list 48063 * @see Recommendation ITU-T H.264, Section 7.3.2.1.1.1 48064 */ 48065 48066 skipScalingList = function (count, expGolombDecoder) { 48067 var lastScale = 8, 48068 nextScale = 8, 48069 j, 48070 deltaScale; 48071 for (j = 0; j < count; j++) { 48072 if (nextScale !== 0) { 48073 deltaScale = expGolombDecoder.readExpGolomb(); 48074 nextScale = (lastScale + deltaScale + 256) % 256; 48075 } 48076 lastScale = nextScale === 0 ? lastScale : nextScale; 48077 } 48078 }; 48079 /** 48080 * Expunge any "Emulation Prevention" bytes from a "Raw Byte 48081 * Sequence Payload" 48082 * @param data {Uint8Array} the bytes of a RBSP from a NAL 48083 * unit 48084 * @return {Uint8Array} the RBSP without any Emulation 48085 * Prevention Bytes 48086 */ 48087 48088 discardEmulationPreventionBytes = function (data) { 48089 var length = data.byteLength, 48090 emulationPreventionBytesPositions = [], 48091 i = 1, 48092 newLength, 48093 newData; // Find all `Emulation Prevention Bytes` 48094 48095 while (i < length - 2) { 48096 if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0x03) { 48097 emulationPreventionBytesPositions.push(i + 2); 48098 i += 2; 48099 } else { 48100 i++; 48101 } 48102 } // If no Emulation Prevention Bytes were found just return the original 48103 // array 48104 48105 if (emulationPreventionBytesPositions.length === 0) { 48106 return data; 48107 } // Create a new array to hold the NAL unit data 48108 48109 newLength = length - emulationPreventionBytesPositions.length; 48110 newData = new Uint8Array(newLength); 48111 var sourceIndex = 0; 48112 for (i = 0; i < newLength; sourceIndex++, i++) { 48113 if (sourceIndex === emulationPreventionBytesPositions[0]) { 48114 // Skip this byte 48115 sourceIndex++; // Remove this position index 48116 48117 emulationPreventionBytesPositions.shift(); 48118 } 48119 newData[i] = data[sourceIndex]; 48120 } 48121 return newData; 48122 }; 48123 /** 48124 * Read a sequence parameter set and return some interesting video 48125 * properties. A sequence parameter set is the H264 metadata that 48126 * describes the properties of upcoming video frames. 48127 * @param data {Uint8Array} the bytes of a sequence parameter set 48128 * @return {object} an object with configuration parsed from the 48129 * sequence parameter set, including the dimensions of the 48130 * associated video frames. 48131 */ 48132 48133 readSequenceParameterSet = function (data) { 48134 var frameCropLeftOffset = 0, 48135 frameCropRightOffset = 0, 48136 frameCropTopOffset = 0, 48137 frameCropBottomOffset = 0, 48138 expGolombDecoder, 48139 profileIdc, 48140 levelIdc, 48141 profileCompatibility, 48142 chromaFormatIdc, 48143 picOrderCntType, 48144 numRefFramesInPicOrderCntCycle, 48145 picWidthInMbsMinus1, 48146 picHeightInMapUnitsMinus1, 48147 frameMbsOnlyFlag, 48148 scalingListCount, 48149 sarRatio = [1, 1], 48150 aspectRatioIdc, 48151 i; 48152 expGolombDecoder = new ExpGolomb(data); 48153 profileIdc = expGolombDecoder.readUnsignedByte(); // profile_idc 48154 48155 profileCompatibility = expGolombDecoder.readUnsignedByte(); // constraint_set[0-5]_flag 48156 48157 levelIdc = expGolombDecoder.readUnsignedByte(); // level_idc u(8) 48158 48159 expGolombDecoder.skipUnsignedExpGolomb(); // seq_parameter_set_id 48160 // some profiles have more optional data we don't need 48161 48162 if (PROFILES_WITH_OPTIONAL_SPS_DATA[profileIdc]) { 48163 chromaFormatIdc = expGolombDecoder.readUnsignedExpGolomb(); 48164 if (chromaFormatIdc === 3) { 48165 expGolombDecoder.skipBits(1); // separate_colour_plane_flag 48166 }
vendor: 10,354 bytes, lines 48167-48435
48167 48168 expGolombDecoder.skipUnsignedExpGolomb(); // bit_depth_luma_minus8 48169 48170 expGolombDecoder.skipUnsignedExpGolomb(); // bit_depth_chroma_minus8 48171 48172 expGolombDecoder.skipBits(1); // qpprime_y_zero_transform_bypass_flag 48173 48174 if (expGolombDecoder.readBoolean()) { 48175 // seq_scaling_matrix_present_flag 48176 scalingListCount = chromaFormatIdc !== 3 ? 8 : 12; 48177 for (i = 0; i < scalingListCount; i++) { 48178 if (expGolombDecoder.readBoolean()) { 48179 // seq_scaling_list_present_flag[ i ] 48180 if (i < 6) { 48181 skipScalingList(16, expGolombDecoder); 48182 } else { 48183 skipScalingList(64, expGolombDecoder); 48184 } 48185 } 48186 } 48187 } 48188 } 48189 expGolombDecoder.skipUnsignedExpGolomb(); // log2_max_frame_num_minus4 48190 48191 picOrderCntType = expGolombDecoder.readUnsignedExpGolomb(); 48192 if (picOrderCntType === 0) { 48193 expGolombDecoder.readUnsignedExpGolomb(); // log2_max_pic_order_cnt_lsb_minus4 48194 } else if (picOrderCntType === 1) { 48195 expGolombDecoder.skipBits(1); // delta_pic_order_always_zero_flag 48196 48197 expGolombDecoder.skipExpGolomb(); // offset_for_non_ref_pic 48198 48199 expGolombDecoder.skipExpGolomb(); // offset_for_top_to_bottom_field 48200 48201 numRefFramesInPicOrderCntCycle = expGolombDecoder.readUnsignedExpGolomb(); 48202 for (i = 0; i < numRefFramesInPicOrderCntCycle; i++) { 48203 expGolombDecoder.skipExpGolomb(); // offset_for_ref_frame[ i ] 48204 } 48205 } 48206 48207 expGolombDecoder.skipUnsignedExpGolomb(); // max_num_ref_frames 48208 48209 expGolombDecoder.skipBits(1); // gaps_in_frame_num_value_allowed_flag 48210 48211 picWidthInMbsMinus1 = expGolombDecoder.readUnsignedExpGolomb(); 48212 picHeightInMapUnitsMinus1 = expGolombDecoder.readUnsignedExpGolomb(); 48213 frameMbsOnlyFlag = expGolombDecoder.readBits(1); 48214 if (frameMbsOnlyFlag === 0) { 48215 expGolombDecoder.skipBits(1); // mb_adaptive_frame_field_flag 48216 } 48217 48218 expGolombDecoder.skipBits(1); // direct_8x8_inference_flag 48219 48220 if (expGolombDecoder.readBoolean()) { 48221 // frame_cropping_flag 48222 frameCropLeftOffset = expGolombDecoder.readUnsignedExpGolomb(); 48223 frameCropRightOffset = expGolombDecoder.readUnsignedExpGolomb(); 48224 frameCropTopOffset = expGolombDecoder.readUnsignedExpGolomb(); 48225 frameCropBottomOffset = expGolombDecoder.readUnsignedExpGolomb(); 48226 } 48227 if (expGolombDecoder.readBoolean()) { 48228 // vui_parameters_present_flag 48229 if (expGolombDecoder.readBoolean()) { 48230 // aspect_ratio_info_present_flag 48231 aspectRatioIdc = expGolombDecoder.readUnsignedByte(); 48232 switch (aspectRatioIdc) { 48233 case 1: 48234 sarRatio = [1, 1]; 48235 break; 48236 case 2: 48237 sarRatio = [12, 11]; 48238 break; 48239 case 3: 48240 sarRatio = [10, 11]; 48241 break; 48242 case 4: 48243 sarRatio = [16, 11]; 48244 break; 48245 case 5: 48246 sarRatio = [40, 33]; 48247 break; 48248 case 6: 48249 sarRatio = [24, 11]; 48250 break; 48251 case 7: 48252 sarRatio = [20, 11]; 48253 break; 48254 case 8: 48255 sarRatio = [32, 11]; 48256 break; 48257 case 9: 48258 sarRatio = [80, 33]; 48259 break; 48260 case 10: 48261 sarRatio = [18, 11]; 48262 break; 48263 case 11: 48264 sarRatio = [15, 11]; 48265 break; 48266 case 12: 48267 sarRatio = [64, 33]; 48268 break; 48269 case 13: 48270 sarRatio = [160, 99]; 48271 break; 48272 case 14: 48273 sarRatio = [4, 3]; 48274 break; 48275 case 15: 48276 sarRatio = [3, 2]; 48277 break; 48278 case 16: 48279 sarRatio = [2, 1]; 48280 break; 48281 case 255: 48282 { 48283 sarRatio = [expGolombDecoder.readUnsignedByte() << 8 | expGolombDecoder.readUnsignedByte(), expGolombDecoder.readUnsignedByte() << 8 | expGolombDecoder.readUnsignedByte()]; 48284 break; 48285 } 48286 } 48287 if (sarRatio) { 48288 sarRatio[0] / sarRatio[1]; 48289 } 48290 } 48291 } 48292 return { 48293 profileIdc: profileIdc, 48294 levelIdc: levelIdc, 48295 profileCompatibility: profileCompatibility, 48296 width: (picWidthInMbsMinus1 + 1) * 16 - frameCropLeftOffset * 2 - frameCropRightOffset * 2, 48297 height: (2 - frameMbsOnlyFlag) * (picHeightInMapUnitsMinus1 + 1) * 16 - frameCropTopOffset * 2 - frameCropBottomOffset * 2, 48298 // sar is sample aspect ratio 48299 sarRatio: sarRatio 48300 }; 48301 }; 48302 }; 48303 H264Stream$1.prototype = new Stream$2(); 48304 var h264 = { 48305 H264Stream: H264Stream$1, 48306 NalByteStream: NalByteStream 48307 }; 48308 /** 48309 * mux.js 48310 * 48311 * Copyright (c) Brightcove 48312 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 48313 * 48314 * Utilities to detect basic properties and metadata about Aac data. 48315 */ 48316 48317 var ADTS_SAMPLING_FREQUENCIES = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350]; 48318 var parseId3TagSize = function (header, byteIndex) { 48319 var returnSize = header[byteIndex + 6] << 21 | header[byteIndex + 7] << 14 | header[byteIndex + 8] << 7 | header[byteIndex + 9], 48320 flags = header[byteIndex + 5], 48321 footerPresent = (flags & 16) >> 4; // if we get a negative returnSize clamp it to 0 48322 48323 returnSize = returnSize >= 0 ? returnSize : 0; 48324 if (footerPresent) { 48325 return returnSize + 20; 48326 } 48327 return returnSize + 10; 48328 }; 48329 var getId3Offset = function (data, offset) { 48330 if (data.length - offset < 10 || data[offset] !== 'I'.charCodeAt(0) || data[offset + 1] !== 'D'.charCodeAt(0) || data[offset + 2] !== '3'.charCodeAt(0)) { 48331 return offset; 48332 } 48333 offset += parseId3TagSize(data, offset); 48334 return getId3Offset(data, offset); 48335 }; // TODO: use vhs-utils 48336 48337 var isLikelyAacData$1 = function (data) { 48338 var offset = getId3Offset(data, 0); 48339 return data.length >= offset + 2 && (data[offset] & 0xFF) === 0xFF && (data[offset + 1] & 0xF0) === 0xF0 && 48340 // verify that the 2 layer bits are 0, aka this 48341 // is not mp3 data but aac data. 48342 (data[offset + 1] & 0x16) === 0x10; 48343 }; 48344 var parseSyncSafeInteger = function (data) { 48345 return data[0] << 21 | data[1] << 14 | data[2] << 7 | data[3]; 48346 }; // return a percent-encoded representation of the specified byte range 48347 // @see http://en.wikipedia.org/wiki/Percent-encoding 48348 48349 var percentEncode = function (bytes, start, end) { 48350 var i, 48351 result = ''; 48352 for (i = start; i < end; i++) { 48353 result += '%' + ('00' + bytes[i].toString(16)).slice(-2); 48354 } 48355 return result; 48356 }; // return the string representation of the specified byte range, 48357 // interpreted as ISO-8859-1. 48358 48359 var parseIso88591 = function (bytes, start, end) { 48360 return unescape(percentEncode(bytes, start, end)); // jshint ignore:line 48361 }; 48362 48363 var parseAdtsSize = function (header, byteIndex) { 48364 var lowThree = (header[byteIndex + 5] & 0xE0) >> 5, 48365 middle = header[byteIndex + 4] << 3, 48366 highTwo = header[byteIndex + 3] & 0x3 << 11; 48367 return highTwo | middle | lowThree; 48368 }; 48369 var parseType$4 = function (header, byteIndex) { 48370 if (header[byteIndex] === 'I'.charCodeAt(0) && header[byteIndex + 1] === 'D'.charCodeAt(0) && header[byteIndex + 2] === '3'.charCodeAt(0)) { 48371 return 'timed-metadata'; 48372 } else if (header[byteIndex] & 0xff === 0xff && (header[byteIndex + 1] & 0xf0) === 0xf0) { 48373 return 'audio'; 48374 } 48375 return null; 48376 }; 48377 var parseSampleRate = function (packet) { 48378 var i = 0; 48379 while (i + 5 < packet.length) { 48380 if (packet[i] !== 0xFF || (packet[i + 1] & 0xF6) !== 0xF0) { 48381 // If a valid header was not found, jump one forward and attempt to 48382 // find a valid ADTS header starting at the next byte 48383 i++; 48384 continue; 48385 } 48386 return ADTS_SAMPLING_FREQUENCIES[(packet[i + 2] & 0x3c) >>> 2]; 48387 } 48388 return null; 48389 }; 48390 var parseAacTimestamp = function (packet) { 48391 var frameStart, frameSize, frame, frameHeader; // find the start of the first frame and the end of the tag 48392 48393 frameStart = 10; 48394 if (packet[5] & 0x40) { 48395 // advance the frame start past the extended header 48396 frameStart += 4; // header size field 48397 48398 frameStart += parseSyncSafeInteger(packet.subarray(10, 14)); 48399 } // parse one or more ID3 frames 48400 // http://id3.org/id3v2.3.0#ID3v2_frame_overview 48401 48402 do { 48403 // determine the number of bytes in this frame 48404 frameSize = parseSyncSafeInteger(packet.subarray(frameStart + 4, frameStart + 8)); 48405 if (frameSize < 1) { 48406 return null; 48407 } 48408 frameHeader = String.fromCharCode(packet[frameStart], packet[frameStart + 1], packet[frameStart + 2], packet[frameStart + 3]); 48409 if (frameHeader === 'PRIV') { 48410 frame = packet.subarray(frameStart + 10, frameStart + frameSize + 10); 48411 for (var i = 0; i < frame.byteLength; i++) { 48412 if (frame[i] === 0) { 48413 var owner = parseIso88591(frame, 0, i); 48414 if (owner === 'com.apple.streaming.transportStreamTimestamp') { 48415 var d = frame.subarray(i + 1); 48416 var size = (d[3] & 0x01) << 30 | d[4] << 22 | d[5] << 14 | d[6] << 6 | d[7] >>> 2; 48417 size *= 4; 48418 size += d[7] & 0x03; 48419 return size; 48420 } 48421 break; 48422 } 48423 } 48424 } 48425 frameStart += 10; // advance past the frame header 48426 48427 frameStart += frameSize; // advance past the frame body 48428 } while (frameStart < packet.byteLength); 48429 return null; 48430 }; 48431 var utils = { 48432 isLikelyAacData: isLikelyAacData$1, 48433 parseId3TagSize: parseId3TagSize, 48434 parseAdtsSize: parseAdtsSize, 48435 parseType: parseType$4
vendor: 17,397 bytes, lines 48435-48865
48435, 48436 parseSampleRate: parseSampleRate, 48437 parseAacTimestamp: parseAacTimestamp 48438 }; 48439 /** 48440 * mux.js 48441 * 48442 * Copyright (c) Brightcove 48443 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 48444 * 48445 * A stream-based aac to mp4 converter. This utility can be used to 48446 * deliver mp4s to a SourceBuffer on platforms that support native 48447 * Media Source Extensions. 48448 */ 48449 48450 var Stream$1 = stream; 48451 var aacUtils = utils; // Constants 48452 48453 var AacStream$1; 48454 /** 48455 * Splits an incoming stream of binary data into ADTS and ID3 Frames. 48456 */ 48457 48458 AacStream$1 = function () { 48459 var everything = new Uint8Array(), 48460 timeStamp = 0; 48461 AacStream$1.prototype.init.call(this); 48462 this.setTimestamp = function (timestamp) { 48463 timeStamp = timestamp; 48464 }; 48465 this.push = function (bytes) { 48466 var frameSize = 0, 48467 byteIndex = 0, 48468 bytesLeft, 48469 chunk, 48470 packet, 48471 tempLength; // If there are bytes remaining from the last segment, prepend them to the 48472 // bytes that were pushed in 48473 48474 if (everything.length) { 48475 tempLength = everything.length; 48476 everything = new Uint8Array(bytes.byteLength + tempLength); 48477 everything.set(everything.subarray(0, tempLength)); 48478 everything.set(bytes, tempLength); 48479 } else { 48480 everything = bytes; 48481 } 48482 while (everything.length - byteIndex >= 3) { 48483 if (everything[byteIndex] === 'I'.charCodeAt(0) && everything[byteIndex + 1] === 'D'.charCodeAt(0) && everything[byteIndex + 2] === '3'.charCodeAt(0)) { 48484 // Exit early because we don't have enough to parse 48485 // the ID3 tag header 48486 if (everything.length - byteIndex < 10) { 48487 break; 48488 } // check framesize 48489 48490 frameSize = aacUtils.parseId3TagSize(everything, byteIndex); // Exit early if we don't have enough in the buffer 48491 // to emit a full packet 48492 // Add to byteIndex to support multiple ID3 tags in sequence 48493 48494 if (byteIndex + frameSize > everything.length) { 48495 break; 48496 } 48497 chunk = { 48498 type: 'timed-metadata', 48499 data: everything.subarray(byteIndex, byteIndex + frameSize) 48500 }; 48501 this.trigger('data', chunk); 48502 byteIndex += frameSize; 48503 continue; 48504 } else if ((everything[byteIndex] & 0xff) === 0xff && (everything[byteIndex + 1] & 0xf0) === 0xf0) { 48505 // Exit early because we don't have enough to parse 48506 // the ADTS frame header 48507 if (everything.length - byteIndex < 7) { 48508 break; 48509 } 48510 frameSize = aacUtils.parseAdtsSize(everything, byteIndex); // Exit early if we don't have enough in the buffer 48511 // to emit a full packet 48512 48513 if (byteIndex + frameSize > everything.length) { 48514 break; 48515 } 48516 packet = { 48517 type: 'audio', 48518 data: everything.subarray(byteIndex, byteIndex + frameSize), 48519 pts: timeStamp, 48520 dts: timeStamp 48521 }; 48522 this.trigger('data', packet); 48523 byteIndex += frameSize; 48524 continue; 48525 } 48526 byteIndex++; 48527 } 48528 bytesLeft = everything.length - byteIndex; 48529 if (bytesLeft > 0) { 48530 everything = everything.subarray(byteIndex); 48531 } else { 48532 everything = new Uint8Array(); 48533 } 48534 }; 48535 this.reset = function () { 48536 everything = new Uint8Array(); 48537 this.trigger('reset'); 48538 }; 48539 this.endTimeline = function () { 48540 everything = new Uint8Array(); 48541 this.trigger('endedtimeline'); 48542 }; 48543 }; 48544 AacStream$1.prototype = new Stream$1(); 48545 var aac = AacStream$1; 48546 var AUDIO_PROPERTIES$1 = ['audioobjecttype', 'channelcount', 'samplerate', 'samplingfrequencyindex', 'samplesize']; 48547 var audioProperties = AUDIO_PROPERTIES$1; 48548 var VIDEO_PROPERTIES$1 = ['width', 'height', 'profileIdc', 'levelIdc', 'profileCompatibility', 'sarRatio']; 48549 var videoProperties = VIDEO_PROPERTIES$1; 48550 /** 48551 * mux.js 48552 * 48553 * Copyright (c) Brightcove 48554 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 48555 * 48556 * A stream-based mp2t to mp4 converter. This utility can be used to 48557 * deliver mp4s to a SourceBuffer on platforms that support native 48558 * Media Source Extensions. 48559 */ 48560 48561 var Stream = stream; 48562 var mp4 = mp4Generator; 48563 var frameUtils = frameUtils$1; 48564 var audioFrameUtils = audioFrameUtils$1; 48565 var trackDecodeInfo = trackDecodeInfo$1; 48566 var m2ts = m2ts_1; 48567 var clock = clock$2; 48568 var AdtsStream = adts; 48569 var H264Stream = h264.H264Stream; 48570 var AacStream = aac; 48571 var isLikelyAacData = utils.isLikelyAacData; 48572 var ONE_SECOND_IN_TS$1 = clock$2.ONE_SECOND_IN_TS; 48573 var AUDIO_PROPERTIES = audioProperties; 48574 var VIDEO_PROPERTIES = videoProperties; // object types 48575 48576 var VideoSegmentStream, AudioSegmentStream, Transmuxer, CoalesceStream; 48577 var retriggerForStream = function (key, event) { 48578 event.stream = key; 48579 this.trigger('log', event); 48580 }; 48581 var addPipelineLogRetriggers = function (transmuxer, pipeline) { 48582 var keys = Object.keys(pipeline); 48583 for (var i = 0; i < keys.length; i++) { 48584 var key = keys[i]; // skip non-stream keys and headOfPipeline 48585 // which is just a duplicate 48586 48587 if (key === 'headOfPipeline' || !pipeline[key].on) { 48588 continue; 48589 } 48590 pipeline[key].on('log', retriggerForStream.bind(transmuxer, key)); 48591 } 48592 }; 48593 /** 48594 * Compare two arrays (even typed) for same-ness 48595 */ 48596 48597 var arrayEquals = function (a, b) { 48598 var i; 48599 if (a.length !== b.length) { 48600 return false; 48601 } // compare the value of each element in the array 48602 48603 for (i = 0; i < a.length; i++) { 48604 if (a[i] !== b[i]) { 48605 return false; 48606 } 48607 } 48608 return true; 48609 }; 48610 var generateSegmentTimingInfo = function (baseMediaDecodeTime, startDts, startPts, endDts, endPts, prependedContentDuration) { 48611 var ptsOffsetFromDts = startPts - startDts, 48612 decodeDuration = endDts - startDts, 48613 presentationDuration = endPts - startPts; // The PTS and DTS values are based on the actual stream times from the segment, 48614 // however, the player time values will reflect a start from the baseMediaDecodeTime. 48615 // In order to provide relevant values for the player times, base timing info on the 48616 // baseMediaDecodeTime and the DTS and PTS durations of the segment. 48617 48618 return { 48619 start: { 48620 dts: baseMediaDecodeTime, 48621 pts: baseMediaDecodeTime + ptsOffsetFromDts 48622 }, 48623 end: { 48624 dts: baseMediaDecodeTime + decodeDuration, 48625 pts: baseMediaDecodeTime + presentationDuration 48626 }, 48627 prependedContentDuration: prependedContentDuration, 48628 baseMediaDecodeTime: baseMediaDecodeTime 48629 }; 48630 }; 48631 /** 48632 * Constructs a single-track, ISO BMFF media segment from AAC data 48633 * events. The output of this stream can be fed to a SourceBuffer 48634 * configured with a suitable initialization segment. 48635 * @param track {object} track metadata configuration 48636 * @param options {object} transmuxer options object 48637 * @param options.keepOriginalTimestamps {boolean} If true, keep the timestamps 48638 * in the source; false to adjust the first segment to start at 0. 48639 */ 48640 48641 AudioSegmentStream = function (track, options) { 48642 var adtsFrames = [], 48643 sequenceNumber, 48644 earliestAllowedDts = 0, 48645 audioAppendStartTs = 0, 48646 videoBaseMediaDecodeTime = Infinity; 48647 options = options || {}; 48648 sequenceNumber = options.firstSequenceNumber || 0; 48649 AudioSegmentStream.prototype.init.call(this); 48650 this.push = function (data) { 48651 trackDecodeInfo.collectDtsInfo(track, data); 48652 if (track) { 48653 AUDIO_PROPERTIES.forEach(function (prop) { 48654 track[prop] = data[prop]; 48655 }); 48656 } // buffer audio data until end() is called 48657 48658 adtsFrames.push(data); 48659 }; 48660 this.setEarliestDts = function (earliestDts) { 48661 earliestAllowedDts = earliestDts; 48662 }; 48663 this.setVideoBaseMediaDecodeTime = function (baseMediaDecodeTime) { 48664 videoBaseMediaDecodeTime = baseMediaDecodeTime; 48665 }; 48666 this.setAudioAppendStart = function (timestamp) { 48667 audioAppendStartTs = timestamp; 48668 }; 48669 this.flush = function () { 48670 var frames, moof, mdat, boxes, frameDuration, segmentDuration, videoClockCyclesOfSilencePrefixed; // return early if no audio data has been observed 48671 48672 if (adtsFrames.length === 0) { 48673 this.trigger('done', 'AudioSegmentStream'); 48674 return; 48675 } 48676 frames = audioFrameUtils.trimAdtsFramesByEarliestDts(adtsFrames, track, earliestAllowedDts); 48677 track.baseMediaDecodeTime = trackDecodeInfo.calculateTrackBaseMediaDecodeTime(track, options.keepOriginalTimestamps); // amount of audio filled but the value is in video clock rather than audio clock 48678 48679 videoClockCyclesOfSilencePrefixed = audioFrameUtils.prefixWithSilence(track, frames, audioAppendStartTs, videoBaseMediaDecodeTime); // we have to build the index from byte locations to 48680 // samples (that is, adts frames) in the audio data 48681 48682 track.samples = audioFrameUtils.generateSampleTable(frames); // concatenate the audio data to constuct the mdat 48683 48684 mdat = mp4.mdat(audioFrameUtils.concatenateFrameData(frames)); 48685 adtsFrames = []; 48686 moof = mp4.moof(sequenceNumber, [track]); 48687 boxes = new Uint8Array(moof.byteLength + mdat.byteLength); // bump the sequence number for next time 48688 48689 sequenceNumber++; 48690 boxes.set(moof); 48691 boxes.set(mdat, moof.byteLength); 48692 trackDecodeInfo.clearDtsInfo(track); 48693 frameDuration = Math.ceil(ONE_SECOND_IN_TS$1 * 1024 / track.samplerate); // TODO this check was added to maintain backwards compatibility (particularly with 48694 // tests) on adding the timingInfo event. However, it seems unlikely that there's a 48695 // valid use-case where an init segment/data should be triggered without associated 48696 // frames. Leaving for now, but should be looked into. 48697 48698 if (frames.length) { 48699 segmentDuration = frames.length * frameDuration; 48700 this.trigger('segmentTimingInfo', generateSegmentTimingInfo( 48701 // The audio track's baseMediaDecodeTime is in audio clock cycles, but the 48702 // frame info is in video clock cycles. Convert to match expectation of 48703 // listeners (that all timestamps will be based on video clock cycles). 48704 clock.audioTsToVideoTs(track.baseMediaDecodeTime, track.samplerate), 48705 // frame times are already in video clock, as is segment duration 48706 frames[0].dts, frames[0].pts, frames[0].dts + segmentDuration, frames[0].pts + segmentDuration, videoClockCyclesOfSilencePrefixed || 0)); 48707 this.trigger('timingInfo', { 48708 start: frames[0].pts, 48709 end: frames[0].pts + segmentDuration 48710 }); 48711 } 48712 this.trigger('data', { 48713 track: track, 48714 boxes: boxes 48715 }); 48716 this.trigger('done', 'AudioSegmentStream'); 48717 }; 48718 this.reset = function () { 48719 trackDecodeInfo.clearDtsInfo(track); 48720 adtsFrames = []; 48721 this.trigger('reset'); 48722 }; 48723 }; 48724 AudioSegmentStream.prototype = new Stream(); 48725 /** 48726 * Constructs a single-track, ISO BMFF media segment from H264 data 48727 * events. The output of this stream can be fed to a SourceBuffer 48728 * configured with a suitable initialization segment. 48729 * @param track {object} track metadata configuration 48730 * @param options {object} transmuxer options object 48731 * @param options.alignGopsAtEnd {boolean} If true, start from the end of the 48732 * gopsToAlignWith list when attempting to align gop pts 48733 * @param options.keepOriginalTimestamps {boolean} If true, keep the timestamps 48734 * in the source; false to adjust the first segment to start at 0. 48735 */ 48736 48737 VideoSegmentStream = function (track, options) { 48738 var sequenceNumber, 48739 nalUnits = [], 48740 gopsToAlignWith = [], 48741 config, 48742 pps; 48743 options = options || {}; 48744 sequenceNumber = options.firstSequenceNumber || 0; 48745 VideoSegmentStream.prototype.init.call(this); 48746 delete track.minPTS; 48747 this.gopCache_ = []; 48748 /** 48749 * Constructs a ISO BMFF segment given H264 nalUnits 48750 * @param {Object} nalUnit A data event representing a nalUnit 48751 * @param {String} nalUnit.nalUnitType 48752 * @param {Object} nalUnit.config Properties for a mp4 track 48753 * @param {Uint8Array} nalUnit.data The nalUnit bytes 48754 * @see lib/codecs/h264.js 48755 **/ 48756 48757 this.push = function (nalUnit) { 48758 trackDecodeInfo.collectDtsInfo(track, nalUnit); // record the track config 48759 48760 if (nalUnit.nalUnitType === 'seq_parameter_set_rbsp' && !config) { 48761 config = nalUnit.config; 48762 track.sps = [nalUnit.data]; 48763 VIDEO_PROPERTIES.forEach(function (prop) { 48764 track[prop] = config[prop]; 48765 }, this); 48766 } 48767 if (nalUnit.nalUnitType === 'pic_parameter_set_rbsp' && !pps) { 48768 pps = nalUnit.data; 48769 track.pps = [nalUnit.data]; 48770 } // buffer video until flush() is called 48771 48772 nalUnits.push(nalUnit); 48773 }; 48774 /** 48775 * Pass constructed ISO BMFF track and boxes on to the 48776 * next stream in the pipeline 48777 **/ 48778 48779 this.flush = function () { 48780 var frames, 48781 gopForFusion, 48782 gops, 48783 moof, 48784 mdat, 48785 boxes, 48786 prependedContentDuration = 0, 48787 firstGop, 48788 lastGop; // Throw away nalUnits at the start of the byte stream until 48789 // we find the first AUD 48790 48791 while (nalUnits.length) { 48792 if (nalUnits[0].nalUnitType === 'access_unit_delimiter_rbsp') { 48793 break; 48794 } 48795 nalUnits.shift(); 48796 } // Return early if no video data has been observed 48797 48798 if (nalUnits.length === 0) { 48799 this.resetStream_(); 48800 this.trigger('done', 'VideoSegmentStream'); 48801 return; 48802 } // Organize the raw nal-units into arrays that represent 48803 // higher-level constructs such as frames and gops 48804 // (group-of-pictures) 48805 48806 frames = frameUtils.groupNalsIntoFrames(nalUnits); 48807 gops = frameUtils.groupFramesIntoGops(frames); // If the first frame of this fragment is not a keyframe we have 48808 // a problem since MSE (on Chrome) requires a leading keyframe. 48809 // 48810 // We have two approaches to repairing this situation: 48811 // 1) GOP-FUSION: 48812 // This is where we keep track of the GOPS (group-of-pictures) 48813 // from previous fragments and attempt to find one that we can 48814 // prepend to the current fragment in order to create a valid 48815 // fragment. 48816 // 2) KEYFRAME-PULLING: 48817 // Here we search for the first keyframe in the fragment and 48818 // throw away all the frames between the start of the fragment 48819 // and that keyframe. We then extend the duration and pull the 48820 // PTS of the keyframe forward so that it covers the time range 48821 // of the frames that were disposed of. 48822 // 48823 // #1 is far prefereable over #2 which can cause "stuttering" but 48824 // requires more things to be just right. 48825 48826 if (!gops[0][0].keyFrame) { 48827 // Search for a gop for fusion from our gopCache 48828 gopForFusion = this.getGopForFusion_(nalUnits[0], track); 48829 if (gopForFusion) { 48830 // in order to provide more accurate timing information about the segment, save 48831 // the number of seconds prepended to the original segment due to GOP fusion 48832 prependedContentDuration = gopForFusion.duration; 48833 gops.unshift(gopForFusion); // Adjust Gops' metadata to account for the inclusion of the 48834 // new gop at the beginning 48835 48836 gops.byteLength += gopForFusion.byteLength; 48837 gops.nalCount += gopForFusion.nalCount; 48838 gops.pts = gopForFusion.pts; 48839 gops.dts = gopForFusion.dts; 48840 gops.duration += gopForFusion.duration; 48841 } else { 48842 // If we didn't find a candidate gop fall back to keyframe-pulling 48843 gops = frameUtils.extendFirstKeyFrame(gops); 48844 } 48845 } // Trim gops to align with gopsToAlignWith 48846 48847 if (gopsToAlignWith.length) { 48848 var alignedGops; 48849 if (options.alignGopsAtEnd) { 48850 alignedGops = this.alignGopsAtEnd_(gops); 48851 } else { 48852 alignedGops = this.alignGopsAtStart_(gops); 48853 } 48854 if (!alignedGops) { 48855 // save all the nals in the last GOP into the gop cache 48856 this.gopCache_.unshift({ 48857 gop: gops.pop(), 48858 pps: track.pps, 48859 sps: track.sps 48860 }); // Keep a maximum of 6 GOPs in the cache 48861 48862 this.gopCache_.length = Math.min(6, this.gopCache_.length); // Clear nalUnits 48863 48864 nalUnits = []; // return early no gops can be aligned with desired gopsToAlignWith 48865
vendor: 6,878 bytes, lines 48866-49031
48866 this.resetStream_(); 48867 this.trigger('done', 'VideoSegmentStream'); 48868 return; 48869 } // Some gops were trimmed. clear dts info so minSegmentDts and pts are correct 48870 // when recalculated before sending off to CoalesceStream 48871 48872 trackDecodeInfo.clearDtsInfo(track); 48873 gops = alignedGops; 48874 } 48875 trackDecodeInfo.collectDtsInfo(track, gops); // First, we have to build the index from byte locations to 48876 // samples (that is, frames) in the video data 48877 48878 track.samples = frameUtils.generateSampleTable(gops); // Concatenate the video data and construct the mdat 48879 48880 mdat = mp4.mdat(frameUtils.concatenateNalData(gops)); 48881 track.baseMediaDecodeTime = trackDecodeInfo.calculateTrackBaseMediaDecodeTime(track, options.keepOriginalTimestamps); 48882 this.trigger('processedGopsInfo', gops.map(function (gop) { 48883 return { 48884 pts: gop.pts, 48885 dts: gop.dts, 48886 byteLength: gop.byteLength 48887 }; 48888 })); 48889 firstGop = gops[0]; 48890 lastGop = gops[gops.length - 1]; 48891 this.trigger('segmentTimingInfo', generateSegmentTimingInfo(track.baseMediaDecodeTime, firstGop.dts, firstGop.pts, lastGop.dts + lastGop.duration, lastGop.pts + lastGop.duration, prependedContentDuration)); 48892 this.trigger('timingInfo', { 48893 start: gops[0].pts, 48894 end: gops[gops.length - 1].pts + gops[gops.length - 1].duration 48895 }); // save all the nals in the last GOP into the gop cache 48896 48897 this.gopCache_.unshift({ 48898 gop: gops.pop(), 48899 pps: track.pps, 48900 sps: track.sps 48901 }); // Keep a maximum of 6 GOPs in the cache 48902 48903 this.gopCache_.length = Math.min(6, this.gopCache_.length); // Clear nalUnits 48904 48905 nalUnits = []; 48906 this.trigger('baseMediaDecodeTime', track.baseMediaDecodeTime); 48907 this.trigger('timelineStartInfo', track.timelineStartInfo); 48908 moof = mp4.moof(sequenceNumber, [track]); // it would be great to allocate this array up front instead of 48909 // throwing away hundreds of media segment fragments 48910 48911 boxes = new Uint8Array(moof.byteLength + mdat.byteLength); // Bump the sequence number for next time 48912 48913 sequenceNumber++; 48914 boxes.set(moof); 48915 boxes.set(mdat, moof.byteLength); 48916 this.trigger('data', { 48917 track: track, 48918 boxes: boxes 48919 }); 48920 this.resetStream_(); // Continue with the flush process now 48921 48922 this.trigger('done', 'VideoSegmentStream'); 48923 }; 48924 this.reset = function () { 48925 this.resetStream_(); 48926 nalUnits = []; 48927 this.gopCache_.length = 0; 48928 gopsToAlignWith.length = 0; 48929 this.trigger('reset'); 48930 }; 48931 this.resetStream_ = function () { 48932 trackDecodeInfo.clearDtsInfo(track); // reset config and pps because they may differ across segments 48933 // for instance, when we are rendition switching 48934 48935 config = undefined; 48936 pps = undefined; 48937 }; // Search for a candidate Gop for gop-fusion from the gop cache and 48938 // return it or return null if no good candidate was found 48939 48940 this.getGopForFusion_ = function (nalUnit) { 48941 var halfSecond = 45000, 48942 // Half-a-second in a 90khz clock 48943 allowableOverlap = 10000, 48944 // About 3 frames @ 30fps 48945 nearestDistance = Infinity, 48946 dtsDistance, 48947 nearestGopObj, 48948 currentGop, 48949 currentGopObj, 48950 i; // Search for the GOP nearest to the beginning of this nal unit 48951 48952 for (i = 0; i < this.gopCache_.length; i++) { 48953 currentGopObj = this.gopCache_[i]; 48954 currentGop = currentGopObj.gop; // Reject Gops with different SPS or PPS 48955 48956 if (!(track.pps && arrayEquals(track.pps[0], currentGopObj.pps[0])) || !(track.sps && arrayEquals(track.sps[0], currentGopObj.sps[0]))) { 48957 continue; 48958 } // Reject Gops that would require a negative baseMediaDecodeTime 48959 48960 if (currentGop.dts < track.timelineStartInfo.dts) { 48961 continue; 48962 } // The distance between the end of the gop and the start of the nalUnit 48963 48964 dtsDistance = nalUnit.dts - currentGop.dts - currentGop.duration; // Only consider GOPS that start before the nal unit and end within 48965 // a half-second of the nal unit 48966 48967 if (dtsDistance >= -allowableOverlap && dtsDistance <= halfSecond) { 48968 // Always use the closest GOP we found if there is more than 48969 // one candidate 48970 if (!nearestGopObj || nearestDistance > dtsDistance) { 48971 nearestGopObj = currentGopObj; 48972 nearestDistance = dtsDistance; 48973 } 48974 } 48975 } 48976 if (nearestGopObj) { 48977 return nearestGopObj.gop; 48978 } 48979 return null; 48980 }; // trim gop list to the first gop found that has a matching pts with a gop in the list 48981 // of gopsToAlignWith starting from the START of the list 48982 48983 this.alignGopsAtStart_ = function (gops) { 48984 var alignIndex, gopIndex, align, gop, byteLength, nalCount, duration, alignedGops; 48985 byteLength = gops.byteLength; 48986 nalCount = gops.nalCount; 48987 duration = gops.duration; 48988 alignIndex = gopIndex = 0; 48989 while (alignIndex < gopsToAlignWith.length && gopIndex < gops.length) { 48990 align = gopsToAlignWith[alignIndex]; 48991 gop = gops[gopIndex]; 48992 if (align.pts === gop.pts) { 48993 break; 48994 } 48995 if (gop.pts > align.pts) { 48996 // this current gop starts after the current gop we want to align on, so increment 48997 // align index 48998 alignIndex++; 48999 continue; 49000 } // current gop starts before the current gop we want to align on. so increment gop 49001 // index 49002 49003 gopIndex++; 49004 byteLength -= gop.byteLength; 49005 nalCount -= gop.nalCount; 49006 duration -= gop.duration; 49007 } 49008 if (gopIndex === 0) { 49009 // no gops to trim 49010 return gops; 49011 } 49012 if (gopIndex === gops.length) { 49013 // all gops trimmed, skip appending all gops 49014 return null; 49015 } 49016 alignedGops = gops.slice(gopIndex); 49017 alignedGops.byteLength = byteLength; 49018 alignedGops.duration = duration; 49019 alignedGops.nalCount = nalCount; 49020 alignedGops.pts = alignedGops[0].pts; 49021 alignedGops.dts = alignedGops[0].dts; 49022 return alignedGops; 49023 }; // trim gop list to the first gop found that has a matching pts with a gop in the list 49024 // of gopsToAlignWith starting from the END of the list 49025 49026 this.alignGopsAtEnd_ = function (gops) { 49027 var alignIndex, gopIndex, align, gop, alignEndIndex, matchFound; 49028 alignIndex = gopsToAlignWith.length - 1; 49029 gopIndex = gops.length - 1; 49030 alignEndIndex = null; 49031 matchFound = false;
vendor: 22,196 bytes, lines 49032-49524
49032 while (alignIndex >= 0 && gopIndex >= 0) { 49033 align = gopsToAlignWith[alignIndex]; 49034 gop = gops[gopIndex]; 49035 if (align.pts === gop.pts) { 49036 matchFound = true; 49037 break; 49038 } 49039 if (align.pts > gop.pts) { 49040 alignIndex--; 49041 continue; 49042 } 49043 if (alignIndex === gopsToAlignWith.length - 1) { 49044 // gop.pts is greater than the last alignment candidate. If no match is found 49045 // by the end of this loop, we still want to append gops that come after this 49046 // point 49047 alignEndIndex = gopIndex; 49048 } 49049 gopIndex--; 49050 } 49051 if (!matchFound && alignEndIndex === null) { 49052 return null; 49053 } 49054 var trimIndex; 49055 if (matchFound) { 49056 trimIndex = gopIndex; 49057 } else { 49058 trimIndex = alignEndIndex; 49059 } 49060 if (trimIndex === 0) { 49061 return gops; 49062 } 49063 var alignedGops = gops.slice(trimIndex); 49064 var metadata = alignedGops.reduce(function (total, gop) { 49065 total.byteLength += gop.byteLength; 49066 total.duration += gop.duration; 49067 total.nalCount += gop.nalCount; 49068 return total; 49069 }, { 49070 byteLength: 0, 49071 duration: 0, 49072 nalCount: 0 49073 }); 49074 alignedGops.byteLength = metadata.byteLength; 49075 alignedGops.duration = metadata.duration; 49076 alignedGops.nalCount = metadata.nalCount; 49077 alignedGops.pts = alignedGops[0].pts; 49078 alignedGops.dts = alignedGops[0].dts; 49079 return alignedGops; 49080 }; 49081 this.alignGopsWith = function (newGopsToAlignWith) { 49082 gopsToAlignWith = newGopsToAlignWith; 49083 }; 49084 }; 49085 VideoSegmentStream.prototype = new Stream(); 49086 /** 49087 * A Stream that can combine multiple streams (ie. audio & video) 49088 * into a single output segment for MSE. Also supports audio-only 49089 * and video-only streams. 49090 * @param options {object} transmuxer options object 49091 * @param options.keepOriginalTimestamps {boolean} If true, keep the timestamps 49092 * in the source; false to adjust the first segment to start at media timeline start. 49093 */ 49094 49095 CoalesceStream = function (options, metadataStream) { 49096 // Number of Tracks per output segment 49097 // If greater than 1, we combine multiple 49098 // tracks into a single segment 49099 this.numberOfTracks = 0; 49100 this.metadataStream = metadataStream; 49101 options = options || {}; 49102 if (typeof options.remux !== 'undefined') { 49103 this.remuxTracks = !!options.remux; 49104 } else { 49105 this.remuxTracks = true; 49106 } 49107 if (typeof options.keepOriginalTimestamps === 'boolean') { 49108 this.keepOriginalTimestamps = options.keepOriginalTimestamps; 49109 } else { 49110 this.keepOriginalTimestamps = false; 49111 } 49112 this.pendingTracks = []; 49113 this.videoTrack = null; 49114 this.pendingBoxes = []; 49115 this.pendingCaptions = []; 49116 this.pendingMetadata = []; 49117 this.pendingBytes = 0; 49118 this.emittedTracks = 0; 49119 CoalesceStream.prototype.init.call(this); // Take output from multiple 49120 49121 this.push = function (output) { 49122 // buffer incoming captions until the associated video segment 49123 // finishes 49124 if (output.content || output.text) { 49125 return this.pendingCaptions.push(output); 49126 } // buffer incoming id3 tags until the final flush 49127 49128 if (output.frames) { 49129 return this.pendingMetadata.push(output); 49130 } // Add this track to the list of pending tracks and store 49131 // important information required for the construction of 49132 // the final segment 49133 49134 this.pendingTracks.push(output.track); 49135 this.pendingBytes += output.boxes.byteLength; // TODO: is there an issue for this against chrome? 49136 // We unshift audio and push video because 49137 // as of Chrome 75 when switching from 49138 // one init segment to another if the video 49139 // mdat does not appear after the audio mdat 49140 // only audio will play for the duration of our transmux. 49141 49142 if (output.track.type === 'video') { 49143 this.videoTrack = output.track; 49144 this.pendingBoxes.push(output.boxes); 49145 } 49146 if (output.track.type === 'audio') { 49147 this.audioTrack = output.track; 49148 this.pendingBoxes.unshift(output.boxes); 49149 } 49150 }; 49151 }; 49152 CoalesceStream.prototype = new Stream(); 49153 CoalesceStream.prototype.flush = function (flushSource) { 49154 var offset = 0, 49155 event = { 49156 captions: [], 49157 captionStreams: {}, 49158 metadata: [], 49159 info: {} 49160 }, 49161 caption, 49162 id3, 49163 initSegment, 49164 timelineStartPts = 0, 49165 i; 49166 if (this.pendingTracks.length < this.numberOfTracks) { 49167 if (flushSource !== 'VideoSegmentStream' && flushSource !== 'AudioSegmentStream') { 49168 // Return because we haven't received a flush from a data-generating 49169 // portion of the segment (meaning that we have only recieved meta-data 49170 // or captions.) 49171 return; 49172 } else if (this.remuxTracks) { 49173 // Return until we have enough tracks from the pipeline to remux (if we 49174 // are remuxing audio and video into a single MP4) 49175 return; 49176 } else if (this.pendingTracks.length === 0) { 49177 // In the case where we receive a flush without any data having been 49178 // received we consider it an emitted track for the purposes of coalescing 49179 // `done` events. 49180 // We do this for the case where there is an audio and video track in the 49181 // segment but no audio data. (seen in several playlists with alternate 49182 // audio tracks and no audio present in the main TS segments.) 49183 this.emittedTracks++; 49184 if (this.emittedTracks >= this.numberOfTracks) { 49185 this.trigger('done'); 49186 this.emittedTracks = 0; 49187 } 49188 return; 49189 } 49190 } 49191 if (this.videoTrack) { 49192 timelineStartPts = this.videoTrack.timelineStartInfo.pts; 49193 VIDEO_PROPERTIES.forEach(function (prop) { 49194 event.info[prop] = this.videoTrack[prop]; 49195 }, this); 49196 } else if (this.audioTrack) { 49197 timelineStartPts = this.audioTrack.timelineStartInfo.pts; 49198 AUDIO_PROPERTIES.forEach(function (prop) { 49199 event.info[prop] = this.audioTrack[prop]; 49200 }, this); 49201 } 49202 if (this.videoTrack || this.audioTrack) { 49203 if (this.pendingTracks.length === 1) { 49204 event.type = this.pendingTracks[0].type; 49205 } else { 49206 event.type = 'combined'; 49207 } 49208 this.emittedTracks += this.pendingTracks.length; 49209 initSegment = mp4.initSegment(this.pendingTracks); // Create a new typed array to hold the init segment 49210 49211 event.initSegment = new Uint8Array(initSegment.byteLength); // Create an init segment containing a moov 49212 // and track definitions 49213 49214 event.initSegment.set(initSegment); // Create a new typed array to hold the moof+mdats 49215 49216 event.data = new Uint8Array(this.pendingBytes); // Append each moof+mdat (one per track) together 49217 49218 for (i = 0; i < this.pendingBoxes.length; i++) { 49219 event.data.set(this.pendingBoxes[i], offset); 49220 offset += this.pendingBoxes[i].byteLength; 49221 } // Translate caption PTS times into second offsets to match the 49222 // video timeline for the segment, and add track info 49223 49224 for (i = 0; i < this.pendingCaptions.length; i++) { 49225 caption = this.pendingCaptions[i]; 49226 caption.startTime = clock.metadataTsToSeconds(caption.startPts, timelineStartPts, this.keepOriginalTimestamps); 49227 caption.endTime = clock.metadataTsToSeconds(caption.endPts, timelineStartPts, this.keepOriginalTimestamps); 49228 event.captionStreams[caption.stream] = true; 49229 event.captions.push(caption); 49230 } // Translate ID3 frame PTS times into second offsets to match the 49231 // video timeline for the segment 49232 49233 for (i = 0; i < this.pendingMetadata.length; i++) { 49234 id3 = this.pendingMetadata[i]; 49235 id3.cueTime = clock.metadataTsToSeconds(id3.pts, timelineStartPts, this.keepOriginalTimestamps); 49236 event.metadata.push(id3); 49237 } // We add this to every single emitted segment even though we only need 49238 // it for the first 49239 49240 event.metadata.dispatchType = this.metadataStream.dispatchType; // Reset stream state 49241 49242 this.pendingTracks.length = 0; 49243 this.videoTrack = null; 49244 this.pendingBoxes.length = 0; 49245 this.pendingCaptions.length = 0; 49246 this.pendingBytes = 0; 49247 this.pendingMetadata.length = 0; // Emit the built segment 49248 // We include captions and ID3 tags for backwards compatibility, 49249 // ideally we should send only video and audio in the data event 49250 49251 this.trigger('data', event); // Emit each caption to the outside world 49252 // Ideally, this would happen immediately on parsing captions, 49253 // but we need to ensure that video data is sent back first 49254 // so that caption timing can be adjusted to match video timing 49255 49256 for (i = 0; i < event.captions.length; i++) { 49257 caption = event.captions[i]; 49258 this.trigger('caption', caption); 49259 } // Emit each id3 tag to the outside world 49260 // Ideally, this would happen immediately on parsing the tag, 49261 // but we need to ensure that video data is sent back first 49262 // so that ID3 frame timing can be adjusted to match video timing 49263 49264 for (i = 0; i < event.metadata.length; i++) { 49265 id3 = event.metadata[i]; 49266 this.trigger('id3Frame', id3); 49267 } 49268 } // Only emit `done` if all tracks have been flushed and emitted 49269 49270 if (this.emittedTracks >= this.numberOfTracks) { 49271 this.trigger('done'); 49272 this.emittedTracks = 0; 49273 } 49274 }; 49275 CoalesceStream.prototype.setRemux = function (val) { 49276 this.remuxTracks = val; 49277 }; 49278 /** 49279 * A Stream that expects MP2T binary data as input and produces 49280 * corresponding media segments, suitable for use with Media Source 49281 * Extension (MSE) implementations that support the ISO BMFF byte 49282 * stream format, like Chrome. 49283 */ 49284 49285 Transmuxer = function (options) { 49286 var self = this, 49287 hasFlushed = true, 49288 videoTrack, 49289 audioTrack; 49290 Transmuxer.prototype.init.call(this); 49291 options = options || {}; 49292 this.baseMediaDecodeTime = options.baseMediaDecodeTime || 0; 49293 this.transmuxPipeline_ = {}; 49294 this.setupAacPipeline = function () { 49295 var pipeline = {}; 49296 this.transmuxPipeline_ = pipeline; 49297 pipeline.type = 'aac'; 49298 pipeline.metadataStream = new m2ts.MetadataStream(); // set up the parsing pipeline 49299 49300 pipeline.aacStream = new AacStream(); 49301 pipeline.audioTimestampRolloverStream = new m2ts.TimestampRolloverStream('audio'); 49302 pipeline.timedMetadataTimestampRolloverStream = new m2ts.TimestampRolloverStream('timed-metadata'); 49303 pipeline.adtsStream = new AdtsStream(); 49304 pipeline.coalesceStream = new CoalesceStream(options, pipeline.metadataStream); 49305 pipeline.headOfPipeline = pipeline.aacStream; 49306 pipeline.aacStream.pipe(pipeline.audioTimestampRolloverStream).pipe(pipeline.adtsStream); 49307 pipeline.aacStream.pipe(pipeline.timedMetadataTimestampRolloverStream).pipe(pipeline.metadataStream).pipe(pipeline.coalesceStream); 49308 pipeline.metadataStream.on('timestamp', function (frame) { 49309 pipeline.aacStream.setTimestamp(frame.timeStamp); 49310 }); 49311 pipeline.aacStream.on('data', function (data) { 49312 if (data.type !== 'timed-metadata' && data.type !== 'audio' || pipeline.audioSegmentStream) { 49313 return; 49314 } 49315 audioTrack = audioTrack || { 49316 timelineStartInfo: { 49317 baseMediaDecodeTime: self.baseMediaDecodeTime 49318 }, 49319 codec: 'adts', 49320 type: 'audio' 49321 }; // hook up the audio segment stream to the first track with aac data 49322 49323 pipeline.coalesceStream.numberOfTracks++; 49324 pipeline.audioSegmentStream = new AudioSegmentStream(audioTrack, options); 49325 pipeline.audioSegmentStream.on('log', self.getLogTrigger_('audioSegmentStream')); 49326 pipeline.audioSegmentStream.on('timingInfo', self.trigger.bind(self, 'audioTimingInfo')); // Set up the final part of the audio pipeline 49327 49328 pipeline.adtsStream.pipe(pipeline.audioSegmentStream).pipe(pipeline.coalesceStream); // emit pmt info 49329 49330 self.trigger('trackinfo', { 49331 hasAudio: !!audioTrack, 49332 hasVideo: !!videoTrack 49333 }); 49334 }); // Re-emit any data coming from the coalesce stream to the outside world 49335 49336 pipeline.coalesceStream.on('data', this.trigger.bind(this, 'data')); // Let the consumer know we have finished flushing the entire pipeline 49337 49338 pipeline.coalesceStream.on('done', this.trigger.bind(this, 'done')); 49339 addPipelineLogRetriggers(this, pipeline); 49340 }; 49341 this.setupTsPipeline = function () { 49342 var pipeline = {}; 49343 this.transmuxPipeline_ = pipeline; 49344 pipeline.type = 'ts'; 49345 pipeline.metadataStream = new m2ts.MetadataStream(); // set up the parsing pipeline 49346 49347 pipeline.packetStream = new m2ts.TransportPacketStream(); 49348 pipeline.parseStream = new m2ts.TransportParseStream(); 49349 pipeline.elementaryStream = new m2ts.ElementaryStream(); 49350 pipeline.timestampRolloverStream = new m2ts.TimestampRolloverStream(); 49351 pipeline.adtsStream = new AdtsStream(); 49352 pipeline.h264Stream = new H264Stream(); 49353 pipeline.captionStream = new m2ts.CaptionStream(options); 49354 pipeline.coalesceStream = new CoalesceStream(options, pipeline.metadataStream); 49355 pipeline.headOfPipeline = pipeline.packetStream; // disassemble MPEG2-TS packets into elementary streams 49356 49357 pipeline.packetStream.pipe(pipeline.parseStream).pipe(pipeline.elementaryStream).pipe(pipeline.timestampRolloverStream); // !!THIS ORDER IS IMPORTANT!! 49358 // demux the streams 49359 49360 pipeline.timestampRolloverStream.pipe(pipeline.h264Stream); 49361 pipeline.timestampRolloverStream.pipe(pipeline.adtsStream); 49362 pipeline.timestampRolloverStream.pipe(pipeline.metadataStream).pipe(pipeline.coalesceStream); // Hook up CEA-608/708 caption stream 49363 49364 pipeline.h264Stream.pipe(pipeline.captionStream).pipe(pipeline.coalesceStream); 49365 pipeline.elementaryStream.on('data', function (data) { 49366 var i; 49367 if (data.type === 'metadata') { 49368 i = data.tracks.length; // scan the tracks listed in the metadata 49369 49370 while (i--) { 49371 if (!videoTrack && data.tracks[i].type === 'video') { 49372 videoTrack = data.tracks[i]; 49373 videoTrack.timelineStartInfo.baseMediaDecodeTime = self.baseMediaDecodeTime; 49374 } else if (!audioTrack && data.tracks[i].type === 'audio') { 49375 audioTrack = data.tracks[i]; 49376 audioTrack.timelineStartInfo.baseMediaDecodeTime = self.baseMediaDecodeTime; 49377 } 49378 } // hook up the video segment stream to the first track with h264 data 49379 49380 if (videoTrack && !pipeline.videoSegmentStream) { 49381 pipeline.coalesceStream.numberOfTracks++; 49382 pipeline.videoSegmentStream = new VideoSegmentStream(videoTrack, options); 49383 pipeline.videoSegmentStream.on('log', self.getLogTrigger_('videoSegmentStream')); 49384 pipeline.videoSegmentStream.on('timelineStartInfo', function (timelineStartInfo) { 49385 // When video emits timelineStartInfo data after a flush, we forward that 49386 // info to the AudioSegmentStream, if it exists, because video timeline 49387 // data takes precedence. Do not do this if keepOriginalTimestamps is set, 49388 // because this is a particularly subtle form of timestamp alteration. 49389 if (audioTrack && !options.keepOriginalTimestamps) { 49390 audioTrack.timelineStartInfo = timelineStartInfo; // On the first segment we trim AAC frames that exist before the 49391 // very earliest DTS we have seen in video because Chrome will 49392 // interpret any video track with a baseMediaDecodeTime that is 49393 // non-zero as a gap. 49394 49395 pipeline.audioSegmentStream.setEarliestDts(timelineStartInfo.dts - self.baseMediaDecodeTime); 49396 } 49397 }); 49398 pipeline.videoSegmentStream.on('processedGopsInfo', self.trigger.bind(self, 'gopInfo')); 49399 pipeline.videoSegmentStream.on('segmentTimingInfo', self.trigger.bind(self, 'videoSegmentTimingInfo')); 49400 pipeline.videoSegmentStream.on('baseMediaDecodeTime', function (baseMediaDecodeTime) { 49401 if (audioTrack) { 49402 pipeline.audioSegmentStream.setVideoBaseMediaDecodeTime(baseMediaDecodeTime); 49403 } 49404 }); 49405 pipeline.videoSegmentStream.on('timingInfo', self.trigger.bind(self, 'videoTimingInfo')); // Set up the final part of the video pipeline 49406 49407 pipeline.h264Stream.pipe(pipeline.videoSegmentStream).pipe(pipeline.coalesceStream); 49408 } 49409 if (audioTrack && !pipeline.audioSegmentStream) { 49410 // hook up the audio segment stream to the first track with aac data 49411 pipeline.coalesceStream.numberOfTracks++; 49412 pipeline.audioSegmentStream = new AudioSegmentStream(audioTrack, options); 49413 pipeline.audioSegmentStream.on('log', self.getLogTrigger_('audioSegmentStream')); 49414 pipeline.audioSegmentStream.on('timingInfo', self.trigger.bind(self, 'audioTimingInfo')); 49415 pipeline.audioSegmentStream.on('segmentTimingInfo', self.trigger.bind(self, 'audioSegmentTimingInfo')); // Set up the final part of the audio pipeline 49416 49417 pipeline.adtsStream.pipe(pipeline.audioSegmentStream).pipe(pipeline.coalesceStream); 49418 } // emit pmt info 49419 49420 self.trigger('trackinfo', { 49421 hasAudio: !!audioTrack, 49422 hasVideo: !!videoTrack 49423 }); 49424 } 49425 }); // Re-emit any data coming from the coalesce stream to the outside world 49426 49427 pipeline.coalesceStream.on('data', this.trigger.bind(this, 'data')); 49428 pipeline.coalesceStream.on('id3Frame', function (id3Frame) { 49429 id3Frame.dispatchType = pipeline.metadataStream.dispatchType; 49430 self.trigger('id3Frame', id3Frame); 49431 }); 49432 pipeline.coalesceStream.on('caption', this.trigger.bind(this, 'caption')); // Let the consumer know we have finished flushing the entire pipeline 49433 49434 pipeline.coalesceStream.on('done', this.trigger.bind(this, 'done')); 49435 addPipelineLogRetriggers(this, pipeline); 49436 }; // hook up the segment streams once track metadata is delivered 49437 49438 this.setBaseMediaDecodeTime = function (baseMediaDecodeTime) { 49439 var pipeline = this.transmuxPipeline_; 49440 if (!options.keepOriginalTimestamps) { 49441 this.baseMediaDecodeTime = baseMediaDecodeTime; 49442 } 49443 if (audioTrack) { 49444 audioTrack.timelineStartInfo.dts = undefined; 49445 audioTrack.timelineStartInfo.pts = undefined; 49446 trackDecodeInfo.clearDtsInfo(audioTrack); 49447 if (pipeline.audioTimestampRolloverStream) { 49448 pipeline.audioTimestampRolloverStream.discontinuity(); 49449 } 49450 } 49451 if (videoTrack) { 49452 if (pipeline.videoSegmentStream) { 49453 pipeline.videoSegmentStream.gopCache_ = []; 49454 } 49455 videoTrack.timelineStartInfo.dts = undefined; 49456 videoTrack.timelineStartInfo.pts = undefined; 49457 trackDecodeInfo.clearDtsInfo(videoTrack); 49458 pipeline.captionStream.reset(); 49459 } 49460 if (pipeline.timestampRolloverStream) { 49461 pipeline.timestampRolloverStream.discontinuity(); 49462 } 49463 }; 49464 this.setAudioAppendStart = function (timestamp) { 49465 if (audioTrack) { 49466 this.transmuxPipeline_.audioSegmentStream.setAudioAppendStart(timestamp); 49467 } 49468 }; 49469 this.setRemux = function (val) { 49470 var pipeline = this.transmuxPipeline_; 49471 options.remux = val; 49472 if (pipeline && pipeline.coalesceStream) { 49473 pipeline.coalesceStream.setRemux(val); 49474 } 49475 }; 49476 this.alignGopsWith = function (gopsToAlignWith) { 49477 if (videoTrack && this.transmuxPipeline_.videoSegmentStream) { 49478 this.transmuxPipeline_.videoSegmentStream.alignGopsWith(gopsToAlignWith); 49479 } 49480 }; 49481 this.getLogTrigger_ = function (key) { 49482 var self = this; 49483 return function (event) { 49484 event.stream = key; 49485 self.trigger('log', event); 49486 }; 49487 }; // feed incoming data to the front of the parsing pipeline 49488 49489 this.push = function (data) { 49490 if (hasFlushed) { 49491 var isAac = isLikelyAacData(data); 49492 if (isAac && this.transmuxPipeline_.type !== 'aac') { 49493 this.setupAacPipeline(); 49494 } else if (!isAac && this.transmuxPipeline_.type !== 'ts') { 49495 this.setupTsPipeline(); 49496 } 49497 hasFlushed = false; 49498 } 49499 this.transmuxPipeline_.headOfPipeline.push(data); 49500 }; // flush any buffered data 49501 49502 this.flush = function () { 49503 hasFlushed = true; // Start at the top of the pipeline and flush all pending work 49504 49505 this.transmuxPipeline_.headOfPipeline.flush(); 49506 }; 49507 this.endTimeline = function () { 49508 this.transmuxPipeline_.headOfPipeline.endTimeline(); 49509 }; 49510 this.reset = function () { 49511 if (this.transmuxPipeline_.headOfPipeline) { 49512 this.transmuxPipeline_.headOfPipeline.reset(); 49513 } 49514 }; // Caption data has to be reset when seeking outside buffered range 49515 49516 this.resetCaptions = function () { 49517 if (this.transmuxPipeline_.captionStream) { 49518 this.transmuxPipeline_.captionStream.reset(); 49519 } 49520 }; 49521 }; 49522 Transmuxer.prototype = new Stream(); 49523 var transmuxer = { 49524 Transmuxer: Transmuxer
vendor: 4,765 bytes, lines 49524-49661
49524, 49525 VideoSegmentStream: VideoSegmentStream, 49526 AudioSegmentStream: AudioSegmentStream, 49527 AUDIO_PROPERTIES: AUDIO_PROPERTIES, 49528 VIDEO_PROPERTIES: VIDEO_PROPERTIES, 49529 // exported for testing 49530 generateSegmentTimingInfo: generateSegmentTimingInfo 49531 }; 49532 /** 49533 * mux.js 49534 * 49535 * Copyright (c) Brightcove 49536 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 49537 */ 49538 49539 var toUnsigned$3 = function (value) { 49540 return value >>> 0; 49541 }; 49542 var toHexString$1 = function (value) { 49543 return ('00' + value.toString(16)).slice(-2); 49544 }; 49545 var bin = { 49546 toUnsigned: toUnsigned$3, 49547 toHexString: toHexString$1 49548 }; 49549 var parseType$3 = function (buffer) { 49550 var result = ''; 49551 result += String.fromCharCode(buffer[0]); 49552 result += String.fromCharCode(buffer[1]); 49553 result += String.fromCharCode(buffer[2]); 49554 result += String.fromCharCode(buffer[3]); 49555 return result; 49556 }; 49557 var parseType_1 = parseType$3; 49558 var toUnsigned$2 = bin.toUnsigned; 49559 var parseType$2 = parseType_1; 49560 var findBox$2 = function (data, path) { 49561 var results = [], 49562 i, 49563 size, 49564 type, 49565 end, 49566 subresults; 49567 if (!path.length) { 49568 // short-circuit the search for empty paths 49569 return null; 49570 } 49571 for (i = 0; i < data.byteLength;) { 49572 size = toUnsigned$2(data[i] << 24 | data[i + 1] << 16 | data[i + 2] << 8 | data[i + 3]); 49573 type = parseType$2(data.subarray(i + 4, i + 8)); 49574 end = size > 1 ? i + size : data.byteLength; 49575 if (type === path[0]) { 49576 if (path.length === 1) { 49577 // this is the end of the path and we've found the box we were 49578 // looking for 49579 results.push(data.subarray(i + 8, end)); 49580 } else { 49581 // recursively search for the next box along the path 49582 subresults = findBox$2(data.subarray(i + 8, end), path.slice(1)); 49583 if (subresults.length) { 49584 results = results.concat(subresults); 49585 } 49586 } 49587 } 49588 i = end; 49589 } // we've finished searching all of data 49590 49591 return results; 49592 }; 49593 var findBox_1 = findBox$2; 49594 var toUnsigned$1 = bin.toUnsigned; 49595 var getUint64$2 = numbers.getUint64; 49596 var tfdt = function (data) { 49597 var result = { 49598 version: data[0], 49599 flags: new Uint8Array(data.subarray(1, 4)) 49600 }; 49601 if (result.version === 1) { 49602 result.baseMediaDecodeTime = getUint64$2(data.subarray(4)); 49603 } else { 49604 result.baseMediaDecodeTime = toUnsigned$1(data[4] << 24 | data[5] << 16 | data[6] << 8 | data[7]); 49605 } 49606 return result; 49607 }; 49608 var parseTfdt$2 = tfdt; 49609 var parseSampleFlags$1 = function (flags) { 49610 return { 49611 isLeading: (flags[0] & 0x0c) >>> 2, 49612 dependsOn: flags[0] & 0x03, 49613 isDependedOn: (flags[1] & 0xc0) >>> 6, 49614 hasRedundancy: (flags[1] & 0x30) >>> 4, 49615 paddingValue: (flags[1] & 0x0e) >>> 1, 49616 isNonSyncSample: flags[1] & 0x01, 49617 degradationPriority: flags[2] << 8 | flags[3] 49618 }; 49619 }; 49620 var parseSampleFlags_1 = parseSampleFlags$1; 49621 var parseSampleFlags = parseSampleFlags_1; 49622 var trun = function (data) { 49623 var result = { 49624 version: data[0], 49625 flags: new Uint8Array(data.subarray(1, 4)), 49626 samples: [] 49627 }, 49628 view = new DataView(data.buffer, data.byteOffset, data.byteLength), 49629 // Flag interpretation 49630 dataOffsetPresent = result.flags[2] & 0x01, 49631 // compare with 2nd byte of 0x1 49632 firstSampleFlagsPresent = result.flags[2] & 0x04, 49633 // compare with 2nd byte of 0x4 49634 sampleDurationPresent = result.flags[1] & 0x01, 49635 // compare with 2nd byte of 0x100 49636 sampleSizePresent = result.flags[1] & 0x02, 49637 // compare with 2nd byte of 0x200 49638 sampleFlagsPresent = result.flags[1] & 0x04, 49639 // compare with 2nd byte of 0x400 49640 sampleCompositionTimeOffsetPresent = result.flags[1] & 0x08, 49641 // compare with 2nd byte of 0x800 49642 sampleCount = view.getUint32(4), 49643 offset = 8, 49644 sample; 49645 if (dataOffsetPresent) { 49646 // 32 bit signed integer 49647 result.dataOffset = view.getInt32(offset); 49648 offset += 4; 49649 } // Overrides the flags for the first sample only. The order of 49650 // optional values will be: duration, size, compositionTimeOffset 49651 49652 if (firstSampleFlagsPresent && sampleCount) { 49653 sample = { 49654 flags: parseSampleFlags(data.subarray(offset, offset + 4)) 49655 }; 49656 offset += 4; 49657 if (sampleDurationPresent) { 49658 sample.duration = view.getUint32(offset); 49659 offset += 4; 49660 } 49661 if (sampleSizePresent) {
vendor: 4,860 bytes, lines 49662-49807
49662 sample.size = view.getUint32(offset); 49663 offset += 4; 49664 } 49665 if (sampleCompositionTimeOffsetPresent) { 49666 if (result.version === 1) { 49667 sample.compositionTimeOffset = view.getInt32(offset); 49668 } else { 49669 sample.compositionTimeOffset = view.getUint32(offset); 49670 } 49671 offset += 4; 49672 } 49673 result.samples.push(sample); 49674 sampleCount--; 49675 } 49676 while (sampleCount--) { 49677 sample = {}; 49678 if (sampleDurationPresent) { 49679 sample.duration = view.getUint32(offset); 49680 offset += 4; 49681 } 49682 if (sampleSizePresent) { 49683 sample.size = view.getUint32(offset); 49684 offset += 4; 49685 } 49686 if (sampleFlagsPresent) { 49687 sample.flags = parseSampleFlags(data.subarray(offset, offset + 4)); 49688 offset += 4; 49689 } 49690 if (sampleCompositionTimeOffsetPresent) { 49691 if (result.version === 1) { 49692 sample.compositionTimeOffset = view.getInt32(offset); 49693 } else { 49694 sample.compositionTimeOffset = view.getUint32(offset); 49695 } 49696 offset += 4; 49697 } 49698 result.samples.push(sample); 49699 } 49700 return result; 49701 }; 49702 var parseTrun$2 = trun; 49703 var tfhd = function (data) { 49704 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 49705 result = { 49706 version: data[0], 49707 flags: new Uint8Array(data.subarray(1, 4)), 49708 trackId: view.getUint32(4) 49709 }, 49710 baseDataOffsetPresent = result.flags[2] & 0x01, 49711 sampleDescriptionIndexPresent = result.flags[2] & 0x02, 49712 defaultSampleDurationPresent = result.flags[2] & 0x08, 49713 defaultSampleSizePresent = result.flags[2] & 0x10, 49714 defaultSampleFlagsPresent = result.flags[2] & 0x20, 49715 durationIsEmpty = result.flags[0] & 0x010000, 49716 defaultBaseIsMoof = result.flags[0] & 0x020000, 49717 i; 49718 i = 8; 49719 if (baseDataOffsetPresent) { 49720 i += 4; // truncate top 4 bytes 49721 // FIXME: should we read the full 64 bits? 49722 49723 result.baseDataOffset = view.getUint32(12); 49724 i += 4; 49725 } 49726 if (sampleDescriptionIndexPresent) { 49727 result.sampleDescriptionIndex = view.getUint32(i); 49728 i += 4; 49729 } 49730 if (defaultSampleDurationPresent) { 49731 result.defaultSampleDuration = view.getUint32(i); 49732 i += 4; 49733 } 49734 if (defaultSampleSizePresent) { 49735 result.defaultSampleSize = view.getUint32(i); 49736 i += 4; 49737 } 49738 if (defaultSampleFlagsPresent) { 49739 result.defaultSampleFlags = view.getUint32(i); 49740 } 49741 if (durationIsEmpty) { 49742 result.durationIsEmpty = true; 49743 } 49744 if (!baseDataOffsetPresent && defaultBaseIsMoof) { 49745 result.baseDataOffsetIsMoof = true; 49746 } 49747 return result; 49748 }; 49749 var parseTfhd$2 = tfhd; 49750 var win; 49751 if (typeof window !== "undefined") { 49752 win = window; 49753 } else if (typeof commonjsGlobal !== "undefined") { 49754 win = commonjsGlobal; 49755 } else if (typeof self !== "undefined") { 49756 win = self; 49757 } else { 49758 win = {}; 49759 } 49760 var window_1 = win; 49761 /** 49762 * mux.js 49763 * 49764 * Copyright (c) Brightcove 49765 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 49766 * 49767 * Reads in-band CEA-708 captions out of FMP4 segments. 49768 * @see https://en.wikipedia.org/wiki/CEA-708 49769 */ 49770 49771 var discardEmulationPreventionBytes = captionPacketParser.discardEmulationPreventionBytes; 49772 var CaptionStream = captionStream.CaptionStream; 49773 var findBox$1 = findBox_1; 49774 var parseTfdt$1 = parseTfdt$2; 49775 var parseTrun$1 = parseTrun$2; 49776 var parseTfhd$1 = parseTfhd$2; 49777 var window$2 = window_1; 49778 /** 49779 * Maps an offset in the mdat to a sample based on the the size of the samples. 49780 * Assumes that `parseSamples` has been called first. 49781 * 49782 * @param {Number} offset - The offset into the mdat 49783 * @param {Object[]} samples - An array of samples, parsed using `parseSamples` 49784 * @return {?Object} The matching sample, or null if no match was found. 49785 * 49786 * @see ISO-BMFF-12/2015, Section 8.8.8 49787 **/ 49788 49789 var mapToSample = function (offset, samples) { 49790 var approximateOffset = offset; 49791 for (var i = 0; i < samples.length; i++) { 49792 var sample = samples[i]; 49793 if (approximateOffset < sample.size) { 49794 return sample; 49795 } 49796 approximateOffset -= sample.size; 49797 } 49798 return null; 49799 }; 49800 /** 49801 * Finds SEI nal units contained in a Media Data Box. 49802 * Assumes that `parseSamples` has been called first. 49803 * 49804 * @param {Uint8Array} avcStream - The bytes of the mdat 49805 * @param {Object[]} samples - The samples parsed out by `parseSamples` 49806 * @param {Number} trackId - The trackId of this video track 49807 * @return {Object[]}
vendor: 19,650 bytes, lines 49807-50335
49807 seiNals - the parsed SEI NALUs found. 49808 * The contents of the seiNal should match what is expected by 49809 * CaptionStream.push (nalUnitType, size, data, escapedRBSP, pts, dts) 49810 * 49811 * @see ISO-BMFF-12/2015, Section 8.1.1 49812 * @see Rec. ITU-T H.264, 7.3.2.3.1 49813 **/ 49814 49815 var findSeiNals = function (avcStream, samples, trackId) { 49816 var avcView = new DataView(avcStream.buffer, avcStream.byteOffset, avcStream.byteLength), 49817 result = { 49818 logs: [], 49819 seiNals: [] 49820 }, 49821 seiNal, 49822 i, 49823 length, 49824 lastMatchedSample; 49825 for (i = 0; i + 4 < avcStream.length; i += length) { 49826 length = avcView.getUint32(i); 49827 i += 4; // Bail if this doesn't appear to be an H264 stream 49828 49829 if (length <= 0) { 49830 continue; 49831 } 49832 switch (avcStream[i] & 0x1F) { 49833 case 0x06: 49834 var data = avcStream.subarray(i + 1, i + 1 + length); 49835 var matchingSample = mapToSample(i, samples); 49836 seiNal = { 49837 nalUnitType: 'sei_rbsp', 49838 size: length, 49839 data: data, 49840 escapedRBSP: discardEmulationPreventionBytes(data), 49841 trackId: trackId 49842 }; 49843 if (matchingSample) { 49844 seiNal.pts = matchingSample.pts; 49845 seiNal.dts = matchingSample.dts; 49846 lastMatchedSample = matchingSample; 49847 } else if (lastMatchedSample) { 49848 // If a matching sample cannot be found, use the last 49849 // sample's values as they should be as close as possible 49850 seiNal.pts = lastMatchedSample.pts; 49851 seiNal.dts = lastMatchedSample.dts; 49852 } else { 49853 result.logs.push({ 49854 level: 'warn', 49855 message: 'We\'ve encountered a nal unit without data at ' + i + ' for trackId ' + trackId + '. See mux.js#223.' 49856 }); 49857 break; 49858 } 49859 result.seiNals.push(seiNal); 49860 break; 49861 } 49862 } 49863 return result; 49864 }; 49865 /** 49866 * Parses sample information out of Track Run Boxes and calculates 49867 * the absolute presentation and decode timestamps of each sample. 49868 * 49869 * @param {Array<Uint8Array>} truns - The Trun Run boxes to be parsed 49870 * @param {Number|BigInt} baseMediaDecodeTime - base media decode time from tfdt 49871 @see ISO-BMFF-12/2015, Section 8.8.12 49872 * @param {Object} tfhd - The parsed Track Fragment Header 49873 * @see inspect.parseTfhd 49874 * @return {Object[]} the parsed samples 49875 * 49876 * @see ISO-BMFF-12/2015, Section 8.8.8 49877 **/ 49878 49879 var parseSamples = function (truns, baseMediaDecodeTime, tfhd) { 49880 var currentDts = baseMediaDecodeTime; 49881 var defaultSampleDuration = tfhd.defaultSampleDuration || 0; 49882 var defaultSampleSize = tfhd.defaultSampleSize || 0; 49883 var trackId = tfhd.trackId; 49884 var allSamples = []; 49885 truns.forEach(function (trun) { 49886 // Note: We currently do not parse the sample table as well 49887 // as the trun. It's possible some sources will require this. 49888 // moov > trak > mdia > minf > stbl 49889 var trackRun = parseTrun$1(trun); 49890 var samples = trackRun.samples; 49891 samples.forEach(function (sample) { 49892 if (sample.duration === undefined) { 49893 sample.duration = defaultSampleDuration; 49894 } 49895 if (sample.size === undefined) { 49896 sample.size = defaultSampleSize; 49897 } 49898 sample.trackId = trackId; 49899 sample.dts = currentDts; 49900 if (sample.compositionTimeOffset === undefined) { 49901 sample.compositionTimeOffset = 0; 49902 } 49903 if (typeof currentDts === 'bigint') { 49904 sample.pts = currentDts + window$2.BigInt(sample.compositionTimeOffset); 49905 currentDts += window$2.BigInt(sample.duration); 49906 } else { 49907 sample.pts = currentDts + sample.compositionTimeOffset; 49908 currentDts += sample.duration; 49909 } 49910 }); 49911 allSamples = allSamples.concat(samples); 49912 }); 49913 return allSamples; 49914 }; 49915 /** 49916 * Parses out caption nals from an FMP4 segment's video tracks. 49917 * 49918 * @param {Uint8Array} segment - The bytes of a single segment 49919 * @param {Number} videoTrackId - The trackId of a video track in the segment 49920 * @return {Object.<Number, Object[]>} A mapping of video trackId to 49921 * a list of seiNals found in that track 49922 **/ 49923 49924 var parseCaptionNals = function (segment, videoTrackId) { 49925 // To get the samples 49926 var trafs = findBox$1(segment, ['moof', 'traf']); // To get SEI NAL units 49927 49928 var mdats = findBox$1(segment, ['mdat']); 49929 var captionNals = {}; 49930 var mdatTrafPairs = []; // Pair up each traf with a mdat as moofs and mdats are in pairs 49931 49932 mdats.forEach(function (mdat, index) { 49933 var matchingTraf = trafs[index]; 49934 mdatTrafPairs.push({ 49935 mdat: mdat, 49936 traf: matchingTraf 49937 }); 49938 }); 49939 mdatTrafPairs.forEach(function (pair) { 49940 var mdat = pair.mdat; 49941 var traf = pair.traf; 49942 var tfhd = findBox$1(traf, ['tfhd']); // Exactly 1 tfhd per traf 49943 49944 var headerInfo = parseTfhd$1(tfhd[0]); 49945 var trackId = headerInfo.trackId; 49946 var tfdt = findBox$1(traf, ['tfdt']); // Either 0 or 1 tfdt per traf 49947 49948 var baseMediaDecodeTime = tfdt.length > 0 ? parseTfdt$1(tfdt[0]).baseMediaDecodeTime : 0; 49949 var truns = findBox$1(traf, ['trun']); 49950 var samples; 49951 var result; // Only parse video data for the chosen video track 49952 49953 if (videoTrackId === trackId && truns.length > 0) { 49954 samples = parseSamples(truns, baseMediaDecodeTime, headerInfo); 49955 result = findSeiNals(mdat, samples, trackId); 49956 if (!captionNals[trackId]) { 49957 captionNals[trackId] = { 49958 seiNals: [], 49959 logs: [] 49960 }; 49961 } 49962 captionNals[trackId].seiNals = captionNals[trackId].seiNals.concat(result.seiNals); 49963 captionNals[trackId].logs = captionNals[trackId].logs.concat(result.logs); 49964 } 49965 }); 49966 return captionNals; 49967 }; 49968 /** 49969 * Parses out inband captions from an MP4 container and returns 49970 * caption objects that can be used by WebVTT and the TextTrack API. 49971 * @see https://developer.mozilla.org/en-US/docs/Web/API/VTTCue 49972 * @see https://developer.mozilla.org/en-US/docs/Web/API/TextTrack 49973 * Assumes that `probe.getVideoTrackIds` and `probe.timescale` have been called first 49974 * 49975 * @param {Uint8Array} segment - The fmp4 segment containing embedded captions 49976 * @param {Number} trackId - The id of the video track to parse 49977 * @param {Number} timescale - The timescale for the video track from the init segment 49978 * 49979 * @return {?Object[]} parsedCaptions - A list of captions or null if no video tracks 49980 * @return {Number} parsedCaptions[].startTime - The time to show the caption in seconds 49981 * @return {Number} parsedCaptions[].endTime - The time to stop showing the caption in seconds 49982 * @return {Object[]} parsedCaptions[].content - A list of individual caption segments 49983 * @return {String} parsedCaptions[].content.text - The visible content of the caption segment 49984 * @return {Number} parsedCaptions[].content.line - The line height from 1-15 for positioning of the caption segment 49985 * @return {Number} parsedCaptions[].content.position - The column indent percentage for cue positioning from 10-80 49986 **/ 49987 49988 var parseEmbeddedCaptions = function (segment, trackId, timescale) { 49989 var captionNals; // the ISO-BMFF spec says that trackId can't be zero, but there's some broken content out there 49990 49991 if (trackId === null) { 49992 return null; 49993 } 49994 captionNals = parseCaptionNals(segment, trackId); 49995 var trackNals = captionNals[trackId] || {}; 49996 return { 49997 seiNals: trackNals.seiNals, 49998 logs: trackNals.logs, 49999 timescale: timescale 50000 }; 50001 }; 50002 /** 50003 * Converts SEI NALUs into captions that can be used by video.js 50004 **/ 50005 50006 var CaptionParser = function () { 50007 var isInitialized = false; 50008 var captionStream; // Stores segments seen before trackId and timescale are set 50009 50010 var segmentCache; // Stores video track ID of the track being parsed 50011 50012 var trackId; // Stores the timescale of the track being parsed 50013 50014 var timescale; // Stores captions parsed so far 50015 50016 var parsedCaptions; // Stores whether we are receiving partial data or not 50017 50018 var parsingPartial; 50019 /** 50020 * A method to indicate whether a CaptionParser has been initalized 50021 * @returns {Boolean} 50022 **/ 50023 50024 this.isInitialized = function () { 50025 return isInitialized; 50026 }; 50027 /** 50028 * Initializes the underlying CaptionStream, SEI NAL parsing 50029 * and management, and caption collection 50030 **/ 50031 50032 this.init = function (options) { 50033 captionStream = new CaptionStream(); 50034 isInitialized = true; 50035 parsingPartial = options ? options.isPartial : false; // Collect dispatched captions 50036 50037 captionStream.on('data', function (event) { 50038 // Convert to seconds in the source's timescale 50039 event.startTime = event.startPts / timescale; 50040 event.endTime = event.endPts / timescale; 50041 parsedCaptions.captions.push(event); 50042 parsedCaptions.captionStreams[event.stream] = true; 50043 }); 50044 captionStream.on('log', function (log) { 50045 parsedCaptions.logs.push(log); 50046 }); 50047 }; 50048 /** 50049 * Determines if a new video track will be selected 50050 * or if the timescale changed 50051 * @return {Boolean} 50052 **/ 50053 50054 this.isNewInit = function (videoTrackIds, timescales) { 50055 if (videoTrackIds && videoTrackIds.length === 0 || timescales && typeof timescales === 'object' && Object.keys(timescales).length === 0) { 50056 return false; 50057 } 50058 return trackId !== videoTrackIds[0] || timescale !== timescales[trackId]; 50059 }; 50060 /** 50061 * Parses out SEI captions and interacts with underlying 50062 * CaptionStream to return dispatched captions 50063 * 50064 * @param {Uint8Array} segment - The fmp4 segment containing embedded captions 50065 * @param {Number[]} videoTrackIds - A list of video tracks found in the init segment 50066 * @param {Object.<Number, Number>} timescales - The timescales found in the init segment 50067 * @see parseEmbeddedCaptions 50068 * @see m2ts/caption-stream.js 50069 **/ 50070 50071 this.parse = function (segment, videoTrackIds, timescales) { 50072 var parsedData; 50073 if (!this.isInitialized()) { 50074 return null; // This is not likely to be a video segment 50075 } else if (!videoTrackIds || !timescales) { 50076 return null; 50077 } else if (this.isNewInit(videoTrackIds, timescales)) { 50078 // Use the first video track only as there is no 50079 // mechanism to switch to other video tracks 50080 trackId = videoTrackIds[0]; 50081 timescale = timescales[trackId]; // If an init segment has not been seen yet, hold onto segment 50082 // data until we have one. 50083 // the ISO-BMFF spec says that trackId can't be zero, but there's some broken content out there 50084 } else if (trackId === null || !timescale) { 50085 segmentCache.push(segment); 50086 return null; 50087 } // Now that a timescale and trackId is set, parse cached segments 50088 50089 while (segmentCache.length > 0) { 50090 var cachedSegment = segmentCache.shift(); 50091 this.parse(cachedSegment, videoTrackIds, timescales); 50092 } 50093 parsedData = parseEmbeddedCaptions(segment, trackId, timescale); 50094 if (parsedData && parsedData.logs) { 50095 parsedCaptions.logs = parsedCaptions.logs.concat(parsedData.logs); 50096 } 50097 if (parsedData === null || !parsedData.seiNals) { 50098 if (parsedCaptions.logs.length) { 50099 return { 50100 logs: parsedCaptions.logs, 50101 captions: [], 50102 captionStreams: [] 50103 }; 50104 } 50105 return null; 50106 } 50107 this.pushNals(parsedData.seiNals); // Force the parsed captions to be dispatched 50108 50109 this.flushStream(); 50110 return parsedCaptions; 50111 }; 50112 /** 50113 * Pushes SEI NALUs onto CaptionStream 50114 * @param {Object[]} nals - A list of SEI nals parsed using `parseCaptionNals` 50115 * Assumes that `parseCaptionNals` has been called first 50116 * @see m2ts/caption-stream.js 50117 **/ 50118 50119 this.pushNals = function (nals) { 50120 if (!this.isInitialized() || !nals || nals.length === 0) { 50121 return null; 50122 } 50123 nals.forEach(function (nal) { 50124 captionStream.push(nal); 50125 }); 50126 }; 50127 /** 50128 * Flushes underlying CaptionStream to dispatch processed, displayable captions 50129 * @see m2ts/caption-stream.js 50130 **/ 50131 50132 this.flushStream = function () { 50133 if (!this.isInitialized()) { 50134 return null; 50135 } 50136 if (!parsingPartial) { 50137 captionStream.flush(); 50138 } else { 50139 captionStream.partialFlush(); 50140 } 50141 }; 50142 /** 50143 * Reset caption buckets for new data 50144 **/ 50145 50146 this.clearParsedCaptions = function () { 50147 parsedCaptions.captions = []; 50148 parsedCaptions.captionStreams = {}; 50149 parsedCaptions.logs = []; 50150 }; 50151 /** 50152 * Resets underlying CaptionStream 50153 * @see m2ts/caption-stream.js 50154 **/ 50155 50156 this.resetCaptionStream = function () { 50157 if (!this.isInitialized()) { 50158 return null; 50159 } 50160 captionStream.reset(); 50161 }; 50162 /** 50163 * Convenience method to clear all captions flushed from the 50164 * CaptionStream and still being parsed 50165 * @see m2ts/caption-stream.js 50166 **/ 50167 50168 this.clearAllCaptions = function () { 50169 this.clearParsedCaptions(); 50170 this.resetCaptionStream(); 50171 }; 50172 /** 50173 * Reset caption parser 50174 **/ 50175 50176 this.reset = function () { 50177 segmentCache = []; 50178 trackId = null; 50179 timescale = null; 50180 if (!parsedCaptions) { 50181 parsedCaptions = { 50182 captions: [], 50183 // CC1, CC2, CC3, CC4 50184 captionStreams: {}, 50185 logs: [] 50186 }; 50187 } else { 50188 this.clearParsedCaptions(); 50189 } 50190 this.resetCaptionStream(); 50191 }; 50192 this.reset(); 50193 }; 50194 var captionParser = CaptionParser; 50195 /** 50196 * Returns the first string in the data array ending with a null char '\0' 50197 * @param {UInt8} data 50198 * @returns the string with the null char 50199 */ 50200 50201 var uint8ToCString$1 = function (data) { 50202 var index = 0; 50203 var curChar = String.fromCharCode(data[index]); 50204 var retString = ''; 50205 while (curChar !== '\0') { 50206 retString += curChar; 50207 index++; 50208 curChar = String.fromCharCode(data[index]); 50209 } // Add nullChar 50210 50211 retString += curChar; 50212 return retString; 50213 }; 50214 var string = { 50215 uint8ToCString: uint8ToCString$1 50216 }; 50217 var uint8ToCString = string.uint8ToCString; 50218 var getUint64$1 = numbers.getUint64; 50219 /** 50220 * Based on: ISO/IEC 23009 Section: 5.10.3.3 50221 * References: 50222 * https://dashif-documents.azurewebsites.net/Events/master/event.html#emsg-format 50223 * https://aomediacodec.github.io/id3-emsg/ 50224 * 50225 * Takes emsg box data as a uint8 array and returns a emsg box object 50226 * @param {UInt8Array} boxData data from emsg box 50227 * @returns A parsed emsg box object 50228 */ 50229 50230 var parseEmsgBox = function (boxData) { 50231 // version + flags 50232 var offset = 4; 50233 var version = boxData[0]; 50234 var scheme_id_uri, value, timescale, presentation_time, presentation_time_delta, event_duration, id, message_data; 50235 if (version === 0) { 50236 scheme_id_uri = uint8ToCString(boxData.subarray(offset)); 50237 offset += scheme_id_uri.length; 50238 value = uint8ToCString(boxData.subarray(offset)); 50239 offset += value.length; 50240 var dv = new DataView(boxData.buffer); 50241 timescale = dv.getUint32(offset); 50242 offset += 4; 50243 presentation_time_delta = dv.getUint32(offset); 50244 offset += 4; 50245 event_duration = dv.getUint32(offset); 50246 offset += 4; 50247 id = dv.getUint32(offset); 50248 offset += 4; 50249 } else if (version === 1) { 50250 var dv = new DataView(boxData.buffer); 50251 timescale = dv.getUint32(offset); 50252 offset += 4; 50253 presentation_time = getUint64$1(boxData.subarray(offset)); 50254 offset += 8; 50255 event_duration = dv.getUint32(offset); 50256 offset += 4; 50257 id = dv.getUint32(offset); 50258 offset += 4; 50259 scheme_id_uri = uint8ToCString(boxData.subarray(offset)); 50260 offset += scheme_id_uri.length; 50261 value = uint8ToCString(boxData.subarray(offset)); 50262 offset += value.length; 50263 } 50264 message_data = new Uint8Array(boxData.subarray(offset, boxData.byteLength)); 50265 var emsgBox = { 50266 scheme_id_uri, 50267 value, 50268 // if timescale is undefined or 0 set to 1 50269 timescale: timescale ? timescale : 1, 50270 presentation_time, 50271 presentation_time_delta, 50272 event_duration, 50273 id, 50274 message_data 50275 }; 50276 return isValidEmsgBox(version, emsgBox) ? emsgBox : undefined; 50277 }; 50278 /** 50279 * Scales a presentation time or time delta with an offset with a provided timescale 50280 * @param {number} presentationTime 50281 * @param {number} timescale 50282 * @param {number} timeDelta 50283 * @param {number} offset 50284 * @returns the scaled time as a number 50285 */ 50286 50287 var scaleTime = function (presentationTime, timescale, timeDelta, offset) { 50288 return presentationTime || presentationTime === 0 ? presentationTime / timescale : offset + timeDelta / timescale; 50289 }; 50290 /** 50291 * Checks the emsg box data for validity based on the version 50292 * @param {number} version of the emsg box to validate 50293 * @param {Object} emsg the emsg data to validate 50294 * @returns if the box is valid as a boolean 50295 */ 50296 50297 var isValidEmsgBox = function (version, emsg) { 50298 var hasScheme = emsg.scheme_id_uri !== '\0'; 50299 var isValidV0Box = version === 0 && isDefined(emsg.presentation_time_delta) && hasScheme; 50300 var isValidV1Box = version === 1 && isDefined(emsg.presentation_time) && hasScheme; // Only valid versions of emsg are 0 and 1 50301 50302 return !(version > 1) && isValidV0Box || isValidV1Box; 50303 }; // Utility function to check if an object is defined 50304 50305 var isDefined = function (data) { 50306 return data !== undefined || data !== null; 50307 }; 50308 var emsg$1 = { 50309 parseEmsgBox: parseEmsgBox, 50310 scaleTime: scaleTime 50311 }; 50312 /** 50313 * mux.js 50314 * 50315 * Copyright (c) Brightcove 50316 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 50317 * 50318 * Utilities to detect basic properties and metadata about MP4s. 50319 */ 50320 50321 var toUnsigned = bin.toUnsigned; 50322 var toHexString = bin.toHexString; 50323 var findBox = findBox_1; 50324 var parseType$1 = parseType_1; 50325 var emsg = emsg$1; 50326 var parseTfhd = parseTfhd$2; 50327 var parseTrun = parseTrun$2; 50328 var parseTfdt = parseTfdt$2; 50329 var getUint64 = numbers.getUint64; 50330 var timescale, startTime, compositionStartTime, getVideoTrackIds, getTracks, getTimescaleFromMediaHeader, getEmsgID3; 50331 var window$1 = window_1; 50332 var parseId3Frames = parseId3.parseId3Frames; 50333 /** 50334 * Parses an MP4 initialization segment and extracts the timescale 50335 * values for any declared tracks. Timescale values indicate the
50336 * number of clock ticks per second to assume for time-based values 50337 * elsewhere in the MP4. 50338 * 50339 * To determine the start time of an MP4, you need two pieces of 50340 * information: the timescale unit and the earliest base media decode 50341 * time. Multiple timescales can be specified within an MP4 but the 50342 * base media decode time is always expressed in the timescale from 50343 * the media header box for the track: 50344 * ``` 50345 * moov > trak > mdia > mdhd.timescale 50346 * ``` 50347 * @param init {Uint8Array} the bytes of the init segment 50348 * @return {object} a hash of track ids to timescale values or null if 50349 * the init segment is malformed. 50350 */ 50351 50352 timescale = function (init) { 50353 var result = {}, 50354 traks = findBox(init, ['moov', 'trak']); // mdhd timescale 50355 50356 return traks.reduce(function (result, trak) { 50357 var tkhd, version, index, id, mdhd; 50358 tkhd = findBox(trak, ['tkhd'])[0]; 50359 if (!tkhd) { 50360 return null; 50361 } 50362 version = tkhd[0]; 50363 index = version === 0 ? 12 : 20; 50364 id = toUnsigned(tkhd[index] << 24 | tkhd[index + 1] << 16 | tkhd[index + 2] << 8 | tkhd[index + 3]); 50365 mdhd = findBox(trak, ['mdia', 'mdhd'])[0]; 50366 if (!mdhd) { 50367 return null; 50368 } 50369 version = mdhd[0]; 50370 index = version === 0 ? 12 : 20; 50371 result[id] = toUnsigned(mdhd[index] << 24 | mdhd[index + 1] << 16 | mdhd[index + 2] << 8 | mdhd[index + 3]); 50372 return result; 50373 }, result); 50374 }; 50375 /** 50376 * Determine the base media decode start time, in seconds, for an MP4 50377 * fragment. If multiple fragments are specified, the earliest time is 50378 * returned. 50379 * 50380 * The base media decode time can be parsed from track fragment 50381 * metadata: 50382 * ``` 50383 * moof > traf > tfdt.baseMediaDecodeTime 50384 * ``` 50385 * It requires the timescale value from the mdhd to interpret. 50386 * 50387 * @param timescale {object} a hash of track ids to timescale values. 50388 * @return {number} the earliest base media decode start time for the 50389 * fragment, in seconds 50390 */ 50391 50392 startTime = function (timescale, fragment) { 50393 var trafs; // we need info from two childrend of each track fragment box 50394 50395 trafs = findBox(fragment, ['moof', 'traf']); // determine the start times for each track 50396 50397 var lowestTime = trafs.reduce(function (acc, traf) { 50398 var tfhd = findBox(traf, ['tfhd'])[0]; // get the track id from the tfhd 50399 50400 var id = toUnsigned(tfhd[4] << 24 | tfhd[5] << 16 | tfhd[6] << 8 | tfhd[7]); // assume a 90kHz clock if no timescale was specified 50401 50402 var scale = timescale[id] || 90e3; // get the base media decode time from the tfdt 50403 50404 var tfdt = findBox(traf, ['tfdt'])[0]; 50405 var dv = new DataView(tfdt.buffer, tfdt.byteOffset, tfdt.byteLength); 50406 var baseTime; // version 1 is 64 bit 50407 50408 if (tfdt[0] === 1) { 50409 baseTime = getUint64(tfdt.subarray(4, 12)); 50410 } else { 50411 baseTime = dv.getUint32(4); 50412 }
vendor: 16,117 bytes, lines 50412-50800
50412 // convert base time to seconds if it is a valid number. 50413 50414 let seconds; 50415 if (typeof baseTime === 'bigint') { 50416 seconds = baseTime / window$1.BigInt(scale); 50417 } else if (typeof baseTime === 'number' && !isNaN(baseTime)) { 50418 seconds = baseTime / scale; 50419 } 50420 if (seconds < Number.MAX_SAFE_INTEGER) { 50421 seconds = Number(seconds); 50422 } 50423 if (seconds < acc) { 50424 acc = seconds; 50425 } 50426 return acc; 50427 }, Infinity); 50428 return typeof lowestTime === 'bigint' || isFinite(lowestTime) ? lowestTime : 0; 50429 }; 50430 /** 50431 * Determine the composition start, in seconds, for an MP4 50432 * fragment. 50433 * 50434 * The composition start time of a fragment can be calculated using the base 50435 * media decode time, composition time offset, and timescale, as follows: 50436 * 50437 * compositionStartTime = (baseMediaDecodeTime + compositionTimeOffset) / timescale 50438 * 50439 * All of the aforementioned information is contained within a media fragment's 50440 * `traf` box, except for timescale info, which comes from the initialization 50441 * segment, so a track id (also contained within a `traf`) is also necessary to 50442 * associate it with a timescale 50443 * 50444 * 50445 * @param timescales {object} - a hash of track ids to timescale values. 50446 * @param fragment {Unit8Array} - the bytes of a media segment 50447 * @return {number} the composition start time for the fragment, in seconds 50448 **/ 50449 50450 compositionStartTime = function (timescales, fragment) { 50451 var trafBoxes = findBox(fragment, ['moof', 'traf']); 50452 var baseMediaDecodeTime = 0; 50453 var compositionTimeOffset = 0; 50454 var trackId; 50455 if (trafBoxes && trafBoxes.length) { 50456 // The spec states that track run samples contained within a `traf` box are contiguous, but 50457 // it does not explicitly state whether the `traf` boxes themselves are contiguous. 50458 // We will assume that they are, so we only need the first to calculate start time. 50459 var tfhd = findBox(trafBoxes[0], ['tfhd'])[0]; 50460 var trun = findBox(trafBoxes[0], ['trun'])[0]; 50461 var tfdt = findBox(trafBoxes[0], ['tfdt'])[0]; 50462 if (tfhd) { 50463 var parsedTfhd = parseTfhd(tfhd); 50464 trackId = parsedTfhd.trackId; 50465 } 50466 if (tfdt) { 50467 var parsedTfdt = parseTfdt(tfdt); 50468 baseMediaDecodeTime = parsedTfdt.baseMediaDecodeTime; 50469 } 50470 if (trun) { 50471 var parsedTrun = parseTrun(trun); 50472 if (parsedTrun.samples && parsedTrun.samples.length) { 50473 compositionTimeOffset = parsedTrun.samples[0].compositionTimeOffset || 0; 50474 } 50475 } 50476 } // Get timescale for this specific track. Assume a 90kHz clock if no timescale was 50477 // specified. 50478 50479 var timescale = timescales[trackId] || 90e3; // return the composition start time, in seconds 50480 50481 if (typeof baseMediaDecodeTime === 'bigint') { 50482 compositionTimeOffset = window$1.BigInt(compositionTimeOffset); 50483 timescale = window$1.BigInt(timescale); 50484 } 50485 var result = (baseMediaDecodeTime + compositionTimeOffset) / timescale; 50486 if (typeof result === 'bigint' && result < Number.MAX_SAFE_INTEGER) { 50487 result = Number(result); 50488 } 50489 return result; 50490 }; 50491 /** 50492 * Find the trackIds of the video tracks in this source. 50493 * Found by parsing the Handler Reference and Track Header Boxes: 50494 * moov > trak > mdia > hdlr 50495 * moov > trak > tkhd 50496 * 50497 * @param {Uint8Array} init - The bytes of the init segment for this source 50498 * @return {Number[]} A list of trackIds 50499 * 50500 * @see ISO-BMFF-12/2015, Section 8.4.3 50501 **/ 50502 50503 getVideoTrackIds = function (init) { 50504 var traks = findBox(init, ['moov', 'trak']); 50505 var videoTrackIds = []; 50506 traks.forEach(function (trak) { 50507 var hdlrs = findBox(trak, ['mdia', 'hdlr']); 50508 var tkhds = findBox(trak, ['tkhd']); 50509 hdlrs.forEach(function (hdlr, index) { 50510 var handlerType = parseType$1(hdlr.subarray(8, 12)); 50511 var tkhd = tkhds[index]; 50512 var view; 50513 var version; 50514 var trackId; 50515 if (handlerType === 'vide') { 50516 view = new DataView(tkhd.buffer, tkhd.byteOffset, tkhd.byteLength); 50517 version = view.getUint8(0); 50518 trackId = version === 0 ? view.getUint32(12) : view.getUint32(20); 50519 videoTrackIds.push(trackId); 50520 } 50521 }); 50522 }); 50523 return videoTrackIds; 50524 }; 50525 getTimescaleFromMediaHeader = function (mdhd) { 50526 // mdhd is a FullBox, meaning it will have its own version as the first byte 50527 var version = mdhd[0]; 50528 var index = version === 0 ? 12 : 20; 50529 return toUnsigned(mdhd[index] << 24 | mdhd[index + 1] << 16 | mdhd[index + 2] << 8 | mdhd[index + 3]); 50530 }; 50531 /** 50532 * Get all the video, audio, and hint tracks from a non fragmented 50533 * mp4 segment 50534 */ 50535 50536 getTracks = function (init) { 50537 var traks = findBox(init, ['moov', 'trak']); 50538 var tracks = []; 50539 traks.forEach(function (trak) { 50540 var track = {}; 50541 var tkhd = findBox(trak, ['tkhd'])[0]; 50542 var view, tkhdVersion; // id 50543 50544 if (tkhd) { 50545 view = new DataView(tkhd.buffer, tkhd.byteOffset, tkhd.byteLength); 50546 tkhdVersion = view.getUint8(0); 50547 track.id = tkhdVersion === 0 ? view.getUint32(12) : view.getUint32(20); 50548 } 50549 var hdlr = findBox(trak, ['mdia', 'hdlr'])[0]; // type 50550 50551 if (hdlr) { 50552 var type = parseType$1(hdlr.subarray(8, 12)); 50553 if (type === 'vide') { 50554 track.type = 'video'; 50555 } else if (type === 'soun') { 50556 track.type = 'audio'; 50557 } else { 50558 track.type = type; 50559 } 50560 } // codec 50561 50562 var stsd = findBox(trak, ['mdia', 'minf', 'stbl', 'stsd'])[0]; 50563 if (stsd) { 50564 var sampleDescriptions = stsd.subarray(8); // gives the codec type string 50565 50566 track.codec = parseType$1(sampleDescriptions.subarray(4, 8)); 50567 var codecBox = findBox(sampleDescriptions, [track.codec])[0]; 50568 var codecConfig, codecConfigType; 50569 if (codecBox) { 50570 // https://tools.ietf.org/html/rfc6381#section-3.3 50571 if (/^[asm]vc[1-9]$/i.test(track.codec)) { 50572 // we don't need anything but the "config" parameter of the 50573 // avc1 codecBox 50574 codecConfig = codecBox.subarray(78); 50575 codecConfigType = parseType$1(codecConfig.subarray(4, 8)); 50576 if (codecConfigType === 'avcC' && codecConfig.length > 11) { 50577 track.codec += '.'; // left padded with zeroes for single digit hex 50578 // profile idc 50579 50580 track.codec += toHexString(codecConfig[9]); // the byte containing the constraint_set flags 50581 50582 track.codec += toHexString(codecConfig[10]); // level idc 50583 50584 track.codec += toHexString(codecConfig[11]); 50585 } else { 50586 // TODO: show a warning that we couldn't parse the codec 50587 // and are using the default 50588 track.codec = 'avc1.4d400d'; 50589 } 50590 } else if (/^mp4[a,v]$/i.test(track.codec)) { 50591 // we do not need anything but the streamDescriptor of the mp4a codecBox 50592 codecConfig = codecBox.subarray(28); 50593 codecConfigType = parseType$1(codecConfig.subarray(4, 8)); 50594 if (codecConfigType === 'esds' && codecConfig.length > 20 && codecConfig[19] !== 0) { 50595 track.codec += '.' + toHexString(codecConfig[19]); // this value is only a single digit 50596 50597 track.codec += '.' + toHexString(codecConfig[20] >>> 2 & 0x3f).replace(/^0/, ''); 50598 } else { 50599 // TODO: show a warning that we couldn't parse the codec 50600 // and are using the default 50601 track.codec = 'mp4a.40.2'; 50602 } 50603 } else { 50604 // flac, opus, etc 50605 track.codec = track.codec.toLowerCase(); 50606 } 50607 } 50608 } 50609 var mdhd = findBox(trak, ['mdia', 'mdhd'])[0]; 50610 if (mdhd) { 50611 track.timescale = getTimescaleFromMediaHeader(mdhd); 50612 } 50613 tracks.push(track); 50614 }); 50615 return tracks; 50616 }; 50617 /** 50618 * Returns an array of emsg ID3 data from the provided segmentData. 50619 * An offset can also be provided as the Latest Arrival Time to calculate 50620 * the Event Start Time of v0 EMSG boxes. 50621 * See: https://dashif-documents.azurewebsites.net/Events/master/event.html#Inband-event-timing 50622 * 50623 * @param {Uint8Array} segmentData the segment byte array. 50624 * @param {number} offset the segment start time or Latest Arrival Time, 50625 * @return {Object[]} an array of ID3 parsed from EMSG boxes 50626 */ 50627 50628 getEmsgID3 = function (segmentData, offset = 0) { 50629 var emsgBoxes = findBox(segmentData, ['emsg']); 50630 return emsgBoxes.map(data => { 50631 var parsedBox = emsg.parseEmsgBox(new Uint8Array(data)); 50632 var parsedId3Frames = parseId3Frames(parsedBox.message_data); 50633 return { 50634 cueTime: emsg.scaleTime(parsedBox.presentation_time, parsedBox.timescale, parsedBox.presentation_time_delta, offset), 50635 duration: emsg.scaleTime(parsedBox.event_duration, parsedBox.timescale), 50636 frames: parsedId3Frames 50637 }; 50638 }); 50639 }; 50640 var probe$2 = { 50641 // export mp4 inspector's findBox and parseType for backwards compatibility 50642 findBox: findBox, 50643 parseType: parseType$1, 50644 timescale: timescale, 50645 startTime: startTime, 50646 compositionStartTime: compositionStartTime, 50647 videoTrackIds: getVideoTrackIds, 50648 tracks: getTracks, 50649 getTimescaleFromMediaHeader: getTimescaleFromMediaHeader, 50650 getEmsgID3: getEmsgID3 50651 }; 50652 /** 50653 * mux.js 50654 * 50655 * Copyright (c) Brightcove 50656 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 50657 * 50658 * Utilities to detect basic properties and metadata about TS Segments. 50659 */ 50660 50661 var StreamTypes$1 = streamTypes; 50662 var parsePid = function (packet) { 50663 var pid = packet[1] & 0x1f; 50664 pid <<= 8; 50665 pid |= packet[2]; 50666 return pid; 50667 }; 50668 var parsePayloadUnitStartIndicator = function (packet) { 50669 return !!(packet[1] & 0x40); 50670 }; 50671 var parseAdaptionField = function (packet) { 50672 var offset = 0; // if an adaption field is present, its length is specified by the 50673 // fifth byte of the TS packet header. The adaptation field is 50674 // used to add stuffing to PES packets that don't fill a complete 50675 // TS packet, and to specify some forms of timing and control data 50676 // that we do not currently use. 50677 50678 if ((packet[3] & 0x30) >>> 4 > 0x01) { 50679 offset += packet[4] + 1; 50680 } 50681 return offset; 50682 }; 50683 var parseType = function (packet, pmtPid) { 50684 var pid = parsePid(packet); 50685 if (pid === 0) { 50686 return 'pat'; 50687 } else if (pid === pmtPid) { 50688 return 'pmt'; 50689 } else if (pmtPid) { 50690 return 'pes'; 50691 } 50692 return null; 50693 }; 50694 var parsePat = function (packet) { 50695 var pusi = parsePayloadUnitStartIndicator(packet); 50696 var offset = 4 + parseAdaptionField(packet); 50697 if (pusi) { 50698 offset += packet[offset] + 1; 50699 } 50700 return (packet[offset + 10] & 0x1f) << 8 | packet[offset + 11]; 50701 }; 50702 var parsePmt = function (packet) { 50703 var programMapTable = {}; 50704 var pusi = parsePayloadUnitStartIndicator(packet); 50705 var payloadOffset = 4 + parseAdaptionField(packet); 50706 if (pusi) { 50707 payloadOffset += packet[payloadOffset] + 1; 50708 } // PMTs can be sent ahead of the time when they should actually 50709 // take effect. We don't believe this should ever be the case 50710 // for HLS but we'll ignore "forward" PMT declarations if we see 50711 // them. Future PMT declarations have the current_next_indicator 50712 // set to zero. 50713 50714 if (!(packet[payloadOffset + 5] & 0x01)) { 50715 return; 50716 } 50717 var sectionLength, tableEnd, programInfoLength; // the mapping table ends at the end of the current section 50718 50719 sectionLength = (packet[payloadOffset + 1] & 0x0f) << 8 | packet[payloadOffset + 2]; 50720 tableEnd = 3 + sectionLength - 4; // to determine where the table is, we have to figure out how 50721 // long the program info descriptors are 50722 50723 programInfoLength = (packet[payloadOffset + 10] & 0x0f) << 8 | packet[payloadOffset + 11]; // advance the offset to the first entry in the mapping table 50724 50725 var offset = 12 + programInfoLength; 50726 while (offset < tableEnd) { 50727 var i = payloadOffset + offset; // add an entry that maps the elementary_pid to the stream_type 50728 50729 programMapTable[(packet[i + 1] & 0x1F) << 8 | packet[i + 2]] = packet[i]; // move to the next table entry 50730 // skip past the elementary stream descriptors, if present 50731 50732 offset += ((packet[i + 3] & 0x0F) << 8 | packet[i + 4]) + 5; 50733 } 50734 return programMapTable; 50735 }; 50736 var parsePesType = function (packet, programMapTable) { 50737 var pid = parsePid(packet); 50738 var type = programMapTable[pid]; 50739 switch (type) { 50740 case StreamTypes$1.H264_STREAM_TYPE: 50741 return 'video'; 50742 case StreamTypes$1.ADTS_STREAM_TYPE: 50743 return 'audio'; 50744 case StreamTypes$1.METADATA_STREAM_TYPE: 50745 return 'timed-metadata'; 50746 default: 50747 return null; 50748 } 50749 }; 50750 var parsePesTime = function (packet) { 50751 var pusi = parsePayloadUnitStartIndicator(packet); 50752 if (!pusi) { 50753 return null; 50754 } 50755 var offset = 4 + parseAdaptionField(packet); 50756 if (offset >= packet.byteLength) { 50757 // From the H 222.0 MPEG-TS spec 50758 // "For transport stream packets carrying PES packets, stuffing is needed when there 50759 // is insufficient PES packet data to completely fill the transport stream packet 50760 // payload bytes. Stuffing is accomplished by defining an adaptation field longer than 50761 // the sum of the lengths of the data elements in it, so that the payload bytes 50762 // remaining after the adaptation field exactly accommodates the available PES packet 50763 // data." 50764 // 50765 // If the offset is >= the length of the packet, then the packet contains no data 50766 // and instead is just adaption field stuffing bytes 50767 return null; 50768 } 50769 var pes = null; 50770 var ptsDtsFlags; // PES packets may be annotated with a PTS value, or a PTS value 50771 // and a DTS value. Determine what combination of values is 50772 // available to work with. 50773 50774 ptsDtsFlags = packet[offset + 7]; // PTS and DTS are normally stored as a 33-bit number. Javascript 50775 // performs all bitwise operations on 32-bit integers but javascript 50776 // supports a much greater range (52-bits) of integer using standard 50777 // mathematical operations. 50778 // We construct a 31-bit value using bitwise operators over the 31 50779 // most significant bits and then multiply by 4 (equal to a left-shift 50780 // of 2) before we add the final 2 least significant bits of the 50781 // timestamp (equal to an OR.) 50782 50783 if (ptsDtsFlags & 0xC0) { 50784 pes = {}; // the PTS and DTS are not written out directly. For information 50785 // on how they are encoded, see 50786 // http://dvd.sourceforge.net/dvdinfo/pes-hdr.html 50787 50788 pes.pts = (packet[offset + 9] & 0x0E) << 27 | (packet[offset + 10] & 0xFF) << 20 | (packet[offset + 11] & 0xFE) << 12 | (packet[offset + 12] & 0xFF) << 5 | (packet[offset + 13] & 0xFE) >>> 3; 50789 pes.pts *= 4; // Left shift by 2 50790 50791 pes.pts += (packet[offset + 13] & 0x06) >>> 1; // OR by the two LSBs 50792 50793 pes.dts = pes.pts; 50794 if (ptsDtsFlags & 0x40) { 50795 pes.dts = (packet[offset + 14] & 0x0E) << 27 | (packet[offset + 15] & 0xFF) << 20 | (packet[offset + 16] & 0xFE) << 12 | (packet[offset + 17] & 0xFF) << 5 | (packet[offset + 18] & 0xFE) >>> 3; 50796 pes.dts *= 4; // Left shift by 2 50797 50798 pes.dts += (packet[offset + 18] & 0x06) >>> 1; // OR by the two LSBs 50799 } 50800 }
vendor: 31,773 bytes, lines 50801-51702
50801 50802 return pes; 50803 }; 50804 var parseNalUnitType = function (type) { 50805 switch (type) { 50806 case 0x05: 50807 return 'slice_layer_without_partitioning_rbsp_idr'; 50808 case 0x06: 50809 return 'sei_rbsp'; 50810 case 0x07: 50811 return 'seq_parameter_set_rbsp'; 50812 case 0x08: 50813 return 'pic_parameter_set_rbsp'; 50814 case 0x09: 50815 return 'access_unit_delimiter_rbsp'; 50816 default: 50817 return null; 50818 } 50819 }; 50820 var videoPacketContainsKeyFrame = function (packet) { 50821 var offset = 4 + parseAdaptionField(packet); 50822 var frameBuffer = packet.subarray(offset); 50823 var frameI = 0; 50824 var frameSyncPoint = 0; 50825 var foundKeyFrame = false; 50826 var nalType; // advance the sync point to a NAL start, if necessary 50827 50828 for (; frameSyncPoint < frameBuffer.byteLength - 3; frameSyncPoint++) { 50829 if (frameBuffer[frameSyncPoint + 2] === 1) { 50830 // the sync point is properly aligned 50831 frameI = frameSyncPoint + 5; 50832 break; 50833 } 50834 } 50835 while (frameI < frameBuffer.byteLength) { 50836 // look at the current byte to determine if we've hit the end of 50837 // a NAL unit boundary 50838 switch (frameBuffer[frameI]) { 50839 case 0: 50840 // skip past non-sync sequences 50841 if (frameBuffer[frameI - 1] !== 0) { 50842 frameI += 2; 50843 break; 50844 } else if (frameBuffer[frameI - 2] !== 0) { 50845 frameI++; 50846 break; 50847 } 50848 if (frameSyncPoint + 3 !== frameI - 2) { 50849 nalType = parseNalUnitType(frameBuffer[frameSyncPoint + 3] & 0x1f); 50850 if (nalType === 'slice_layer_without_partitioning_rbsp_idr') { 50851 foundKeyFrame = true; 50852 } 50853 } // drop trailing zeroes 50854 50855 do { 50856 frameI++; 50857 } while (frameBuffer[frameI] !== 1 && frameI < frameBuffer.length); 50858 frameSyncPoint = frameI - 2; 50859 frameI += 3; 50860 break; 50861 case 1: 50862 // skip past non-sync sequences 50863 if (frameBuffer[frameI - 1] !== 0 || frameBuffer[frameI - 2] !== 0) { 50864 frameI += 3; 50865 break; 50866 } 50867 nalType = parseNalUnitType(frameBuffer[frameSyncPoint + 3] & 0x1f); 50868 if (nalType === 'slice_layer_without_partitioning_rbsp_idr') { 50869 foundKeyFrame = true; 50870 } 50871 frameSyncPoint = frameI - 2; 50872 frameI += 3; 50873 break; 50874 default: 50875 // the current byte isn't a one or zero, so it cannot be part 50876 // of a sync sequence 50877 frameI += 3; 50878 break; 50879 } 50880 } 50881 frameBuffer = frameBuffer.subarray(frameSyncPoint); 50882 frameI -= frameSyncPoint; 50883 frameSyncPoint = 0; // parse the final nal 50884 50885 if (frameBuffer && frameBuffer.byteLength > 3) { 50886 nalType = parseNalUnitType(frameBuffer[frameSyncPoint + 3] & 0x1f); 50887 if (nalType === 'slice_layer_without_partitioning_rbsp_idr') { 50888 foundKeyFrame = true; 50889 } 50890 } 50891 return foundKeyFrame; 50892 }; 50893 var probe$1 = { 50894 parseType: parseType, 50895 parsePat: parsePat, 50896 parsePmt: parsePmt, 50897 parsePayloadUnitStartIndicator: parsePayloadUnitStartIndicator, 50898 parsePesType: parsePesType, 50899 parsePesTime: parsePesTime, 50900 videoPacketContainsKeyFrame: videoPacketContainsKeyFrame 50901 }; 50902 /** 50903 * mux.js 50904 * 50905 * Copyright (c) Brightcove 50906 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 50907 * 50908 * Parse mpeg2 transport stream packets to extract basic timing information 50909 */ 50910 50911 var StreamTypes = streamTypes; 50912 var handleRollover = timestampRolloverStream.handleRollover; 50913 var probe = {}; 50914 probe.ts = probe$1; 50915 probe.aac = utils; 50916 var ONE_SECOND_IN_TS = clock$2.ONE_SECOND_IN_TS; 50917 var MP2T_PACKET_LENGTH = 188, 50918 // bytes 50919 SYNC_BYTE = 0x47; 50920 /** 50921 * walks through segment data looking for pat and pmt packets to parse out 50922 * program map table information 50923 */ 50924 50925 var parsePsi_ = function (bytes, pmt) { 50926 var startIndex = 0, 50927 endIndex = MP2T_PACKET_LENGTH, 50928 packet, 50929 type; 50930 while (endIndex < bytes.byteLength) { 50931 // Look for a pair of start and end sync bytes in the data.. 50932 if (bytes[startIndex] === SYNC_BYTE && bytes[endIndex] === SYNC_BYTE) { 50933 // We found a packet 50934 packet = bytes.subarray(startIndex, endIndex); 50935 type = probe.ts.parseType(packet, pmt.pid); 50936 switch (type) { 50937 case 'pat': 50938 pmt.pid = probe.ts.parsePat(packet); 50939 break; 50940 case 'pmt': 50941 var table = probe.ts.parsePmt(packet); 50942 pmt.table = pmt.table || {}; 50943 Object.keys(table).forEach(function (key) { 50944 pmt.table[key] = table[key]; 50945 }); 50946 break; 50947 } 50948 startIndex += MP2T_PACKET_LENGTH; 50949 endIndex += MP2T_PACKET_LENGTH; 50950 continue; 50951 } // If we get here, we have somehow become de-synchronized and we need to step 50952 // forward one byte at a time until we find a pair of sync bytes that denote 50953 // a packet 50954 50955 startIndex++; 50956 endIndex++; 50957 } 50958 }; 50959 /** 50960 * walks through the segment data from the start and end to get timing information 50961 * for the first and last audio pes packets 50962 */ 50963 50964 var parseAudioPes_ = function (bytes, pmt, result) { 50965 var startIndex = 0, 50966 endIndex = MP2T_PACKET_LENGTH, 50967 packet, 50968 type, 50969 pesType, 50970 pusi, 50971 parsed; 50972 var endLoop = false; // Start walking from start of segment to get first audio packet 50973 50974 while (endIndex <= bytes.byteLength) { 50975 // Look for a pair of start and end sync bytes in the data.. 50976 if (bytes[startIndex] === SYNC_BYTE && (bytes[endIndex] === SYNC_BYTE || endIndex === bytes.byteLength)) { 50977 // We found a packet 50978 packet = bytes.subarray(startIndex, endIndex); 50979 type = probe.ts.parseType(packet, pmt.pid); 50980 switch (type) { 50981 case 'pes': 50982 pesType = probe.ts.parsePesType(packet, pmt.table); 50983 pusi = probe.ts.parsePayloadUnitStartIndicator(packet); 50984 if (pesType === 'audio' && pusi) { 50985 parsed = probe.ts.parsePesTime(packet); 50986 if (parsed) { 50987 parsed.type = 'audio'; 50988 result.audio.push(parsed); 50989 endLoop = true; 50990 } 50991 } 50992 break; 50993 } 50994 if (endLoop) { 50995 break; 50996 } 50997 startIndex += MP2T_PACKET_LENGTH; 50998 endIndex += MP2T_PACKET_LENGTH; 50999 continue; 51000 } // If we get here, we have somehow become de-synchronized and we need to step 51001 // forward one byte at a time until we find a pair of sync bytes that denote 51002 // a packet 51003 51004 startIndex++; 51005 endIndex++; 51006 } // Start walking from end of segment to get last audio packet 51007 51008 endIndex = bytes.byteLength; 51009 startIndex = endIndex - MP2T_PACKET_LENGTH; 51010 endLoop = false; 51011 while (startIndex >= 0) { 51012 // Look for a pair of start and end sync bytes in the data.. 51013 if (bytes[startIndex] === SYNC_BYTE && (bytes[endIndex] === SYNC_BYTE || endIndex === bytes.byteLength)) { 51014 // We found a packet 51015 packet = bytes.subarray(startIndex, endIndex); 51016 type = probe.ts.parseType(packet, pmt.pid); 51017 switch (type) { 51018 case 'pes': 51019 pesType = probe.ts.parsePesType(packet, pmt.table); 51020 pusi = probe.ts.parsePayloadUnitStartIndicator(packet); 51021 if (pesType === 'audio' && pusi) { 51022 parsed = probe.ts.parsePesTime(packet); 51023 if (parsed) { 51024 parsed.type = 'audio'; 51025 result.audio.push(parsed); 51026 endLoop = true; 51027 } 51028 } 51029 break; 51030 } 51031 if (endLoop) { 51032 break; 51033 } 51034 startIndex -= MP2T_PACKET_LENGTH; 51035 endIndex -= MP2T_PACKET_LENGTH; 51036 continue; 51037 } // If we get here, we have somehow become de-synchronized and we need to step 51038 // forward one byte at a time until we find a pair of sync bytes that denote 51039 // a packet 51040 51041 startIndex--; 51042 endIndex--; 51043 } 51044 }; 51045 /** 51046 * walks through the segment data from the start and end to get timing information 51047 * for the first and last video pes packets as well as timing information for the first 51048 * key frame. 51049 */ 51050 51051 var parseVideoPes_ = function (bytes, pmt, result) { 51052 var startIndex = 0, 51053 endIndex = MP2T_PACKET_LENGTH, 51054 packet, 51055 type, 51056 pesType, 51057 pusi, 51058 parsed, 51059 frame, 51060 i, 51061 pes; 51062 var endLoop = false; 51063 var currentFrame = { 51064 data: [], 51065 size: 0 51066 }; // Start walking from start of segment to get first video packet 51067 51068 while (endIndex < bytes.byteLength) { 51069 // Look for a pair of start and end sync bytes in the data.. 51070 if (bytes[startIndex] === SYNC_BYTE && bytes[endIndex] === SYNC_BYTE) { 51071 // We found a packet 51072 packet = bytes.subarray(startIndex, endIndex); 51073 type = probe.ts.parseType(packet, pmt.pid); 51074 switch (type) { 51075 case 'pes': 51076 pesType = probe.ts.parsePesType(packet, pmt.table); 51077 pusi = probe.ts.parsePayloadUnitStartIndicator(packet); 51078 if (pesType === 'video') { 51079 if (pusi && !endLoop) { 51080 parsed = probe.ts.parsePesTime(packet); 51081 if (parsed) { 51082 parsed.type = 'video'; 51083 result.video.push(parsed); 51084 endLoop = true; 51085 } 51086 } 51087 if (!result.firstKeyFrame) { 51088 if (pusi) { 51089 if (currentFrame.size !== 0) { 51090 frame = new Uint8Array(currentFrame.size); 51091 i = 0; 51092 while (currentFrame.data.length) { 51093 pes = currentFrame.data.shift(); 51094 frame.set(pes, i); 51095 i += pes.byteLength; 51096 } 51097 if (probe.ts.videoPacketContainsKeyFrame(frame)) { 51098 var firstKeyFrame = probe.ts.parsePesTime(frame); // PTS/DTS may not be available. Simply *not* setting 51099 // the keyframe seems to work fine with HLS playback 51100 // and definitely preferable to a crash with TypeError... 51101 51102 if (firstKeyFrame) { 51103 result.firstKeyFrame = firstKeyFrame; 51104 result.firstKeyFrame.type = 'video'; 51105 } else { 51106 // eslint-disable-next-line 51107 console.warn('Failed to extract PTS/DTS from PES at first keyframe. ' + 'This could be an unusual TS segment, or else mux.js did not ' + 'parse your TS segment correctly. If you know your TS ' + 'segments do contain PTS/DTS on keyframes please file a bug ' + 'report! You can try ffprobe to double check for yourself.'); 51108 } 51109 } 51110 currentFrame.size = 0; 51111 } 51112 } 51113 currentFrame.data.push(packet); 51114 currentFrame.size += packet.byteLength; 51115 } 51116 } 51117 break; 51118 } 51119 if (endLoop && result.firstKeyFrame) { 51120 break; 51121 } 51122 startIndex += MP2T_PACKET_LENGTH; 51123 endIndex += MP2T_PACKET_LENGTH; 51124 continue; 51125 } // If we get here, we have somehow become de-synchronized and we need to step 51126 // forward one byte at a time until we find a pair of sync bytes that denote 51127 // a packet 51128 51129 startIndex++; 51130 endIndex++; 51131 } // Start walking from end of segment to get last video packet 51132 51133 endIndex = bytes.byteLength; 51134 startIndex = endIndex - MP2T_PACKET_LENGTH; 51135 endLoop = false; 51136 while (startIndex >= 0) { 51137 // Look for a pair of start and end sync bytes in the data.. 51138 if (bytes[startIndex] === SYNC_BYTE && bytes[endIndex] === SYNC_BYTE) { 51139 // We found a packet 51140 packet = bytes.subarray(startIndex, endIndex); 51141 type = probe.ts.parseType(packet, pmt.pid); 51142 switch (type) { 51143 case 'pes': 51144 pesType = probe.ts.parsePesType(packet, pmt.table); 51145 pusi = probe.ts.parsePayloadUnitStartIndicator(packet); 51146 if (pesType === 'video' && pusi) { 51147 parsed = probe.ts.parsePesTime(packet); 51148 if (parsed) { 51149 parsed.type = 'video'; 51150 result.video.push(parsed); 51151 endLoop = true; 51152 } 51153 } 51154 break; 51155 } 51156 if (endLoop) { 51157 break; 51158 } 51159 startIndex -= MP2T_PACKET_LENGTH; 51160 endIndex -= MP2T_PACKET_LENGTH; 51161 continue; 51162 } // If we get here, we have somehow become de-synchronized and we need to step 51163 // forward one byte at a time until we find a pair of sync bytes that denote 51164 // a packet 51165 51166 startIndex--; 51167 endIndex--; 51168 } 51169 }; 51170 /** 51171 * Adjusts the timestamp information for the segment to account for 51172 * rollover and convert to seconds based on pes packet timescale (90khz clock) 51173 */ 51174 51175 var adjustTimestamp_ = function (segmentInfo, baseTimestamp) { 51176 if (segmentInfo.audio && segmentInfo.audio.length) { 51177 var audioBaseTimestamp = baseTimestamp; 51178 if (typeof audioBaseTimestamp === 'undefined' || isNaN(audioBaseTimestamp)) { 51179 audioBaseTimestamp = segmentInfo.audio[0].dts; 51180 } 51181 segmentInfo.audio.forEach(function (info) { 51182 info.dts = handleRollover(info.dts, audioBaseTimestamp); 51183 info.pts = handleRollover(info.pts, audioBaseTimestamp); // time in seconds 51184 51185 info.dtsTime = info.dts / ONE_SECOND_IN_TS; 51186 info.ptsTime = info.pts / ONE_SECOND_IN_TS; 51187 }); 51188 } 51189 if (segmentInfo.video && segmentInfo.video.length) { 51190 var videoBaseTimestamp = baseTimestamp; 51191 if (typeof videoBaseTimestamp === 'undefined' || isNaN(videoBaseTimestamp)) { 51192 videoBaseTimestamp = segmentInfo.video[0].dts; 51193 } 51194 segmentInfo.video.forEach(function (info) { 51195 info.dts = handleRollover(info.dts, videoBaseTimestamp); 51196 info.pts = handleRollover(info.pts, videoBaseTimestamp); // time in seconds 51197 51198 info.dtsTime = info.dts / ONE_SECOND_IN_TS; 51199 info.ptsTime = info.pts / ONE_SECOND_IN_TS; 51200 }); 51201 if (segmentInfo.firstKeyFrame) { 51202 var frame = segmentInfo.firstKeyFrame; 51203 frame.dts = handleRollover(frame.dts, videoBaseTimestamp); 51204 frame.pts = handleRollover(frame.pts, videoBaseTimestamp); // time in seconds 51205 51206 frame.dtsTime = frame.dts / ONE_SECOND_IN_TS; 51207 frame.ptsTime = frame.pts / ONE_SECOND_IN_TS; 51208 } 51209 } 51210 }; 51211 /** 51212 * inspects the aac data stream for start and end time information 51213 */ 51214 51215 var inspectAac_ = function (bytes) { 51216 var endLoop = false, 51217 audioCount = 0, 51218 sampleRate = null, 51219 timestamp = null, 51220 frameSize = 0, 51221 byteIndex = 0, 51222 packet; 51223 while (bytes.length - byteIndex >= 3) { 51224 var type = probe.aac.parseType(bytes, byteIndex); 51225 switch (type) { 51226 case 'timed-metadata': 51227 // Exit early because we don't have enough to parse 51228 // the ID3 tag header 51229 if (bytes.length - byteIndex < 10) { 51230 endLoop = true; 51231 break; 51232 } 51233 frameSize = probe.aac.parseId3TagSize(bytes, byteIndex); // Exit early if we don't have enough in the buffer 51234 // to emit a full packet 51235 51236 if (frameSize > bytes.length) { 51237 endLoop = true; 51238 break; 51239 } 51240 if (timestamp === null) { 51241 packet = bytes.subarray(byteIndex, byteIndex + frameSize); 51242 timestamp = probe.aac.parseAacTimestamp(packet); 51243 } 51244 byteIndex += frameSize; 51245 break; 51246 case 'audio': 51247 // Exit early because we don't have enough to parse 51248 // the ADTS frame header 51249 if (bytes.length - byteIndex < 7) { 51250 endLoop = true; 51251 break; 51252 } 51253 frameSize = probe.aac.parseAdtsSize(bytes, byteIndex); // Exit early if we don't have enough in the buffer 51254 // to emit a full packet 51255 51256 if (frameSize > bytes.length) { 51257 endLoop = true; 51258 break; 51259 } 51260 if (sampleRate === null) { 51261 packet = bytes.subarray(byteIndex, byteIndex + frameSize); 51262 sampleRate = probe.aac.parseSampleRate(packet); 51263 } 51264 audioCount++; 51265 byteIndex += frameSize; 51266 break; 51267 default: 51268 byteIndex++; 51269 break; 51270 } 51271 if (endLoop) { 51272 return null; 51273 } 51274 } 51275 if (sampleRate === null || timestamp === null) { 51276 return null; 51277 } 51278 var audioTimescale = ONE_SECOND_IN_TS / sampleRate; 51279 var result = { 51280 audio: [{ 51281 type: 'audio', 51282 dts: timestamp, 51283 pts: timestamp 51284 }, { 51285 type: 'audio', 51286 dts: timestamp + audioCount * 1024 * audioTimescale, 51287 pts: timestamp + audioCount * 1024 * audioTimescale 51288 }] 51289 }; 51290 return result; 51291 }; 51292 /** 51293 * inspects the transport stream segment data for start and end time information 51294 * of the audio and video tracks (when present) as well as the first key frame's 51295 * start time. 51296 */ 51297 51298 var inspectTs_ = function (bytes) { 51299 var pmt = { 51300 pid: null, 51301 table: null 51302 }; 51303 var result = {}; 51304 parsePsi_(bytes, pmt); 51305 for (var pid in pmt.table) { 51306 if (pmt.table.hasOwnProperty(pid)) { 51307 var type = pmt.table[pid]; 51308 switch (type) { 51309 case StreamTypes.H264_STREAM_TYPE: 51310 result.video = []; 51311 parseVideoPes_(bytes, pmt, result); 51312 if (result.video.length === 0) { 51313 delete result.video; 51314 } 51315 break; 51316 case StreamTypes.ADTS_STREAM_TYPE: 51317 result.audio = []; 51318 parseAudioPes_(bytes, pmt, result); 51319 if (result.audio.length === 0) { 51320 delete result.audio; 51321 } 51322 break; 51323 } 51324 } 51325 } 51326 return result; 51327 }; 51328 /** 51329 * Inspects segment byte data and returns an object with start and end timing information 51330 * 51331 * @param {Uint8Array} bytes The segment byte data 51332 * @param {Number} baseTimestamp Relative reference timestamp used when adjusting frame 51333 * timestamps for rollover. This value must be in 90khz clock. 51334 * @return {Object} Object containing start and end frame timing info of segment. 51335 */ 51336 51337 var inspect = function (bytes, baseTimestamp) { 51338 var isAacData = probe.aac.isLikelyAacData(bytes); 51339 var result; 51340 if (isAacData) { 51341 result = inspectAac_(bytes); 51342 } else { 51343 result = inspectTs_(bytes); 51344 } 51345 if (!result || !result.audio && !result.video) { 51346 return null; 51347 } 51348 adjustTimestamp_(result, baseTimestamp); 51349 return result; 51350 }; 51351 var tsInspector = { 51352 inspect: inspect, 51353 parseAudioPes_: parseAudioPes_ 51354 }; 51355 /* global self */ 51356 51357 /** 51358 * Re-emits transmuxer events by converting them into messages to the 51359 * world outside the worker. 51360 * 51361 * @param {Object} transmuxer the transmuxer to wire events on 51362 * @private 51363 */ 51364 51365 const wireTransmuxerEvents = function (self, transmuxer) { 51366 transmuxer.on('data', function (segment) { 51367 // transfer ownership of the underlying ArrayBuffer 51368 // instead of doing a copy to save memory 51369 // ArrayBuffers are transferable but generic TypedArrays are not 51370 // @link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers#Passing_data_by_transferring_ownership_(transferable_objects) 51371 const initArray = segment.initSegment; 51372 segment.initSegment = { 51373 data: initArray.buffer, 51374 byteOffset: initArray.byteOffset, 51375 byteLength: initArray.byteLength 51376 }; 51377 const typedArray = segment.data; 51378 segment.data = typedArray.buffer; 51379 self.postMessage({ 51380 action: 'data', 51381 segment, 51382 byteOffset: typedArray.byteOffset, 51383 byteLength: typedArray.byteLength 51384 }, [segment.data]); 51385 }); 51386 transmuxer.on('done', function (data) { 51387 self.postMessage({ 51388 action: 'done' 51389 }); 51390 }); 51391 transmuxer.on('gopInfo', function (gopInfo) { 51392 self.postMessage({ 51393 action: 'gopInfo', 51394 gopInfo 51395 }); 51396 }); 51397 transmuxer.on('videoSegmentTimingInfo', function (timingInfo) { 51398 const videoSegmentTimingInfo = { 51399 start: { 51400 decode: clock$2.videoTsToSeconds(timingInfo.start.dts), 51401 presentation: clock$2.videoTsToSeconds(timingInfo.start.pts) 51402 }, 51403 end: { 51404 decode: clock$2.videoTsToSeconds(timingInfo.end.dts), 51405 presentation: clock$2.videoTsToSeconds(timingInfo.end.pts) 51406 }, 51407 baseMediaDecodeTime: clock$2.videoTsToSeconds(timingInfo.baseMediaDecodeTime) 51408 }; 51409 if (timingInfo.prependedContentDuration) { 51410 videoSegmentTimingInfo.prependedContentDuration = clock$2.videoTsToSeconds(timingInfo.prependedContentDuration); 51411 } 51412 self.postMessage({ 51413 action: 'videoSegmentTimingInfo', 51414 videoSegmentTimingInfo 51415 }); 51416 }); 51417 transmuxer.on('audioSegmentTimingInfo', function (timingInfo) { 51418 // Note that all times for [audio/video]SegmentTimingInfo events are in video clock 51419 const audioSegmentTimingInfo = { 51420 start: { 51421 decode: clock$2.videoTsToSeconds(timingInfo.start.dts), 51422 presentation: clock$2.videoTsToSeconds(timingInfo.start.pts) 51423 }, 51424 end: { 51425 decode: clock$2.videoTsToSeconds(timingInfo.end.dts), 51426 presentation: clock$2.videoTsToSeconds(timingInfo.end.pts) 51427 }, 51428 baseMediaDecodeTime: clock$2.videoTsToSeconds(timingInfo.baseMediaDecodeTime) 51429 }; 51430 if (timingInfo.prependedContentDuration) { 51431 audioSegmentTimingInfo.prependedContentDuration = clock$2.videoTsToSeconds(timingInfo.prependedContentDuration); 51432 } 51433 self.postMessage({ 51434 action: 'audioSegmentTimingInfo', 51435 audioSegmentTimingInfo 51436 }); 51437 }); 51438 transmuxer.on('id3Frame', function (id3Frame) { 51439 self.postMessage({ 51440 action: 'id3Frame', 51441 id3Frame 51442 }); 51443 }); 51444 transmuxer.on('caption', function (caption) { 51445 self.postMessage({ 51446 action: 'caption', 51447 caption 51448 }); 51449 }); 51450 transmuxer.on('trackinfo', function (trackInfo) { 51451 self.postMessage({ 51452 action: 'trackinfo', 51453 trackInfo 51454 }); 51455 }); 51456 transmuxer.on('audioTimingInfo', function (audioTimingInfo) { 51457 // convert to video TS since we prioritize video time over audio 51458 self.postMessage({ 51459 action: 'audioTimingInfo', 51460 audioTimingInfo: { 51461 start: clock$2.videoTsToSeconds(audioTimingInfo.start), 51462 end: clock$2.videoTsToSeconds(audioTimingInfo.end) 51463 } 51464 }); 51465 }); 51466 transmuxer.on('videoTimingInfo', function (videoTimingInfo) { 51467 self.postMessage({ 51468 action: 'videoTimingInfo', 51469 videoTimingInfo: { 51470 start: clock$2.videoTsToSeconds(videoTimingInfo.start), 51471 end: clock$2.videoTsToSeconds(videoTimingInfo.end) 51472 } 51473 }); 51474 }); 51475 transmuxer.on('log', function (log) { 51476 self.postMessage({ 51477 action: 'log', 51478 log 51479 }); 51480 }); 51481 }; 51482 /** 51483 * All incoming messages route through this hash. If no function exists 51484 * to handle an incoming message, then we ignore the message. 51485 * 51486 * @class MessageHandlers 51487 * @param {Object} options the options to initialize with 51488 */ 51489 51490 class MessageHandlers { 51491 constructor(self, options) { 51492 this.options = options || {}; 51493 this.self = self; 51494 this.init(); 51495 } 51496 /** 51497 * initialize our web worker and wire all the events. 51498 */ 51499 51500 init() { 51501 if (this.transmuxer) { 51502 this.transmuxer.dispose(); 51503 } 51504 this.transmuxer = new transmuxer.Transmuxer(this.options); 51505 wireTransmuxerEvents(this.self, this.transmuxer); 51506 } 51507 pushMp4Captions(data) { 51508 if (!this.captionParser) { 51509 this.captionParser = new captionParser(); 51510 this.captionParser.init(); 51511 } 51512 const segment = new Uint8Array(data.data, data.byteOffset, data.byteLength); 51513 const parsed = this.captionParser.parse(segment, data.trackIds, data.timescales); 51514 this.self.postMessage({ 51515 action: 'mp4Captions', 51516 captions: parsed && parsed.captions || [], 51517 logs: parsed && parsed.logs || [], 51518 data: segment.buffer 51519 }, [segment.buffer]); 51520 } 51521 probeMp4StartTime({ 51522 timescales, 51523 data 51524 }) { 51525 const startTime = probe$2.startTime(timescales, data); 51526 this.self.postMessage({ 51527 action: 'probeMp4StartTime', 51528 startTime, 51529 data 51530 }, [data.buffer]); 51531 } 51532 probeMp4Tracks({ 51533 data 51534 }) { 51535 const tracks = probe$2.tracks(data); 51536 this.self.postMessage({ 51537 action: 'probeMp4Tracks', 51538 tracks, 51539 data 51540 }, [data.buffer]); 51541 } 51542 /** 51543 * Probes an mp4 segment for EMSG boxes containing ID3 data. 51544 * https://aomediacodec.github.io/id3-emsg/ 51545 * 51546 * @param {Uint8Array} data segment data 51547 * @param {number} offset segment start time 51548 * @return {Object[]} an array of ID3 frames 51549 */ 51550 51551 probeEmsgID3({ 51552 data, 51553 offset 51554 }) { 51555 const id3Frames = probe$2.getEmsgID3(data, offset); 51556 this.self.postMessage({ 51557 action: 'probeEmsgID3', 51558 id3Frames, 51559 emsgData: data 51560 }, [data.buffer]); 51561 } 51562 /** 51563 * Probe an mpeg2-ts segment to determine the start time of the segment in it's 51564 * internal "media time," as well as whether it contains video and/or audio. 51565 * 51566 * @private 51567 * @param {Uint8Array} bytes - segment bytes 51568 * @param {number} baseStartTime 51569 * Relative reference timestamp used when adjusting frame timestamps for rollover. 51570 * This value should be in seconds, as it's converted to a 90khz clock within the 51571 * function body. 51572 * @return {Object} The start time of the current segment in "media time" as well as 51573 * whether it contains video and/or audio 51574 */ 51575 51576 probeTs({ 51577 data, 51578 baseStartTime 51579 }) { 51580 const tsStartTime = typeof baseStartTime === 'number' && !isNaN(baseStartTime) ? baseStartTime * clock$2.ONE_SECOND_IN_TS : void 0; 51581 const timeInfo = tsInspector.inspect(data, tsStartTime); 51582 let result = null; 51583 if (timeInfo) { 51584 result = { 51585 // each type's time info comes back as an array of 2 times, start and end 51586 hasVideo: timeInfo.video && timeInfo.video.length === 2 || false, 51587 hasAudio: timeInfo.audio && timeInfo.audio.length === 2 || false 51588 }; 51589 if (result.hasVideo) { 51590 result.videoStart = timeInfo.video[0].ptsTime; 51591 } 51592 if (result.hasAudio) { 51593 result.audioStart = timeInfo.audio[0].ptsTime; 51594 } 51595 } 51596 this.self.postMessage({ 51597 action: 'probeTs', 51598 result, 51599 data 51600 }, [data.buffer]); 51601 } 51602 clearAllMp4Captions() { 51603 if (this.captionParser) { 51604 this.captionParser.clearAllCaptions(); 51605 } 51606 } 51607 clearParsedMp4Captions() { 51608 if (this.captionParser) { 51609 this.captionParser.clearParsedCaptions(); 51610 } 51611 } 51612 /** 51613 * Adds data (a ts segment) to the start of the transmuxer pipeline for 51614 * processing. 51615 * 51616 * @param {ArrayBuffer} data data to push into the muxer 51617 */ 51618 51619 push(data) { 51620 // Cast array buffer to correct type for transmuxer 51621 const segment = new Uint8Array(data.data, data.byteOffset, data.byteLength); 51622 this.transmuxer.push(segment); 51623 } 51624 /** 51625 * Recreate the transmuxer so that the next segment added via `push` 51626 * start with a fresh transmuxer. 51627 */ 51628 51629 reset() { 51630 this.transmuxer.reset(); 51631 } 51632 /** 51633 * Set the value that will be used as the `baseMediaDecodeTime` time for the 51634 * next segment pushed in. Subsequent segments will have their `baseMediaDecodeTime` 51635 * set relative to the first based on the PTS values. 51636 * 51637 * @param {Object} data used to set the timestamp offset in the muxer 51638 */ 51639 51640 setTimestampOffset(data) { 51641 const timestampOffset = data.timestampOffset || 0; 51642 this.transmuxer.setBaseMediaDecodeTime(Math.round(clock$2.secondsToVideoTs(timestampOffset))); 51643 } 51644 setAudioAppendStart(data) { 51645 this.transmuxer.setAudioAppendStart(Math.ceil(clock$2.secondsToVideoTs(data.appendStart))); 51646 } 51647 setRemux(data) { 51648 this.transmuxer.setRemux(data.remux); 51649 } 51650 /** 51651 * Forces the pipeline to finish processing the last segment and emit it's 51652 * results. 51653 * 51654 * @param {Object} data event data, not really used 51655 */ 51656 51657 flush(data) { 51658 this.transmuxer.flush(); // transmuxed done action is fired after both audio/video pipelines are flushed 51659 51660 self.postMessage({ 51661 action: 'done', 51662 type: 'transmuxed' 51663 }); 51664 } 51665 endTimeline() { 51666 this.transmuxer.endTimeline(); // transmuxed endedtimeline action is fired after both audio/video pipelines end their 51667 // timelines 51668 51669 self.postMessage({ 51670 action: 'endedtimeline', 51671 type: 'transmuxed' 51672 }); 51673 } 51674 alignGopsWith(data) { 51675 this.transmuxer.alignGopsWith(data.gopsToAlignWith.slice()); 51676 } 51677 } 51678 /** 51679 * Our web worker interface so that things can talk to mux.js 51680 * that will be running in a web worker. the scope is passed to this by 51681 * webworkify. 51682 * 51683 * @param {Object} self the scope for the web worker 51684 */ 51685 51686 self.onmessage = function (event) { 51687 if (event.data.action === 'init' && event.data.options) { 51688 this.messageHandlers = new MessageHandlers(self, event.data.options); 51689 return; 51690 } 51691 if (!this.messageHandlers) { 51692 this.messageHandlers = new MessageHandlers(self); 51693 } 51694 if (event.data && event.data.action && event.data.action !== 'init') { 51695 if (this.messageHandlers[event.data.action]) { 51696 this.messageHandlers[event.data.action](event.data); 51697 } 51698 } 51699 }; 51700 })); 51701 var TransmuxWorker = factory(workerCode$1); 51702 /* rollup-plugin-worker-factory end for worker!/
vendor: 40,469 bytes, lines 51702-52864
51702home/runner/work/http-streaming/http-streaming/src/transmuxer-worker.js */ 51703 51704 const handleData_ = (event, transmuxedData, callback) => { 51705 const { 51706 type, 51707 initSegment, 51708 captions, 51709 captionStreams, 51710 metadata, 51711 videoFrameDtsTime, 51712 videoFramePtsTime 51713 } = event.data.segment; 51714 transmuxedData.buffer.push({ 51715 captions, 51716 captionStreams, 51717 metadata 51718 }); 51719 const boxes = event.data.segment.boxes || { 51720 data: event.data.segment.data 51721 }; 51722 const result = { 51723 type, 51724 // cast ArrayBuffer to TypedArray 51725 data: new Uint8Array(boxes.data, boxes.data.byteOffset, boxes.data.byteLength), 51726 initSegment: new Uint8Array(initSegment.data, initSegment.byteOffset, initSegment.byteLength) 51727 }; 51728 if (typeof videoFrameDtsTime !== 'undefined') { 51729 result.videoFrameDtsTime = videoFrameDtsTime; 51730 } 51731 if (typeof videoFramePtsTime !== 'undefined') { 51732 result.videoFramePtsTime = videoFramePtsTime; 51733 } 51734 callback(result); 51735 }; 51736 const handleDone_ = ({ 51737 transmuxedData, 51738 callback 51739 }) => { 51740 // Previously we only returned data on data events, 51741 // not on done events. Clear out the buffer to keep that consistent. 51742 transmuxedData.buffer = []; // all buffers should have been flushed from the muxer, so start processing anything we 51743 // have received 51744 51745 callback(transmuxedData); 51746 }; 51747 const handleGopInfo_ = (event, transmuxedData) => { 51748 transmuxedData.gopInfo = event.data.gopInfo; 51749 }; 51750 const processTransmux = options => { 51751 const { 51752 transmuxer, 51753 bytes, 51754 audioAppendStart, 51755 gopsToAlignWith, 51756 remux, 51757 onData, 51758 onTrackInfo, 51759 onAudioTimingInfo, 51760 onVideoTimingInfo, 51761 onVideoSegmentTimingInfo, 51762 onAudioSegmentTimingInfo, 51763 onId3, 51764 onCaptions, 51765 onDone, 51766 onEndedTimeline, 51767 onTransmuxerLog, 51768 isEndOfTimeline 51769 } = options; 51770 const transmuxedData = { 51771 buffer: [] 51772 }; 51773 let waitForEndedTimelineEvent = isEndOfTimeline; 51774 const handleMessage = event => { 51775 if (transmuxer.currentTransmux !== options) { 51776 // disposed 51777 return; 51778 } 51779 if (event.data.action === 'data') { 51780 handleData_(event, transmuxedData, onData); 51781 } 51782 if (event.data.action === 'trackinfo') { 51783 onTrackInfo(event.data.trackInfo); 51784 } 51785 if (event.data.action === 'gopInfo') { 51786 handleGopInfo_(event, transmuxedData); 51787 } 51788 if (event.data.action === 'audioTimingInfo') { 51789 onAudioTimingInfo(event.data.audioTimingInfo); 51790 } 51791 if (event.data.action === 'videoTimingInfo') { 51792 onVideoTimingInfo(event.data.videoTimingInfo); 51793 } 51794 if (event.data.action === 'videoSegmentTimingInfo') { 51795 onVideoSegmentTimingInfo(event.data.videoSegmentTimingInfo); 51796 } 51797 if (event.data.action === 'audioSegmentTimingInfo') { 51798 onAudioSegmentTimingInfo(event.data.audioSegmentTimingInfo); 51799 } 51800 if (event.data.action === 'id3Frame') { 51801 onId3([event.data.id3Frame], event.data.id3Frame.dispatchType); 51802 } 51803 if (event.data.action === 'caption') { 51804 onCaptions(event.data.caption); 51805 } 51806 if (event.data.action === 'endedtimeline') { 51807 waitForEndedTimelineEvent = false; 51808 onEndedTimeline(); 51809 } 51810 if (event.data.action === 'log') { 51811 onTransmuxerLog(event.data.log); 51812 } // wait for the transmuxed event since we may have audio and video 51813 51814 if (event.data.type !== 'transmuxed') { 51815 return; 51816 } // If the "endedtimeline" event has not yet fired, and this segment represents the end 51817 // of a timeline, that means there may still be data events before the segment 51818 // processing can be considerred complete. In that case, the final event should be 51819 // an "endedtimeline" event with the type "transmuxed." 51820 51821 if (waitForEndedTimelineEvent) { 51822 return; 51823 } 51824 transmuxer.onmessage = null; 51825 handleDone_({ 51826 transmuxedData, 51827 callback: onDone 51828 }); 51829 /* eslint-disable no-use-before-define */ 51830 51831 dequeue(transmuxer); 51832 /* eslint-enable */ 51833 }; 51834 51835 transmuxer.onmessage = handleMessage; 51836 if (audioAppendStart) { 51837 transmuxer.postMessage({ 51838 action: 'setAudioAppendStart', 51839 appendStart: audioAppendStart 51840 }); 51841 } // allow empty arrays to be passed to clear out GOPs 51842 51843 if (Array.isArray(gopsToAlignWith)) { 51844 transmuxer.postMessage({ 51845 action: 'alignGopsWith', 51846 gopsToAlignWith 51847 }); 51848 } 51849 if (typeof remux !== 'undefined') { 51850 transmuxer.postMessage({ 51851 action: 'setRemux', 51852 remux 51853 }); 51854 } 51855 if (bytes.byteLength) { 51856 const buffer = bytes instanceof ArrayBuffer ? bytes : bytes.buffer; 51857 const byteOffset = bytes instanceof ArrayBuffer ? 0 : bytes.byteOffset; 51858 transmuxer.postMessage({ 51859 action: 'push', 51860 // Send the typed-array of data as an ArrayBuffer so that 51861 // it can be sent as a "Transferable" and avoid the costly 51862 // memory copy 51863 data: buffer, 51864 // To recreate the original typed-array, we need information 51865 // about what portion of the ArrayBuffer it was a view into 51866 byteOffset, 51867 byteLength: bytes.byteLength 51868 }, [buffer]); 51869 } 51870 if (isEndOfTimeline) { 51871 transmuxer.postMessage({ 51872 action: 'endTimeline' 51873 }); 51874 } // even if we didn't push any bytes, we have to make sure we flush in case we reached 51875 // the end of the segment 51876 51877 transmuxer.postMessage({ 51878 action: 'flush' 51879 }); 51880 }; 51881 const dequeue = transmuxer => { 51882 transmuxer.currentTransmux = null; 51883 if (transmuxer.transmuxQueue.length) { 51884 transmuxer.currentTransmux = transmuxer.transmuxQueue.shift(); 51885 if (typeof transmuxer.currentTransmux === 'function') { 51886 transmuxer.currentTransmux(); 51887 } else { 51888 processTransmux(transmuxer.currentTransmux); 51889 } 51890 } 51891 }; 51892 const processAction = (transmuxer, action) => { 51893 transmuxer.postMessage({ 51894 action 51895 }); 51896 dequeue(transmuxer); 51897 }; 51898 const enqueueAction = (action, transmuxer) => { 51899 if (!transmuxer.currentTransmux) { 51900 transmuxer.currentTransmux = action; 51901 processAction(transmuxer, action); 51902 return; 51903 } 51904 transmuxer.transmuxQueue.push(processAction.bind(null, transmuxer, action)); 51905 }; 51906 const reset = transmuxer => { 51907 enqueueAction('reset', transmuxer); 51908 }; 51909 const endTimeline = transmuxer => { 51910 enqueueAction('endTimeline', transmuxer); 51911 }; 51912 const transmux = options => { 51913 if (!options.transmuxer.currentTransmux) { 51914 options.transmuxer.currentTransmux = options; 51915 processTransmux(options); 51916 return; 51917 } 51918 options.transmuxer.transmuxQueue.push(options); 51919 }; 51920 const createTransmuxer = options => { 51921 const transmuxer = new TransmuxWorker(); 51922 transmuxer.currentTransmux = null; 51923 transmuxer.transmuxQueue = []; 51924 const term = transmuxer.terminate; 51925 transmuxer.terminate = () => { 51926 transmuxer.currentTransmux = null; 51927 transmuxer.transmuxQueue.length = 0; 51928 return term.call(transmuxer); 51929 }; 51930 transmuxer.postMessage({ 51931 action: 'init', 51932 options 51933 }); 51934 return transmuxer; 51935 }; 51936 var segmentTransmuxer = { 51937 reset, 51938 endTimeline, 51939 transmux, 51940 createTransmuxer 51941 }; 51942 const workerCallback = function (options) { 51943 const transmuxer = options.transmuxer; 51944 const endAction = options.endAction || options.action; 51945 const callback = options.callback; 51946 const message = _extends$1({}, options, { 51947 endAction: null, 51948 transmuxer: null, 51949 callback: null 51950 }); 51951 const listenForEndEvent = event => { 51952 if (event.data.action !== endAction) { 51953 return; 51954 } 51955 transmuxer.removeEventListener('message', listenForEndEvent); // transfer ownership of bytes back to us. 51956 51957 if (event.data.data) { 51958 event.data.data = new Uint8Array(event.data.data, options.byteOffset || 0, options.byteLength || event.data.data.byteLength); 51959 if (options.data) { 51960 options.data = event.data.data; 51961 } 51962 } 51963 callback(event.data); 51964 }; 51965 transmuxer.addEventListener('message', listenForEndEvent); 51966 if (options.data) { 51967 const isArrayBuffer = options.data instanceof ArrayBuffer; 51968 message.byteOffset = isArrayBuffer ? 0 : options.data.byteOffset; 51969 message.byteLength = options.data.byteLength; 51970 const transfers = [isArrayBuffer ? options.data : options.data.buffer]; 51971 transmuxer.postMessage(message, transfers); 51972 } else { 51973 transmuxer.postMessage(message); 51974 } 51975 }; 51976 const REQUEST_ERRORS = { 51977 FAILURE: 2, 51978 TIMEOUT: -101, 51979 ABORTED: -102 51980 }; 51981 /** 51982 * Abort all requests 51983 * 51984 * @param {Object} activeXhrs - an object that tracks all XHR requests 51985 */ 51986 51987 const abortAll = activeXhrs => { 51988 activeXhrs.forEach(xhr => { 51989 xhr.abort(); 51990 }); 51991 }; 51992 /** 51993 * Gather important bandwidth stats once a request has completed 51994 * 51995 * @param {Object} request - the XHR request from which to gather stats 51996 */ 51997 51998 const getRequestStats = request => { 51999 return { 52000 bandwidth: request.bandwidth, 52001 bytesReceived: request.bytesReceived || 0, 52002 roundTripTime: request.roundTripTime || 0 52003 }; 52004 }; 52005 /** 52006 * If possible gather bandwidth stats as a request is in 52007 * progress 52008 * 52009 * @param {Event} progressEvent - an event object from an XHR's progress event 52010 */ 52011 52012 const getProgressStats = progressEvent => { 52013 const request = progressEvent.target; 52014 const roundTripTime = Date.now() - request.requestTime; 52015 const stats = { 52016 bandwidth: Infinity, 52017 bytesReceived: 0, 52018 roundTripTime: roundTripTime || 0 52019 }; 52020 stats.bytesReceived = progressEvent.loaded; // This can result in Infinity if stats.roundTripTime is 0 but that is ok 52021 // because we should only use bandwidth stats on progress to determine when 52022 // abort a request early due to insufficient bandwidth 52023 52024 stats.bandwidth = Math.floor(stats.bytesReceived / stats.roundTripTime * 8 * 1000); 52025 return stats; 52026 }; 52027 /** 52028 * Handle all error conditions in one place and return an object 52029 * with all the information 52030 * 52031 * @param {Error|null} error - if non-null signals an error occured with the XHR 52032 * @param {Object} request - the XHR request that possibly generated the error 52033 */ 52034 52035 const handleErrors = (error, request) => { 52036 if (request.timedout) { 52037 return { 52038 status: request.status, 52039 message: 'HLS request timed-out at URL: ' + request.uri, 52040 code: REQUEST_ERRORS.TIMEOUT, 52041 xhr: request 52042 }; 52043 } 52044 if (request.aborted) { 52045 return { 52046 status: request.status, 52047 message: 'HLS request aborted at URL: ' + request.uri, 52048 code: REQUEST_ERRORS.ABORTED, 52049 xhr: request 52050 }; 52051 } 52052 if (error) { 52053 return { 52054 status: request.status, 52055 message: 'HLS request errored at URL: ' + request.uri, 52056 code: REQUEST_ERRORS.FAILURE, 52057 xhr: request 52058 }; 52059 } 52060 if (request.responseType === 'arraybuffer' && request.response.byteLength === 0) { 52061 return { 52062 status: request.status, 52063 message: 'Empty HLS response at URL: ' + request.uri, 52064 code: REQUEST_ERRORS.FAILURE, 52065 xhr: request 52066 }; 52067 } 52068 return null; 52069 }; 52070 /** 52071 * Handle responses for key data and convert the key data to the correct format 52072 * for the decryption step later 52073 * 52074 * @param {Object} segment - a simplified copy of the segmentInfo object 52075 * from SegmentLoader 52076 * @param {Array} objects - objects to add the key bytes to. 52077 * @param {Function} finishProcessingFn - a callback to execute to continue processing 52078 * this request 52079 */ 52080 52081 const handleKeyResponse = (segment, objects, finishProcessingFn) => (error, request) => { 52082 const response = request.response; 52083 const errorObj = handleErrors(error, request); 52084 if (errorObj) { 52085 return finishProcessingFn(errorObj, segment); 52086 } 52087 if (response.byteLength !== 16) { 52088 return finishProcessingFn({ 52089 status: request.status, 52090 message: 'Invalid HLS key at URL: ' + request.uri, 52091 code: REQUEST_ERRORS.FAILURE, 52092 xhr: request 52093 }, segment); 52094 } 52095 const view = new DataView(response); 52096 const bytes = new Uint32Array([view.getUint32(0), view.getUint32(4), view.getUint32(8), view.getUint32(12)]); 52097 for (let i = 0; i < objects.length; i++) { 52098 objects[i].bytes = bytes; 52099 } 52100 return finishProcessingFn(null, segment); 52101 }; 52102 const parseInitSegment = (segment, callback) => { 52103 const type = detectContainerForBytes(segment.map.bytes); // TODO: We should also handle ts init segments here, but we 52104 // only know how to parse mp4 init segments at the moment 52105 52106 if (type !== 'mp4') { 52107 const uri = segment.map.resolvedUri || segment.map.uri; 52108 return callback({ 52109 internal: true, 52110 message: `Found unsupported ${type || 'unknown'} container for initialization segment at URL: ${uri}`, 52111 code: REQUEST_ERRORS.FAILURE 52112 }); 52113 } 52114 workerCallback({ 52115 action: 'probeMp4Tracks', 52116 data: segment.map.bytes, 52117 transmuxer: segment.transmuxer, 52118 callback: ({ 52119 tracks, 52120 data 52121 }) => { 52122 // transfer bytes back to us 52123 segment.map.bytes = data; 52124 tracks.forEach(function (track) { 52125 segment.map.tracks = segment.map.tracks || {}; // only support one track of each type for now 52126 52127 if (segment.map.tracks[track.type]) { 52128 return; 52129 } 52130 segment.map.tracks[track.type] = track; 52131 if (typeof track.id === 'number' && track.timescale) { 52132 segment.map.timescales = segment.map.timescales || {}; 52133 segment.map.timescales[track.id] = track.timescale; 52134 } 52135 }); 52136 return callback(null); 52137 } 52138 }); 52139 }; 52140 /** 52141 * Handle init-segment responses 52142 * 52143 * @param {Object} segment - a simplified copy of the segmentInfo object 52144 * from SegmentLoader 52145 * @param {Function} finishProcessingFn - a callback to execute to continue processing 52146 * this request 52147 */ 52148 52149 const handleInitSegmentResponse = ({ 52150 segment, 52151 finishProcessingFn 52152 }) => (error, request) => { 52153 const errorObj = handleErrors(error, request); 52154 if (errorObj) { 52155 return finishProcessingFn(errorObj, segment); 52156 } 52157 const bytes = new Uint8Array(request.response); // init segment is encypted, we will have to wait 52158 // until the key request is done to decrypt. 52159 52160 if (segment.map.key) { 52161 segment.map.encryptedBytes = bytes; 52162 return finishProcessingFn(null, segment); 52163 } 52164 segment.map.bytes = bytes; 52165 parseInitSegment(segment, function (parseError) { 52166 if (parseError) { 52167 parseError.xhr = request; 52168 parseError.status = request.status; 52169 return finishProcessingFn(parseError, segment); 52170 } 52171 finishProcessingFn(null, segment); 52172 }); 52173 }; 52174 /** 52175 * Response handler for segment-requests being sure to set the correct 52176 * property depending on whether the segment is encryped or not 52177 * Also records and keeps track of stats that are used for ABR purposes 52178 * 52179 * @param {Object} segment - a simplified copy of the segmentInfo object 52180 * from SegmentLoader 52181 * @param {Function} finishProcessingFn - a callback to execute to continue processing 52182 * this request 52183 */ 52184 52185 const handleSegmentResponse = ({ 52186 segment, 52187 finishProcessingFn, 52188 responseType 52189 }) => (error, request) => { 52190 const errorObj = handleErrors(error, request); 52191 if (errorObj) { 52192 return finishProcessingFn(errorObj, segment); 52193 } 52194 const newBytes = 52195 // although responseText "should" exist, this guard serves to prevent an error being 52196 // thrown for two primary cases: 52197 // 1. the mime type override stops working, or is not implemented for a specific 52198 // browser 52199 // 2. when using mock XHR libraries like sinon that do not allow the override behavior 52200 responseType === 'arraybuffer' || !request.responseText ? request.response : stringToArrayBuffer(request.responseText.substring(segment.lastReachedChar || 0)); 52201 segment.stats = getRequestStats(request); 52202 if (segment.key) { 52203 segment.encryptedBytes = new Uint8Array(newBytes); 52204 } else { 52205 segment.bytes = new Uint8Array(newBytes); 52206 } 52207 return finishProcessingFn(null, segment); 52208 }; 52209 const transmuxAndNotify = ({ 52210 segment, 52211 bytes, 52212 trackInfoFn, 52213 timingInfoFn, 52214 videoSegmentTimingInfoFn, 52215 audioSegmentTimingInfoFn, 52216 id3Fn, 52217 captionsFn, 52218 isEndOfTimeline, 52219 endedTimelineFn, 52220 dataFn, 52221 doneFn, 52222 onTransmuxerLog 52223 }) => { 52224 const fmp4Tracks = segment.map && segment.map.tracks || {}; 52225 const isMuxed = Boolean(fmp4Tracks.audio && fmp4Tracks.video); // Keep references to each function so we can null them out after we're done with them. 52226 // One reason for this is that in the case of full segments, we want to trust start 52227 // times from the probe, rather than the transmuxer. 52228 52229 let audioStartFn = timingInfoFn.bind(null, segment, 'audio', 'start'); 52230 const audioEndFn = timingInfoFn.bind(null, segment, 'audio', 'end'); 52231 let videoStartFn = timingInfoFn.bind(null, segment, 'video', 'start'); 52232 const videoEndFn = timingInfoFn.bind(null, segment, 'video', 'end'); 52233 const finish = () => transmux({ 52234 bytes, 52235 transmuxer: segment.transmuxer, 52236 audioAppendStart: segment.audioAppendStart, 52237 gopsToAlignWith: segment.gopsToAlignWith, 52238 remux: isMuxed, 52239 onData: result => { 52240 result.type = result.type === 'combined' ? 'video' : result.type; 52241 dataFn(segment, result); 52242 }, 52243 onTrackInfo: trackInfo => { 52244 if (trackInfoFn) { 52245 if (isMuxed) { 52246 trackInfo.isMuxed = true; 52247 } 52248 trackInfoFn(segment, trackInfo); 52249 } 52250 }, 52251 onAudioTimingInfo: audioTimingInfo => { 52252 // we only want the first start value we encounter 52253 if (audioStartFn && typeof audioTimingInfo.start !== 'undefined') { 52254 audioStartFn(audioTimingInfo.start); 52255 audioStartFn = null; 52256 } // we want to continually update the end time 52257 52258 if (audioEndFn && typeof audioTimingInfo.end !== 'undefined') { 52259 audioEndFn(audioTimingInfo.end); 52260 } 52261 }, 52262 onVideoTimingInfo: videoTimingInfo => { 52263 // we only want the first start value we encounter 52264 if (videoStartFn && typeof videoTimingInfo.start !== 'undefined') { 52265 videoStartFn(videoTimingInfo.start); 52266 videoStartFn = null; 52267 } // we want to continually update the end time 52268 52269 if (videoEndFn && typeof videoTimingInfo.end !== 'undefined') { 52270 videoEndFn(videoTimingInfo.end); 52271 } 52272 }, 52273 onVideoSegmentTimingInfo: videoSegmentTimingInfo => { 52274 videoSegmentTimingInfoFn(videoSegmentTimingInfo); 52275 }, 52276 onAudioSegmentTimingInfo: audioSegmentTimingInfo => { 52277 audioSegmentTimingInfoFn(audioSegmentTimingInfo); 52278 }, 52279 onId3: (id3Frames, dispatchType) => { 52280 id3Fn(segment, id3Frames, dispatchType); 52281 }, 52282 onCaptions: captions => { 52283 captionsFn(segment, [captions]); 52284 }, 52285 isEndOfTimeline, 52286 onEndedTimeline: () => { 52287 endedTimelineFn(); 52288 }, 52289 onTransmuxerLog, 52290 onDone: result => { 52291 if (!doneFn) { 52292 return; 52293 } 52294 result.type = result.type === 'combined' ? 'video' : result.type; 52295 doneFn(null, segment, result); 52296 } 52297 }); // In the transmuxer, we don't yet have the ability to extract a "proper" start time. 52298 // Meaning cached frame data may corrupt our notion of where this segment 52299 // really starts. To get around this, probe for the info needed. 52300 52301 workerCallback({ 52302 action: 'probeTs', 52303 transmuxer: segment.transmuxer, 52304 data: bytes, 52305 baseStartTime: segment.baseStartTime, 52306 callback: data => { 52307 segment.bytes = bytes = data.data; 52308 const probeResult = data.result; 52309 if (probeResult) { 52310 trackInfoFn(segment, { 52311 hasAudio: probeResult.hasAudio, 52312 hasVideo: probeResult.hasVideo, 52313 isMuxed 52314 }); 52315 trackInfoFn = null; 52316 } 52317 finish(); 52318 } 52319 }); 52320 }; 52321 const handleSegmentBytes = ({ 52322 segment, 52323 bytes, 52324 trackInfoFn, 52325 timingInfoFn, 52326 videoSegmentTimingInfoFn, 52327 audioSegmentTimingInfoFn, 52328 id3Fn, 52329 captionsFn, 52330 isEndOfTimeline, 52331 endedTimelineFn, 52332 dataFn, 52333 doneFn, 52334 onTransmuxerLog 52335 }) => { 52336 let bytesAsUint8Array = new Uint8Array(bytes); // TODO: 52337 // We should have a handler that fetches the number of bytes required 52338 // to check if something is fmp4. This will allow us to save bandwidth 52339 // because we can only exclude a playlist and abort requests 52340 // by codec after trackinfo triggers. 52341 52342 if (isLikelyFmp4MediaSegment(bytesAsUint8Array)) { 52343 segment.isFmp4 = true; 52344 const { 52345 tracks 52346 } = segment.map; 52347 const trackInfo = { 52348 isFmp4: true, 52349 hasVideo: !!tracks.video, 52350 hasAudio: !!tracks.audio 52351 }; // if we have a audio track, with a codec that is not set to 52352 // encrypted audio 52353 52354 if (tracks.audio && tracks.audio.codec && tracks.audio.codec !== 'enca') { 52355 trackInfo.audioCodec = tracks.audio.codec; 52356 } // if we have a video track, with a codec that is not set to 52357 // encrypted video 52358 52359 if (tracks.video && tracks.video.codec && tracks.video.codec !== 'encv') { 52360 trackInfo.videoCodec = tracks.video.codec; 52361 } 52362 if (tracks.video && tracks.audio) { 52363 trackInfo.isMuxed = true; 52364 } // since we don't support appending fmp4 data on progress, we know we have the full 52365 // segment here 52366 52367 trackInfoFn(segment, trackInfo); // The probe doesn't provide the segment end time, so only callback with the start 52368 // time. The end time can be roughly calculated by the receiver using the duration. 52369 // 52370 // Note that the start time returned by the probe reflects the baseMediaDecodeTime, as 52371 // that is the true start of the segment (where the playback engine should begin 52372 // decoding). 52373 52374 const finishLoading = (captions, id3Frames) => { 52375 // if the track still has audio at this point it is only possible 52376 // for it to be audio only. See `tracks.video && tracks.audio` if statement 52377 // above. 52378 // we make sure to use segment.bytes here as that 52379 dataFn(segment, { 52380 data: bytesAsUint8Array, 52381 type: trackInfo.hasAudio && !trackInfo.isMuxed ? 'audio' : 'video' 52382 }); 52383 if (id3Frames && id3Frames.length) { 52384 id3Fn(segment, id3Frames); 52385 } 52386 if (captions && captions.length) { 52387 captionsFn(segment, captions); 52388 } 52389 doneFn(null, segment, {}); 52390 }; 52391 workerCallback({ 52392 action: 'probeMp4StartTime', 52393 timescales: segment.map.timescales, 52394 data: bytesAsUint8Array, 52395 transmuxer: segment.transmuxer, 52396 callback: ({ 52397 data, 52398 startTime 52399 }) => { 52400 // transfer bytes back to us 52401 bytes = data.buffer; 52402 segment.bytes = bytesAsUint8Array = data; 52403 if (trackInfo.hasAudio && !trackInfo.isMuxed) { 52404 timingInfoFn(segment, 'audio', 'start', startTime); 52405 } 52406 if (trackInfo.hasVideo) { 52407 timingInfoFn(segment, 'video', 'start', startTime); 52408 } 52409 workerCallback({ 52410 action: 'probeEmsgID3', 52411 data: bytesAsUint8Array, 52412 transmuxer: segment.transmuxer, 52413 offset: startTime, 52414 callback: ({ 52415 emsgData, 52416 id3Frames 52417 }) => { 52418 // transfer bytes back to us 52419 bytes = emsgData.buffer; 52420 segment.bytes = bytesAsUint8Array = emsgData; // Run through the CaptionParser in case there are captions. 52421 // Initialize CaptionParser if it hasn't been yet 52422 52423 if (!tracks.video || !emsgData.byteLength || !segment.transmuxer) { 52424 finishLoading(undefined, id3Frames); 52425 return; 52426 } 52427 workerCallback({ 52428 action: 'pushMp4Captions', 52429 endAction: 'mp4Captions', 52430 transmuxer: segment.transmuxer, 52431 data: bytesAsUint8Array, 52432 timescales: segment.map.timescales, 52433 trackIds: [tracks.video.id], 52434 callback: message => { 52435 // transfer bytes back to us 52436 bytes = message.data.buffer; 52437 segment.bytes = bytesAsUint8Array = message.data; 52438 message.logs.forEach(function (log) { 52439 onTransmuxerLog(merge(log, { 52440 stream: 'mp4CaptionParser' 52441 })); 52442 }); 52443 finishLoading(message.captions, id3Frames); 52444 } 52445 }); 52446 } 52447 }); 52448 } 52449 }); 52450 return; 52451 } // VTT or other segments that don't need processing 52452 52453 if (!segment.transmuxer) { 52454 doneFn(null, segment, {}); 52455 return; 52456 } 52457 if (typeof segment.container === 'undefined') { 52458 segment.container = detectContainerForBytes(bytesAsUint8Array); 52459 } 52460 if (segment.container !== 'ts' && segment.container !== 'aac') { 52461 trackInfoFn(segment, { 52462 hasAudio: false, 52463 hasVideo: false 52464 }); 52465 doneFn(null, segment, {}); 52466 return; 52467 } // ts or aac 52468 52469 transmuxAndNotify({ 52470 segment, 52471 bytes, 52472 trackInfoFn, 52473 timingInfoFn, 52474 videoSegmentTimingInfoFn, 52475 audioSegmentTimingInfoFn, 52476 id3Fn, 52477 captionsFn, 52478 isEndOfTimeline, 52479 endedTimelineFn, 52480 dataFn, 52481 doneFn, 52482 onTransmuxerLog 52483 }); 52484 }; 52485 const decrypt = function ({ 52486 id, 52487 key, 52488 encryptedBytes, 52489 decryptionWorker 52490 }, callback) { 52491 const decryptionHandler = event => { 52492 if (event.data.source === id) { 52493 decryptionWorker.removeEventListener('message', decryptionHandler); 52494 const decrypted = event.data.decrypted; 52495 callback(new Uint8Array(decrypted.bytes, decrypted.byteOffset, decrypted.byteLength)); 52496 } 52497 }; 52498 decryptionWorker.addEventListener('message', decryptionHandler); 52499 let keyBytes; 52500 if (key.bytes.slice) { 52501 keyBytes = key.bytes.slice(); 52502 } else { 52503 keyBytes = new Uint32Array(Array.prototype.slice.call(key.bytes)); 52504 } // incrementally decrypt the bytes 52505 52506 decryptionWorker.postMessage(createTransferableMessage({ 52507 source: id, 52508 encrypted: encryptedBytes, 52509 key: keyBytes, 52510 iv: key.iv 52511 }), [encryptedBytes.buffer, keyBytes.buffer]); 52512 }; 52513 /** 52514 * Decrypt the segment via the decryption web worker 52515 * 52516 * @param {WebWorker} decryptionWorker - a WebWorker interface to AES-128 decryption 52517 * routines 52518 * @param {Object} segment - a simplified copy of the segmentInfo object 52519 * from SegmentLoader 52520 * @param {Function} trackInfoFn - a callback that receives track info 52521 * @param {Function} timingInfoFn - a callback that receives timing info 52522 * @param {Function} videoSegmentTimingInfoFn 52523 * a callback that receives video timing info based on media times and 52524 * any adjustments made by the transmuxer 52525 * @param {Function} audioSegmentTimingInfoFn 52526 * a callback that receives audio timing info based on media times and 52527 * any adjustments made by the transmuxer 52528 * @param {boolean} isEndOfTimeline 52529 * true if this segment represents the last segment in a timeline 52530 * @param {Function} endedTimelineFn 52531 * a callback made when a timeline is ended, will only be called if 52532 * isEndOfTimeline is true 52533 * @param {Function} dataFn - a callback that is executed when segment bytes are available 52534 * and ready to use 52535 * @param {Function} doneFn - a callback that is executed after decryption has completed 52536 */ 52537 52538 const decryptSegment = ({ 52539 decryptionWorker, 52540 segment, 52541 trackInfoFn, 52542 timingInfoFn, 52543 videoSegmentTimingInfoFn, 52544 audioSegmentTimingInfoFn, 52545 id3Fn, 52546 captionsFn, 52547 isEndOfTimeline, 52548 endedTimelineFn, 52549 dataFn, 52550 doneFn, 52551 onTransmuxerLog 52552 }) => { 52553 decrypt({ 52554 id: segment.requestId, 52555 key: segment.key, 52556 encryptedBytes: segment.encryptedBytes, 52557 decryptionWorker 52558 }, decryptedBytes => { 52559 segment.bytes = decryptedBytes; 52560 handleSegmentBytes({ 52561 segment, 52562 bytes: segment.bytes, 52563 trackInfoFn, 52564 timingInfoFn, 52565 videoSegmentTimingInfoFn, 52566 audioSegmentTimingInfoFn, 52567 id3Fn, 52568 captionsFn, 52569 isEndOfTimeline, 52570 endedTimelineFn, 52571 dataFn, 52572 doneFn, 52573 onTransmuxerLog 52574 }); 52575 }); 52576 }; 52577 /** 52578 * This function waits for all XHRs to finish (with either success or failure) 52579 * before continueing processing via it's callback. The function gathers errors 52580 * from each request into a single errors array so that the error status for 52581 * each request can be examined later. 52582 * 52583 * @param {Object} activeXhrs - an object that tracks all XHR requests 52584 * @param {WebWorker} decryptionWorker - a WebWorker interface to AES-128 decryption 52585 * routines 52586 * @param {Function} trackInfoFn - a callback that receives track info 52587 * @param {Function} timingInfoFn - a callback that receives timing info 52588 * @param {Function} videoSegmentTimingInfoFn 52589 * a callback that receives video timing info based on media times and 52590 * any adjustments made by the transmuxer 52591 * @param {Function} audioSegmentTimingInfoFn 52592 * a callback that receives audio timing info based on media times and 52593 * any adjustments made by the transmuxer 52594 * @param {Function} id3Fn - a callback that receives ID3 metadata 52595 * @param {Function} captionsFn - a callback that receives captions 52596 * @param {boolean} isEndOfTimeline 52597 * true if this segment represents the last segment in a timeline 52598 * @param {Function} endedTimelineFn 52599 * a callback made when a timeline is ended, will only be called if 52600 * isEndOfTimeline is true 52601 * @param {Function} dataFn - a callback that is executed when segment bytes are available 52602 * and ready to use 52603 * @param {Function} doneFn - a callback that is executed after all resources have been 52604 * downloaded and any decryption completed 52605 */ 52606 52607 const waitForCompletion = ({ 52608 activeXhrs, 52609 decryptionWorker, 52610 trackInfoFn, 52611 timingInfoFn, 52612 videoSegmentTimingInfoFn, 52613 audioSegmentTimingInfoFn, 52614 id3Fn, 52615 captionsFn, 52616 isEndOfTimeline, 52617 endedTimelineFn, 52618 dataFn, 52619 doneFn, 52620 onTransmuxerLog 52621 }) => { 52622 let count = 0; 52623 let didError = false; 52624 return (error, segment) => { 52625 if (didError) { 52626 return; 52627 } 52628 if (error) { 52629 didError = true; // If there are errors, we have to abort any outstanding requests 52630 52631 abortAll(activeXhrs); // Even though the requests above are aborted, and in theory we could wait until we 52632 // handle the aborted events from those requests, there are some cases where we may 52633 // never get an aborted event. For instance, if the network connection is lost and 52634 // there were two requests, the first may have triggered an error immediately, while 52635 // the second request remains unsent. In that case, the aborted algorithm will not 52636 // trigger an abort: see https://xhr.spec.whatwg.org/#the-abort()-method 52637 // 52638 // We also can't rely on the ready state of the XHR, since the request that 52639 // triggered the connection error may also show as a ready state of 0 (unsent). 52640 // Therefore, we have to finish this group of requests immediately after the first 52641 // seen error. 52642 52643 return doneFn(error, segment); 52644 } 52645 count += 1; 52646 if (count === activeXhrs.length) { 52647 const segmentFinish = function () { 52648 if (segment.encryptedBytes) { 52649 return decryptSegment({ 52650 decryptionWorker, 52651 segment, 52652 trackInfoFn, 52653 timingInfoFn, 52654 videoSegmentTimingInfoFn, 52655 audioSegmentTimingInfoFn, 52656 id3Fn, 52657 captionsFn, 52658 isEndOfTimeline, 52659 endedTimelineFn, 52660 dataFn, 52661 doneFn, 52662 onTransmuxerLog 52663 }); 52664 } // Otherwise, everything is ready just continue 52665 52666 handleSegmentBytes({ 52667 segment, 52668 bytes: segment.bytes, 52669 trackInfoFn, 52670 timingInfoFn, 52671 videoSegmentTimingInfoFn, 52672 audioSegmentTimingInfoFn, 52673 id3Fn, 52674 captionsFn, 52675 isEndOfTimeline, 52676 endedTimelineFn, 52677 dataFn, 52678 doneFn, 52679 onTransmuxerLog 52680 }); 52681 }; // Keep track of when *all* of the requests have completed 52682 52683 segment.endOfAllRequests = Date.now(); 52684 if (segment.map && segment.map.encryptedBytes && !segment.map.bytes) { 52685 return decrypt({ 52686 decryptionWorker, 52687 // add -init to the "id" to differentiate between segment 52688 // and init segment decryption, just in case they happen 52689 // at the same time at some point in the future. 52690 id: segment.requestId + '-init', 52691 encryptedBytes: segment.map.encryptedBytes, 52692 key: segment.map.key 52693 }, decryptedBytes => { 52694 segment.map.bytes = decryptedBytes; 52695 parseInitSegment(segment, parseError => { 52696 if (parseError) { 52697 abortAll(activeXhrs); 52698 return doneFn(parseError, segment); 52699 } 52700 segmentFinish(); 52701 }); 52702 }); 52703 } 52704 segmentFinish(); 52705 } 52706 }; 52707 }; 52708 /** 52709 * Calls the abort callback if any request within the batch was aborted. Will only call 52710 * the callback once per batch of requests, even if multiple were aborted. 52711 * 52712 * @param {Object} loadendState - state to check to see if the abort function was called 52713 * @param {Function} abortFn - callback to call for abort 52714 */ 52715 52716 const handleLoadEnd = ({ 52717 loadendState, 52718 abortFn 52719 }) => event => { 52720 const request = event.target; 52721 if (request.aborted && abortFn && !loadendState.calledAbortFn) { 52722 abortFn(); 52723 loadendState.calledAbortFn = true; 52724 } 52725 }; 52726 /** 52727 * Simple progress event callback handler that gathers some stats before 52728 * executing a provided callback with the `segment` object 52729 * 52730 * @param {Object} segment - a simplified copy of the segmentInfo object 52731 * from SegmentLoader 52732 * @param {Function} progressFn - a callback that is executed each time a progress event 52733 * is received 52734 * @param {Function} trackInfoFn - a callback that receives track info 52735 * @param {Function} timingInfoFn - a callback that receives timing info 52736 * @param {Function} videoSegmentTimingInfoFn 52737 * a callback that receives video timing info based on media times and 52738 * any adjustments made by the transmuxer 52739 * @param {Function} audioSegmentTimingInfoFn 52740 * a callback that receives audio timing info based on media times and 52741 * any adjustments made by the transmuxer 52742 * @param {boolean} isEndOfTimeline 52743 * true if this segment represents the last segment in a timeline 52744 * @param {Function} endedTimelineFn 52745 * a callback made when a timeline is ended, will only be called if 52746 * isEndOfTimeline is true 52747 * @param {Function} dataFn - a callback that is executed when segment bytes are available 52748 * and ready to use 52749 * @param {Event} event - the progress event object from XMLHttpRequest 52750 */ 52751 52752 const handleProgress = ({ 52753 segment, 52754 progressFn, 52755 trackInfoFn, 52756 timingInfoFn, 52757 videoSegmentTimingInfoFn, 52758 audioSegmentTimingInfoFn, 52759 id3Fn, 52760 captionsFn, 52761 isEndOfTimeline, 52762 endedTimelineFn, 52763 dataFn 52764 }) => event => { 52765 const request = event.target; 52766 if (request.aborted) { 52767 return; 52768 } 52769 segment.stats = merge(segment.stats, getProgressStats(event)); // record the time that we receive the first byte of data 52770 52771 if (!segment.stats.firstBytesReceivedAt && segment.stats.bytesReceived) { 52772 segment.stats.firstBytesReceivedAt = Date.now(); 52773 } 52774 return progressFn(event, segment); 52775 }; 52776 /** 52777 * Load all resources and does any processing necessary for a media-segment 52778 * 52779 * Features: 52780 * decrypts the media-segment if it has a key uri and an iv 52781 * aborts *all* requests if *any* one request fails 52782 * 52783 * The segment object, at minimum, has the following format: 52784 * { 52785 * resolvedUri: String, 52786 * [transmuxer]: Object, 52787 * [byterange]: { 52788 * offset: Number, 52789 * length: Number 52790 * }, 52791 * [key]: { 52792 * resolvedUri: String 52793 * [byterange]: { 52794 * offset: Number, 52795 * length: Number 52796 * }, 52797 * iv: { 52798 * bytes: Uint32Array 52799 * } 52800 * }, 52801 * [map]: { 52802 * resolvedUri: String, 52803 * [byterange]: { 52804 * offset: Number, 52805 * length: Number 52806 * }, 52807 * [bytes]: Uint8Array 52808 * } 52809 * } 52810 * ...where [name] denotes optional properties 52811 * 52812 * @param {Function} xhr - an instance of the xhr wrapper in xhr.js 52813 * @param {Object} xhrOptions - the base options to provide to all xhr requests 52814 * @param {WebWorker} decryptionWorker - a WebWorker interface to AES-128 52815 * decryption routines 52816 * @param {Object} segment - a simplified copy of the segmentInfo object 52817 * from SegmentLoader 52818 * @param {Function} abortFn - a callback called (only once) if any piece of a request was 52819 * aborted 52820 * @param {Function} progressFn - a callback that receives progress events from the main 52821 * segment's xhr request 52822 * @param {Function} trackInfoFn - a callback that receives track info 52823 * @param {Function} timingInfoFn - a callback that receives timing info 52824 * @param {Function} videoSegmentTimingInfoFn 52825 * a callback that receives video timing info based on media times and 52826 * any adjustments made by the transmuxer 52827 * @param {Function} audioSegmentTimingInfoFn 52828 * a callback that receives audio timing info based on media times and 52829 * any adjustments made by the transmuxer 52830 * @param {Function} id3Fn - a callback that receives ID3 metadata 52831 * @param {Function} captionsFn - a callback that receives captions 52832 * @param {boolean} isEndOfTimeline 52833 * true if this segment represents the last segment in a timeline 52834 * @param {Function} endedTimelineFn 52835 * a callback made when a timeline is ended, will only be called if 52836 * isEndOfTimeline is true 52837 * @param {Function} dataFn - a callback that receives data from the main segment's xhr 52838 * request, transmuxed if needed 52839 * @param {Function} doneFn - a callback that is executed only once all requests have 52840 * succeeded or failed 52841 * @return {Function} a function that, when invoked, immediately aborts all 52842 * outstanding requests 52843 */ 52844 52845 const mediaSegmentRequest = ({ 52846 xhr, 52847 xhrOptions, 52848 decryptionWorker, 52849 segment, 52850 abortFn, 52851 progressFn, 52852 trackInfoFn, 52853 timingInfoFn, 52854 videoSegmentTimingInfoFn, 52855 audioSegmentTimingInfoFn, 52856 id3Fn, 52857 captionsFn, 52858 isEndOfTimeline, 52859 endedTimelineFn, 52860 dataFn, 52861 doneFn, 52862 onTransmuxerLog 52863 }) => { 52864 const activeXhrs = [];
vendor: 4,917 bytes, lines 52865-52995
52865 const finishProcessingFn = waitForCompletion({ 52866 activeXhrs, 52867 decryptionWorker, 52868 trackInfoFn, 52869 timingInfoFn, 52870 videoSegmentTimingInfoFn, 52871 audioSegmentTimingInfoFn, 52872 id3Fn, 52873 captionsFn, 52874 isEndOfTimeline, 52875 endedTimelineFn, 52876 dataFn, 52877 doneFn, 52878 onTransmuxerLog 52879 }); // optionally, request the decryption key 52880 52881 if (segment.key && !segment.key.bytes) { 52882 const objects = [segment.key]; 52883 if (segment.map && !segment.map.bytes && segment.map.key && segment.map.key.resolvedUri === segment.key.resolvedUri) { 52884 objects.push(segment.map.key); 52885 } 52886 const keyRequestOptions = merge(xhrOptions, { 52887 uri: segment.key.resolvedUri, 52888 responseType: 'arraybuffer' 52889 }); 52890 const keyRequestCallback = handleKeyResponse(segment, objects, finishProcessingFn); 52891 const keyXhr = xhr(keyRequestOptions, keyRequestCallback); 52892 activeXhrs.push(keyXhr); 52893 } // optionally, request the associated media init segment 52894 52895 if (segment.map && !segment.map.bytes) { 52896 const differentMapKey = segment.map.key && (!segment.key || segment.key.resolvedUri !== segment.map.key.resolvedUri); 52897 if (differentMapKey) { 52898 const mapKeyRequestOptions = merge(xhrOptions, { 52899 uri: segment.map.key.resolvedUri, 52900 responseType: 'arraybuffer' 52901 }); 52902 const mapKeyRequestCallback = handleKeyResponse(segment, [segment.map.key], finishProcessingFn); 52903 const mapKeyXhr = xhr(mapKeyRequestOptions, mapKeyRequestCallback); 52904 activeXhrs.push(mapKeyXhr); 52905 } 52906 const initSegmentOptions = merge(xhrOptions, { 52907 uri: segment.map.resolvedUri, 52908 responseType: 'arraybuffer', 52909 headers: segmentXhrHeaders(segment.map) 52910 }); 52911 const initSegmentRequestCallback = handleInitSegmentResponse({ 52912 segment, 52913 finishProcessingFn 52914 }); 52915 const initSegmentXhr = xhr(initSegmentOptions, initSegmentRequestCallback); 52916 activeXhrs.push(initSegmentXhr); 52917 } 52918 const segmentRequestOptions = merge(xhrOptions, { 52919 uri: segment.part && segment.part.resolvedUri || segment.resolvedUri, 52920 responseType: 'arraybuffer', 52921 headers: segmentXhrHeaders(segment) 52922 }); 52923 const segmentRequestCallback = handleSegmentResponse({ 52924 segment, 52925 finishProcessingFn, 52926 responseType: segmentRequestOptions.responseType 52927 }); 52928 const segmentXhr = xhr(segmentRequestOptions, segmentRequestCallback); 52929 segmentXhr.addEventListener('progress', handleProgress({ 52930 segment, 52931 progressFn, 52932 trackInfoFn, 52933 timingInfoFn, 52934 videoSegmentTimingInfoFn, 52935 audioSegmentTimingInfoFn, 52936 id3Fn, 52937 captionsFn, 52938 isEndOfTimeline, 52939 endedTimelineFn, 52940 dataFn 52941 })); 52942 activeXhrs.push(segmentXhr); // since all parts of the request must be considered, but should not make callbacks 52943 // multiple times, provide a shared state object 52944 52945 const loadendState = {}; 52946 activeXhrs.forEach(activeXhr => { 52947 activeXhr.addEventListener('loadend', handleLoadEnd({ 52948 loadendState, 52949 abortFn 52950 })); 52951 }); 52952 return () => abortAll(activeXhrs); 52953 }; 52954 52955 /** 52956 * @file - codecs.js - Handles tasks regarding codec strings such as translating them to 52957 * codec strings, or translating codec strings into objects that can be examined. 52958 */ 52959 const logFn$1 = logger('CodecUtils'); 52960 /** 52961 * Returns a set of codec strings parsed from the playlist or the default 52962 * codec strings if no codecs were specified in the playlist 52963 * 52964 * @param {Playlist} media the current media playlist 52965 * @return {Object} an object with the video and audio codecs 52966 */ 52967 52968 const getCodecs = function (media) { 52969 // if the codecs were explicitly specified, use them instead of the 52970 // defaults 52971 const mediaAttributes = media.attributes || {}; 52972 if (mediaAttributes.CODECS) { 52973 return parseCodecs(mediaAttributes.CODECS); 52974 } 52975 }; 52976 const isMaat = (main, media) => { 52977 const mediaAttributes = media.attributes || {}; 52978 return main && main.mediaGroups && main.mediaGroups.AUDIO && mediaAttributes.AUDIO && main.mediaGroups.AUDIO[mediaAttributes.AUDIO]; 52979 }; 52980 const isMuxed = (main, media) => { 52981 if (!isMaat(main, media)) { 52982 return true; 52983 } 52984 const mediaAttributes = media.attributes || {}; 52985 const audioGroup = main.mediaGroups.AUDIO[mediaAttributes.AUDIO]; 52986 for (const groupId in audioGroup) { 52987 // If an audio group has a URI (the case for HLS, as HLS will use external playlists), 52988 // or there are listed playlists (the case for DASH, as the manifest will have already 52989 // provided all of the details necessary to generate the audio playlist, as opposed to 52990 // HLS' externally requested playlists), then the content is demuxed. 52991 if (!audioGroup[groupId].uri && !audioGroup[groupId].playlists) { 52992 return true; 52993 } 52994 } 52995 return false;
vendor: 5,328 bytes, lines 52996-53150
52996 }; 52997 const unwrapCodecList = function (codecList) { 52998 const codecs = {}; 52999 codecList.forEach(({ 53000 mediaType, 53001 type, 53002 details 53003 }) => { 53004 codecs[mediaType] = codecs[mediaType] || []; 53005 codecs[mediaType].push(translateLegacyCodec(`${type}${details}`)); 53006 }); 53007 Object.keys(codecs).forEach(function (mediaType) { 53008 if (codecs[mediaType].length > 1) { 53009 logFn$1(`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.`); 53010 codecs[mediaType] = null; 53011 return; 53012 } 53013 codecs[mediaType] = codecs[mediaType][0]; 53014 }); 53015 return codecs; 53016 }; 53017 const codecCount = function (codecObj) { 53018 let count = 0; 53019 if (codecObj.audio) { 53020 count++; 53021 } 53022 if (codecObj.video) { 53023 count++; 53024 } 53025 return count; 53026 }; 53027 /** 53028 * Calculates the codec strings for a working configuration of 53029 * SourceBuffers to play variant streams in a main playlist. If 53030 * there is no possible working configuration, an empty object will be 53031 * returned. 53032 * 53033 * @param main {Object} the m3u8 object for the main playlist 53034 * @param media {Object} the m3u8 object for the variant playlist 53035 * @return {Object} the codec strings. 53036 * 53037 * @private 53038 */ 53039 53040 const codecsForPlaylist = function (main, media) { 53041 const mediaAttributes = media.attributes || {}; 53042 const codecInfo = unwrapCodecList(getCodecs(media) || []); // HLS with multiple-audio tracks must always get an audio codec. 53043 // Put another way, there is no way to have a video-only multiple-audio HLS! 53044 53045 if (isMaat(main, media) && !codecInfo.audio) { 53046 if (!isMuxed(main, media)) { 53047 // It is possible for codecs to be specified on the audio media group playlist but 53048 // not on the rendition playlist. This is mostly the case for DASH, where audio and 53049 // video are always separate (and separately specified). 53050 const defaultCodecs = unwrapCodecList(codecsFromDefault(main, mediaAttributes.AUDIO) || []); 53051 if (defaultCodecs.audio) { 53052 codecInfo.audio = defaultCodecs.audio; 53053 } 53054 } 53055 } 53056 return codecInfo; 53057 }; 53058 const logFn = logger('PlaylistSelector'); 53059 const representationToString = function (representation) { 53060 if (!representation || !representation.playlist) { 53061 return; 53062 } 53063 const playlist = representation.playlist; 53064 return JSON.stringify({ 53065 id: playlist.id, 53066 bandwidth: representation.bandwidth, 53067 width: representation.width, 53068 height: representation.height, 53069 codecs: playlist.attributes && playlist.attributes.CODECS || '' 53070 }); 53071 }; // Utilities 53072 53073 /** 53074 * Returns the CSS value for the specified property on an element 53075 * using `getComputedStyle`. Firefox has a long-standing issue where 53076 * getComputedStyle() may return null when running in an iframe with 53077 * `display: none`. 53078 * 53079 * @see https://bugzilla.mozilla.org/show_bug.cgi?id=548397 53080 * @param {HTMLElement} el the htmlelement to work on 53081 * @param {string} the proprety to get the style for 53082 */ 53083 53084 const safeGetComputedStyle = function (el, property) { 53085 if (!el) { 53086 return ''; 53087 } 53088 const result = window.getComputedStyle(el); 53089 if (!result) { 53090 return ''; 53091 } 53092 return result[property]; 53093 }; 53094 /** 53095 * Resuable stable sort function 53096 * 53097 * @param {Playlists} array 53098 * @param {Function} sortFn Different comparators 53099 * @function stableSort 53100 */ 53101 53102 const stableSort = function (array, sortFn) { 53103 const newArray = array.slice(); 53104 array.sort(function (left, right) { 53105 const cmp = sortFn(left, right); 53106 if (cmp === 0) { 53107 return newArray.indexOf(left) - newArray.indexOf(right); 53108 } 53109 return cmp; 53110 }); 53111 }; 53112 /** 53113 * A comparator function to sort two playlist object by bandwidth. 53114 * 53115 * @param {Object} left a media playlist object 53116 * @param {Object} right a media playlist object 53117 * @return {number} Greater than zero if the bandwidth attribute of 53118 * left is greater than the corresponding attribute of right. Less 53119 * than zero if the bandwidth of right is greater than left and 53120 * exactly zero if the two are equal. 53121 */ 53122 53123 const comparePlaylistBandwidth = function (left, right) { 53124 let leftBandwidth; 53125 let rightBandwidth; 53126 if (left.attributes.BANDWIDTH) { 53127 leftBandwidth = left.attributes.BANDWIDTH; 53128 } 53129 leftBandwidth = leftBandwidth || window.Number.MAX_VALUE; 53130 if (right.attributes.BANDWIDTH) { 53131 rightBandwidth = right.attributes.BANDWIDTH; 53132 } 53133 rightBandwidth = rightBandwidth || window.Number.MAX_VALUE; 53134 return leftBandwidth - rightBandwidth; 53135 }; 53136 /** 53137 * A comparator function to sort two playlist object by resolution (width). 53138 * 53139 * @param {Object} left a media playlist object 53140 * @param {Object} right a media playlist object 53141 * @return {number} Greater than zero if the resolution.width attribute of 53142 * left is greater than the corresponding attribute of right. Less 53143 * than zero if the resolution.width of right is greater than left and 53144 * exactly zero if the two are equal. 53145 */ 53146 53147 const comparePlaylistResolution = function (left, right) { 53148 let leftWidth; 53149 let rightWidth; 53150 if (left.attributes.RESOLUTION && left.attributes.RESOLUTION.width) {
53151 leftWidth = left.attributes.RESOLUTION.width; 53152 } 53153 leftWidth = leftWidth || window.Number.MAX_VALUE; 53154 if (right.attributes.RESOLUTION && right.attributes.RESOLUTION.width) { 53155 rightWidth = right.attributes.RESOLUTION.width; 53156 } 53157 rightWidth = rightWidth || window.Number.MAX_VALUE; // NOTE - Fallback to bandwidth sort as appropriate in cases where multiple renditions 53158 // have the same media dimensions/ resolution 53159 53160 if (leftWidth === rightWidth && left.attributes.BANDWIDTH && right.attributes.BANDWIDTH) { 53161 return left.attributes.BANDWIDTH - right.attributes.BANDWIDTH; 53162 } 53163 return leftWidth - rightWidth; 53164 }; 53165 /** 53166 * Chooses the appropriate media playlist based on bandwidth and player size 53167 * 53168 * @param {Object} main 53169 * Object representation of the main manifest 53170 * @param {number} playerBandwidth 53171 * Current calculated bandwidth of the player 53172 * @param {number} playerWidth 53173 * Current width of the player element (should account for the device pixel ratio) 53174 * @param {number} playerHeight 53175 * Current height of the player element (should account for the device pixel ratio) 53176 * @param {boolean} limitRenditionByPlayerDimensions 53177 * True if the player width and height should be used during the selection, false otherwise 53178 * @param {Object} playlistController 53179 * the current playlistController object 53180 * @return {Playlist} the highest bitrate playlist less than the 53181 * currently detected bandwidth, accounting for some amount of 53182 * bandwidth variance 53183 */ 53184 53185 let simpleSelector = function (main, playerBandwidth, playerWidth, playerHeight, limitRenditionByPlayerDimensions, playlistController) { 53186 // If we end up getting called before `main` is available, exit early 53187 if (!main) { 53188 return; 53189 } 53190 const options = { 53191 bandwidth: playerBandwidth, 53192 width: playerWidth,
vendor: 14,553 bytes, lines 53193-53486
53193 height: playerHeight, 53194 limitRenditionByPlayerDimensions 53195 }; 53196 let playlists = main.playlists; // if playlist is audio only, select between currently active audio group playlists. 53197 53198 if (Playlist.isAudioOnly(main)) { 53199 playlists = playlistController.getAudioTrackPlaylists_(); // add audioOnly to options so that we log audioOnly: true 53200 // at the buttom of this function for debugging. 53201 53202 options.audioOnly = true; 53203 } // convert the playlists to an intermediary representation to make comparisons easier 53204 53205 let sortedPlaylistReps = playlists.map(playlist => { 53206 let bandwidth; 53207 const width = playlist.attributes && playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.width; 53208 const height = playlist.attributes && playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.height; 53209 bandwidth = playlist.attributes && playlist.attributes.BANDWIDTH; 53210 bandwidth = bandwidth || window.Number.MAX_VALUE; 53211 return { 53212 bandwidth, 53213 width, 53214 height, 53215 playlist 53216 }; 53217 }); 53218 stableSort(sortedPlaylistReps, (left, right) => left.bandwidth - right.bandwidth); // filter out any playlists that have been excluded due to 53219 // incompatible configurations 53220 53221 sortedPlaylistReps = sortedPlaylistReps.filter(rep => !Playlist.isIncompatible(rep.playlist)); // filter out any playlists that have been disabled manually through the representations 53222 // api or excluded temporarily due to playback errors. 53223 53224 let enabledPlaylistReps = sortedPlaylistReps.filter(rep => Playlist.isEnabled(rep.playlist)); 53225 if (!enabledPlaylistReps.length) { 53226 // if there are no enabled playlists, then they have all been excluded or disabled 53227 // by the user through the representations api. In this case, ignore exclusion and 53228 // fallback to what the user wants by using playlists the user has not disabled. 53229 enabledPlaylistReps = sortedPlaylistReps.filter(rep => !Playlist.isDisabled(rep.playlist)); 53230 } // filter out any variant that has greater effective bitrate 53231 // than the current estimated bandwidth 53232 53233 const bandwidthPlaylistReps = enabledPlaylistReps.filter(rep => rep.bandwidth * Config.BANDWIDTH_VARIANCE < playerBandwidth); 53234 let highestRemainingBandwidthRep = bandwidthPlaylistReps[bandwidthPlaylistReps.length - 1]; // get all of the renditions with the same (highest) bandwidth 53235 // and then taking the very first element 53236 53237 const bandwidthBestRep = bandwidthPlaylistReps.filter(rep => rep.bandwidth === highestRemainingBandwidthRep.bandwidth)[0]; // if we're not going to limit renditions by player size, make an early decision. 53238 53239 if (limitRenditionByPlayerDimensions === false) { 53240 const chosenRep = bandwidthBestRep || enabledPlaylistReps[0] || sortedPlaylistReps[0]; 53241 if (chosenRep && chosenRep.playlist) { 53242 let type = 'sortedPlaylistReps'; 53243 if (bandwidthBestRep) { 53244 type = 'bandwidthBestRep'; 53245 } 53246 if (enabledPlaylistReps[0]) { 53247 type = 'enabledPlaylistReps'; 53248 } 53249 logFn(`choosing ${representationToString(chosenRep)} using ${type} with options`, options); 53250 return chosenRep.playlist; 53251 } 53252 logFn('could not choose a playlist with options', options); 53253 return null; 53254 } // filter out playlists without resolution information 53255 53256 const haveResolution = bandwidthPlaylistReps.filter(rep => rep.width && rep.height); // sort variants by resolution 53257 53258 stableSort(haveResolution, (left, right) => left.width - right.width); // if we have the exact resolution as the player use it 53259 53260 const resolutionBestRepList = haveResolution.filter(rep => rep.width === playerWidth && rep.height === playerHeight); 53261 highestRemainingBandwidthRep = resolutionBestRepList[resolutionBestRepList.length - 1]; // ensure that we pick the highest bandwidth variant that have exact resolution 53262 53263 const resolutionBestRep = resolutionBestRepList.filter(rep => rep.bandwidth === highestRemainingBandwidthRep.bandwidth)[0]; 53264 let resolutionPlusOneList; 53265 let resolutionPlusOneSmallest; 53266 let resolutionPlusOneRep; // find the smallest variant that is larger than the player 53267 // if there is no match of exact resolution 53268 53269 if (!resolutionBestRep) { 53270 resolutionPlusOneList = haveResolution.filter(rep => rep.width > playerWidth || rep.height > playerHeight); // find all the variants have the same smallest resolution 53271 53272 resolutionPlusOneSmallest = resolutionPlusOneList.filter(rep => rep.width === resolutionPlusOneList[0].width && rep.height === resolutionPlusOneList[0].height); // ensure that we also pick the highest bandwidth variant that 53273 // is just-larger-than the video player 53274 53275 highestRemainingBandwidthRep = resolutionPlusOneSmallest[resolutionPlusOneSmallest.length - 1]; 53276 resolutionPlusOneRep = resolutionPlusOneSmallest.filter(rep => rep.bandwidth === highestRemainingBandwidthRep.bandwidth)[0]; 53277 } 53278 let leastPixelDiffRep; // If this selector proves to be better than others, 53279 // resolutionPlusOneRep and resolutionBestRep and all 53280 // the code involving them should be removed. 53281 53282 if (playlistController.leastPixelDiffSelector) { 53283 // find the variant that is closest to the player's pixel size 53284 const leastPixelDiffList = haveResolution.map(rep => { 53285 rep.pixelDiff = Math.abs(rep.width - playerWidth) + Math.abs(rep.height - playerHeight); 53286 return rep; 53287 }); // get the highest bandwidth, closest resolution playlist 53288 53289 stableSort(leastPixelDiffList, (left, right) => { 53290 // sort by highest bandwidth if pixelDiff is the same 53291 if (left.pixelDiff === right.pixelDiff) { 53292 return right.bandwidth - left.bandwidth; 53293 } 53294 return left.pixelDiff - right.pixelDiff; 53295 }); 53296 leastPixelDiffRep = leastPixelDiffList[0]; 53297 } // fallback chain of variants 53298 53299 const chosenRep = leastPixelDiffRep || resolutionPlusOneRep || resolutionBestRep || bandwidthBestRep || enabledPlaylistReps[0] || sortedPlaylistReps[0]; 53300 if (chosenRep && chosenRep.playlist) { 53301 let type = 'sortedPlaylistReps'; 53302 if (leastPixelDiffRep) { 53303 type = 'leastPixelDiffRep'; 53304 } else if (resolutionPlusOneRep) { 53305 type = 'resolutionPlusOneRep'; 53306 } else if (resolutionBestRep) { 53307 type = 'resolutionBestRep'; 53308 } else if (bandwidthBestRep) { 53309 type = 'bandwidthBestRep'; 53310 } else if (enabledPlaylistReps[0]) { 53311 type = 'enabledPlaylistReps'; 53312 } 53313 logFn(`choosing ${representationToString(chosenRep)} using ${type} with options`, options); 53314 return chosenRep.playlist; 53315 } 53316 logFn('could not choose a playlist with options', options); 53317 return null; 53318 }; 53319 53320 /** 53321 * Chooses the appropriate media playlist based on the most recent 53322 * bandwidth estimate and the player size. 53323 * 53324 * Expects to be called within the context of an instance of VhsHandler 53325 * 53326 * @return {Playlist} the highest bitrate playlist less than the 53327 * currently detected bandwidth, accounting for some amount of 53328 * bandwidth variance 53329 */ 53330 53331 const lastBandwidthSelector = function () { 53332 const pixelRatio = this.useDevicePixelRatio ? window.devicePixelRatio || 1 : 1; 53333 return simpleSelector(this.playlists.main, this.systemBandwidth, parseInt(safeGetComputedStyle(this.tech_.el(), 'width'), 10) * pixelRatio, parseInt(safeGetComputedStyle(this.tech_.el(), 'height'), 10) * pixelRatio, this.limitRenditionByPlayerDimensions, this.playlistController_); 53334 }; 53335 /** 53336 * Chooses the appropriate media playlist based on an 53337 * exponential-weighted moving average of the bandwidth after 53338 * filtering for player size. 53339 * 53340 * Expects to be called within the context of an instance of VhsHandler 53341 * 53342 * @param {number} decay - a number between 0 and 1. Higher values of 53343 * this parameter will cause previous bandwidth estimates to lose 53344 * significance more quickly. 53345 * @return {Function} a function which can be invoked to create a new 53346 * playlist selector function. 53347 * @see https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average 53348 */ 53349 53350 const movingAverageBandwidthSelector = function (decay) { 53351 let average = -1; 53352 let lastSystemBandwidth = -1; 53353 if (decay < 0 || decay > 1) { 53354 throw new Error('Moving average bandwidth decay must be between 0 and 1.'); 53355 } 53356 return function () { 53357 const pixelRatio = this.useDevicePixelRatio ? window.devicePixelRatio || 1 : 1; 53358 if (average < 0) { 53359 average = this.systemBandwidth; 53360 lastSystemBandwidth = this.systemBandwidth; 53361 } // stop the average value from decaying for every 250ms 53362 // when the systemBandwidth is constant 53363 // and 53364 // stop average from setting to a very low value when the 53365 // systemBandwidth becomes 0 in case of chunk cancellation 53366 53367 if (this.systemBandwidth > 0 && this.systemBandwidth !== lastSystemBandwidth) { 53368 average = decay * this.systemBandwidth + (1 - decay) * average; 53369 lastSystemBandwidth = this.systemBandwidth; 53370 } 53371 return simpleSelector(this.playlists.main, average, parseInt(safeGetComputedStyle(this.tech_.el(), 'width'), 10) * pixelRatio, parseInt(safeGetComputedStyle(this.tech_.el(), 'height'), 10) * pixelRatio, this.limitRenditionByPlayerDimensions, this.playlistController_); 53372 }; 53373 }; 53374 /** 53375 * Chooses the appropriate media playlist based on the potential to rebuffer 53376 * 53377 * @param {Object} settings 53378 * Object of information required to use this selector 53379 * @param {Object} settings.main 53380 * Object representation of the main manifest 53381 * @param {number} settings.currentTime 53382 * The current time of the player 53383 * @param {number} settings.bandwidth 53384 * Current measured bandwidth 53385 * @param {number} settings.duration 53386 * Duration of the media 53387 * @param {number} settings.segmentDuration 53388 * Segment duration to be used in round trip time calculations 53389 * @param {number} settings.timeUntilRebuffer 53390 * Time left in seconds until the player has to rebuffer 53391 * @param {number} settings.currentTimeline 53392 * The current timeline segments are being loaded from 53393 * @param {SyncController} settings.syncController 53394 * SyncController for determining if we have a sync point for a given playlist 53395 * @return {Object|null} 53396 * {Object} return.playlist 53397 * The highest bandwidth playlist with the least amount of rebuffering 53398 * {Number} return.rebufferingImpact 53399 * The amount of time in seconds switching to this playlist will rebuffer. A 53400 * negative value means that switching will cause zero rebuffering. 53401 */ 53402 53403 const minRebufferMaxBandwidthSelector = function (settings) { 53404 const { 53405 main, 53406 currentTime, 53407 bandwidth, 53408 duration, 53409 segmentDuration, 53410 timeUntilRebuffer, 53411 currentTimeline, 53412 syncController 53413 } = settings; // filter out any playlists that have been excluded due to 53414 // incompatible configurations 53415 53416 const compatiblePlaylists = main.playlists.filter(playlist => !Playlist.isIncompatible(playlist)); // filter out any playlists that have been disabled manually through the representations 53417 // api or excluded temporarily due to playback errors. 53418 53419 let enabledPlaylists = compatiblePlaylists.filter(Playlist.isEnabled); 53420 if (!enabledPlaylists.length) { 53421 // if there are no enabled playlists, then they have all been excluded or disabled 53422 // by the user through the representations api. In this case, ignore exclusion and 53423 // fallback to what the user wants by using playlists the user has not disabled. 53424 enabledPlaylists = compatiblePlaylists.filter(playlist => !Playlist.isDisabled(playlist)); 53425 } 53426 const bandwidthPlaylists = enabledPlaylists.filter(Playlist.hasAttribute.bind(null, 'BANDWIDTH')); 53427 const rebufferingEstimates = bandwidthPlaylists.map(playlist => { 53428 const syncPoint = syncController.getSyncPoint(playlist, duration, currentTimeline, currentTime); // If there is no sync point for this playlist, switching to it will require a 53429 // sync request first. This will double the request time 53430 53431 const numRequests = syncPoint ? 1 : 2; 53432 const requestTimeEstimate = Playlist.estimateSegmentRequestTime(segmentDuration, bandwidth, playlist); 53433 const rebufferingImpact = requestTimeEstimate * numRequests - timeUntilRebuffer; 53434 return { 53435 playlist, 53436 rebufferingImpact 53437 }; 53438 }); 53439 const noRebufferingPlaylists = rebufferingEstimates.filter(estimate => estimate.rebufferingImpact <= 0); // Sort by bandwidth DESC 53440 53441 stableSort(noRebufferingPlaylists, (a, b) => comparePlaylistBandwidth(b.playlist, a.playlist)); 53442 if (noRebufferingPlaylists.length) { 53443 return noRebufferingPlaylists[0]; 53444 } 53445 stableSort(rebufferingEstimates, (a, b) => a.rebufferingImpact - b.rebufferingImpact); 53446 return rebufferingEstimates[0] || null; 53447 }; 53448 /** 53449 * Chooses the appropriate media playlist, which in this case is the lowest bitrate 53450 * one with video. If no renditions with video exist, return the lowest audio rendition. 53451 * 53452 * Expects to be called within the context of an instance of VhsHandler 53453 * 53454 * @return {Object|null} 53455 * {Object} return.playlist 53456 * The lowest bitrate playlist that contains a video codec. If no such rendition 53457 * exists pick the lowest audio rendition. 53458 */ 53459 53460 const lowestBitrateCompatibleVariantSelector = function () { 53461 // filter out any playlists that have been excluded due to 53462 // incompatible configurations or playback errors 53463 const playlists = this.playlists.main.playlists.filter(Playlist.isEnabled); // Sort ascending by bitrate 53464 53465 stableSort(playlists, (a, b) => comparePlaylistBandwidth(a, b)); // Parse and assume that playlists with no video codec have no video 53466 // (this is not necessarily true, although it is generally true). 53467 // 53468 // If an entire manifest has no valid videos everything will get filtered 53469 // out. 53470 53471 const playlistsWithVideo = playlists.filter(playlist => !!codecsForPlaylist(this.playlists.main, playlist).video); 53472 return playlistsWithVideo[0] || null; 53473 }; 53474 53475 /** 53476 * Combine all segments into a single Uint8Array 53477 * 53478 * @param {Object} segmentObj 53479 * @return {Uint8Array} concatenated bytes 53480 * @private 53481 */ 53482 const concatSegments = segmentObj => { 53483 let offset = 0; 53484 let tempBuffer; 53485 if (segmentObj.bytes) { 53486 tempBuffer = new Uint8Array(segmentObj.bytes);
53486 // combine the individual segments into one large typed-array 53487 53488 segmentObj.segments.forEach(segment => { 53489 tempBuffer.set(segment, offset); 53490 offset += segment.byteLength; 53491 }); 53492 } 53493 return tempBuffer; 53494 };
vendor: 15,181 bytes, lines 53494-53960
53494 53495 53496 /** 53497 * @file text-tracks.js 53498 */ 53499 /** 53500 * Create captions text tracks on video.js if they do not exist 53501 * 53502 * @param {Object} inbandTextTracks a reference to current inbandTextTracks 53503 * @param {Object} tech the video.js tech 53504 * @param {Object} captionStream the caption stream to create 53505 * @private 53506 */ 53507 53508 const createCaptionsTrackIfNotExists = function (inbandTextTracks, tech, captionStream) { 53509 if (!inbandTextTracks[captionStream]) { 53510 tech.trigger({ 53511 type: 'usage', 53512 name: 'vhs-608' 53513 }); 53514 let instreamId = captionStream; // we need to translate SERVICEn for 708 to how mux.js currently labels them 53515 53516 if (/^cc708_/.test(captionStream)) { 53517 instreamId = 'SERVICE' + captionStream.split('_')[1]; 53518 } 53519 const track = tech.textTracks().getTrackById(instreamId); 53520 if (track) { 53521 // Resuse an existing track with a CC# id because this was 53522 // very likely created by videojs-contrib-hls from information 53523 // in the m3u8 for us to use 53524 inbandTextTracks[captionStream] = track; 53525 } else { 53526 // This section gets called when we have caption services that aren't specified in the manifest. 53527 // Manifest level caption services are handled in media-groups.js under CLOSED-CAPTIONS. 53528 const captionServices = tech.options_.vhs && tech.options_.vhs.captionServices || {}; 53529 let label = captionStream; 53530 let language = captionStream; 53531 let def = false; 53532 const captionService = captionServices[instreamId]; 53533 if (captionService) { 53534 label = captionService.label; 53535 language = captionService.language; 53536 def = captionService.default; 53537 } // Otherwise, create a track with the default `CC#` label and 53538 // without a language 53539 53540 inbandTextTracks[captionStream] = tech.addRemoteTextTrack({ 53541 kind: 'captions', 53542 id: instreamId, 53543 // TODO: investigate why this doesn't seem to turn the caption on by default 53544 default: def, 53545 label, 53546 language 53547 }, false).track; 53548 } 53549 } 53550 }; 53551 /** 53552 * Add caption text track data to a source handler given an array of captions 53553 * 53554 * @param {Object} 53555 * @param {Object} inbandTextTracks the inband text tracks 53556 * @param {number} timestampOffset the timestamp offset of the source buffer 53557 * @param {Array} captionArray an array of caption data 53558 * @private 53559 */ 53560 53561 const addCaptionData = function ({ 53562 inbandTextTracks, 53563 captionArray, 53564 timestampOffset 53565 }) { 53566 if (!captionArray) { 53567 return; 53568 } 53569 const Cue = window.WebKitDataCue || window.VTTCue; 53570 captionArray.forEach(caption => { 53571 const track = caption.stream; // in CEA 608 captions, video.js/mux.js sends a content array 53572 // with positioning data 53573 53574 if (caption.content) { 53575 caption.content.forEach(value => { 53576 const cue = new Cue(caption.startTime + timestampOffset, caption.endTime + timestampOffset, value.text); 53577 cue.line = value.line; 53578 cue.align = 'left'; 53579 cue.position = value.position; 53580 cue.positionAlign = 'line-left'; 53581 inbandTextTracks[track].addCue(cue); 53582 }); 53583 } else { 53584 // otherwise, a text value with combined captions is sent 53585 inbandTextTracks[track].addCue(new Cue(caption.startTime + timestampOffset, caption.endTime + timestampOffset, caption.text)); 53586 } 53587 }); 53588 }; 53589 /** 53590 * Define properties on a cue for backwards compatability, 53591 * but warn the user that the way that they are using it 53592 * is depricated and will be removed at a later date. 53593 * 53594 * @param {Cue} cue the cue to add the properties on 53595 * @private 53596 */ 53597 53598 const deprecateOldCue = function (cue) { 53599 Object.defineProperties(cue.frame, { 53600 id: { 53601 get() { 53602 videojs.log.warn('cue.frame.id is deprecated. Use cue.value.key instead.'); 53603 return cue.value.key; 53604 } 53605 }, 53606 value: { 53607 get() { 53608 videojs.log.warn('cue.frame.value is deprecated. Use cue.value.data instead.'); 53609 return cue.value.data; 53610 } 53611 }, 53612 privateData: { 53613 get() { 53614 videojs.log.warn('cue.frame.privateData is deprecated. Use cue.value.data instead.'); 53615 return cue.value.data; 53616 } 53617 } 53618 }); 53619 }; 53620 /** 53621 * Add metadata text track data to a source handler given an array of metadata 53622 * 53623 * @param {Object} 53624 * @param {Object} inbandTextTracks the inband text tracks 53625 * @param {Array} metadataArray an array of meta data 53626 * @param {number} timestampOffset the timestamp offset of the source buffer 53627 * @param {number} videoDuration the duration of the video 53628 * @private 53629 */ 53630 53631 const addMetadata = ({ 53632 inbandTextTracks, 53633 metadataArray, 53634 timestampOffset, 53635 videoDuration 53636 }) => { 53637 if (!metadataArray) { 53638 return; 53639 } 53640 const Cue = window.WebKitDataCue || window.VTTCue; 53641 const metadataTrack = inbandTextTracks.metadataTrack_; 53642 if (!metadataTrack) { 53643 return; 53644 } 53645 metadataArray.forEach(metadata => { 53646 const time = metadata.cueTime + timestampOffset; // if time isn't a finite number between 0 and Infinity, like NaN, 53647 // ignore this bit of metadata. 53648 // This likely occurs when you have an non-timed ID3 tag like TIT2, 53649 // which is the "Title/Songname/Content description" frame 53650 53651 if (typeof time !== 'number' || window.isNaN(time) || time < 0 || !(time < Infinity)) { 53652 return; 53653 } // If we have no frames, we can't create a cue. 53654 53655 if (!metadata.frames || !metadata.frames.length) { 53656 return; 53657 } 53658 metadata.frames.forEach(frame => { 53659 const cue = new Cue(time, time, frame.value || frame.url || frame.data || ''); 53660 cue.frame = frame; 53661 cue.value = frame; 53662 deprecateOldCue(cue); 53663 metadataTrack.addCue(cue); 53664 }); 53665 }); 53666 if (!metadataTrack.cues || !metadataTrack.cues.length) { 53667 return; 53668 } // Updating the metadeta cues so that 53669 // the endTime of each cue is the startTime of the next cue 53670 // the endTime of last cue is the duration of the video 53671 53672 const cues = metadataTrack.cues; 53673 const cuesArray = []; // Create a copy of the TextTrackCueList... 53674 // ...disregarding cues with a falsey value 53675 53676 for (let i = 0; i < cues.length; i++) { 53677 if (cues[i]) { 53678 cuesArray.push(cues[i]); 53679 } 53680 } // Group cues by their startTime value 53681 53682 const cuesGroupedByStartTime = cuesArray.reduce((obj, cue) => { 53683 const timeSlot = obj[cue.startTime] || []; 53684 timeSlot.push(cue); 53685 obj[cue.startTime] = timeSlot; 53686 return obj; 53687 }, {}); // Sort startTimes by ascending order 53688 53689 const sortedStartTimes = Object.keys(cuesGroupedByStartTime).sort((a, b) => Number(a) - Number(b)); // Map each cue group's endTime to the next group's startTime 53690 53691 sortedStartTimes.forEach((startTime, idx) => { 53692 const cueGroup = cuesGroupedByStartTime[startTime]; 53693 const finiteDuration = isFinite(videoDuration) ? videoDuration : startTime; 53694 const nextTime = Number(sortedStartTimes[idx + 1]) || finiteDuration; // Map each cue's endTime the next group's startTime 53695 53696 cueGroup.forEach(cue => { 53697 cue.endTime = nextTime; 53698 }); 53699 }); 53700 }; // object for mapping daterange attributes 53701 53702 const dateRangeAttr = { 53703 id: 'ID', 53704 class: 'CLASS', 53705 startDate: 'START-DATE', 53706 duration: 'DURATION', 53707 endDate: 'END-DATE', 53708 endOnNext: 'END-ON-NEXT', 53709 plannedDuration: 'PLANNED-DURATION', 53710 scte35Out: 'SCTE35-OUT', 53711 scte35In: 'SCTE35-IN' 53712 }; 53713 const dateRangeKeysToOmit = new Set(['id', 'class', 'startDate', 'duration', 'endDate', 'endOnNext', 'startTime', 'endTime', 'processDateRange']); 53714 /** 53715 * Add DateRange metadata text track to a source handler given an array of metadata 53716 * 53717 * @param {Object} 53718 * @param {Object} inbandTextTracks the inband text tracks 53719 * @param {Array} dateRanges parsed media playlist 53720 * @private 53721 */ 53722 53723 const addDateRangeMetadata = ({ 53724 inbandTextTracks, 53725 dateRanges 53726 }) => { 53727 const metadataTrack = inbandTextTracks.metadataTrack_; 53728 if (!metadataTrack) { 53729 return; 53730 } 53731 const Cue = window.WebKitDataCue || window.VTTCue; 53732 dateRanges.forEach(dateRange => { 53733 // we generate multiple cues for each date range with different attributes 53734 for (const key of Object.keys(dateRange)) { 53735 if (dateRangeKeysToOmit.has(key)) { 53736 continue; 53737 } 53738 const cue = new Cue(dateRange.startTime, dateRange.endTime, ''); 53739 cue.id = dateRange.id; 53740 cue.type = 'com.apple.quicktime.HLS'; 53741 cue.value = { 53742 key: dateRangeAttr[key], 53743 data: dateRange[key] 53744 }; 53745 if (key === 'scte35Out' || key === 'scte35In') { 53746 cue.value.data = new Uint8Array(cue.value.data.match(/[\da-f]{2}/gi)).buffer; 53747 } 53748 metadataTrack.addCue(cue); 53749 } 53750 dateRange.processDateRange(); 53751 }); 53752 }; 53753 /** 53754 * Create metadata text track on video.js if it does not exist 53755 * 53756 * @param {Object} inbandTextTracks a reference to current inbandTextTracks 53757 * @param {string} dispatchType the inband metadata track dispatch type 53758 * @param {Object} tech the video.js tech 53759 * @private 53760 */ 53761 53762 const createMetadataTrackIfNotExists = (inbandTextTracks, dispatchType, tech) => { 53763 if (inbandTextTracks.metadataTrack_) { 53764 return; 53765 } 53766 inbandTextTracks.metadataTrack_ = tech.addRemoteTextTrack({ 53767 kind: 'metadata', 53768 label: 'Timed Metadata' 53769 }, false).track; 53770 if (!videojs.browser.IS_ANY_SAFARI) { 53771 inbandTextTracks.metadataTrack_.inBandMetadataTrackDispatchType = dispatchType; 53772 } 53773 }; 53774 /** 53775 * Remove cues from a track on video.js. 53776 * 53777 * @param {Double} start start of where we should remove the cue 53778 * @param {Double} end end of where the we should remove the cue 53779 * @param {Object} track the text track to remove the cues from 53780 * @private 53781 */ 53782 53783 const removeCuesFromTrack = function (start, end, track) { 53784 let i; 53785 let cue; 53786 if (!track) { 53787 return; 53788 } 53789 if (!track.cues) { 53790 return; 53791 } 53792 i = track.cues.length; 53793 while (i--) { 53794 cue = track.cues[i]; // Remove any cue within the provided start and end time 53795 53796 if (cue.startTime >= start && cue.endTime <= end) { 53797 track.removeCue(cue); 53798 } 53799 } 53800 }; 53801 /** 53802 * Remove duplicate cues from a track on video.js (a cue is considered a 53803 * duplicate if it has the same time interval and text as another) 53804 * 53805 * @param {Object} track the text track to remove the duplicate cues from 53806 * @private 53807 */ 53808 53809 const removeDuplicateCuesFromTrack = function (track) { 53810 const cues = track.cues; 53811 if (!cues) { 53812 return; 53813 } 53814 const uniqueCues = {}; 53815 for (let i = cues.length - 1; i >= 0; i--) { 53816 const cue = cues[i]; 53817 const cueKey = `${cue.startTime}-${cue.endTime}-${cue.text}`; 53818 if (uniqueCues[cueKey]) { 53819 track.removeCue(cue); 53820 } else { 53821 uniqueCues[cueKey] = cue; 53822 } 53823 } 53824 }; 53825 53826 /** 53827 * Returns a list of gops in the buffer that have a pts value of 3 seconds or more in 53828 * front of current time. 53829 * 53830 * @param {Array} buffer 53831 * The current buffer of gop information 53832 * @param {number} currentTime 53833 * The current time 53834 * @param {Double} mapping 53835 * Offset to map display time to stream presentation time 53836 * @return {Array} 53837 * List of gops considered safe to append over 53838 */ 53839 53840 const gopsSafeToAlignWith = (buffer, currentTime, mapping) => { 53841 if (typeof currentTime === 'undefined' || currentTime === null || !buffer.length) { 53842 return []; 53843 } // pts value for current time + 3 seconds to give a bit more wiggle room 53844 53845 const currentTimePts = Math.ceil((currentTime - mapping + 3) * clock_1); 53846 let i; 53847 for (i = 0; i < buffer.length; i++) { 53848 if (buffer[i].pts > currentTimePts) { 53849 break; 53850 } 53851 } 53852 return buffer.slice(i); 53853 }; 53854 /** 53855 * Appends gop information (timing and byteLength) received by the transmuxer for the 53856 * gops appended in the last call to appendBuffer 53857 * 53858 * @param {Array} buffer 53859 * The current buffer of gop information 53860 * @param {Array} gops 53861 * List of new gop information 53862 * @param {boolean} replace 53863 * If true, replace the buffer with the new gop information. If false, append the 53864 * new gop information to the buffer in the right location of time. 53865 * @return {Array} 53866 * Updated list of gop information 53867 */ 53868 53869 const updateGopBuffer = (buffer, gops, replace) => { 53870 if (!gops.length) { 53871 return buffer; 53872 } 53873 if (replace) { 53874 // If we are in safe append mode, then completely overwrite the gop buffer 53875 // with the most recent appeneded data. This will make sure that when appending 53876 // future segments, we only try to align with gops that are both ahead of current 53877 // time and in the last segment appended. 53878 return gops.slice(); 53879 } 53880 const start = gops[0].pts; 53881 let i = 0; 53882 for (i; i < buffer.length; i++) { 53883 if (buffer[i].pts >= start) { 53884 break; 53885 } 53886 } 53887 return buffer.slice(0, i).concat(gops); 53888 }; 53889 /** 53890 * Removes gop information in buffer that overlaps with provided start and end 53891 * 53892 * @param {Array} buffer 53893 * The current buffer of gop information 53894 * @param {Double} start 53895 * position to start the remove at 53896 * @param {Double} end 53897 * position to end the remove at 53898 * @param {Double} mapping 53899 * Offset to map display time to stream presentation time 53900 */ 53901 53902 const removeGopBuffer = (buffer, start, end, mapping) => { 53903 const startPts = Math.ceil((start - mapping) * clock_1); 53904 const endPts = Math.ceil((end - mapping) * clock_1); 53905 const updatedBuffer = buffer.slice(); 53906 let i = buffer.length; 53907 while (i--) { 53908 if (buffer[i].pts <= endPts) { 53909 break; 53910 } 53911 } 53912 if (i === -1) { 53913 // no removal because end of remove range is before start of buffer 53914 return updatedBuffer; 53915 } 53916 let j = i + 1; 53917 while (j--) { 53918 if (buffer[j].pts <= startPts) { 53919 break; 53920 } 53921 } // clamp remove range start to 0 index 53922 53923 j = Math.max(j, 0); 53924 updatedBuffer.splice(j, i - j + 1); 53925 return updatedBuffer; 53926 }; 53927 const shallowEqual = function (a, b) { 53928 // if both are undefined 53929 // or one or the other is undefined 53930 // they are not equal 53931 if (!a && !b || !a && b || a && !b) { 53932 return false; 53933 } // they are the same object and thus, equal 53934 53935 if (a === b) { 53936 return true; 53937 } // sort keys so we can make sure they have 53938 // all the same keys later. 53939 53940 const akeys = Object.keys(a).sort(); 53941 const bkeys = Object.keys(b).sort(); // different number of keys, not equal 53942 53943 if (akeys.length !== bkeys.length) { 53944 return false; 53945 } 53946 for (let i = 0; i < akeys.length; i++) { 53947 const key = akeys[i]; // different sorted keys, not equal 53948 53949 if (key !== bkeys[i]) { 53950 return false; 53951 } // different values, not equal 53952 53953 if (a[key] !== b[key]) { 53954 return false; 53955 } 53956 } 53957 return true; 53958 }; 53959 53960 /
53960/ https://www.w3.org/TR/WebIDL-1/#quotaexceedederror 53961 const QUOTA_EXCEEDED_ERR = 22
vendor: 16,403 bytes, lines 53961-54305
53961; 53962 53963 /** 53964 * The segment loader has no recourse except to fetch a segment in the 53965 * current playlist and use the internal timestamps in that segment to 53966 * generate a syncPoint. This function returns a good candidate index 53967 * for that process. 53968 * 53969 * @param {Array} segments - the segments array from a playlist. 53970 * @return {number} An index of a segment from the playlist to load 53971 */ 53972 53973 const getSyncSegmentCandidate = function (currentTimeline, segments, targetTime) { 53974 segments = segments || []; 53975 const timelineSegments = []; 53976 let time = 0; 53977 for (let i = 0; i < segments.length; i++) { 53978 const segment = segments[i]; 53979 if (currentTimeline === segment.timeline) { 53980 timelineSegments.push(i); 53981 time += segment.duration; 53982 if (time > targetTime) { 53983 return i; 53984 } 53985 } 53986 } 53987 if (timelineSegments.length === 0) { 53988 return 0; 53989 } // default to the last timeline segment 53990 53991 return timelineSegments[timelineSegments.length - 1]; 53992 }; // In the event of a quota exceeded error, keep at least one second of back buffer. This 53993 // number was arbitrarily chosen and may be updated in the future, but seemed reasonable 53994 // as a start to prevent any potential issues with removing content too close to the 53995 // playhead. 53996 53997 const MIN_BACK_BUFFER = 1; // in ms 53998 53999 const CHECK_BUFFER_DELAY = 500; 54000 const finite = num => typeof num === 'number' && isFinite(num); // With most content hovering around 30fps, if a segment has a duration less than a half 54001 // frame at 30fps or one frame at 60fps, the bandwidth and throughput calculations will 54002 // not accurately reflect the rest of the content. 54003 54004 const MIN_SEGMENT_DURATION_TO_SAVE_STATS = 1 / 60; 54005 const illegalMediaSwitch = (loaderType, startingMedia, trackInfo) => { 54006 // Although these checks should most likely cover non 'main' types, for now it narrows 54007 // the scope of our checks. 54008 if (loaderType !== 'main' || !startingMedia || !trackInfo) { 54009 return null; 54010 } 54011 if (!trackInfo.hasAudio && !trackInfo.hasVideo) { 54012 return 'Neither audio nor video found in segment.'; 54013 } 54014 if (startingMedia.hasVideo && !trackInfo.hasVideo) { 54015 return 'Only audio found in segment when we expected video.' + ' We can\'t switch to audio only from a stream that had video.' + ' To get rid of this message, please add codec information to the manifest.'; 54016 } 54017 if (!startingMedia.hasVideo && trackInfo.hasVideo) { 54018 return 'Video found in segment when we expected only audio.' + ' We can\'t switch to a stream with video from an audio only stream.' + ' To get rid of this message, please add codec information to the manifest.'; 54019 } 54020 return null; 54021 }; 54022 /** 54023 * Calculates a time value that is safe to remove from the back buffer without interrupting 54024 * playback. 54025 * 54026 * @param {TimeRange} seekable 54027 * The current seekable range 54028 * @param {number} currentTime 54029 * The current time of the player 54030 * @param {number} targetDuration 54031 * The target duration of the current playlist 54032 * @return {number} 54033 * Time that is safe to remove from the back buffer without interrupting playback 54034 */ 54035 54036 const safeBackBufferTrimTime = (seekable, currentTime, targetDuration) => { 54037 // 30 seconds before the playhead provides a safe default for trimming. 54038 // 54039 // Choosing a reasonable default is particularly important for high bitrate content and 54040 // VOD videos/live streams with large windows, as the buffer may end up overfilled and 54041 // throw an APPEND_BUFFER_ERR. 54042 let trimTime = currentTime - Config.BACK_BUFFER_LENGTH; 54043 if (seekable.length) { 54044 // Some live playlists may have a shorter window of content than the full allowed back 54045 // buffer. For these playlists, don't save content that's no longer within the window. 54046 trimTime = Math.max(trimTime, seekable.start(0)); 54047 } // Don't remove within target duration of the current time to avoid the possibility of 54048 // removing the GOP currently being played, as removing it can cause playback stalls. 54049 54050 const maxTrimTime = currentTime - targetDuration; 54051 return Math.min(maxTrimTime, trimTime); 54052 }; 54053 const segmentInfoString = segmentInfo => { 54054 const { 54055 startOfSegment, 54056 duration, 54057 segment, 54058 part, 54059 playlist: { 54060 mediaSequence: seq, 54061 id, 54062 segments = [] 54063 }, 54064 mediaIndex: index, 54065 partIndex, 54066 timeline 54067 } = segmentInfo; 54068 const segmentLen = segments.length - 1; 54069 let selection = 'mediaIndex/partIndex increment'; 54070 if (segmentInfo.getMediaInfoForTime) { 54071 selection = `getMediaInfoForTime (${segmentInfo.getMediaInfoForTime})`; 54072 } else if (segmentInfo.isSyncRequest) { 54073 selection = 'getSyncSegmentCandidate (isSyncRequest)'; 54074 } 54075 if (segmentInfo.independent) { 54076 selection += ` with independent ${segmentInfo.independent}`; 54077 } 54078 const hasPartIndex = typeof partIndex === 'number'; 54079 const name = segmentInfo.segment.uri ? 'segment' : 'pre-segment'; 54080 const zeroBasedPartCount = hasPartIndex ? getKnownPartCount({ 54081 preloadSegment: segment 54082 }) - 1 : 0; 54083 return `${name} [${seq + index}/${seq + segmentLen}]` + (hasPartIndex ? ` part [${partIndex}/${zeroBasedPartCount}]` : '') + ` segment start/end [${segment.start} => ${segment.end}]` + (hasPartIndex ? ` part start/end [${part.start} => ${part.end}]` : '') + ` startOfSegment [${startOfSegment}]` + ` duration [${duration}]` + ` timeline [${timeline}]` + ` selected by [${selection}]` + ` playlist [${id}]`; 54084 }; 54085 const timingInfoPropertyForMedia = mediaType => `${mediaType}TimingInfo`; 54086 /** 54087 * Returns the timestamp offset to use for the segment. 54088 * 54089 * @param {number} segmentTimeline 54090 * The timeline of the segment 54091 * @param {number} currentTimeline 54092 * The timeline currently being followed by the loader 54093 * @param {number} startOfSegment 54094 * The estimated segment start 54095 * @param {TimeRange[]} buffered 54096 * The loader's buffer 54097 * @param {boolean} overrideCheck 54098 * If true, no checks are made to see if the timestamp offset value should be set, 54099 * but sets it directly to a value. 54100 * 54101 * @return {number|null} 54102 * Either a number representing a new timestamp offset, or null if the segment is 54103 * part of the same timeline 54104 */ 54105 54106 const timestampOffsetForSegment = ({ 54107 segmentTimeline, 54108 currentTimeline, 54109 startOfSegment, 54110 buffered, 54111 overrideCheck 54112 }) => { 54113 // Check to see if we are crossing a discontinuity to see if we need to set the 54114 // timestamp offset on the transmuxer and source buffer. 54115 // 54116 // Previously, we changed the timestampOffset if the start of this segment was less than 54117 // the currently set timestampOffset, but this isn't desirable as it can produce bad 54118 // behavior, especially around long running live streams. 54119 if (!overrideCheck && segmentTimeline === currentTimeline) { 54120 return null; 54121 } // When changing renditions, it's possible to request a segment on an older timeline. For 54122 // instance, given two renditions with the following: 54123 // 54124 // #EXTINF:10 54125 // segment1 54126 // #EXT-X-DISCONTINUITY 54127 // #EXTINF:10 54128 // segment2 54129 // #EXTINF:10 54130 // segment3 54131 // 54132 // And the current player state: 54133 // 54134 // current time: 8 54135 // buffer: 0 => 20 54136 // 54137 // The next segment on the current rendition would be segment3, filling the buffer from 54138 // 20s onwards. However, if a rendition switch happens after segment2 was requested, 54139 // then the next segment to be requested will be segment1 from the new rendition in 54140 // order to fill time 8 and onwards. Using the buffered end would result in repeated 54141 // content (since it would position segment1 of the new rendition starting at 20s). This 54142 // case can be identified when the new segment's timeline is a prior value. Instead of 54143 // using the buffered end, the startOfSegment can be used, which, hopefully, will be 54144 // more accurate to the actual start time of the segment. 54145 54146 if (segmentTimeline < currentTimeline) { 54147 return startOfSegment; 54148 } // segmentInfo.startOfSegment used to be used as the timestamp offset, however, that 54149 // value uses the end of the last segment if it is available. While this value 54150 // should often be correct, it's better to rely on the buffered end, as the new 54151 // content post discontinuity should line up with the buffered end as if it were 54152 // time 0 for the new content. 54153 54154 return buffered.length ? buffered.end(buffered.length - 1) : startOfSegment; 54155 }; 54156 /** 54157 * Returns whether or not the loader should wait for a timeline change from the timeline 54158 * change controller before processing the segment. 54159 * 54160 * Primary timing in VHS goes by video. This is different from most media players, as 54161 * audio is more often used as the primary timing source. For the foreseeable future, VHS 54162 * will continue to use video as the primary timing source, due to the current logic and 54163 * expectations built around it. 54164 54165 * Since the timing follows video, in order to maintain sync, the video loader is 54166 * responsible for setting both audio and video source buffer timestamp offsets. 54167 * 54168 * Setting different values for audio and video source buffers could lead to 54169 * desyncing. The following examples demonstrate some of the situations where this 54170 * distinction is important. Note that all of these cases involve demuxed content. When 54171 * content is muxed, the audio and video are packaged together, therefore syncing 54172 * separate media playlists is not an issue. 54173 * 54174 * CASE 1: Audio prepares to load a new timeline before video: 54175 * 54176 * Timeline: 0 1 54177 * Audio Segments: 0 1 2 3 4 5 DISCO 6 7 8 9 54178 * Audio Loader: ^ 54179 * Video Segments: 0 1 2 3 4 5 DISCO 6 7 8 9 54180 * Video Loader ^ 54181 * 54182 * In the above example, the audio loader is preparing to load the 6th segment, the first 54183 * after a discontinuity, while the video loader is still loading the 5th segment, before 54184 * the discontinuity. 54185 * 54186 * If the audio loader goes ahead and loads and appends the 6th segment before the video 54187 * loader crosses the discontinuity, then when appended, the 6th audio segment will use 54188 * the timestamp offset from timeline 0. This will likely lead to desyncing. In addition, 54189 * the audio loader must provide the audioAppendStart value to trim the content in the 54190 * transmuxer, and that value relies on the audio timestamp offset. Since the audio 54191 * timestamp offset is set by the video (main) loader, the audio loader shouldn't load the 54192 * segment until that value is provided. 54193 * 54194 * CASE 2: Video prepares to load a new timeline before audio: 54195 * 54196 * Timeline: 0 1 54197 * Audio Segments: 0 1 2 3 4 5 DISCO 6 7 8 9 54198 * Audio Loader: ^ 54199 * Video Segments: 0 1 2 3 4 5 DISCO 6 7 8 9 54200 * Video Loader ^ 54201 * 54202 * In the above example, the video loader is preparing to load the 6th segment, the first 54203 * after a discontinuity, while the audio loader is still loading the 5th segment, before 54204 * the discontinuity. 54205 * 54206 * If the video loader goes ahead and loads and appends the 6th segment, then once the 54207 * segment is loaded and processed, both the video and audio timestamp offsets will be 54208 * set, since video is used as the primary timing source. This is to ensure content lines 54209 * up appropriately, as any modifications to the video timing are reflected by audio when 54210 * the video loader sets the audio and video timestamp offsets to the same value. However, 54211 * setting the timestamp offset for audio before audio has had a chance to change 54212 * timelines will likely lead to desyncing, as the audio loader will append segment 5 with 54213 * a timestamp intended to apply to segments from timeline 1 rather than timeline 0. 54214 * 54215 * CASE 3: When seeking, audio prepares to load a new timeline before video 54216 * 54217 * Timeline: 0 1 54218 * Audio Segments: 0 1 2 3 4 5 DISCO 6 7 8 9 54219 * Audio Loader: ^ 54220 * Video Segments: 0 1 2 3 4 5 DISCO 6 7 8 9 54221 * Video Loader ^ 54222 * 54223 * In the above example, both audio and video loaders are loading segments from timeline 54224 * 0, but imagine that the seek originated from timeline 1. 54225 * 54226 * When seeking to a new timeline, the timestamp offset will be set based on the expected 54227 * segment start of the loaded video segment. In order to maintain sync, the audio loader 54228 * must wait for the video loader to load its segment and update both the audio and video 54229 * timestamp offsets before it may load and append its own segment. This is the case 54230 * whether the seek results in a mismatched segment request (e.g., the audio loader 54231 * chooses to load segment 3 and the video loader chooses to load segment 4) or the 54232 * loaders choose to load the same segment index from each playlist, as the segments may 54233 * not be aligned perfectly, even for matching segment indexes. 54234 * 54235 * @param {Object} timelinechangeController 54236 * @param {number} currentTimeline 54237 * The timeline currently being followed by the loader 54238 * @param {number} segmentTimeline 54239 * The timeline of the segment being loaded 54240 * @param {('main'|'audio')} loaderType 54241 * The loader type 54242 * @param {boolean} audioDisabled 54243 * Whether the audio is disabled for the loader. This should only be true when the 54244 * loader may have muxed audio in its segment, but should not append it, e.g., for 54245 * the main loader when an alternate audio playlist is active. 54246 * 54247 * @return {boolean} 54248 * Whether the loader should wait for a timeline change from the timeline change 54249 * controller before processing the segment 54250 */ 54251 54252 const shouldWaitForTimelineChange = ({ 54253 timelineChangeController, 54254 currentTimeline, 54255 segmentTimeline, 54256 loaderType, 54257 audioDisabled 54258 }) => { 54259 if (currentTimeline === segmentTimeline) { 54260 return false; 54261 } 54262 if (loaderType === 'audio') { 54263 const lastMainTimelineChange = timelineChangeController.lastTimelineChange({ 54264 type: 'main' 54265 }); // Audio loader should wait if: 54266 // 54267 // * main hasn't had a timeline change yet (thus has not loaded its first segment) 54268 // * main hasn't yet changed to the timeline audio is looking to load 54269 54270 return !lastMainTimelineChange || lastMainTimelineChange.to !== segmentTimeline; 54271 } // The main loader only needs to wait for timeline changes if there's demuxed audio. 54272 // Otherwise, there's nothing to wait for, since audio would be muxed into the main 54273 // loader's segments (or the content is audio/video only and handled by the main 54274 // loader). 54275 54276 if (loaderType === 'main' && audioDisabled) { 54277 const pendingAudioTimelineChange = timelineChangeController.pendingTimelineChange({ 54278 type: 'audio' 54279 }); // Main loader should wait for the audio loader if audio is not pending a timeline 54280 // change to the current timeline. 54281 // 54282 // Since the main loader is responsible for setting the timestamp offset for both 54283 // audio and video, the main loader must wait for audio to be about to change to its 54284 // timeline before setting the offset, otherwise, if audio is behind in loading, 54285 // segments from the previous timeline would be adjusted by the new timestamp offset. 54286 // 54287 // This requirement means that video will not cross a timeline until the audio is 54288 // about to cross to it, so that way audio and video will always cross the timeline 54289 // together. 54290 // 54291 // In addition to normal timeline changes, these rules also apply to the start of a 54292 // stream (going from a non-existent timeline, -1, to timeline 0). It's important 54293 // that these rules apply to the first timeline change because if they did not, it's 54294 // possible that the main loader will cross two timelines before the audio loader has 54295 // crossed one. Logic may be implemented to handle the startup as a special case, but 54296 // it's easier to simply treat all timeline changes the same. 54297 54298 if (pendingAudioTimelineChange && pendingAudioTimelineChange.to === segmentTimeline) { 54299 return false; 54300 } 54301 return true; 54302 } 54303 return false; 54304 }; 54305 const
vendor: 5,881 bytes, lines 54305-54449
54305mediaDuration = timingInfos => { 54306 let maxDuration = 0; 54307 ['video', 'audio'].forEach(function (type) { 54308 const typeTimingInfo = timingInfos[`${type}TimingInfo`]; 54309 if (!typeTimingInfo) { 54310 return; 54311 } 54312 const { 54313 start, 54314 end 54315 } = typeTimingInfo; 54316 let duration; 54317 if (typeof start === 'bigint' || typeof end === 'bigint') { 54318 duration = window.BigInt(end) - window.BigInt(start); 54319 } else if (typeof start === 'number' && typeof end === 'number') { 54320 duration = end - start; 54321 } 54322 if (typeof duration !== 'undefined' && duration > maxDuration) { 54323 maxDuration = duration; 54324 } 54325 }); // convert back to a number if it is lower than MAX_SAFE_INTEGER 54326 // as we only need BigInt when we are above that. 54327 54328 if (typeof maxDuration === 'bigint' && maxDuration < Number.MAX_SAFE_INTEGER) { 54329 maxDuration = Number(maxDuration); 54330 } 54331 return maxDuration; 54332 }; 54333 const segmentTooLong = ({ 54334 segmentDuration, 54335 maxDuration 54336 }) => { 54337 // 0 duration segments are most likely due to metadata only segments or a lack of 54338 // information. 54339 if (!segmentDuration) { 54340 return false; 54341 } // For HLS: 54342 // 54343 // https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.3.1 54344 // The EXTINF duration of each Media Segment in the Playlist 54345 // file, when rounded to the nearest integer, MUST be less than or equal 54346 // to the target duration; longer segments can trigger playback stalls 54347 // or other errors. 54348 // 54349 // For DASH, the mpd-parser uses the largest reported segment duration as the target 54350 // duration. Although that reported duration is occasionally approximate (i.e., not 54351 // exact), a strict check may report that a segment is too long more often in DASH. 54352 54353 return Math.round(segmentDuration) > maxDuration + TIME_FUDGE_FACTOR; 54354 }; 54355 const getTroublesomeSegmentDurationMessage = (segmentInfo, sourceType) => { 54356 // Right now we aren't following DASH's timing model exactly, so only perform 54357 // this check for HLS content. 54358 if (sourceType !== 'hls') { 54359 return null; 54360 } 54361 const segmentDuration = mediaDuration({ 54362 audioTimingInfo: segmentInfo.audioTimingInfo, 54363 videoTimingInfo: segmentInfo.videoTimingInfo 54364 }); // Don't report if we lack information. 54365 // 54366 // If the segment has a duration of 0 it is either a lack of information or a 54367 // metadata only segment and shouldn't be reported here. 54368 54369 if (!segmentDuration) { 54370 return null; 54371 } 54372 const targetDuration = segmentInfo.playlist.targetDuration; 54373 const isSegmentWayTooLong = segmentTooLong({ 54374 segmentDuration, 54375 maxDuration: targetDuration * 2 54376 }); 54377 const isSegmentSlightlyTooLong = segmentTooLong({ 54378 segmentDuration, 54379 maxDuration: targetDuration 54380 }); 54381 const segmentTooLongMessage = `Segment with index ${segmentInfo.mediaIndex} ` + `from playlist ${segmentInfo.playlist.id} ` + `has a duration of ${segmentDuration} ` + `when the reported duration is ${segmentInfo.duration} ` + `and the target duration is ${targetDuration}. ` + 'For HLS content, a duration in excess of the target duration may result in ' + 'playback issues. See the HLS specification section on EXT-X-TARGETDURATION for ' + 'more details: ' + 'https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.3.1'; 54382 if (isSegmentWayTooLong || isSegmentSlightlyTooLong) { 54383 return { 54384 severity: isSegmentWayTooLong ? 'warn' : 'info', 54385 message: segmentTooLongMessage 54386 }; 54387 } 54388 return null; 54389 }; 54390 /** 54391 * An object that manages segment loading and appending. 54392 * 54393 * @class SegmentLoader 54394 * @param {Object} options required and optional options 54395 * @extends videojs.EventTarget 54396 */ 54397 54398 class SegmentLoader extends videojs.EventTarget { 54399 constructor(settings, options = {}) { 54400 super(); // check pre-conditions 54401 54402 if (!settings) { 54403 throw new TypeError('Initialization settings are required'); 54404 } 54405 if (typeof settings.currentTime !== 'function') { 54406 throw new TypeError('No currentTime getter specified'); 54407 } 54408 if (!settings.mediaSource) { 54409 throw new TypeError('No MediaSource specified'); 54410 } // public properties 54411 54412 this.bandwidth = settings.bandwidth; 54413 this.throughput = { 54414 rate: 0, 54415 count: 0 54416 }; 54417 this.roundTrip = NaN; 54418 this.resetStats_(); 54419 this.mediaIndex = null; 54420 this.partIndex = null; // private settings 54421 54422 this.hasPlayed_ = settings.hasPlayed; 54423 this.currentTime_ = settings.currentTime; 54424 this.seekable_ = settings.seekable; 54425 this.seeking_ = settings.seeking; 54426 this.duration_ = settings.duration; 54427 this.mediaSource_ = settings.mediaSource; 54428 this.vhs_ = settings.vhs; 54429 this.loaderType_ = settings.loaderType; 54430 this.currentMediaInfo_ = void 0; 54431 this.startingMediaInfo_ = void 0; 54432 this.segmentMetadataTrack_ = settings.segmentMetadataTrack; 54433 this.goalBufferLength_ = settings.goalBufferLength; 54434 this.sourceType_ = settings.sourceType; 54435 this.sourceUpdater_ = settings.sourceUpdater; 54436 this.inbandTextTracks_ = settings.inbandTextTracks; 54437 this.state_ = 'INIT'; 54438 this.timelineChangeController_ = settings.timelineChangeController; 54439 this.shouldSaveSegmentTimingInfo_ = true; 54440 this.parse708captions_ = settings.parse708captions; 54441 this.useDtsForTimestampOffset_ = settings.useDtsForTimestampOffset; 54442 this.captionServices_ = settings.captionServices; 54443 this.exactManifestTimings = settings.exactManifestTimings; 54444 this.addMetadataToTextTrack = settings.addMetadataToTextTrack; // private instance variables 54445 54446 this.checkBufferTimeout_ = null; 54447 this.error_ = void 0; 54448 this.currentTimeline_ = -1; 54449 this.shouldForceTimestampOffsetAfterResync_ = false;
vendor: 5,516 bytes, lines 54450-54612
54450 this.pendingSegment_ = null; 54451 this.xhrOptions_ = null; 54452 this.pendingSegments_ = []; 54453 this.audioDisabled_ = false; 54454 this.isPendingTimestampOffset_ = false; // TODO possibly move gopBuffer and timeMapping info to a separate controller 54455 54456 this.gopBuffer_ = []; 54457 this.timeMapping_ = 0; 54458 this.safeAppend_ = false; 54459 this.appendInitSegment_ = { 54460 audio: true, 54461 video: true 54462 }; 54463 this.playlistOfLastInitSegment_ = { 54464 audio: null, 54465 video: null 54466 }; 54467 this.callQueue_ = []; // If the segment loader prepares to load a segment, but does not have enough 54468 // information yet to start the loading process (e.g., if the audio loader wants to 54469 // load a segment from the next timeline but the main loader hasn't yet crossed that 54470 // timeline), then the load call will be added to the queue until it is ready to be 54471 // processed. 54472 54473 this.loadQueue_ = []; 54474 this.metadataQueue_ = { 54475 id3: [], 54476 caption: [] 54477 }; 54478 this.waitingOnRemove_ = false; 54479 this.quotaExceededErrorRetryTimeout_ = null; // Fragmented mp4 playback 54480 54481 this.activeInitSegmentId_ = null; 54482 this.initSegments_ = {}; // HLSe playback 54483 54484 this.cacheEncryptionKeys_ = settings.cacheEncryptionKeys; 54485 this.keyCache_ = {}; 54486 this.decrypter_ = settings.decrypter; // Manages the tracking and generation of sync-points, mappings 54487 // between a time in the display time and a segment index within 54488 // a playlist 54489 54490 this.syncController_ = settings.syncController; 54491 this.syncPoint_ = { 54492 segmentIndex: 0, 54493 time: 0 54494 }; 54495 this.transmuxer_ = this.createTransmuxer_(); 54496 this.triggerSyncInfoUpdate_ = () => this.trigger('syncinfoupdate'); 54497 this.syncController_.on('syncinfoupdate', this.triggerSyncInfoUpdate_); 54498 this.mediaSource_.addEventListener('sourceopen', () => { 54499 if (!this.isEndOfStream_()) { 54500 this.ended_ = false; 54501 } 54502 }); // ...for determining the fetch location 54503 54504 this.fetchAtBuffer_ = false; 54505 this.logger_ = logger(`SegmentLoader[${this.loaderType_}]`); 54506 Object.defineProperty(this, 'state', { 54507 get() { 54508 return this.state_; 54509 }, 54510 set(newState) { 54511 if (newState !== this.state_) { 54512 this.logger_(`${this.state_} -> ${newState}`); 54513 this.state_ = newState; 54514 this.trigger('statechange'); 54515 } 54516 } 54517 }); 54518 this.sourceUpdater_.on('ready', () => { 54519 if (this.hasEnoughInfoToAppend_()) { 54520 this.processCallQueue_(); 54521 } 54522 }); // Only the main loader needs to listen for pending timeline changes, as the main 54523 // loader should wait for audio to be ready to change its timeline so that both main 54524 // and audio timelines change together. For more details, see the 54525 // shouldWaitForTimelineChange function. 54526 54527 if (this.loaderType_ === 'main') { 54528 this.timelineChangeController_.on('pendingtimelinechange', () => { 54529 if (this.hasEnoughInfoToAppend_()) { 54530 this.processCallQueue_(); 54531 } 54532 }); 54533 } // The main loader only listens on pending timeline changes, but the audio loader, 54534 // since its loads follow main, needs to listen on timeline changes. For more details, 54535 // see the shouldWaitForTimelineChange function. 54536 54537 if (this.loaderType_ === 'audio') { 54538 this.timelineChangeController_.on('timelinechange', () => { 54539 if (this.hasEnoughInfoToLoad_()) { 54540 this.processLoadQueue_(); 54541 } 54542 if (this.hasEnoughInfoToAppend_()) { 54543 this.processCallQueue_(); 54544 } 54545 }); 54546 } 54547 } 54548 createTransmuxer_() { 54549 return segmentTransmuxer.createTransmuxer({ 54550 remux: false, 54551 alignGopsAtEnd: this.safeAppend_, 54552 keepOriginalTimestamps: true, 54553 parse708captions: this.parse708captions_, 54554 captionServices: this.captionServices_ 54555 }); 54556 } 54557 /** 54558 * reset all of our media stats 54559 * 54560 * @private 54561 */ 54562 54563 resetStats_() { 54564 this.mediaBytesTransferred = 0; 54565 this.mediaRequests = 0; 54566 this.mediaRequestsAborted = 0; 54567 this.mediaRequestsTimedout = 0; 54568 this.mediaRequestsErrored = 0; 54569 this.mediaTransferDuration = 0; 54570 this.mediaSecondsLoaded = 0; 54571 this.mediaAppends = 0; 54572 } 54573 /** 54574 * dispose of the SegmentLoader and reset to the default state 54575 */ 54576 54577 dispose() { 54578 this.trigger('dispose'); 54579 this.state = 'DISPOSED'; 54580 this.pause(); 54581 this.abort_(); 54582 if (this.transmuxer_) { 54583 this.transmuxer_.terminate(); 54584 } 54585 this.resetStats_(); 54586 if (this.checkBufferTimeout_) { 54587 window.clearTimeout(this.checkBufferTimeout_); 54588 } 54589 if (this.syncController_ && this.triggerSyncInfoUpdate_) { 54590 this.syncController_.off('syncinfoupdate', this.triggerSyncInfoUpdate_); 54591 } 54592 this.off(); 54593 } 54594 setAudio(enable) { 54595 this.audioDisabled_ = !enable; 54596 if (enable) { 54597 this.appendInitSegment_.audio = true; 54598 } else { 54599 // remove current track audio if it gets disabled 54600 this.sourceUpdater_.removeAudio(0, this.duration_()); 54601 } 54602 } 54603 /** 54604 * abort anything that is currently doing on with the SegmentLoader 54605 * and reset to a default state 54606 */ 54607 54608 abort() { 54609 if (this.state !== 'WAITING') { 54610 if (this.pendingSegment_) { 54611 this.pendingSegment_ = null; 54612 }
vendor: 6,902 bytes, lines 54612-54833
54612 54613 return; 54614 } 54615 this.abort_(); // We aborted the requests we were waiting on, so reset the loader's state to READY 54616 // since we are no longer "waiting" on any requests. XHR callback is not always run 54617 // when the request is aborted. This will prevent the loader from being stuck in the 54618 // WAITING state indefinitely. 54619 54620 this.state = 'READY'; // don't wait for buffer check timeouts to begin fetching the 54621 // next segment 54622 54623 if (!this.paused()) { 54624 this.monitorBuffer_(); 54625 } 54626 } 54627 /** 54628 * abort all pending xhr requests and null any pending segements 54629 * 54630 * @private 54631 */ 54632 54633 abort_() { 54634 if (this.pendingSegment_ && this.pendingSegment_.abortRequests) { 54635 this.pendingSegment_.abortRequests(); 54636 } // clear out the segment being processed 54637 54638 this.pendingSegment_ = null; 54639 this.callQueue_ = []; 54640 this.loadQueue_ = []; 54641 this.metadataQueue_.id3 = []; 54642 this.metadataQueue_.caption = []; 54643 this.timelineChangeController_.clearPendingTimelineChange(this.loaderType_); 54644 this.waitingOnRemove_ = false; 54645 window.clearTimeout(this.quotaExceededErrorRetryTimeout_); 54646 this.quotaExceededErrorRetryTimeout_ = null; 54647 } 54648 checkForAbort_(requestId) { 54649 // If the state is APPENDING, then aborts will not modify the state, meaning the first 54650 // callback that happens should reset the state to READY so that loading can continue. 54651 if (this.state === 'APPENDING' && !this.pendingSegment_) { 54652 this.state = 'READY'; 54653 return true; 54654 } 54655 if (!this.pendingSegment_ || this.pendingSegment_.requestId !== requestId) { 54656 return true; 54657 } 54658 return false; 54659 } 54660 /** 54661 * set an error on the segment loader and null out any pending segements 54662 * 54663 * @param {Error} error the error to set on the SegmentLoader 54664 * @return {Error} the error that was set or that is currently set 54665 */ 54666 54667 error(error) { 54668 if (typeof error !== 'undefined') { 54669 this.logger_('error occurred:', error); 54670 this.error_ = error; 54671 } 54672 this.pendingSegment_ = null; 54673 return this.error_; 54674 } 54675 endOfStream() { 54676 this.ended_ = true; 54677 if (this.transmuxer_) { 54678 // need to clear out any cached data to prepare for the new segment 54679 segmentTransmuxer.reset(this.transmuxer_); 54680 } 54681 this.gopBuffer_.length = 0; 54682 this.pause(); 54683 this.trigger('ended'); 54684 } 54685 /** 54686 * Indicates which time ranges are buffered 54687 * 54688 * @return {TimeRange} 54689 * TimeRange object representing the current buffered ranges 54690 */ 54691 54692 buffered_() { 54693 const trackInfo = this.getMediaInfo_(); 54694 if (!this.sourceUpdater_ || !trackInfo) { 54695 return createTimeRanges(); 54696 } 54697 if (this.loaderType_ === 'main') { 54698 const { 54699 hasAudio, 54700 hasVideo, 54701 isMuxed 54702 } = trackInfo; 54703 if (hasVideo && hasAudio && !this.audioDisabled_ && !isMuxed) { 54704 return this.sourceUpdater_.buffered(); 54705 } 54706 if (hasVideo) { 54707 return this.sourceUpdater_.videoBuffered(); 54708 } 54709 } // One case that can be ignored for now is audio only with alt audio, 54710 // as we don't yet have proper support for that. 54711 54712 return this.sourceUpdater_.audioBuffered(); 54713 } 54714 /** 54715 * Gets and sets init segment for the provided map 54716 * 54717 * @param {Object} map 54718 * The map object representing the init segment to get or set 54719 * @param {boolean=} set 54720 * If true, the init segment for the provided map should be saved 54721 * @return {Object} 54722 * map object for desired init segment 54723 */ 54724 54725 initSegmentForMap(map, set = false) { 54726 if (!map) { 54727 return null; 54728 } 54729 const id = initSegmentId(map); 54730 let storedMap = this.initSegments_[id]; 54731 if (set && !storedMap && map.bytes) { 54732 this.initSegments_[id] = storedMap = { 54733 resolvedUri: map.resolvedUri, 54734 byterange: map.byterange, 54735 bytes: map.bytes, 54736 tracks: map.tracks, 54737 timescales: map.timescales 54738 }; 54739 } 54740 return storedMap || map; 54741 } 54742 /** 54743 * Gets and sets key for the provided key 54744 * 54745 * @param {Object} key 54746 * The key object representing the key to get or set 54747 * @param {boolean=} set 54748 * If true, the key for the provided key should be saved 54749 * @return {Object} 54750 * Key object for desired key 54751 */ 54752 54753 segmentKey(key, set = false) { 54754 if (!key) { 54755 return null; 54756 } 54757 const id = segmentKeyId(key); 54758 let storedKey = this.keyCache_[id]; // TODO: We should use the HTTP Expires header to invalidate our cache per 54759 // https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-6.2.3 54760 54761 if (this.cacheEncryptionKeys_ && set && !storedKey && key.bytes) { 54762 this.keyCache_[id] = storedKey = { 54763 resolvedUri: key.resolvedUri, 54764 bytes: key.bytes 54765 }; 54766 } 54767 const result = { 54768 resolvedUri: (storedKey || key).resolvedUri 54769 }; 54770 if (storedKey) { 54771 result.bytes = storedKey.bytes; 54772 } 54773 return result; 54774 } 54775 /** 54776 * Returns true if all configuration required for loading is present, otherwise false. 54777 * 54778 * @return {boolean} True if the all configuration is ready for loading 54779 * @private 54780 */ 54781 54782 couldBeginLoading_() { 54783 return this.playlist_ && !this.paused(); 54784 } 54785 /** 54786 * load a playlist and start to fill the buffer 54787 */ 54788 54789 load() { 54790 // un-pause 54791 this.monitorBuffer_(); // if we don't have a playlist yet, keep waiting for one to be 54792 // specified 54793 54794 if (!this.playlist_) { 54795 return; 54796 } // if all the configuration is ready, initialize and begin loading 54797 54798 if (this.state === 'INIT' && this.couldBeginLoading_()) { 54799 return this.init_(); 54800 } // if we're in the middle of processing a segment already, don't 54801 // kick off an additional segment request 54802 54803 if (!this.couldBeginLoading_() || this.state !== 'READY' && this.state !== 'INIT') { 54804 return; 54805 } 54806 this.state = 'READY'; 54807 } 54808 /** 54809 * Once all the starting parameters have been specified, begin 54810 * operation. This method should only be invoked from the INIT 54811 * state. 54812 * 54813 * @private 54814 */ 54815 54816 init_() { 54817 this.state = 'READY'; // if this is the audio segment loader, and it hasn't been inited before, then any old 54818 // audio data from the muxed content should be removed 54819 54820 this.resetEverything(); 54821 return this.monitorBuffer_(); 54822 } 54823 /** 54824 * set a playlist on the segment loader 54825 * 54826 * @param {PlaylistLoader} media the playlist to set on the segment loader 54827 */ 54828 54829 playlist(newPlaylist, options = {}) { 54830 if (!newPlaylist) { 54831 return; 54832 } 54833
vendor: 29,654 bytes, lines 54833-55511
54833const oldPlaylist = this.playlist_; 54834 const segmentInfo = this.pendingSegment_; 54835 this.playlist_ = newPlaylist; 54836 this.xhrOptions_ = options; // when we haven't started playing yet, the start of a live playlist 54837 // is always our zero-time so force a sync update each time the playlist 54838 // is refreshed from the server 54839 // 54840 // Use the INIT state to determine if playback has started, as the playlist sync info 54841 // should be fixed once requests begin (as sync points are generated based on sync 54842 // info), but not before then. 54843 54844 if (this.state === 'INIT') { 54845 newPlaylist.syncInfo = { 54846 mediaSequence: newPlaylist.mediaSequence, 54847 time: 0 54848 }; // Setting the date time mapping means mapping the program date time (if available) 54849 // to time 0 on the player's timeline. The playlist's syncInfo serves a similar 54850 // purpose, mapping the initial mediaSequence to time zero. Since the syncInfo can 54851 // be updated as the playlist is refreshed before the loader starts loading, the 54852 // program date time mapping needs to be updated as well. 54853 // 54854 // This mapping is only done for the main loader because a program date time should 54855 // map equivalently between playlists. 54856 54857 if (this.loaderType_ === 'main') { 54858 this.syncController_.setDateTimeMappingForStart(newPlaylist); 54859 } 54860 } 54861 let oldId = null; 54862 if (oldPlaylist) { 54863 if (oldPlaylist.id) { 54864 oldId = oldPlaylist.id; 54865 } else if (oldPlaylist.uri) { 54866 oldId = oldPlaylist.uri; 54867 } 54868 } 54869 this.logger_(`playlist update [${oldId} => ${newPlaylist.id || newPlaylist.uri}]`); 54870 this.syncController_.updateMediaSequenceMap(newPlaylist, this.currentTime_(), this.loaderType_); // in VOD, this is always a rendition switch (or we updated our syncInfo above) 54871 // in LIVE, we always want to update with new playlists (including refreshes) 54872 54873 this.trigger('syncinfoupdate'); // if we were unpaused but waiting for a playlist, start 54874 // buffering now 54875 54876 if (this.state === 'INIT' && this.couldBeginLoading_()) { 54877 return this.init_(); 54878 } 54879 if (!oldPlaylist || oldPlaylist.uri !== newPlaylist.uri) { 54880 if (this.mediaIndex !== null) { 54881 // we must reset/resync the segment loader when we switch renditions and 54882 // the segment loader is already synced to the previous rendition 54883 // We only want to reset the loader here for LLHLS playback, as resetLoader sets fetchAtBuffer_ 54884 // to false, resulting in fetching segments at currentTime and causing repeated 54885 // same-segment requests on playlist change. This erroneously drives up the playback watcher 54886 // stalled segment count, as re-requesting segments at the currentTime or browser cached segments 54887 // will not change the buffer. 54888 // Reference for LLHLS fixes: https://github.com/videojs/http-streaming/pull/1201 54889 const isLLHLS = !newPlaylist.endList && typeof newPlaylist.partTargetDuration === 'number'; 54890 if (isLLHLS) { 54891 this.resetLoader(); 54892 } else { 54893 this.resyncLoader(); 54894 } 54895 } 54896 this.currentMediaInfo_ = void 0; 54897 this.trigger('playlistupdate'); // the rest of this function depends on `oldPlaylist` being defined 54898 54899 return; 54900 } // we reloaded the same playlist so we are in a live scenario 54901 // and we will likely need to adjust the mediaIndex 54902 54903 const mediaSequenceDiff = newPlaylist.mediaSequence - oldPlaylist.mediaSequence; 54904 this.logger_(`live window shift [${mediaSequenceDiff}]`); // update the mediaIndex on the SegmentLoader 54905 // this is important because we can abort a request and this value must be 54906 // equal to the last appended mediaIndex 54907 54908 if (this.mediaIndex !== null) { 54909 this.mediaIndex -= mediaSequenceDiff; // this can happen if we are going to load the first segment, but get a playlist 54910 // update during that. mediaIndex would go from 0 to -1 if mediaSequence in the 54911 // new playlist was incremented by 1. 54912 54913 if (this.mediaIndex < 0) { 54914 this.mediaIndex = null; 54915 this.partIndex = null; 54916 } else { 54917 const segment = this.playlist_.segments[this.mediaIndex]; // partIndex should remain the same for the same segment 54918 // unless parts fell off of the playlist for this segment. 54919 // In that case we need to reset partIndex and resync 54920 54921 if (this.partIndex && (!segment.parts || !segment.parts.length || !segment.parts[this.partIndex])) { 54922 const mediaIndex = this.mediaIndex; 54923 this.logger_(`currently processing part (index ${this.partIndex}) no longer exists.`); 54924 this.resetLoader(); // We want to throw away the partIndex and the data associated with it, 54925 // as the part was dropped from our current playlists segment. 54926 // The mediaIndex will still be valid so keep that around. 54927 54928 this.mediaIndex = mediaIndex; 54929 } 54930 } 54931 } // update the mediaIndex on the SegmentInfo object 54932 // this is important because we will update this.mediaIndex with this value 54933 // in `handleAppendsDone_` after the segment has been successfully appended 54934 54935 if (segmentInfo) { 54936 segmentInfo.mediaIndex -= mediaSequenceDiff; 54937 if (segmentInfo.mediaIndex < 0) { 54938 segmentInfo.mediaIndex = null; 54939 segmentInfo.partIndex = null; 54940 } else { 54941 // we need to update the referenced segment so that timing information is 54942 // saved for the new playlist's segment, however, if the segment fell off the 54943 // playlist, we can leave the old reference and just lose the timing info 54944 if (segmentInfo.mediaIndex >= 0) { 54945 segmentInfo.segment = newPlaylist.segments[segmentInfo.mediaIndex]; 54946 } 54947 if (segmentInfo.partIndex >= 0 && segmentInfo.segment.parts) { 54948 segmentInfo.part = segmentInfo.segment.parts[segmentInfo.partIndex]; 54949 } 54950 } 54951 } 54952 this.syncController_.saveExpiredSegmentInfo(oldPlaylist, newPlaylist); 54953 } 54954 /** 54955 * Prevent the loader from fetching additional segments. If there 54956 * is a segment request outstanding, it will finish processing 54957 * before the loader halts. A segment loader can be unpaused by 54958 * calling load(). 54959 */ 54960 54961 pause() { 54962 if (this.checkBufferTimeout_) { 54963 window.clearTimeout(this.checkBufferTimeout_); 54964 this.checkBufferTimeout_ = null; 54965 } 54966 } 54967 /** 54968 * Returns whether the segment loader is fetching additional 54969 * segments when given the opportunity. This property can be 54970 * modified through calls to pause() and load(). 54971 */ 54972 54973 paused() { 54974 return this.checkBufferTimeout_ === null; 54975 } 54976 /** 54977 * Delete all the buffered data and reset the SegmentLoader 54978 * 54979 * @param {Function} [done] an optional callback to be executed when the remove 54980 * operation is complete 54981 */ 54982 54983 resetEverything(done) { 54984 this.ended_ = false; 54985 this.activeInitSegmentId_ = null; 54986 this.appendInitSegment_ = { 54987 audio: true, 54988 video: true 54989 }; 54990 this.resetLoader(); // remove from 0, the earliest point, to Infinity, to signify removal of everything. 54991 // VTT Segment Loader doesn't need to do anything but in the regular SegmentLoader, 54992 // we then clamp the value to duration if necessary. 54993 54994 this.remove(0, Infinity, done); // clears fmp4 captions 54995 54996 if (this.transmuxer_) { 54997 this.transmuxer_.postMessage({ 54998 action: 'clearAllMp4Captions' 54999 }); // reset the cache in the transmuxer 55000 55001 this.transmuxer_.postMessage({ 55002 action: 'reset' 55003 }); 55004 } 55005 } 55006 /** 55007 * Force the SegmentLoader to resync and start loading around the currentTime instead 55008 * of starting at the end of the buffer 55009 * 55010 * Useful for fast quality changes 55011 */ 55012 55013 resetLoader() { 55014 this.fetchAtBuffer_ = false; 55015 this.resyncLoader(); 55016 } 55017 /** 55018 * Force the SegmentLoader to restart synchronization and make a conservative guess 55019 * before returning to the simple walk-forward method 55020 */ 55021 55022 resyncLoader() { 55023 if (this.transmuxer_) { 55024 // need to clear out any cached data to prepare for the new segment 55025 segmentTransmuxer.reset(this.transmuxer_); 55026 } 55027 this.mediaIndex = null; 55028 this.partIndex = null; 55029 this.syncPoint_ = null; 55030 this.isPendingTimestampOffset_ = false; 55031 this.shouldForceTimestampOffsetAfterResync_ = true; 55032 this.callQueue_ = []; 55033 this.loadQueue_ = []; 55034 this.metadataQueue_.id3 = []; 55035 this.metadataQueue_.caption = []; 55036 this.abort(); 55037 if (this.transmuxer_) { 55038 this.transmuxer_.postMessage({ 55039 action: 'clearParsedMp4Captions' 55040 }); 55041 } 55042 } 55043 /** 55044 * Remove any data in the source buffer between start and end times 55045 * 55046 * @param {number} start - the start time of the region to remove from the buffer 55047 * @param {number} end - the end time of the region to remove from the buffer 55048 * @param {Function} [done] - an optional callback to be executed when the remove 55049 * @param {boolean} force - force all remove operations to happen 55050 * operation is complete 55051 */ 55052 55053 remove(start, end, done = () => {}, force = false) { 55054 // clamp end to duration if we need to remove everything. 55055 // This is due to a browser bug that causes issues if we remove to Infinity. 55056 // videojs/videojs-contrib-hls#1225 55057 if (end === Infinity) { 55058 end = this.duration_(); 55059 } // skip removes that would throw an error 55060 // commonly happens during a rendition switch at the start of a video 55061 // from start 0 to end 0 55062 55063 if (end <= start) { 55064 this.logger_('skipping remove because end ${end} is <= start ${start}'); 55065 return; 55066 } 55067 if (!this.sourceUpdater_ || !this.getMediaInfo_()) { 55068 this.logger_('skipping remove because no source updater or starting media info'); // nothing to remove if we haven't processed any media 55069 55070 return; 55071 } // set it to one to complete this function's removes 55072 55073 let removesRemaining = 1; 55074 const removeFinished = () => { 55075 removesRemaining--; 55076 if (removesRemaining === 0) { 55077 done(); 55078 } 55079 }; 55080 if (force || !this.audioDisabled_) { 55081 removesRemaining++; 55082 this.sourceUpdater_.removeAudio(start, end, removeFinished); 55083 } // While it would be better to only remove video if the main loader has video, this 55084 // should be safe with audio only as removeVideo will call back even if there's no 55085 // video buffer. 55086 // 55087 // In theory we can check to see if there's video before calling the remove, but in 55088 // the event that we're switching between renditions and from video to audio only 55089 // (when we add support for that), we may need to clear the video contents despite 55090 // what the new media will contain. 55091 55092 if (force || this.loaderType_ === 'main') { 55093 this.gopBuffer_ = removeGopBuffer(this.gopBuffer_, start, end, this.timeMapping_); 55094 removesRemaining++; 55095 this.sourceUpdater_.removeVideo(start, end, removeFinished); 55096 } // remove any captions and ID3 tags 55097 55098 for (const track in this.inbandTextTracks_) { 55099 removeCuesFromTrack(start, end, this.inbandTextTracks_[track]); 55100 } 55101 removeCuesFromTrack(start, end, this.segmentMetadataTrack_); // finished this function's removes 55102 55103 removeFinished(); 55104 } 55105 /** 55106 * (re-)schedule monitorBufferTick_ to run as soon as possible 55107 * 55108 * @private 55109 */ 55110 55111 monitorBuffer_() { 55112 if (this.checkBufferTimeout_) { 55113 window.clearTimeout(this.checkBufferTimeout_); 55114 } 55115 this.checkBufferTimeout_ = window.setTimeout(this.monitorBufferTick_.bind(this), 1); 55116 } 55117 /** 55118 * As long as the SegmentLoader is in the READY state, periodically 55119 * invoke fillBuffer_(). 55120 * 55121 * @private 55122 */ 55123 55124 monitorBufferTick_() { 55125 if (this.state === 'READY') { 55126 this.fillBuffer_(); 55127 } 55128 if (this.checkBufferTimeout_) { 55129 window.clearTimeout(this.checkBufferTimeout_); 55130 } 55131 this.checkBufferTimeout_ = window.setTimeout(this.monitorBufferTick_.bind(this), CHECK_BUFFER_DELAY); 55132 } 55133 /** 55134 * fill the buffer with segements unless the sourceBuffers are 55135 * currently updating 55136 * 55137 * Note: this function should only ever be called by monitorBuffer_ 55138 * and never directly 55139 * 55140 * @private 55141 */ 55142 55143 fillBuffer_() { 55144 // TODO since the source buffer maintains a queue, and we shouldn't call this function 55145 // except when we're ready for the next segment, this check can most likely be removed 55146 if (this.sourceUpdater_.updating()) { 55147 return; 55148 } // see if we need to begin loading immediately 55149 55150 const segmentInfo = this.chooseNextRequest_(); 55151 if (!segmentInfo) { 55152 return; 55153 } 55154 if (typeof segmentInfo.timestampOffset === 'number') { 55155 this.isPendingTimestampOffset_ = false; 55156 this.timelineChangeController_.pendingTimelineChange({ 55157 type: this.loaderType_, 55158 from: this.currentTimeline_, 55159 to: segmentInfo.timeline 55160 }); 55161 } 55162 this.loadSegment_(segmentInfo); 55163 } 55164 /** 55165 * Determines if we should call endOfStream on the media source based 55166 * on the state of the buffer or if appened segment was the final 55167 * segment in the playlist. 55168 * 55169 * @param {number} [mediaIndex] the media index of segment we last appended 55170 * @param {Object} [playlist] a media playlist object 55171 * @return {boolean} do we need to call endOfStream on the MediaSource 55172 */ 55173 55174 isEndOfStream_(mediaIndex = this.mediaIndex, playlist = this.playlist_, partIndex = this.partIndex) { 55175 if (!playlist || !this.mediaSource_) { 55176 return false; 55177 } 55178 const segment = typeof mediaIndex === 'number' && playlist.segments[mediaIndex]; // mediaIndex is zero based but length is 1 based 55179 55180 const appendedLastSegment = mediaIndex + 1 === playlist.segments.length; // true if there are no parts, or this is the last part. 55181 55182 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 55183 // so that MediaSources can trigger the `ended` event when it runs out of 55184 // buffered data instead of waiting for me 55185 55186 return playlist.endList && this.mediaSource_.readyState === 'open' && appendedLastSegment && appendedLastPart; 55187 } 55188 /** 55189 * Determines what request should be made given current segment loader state. 55190 * 55191 * @return {Object} a request object that describes the segment/part to load 55192 */ 55193 55194 chooseNextRequest_() { 55195 const buffered = this.buffered_(); 55196 const bufferedEnd = lastBufferedEnd(buffered) || 0; 55197 const bufferedTime = timeAheadOf(buffered, this.currentTime_()); 55198 const preloaded = !this.hasPlayed_() && bufferedTime >= 1; 55199 const haveEnoughBuffer = bufferedTime >= this.goalBufferLength_(); 55200 const segments = this.playlist_.segments; // return no segment if: 55201 // 1. we don't have segments 55202 // 2. The video has not yet played and we already downloaded a segment 55203 // 3. we already have enough buffered time 55204 55205 if (!segments.length || preloaded || haveEnoughBuffer) { 55206 return null; 55207 } 55208 this.syncPoint_ = this.syncPoint_ || this.syncController_.getSyncPoint(this.playlist_, this.duration_(), this.currentTimeline_, this.currentTime_(), this.loaderType_); 55209 const next = { 55210 partIndex: null, 55211 mediaIndex: null, 55212 startOfSegment: null, 55213 playlist: this.playlist_, 55214 isSyncRequest: Boolean(!this.syncPoint_) 55215 }; 55216 if (next.isSyncRequest) { 55217 next.mediaIndex = getSyncSegmentCandidate(this.currentTimeline_, segments, bufferedEnd); 55218 this.logger_(`choose next request. Can not find sync point. Fallback to media Index: ${next.mediaIndex}`); 55219 } else if (this.mediaIndex !== null) { 55220 const segment = segments[this.mediaIndex]; 55221 const partIndex = typeof this.partIndex === 'number' ? this.partIndex : -1; 55222 next.startOfSegment = segment.end ? segment.end : bufferedEnd; 55223 if (segment.parts && segment.parts[partIndex + 1]) { 55224 next.mediaIndex = this.mediaIndex; 55225 next.partIndex = partIndex + 1; 55226 } else { 55227 next.mediaIndex = this.mediaIndex + 1; 55228 } 55229 } else { 55230 // Find the segment containing the end of the buffer or current time. 55231 const { 55232 segmentIndex, 55233 startTime, 55234 partIndex 55235 } = Playlist.getMediaInfoForTime({ 55236 exactManifestTimings: this.exactManifestTimings, 55237 playlist: this.playlist_, 55238 currentTime: this.fetchAtBuffer_ ? bufferedEnd : this.currentTime_(), 55239 startingPartIndex: this.syncPoint_.partIndex, 55240 startingSegmentIndex: this.syncPoint_.segmentIndex, 55241 startTime: this.syncPoint_.time 55242 }); 55243 next.getMediaInfoForTime = this.fetchAtBuffer_ ? `bufferedEnd ${bufferedEnd}` : `currentTime ${this.currentTime_()}`; 55244 next.mediaIndex = segmentIndex; 55245 next.startOfSegment = startTime; 55246 next.partIndex = partIndex; 55247 this.logger_(`choose next request. Playlist switched and we have a sync point. Media Index: ${next.mediaIndex} `); 55248 } 55249 const nextSegment = segments[next.mediaIndex]; 55250 let nextPart = nextSegment && typeof next.partIndex === 'number' && nextSegment.parts && nextSegment.parts[next.partIndex]; // if the next segment index is invalid or 55251 // the next partIndex is invalid do not choose a next segment. 55252 55253 if (!nextSegment || typeof next.partIndex === 'number' && !nextPart) { 55254 return null; 55255 } // if the next segment has parts, and we don't have a partIndex. 55256 // Set partIndex to 0 55257 55258 if (typeof next.partIndex !== 'number' && nextSegment.parts) { 55259 next.partIndex = 0; 55260 nextPart = nextSegment.parts[0]; 55261 } // independentSegments applies to every segment in a playlist. If independentSegments appears in a main playlist, 55262 // it applies to each segment in each media playlist. 55263 // https://datatracker.ietf.org/doc/html/draft-pantos-http-live-streaming-23#section-4.3.5.1 55264 55265 const hasIndependentSegments = this.vhs_.playlists && this.vhs_.playlists.main && this.vhs_.playlists.main.independentSegments || this.playlist_.independentSegments; // if we have no buffered data then we need to make sure 55266 // that the next part we append is "independent" if possible. 55267 // So we check if the previous part is independent, and request 55268 // it if it is. 55269 55270 if (!bufferedTime && nextPart && !hasIndependentSegments && !nextPart.independent) { 55271 if (next.partIndex === 0) { 55272 const lastSegment = segments[next.mediaIndex - 1]; 55273 const lastSegmentLastPart = lastSegment.parts && lastSegment.parts.length && lastSegment.parts[lastSegment.parts.length - 1]; 55274 if (lastSegmentLastPart && lastSegmentLastPart.independent) { 55275 next.mediaIndex -= 1; 55276 next.partIndex = lastSegment.parts.length - 1; 55277 next.independent = 'previous segment'; 55278 } 55279 } else if (nextSegment.parts[next.partIndex - 1].independent) { 55280 next.partIndex -= 1; 55281 next.independent = 'previous part'; 55282 } 55283 } 55284 const ended = this.mediaSource_ && this.mediaSource_.readyState === 'ended'; // do not choose a next segment if all of the following: 55285 // 1. this is the last segment in the playlist 55286 // 2. end of stream has been called on the media source already 55287 // 3. the player is not seeking 55288 55289 if (next.mediaIndex >= segments.length - 1 && ended && !this.seeking_()) { 55290 return null; 55291 } 55292 if (this.shouldForceTimestampOffsetAfterResync_) { 55293 this.shouldForceTimestampOffsetAfterResync_ = false; 55294 next.forceTimestampOffset = true; 55295 this.logger_('choose next request. Force timestamp offset after loader resync'); 55296 } 55297 return this.generateSegmentInfo_(next); 55298 } 55299 generateSegmentInfo_(options) { 55300 const { 55301 independent, 55302 playlist, 55303 mediaIndex, 55304 startOfSegment, 55305 isSyncRequest, 55306 partIndex, 55307 forceTimestampOffset, 55308 getMediaInfoForTime 55309 } = options; 55310 const segment = playlist.segments[mediaIndex]; 55311 const part = typeof partIndex === 'number' && segment.parts[partIndex]; 55312 const segmentInfo = { 55313 requestId: 'segment-loader-' + Math.random(), 55314 // resolve the segment URL relative to the playlist 55315 uri: part && part.resolvedUri || segment.resolvedUri, 55316 // the segment's mediaIndex at the time it was requested 55317 mediaIndex, 55318 partIndex: part ? partIndex : null, 55319 // whether or not to update the SegmentLoader's state with this 55320 // segment's mediaIndex 55321 isSyncRequest, 55322 startOfSegment, 55323 // the segment's playlist 55324 playlist, 55325 // unencrypted bytes of the segment 55326 bytes: null, 55327 // when a key is defined for this segment, the encrypted bytes 55328 encryptedBytes: null, 55329 // The target timestampOffset for this segment when we append it 55330 // to the source buffer 55331 timestampOffset: null, 55332 // The timeline that the segment is in 55333 timeline: segment.timeline, 55334 // The expected duration of the segment in seconds 55335 duration: part && part.duration || segment.duration, 55336 // retain the segment in case the playlist updates while doing an async process 55337 segment, 55338 part, 55339 byteLength: 0, 55340 transmuxer: this.transmuxer_, 55341 // type of getMediaInfoForTime that was used to get this segment 55342 getMediaInfoForTime, 55343 independent 55344 }; 55345 const overrideCheck = typeof forceTimestampOffset !== 'undefined' ? forceTimestampOffset : this.isPendingTimestampOffset_; 55346 segmentInfo.timestampOffset = this.timestampOffsetForSegment_({ 55347 segmentTimeline: segment.timeline, 55348 currentTimeline: this.currentTimeline_, 55349 startOfSegment, 55350 buffered: this.buffered_(), 55351 overrideCheck 55352 }); 55353 const audioBufferedEnd = lastBufferedEnd(this.sourceUpdater_.audioBuffered()); 55354 if (typeof audioBufferedEnd === 'number') { 55355 // since the transmuxer is using the actual timing values, but the buffer is 55356 // adjusted by the timestamp offset, we must adjust the value here 55357 segmentInfo.audioAppendStart = audioBufferedEnd - this.sourceUpdater_.audioTimestampOffset(); 55358 } 55359 if (this.sourceUpdater_.videoBuffered().length) { 55360 segmentInfo.gopsToAlignWith = gopsSafeToAlignWith(this.gopBuffer_, 55361 // since the transmuxer is using the actual timing values, but the time is 55362 // adjusted by the timestmap offset, we must adjust the value here 55363 this.currentTime_() - this.sourceUpdater_.videoTimestampOffset(), this.timeMapping_); 55364 } 55365 return segmentInfo; 55366 } // get the timestampoffset for a segment, 55367 // added so that vtt segment loader can override and prevent 55368 // adding timestamp offsets. 55369 55370 timestampOffsetForSegment_(options) { 55371 return timestampOffsetForSegment(options); 55372 } 55373 /** 55374 * Determines if the network has enough bandwidth to complete the current segment 55375 * request in a timely manner. If not, the request will be aborted early and bandwidth 55376 * updated to trigger a playlist switch. 55377 * 55378 * @param {Object} stats 55379 * Object containing stats about the request timing and size 55380 * @private 55381 */ 55382 55383 earlyAbortWhenNeeded_(stats) { 55384 if (this.vhs_.tech_.paused() || 55385 // Don't abort if the current playlist is on the lowestEnabledRendition 55386 // TODO: Replace using timeout with a boolean indicating whether this playlist is 55387 // the lowestEnabledRendition. 55388 !this.xhrOptions_.timeout || 55389 // Don't abort if we have no bandwidth information to estimate segment sizes 55390 !this.playlist_.attributes.BANDWIDTH) { 55391 return; 55392 } // Wait at least 1 second since the first byte of data has been received before 55393 // using the calculated bandwidth from the progress event to allow the bitrate 55394 // to stabilize 55395 55396 if (Date.now() - (stats.firstBytesReceivedAt || Date.now()) < 1000) { 55397 return; 55398 } 55399 const currentTime = this.currentTime_(); 55400 const measuredBandwidth = stats.bandwidth; 55401 const segmentDuration = this.pendingSegment_.duration; 55402 const requestTimeRemaining = Playlist.estimateSegmentRequestTime(segmentDuration, measuredBandwidth, this.playlist_, stats.bytesReceived); // Subtract 1 from the timeUntilRebuffer so we still consider an early abort 55403 // if we are only left with less than 1 second when the request completes. 55404 // A negative timeUntilRebuffering indicates we are already rebuffering 55405 55406 const timeUntilRebuffer$1 = timeUntilRebuffer(this.buffered_(), currentTime, this.vhs_.tech_.playbackRate()) - 1; // Only consider aborting early if the estimated time to finish the download 55407 // is larger than the estimated time until the player runs out of forward buffer 55408 55409 if (requestTimeRemaining <= timeUntilRebuffer$1) { 55410 return; 55411 } 55412 const switchCandidate = minRebufferMaxBandwidthSelector({ 55413 main: this.vhs_.playlists.main, 55414 currentTime, 55415 bandwidth: measuredBandwidth, 55416 duration: this.duration_(), 55417 segmentDuration, 55418 timeUntilRebuffer: timeUntilRebuffer$1, 55419 currentTimeline: this.currentTimeline_, 55420 syncController: this.syncController_ 55421 }); 55422 if (!switchCandidate) { 55423 return; 55424 } 55425 const rebufferingImpact = requestTimeRemaining - timeUntilRebuffer$1; 55426 const timeSavedBySwitching = rebufferingImpact - switchCandidate.rebufferingImpact; 55427 let minimumTimeSaving = 0.5; // If we are already rebuffering, increase the amount of variance we add to the 55428 // potential round trip time of the new request so that we are not too aggressive 55429 // with switching to a playlist that might save us a fraction of a second. 55430 55431 if (timeUntilRebuffer$1 <= TIME_FUDGE_FACTOR) { 55432 minimumTimeSaving = 1; 55433 } 55434 if (!switchCandidate.playlist || switchCandidate.playlist.uri === this.playlist_.uri || timeSavedBySwitching < minimumTimeSaving) { 55435 return; 55436 } // set the bandwidth to that of the desired playlist being sure to scale by 55437 // BANDWIDTH_VARIANCE and add one so the playlist selector does not exclude it 55438 // don't trigger a bandwidthupdate as the bandwidth is artifial 55439 55440 this.bandwidth = switchCandidate.playlist.attributes.BANDWIDTH * Config.BANDWIDTH_VARIANCE + 1; 55441 this.trigger('earlyabort'); 55442 } 55443 handleAbort_(segmentInfo) { 55444 this.logger_(`Aborting ${segmentInfoString(segmentInfo)}`); 55445 this.mediaRequestsAborted += 1; 55446 } 55447 /** 55448 * XHR `progress` event handler 55449 * 55450 * @param {Event} 55451 * The XHR `progress` event 55452 * @param {Object} simpleSegment 55453 * A simplified segment object copy 55454 * @private 55455 */ 55456 55457 handleProgress_(event, simpleSegment) { 55458 this.earlyAbortWhenNeeded_(simpleSegment.stats); 55459 if (this.checkForAbort_(simpleSegment.requestId)) { 55460 return; 55461 } 55462 this.trigger('progress'); 55463 } 55464 handleTrackInfo_(simpleSegment, trackInfo) { 55465 this.earlyAbortWhenNeeded_(simpleSegment.stats); 55466 if (this.checkForAbort_(simpleSegment.requestId)) { 55467 return; 55468 } 55469 if (this.checkForIllegalMediaSwitch(trackInfo)) { 55470 return; 55471 } 55472 trackInfo = trackInfo || {}; // When we have track info, determine what media types this loader is dealing with. 55473 // Guard against cases where we're not getting track info at all until we are 55474 // certain that all streams will provide it. 55475 55476 if (!shallowEqual(this.currentMediaInfo_, trackInfo)) { 55477 this.appendInitSegment_ = { 55478 audio: true, 55479 video: true 55480 }; 55481 this.startingMediaInfo_ = trackInfo; 55482 this.currentMediaInfo_ = trackInfo; 55483 this.logger_('trackinfo update', trackInfo); 55484 this.trigger('trackinfo'); 55485 } // trackinfo may cause an abort if the trackinfo 55486 // causes a codec change to an unsupported codec. 55487 55488 if (this.checkForAbort_(simpleSegment.requestId)) { 55489 return; 55490 } // set trackinfo on the pending segment so that 55491 // it can append. 55492 55493 this.pendingSegment_.trackInfo = trackInfo; // check if any calls were waiting on the track info 55494 55495 if (this.hasEnoughInfoToAppend_()) { 55496 this.processCallQueue_(); 55497 } 55498 } 55499 handleTimingInfo_(simpleSegment, mediaType, timeType, time) { 55500 this.earlyAbortWhenNeeded_(simpleSegment.stats); 55501 if (this.checkForAbort_(simpleSegment.requestId)) { 55502 return; 55503 } 55504 const segmentInfo = this.pendingSegment_; 55505 const timingInfoProperty = timingInfoPropertyForMedia(mediaType); 55506 segmentInfo[timingInfoProperty] = segmentInfo[timingInfoProperty] || {}; 55507 segmentInfo[timingInfoProperty][timeType] = time; 55508 this.logger_(`timinginfo: ${mediaType} - ${timeType} - ${time}`); // check if any calls were waiting on the timing info 55509 55510 if (this.hasEnoughInfoToAppend_()) { 55511 this.processCallQueue_(
vendor: 8,510 bytes, lines 55511-55703
55511); 55512 } 55513 } 55514 handleCaptions_(simpleSegment, captionData) { 55515 this.earlyAbortWhenNeeded_(simpleSegment.stats); 55516 if (this.checkForAbort_(simpleSegment.requestId)) { 55517 return; 55518 } // This could only happen with fmp4 segments, but 55519 // should still not happen in general 55520 55521 if (captionData.length === 0) { 55522 this.logger_('SegmentLoader received no captions from a caption event'); 55523 return; 55524 } 55525 const segmentInfo = this.pendingSegment_; // Wait until we have some video data so that caption timing 55526 // can be adjusted by the timestamp offset 55527 55528 if (!segmentInfo.hasAppendedData_) { 55529 this.metadataQueue_.caption.push(this.handleCaptions_.bind(this, simpleSegment, captionData)); 55530 return; 55531 } 55532 const timestampOffset = this.sourceUpdater_.videoTimestampOffset() === null ? this.sourceUpdater_.audioTimestampOffset() : this.sourceUpdater_.videoTimestampOffset(); 55533 const captionTracks = {}; // get total start/end and captions for each track/stream 55534 55535 captionData.forEach(caption => { 55536 // caption.stream is actually a track name... 55537 // set to the existing values in tracks or default values 55538 captionTracks[caption.stream] = captionTracks[caption.stream] || { 55539 // Infinity, as any other value will be less than this 55540 startTime: Infinity, 55541 captions: [], 55542 // 0 as an other value will be more than this 55543 endTime: 0 55544 }; 55545 const captionTrack = captionTracks[caption.stream]; 55546 captionTrack.startTime = Math.min(captionTrack.startTime, caption.startTime + timestampOffset); 55547 captionTrack.endTime = Math.max(captionTrack.endTime, caption.endTime + timestampOffset); 55548 captionTrack.captions.push(caption); 55549 }); 55550 Object.keys(captionTracks).forEach(trackName => { 55551 const { 55552 startTime, 55553 endTime, 55554 captions 55555 } = captionTracks[trackName]; 55556 const inbandTextTracks = this.inbandTextTracks_; 55557 this.logger_(`adding cues from ${startTime} -> ${endTime} for ${trackName}`); 55558 createCaptionsTrackIfNotExists(inbandTextTracks, this.vhs_.tech_, trackName); // clear out any cues that start and end at the same time period for the same track. 55559 // We do this because a rendition change that also changes the timescale for captions 55560 // will result in captions being re-parsed for certain segments. If we add them again 55561 // without clearing we will have two of the same captions visible. 55562 55563 removeCuesFromTrack(startTime, endTime, inbandTextTracks[trackName]); 55564 addCaptionData({ 55565 captionArray: captions, 55566 inbandTextTracks, 55567 timestampOffset 55568 }); 55569 }); // Reset stored captions since we added parsed 55570 // captions to a text track at this point 55571 55572 if (this.transmuxer_) { 55573 this.transmuxer_.postMessage({ 55574 action: 'clearParsedMp4Captions' 55575 }); 55576 } 55577 } 55578 handleId3_(simpleSegment, id3Frames, dispatchType) { 55579 this.earlyAbortWhenNeeded_(simpleSegment.stats); 55580 if (this.checkForAbort_(simpleSegment.requestId)) { 55581 return; 55582 } 55583 const segmentInfo = this.pendingSegment_; // we need to have appended data in order for the timestamp offset to be set 55584 55585 if (!segmentInfo.hasAppendedData_) { 55586 this.metadataQueue_.id3.push(this.handleId3_.bind(this, simpleSegment, id3Frames, dispatchType)); 55587 return; 55588 } 55589 this.addMetadataToTextTrack(dispatchType, id3Frames, this.duration_()); 55590 } 55591 processMetadataQueue_() { 55592 this.metadataQueue_.id3.forEach(fn => fn()); 55593 this.metadataQueue_.caption.forEach(fn => fn()); 55594 this.metadataQueue_.id3 = []; 55595 this.metadataQueue_.caption = []; 55596 } 55597 processCallQueue_() { 55598 const callQueue = this.callQueue_; // Clear out the queue before the queued functions are run, since some of the 55599 // functions may check the length of the load queue and default to pushing themselves 55600 // back onto the queue. 55601 55602 this.callQueue_ = []; 55603 callQueue.forEach(fun => fun()); 55604 } 55605 processLoadQueue_() { 55606 const loadQueue = this.loadQueue_; // Clear out the queue before the queued functions are run, since some of the 55607 // functions may check the length of the load queue and default to pushing themselves 55608 // back onto the queue. 55609 55610 this.loadQueue_ = []; 55611 loadQueue.forEach(fun => fun()); 55612 } 55613 /** 55614 * Determines whether the loader has enough info to load the next segment. 55615 * 55616 * @return {boolean} 55617 * Whether or not the loader has enough info to load the next segment 55618 */ 55619 55620 hasEnoughInfoToLoad_() { 55621 // Since primary timing goes by video, only the audio loader potentially needs to wait 55622 // to load. 55623 if (this.loaderType_ !== 'audio') { 55624 return true; 55625 } 55626 const segmentInfo = this.pendingSegment_; // A fill buffer must have already run to establish a pending segment before there's 55627 // enough info to load. 55628 55629 if (!segmentInfo) { 55630 return false; 55631 } // The first segment can and should be loaded immediately so that source buffers are 55632 // created together (before appending). Source buffer creation uses the presence of 55633 // audio and video data to determine whether to create audio/video source buffers, and 55634 // uses processed (transmuxed or parsed) media to determine the types required. 55635 55636 if (!this.getCurrentMediaInfo_()) { 55637 return true; 55638 } 55639 if ( 55640 // Technically, instead of waiting to load a segment on timeline changes, a segment 55641 // can be requested and downloaded and only wait before it is transmuxed or parsed. 55642 // But in practice, there are a few reasons why it is better to wait until a loader 55643 // is ready to append that segment before requesting and downloading: 55644 // 55645 // 1. Because audio and main loaders cross discontinuities together, if this loader 55646 // is waiting for the other to catch up, then instead of requesting another 55647 // segment and using up more bandwidth, by not yet loading, more bandwidth is 55648 // allotted to the loader currently behind. 55649 // 2. media-segment-request doesn't have to have logic to consider whether a segment 55650 // is ready to be processed or not, isolating the queueing behavior to the loader. 55651 // 3. The audio loader bases some of its segment properties on timing information 55652 // provided by the main loader, meaning that, if the logic for waiting on 55653 // processing was in media-segment-request, then it would also need to know how 55654 // to re-generate the segment information after the main loader caught up. 55655 shouldWaitForTimelineChange({ 55656 timelineChangeController: this.timelineChangeController_, 55657 currentTimeline: this.currentTimeline_, 55658 segmentTimeline: segmentInfo.timeline, 55659 loaderType: this.loaderType_, 55660 audioDisabled: this.audioDisabled_ 55661 })) { 55662 return false; 55663 } 55664 return true; 55665 } 55666 getCurrentMediaInfo_(segmentInfo = this.pendingSegment_) { 55667 return segmentInfo && segmentInfo.trackInfo || this.currentMediaInfo_; 55668 } 55669 getMediaInfo_(segmentInfo = this.pendingSegment_) { 55670 return this.getCurrentMediaInfo_(segmentInfo) || this.startingMediaInfo_; 55671 } 55672 getPendingSegmentPlaylist() { 55673 return this.pendingSegment_ ? this.pendingSegment_.playlist : null; 55674 } 55675 hasEnoughInfoToAppend_() { 55676 if (!this.sourceUpdater_.ready()) { 55677 return false; 55678 } // If content needs to be removed or the loader is waiting on an append reattempt, 55679 // then no additional content should be appended until the prior append is resolved. 55680 55681 if (this.waitingOnRemove_ || this.quotaExceededErrorRetryTimeout_) { 55682 return false; 55683 } 55684 const segmentInfo = this.pendingSegment_; 55685 const trackInfo = this.getCurrentMediaInfo_(); // no segment to append any data for or 55686 // we do not have information on this specific 55687 // segment yet 55688 55689 if (!segmentInfo || !trackInfo) { 55690 return false; 55691 } 55692 const { 55693 hasAudio, 55694 hasVideo, 55695 isMuxed 55696 } = trackInfo; 55697 if (hasVideo && !segmentInfo.videoTimingInfo) { 55698 return false; 55699 } // muxed content only relies on video timing information for now. 55700 55701 if (hasAudio && !this.audioDisabled_ && !isMuxed && !segmentInfo.audioTimingInfo) { 55702 return false; 55703 }
vendor: 24,829 bytes, lines 55703-56235
55703 55704 if (shouldWaitForTimelineChange({ 55705 timelineChangeController: this.timelineChangeController_, 55706 currentTimeline: this.currentTimeline_, 55707 segmentTimeline: segmentInfo.timeline, 55708 loaderType: this.loaderType_, 55709 audioDisabled: this.audioDisabled_ 55710 })) { 55711 return false; 55712 } 55713 return true; 55714 } 55715 handleData_(simpleSegment, result) { 55716 this.earlyAbortWhenNeeded_(simpleSegment.stats); 55717 if (this.checkForAbort_(simpleSegment.requestId)) { 55718 return; 55719 } // If there's anything in the call queue, then this data came later and should be 55720 // executed after the calls currently queued. 55721 55722 if (this.callQueue_.length || !this.hasEnoughInfoToAppend_()) { 55723 this.callQueue_.push(this.handleData_.bind(this, simpleSegment, result)); 55724 return; 55725 } 55726 const segmentInfo = this.pendingSegment_; // update the time mapping so we can translate from display time to media time 55727 55728 this.setTimeMapping_(segmentInfo.timeline); // for tracking overall stats 55729 55730 this.updateMediaSecondsLoaded_(segmentInfo.part || segmentInfo.segment); // Note that the state isn't changed from loading to appending. This is because abort 55731 // logic may change behavior depending on the state, and changing state too early may 55732 // inflate our estimates of bandwidth. In the future this should be re-examined to 55733 // note more granular states. 55734 // don't process and append data if the mediaSource is closed 55735 55736 if (this.mediaSource_.readyState === 'closed') { 55737 return; 55738 } // if this request included an initialization segment, save that data 55739 // to the initSegment cache 55740 55741 if (simpleSegment.map) { 55742 simpleSegment.map = this.initSegmentForMap(simpleSegment.map, true); // move over init segment properties to media request 55743 55744 segmentInfo.segment.map = simpleSegment.map; 55745 } // if this request included a segment key, save that data in the cache 55746 55747 if (simpleSegment.key) { 55748 this.segmentKey(simpleSegment.key, true); 55749 } 55750 segmentInfo.isFmp4 = simpleSegment.isFmp4; 55751 segmentInfo.timingInfo = segmentInfo.timingInfo || {}; 55752 if (segmentInfo.isFmp4) { 55753 this.trigger('fmp4'); 55754 segmentInfo.timingInfo.start = segmentInfo[timingInfoPropertyForMedia(result.type)].start; 55755 } else { 55756 const trackInfo = this.getCurrentMediaInfo_(); 55757 const useVideoTimingInfo = this.loaderType_ === 'main' && trackInfo && trackInfo.hasVideo; 55758 let firstVideoFrameTimeForData; 55759 if (useVideoTimingInfo) { 55760 firstVideoFrameTimeForData = segmentInfo.videoTimingInfo.start; 55761 } // Segment loader knows more about segment timing than the transmuxer (in certain 55762 // aspects), so make any changes required for a more accurate start time. 55763 // Don't set the end time yet, as the segment may not be finished processing. 55764 55765 segmentInfo.timingInfo.start = this.trueSegmentStart_({ 55766 currentStart: segmentInfo.timingInfo.start, 55767 playlist: segmentInfo.playlist, 55768 mediaIndex: segmentInfo.mediaIndex, 55769 currentVideoTimestampOffset: this.sourceUpdater_.videoTimestampOffset(), 55770 useVideoTimingInfo, 55771 firstVideoFrameTimeForData, 55772 videoTimingInfo: segmentInfo.videoTimingInfo, 55773 audioTimingInfo: segmentInfo.audioTimingInfo 55774 }); 55775 } // Init segments for audio and video only need to be appended in certain cases. Now 55776 // that data is about to be appended, we can check the final cases to determine 55777 // whether we should append an init segment. 55778 55779 this.updateAppendInitSegmentStatus(segmentInfo, result.type); // Timestamp offset should be updated once we get new data and have its timing info, 55780 // as we use the start of the segment to offset the best guess (playlist provided) 55781 // timestamp offset. 55782 55783 this.updateSourceBufferTimestampOffset_(segmentInfo); // if this is a sync request we need to determine whether it should 55784 // be appended or not. 55785 55786 if (segmentInfo.isSyncRequest) { 55787 // first save/update our timing info for this segment. 55788 // this is what allows us to choose an accurate segment 55789 // and the main reason we make a sync request. 55790 this.updateTimingInfoEnd_(segmentInfo); 55791 this.syncController_.saveSegmentTimingInfo({ 55792 segmentInfo, 55793 shouldSaveTimelineMapping: this.loaderType_ === 'main' 55794 }); 55795 const next = this.chooseNextRequest_(); // If the sync request isn't the segment that would be requested next 55796 // after taking into account its timing info, do not append it. 55797 55798 if (next.mediaIndex !== segmentInfo.mediaIndex || next.partIndex !== segmentInfo.partIndex) { 55799 this.logger_('sync segment was incorrect, not appending'); 55800 return; 55801 } // otherwise append it like any other segment as our guess was correct. 55802 55803 this.logger_('sync segment was correct, appending'); 55804 } // Save some state so that in the future anything waiting on first append (and/or 55805 // timestamp offset(s)) can process immediately. While the extra state isn't optimal, 55806 // we need some notion of whether the timestamp offset or other relevant information 55807 // has had a chance to be set. 55808 55809 segmentInfo.hasAppendedData_ = true; // Now that the timestamp offset should be set, we can append any waiting ID3 tags. 55810 55811 this.processMetadataQueue_(); 55812 this.appendData_(segmentInfo, result); 55813 } 55814 updateAppendInitSegmentStatus(segmentInfo, type) { 55815 // alt audio doesn't manage timestamp offset 55816 if (this.loaderType_ === 'main' && typeof segmentInfo.timestampOffset === 'number' && 55817 // in the case that we're handling partial data, we don't want to append an init 55818 // segment for each chunk 55819 !segmentInfo.changedTimestampOffset) { 55820 // if the timestamp offset changed, the timeline may have changed, so we have to re- 55821 // append init segments 55822 this.appendInitSegment_ = { 55823 audio: true, 55824 video: true 55825 }; 55826 } 55827 if (this.playlistOfLastInitSegment_[type] !== segmentInfo.playlist) { 55828 // make sure we append init segment on playlist changes, in case the media config 55829 // changed 55830 this.appendInitSegment_[type] = true; 55831 } 55832 } 55833 getInitSegmentAndUpdateState_({ 55834 type, 55835 initSegment, 55836 map, 55837 playlist 55838 }) { 55839 // "The EXT-X-MAP tag specifies how to obtain the Media Initialization Section 55840 // (Section 3) required to parse the applicable Media Segments. It applies to every 55841 // Media Segment that appears after it in the Playlist until the next EXT-X-MAP tag 55842 // or until the end of the playlist." 55843 // https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-4.3.2.5 55844 if (map) { 55845 const id = initSegmentId(map); 55846 if (this.activeInitSegmentId_ === id) { 55847 // don't need to re-append the init segment if the ID matches 55848 return null; 55849 } // a map-specified init segment takes priority over any transmuxed (or otherwise 55850 // obtained) init segment 55851 // 55852 // this also caches the init segment for later use 55853 55854 initSegment = this.initSegmentForMap(map, true).bytes; 55855 this.activeInitSegmentId_ = id; 55856 } // We used to always prepend init segments for video, however, that shouldn't be 55857 // necessary. Instead, we should only append on changes, similar to what we've always 55858 // done for audio. This is more important (though may not be that important) for 55859 // frame-by-frame appending for LHLS, simply because of the increased quantity of 55860 // appends. 55861 55862 if (initSegment && this.appendInitSegment_[type]) { 55863 // Make sure we track the playlist that we last used for the init segment, so that 55864 // we can re-append the init segment in the event that we get data from a new 55865 // playlist. Discontinuities and track changes are handled in other sections. 55866 this.playlistOfLastInitSegment_[type] = playlist; // Disable future init segment appends for this type. Until a change is necessary. 55867 55868 this.appendInitSegment_[type] = false; // we need to clear out the fmp4 active init segment id, since 55869 // we are appending the muxer init segment 55870 55871 this.activeInitSegmentId_ = null; 55872 return initSegment; 55873 } 55874 return null; 55875 } 55876 handleQuotaExceededError_({ 55877 segmentInfo, 55878 type, 55879 bytes 55880 }, error) { 55881 const audioBuffered = this.sourceUpdater_.audioBuffered(); 55882 const videoBuffered = this.sourceUpdater_.videoBuffered(); // For now we're ignoring any notion of gaps in the buffer, but they, in theory, 55883 // should be cleared out during the buffer removals. However, log in case it helps 55884 // debug. 55885 55886 if (audioBuffered.length > 1) { 55887 this.logger_('On QUOTA_EXCEEDED_ERR, found gaps in the audio buffer: ' + timeRangesToArray(audioBuffered).join(', ')); 55888 } 55889 if (videoBuffered.length > 1) { 55890 this.logger_('On QUOTA_EXCEEDED_ERR, found gaps in the video buffer: ' + timeRangesToArray(videoBuffered).join(', ')); 55891 } 55892 const audioBufferStart = audioBuffered.length ? audioBuffered.start(0) : 0; 55893 const audioBufferEnd = audioBuffered.length ? audioBuffered.end(audioBuffered.length - 1) : 0; 55894 const videoBufferStart = videoBuffered.length ? videoBuffered.start(0) : 0; 55895 const videoBufferEnd = videoBuffered.length ? videoBuffered.end(videoBuffered.length - 1) : 0; 55896 if (audioBufferEnd - audioBufferStart <= MIN_BACK_BUFFER && videoBufferEnd - videoBufferStart <= MIN_BACK_BUFFER) { 55897 // Can't remove enough buffer to make room for new segment (or the browser doesn't 55898 // allow for appends of segments this size). In the future, it may be possible to 55899 // split up the segment and append in pieces, but for now, error out this playlist 55900 // in an attempt to switch to a more manageable rendition. 55901 this.logger_('On QUOTA_EXCEEDED_ERR, single segment too large to append to ' + 'buffer, triggering an error. ' + `Appended byte length: ${bytes.byteLength}, ` + `audio buffer: ${timeRangesToArray(audioBuffered).join(', ')}, ` + `video buffer: ${timeRangesToArray(videoBuffered).join(', ')}, `); 55902 this.error({ 55903 message: 'Quota exceeded error with append of a single segment of content', 55904 excludeUntil: Infinity 55905 }); 55906 this.trigger('error'); 55907 return; 55908 } // To try to resolve the quota exceeded error, clear back buffer and retry. This means 55909 // that the segment-loader should block on future events until this one is handled, so 55910 // that it doesn't keep moving onto further segments. Adding the call to the call 55911 // queue will prevent further appends until waitingOnRemove_ and 55912 // quotaExceededErrorRetryTimeout_ are cleared. 55913 // 55914 // Note that this will only block the current loader. In the case of demuxed content, 55915 // the other load may keep filling as fast as possible. In practice, this should be 55916 // OK, as it is a rare case when either audio has a high enough bitrate to fill up a 55917 // source buffer, or video fills without enough room for audio to append (and without 55918 // the availability of clearing out seconds of back buffer to make room for audio). 55919 // But it might still be good to handle this case in the future as a TODO. 55920 55921 this.waitingOnRemove_ = true; 55922 this.callQueue_.push(this.appendToSourceBuffer_.bind(this, { 55923 segmentInfo, 55924 type, 55925 bytes 55926 })); 55927 const currentTime = this.currentTime_(); // Try to remove as much audio and video as possible to make room for new content 55928 // before retrying. 55929 55930 const timeToRemoveUntil = currentTime - MIN_BACK_BUFFER; 55931 this.logger_(`On QUOTA_EXCEEDED_ERR, removing audio/video from 0 to ${timeToRemoveUntil}`); 55932 this.remove(0, timeToRemoveUntil, () => { 55933 this.logger_(`On QUOTA_EXCEEDED_ERR, retrying append in ${MIN_BACK_BUFFER}s`); 55934 this.waitingOnRemove_ = false; // wait the length of time alotted in the back buffer to prevent wasted 55935 // attempts (since we can't clear less than the minimum) 55936 55937 this.quotaExceededErrorRetryTimeout_ = window.setTimeout(() => { 55938 this.logger_('On QUOTA_EXCEEDED_ERR, re-processing call queue'); 55939 this.quotaExceededErrorRetryTimeout_ = null; 55940 this.processCallQueue_(); 55941 }, MIN_BACK_BUFFER * 1000); 55942 }, true); 55943 } 55944 handleAppendError_({ 55945 segmentInfo, 55946 type, 55947 bytes 55948 }, error) { 55949 // if there's no error, nothing to do 55950 if (!error) { 55951 return; 55952 } 55953 if (error.code === QUOTA_EXCEEDED_ERR) { 55954 this.handleQuotaExceededError_({ 55955 segmentInfo, 55956 type, 55957 bytes 55958 }); // A quota exceeded error should be recoverable with a future re-append, so no need 55959 // to trigger an append error. 55960 55961 return; 55962 } 55963 this.logger_('Received non QUOTA_EXCEEDED_ERR on append', error); 55964 this.error(`${type} append of ${bytes.length}b failed for segment ` + `#${segmentInfo.mediaIndex} in playlist ${segmentInfo.playlist.id}`); // If an append errors, we often can't recover. 55965 // (see https://w3c.github.io/media-source/#sourcebuffer-append-error). 55966 // 55967 // Trigger a special error so that it can be handled separately from normal, 55968 // recoverable errors. 55969 55970 this.trigger('appenderror'); 55971 } 55972 appendToSourceBuffer_({ 55973 segmentInfo, 55974 type, 55975 initSegment, 55976 data, 55977 bytes 55978 }) { 55979 // If this is a re-append, bytes were already created and don't need to be recreated 55980 if (!bytes) { 55981 const segments = [data]; 55982 let byteLength = data.byteLength; 55983 if (initSegment) { 55984 // if the media initialization segment is changing, append it before the content 55985 // segment 55986 segments.unshift(initSegment); 55987 byteLength += initSegment.byteLength; 55988 } // Technically we should be OK appending the init segment separately, however, we 55989 // haven't yet tested that, and prepending is how we have always done things. 55990 55991 bytes = concatSegments({ 55992 bytes: byteLength, 55993 segments 55994 }); 55995 } 55996 this.sourceUpdater_.appendBuffer({ 55997 segmentInfo, 55998 type, 55999 bytes 56000 }, this.handleAppendError_.bind(this, { 56001 segmentInfo, 56002 type, 56003 bytes 56004 })); 56005 } 56006 handleSegmentTimingInfo_(type, requestId, segmentTimingInfo) { 56007 if (!this.pendingSegment_ || requestId !== this.pendingSegment_.requestId) { 56008 return; 56009 } 56010 const segment = this.pendingSegment_.segment; 56011 const timingInfoProperty = `${type}TimingInfo`; 56012 if (!segment[timingInfoProperty]) { 56013 segment[timingInfoProperty] = {}; 56014 } 56015 segment[timingInfoProperty].transmuxerPrependedSeconds = segmentTimingInfo.prependedContentDuration || 0; 56016 segment[timingInfoProperty].transmuxedPresentationStart = segmentTimingInfo.start.presentation; 56017 segment[timingInfoProperty].transmuxedDecodeStart = segmentTimingInfo.start.decode; 56018 segment[timingInfoProperty].transmuxedPresentationEnd = segmentTimingInfo.end.presentation; 56019 segment[timingInfoProperty].transmuxedDecodeEnd = segmentTimingInfo.end.decode; // mainly used as a reference for debugging 56020 56021 segment[timingInfoProperty].baseMediaDecodeTime = segmentTimingInfo.baseMediaDecodeTime; 56022 } 56023 appendData_(segmentInfo, result) { 56024 const { 56025 type, 56026 data 56027 } = result; 56028 if (!data || !data.byteLength) { 56029 return; 56030 } 56031 if (type === 'audio' && this.audioDisabled_) { 56032 return; 56033 } 56034 const initSegment = this.getInitSegmentAndUpdateState_({ 56035 type, 56036 initSegment: result.initSegment, 56037 playlist: segmentInfo.playlist, 56038 map: segmentInfo.isFmp4 ? segmentInfo.segment.map : null 56039 }); 56040 this.appendToSourceBuffer_({ 56041 segmentInfo, 56042 type, 56043 initSegment, 56044 data 56045 }); 56046 } 56047 /** 56048 * load a specific segment from a request into the buffer 56049 * 56050 * @private 56051 */ 56052 56053 loadSegment_(segmentInfo) { 56054 this.state = 'WAITING'; 56055 this.pendingSegment_ = segmentInfo; 56056 this.trimBackBuffer_(segmentInfo); 56057 if (typeof segmentInfo.timestampOffset === 'number') { 56058 if (this.transmuxer_) { 56059 this.transmuxer_.postMessage({ 56060 action: 'clearAllMp4Captions' 56061 }); 56062 } 56063 } 56064 if (!this.hasEnoughInfoToLoad_()) { 56065 this.loadQueue_.push(() => { 56066 // regenerate the audioAppendStart, timestampOffset, etc as they 56067 // may have changed since this function was added to the queue. 56068 const options = _extends$1({}, segmentInfo, { 56069 forceTimestampOffset: true 56070 }); 56071 _extends$1(segmentInfo, this.generateSegmentInfo_(options)); 56072 this.isPendingTimestampOffset_ = false; 56073 this.updateTransmuxerAndRequestSegment_(segmentInfo); 56074 }); 56075 return; 56076 } 56077 this.updateTransmuxerAndRequestSegment_(segmentInfo); 56078 } 56079 updateTransmuxerAndRequestSegment_(segmentInfo) { 56080 // We'll update the source buffer's timestamp offset once we have transmuxed data, but 56081 // the transmuxer still needs to be updated before then. 56082 // 56083 // Even though keepOriginalTimestamps is set to true for the transmuxer, timestamp 56084 // offset must be passed to the transmuxer for stream correcting adjustments. 56085 if (this.shouldUpdateTransmuxerTimestampOffset_(segmentInfo.timestampOffset)) { 56086 this.gopBuffer_.length = 0; // gopsToAlignWith was set before the GOP buffer was cleared 56087 56088 segmentInfo.gopsToAlignWith = []; 56089 this.timeMapping_ = 0; // reset values in the transmuxer since a discontinuity should start fresh 56090 56091 this.transmuxer_.postMessage({ 56092 action: 'reset' 56093 }); 56094 this.transmuxer_.postMessage({ 56095 action: 'setTimestampOffset', 56096 timestampOffset: segmentInfo.timestampOffset 56097 }); 56098 } 56099 const simpleSegment = this.createSimplifiedSegmentObj_(segmentInfo); 56100 const isEndOfStream = this.isEndOfStream_(segmentInfo.mediaIndex, segmentInfo.playlist, segmentInfo.partIndex); 56101 const isWalkingForward = this.mediaIndex !== null; 56102 const isDiscontinuity = segmentInfo.timeline !== this.currentTimeline_ && 56103 // currentTimeline starts at -1, so we shouldn't end the timeline switching to 0, 56104 // the first timeline 56105 segmentInfo.timeline > 0; 56106 const isEndOfTimeline = isEndOfStream || isWalkingForward && isDiscontinuity; 56107 this.logger_(`Requesting ${segmentInfoString(segmentInfo)}`); // If there's an init segment associated with this segment, but it is not cached (identified by a lack of bytes), 56108 // then this init segment has never been seen before and should be appended. 56109 // 56110 // At this point the content type (audio/video or both) is not yet known, but it should be safe to set 56111 // both to true and leave the decision of whether to append the init segment to append time. 56112 56113 if (simpleSegment.map && !simpleSegment.map.bytes) { 56114 this.logger_('going to request init segment.'); 56115 this.appendInitSegment_ = { 56116 video: true, 56117 audio: true 56118 }; 56119 } 56120 segmentInfo.abortRequests = mediaSegmentRequest({ 56121 xhr: this.vhs_.xhr, 56122 xhrOptions: this.xhrOptions_, 56123 decryptionWorker: this.decrypter_, 56124 segment: simpleSegment, 56125 abortFn: this.handleAbort_.bind(this, segmentInfo), 56126 progressFn: this.handleProgress_.bind(this), 56127 trackInfoFn: this.handleTrackInfo_.bind(this), 56128 timingInfoFn: this.handleTimingInfo_.bind(this), 56129 videoSegmentTimingInfoFn: this.handleSegmentTimingInfo_.bind(this, 'video', segmentInfo.requestId), 56130 audioSegmentTimingInfoFn: this.handleSegmentTimingInfo_.bind(this, 'audio', segmentInfo.requestId), 56131 captionsFn: this.handleCaptions_.bind(this), 56132 isEndOfTimeline, 56133 endedTimelineFn: () => { 56134 this.logger_('received endedtimeline callback'); 56135 }, 56136 id3Fn: this.handleId3_.bind(this), 56137 dataFn: this.handleData_.bind(this), 56138 doneFn: this.segmentRequestFinished_.bind(this), 56139 onTransmuxerLog: ({ 56140 message, 56141 level, 56142 stream 56143 }) => { 56144 this.logger_(`${segmentInfoString(segmentInfo)} logged from transmuxer stream ${stream} as a ${level}: ${message}`); 56145 } 56146 }); 56147 } 56148 /** 56149 * trim the back buffer so that we don't have too much data 56150 * in the source buffer 56151 * 56152 * @private 56153 * 56154 * @param {Object} segmentInfo - the current segment 56155 */ 56156 56157 trimBackBuffer_(segmentInfo) { 56158 const removeToTime = safeBackBufferTrimTime(this.seekable_(), this.currentTime_(), this.playlist_.targetDuration || 10); // Chrome has a hard limit of 150MB of 56159 // buffer and a very conservative "garbage collector" 56160 // We manually clear out the old buffer to ensure 56161 // we don't trigger the QuotaExceeded error 56162 // on the source buffer during subsequent appends 56163 56164 if (removeToTime > 0) { 56165 this.remove(0, removeToTime); 56166 } 56167 } 56168 /** 56169 * created a simplified copy of the segment object with just the 56170 * information necessary to perform the XHR and decryption 56171 * 56172 * @private 56173 * 56174 * @param {Object} segmentInfo - the current segment 56175 * @return {Object} a simplified segment object copy 56176 */ 56177 56178 createSimplifiedSegmentObj_(segmentInfo) { 56179 const segment = segmentInfo.segment; 56180 const part = segmentInfo.part; 56181 const simpleSegment = { 56182 resolvedUri: part ? part.resolvedUri : segment.resolvedUri, 56183 byterange: part ? part.byterange : segment.byterange, 56184 requestId: segmentInfo.requestId, 56185 transmuxer: segmentInfo.transmuxer, 56186 audioAppendStart: segmentInfo.audioAppendStart, 56187 gopsToAlignWith: segmentInfo.gopsToAlignWith, 56188 part: segmentInfo.part 56189 }; 56190 const previousSegment = segmentInfo.playlist.segments[segmentInfo.mediaIndex - 1]; 56191 if (previousSegment && previousSegment.timeline === segment.timeline) { 56192 // The baseStartTime of a segment is used to handle rollover when probing the TS 56193 // segment to retrieve timing information. Since the probe only looks at the media's 56194 // times (e.g., PTS and DTS values of the segment), and doesn't consider the 56195 // player's time (e.g., player.currentTime()), baseStartTime should reflect the 56196 // media time as well. transmuxedDecodeEnd represents the end time of a segment, in 56197 // seconds of media time, so should be used here. The previous segment is used since 56198 // the end of the previous segment should represent the beginning of the current 56199 // segment, so long as they are on the same timeline. 56200 if (previousSegment.videoTimingInfo) { 56201 simpleSegment.baseStartTime = previousSegment.videoTimingInfo.transmuxedDecodeEnd; 56202 } else if (previousSegment.audioTimingInfo) { 56203 simpleSegment.baseStartTime = previousSegment.audioTimingInfo.transmuxedDecodeEnd; 56204 } 56205 } 56206 if (segment.key) { 56207 // if the media sequence is greater than 2^32, the IV will be incorrect 56208 // assuming 10s segments, that would be about 1300 years 56209 const iv = segment.key.iv || new Uint32Array([0, 0, 0, segmentInfo.mediaIndex + segmentInfo.playlist.mediaSequence]); 56210 simpleSegment.key = this.segmentKey(segment.key); 56211 simpleSegment.key.iv = iv; 56212 } 56213 if (segment.map) { 56214 simpleSegment.map = this.initSegmentForMap(segment.map); 56215 } 56216 return simpleSegment; 56217 } 56218 saveTransferStats_(stats) { 56219 // every request counts as a media request even if it has been aborted 56220 // or canceled due to a timeout 56221 this.mediaRequests += 1; 56222 if (stats) { 56223 this.mediaBytesTransferred += stats.bytesReceived; 56224 this.mediaTransferDuration += stats.roundTripTime; 56225 } 56226 } 56227 saveBandwidthRelatedStats_(duration, stats) { 56228 // byteLength will be used for throughput, and should be based on bytes receieved, 56229 // which we only know at the end of the request and should reflect total bytes 56230 // downloaded rather than just bytes processed from components of the segment 56231 this.pendingSegment_.byteLength = stats.bytesReceived; 56232 if (duration < MIN_SEGMENT_DURATION_TO_SAVE_STATS) { 56233 this.logger_(`Ignoring segment's bandwidth because its duration of ${duration}` + ` is less than the min to record ${MIN_SEGMENT_DURATION_TO_SAVE_STATS}`); 56234 return; 56235 }
vendor: 12,384 bytes, lines 56235-56534
56235 56236 this.bandwidth = stats.bandwidth; 56237 this.roundTrip = stats.roundTripTime; 56238 } 56239 handleTimeout_() { 56240 // although the VTT segment loader bandwidth isn't really used, it's good to 56241 // maintain functinality between segment loaders 56242 this.mediaRequestsTimedout += 1; 56243 this.bandwidth = 1; 56244 this.roundTrip = NaN; 56245 this.trigger('bandwidthupdate'); 56246 this.trigger('timeout'); 56247 } 56248 /** 56249 * Handle the callback from the segmentRequest function and set the 56250 * associated SegmentLoader state and errors if necessary 56251 * 56252 * @private 56253 */ 56254 56255 segmentRequestFinished_(error, simpleSegment, result) { 56256 // TODO handle special cases, e.g., muxed audio/video but only audio in the segment 56257 // check the call queue directly since this function doesn't need to deal with any 56258 // data, and can continue even if the source buffers are not set up and we didn't get 56259 // any data from the segment 56260 if (this.callQueue_.length) { 56261 this.callQueue_.push(this.segmentRequestFinished_.bind(this, error, simpleSegment, result)); 56262 return; 56263 } 56264 this.saveTransferStats_(simpleSegment.stats); // The request was aborted and the SegmentLoader has already been reset 56265 56266 if (!this.pendingSegment_) { 56267 return; 56268 } // the request was aborted and the SegmentLoader has already started 56269 // another request. this can happen when the timeout for an aborted 56270 // request triggers due to a limitation in the XHR library 56271 // do not count this as any sort of request or we risk double-counting 56272 56273 if (simpleSegment.requestId !== this.pendingSegment_.requestId) { 56274 return; 56275 } // an error occurred from the active pendingSegment_ so reset everything 56276 56277 if (error) { 56278 this.pendingSegment_ = null; 56279 this.state = 'READY'; // aborts are not a true error condition and nothing corrective needs to be done 56280 56281 if (error.code === REQUEST_ERRORS.ABORTED) { 56282 return; 56283 } 56284 this.pause(); // the error is really just that at least one of the requests timed-out 56285 // set the bandwidth to a very low value and trigger an ABR switch to 56286 // take emergency action 56287 56288 if (error.code === REQUEST_ERRORS.TIMEOUT) { 56289 this.handleTimeout_(); 56290 return; 56291 } // if control-flow has arrived here, then the error is real 56292 // emit an error event to exclude the current playlist 56293 56294 this.mediaRequestsErrored += 1; 56295 this.error(error); 56296 this.trigger('error'); 56297 return; 56298 } 56299 const segmentInfo = this.pendingSegment_; // the response was a success so set any bandwidth stats the request 56300 // generated for ABR purposes 56301 56302 this.saveBandwidthRelatedStats_(segmentInfo.duration, simpleSegment.stats); 56303 segmentInfo.endOfAllRequests = simpleSegment.endOfAllRequests; 56304 if (result.gopInfo) { 56305 this.gopBuffer_ = updateGopBuffer(this.gopBuffer_, result.gopInfo, this.safeAppend_); 56306 } // Although we may have already started appending on progress, we shouldn't switch the 56307 // state away from loading until we are officially done loading the segment data. 56308 56309 this.state = 'APPENDING'; // used for testing 56310 56311 this.trigger('appending'); 56312 this.waitForAppendsToComplete_(segmentInfo); 56313 } 56314 setTimeMapping_(timeline) { 56315 const timelineMapping = this.syncController_.mappingForTimeline(timeline); 56316 if (timelineMapping !== null) { 56317 this.timeMapping_ = timelineMapping; 56318 } 56319 } 56320 updateMediaSecondsLoaded_(segment) { 56321 if (typeof segment.start === 'number' && typeof segment.end === 'number') { 56322 this.mediaSecondsLoaded += segment.end - segment.start; 56323 } else { 56324 this.mediaSecondsLoaded += segment.duration; 56325 } 56326 } 56327 shouldUpdateTransmuxerTimestampOffset_(timestampOffset) { 56328 if (timestampOffset === null) { 56329 return false; 56330 } // note that we're potentially using the same timestamp offset for both video and 56331 // audio 56332 56333 if (this.loaderType_ === 'main' && timestampOffset !== this.sourceUpdater_.videoTimestampOffset()) { 56334 return true; 56335 } 56336 if (!this.audioDisabled_ && timestampOffset !== this.sourceUpdater_.audioTimestampOffset()) { 56337 return true; 56338 } 56339 return false; 56340 } 56341 trueSegmentStart_({ 56342 currentStart, 56343 playlist, 56344 mediaIndex, 56345 firstVideoFrameTimeForData, 56346 currentVideoTimestampOffset, 56347 useVideoTimingInfo, 56348 videoTimingInfo, 56349 audioTimingInfo 56350 }) { 56351 if (typeof currentStart !== 'undefined') { 56352 // if start was set once, keep using it 56353 return currentStart; 56354 } 56355 if (!useVideoTimingInfo) { 56356 return audioTimingInfo.start; 56357 } 56358 const previousSegment = playlist.segments[mediaIndex - 1]; // The start of a segment should be the start of the first full frame contained 56359 // within that segment. Since the transmuxer maintains a cache of incomplete data 56360 // from and/or the last frame seen, the start time may reflect a frame that starts 56361 // in the previous segment. Check for that case and ensure the start time is 56362 // accurate for the segment. 56363 56364 if (mediaIndex === 0 || !previousSegment || typeof previousSegment.start === 'undefined' || previousSegment.end !== firstVideoFrameTimeForData + currentVideoTimestampOffset) { 56365 return firstVideoFrameTimeForData; 56366 } 56367 return videoTimingInfo.start; 56368 } 56369 waitForAppendsToComplete_(segmentInfo) { 56370 const trackInfo = this.getCurrentMediaInfo_(segmentInfo); 56371 if (!trackInfo) { 56372 this.error({ 56373 message: 'No starting media returned, likely due to an unsupported media format.', 56374 playlistExclusionDuration: Infinity 56375 }); 56376 this.trigger('error'); 56377 return; 56378 } // Although transmuxing is done, appends may not yet be finished. Throw a marker 56379 // on each queue this loader is responsible for to ensure that the appends are 56380 // complete. 56381 56382 const { 56383 hasAudio, 56384 hasVideo, 56385 isMuxed 56386 } = trackInfo; 56387 const waitForVideo = this.loaderType_ === 'main' && hasVideo; 56388 const waitForAudio = !this.audioDisabled_ && hasAudio && !isMuxed; 56389 segmentInfo.waitingOnAppends = 0; // segments with no data 56390 56391 if (!segmentInfo.hasAppendedData_) { 56392 if (!segmentInfo.timingInfo && typeof segmentInfo.timestampOffset === 'number') { 56393 // When there's no audio or video data in the segment, there's no audio or video 56394 // timing information. 56395 // 56396 // If there's no audio or video timing information, then the timestamp offset 56397 // can't be adjusted to the appropriate value for the transmuxer and source 56398 // buffers. 56399 // 56400 // Therefore, the next segment should be used to set the timestamp offset. 56401 this.isPendingTimestampOffset_ = true; 56402 } // override settings for metadata only segments 56403 56404 segmentInfo.timingInfo = { 56405 start: 0 56406 }; 56407 segmentInfo.waitingOnAppends++; 56408 if (!this.isPendingTimestampOffset_) { 56409 // update the timestampoffset 56410 this.updateSourceBufferTimestampOffset_(segmentInfo); // make sure the metadata queue is processed even though we have 56411 // no video/audio data. 56412 56413 this.processMetadataQueue_(); 56414 } // append is "done" instantly with no data. 56415 56416 this.checkAppendsDone_(segmentInfo); 56417 return; 56418 } // Since source updater could call back synchronously, do the increments first. 56419 56420 if (waitForVideo) { 56421 segmentInfo.waitingOnAppends++; 56422 } 56423 if (waitForAudio) { 56424 segmentInfo.waitingOnAppends++; 56425 } 56426 if (waitForVideo) { 56427 this.sourceUpdater_.videoQueueCallback(this.checkAppendsDone_.bind(this, segmentInfo)); 56428 } 56429 if (waitForAudio) { 56430 this.sourceUpdater_.audioQueueCallback(this.checkAppendsDone_.bind(this, segmentInfo)); 56431 } 56432 } 56433 checkAppendsDone_(segmentInfo) { 56434 if (this.checkForAbort_(segmentInfo.requestId)) { 56435 return; 56436 } 56437 segmentInfo.waitingOnAppends--; 56438 if (segmentInfo.waitingOnAppends === 0) { 56439 this.handleAppendsDone_(); 56440 } 56441 } 56442 checkForIllegalMediaSwitch(trackInfo) { 56443 const illegalMediaSwitchError = illegalMediaSwitch(this.loaderType_, this.getCurrentMediaInfo_(), trackInfo); 56444 if (illegalMediaSwitchError) { 56445 this.error({ 56446 message: illegalMediaSwitchError, 56447 playlistExclusionDuration: Infinity 56448 }); 56449 this.trigger('error'); 56450 return true; 56451 } 56452 return false; 56453 } 56454 updateSourceBufferTimestampOffset_(segmentInfo) { 56455 if (segmentInfo.timestampOffset === null || 56456 // we don't yet have the start for whatever media type (video or audio) has 56457 // priority, timing-wise, so we must wait 56458 typeof segmentInfo.timingInfo.start !== 'number' || 56459 // already updated the timestamp offset for this segment 56460 segmentInfo.changedTimestampOffset || 56461 // the alt audio loader should not be responsible for setting the timestamp offset 56462 this.loaderType_ !== 'main') { 56463 return; 56464 } 56465 let didChange = false; // Primary timing goes by video, and audio is trimmed in the transmuxer, meaning that 56466 // the timing info here comes from video. In the event that the audio is longer than 56467 // the video, this will trim the start of the audio. 56468 // This also trims any offset from 0 at the beginning of the media 56469 56470 segmentInfo.timestampOffset -= this.getSegmentStartTimeForTimestampOffsetCalculation_({ 56471 videoTimingInfo: segmentInfo.segment.videoTimingInfo, 56472 audioTimingInfo: segmentInfo.segment.audioTimingInfo, 56473 timingInfo: segmentInfo.timingInfo 56474 }); // In the event that there are part segment downloads, each will try to update the 56475 // timestamp offset. Retaining this bit of state prevents us from updating in the 56476 // future (within the same segment), however, there may be a better way to handle it. 56477 56478 segmentInfo.changedTimestampOffset = true; 56479 if (segmentInfo.timestampOffset !== this.sourceUpdater_.videoTimestampOffset()) { 56480 this.sourceUpdater_.videoTimestampOffset(segmentInfo.timestampOffset); 56481 didChange = true; 56482 } 56483 if (segmentInfo.timestampOffset !== this.sourceUpdater_.audioTimestampOffset()) { 56484 this.sourceUpdater_.audioTimestampOffset(segmentInfo.timestampOffset); 56485 didChange = true; 56486 } 56487 if (didChange) { 56488 this.trigger('timestampoffset'); 56489 } 56490 } 56491 getSegmentStartTimeForTimestampOffsetCalculation_({ 56492 videoTimingInfo, 56493 audioTimingInfo, 56494 timingInfo 56495 }) { 56496 if (!this.useDtsForTimestampOffset_) { 56497 return timingInfo.start; 56498 } 56499 if (videoTimingInfo && typeof videoTimingInfo.transmuxedDecodeStart === 'number') { 56500 return videoTimingInfo.transmuxedDecodeStart; 56501 } // handle audio only 56502 56503 if (audioTimingInfo && typeof audioTimingInfo.transmuxedDecodeStart === 'number') { 56504 return audioTimingInfo.transmuxedDecodeStart; 56505 } // handle content not transmuxed (e.g., MP4) 56506 56507 return timingInfo.start; 56508 } 56509 updateTimingInfoEnd_(segmentInfo) { 56510 segmentInfo.timingInfo = segmentInfo.timingInfo || {}; 56511 const trackInfo = this.getMediaInfo_(); 56512 const useVideoTimingInfo = this.loaderType_ === 'main' && trackInfo && trackInfo.hasVideo; 56513 const prioritizedTimingInfo = useVideoTimingInfo && segmentInfo.videoTimingInfo ? segmentInfo.videoTimingInfo : segmentInfo.audioTimingInfo; 56514 if (!prioritizedTimingInfo) { 56515 return; 56516 } 56517 segmentInfo.timingInfo.end = typeof prioritizedTimingInfo.end === 'number' ? 56518 // End time may not exist in a case where we aren't parsing the full segment (one 56519 // current example is the case of fmp4), so use the rough duration to calculate an 56520 // end time. 56521 prioritizedTimingInfo.end : prioritizedTimingInfo.start + segmentInfo.duration; 56522 } 56523 /** 56524 * callback to run when appendBuffer is finished. detects if we are 56525 * in a good state to do things with the data we got, or if we need 56526 * to wait for more 56527 * 56528 * @private 56529 */ 56530 56531 handleAppendsDone_() { 56532 // appendsdone can cause an abort 56533 if (this.pendingSegment_) { 56534
56534this.trigger('appendsdone'); 56535 } 56536 if (!this.pendingSegment_) { 56537 this.state = 'READY'; // TODO should this move into this.checkForAbort to speed up requests post abort in 56538 // all appending cases? 56539 56540 if (!this.paused()) { 56541 this.monitorBuffer_(); 56542 } 56543 return; 56544 } 56545 const segmentInfo = this.pendingSegment_;
vendor: 10,130 bytes, lines 56545-56772
56545 // Now that the end of the segment has been reached, we can set the end time. It's 56546 // best to wait until all appends are done so we're sure that the primary media is 56547 // finished (and we have its end time). 56548 56549 this.updateTimingInfoEnd_(segmentInfo); 56550 if (this.shouldSaveSegmentTimingInfo_) { 56551 // Timeline mappings should only be saved for the main loader. This is for multiple 56552 // reasons: 56553 // 56554 // 1) Only one mapping is saved per timeline, meaning that if both the audio loader 56555 // and the main loader try to save the timeline mapping, whichever comes later 56556 // will overwrite the first. In theory this is OK, as the mappings should be the 56557 // same, however, it breaks for (2) 56558 // 2) In the event of a live stream, the initial live point will make for a somewhat 56559 // arbitrary mapping. If audio and video streams are not perfectly in-sync, then 56560 // the mapping will be off for one of the streams, dependent on which one was 56561 // first saved (see (1)). 56562 // 3) Primary timing goes by video in VHS, so the mapping should be video. 56563 // 56564 // Since the audio loader will wait for the main loader to load the first segment, 56565 // the main loader will save the first timeline mapping, and ensure that there won't 56566 // be a case where audio loads two segments without saving a mapping (thus leading 56567 // to missing segment timing info). 56568 this.syncController_.saveSegmentTimingInfo({ 56569 segmentInfo, 56570 shouldSaveTimelineMapping: this.loaderType_ === 'main' 56571 }); 56572 } 56573 const segmentDurationMessage = getTroublesomeSegmentDurationMessage(segmentInfo, this.sourceType_); 56574 if (segmentDurationMessage) { 56575 if (segmentDurationMessage.severity === 'warn') { 56576 videojs.log.warn(segmentDurationMessage.message); 56577 } else { 56578 this.logger_(segmentDurationMessage.message); 56579 } 56580 } 56581 this.recordThroughput_(segmentInfo); 56582 this.pendingSegment_ = null; 56583 this.state = 'READY'; 56584 if (segmentInfo.isSyncRequest) { 56585 this.trigger('syncinfoupdate'); // if the sync request was not appended 56586 // then it was not the correct segment. 56587 // throw it away and use the data it gave us 56588 // to get the correct one. 56589 56590 if (!segmentInfo.hasAppendedData_) { 56591 this.logger_(`Throwing away un-appended sync request ${segmentInfoString(segmentInfo)}`); 56592 return; 56593 } 56594 } 56595 this.logger_(`Appended ${segmentInfoString(segmentInfo)}`); 56596 this.addSegmentMetadataCue_(segmentInfo); 56597 this.fetchAtBuffer_ = true; 56598 if (this.currentTimeline_ !== segmentInfo.timeline) { 56599 this.timelineChangeController_.lastTimelineChange({ 56600 type: this.loaderType_, 56601 from: this.currentTimeline_, 56602 to: segmentInfo.timeline 56603 }); // If audio is not disabled, the main segment loader is responsible for updating 56604 // the audio timeline as well. If the content is video only, this won't have any 56605 // impact. 56606 56607 if (this.loaderType_ === 'main' && !this.audioDisabled_) { 56608 this.timelineChangeController_.lastTimelineChange({ 56609 type: 'audio', 56610 from: this.currentTimeline_, 56611 to: segmentInfo.timeline 56612 }); 56613 } 56614 } 56615 this.currentTimeline_ = segmentInfo.timeline; // We must update the syncinfo to recalculate the seekable range before 56616 // the following conditional otherwise it may consider this a bad "guess" 56617 // and attempt to resync when the post-update seekable window and live 56618 // point would mean that this was the perfect segment to fetch 56619 56620 this.trigger('syncinfoupdate'); 56621 const segment = segmentInfo.segment; 56622 const part = segmentInfo.part; 56623 const badSegmentGuess = segment.end && this.currentTime_() - segment.end > segmentInfo.playlist.targetDuration * 3; 56624 const badPartGuess = part && part.end && this.currentTime_() - part.end > segmentInfo.playlist.partTargetDuration * 3; // If we previously appended a segment/part that ends more than 3 part/targetDurations before 56625 // the currentTime_ that means that our conservative guess was too conservative. 56626 // In that case, reset the loader state so that we try to use any information gained 56627 // from the previous request to create a new, more accurate, sync-point. 56628 56629 if (badSegmentGuess || badPartGuess) { 56630 this.logger_(`bad ${badSegmentGuess ? 'segment' : 'part'} ${segmentInfoString(segmentInfo)}`); 56631 this.resetEverything(); 56632 return; 56633 } 56634 const isWalkingForward = this.mediaIndex !== null; // Don't do a rendition switch unless we have enough time to get a sync segment 56635 // and conservatively guess 56636 56637 if (isWalkingForward) { 56638 this.trigger('bandwidthupdate'); 56639 } 56640 this.trigger('progress'); 56641 this.mediaIndex = segmentInfo.mediaIndex; 56642 this.partIndex = segmentInfo.partIndex; // any time an update finishes and the last segment is in the 56643 // buffer, end the stream. this ensures the "ended" event will 56644 // fire if playback reaches that point. 56645 56646 if (this.isEndOfStream_(segmentInfo.mediaIndex, segmentInfo.playlist, segmentInfo.partIndex)) { 56647 this.endOfStream(); 56648 } // used for testing 56649 56650 this.trigger('appended'); 56651 if (segmentInfo.hasAppendedData_) { 56652 this.mediaAppends++; 56653 } 56654 if (!this.paused()) { 56655 this.monitorBuffer_(); 56656 } 56657 } 56658 /** 56659 * Records the current throughput of the decrypt, transmux, and append 56660 * portion of the semgment pipeline. `throughput.rate` is a the cumulative 56661 * moving average of the throughput. `throughput.count` is the number of 56662 * data points in the average. 56663 * 56664 * @private 56665 * @param {Object} segmentInfo the object returned by loadSegment 56666 */ 56667 56668 recordThroughput_(segmentInfo) { 56669 if (segmentInfo.duration < MIN_SEGMENT_DURATION_TO_SAVE_STATS) { 56670 this.logger_(`Ignoring segment's throughput because its duration of ${segmentInfo.duration}` + ` is less than the min to record ${MIN_SEGMENT_DURATION_TO_SAVE_STATS}`); 56671 return; 56672 } 56673 const rate = this.throughput.rate; // Add one to the time to ensure that we don't accidentally attempt to divide 56674 // by zero in the case where the throughput is ridiculously high 56675 56676 const segmentProcessingTime = Date.now() - segmentInfo.endOfAllRequests + 1; // Multiply by 8000 to convert from bytes/millisecond to bits/second 56677 56678 const segmentProcessingThroughput = Math.floor(segmentInfo.byteLength / segmentProcessingTime * 8 * 1000); // This is just a cumulative moving average calculation: 56679 // newAvg = oldAvg + (sample - oldAvg) / (sampleCount + 1) 56680 56681 this.throughput.rate += (segmentProcessingThroughput - rate) / ++this.throughput.count; 56682 } 56683 /** 56684 * Adds a cue to the segment-metadata track with some metadata information about the 56685 * segment 56686 * 56687 * @private 56688 * @param {Object} segmentInfo 56689 * the object returned by loadSegment 56690 * @method addSegmentMetadataCue_ 56691 */ 56692 56693 addSegmentMetadataCue_(segmentInfo) { 56694 if (!this.segmentMetadataTrack_) { 56695 return; 56696 } 56697 const segment = segmentInfo.segment; 56698 const start = segment.start; 56699 const end = segment.end; // Do not try adding the cue if the start and end times are invalid. 56700 56701 if (!finite(start) || !finite(end)) { 56702 return; 56703 } 56704 removeCuesFromTrack(start, end, this.segmentMetadataTrack_); 56705 const Cue = window.WebKitDataCue || window.VTTCue; 56706 const value = { 56707 custom: segment.custom, 56708 dateTimeObject: segment.dateTimeObject, 56709 dateTimeString: segment.dateTimeString, 56710 programDateTime: segment.programDateTime, 56711 bandwidth: segmentInfo.playlist.attributes.BANDWIDTH, 56712 resolution: segmentInfo.playlist.attributes.RESOLUTION, 56713 codecs: segmentInfo.playlist.attributes.CODECS, 56714 byteLength: segmentInfo.byteLength, 56715 uri: segmentInfo.uri, 56716 timeline: segmentInfo.timeline, 56717 playlist: segmentInfo.playlist.id, 56718 start, 56719 end 56720 }; 56721 const data = JSON.stringify(value); 56722 const cue = new Cue(start, end, data); // Attach the metadata to the value property of the cue to keep consistency between 56723 // the differences of WebKitDataCue in safari and VTTCue in other browsers 56724 56725 cue.value = value; 56726 this.segmentMetadataTrack_.addCue(cue); 56727 } 56728 } 56729 function noop() {} 56730 const toTitleCase = function (string) { 56731 if (typeof string !== 'string') { 56732 return string; 56733 } 56734 return string.replace(/./, w => w.toUpperCase()); 56735 }; 56736 56737 /** 56738 * @file source-updater.js 56739 */ 56740 const bufferTypes = ['video', 'audio']; 56741 const updating = (type, sourceUpdater) => { 56742 const sourceBuffer = sourceUpdater[`${type}Buffer`]; 56743 return sourceBuffer && sourceBuffer.updating || sourceUpdater.queuePending[type]; 56744 }; 56745 const nextQueueIndexOfType = (type, queue) => { 56746 for (let i = 0; i < queue.length; i++) { 56747 const queueEntry = queue[i]; 56748 if (queueEntry.type === 'mediaSource') { 56749 // If the next entry is a media source entry (uses multiple source buffers), block 56750 // processing to allow it to go through first. 56751 return null; 56752 } 56753 if (queueEntry.type === type) { 56754 return i; 56755 } 56756 } 56757 return null; 56758 }; 56759 const shiftQueue = (type, sourceUpdater) => { 56760 if (sourceUpdater.queue.length === 0) { 56761 return; 56762 } 56763 let queueIndex = 0; 56764 let queueEntry = sourceUpdater.queue[queueIndex]; 56765 if (queueEntry.type === 'mediaSource') { 56766 if (!sourceUpdater.updating() && sourceUpdater.mediaSource.readyState !== 'closed') { 56767 sourceUpdater.queue.shift(); 56768 queueEntry.action(sourceUpdater); 56769 if (queueEntry.doneFn) { 56770 queueEntry.doneFn(); 56771 } // Only specific source buffer actions must wait for async updateend events. Media 56772 // Source actions process synchronously. Therefore, both audio and video source
vendor: 8,055 bytes, lines 56773-56947
56773 // buffers are now clear to process the next queue entries. 56774 56775 shiftQueue('audio', sourceUpdater); 56776 shiftQueue('video', sourceUpdater); 56777 } // Media Source actions require both source buffers, so if the media source action 56778 // couldn't process yet (because one or both source buffers are busy), block other 56779 // queue actions until both are available and the media source action can process. 56780 56781 return; 56782 } 56783 if (type === 'mediaSource') { 56784 // If the queue was shifted by a media source action (this happens when pushing a 56785 // media source action onto the queue), then it wasn't from an updateend event from an 56786 // audio or video source buffer, so there's no change from previous state, and no 56787 // processing should be done. 56788 return; 56789 } // Media source queue entries don't need to consider whether the source updater is 56790 // started (i.e., source buffers are created) as they don't need the source buffers, but 56791 // source buffer queue entries do. 56792 56793 if (!sourceUpdater.ready() || sourceUpdater.mediaSource.readyState === 'closed' || updating(type, sourceUpdater)) { 56794 return; 56795 } 56796 if (queueEntry.type !== type) { 56797 queueIndex = nextQueueIndexOfType(type, sourceUpdater.queue); 56798 if (queueIndex === null) { 56799 // Either there's no queue entry that uses this source buffer type in the queue, or 56800 // there's a media source queue entry before the next entry of this type, in which 56801 // case wait for that action to process first. 56802 return; 56803 } 56804 queueEntry = sourceUpdater.queue[queueIndex]; 56805 } 56806 sourceUpdater.queue.splice(queueIndex, 1); // Keep a record that this source buffer type is in use. 56807 // 56808 // The queue pending operation must be set before the action is performed in the event 56809 // that the action results in a synchronous event that is acted upon. For instance, if 56810 // an exception is thrown that can be handled, it's possible that new actions will be 56811 // appended to an empty queue and immediately executed, but would not have the correct 56812 // pending information if this property was set after the action was performed. 56813 56814 sourceUpdater.queuePending[type] = queueEntry; 56815 queueEntry.action(type, sourceUpdater); 56816 if (!queueEntry.doneFn) { 56817 // synchronous operation, process next entry 56818 sourceUpdater.queuePending[type] = null; 56819 shiftQueue(type, sourceUpdater); 56820 return; 56821 } 56822 }; 56823 const cleanupBuffer = (type, sourceUpdater) => { 56824 const buffer = sourceUpdater[`${type}Buffer`]; 56825 const titleType = toTitleCase(type); 56826 if (!buffer) { 56827 return; 56828 } 56829 buffer.removeEventListener('updateend', sourceUpdater[`on${titleType}UpdateEnd_`]); 56830 buffer.removeEventListener('error', sourceUpdater[`on${titleType}Error_`]); 56831 sourceUpdater.codecs[type] = null; 56832 sourceUpdater[`${type}Buffer`] = null; 56833 }; 56834 const inSourceBuffers = (mediaSource, sourceBuffer) => mediaSource && sourceBuffer && Array.prototype.indexOf.call(mediaSource.sourceBuffers, sourceBuffer) !== -1; 56835 const actions = { 56836 appendBuffer: (bytes, segmentInfo, onError) => (type, sourceUpdater) => { 56837 const sourceBuffer = sourceUpdater[`${type}Buffer`]; // can't do anything if the media source / source buffer is null 56838 // or the media source does not contain this source buffer. 56839 56840 if (!inSourceBuffers(sourceUpdater.mediaSource, sourceBuffer)) { 56841 return; 56842 } 56843 sourceUpdater.logger_(`Appending segment ${segmentInfo.mediaIndex}'s ${bytes.length} bytes to ${type}Buffer`); 56844 try { 56845 sourceBuffer.appendBuffer(bytes); 56846 } catch (e) { 56847 sourceUpdater.logger_(`Error with code ${e.code} ` + (e.code === QUOTA_EXCEEDED_ERR ? '(QUOTA_EXCEEDED_ERR) ' : '') + `when appending segment ${segmentInfo.mediaIndex} to ${type}Buffer`); 56848 sourceUpdater.queuePending[type] = null; 56849 onError(e); 56850 } 56851 }, 56852 remove: (start, end) => (type, sourceUpdater) => { 56853 const sourceBuffer = sourceUpdater[`${type}Buffer`]; // can't do anything if the media source / source buffer is null 56854 // or the media source does not contain this source buffer. 56855 56856 if (!inSourceBuffers(sourceUpdater.mediaSource, sourceBuffer)) { 56857 return; 56858 } 56859 sourceUpdater.logger_(`Removing ${start} to ${end} from ${type}Buffer`); 56860 try { 56861 sourceBuffer.remove(start, end); 56862 } catch (e) { 56863 sourceUpdater.logger_(`Remove ${start} to ${end} from ${type}Buffer failed`); 56864 } 56865 }, 56866 timestampOffset: offset => (type, sourceUpdater) => { 56867 const sourceBuffer = sourceUpdater[`${type}Buffer`]; // can't do anything if the media source / source buffer is null 56868 // or the media source does not contain this source buffer. 56869 56870 if (!inSourceBuffers(sourceUpdater.mediaSource, sourceBuffer)) { 56871 return; 56872 } 56873 sourceUpdater.logger_(`Setting ${type}timestampOffset to ${offset}`); 56874 sourceBuffer.timestampOffset = offset; 56875 }, 56876 callback: callback => (type, sourceUpdater) => { 56877 callback(); 56878 }, 56879 endOfStream: error => sourceUpdater => { 56880 if (sourceUpdater.mediaSource.readyState !== 'open') { 56881 return; 56882 } 56883 sourceUpdater.logger_(`Calling mediaSource endOfStream(${error || ''})`); 56884 try { 56885 sourceUpdater.mediaSource.endOfStream(error); 56886 } catch (e) { 56887 videojs.log.warn('Failed to call media source endOfStream', e); 56888 } 56889 }, 56890 duration: duration => sourceUpdater => { 56891 sourceUpdater.logger_(`Setting mediaSource duration to ${duration}`); 56892 try { 56893 sourceUpdater.mediaSource.duration = duration; 56894 } catch (e) { 56895 videojs.log.warn('Failed to set media source duration', e); 56896 } 56897 }, 56898 abort: () => (type, sourceUpdater) => { 56899 if (sourceUpdater.mediaSource.readyState !== 'open') { 56900 return; 56901 } 56902 const sourceBuffer = sourceUpdater[`${type}Buffer`]; // can't do anything if the media source / source buffer is null 56903 // or the media source does not contain this source buffer. 56904 56905 if (!inSourceBuffers(sourceUpdater.mediaSource, sourceBuffer)) { 56906 return; 56907 } 56908 sourceUpdater.logger_(`calling abort on ${type}Buffer`); 56909 try { 56910 sourceBuffer.abort(); 56911 } catch (e) { 56912 videojs.log.warn(`Failed to abort on ${type}Buffer`, e); 56913 } 56914 }, 56915 addSourceBuffer: (type, codec) => sourceUpdater => { 56916 const titleType = toTitleCase(type); 56917 const mime = getMimeForCodec(codec); 56918 sourceUpdater.logger_(`Adding ${type}Buffer with codec ${codec} to mediaSource`); 56919 const sourceBuffer = sourceUpdater.mediaSource.addSourceBuffer(mime); 56920 sourceBuffer.addEventListener('updateend', sourceUpdater[`on${titleType}UpdateEnd_`]); 56921 sourceBuffer.addEventListener('error', sourceUpdater[`on${titleType}Error_`]); 56922 sourceUpdater.codecs[type] = codec; 56923 sourceUpdater[`${type}Buffer`] = sourceBuffer; 56924 }, 56925 removeSourceBuffer: type => sourceUpdater => { 56926 const sourceBuffer = sourceUpdater[`${type}Buffer`]; 56927 cleanupBuffer(type, sourceUpdater); // can't do anything if the media source / source buffer is null 56928 // or the media source does not contain this source buffer. 56929 56930 if (!inSourceBuffers(sourceUpdater.mediaSource, sourceBuffer)) { 56931 return; 56932 } 56933 sourceUpdater.logger_(`Removing ${type}Buffer with codec ${sourceUpdater.codecs[type]} from mediaSource`); 56934 try { 56935 sourceUpdater.mediaSource.removeSourceBuffer(sourceBuffer); 56936 } catch (e) { 56937 videojs.log.warn(`Failed to removeSourceBuffer ${type}Buffer`, e); 56938 } 56939 }, 56940 changeType: codec => (type, sourceUpdater) => { 56941 const sourceBuffer = sourceUpdater[`${type}Buffer`]; 56942 const mime = getMimeForCodec(codec); // can't do anything if the media source / source buffer is null 56943 // or the media source does not contain this source buffer. 56944 56945 if (!inSourceBuffers(sourceUpdater.mediaSource, sourceBuffer)) { 56946 return; 56947 } // do not update codec if we don't need to.
vendor: 6,410 bytes, lines 56948-57146
56948 56949 if (sourceUpdater.codecs[type] === codec) { 56950 return; 56951 } 56952 sourceUpdater.logger_(`changing ${type}Buffer codec from ${sourceUpdater.codecs[type]} to ${codec}`); 56953 sourceBuffer.changeType(mime); 56954 sourceUpdater.codecs[type] = codec; 56955 } 56956 }; 56957 const pushQueue = ({ 56958 type, 56959 sourceUpdater, 56960 action, 56961 doneFn, 56962 name 56963 }) => { 56964 sourceUpdater.queue.push({ 56965 type, 56966 action, 56967 doneFn, 56968 name 56969 }); 56970 shiftQueue(type, sourceUpdater); 56971 }; 56972 const onUpdateend = (type, sourceUpdater) => e => { 56973 // Although there should, in theory, be a pending action for any updateend receieved, 56974 // there are some actions that may trigger updateend events without set definitions in 56975 // the w3c spec. For instance, setting the duration on the media source may trigger 56976 // updateend events on source buffers. This does not appear to be in the spec. As such, 56977 // if we encounter an updateend without a corresponding pending action from our queue 56978 // for that source buffer type, process the next action. 56979 if (sourceUpdater.queuePending[type]) { 56980 const doneFn = sourceUpdater.queuePending[type].doneFn; 56981 sourceUpdater.queuePending[type] = null; 56982 if (doneFn) { 56983 // if there's an error, report it 56984 doneFn(sourceUpdater[`${type}Error_`]); 56985 } 56986 } 56987 shiftQueue(type, sourceUpdater); 56988 }; 56989 /** 56990 * A queue of callbacks to be serialized and applied when a 56991 * MediaSource and its associated SourceBuffers are not in the 56992 * updating state. It is used by the segment loader to update the 56993 * underlying SourceBuffers when new data is loaded, for instance. 56994 * 56995 * @class SourceUpdater 56996 * @param {MediaSource} mediaSource the MediaSource to create the SourceBuffer from 56997 * @param {string} mimeType the desired MIME type of the underlying SourceBuffer 56998 */ 56999 57000 class SourceUpdater extends videojs.EventTarget { 57001 constructor(mediaSource) { 57002 super(); 57003 this.mediaSource = mediaSource; 57004 this.sourceopenListener_ = () => shiftQueue('mediaSource', this); 57005 this.mediaSource.addEventListener('sourceopen', this.sourceopenListener_); 57006 this.logger_ = logger('SourceUpdater'); // initial timestamp offset is 0 57007 57008 this.audioTimestampOffset_ = 0; 57009 this.videoTimestampOffset_ = 0; 57010 this.queue = []; 57011 this.queuePending = { 57012 audio: null, 57013 video: null 57014 }; 57015 this.delayedAudioAppendQueue_ = []; 57016 this.videoAppendQueued_ = false; 57017 this.codecs = {}; 57018 this.onVideoUpdateEnd_ = onUpdateend('video', this); 57019 this.onAudioUpdateEnd_ = onUpdateend('audio', this); 57020 this.onVideoError_ = e => { 57021 // used for debugging 57022 this.videoError_ = e; 57023 }; 57024 this.onAudioError_ = e => { 57025 // used for debugging 57026 this.audioError_ = e; 57027 }; 57028 this.createdSourceBuffers_ = false; 57029 this.initializedEme_ = false; 57030 this.triggeredReady_ = false; 57031 } 57032 initializedEme() { 57033 this.initializedEme_ = true; 57034 this.triggerReady(); 57035 } 57036 hasCreatedSourceBuffers() { 57037 // if false, likely waiting on one of the segment loaders to get enough data to create 57038 // source buffers 57039 return this.createdSourceBuffers_; 57040 } 57041 hasInitializedAnyEme() { 57042 return this.initializedEme_; 57043 } 57044 ready() { 57045 return this.hasCreatedSourceBuffers() && this.hasInitializedAnyEme(); 57046 } 57047 createSourceBuffers(codecs) { 57048 if (this.hasCreatedSourceBuffers()) { 57049 // already created them before 57050 return; 57051 } // the intial addOrChangeSourceBuffers will always be 57052 // two add buffers. 57053 57054 this.addOrChangeSourceBuffers(codecs); 57055 this.createdSourceBuffers_ = true; 57056 this.trigger('createdsourcebuffers'); 57057 this.triggerReady(); 57058 } 57059 triggerReady() { 57060 // only allow ready to be triggered once, this prevents the case 57061 // where: 57062 // 1. we trigger createdsourcebuffers 57063 // 2. ie 11 synchronously initializates eme 57064 // 3. the synchronous initialization causes us to trigger ready 57065 // 4. We go back to the ready check in createSourceBuffers and ready is triggered again. 57066 if (this.ready() && !this.triggeredReady_) { 57067 this.triggeredReady_ = true; 57068 this.trigger('ready'); 57069 } 57070 } 57071 /** 57072 * Add a type of source buffer to the media source. 57073 * 57074 * @param {string} type 57075 * The type of source buffer to add. 57076 * 57077 * @param {string} codec 57078 * The codec to add the source buffer with. 57079 */ 57080 57081 addSourceBuffer(type, codec) { 57082 pushQueue({ 57083 type: 'mediaSource', 57084 sourceUpdater: this, 57085 action: actions.addSourceBuffer(type, codec), 57086 name: 'addSourceBuffer' 57087 }); 57088 } 57089 /** 57090 * call abort on a source buffer. 57091 * 57092 * @param {string} type 57093 * The type of source buffer to call abort on. 57094 */ 57095 57096 abort(type) { 57097 pushQueue({ 57098 type, 57099 sourceUpdater: this, 57100 action: actions.abort(type), 57101 name: 'abort' 57102 }); 57103 } 57104 /** 57105 * Call removeSourceBuffer and remove a specific type 57106 * of source buffer on the mediaSource. 57107 * 57108 * @param {string} type 57109 * The type of source buffer to remove. 57110 */ 57111 57112 removeSourceBuffer(type) { 57113 if (!this.canRemoveSourceBuffer()) { 57114 videojs.log.error('removeSourceBuffer is not supported!'); 57115 return; 57116 } 57117 pushQueue({ 57118 type: 'mediaSource', 57119 sourceUpdater: this, 57120 action: actions.removeSourceBuffer(type), 57121 name: 'removeSourceBuffer' 57122 }); 57123 } 57124 /** 57125 * Whether or not the removeSourceBuffer function is supported 57126 * on the mediaSource. 57127 * 57128 * @return {boolean} 57129 * if removeSourceBuffer can be called. 57130 */ 57131 57132 canRemoveSourceBuffer() { 57133 // As of Firefox 83 removeSourceBuffer 57134 // throws errors, so we report that it does not support this. 57135 return !videojs.browser.IS_FIREFOX && window.MediaSource && window.MediaSource.prototype && typeof window.MediaSource.prototype.removeSourceBuffer === 'function'; 57136 } 57137 /** 57138 * Whether or not the changeType function is supported 57139 * on our SourceBuffers. 57140 * 57141 * @return {boolean} 57142 * if changeType can be called. 57143 */ 57144 57145 static canChangeType() { 57146 return window.SourceBuffer && window.SourceBuffer.prototype && typeof window
vendor: 9,491 bytes, lines 57146-57436
57146.SourceBuffer.prototype.changeType === 'function'; 57147 } 57148 /** 57149 * Whether or not the changeType function is supported 57150 * on our SourceBuffers. 57151 * 57152 * @return {boolean} 57153 * if changeType can be called. 57154 */ 57155 57156 canChangeType() { 57157 return this.constructor.canChangeType(); 57158 } 57159 /** 57160 * Call the changeType function on a source buffer, given the code and type. 57161 * 57162 * @param {string} type 57163 * The type of source buffer to call changeType on. 57164 * 57165 * @param {string} codec 57166 * The codec string to change type with on the source buffer. 57167 */ 57168 57169 changeType(type, codec) { 57170 if (!this.canChangeType()) { 57171 videojs.log.error('changeType is not supported!'); 57172 return; 57173 } 57174 pushQueue({ 57175 type, 57176 sourceUpdater: this, 57177 action: actions.changeType(codec), 57178 name: 'changeType' 57179 }); 57180 } 57181 /** 57182 * Add source buffers with a codec or, if they are already created, 57183 * call changeType on source buffers using changeType. 57184 * 57185 * @param {Object} codecs 57186 * Codecs to switch to 57187 */ 57188 57189 addOrChangeSourceBuffers(codecs) { 57190 if (!codecs || typeof codecs !== 'object' || Object.keys(codecs).length === 0) { 57191 throw new Error('Cannot addOrChangeSourceBuffers to undefined codecs'); 57192 } 57193 Object.keys(codecs).forEach(type => { 57194 const codec = codecs[type]; 57195 if (!this.hasCreatedSourceBuffers()) { 57196 return this.addSourceBuffer(type, codec); 57197 } 57198 if (this.canChangeType()) { 57199 this.changeType(type, codec); 57200 } 57201 }); 57202 } 57203 /** 57204 * Queue an update to append an ArrayBuffer. 57205 * 57206 * @param {MediaObject} object containing audioBytes and/or videoBytes 57207 * @param {Function} done the function to call when done 57208 * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-appendBuffer-void-ArrayBuffer-data 57209 */ 57210 57211 appendBuffer(options, doneFn) { 57212 const { 57213 segmentInfo, 57214 type, 57215 bytes 57216 } = options; 57217 this.processedAppend_ = true; 57218 if (type === 'audio' && this.videoBuffer && !this.videoAppendQueued_) { 57219 this.delayedAudioAppendQueue_.push([options, doneFn]); 57220 this.logger_(`delayed audio append of ${bytes.length} until video append`); 57221 return; 57222 } // In the case of certain errors, for instance, QUOTA_EXCEEDED_ERR, updateend will 57223 // not be fired. This means that the queue will be blocked until the next action 57224 // taken by the segment-loader. Provide a mechanism for segment-loader to handle 57225 // these errors by calling the doneFn with the specific error. 57226 57227 const onError = doneFn; 57228 pushQueue({ 57229 type, 57230 sourceUpdater: this, 57231 action: actions.appendBuffer(bytes, segmentInfo || { 57232 mediaIndex: -1 57233 }, onError), 57234 doneFn, 57235 name: 'appendBuffer' 57236 }); 57237 if (type === 'video') { 57238 this.videoAppendQueued_ = true; 57239 if (!this.delayedAudioAppendQueue_.length) { 57240 return; 57241 } 57242 const queue = this.delayedAudioAppendQueue_.slice(); 57243 this.logger_(`queuing delayed audio ${queue.length} appendBuffers`); 57244 this.delayedAudioAppendQueue_.length = 0; 57245 queue.forEach(que => { 57246 this.appendBuffer.apply(this, que); 57247 }); 57248 } 57249 } 57250 /** 57251 * Get the audio buffer's buffered timerange. 57252 * 57253 * @return {TimeRange} 57254 * The audio buffer's buffered time range 57255 */ 57256 57257 audioBuffered() { 57258 // no media source/source buffer or it isn't in the media sources 57259 // source buffer list 57260 if (!inSourceBuffers(this.mediaSource, this.audioBuffer)) { 57261 return createTimeRanges(); 57262 } 57263 return this.audioBuffer.buffered ? this.audioBuffer.buffered : createTimeRanges(); 57264 } 57265 /** 57266 * Get the video buffer's buffered timerange. 57267 * 57268 * @return {TimeRange} 57269 * The video buffer's buffered time range 57270 */ 57271 57272 videoBuffered() { 57273 // no media source/source buffer or it isn't in the media sources 57274 // source buffer list 57275 if (!inSourceBuffers(this.mediaSource, this.videoBuffer)) { 57276 return createTimeRanges(); 57277 } 57278 return this.videoBuffer.buffered ? this.videoBuffer.buffered : createTimeRanges(); 57279 } 57280 /** 57281 * Get a combined video/audio buffer's buffered timerange. 57282 * 57283 * @return {TimeRange} 57284 * the combined time range 57285 */ 57286 57287 buffered() { 57288 const video = inSourceBuffers(this.mediaSource, this.videoBuffer) ? this.videoBuffer : null; 57289 const audio = inSourceBuffers(this.mediaSource, this.audioBuffer) ? this.audioBuffer : null; 57290 if (audio && !video) { 57291 return this.audioBuffered(); 57292 } 57293 if (video && !audio) { 57294 return this.videoBuffered(); 57295 } 57296 return bufferIntersection(this.audioBuffered(), this.videoBuffered()); 57297 } 57298 /** 57299 * Add a callback to the queue that will set duration on the mediaSource. 57300 * 57301 * @param {number} duration 57302 * The duration to set 57303 * 57304 * @param {Function} [doneFn] 57305 * function to run after duration has been set. 57306 */ 57307 57308 setDuration(duration, doneFn = noop) { 57309 // In order to set the duration on the media source, it's necessary to wait for all 57310 // source buffers to no longer be updating. "If the updating attribute equals true on 57311 // any SourceBuffer in sourceBuffers, then throw an InvalidStateError exception and 57312 // abort these steps." (source: https://www.w3.org/TR/media-source/#attributes). 57313 pushQueue({ 57314 type: 'mediaSource', 57315 sourceUpdater: this, 57316 action: actions.duration(duration), 57317 name: 'duration', 57318 doneFn 57319 }); 57320 } 57321 /** 57322 * Add a mediaSource endOfStream call to the queue 57323 * 57324 * @param {Error} [error] 57325 * Call endOfStream with an error 57326 * 57327 * @param {Function} [doneFn] 57328 * A function that should be called when the 57329 * endOfStream call has finished. 57330 */ 57331 57332 endOfStream(error = null, doneFn = noop) { 57333 if (typeof error !== 'string') { 57334 error = undefined; 57335 } // In order to set the duration on the media source, it's necessary to wait for all 57336 // source buffers to no longer be updating. "If the updating attribute equals true on 57337 // any SourceBuffer in sourceBuffers, then throw an InvalidStateError exception and 57338 // abort these steps." (source: https://www.w3.org/TR/media-source/#attributes). 57339 57340 pushQueue({ 57341 type: 'mediaSource', 57342 sourceUpdater: this, 57343 action: actions.endOfStream(error), 57344 name: 'endOfStream', 57345 doneFn 57346 }); 57347 } 57348 /** 57349 * Queue an update to remove a time range from the buffer. 57350 * 57351 * @param {number} start where to start the removal 57352 * @param {number} end where to end the removal 57353 * @param {Function} [done=noop] optional callback to be executed when the remove 57354 * operation is complete 57355 * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-remove-void-double-start-unrestricted-double-end 57356 */ 57357 57358 removeAudio(start, end, done = noop) { 57359 if (!this.audioBuffered().length || this.audioBuffered().end(0) === 0) { 57360 done(); 57361 return; 57362 } 57363 pushQueue({ 57364 type: 'audio', 57365 sourceUpdater: this, 57366 action: actions.remove(start, end), 57367 doneFn: done, 57368 name: 'remove' 57369 }); 57370 } 57371 /** 57372 * Queue an update to remove a time range from the buffer. 57373 * 57374 * @param {number} start where to start the removal 57375 * @param {number} end where to end the removal 57376 * @param {Function} [done=noop] optional callback to be executed when the remove 57377 * operation is complete 57378 * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-remove-void-double-start-unrestricted-double-end 57379 */ 57380 57381 removeVideo(start, end, done = noop) { 57382 if (!this.videoBuffered().length || this.videoBuffered().end(0) === 0) { 57383 done(); 57384 return; 57385 } 57386 pushQueue({ 57387 type: 'video', 57388 sourceUpdater: this, 57389 action: actions.remove(start, end), 57390 doneFn: done, 57391 name: 'remove' 57392 }); 57393 } 57394 /** 57395 * Whether the underlying sourceBuffer is updating or not 57396 * 57397 * @return {boolean} the updating status of the SourceBuffer 57398 */ 57399 57400 updating() { 57401 // the audio/video source buffer is updating 57402 if (updating('audio', this) || updating('video', this)) { 57403 return true; 57404 } 57405 return false; 57406 } 57407 /** 57408 * Set/get the timestampoffset on the audio SourceBuffer 57409 * 57410 * @return {number} the timestamp offset 57411 */ 57412 57413 audioTimestampOffset(offset) { 57414 if (typeof offset !== 'undefined' && this.audioBuffer && 57415 // no point in updating if it's the same 57416 this.audioTimestampOffset_ !== offset) { 57417 pushQueue({ 57418 type: 'audio', 57419 sourceUpdater: this, 57420 action: actions.timestampOffset(offset), 57421 name: 'timestampOffset' 57422 }); 57423 this.audioTimestampOffset_ = offset; 57424 } 57425 return this.audioTimestampOffset_; 57426 } 57427 /** 57428 * Set/get the timestampoffset on the video SourceBuffer 57429 * 57430 * @return {number} the timestamp offset 57431 */ 57432 57433 videoTimestampOffset(offset) { 57434 if (typeof offset !== 'undefined' && this.videoBuffer && 57435 // no point in updating if it's the same 57436 this.videoTimestampOffset
vendor: 28,116 bytes, lines 57436-58234
57436 !== offset) { 57437 pushQueue({ 57438 type: 'video', 57439 sourceUpdater: this, 57440 action: actions.timestampOffset(offset), 57441 name: 'timestampOffset' 57442 }); 57443 this.videoTimestampOffset_ = offset; 57444 } 57445 return this.videoTimestampOffset_; 57446 } 57447 /** 57448 * Add a function to the queue that will be called 57449 * when it is its turn to run in the audio queue. 57450 * 57451 * @param {Function} callback 57452 * The callback to queue. 57453 */ 57454 57455 audioQueueCallback(callback) { 57456 if (!this.audioBuffer) { 57457 return; 57458 } 57459 pushQueue({ 57460 type: 'audio', 57461 sourceUpdater: this, 57462 action: actions.callback(callback), 57463 name: 'callback' 57464 }); 57465 } 57466 /** 57467 * Add a function to the queue that will be called 57468 * when it is its turn to run in the video queue. 57469 * 57470 * @param {Function} callback 57471 * The callback to queue. 57472 */ 57473 57474 videoQueueCallback(callback) { 57475 if (!this.videoBuffer) { 57476 return; 57477 } 57478 pushQueue({ 57479 type: 'video', 57480 sourceUpdater: this, 57481 action: actions.callback(callback), 57482 name: 'callback' 57483 }); 57484 } 57485 /** 57486 * dispose of the source updater and the underlying sourceBuffer 57487 */ 57488 57489 dispose() { 57490 this.trigger('dispose'); 57491 bufferTypes.forEach(type => { 57492 this.abort(type); 57493 if (this.canRemoveSourceBuffer()) { 57494 this.removeSourceBuffer(type); 57495 } else { 57496 this[`${type}QueueCallback`](() => cleanupBuffer(type, this)); 57497 } 57498 }); 57499 this.videoAppendQueued_ = false; 57500 this.delayedAudioAppendQueue_.length = 0; 57501 if (this.sourceopenListener_) { 57502 this.mediaSource.removeEventListener('sourceopen', this.sourceopenListener_); 57503 } 57504 this.off(); 57505 } 57506 } 57507 const uint8ToUtf8 = uintArray => decodeURIComponent(escape(String.fromCharCode.apply(null, uintArray))); 57508 const bufferToHexString = buffer => { 57509 const uInt8Buffer = new Uint8Array(buffer); 57510 return Array.from(uInt8Buffer).map(byte => byte.toString(16).padStart(2, '0')).join(''); 57511 }; 57512 57513 /** 57514 * @file vtt-segment-loader.js 57515 */ 57516 const VTT_LINE_TERMINATORS = new Uint8Array('\n\n'.split('').map(char => char.charCodeAt(0))); 57517 class NoVttJsError extends Error { 57518 constructor() { 57519 super('Trying to parse received VTT cues, but there is no WebVTT. Make sure vtt.js is loaded.'); 57520 } 57521 } 57522 /** 57523 * An object that manages segment loading and appending. 57524 * 57525 * @class VTTSegmentLoader 57526 * @param {Object} options required and optional options 57527 * @extends videojs.EventTarget 57528 */ 57529 57530 class VTTSegmentLoader extends SegmentLoader { 57531 constructor(settings, options = {}) { 57532 super(settings, options); // SegmentLoader requires a MediaSource be specified or it will throw an error; 57533 // however, VTTSegmentLoader has no need of a media source, so delete the reference 57534 57535 this.mediaSource_ = null; 57536 this.subtitlesTrack_ = null; 57537 this.loaderType_ = 'subtitle'; 57538 this.featuresNativeTextTracks_ = settings.featuresNativeTextTracks; 57539 this.loadVttJs = settings.loadVttJs; // The VTT segment will have its own time mappings. Saving VTT segment timing info in 57540 // the sync controller leads to improper behavior. 57541 57542 this.shouldSaveSegmentTimingInfo_ = false; 57543 } 57544 createTransmuxer_() { 57545 // don't need to transmux any subtitles 57546 return null; 57547 } 57548 /** 57549 * Indicates which time ranges are buffered 57550 * 57551 * @return {TimeRange} 57552 * TimeRange object representing the current buffered ranges 57553 */ 57554 57555 buffered_() { 57556 if (!this.subtitlesTrack_ || !this.subtitlesTrack_.cues || !this.subtitlesTrack_.cues.length) { 57557 return createTimeRanges(); 57558 } 57559 const cues = this.subtitlesTrack_.cues; 57560 const start = cues[0].startTime; 57561 const end = cues[cues.length - 1].startTime; 57562 return createTimeRanges([[start, end]]); 57563 } 57564 /** 57565 * Gets and sets init segment for the provided map 57566 * 57567 * @param {Object} map 57568 * The map object representing the init segment to get or set 57569 * @param {boolean=} set 57570 * If true, the init segment for the provided map should be saved 57571 * @return {Object} 57572 * map object for desired init segment 57573 */ 57574 57575 initSegmentForMap(map, set = false) { 57576 if (!map) { 57577 return null; 57578 } 57579 const id = initSegmentId(map); 57580 let storedMap = this.initSegments_[id]; 57581 if (set && !storedMap && map.bytes) { 57582 // append WebVTT line terminators to the media initialization segment if it exists 57583 // to follow the WebVTT spec (https://w3c.github.io/webvtt/#file-structure) that 57584 // requires two or more WebVTT line terminators between the WebVTT header and the 57585 // rest of the file 57586 const combinedByteLength = VTT_LINE_TERMINATORS.byteLength + map.bytes.byteLength; 57587 const combinedSegment = new Uint8Array(combinedByteLength); 57588 combinedSegment.set(map.bytes); 57589 combinedSegment.set(VTT_LINE_TERMINATORS, map.bytes.byteLength); 57590 this.initSegments_[id] = storedMap = { 57591 resolvedUri: map.resolvedUri, 57592 byterange: map.byterange, 57593 bytes: combinedSegment 57594 }; 57595 } 57596 return storedMap || map; 57597 } 57598 /** 57599 * Returns true if all configuration required for loading is present, otherwise false. 57600 * 57601 * @return {boolean} True if the all configuration is ready for loading 57602 * @private 57603 */ 57604 57605 couldBeginLoading_() { 57606 return this.playlist_ && this.subtitlesTrack_ && !this.paused(); 57607 } 57608 /** 57609 * Once all the starting parameters have been specified, begin 57610 * operation. This method should only be invoked from the INIT 57611 * state. 57612 * 57613 * @private 57614 */ 57615 57616 init_() { 57617 this.state = 'READY'; 57618 this.resetEverything(); 57619 return this.monitorBuffer_(); 57620 } 57621 /** 57622 * Set a subtitle track on the segment loader to add subtitles to 57623 * 57624 * @param {TextTrack=} track 57625 * The text track to add loaded subtitles to 57626 * @return {TextTrack} 57627 * Returns the subtitles track 57628 */ 57629 57630 track(track) { 57631 if (typeof track === 'undefined') { 57632 return this.subtitlesTrack_; 57633 } 57634 this.subtitlesTrack_ = track; // if we were unpaused but waiting for a sourceUpdater, start 57635 // buffering now 57636 57637 if (this.state === 'INIT' && this.couldBeginLoading_()) { 57638 this.init_(); 57639 } 57640 return this.subtitlesTrack_; 57641 } 57642 /** 57643 * Remove any data in the source buffer between start and end times 57644 * 57645 * @param {number} start - the start time of the region to remove from the buffer 57646 * @param {number} end - the end time of the region to remove from the buffer 57647 */ 57648 57649 remove(start, end) { 57650 removeCuesFromTrack(start, end, this.subtitlesTrack_); 57651 } 57652 /** 57653 * fill the buffer with segements unless the sourceBuffers are 57654 * currently updating 57655 * 57656 * Note: this function should only ever be called by monitorBuffer_ 57657 * and never directly 57658 * 57659 * @private 57660 */ 57661 57662 fillBuffer_() { 57663 // see if we need to begin loading immediately 57664 const segmentInfo = this.chooseNextRequest_(); 57665 if (!segmentInfo) { 57666 return; 57667 } 57668 if (this.syncController_.timestampOffsetForTimeline(segmentInfo.timeline) === null) { 57669 // We don't have the timestamp offset that we need to sync subtitles. 57670 // Rerun on a timestamp offset or user interaction. 57671 const checkTimestampOffset = () => { 57672 this.state = 'READY'; 57673 if (!this.paused()) { 57674 // if not paused, queue a buffer check as soon as possible 57675 this.monitorBuffer_(); 57676 } 57677 }; 57678 this.syncController_.one('timestampoffset', checkTimestampOffset); 57679 this.state = 'WAITING_ON_TIMELINE'; 57680 return; 57681 } 57682 this.loadSegment_(segmentInfo); 57683 } // never set a timestamp offset for vtt segments. 57684 57685 timestampOffsetForSegment_() { 57686 return null; 57687 } 57688 chooseNextRequest_() { 57689 return this.skipEmptySegments_(super.chooseNextRequest_()); 57690 } 57691 /** 57692 * Prevents the segment loader from requesting segments we know contain no subtitles 57693 * by walking forward until we find the next segment that we don't know whether it is 57694 * empty or not. 57695 * 57696 * @param {Object} segmentInfo 57697 * a segment info object that describes the current segment 57698 * @return {Object} 57699 * a segment info object that describes the current segment 57700 */ 57701 57702 skipEmptySegments_(segmentInfo) { 57703 while (segmentInfo && segmentInfo.segment.empty) { 57704 // stop at the last possible segmentInfo 57705 if (segmentInfo.mediaIndex + 1 >= segmentInfo.playlist.segments.length) { 57706 segmentInfo = null; 57707 break; 57708 } 57709 segmentInfo = this.generateSegmentInfo_({ 57710 playlist: segmentInfo.playlist, 57711 mediaIndex: segmentInfo.mediaIndex + 1, 57712 startOfSegment: segmentInfo.startOfSegment + segmentInfo.duration, 57713 isSyncRequest: segmentInfo.isSyncRequest 57714 }); 57715 } 57716 return segmentInfo; 57717 } 57718 stopForError(error) { 57719 this.error(error); 57720 this.state = 'READY'; 57721 this.pause(); 57722 this.trigger('error'); 57723 } 57724 /** 57725 * append a decrypted segement to the SourceBuffer through a SourceUpdater 57726 * 57727 * @private 57728 */ 57729 57730 segmentRequestFinished_(error, simpleSegment, result) { 57731 if (!this.subtitlesTrack_) { 57732 this.state = 'READY'; 57733 return; 57734 } 57735 this.saveTransferStats_(simpleSegment.stats); // the request was aborted 57736 57737 if (!this.pendingSegment_) { 57738 this.state = 'READY'; 57739 this.mediaRequestsAborted += 1; 57740 return; 57741 } 57742 if (error) { 57743 if (error.code === REQUEST_ERRORS.TIMEOUT) { 57744 this.handleTimeout_(); 57745 } 57746 if (error.code === REQUEST_ERRORS.ABORTED) { 57747 this.mediaRequestsAborted += 1; 57748 } else { 57749 this.mediaRequestsErrored += 1; 57750 } 57751 this.stopForError(error); 57752 return; 57753 } 57754 const segmentInfo = this.pendingSegment_; // although the VTT segment loader bandwidth isn't really used, it's good to 57755 // maintain functionality between segment loaders 57756 57757 this.saveBandwidthRelatedStats_(segmentInfo.duration, simpleSegment.stats); // if this request included a segment key, save that data in the cache 57758 57759 if (simpleSegment.key) { 57760 this.segmentKey(simpleSegment.key, true); 57761 } 57762 this.state = 'APPENDING'; // used for tests 57763 57764 this.trigger('appending'); 57765 const segment = segmentInfo.segment; 57766 if (segment.map) { 57767 segment.map.bytes = simpleSegment.map.bytes; 57768 } 57769 segmentInfo.bytes = simpleSegment.bytes; // Make sure that vttjs has loaded, otherwise, load it and wait till it finished loading 57770 57771 if (typeof window.WebVTT !== 'function' && typeof this.loadVttJs === 'function') { 57772 this.state = 'WAITING_ON_VTTJS'; // should be fine to call multiple times 57773 // script will be loaded once but multiple listeners will be added to the queue, which is expected. 57774 57775 this.loadVttJs().then(() => this.segmentRequestFinished_(error, simpleSegment, result), () => this.stopForError({ 57776 message: 'Error loading vtt.js' 57777 })); 57778 return; 57779 } 57780 segment.requested = true; 57781 try { 57782 this.parseVTTCues_(segmentInfo); 57783 } catch (e) { 57784 this.stopForError({ 57785 message: e.message 57786 }); 57787 return; 57788 } 57789 this.updateTimeMapping_(segmentInfo, this.syncController_.timelines[segmentInfo.timeline], this.playlist_); 57790 if (segmentInfo.cues.length) { 57791 segmentInfo.timingInfo = { 57792 start: segmentInfo.cues[0].startTime, 57793 end: segmentInfo.cues[segmentInfo.cues.length - 1].endTime 57794 }; 57795 } else { 57796 segmentInfo.timingInfo = { 57797 start: segmentInfo.startOfSegment, 57798 end: segmentInfo.startOfSegment + segmentInfo.duration 57799 }; 57800 } 57801 if (segmentInfo.isSyncRequest) { 57802 this.trigger('syncinfoupdate'); 57803 this.pendingSegment_ = null; 57804 this.state = 'READY'; 57805 return; 57806 } 57807 segmentInfo.byteLength = segmentInfo.bytes.byteLength; 57808 this.mediaSecondsLoaded += segment.duration; // Create VTTCue instances for each cue in the new segment and add them to 57809 // the subtitle track 57810 57811 segmentInfo.cues.forEach(cue => { 57812 this.subtitlesTrack_.addCue(this.featuresNativeTextTracks_ ? new window.VTTCue(cue.startTime, cue.endTime, cue.text) : cue); 57813 }); // Remove any duplicate cues from the subtitle track. The WebVTT spec allows 57814 // cues to have identical time-intervals, but if the text is also identical 57815 // we can safely assume it is a duplicate that can be removed (ex. when a cue 57816 // "overlaps" VTT segments) 57817 57818 removeDuplicateCuesFromTrack(this.subtitlesTrack_); 57819 this.handleAppendsDone_(); 57820 } 57821 handleData_() {// noop as we shouldn't be getting video/audio data captions 57822 // that we do not support here. 57823 } 57824 updateTimingInfoEnd_() {// noop 57825 } 57826 /** 57827 * Uses the WebVTT parser to parse the segment response 57828 * 57829 * @throws NoVttJsError 57830 * 57831 * @param {Object} segmentInfo 57832 * a segment info object that describes the current segment 57833 * @private 57834 */ 57835 57836 parseVTTCues_(segmentInfo) { 57837 let decoder; 57838 let decodeBytesToString = false; 57839 if (typeof window.WebVTT !== 'function') { 57840 // caller is responsible for exception handling. 57841 throw new NoVttJsError(); 57842 } 57843 if (typeof window.TextDecoder === 'function') { 57844 decoder = new window.TextDecoder('utf8'); 57845 } else { 57846 decoder = window.WebVTT.StringDecoder(); 57847 decodeBytesToString = true; 57848 } 57849 const parser = new window.WebVTT.Parser(window, window.vttjs, decoder); 57850 segmentInfo.cues = []; 57851 segmentInfo.timestampmap = { 57852 MPEGTS: 0, 57853 LOCAL: 0 57854 }; 57855 parser.oncue = segmentInfo.cues.push.bind(segmentInfo.cues); 57856 parser.ontimestampmap = map => { 57857 segmentInfo.timestampmap = map; 57858 }; 57859 parser.onparsingerror = error => { 57860 videojs.log.warn('Error encountered when parsing cues: ' + error.message); 57861 }; 57862 if (segmentInfo.segment.map) { 57863 let mapData = segmentInfo.segment.map.bytes; 57864 if (decodeBytesToString) { 57865 mapData = uint8ToUtf8(mapData); 57866 } 57867 parser.parse(mapData); 57868 } 57869 let segmentData = segmentInfo.bytes; 57870 if (decodeBytesToString) { 57871 segmentData = uint8ToUtf8(segmentData); 57872 } 57873 parser.parse(segmentData); 57874 parser.flush(); 57875 } 57876 /** 57877 * Updates the start and end times of any cues parsed by the WebVTT parser using 57878 * the information parsed from the X-TIMESTAMP-MAP header and a TS to media time mapping 57879 * from the SyncController 57880 * 57881 * @param {Object} segmentInfo 57882 * a segment info object that describes the current segment 57883 * @param {Object} mappingObj 57884 * object containing a mapping from TS to media time 57885 * @param {Object} playlist 57886 * the playlist object containing the segment 57887 * @private 57888 */ 57889 57890 updateTimeMapping_(segmentInfo, mappingObj, playlist) { 57891 const segment = segmentInfo.segment; 57892 if (!mappingObj) { 57893 // If the sync controller does not have a mapping of TS to Media Time for the 57894 // timeline, then we don't have enough information to update the cue 57895 // start/end times 57896 return; 57897 } 57898 if (!segmentInfo.cues.length) { 57899 // If there are no cues, we also do not have enough information to figure out 57900 // segment timing. Mark that the segment contains no cues so we don't re-request 57901 // an empty segment. 57902 segment.empty = true; 57903 return; 57904 } 57905 const timestampmap = segmentInfo.timestampmap; 57906 const diff = timestampmap.MPEGTS / clock_1 - timestampmap.LOCAL + mappingObj.mapping; 57907 segmentInfo.cues.forEach(cue => { 57908 // First convert cue time to TS time using the timestamp-map provided within the vtt 57909 cue.startTime += diff; 57910 cue.endTime += diff; 57911 }); 57912 if (!playlist.syncInfo) { 57913 const firstStart = segmentInfo.cues[0].startTime; 57914 const lastStart = segmentInfo.cues[segmentInfo.cues.length - 1].startTime; 57915 playlist.syncInfo = { 57916 mediaSequence: playlist.mediaSequence + segmentInfo.mediaIndex, 57917 time: Math.min(firstStart, lastStart - segment.duration) 57918 }; 57919 } 57920 } 57921 } 57922 57923 /** 57924 * @file ad-cue-tags.js 57925 */ 57926 /** 57927 * Searches for an ad cue that overlaps with the given mediaTime 57928 * 57929 * @param {Object} track 57930 * the track to find the cue for 57931 * 57932 * @param {number} mediaTime 57933 * the time to find the cue at 57934 * 57935 * @return {Object|null} 57936 * the found cue or null 57937 */ 57938 57939 const findAdCue = function (track, mediaTime) { 57940 const cues = track.cues; 57941 for (let i = 0; i < cues.length; i++) { 57942 const cue = cues[i]; 57943 if (mediaTime >= cue.adStartTime && mediaTime <= cue.adEndTime) { 57944 return cue; 57945 } 57946 } 57947 return null; 57948 }; 57949 const updateAdCues = function (media, track, offset = 0) { 57950 if (!media.segments) { 57951 return; 57952 } 57953 let mediaTime = offset; 57954 let cue; 57955 for (let i = 0; i < media.segments.length; i++) { 57956 const segment = media.segments[i]; 57957 if (!cue) { 57958 // Since the cues will span for at least the segment duration, adding a fudge 57959 // factor of half segment duration will prevent duplicate cues from being 57960 // created when timing info is not exact (e.g. cue start time initialized 57961 // at 10.006677, but next call mediaTime is 10.003332 ) 57962 cue = findAdCue(track, mediaTime + segment.duration / 2); 57963 } 57964 if (cue) { 57965 if ('cueIn' in segment) { 57966 // Found a CUE-IN so end the cue 57967 cue.endTime = mediaTime; 57968 cue.adEndTime = mediaTime; 57969 mediaTime += segment.duration; 57970 cue = null; 57971 continue; 57972 } 57973 if (mediaTime < cue.endTime) { 57974 // Already processed this mediaTime for this cue 57975 mediaTime += segment.duration; 57976 continue; 57977 } // otherwise extend cue until a CUE-IN is found 57978 57979 cue.endTime += segment.duration; 57980 } else { 57981 if ('cueOut' in segment) { 57982 cue = new window.VTTCue(mediaTime, mediaTime + segment.duration, segment.cueOut); 57983 cue.adStartTime = mediaTime; // Assumes tag format to be 57984 // #EXT-X-CUE-OUT:30 57985 57986 cue.adEndTime = mediaTime + parseFloat(segment.cueOut); 57987 track.addCue(cue); 57988 } 57989 if ('cueOutCont' in segment) { 57990 // Entered into the middle of an ad cue 57991 // Assumes tag formate to be 57992 // #EXT-X-CUE-OUT-CONT:10/30 57993 const [adOffset, adTotal] = segment.cueOutCont.split('/').map(parseFloat); 57994 cue = new window.VTTCue(mediaTime, mediaTime + segment.duration, ''); 57995 cue.adStartTime = mediaTime - adOffset; 57996 cue.adEndTime = cue.adStartTime + adTotal; 57997 track.addCue(cue); 57998 } 57999 } 58000 mediaTime += segment.duration; 58001 } 58002 }; 58003 58004 /** 58005 * @file sync-controller.js 58006 */ 58007 // synchronize expired playlist segments. 58008 // the max media sequence diff is 48 hours of live stream 58009 // content with two second segments. Anything larger than that 58010 // will likely be invalid. 58011 58012 const MAX_MEDIA_SEQUENCE_DIFF_FOR_SYNC = 86400; 58013 const syncPointStrategies = [ 58014 // Stategy "VOD": Handle the VOD-case where the sync-point is *always* 58015 // the equivalence display-time 0 === segment-index 0 58016 { 58017 name: 'VOD', 58018 run: (syncController, playlist, duration, currentTimeline, currentTime) => { 58019 if (duration !== Infinity) { 58020 const syncPoint = { 58021 time: 0, 58022 segmentIndex: 0, 58023 partIndex: null 58024 }; 58025 return syncPoint; 58026 } 58027 return null; 58028 } 58029 }, { 58030 name: 'MediaSequence', 58031 /** 58032 * run media sequence strategy 58033 * 58034 * @param {SyncController} syncController 58035 * @param {Object} playlist 58036 * @param {number} duration 58037 * @param {number} currentTimeline 58038 * @param {number} currentTime 58039 * @param {string} type 58040 */ 58041 run: (syncController, playlist, duration, currentTimeline, currentTime, type) => { 58042 if (!type) { 58043 return null; 58044 } 58045 const mediaSequenceMap = syncController.getMediaSequenceMap(type); 58046 if (!mediaSequenceMap || mediaSequenceMap.size === 0) { 58047 return null; 58048 } 58049 if (playlist.mediaSequence === undefined || !Array.isArray(playlist.segments) || !playlist.segments.length) { 58050 return null; 58051 } 58052 let currentMediaSequence = playlist.mediaSequence; 58053 let segmentIndex = 0; 58054 for (const segment of playlist.segments) { 58055 const range = mediaSequenceMap.get(currentMediaSequence); 58056 if (!range) { 58057 // unexpected case 58058 // we expect this playlist to be the same playlist in the map 58059 // just break from the loop and move forward to the next strategy 58060 break; 58061 } 58062 if (currentTime >= range.start && currentTime < range.end) { 58063 // we found segment 58064 if (Array.isArray(segment.parts) && segment.parts.length) { 58065 let currentPartStart = range.start; 58066 let partIndex = 0; 58067 for (const part of segment.parts) { 58068 const start = currentPartStart; 58069 const end = start + part.duration; 58070 if (currentTime >= start && currentTime < end) { 58071 return { 58072 time: range.start, 58073 segmentIndex, 58074 partIndex 58075 }; 58076 } 58077 partIndex++; 58078 currentPartStart = end; 58079 } 58080 } // no parts found, return sync point for segment 58081 58082 return { 58083 time: range.start, 58084 segmentIndex, 58085 partIndex: null 58086 }; 58087 } 58088 segmentIndex++; 58089 currentMediaSequence++; 58090 } // we didn't find any segments for provided current time 58091 58092 return null; 58093 } 58094 }, 58095 // Stategy "ProgramDateTime": We have a program-date-time tag in this playlist 58096 { 58097 name: 'ProgramDateTime', 58098 run: (syncController, playlist, duration, currentTimeline, currentTime) => { 58099 if (!Object.keys(syncController.timelineToDatetimeMappings).length) { 58100 return null; 58101 } 58102 let syncPoint = null; 58103 let lastDistance = null; 58104 const partsAndSegments = getPartsAndSegments(playlist); 58105 currentTime = currentTime || 0; 58106 for (let i = 0; i < partsAndSegments.length; i++) { 58107 // start from the end and loop backwards for live 58108 // or start from the front and loop forwards for non-live 58109 const index = playlist.endList || currentTime === 0 ? i : partsAndSegments.length - (i + 1); 58110 const partAndSegment = partsAndSegments[index]; 58111 const segment = partAndSegment.segment; 58112 const datetimeMapping = syncController.timelineToDatetimeMappings[segment.timeline]; 58113 if (!datetimeMapping || !segment.dateTimeObject) { 58114 continue; 58115 } 58116 const segmentTime = segment.dateTimeObject.getTime() / 1000; 58117 let start = segmentTime + datetimeMapping; // take part duration into account. 58118 58119 if (segment.parts && typeof partAndSegment.partIndex === 'number') { 58120 for (let z = 0; z < partAndSegment.partIndex; z++) { 58121 start += segment.parts[z].duration; 58122 } 58123 } 58124 const distance = Math.abs(currentTime - start); // Once the distance begins to increase, or if distance is 0, we have passed 58125 // currentTime and can stop looking for better candidates 58126 58127 if (lastDistance !== null && (distance === 0 || lastDistance < distance)) { 58128 break; 58129 } 58130 lastDistance = distance; 58131 syncPoint = { 58132 time: start, 58133 segmentIndex: partAndSegment.segmentIndex, 58134 partIndex: partAndSegment.partIndex 58135 }; 58136 } 58137 return syncPoint; 58138 } 58139 }, 58140 // Stategy "Segment": We have a known time mapping for a timeline and a 58141 // segment in the current timeline with timing data 58142 { 58143 name: 'Segment', 58144 run: (syncController, playlist, duration, currentTimeline, currentTime) => { 58145 let syncPoint = null; 58146 let lastDistance = null; 58147 currentTime = currentTime || 0; 58148 const partsAndSegments = getPartsAndSegments(playlist); 58149 for (let i = 0; i < partsAndSegments.length; i++) { 58150 // start from the end and loop backwards for live 58151 // or start from the front and loop forwards for non-live 58152 const index = playlist.endList || currentTime === 0 ? i : partsAndSegments.length - (i + 1); 58153 const partAndSegment = partsAndSegments[index]; 58154 const segment = partAndSegment.segment; 58155 const start = partAndSegment.part && partAndSegment.part.start || segment && segment.start; 58156 if (segment.timeline === currentTimeline && typeof start !== 'undefined') { 58157 const distance = Math.abs(currentTime - start); // Once the distance begins to increase, we have passed 58158 // currentTime and can stop looking for better candidates 58159 58160 if (lastDistance !== null && lastDistance < distance) { 58161 break; 58162 } 58163 if (!syncPoint || lastDistance === null || lastDistance >= distance) { 58164 lastDistance = distance; 58165 syncPoint = { 58166 time: start, 58167 segmentIndex: partAndSegment.segmentIndex, 58168 partIndex: partAndSegment.partIndex 58169 }; 58170 } 58171 } 58172 } 58173 return syncPoint; 58174 } 58175 }, 58176 // Stategy "Discontinuity": We have a discontinuity with a known 58177 // display-time 58178 { 58179 name: 'Discontinuity', 58180 run: (syncController, playlist, duration, currentTimeline, currentTime) => { 58181 let syncPoint = null; 58182 currentTime = currentTime || 0; 58183 if (playlist.discontinuityStarts && playlist.discontinuityStarts.length) { 58184 let lastDistance = null; 58185 for (let i = 0; i < playlist.discontinuityStarts.length; i++) { 58186 const segmentIndex = playlist.discontinuityStarts[i]; 58187 const discontinuity = playlist.discontinuitySequence + i + 1; 58188 const discontinuitySync = syncController.discontinuities[discontinuity]; 58189 if (discontinuitySync) { 58190 const distance = Math.abs(currentTime - discontinuitySync.time); // Once the distance begins to increase, we have passed 58191 // currentTime and can stop looking for better candidates 58192 58193 if (lastDistance !== null && lastDistance < distance) { 58194 break; 58195 } 58196 if (!syncPoint || lastDistance === null || lastDistance >= distance) { 58197 lastDistance = distance; 58198 syncPoint = { 58199 time: discontinuitySync.time, 58200 segmentIndex, 58201 partIndex: null 58202 }; 58203 } 58204 } 58205 } 58206 } 58207 return syncPoint; 58208 } 58209 }, 58210 // Stategy "Playlist": We have a playlist with a known mapping of 58211 // segment index to display time 58212 { 58213 name: 'Playlist', 58214 run: (syncController, playlist, duration, currentTimeline, currentTime) => { 58215 if (playlist.syncInfo) { 58216 const syncPoint = { 58217 time: playlist.syncInfo.time, 58218 segmentIndex: playlist.syncInfo.mediaSequence - playlist.mediaSequence, 58219 partIndex: null 58220 }; 58221 return syncPoint; 58222 } 58223 return null; 58224 } 58225 }]; 58226 class SyncController extends videojs.EventTarget { 58227 constructor(options = {}) { 58228 super(); // ...for synching across variants 58229 58230 this.timelines = []; 58231 this.discontinuities = []; 58232 this.timelineToDatetimeMappings = {}; 58233 /** 58234 * @type {Map<string, Map<number, { start: number, end: number }>
58234>} 58235 * @private 58236 */ 58237 58238 this.mediaSequenceStorage_ = new Map(); 58239 this.logger_ = logger('SyncController'); 58240 } 58241 /** 58242 * Get media sequence map by type 58243 * 58244 * @param {string} type - segment loader type 58245 * @return {Map<number, { start: number, end: number }> | undefined} 58246 */ 58247 58248 getMediaSequenceMap(type) { 58249 return this.mediaSequenceStorage_.get(type); 58250 } 58251 /** 58252 * Update Media Sequence Map -> <MediaSequence, Range> 58253 * 58254 * @param {Object} playlist - parsed playlist 58255 * @param {number} currentTime - current player's time 58256 * @param {string} type - segment loader type 58257 * @return {void} 58258 */ 58259 58260 updateMediaSequenceMap(playlist, currentTime, type) { 58261 // we should not process this playlist if it does not have mediaSequence or segments 58262 if (playlist.mediaSequence === undefined || !Array.isArray(playlist.segments) || !playlist.segments.length) { 58263 return; 58264 } 58265 const currentMap = this.getMediaSequenceMap(type); 58266 const result = new Map(); 58267 let currentMediaSequence = playlist.mediaSequence; 58268 let currentBaseTime; 58269 if (!currentMap) { 58270 // first playlist setup: 58271 currentBaseTime = 0; 58272 } else if (currentMap.has(playlist.mediaSequence)) { 58273 // further playlists setup: 58274 currentBaseTime = currentMap.get(playlist.mediaSequence).start; 58275 } else { 58276 // it seems like we have a gap between playlists, use current time as a fallback: 58277 this.logger_(`MediaSequence sync for ${type} segment loader - received a gap between playlists. 58278Fallback base time to: ${currentTime}. 58279Received media sequence: ${currentMediaSequence}. 58280Current map: `, currentMap); 58281 currentBaseTime = currentTime; 58282 } 58283 this.logger_(`MediaSequence sync for ${type} segment loader. 58284Received media sequence: ${currentMediaSequence}. 58285base time is ${currentBaseTime} 58286Current map: `, currentMap); 58287 playlist.segments.forEach(segment => { 58288 const start = currentBaseTime; 58289 const end = start + segment.duration; 58290 const range = { 58291 start, 58292 end 58293 }; 58294 result.set(currentMediaSequence, range); 58295 currentMediaSequence++; 58296 currentBaseTime = end; 58297 }); 58298 this.mediaSequenceStorage_.set(type, result); 58299 } 58300 /** 58301 * Find a sync-point for the playlist specified 58302 * 58303 * A sync-point is defined as a known mapping from display-time to 58304 * a segment-index in the current playlist. 58305 * 58306 * @param {Playlist} playlist 58307 * The playlist that needs a sync-point 58308 * @param {number} duration 58309 * Duration of the MediaSource (Infinite if playing a live source) 58310 * @param {number} currentTimeline 58311 * The last timeline from which a segment was loaded 58312 * @param {number} currentTime 58313 * Current player's time 58314 * @param {string} type 58315 * Segment loader type 58316 * @return {Object} 58317 * A sync-point object 58318 */ 58319 58320 getSyncPoint(playlist, duration, currentTimeline, currentTime, type) { 58321 // Always use VOD sync point for VOD 58322 if (duration !== Infinity) { 58323 const vodSyncPointStrategy = syncPointStrategies.find(({ 58324 name 58325 }) => name === 'VOD'); 58326 return vodSyncPointStrategy.run(this, playlist, duration); 58327 } 58328 const syncPoints = this.runStrategies_(playlist, duration, currentTimeline, currentTime, type); 58329 if (!syncPoints.length) { 58330 // Signal that we need to attempt to get a sync-point manually 58331 // by fetching a segment in the playlist and constructing 58332 // a sync-point from that information 58333 return null; 58334 } // If we have exact match just return it instead of finding the nearest distance 58335 58336 for (const syncPointInfo of syncPoints) { 58337 const { 58338 syncPoint, 58339 strategy 58340 } = syncPointInfo; 58341 const { 58342 segmentIndex, 58343 time 58344 } = syncPoint; 58345 if (segmentIndex < 0) { 58346 continue; 58347 } 58348 const selectedSegment = playlist.segments[segmentIndex]; 58349 const start = time; 58350 const end = start + selectedSegment.duration; 58351 this.logger_(`Strategy: ${strategy}. Current time: ${currentTime}. selected segment: ${segmentIndex}. Time: [${start} -> ${end}]}`); 58352 if (currentTime >= start && currentTime < end) { 58353 this.logger_('Found sync point with exact match: ', syncPoint); 58354 return syncPoint; 58355 } 58356 } // Now find the sync-point that is closest to the currentTime because 58357 // that should result in the most accurate guess about which segment 58358 // to fetch 58359 58360 return this.selectSyncPoint_(syncPoints, { 58361 key: 'time', 58362 value: currentTime 58363 }); 58364 } 58365 /** 58366 * Calculate the amount of time that has expired off the playlist during playback 58367 * 58368 * @param {Playlist} playlist 58369 * Playlist object to calculate expired from 58370 * @param {number} duration 58371 * Duration of the MediaSource (Infinity if playling a live source) 58372 * @return {number|null} 58373 * The amount of time that has expired off the playlist during playback. Null 58374 * if no sync-points for the playlist can be found. 58375 */ 58376 58377 getExpiredTime(playlist, duration) { 58378 if (!playlist || !playlist.segments) { 58379 return null; 58380 } 58381 const syncPoints = this.runStrategies_(playlist, duration, playlist.discontinuitySequence, 0, 'main'); // Without sync-points, there is not enough information to determine the expired time 58382 58383 if (!syncPoints.length) { 58384 return null; 58385 } 58386 const syncPoint = this.selectSyncPoint_(syncPoints, { 58387 key: 'segmentIndex', 58388 value: 0 58389 }); // If the sync-point is beyond the start of the playlist, we want to subtract the 58390 // duration from index 0 to syncPoint.segmentIndex instead of adding. 58391 58392 if (syncPoint.segmentIndex > 0) { 58393 syncPoint.time *= -1; 58394 } 58395 return Math.abs(syncPoint.time + sumDurations({ 58396 defaultDuration: playlist.targetDuration, 58397 durationList: playlist.segments, 58398 startIndex: syncPoint.segmentIndex, 58399 endIndex: 0 58400 })); 58401 } 58402 /** 58403 * Runs each sync-point strategy and returns a list of sync-points returned by the 58404 * strategies 58405 * 58406 * @private 58407 * @param {Playlist} playlist 58408 * The playlist that needs a sync-point 58409 * @param {number} duration 58410 * Duration of the MediaSource (Infinity if playing a live source) 58411 * @param {number} currentTimeline 58412 * The last timeline from which a segment was loaded 58413 * @param {number} currentTime
vendor: 16,973 bytes, lines 58414-58861
58414 * Current player's time 58415 * @param {string} type 58416 * Segment loader type 58417 * @return {Array} 58418 * A list of sync-point objects 58419 */ 58420 58421 runStrategies_(playlist, duration, currentTimeline, currentTime, type) { 58422 const syncPoints = []; // Try to find a sync-point in by utilizing various strategies... 58423 58424 for (let i = 0; i < syncPointStrategies.length; i++) { 58425 const strategy = syncPointStrategies[i]; 58426 const syncPoint = strategy.run(this, playlist, duration, currentTimeline, currentTime, type); 58427 if (syncPoint) { 58428 syncPoint.strategy = strategy.name; 58429 syncPoints.push({ 58430 strategy: strategy.name, 58431 syncPoint 58432 }); 58433 } 58434 } 58435 return syncPoints; 58436 } 58437 /** 58438 * Selects the sync-point nearest the specified target 58439 * 58440 * @private 58441 * @param {Array} syncPoints 58442 * List of sync-points to select from 58443 * @param {Object} target 58444 * Object specifying the property and value we are targeting 58445 * @param {string} target.key 58446 * Specifies the property to target. Must be either 'time' or 'segmentIndex' 58447 * @param {number} target.value 58448 * The value to target for the specified key. 58449 * @return {Object} 58450 * The sync-point nearest the target 58451 */ 58452 58453 selectSyncPoint_(syncPoints, target) { 58454 let bestSyncPoint = syncPoints[0].syncPoint; 58455 let bestDistance = Math.abs(syncPoints[0].syncPoint[target.key] - target.value); 58456 let bestStrategy = syncPoints[0].strategy; 58457 for (let i = 1; i < syncPoints.length; i++) { 58458 const newDistance = Math.abs(syncPoints[i].syncPoint[target.key] - target.value); 58459 if (newDistance < bestDistance) { 58460 bestDistance = newDistance; 58461 bestSyncPoint = syncPoints[i].syncPoint; 58462 bestStrategy = syncPoints[i].strategy; 58463 } 58464 } 58465 this.logger_(`syncPoint for [${target.key}: ${target.value}] chosen with strategy` + ` [${bestStrategy}]: [time:${bestSyncPoint.time},` + ` segmentIndex:${bestSyncPoint.segmentIndex}` + (typeof bestSyncPoint.partIndex === 'number' ? `,partIndex:${bestSyncPoint.partIndex}` : '') + ']'); 58466 return bestSyncPoint; 58467 } 58468 /** 58469 * Save any meta-data present on the segments when segments leave 58470 * the live window to the playlist to allow for synchronization at the 58471 * playlist level later. 58472 * 58473 * @param {Playlist} oldPlaylist - The previous active playlist 58474 * @param {Playlist} newPlaylist - The updated and most current playlist 58475 */ 58476 58477 saveExpiredSegmentInfo(oldPlaylist, newPlaylist) { 58478 const mediaSequenceDiff = newPlaylist.mediaSequence - oldPlaylist.mediaSequence; // Ignore large media sequence gaps 58479 58480 if (mediaSequenceDiff > MAX_MEDIA_SEQUENCE_DIFF_FOR_SYNC) { 58481 videojs.log.warn(`Not saving expired segment info. Media sequence gap ${mediaSequenceDiff} is too large.`); 58482 return; 58483 } // When a segment expires from the playlist and it has a start time 58484 // save that information as a possible sync-point reference in future 58485 58486 for (let i = mediaSequenceDiff - 1; i >= 0; i--) { 58487 const lastRemovedSegment = oldPlaylist.segments[i]; 58488 if (lastRemovedSegment && typeof lastRemovedSegment.start !== 'undefined') { 58489 newPlaylist.syncInfo = { 58490 mediaSequence: oldPlaylist.mediaSequence + i, 58491 time: lastRemovedSegment.start 58492 }; 58493 this.logger_(`playlist refresh sync: [time:${newPlaylist.syncInfo.time},` + ` mediaSequence: ${newPlaylist.syncInfo.mediaSequence}]`); 58494 this.trigger('syncinfoupdate'); 58495 break; 58496 } 58497 } 58498 } 58499 /** 58500 * Save the mapping from playlist's ProgramDateTime to display. This should only happen 58501 * before segments start to load. 58502 * 58503 * @param {Playlist} playlist - The currently active playlist 58504 */ 58505 58506 setDateTimeMappingForStart(playlist) { 58507 // It's possible for the playlist to be updated before playback starts, meaning time 58508 // zero is not yet set. If, during these playlist refreshes, a discontinuity is 58509 // crossed, then the old time zero mapping (for the prior timeline) would be retained 58510 // unless the mappings are cleared. 58511 this.timelineToDatetimeMappings = {}; 58512 if (playlist.segments && playlist.segments.length && playlist.segments[0].dateTimeObject) { 58513 const firstSegment = playlist.segments[0]; 58514 const playlistTimestamp = firstSegment.dateTimeObject.getTime() / 1000; 58515 this.timelineToDatetimeMappings[firstSegment.timeline] = -playlistTimestamp; 58516 } 58517 } 58518 /** 58519 * Calculates and saves timeline mappings, playlist sync info, and segment timing values 58520 * based on the latest timing information. 58521 * 58522 * @param {Object} options 58523 * Options object 58524 * @param {SegmentInfo} options.segmentInfo 58525 * The current active request information 58526 * @param {boolean} options.shouldSaveTimelineMapping 58527 * If there's a timeline change, determines if the timeline mapping should be 58528 * saved for timeline mapping and program date time mappings. 58529 */ 58530 58531 saveSegmentTimingInfo({ 58532 segmentInfo, 58533 shouldSaveTimelineMapping 58534 }) { 58535 const didCalculateSegmentTimeMapping = this.calculateSegmentTimeMapping_(segmentInfo, segmentInfo.timingInfo, shouldSaveTimelineMapping); 58536 const segment = segmentInfo.segment; 58537 if (didCalculateSegmentTimeMapping) { 58538 this.saveDiscontinuitySyncInfo_(segmentInfo); // If the playlist does not have sync information yet, record that information 58539 // now with segment timing information 58540 58541 if (!segmentInfo.playlist.syncInfo) { 58542 segmentInfo.playlist.syncInfo = { 58543 mediaSequence: segmentInfo.playlist.mediaSequence + segmentInfo.mediaIndex, 58544 time: segment.start 58545 }; 58546 } 58547 } 58548 const dateTime = segment.dateTimeObject; 58549 if (segment.discontinuity && shouldSaveTimelineMapping && dateTime) { 58550 this.timelineToDatetimeMappings[segment.timeline] = -(dateTime.getTime() / 1000); 58551 } 58552 } 58553 timestampOffsetForTimeline(timeline) { 58554 if (typeof this.timelines[timeline] === 'undefined') { 58555 return null; 58556 } 58557 return this.timelines[timeline].time; 58558 } 58559 mappingForTimeline(timeline) { 58560 if (typeof this.timelines[timeline] === 'undefined') { 58561 return null; 58562 } 58563 return this.timelines[timeline].mapping; 58564 } 58565 /** 58566 * Use the "media time" for a segment to generate a mapping to "display time" and 58567 * save that display time to the segment. 58568 * 58569 * @private 58570 * @param {SegmentInfo} segmentInfo 58571 * The current active request information 58572 * @param {Object} timingInfo 58573 * The start and end time of the current segment in "media time" 58574 * @param {boolean} shouldSaveTimelineMapping 58575 * If there's a timeline change, determines if the timeline mapping should be 58576 * saved in timelines. 58577 * @return {boolean} 58578 * Returns false if segment time mapping could not be calculated 58579 */ 58580 58581 calculateSegmentTimeMapping_(segmentInfo, timingInfo, shouldSaveTimelineMapping) { 58582 // TODO: remove side effects 58583 const segment = segmentInfo.segment; 58584 const part = segmentInfo.part; 58585 let mappingObj = this.timelines[segmentInfo.timeline]; 58586 let start; 58587 let end; 58588 if (typeof segmentInfo.timestampOffset === 'number') { 58589 mappingObj = { 58590 time: segmentInfo.startOfSegment, 58591 mapping: segmentInfo.startOfSegment - timingInfo.start 58592 }; 58593 if (shouldSaveTimelineMapping) { 58594 this.timelines[segmentInfo.timeline] = mappingObj; 58595 this.trigger('timestampoffset'); 58596 this.logger_(`time mapping for timeline ${segmentInfo.timeline}: ` + `[time: ${mappingObj.time}] [mapping: ${mappingObj.mapping}]`); 58597 } 58598 start = segmentInfo.startOfSegment; 58599 end = timingInfo.end + mappingObj.mapping; 58600 } else if (mappingObj) { 58601 start = timingInfo.start + mappingObj.mapping; 58602 end = timingInfo.end + mappingObj.mapping; 58603 } else { 58604 return false; 58605 } 58606 if (part) { 58607 part.start = start; 58608 part.end = end; 58609 } // If we don't have a segment start yet or the start value we got 58610 // is less than our current segment.start value, save a new start value. 58611 // We have to do this because parts will have segment timing info saved 58612 // multiple times and we want segment start to be the earliest part start 58613 // value for that segment. 58614 58615 if (!segment.start || start < segment.start) { 58616 segment.start = start; 58617 } 58618 segment.end = end; 58619 return true; 58620 } 58621 /** 58622 * Each time we have discontinuity in the playlist, attempt to calculate the location 58623 * in display of the start of the discontinuity and save that. We also save an accuracy 58624 * value so that we save values with the most accuracy (closest to 0.) 58625 * 58626 * @private 58627 * @param {SegmentInfo} segmentInfo - The current active request information 58628 */ 58629 58630 saveDiscontinuitySyncInfo_(segmentInfo) { 58631 const playlist = segmentInfo.playlist; 58632 const segment = segmentInfo.segment; // If the current segment is a discontinuity then we know exactly where 58633 // the start of the range and it's accuracy is 0 (greater accuracy values 58634 // mean more approximation) 58635 58636 if (segment.discontinuity) { 58637 this.discontinuities[segment.timeline] = { 58638 time: segment.start, 58639 accuracy: 0 58640 }; 58641 } else if (playlist.discontinuityStarts && playlist.discontinuityStarts.length) { 58642 // Search for future discontinuities that we can provide better timing 58643 // information for and save that information for sync purposes 58644 for (let i = 0; i < playlist.discontinuityStarts.length; i++) { 58645 const segmentIndex = playlist.discontinuityStarts[i]; 58646 const discontinuity = playlist.discontinuitySequence + i + 1; 58647 const mediaIndexDiff = segmentIndex - segmentInfo.mediaIndex; 58648 const accuracy = Math.abs(mediaIndexDiff); 58649 if (!this.discontinuities[discontinuity] || this.discontinuities[discontinuity].accuracy > accuracy) { 58650 let time; 58651 if (mediaIndexDiff < 0) { 58652 time = segment.start - sumDurations({ 58653 defaultDuration: playlist.targetDuration, 58654 durationList: playlist.segments, 58655 startIndex: segmentInfo.mediaIndex, 58656 endIndex: segmentIndex 58657 }); 58658 } else { 58659 time = segment.end + sumDurations({ 58660 defaultDuration: playlist.targetDuration, 58661 durationList: playlist.segments, 58662 startIndex: segmentInfo.mediaIndex + 1, 58663 endIndex: segmentIndex 58664 }); 58665 } 58666 this.discontinuities[discontinuity] = { 58667 time, 58668 accuracy 58669 }; 58670 } 58671 } 58672 } 58673 } 58674 dispose() { 58675 this.trigger('dispose'); 58676 this.off(); 58677 } 58678 } 58679 58680 /** 58681 * The TimelineChangeController acts as a source for segment loaders to listen for and 58682 * keep track of latest and pending timeline changes. This is useful to ensure proper 58683 * sync, as each loader may need to make a consideration for what timeline the other 58684 * loader is on before making changes which could impact the other loader's media. 58685 * 58686 * @class TimelineChangeController 58687 * @extends videojs.EventTarget 58688 */ 58689 58690 class TimelineChangeController extends videojs.EventTarget { 58691 constructor() { 58692 super(); 58693 this.pendingTimelineChanges_ = {}; 58694 this.lastTimelineChanges_ = {}; 58695 } 58696 clearPendingTimelineChange(type) { 58697 this.pendingTimelineChanges_[type] = null; 58698 this.trigger('pendingtimelinechange'); 58699 } 58700 pendingTimelineChange({ 58701 type, 58702 from, 58703 to 58704 }) { 58705 if (typeof from === 'number' && typeof to === 'number') { 58706 this.pendingTimelineChanges_[type] = { 58707 type, 58708 from, 58709 to 58710 }; 58711 this.trigger('pendingtimelinechange'); 58712 } 58713 return this.pendingTimelineChanges_[type]; 58714 } 58715 lastTimelineChange({ 58716 type, 58717 from, 58718 to 58719 }) { 58720 if (typeof from === 'number' && typeof to === 'number') { 58721 this.lastTimelineChanges_[type] = { 58722 type, 58723 from, 58724 to 58725 }; 58726 delete this.pendingTimelineChanges_[type]; 58727 this.trigger('timelinechange'); 58728 } 58729 return this.lastTimelineChanges_[type]; 58730 } 58731 dispose() { 58732 this.trigger('dispose'); 58733 this.pendingTimelineChanges_ = {}; 58734 this.lastTimelineChanges_ = {}; 58735 this.off(); 58736 } 58737 } 58738 58739 /* rollup-plugin-worker-factory start for worker!/home/runner/work/http-streaming/http-streaming/src/decrypter-worker.js */ 58740 const workerCode = transform(getWorkerString(function () { 58741 /** 58742 * @file stream.js 58743 */ 58744 58745 /** 58746 * A lightweight readable stream implemention that handles event dispatching. 58747 * 58748 * @class Stream 58749 */ 58750 58751 var Stream = /*#__PURE__*/function () { 58752 function Stream() { 58753 this.listeners = {}; 58754 } 58755 /** 58756 * Add a listener for a specified event type. 58757 * 58758 * @param {string} type the event name 58759 * @param {Function} listener the callback to be invoked when an event of 58760 * the specified type occurs 58761 */ 58762 58763 var _proto = Stream.prototype; 58764 _proto.on = function on(type, listener) { 58765 if (!this.listeners[type]) { 58766 this.listeners[type] = []; 58767 } 58768 this.listeners[type].push(listener); 58769 } 58770 /** 58771 * Remove a listener for a specified event type. 58772 * 58773 * @param {string} type the event name 58774 * @param {Function} listener a function previously registered for this 58775 * type of event through `on` 58776 * @return {boolean} if we could turn it off or not 58777 */; 58778 58779 _proto.off = function off(type, listener) { 58780 if (!this.listeners[type]) { 58781 return false; 58782 } 58783 var index = this.listeners[type].indexOf(listener); // TODO: which is better? 58784 // In Video.js we slice listener functions 58785 // on trigger so that it does not mess up the order 58786 // while we loop through. 58787 // 58788 // Here we slice on off so that the loop in trigger 58789 // can continue using it's old reference to loop without 58790 // messing up the order. 58791 58792 this.listeners[type] = this.listeners[type].slice(0); 58793 this.listeners[type].splice(index, 1); 58794 return index > -1; 58795 } 58796 /** 58797 * Trigger an event of the specified type on this stream. Any additional 58798 * arguments to this function are passed as parameters to event listeners. 58799 * 58800 * @param {string} type the event name 58801 */; 58802 58803 _proto.trigger = function trigger(type) { 58804 var callbacks = this.listeners[type]; 58805 if (!callbacks) { 58806 return; 58807 } // Slicing the arguments on every invocation of this method 58808 // can add a significant amount of overhead. Avoid the 58809 // intermediate object creation for the common case of a 58810 // single callback argument 58811 58812 if (arguments.length === 2) { 58813 var length = callbacks.length; 58814 for (var i = 0; i < length; ++i) { 58815 callbacks[i].call(this, arguments[1]); 58816 } 58817 } else { 58818 var args = Array.prototype.slice.call(arguments, 1); 58819 var _length = callbacks.length; 58820 for (var _i = 0; _i < _length; ++_i) { 58821 callbacks[_i].apply(this, args); 58822 } 58823 } 58824 } 58825 /** 58826 * Destroys the stream and cleans up. 58827 */; 58828 58829 _proto.dispose = function dispose() { 58830 this.listeners = {}; 58831 } 58832 /** 58833 * Forwards all `data` events on this stream to the destination stream. The 58834 * destination stream should provide a method `push` to receive the data 58835 * events as they arrive. 58836 * 58837 * @param {Stream} destination the stream that will receive all `data` events 58838 * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options 58839 */; 58840 58841 _proto.pipe = function pipe(destination) { 58842 this.on('data', function (data) { 58843 destination.push(data); 58844 }); 58845 }; 58846 return Stream; 58847 }(); 58848 /*! @name pkcs7 @version 1.0.4 @license Apache-2.0 */ 58849 58850 /** 58851 * Returns the subarray of a Uint8Array without PKCS#7 padding. 58852 * 58853 * @param padded {Uint8Array} unencrypted bytes that have been padded 58854 * @return {Uint8Array} the unpadded bytes 58855 * @see http://tools.ietf.org/html/rfc5652 58856 */ 58857 58858 function unpad(padded) { 58859 return padded.subarray(0, padded.byteLength - padded[padded.byteLength - 1]); 58860 } 58861 /*! @name aes-decrypter @version 4.0.1
vendor: 17,382 bytes, lines 58861-59333
58861 @license Apache-2.0 */ 58862 58863 /** 58864 * @file aes.js 58865 * 58866 * This file contains an adaptation of the AES decryption algorithm 58867 * from the Standford Javascript Cryptography Library. That work is 58868 * covered by the following copyright and permissions notice: 58869 * 58870 * Copyright 2009-2010 Emily Stark, Mike Hamburg, Dan Boneh. 58871 * All rights reserved. 58872 * 58873 * Redistribution and use in source and binary forms, with or without 58874 * modification, are permitted provided that the following conditions are 58875 * met: 58876 * 58877 * 1. Redistributions of source code must retain the above copyright 58878 * notice, this list of conditions and the following disclaimer. 58879 * 58880 * 2. Redistributions in binary form must reproduce the above 58881 * copyright notice, this list of conditions and the following 58882 * disclaimer in the documentation and/or other materials provided 58883 * with the distribution. 58884 * 58885 * THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR 58886 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 58887 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 58888 * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> OR CONTRIBUTORS BE 58889 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 58890 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 58891 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 58892 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 58893 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 58894 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN 58895 * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 58896 * 58897 * The views and conclusions contained in the software and documentation 58898 * are those of the authors and should not be interpreted as representing 58899 * official policies, either expressed or implied, of the authors. 58900 */ 58901 58902 /** 58903 * Expand the S-box tables. 58904 * 58905 * @private 58906 */ 58907 58908 const precompute = function () { 58909 const tables = [[[], [], [], [], []], [[], [], [], [], []]]; 58910 const encTable = tables[0]; 58911 const decTable = tables[1]; 58912 const sbox = encTable[4]; 58913 const sboxInv = decTable[4]; 58914 let i; 58915 let x; 58916 let xInv; 58917 const d = []; 58918 const th = []; 58919 let x2; 58920 let x4; 58921 let x8; 58922 let s; 58923 let tEnc; 58924 let tDec; // Compute double and third tables 58925 58926 for (i = 0; i < 256; i++) { 58927 th[(d[i] = i << 1 ^ (i >> 7) * 283) ^ i] = i; 58928 } 58929 for (x = xInv = 0; !sbox[x]; x ^= x2 || 1, xInv = th[xInv] || 1) { 58930 // Compute sbox 58931 s = xInv ^ xInv << 1 ^ xInv << 2 ^ xInv << 3 ^ xInv << 4; 58932 s = s >> 8 ^ s & 255 ^ 99; 58933 sbox[x] = s; 58934 sboxInv[s] = x; // Compute MixColumns 58935 58936 x8 = d[x4 = d[x2 = d[x]]]; 58937 tDec = x8 * 0x1010101 ^ x4 * 0x10001 ^ x2 * 0x101 ^ x * 0x1010100; 58938 tEnc = d[s] * 0x101 ^ s * 0x1010100; 58939 for (i = 0; i < 4; i++) { 58940 encTable[i][x] = tEnc = tEnc << 24 ^ tEnc >>> 8; 58941 decTable[i][s] = tDec = tDec << 24 ^ tDec >>> 8; 58942 } 58943 } // Compactify. Considerable speedup on Firefox. 58944 58945 for (i = 0; i < 5; i++) { 58946 encTable[i] = encTable[i].slice(0); 58947 decTable[i] = decTable[i].slice(0); 58948 } 58949 return tables; 58950 }; 58951 let aesTables = null; 58952 /** 58953 * Schedule out an AES key for both encryption and decryption. This 58954 * is a low-level class. Use a cipher mode to do bulk encryption. 58955 * 58956 * @class AES 58957 * @param key {Array} The key as an array of 4, 6 or 8 words. 58958 */ 58959 58960 class AES { 58961 constructor(key) { 58962 /** 58963 * The expanded S-box and inverse S-box tables. These will be computed 58964 * on the client so that we don't have to send them down the wire. 58965 * 58966 * There are two tables, _tables[0] is for encryption and 58967 * _tables[1] is for decryption. 58968 * 58969 * The first 4 sub-tables are the expanded S-box with MixColumns. The 58970 * last (_tables[01][4]) is the S-box itself. 58971 * 58972 * @private 58973 */ 58974 // if we have yet to precompute the S-box tables 58975 // do so now 58976 if (!aesTables) { 58977 aesTables = precompute(); 58978 } // then make a copy of that object for use 58979 58980 this._tables = [[aesTables[0][0].slice(), aesTables[0][1].slice(), aesTables[0][2].slice(), aesTables[0][3].slice(), aesTables[0][4].slice()], [aesTables[1][0].slice(), aesTables[1][1].slice(), aesTables[1][2].slice(), aesTables[1][3].slice(), aesTables[1][4].slice()]]; 58981 let i; 58982 let j; 58983 let tmp; 58984 const sbox = this._tables[0][4]; 58985 const decTable = this._tables[1]; 58986 const keyLen = key.length; 58987 let rcon = 1; 58988 if (keyLen !== 4 && keyLen !== 6 && keyLen !== 8) { 58989 throw new Error('Invalid aes key size'); 58990 } 58991 const encKey = key.slice(0); 58992 const decKey = []; 58993 this._key = [encKey, decKey]; // schedule encryption keys 58994 58995 for (i = keyLen; i < 4 * keyLen + 28; i++) { 58996 tmp = encKey[i - 1]; // apply sbox 58997 58998 if (i % keyLen === 0 || keyLen === 8 && i % keyLen === 4) { 58999 tmp = sbox[tmp >>> 24] << 24 ^ sbox[tmp >> 16 & 255] << 16 ^ sbox[tmp >> 8 & 255] << 8 ^ sbox[tmp & 255]; // shift rows and add rcon 59000 59001 if (i % keyLen === 0) { 59002 tmp = tmp << 8 ^ tmp >>> 24 ^ rcon << 24; 59003 rcon = rcon << 1 ^ (rcon >> 7) * 283; 59004 } 59005 } 59006 encKey[i] = encKey[i - keyLen] ^ tmp; 59007 } // schedule decryption keys 59008 59009 for (j = 0; i; j++, i--) { 59010 tmp = encKey[j & 3 ? i : i - 4]; 59011 if (i <= 4 || j < 4) { 59012 decKey[j] = tmp; 59013 } else { 59014 decKey[j] = decTable[0][sbox[tmp >>> 24]] ^ decTable[1][sbox[tmp >> 16 & 255]] ^ decTable[2][sbox[tmp >> 8 & 255]] ^ decTable[3][sbox[tmp & 255]]; 59015 } 59016 } 59017 } 59018 /** 59019 * Decrypt 16 bytes, specified as four 32-bit words. 59020 * 59021 * @param {number} encrypted0 the first word to decrypt 59022 * @param {number} encrypted1 the second word to decrypt 59023 * @param {number} encrypted2 the third word to decrypt 59024 * @param {number} encrypted3 the fourth word to decrypt 59025 * @param {Int32Array} out the array to write the decrypted words 59026 * into 59027 * @param {number} offset the offset into the output array to start 59028 * writing results 59029 * @return {Array} The plaintext. 59030 */ 59031 59032 decrypt(encrypted0, encrypted1, encrypted2, encrypted3, out, offset) { 59033 const key = this._key[1]; // state variables a,b,c,d are loaded with pre-whitened data 59034 59035 let a = encrypted0 ^ key[0]; 59036 let b = encrypted3 ^ key[1]; 59037 let c = encrypted2 ^ key[2]; 59038 let d = encrypted1 ^ key[3]; 59039 let a2; 59040 let b2; 59041 let c2; // key.length === 2 ? 59042 59043 const nInnerRounds = key.length / 4 - 2; 59044 let i; 59045 let kIndex = 4; 59046 const table = this._tables[1]; // load up the tables 59047 59048 const table0 = table[0]; 59049 const table1 = table[1]; 59050 const table2 = table[2]; 59051 const table3 = table[3]; 59052 const sbox = table[4]; // Inner rounds. Cribbed from OpenSSL. 59053 59054 for (i = 0; i < nInnerRounds; i++) { 59055 a2 = table0[a >>> 24] ^ table1[b >> 16 & 255] ^ table2[c >> 8 & 255] ^ table3[d & 255] ^ key[kIndex]; 59056 b2 = table0[b >>> 24] ^ table1[c >> 16 & 255] ^ table2[d >> 8 & 255] ^ table3[a & 255] ^ key[kIndex + 1]; 59057 c2 = table0[c >>> 24] ^ table1[d >> 16 & 255] ^ table2[a >> 8 & 255] ^ table3[b & 255] ^ key[kIndex + 2]; 59058 d = table0[d >>> 24] ^ table1[a >> 16 & 255] ^ table2[b >> 8 & 255] ^ table3[c & 255] ^ key[kIndex + 3]; 59059 kIndex += 4; 59060 a = a2; 59061 b = b2; 59062 c = c2; 59063 } // Last round. 59064 59065 for (i = 0; i < 4; i++) { 59066 out[(3 & -i) + offset] = sbox[a >>> 24] << 24 ^ sbox[b >> 16 & 255] << 16 ^ sbox[c >> 8 & 255] << 8 ^ sbox[d & 255] ^ key[kIndex++]; 59067 a2 = a; 59068 a = b; 59069 b = c; 59070 c = d; 59071 d = a2; 59072 } 59073 } 59074 } 59075 /** 59076 * @file async-stream.js 59077 */ 59078 59079 /** 59080 * A wrapper around the Stream class to use setTimeout 59081 * and run stream "jobs" Asynchronously 59082 * 59083 * @class AsyncStream 59084 * @extends Stream 59085 */ 59086 59087 class AsyncStream extends Stream { 59088 constructor() { 59089 super(Stream); 59090 this.jobs = []; 59091 this.delay = 1; 59092 this.timeout_ = null; 59093 } 59094 /** 59095 * process an async job 59096 * 59097 * @private 59098 */ 59099 59100 processJob_() { 59101 this.jobs.shift()(); 59102 if (this.jobs.length) { 59103 this.timeout_ = setTimeout(this.processJob_.bind(this), this.delay); 59104 } else { 59105 this.timeout_ = null; 59106 } 59107 } 59108 /** 59109 * push a job into the stream 59110 * 59111 * @param {Function} job the job to push into the stream 59112 */ 59113 59114 push(job) { 59115 this.jobs.push(job); 59116 if (!this.timeout_) { 59117 this.timeout_ = setTimeout(this.processJob_.bind(this), this.delay); 59118 } 59119 } 59120 } 59121 /** 59122 * @file decrypter.js 59123 * 59124 * An asynchronous implementation of AES-128 CBC decryption with 59125 * PKCS#7 padding. 59126 */ 59127 59128 /** 59129 * Convert network-order (big-endian) bytes into their little-endian 59130 * representation. 59131 */ 59132 59133 const ntoh = function (word) { 59134 return word << 24 | (word & 0xff00) << 8 | (word & 0xff0000) >> 8 | word >>> 24; 59135 }; 59136 /** 59137 * Decrypt bytes using AES-128 with CBC and PKCS#7 padding. 59138 * 59139 * @param {Uint8Array} encrypted the encrypted bytes 59140 * @param {Uint32Array} key the bytes of the decryption key 59141 * @param {Uint32Array} initVector the initialization vector (IV) to 59142 * use for the first round of CBC. 59143 * @return {Uint8Array} the decrypted bytes 59144 * 59145 * @see http://en.wikipedia.org/wiki/Advanced_Encryption_Standard 59146 * @see http://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_Block_Chaining_.28CBC.29 59147 * @see https://tools.ietf.org/html/rfc2315 59148 */ 59149 59150 const decrypt = function (encrypted, key, initVector) { 59151 // word-level access to the encrypted bytes 59152 const encrypted32 = new Int32Array(encrypted.buffer, encrypted.byteOffset, encrypted.byteLength >> 2); 59153 const decipher = new AES(Array.prototype.slice.call(key)); // byte and word-level access for the decrypted output 59154 59155 const decrypted = new Uint8Array(encrypted.byteLength); 59156 const decrypted32 = new Int32Array(decrypted.buffer); // temporary variables for working with the IV, encrypted, and 59157 // decrypted data 59158 59159 let init0; 59160 let init1; 59161 let init2; 59162 let init3; 59163 let encrypted0; 59164 let encrypted1; 59165 let encrypted2; 59166 let encrypted3; // iteration variable 59167 59168 let wordIx; // pull out the words of the IV to ensure we don't modify the 59169 // passed-in reference and easier access 59170 59171 init0 = initVector[0]; 59172 init1 = initVector[1]; 59173 init2 = initVector[2]; 59174 init3 = initVector[3]; // decrypt four word sequences, applying cipher-block chaining (CBC) 59175 // to each decrypted block 59176 59177 for (wordIx = 0; wordIx < encrypted32.length; wordIx += 4) { 59178 // convert big-endian (network order) words into little-endian 59179 // (javascript order) 59180 encrypted0 = ntoh(encrypted32[wordIx]); 59181 encrypted1 = ntoh(encrypted32[wordIx + 1]); 59182 encrypted2 = ntoh(encrypted32[wordIx + 2]); 59183 encrypted3 = ntoh(encrypted32[wordIx + 3]); // decrypt the block 59184 59185 decipher.decrypt(encrypted0, encrypted1, encrypted2, encrypted3, decrypted32, wordIx); // XOR with the IV, and restore network byte-order to obtain the 59186 // plaintext 59187 59188 decrypted32[wordIx] = ntoh(decrypted32[wordIx] ^ init0); 59189 decrypted32[wordIx + 1] = ntoh(decrypted32[wordIx + 1] ^ init1); 59190 decrypted32[wordIx + 2] = ntoh(decrypted32[wordIx + 2] ^ init2); 59191 decrypted32[wordIx + 3] = ntoh(decrypted32[wordIx + 3] ^ init3); // setup the IV for the next round 59192 59193 init0 = encrypted0; 59194 init1 = encrypted1; 59195 init2 = encrypted2; 59196 init3 = encrypted3; 59197 } 59198 return decrypted; 59199 }; 59200 /** 59201 * The `Decrypter` class that manages decryption of AES 59202 * data through `AsyncStream` objects and the `decrypt` 59203 * function 59204 * 59205 * @param {Uint8Array} encrypted the encrypted bytes 59206 * @param {Uint32Array} key the bytes of the decryption key 59207 * @param {Uint32Array} initVector the initialization vector (IV) to 59208 * @param {Function} done the function to run when done 59209 * @class Decrypter 59210 */ 59211 59212 class Decrypter { 59213 constructor(encrypted, key, initVector, done) { 59214 const step = Decrypter.STEP; 59215 const encrypted32 = new Int32Array(encrypted.buffer); 59216 const decrypted = new Uint8Array(encrypted.byteLength); 59217 let i = 0; 59218 this.asyncStream_ = new AsyncStream(); // split up the encryption job and do the individual chunks asynchronously 59219 59220 this.asyncStream_.push(this.decryptChunk_(encrypted32.subarray(i, i + step), key, initVector, decrypted)); 59221 for (i = step; i < encrypted32.length; i += step) { 59222 initVector = new Uint32Array([ntoh(encrypted32[i - 4]), ntoh(encrypted32[i - 3]), ntoh(encrypted32[i - 2]), ntoh(encrypted32[i - 1])]); 59223 this.asyncStream_.push(this.decryptChunk_(encrypted32.subarray(i, i + step), key, initVector, decrypted)); 59224 } // invoke the done() callback when everything is finished 59225 59226 this.asyncStream_.push(function () { 59227 // remove pkcs#7 padding from the decrypted bytes 59228 done(null, unpad(decrypted)); 59229 }); 59230 } 59231 /** 59232 * a getter for step the maximum number of bytes to process at one time 59233 * 59234 * @return {number} the value of step 32000 59235 */ 59236 59237 static get STEP() { 59238 // 4 * 8000; 59239 return 32000; 59240 } 59241 /** 59242 * @private 59243 */ 59244 59245 decryptChunk_(encrypted, key, initVector, decrypted) { 59246 return function () { 59247 const bytes = decrypt(encrypted, key, initVector); 59248 decrypted.set(bytes, encrypted.byteOffset); 59249 }; 59250 } 59251 } 59252 var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; 59253 var win; 59254 if (typeof window !== "undefined") { 59255 win = window; 59256 } else if (typeof commonjsGlobal !== "undefined") { 59257 win = commonjsGlobal; 59258 } else if (typeof self !== "undefined") { 59259 win = self; 59260 } else { 59261 win = {}; 59262 } 59263 var window_1 = win; 59264 var isArrayBufferView = function isArrayBufferView(obj) { 59265 if (ArrayBuffer.isView === 'function') { 59266 return ArrayBuffer.isView(obj); 59267 } 59268 return obj && obj.buffer instanceof ArrayBuffer; 59269 }; 59270 var BigInt = window_1.BigInt || Number; 59271 [BigInt('0x1'), BigInt('0x100'), BigInt('0x10000'), BigInt('0x1000000'), BigInt('0x100000000'), BigInt('0x10000000000'), BigInt('0x1000000000000'), BigInt('0x100000000000000'), BigInt('0x10000000000000000')]; 59272 (function () { 59273 var a = new Uint16Array([0xFFCC]); 59274 var b = new Uint8Array(a.buffer, a.byteOffset, a.byteLength); 59275 if (b[0] === 0xFF) { 59276 return 'big'; 59277 } 59278 if (b[0] === 0xCC) { 59279 return 'little'; 59280 } 59281 return 'unknown'; 59282 })(); 59283 /** 59284 * Creates an object for sending to a web worker modifying properties that are TypedArrays 59285 * into a new object with seperated properties for the buffer, byteOffset, and byteLength. 59286 * 59287 * @param {Object} message 59288 * Object of properties and values to send to the web worker 59289 * @return {Object} 59290 * Modified message with TypedArray values expanded 59291 * @function createTransferableMessage 59292 */ 59293 59294 const createTransferableMessage = function (message) { 59295 const transferable = {}; 59296 Object.keys(message).forEach(key => { 59297 const value = message[key]; 59298 if (isArrayBufferView(value)) { 59299 transferable[key] = { 59300 bytes: value.buffer, 59301 byteOffset: value.byteOffset, 59302 byteLength: value.byteLength 59303 }; 59304 } else { 59305 transferable[key] = value; 59306 } 59307 }); 59308 return transferable; 59309 }; 59310 /* global self */ 59311 59312 /** 59313 * Our web worker interface so that things can talk to aes-decrypter 59314 * that will be running in a web worker. the scope is passed to this by 59315 * webworkify. 59316 */ 59317 59318 self.onmessage = function (event) { 59319 const data = event.data; 59320 const encrypted = new Uint8Array(data.encrypted.bytes, data.encrypted.byteOffset, data.encrypted.byteLength); 59321 const key = new Uint32Array(data.key.bytes, data.key.byteOffset, data.key.byteLength / 4); 59322 const iv = new Uint32Array(data.iv.bytes, data.iv.byteOffset, data.iv.byteLength / 4); 59323 /* eslint-disable no-new, handle-callback-err */ 59324 59325 new Decrypter(encrypted, key, iv, function (err, bytes) { 59326 self.postMessage(createTransferableMessage({ 59327 source: data.source, 59328 decrypted: bytes 59329 }), [bytes.buffer]); 59330 }); 59331 /* eslint-enable */ 59332 }; 59333 }));
59334 59335 var Decrypter = factory(workerCode); 59336 /* rollup-plugin-worker-factory end for worker!/home/runner/work/http-streaming
vendor: 21,837 bytes, lines 59336-59940
59336/http-streaming/src/decrypter-worker.js */ 59337 59338 /** 59339 * Convert the properties of an HLS track into an audioTrackKind. 59340 * 59341 * @private 59342 */ 59343 59344 const audioTrackKind_ = properties => { 59345 let kind = properties.default ? 'main' : 'alternative'; 59346 if (properties.characteristics && properties.characteristics.indexOf('public.accessibility.describes-video') >= 0) { 59347 kind = 'main-desc'; 59348 } 59349 return kind; 59350 }; 59351 /** 59352 * Pause provided segment loader and playlist loader if active 59353 * 59354 * @param {SegmentLoader} segmentLoader 59355 * SegmentLoader to pause 59356 * @param {Object} mediaType 59357 * Active media type 59358 * @function stopLoaders 59359 */ 59360 59361 const stopLoaders = (segmentLoader, mediaType) => { 59362 segmentLoader.abort(); 59363 segmentLoader.pause(); 59364 if (mediaType && mediaType.activePlaylistLoader) { 59365 mediaType.activePlaylistLoader.pause(); 59366 mediaType.activePlaylistLoader = null; 59367 } 59368 }; 59369 /** 59370 * Start loading provided segment loader and playlist loader 59371 * 59372 * @param {PlaylistLoader} playlistLoader 59373 * PlaylistLoader to start loading 59374 * @param {Object} mediaType 59375 * Active media type 59376 * @function startLoaders 59377 */ 59378 59379 const startLoaders = (playlistLoader, mediaType) => { 59380 // Segment loader will be started after `loadedmetadata` or `loadedplaylist` from the 59381 // playlist loader 59382 mediaType.activePlaylistLoader = playlistLoader; 59383 playlistLoader.load(); 59384 }; 59385 /** 59386 * Returns a function to be called when the media group changes. It performs a 59387 * non-destructive (preserve the buffer) resync of the SegmentLoader. This is because a 59388 * change of group is merely a rendition switch of the same content at another encoding, 59389 * rather than a change of content, such as switching audio from English to Spanish. 59390 * 59391 * @param {string} type 59392 * MediaGroup type 59393 * @param {Object} settings 59394 * Object containing required information for media groups 59395 * @return {Function} 59396 * Handler for a non-destructive resync of SegmentLoader when the active media 59397 * group changes. 59398 * @function onGroupChanged 59399 */ 59400 59401 const onGroupChanged = (type, settings) => () => { 59402 const { 59403 segmentLoaders: { 59404 [type]: segmentLoader, 59405 main: mainSegmentLoader 59406 }, 59407 mediaTypes: { 59408 [type]: mediaType 59409 } 59410 } = settings; 59411 const activeTrack = mediaType.activeTrack(); 59412 const activeGroup = mediaType.getActiveGroup(); 59413 const previousActiveLoader = mediaType.activePlaylistLoader; 59414 const lastGroup = mediaType.lastGroup_; // the group did not change do nothing 59415 59416 if (activeGroup && lastGroup && activeGroup.id === lastGroup.id) { 59417 return; 59418 } 59419 mediaType.lastGroup_ = activeGroup; 59420 mediaType.lastTrack_ = activeTrack; 59421 stopLoaders(segmentLoader, mediaType); 59422 if (!activeGroup || activeGroup.isMainPlaylist) { 59423 // there is no group active or active group is a main playlist and won't change 59424 return; 59425 } 59426 if (!activeGroup.playlistLoader) { 59427 if (previousActiveLoader) { 59428 // The previous group had a playlist loader but the new active group does not 59429 // this means we are switching from demuxed to muxed audio. In this case we want to 59430 // do a destructive reset of the main segment loader and not restart the audio 59431 // loaders. 59432 mainSegmentLoader.resetEverything(); 59433 } 59434 return; 59435 } // Non-destructive resync 59436 59437 segmentLoader.resyncLoader(); 59438 startLoaders(activeGroup.playlistLoader, mediaType); 59439 }; 59440 const onGroupChanging = (type, settings) => () => { 59441 const { 59442 segmentLoaders: { 59443 [type]: segmentLoader 59444 }, 59445 mediaTypes: { 59446 [type]: mediaType 59447 } 59448 } = settings; 59449 mediaType.lastGroup_ = null; 59450 segmentLoader.abort(); 59451 segmentLoader.pause(); 59452 }; 59453 /** 59454 * Returns a function to be called when the media track changes. It performs a 59455 * destructive reset of the SegmentLoader to ensure we start loading as close to 59456 * currentTime as possible. 59457 * 59458 * @param {string} type 59459 * MediaGroup type 59460 * @param {Object} settings 59461 * Object containing required information for media groups 59462 * @return {Function} 59463 * Handler for a destructive reset of SegmentLoader when the active media 59464 * track changes. 59465 * @function onTrackChanged 59466 */ 59467 59468 const onTrackChanged = (type, settings) => () => { 59469 const { 59470 mainPlaylistLoader, 59471 segmentLoaders: { 59472 [type]: segmentLoader, 59473 main: mainSegmentLoader 59474 }, 59475 mediaTypes: { 59476 [type]: mediaType 59477 } 59478 } = settings; 59479 const activeTrack = mediaType.activeTrack(); 59480 const activeGroup = mediaType.getActiveGroup(); 59481 const previousActiveLoader = mediaType.activePlaylistLoader; 59482 const lastTrack = mediaType.lastTrack_; // track did not change, do nothing 59483 59484 if (lastTrack && activeTrack && lastTrack.id === activeTrack.id) { 59485 return; 59486 } 59487 mediaType.lastGroup_ = activeGroup; 59488 mediaType.lastTrack_ = activeTrack; 59489 stopLoaders(segmentLoader, mediaType); 59490 if (!activeGroup) { 59491 // there is no group active so we do not want to restart loaders 59492 return; 59493 } 59494 if (activeGroup.isMainPlaylist) { 59495 // track did not change, do nothing 59496 if (!activeTrack || !lastTrack || activeTrack.id === lastTrack.id) { 59497 return; 59498 } 59499 const pc = settings.vhs.playlistController_; 59500 const newPlaylist = pc.selectPlaylist(); // media will not change do nothing 59501 59502 if (pc.media() === newPlaylist) { 59503 return; 59504 } 59505 mediaType.logger_(`track change. Switching main audio from ${lastTrack.id} to ${activeTrack.id}`); 59506 mainPlaylistLoader.pause(); 59507 mainSegmentLoader.resetEverything(); 59508 pc.fastQualityChange_(newPlaylist); 59509 return; 59510 } 59511 if (type === 'AUDIO') { 59512 if (!activeGroup.playlistLoader) { 59513 // when switching from demuxed audio/video to muxed audio/video (noted by no 59514 // playlist loader for the audio group), we want to do a destructive reset of the 59515 // main segment loader and not restart the audio loaders 59516 mainSegmentLoader.setAudio(true); // don't have to worry about disabling the audio of the audio segment loader since 59517 // it should be stopped 59518 59519 mainSegmentLoader.resetEverything(); 59520 return; 59521 } // although the segment loader is an audio segment loader, call the setAudio 59522 // function to ensure it is prepared to re-append the init segment (or handle other 59523 // config changes) 59524 59525 segmentLoader.setAudio(true); 59526 mainSegmentLoader.setAudio(false); 59527 } 59528 if (previousActiveLoader === activeGroup.playlistLoader) { 59529 // Nothing has actually changed. This can happen because track change events can fire 59530 // multiple times for a "single" change. One for enabling the new active track, and 59531 // one for disabling the track that was active 59532 startLoaders(activeGroup.playlistLoader, mediaType); 59533 return; 59534 } 59535 if (segmentLoader.track) { 59536 // For WebVTT, set the new text track in the segmentloader 59537 segmentLoader.track(activeTrack); 59538 } // destructive reset 59539 59540 segmentLoader.resetEverything(); 59541 startLoaders(activeGroup.playlistLoader, mediaType); 59542 }; 59543 const onError = { 59544 /** 59545 * Returns a function to be called when a SegmentLoader or PlaylistLoader encounters 59546 * an error. 59547 * 59548 * @param {string} type 59549 * MediaGroup type 59550 * @param {Object} settings 59551 * Object containing required information for media groups 59552 * @return {Function} 59553 * Error handler. Logs warning (or error if the playlist is excluded) to 59554 * console and switches back to default audio track. 59555 * @function onError.AUDIO 59556 */ 59557 AUDIO: (type, settings) => () => { 59558 const { 59559 mediaTypes: { 59560 [type]: mediaType 59561 }, 59562 excludePlaylist 59563 } = settings; // switch back to default audio track 59564 59565 const activeTrack = mediaType.activeTrack(); 59566 const activeGroup = mediaType.activeGroup(); 59567 const id = (activeGroup.filter(group => group.default)[0] || activeGroup[0]).id; 59568 const defaultTrack = mediaType.tracks[id]; 59569 if (activeTrack === defaultTrack) { 59570 // Default track encountered an error. All we can do now is exclude the current 59571 // rendition and hope another will switch audio groups 59572 excludePlaylist({ 59573 error: { 59574 message: 'Problem encountered loading the default audio track.' 59575 } 59576 }); 59577 return; 59578 } 59579 videojs.log.warn('Problem encountered loading the alternate audio track.' + 'Switching back to default.'); 59580 for (const trackId in mediaType.tracks) { 59581 mediaType.tracks[trackId].enabled = mediaType.tracks[trackId] === defaultTrack; 59582 } 59583 mediaType.onTrackChanged(); 59584 }, 59585 /** 59586 * Returns a function to be called when a SegmentLoader or PlaylistLoader encounters 59587 * an error. 59588 * 59589 * @param {string} type 59590 * MediaGroup type 59591 * @param {Object} settings 59592 * Object containing required information for media groups 59593 * @return {Function} 59594 * Error handler. Logs warning to console and disables the active subtitle track 59595 * @function onError.SUBTITLES 59596 */ 59597 SUBTITLES: (type, settings) => () => { 59598 const { 59599 mediaTypes: { 59600 [type]: mediaType 59601 } 59602 } = settings; 59603 videojs.log.warn('Problem encountered loading the subtitle track.' + 'Disabling subtitle track.'); 59604 const track = mediaType.activeTrack(); 59605 if (track) { 59606 track.mode = 'disabled'; 59607 } 59608 mediaType.onTrackChanged(); 59609 } 59610 }; 59611 const setupListeners = { 59612 /** 59613 * Setup event listeners for audio playlist loader 59614 * 59615 * @param {string} type 59616 * MediaGroup type 59617 * @param {PlaylistLoader|null} playlistLoader 59618 * PlaylistLoader to register listeners on 59619 * @param {Object} settings 59620 * Object containing required information for media groups 59621 * @function setupListeners.AUDIO 59622 */ 59623 AUDIO: (type, playlistLoader, settings) => { 59624 if (!playlistLoader) { 59625 // no playlist loader means audio will be muxed with the video 59626 return; 59627 } 59628 const { 59629 tech, 59630 requestOptions, 59631 segmentLoaders: { 59632 [type]: segmentLoader 59633 } 59634 } = settings; 59635 playlistLoader.on('loadedmetadata', () => { 59636 const media = playlistLoader.media(); 59637 segmentLoader.playlist(media, requestOptions); // if the video is already playing, or if this isn't a live video and preload 59638 // permits, start downloading segments 59639 59640 if (!tech.paused() || media.endList && tech.preload() !== 'none') { 59641 segmentLoader.load(); 59642 } 59643 }); 59644 playlistLoader.on('loadedplaylist', () => { 59645 segmentLoader.playlist(playlistLoader.media(), requestOptions); // If the player isn't paused, ensure that the segment loader is running 59646 59647 if (!tech.paused()) { 59648 segmentLoader.load(); 59649 } 59650 }); 59651 playlistLoader.on('error', onError[type](type, settings)); 59652 }, 59653 /** 59654 * Setup event listeners for subtitle playlist loader 59655 * 59656 * @param {string} type 59657 * MediaGroup type 59658 * @param {PlaylistLoader|null} playlistLoader 59659 * PlaylistLoader to register listeners on 59660 * @param {Object} settings 59661 * Object containing required information for media groups 59662 * @function setupListeners.SUBTITLES 59663 */ 59664 SUBTITLES: (type, playlistLoader, settings) => { 59665 const { 59666 tech, 59667 requestOptions, 59668 segmentLoaders: { 59669 [type]: segmentLoader 59670 }, 59671 mediaTypes: { 59672 [type]: mediaType 59673 } 59674 } = settings; 59675 playlistLoader.on('loadedmetadata', () => { 59676 const media = playlistLoader.media(); 59677 segmentLoader.playlist(media, requestOptions); 59678 segmentLoader.track(mediaType.activeTrack()); // if the video is already playing, or if this isn't a live video and preload 59679 // permits, start downloading segments 59680 59681 if (!tech.paused() || media.endList && tech.preload() !== 'none') { 59682 segmentLoader.load(); 59683 } 59684 }); 59685 playlistLoader.on('loadedplaylist', () => { 59686 segmentLoader.playlist(playlistLoader.media(), requestOptions); // If the player isn't paused, ensure that the segment loader is running 59687 59688 if (!tech.paused()) { 59689 segmentLoader.load(); 59690 } 59691 }); 59692 playlistLoader.on('error', onError[type](type, settings)); 59693 } 59694 }; 59695 const initialize = { 59696 /** 59697 * Setup PlaylistLoaders and AudioTracks for the audio groups 59698 * 59699 * @param {string} type 59700 * MediaGroup type 59701 * @param {Object} settings 59702 * Object containing required information for media groups 59703 * @function initialize.AUDIO 59704 */ 59705 'AUDIO': (type, settings) => { 59706 const { 59707 vhs, 59708 sourceType, 59709 segmentLoaders: { 59710 [type]: segmentLoader 59711 }, 59712 requestOptions, 59713 main: { 59714 mediaGroups 59715 }, 59716 mediaTypes: { 59717 [type]: { 59718 groups, 59719 tracks, 59720 logger_ 59721 } 59722 }, 59723 mainPlaylistLoader 59724 } = settings; 59725 const audioOnlyMain = isAudioOnly(mainPlaylistLoader.main); // force a default if we have none 59726 59727 if (!mediaGroups[type] || Object.keys(mediaGroups[type]).length === 0) { 59728 mediaGroups[type] = { 59729 main: { 59730 default: { 59731 default: true 59732 } 59733 } 59734 }; 59735 if (audioOnlyMain) { 59736 mediaGroups[type].main.default.playlists = mainPlaylistLoader.main.playlists; 59737 } 59738 } 59739 for (const groupId in mediaGroups[type]) { 59740 if (!groups[groupId]) { 59741 groups[groupId] = []; 59742 } 59743 for (const variantLabel in mediaGroups[type][groupId]) { 59744 let properties = mediaGroups[type][groupId][variantLabel]; 59745 let playlistLoader; 59746 if (audioOnlyMain) { 59747 logger_(`AUDIO group '${groupId}' label '${variantLabel}' is a main playlist`); 59748 properties.isMainPlaylist = true; 59749 playlistLoader = null; // if vhs-json was provided as the source, and the media playlist was resolved, 59750 // use the resolved media playlist object 59751 } else if (sourceType === 'vhs-json' && properties.playlists) { 59752 playlistLoader = new PlaylistLoader(properties.playlists[0], vhs, requestOptions); 59753 } else if (properties.resolvedUri) { 59754 playlistLoader = new PlaylistLoader(properties.resolvedUri, vhs, requestOptions); // TODO: dash isn't the only type with properties.playlists 59755 // should we even have properties.playlists in this check. 59756 } else if (properties.playlists && sourceType === 'dash') { 59757 playlistLoader = new DashPlaylistLoader(properties.playlists[0], vhs, requestOptions, mainPlaylistLoader); 59758 } else { 59759 // no resolvedUri means the audio is muxed with the video when using this 59760 // audio track 59761 playlistLoader = null; 59762 } 59763 properties = merge({ 59764 id: variantLabel, 59765 playlistLoader 59766 }, properties); 59767 setupListeners[type](type, properties.playlistLoader, settings); 59768 groups[groupId].push(properties); 59769 if (typeof tracks[variantLabel] === 'undefined') { 59770 const track = new videojs.AudioTrack({ 59771 id: variantLabel, 59772 kind: audioTrackKind_(properties), 59773 enabled: false, 59774 language: properties.language, 59775 default: properties.default, 59776 label: variantLabel 59777 }); 59778 tracks[variantLabel] = track; 59779 } 59780 } 59781 } // setup single error event handler for the segment loader 59782 59783 segmentLoader.on('error', onError[type](type, settings)); 59784 }, 59785 /** 59786 * Setup PlaylistLoaders and TextTracks for the subtitle groups 59787 * 59788 * @param {string} type 59789 * MediaGroup type 59790 * @param {Object} settings 59791 * Object containing required information for media groups 59792 * @function initialize.SUBTITLES 59793 */ 59794 'SUBTITLES': (type, settings) => { 59795 const { 59796 tech, 59797 vhs, 59798 sourceType, 59799 segmentLoaders: { 59800 [type]: segmentLoader 59801 }, 59802 requestOptions, 59803 main: { 59804 mediaGroups 59805 }, 59806 mediaTypes: { 59807 [type]: { 59808 groups, 59809 tracks 59810 } 59811 }, 59812 mainPlaylistLoader 59813 } = settings; 59814 for (const groupId in mediaGroups[type]) { 59815 if (!groups[groupId]) { 59816 groups[groupId] = []; 59817 } 59818 for (const variantLabel in mediaGroups[type][groupId]) { 59819 if (!vhs.options_.useForcedSubtitles && mediaGroups[type][groupId][variantLabel].forced) { 59820 // Subtitle playlists with the forced attribute are not selectable in Safari. 59821 // According to Apple's HLS Authoring Specification: 59822 // If content has forced subtitles and regular subtitles in a given language, 59823 // the regular subtitles track in that language MUST contain both the forced 59824 // subtitles and the regular subtitles for that language. 59825 // Because of this requirement and that Safari does not add forced subtitles, 59826 // forced subtitles are skipped here to maintain consistent experience across 59827 // all platforms 59828 continue; 59829 } 59830 let properties = mediaGroups[type][groupId][variantLabel]; 59831 let playlistLoader; 59832 if (sourceType === 'hls') { 59833 playlistLoader = new PlaylistLoader(properties.resolvedUri, vhs, requestOptions); 59834 } else if (sourceType === 'dash') { 59835 const playlists = properties.playlists.filter(p => p.excludeUntil !== Infinity); 59836 if (!playlists.length) { 59837 return; 59838 } 59839 playlistLoader = new DashPlaylistLoader(properties.playlists[0], vhs, requestOptions, mainPlaylistLoader); 59840 } else if (sourceType === 'vhs-json') { 59841 playlistLoader = new PlaylistLoader( 59842 // if the vhs-json object included the media playlist, use the media playlist 59843 // as provided, otherwise use the resolved URI to load the playlist 59844 properties.playlists ? properties.playlists[0] : properties.resolvedUri, vhs, requestOptions); 59845 } 59846 properties = merge({ 59847 id: variantLabel, 59848 playlistLoader 59849 }, properties); 59850 setupListeners[type](type, properties.playlistLoader, settings); 59851 groups[groupId].push(properties); 59852 if (typeof tracks[variantLabel] === 'undefined') { 59853 const track = tech.addRemoteTextTrack({ 59854 id: variantLabel, 59855 kind: 'subtitles', 59856 default: properties.default && properties.autoselect, 59857 language: properties.language, 59858 label: variantLabel 59859 }, false).track; 59860 tracks[variantLabel] = track; 59861 } 59862 } 59863 } // setup single error event handler for the segment loader 59864 59865 segmentLoader.on('error', onError[type](type, settings)); 59866 }, 59867 /** 59868 * Setup TextTracks for the closed-caption groups 59869 * 59870 * @param {String} type 59871 * MediaGroup type 59872 * @param {Object} settings 59873 * Object containing required information for media groups 59874 * @function initialize['CLOSED-CAPTIONS'] 59875 */ 59876 'CLOSED-CAPTIONS': (type, settings) => { 59877 const { 59878 tech, 59879 main: { 59880 mediaGroups 59881 }, 59882 mediaTypes: { 59883 [type]: { 59884 groups, 59885 tracks 59886 } 59887 } 59888 } = settings; 59889 for (const groupId in mediaGroups[type]) { 59890 if (!groups[groupId]) { 59891 groups[groupId] = []; 59892 } 59893 for (const variantLabel in mediaGroups[type][groupId]) { 59894 const properties = mediaGroups[type][groupId][variantLabel]; // Look for either 608 (CCn) or 708 (SERVICEn) caption services 59895 59896 if (!/^(?:CC|SERVICE)/.test(properties.instreamId)) { 59897 continue; 59898 } 59899 const captionServices = tech.options_.vhs && tech.options_.vhs.captionServices || {}; 59900 let newProps = { 59901 label: variantLabel, 59902 language: properties.language, 59903 instreamId: properties.instreamId, 59904 default: properties.default && properties.autoselect 59905 }; 59906 if (captionServices[newProps.instreamId]) { 59907 newProps = merge(newProps, captionServices[newProps.instreamId]); 59908 } 59909 if (newProps.default === undefined) { 59910 delete newProps.default; 59911 } // No PlaylistLoader is required for Closed-Captions because the captions are 59912 // embedded within the video stream 59913 59914 groups[groupId].push(merge({ 59915 id: variantLabel 59916 }, properties)); 59917 if (typeof tracks[variantLabel] === 'undefined') { 59918 const track = tech.addRemoteTextTrack({ 59919 id: newProps.instreamId, 59920 kind: 'captions', 59921 default: newProps.default, 59922 language: newProps.language, 59923 label: newProps.label 59924 }, false).track; 59925 tracks[variantLabel] = track; 59926 } 59927 } 59928 } 59929 } 59930 }; 59931 const groupMatch = (list, media) => { 59932 for (let i = 0; i < list.length; i++) { 59933 if (playlistMatch(media, list[i])) { 59934 return true; 59935 } 59936 if (list[i].playlists && groupMatch(list[i].playlists, media)) { 59937 return true; 59938 } 59939 } 59940 return false;
vendor: 14,796 bytes, lines 59941-60339
59941 }; 59942 /** 59943 * Returns a function used to get the active group of the provided type 59944 * 59945 * @param {string} type 59946 * MediaGroup type 59947 * @param {Object} settings 59948 * Object containing required information for media groups 59949 * @return {Function} 59950 * Function that returns the active media group for the provided type. Takes an 59951 * optional parameter {TextTrack} track. If no track is provided, a list of all 59952 * variants in the group, otherwise the variant corresponding to the provided 59953 * track is returned. 59954 * @function activeGroup 59955 */ 59956 59957 const activeGroup = (type, settings) => track => { 59958 const { 59959 mainPlaylistLoader, 59960 mediaTypes: { 59961 [type]: { 59962 groups 59963 } 59964 } 59965 } = settings; 59966 const media = mainPlaylistLoader.media(); 59967 if (!media) { 59968 return null; 59969 } 59970 let variants = null; // set to variants to main media active group 59971 59972 if (media.attributes[type]) { 59973 variants = groups[media.attributes[type]]; 59974 } 59975 const groupKeys = Object.keys(groups); 59976 if (!variants) { 59977 // find the mainPlaylistLoader media 59978 // that is in a media group if we are dealing 59979 // with audio only 59980 if (type === 'AUDIO' && groupKeys.length > 1 && isAudioOnly(settings.main)) { 59981 for (let i = 0; i < groupKeys.length; i++) { 59982 const groupPropertyList = groups[groupKeys[i]]; 59983 if (groupMatch(groupPropertyList, media)) { 59984 variants = groupPropertyList; 59985 break; 59986 } 59987 } // use the main group if it exists 59988 } else if (groups.main) { 59989 variants = groups.main; // only one group, use that one 59990 } else if (groupKeys.length === 1) { 59991 variants = groups[groupKeys[0]]; 59992 } 59993 } 59994 if (typeof track === 'undefined') { 59995 return variants; 59996 } 59997 if (track === null || !variants) { 59998 // An active track was specified so a corresponding group is expected. track === null 59999 // means no track is currently active so there is no corresponding group 60000 return null; 60001 } 60002 return variants.filter(props => props.id === track.id)[0] || null; 60003 }; 60004 const activeTrack = { 60005 /** 60006 * Returns a function used to get the active track of type provided 60007 * 60008 * @param {string} type 60009 * MediaGroup type 60010 * @param {Object} settings 60011 * Object containing required information for media groups 60012 * @return {Function} 60013 * Function that returns the active media track for the provided type. Returns 60014 * null if no track is active 60015 * @function activeTrack.AUDIO 60016 */ 60017 AUDIO: (type, settings) => () => { 60018 const { 60019 mediaTypes: { 60020 [type]: { 60021 tracks 60022 } 60023 } 60024 } = settings; 60025 for (const id in tracks) { 60026 if (tracks[id].enabled) { 60027 return tracks[id]; 60028 } 60029 } 60030 return null; 60031 }, 60032 /** 60033 * Returns a function used to get the active track of type provided 60034 * 60035 * @param {string} type 60036 * MediaGroup type 60037 * @param {Object} settings 60038 * Object containing required information for media groups 60039 * @return {Function} 60040 * Function that returns the active media track for the provided type. Returns 60041 * null if no track is active 60042 * @function activeTrack.SUBTITLES 60043 */ 60044 SUBTITLES: (type, settings) => () => { 60045 const { 60046 mediaTypes: { 60047 [type]: { 60048 tracks 60049 } 60050 } 60051 } = settings; 60052 for (const id in tracks) { 60053 if (tracks[id].mode === 'showing' || tracks[id].mode === 'hidden') { 60054 return tracks[id]; 60055 } 60056 } 60057 return null; 60058 } 60059 }; 60060 const getActiveGroup = (type, { 60061 mediaTypes 60062 }) => () => { 60063 const activeTrack_ = mediaTypes[type].activeTrack(); 60064 if (!activeTrack_) { 60065 return null; 60066 } 60067 return mediaTypes[type].activeGroup(activeTrack_); 60068 }; 60069 /** 60070 * Setup PlaylistLoaders and Tracks for media groups (Audio, Subtitles, 60071 * Closed-Captions) specified in the main manifest. 60072 * 60073 * @param {Object} settings 60074 * Object containing required information for setting up the media groups 60075 * @param {Tech} settings.tech 60076 * The tech of the player 60077 * @param {Object} settings.requestOptions 60078 * XHR request options used by the segment loaders 60079 * @param {PlaylistLoader} settings.mainPlaylistLoader 60080 * PlaylistLoader for the main source 60081 * @param {VhsHandler} settings.vhs 60082 * VHS SourceHandler 60083 * @param {Object} settings.main 60084 * The parsed main manifest 60085 * @param {Object} settings.mediaTypes 60086 * Object to store the loaders, tracks, and utility methods for each media type 60087 * @param {Function} settings.excludePlaylist 60088 * Excludes the current rendition and forces a rendition switch. 60089 * @function setupMediaGroups 60090 */ 60091 60092 const setupMediaGroups = settings => { 60093 ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach(type => { 60094 initialize[type](type, settings); 60095 }); 60096 const { 60097 mediaTypes, 60098 mainPlaylistLoader, 60099 tech, 60100 vhs, 60101 segmentLoaders: { 60102 ['AUDIO']: audioSegmentLoader, 60103 main: mainSegmentLoader 60104 } 60105 } = settings; // setup active group and track getters and change event handlers 60106 60107 ['AUDIO', 'SUBTITLES'].forEach(type => { 60108 mediaTypes[type].activeGroup = activeGroup(type, settings); 60109 mediaTypes[type].activeTrack = activeTrack[type](type, settings); 60110 mediaTypes[type].onGroupChanged = onGroupChanged(type, settings); 60111 mediaTypes[type].onGroupChanging = onGroupChanging(type, settings); 60112 mediaTypes[type].onTrackChanged = onTrackChanged(type, settings); 60113 mediaTypes[type].getActiveGroup = getActiveGroup(type, settings); 60114 }); // DO NOT enable the default subtitle or caption track. 60115 // DO enable the default audio track 60116 60117 const audioGroup = mediaTypes.AUDIO.activeGroup(); 60118 if (audioGroup) { 60119 const groupId = (audioGroup.filter(group => group.default)[0] || audioGroup[0]).id; 60120 mediaTypes.AUDIO.tracks[groupId].enabled = true; 60121 mediaTypes.AUDIO.onGroupChanged(); 60122 mediaTypes.AUDIO.onTrackChanged(); 60123 const activeAudioGroup = mediaTypes.AUDIO.getActiveGroup(); // a similar check for handling setAudio on each loader is run again each time the 60124 // track is changed, but needs to be handled here since the track may not be considered 60125 // changed on the first call to onTrackChanged 60126 60127 if (!activeAudioGroup.playlistLoader) { 60128 // either audio is muxed with video or the stream is audio only 60129 mainSegmentLoader.setAudio(true); 60130 } else { 60131 // audio is demuxed 60132 mainSegmentLoader.setAudio(false); 60133 audioSegmentLoader.setAudio(true); 60134 } 60135 } 60136 mainPlaylistLoader.on('mediachange', () => { 60137 ['AUDIO', 'SUBTITLES'].forEach(type => mediaTypes[type].onGroupChanged()); 60138 }); 60139 mainPlaylistLoader.on('mediachanging', () => { 60140 ['AUDIO', 'SUBTITLES'].forEach(type => mediaTypes[type].onGroupChanging()); 60141 }); // custom audio track change event handler for usage event 60142 60143 const onAudioTrackChanged = () => { 60144 mediaTypes.AUDIO.onTrackChanged(); 60145 tech.trigger({ 60146 type: 'usage', 60147 name: 'vhs-audio-change' 60148 }); 60149 }; 60150 tech.audioTracks().addEventListener('change', onAudioTrackChanged); 60151 tech.remoteTextTracks().addEventListener('change', mediaTypes.SUBTITLES.onTrackChanged); 60152 vhs.on('dispose', () => { 60153 tech.audioTracks().removeEventListener('change', onAudioTrackChanged); 60154 tech.remoteTextTracks().removeEventListener('change', mediaTypes.SUBTITLES.onTrackChanged); 60155 }); // clear existing audio tracks and add the ones we just created 60156 60157 tech.clearTracks('audio'); 60158 for (const id in mediaTypes.AUDIO.tracks) { 60159 tech.audioTracks().addTrack(mediaTypes.AUDIO.tracks[id]); 60160 } 60161 }; 60162 /** 60163 * Creates skeleton object used to store the loaders, tracks, and utility methods for each 60164 * media type 60165 * 60166 * @return {Object} 60167 * Object to store the loaders, tracks, and utility methods for each media type 60168 * @function createMediaTypes 60169 */ 60170 60171 const createMediaTypes = () => { 60172 const mediaTypes = {}; 60173 ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach(type => { 60174 mediaTypes[type] = { 60175 groups: {}, 60176 tracks: {}, 60177 activePlaylistLoader: null, 60178 activeGroup: noop, 60179 activeTrack: noop, 60180 getActiveGroup: noop, 60181 onGroupChanged: noop, 60182 onTrackChanged: noop, 60183 lastTrack_: null, 60184 logger_: logger(`MediaGroups[${type}]`) 60185 }; 60186 }); 60187 return mediaTypes; 60188 }; 60189 60190 /** 60191 * A utility class for setting properties and maintaining the state of the content steering manifest. 60192 * 60193 * Content Steering manifest format: 60194 * VERSION: number (required) currently only version 1 is supported. 60195 * TTL: number in seconds (optional) until the next content steering manifest reload. 60196 * RELOAD-URI: string (optional) uri to fetch the next content steering manifest. 60197 * SERVICE-LOCATION-PRIORITY or PATHWAY-PRIORITY a non empty array of unique string values. 60198 * PATHWAY-CLONES: array (optional) (HLS only) pathway clone objects to copy from other playlists. 60199 */ 60200 60201 class SteeringManifest { 60202 constructor() { 60203 this.priority_ = []; 60204 this.pathwayClones_ = new Map(); 60205 } 60206 set version(number) { 60207 // Only version 1 is currently supported for both DASH and HLS. 60208 if (number === 1) { 60209 this.version_ = number; 60210 } 60211 } 60212 set ttl(seconds) { 60213 // TTL = time-to-live, default = 300 seconds. 60214 this.ttl_ = seconds || 300; 60215 } 60216 set reloadUri(uri) { 60217 if (uri) { 60218 // reload URI can be relative to the previous reloadUri. 60219 this.reloadUri_ = resolveUrl(this.reloadUri_, uri); 60220 } 60221 } 60222 set priority(array) { 60223 // priority must be non-empty and unique values. 60224 if (array && array.length) { 60225 this.priority_ = array; 60226 } 60227 } 60228 set pathwayClones(array) { 60229 // pathwayClones must be non-empty. 60230 if (array && array.length) { 60231 this.pathwayClones_ = new Map(array.map(clone => [clone.ID, clone])); 60232 } 60233 } 60234 get version() { 60235 return this.version_; 60236 } 60237 get ttl() { 60238 return this.ttl_; 60239 } 60240 get reloadUri() { 60241 return this.reloadUri_; 60242 } 60243 get priority() { 60244 return this.priority_; 60245 } 60246 get pathwayClones() { 60247 return this.pathwayClones_; 60248 } 60249 } 60250 /** 60251 * This class represents a content steering manifest and associated state. See both HLS and DASH specifications. 60252 * HLS: https://developer.apple.com/streaming/HLSContentSteeringSpecification.pdf and 60253 * https://datatracker.ietf.org/doc/draft-pantos-hls-rfc8216bis/ section 4.4.6.6. 60254 * DASH: https://dashif.org/docs/DASH-IF-CTS-00XX-Content-Steering-Community-Review.pdf 60255 * 60256 * @param {function} xhr for making a network request from the browser. 60257 * @param {function} bandwidth for fetching the current bandwidth from the main segment loader. 60258 */ 60259 60260 class ContentSteeringController extends videojs.EventTarget { 60261 constructor(xhr, bandwidth) { 60262 super(); 60263 this.currentPathway = null; 60264 this.defaultPathway = null; 60265 this.queryBeforeStart = false; 60266 this.availablePathways_ = new Set(); 60267 this.steeringManifest = new SteeringManifest(); 60268 this.proxyServerUrl_ = null; 60269 this.manifestType_ = null; 60270 this.ttlTimeout_ = null; 60271 this.request_ = null; 60272 this.currentPathwayClones = new Map(); 60273 this.nextPathwayClones = new Map(); 60274 this.excludedSteeringManifestURLs = new Set(); 60275 this.logger_ = logger('Content Steering'); 60276 this.xhr_ = xhr; 60277 this.getBandwidth_ = bandwidth; 60278 } 60279 /** 60280 * Assigns the content steering tag properties to the steering controller 60281 * 60282 * @param {string} baseUrl the baseURL from the main manifest for resolving the steering manifest url 60283 * @param {Object} steeringTag the content steering tag from the main manifest 60284 */ 60285 60286 assignTagProperties(baseUrl, steeringTag) { 60287 this.manifestType_ = steeringTag.serverUri ? 'HLS' : 'DASH'; // serverUri is HLS serverURL is DASH 60288 60289 const steeringUri = steeringTag.serverUri || steeringTag.serverURL; 60290 if (!steeringUri) { 60291 this.logger_(`steering manifest URL is ${steeringUri}, cannot request steering manifest.`); 60292 this.trigger('error'); 60293 return; 60294 } // Content steering manifests can be encoded as a data URI. We can decode, parse and return early if that's the case. 60295 60296 if (steeringUri.startsWith('data:')) { 60297 this.decodeDataUriManifest_(steeringUri.substring(steeringUri.indexOf(',') + 1)); 60298 return; 60299 } // reloadUri is the resolution of the main manifest URL and steering URL. 60300 60301 this.steeringManifest.reloadUri = resolveUrl(baseUrl, steeringUri); // pathwayId is HLS defaultServiceLocation is DASH 60302 60303 this.defaultPathway = steeringTag.pathwayId || steeringTag.defaultServiceLocation; // currently only DASH supports the following properties on <ContentSteering> tags. 60304 60305 this.queryBeforeStart = steeringTag.queryBeforeStart; 60306 this.proxyServerUrl_ = steeringTag.proxyServerURL; // trigger a steering event if we have a pathway from the content steering tag. 60307 // this tells VHS which segment pathway to start with. 60308 // If queryBeforeStart is true we need to wait for the steering manifest response. 60309 60310 if (this.defaultPathway && !this.queryBeforeStart) { 60311 this.trigger('content-steering'); 60312 } 60313 } 60314 /** 60315 * Requests the content steering manifest and parse the response. This should only be called after 60316 * assignTagProperties was called with a content steering tag. 60317 * 60318 * @param {string} initialUri The optional uri to make the request with. 60319 * If set, the request should be made with exactly what is passed in this variable. 60320 * This scenario should only happen once on initalization. 60321 */ 60322 60323 requestSteeringManifest(initial) { 60324 const reloadUri = this.steeringManifest.reloadUri; 60325 if (!reloadUri) { 60326 return; 60327 } // We currently don't support passing MPD query parameters directly to the content steering URL as this requires 60328 // ExtUrlQueryInfo tag support. See the DASH content steering spec section 8.1. 60329 // This request URI accounts for manifest URIs that have been excluded. 60330 60331 const uri = initial ? reloadUri : this.getRequestURI(reloadUri); // If there are no valid manifest URIs, we should stop content steering. 60332 60333 if (!uri) { 60334 this.logger_('No valid content steering manifest URIs. Stopping content steering.'); 60335 this.trigger('error'); 60336 this.dispose(); 60337 return; 60338 } 60339
60339this.request_ = this.xhr_({ 60340 uri 60341 }, (error, errorInfo) => { 60342 if (error) { 60343 // If the client receives HTTP 410 Gone in response to a manifest request, 60344 // it MUST NOT issue another request for that URI for the remainder of the 60345 // playback session. It MAY continue to use the most-recently obtained set 60346 // of Pathways. 60347 if (errorInfo.status === 410) { 60348 this.logger_(`manifest request 410 ${error}.`); 60349 this.logger_(`There will be no more content steering requests to ${uri} this session.`); 60350 this.excludedSteeringManifestURLs.add(uri); 60351 return; 60352 } // If the client receives HTTP 429 Too Many Requests with a Retry-After 60353 // header in response to a manifest request, it SHOULD wait until the time 60354 // specified by the Retry-After header to reissue the request. 60355 60356 if (errorInfo.status === 429) { 60357 const retrySeconds = errorInfo.responseHeaders['retry-after']; 60358 this.logger_(`manifest request 429 ${error}.`); 60359 this.logger_(`content steering will retry in ${retrySeconds} seconds.`); 60360 this.startTTLTimeout_(parseInt(retrySeconds, 10)); 60361 return; 60362 } // If the Steering Manifest cannot be loaded and parsed correctly, the 60363 // client SHOULD continue to use the previous values and attempt to reload 60364 // it after waiting for the previously-specified TTL (or 5 minutes if 60365 // none). 60366 60367 this.logger_(`manifest failed to load ${error}.`); 60368 this.startTTLTimeout_(); 60369 return; 60370 } 60371 const steeringManifestJson = JSON.parse(this.request_.responseText); 60372 this.assignSteeringProperties_(steeringManifestJson
vendor: 8,265 bytes, lines 60372-60598
60372); 60373 this.startTTLTimeout_(); 60374 }); 60375 } 60376 /** 60377 * Set the proxy server URL and add the steering manifest url as a URI encoded parameter. 60378 * 60379 * @param {string} steeringUrl the steering manifest url 60380 * @return the steering manifest url to a proxy server with all parameters set 60381 */ 60382 60383 setProxyServerUrl_(steeringUrl) { 60384 const steeringUrlObject = new window.URL(steeringUrl); 60385 const proxyServerUrlObject = new window.URL(this.proxyServerUrl_); 60386 proxyServerUrlObject.searchParams.set('url', encodeURI(steeringUrlObject.toString())); 60387 return this.setSteeringParams_(proxyServerUrlObject.toString()); 60388 } 60389 /** 60390 * Decodes and parses the data uri encoded steering manifest 60391 * 60392 * @param {string} dataUri the data uri to be decoded and parsed. 60393 */ 60394 60395 decodeDataUriManifest_(dataUri) { 60396 const steeringManifestJson = JSON.parse(window.atob(dataUri)); 60397 this.assignSteeringProperties_(steeringManifestJson); 60398 } 60399 /** 60400 * Set the HLS or DASH content steering manifest request query parameters. For example: 60401 * _HLS_pathway="<CURRENT-PATHWAY-ID>" and _HLS_throughput=<THROUGHPUT> 60402 * _DASH_pathway and _DASH_throughput 60403 * 60404 * @param {string} uri to add content steering server parameters to. 60405 * @return a new uri as a string with the added steering query parameters. 60406 */ 60407 60408 setSteeringParams_(url) { 60409 const urlObject = new window.URL(url); 60410 const path = this.getPathway(); 60411 const networkThroughput = this.getBandwidth_(); 60412 if (path) { 60413 const pathwayKey = `_${this.manifestType_}_pathway`; 60414 urlObject.searchParams.set(pathwayKey, path); 60415 } 60416 if (networkThroughput) { 60417 const throughputKey = `_${this.manifestType_}_throughput`; 60418 urlObject.searchParams.set(throughputKey, networkThroughput); 60419 } 60420 return urlObject.toString(); 60421 } 60422 /** 60423 * Assigns the current steering manifest properties and to the SteeringManifest object 60424 * 60425 * @param {Object} steeringJson the raw JSON steering manifest 60426 */ 60427 60428 assignSteeringProperties_(steeringJson) { 60429 this.steeringManifest.version = steeringJson.VERSION; 60430 if (!this.steeringManifest.version) { 60431 this.logger_(`manifest version is ${steeringJson.VERSION}, which is not supported.`); 60432 this.trigger('error'); 60433 return; 60434 } 60435 this.steeringManifest.ttl = steeringJson.TTL; 60436 this.steeringManifest.reloadUri = steeringJson['RELOAD-URI']; // HLS = PATHWAY-PRIORITY required. DASH = SERVICE-LOCATION-PRIORITY optional 60437 60438 this.steeringManifest.priority = steeringJson['PATHWAY-PRIORITY'] || steeringJson['SERVICE-LOCATION-PRIORITY']; // Pathway clones to be created/updated in HLS. 60439 // See section 7.2 https://datatracker.ietf.org/doc/draft-pantos-hls-rfc8216bis/ 60440 60441 this.steeringManifest.pathwayClones = steeringJson['PATHWAY-CLONES']; 60442 this.nextPathwayClones = this.steeringManifest.pathwayClones; // 1. apply first pathway from the array. 60443 // 2. if first pathway doesn't exist in manifest, try next pathway. 60444 // a. if all pathways are exhausted, ignore the steering manifest priority. 60445 // 3. if segments fail from an established pathway, try all variants/renditions, then exclude the failed pathway. 60446 // a. exclude a pathway for a minimum of the last TTL duration. Meaning, from the next steering response, 60447 // the excluded pathway will be ignored. 60448 // See excludePathway usage in excludePlaylist(). 60449 // If there are no available pathways, we need to stop content steering. 60450 60451 if (!this.availablePathways_.size) { 60452 this.logger_('There are no available pathways for content steering. Ending content steering.'); 60453 this.trigger('error'); 60454 this.dispose(); 60455 } 60456 const chooseNextPathway = pathwaysByPriority => { 60457 for (const path of pathwaysByPriority) { 60458 if (this.availablePathways_.has(path)) { 60459 return path; 60460 } 60461 } // If no pathway matches, ignore the manifest and choose the first available. 60462 60463 return [...this.availablePathways_][0]; 60464 }; 60465 const nextPathway = chooseNextPathway(this.steeringManifest.priority); 60466 if (this.currentPathway !== nextPathway) { 60467 this.currentPathway = nextPathway; 60468 this.trigger('content-steering'); 60469 } 60470 } 60471 /** 60472 * Returns the pathway to use for steering decisions 60473 * 60474 * @return {string} returns the current pathway or the default 60475 */ 60476 60477 getPathway() { 60478 return this.currentPathway || this.defaultPathway; 60479 } 60480 /** 60481 * Chooses the manifest request URI based on proxy URIs and server URLs. 60482 * Also accounts for exclusion on certain manifest URIs. 60483 * 60484 * @param {string} reloadUri the base uri before parameters 60485 * 60486 * @return {string} the final URI for the request to the manifest server. 60487 */ 60488 60489 getRequestURI(reloadUri) { 60490 if (!reloadUri) { 60491 return null; 60492 } 60493 const isExcluded = uri => this.excludedSteeringManifestURLs.has(uri); 60494 if (this.proxyServerUrl_) { 60495 const proxyURI = this.setProxyServerUrl_(reloadUri); 60496 if (!isExcluded(proxyURI)) { 60497 return proxyURI; 60498 } 60499 } 60500 const steeringURI = this.setSteeringParams_(reloadUri); 60501 if (!isExcluded(steeringURI)) { 60502 return steeringURI; 60503 } // Return nothing if all valid manifest URIs are excluded. 60504 60505 return null; 60506 } 60507 /** 60508 * Start the timeout for re-requesting the steering manifest at the TTL interval. 60509 * 60510 * @param {number} ttl time in seconds of the timeout. Defaults to the 60511 * ttl interval in the steering manifest 60512 */ 60513 60514 startTTLTimeout_(ttl = this.steeringManifest.ttl) { 60515 // 300 (5 minutes) is the default value. 60516 const ttlMS = ttl * 1000; 60517 this.ttlTimeout_ = window.setTimeout(() => { 60518 this.requestSteeringManifest(); 60519 }, ttlMS); 60520 } 60521 /** 60522 * Clear the TTL timeout if necessary. 60523 */ 60524 60525 clearTTLTimeout_() { 60526 window.clearTimeout(this.ttlTimeout_); 60527 this.ttlTimeout_ = null; 60528 } 60529 /** 60530 * aborts any current steering xhr and sets the current request object to null 60531 */ 60532 60533 abort() { 60534 if (this.request_) { 60535 this.request_.abort(); 60536 } 60537 this.request_ = null; 60538 } 60539 /** 60540 * aborts steering requests clears the ttl timeout and resets all properties. 60541 */ 60542 60543 dispose() { 60544 this.off('content-steering'); 60545 this.off('error'); 60546 this.abort(); 60547 this.clearTTLTimeout_(); 60548 this.currentPathway = null; 60549 this.defaultPathway = null; 60550 this.queryBeforeStart = null; 60551 this.proxyServerUrl_ = null; 60552 this.manifestType_ = null; 60553 this.ttlTimeout_ = null; 60554 this.request_ = null; 60555 this.excludedSteeringManifestURLs = new Set(); 60556 this.availablePathways_ = new Set(); 60557 this.steeringManifest = new SteeringManifest(); 60558 } 60559 /** 60560 * adds a pathway to the available pathways set 60561 * 60562 * @param {string} pathway the pathway string to add 60563 */ 60564 60565 addAvailablePathway(pathway) { 60566 if (pathway) { 60567 this.availablePathways_.add(pathway); 60568 } 60569 } 60570 /** 60571 * Clears all pathways from the available pathways set 60572 */ 60573 60574 clearAvailablePathways() { 60575 this.availablePathways_.clear(); 60576 } 60577 /** 60578 * Removes a pathway from the available pathways set. 60579 */ 60580 60581 excludePathway(pathway) { 60582 return this.availablePathways_.delete(pathway); 60583 } 60584 /** 60585 * Checks the refreshed DASH manifest content steering tag for changes. 60586 * 60587 * @param {string} baseURL new steering tag on DASH manifest refresh 60588 * @param {Object} newTag the new tag to check for changes 60589 * @return a true or false whether the new tag has different values 60590 */ 60591 60592 didDASHTagChange(baseURL, newTag) { 60593 return !newTag && this.steeringManifest.reloadUri || newTag && (resolveUrl(baseURL, newTag.serverURL) !== this.steeringManifest.reloadUri || newTag.defaultServiceLocation !== this.defaultPathway || newTag.queryBeforeStart !== this.queryBeforeStart || newTag.proxyServerURL !== this.proxyServerUrl_); 60594 } 60595 getAvailablePathways() { 60596 return this.availablePathways_; 60597 } 60598 }
vendor: 6,830 bytes, lines 60599-60760
60599 60600 /** 60601 * @file playlist-controller.js 60602 */ 60603 const ABORT_EARLY_EXCLUSION_SECONDS = 10; 60604 let Vhs$1; // SegmentLoader stats that need to have each loader's 60605 // values summed to calculate the final value 60606 60607 const loaderStats = ['mediaRequests', 'mediaRequestsAborted', 'mediaRequestsTimedout', 'mediaRequestsErrored', 'mediaTransferDuration', 'mediaBytesTransferred', 'mediaAppends']; 60608 const sumLoaderStat = function (stat) { 60609 return this.audioSegmentLoader_[stat] + this.mainSegmentLoader_[stat]; 60610 }; 60611 const shouldSwitchToMedia = function ({ 60612 currentPlaylist, 60613 buffered, 60614 currentTime, 60615 nextPlaylist, 60616 bufferLowWaterLine, 60617 bufferHighWaterLine, 60618 duration, 60619 bufferBasedABR, 60620 log 60621 }) { 60622 // we have no other playlist to switch to 60623 if (!nextPlaylist) { 60624 videojs.log.warn('We received no playlist to switch to. Please check your stream.'); 60625 return false; 60626 } 60627 const sharedLogLine = `allowing switch ${currentPlaylist && currentPlaylist.id || 'null'} -> ${nextPlaylist.id}`; 60628 if (!currentPlaylist) { 60629 log(`${sharedLogLine} as current playlist is not set`); 60630 return true; 60631 } // no need to switch if playlist is the same 60632 60633 if (nextPlaylist.id === currentPlaylist.id) { 60634 return false; 60635 } // determine if current time is in a buffered range. 60636 60637 const isBuffered = Boolean(findRange(buffered, currentTime).length); // If the playlist is live, then we want to not take low water line into account. 60638 // This is because in LIVE, the player plays 3 segments from the end of the 60639 // playlist, and if `BUFFER_LOW_WATER_LINE` is greater than the duration availble 60640 // in those segments, a viewer will never experience a rendition upswitch. 60641 60642 if (!currentPlaylist.endList) { 60643 // For LLHLS live streams, don't switch renditions before playback has started, as it almost 60644 // doubles the time to first playback. 60645 if (!isBuffered && typeof currentPlaylist.partTargetDuration === 'number') { 60646 log(`not ${sharedLogLine} as current playlist is live llhls, but currentTime isn't in buffered.`); 60647 return false; 60648 } 60649 log(`${sharedLogLine} as current playlist is live`); 60650 return true; 60651 } 60652 const forwardBuffer = timeAheadOf(buffered, currentTime); 60653 const maxBufferLowWaterLine = bufferBasedABR ? Config.EXPERIMENTAL_MAX_BUFFER_LOW_WATER_LINE : Config.MAX_BUFFER_LOW_WATER_LINE; // For the same reason as LIVE, we ignore the low water line when the VOD 60654 // duration is below the max potential low water line 60655 60656 if (duration < maxBufferLowWaterLine) { 60657 log(`${sharedLogLine} as duration < max low water line (${duration} < ${maxBufferLowWaterLine})`); 60658 return true; 60659 } 60660 const nextBandwidth = nextPlaylist.attributes.BANDWIDTH; 60661 const currBandwidth = currentPlaylist.attributes.BANDWIDTH; // when switching down, if our buffer is lower than the high water line, 60662 // we can switch down 60663 60664 if (nextBandwidth < currBandwidth && (!bufferBasedABR || forwardBuffer < bufferHighWaterLine)) { 60665 let logLine = `${sharedLogLine} as next bandwidth < current bandwidth (${nextBandwidth} < ${currBandwidth})`; 60666 if (bufferBasedABR) { 60667 logLine += ` and forwardBuffer < bufferHighWaterLine (${forwardBuffer} < ${bufferHighWaterLine})`; 60668 } 60669 log(logLine); 60670 return true; 60671 } // and if our buffer is higher than the low water line, 60672 // we can switch up 60673 60674 if ((!bufferBasedABR || nextBandwidth > currBandwidth) && forwardBuffer >= bufferLowWaterLine) { 60675 let logLine = `${sharedLogLine} as forwardBuffer >= bufferLowWaterLine (${forwardBuffer} >= ${bufferLowWaterLine})`; 60676 if (bufferBasedABR) { 60677 logLine += ` and next bandwidth > current bandwidth (${nextBandwidth} > ${currBandwidth})`; 60678 } 60679 log(logLine); 60680 return true; 60681 } 60682 log(`not ${sharedLogLine} as no switching criteria met`); 60683 return false; 60684 }; 60685 /** 60686 * the main playlist controller controller all interactons 60687 * between playlists and segmentloaders. At this time this mainly 60688 * involves a main playlist and a series of audio playlists 60689 * if they are available 60690 * 60691 * @class PlaylistController 60692 * @extends videojs.EventTarget 60693 */ 60694 60695 class PlaylistController extends videojs.EventTarget { 60696 constructor(options) { 60697 super(); 60698 const { 60699 src, 60700 withCredentials, 60701 tech, 60702 bandwidth, 60703 externVhs, 60704 useCueTags, 60705 playlistExclusionDuration, 60706 enableLowInitialPlaylist, 60707 sourceType, 60708 cacheEncryptionKeys, 60709 bufferBasedABR, 60710 leastPixelDiffSelector, 60711 captionServices 60712 } = options; 60713 if (!src) { 60714 throw new Error('A non-empty playlist URL or JSON manifest string is required'); 60715 } 60716 let { 60717 maxPlaylistRetries 60718 } = options; 60719 if (maxPlaylistRetries === null || typeof maxPlaylistRetries === 'undefined') { 60720 maxPlaylistRetries = Infinity; 60721 } 60722 Vhs$1 = externVhs; 60723 this.bufferBasedABR = Boolean(bufferBasedABR); 60724 this.leastPixelDiffSelector = Boolean(leastPixelDiffSelector); 60725 this.withCredentials = withCredentials; 60726 this.tech_ = tech; 60727 this.vhs_ = tech.vhs; 60728 this.sourceType_ = sourceType; 60729 this.useCueTags_ = useCueTags; 60730 this.playlistExclusionDuration = playlistExclusionDuration; 60731 this.maxPlaylistRetries = maxPlaylistRetries; 60732 this.enableLowInitialPlaylist = enableLowInitialPlaylist; 60733 if (this.useCueTags_) { 60734 this.cueTagsTrack_ = this.tech_.addTextTrack('metadata', 'ad-cues'); 60735 this.cueTagsTrack_.inBandMetadataTrackDispatchType = ''; 60736 } 60737 this.requestOptions_ = { 60738 withCredentials, 60739 maxPlaylistRetries, 60740 timeout: null 60741 }; 60742 this.on('error', this.pauseLoading); 60743 this.mediaTypes_ = createMediaTypes(); 60744 this.mediaSource = new window.MediaSource(); 60745 this.handleDurationChange_ = this.handleDurationChange_.bind(this); 60746 this.handleSourceOpen_ = this.handleSourceOpen_.bind(this); 60747 this.handleSourceEnded_ = this.handleSourceEnded_.bind(this); 60748 this.mediaSource.addEventListener('durationchange', this.handleDurationChange_); // load the media source into the player 60749 60750 this.mediaSource.addEventListener('sourceopen', this.handleSourceOpen_); 60751 this.mediaSource.addEventListener('sourceended', this.handleSourceEnded_); // we don't have to handle sourceclose since dispose will handle termination of 60752 // everything, and the MediaSource should not be detached without a proper disposal 60753 60754 this.seekable_ = createTimeRanges(); 60755 this.hasPlayed_ = false; 60756 this.syncController_ = new SyncController(options); 60757 this.segmentMetadataTrack_ = tech.addRemoteTextTrack({ 60758 kind: 'metadata', 60759 label: 'segment-metadata' 60760 }, false).track
vendor: 6,152 bytes, lines 60760-60909
60760; 60761 this.decrypter_ = new Decrypter(); 60762 this.sourceUpdater_ = new SourceUpdater(this.mediaSource); 60763 this.inbandTextTracks_ = {}; 60764 this.timelineChangeController_ = new TimelineChangeController(); 60765 this.keyStatusMap_ = new Map(); 60766 const segmentLoaderSettings = { 60767 vhs: this.vhs_, 60768 parse708captions: options.parse708captions, 60769 useDtsForTimestampOffset: options.useDtsForTimestampOffset, 60770 captionServices, 60771 mediaSource: this.mediaSource, 60772 currentTime: this.tech_.currentTime.bind(this.tech_), 60773 seekable: () => this.seekable(), 60774 seeking: () => this.tech_.seeking(), 60775 duration: () => this.duration(), 60776 hasPlayed: () => this.hasPlayed_, 60777 goalBufferLength: () => this.goalBufferLength(), 60778 bandwidth, 60779 syncController: this.syncController_, 60780 decrypter: this.decrypter_, 60781 sourceType: this.sourceType_, 60782 inbandTextTracks: this.inbandTextTracks_, 60783 cacheEncryptionKeys, 60784 sourceUpdater: this.sourceUpdater_, 60785 timelineChangeController: this.timelineChangeController_, 60786 exactManifestTimings: options.exactManifestTimings, 60787 addMetadataToTextTrack: this.addMetadataToTextTrack.bind(this) 60788 }; // The source type check not only determines whether a special DASH playlist loader 60789 // should be used, but also covers the case where the provided src is a vhs-json 60790 // manifest object (instead of a URL). In the case of vhs-json, the default 60791 // PlaylistLoader should be used. 60792 60793 this.mainPlaylistLoader_ = this.sourceType_ === 'dash' ? new DashPlaylistLoader(src, this.vhs_, merge(this.requestOptions_, { 60794 addMetadataToTextTrack: this.addMetadataToTextTrack.bind(this) 60795 })) : new PlaylistLoader(src, this.vhs_, merge(this.requestOptions_, { 60796 addDateRangesToTextTrack: this.addDateRangesToTextTrack_.bind(this) 60797 })); 60798 this.setupMainPlaylistLoaderListeners_(); // setup segment loaders 60799 // combined audio/video or just video when alternate audio track is selected 60800 60801 this.mainSegmentLoader_ = new SegmentLoader(merge(segmentLoaderSettings, { 60802 segmentMetadataTrack: this.segmentMetadataTrack_, 60803 loaderType: 'main' 60804 }), options); // alternate audio track 60805 60806 this.audioSegmentLoader_ = new SegmentLoader(merge(segmentLoaderSettings, { 60807 loaderType: 'audio' 60808 }), options); 60809 this.subtitleSegmentLoader_ = new VTTSegmentLoader(merge(segmentLoaderSettings, { 60810 loaderType: 'vtt', 60811 featuresNativeTextTracks: this.tech_.featuresNativeTextTracks, 60812 loadVttJs: () => new Promise((resolve, reject) => { 60813 function onLoad() { 60814 tech.off('vttjserror', onError); 60815 resolve(); 60816 } 60817 function onError() { 60818 tech.off('vttjsloaded', onLoad); 60819 reject(); 60820 } 60821 tech.one('vttjsloaded', onLoad); 60822 tech.one('vttjserror', onError); // safe to call multiple times, script will be loaded only once: 60823 60824 tech.addWebVttScript_(); 60825 }) 60826 }), options); 60827 const getBandwidth = () => { 60828 return this.mainSegmentLoader_.bandwidth; 60829 }; 60830 this.contentSteeringController_ = new ContentSteeringController(this.vhs_.xhr, getBandwidth); 60831 this.setupSegmentLoaderListeners_(); 60832 if (this.bufferBasedABR) { 60833 this.mainPlaylistLoader_.one('loadedplaylist', () => this.startABRTimer_()); 60834 this.tech_.on('pause', () => this.stopABRTimer_()); 60835 this.tech_.on('play', () => this.startABRTimer_()); 60836 } // Create SegmentLoader stat-getters 60837 // mediaRequests_ 60838 // mediaRequestsAborted_ 60839 // mediaRequestsTimedout_ 60840 // mediaRequestsErrored_ 60841 // mediaTransferDuration_ 60842 // mediaBytesTransferred_ 60843 // mediaAppends_ 60844 60845 loaderStats.forEach(stat => { 60846 this[stat + '_'] = sumLoaderStat.bind(this, stat); 60847 }); 60848 this.logger_ = logger('pc'); 60849 this.triggeredFmp4Usage = false; 60850 if (this.tech_.preload() === 'none') { 60851 this.loadOnPlay_ = () => { 60852 this.loadOnPlay_ = null; 60853 this.mainPlaylistLoader_.load(); 60854 }; 60855 this.tech_.one('play', this.loadOnPlay_); 60856 } else { 60857 this.mainPlaylistLoader_.load(); 60858 } 60859 this.timeToLoadedData__ = -1; 60860 this.mainAppendsToLoadedData__ = -1; 60861 this.audioAppendsToLoadedData__ = -1; 60862 const event = this.tech_.preload() === 'none' ? 'play' : 'loadstart'; // start the first frame timer on loadstart or play (for preload none) 60863 60864 this.tech_.one(event, () => { 60865 const timeToLoadedDataStart = Date.now(); 60866 this.tech_.one('loadeddata', () => { 60867 this.timeToLoadedData__ = Date.now() - timeToLoadedDataStart; 60868 this.mainAppendsToLoadedData__ = this.mainSegmentLoader_.mediaAppends; 60869 this.audioAppendsToLoadedData__ = this.audioSegmentLoader_.mediaAppends; 60870 }); 60871 }); 60872 } 60873 mainAppendsToLoadedData_() { 60874 return this.mainAppendsToLoadedData__; 60875 } 60876 audioAppendsToLoadedData_() { 60877 return this.audioAppendsToLoadedData__; 60878 } 60879 appendsToLoadedData_() { 60880 const main = this.mainAppendsToLoadedData_(); 60881 const audio = this.audioAppendsToLoadedData_(); 60882 if (main === -1 || audio === -1) { 60883 return -1; 60884 } 60885 return main + audio; 60886 } 60887 timeToLoadedData_() { 60888 return this.timeToLoadedData__; 60889 } 60890 /** 60891 * Run selectPlaylist and switch to the new playlist if we should 60892 * 60893 * @param {string} [reason=abr] a reason for why the ABR check is made 60894 * @private 60895 */ 60896 60897 checkABR_(reason = 'abr') { 60898 const nextPlaylist = this.selectPlaylist(); 60899 if (nextPlaylist && this.shouldSwitchToMedia_(nextPlaylist)) { 60900 this.switchMedia_(nextPlaylist, reason); 60901 } 60902 } 60903 switchMedia_(playlist, cause, delay) { 60904 const oldMedia = this.media(); 60905 const oldId = oldMedia && (oldMedia.id || oldMedia.uri); 60906 const newId = playlist && (playlist.id || playlist.uri); 60907 if (oldId && oldId !== newId) { 60908 this.logger_(`switch media ${oldId} -> ${newId} from ${cause}`); 60909
vendor: 2,096 bytes, lines 60909-60964
60909this.tech_.trigger({ 60910 type: 'usage', 60911 name: `vhs-rendition-change-${cause}` 60912 }); 60913 } 60914 this.mainPlaylistLoader_.media(playlist, delay); 60915 } 60916 /** 60917 * A function that ensures we switch our playlists inside of `mediaTypes` 60918 * to match the current `serviceLocation` provided by the contentSteering controller. 60919 * We want to check media types of `AUDIO`, `SUBTITLES`, and `CLOSED-CAPTIONS`. 60920 * 60921 * This should only be called on a DASH playback scenario while using content steering. 60922 * This is necessary due to differences in how media in HLS manifests are generally tied to 60923 * a video playlist, where in DASH that is not always the case. 60924 */ 60925 60926 switchMediaForDASHContentSteering_() { 60927 ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach(type => { 60928 const mediaType = this.mediaTypes_[type]; 60929 const activeGroup = mediaType ? mediaType.activeGroup() : null; 60930 const pathway = this.contentSteeringController_.getPathway(); 60931 if (activeGroup && pathway) { 60932 // activeGroup can be an array or a single group 60933 const mediaPlaylists = activeGroup.length ? activeGroup[0].playlists : activeGroup.playlists; 60934 const dashMediaPlaylists = mediaPlaylists.filter(p => p.attributes.serviceLocation === pathway); // Switch the current active playlist to the correct CDN 60935 60936 if (dashMediaPlaylists.length) { 60937 this.mediaTypes_[type].activePlaylistLoader.media(dashMediaPlaylists[0]); 60938 } 60939 } 60940 }); 60941 } 60942 /** 60943 * Start a timer that periodically calls checkABR_ 60944 * 60945 * @private 60946 */ 60947 60948 startABRTimer_() { 60949 this.stopABRTimer_(); 60950 this.abrTimer_ = window.setInterval(() => this.checkABR_(), 250); 60951 } 60952 /** 60953 * Stop the timer that periodically calls checkABR_ 60954 * 60955 * @private 60956 */ 60957 60958 stopABRTimer_() { 60959 // if we're scrubbing, we don't need to pause. 60960 // This getter will be added to Video.js in version 7.11. 60961 if (this.tech_.scrubbing && this.tech_.scrubbing()) { 60962 return; 60963 } 60964 window
vendor: 7,520 bytes, lines 60964-61145
60964.clearInterval(this.abrTimer_); 60965 this.abrTimer_ = null; 60966 } 60967 /** 60968 * Get a list of playlists for the currently selected audio playlist 60969 * 60970 * @return {Array} the array of audio playlists 60971 */ 60972 60973 getAudioTrackPlaylists_() { 60974 const main = this.main(); 60975 const defaultPlaylists = main && main.playlists || []; // if we don't have any audio groups then we can only 60976 // assume that the audio tracks are contained in main 60977 // playlist array, use that or an empty array. 60978 60979 if (!main || !main.mediaGroups || !main.mediaGroups.AUDIO) { 60980 return defaultPlaylists; 60981 } 60982 const AUDIO = main.mediaGroups.AUDIO; 60983 const groupKeys = Object.keys(AUDIO); 60984 let track; // get the current active track 60985 60986 if (Object.keys(this.mediaTypes_.AUDIO.groups).length) { 60987 track = this.mediaTypes_.AUDIO.activeTrack(); // or get the default track from main if mediaTypes_ isn't setup yet 60988 } else { 60989 // default group is `main` or just the first group. 60990 const defaultGroup = AUDIO.main || groupKeys.length && AUDIO[groupKeys[0]]; 60991 for (const label in defaultGroup) { 60992 if (defaultGroup[label].default) { 60993 track = { 60994 label 60995 }; 60996 break; 60997 } 60998 } 60999 } // no active track no playlists. 61000 61001 if (!track) { 61002 return defaultPlaylists; 61003 } 61004 const playlists = []; // get all of the playlists that are possible for the 61005 // active track. 61006 61007 for (const group in AUDIO) { 61008 if (AUDIO[group][track.label]) { 61009 const properties = AUDIO[group][track.label]; 61010 if (properties.playlists && properties.playlists.length) { 61011 playlists.push.apply(playlists, properties.playlists); 61012 } else if (properties.uri) { 61013 playlists.push(properties); 61014 } else if (main.playlists.length) { 61015 // if an audio group does not have a uri 61016 // see if we have main playlists that use it as a group. 61017 // if we do then add those to the playlists list. 61018 for (let i = 0; i < main.playlists.length; i++) { 61019 const playlist = main.playlists[i]; 61020 if (playlist.attributes && playlist.attributes.AUDIO && playlist.attributes.AUDIO === group) { 61021 playlists.push(playlist); 61022 } 61023 } 61024 } 61025 } 61026 } 61027 if (!playlists.length) { 61028 return defaultPlaylists; 61029 } 61030 return playlists; 61031 } 61032 /** 61033 * Register event handlers on the main playlist loader. A helper 61034 * function for construction time. 61035 * 61036 * @private 61037 */ 61038 61039 setupMainPlaylistLoaderListeners_() { 61040 this.mainPlaylistLoader_.on('loadedmetadata', () => { 61041 const media = this.mainPlaylistLoader_.media(); 61042 const requestTimeout = media.targetDuration * 1.5 * 1000; // If we don't have any more available playlists, we don't want to 61043 // timeout the request. 61044 61045 if (isLowestEnabledRendition(this.mainPlaylistLoader_.main, this.mainPlaylistLoader_.media())) { 61046 this.requestOptions_.timeout = 0; 61047 } else { 61048 this.requestOptions_.timeout = requestTimeout; 61049 } // if this isn't a live video and preload permits, start 61050 // downloading segments 61051 61052 if (media.endList && this.tech_.preload() !== 'none') { 61053 this.mainSegmentLoader_.playlist(media, this.requestOptions_); 61054 this.mainSegmentLoader_.load(); 61055 } 61056 setupMediaGroups({ 61057 sourceType: this.sourceType_, 61058 segmentLoaders: { 61059 AUDIO: this.audioSegmentLoader_, 61060 SUBTITLES: this.subtitleSegmentLoader_, 61061 main: this.mainSegmentLoader_ 61062 }, 61063 tech: this.tech_, 61064 requestOptions: this.requestOptions_, 61065 mainPlaylistLoader: this.mainPlaylistLoader_, 61066 vhs: this.vhs_, 61067 main: this.main(), 61068 mediaTypes: this.mediaTypes_, 61069 excludePlaylist: this.excludePlaylist.bind(this) 61070 }); 61071 this.triggerPresenceUsage_(this.main(), media); 61072 this.setupFirstPlay(); 61073 if (!this.mediaTypes_.AUDIO.activePlaylistLoader || this.mediaTypes_.AUDIO.activePlaylistLoader.media()) { 61074 this.trigger('selectedinitialmedia'); 61075 } else { 61076 // We must wait for the active audio playlist loader to 61077 // finish setting up before triggering this event so the 61078 // representations API and EME setup is correct 61079 this.mediaTypes_.AUDIO.activePlaylistLoader.one('loadedmetadata', () => { 61080 this.trigger('selectedinitialmedia'); 61081 }); 61082 } 61083 }); 61084 this.mainPlaylistLoader_.on('loadedplaylist', () => { 61085 if (this.loadOnPlay_) { 61086 this.tech_.off('play', this.loadOnPlay_); 61087 } 61088 let updatedPlaylist = this.mainPlaylistLoader_.media(); 61089 if (!updatedPlaylist) { 61090 // Add content steering listeners on first load and init. 61091 this.attachContentSteeringListeners_(); 61092 this.initContentSteeringController_(); // exclude any variants that are not supported by the browser before selecting 61093 // an initial media as the playlist selectors do not consider browser support 61094 61095 this.excludeUnsupportedVariants_(); 61096 let selectedMedia; 61097 if (this.enableLowInitialPlaylist) { 61098 selectedMedia = this.selectInitialPlaylist(); 61099 } 61100 if (!selectedMedia) { 61101 selectedMedia = this.selectPlaylist(); 61102 } 61103 if (!selectedMedia || !this.shouldSwitchToMedia_(selectedMedia)) { 61104 return; 61105 } 61106 this.initialMedia_ = selectedMedia; 61107 this.switchMedia_(this.initialMedia_, 'initial'); // Under the standard case where a source URL is provided, loadedplaylist will 61108 // fire again since the playlist will be requested. In the case of vhs-json 61109 // (where the manifest object is provided as the source), when the media 61110 // playlist's `segments` list is already available, a media playlist won't be 61111 // requested, and loadedplaylist won't fire again, so the playlist handler must be 61112 // called on its own here. 61113 61114 const haveJsonSource = this.sourceType_ === 'vhs-json' && this.initialMedia_.segments; 61115 if (!haveJsonSource) { 61116 return; 61117 } 61118 updatedPlaylist = this.initialMedia_; 61119 } 61120 this.handleUpdatedMediaPlaylist(updatedPlaylist); 61121 }); 61122 this.mainPlaylistLoader_.on('error', () => { 61123 const error = this.mainPlaylistLoader_.error; 61124 this.excludePlaylist({ 61125 playlistToExclude: error.playlist, 61126 error 61127 }); 61128 }); 61129 this.mainPlaylistLoader_.on('mediachanging', () => { 61130 this.mainSegmentLoader_.abort(); 61131 this.mainSegmentLoader_.pause(); 61132 }); 61133 this.mainPlaylistLoader_.on('mediachange', () => { 61134 const media = this.mainPlaylistLoader_.media(); 61135 const requestTimeout = media.targetDuration * 1.5 * 1000; // If we don't have any more available playlists, we don't want to 61136 // timeout the request. 61137 61138 if (isLowestEnabledRendition(this.mainPlaylistLoader_.main, this.mainPlaylistLoader_.media())) { 61139 this.requestOptions_.timeout = 0; 61140 } else { 61141 this.requestOptions_.timeout = requestTimeout; 61142 } 61143 if (this.sourceType_ === 'dash') { 61144 // we don't want to re-request the same hls playlist right after it was changed 61145
61145this.mainPlaylistLoader_.load(); 61146 } // TODO: Create a new event on the PlaylistLoader that signals 61147 // that the segments have changed in some way and use that to 61148 // update the SegmentLoader instead of doing it twice here and 61149 // on `loadedplaylist` 61150 61151 this.mainSegmentLoader_.pause(); 61152 this.mainSegmentLoader_.playlist(media, this.requestOptions_); 61153 if (this.waitingForFastQualityPlaylistReceived_) { 61154 this.runFastQualitySwitch_(); 61155 } else { 61156 this.mainSegmentLoader_.load(); 61157 } 61158 this.tech_.trigger({ 61159 type: 'mediachange', 61160 bubbles: true 61161 }); 61162 }); 61163 this.mainPlaylistLoader_.on('playlistunchanged', () => { 61164 const updatedPlaylist = this.mainPlaylistLoader_.media(); // ignore unchanged playlists that have already been 61165 // excluded for not-changing. We likely just have a really slowly updating 61166 // playlist. 61167 61168 if (updatedPlaylist.lastExcludeReason_ === 'playlist-unchanged') { 61169 return; 61170 } 61171 const playlistOutdated = this.stuckAtPlaylistEnd_(updatedPlaylist); 61172 if (playlistOutdated) { 61173 // Playlist has stopped updating and we're stuck at its end. Try to 61174 // exclude it and switch to another playlist in the hope that that 61175 // one is updating (and give the player a chance to re-adjust to the 61176 // safe live point). 61177 this.excludePlaylist({ 61178 error: { 61179 message: 'Playlist no longer updating.', 61180 reason: 'playlist-unchanged' 61181 } 61182 }); // useful for monitoring QoS 61183 61184 this.tech_.trigger('playliststuck'); 61185 } 61186 }); 61187 this.mainPlaylistLoader_.on('renditiondisabled', () => { 61188 this.tech_.trigger({ 61189 type: 'usage', 61190 name: 'vhs-rendition-disabled' 61191 }); 61192 }); 61193 this.mainPlaylistLoader_.on('renditionenabled', () => { 61194 this.tech_.trigger({ 61195 type: 'usage', 61196 name: 'vhs-rendition-enabled'
vendor: 5,808 bytes, lines 61196-61361
61196 61197 }); 61198 }); 61199 } 61200 /** 61201 * Given an updated media playlist (whether it was loaded for the first time, or 61202 * refreshed for live playlists), update any relevant properties and state to reflect 61203 * changes in the media that should be accounted for (e.g., cues and duration). 61204 * 61205 * @param {Object} updatedPlaylist the updated media playlist object 61206 * 61207 * @private 61208 */ 61209 61210 handleUpdatedMediaPlaylist(updatedPlaylist) { 61211 if (this.useCueTags_) { 61212 this.updateAdCues_(updatedPlaylist); 61213 } // TODO: Create a new event on the PlaylistLoader that signals 61214 // that the segments have changed in some way and use that to 61215 // update the SegmentLoader instead of doing it twice here and 61216 // on `mediachange` 61217 61218 this.mainSegmentLoader_.pause(); 61219 this.mainSegmentLoader_.playlist(updatedPlaylist, this.requestOptions_); 61220 if (this.waitingForFastQualityPlaylistReceived_) { 61221 this.runFastQualitySwitch_(); 61222 } 61223 this.updateDuration(!updatedPlaylist.endList); // If the player isn't paused, ensure that the segment loader is running, 61224 // as it is possible that it was temporarily stopped while waiting for 61225 // a playlist (e.g., in case the playlist errored and we re-requested it). 61226 61227 if (!this.tech_.paused()) { 61228 this.mainSegmentLoader_.load(); 61229 if (this.audioSegmentLoader_) { 61230 this.audioSegmentLoader_.load(); 61231 } 61232 } 61233 } 61234 /** 61235 * A helper function for triggerring presence usage events once per source 61236 * 61237 * @private 61238 */ 61239 61240 triggerPresenceUsage_(main, media) { 61241 const mediaGroups = main.mediaGroups || {}; 61242 let defaultDemuxed = true; 61243 const audioGroupKeys = Object.keys(mediaGroups.AUDIO); 61244 for (const mediaGroup in mediaGroups.AUDIO) { 61245 for (const label in mediaGroups.AUDIO[mediaGroup]) { 61246 const properties = mediaGroups.AUDIO[mediaGroup][label]; 61247 if (!properties.uri) { 61248 defaultDemuxed = false; 61249 } 61250 } 61251 } 61252 if (defaultDemuxed) { 61253 this.tech_.trigger({ 61254 type: 'usage', 61255 name: 'vhs-demuxed' 61256 }); 61257 } 61258 if (Object.keys(mediaGroups.SUBTITLES).length) { 61259 this.tech_.trigger({ 61260 type: 'usage', 61261 name: 'vhs-webvtt' 61262 }); 61263 } 61264 if (Vhs$1.Playlist.isAes(media)) { 61265 this.tech_.trigger({ 61266 type: 'usage', 61267 name: 'vhs-aes' 61268 }); 61269 } 61270 if (audioGroupKeys.length && Object.keys(mediaGroups.AUDIO[audioGroupKeys[0]]).length > 1) { 61271 this.tech_.trigger({ 61272 type: 'usage', 61273 name: 'vhs-alternate-audio' 61274 }); 61275 } 61276 if (this.useCueTags_) { 61277 this.tech_.trigger({ 61278 type: 'usage', 61279 name: 'vhs-playlist-cue-tags' 61280 }); 61281 } 61282 } 61283 shouldSwitchToMedia_(nextPlaylist) { 61284 const currentPlaylist = this.mainPlaylistLoader_.media() || this.mainPlaylistLoader_.pendingMedia_; 61285 const currentTime = this.tech_.currentTime(); 61286 const bufferLowWaterLine = this.bufferLowWaterLine(); 61287 const bufferHighWaterLine = this.bufferHighWaterLine(); 61288 const buffered = this.tech_.buffered(); 61289 return shouldSwitchToMedia({ 61290 buffered, 61291 currentTime, 61292 currentPlaylist, 61293 nextPlaylist, 61294 bufferLowWaterLine, 61295 bufferHighWaterLine, 61296 duration: this.duration(), 61297 bufferBasedABR: this.bufferBasedABR, 61298 log: this.logger_ 61299 }); 61300 } 61301 /** 61302 * Register event handlers on the segment loaders. A helper function 61303 * for construction time. 61304 * 61305 * @private 61306 */ 61307 61308 setupSegmentLoaderListeners_() { 61309 this.mainSegmentLoader_.on('bandwidthupdate', () => { 61310 // Whether or not buffer based ABR or another ABR is used, on a bandwidth change it's 61311 // useful to check to see if a rendition switch should be made. 61312 this.checkABR_('bandwidthupdate'); 61313 this.tech_.trigger('bandwidthupdate'); 61314 }); 61315 this.mainSegmentLoader_.on('timeout', () => { 61316 if (this.bufferBasedABR) { 61317 // If a rendition change is needed, then it would've be done on `bandwidthupdate`. 61318 // Here the only consideration is that for buffer based ABR there's no guarantee 61319 // of an immediate switch (since the bandwidth is averaged with a timeout 61320 // bandwidth value of 1), so force a load on the segment loader to keep it going. 61321 this.mainSegmentLoader_.load(); 61322 } 61323 }); // `progress` events are not reliable enough of a bandwidth measure to trigger buffer 61324 // based ABR. 61325 61326 if (!this.bufferBasedABR) { 61327 this.mainSegmentLoader_.on('progress', () => { 61328 this.trigger('progress'); 61329 }); 61330 } 61331 this.mainSegmentLoader_.on('error', () => { 61332 const error = this.mainSegmentLoader_.error(); 61333 this.excludePlaylist({ 61334 playlistToExclude: error.playlist, 61335 error 61336 }); 61337 }); 61338 this.mainSegmentLoader_.on('appenderror', () => { 61339 this.error = this.mainSegmentLoader_.error_; 61340 this.trigger('error'); 61341 }); 61342 this.mainSegmentLoader_.on('syncinfoupdate', () => { 61343 this.onSyncInfoUpdate_(); 61344 }); 61345 this.mainSegmentLoader_.on('timestampoffset', () => { 61346 this.tech_.trigger({ 61347 type: 'usage', 61348 name: 'vhs-timestamp-offset' 61349 }); 61350 }); 61351 this.audioSegmentLoader_.on('syncinfoupdate', () => { 61352 this.onSyncInfoUpdate_(); 61353 }); 61354 this.audioSegmentLoader_.on('appenderror', () => { 61355 this.error = this.audioSegmentLoader_.error_; 61356 this.trigger('error'); 61357 }); 61358 this.mainSegmentLoader_.on('ended', () => { 61359 this.logger_('main segment loader ended'); 61360 this.onEndOfStream(); 61361 });
vendor: 5,597 bytes, lines 61361-61510
61361 61362 this.mainSegmentLoader_.on('earlyabort', event => { 61363 // never try to early abort with the new ABR algorithm 61364 if (this.bufferBasedABR) { 61365 return; 61366 } 61367 this.delegateLoaders_('all', ['abort']); 61368 this.excludePlaylist({ 61369 error: { 61370 message: 'Aborted early because there isn\'t enough bandwidth to complete ' + 'the request without rebuffering.' 61371 }, 61372 playlistExclusionDuration: ABORT_EARLY_EXCLUSION_SECONDS 61373 }); 61374 }); 61375 const updateCodecs = () => { 61376 if (!this.sourceUpdater_.hasCreatedSourceBuffers()) { 61377 return this.tryToCreateSourceBuffers_(); 61378 } 61379 const codecs = this.getCodecsOrExclude_(); // no codecs means that the playlist was excluded 61380 61381 if (!codecs) { 61382 return; 61383 } 61384 this.sourceUpdater_.addOrChangeSourceBuffers(codecs); 61385 }; 61386 this.mainSegmentLoader_.on('trackinfo', updateCodecs); 61387 this.audioSegmentLoader_.on('trackinfo', updateCodecs); 61388 this.mainSegmentLoader_.on('fmp4', () => { 61389 if (!this.triggeredFmp4Usage) { 61390 this.tech_.trigger({ 61391 type: 'usage', 61392 name: 'vhs-fmp4' 61393 }); 61394 this.triggeredFmp4Usage = true; 61395 } 61396 }); 61397 this.audioSegmentLoader_.on('fmp4', () => { 61398 if (!this.triggeredFmp4Usage) { 61399 this.tech_.trigger({ 61400 type: 'usage', 61401 name: 'vhs-fmp4' 61402 }); 61403 this.triggeredFmp4Usage = true; 61404 } 61405 }); 61406 this.audioSegmentLoader_.on('ended', () => { 61407 this.logger_('audioSegmentLoader ended'); 61408 this.onEndOfStream(); 61409 }); 61410 } 61411 mediaSecondsLoaded_() { 61412 return Math.max(this.audioSegmentLoader_.mediaSecondsLoaded + this.mainSegmentLoader_.mediaSecondsLoaded); 61413 } 61414 /** 61415 * Call load on our SegmentLoaders 61416 */ 61417 61418 load() { 61419 this.mainSegmentLoader_.load(); 61420 if (this.mediaTypes_.AUDIO.activePlaylistLoader) { 61421 this.audioSegmentLoader_.load(); 61422 } 61423 if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) { 61424 this.subtitleSegmentLoader_.load(); 61425 } 61426 } 61427 /** 61428 * Re-tune playback quality level for the current player 61429 * conditions. This method will perform destructive actions like removing 61430 * already buffered content in order to readjust the currently active 61431 * playlist quickly. This is good for manual quality changes 61432 * 61433 * @private 61434 */ 61435 61436 fastQualityChange_(media = this.selectPlaylist()) { 61437 if (media && media === this.mainPlaylistLoader_.media()) { 61438 this.logger_('skipping fastQualityChange because new media is same as old'); 61439 return; 61440 } 61441 this.switchMedia_(media, 'fast-quality'); // we would like to avoid race condition when we call fastQuality, 61442 // reset everything and start loading segments from prev segments instead of new because new playlist is not received yet 61443 61444 this.waitingForFastQualityPlaylistReceived_ = true; 61445 } 61446 runFastQualitySwitch_() { 61447 this.waitingForFastQualityPlaylistReceived_ = false; // Delete all buffered data to allow an immediate quality switch, then seek to give 61448 // the browser a kick to remove any cached frames from the previous rendtion (.04 seconds 61449 // ahead was roughly the minimum that will accomplish this across a variety of content 61450 // in IE and Edge, but seeking in place is sufficient on all other browsers) 61451 // Edge/IE bug: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/14600375/ 61452 // Chrome bug: https://bugs.chromium.org/p/chromium/issues/detail?id=651904 61453 61454 this.mainSegmentLoader_.pause(); 61455 this.mainSegmentLoader_.resetEverything(() => { 61456 this.tech_.setCurrentTime(this.tech_.currentTime()); 61457 }); // don't need to reset audio as it is reset when media changes 61458 } 61459 /** 61460 * Begin playback. 61461 */ 61462 61463 play() { 61464 if (this.setupFirstPlay()) { 61465 return; 61466 } 61467 if (this.tech_.ended()) { 61468 this.tech_.setCurrentTime(0); 61469 } 61470 if (this.hasPlayed_) { 61471 this.load(); 61472 } 61473 const seekable = this.tech_.seekable(); // if the viewer has paused and we fell out of the live window, 61474 // seek forward to the live point 61475 61476 if (this.tech_.duration() === Infinity) { 61477 if (this.tech_.currentTime() < seekable.start(0)) { 61478 return this.tech_.setCurrentTime(seekable.end(seekable.length - 1)); 61479 } 61480 } 61481 } 61482 /** 61483 * Seek to the latest media position if this is a live video and the 61484 * player and video are loaded and initialized. 61485 */ 61486 61487 setupFirstPlay() { 61488 const media = this.mainPlaylistLoader_.media(); // Check that everything is ready to begin buffering for the first call to play 61489 // If 1) there is no active media 61490 // 2) the player is paused 61491 // 3) the first play has already been setup 61492 // then exit early 61493 61494 if (!media || this.tech_.paused() || this.hasPlayed_) { 61495 return false; 61496 } // when the video is a live stream and/or has a start time 61497 61498 if (!media.endList || media.start) { 61499 const seekable = this.seekable(); 61500 if (!seekable.length) { 61501 // without a seekable range, the player cannot seek to begin buffering at the 61502 // live or start point 61503 return false; 61504 } 61505 const seekableEnd = seekable.end(0); 61506 let startPoint = seekableEnd; 61507 if (media.start) { 61508 const offset = media.start.timeOffset; 61509 if (offset < 0) { 61510 startPoint = Math.max(seekableEnd + offset
vendor: 15,902 bytes, lines 61510-61899
61510, seekable.start(0)); 61511 } else { 61512 startPoint = Math.min(seekableEnd, offset); 61513 } 61514 } // trigger firstplay to inform the source handler to ignore the next seek event 61515 61516 this.trigger('firstplay'); // seek to the live point 61517 61518 this.tech_.setCurrentTime(startPoint); 61519 } 61520 this.hasPlayed_ = true; // we can begin loading now that everything is ready 61521 61522 this.load(); 61523 return true; 61524 } 61525 /** 61526 * handle the sourceopen event on the MediaSource 61527 * 61528 * @private 61529 */ 61530 61531 handleSourceOpen_() { 61532 // Only attempt to create the source buffer if none already exist. 61533 // handleSourceOpen is also called when we are "re-opening" a source buffer 61534 // after `endOfStream` has been called (in response to a seek for instance) 61535 this.tryToCreateSourceBuffers_(); // if autoplay is enabled, begin playback. This is duplicative of 61536 // code in video.js but is required because play() must be invoked 61537 // *after* the media source has opened. 61538 61539 if (this.tech_.autoplay()) { 61540 const playPromise = this.tech_.play(); // Catch/silence error when a pause interrupts a play request 61541 // on browsers which return a promise 61542 61543 if (typeof playPromise !== 'undefined' && typeof playPromise.then === 'function') { 61544 playPromise.then(null, e => {}); 61545 } 61546 } 61547 this.trigger('sourceopen'); 61548 } 61549 /** 61550 * handle the sourceended event on the MediaSource 61551 * 61552 * @private 61553 */ 61554 61555 handleSourceEnded_() { 61556 if (!this.inbandTextTracks_.metadataTrack_) { 61557 return; 61558 } 61559 const cues = this.inbandTextTracks_.metadataTrack_.cues; 61560 if (!cues || !cues.length) { 61561 return; 61562 } 61563 const duration = this.duration(); 61564 cues[cues.length - 1].endTime = isNaN(duration) || Math.abs(duration) === Infinity ? Number.MAX_VALUE : duration; 61565 } 61566 /** 61567 * handle the durationchange event on the MediaSource 61568 * 61569 * @private 61570 */ 61571 61572 handleDurationChange_() { 61573 this.tech_.trigger('durationchange'); 61574 } 61575 /** 61576 * Calls endOfStream on the media source when all active stream types have called 61577 * endOfStream 61578 * 61579 * @param {string} streamType 61580 * Stream type of the segment loader that called endOfStream 61581 * @private 61582 */ 61583 61584 onEndOfStream() { 61585 let isEndOfStream = this.mainSegmentLoader_.ended_; 61586 if (this.mediaTypes_.AUDIO.activePlaylistLoader) { 61587 const mainMediaInfo = this.mainSegmentLoader_.getCurrentMediaInfo_(); // if the audio playlist loader exists, then alternate audio is active 61588 61589 if (!mainMediaInfo || mainMediaInfo.hasVideo) { 61590 // if we do not know if the main segment loader contains video yet or if we 61591 // definitively know the main segment loader contains video, then we need to wait 61592 // for both main and audio segment loaders to call endOfStream 61593 isEndOfStream = isEndOfStream && this.audioSegmentLoader_.ended_; 61594 } else { 61595 // otherwise just rely on the audio loader 61596 isEndOfStream = this.audioSegmentLoader_.ended_; 61597 } 61598 } 61599 if (!isEndOfStream) { 61600 return; 61601 } 61602 this.stopABRTimer_(); 61603 this.sourceUpdater_.endOfStream(); 61604 } 61605 /** 61606 * Check if a playlist has stopped being updated 61607 * 61608 * @param {Object} playlist the media playlist object 61609 * @return {boolean} whether the playlist has stopped being updated or not 61610 */ 61611 61612 stuckAtPlaylistEnd_(playlist) { 61613 const seekable = this.seekable(); 61614 if (!seekable.length) { 61615 // playlist doesn't have enough information to determine whether we are stuck 61616 return false; 61617 } 61618 const expired = this.syncController_.getExpiredTime(playlist, this.duration()); 61619 if (expired === null) { 61620 return false; 61621 } // does not use the safe live end to calculate playlist end, since we 61622 // don't want to say we are stuck while there is still content 61623 61624 const absolutePlaylistEnd = Vhs$1.Playlist.playlistEnd(playlist, expired); 61625 const currentTime = this.tech_.currentTime(); 61626 const buffered = this.tech_.buffered(); 61627 if (!buffered.length) { 61628 // return true if the playhead reached the absolute end of the playlist 61629 return absolutePlaylistEnd - currentTime <= SAFE_TIME_DELTA; 61630 } 61631 const bufferedEnd = buffered.end(buffered.length - 1); // return true if there is too little buffer left and buffer has reached absolute 61632 // end of playlist 61633 61634 return bufferedEnd - currentTime <= SAFE_TIME_DELTA && absolutePlaylistEnd - bufferedEnd <= SAFE_TIME_DELTA; 61635 } 61636 /** 61637 * Exclude a playlist for a set amount of time, making it unavailable for selection by 61638 * the rendition selection algorithm, then force a new playlist (rendition) selection. 61639 * 61640 * @param {Object=} playlistToExclude 61641 * the playlist to exclude, defaults to the currently selected playlist 61642 * @param {Object=} error 61643 * an optional error 61644 * @param {number=} playlistExclusionDuration 61645 * an optional number of seconds to exclude the playlist 61646 */ 61647 61648 excludePlaylist({ 61649 playlistToExclude = this.mainPlaylistLoader_.media(), 61650 error = {}, 61651 playlistExclusionDuration 61652 }) { 61653 // If the `error` was generated by the playlist loader, it will contain 61654 // the playlist we were trying to load (but failed) and that should be 61655 // excluded instead of the currently selected playlist which is likely 61656 // out-of-date in this scenario 61657 playlistToExclude = playlistToExclude || this.mainPlaylistLoader_.media(); 61658 playlistExclusionDuration = playlistExclusionDuration || error.playlistExclusionDuration || this.playlistExclusionDuration; // If there is no current playlist, then an error occurred while we were 61659 // trying to load the main OR while we were disposing of the tech 61660 61661 if (!playlistToExclude) { 61662 this.error = error; 61663 if (this.mediaSource.readyState !== 'open') { 61664 this.trigger('error'); 61665 } else { 61666 this.sourceUpdater_.endOfStream('network'); 61667 } 61668 return; 61669 } 61670 playlistToExclude.playlistErrors_++; 61671 const playlists = this.mainPlaylistLoader_.main.playlists; 61672 const enabledPlaylists = playlists.filter(isEnabled); 61673 const isFinalRendition = enabledPlaylists.length === 1 && enabledPlaylists[0] === playlistToExclude; // Don't exclude the only playlist unless it was excluded 61674 // forever 61675 61676 if (playlists.length === 1 && playlistExclusionDuration !== Infinity) { 61677 videojs.log.warn(`Problem encountered with playlist ${playlistToExclude.id}. ` + 'Trying again since it is the only playlist.'); 61678 this.tech_.trigger('retryplaylist'); // if this is a final rendition, we should delay 61679 61680 return this.mainPlaylistLoader_.load(isFinalRendition); 61681 } 61682 if (isFinalRendition) { 61683 // If we're content steering, try other pathways. 61684 if (this.main().contentSteering) { 61685 const pathway = this.pathwayAttribute_(playlistToExclude); // Ignore at least 1 steering manifest refresh. 61686 61687 const reIncludeDelay = this.contentSteeringController_.steeringManifest.ttl * 1000; 61688 this.contentSteeringController_.excludePathway(pathway); 61689 this.excludeThenChangePathway_(); 61690 setTimeout(() => { 61691 this.contentSteeringController_.addAvailablePathway(pathway); 61692 }, reIncludeDelay); 61693 return; 61694 } // Since we're on the final non-excluded playlist, and we're about to exclude 61695 // it, instead of erring the player or retrying this playlist, clear out the current 61696 // exclusion list. This allows other playlists to be attempted in case any have been 61697 // fixed. 61698 61699 let reincluded = false; 61700 playlists.forEach(playlist => { 61701 // skip current playlist which is about to be excluded 61702 if (playlist === playlistToExclude) { 61703 return; 61704 } 61705 const excludeUntil = playlist.excludeUntil; // a playlist cannot be reincluded if it wasn't excluded to begin with. 61706 61707 if (typeof excludeUntil !== 'undefined' && excludeUntil !== Infinity) { 61708 reincluded = true; 61709 delete playlist.excludeUntil; 61710 } 61711 }); 61712 if (reincluded) { 61713 videojs.log.warn('Removing other playlists from the exclusion list because the last ' + 'rendition is about to be excluded.'); // Technically we are retrying a playlist, in that we are simply retrying a previous 61714 // playlist. This is needed for users relying on the retryplaylist event to catch a 61715 // case where the player might be stuck and looping through "dead" playlists. 61716 61717 this.tech_.trigger('retryplaylist'); 61718 } 61719 } // Exclude this playlist 61720 61721 let excludeUntil; 61722 if (playlistToExclude.playlistErrors_ > this.maxPlaylistRetries) { 61723 excludeUntil = Infinity; 61724 } else { 61725 excludeUntil = Date.now() + playlistExclusionDuration * 1000; 61726 } 61727 playlistToExclude.excludeUntil = excludeUntil; 61728 if (error.reason) { 61729 playlistToExclude.lastExcludeReason_ = error.reason; 61730 } 61731 this.tech_.trigger('excludeplaylist'); 61732 this.tech_.trigger({ 61733 type: 'usage', 61734 name: 'vhs-rendition-excluded' 61735 }); // TODO: only load a new playlist if we're excluding the current playlist 61736 // If this function was called with a playlist that's not the current active playlist 61737 // (e.g., media().id !== playlistToExclude.id), 61738 // then a new playlist should not be selected and loaded, as there's nothing wrong with the current playlist. 61739 61740 const nextPlaylist = this.selectPlaylist(); 61741 if (!nextPlaylist) { 61742 this.error = 'Playback cannot continue. No available working or supported playlists.'; 61743 this.trigger('error'); 61744 return; 61745 } 61746 const logFn = error.internal ? this.logger_ : videojs.log.warn; 61747 const errorMessage = error.message ? ' ' + error.message : ''; 61748 logFn(`${error.internal ? 'Internal problem' : 'Problem'} encountered with playlist ${playlistToExclude.id}.` + `${errorMessage} Switching to playlist ${nextPlaylist.id}.`); // if audio group changed reset audio loaders 61749 61750 if (nextPlaylist.attributes.AUDIO !== playlistToExclude.attributes.AUDIO) { 61751 this.delegateLoaders_('audio', ['abort', 'pause']); 61752 } // if subtitle group changed reset subtitle loaders 61753 61754 if (nextPlaylist.attributes.SUBTITLES !== playlistToExclude.attributes.SUBTITLES) { 61755 this.delegateLoaders_('subtitle', ['abort', 'pause']); 61756 } 61757 this.delegateLoaders_('main', ['abort', 'pause']); 61758 const delayDuration = nextPlaylist.targetDuration / 2 * 1000 || 5 * 1000; 61759 const shouldDelay = typeof nextPlaylist.lastRequest === 'number' && Date.now() - nextPlaylist.lastRequest <= delayDuration; // delay if it's a final rendition or if the last refresh is sooner than half targetDuration 61760 61761 return this.switchMedia_(nextPlaylist, 'exclude', isFinalRendition || shouldDelay); 61762 } 61763 /** 61764 * Pause all segment/playlist loaders 61765 */ 61766 61767 pauseLoading() { 61768 this.delegateLoaders_('all', ['abort', 'pause']); 61769 this.stopABRTimer_(); 61770 } 61771 /** 61772 * Call a set of functions in order on playlist loaders, segment loaders, 61773 * or both types of loaders. 61774 * 61775 * @param {string} filter 61776 * Filter loaders that should call fnNames using a string. Can be: 61777 * * all - run on all loaders 61778 * * audio - run on all audio loaders 61779 * * subtitle - run on all subtitle loaders 61780 * * main - run on the main loaders 61781 * 61782 * @param {Array|string} fnNames 61783 * A string or array of function names to call. 61784 */ 61785 61786 delegateLoaders_(filter, fnNames) { 61787 const loaders = []; 61788 const dontFilterPlaylist = filter === 'all'; 61789 if (dontFilterPlaylist || filter === 'main') { 61790 loaders.push(this.mainPlaylistLoader_); 61791 } 61792 const mediaTypes = []; 61793 if (dontFilterPlaylist || filter === 'audio') { 61794 mediaTypes.push('AUDIO'); 61795 } 61796 if (dontFilterPlaylist || filter === 'subtitle') { 61797 mediaTypes.push('CLOSED-CAPTIONS'); 61798 mediaTypes.push('SUBTITLES'); 61799 } 61800 mediaTypes.forEach(mediaType => { 61801 const loader = this.mediaTypes_[mediaType] && this.mediaTypes_[mediaType].activePlaylistLoader; 61802 if (loader) { 61803 loaders.push(loader); 61804 } 61805 }); 61806 ['main', 'audio', 'subtitle'].forEach(name => { 61807 const loader = this[`${name}SegmentLoader_`]; 61808 if (loader && (filter === name || filter === 'all')) { 61809 loaders.push(loader); 61810 } 61811 }); 61812 loaders.forEach(loader => fnNames.forEach(fnName => { 61813 if (typeof loader[fnName] === 'function') { 61814 loader[fnName](); 61815 } 61816 })); 61817 } 61818 /** 61819 * set the current time on all segment loaders 61820 * 61821 * @param {TimeRange} currentTime the current time to set 61822 * @return {TimeRange} the current time 61823 */ 61824 61825 setCurrentTime(currentTime) { 61826 const buffered = findRange(this.tech_.buffered(), currentTime); 61827 if (!(this.mainPlaylistLoader_ && this.mainPlaylistLoader_.media())) { 61828 // return immediately if the metadata is not ready yet 61829 return 0; 61830 } // it's clearly an edge-case but don't thrown an error if asked to 61831 // seek within an empty playlist 61832 61833 if (!this.mainPlaylistLoader_.media().segments) { 61834 return 0; 61835 } // if the seek location is already buffered, continue buffering as usual 61836 61837 if (buffered && buffered.length) { 61838 return currentTime; 61839 } // cancel outstanding requests so we begin buffering at the new 61840 // location 61841 61842 this.mainSegmentLoader_.pause(); 61843 this.mainSegmentLoader_.resetEverything(); 61844 if (this.mediaTypes_.AUDIO.activePlaylistLoader) { 61845 this.audioSegmentLoader_.pause(); 61846 this.audioSegmentLoader_.resetEverything(); 61847 } 61848 if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) { 61849 this.subtitleSegmentLoader_.pause(); 61850 this.subtitleSegmentLoader_.resetEverything(); 61851 } // start segment loader loading in case they are paused 61852 61853 this.load(); 61854 } 61855 /** 61856 * get the current duration 61857 * 61858 * @return {TimeRange} the duration 61859 */ 61860 61861 duration() { 61862 if (!this.mainPlaylistLoader_) { 61863 return 0; 61864 } 61865 const media = this.mainPlaylistLoader_.media(); 61866 if (!media) { 61867 // no playlists loaded yet, so can't determine a duration 61868 return 0; 61869 } // Don't rely on the media source for duration in the case of a live playlist since 61870 // setting the native MediaSource's duration to infinity ends up with consequences to 61871 // seekable behavior. See https://github.com/w3c/media-source/issues/5 for details. 61872 // 61873 // This is resolved in the spec by https://github.com/w3c/media-source/pull/92, 61874 // however, few browsers have support for setLiveSeekableRange() 61875 // https://developer.mozilla.org/en-US/docs/Web/API/MediaSource/setLiveSeekableRange 61876 // 61877 // Until a time when the duration of the media source can be set to infinity, and a 61878 // seekable range specified across browsers, just return Infinity. 61879 61880 if (!media.endList) { 61881 return Infinity; 61882 } // Since this is a VOD video, it is safe to rely on the media source's duration (if 61883 // available). If it's not available, fall back to a playlist-calculated estimate. 61884 61885 if (this.mediaSource) { 61886 return this.mediaSource.duration; 61887 } 61888 return Vhs$1.Playlist.duration(media); 61889 } 61890 /** 61891 * check the seekable range 61892 * 61893 * @return {TimeRange} the seekable range 61894 */ 61895 61896 seekable() { 61897 return this.seekable_; 61898 } 61899
vendor: 3,583 bytes, lines 61899-61970
61899onSyncInfoUpdate_() { 61900 let audioSeekable; // TODO check for creation of both source buffers before updating seekable 61901 // 61902 // A fix was made to this function where a check for 61903 // this.sourceUpdater_.hasCreatedSourceBuffers 61904 // was added to ensure that both source buffers were created before seekable was 61905 // updated. However, it originally had a bug where it was checking for a true and 61906 // returning early instead of checking for false. Setting it to check for false to 61907 // return early though created other issues. A call to play() would check for seekable 61908 // end without verifying that a seekable range was present. In addition, even checking 61909 // for that didn't solve some issues, as handleFirstPlay is sometimes worked around 61910 // due to a media update calling load on the segment loaders, skipping a seek to live, 61911 // thereby starting live streams at the beginning of the stream rather than at the end. 61912 // 61913 // This conditional should be fixed to wait for the creation of two source buffers at 61914 // the same time as the other sections of code are fixed to properly seek to live and 61915 // not throw an error due to checking for a seekable end when no seekable range exists. 61916 // 61917 // For now, fall back to the older behavior, with the understanding that the seekable 61918 // range may not be completely correct, leading to a suboptimal initial live point. 61919 61920 if (!this.mainPlaylistLoader_) { 61921 return; 61922 } 61923 let media = this.mainPlaylistLoader_.media(); 61924 if (!media) { 61925 return; 61926 } 61927 let expired = this.syncController_.getExpiredTime(media, this.duration()); 61928 if (expired === null) { 61929 // not enough information to update seekable 61930 return; 61931 } 61932 const main = this.mainPlaylistLoader_.main; 61933 const mainSeekable = Vhs$1.Playlist.seekable(media, expired, Vhs$1.Playlist.liveEdgeDelay(main, media)); 61934 if (mainSeekable.length === 0) { 61935 return; 61936 } 61937 if (this.mediaTypes_.AUDIO.activePlaylistLoader) { 61938 media = this.mediaTypes_.AUDIO.activePlaylistLoader.media(); 61939 expired = this.syncController_.getExpiredTime(media, this.duration()); 61940 if (expired === null) { 61941 return; 61942 } 61943 audioSeekable = Vhs$1.Playlist.seekable(media, expired, Vhs$1.Playlist.liveEdgeDelay(main, media)); 61944 if (audioSeekable.length === 0) { 61945 return; 61946 } 61947 } 61948 let oldEnd; 61949 let oldStart; 61950 if (this.seekable_ && this.seekable_.length) { 61951 oldEnd = this.seekable_.end(0); 61952 oldStart = this.seekable_.start(0); 61953 } 61954 if (!audioSeekable) { 61955 // seekable has been calculated based on buffering video data so it 61956 // can be returned directly 61957 this.seekable_ = mainSeekable; 61958 } else if (audioSeekable.start(0) > mainSeekable.end(0) || mainSeekable.start(0) > audioSeekable.end(0)) { 61959 // seekables are pretty far off, rely on main 61960 this.seekable_ = mainSeekable; 61961 } else { 61962 this.seekable_ = createTimeRanges([[audioSeekable.start(0) > mainSeekable.start(0) ? audioSeekable.start(0) : mainSeekable.start(0), audioSeekable.end(0) < mainSeekable.end(0) ? audioSeekable.end(0) : mainSeekable.end(0)]]); 61963 } // seekable is the same as last time 61964 61965 if (this.seekable_ && this.seekable_.length) { 61966 if (this.seekable_.end(0) === oldEnd && this.seekable_.start(0) === oldStart) { 61967 return; 61968 } 61969 } 61970 this.logger_(`seekable updated [${printableRange(this.seekable_)}]`
vendor: 5,513 bytes, lines 61970-62098
61970); 61971 this.tech_.trigger('seekablechanged'); 61972 } 61973 /** 61974 * Update the player duration 61975 */ 61976 61977 updateDuration(isLive) { 61978 if (this.updateDuration_) { 61979 this.mediaSource.removeEventListener('sourceopen', this.updateDuration_); 61980 this.updateDuration_ = null; 61981 } 61982 if (this.mediaSource.readyState !== 'open') { 61983 this.updateDuration_ = this.updateDuration.bind(this, isLive); 61984 this.mediaSource.addEventListener('sourceopen', this.updateDuration_); 61985 return; 61986 } 61987 if (isLive) { 61988 const seekable = this.seekable(); 61989 if (!seekable.length) { 61990 return; 61991 } // Even in the case of a live playlist, the native MediaSource's duration should not 61992 // be set to Infinity (even though this would be expected for a live playlist), since 61993 // setting the native MediaSource's duration to infinity ends up with consequences to 61994 // seekable behavior. See https://github.com/w3c/media-source/issues/5 for details. 61995 // 61996 // This is resolved in the spec by https://github.com/w3c/media-source/pull/92, 61997 // however, few browsers have support for setLiveSeekableRange() 61998 // https://developer.mozilla.org/en-US/docs/Web/API/MediaSource/setLiveSeekableRange 61999 // 62000 // Until a time when the duration of the media source can be set to infinity, and a 62001 // seekable range specified across browsers, the duration should be greater than or 62002 // equal to the last possible seekable value. 62003 // MediaSource duration starts as NaN 62004 // It is possible (and probable) that this case will never be reached for many 62005 // sources, since the MediaSource reports duration as the highest value without 62006 // accounting for timestamp offset. For example, if the timestamp offset is -100 and 62007 // we buffered times 0 to 100 with real times of 100 to 200, even though current 62008 // time will be between 0 and 100, the native media source may report the duration 62009 // as 200. However, since we report duration separate from the media source (as 62010 // Infinity), and as long as the native media source duration value is greater than 62011 // our reported seekable range, seeks will work as expected. The large number as 62012 // duration for live is actually a strategy used by some players to work around the 62013 // issue of live seekable ranges cited above. 62014 62015 if (isNaN(this.mediaSource.duration) || this.mediaSource.duration < seekable.end(seekable.length - 1)) { 62016 this.sourceUpdater_.setDuration(seekable.end(seekable.length - 1)); 62017 } 62018 return; 62019 } 62020 const buffered = this.tech_.buffered(); 62021 let duration = Vhs$1.Playlist.duration(this.mainPlaylistLoader_.media()); 62022 if (buffered.length > 0) { 62023 duration = Math.max(duration, buffered.end(buffered.length - 1)); 62024 } 62025 if (this.mediaSource.duration !== duration) { 62026 this.sourceUpdater_.setDuration(duration); 62027 } 62028 } 62029 /** 62030 * dispose of the PlaylistController and everything 62031 * that it controls 62032 */ 62033 62034 dispose() { 62035 this.trigger('dispose'); 62036 this.decrypter_.terminate(); 62037 this.mainPlaylistLoader_.dispose(); 62038 this.mainSegmentLoader_.dispose(); 62039 this.contentSteeringController_.dispose(); 62040 this.keyStatusMap_.clear(); 62041 if (this.loadOnPlay_) { 62042 this.tech_.off('play', this.loadOnPlay_); 62043 } 62044 ['AUDIO', 'SUBTITLES'].forEach(type => { 62045 const groups = this.mediaTypes_[type].groups; 62046 for (const id in groups) { 62047 groups[id].forEach(group => { 62048 if (group.playlistLoader) { 62049 group.playlistLoader.dispose(); 62050 } 62051 }); 62052 } 62053 }); 62054 this.audioSegmentLoader_.dispose(); 62055 this.subtitleSegmentLoader_.dispose(); 62056 this.sourceUpdater_.dispose(); 62057 this.timelineChangeController_.dispose(); 62058 this.stopABRTimer_(); 62059 if (this.updateDuration_) { 62060 this.mediaSource.removeEventListener('sourceopen', this.updateDuration_); 62061 } 62062 this.mediaSource.removeEventListener('durationchange', this.handleDurationChange_); // load the media source into the player 62063 62064 this.mediaSource.removeEventListener('sourceopen', this.handleSourceOpen_); 62065 this.mediaSource.removeEventListener('sourceended', this.handleSourceEnded_); 62066 this.off(); 62067 } 62068 /** 62069 * return the main playlist object if we have one 62070 * 62071 * @return {Object} the main playlist object that we parsed 62072 */ 62073 62074 main() { 62075 return this.mainPlaylistLoader_.main; 62076 } 62077 /** 62078 * return the currently selected playlist 62079 * 62080 * @return {Object} the currently selected playlist object that we parsed 62081 */ 62082 62083 media() { 62084 // playlist loader will not return media if it has not been fully loaded 62085 return this.mainPlaylistLoader_.media() || this.initialMedia_; 62086 } 62087 areMediaTypesKnown_() { 62088 const usingAudioLoader = !!this.mediaTypes_.AUDIO.activePlaylistLoader; 62089 const hasMainMediaInfo = !!this.mainSegmentLoader_.getCurrentMediaInfo_(); // if we are not using an audio loader, then we have audio media info 62090 // otherwise check on the segment loader. 62091 62092 const hasAudioMediaInfo = !usingAudioLoader ? true : !!this.audioSegmentLoader_.getCurrentMediaInfo_(); // one or both loaders has not loaded sufficently to get codecs 62093 62094 if (!hasMainMediaInfo || !hasAudioMediaInfo) { 62095 return false; 62096 } 62097 return true; 62098 }
vendor: 6,207 bytes, lines 62098-62246
62098 62099 getCodecsOrExclude_() { 62100 const media = { 62101 main: this.mainSegmentLoader_.getCurrentMediaInfo_() || {}, 62102 audio: this.audioSegmentLoader_.getCurrentMediaInfo_() || {} 62103 }; 62104 const playlist = this.mainSegmentLoader_.getPendingSegmentPlaylist() || this.media(); // set "main" media equal to video 62105 62106 media.video = media.main; 62107 const playlistCodecs = codecsForPlaylist(this.main(), playlist); 62108 const codecs = {}; 62109 const usingAudioLoader = !!this.mediaTypes_.AUDIO.activePlaylistLoader; 62110 if (media.main.hasVideo) { 62111 codecs.video = playlistCodecs.video || media.main.videoCodec || DEFAULT_VIDEO_CODEC; 62112 } 62113 if (media.main.isMuxed) { 62114 codecs.video += `,${playlistCodecs.audio || media.main.audioCodec || DEFAULT_AUDIO_CODEC}`; 62115 } 62116 if (media.main.hasAudio && !media.main.isMuxed || media.audio.hasAudio || usingAudioLoader) { 62117 codecs.audio = playlistCodecs.audio || media.main.audioCodec || media.audio.audioCodec || DEFAULT_AUDIO_CODEC; // set audio isFmp4 so we use the correct "supports" function below 62118 62119 media.audio.isFmp4 = media.main.hasAudio && !media.main.isMuxed ? media.main.isFmp4 : media.audio.isFmp4; 62120 } // no codecs, no playback. 62121 62122 if (!codecs.audio && !codecs.video) { 62123 this.excludePlaylist({ 62124 playlistToExclude: playlist, 62125 error: { 62126 message: 'Could not determine codecs for playlist.' 62127 }, 62128 playlistExclusionDuration: Infinity 62129 }); 62130 return; 62131 } // fmp4 relies on browser support, while ts relies on muxer support 62132 62133 const supportFunction = (isFmp4, codec) => isFmp4 ? browserSupportsCodec(codec) : muxerSupportsCodec(codec); 62134 const unsupportedCodecs = {}; 62135 let unsupportedAudio; 62136 ['video', 'audio'].forEach(function (type) { 62137 if (codecs.hasOwnProperty(type) && !supportFunction(media[type].isFmp4, codecs[type])) { 62138 const supporter = media[type].isFmp4 ? 'browser' : 'muxer'; 62139 unsupportedCodecs[supporter] = unsupportedCodecs[supporter] || []; 62140 unsupportedCodecs[supporter].push(codecs[type]); 62141 if (type === 'audio') { 62142 unsupportedAudio = supporter; 62143 } 62144 } 62145 }); 62146 if (usingAudioLoader && unsupportedAudio && playlist.attributes.AUDIO) { 62147 const audioGroup = playlist.attributes.AUDIO; 62148 this.main().playlists.forEach(variant => { 62149 const variantAudioGroup = variant.attributes && variant.attributes.AUDIO; 62150 if (variantAudioGroup === audioGroup && variant !== playlist) { 62151 variant.excludeUntil = Infinity; 62152 } 62153 }); 62154 this.logger_(`excluding audio group ${audioGroup} as ${unsupportedAudio} does not support codec(s): "${codecs.audio}"`); 62155 } // if we have any unsupported codecs exclude this playlist. 62156 62157 if (Object.keys(unsupportedCodecs).length) { 62158 const message = Object.keys(unsupportedCodecs).reduce((acc, supporter) => { 62159 if (acc) { 62160 acc += ', '; 62161 } 62162 acc += `${supporter} does not support codec(s): "${unsupportedCodecs[supporter].join(',')}"`; 62163 return acc; 62164 }, '') + '.'; 62165 this.excludePlaylist({ 62166 playlistToExclude: playlist, 62167 error: { 62168 internal: true, 62169 message 62170 }, 62171 playlistExclusionDuration: Infinity 62172 }); 62173 return; 62174 } // check if codec switching is happening 62175 62176 if (this.sourceUpdater_.hasCreatedSourceBuffers() && !this.sourceUpdater_.canChangeType()) { 62177 const switchMessages = []; 62178 ['video', 'audio'].forEach(type => { 62179 const newCodec = (parseCodecs(this.sourceUpdater_.codecs[type] || '')[0] || {}).type; 62180 const oldCodec = (parseCodecs(codecs[type] || '')[0] || {}).type; 62181 if (newCodec && oldCodec && newCodec.toLowerCase() !== oldCodec.toLowerCase()) { 62182 switchMessages.push(`"${this.sourceUpdater_.codecs[type]}" -> "${codecs[type]}"`); 62183 } 62184 }); 62185 if (switchMessages.length) { 62186 this.excludePlaylist({ 62187 playlistToExclude: playlist, 62188 error: { 62189 message: `Codec switching not supported: ${switchMessages.join(', ')}.`, 62190 internal: true 62191 }, 62192 playlistExclusionDuration: Infinity 62193 }); 62194 return; 62195 } 62196 } // TODO: when using the muxer shouldn't we just return 62197 // the codecs that the muxer outputs? 62198 62199 return codecs; 62200 } 62201 /** 62202 * Create source buffers and exlude any incompatible renditions. 62203 * 62204 * @private 62205 */ 62206 62207 tryToCreateSourceBuffers_() { 62208 // media source is not ready yet or sourceBuffers are already 62209 // created. 62210 if (this.mediaSource.readyState !== 'open' || this.sourceUpdater_.hasCreatedSourceBuffers()) { 62211 return; 62212 } 62213 if (!this.areMediaTypesKnown_()) { 62214 return; 62215 } 62216 const codecs = this.getCodecsOrExclude_(); // no codecs means that the playlist was excluded 62217 62218 if (!codecs) { 62219 return; 62220 } 62221 this.sourceUpdater_.createSourceBuffers(codecs); 62222 const codecString = [codecs.video, codecs.audio].filter(Boolean).join(','); 62223 this.excludeIncompatibleVariants_(codecString); 62224 } 62225 /** 62226 * Excludes playlists with codecs that are unsupported by the muxer and browser. 62227 */ 62228 62229 excludeUnsupportedVariants_() { 62230 const playlists = this.main().playlists; 62231 const ids = []; // TODO: why don't we have a property to loop through all 62232 // playlist? Why did we ever mix indexes and keys? 62233 62234 Object.keys(playlists).forEach(key => { 62235 const variant = playlists[key]; // check if we already processed this playlist. 62236 62237 if (ids.indexOf(variant.id) !== -1) { 62238 return; 62239 } 62240 ids.push(variant.id); 62241 const codecs = codecsForPlaylist(this.main, variant); 62242 const unsupported = []; 62243 if (codecs.audio && !muxerSupportsCodec(codecs.audio) && !browserSupportsCodec(codecs.audio)) { 62244 unsupported.push(`audio codec ${codecs.audio}`); 62245 } 62246 if (codecs.video && !muxerSupportsCodec(codecs.video) && !browserSupportsCodec(codecs.video
vendor: 20,249 bytes, lines 62246-62717
62246)) { 62247 unsupported.push(`video codec ${codecs.video}`); 62248 } 62249 if (codecs.text && codecs.text === 'stpp.ttml.im1t') { 62250 unsupported.push(`text codec ${codecs.text}`); 62251 } 62252 if (unsupported.length) { 62253 variant.excludeUntil = Infinity; 62254 this.logger_(`excluding ${variant.id} for unsupported: ${unsupported.join(', ')}`); 62255 } 62256 }); 62257 } 62258 /** 62259 * Exclude playlists that are known to be codec or 62260 * stream-incompatible with the SourceBuffer configuration. For 62261 * instance, Media Source Extensions would cause the video element to 62262 * stall waiting for video data if you switched from a variant with 62263 * video and audio to an audio-only one. 62264 * 62265 * @param {Object} media a media playlist compatible with the current 62266 * set of SourceBuffers. Variants in the current main playlist that 62267 * do not appear to have compatible codec or stream configurations 62268 * will be excluded from the default playlist selection algorithm 62269 * indefinitely. 62270 * @private 62271 */ 62272 62273 excludeIncompatibleVariants_(codecString) { 62274 const ids = []; 62275 const playlists = this.main().playlists; 62276 const codecs = unwrapCodecList(parseCodecs(codecString)); 62277 const codecCount_ = codecCount(codecs); 62278 const videoDetails = codecs.video && parseCodecs(codecs.video)[0] || null; 62279 const audioDetails = codecs.audio && parseCodecs(codecs.audio)[0] || null; 62280 Object.keys(playlists).forEach(key => { 62281 const variant = playlists[key]; // check if we already processed this playlist. 62282 // or it if it is already excluded forever. 62283 62284 if (ids.indexOf(variant.id) !== -1 || variant.excludeUntil === Infinity) { 62285 return; 62286 } 62287 ids.push(variant.id); 62288 const exclusionReasons = []; // get codecs from the playlist for this variant 62289 62290 const variantCodecs = codecsForPlaylist(this.mainPlaylistLoader_.main, variant); 62291 const variantCodecCount = codecCount(variantCodecs); // if no codecs are listed, we cannot determine that this 62292 // variant is incompatible. Wait for mux.js to probe 62293 62294 if (!variantCodecs.audio && !variantCodecs.video) { 62295 return; 62296 } // TODO: we can support this by removing the 62297 // old media source and creating a new one, but it will take some work. 62298 // The number of streams cannot change 62299 62300 if (variantCodecCount !== codecCount_) { 62301 exclusionReasons.push(`codec count "${variantCodecCount}" !== "${codecCount_}"`); 62302 } // only exclude playlists by codec change, if codecs cannot switch 62303 // during playback. 62304 62305 if (!this.sourceUpdater_.canChangeType()) { 62306 const variantVideoDetails = variantCodecs.video && parseCodecs(variantCodecs.video)[0] || null; 62307 const variantAudioDetails = variantCodecs.audio && parseCodecs(variantCodecs.audio)[0] || null; // the video codec cannot change 62308 62309 if (variantVideoDetails && videoDetails && variantVideoDetails.type.toLowerCase() !== videoDetails.type.toLowerCase()) { 62310 exclusionReasons.push(`video codec "${variantVideoDetails.type}" !== "${videoDetails.type}"`); 62311 } // the audio codec cannot change 62312 62313 if (variantAudioDetails && audioDetails && variantAudioDetails.type.toLowerCase() !== audioDetails.type.toLowerCase()) { 62314 exclusionReasons.push(`audio codec "${variantAudioDetails.type}" !== "${audioDetails.type}"`); 62315 } 62316 } 62317 if (exclusionReasons.length) { 62318 variant.excludeUntil = Infinity; 62319 this.logger_(`excluding ${variant.id}: ${exclusionReasons.join(' && ')}`); 62320 } 62321 }); 62322 } 62323 updateAdCues_(media) { 62324 let offset = 0; 62325 const seekable = this.seekable(); 62326 if (seekable.length) { 62327 offset = seekable.start(0); 62328 } 62329 updateAdCues(media, this.cueTagsTrack_, offset); 62330 } 62331 /** 62332 * Calculates the desired forward buffer length based on current time 62333 * 62334 * @return {number} Desired forward buffer length in seconds 62335 */ 62336 62337 goalBufferLength() { 62338 const currentTime = this.tech_.currentTime(); 62339 const initial = Config.GOAL_BUFFER_LENGTH; 62340 const rate = Config.GOAL_BUFFER_LENGTH_RATE; 62341 const max = Math.max(initial, Config.MAX_GOAL_BUFFER_LENGTH); 62342 return Math.min(initial + currentTime * rate, max); 62343 } 62344 /** 62345 * Calculates the desired buffer low water line based on current time 62346 * 62347 * @return {number} Desired buffer low water line in seconds 62348 */ 62349 62350 bufferLowWaterLine() { 62351 const currentTime = this.tech_.currentTime(); 62352 const initial = Config.BUFFER_LOW_WATER_LINE; 62353 const rate = Config.BUFFER_LOW_WATER_LINE_RATE; 62354 const max = Math.max(initial, Config.MAX_BUFFER_LOW_WATER_LINE); 62355 const newMax = Math.max(initial, Config.EXPERIMENTAL_MAX_BUFFER_LOW_WATER_LINE); 62356 return Math.min(initial + currentTime * rate, this.bufferBasedABR ? newMax : max); 62357 } 62358 bufferHighWaterLine() { 62359 return Config.BUFFER_HIGH_WATER_LINE; 62360 } 62361 addDateRangesToTextTrack_(dateRanges) { 62362 createMetadataTrackIfNotExists(this.inbandTextTracks_, 'com.apple.streaming', this.tech_); 62363 addDateRangeMetadata({ 62364 inbandTextTracks: this.inbandTextTracks_, 62365 dateRanges 62366 }); 62367 } 62368 addMetadataToTextTrack(dispatchType, metadataArray, videoDuration) { 62369 const timestampOffset = this.sourceUpdater_.videoBuffer ? this.sourceUpdater_.videoTimestampOffset() : this.sourceUpdater_.audioTimestampOffset(); // There's potentially an issue where we could double add metadata if there's a muxed 62370 // audio/video source with a metadata track, and an alt audio with a metadata track. 62371 // However, this probably won't happen, and if it does it can be handled then. 62372 62373 createMetadataTrackIfNotExists(this.inbandTextTracks_, dispatchType, this.tech_); 62374 addMetadata({ 62375 inbandTextTracks: this.inbandTextTracks_, 62376 metadataArray, 62377 timestampOffset, 62378 videoDuration 62379 }); 62380 } 62381 /** 62382 * Utility for getting the pathway or service location from an HLS or DASH playlist. 62383 * 62384 * @param {Object} playlist for getting pathway from. 62385 * @return the pathway attribute of a playlist 62386 */ 62387 62388 pathwayAttribute_(playlist) { 62389 return playlist.attributes['PATHWAY-ID'] || playlist.attributes.serviceLocation; 62390 } 62391 /** 62392 * Initialize available pathways and apply the tag properties. 62393 */ 62394 62395 initContentSteeringController_() { 62396 const main = this.main(); 62397 if (!main.contentSteering) { 62398 return; 62399 } 62400 for (const playlist of main.playlists) { 62401 this.contentSteeringController_.addAvailablePathway(this.pathwayAttribute_(playlist)); 62402 } 62403 this.contentSteeringController_.assignTagProperties(main.uri, main.contentSteering); // request the steering manifest immediately if queryBeforeStart is set. 62404 62405 if (this.contentSteeringController_.queryBeforeStart) { 62406 // When queryBeforeStart is true, initial request should omit steering parameters. 62407 this.contentSteeringController_.requestSteeringManifest(true); 62408 return; 62409 } // otherwise start content steering after playback starts 62410 62411 this.tech_.one('canplay', () => { 62412 this.contentSteeringController_.requestSteeringManifest(); 62413 }); 62414 } 62415 /** 62416 * Reset the content steering controller and re-init. 62417 */ 62418 62419 resetContentSteeringController_() { 62420 this.contentSteeringController_.clearAvailablePathways(); 62421 this.contentSteeringController_.dispose(); 62422 this.initContentSteeringController_(); 62423 } 62424 /** 62425 * Attaches the listeners for content steering. 62426 */ 62427 62428 attachContentSteeringListeners_() { 62429 this.contentSteeringController_.on('content-steering', this.excludeThenChangePathway_.bind(this)); 62430 if (this.sourceType_ === 'dash') { 62431 this.mainPlaylistLoader_.on('loadedplaylist', () => { 62432 const main = this.main(); // check if steering tag or pathways changed. 62433 62434 const didDashTagChange = this.contentSteeringController_.didDASHTagChange(main.uri, main.contentSteering); 62435 const didPathwaysChange = () => { 62436 const availablePathways = this.contentSteeringController_.getAvailablePathways(); 62437 const newPathways = []; 62438 for (const playlist of main.playlists) { 62439 const serviceLocation = playlist.attributes.serviceLocation; 62440 if (serviceLocation) { 62441 newPathways.push(serviceLocation); 62442 if (!availablePathways.has(serviceLocation)) { 62443 return true; 62444 } 62445 } 62446 } // If we have no new serviceLocations and previously had availablePathways 62447 62448 if (!newPathways.length && availablePathways.size) { 62449 return true; 62450 } 62451 return false; 62452 }; 62453 if (didDashTagChange || didPathwaysChange()) { 62454 this.resetContentSteeringController_(); 62455 } 62456 }); 62457 } 62458 } 62459 /** 62460 * Simple exclude and change playlist logic for content steering. 62461 */ 62462 62463 excludeThenChangePathway_() { 62464 const currentPathway = this.contentSteeringController_.getPathway(); 62465 if (!currentPathway) { 62466 return; 62467 } 62468 this.handlePathwayClones_(); 62469 const main = this.main(); 62470 const playlists = main.playlists; 62471 const ids = new Set(); 62472 let didEnablePlaylists = false; 62473 Object.keys(playlists).forEach(key => { 62474 const variant = playlists[key]; 62475 const pathwayId = this.pathwayAttribute_(variant); 62476 const differentPathwayId = pathwayId && currentPathway !== pathwayId; 62477 const steeringExclusion = variant.excludeUntil === Infinity && variant.lastExcludeReason_ === 'content-steering'; 62478 if (steeringExclusion && !differentPathwayId) { 62479 delete variant.excludeUntil; 62480 delete variant.lastExcludeReason_; 62481 didEnablePlaylists = true; 62482 } 62483 const noExcludeUntil = !variant.excludeUntil && variant.excludeUntil !== Infinity; 62484 const shouldExclude = !ids.has(variant.id) && differentPathwayId && noExcludeUntil; 62485 if (!shouldExclude) { 62486 return; 62487 } 62488 ids.add(variant.id); 62489 variant.excludeUntil = Infinity; 62490 variant.lastExcludeReason_ = 'content-steering'; // TODO: kind of spammy, maybe move this. 62491 62492 this.logger_(`excluding ${variant.id} for ${variant.lastExcludeReason_}`); 62493 }); 62494 if (this.contentSteeringController_.manifestType_ === 'DASH') { 62495 Object.keys(this.mediaTypes_).forEach(key => { 62496 const type = this.mediaTypes_[key]; 62497 if (type.activePlaylistLoader) { 62498 const currentPlaylist = type.activePlaylistLoader.media_; // Check if the current media playlist matches the current CDN 62499 62500 if (currentPlaylist && currentPlaylist.attributes.serviceLocation !== currentPathway) { 62501 didEnablePlaylists = true; 62502 } 62503 } 62504 }); 62505 } 62506 if (didEnablePlaylists) { 62507 this.changeSegmentPathway_(); 62508 } 62509 } 62510 /** 62511 * Add, update, or delete playlists and media groups for 62512 * the pathway clones for HLS Content Steering. 62513 * 62514 * See https://datatracker.ietf.org/doc/draft-pantos-hls-rfc8216bis/ 62515 * 62516 * NOTE: Pathway cloning does not currently support the `PER_VARIANT_URIS` and 62517 * `PER_RENDITION_URIS` as we do not handle `STABLE-VARIANT-ID` or 62518 * `STABLE-RENDITION-ID` values. 62519 */ 62520 62521 handlePathwayClones_() { 62522 const main = this.main(); 62523 const playlists = main.playlists; 62524 const currentPathwayClones = this.contentSteeringController_.currentPathwayClones; 62525 const nextPathwayClones = this.contentSteeringController_.nextPathwayClones; 62526 const hasClones = currentPathwayClones && currentPathwayClones.size || nextPathwayClones && nextPathwayClones.size; 62527 if (!hasClones) { 62528 return; 62529 } 62530 for (const [id, clone] of currentPathwayClones.entries()) { 62531 const newClone = nextPathwayClones.get(id); // Delete the old pathway clone. 62532 62533 if (!newClone) { 62534 this.mainPlaylistLoader_.updateOrDeleteClone(clone); 62535 this.contentSteeringController_.excludePathway(id); 62536 } 62537 } 62538 for (const [id, clone] of nextPathwayClones.entries()) { 62539 const oldClone = currentPathwayClones.get(id); // Create a new pathway if it is a new pathway clone object. 62540 62541 if (!oldClone) { 62542 const playlistsToClone = playlists.filter(p => { 62543 return p.attributes['PATHWAY-ID'] === clone['BASE-ID']; 62544 }); 62545 playlistsToClone.forEach(p => { 62546 this.mainPlaylistLoader_.addClonePathway(clone, p); 62547 }); 62548 this.contentSteeringController_.addAvailablePathway(id); 62549 continue; 62550 } // There have not been changes to the pathway clone object, so skip. 62551 62552 if (this.equalPathwayClones_(oldClone, clone)) { 62553 continue; 62554 } // Update a preexisting cloned pathway. 62555 // True is set for the update flag. 62556 62557 this.mainPlaylistLoader_.updateOrDeleteClone(clone, true); 62558 this.contentSteeringController_.addAvailablePathway(id); 62559 } // Deep copy contents of next to current pathways. 62560 62561 this.contentSteeringController_.currentPathwayClones = new Map(JSON.parse(JSON.stringify([...nextPathwayClones]))); 62562 } 62563 /** 62564 * Determines whether two pathway clone objects are equivalent. 62565 * 62566 * @param {Object} a The first pathway clone object. 62567 * @param {Object} b The second pathway clone object. 62568 * @return {boolean} True if the pathway clone objects are equal, false otherwise. 62569 */ 62570 62571 equalPathwayClones_(a, b) { 62572 if (a['BASE-ID'] !== b['BASE-ID'] || a.ID !== b.ID || a['URI-REPLACEMENT'].HOST !== b['URI-REPLACEMENT'].HOST) { 62573 return false; 62574 } 62575 const aParams = a['URI-REPLACEMENT'].PARAMS; 62576 const bParams = b['URI-REPLACEMENT'].PARAMS; // We need to iterate through both lists of params because one could be 62577 // missing a parameter that the other has. 62578 62579 for (const p in aParams) { 62580 if (aParams[p] !== bParams[p]) { 62581 return false; 62582 } 62583 } 62584 for (const p in bParams) { 62585 if (aParams[p] !== bParams[p]) { 62586 return false; 62587 } 62588 } 62589 return true; 62590 } 62591 /** 62592 * Changes the current playlists for audio, video and subtitles after a new pathway 62593 * is chosen from content steering. 62594 */ 62595 62596 changeSegmentPathway_() { 62597 const nextPlaylist = this.selectPlaylist(); 62598 this.pauseLoading(); // Switch audio and text track playlists if necessary in DASH 62599 62600 if (this.contentSteeringController_.manifestType_ === 'DASH') { 62601 this.switchMediaForDASHContentSteering_(); 62602 } 62603 this.switchMedia_(nextPlaylist, 'content-steering'); 62604 } 62605 /** 62606 * Iterates through playlists and check their keyId set and compare with the 62607 * keyStatusMap, only enable playlists that have a usable key. If the playlist 62608 * has no keyId leave it enabled by default. 62609 */ 62610 62611 excludeNonUsablePlaylistsByKeyId_() { 62612 if (!this.mainPlaylistLoader_ || !this.mainPlaylistLoader_.main) { 62613 return; 62614 } 62615 let nonUsableKeyStatusCount = 0; 62616 const NON_USABLE = 'non-usable'; 62617 this.mainPlaylistLoader_.main.playlists.forEach(playlist => { 62618 const keyIdSet = this.mainPlaylistLoader_.getKeyIdSet(playlist); // If the playlist doesn't have keyIDs lets not exclude it. 62619 62620 if (!keyIdSet || !keyIdSet.size) { 62621 return; 62622 } 62623 keyIdSet.forEach(key => { 62624 const USABLE = 'usable'; 62625 const hasUsableKeyStatus = this.keyStatusMap_.has(key) && this.keyStatusMap_.get(key) === USABLE; 62626 const nonUsableExclusion = playlist.lastExcludeReason_ === NON_USABLE && playlist.excludeUntil === Infinity; 62627 if (!hasUsableKeyStatus) { 62628 // Only exclude playlists that haven't already been excluded as non-usable. 62629 if (playlist.excludeUntil !== Infinity && playlist.lastExcludeReason_ !== NON_USABLE) { 62630 playlist.excludeUntil = Infinity; 62631 playlist.lastExcludeReason_ = NON_USABLE; 62632 this.logger_(`excluding playlist ${playlist.id} because the key ID ${key} doesn't exist in the keyStatusMap or is not ${USABLE}`); 62633 } // count all nonUsableKeyStatus 62634 62635 nonUsableKeyStatusCount++; 62636 } else if (hasUsableKeyStatus && nonUsableExclusion) { 62637 delete playlist.excludeUntil; 62638 delete playlist.lastExcludeReason_; 62639 this.logger_(`enabling playlist ${playlist.id} because key ID ${key} is ${USABLE}`); 62640 } 62641 }); 62642 }); // If for whatever reason every playlist has a non usable key status. Lets try re-including the SD renditions as a failsafe. 62643 62644 if (nonUsableKeyStatusCount >= this.mainPlaylistLoader_.main.playlists.length) { 62645 this.mainPlaylistLoader_.main.playlists.forEach(playlist => { 62646 const isNonHD = playlist && playlist.attributes && playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.height < 720; 62647 const excludedForNonUsableKey = playlist.excludeUntil === Infinity && playlist.lastExcludeReason_ === NON_USABLE; 62648 if (isNonHD && excludedForNonUsableKey) { 62649 // Only delete the excludeUntil so we don't try and re-exclude these playlists. 62650 delete playlist.excludeUntil; 62651 videojs.log.warn(`enabling non-HD playlist ${playlist.id} because all playlists were excluded due to ${NON_USABLE} key IDs`); 62652 } 62653 }); 62654 } 62655 } 62656 /** 62657 * Adds a keystatus to the keystatus map, tries to convert to string if necessary. 62658 * 62659 * @param {any} keyId the keyId to add a status for 62660 * @param {string} status the status of the keyId 62661 */ 62662 62663 addKeyStatus_(keyId, status) { 62664 const isString = typeof keyId === 'string'; 62665 const keyIdHexString = isString ? keyId : bufferToHexString(keyId); 62666 const formattedKeyIdString = keyIdHexString.slice(0, 32).toLowerCase(); 62667 this.logger_(`KeyStatus '${status}' with key ID ${formattedKeyIdString} added to the keyStatusMap`); 62668 this.keyStatusMap_.set(formattedKeyIdString, status); 62669 } 62670 /** 62671 * Utility function for adding key status to the keyStatusMap and filtering usable encrypted playlists. 62672 * 62673 * @param {any} keyId the keyId from the keystatuschange event 62674 * @param {string} status the key status string 62675 */ 62676 62677 updatePlaylistByKeyStatus(keyId, status) { 62678 this.addKeyStatus_(keyId, status); 62679 if (!this.waitingForFastQualityPlaylistReceived_) { 62680 this.excludeNonUsableThenChangePlaylist_(); 62681 } // Listen to loadedplaylist with a single listener and check for new contentProtection elements when a playlist is updated. 62682 62683 this.mainPlaylistLoader_.off('loadedplaylist', this.excludeNonUsableThenChangePlaylist_.bind(this)); 62684 this.mainPlaylistLoader_.on('loadedplaylist', this.excludeNonUsableThenChangePlaylist_.bind(this)); 62685 } 62686 excludeNonUsableThenChangePlaylist_() { 62687 this.excludeNonUsablePlaylistsByKeyId_(); 62688 this.fastQualityChange_(); 62689 } 62690 } 62691 62692 /** 62693 * Returns a function that acts as the Enable/disable playlist function. 62694 * 62695 * @param {PlaylistLoader} loader - The main playlist loader 62696 * @param {string} playlistID - id of the playlist 62697 * @param {Function} changePlaylistFn - A function to be called after a 62698 * playlist's enabled-state has been changed. Will NOT be called if a 62699 * playlist's enabled-state is unchanged 62700 * @param {boolean=} enable - Value to set the playlist enabled-state to 62701 * or if undefined returns the current enabled-state for the playlist 62702 * @return {Function} Function for setting/getting enabled 62703 */ 62704 62705 const enableFunction = (loader, playlistID, changePlaylistFn) => enable => { 62706 const playlist = loader.main.playlists[playlistID]; 62707 const incompatible = isIncompatible(playlist); 62708 const currentlyEnabled = isEnabled(playlist); 62709 if (typeof enable === 'undefined') { 62710 return currentlyEnabled; 62711 } 62712 if (enable) { 62713 delete playlist.disabled; 62714 } else { 62715 playlist.disabled = true; 62716 } 62717
vendor: 12,266 bytes, lines 62717-63012
62717if (enable !== currentlyEnabled && !incompatible) { 62718 // Ensure the outside world knows about our changes 62719 changePlaylistFn(); 62720 if (enable) { 62721 loader.trigger('renditionenabled'); 62722 } else { 62723 loader.trigger('renditiondisabled'); 62724 } 62725 } 62726 return enable; 62727 }; 62728 /** 62729 * The representation object encapsulates the publicly visible information 62730 * in a media playlist along with a setter/getter-type function (enabled) 62731 * for changing the enabled-state of a particular playlist entry 62732 * 62733 * @class Representation 62734 */ 62735 62736 class Representation { 62737 constructor(vhsHandler, playlist, id) { 62738 const { 62739 playlistController_: pc 62740 } = vhsHandler; 62741 const qualityChangeFunction = pc.fastQualityChange_.bind(pc); // some playlist attributes are optional 62742 62743 if (playlist.attributes) { 62744 const resolution = playlist.attributes.RESOLUTION; 62745 this.width = resolution && resolution.width; 62746 this.height = resolution && resolution.height; 62747 this.bandwidth = playlist.attributes.BANDWIDTH; 62748 this.frameRate = playlist.attributes['FRAME-RATE']; 62749 } 62750 this.codecs = codecsForPlaylist(pc.main(), playlist); 62751 this.playlist = playlist; // The id is simply the ordinality of the media playlist 62752 // within the main playlist 62753 62754 this.id = id; // Partially-apply the enableFunction to create a playlist- 62755 // specific variant 62756 62757 this.enabled = enableFunction(vhsHandler.playlists, playlist.id, qualityChangeFunction); 62758 } 62759 } 62760 /** 62761 * A mixin function that adds the `representations` api to an instance 62762 * of the VhsHandler class 62763 * 62764 * @param {VhsHandler} vhsHandler - An instance of VhsHandler to add the 62765 * representation API into 62766 */ 62767 62768 const renditionSelectionMixin = function (vhsHandler) { 62769 // Add a single API-specific function to the VhsHandler instance 62770 vhsHandler.representations = () => { 62771 const main = vhsHandler.playlistController_.main(); 62772 const playlists = isAudioOnly(main) ? vhsHandler.playlistController_.getAudioTrackPlaylists_() : main.playlists; 62773 if (!playlists) { 62774 return []; 62775 } 62776 return playlists.filter(media => !isIncompatible(media)).map((e, i) => new Representation(vhsHandler, e, e.id)); 62777 }; 62778 }; 62779 62780 /** 62781 * @file playback-watcher.js 62782 * 62783 * Playback starts, and now my watch begins. It shall not end until my death. I shall 62784 * take no wait, hold no uncleared timeouts, father no bad seeks. I shall wear no crowns 62785 * and win no glory. I shall live and die at my post. I am the corrector of the underflow. 62786 * I am the watcher of gaps. I am the shield that guards the realms of seekable. I pledge 62787 * my life and honor to the Playback Watch, for this Player and all the Players to come. 62788 */ 62789 62790 const timerCancelEvents = ['seeking', 'seeked', 'pause', 'playing', 'error']; 62791 /** 62792 * @class PlaybackWatcher 62793 */ 62794 62795 class PlaybackWatcher { 62796 /** 62797 * Represents an PlaybackWatcher object. 62798 * 62799 * @class 62800 * @param {Object} options an object that includes the tech and settings 62801 */ 62802 constructor(options) { 62803 this.playlistController_ = options.playlistController; 62804 this.tech_ = options.tech; 62805 this.seekable = options.seekable; 62806 this.allowSeeksWithinUnsafeLiveWindow = options.allowSeeksWithinUnsafeLiveWindow; 62807 this.liveRangeSafeTimeDelta = options.liveRangeSafeTimeDelta; 62808 this.media = options.media; 62809 this.consecutiveUpdates = 0; 62810 this.lastRecordedTime = null; 62811 this.checkCurrentTimeTimeout_ = null; 62812 this.logger_ = logger('PlaybackWatcher'); 62813 this.logger_('initialize'); 62814 const playHandler = () => this.monitorCurrentTime_(); 62815 const canPlayHandler = () => this.monitorCurrentTime_(); 62816 const waitingHandler = () => this.techWaiting_(); 62817 const cancelTimerHandler = () => this.resetTimeUpdate_(); 62818 const pc = this.playlistController_; 62819 const loaderTypes = ['main', 'subtitle', 'audio']; 62820 const loaderChecks = {}; 62821 loaderTypes.forEach(type => { 62822 loaderChecks[type] = { 62823 reset: () => this.resetSegmentDownloads_(type), 62824 updateend: () => this.checkSegmentDownloads_(type) 62825 }; 62826 pc[`${type}SegmentLoader_`].on('appendsdone', loaderChecks[type].updateend); // If a rendition switch happens during a playback stall where the buffer 62827 // isn't changing we want to reset. We cannot assume that the new rendition 62828 // will also be stalled, until after new appends. 62829 62830 pc[`${type}SegmentLoader_`].on('playlistupdate', loaderChecks[type].reset); // Playback stalls should not be detected right after seeking. 62831 // This prevents one segment playlists (single vtt or single segment content) 62832 // from being detected as stalling. As the buffer will not change in those cases, since 62833 // the buffer is the entire video duration. 62834 62835 this.tech_.on(['seeked', 'seeking'], loaderChecks[type].reset); 62836 }); 62837 /** 62838 * We check if a seek was into a gap through the following steps: 62839 * 1. We get a seeking event and we do not get a seeked event. This means that 62840 * a seek was attempted but not completed. 62841 * 2. We run `fixesBadSeeks_` on segment loader appends. This means that we already 62842 * removed everything from our buffer and appended a segment, and should be ready 62843 * to check for gaps. 62844 */ 62845 62846 const setSeekingHandlers = fn => { 62847 ['main', 'audio'].forEach(type => { 62848 pc[`${type}SegmentLoader_`][fn]('appended', this.seekingAppendCheck_); 62849 }); 62850 }; 62851 this.seekingAppendCheck_ = () => { 62852 if (this.fixesBadSeeks_()) { 62853 this.consecutiveUpdates = 0; 62854 this.lastRecordedTime = this.tech_.currentTime(); 62855 setSeekingHandlers('off'); 62856 } 62857 }; 62858 this.clearSeekingAppendCheck_ = () => setSeekingHandlers('off'); 62859 this.watchForBadSeeking_ = () => { 62860 this.clearSeekingAppendCheck_(); 62861 setSeekingHandlers('on'); 62862 }; 62863 this.tech_.on('seeked', this.clearSeekingAppendCheck_); 62864 this.tech_.on('seeking', this.watchForBadSeeking_); 62865 this.tech_.on('waiting', waitingHandler); 62866 this.tech_.on(timerCancelEvents, cancelTimerHandler); 62867 this.tech_.on('canplay', canPlayHandler); 62868 /* 62869 An edge case exists that results in gaps not being skipped when they exist at the beginning of a stream. This case 62870 is surfaced in one of two ways: 62871 1) The `waiting` event is fired before the player has buffered content, making it impossible 62872 to find or skip the gap. The `waiting` event is followed by a `play` event. On first play 62873 we can check if playback is stalled due to a gap, and skip the gap if necessary. 62874 2) A source with a gap at the beginning of the stream is loaded programatically while the player 62875 is in a playing state. To catch this case, it's important that our one-time play listener is setup 62876 even if the player is in a playing state 62877 */ 62878 62879 this.tech_.one('play', playHandler); // Define the dispose function to clean up our events 62880 62881 this.dispose = () => { 62882 this.clearSeekingAppendCheck_(); 62883 this.logger_('dispose'); 62884 this.tech_.off('waiting', waitingHandler); 62885 this.tech_.off(timerCancelEvents, cancelTimerHandler); 62886 this.tech_.off('canplay', canPlayHandler); 62887 this.tech_.off('play', playHandler); 62888 this.tech_.off('seeking', this.watchForBadSeeking_); 62889 this.tech_.off('seeked', this.clearSeekingAppendCheck_); 62890 loaderTypes.forEach(type => { 62891 pc[`${type}SegmentLoader_`].off('appendsdone', loaderChecks[type].updateend); 62892 pc[`${type}SegmentLoader_`].off('playlistupdate', loaderChecks[type].reset); 62893 this.tech_.off(['seeked', 'seeking'], loaderChecks[type].reset); 62894 }); 62895 if (this.checkCurrentTimeTimeout_) { 62896 window.clearTimeout(this.checkCurrentTimeTimeout_); 62897 } 62898 this.resetTimeUpdate_(); 62899 }; 62900 } 62901 /** 62902 * Periodically check current time to see if playback stopped 62903 * 62904 * @private 62905 */ 62906 62907 monitorCurrentTime_() { 62908 this.checkCurrentTime_(); 62909 if (this.checkCurrentTimeTimeout_) { 62910 window.clearTimeout(this.checkCurrentTimeTimeout_); 62911 } // 42 = 24 fps // 250 is what Webkit uses // FF uses 15 62912 62913 this.checkCurrentTimeTimeout_ = window.setTimeout(this.monitorCurrentTime_.bind(this), 250); 62914 } 62915 /** 62916 * Reset stalled download stats for a specific type of loader 62917 * 62918 * @param {string} type 62919 * The segment loader type to check. 62920 * 62921 * @listens SegmentLoader#playlistupdate 62922 * @listens Tech#seeking 62923 * @listens Tech#seeked 62924 */ 62925 62926 resetSegmentDownloads_(type) { 62927 const loader = this.playlistController_[`${type}SegmentLoader_`]; 62928 if (this[`${type}StalledDownloads_`] > 0) { 62929 this.logger_(`resetting possible stalled download count for ${type} loader`); 62930 } 62931 this[`${type}StalledDownloads_`] = 0; 62932 this[`${type}Buffered_`] = loader.buffered_(); 62933 } 62934 /** 62935 * Checks on every segment `appendsdone` to see 62936 * if segment appends are making progress. If they are not 62937 * and we are still downloading bytes. We exclude the playlist. 62938 * 62939 * @param {string} type 62940 * The segment loader type to check. 62941 * 62942 * @listens SegmentLoader#appendsdone 62943 */ 62944 62945 checkSegmentDownloads_(type) { 62946 const pc = this.playlistController_; 62947 const loader = pc[`${type}SegmentLoader_`]; 62948 const buffered = loader.buffered_(); 62949 const isBufferedDifferent = isRangeDifferent(this[`${type}Buffered_`], buffered); 62950 this[`${type}Buffered_`] = buffered; // if another watcher is going to fix the issue or 62951 // the buffered value for this loader changed 62952 // appends are working 62953 62954 if (isBufferedDifferent) { 62955 this.resetSegmentDownloads_(type); 62956 return; 62957 } 62958 this[`${type}StalledDownloads_`]++; 62959 this.logger_(`found #${this[`${type}StalledDownloads_`]} ${type} appends that did not increase buffer (possible stalled download)`, { 62960 playlistId: loader.playlist_ && loader.playlist_.id, 62961 buffered: timeRangesToArray(buffered) 62962 }); // after 10 possibly stalled appends with no reset, exclude 62963 62964 if (this[`${type}StalledDownloads_`] < 10) { 62965 return; 62966 } 62967 this.logger_(`${type} loader stalled download exclusion`); 62968 this.resetSegmentDownloads_(type); 62969 this.tech_.trigger({ 62970 type: 'usage', 62971 name: `vhs-${type}-download-exclusion` 62972 }); 62973 if (type === 'subtitle') { 62974 return; 62975 } // TODO: should we exclude audio tracks rather than main tracks 62976 // when type is audio? 62977 62978 pc.excludePlaylist({ 62979 error: { 62980 message: `Excessive ${type} segment downloading detected.` 62981 }, 62982 playlistExclusionDuration: Infinity 62983 }); 62984 } 62985 /** 62986 * The purpose of this function is to emulate the "waiting" event on 62987 * browsers that do not emit it when they are waiting for more 62988 * data to continue playback 62989 * 62990 * @private 62991 */ 62992 62993 checkCurrentTime_() { 62994 if (this.tech_.paused() || this.tech_.seeking()) { 62995 return; 62996 } 62997 const currentTime = this.tech_.currentTime(); 62998 const buffered = this.tech_.buffered(); 62999 if (this.lastRecordedTime === currentTime && (!buffered.length || currentTime + SAFE_TIME_DELTA >= buffered.end(buffered.length - 1))) { 63000 // If current time is at the end of the final buffered region, then any playback 63001 // stall is most likely caused by buffering in a low bandwidth environment. The tech 63002 // should fire a `waiting` event in this scenario, but due to browser and tech 63003 // inconsistencies. Calling `techWaiting_` here allows us to simulate 63004 // responding to a native `waiting` event when the tech fails to emit one. 63005 return this.techWaiting_(); 63006 } 63007 if (this.consecutiveUpdates >= 5 && currentTime === this.lastRecordedTime) { 63008 this.consecutiveUpdates++; 63009 this.waiting_(); 63010 } else if (currentTime === this.lastRecordedTime) { 63011 this.consecutiveUpdates++; 63012 } else {
vendor: 11,124 bytes, lines 63012-63273
63012 63013 this.consecutiveUpdates = 0; 63014 this.lastRecordedTime = currentTime; 63015 } 63016 } 63017 /** 63018 * Resets the 'timeupdate' mechanism designed to detect that we are stalled 63019 * 63020 * @private 63021 */ 63022 63023 resetTimeUpdate_() { 63024 this.consecutiveUpdates = 0; 63025 } 63026 /** 63027 * Fixes situations where there's a bad seek 63028 * 63029 * @return {boolean} whether an action was taken to fix the seek 63030 * @private 63031 */ 63032 63033 fixesBadSeeks_() { 63034 const seeking = this.tech_.seeking(); 63035 if (!seeking) { 63036 return false; 63037 } // TODO: It's possible that these seekable checks should be moved out of this function 63038 // and into a function that runs on seekablechange. It's also possible that we only need 63039 // afterSeekableWindow as the buffered check at the bottom is good enough to handle before 63040 // seekable range. 63041 63042 const seekable = this.seekable(); 63043 const currentTime = this.tech_.currentTime(); 63044 const isAfterSeekableRange = this.afterSeekableWindow_(seekable, currentTime, this.media(), this.allowSeeksWithinUnsafeLiveWindow); 63045 let seekTo; 63046 if (isAfterSeekableRange) { 63047 const seekableEnd = seekable.end(seekable.length - 1); // sync to live point (if VOD, our seekable was updated and we're simply adjusting) 63048 63049 seekTo = seekableEnd; 63050 } 63051 if (this.beforeSeekableWindow_(seekable, currentTime)) { 63052 const seekableStart = seekable.start(0); // sync to the beginning of the live window 63053 // provide a buffer of .1 seconds to handle rounding/imprecise numbers 63054 63055 seekTo = seekableStart + ( 63056 // if the playlist is too short and the seekable range is an exact time (can 63057 // happen in live with a 3 segment playlist), then don't use a time delta 63058 seekableStart === seekable.end(0) ? 0 : SAFE_TIME_DELTA); 63059 } 63060 if (typeof seekTo !== 'undefined') { 63061 this.logger_(`Trying to seek outside of seekable at time ${currentTime} with ` + `seekable range ${printableRange(seekable)}. Seeking to ` + `${seekTo}.`); 63062 this.tech_.setCurrentTime(seekTo); 63063 return true; 63064 } 63065 const sourceUpdater = this.playlistController_.sourceUpdater_; 63066 const buffered = this.tech_.buffered(); 63067 const audioBuffered = sourceUpdater.audioBuffer ? sourceUpdater.audioBuffered() : null; 63068 const videoBuffered = sourceUpdater.videoBuffer ? sourceUpdater.videoBuffered() : null; 63069 const media = this.media(); // verify that at least two segment durations or one part duration have been 63070 // appended before checking for a gap. 63071 63072 const minAppendedDuration = media.partTargetDuration ? media.partTargetDuration : (media.targetDuration - TIME_FUDGE_FACTOR) * 2; // verify that at least two segment durations have been 63073 // appended before checking for a gap. 63074 63075 const bufferedToCheck = [audioBuffered, videoBuffered]; 63076 for (let i = 0; i < bufferedToCheck.length; i++) { 63077 // skip null buffered 63078 if (!bufferedToCheck[i]) { 63079 continue; 63080 } 63081 const timeAhead = timeAheadOf(bufferedToCheck[i], currentTime); // if we are less than two video/audio segment durations or one part 63082 // duration behind we haven't appended enough to call this a bad seek. 63083 63084 if (timeAhead < minAppendedDuration) { 63085 return false; 63086 } 63087 } 63088 const nextRange = findNextRange(buffered, currentTime); // we have appended enough content, but we don't have anything buffered 63089 // to seek over the gap 63090 63091 if (nextRange.length === 0) { 63092 return false; 63093 } 63094 seekTo = nextRange.start(0) + SAFE_TIME_DELTA; 63095 this.logger_(`Buffered region starts (${nextRange.start(0)}) ` + ` just beyond seek point (${currentTime}). Seeking to ${seekTo}.`); 63096 this.tech_.setCurrentTime(seekTo); 63097 return true; 63098 } 63099 /** 63100 * Handler for situations when we determine the player is waiting. 63101 * 63102 * @private 63103 */ 63104 63105 waiting_() { 63106 if (this.techWaiting_()) { 63107 return; 63108 } // All tech waiting checks failed. Use last resort correction 63109 63110 const currentTime = this.tech_.currentTime(); 63111 const buffered = this.tech_.buffered(); 63112 const currentRange = findRange(buffered, currentTime); // Sometimes the player can stall for unknown reasons within a contiguous buffered 63113 // region with no indication that anything is amiss (seen in Firefox). Seeking to 63114 // currentTime is usually enough to kickstart the player. This checks that the player 63115 // is currently within a buffered region before attempting a corrective seek. 63116 // Chrome does not appear to continue `timeupdate` events after a `waiting` event 63117 // until there is ~ 3 seconds of forward buffer available. PlaybackWatcher should also 63118 // make sure there is ~3 seconds of forward buffer before taking any corrective action 63119 // to avoid triggering an `unknownwaiting` event when the network is slow. 63120 63121 if (currentRange.length && currentTime + 3 <= currentRange.end(0)) { 63122 this.resetTimeUpdate_(); 63123 this.tech_.setCurrentTime(currentTime); 63124 this.logger_(`Stopped at ${currentTime} while inside a buffered region ` + `[${currentRange.start(0)} -> ${currentRange.end(0)}]. Attempting to resume ` + 'playback by seeking to the current time.'); // unknown waiting corrections may be useful for monitoring QoS 63125 63126 this.tech_.trigger({ 63127 type: 'usage', 63128 name: 'vhs-unknown-waiting' 63129 }); 63130 return; 63131 } 63132 } 63133 /** 63134 * Handler for situations when the tech fires a `waiting` event 63135 * 63136 * @return {boolean} 63137 * True if an action (or none) was needed to correct the waiting. False if no 63138 * checks passed 63139 * @private 63140 */ 63141 63142 techWaiting_() { 63143 const seekable = this.seekable(); 63144 const currentTime = this.tech_.currentTime(); 63145 if (this.tech_.seeking()) { 63146 // Tech is seeking or already waiting on another action, no action needed 63147 return true; 63148 } 63149 if (this.beforeSeekableWindow_(seekable, currentTime)) { 63150 const livePoint = seekable.end(seekable.length - 1); 63151 this.logger_(`Fell out of live window at time ${currentTime}. Seeking to ` + `live point (seekable end) ${livePoint}`); 63152 this.resetTimeUpdate_(); 63153 this.tech_.setCurrentTime(livePoint); // live window resyncs may be useful for monitoring QoS 63154 63155 this.tech_.trigger({ 63156 type: 'usage', 63157 name: 'vhs-live-resync' 63158 }); 63159 return true; 63160 } 63161 const sourceUpdater = this.tech_.vhs.playlistController_.sourceUpdater_; 63162 const buffered = this.tech_.buffered(); 63163 const videoUnderflow = this.videoUnderflow_({ 63164 audioBuffered: sourceUpdater.audioBuffered(), 63165 videoBuffered: sourceUpdater.videoBuffered(), 63166 currentTime 63167 }); 63168 if (videoUnderflow) { 63169 // Even though the video underflowed and was stuck in a gap, the audio overplayed 63170 // the gap, leading currentTime into a buffered range. Seeking to currentTime 63171 // allows the video to catch up to the audio position without losing any audio 63172 // (only suffering ~3 seconds of frozen video and a pause in audio playback). 63173 this.resetTimeUpdate_(); 63174 this.tech_.setCurrentTime(currentTime); // video underflow may be useful for monitoring QoS 63175 63176 this.tech_.trigger({ 63177 type: 'usage', 63178 name: 'vhs-video-underflow' 63179 }); 63180 return true; 63181 } 63182 const nextRange = findNextRange(buffered, currentTime); // check for gap 63183 63184 if (nextRange.length > 0) { 63185 this.logger_(`Stopped at ${currentTime} and seeking to ${nextRange.start(0)}`); 63186 this.resetTimeUpdate_(); 63187 this.skipTheGap_(currentTime); 63188 return true; 63189 } // All checks failed. Returning false to indicate failure to correct waiting 63190 63191 return false; 63192 } 63193 afterSeekableWindow_(seekable, currentTime, playlist, allowSeeksWithinUnsafeLiveWindow = false) { 63194 if (!seekable.length) { 63195 // we can't make a solid case if there's no seekable, default to false 63196 return false; 63197 } 63198 let allowedEnd = seekable.end(seekable.length - 1) + SAFE_TIME_DELTA; 63199 const isLive = !playlist.endList; 63200 const isLLHLS = typeof playlist.partTargetDuration === 'number'; 63201 if (isLive && (isLLHLS || allowSeeksWithinUnsafeLiveWindow)) { 63202 allowedEnd = seekable.end(seekable.length - 1) + playlist.targetDuration * 3; 63203 } 63204 if (currentTime > allowedEnd) { 63205 return true; 63206 } 63207 return false; 63208 } 63209 beforeSeekableWindow_(seekable, currentTime) { 63210 if (seekable.length && 63211 // can't fall before 0 and 0 seekable start identifies VOD stream 63212 seekable.start(0) > 0 && currentTime < seekable.start(0) - this.liveRangeSafeTimeDelta) { 63213 return true; 63214 } 63215 return false; 63216 } 63217 videoUnderflow_({ 63218 videoBuffered, 63219 audioBuffered, 63220 currentTime 63221 }) { 63222 // audio only content will not have video underflow :) 63223 if (!videoBuffered) { 63224 return; 63225 } 63226 let gap; // find a gap in demuxed content. 63227 63228 if (videoBuffered.length && audioBuffered.length) { 63229 // in Chrome audio will continue to play for ~3s when we run out of video 63230 // so we have to check that the video buffer did have some buffer in the 63231 // past. 63232 const lastVideoRange = findRange(videoBuffered, currentTime - 3); 63233 const videoRange = findRange(videoBuffered, currentTime); 63234 const audioRange = findRange(audioBuffered, currentTime); 63235 if (audioRange.length && !videoRange.length && lastVideoRange.length) { 63236 gap = { 63237 start: lastVideoRange.end(0), 63238 end: audioRange.end(0) 63239 }; 63240 } // find a gap in muxed content. 63241 } else { 63242 const nextRange = findNextRange(videoBuffered, currentTime); // Even if there is no available next range, there is still a possibility we are 63243 // stuck in a gap due to video underflow. 63244 63245 if (!nextRange.length) { 63246 gap = this.gapFromVideoUnderflow_(videoBuffered, currentTime); 63247 } 63248 } 63249 if (gap) { 63250 this.logger_(`Encountered a gap in video from ${gap.start} to ${gap.end}. ` + `Seeking to current time ${currentTime}`); 63251 return true; 63252 } 63253 return false; 63254 } 63255 /** 63256 * Timer callback. If playback still has not proceeded, then we seek 63257 * to the start of the next buffered region. 63258 * 63259 * @private 63260 */ 63261 63262 skipTheGap_(scheduledCurrentTime) { 63263 const buffered = this.tech_.buffered(); 63264 const currentTime = this.tech_.currentTime(); 63265 const nextRange = findNextRange(buffered, currentTime); 63266 this.resetTimeUpdate_(); 63267 if (nextRange.length === 0 || currentTime !== scheduledCurrentTime) { 63268 return; 63269 } 63270 this.logger_('skipTheGap_:', 'currentTime:', currentTime, 'scheduled currentTime:', scheduledCurrentTime, 'nextRange start:', nextRange.start(0)); // only seek if we still have not played 63271 63272 this.tech_.setCurrentTime(nextRange.start(0) + TIME_FUDGE_FACTOR); 63273
vendor: 5,726 bytes, lines 63273-63449
63273this.tech_.trigger({ 63274 type: 'usage', 63275 name: 'vhs-gap-skip' 63276 }); 63277 } 63278 gapFromVideoUnderflow_(buffered, currentTime) { 63279 // At least in Chrome, if there is a gap in the video buffer, the audio will continue 63280 // playing for ~3 seconds after the video gap starts. This is done to account for 63281 // video buffer underflow/underrun (note that this is not done when there is audio 63282 // buffer underflow/underrun -- in that case the video will stop as soon as it 63283 // encounters the gap, as audio stalls are more noticeable/jarring to a user than 63284 // video stalls). The player's time will reflect the playthrough of audio, so the 63285 // time will appear as if we are in a buffered region, even if we are stuck in a 63286 // "gap." 63287 // 63288 // Example: 63289 // video buffer: 0 => 10.1, 10.2 => 20 63290 // audio buffer: 0 => 20 63291 // overall buffer: 0 => 10.1, 10.2 => 20 63292 // current time: 13 63293 // 63294 // Chrome's video froze at 10 seconds, where the video buffer encountered the gap, 63295 // however, the audio continued playing until it reached ~3 seconds past the gap 63296 // (13 seconds), at which point it stops as well. Since current time is past the 63297 // gap, findNextRange will return no ranges. 63298 // 63299 // To check for this issue, we see if there is a gap that starts somewhere within 63300 // a 3 second range (3 seconds +/- 1 second) back from our current time. 63301 const gaps = findGaps(buffered); 63302 for (let i = 0; i < gaps.length; i++) { 63303 const start = gaps.start(i); 63304 const end = gaps.end(i); // gap is starts no more than 4 seconds back 63305 63306 if (currentTime - start < 4 && currentTime - start > 2) { 63307 return { 63308 start, 63309 end 63310 }; 63311 } 63312 } 63313 return null; 63314 } 63315 } 63316 const defaultOptions = { 63317 errorInterval: 30, 63318 getSource(next) { 63319 const tech = this.tech({ 63320 IWillNotUseThisInPlugins: true 63321 }); 63322 const sourceObj = tech.currentSource_ || this.currentSource(); 63323 return next(sourceObj); 63324 } 63325 }; 63326 /** 63327 * Main entry point for the plugin 63328 * 63329 * @param {Player} player a reference to a videojs Player instance 63330 * @param {Object} [options] an object with plugin options 63331 * @private 63332 */ 63333 63334 const initPlugin = function (player, options) { 63335 let lastCalled = 0; 63336 let seekTo = 0; 63337 const localOptions = merge(defaultOptions, options); 63338 player.ready(() => { 63339 player.trigger({ 63340 type: 'usage', 63341 name: 'vhs-error-reload-initialized' 63342 }); 63343 }); 63344 /** 63345 * Player modifications to perform that must wait until `loadedmetadata` 63346 * has been triggered 63347 * 63348 * @private 63349 */ 63350 63351 const loadedMetadataHandler = function () { 63352 if (seekTo) { 63353 player.currentTime(seekTo); 63354 } 63355 }; 63356 /** 63357 * Set the source on the player element, play, and seek if necessary 63358 * 63359 * @param {Object} sourceObj An object specifying the source url and mime-type to play 63360 * @private 63361 */ 63362 63363 const setSource = function (sourceObj) { 63364 if (sourceObj === null || sourceObj === undefined) { 63365 return; 63366 } 63367 seekTo = player.duration() !== Infinity && player.currentTime() || 0; 63368 player.one('loadedmetadata', loadedMetadataHandler); 63369 player.src(sourceObj); 63370 player.trigger({ 63371 type: 'usage', 63372 name: 'vhs-error-reload' 63373 }); 63374 player.play(); 63375 }; 63376 /** 63377 * Attempt to get a source from either the built-in getSource function 63378 * or a custom function provided via the options 63379 * 63380 * @private 63381 */ 63382 63383 const errorHandler = function () { 63384 // Do not attempt to reload the source if a source-reload occurred before 63385 // 'errorInterval' time has elapsed since the last source-reload 63386 if (Date.now() - lastCalled < localOptions.errorInterval * 1000) { 63387 player.trigger({ 63388 type: 'usage', 63389 name: 'vhs-error-reload-canceled' 63390 }); 63391 return; 63392 } 63393 if (!localOptions.getSource || typeof localOptions.getSource !== 'function') { 63394 videojs.log.error('ERROR: reloadSourceOnError - The option getSource must be a function!'); 63395 return; 63396 } 63397 lastCalled = Date.now(); 63398 return localOptions.getSource.call(player, setSource); 63399 }; 63400 /** 63401 * Unbind any event handlers that were bound by the plugin 63402 * 63403 * @private 63404 */ 63405 63406 const cleanupEvents = function () { 63407 player.off('loadedmetadata', loadedMetadataHandler); 63408 player.off('error', errorHandler); 63409 player.off('dispose', cleanupEvents); 63410 }; 63411 /** 63412 * Cleanup before re-initializing the plugin 63413 * 63414 * @param {Object} [newOptions] an object with plugin options 63415 * @private 63416 */ 63417 63418 const reinitPlugin = function (newOptions) { 63419 cleanupEvents(); 63420 initPlugin(player, newOptions); 63421 }; 63422 player.on('error', errorHandler); 63423 player.on('dispose', cleanupEvents); // Overwrite the plugin function so that we can correctly cleanup before 63424 // initializing the plugin 63425 63426 player.reloadSourceOnError = reinitPlugin; 63427 }; 63428 /** 63429 * Reload the source when an error is detected as long as there 63430 * wasn't an error previously within the last 30 seconds 63431 * 63432 * @param {Object} [options] an object with plugin options 63433 */ 63434 63435 const reloadSourceOnError = function (options) { 63436 initPlugin(this, options); 63437 }; 63438 var version$4 = "3.9.1"; 63439 var version$3 = "7.0.2"; 63440 var version$2 = "1.3.0"; 63441 var version$1 = "7.1.0"; 63442 var version = "4.0.1"; 63443 63444 /** 63445 * @file videojs-http-streaming.js 63446 * 63447 * The main file for the VHS project. 63448 * License: https://github.com/videojs/videojs-http-streaming/blob/main/LICENSE 63449 */
vendor: 20,454 bytes, lines 63449-64004
63449 63450 const Vhs = { 63451 PlaylistLoader, 63452 Playlist, 63453 utils, 63454 STANDARD_PLAYLIST_SELECTOR: lastBandwidthSelector, 63455 INITIAL_PLAYLIST_SELECTOR: lowestBitrateCompatibleVariantSelector, 63456 lastBandwidthSelector, 63457 movingAverageBandwidthSelector, 63458 comparePlaylistBandwidth, 63459 comparePlaylistResolution, 63460 xhr: xhrFactory() 63461 }; // Define getter/setters for config properties 63462 63463 Object.keys(Config).forEach(prop => { 63464 Object.defineProperty(Vhs, prop, { 63465 get() { 63466 videojs.log.warn(`using Vhs.${prop} is UNSAFE be sure you know what you are doing`); 63467 return Config[prop]; 63468 }, 63469 set(value) { 63470 videojs.log.warn(`using Vhs.${prop} is UNSAFE be sure you know what you are doing`); 63471 if (typeof value !== 'number' || value < 0) { 63472 videojs.log.warn(`value of Vhs.${prop} must be greater than or equal to 0`); 63473 return; 63474 } 63475 Config[prop] = value; 63476 } 63477 }); 63478 }); 63479 const LOCAL_STORAGE_KEY = 'videojs-vhs'; 63480 /** 63481 * Updates the selectedIndex of the QualityLevelList when a mediachange happens in vhs. 63482 * 63483 * @param {QualityLevelList} qualityLevels The QualityLevelList to update. 63484 * @param {PlaylistLoader} playlistLoader PlaylistLoader containing the new media info. 63485 * @function handleVhsMediaChange 63486 */ 63487 63488 const handleVhsMediaChange = function (qualityLevels, playlistLoader) { 63489 const newPlaylist = playlistLoader.media(); 63490 let selectedIndex = -1; 63491 for (let i = 0; i < qualityLevels.length; i++) { 63492 if (qualityLevels[i].id === newPlaylist.id) { 63493 selectedIndex = i; 63494 break; 63495 } 63496 } 63497 qualityLevels.selectedIndex_ = selectedIndex; 63498 qualityLevels.trigger({ 63499 selectedIndex, 63500 type: 'change' 63501 }); 63502 }; 63503 /** 63504 * Adds quality levels to list once playlist metadata is available 63505 * 63506 * @param {QualityLevelList} qualityLevels The QualityLevelList to attach events to. 63507 * @param {Object} vhs Vhs object to listen to for media events. 63508 * @function handleVhsLoadedMetadata 63509 */ 63510 63511 const handleVhsLoadedMetadata = function (qualityLevels, vhs) { 63512 vhs.representations().forEach(rep => { 63513 qualityLevels.addQualityLevel(rep); 63514 }); 63515 handleVhsMediaChange(qualityLevels, vhs.playlists); 63516 }; // VHS is a source handler, not a tech. Make sure attempts to use it 63517 // as one do not cause exceptions. 63518 63519 Vhs.canPlaySource = function () { 63520 return videojs.log.warn('VHS is no longer a tech. Please remove it from ' + 'your player\'s techOrder.'); 63521 }; 63522 const emeKeySystems = (keySystemOptions, mainPlaylist, audioPlaylist) => { 63523 if (!keySystemOptions) { 63524 return keySystemOptions; 63525 } 63526 let codecs = {}; 63527 if (mainPlaylist && mainPlaylist.attributes && mainPlaylist.attributes.CODECS) { 63528 codecs = unwrapCodecList(parseCodecs(mainPlaylist.attributes.CODECS)); 63529 } 63530 if (audioPlaylist && audioPlaylist.attributes && audioPlaylist.attributes.CODECS) { 63531 codecs.audio = audioPlaylist.attributes.CODECS; 63532 } 63533 const videoContentType = getMimeForCodec(codecs.video); 63534 const audioContentType = getMimeForCodec(codecs.audio); // upsert the content types based on the selected playlist 63535 63536 const keySystemContentTypes = {}; 63537 for (const keySystem in keySystemOptions) { 63538 keySystemContentTypes[keySystem] = {}; 63539 if (audioContentType) { 63540 keySystemContentTypes[keySystem].audioContentType = audioContentType; 63541 } 63542 if (videoContentType) { 63543 keySystemContentTypes[keySystem].videoContentType = videoContentType; 63544 } // Default to using the video playlist's PSSH even though they may be different, as 63545 // videojs-contrib-eme will only accept one in the options. 63546 // 63547 // This shouldn't be an issue for most cases as early intialization will handle all 63548 // unique PSSH values, and if they aren't, then encrypted events should have the 63549 // specific information needed for the unique license. 63550 63551 if (mainPlaylist.contentProtection && mainPlaylist.contentProtection[keySystem] && mainPlaylist.contentProtection[keySystem].pssh) { 63552 keySystemContentTypes[keySystem].pssh = mainPlaylist.contentProtection[keySystem].pssh; 63553 } // videojs-contrib-eme accepts the option of specifying: 'com.some.cdm': 'url' 63554 // so we need to prevent overwriting the URL entirely 63555 63556 if (typeof keySystemOptions[keySystem] === 'string') { 63557 keySystemContentTypes[keySystem].url = keySystemOptions[keySystem]; 63558 } 63559 } 63560 return merge(keySystemOptions, keySystemContentTypes); 63561 }; 63562 /** 63563 * @typedef {Object} KeySystems 63564 * 63565 * keySystems configuration for https://github.com/videojs/videojs-contrib-eme 63566 * Note: not all options are listed here. 63567 * 63568 * @property {Uint8Array} [pssh] 63569 * Protection System Specific Header 63570 */ 63571 63572 /** 63573 * Goes through all the playlists and collects an array of KeySystems options objects 63574 * containing each playlist's keySystems and their pssh values, if available. 63575 * 63576 * @param {Object[]} playlists 63577 * The playlists to look through 63578 * @param {string[]} keySystems 63579 * The keySystems to collect pssh values for 63580 * 63581 * @return {KeySystems[]} 63582 * An array of KeySystems objects containing available key systems and their 63583 * pssh values 63584 */ 63585 63586 const getAllPsshKeySystemsOptions = (playlists, keySystems) => { 63587 return playlists.reduce((keySystemsArr, playlist) => { 63588 if (!playlist.contentProtection) { 63589 return keySystemsArr; 63590 } 63591 const keySystemsOptions = keySystems.reduce((keySystemsObj, keySystem) => { 63592 const keySystemOptions = playlist.contentProtection[keySystem]; 63593 if (keySystemOptions && keySystemOptions.pssh) { 63594 keySystemsObj[keySystem] = { 63595 pssh: keySystemOptions.pssh 63596 }; 63597 } 63598 return keySystemsObj; 63599 }, {}); 63600 if (Object.keys(keySystemsOptions).length) { 63601 keySystemsArr.push(keySystemsOptions); 63602 } 63603 return keySystemsArr; 63604 }, []); 63605 }; 63606 /** 63607 * Returns a promise that waits for the 63608 * [eme plugin](https://github.com/videojs/videojs-contrib-eme) to create a key session. 63609 * 63610 * Works around https://bugs.chromium.org/p/chromium/issues/detail?id=895449 in non-IE11 63611 * browsers. 63612 * 63613 * As per the above ticket, this is particularly important for Chrome, where, if 63614 * unencrypted content is appended before encrypted content and the key session has not 63615 * been created, a MEDIA_ERR_DECODE will be thrown once the encrypted content is reached 63616 * during playback. 63617 * 63618 * @param {Object} player 63619 * The player instance 63620 * @param {Object[]} sourceKeySystems 63621 * The key systems options from the player source 63622 * @param {Object} [audioMedia] 63623 * The active audio media playlist (optional) 63624 * @param {Object[]} mainPlaylists 63625 * The playlists found on the main playlist object 63626 * 63627 * @return {Object} 63628 * Promise that resolves when the key session has been created 63629 */ 63630 63631 const waitForKeySessionCreation = ({ 63632 player, 63633 sourceKeySystems, 63634 audioMedia, 63635 mainPlaylists 63636 }) => { 63637 if (!player.eme.initializeMediaKeys) { 63638 return Promise.resolve(); 63639 } // TODO should all audio PSSH values be initialized for DRM? 63640 // 63641 // All unique video rendition pssh values are initialized for DRM, but here only 63642 // the initial audio playlist license is initialized. In theory, an encrypted 63643 // event should be fired if the user switches to an alternative audio playlist 63644 // where a license is required, but this case hasn't yet been tested. In addition, there 63645 // may be many alternate audio playlists unlikely to be used (e.g., multiple different 63646 // languages). 63647 63648 const playlists = audioMedia ? mainPlaylists.concat([audioMedia]) : mainPlaylists; 63649 const keySystemsOptionsArr = getAllPsshKeySystemsOptions(playlists, Object.keys(sourceKeySystems)); 63650 const initializationFinishedPromises = []; 63651 const keySessionCreatedPromises = []; // Since PSSH values are interpreted as initData, EME will dedupe any duplicates. The 63652 // only place where it should not be deduped is for ms-prefixed APIs, but 63653 // the existence of modern EME APIs in addition to 63654 // ms-prefixed APIs on Edge should prevent this from being a concern. 63655 // initializeMediaKeys also won't use the webkit-prefixed APIs. 63656 63657 keySystemsOptionsArr.forEach(keySystemsOptions => { 63658 keySessionCreatedPromises.push(new Promise((resolve, reject) => { 63659 player.tech_.one('keysessioncreated', resolve); 63660 })); 63661 initializationFinishedPromises.push(new Promise((resolve, reject) => { 63662 player.eme.initializeMediaKeys({ 63663 keySystems: keySystemsOptions 63664 }, err => { 63665 if (err) { 63666 reject(err); 63667 return; 63668 } 63669 resolve(); 63670 }); 63671 })); 63672 }); // The reasons Promise.race is chosen over Promise.any: 63673 // 63674 // * Promise.any is only available in Safari 14+. 63675 // * None of these promises are expected to reject. If they do reject, it might be 63676 // better here for the race to surface the rejection, rather than mask it by using 63677 // Promise.any. 63678 63679 return Promise.race([ 63680 // If a session was previously created, these will all finish resolving without 63681 // creating a new session, otherwise it will take until the end of all license 63682 // requests, which is why the key session check is used (to make setup much faster). 63683 Promise.all(initializationFinishedPromises), 63684 // Once a single session is created, the browser knows DRM will be used. 63685 Promise.race(keySessionCreatedPromises)]); 63686 }; 63687 /** 63688 * If the [eme](https://github.com/videojs/videojs-contrib-eme) plugin is available, and 63689 * there are keySystems on the source, sets up source options to prepare the source for 63690 * eme. 63691 * 63692 * @param {Object} player 63693 * The player instance 63694 * @param {Object[]} sourceKeySystems 63695 * The key systems options from the player source 63696 * @param {Object} media 63697 * The active media playlist 63698 * @param {Object} [audioMedia] 63699 * The active audio media playlist (optional) 63700 * 63701 * @return {boolean} 63702 * Whether or not options were configured and EME is available 63703 */ 63704 63705 const setupEmeOptions = ({ 63706 player, 63707 sourceKeySystems, 63708 media, 63709 audioMedia 63710 }) => { 63711 const sourceOptions = emeKeySystems(sourceKeySystems, media, audioMedia); 63712 if (!sourceOptions) { 63713 return false; 63714 } 63715 player.currentSource().keySystems = sourceOptions; // eme handles the rest of the setup, so if it is missing 63716 // do nothing. 63717 63718 if (sourceOptions && !player.eme) { 63719 videojs.log.warn('DRM encrypted source cannot be decrypted without a DRM plugin'); 63720 return false; 63721 } 63722 return true; 63723 }; 63724 const getVhsLocalStorage = () => { 63725 if (!window.localStorage) { 63726 return null; 63727 } 63728 const storedObject = window.localStorage.getItem(LOCAL_STORAGE_KEY); 63729 if (!storedObject) { 63730 return null; 63731 } 63732 try { 63733 return JSON.parse(storedObject); 63734 } catch (e) { 63735 // someone may have tampered with the value 63736 return null; 63737 } 63738 }; 63739 const updateVhsLocalStorage = options => { 63740 if (!window.localStorage) { 63741 return false; 63742 } 63743 let objectToStore = getVhsLocalStorage(); 63744 objectToStore = objectToStore ? merge(objectToStore, options) : options; 63745 try { 63746 window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(objectToStore)); 63747 } catch (e) { 63748 // Throws if storage is full (e.g., always on iOS 5+ Safari private mode, where 63749 // storage is set to 0). 63750 // https://developer.mozilla.org/en-US/docs/Web/API/Storage/setItem#Exceptions 63751 // No need to perform any operation. 63752 return false; 63753 } 63754 return objectToStore; 63755 }; 63756 /** 63757 * Parses VHS-supported media types from data URIs. See 63758 * https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs 63759 * for information on data URIs. 63760 * 63761 * @param {string} dataUri 63762 * The data URI 63763 * 63764 * @return {string|Object} 63765 * The parsed object/string, or the original string if no supported media type 63766 * was found 63767 */ 63768 63769 const expandDataUri = dataUri => { 63770 if (dataUri.toLowerCase().indexOf('data:application/vnd.videojs.vhs+json,') === 0) { 63771 return JSON.parse(dataUri.substring(dataUri.indexOf(',') + 1)); 63772 } // no known case for this data URI, return the string as-is 63773 63774 return dataUri; 63775 }; 63776 /** 63777 * Adds a request hook to an xhr object 63778 * 63779 * @param {Object} xhr object to add the onRequest hook to 63780 * @param {function} callback hook function for an xhr request 63781 */ 63782 63783 const addOnRequestHook = (xhr, callback) => { 63784 if (!xhr._requestCallbackSet) { 63785 xhr._requestCallbackSet = new Set(); 63786 } 63787 xhr._requestCallbackSet.add(callback); 63788 }; 63789 /** 63790 * Adds a response hook to an xhr object 63791 * 63792 * @param {Object} xhr object to add the onResponse hook to 63793 * @param {function} callback hook function for an xhr response 63794 */ 63795 63796 const addOnResponseHook = (xhr, callback) => { 63797 if (!xhr._responseCallbackSet) { 63798 xhr._responseCallbackSet = new Set(); 63799 } 63800 xhr._responseCallbackSet.add(callback); 63801 }; 63802 /** 63803 * Removes a request hook on an xhr object, deletes the onRequest set if empty. 63804 * 63805 * @param {Object} xhr object to remove the onRequest hook from 63806 * @param {function} callback hook function to remove 63807 */ 63808 63809 const removeOnRequestHook = (xhr, callback) => { 63810 if (!xhr._requestCallbackSet) { 63811 return; 63812 } 63813 xhr._requestCallbackSet.delete(callback); 63814 if (!xhr._requestCallbackSet.size) { 63815 delete xhr._requestCallbackSet; 63816 } 63817 }; 63818 /** 63819 * Removes a response hook on an xhr object, deletes the onResponse set if empty. 63820 * 63821 * @param {Object} xhr object to remove the onResponse hook from 63822 * @param {function} callback hook function to remove 63823 */ 63824 63825 const removeOnResponseHook = (xhr, callback) => { 63826 if (!xhr._responseCallbackSet) { 63827 return; 63828 } 63829 xhr._responseCallbackSet.delete(callback); 63830 if (!xhr._responseCallbackSet.size) { 63831 delete xhr._responseCallbackSet; 63832 } 63833 }; 63834 /** 63835 * Whether the browser has built-in HLS support. 63836 */ 63837 63838 Vhs.supportsNativeHls = function () { 63839 if (!document || !document.createElement) { 63840 return false; 63841 } 63842 const video = document.createElement('video'); // native HLS is definitely not supported if HTML5 video isn't 63843 63844 if (!videojs.getTech('Html5').isSupported()) { 63845 return false; 63846 } // HLS manifests can go by many mime-types 63847 63848 const canPlay = [ 63849 // Apple santioned 63850 'application/vnd.apple.mpegurl', 63851 // Apple sanctioned for backwards compatibility 63852 'audio/mpegurl', 63853 // Very common 63854 'audio/x-mpegurl', 63855 // Very common 63856 'application/x-mpegurl', 63857 // Included for completeness 63858 'video/x-mpegurl', 'video/mpegurl', 'application/mpegurl']; 63859 return canPlay.some(function (canItPlay) { 63860 return /maybe|probably/i.test(video.canPlayType(canItPlay)); 63861 }); 63862 }(); 63863 Vhs.supportsNativeDash = function () { 63864 if (!document || !document.createElement || !videojs.getTech('Html5').isSupported()) { 63865 return false; 63866 } 63867 return /maybe|probably/i.test(document.createElement('video').canPlayType('application/dash+xml')); 63868 }(); 63869 Vhs.supportsTypeNatively = type => { 63870 if (type === 'hls') { 63871 return Vhs.supportsNativeHls; 63872 } 63873 if (type === 'dash') { 63874 return Vhs.supportsNativeDash; 63875 } 63876 return false; 63877 }; 63878 /** 63879 * VHS is a source handler, not a tech. Make sure attempts to use it 63880 * as one do not cause exceptions. 63881 */ 63882 63883 Vhs.isSupported = function () { 63884 return videojs.log.warn('VHS is no longer a tech. Please remove it from ' + 'your player\'s techOrder.'); 63885 }; 63886 /** 63887 * A global function for setting an onRequest hook 63888 * 63889 * @param {function} callback for request modifiction 63890 */ 63891 63892 Vhs.xhr.onRequest = function (callback) { 63893 addOnRequestHook(Vhs.xhr, callback); 63894 }; 63895 /** 63896 * A global function for setting an onResponse hook 63897 * 63898 * @param {callback} callback for response data retrieval 63899 */ 63900 63901 Vhs.xhr.onResponse = function (callback) { 63902 addOnResponseHook(Vhs.xhr, callback); 63903 }; 63904 /** 63905 * Deletes a global onRequest callback if it exists 63906 * 63907 * @param {function} callback to delete from the global set 63908 */ 63909 63910 Vhs.xhr.offRequest = function (callback) { 63911 removeOnRequestHook(Vhs.xhr, callback); 63912 }; 63913 /** 63914 * Deletes a global onResponse callback if it exists 63915 * 63916 * @param {function} callback to delete from the global set 63917 */ 63918 63919 Vhs.xhr.offResponse = function (callback) { 63920 removeOnResponseHook(Vhs.xhr, callback); 63921 }; 63922 const Component = videojs.getComponent('Component'); 63923 /** 63924 * The Vhs Handler object, where we orchestrate all of the parts 63925 * of VHS to interact with video.js 63926 * 63927 * @class VhsHandler 63928 * @extends videojs.Component 63929 * @param {Object} source the soruce object 63930 * @param {Tech} tech the parent tech object 63931 * @param {Object} options optional and required options 63932 */ 63933 63934 class VhsHandler extends Component { 63935 constructor(source, tech, options) { 63936 super(tech, options.vhs); // if a tech level `initialBandwidth` option was passed 63937 // use that over the VHS level `bandwidth` option 63938 63939 if (typeof options.initialBandwidth === 'number') { 63940 this.options_.bandwidth = options.initialBandwidth; 63941 } 63942 this.logger_ = logger('VhsHandler'); // we need access to the player in some cases, 63943 // so, get it from Video.js via the `playerId` 63944 63945 if (tech.options_ && tech.options_.playerId) { 63946 const _player = videojs.getPlayer(tech.options_.playerId); 63947 this.player_ = _player; 63948 } 63949 this.tech_ = tech; 63950 this.source_ = source; 63951 this.stats = {}; 63952 this.ignoreNextSeekingEvent_ = false; 63953 this.setOptions_(); 63954 if (this.options_.overrideNative && tech.overrideNativeAudioTracks && tech.overrideNativeVideoTracks) { 63955 tech.overrideNativeAudioTracks(true); 63956 tech.overrideNativeVideoTracks(true); 63957 } else if (this.options_.overrideNative && (tech.featuresNativeVideoTracks || tech.featuresNativeAudioTracks)) { 63958 // overriding native VHS only works if audio tracks have been emulated 63959 // error early if we're misconfigured 63960 throw new Error('Overriding native VHS requires emulated tracks. ' + 'See https://git.io/vMpjB'); 63961 } // listen for fullscreenchange events for this player so that we 63962 // can adjust our quality selection quickly 63963 63964 this.on(document, ['fullscreenchange', 'webkitfullscreenchange', 'mozfullscreenchange', 'MSFullscreenChange'], event => { 63965 const fullscreenElement = document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement; 63966 if (fullscreenElement && fullscreenElement.contains(this.tech_.el())) { 63967 this.playlistController_.fastQualityChange_(); 63968 } else { 63969 // When leaving fullscreen, since the in page pixel dimensions should be smaller 63970 // than full screen, see if there should be a rendition switch down to preserve 63971 // bandwidth. 63972 this.playlistController_.checkABR_(); 63973 } 63974 }); 63975 this.on(this.tech_, 'seeking', function () { 63976 if (this.ignoreNextSeekingEvent_) { 63977 this.ignoreNextSeekingEvent_ = false; 63978 return; 63979 } 63980 this.setCurrentTime(this.tech_.currentTime()); 63981 }); 63982 this.on(this.tech_, 'error', function () { 63983 // verify that the error was real and we are loaded 63984 // enough to have pc loaded. 63985 if (this.tech_.error() && this.playlistController_) { 63986 this.playlistController_.pauseLoading(); 63987 } 63988 }); 63989 this.on(this.tech_, 'play', this.play); 63990 } 63991 /** 63992 * Set VHS options based on options from configuration, as well as partial 63993 * options to be passed at a later time. 63994 * 63995 * @param {Object} options A partial chunk of config options 63996 */ 63997 63998 setOptions_(options = {}) { 63999 this.options_ = merge(this.options_, options); // defaults 64000 64001 this.options_.withCredentials = this.options_.withCredentials || false; 64002 this.options_.limitRenditionByPlayerDimensions = this.options_.limitRenditionByPlayerDimensions === false ? false : true; 64003 this.options_.useDevicePixelRatio = this.options_.useDevicePixelRatio || false; 64004 this.options_.use
vendor: 4,196 bytes, lines 64004-64085
64004BandwidthFromLocalStorage = typeof this.source_.useBandwidthFromLocalStorage !== 'undefined' ? this.source_.useBandwidthFromLocalStorage : this.options_.useBandwidthFromLocalStorage || false; 64005 this.options_.useForcedSubtitles = this.options_.useForcedSubtitles || false; 64006 this.options_.useNetworkInformationApi = this.options_.useNetworkInformationApi || false; 64007 this.options_.useDtsForTimestampOffset = this.options_.useDtsForTimestampOffset || false; 64008 this.options_.customTagParsers = this.options_.customTagParsers || []; 64009 this.options_.customTagMappers = this.options_.customTagMappers || []; 64010 this.options_.cacheEncryptionKeys = this.options_.cacheEncryptionKeys || false; 64011 this.options_.llhls = this.options_.llhls === false ? false : true; 64012 this.options_.bufferBasedABR = this.options_.bufferBasedABR || false; 64013 if (typeof this.options_.playlistExclusionDuration !== 'number') { 64014 this.options_.playlistExclusionDuration = 60; 64015 } 64016 if (typeof this.options_.bandwidth !== 'number') { 64017 if (this.options_.useBandwidthFromLocalStorage) { 64018 const storedObject = getVhsLocalStorage(); 64019 if (storedObject && storedObject.bandwidth) { 64020 this.options_.bandwidth = storedObject.bandwidth; 64021 this.tech_.trigger({ 64022 type: 'usage', 64023 name: 'vhs-bandwidth-from-local-storage' 64024 }); 64025 } 64026 if (storedObject && storedObject.throughput) { 64027 this.options_.throughput = storedObject.throughput; 64028 this.tech_.trigger({ 64029 type: 'usage', 64030 name: 'vhs-throughput-from-local-storage' 64031 }); 64032 } 64033 } 64034 } // if bandwidth was not set by options or pulled from local storage, start playlist 64035 // selection at a reasonable bandwidth 64036 64037 if (typeof this.options_.bandwidth !== 'number') { 64038 this.options_.bandwidth = Config.INITIAL_BANDWIDTH; 64039 } // If the bandwidth number is unchanged from the initial setting 64040 // then this takes precedence over the enableLowInitialPlaylist option 64041 64042 this.options_.enableLowInitialPlaylist = this.options_.enableLowInitialPlaylist && this.options_.bandwidth === Config.INITIAL_BANDWIDTH; // grab options passed to player.src 64043 64044 ['withCredentials', 'useDevicePixelRatio', 'limitRenditionByPlayerDimensions', 'bandwidth', 'customTagParsers', 'customTagMappers', 'cacheEncryptionKeys', 'playlistSelector', 'initialPlaylistSelector', 'bufferBasedABR', 'liveRangeSafeTimeDelta', 'llhls', 'useForcedSubtitles', 'useNetworkInformationApi', 'useDtsForTimestampOffset', 'exactManifestTimings', 'leastPixelDiffSelector'].forEach(option => { 64045 if (typeof this.source_[option] !== 'undefined') { 64046 this.options_[option] = this.source_[option]; 64047 } 64048 }); 64049 this.limitRenditionByPlayerDimensions = this.options_.limitRenditionByPlayerDimensions; 64050 this.useDevicePixelRatio = this.options_.useDevicePixelRatio; 64051 } // alias for public method to set options 64052 64053 setOptions(options = {}) { 64054 this.setOptions_(options); 64055 } 64056 /** 64057 * called when player.src gets called, handle a new source 64058 * 64059 * @param {Object} src the source object to handle 64060 */ 64061 64062 src(src, type) { 64063 // do nothing if the src is falsey 64064 if (!src) { 64065 return; 64066 } 64067 this.setOptions_(); // add main playlist controller options 64068 64069 this.options_.src = expandDataUri(this.source_.src); 64070 this.options_.tech = this.tech_; 64071 this.options_.externVhs = Vhs; 64072 this.options_.sourceType = simpleTypeFromSourceType(type); // Whenever we seek internally, we should update the tech 64073 64074 this.options_.seekTo = time => { 64075 this.tech_.setCurrentTime(time); 64076 }; 64077 this.playlistController_ = new PlaylistController(this.options_); 64078 const playbackWatcherOptions = merge({ 64079 liveRangeSafeTimeDelta: SAFE_TIME_DELTA 64080 }, this.options_, { 64081 seekable: () => this.seekable(), 64082 media: () => this.playlistController_.media(), 64083 playlistController: this.playlistController_ 64084 }); 64085 this.playbackWatcher_ = new PlaybackWatcher(playbackWatcherOptions
vendor: 9,435 bytes, lines 64085-64319
64085); 64086 this.playlistController_.on('error', () => { 64087 const player = videojs.players[this.tech_.options_.playerId]; 64088 let error = this.playlistController_.error; 64089 if (typeof error === 'object' && !error.code) { 64090 error.code = 3; 64091 } else if (typeof error === 'string') { 64092 error = { 64093 message: error, 64094 code: 3 64095 }; 64096 } 64097 player.error(error); 64098 }); 64099 const defaultSelector = this.options_.bufferBasedABR ? Vhs.movingAverageBandwidthSelector(0.55) : Vhs.STANDARD_PLAYLIST_SELECTOR; // `this` in selectPlaylist should be the VhsHandler for backwards 64100 // compatibility with < v2 64101 64102 this.playlistController_.selectPlaylist = this.selectPlaylist ? this.selectPlaylist.bind(this) : defaultSelector.bind(this); 64103 this.playlistController_.selectInitialPlaylist = Vhs.INITIAL_PLAYLIST_SELECTOR.bind(this); // re-expose some internal objects for backwards compatibility with < v2 64104 64105 this.playlists = this.playlistController_.mainPlaylistLoader_; 64106 this.mediaSource = this.playlistController_.mediaSource; // Proxy assignment of some properties to the main playlist 64107 // controller. Using a custom property for backwards compatibility 64108 // with < v2 64109 64110 Object.defineProperties(this, { 64111 selectPlaylist: { 64112 get() { 64113 return this.playlistController_.selectPlaylist; 64114 }, 64115 set(selectPlaylist) { 64116 this.playlistController_.selectPlaylist = selectPlaylist.bind(this); 64117 } 64118 }, 64119 throughput: { 64120 get() { 64121 return this.playlistController_.mainSegmentLoader_.throughput.rate; 64122 }, 64123 set(throughput) { 64124 this.playlistController_.mainSegmentLoader_.throughput.rate = throughput; // By setting `count` to 1 the throughput value becomes the starting value 64125 // for the cumulative average 64126 64127 this.playlistController_.mainSegmentLoader_.throughput.count = 1; 64128 } 64129 }, 64130 bandwidth: { 64131 get() { 64132 let playerBandwidthEst = this.playlistController_.mainSegmentLoader_.bandwidth; 64133 const networkInformation = window.navigator.connection || window.navigator.mozConnection || window.navigator.webkitConnection; 64134 const tenMbpsAsBitsPerSecond = 10e6; 64135 if (this.options_.useNetworkInformationApi && networkInformation) { 64136 // downlink returns Mbps 64137 // https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/downlink 64138 const networkInfoBandwidthEstBitsPerSec = networkInformation.downlink * 1000 * 1000; // downlink maxes out at 10 Mbps. In the event that both networkInformationApi and the player 64139 // estimate a bandwidth greater than 10 Mbps, use the larger of the two estimates to ensure that 64140 // high quality streams are not filtered out. 64141 64142 if (networkInfoBandwidthEstBitsPerSec >= tenMbpsAsBitsPerSecond && playerBandwidthEst >= tenMbpsAsBitsPerSecond) { 64143 playerBandwidthEst = Math.max(playerBandwidthEst, networkInfoBandwidthEstBitsPerSec); 64144 } else { 64145 playerBandwidthEst = networkInfoBandwidthEstBitsPerSec; 64146 } 64147 } 64148 return playerBandwidthEst; 64149 }, 64150 set(bandwidth) { 64151 this.playlistController_.mainSegmentLoader_.bandwidth = bandwidth; // setting the bandwidth manually resets the throughput counter 64152 // `count` is set to zero that current value of `rate` isn't included 64153 // in the cumulative average 64154 64155 this.playlistController_.mainSegmentLoader_.throughput = { 64156 rate: 0, 64157 count: 0 64158 }; 64159 } 64160 }, 64161 /** 64162 * `systemBandwidth` is a combination of two serial processes bit-rates. The first 64163 * is the network bitrate provided by `bandwidth` and the second is the bitrate of 64164 * the entire process after that - decryption, transmuxing, and appending - provided 64165 * by `throughput`. 64166 * 64167 * Since the two process are serial, the overall system bandwidth is given by: 64168 * sysBandwidth = 1 / (1 / bandwidth + 1 / throughput) 64169 */ 64170 systemBandwidth: { 64171 get() { 64172 const invBandwidth = 1 / (this.bandwidth || 1); 64173 let invThroughput; 64174 if (this.throughput > 0) { 64175 invThroughput = 1 / this.throughput; 64176 } else { 64177 invThroughput = 0; 64178 } 64179 const systemBitrate = Math.floor(1 / (invBandwidth + invThroughput)); 64180 return systemBitrate; 64181 }, 64182 set() { 64183 videojs.log.error('The "systemBandwidth" property is read-only'); 64184 } 64185 } 64186 }); 64187 if (this.options_.bandwidth) { 64188 this.bandwidth = this.options_.bandwidth; 64189 } 64190 if (this.options_.throughput) { 64191 this.throughput = this.options_.throughput; 64192 } 64193 Object.defineProperties(this.stats, { 64194 bandwidth: { 64195 get: () => this.bandwidth || 0, 64196 enumerable: true 64197 }, 64198 mediaRequests: { 64199 get: () => this.playlistController_.mediaRequests_() || 0, 64200 enumerable: true 64201 }, 64202 mediaRequestsAborted: { 64203 get: () => this.playlistController_.mediaRequestsAborted_() || 0, 64204 enumerable: true 64205 }, 64206 mediaRequestsTimedout: { 64207 get: () => this.playlistController_.mediaRequestsTimedout_() || 0, 64208 enumerable: true 64209 }, 64210 mediaRequestsErrored: { 64211 get: () => this.playlistController_.mediaRequestsErrored_() || 0, 64212 enumerable: true 64213 }, 64214 mediaTransferDuration: { 64215 get: () => this.playlistController_.mediaTransferDuration_() || 0, 64216 enumerable: true 64217 }, 64218 mediaBytesTransferred: { 64219 get: () => this.playlistController_.mediaBytesTransferred_() || 0, 64220 enumerable: true 64221 }, 64222 mediaSecondsLoaded: { 64223 get: () => this.playlistController_.mediaSecondsLoaded_() || 0, 64224 enumerable: true 64225 }, 64226 mediaAppends: { 64227 get: () => this.playlistController_.mediaAppends_() || 0, 64228 enumerable: true 64229 }, 64230 mainAppendsToLoadedData: { 64231 get: () => this.playlistController_.mainAppendsToLoadedData_() || 0, 64232 enumerable: true 64233 }, 64234 audioAppendsToLoadedData: { 64235 get: () => this.playlistController_.audioAppendsToLoadedData_() || 0, 64236 enumerable: true 64237 }, 64238 appendsToLoadedData: { 64239 get: () => this.playlistController_.appendsToLoadedData_() || 0, 64240 enumerable: true 64241 }, 64242 timeToLoadedData: { 64243 get: () => this.playlistController_.timeToLoadedData_() || 0, 64244 enumerable: true 64245 }, 64246 buffered: { 64247 get: () => timeRangesToArray(this.tech_.buffered()), 64248 enumerable: true 64249 }, 64250 currentTime: { 64251 get: () => this.tech_.currentTime(), 64252 enumerable: true 64253 }, 64254 currentSource: { 64255 get: () => this.tech_.currentSource_, 64256 enumerable: true 64257 }, 64258 currentTech: { 64259 get: () => this.tech_.name_, 64260 enumerable: true 64261 }, 64262 duration: { 64263 get: () => this.tech_.duration(), 64264 enumerable: true 64265 }, 64266 main: { 64267 get: () => this.playlists.main, 64268 enumerable: true 64269 }, 64270 playerDimensions: { 64271 get: () => this.tech_.currentDimensions(), 64272 enumerable: true 64273 }, 64274 seekable: { 64275 get: () => timeRangesToArray(this.tech_.seekable()), 64276 enumerable: true 64277 }, 64278 timestamp: { 64279 get: () => Date.now(), 64280 enumerable: true 64281 }, 64282 videoPlaybackQuality: { 64283 get: () => this.tech_.getVideoPlaybackQuality(), 64284 enumerable: true 64285 } 64286 }); 64287 this.tech_.one('canplay', this.playlistController_.setupFirstPlay.bind(this.playlistController_)); 64288 this.tech_.on('bandwidthupdate', () => { 64289 if (this.options_.useBandwidthFromLocalStorage) { 64290 updateVhsLocalStorage({ 64291 bandwidth: this.bandwidth, 64292 throughput: Math.round(this.throughput) 64293 }); 64294 } 64295 }); 64296 this.playlistController_.on('selectedinitialmedia', () => { 64297 // Add the manual rendition mix-in to VhsHandler 64298 renditionSelectionMixin(this); 64299 }); 64300 this.playlistController_.sourceUpdater_.on('createdsourcebuffers', () => { 64301 this.setupEme_(); 64302 }); // the bandwidth of the primary segment loader is our best 64303 // estimate of overall bandwidth 64304 64305 this.on(this.playlistController_, 'progress', function () { 64306 this.tech_.trigger('progress'); 64307 }); // In the live case, we need to ignore the very first `seeking` event since 64308 // that will be the result of the seek-to-live behavior 64309 64310 this.on(this.playlistController_, 'firstplay', function () { 64311 this.ignoreNextSeekingEvent_ = true; 64312 }); 64313 this.setupQualityLevels_(); // do nothing if the tech has been disposed already 64314 // this can occur if someone sets the src in player.ready(), for instance 64315 64316 if (!this.tech_.el()) { 64317 return; 64318 } 64319 this.mediaSourceUrl_ = window.URL.createObjectURL(this.playlistController_.mediaSource);
64319 64320 this.tech_.src(this.mediaSourceUrl_);
vendor: 7,228 bytes, lines 64320-64545
64320 64321 } 64322 createKeySessions_() { 64323 const audioPlaylistLoader = this.playlistController_.mediaTypes_.AUDIO.activePlaylistLoader; 64324 this.logger_('waiting for EME key session creation'); 64325 waitForKeySessionCreation({ 64326 player: this.player_, 64327 sourceKeySystems: this.source_.keySystems, 64328 audioMedia: audioPlaylistLoader && audioPlaylistLoader.media(), 64329 mainPlaylists: this.playlists.main.playlists 64330 }).then(() => { 64331 this.logger_('created EME key session'); 64332 this.playlistController_.sourceUpdater_.initializedEme(); 64333 }).catch(err => { 64334 this.logger_('error while creating EME key session', err); 64335 this.player_.error({ 64336 message: 'Failed to initialize media keys for EME', 64337 code: 3 64338 }); 64339 }); 64340 } 64341 handleWaitingForKey_() { 64342 // If waitingforkey is fired, it's possible that the data that's necessary to retrieve 64343 // the key is in the manifest. While this should've happened on initial source load, it 64344 // may happen again in live streams where the keys change, and the manifest info 64345 // reflects the update. 64346 // 64347 // Because videojs-contrib-eme compares the PSSH data we send to that of PSSH data it's 64348 // already requested keys for, we don't have to worry about this generating extraneous 64349 // requests. 64350 this.logger_('waitingforkey fired, attempting to create any new key sessions'); 64351 this.createKeySessions_(); 64352 } 64353 /** 64354 * If necessary and EME is available, sets up EME options and waits for key session 64355 * creation. 64356 * 64357 * This function also updates the source updater so taht it can be used, as for some 64358 * browsers, EME must be configured before content is appended (if appending unencrypted 64359 * content before encrypted content). 64360 */ 64361 64362 setupEme_() { 64363 const audioPlaylistLoader = this.playlistController_.mediaTypes_.AUDIO.activePlaylistLoader; 64364 const didSetupEmeOptions = setupEmeOptions({ 64365 player: this.player_, 64366 sourceKeySystems: this.source_.keySystems, 64367 media: this.playlists.media(), 64368 audioMedia: audioPlaylistLoader && audioPlaylistLoader.media() 64369 }); 64370 this.player_.tech_.on('keystatuschange', e => { 64371 this.playlistController_.updatePlaylistByKeyStatus(e.keyId, e.status); 64372 }); 64373 this.handleWaitingForKey_ = this.handleWaitingForKey_.bind(this); 64374 this.player_.tech_.on('waitingforkey', this.handleWaitingForKey_); 64375 if (!didSetupEmeOptions) { 64376 // If EME options were not set up, we've done all we could to initialize EME. 64377 this.playlistController_.sourceUpdater_.initializedEme(); 64378 return; 64379 } 64380 this.createKeySessions_(); 64381 } 64382 /** 64383 * Initializes the quality levels and sets listeners to update them. 64384 * 64385 * @method setupQualityLevels_ 64386 * @private 64387 */ 64388 64389 setupQualityLevels_() { 64390 const player = videojs.players[this.tech_.options_.playerId]; // if there isn't a player or there isn't a qualityLevels plugin 64391 // or qualityLevels_ listeners have already been setup, do nothing. 64392 64393 if (!player || !player.qualityLevels || this.qualityLevels_) { 64394 return; 64395 } 64396 this.qualityLevels_ = player.qualityLevels(); 64397 this.playlistController_.on('selectedinitialmedia', () => { 64398 handleVhsLoadedMetadata(this.qualityLevels_, this); 64399 }); 64400 this.playlists.on('mediachange', () => { 64401 handleVhsMediaChange(this.qualityLevels_, this.playlists); 64402 }); 64403 } 64404 /** 64405 * return the version 64406 */ 64407 64408 static version() { 64409 return { 64410 '@videojs/http-streaming': version$4, 64411 'mux.js': version$3, 64412 'mpd-parser': version$2, 64413 'm3u8-parser': version$1, 64414 'aes-decrypter': version 64415 }; 64416 } 64417 /** 64418 * return the version 64419 */ 64420 64421 version() { 64422 return this.constructor.version(); 64423 } 64424 canChangeType() { 64425 return SourceUpdater.canChangeType(); 64426 } 64427 /** 64428 * Begin playing the video. 64429 */ 64430 64431 play() { 64432 this.playlistController_.play(); 64433 } 64434 /** 64435 * a wrapper around the function in PlaylistController 64436 */ 64437 64438 setCurrentTime(currentTime) { 64439 this.playlistController_.setCurrentTime(currentTime); 64440 } 64441 /** 64442 * a wrapper around the function in PlaylistController 64443 */ 64444 64445 duration() { 64446 return this.playlistController_.duration(); 64447 } 64448 /** 64449 * a wrapper around the function in PlaylistController 64450 */ 64451 64452 seekable() { 64453 return this.playlistController_.seekable(); 64454 } 64455 /** 64456 * Abort all outstanding work and cleanup. 64457 */ 64458 64459 dispose() { 64460 if (this.playbackWatcher_) { 64461 this.playbackWatcher_.dispose(); 64462 } 64463 if (this.playlistController_) { 64464 this.playlistController_.dispose(); 64465 } 64466 if (this.qualityLevels_) { 64467 this.qualityLevels_.dispose(); 64468 } 64469 if (this.tech_ && this.tech_.vhs) { 64470 delete this.tech_.vhs; 64471 } 64472 if (this.mediaSourceUrl_ && window.URL.revokeObjectURL) { 64473 window.URL.revokeObjectURL(this.mediaSourceUrl_); 64474 this.mediaSourceUrl_ = null; 64475 } 64476 if (this.tech_) { 64477 this.tech_.off('waitingforkey', this.handleWaitingForKey_); 64478 } 64479 super.dispose(); 64480 } 64481 convertToProgramTime(time, callback) { 64482 return getProgramTime({ 64483 playlist: this.playlistController_.media(), 64484 time, 64485 callback 64486 }); 64487 } // the player must be playing before calling this 64488 64489 seekToProgramTime(programTime, callback, pauseAfterSeek = true, retryCount = 2) { 64490 return seekToProgramTime({ 64491 programTime, 64492 playlist: this.playlistController_.media(), 64493 retryCount, 64494 pauseAfterSeek, 64495 seekTo: this.options_.seekTo, 64496 tech: this.options_.tech, 64497 callback 64498 }); 64499 } 64500 /** 64501 * Adds the onRequest, onResponse, offRequest and offResponse functions 64502 * to the VhsHandler xhr Object. 64503 */ 64504 64505 setupXhrHooks_() { 64506 /** 64507 * A player function for setting an onRequest hook 64508 * 64509 * @param {function} callback for request modifiction 64510 */ 64511 this.xhr.onRequest = callback => { 64512 addOnRequestHook(this.xhr, callback); 64513 }; 64514 /** 64515 * A player function for setting an onResponse hook 64516 * 64517 * @param {callback} callback for response data retrieval 64518 */ 64519 64520 this.xhr.onResponse = callback => { 64521 addOnResponseHook(this.xhr, callback); 64522 }; 64523 /** 64524 * Deletes a player onRequest callback if it exists 64525 * 64526 * @param {function} callback to delete from the player set 64527 */ 64528 64529 this.xhr.offRequest = callback => { 64530 removeOnRequestHook(this.xhr, callback); 64531 }; 64532 /** 64533 * Deletes a player onResponse callback if it exists 64534 * 64535 * @param {function} callback to delete from the player set 64536 */ 64537 64538 this.xhr.offResponse = callback => { 64539 removeOnResponseHook(this.xhr, callback); 64540 }; // Trigger an event on the player to notify the user that vhs is ready to set xhr hooks. 64541 // This allows hooks to be set before the source is set to vhs when handleSource is called. 64542 64543 this.player_.trigger('xhr-hooks-ready'); 64544 } 64545
vendor: 2,519 bytes, lines 64545-64617
64545} 64546 /** 64547 * The Source Handler object, which informs video.js what additional 64548 * MIME types are supported and sets up playback. It is registered 64549 * automatically to the appropriate tech based on the capabilities of 64550 * the browser it is running in. It is not necessary to use or modify 64551 * this object in normal usage. 64552 */ 64553 64554 const VhsSourceHandler = { 64555 name: 'videojs-http-streaming', 64556 VERSION: version$4, 64557 canHandleSource(srcObj, options = {}) { 64558 const localOptions = merge(videojs.options, options); 64559 return VhsSourceHandler.canPlayType(srcObj.type, localOptions); 64560 }, 64561 handleSource(source, tech, options = {}) { 64562 const localOptions = merge(videojs.options, options); 64563 tech.vhs = new VhsHandler(source, tech, localOptions); 64564 tech.vhs.xhr = xhrFactory(); 64565 tech.vhs.setupXhrHooks_(); 64566 tech.vhs.src(source.src, source.type); 64567 return tech.vhs; 64568 }, 64569 canPlayType(type, options) { 64570 const simpleType = simpleTypeFromSourceType(type); 64571 if (!simpleType) { 64572 return ''; 64573 } 64574 const overrideNative = VhsSourceHandler.getOverrideNative(options); 64575 const supportsTypeNatively = Vhs.supportsTypeNatively(simpleType); 64576 const canUseMsePlayback = !supportsTypeNatively || overrideNative; 64577 return canUseMsePlayback ? 'maybe' : ''; 64578 }, 64579 getOverrideNative(options = {}) { 64580 const { 64581 vhs = {} 64582 } = options; 64583 const defaultOverrideNative = !(videojs.browser.IS_ANY_SAFARI || videojs.browser.IS_IOS); 64584 const { 64585 overrideNative = defaultOverrideNative 64586 } = vhs; 64587 return overrideNative; 64588 } 64589 }; 64590 /** 64591 * Check to see if the native MediaSource object exists and supports 64592 * an MP4 container with both H.264 video and AAC-LC audio. 64593 * 64594 * @return {boolean} if native media sources are supported 64595 */ 64596 64597 const supportsNativeMediaSources = () => { 64598 return browserSupportsCodec('avc1.4d400d,mp4a.40.2'); 64599 }; // register source handlers with the appropriate techs 64600 64601 if (supportsNativeMediaSources()) { 64602 videojs.getTech('Html5').registerSourceHandler(VhsSourceHandler, 0); 64603 } 64604 videojs.VhsHandler = VhsHandler; 64605 videojs.VhsSourceHandler = VhsSourceHandler; 64606 videojs.Vhs = Vhs; 64607 if (!videojs.use) { 64608 videojs.registerComponent('Vhs', Vhs); 64609 } 64610 videojs.options.vhs = videojs.options.vhs || {}; 64611 if (!videojs.getPlugin || !videojs.getPlugin('reloadSourceOnError')) { 64612 videojs.registerPlugin('reloadSourceOnError', reloadSourceOnError); 64613 } 64614 64615 return videojs; 64616 64617}));
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.