vendor: 8,590 bytes, lines 1-271
1/** 2 * @license 3 * Video.js 7.7.4 <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/master/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/master/LICENSE> 11 */ 12 13(function (global, factory) { 14 typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('global/window'), require('global/document')) : 15 typeof define === 'function' && define.amd ? define(['global/window', 'global/document'], factory) : 16 (global = global || self, global.videojs = factory(global.window, global.document)); 17 }(this, function (window$1, document) { 'use strict'; 18 19 window$1 = window$1 && window$1.hasOwnProperty('default') ? window$1['default'] : window$1; 20 document = document && document.hasOwnProperty('default') ? document['default'] : document; 21 22 var version = "7.7.4"; 23 24 /** 25 * @file create-logger.js 26 * @module create-logger 27 */ 28 29 var history = []; 30 /** 31 * Log messages to the console and history based on the type of message 32 * 33 * @private 34 * @param {string} type 35 * The name of the console method to use. 36 * 37 * @param {Array} args 38 * The arguments to be passed to the matching console method. 39 */ 40 41 var LogByTypeFactory = function LogByTypeFactory(name, log) { 42 return function (type, level, args) { 43 var lvl = log.levels[level]; 44 var lvlRegExp = new RegExp("^(" + lvl + ")$"); 45 46 if (type !== 'log') { 47 // Add the type to the front of the message when it's not "log". 48 args.unshift(type.toUpperCase() + ':'); 49 } // Add console prefix after adding to history. 50 51 52 args.unshift(name + ':'); // Add a clone of the args at this point to history. 53 54 if (history) { 55 history.push([].concat(args)); // only store 1000 history entries 56 57 var splice = history.length - 1000; 58 history.splice(0, splice > 0 ? splice : 0); 59 } // If there's no console then don't try to output messages, but they will 60 // still be stored in history. 61 62 63 if (!window$1.console) { 64 return; 65 } // Was setting these once outside of this function, but containing them 66 // in the function makes it easier to test cases where console doesn't exist 67 // when the module is executed. 68 69 70 var fn = window$1.console[type]; 71 72 if (!fn && type === 'debug') { 73 // Certain browsers don't have support for console.debug. For those, we 74 // should default to the closest comparable log. 75 fn = window$1.console.info || window$1.console.log; 76 } // Bail out if there's no console or if this type is not allowed by the 77 // current logging level. 78 79 80 if (!fn || !lvl || !lvlRegExp.test(type)) { 81 return; 82 } 83 84 fn[Array.isArray(args) ? 'apply' : 'call'](window$1.console, args); 85 }; 86 }; 87 88 function createLogger(name) { 89 // This is the private tracking variable for logging level. 90 var level = 'info'; // the curried logByType bound to the specific log and history 91 92 var logByType; 93 /** 94 * Logs plain debug messages. Similar to `console.log`. 95 * 96 * Due to [limitations](https://github.com/jsdoc3/jsdoc/issues/955#issuecomment-313829149) 97 * of our JSDoc template, we cannot properly document this as both a function 98 * and a namespace, so its function signature is documented here. 99 * 100 * #### Arguments 101 * ##### *args 102 * Mixed[] 103 * 104 * Any combination of values that could be passed to `console.log()`. 105 * 106 * #### Return Value 107 * 108 * `undefined` 109 * 110 * @namespace 111 * @param {Mixed[]} args 112 * One or more messages or objects that should be logged. 113 */ 114 115 var log = function log() { 116 for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { 117 args[_key] = arguments[_key]; 118 } 119 120 logByType('log', level, args); 121 }; // This is the logByType helper that the logging methods below use 122 123 124 logByType = LogByTypeFactory(name, log); 125 /** 126 * Create a new sublogger which chains the old name to the new name. 127 * 128 * For example, doing `videojs.log.createLogger('player')` and then using that logger will log the following: 129 * ```js 130 * mylogger('foo'); 131 * // > VIDEOJS: player: foo 132 * ``` 133 * 134 * @param {string} name 135 * The name to add call the new logger 136 * @return {Object} 137 */ 138 139 log.createLogger = function (subname) { 140 return createLogger(name + ': ' + subname); 141 }; 142 /** 143 * Enumeration of available logging levels, where the keys are the level names 144 * and the values are `|`-separated strings containing logging methods allowed 145 * in that logging level. These strings are used to create a regular expression 146 * matching the function name being called. 147 * 148 * Levels provided by Video.js are: 149 * 150 * - `off`: Matches no calls. Any value that can be cast to `false` will have 151 * this effect. The most restrictive. 152 * - `all`: Matches only Video.js-provided functions (`debug`, `log`, 153 * `log.warn`, and `log.error`). 154 * - `debug`: Matches `log.debug`, `log`, `log.warn`, and `log.error` calls. 155 * - `info` (default): Matches `log`, `log.warn`, and `log.error` calls. 156 * - `warn`: Matches `log.warn` and `log.error` calls. 157 * - `error`: Matches only `log.error` calls. 158 * 159 * @type {Object} 160 */ 161 162 163 log.levels = { 164 all: 'debug|log|warn|error', 165 off: '', 166 debug: 'debug|log|warn|error', 167 info: 'log|warn|error', 168 warn: 'warn|error', 169 error: 'error', 170 DEFAULT: level 171 }; 172 /** 173 * Get or set the current logging level. 174 * 175 * If a string matching a key from {@link module:log.levels} is provided, acts 176 * as a setter. 177 * 178 * @param {string} [lvl] 179 * Pass a valid level to set a new logging level. 180 * 181 * @return {string} 182 * The current logging level. 183 */ 184 185 log.level = function (lvl) { 186 if (typeof lvl === 'string') { 187 if (!log.levels.hasOwnProperty(lvl)) { 188 throw new Error("\"" + lvl + "\" in not a valid log level"); 189 } 190 191 level = lvl; 192 } 193 194 return level; 195 }; 196 /** 197 * Returns an array containing everything that has been logged to the history. 198 * 199 * This array is a shallow clone of the internal history record. However, its 200 * contents are _not_ cloned; so, mutating objects inside this array will 201 * mutate them in history. 202 * 203 * @return {Array} 204 */ 205 206 207 log.history = function () { 208 return history ? [].concat(history) : []; 209 }; 210 /** 211 * Allows you to filter the history by the given logger name 212 * 213 * @param {string} fname 214 * The name to filter by 215 * 216 * @return {Array} 217 * The filtered list to return 218 */ 219 220 221 log.history.filter = function (fname) { 222 return (history || []).filter(function (historyItem) { 223 // if the first item in each historyItem includes `fname`, then it's a match 224 return new RegExp(".*" + fname + ".*").test(historyItem[0]); 225 }); 226 }; 227 /** 228 * Clears the internal history tracking, but does not prevent further history 229 * tracking. 230 */ 231 232 233 log.history.clear = function () { 234 if (history) { 235 history.length = 0; 236 } 237 }; 238 /** 239 * Disable history tracking if it is currently enabled. 240 */ 241 242 243 log.history.disable = function () { 244 if (history !== null) { 245 history.length = 0; 246 history = null; 247 } 248 }; 249 /** 250 * Enable history tracking if it is currently disabled. 251 */ 252 253 254 log.history.enable = function () { 255 if (history === null) { 256 history = []; 257 } 258 }; 259 /** 260 * Logs error messages. Similar to `console.error`. 261 * 262 * @param {Mixed[]} args 263 * One or more messages or objects that should be logged as an error 264 */ 265 266 267 log.error = function () { 268 for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { 269 args[_key2] = arguments[_key2]; 270 } 271
272 return logByType('error', level, args); 273 }; 274 /** 275 * Logs warning messages. Similar to `console.warn`. 276 * 277 * @param {Mixed[]} args 278 * One or more messages or objects that should be logged as a warning. 279 */ 280 281 282 log.warn = function () { 283 for (var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { 284 args[_key3] = arguments[_key3]; 285 } 286 287 return logByType('warn', level, args); 288 }; 289 /** 290 * Logs debug messages. Similar to `console.debug`, but may also act as a comparable 291 * log if `console.debug` is not available 292 * 293 * @param {Mixed[]} args 294 * One or more messages or objects that should be logged as debug. 295 */ 296 297 298 log.debug = function () { 299 for (var _len4 = arguments.length, args = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) { 300 args[_key4] = arguments[_key4]; 301 } 302 303 return logByType('debug', level, args); 304 }; 305 306 return log; 307 } 308 309 /** 310 * @file log.js 311 * @module log 312 */ 313 var log = createLogger('VIDEOJS'); 314 var createLogger$1 = log.createLogger; 315 316 function createCommonjsModule(fn, module) { 317 return module = { exports: {} }, fn(module, module.exports), module.exports; 318 } 319 320 var _extends_1 = createCommonjsModule(function (module) { 321 function _extends() { 322 module.exports = _extends = Object.assign || function (target) { 323 for (var i = 1; i < arguments.length; i++) { 324 var source = arguments[i]; 325 326 for (var key in source) { 327 if (Object.prototype.hasOwnProperty.call(source, key)) { 328 target[key] = source[key]; 329 } 330 } 331 } 332 333 return target; 334 }; 335 336 return _extends.apply(this, arguments); 337 } 338 339 module.exports = _extends; 340 }); 341 342 /** 343 * @file obj.js 344 * @module obj 345 */ 346 347 /** 348 * @callback obj:EachCallback 349 * 350 * @param {Mixed} value 351 * The current key for the object that is being iterated over. 352 * 353 * @param {string} key 354 * The current key-value for object that is being iterated over 355 */ 356 357 /** 358 * @callback obj:ReduceCallback 359 * 360 * @param {Mixed} accum 361 * The value that is accumulating over the reduce loop. 362 * 363 * @param {Mixed} value 364 * The current key for the object that is being iterated over. 365 * 366 * @param {string} key 367 * The current key-value for object that is being iterated over 368 * 369 * @return {Mixed} 370 * The new accumulated value. 371 */ 372 var toString = Object.prototype.toString; 373 /** 374 * Get the keys of an Object 375 * 376 * @param {Object} 377 * The Object to get the keys from 378 * 379 * @return {string[]} 380 * An array of the keys from the object. Returns an empty array if the 381 * object passed in was invalid or had no keys. 382 * 383 * @private 384 */ 385 386 var keys = function keys(object) { 387 return isObject(object) ? Object.keys(object) : []; 388 }; 389 /** 390 * Array-like iteration for objects. 391 * 392 * @param {Object} object 393 * The object to iterate over 394 * 395 * @param {obj:EachCallback} fn 396 * The callback function which is called for each key in the object. 397 */ 398 399 400 function each(object, fn) { 401 keys(object).forEach(function (key) { 402 return fn(object[key], key); 403 }); 404 } 405 /** 406 * Array-like reduce for objects. 407 * 408 * @param {Object} object 409 * The Object that you want to reduce. 410 * 411 * @param {Function} fn 412 * A callback function which is called for each key in the object. It 413 * receives the accumulated value and the per-iteration value and key 414 * as arguments. 415 * 416 * @param {Mixed} [initial = 0] 417 * Starting value 418 * 419 * @return {Mixed} 420 * The final accumulated value. 421 */ 422 423 function reduce(object, fn, initial) { 424 if (initial === void 0) { 425 initial = 0; 426 } 427 428 return keys(object).reduce(function (accum, key) { 429 return fn(accum, object[key], key); 430 }, initial); 431 } 432 /** 433 * Object.assign-style object shallow merge/extend. 434 * 435 * @param {Object} target 436 * @param {Object} ...sources 437 * @return {Object} 438 */ 439 440 function assign(target) { 441 for (var _len = arguments.length, sources = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { 442 sources[_key - 1] = arguments[_key]; 443 } 444 445 if (Object.assign) { 446 return _extends_1.apply(void 0, [target].concat(sources)); 447 } 448
vendor: 15,893 bytes, lines 449-994
449 sources.forEach(function (source) { 450 if (!source) { 451 return; 452 } 453 454 each(source, function (value, key) { 455 target[key] = value; 456 }); 457 }); 458 return target; 459 } 460 /** 461 * Returns whether a value is an object of any kind - including DOM nodes, 462 * arrays, regular expressions, etc. Not functions, though. 463 * 464 * This avoids the gotcha where using `typeof` on a `null` value 465 * results in `'object'`. 466 * 467 * @param {Object} value 468 * @return {boolean} 469 */ 470 471 function isObject(value) { 472 return !!value && typeof value === 'object'; 473 } 474 /** 475 * Returns whether an object appears to be a "plain" object - that is, a 476 * direct instance of `Object`. 477 * 478 * @param {Object} value 479 * @return {boolean} 480 */ 481 482 function isPlain(value) { 483 return isObject(value) && toString.call(value) === '[object Object]' && value.constructor === Object; 484 } 485 486 /** 487 * @file computed-style.js 488 * @module computed-style 489 */ 490 /** 491 * A safe getComputedStyle. 492 * 493 * This is needed because in Firefox, if the player is loaded in an iframe with 494 * `display:none`, then `getComputedStyle` returns `null`, so, we do a 495 * null-check to make sure that the player doesn't break in these cases. 496 * 497 * @function 498 * @param {Element} el 499 * The element you want the computed style of 500 * 501 * @param {string} prop 502 * The property name you want 503 * 504 * @see https://bugzilla.mozilla.org/show_bug.cgi?id=548397 505 */ 506 507 function computedStyle(el, prop) { 508 if (!el || !prop) { 509 return ''; 510 } 511 512 if (typeof window$1.getComputedStyle === 'function') { 513 var computedStyleValue = window$1.getComputedStyle(el); 514 return computedStyleValue ? computedStyleValue.getPropertyValue(prop) || computedStyleValue[prop] : ''; 515 } 516 517 return ''; 518 } 519 520 /** 521 * @file dom.js 522 * @module dom 523 */ 524 /** 525 * Detect if a value is a string with any non-whitespace characters. 526 * 527 * @private 528 * @param {string} str 529 * The string to check 530 * 531 * @return {boolean} 532 * Will be `true` if the string is non-blank, `false` otherwise. 533 * 534 */ 535 536 function isNonBlankString(str) { 537 // we use str.trim as it will trim any whitespace characters 538 // from the front or back of non-whitespace characters. aka 539 // Any string that contains non-whitespace characters will 540 // still contain them after `trim` but whitespace only strings 541 // will have a length of 0, failing this check. 542 return typeof str === 'string' && Boolean(str.trim()); 543 } 544 /** 545 * Throws an error if the passed string has whitespace. This is used by 546 * class methods to be relatively consistent with the classList API. 547 * 548 * @private 549 * @param {string} str 550 * The string to check for whitespace. 551 * 552 * @throws {Error} 553 * Throws an error if there is whitespace in the string. 554 */ 555 556 557 function throwIfWhitespace(str) { 558 // str.indexOf instead of regex because str.indexOf is faster performance wise. 559 if (str.indexOf(' ') >= 0) { 560 throw new Error('class has illegal whitespace characters'); 561 } 562 } 563 /** 564 * Produce a regular expression for matching a className within an elements className. 565 * 566 * @private 567 * @param {string} className 568 * The className to generate the RegExp for. 569 * 570 * @return {RegExp} 571 * The RegExp that will check for a specific `className` in an elements 572 * className. 573 */ 574 575 576 function classRegExp(className) { 577 return new RegExp('(^|\\s)' + className + '($|\\s)'); 578 } 579 /** 580 * Whether the current DOM interface appears to be real (i.e. not simulated). 581 * 582 * @return {boolean} 583 * Will be `true` if the DOM appears to be real, `false` otherwise. 584 */ 585 586 587 function isReal() { 588 // Both document and window will never be undefined thanks to `global`. 589 return document === window$1.document; 590 } 591 /** 592 * Determines, via duck typing, whether or not a value is a DOM element. 593 * 594 * @param {Mixed} value 595 * The value to check. 596 * 597 * @return {boolean} 598 * Will be `true` if the value is a DOM element, `false` otherwise. 599 */ 600 601 function isEl(value) { 602 return isObject(value) && value.nodeType === 1; 603 } 604 /** 605 * Determines if the current DOM is embedded in an iframe. 606 * 607 * @return {boolean} 608 * Will be `true` if the DOM is embedded in an iframe, `false` 609 * otherwise. 610 */ 611 612 function isInFrame() { 613 // We need a try/catch here because Safari will throw errors when attempting 614 // to get either `parent` or `self` 615 try { 616 return window$1.parent !== window$1.self; 617 } catch (x) { 618 return true; 619 } 620 } 621 /** 622 * Creates functions to query the DOM using a given method. 623 * 624 * @private 625 * @param {string} method 626 * The method to create the query with. 627 * 628 * @return {Function} 629 * The query method 630 */ 631 632 function createQuerier(method) { 633 return function (selector, context) { 634 if (!isNonBlankString(selector)) { 635 return document[method](null); 636 } 637 638 if (isNonBlankString(context)) { 639 context = document.querySelector(context); 640 } 641 642 var ctx = isEl(context) ? context : document; 643 return ctx[method] && ctx[method](selector); 644 }; 645 } 646 /** 647 * Creates an element and applies properties, attributes, and inserts content. 648 * 649 * @param {string} [tagName='div'] 650 * Name of tag to be created. 651 * 652 * @param {Object} [properties={}] 653 * Element properties to be applied. 654 * 655 * @param {Object} [attributes={}] 656 * Element attributes to be applied. 657 * 658 * @param {module:dom~ContentDescriptor} content 659 * A content descriptor object. 660 * 661 * @return {Element} 662 * The element that was created. 663 */ 664 665 666 function createEl(tagName, properties, attributes, content) { 667 if (tagName === void 0) { 668 tagName = 'div'; 669 } 670 671 if (properties === void 0) { 672 properties = {}; 673 } 674 675 if (attributes === void 0) { 676 attributes = {}; 677 } 678 679 var el = document.createElement(tagName); 680 Object.getOwnPropertyNames(properties).forEach(function (propName) { 681 var val = properties[propName]; // See #2176 682 // We originally were accepting both properties and attributes in the 683 // same object, but that doesn't work so well. 684 685 if (propName.indexOf('aria-') !== -1 || propName === 'role' || propName === 'type') { 686 log.warn('Setting attributes in the second argument of createEl()\n' + 'has been deprecated. Use the third argument instead.\n' + ("createEl(type, properties, attributes). Attempting to set " + propName + " to " + val + ".")); 687 el.setAttribute(propName, val); // Handle textContent since it's not supported everywhere and we have a 688 // method for it. 689 } else if (propName === 'textContent') { 690 textContent(el, val); 691 } else if (el[propName] !== val) { 692 el[propName] = val; 693 } 694 }); 695 Object.getOwnPropertyNames(attributes).forEach(function (attrName) { 696 el.setAttribute(attrName, attributes[attrName]); 697 }); 698 699 if (content) { 700 appendContent(el, content); 701 } 702 703 return el; 704 } 705 /** 706 * Injects text into an element, replacing any existing contents entirely. 707 * 708 * @param {Element} el 709 * The element to add text content into 710 * 711 * @param {string} text 712 * The text content to add. 713 * 714 * @return {Element} 715 * The element with added text content. 716 */ 717 718 function textContent(el, text) { 719 if (typeof el.textContent === 'undefined') { 720 el.innerText = text; 721 } else { 722 el.textContent = text; 723 } 724 725 return el; 726 } 727 /** 728 * Insert an element as the first child node of another 729 * 730 * @param {Element} child 731 * Element to insert 732 * 733 * @param {Element} parent 734 * Element to insert child into 735 */ 736 737 function prependTo(child, parent) { 738 if (parent.firstChild) { 739 parent.insertBefore(child, parent.firstChild); 740 } else { 741 parent.appendChild(child); 742 } 743 } 744 /** 745 * Check if an element has a class name. 746 * 747 * @param {Element} element 748 * Element to check 749 * 750 * @param {string} classToCheck 751 * Class name to check for 752 * 753 * @return {boolean} 754 * Will be `true` if the element has a class, `false` otherwise. 755 * 756 * @throws {Error} 757 * Throws an error if `classToCheck` has white space. 758 */ 759 760 function hasClass(element, classToCheck) { 761 throwIfWhitespace(classToCheck); 762 763 if (element.classList) { 764 return element.classList.contains(classToCheck); 765 } 766 767 return classRegExp(classToCheck).test(element.className); 768 } 769 /** 770 * Add a class name to an element. 771 * 772 * @param {Element} element 773 * Element to add class name to. 774 * 775 * @param {string} classToAdd 776 * Class name to add. 777 * 778 * @return {Element} 779 * The DOM element with the added class name. 780 */ 781 782 function addClass(element, classToAdd) { 783 if (element.classList) { 784 element.classList.add(classToAdd); // Don't need to `throwIfWhitespace` here because `hasElClass` will do it 785 // in the case of classList not being supported. 786 } else if (!hasClass(element, classToAdd)) { 787 element.className = (element.className + ' ' + classToAdd).trim(); 788 } 789 790 return element; 791 } 792 /** 793 * Remove a class name from an element. 794 * 795 * @param {Element} element 796 * Element to remove a class name from. 797 * 798 * @param {string} classToRemove 799 * Class name to remove 800 * 801 * @return {Element} 802 * The DOM element with class name removed. 803 */ 804 805 function removeClass(element, classToRemove) { 806 if (element.classList) { 807 element.classList.remove(classToRemove); 808 } else { 809 throwIfWhitespace(classToRemove); 810 element.className = element.className.split(/\s+/).filter(function (c) { 811 return c !== classToRemove; 812 }).join(' '); 813 } 814 815 return element; 816 } 817 /** 818 * The callback definition for toggleClass. 819 * 820 * @callback module:dom~PredicateCallback 821 * @param {Element} element 822 * The DOM element of the Component. 823 * 824 * @param {string} classToToggle 825 * The `className` that wants to be toggled 826 * 827 * @return {boolean|undefined} 828 * If `true` is returned, the `classToToggle` will be added to the 829 * `element`. If `false`, the `classToToggle` will be removed from 830 * the `element`. If `undefined`, the callback will be ignored. 831 */ 832 833 /** 834 * Adds or removes a class name to/from an element depending on an optional 835 * condition or the presence/absence of the class name. 836 * 837 * @param {Element} element 838 * The element to toggle a class name on. 839 * 840 * @param {string} classToToggle 841 * The class that should be toggled. 842 * 843 * @param {boolean|module:dom~PredicateCallback} [predicate] 844 * See the return value for {@link module:dom~PredicateCallback} 845 * 846 * @return {Element} 847 * The element with a class that has been toggled. 848 */ 849 850 function toggleClass(element, classToToggle, predicate) { 851 // This CANNOT use `classList` internally because IE11 does not support the 852 // second parameter to the `classList.toggle()` method! Which is fine because 853 // `classList` will be used by the add/remove functions. 854 var has = hasClass(element, classToToggle); 855 856 if (typeof predicate === 'function') { 857 predicate = predicate(element, classToToggle); 858 } 859 860 if (typeof predicate !== 'boolean') { 861 predicate = !has; 862 } // If the necessary class operation matches the current state of the 863 // element, no action is required. 864 865 866 if (predicate === has) { 867 return; 868 } 869 870 if (predicate) { 871 addClass(element, classToToggle); 872 } else { 873 removeClass(element, classToToggle); 874 } 875 876 return element; 877 } 878 /** 879 * Apply attributes to an HTML element. 880 * 881 * @param {Element} el 882 * Element to add attributes to. 883 * 884 * @param {Object} [attributes] 885 * Attributes to be applied. 886 */ 887 888 function setAttributes(el, attributes) { 889 Object.getOwnPropertyNames(attributes).forEach(function (attrName) { 890 var attrValue = attributes[attrName]; 891 892 if (attrValue === null || typeof attrValue === 'undefined' || attrValue === false) { 893 el.removeAttribute(attrName); 894 } else { 895 el.setAttribute(attrName, attrValue === true ? '' : attrValue); 896 } 897 }); 898 } 899 /** 900 * Get an element's attribute values, as defined on the HTML tag. 901 * 902 * Attributes are not the same as properties. They're defined on the tag 903 * or with setAttribute. 904 * 905 * @param {Element} tag 906 * Element from which to get tag attributes. 907 * 908 * @return {Object} 909 * All attributes of the element. Boolean attributes will be `true` or 910 * `false`, others will be strings. 911 */ 912 913 function getAttributes(tag) { 914 var obj = {}; // known boolean attributes 915 // we can check for matching boolean properties, but not all browsers 916 // and not all tags know about these attributes, so, we still want to check them manually 917 918 var knownBooleans = ',' + 'autoplay,controls,playsinline,loop,muted,default,defaultMuted' + ','; 919 920 if (tag && tag.attributes && tag.attributes.length > 0) { 921 var attrs = tag.attributes; 922 923 for (var i = attrs.length - 1; i >= 0; i--) { 924 var attrName = attrs[i].name; 925 var attrVal = attrs[i].value; // check for known booleans 926 // the matching element property will return a value for typeof 927 928 if (typeof tag[attrName] === 'boolean' || knownBooleans.indexOf(',' + attrName + ',') !== -1) { 929 // the value of an included boolean attribute is typically an empty 930 // string ('') which would equal false if we just check for a false value. 931 // we also don't want support bad code like autoplay='false' 932 attrVal = attrVal !== null ? true : false; 933 } 934 935 obj[attrName] = attrVal; 936 } 937 } 938 939 return obj; 940 } 941 /** 942 * Get the value of an element's attribute. 943 * 944 * @param {Element} el 945 * A DOM element. 946 * 947 * @param {string} attribute 948 * Attribute to get the value of. 949 * 950 * @return {string} 951 * The value of the attribute. 952 */ 953 954 function getAttribute(el, attribute) { 955 return el.getAttribute(attribute); 956 } 957 /** 958 * Set the value of an element's attribute. 959 * 960 * @param {Element} el 961 * A DOM element. 962 * 963 * @param {string} attribute 964 * Attribute to set. 965 * 966 * @param {string} value 967 * Value to set the attribute to. 968 */ 969 970 function setAttribute(el, attribute, value) { 971 el.setAttribute(attribute, value); 972 } 973 /** 974 * Remove an element's attribute. 975 * 976 * @param {Element} el 977 * A DOM element. 978 * 979 * @param {string} attribute 980 * Attribute to remove. 981 */ 982 983 function removeAttribute(el, attribute) { 984 el.removeAttribute(attribute); 985 } 986 /** 987 * Attempt to block the ability to select text. 988 */ 989 990 function blockTextSelection() { 991 document.body.focus(); 992 993 document.onselectstart = function () { 994 return false;
vendor: 5,520 bytes, lines 995-1186
995 }; 996 } 997 /** 998 * Turn off text selection blocking. 999 */ 1000 1001 function unblockTextSelection() { 1002 document.onselectstart = function () { 1003 return true; 1004 }; 1005 } 1006 /** 1007 * Identical to the native `getBoundingClientRect` function, but ensures that 1008 * the method is supported at all (it is in all browsers we claim to support) 1009 * and that the element is in the DOM before continuing. 1010 * 1011 * This wrapper function also shims properties which are not provided by some 1012 * older browsers (namely, IE8). 1013 * 1014 * Additionally, some browsers do not support adding properties to a 1015 * `ClientRect`/`DOMRect` object; so, we shallow-copy it with the standard 1016 * properties (except `x` and `y` which are not widely supported). This helps 1017 * avoid implementations where keys are non-enumerable. 1018 * 1019 * @param {Element} el 1020 * Element whose `ClientRect` we want to calculate. 1021 * 1022 * @return {Object|undefined} 1023 * Always returns a plain object - or `undefined` if it cannot. 1024 */ 1025 1026 function getBoundingClientRect(el) { 1027 if (el && el.getBoundingClientRect && el.parentNode) { 1028 var rect = el.getBoundingClientRect(); 1029 var result = {}; 1030 ['bottom', 'height', 'left', 'right', 'top', 'width'].forEach(function (k) { 1031 if (rect[k] !== undefined) { 1032 result[k] = rect[k]; 1033 } 1034 }); 1035 1036 if (!result.height) { 1037 result.height = parseFloat(computedStyle(el, 'height')); 1038 } 1039 1040 if (!result.width) { 1041 result.width = parseFloat(computedStyle(el, 'width')); 1042 } 1043 1044 return result; 1045 } 1046 } 1047 /** 1048 * Represents the position of a DOM element on the page. 1049 * 1050 * @typedef {Object} module:dom~Position 1051 * 1052 * @property {number} left 1053 * Pixels to the left. 1054 * 1055 * @property {number} top 1056 * Pixels from the top. 1057 */ 1058 1059 /** 1060 * Get the position of an element in the DOM. 1061 * 1062 * Uses `getBoundingClientRect` technique from John Resig. 1063 * 1064 * @see http://ejohn.org/blog/getboundingclientrect-is-awesome/ 1065 * 1066 * @param {Element} el 1067 * Element from which to get offset. 1068 * 1069 * @return {module:dom~Position} 1070 * The position of the element that was passed in. 1071 */ 1072 1073 function findPosition(el) { 1074 var box; 1075 1076 if (el.getBoundingClientRect && el.parentNode) { 1077 box = el.getBoundingClientRect(); 1078 } 1079 1080 if (!box) { 1081 return { 1082 left: 0, 1083 top: 0 1084 }; 1085 } 1086 1087 var docEl = document.documentElement; 1088 var body = document.body; 1089 var clientLeft = docEl.clientLeft || body.clientLeft || 0; 1090 var scrollLeft = window$1.pageXOffset || body.scrollLeft; 1091 var left = box.left + scrollLeft - clientLeft; 1092 var clientTop = docEl.clientTop || body.clientTop || 0; 1093 var scrollTop = window$1.pageYOffset || body.scrollTop; 1094 var top = box.top + scrollTop - clientTop; // Android sometimes returns slightly off decimal values, so need to round 1095 1096 return { 1097 left: Math.round(left), 1098 top: Math.round(top) 1099 }; 1100 } 1101 /** 1102 * Represents x and y coordinates for a DOM element or mouse pointer. 1103 * 1104 * @typedef {Object} module:dom~Coordinates 1105 * 1106 * @property {number} x 1107 * x coordinate in pixels 1108 * 1109 * @property {number} y 1110 * y coordinate in pixels 1111 */ 1112 1113 /** 1114 * Get the pointer position within an element. 1115 * 1116 * The base on the coordinates are the bottom left of the element. 1117 * 1118 * @param {Element} el 1119 * Element on which to get the pointer position on. 1120 * 1121 * @param {EventTarget~Event} event 1122 * Event object. 1123 * 1124 * @return {module:dom~Coordinates} 1125 * A coordinates object corresponding to the mouse position. 1126 * 1127 */ 1128 1129 function getPointerPosition(el, event) { 1130 var position = {}; 1131 var box = findPosition(el); 1132 var boxW = el.offsetWidth; 1133 var boxH = el.offsetHeight; 1134 var boxY = box.top; 1135 var boxX = box.left; 1136 var pageY = event.pageY; 1137 var pageX = event.pageX; 1138 1139 if (event.changedTouches) { 1140 pageX = event.changedTouches[0].pageX; 1141 pageY = event.changedTouches[0].pageY; 1142 } 1143 1144 position.y = Math.max(0, Math.min(1, (boxY - pageY + boxH) / boxH)); 1145 position.x = Math.max(0, Math.min(1, (pageX - boxX) / boxW)); 1146 return position; 1147 } 1148 /** 1149 * Determines, via duck typing, whether or not a value is a text node. 1150 * 1151 * @param {Mixed} value 1152 * Check if this value is a text node. 1153 * 1154 * @return {boolean} 1155 * Will be `true` if the value is a text node, `false` otherwise. 1156 */ 1157 1158 function isTextNode(value) { 1159 return isObject(value) && value.nodeType === 3; 1160 } 1161 /** 1162 * Empties the contents of an element. 1163 * 1164 * @param {Element} el 1165 * The element to empty children from 1166 * 1167 * @return {Element} 1168 * The element with no children 1169 */ 1170 1171 function emptyEl(el) { 1172 while (el.firstChild) { 1173 el.removeChild(el.firstChild); 1174 } 1175 1176 return el; 1177 } 1178 /** 1179 * This is a mixed value that describes content to be injected into the DOM 1180 * via some method. It can be of the following types: 1181 * 1182 * Type | Description 1183 * -----------|------------- 1184 * `string` | The value will be normalized into a text node. 1185 * `Element` | The value will be accepted as-is. 1186 * `TextNode` | The value will be accepted as-is.
1187 * `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). 1188 * `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. 1189 * 1190 * @typedef {string|Element|TextNode|Array|Function} module:dom~ContentDescriptor 1191 */ 1192 1193 /** 1194 * Normalizes content for eventual insertion into the DOM. 1195 * 1196 * This allows a wide range of content definition methods, but helps protect 1197 * from falling into the trap of simply writing to `innerHTML`, which could 1198 * be an XSS concern. 1199 * 1200 * The content for an element can be passed in multiple types and 1201 * combinations, whose behavior is as follows: 1202 * 1203 * @param {module:dom~ContentDescriptor} content 1204 * A content descriptor value. 1205 * 1206 * @return {Array} 1207 * All of the content that was passed in, normalized to an array of 1208 * elements or text nodes. 1209 */ 1210 1211 function normalizeContent(content) { 1212 // First, invoke content if it is a function. If it produces an array, 1213 // that needs to happen before normalization. 1214 if (typeof content === 'function') { 1215 content = content(); 1216 } // Next up, normalize to an array, so one or many items can be normalized, 1217 // filtered, and returned. 1218 1219 1220 return (Array.isArray(content) ? content : [content]).map(function (value) { 1221 // First, invoke value if it is a function to produce a new value, 1222 // which will be subsequently normalized to a Node of some kind. 1223 if (typeof value === 'function') { 1224 value = value(); 1225 } 1226 1227 if (isEl(value) || isTextNode(value)) { 1228 return value; 1229 } 1230 1231 if (typeof value === 'string' && /\S/.test(value)) { 1232 return document.createTextNode(value); 1233 } 1234 }).filter(function (value) { 1235 return value; 1236 }); 1237 } 1238 /** 1239 * Normalizes and appends content to an element. 1240 * 1241 * @param {Element} el 1242 * Element to append normalized content to. 1243 * 1244 * @param {module:dom~ContentDescriptor} content 1245 * A content descriptor value. 1246 * 1247 * @return {Element} 1248 * The element with appended normalized content. 1249 */ 1250 1251 function appendContent(el, content) { 1252 normalizeContent(content).forEach(function (node) { 1253 return el.appendChild(node); 1254 }); 1255 return el; 1256 } 1257 /** 1258 * Normalizes and inserts content into an element; this is identical to 1259 * `appendContent()`, except it empties the element first. 1260 * 1261 * @param {Element} el 1262 * Element to insert normalized content into. 1263 * 1264 * @param {module:dom~ContentDescriptor} content 1265 * A content descriptor value. 1266 * 1267 * @return {Element} 1268 * The element with inserted normalized content. 1269 */ 1270 1271 function insertContent(el, content) { 1272 return appendContent(emptyEl(el), content); 1273 } 1274 /** 1275 * Check if an event was a single left click. 1276 * 1277 * @param {EventTarget~Event} event 1278 * Event object. 1279 * 1280 * @return {boolean} 1281 * Will be `true` if a single left click, `false` otherwise. 1282 */ 1283 1284 function isSingleLeftClick(event) { 1285 // Note: if you create something draggable, be sure to 1286 // call it on both `mousedown` and `mousemove` event, 1287 // otherwise `mousedown` should be enough for a button 1288 if (event.button === undefined && event.buttons === undefined) { 1289 // Why do we need `buttons` ? 1290 // Because, middle mouse sometimes have this: 1291 // e.button === 0 and e.buttons === 4 1292 // Furthermore, we want to prevent combination click, something like 1293 // HOLD middlemouse then left click, that would be 1294 // e.button === 0, e.buttons === 5 1295 // just `button` is not gonna work 1296 // Alright, then what this block does ? 1297 // this is for chrome `simulate mobile devices`
vendor: 37,694 bytes, lines 1298-2565
1298 // I want to support this as well 1299 return true; 1300 } 1301 1302 if (event.button === 0 && event.buttons === undefined) { 1303 // Touch screen, sometimes on some specific device, `buttons` 1304 // doesn't have anything (safari on ios, blackberry...) 1305 return true; 1306 } // `mouseup` event on a single left click has 1307 // `button` and `buttons` equal to 0 1308 1309 1310 if (event.type === 'mouseup' && event.button === 0 && event.buttons === 0) { 1311 return true; 1312 } 1313 1314 if (event.button !== 0 || event.buttons !== 1) { 1315 // This is the reason we have those if else block above 1316 // if any special case we can catch and let it slide 1317 // we do it above, when get to here, this definitely 1318 // is-not-left-click 1319 return false; 1320 } 1321 1322 return true; 1323 } 1324 /** 1325 * Finds a single DOM element matching `selector` within the optional 1326 * `context` of another DOM element (defaulting to `document`). 1327 * 1328 * @param {string} selector 1329 * A valid CSS selector, which will be passed to `querySelector`. 1330 * 1331 * @param {Element|String} [context=document] 1332 * A DOM element within which to query. Can also be a selector 1333 * string in which case the first matching element will be used 1334 * as context. If missing (or no element matches selector), falls 1335 * back to `document`. 1336 * 1337 * @return {Element|null} 1338 * The element that was found or null. 1339 */ 1340 1341 var $ = createQuerier('querySelector'); 1342 /** 1343 * Finds a all DOM elements matching `selector` within the optional 1344 * `context` of another DOM element (defaulting to `document`). 1345 * 1346 * @param {string} selector 1347 * A valid CSS selector, which will be passed to `querySelectorAll`. 1348 * 1349 * @param {Element|String} [context=document] 1350 * A DOM element within which to query. Can also be a selector 1351 * string in which case the first matching element will be used 1352 * as context. If missing (or no element matches selector), falls 1353 * back to `document`. 1354 * 1355 * @return {NodeList} 1356 * A element list of elements that were found. Will be empty if none 1357 * were found. 1358 * 1359 */ 1360 1361 var $$ = createQuerier('querySelectorAll'); 1362 1363 var Dom = /*#__PURE__*/Object.freeze({ 1364 isReal: isReal, 1365 isEl: isEl, 1366 isInFrame: isInFrame, 1367 createEl: createEl, 1368 textContent: textContent, 1369 prependTo: prependTo, 1370 hasClass: hasClass, 1371 addClass: addClass, 1372 removeClass: removeClass, 1373 toggleClass: toggleClass, 1374 setAttributes: setAttributes, 1375 getAttributes: getAttributes, 1376 getAttribute: getAttribute, 1377 setAttribute: setAttribute, 1378 removeAttribute: removeAttribute, 1379 blockTextSelection: blockTextSelection, 1380 unblockTextSelection: unblockTextSelection, 1381 getBoundingClientRect: getBoundingClientRect, 1382 findPosition: findPosition, 1383 getPointerPosition: getPointerPosition, 1384 isTextNode: isTextNode, 1385 emptyEl: emptyEl, 1386 normalizeContent: normalizeContent, 1387 appendContent: appendContent, 1388 insertContent: insertContent, 1389 isSingleLeftClick: isSingleLeftClick, 1390 $: $, 1391 $$: $$ 1392 }); 1393 1394 /** 1395 * @file setup.js - Functions for setting up a player without 1396 * user interaction based on the data-setup `attribute` of the video tag. 1397 * 1398 * @module setup 1399 */ 1400 var _windowLoaded = false; 1401 var videojs; 1402 /** 1403 * Set up any tags that have a data-setup `attribute` when the player is started. 1404 */ 1405 1406 var autoSetup = function autoSetup() { 1407 // Protect against breakage in non-browser environments and check global autoSetup option. 1408 if (!isReal() || videojs.options.autoSetup === false) { 1409 return; 1410 } 1411 1412 var vids = Array.prototype.slice.call(document.getElementsByTagName('video')); 1413 var audios = Array.prototype.slice.call(document.getElementsByTagName('audio')); 1414 var divs = Array.prototype.slice.call(document.getElementsByTagName('video-js')); 1415 var mediaEls = vids.concat(audios, divs); // Check if any media elements exist 1416 1417 if (mediaEls && mediaEls.length > 0) { 1418 for (var i = 0, e = mediaEls.length; i < e; i++) { 1419 var mediaEl = mediaEls[i]; // Check if element exists, has getAttribute func. 1420 1421 if (mediaEl && mediaEl.getAttribute) { 1422 // Make sure this player hasn't already been set up. 1423 if (mediaEl.player === undefined) { 1424 var options = mediaEl.getAttribute('data-setup'); // Check if data-setup attr exists. 1425 // We only auto-setup if they've added the data-setup attr. 1426 1427 if (options !== null) { 1428 // Create new video.js instance. 1429 videojs(mediaEl); 1430 } 1431 } // If getAttribute isn't defined, we need to wait for the DOM. 1432 1433 } else { 1434 autoSetupTimeout(1); 1435 break; 1436 } 1437 } // No videos were found, so keep looping unless page is finished loading. 1438 1439 } else if (!_windowLoaded) { 1440 autoSetupTimeout(1); 1441 } 1442 }; 1443 /** 1444 * Wait until the page is loaded before running autoSetup. This will be called in 1445 * autoSetup if `hasLoaded` returns false. 1446 * 1447 * @param {number} wait 1448 * How long to wait in ms 1449 * 1450 * @param {module:videojs} [vjs] 1451 * The videojs library function 1452 */ 1453 1454 1455 function autoSetupTimeout(wait, vjs) { 1456 if (vjs) { 1457 videojs = vjs; 1458 } 1459 1460 window$1.setTimeout(autoSetup, wait); 1461 } 1462 /** 1463 * Used to set the internal tracking of window loaded state to true. 1464 * 1465 * @private 1466 */ 1467 1468 1469 function setWindowLoaded() { 1470 _windowLoaded = true; 1471 window$1.removeEventListener('load', setWindowLoaded); 1472 } 1473 1474 if (isReal()) { 1475 if (document.readyState === 'complete') { 1476 setWindowLoaded(); 1477 } else { 1478 /** 1479 * Listen for the load event on window, and set _windowLoaded to true. 1480 * 1481 * We use a standard event listener here to avoid incrementing the GUID 1482 * before any players are created. 1483 * 1484 * @listens load 1485 */ 1486 window$1.addEventListener('load', setWindowLoaded); 1487 } 1488 } 1489 1490 /** 1491 * @file stylesheet.js 1492 * @module stylesheet 1493 */ 1494 /** 1495 * Create a DOM syle element given a className for it. 1496 * 1497 * @param {string} className 1498 * The className to add to the created style element. 1499 * 1500 * @return {Element} 1501 * The element that was created. 1502 */ 1503 1504 var createStyleElement = function createStyleElement(className) { 1505 var style = document.createElement('style'); 1506 style.className = className; 1507 return style; 1508 }; 1509 /** 1510 * Add text to a DOM element. 1511 * 1512 * @param {Element} el 1513 * The Element to add text content to. 1514 * 1515 * @param {string} content 1516 * The text to add to the element. 1517 */ 1518 1519 var setTextContent = function setTextContent(el, content) { 1520 if (el.styleSheet) { 1521 el.styleSheet.cssText = content; 1522 } else { 1523 el.textContent = content; 1524 } 1525 }; 1526 1527 /** 1528 * @file guid.js 1529 * @module guid 1530 */ 1531 // Default value for GUIDs. This allows us to reset the GUID counter in tests. 1532 // 1533 // The initial GUID is 3 because some users have come to rely on the first 1534 // default player ID ending up as `vjs_video_3`. 1535 // 1536 // See: https://github.com/videojs/video.js/pull/6216 1537 var _initialGuid = 3; 1538 /** 1539 * Unique ID for an element or function 1540 * 1541 * @type {Number} 1542 */ 1543 1544 var _guid = _initialGuid; 1545 /** 1546 * Get a unique auto-incrementing ID by number that has not been returned before. 1547 * 1548 * @return {number} 1549 * A new unique ID. 1550 */ 1551 1552 function newGUID() { 1553 return _guid++; 1554 } 1555 1556 /** 1557 * @file dom-data.js 1558 * @module dom-data 1559 */ 1560 var FakeWeakMap; 1561 1562 if (!window$1.WeakMap) { 1563 FakeWeakMap = 1564 /*#__PURE__*/ 1565 function () { 1566 function FakeWeakMap() { 1567 this.vdata = 'vdata' + Math.floor(window$1.performance && window$1.performance.now() || Date.now()); 1568 this.data = {}; 1569 } 1570 1571 var _proto = FakeWeakMap.prototype; 1572 1573 _proto.set = function set(key, value) { 1574 var access = key[this.vdata] || newGUID(); 1575 1576 if (!key[this.vdata]) { 1577 key[this.vdata] = access; 1578 } 1579 1580 this.data[access] = value; 1581 return this; 1582 }; 1583 1584 _proto.get = function get(key) { 1585 var access = key[this.vdata]; // we have data, return it 1586 1587 if (access) { 1588 return this.data[access]; 1589 } // we don't have data, return nothing. 1590 // return undefined explicitly as that's the contract for this method 1591 1592 1593 log('We have no data for this element', key); 1594 return undefined; 1595 }; 1596 1597 _proto.has = function has(key) { 1598 var access = key[this.vdata]; 1599 return access in this.data; 1600 }; 1601 1602 _proto["delete"] = function _delete(key) { 1603 var access = key[this.vdata]; 1604 1605 if (access) { 1606 delete this.data[access]; 1607 delete key[this.vdata]; 1608 } 1609 }; 1610 1611 return FakeWeakMap; 1612 }(); 1613 } 1614 /** 1615 * Element Data Store. 1616 * 1617 * Allows for binding data to an element without putting it directly on the 1618 * element. Ex. Event listeners are stored here. 1619 * (also from jsninja.com, slightly modified and updated for closure compiler) 1620 * 1621 * @type {Object} 1622 * @private 1623 */ 1624 1625 1626 var DomData = window$1.WeakMap ? new WeakMap() : new FakeWeakMap(); 1627 1628 /** 1629 * @file events.js. An Event System (John Resig - Secrets of a JS Ninja http://jsninja.com/) 1630 * (Original book version wasn't completely usable, so fixed some things and made Closure Compiler compatible) 1631 * This should work very similarly to jQuery's events, however it's based off the book version which isn't as 1632 * robust as jquery's, so there's probably some differences. 1633 * 1634 * @file events.js 1635 * @module events 1636 */ 1637 /** 1638 * Clean up the listener cache and dispatchers 1639 * 1640 * @param {Element|Object} elem 1641 * Element to clean up 1642 * 1643 * @param {string} type 1644 * Type of event to clean up 1645 */ 1646 1647 function _cleanUpEvents(elem, type) { 1648 if (!DomData.has(elem)) { 1649 return; 1650 } 1651 1652 var data = DomData.get(elem); // Remove the events of a particular type if there are none left 1653 1654 if (data.handlers[type].length === 0) { 1655 delete data.handlers[type]; // data.handlers[type] = null; 1656 // Setting to null was causing an error with data.handlers 1657 // Remove the meta-handler from the element 1658 1659 if (elem.removeEventListener) { 1660 elem.removeEventListener(type, data.dispatcher, false); 1661 } else if (elem.detachEvent) { 1662 elem.detachEvent('on' + type, data.dispatcher); 1663 } 1664 } // Remove the events object if there are no types left 1665 1666 1667 if (Object.getOwnPropertyNames(data.handlers).length <= 0) { 1668 delete data.handlers; 1669 delete data.dispatcher; 1670 delete data.disabled; 1671 } // Finally remove the element data if there is no data left 1672 1673 1674 if (Object.getOwnPropertyNames(data).length === 0) { 1675 DomData["delete"](elem); 1676 } 1677 } 1678 /** 1679 * Loops through an array of event types and calls the requested method for each type. 1680 * 1681 * @param {Function} fn 1682 * The event method we want to use. 1683 * 1684 * @param {Element|Object} elem 1685 * Element or object to bind listeners to 1686 * 1687 * @param {string} type 1688 * Type of event to bind to. 1689 * 1690 * @param {EventTarget~EventListener} callback 1691 * Event listener. 1692 */ 1693 1694 1695 function _handleMultipleEvents(fn, elem, types, callback) { 1696 types.forEach(function (type) { 1697 // Call the event method for each one of the types 1698 fn(elem, type, callback); 1699 }); 1700 } 1701 /** 1702 * Fix a native event to have standard property values 1703 * 1704 * @param {Object} event 1705 * Event object to fix. 1706 * 1707 * @return {Object} 1708 * Fixed event object. 1709 */ 1710 1711 1712 function fixEvent(event) { 1713 if (event.fixed_) { 1714 return event; 1715 } 1716 1717 function returnTrue() { 1718 return true; 1719 } 1720 1721 function returnFalse() { 1722 return false; 1723 } // Test if fixing up is needed 1724 // Used to check if !event.stopPropagation instead of isPropagationStopped 1725 // But native events return true for stopPropagation, but don't have 1726 // other expected methods like isPropagationStopped. Seems to be a problem 1727 // with the Javascript Ninja code. So we're just overriding all events now. 1728 1729 1730 if (!event || !event.isPropagationStopped) { 1731 var old = event || window$1.event; 1732 event = {}; // Clone the old object so that we can modify the values event = {}; 1733 // IE8 Doesn't like when you mess with native event properties 1734 // Firefox returns false for event.hasOwnProperty('type') and other props 1735 // which makes copying more difficult. 1736 // TODO: Probably best to create a whitelist of event props 1737 1738 for (var key in old) { 1739 // Safari 6.0.3 warns you if you try to copy deprecated layerX/Y 1740 // Chrome warns you if you try to copy deprecated keyboardEvent.keyLocation 1741 // and webkitMovementX/Y 1742 if (key !== 'layerX' && key !== 'layerY' && key !== 'keyLocation' && key !== 'webkitMovementX' && key !== 'webkitMovementY') { 1743 // Chrome 32+ warns if you try to copy deprecated returnValue, but 1744 // we still want to if preventDefault isn't supported (IE8). 1745 if (!(key === 'returnValue' && old.preventDefault)) { 1746 event[key] = old[key]; 1747 } 1748 } 1749 } // The event occurred on this element 1750 1751 1752 if (!event.target) { 1753 event.target = event.srcElement || document; 1754 } // Handle which other element the event is related to 1755 1756 1757 if (!event.relatedTarget) { 1758 event.relatedTarget = event.fromElement === event.target ? event.toElement : event.fromElement; 1759 } // Stop the default browser action 1760 1761 1762 event.preventDefault = function () { 1763 if (old.preventDefault) { 1764 old.preventDefault(); 1765 } 1766 1767 event.returnValue = false; 1768 old.returnValue = false; 1769 event.defaultPrevented = true; 1770 }; 1771 1772 event.defaultPrevented = false; // Stop the event from bubbling 1773 1774 event.stopPropagation = function () { 1775 if (old.stopPropagation) { 1776 old.stopPropagation(); 1777 } 1778 1779 event.cancelBubble = true; 1780 old.cancelBubble = true; 1781 event.isPropagationStopped = returnTrue; 1782 }; 1783 1784 event.isPropagationStopped = returnFalse; // Stop the event from bubbling and executing other handlers 1785 1786 event.stopImmediatePropagation = function () { 1787 if (old.stopImmediatePropagation) { 1788 old.stopImmediatePropagation(); 1789 } 1790 1791 event.isImmediatePropagationStopped = returnTrue; 1792 event.stopPropagation(); 1793 }; 1794 1795 event.isImmediatePropagationStopped = returnFalse; // Handle mouse position 1796 1797 if (event.clientX !== null && event.clientX !== undefined) { 1798 var doc = document.documentElement; 1799 var body = document.body; 1800 event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0); 1801 event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0); 1802 } // Handle key presses 1803 1804 1805 event.which = event.charCode || event.keyCode; // Fix button for mouse clicks: 1806 // 0 == left; 1 == middle; 2 == right 1807 1808 if (event.button !== null && event.button !== undefined) { 1809 // The following is disabled because it does not pass videojs-standard 1810 // and... yikes. 1811 1812 /* eslint-disable */ 1813 event.button = event.button & 1 ? 0 : event.button & 4 ? 1 : event.button & 2 ? 2 : 0; 1814 /* eslint-enable */ 1815 } 1816 } 1817 1818 event.fixed_ = true; // Returns fixed-up instance 1819 1820 return event; 1821 } 1822 /** 1823 * Whether passive event listeners are supported 1824 */ 1825 1826 var _supportsPassive; 1827 1828 var supportsPassive = function supportsPassive() { 1829 if (typeof _supportsPassive !== 'boolean') { 1830 _supportsPassive = false; 1831 1832 try { 1833 var opts = Object.defineProperty({}, 'passive', { 1834 get: function get() { 1835 _supportsPassive = true; 1836 } 1837 }); 1838 window$1.addEventListener('test', null, opts); 1839 window$1.removeEventListener('test', null, opts); 1840 } catch (e) {// disregard 1841 } 1842 } 1843 1844 return _supportsPassive; 1845 }; 1846 /** 1847 * Touch events Chrome expects to be passive 1848 */ 1849 1850 1851 var passiveEvents = ['touchstart', 'touchmove']; 1852 /** 1853 * Add an event listener to element 1854 * It stores the handler function in a separate cache object 1855 * and adds a generic handler to the element's event, 1856 * along with a unique id (guid) to the element. 1857 * 1858 * @param {Element|Object} elem 1859 * Element or object to bind listeners to 1860 * 1861 * @param {string|string[]} type 1862 * Type of event to bind to. 1863 * 1864 * @param {EventTarget~EventListener} fn 1865 * Event listener. 1866 */ 1867 1868 function on(elem, type, fn) { 1869 if (Array.isArray(type)) { 1870 return _handleMultipleEvents(on, elem, type, fn); 1871 } 1872 1873 if (!DomData.has(elem)) { 1874 DomData.set(elem, {}); 1875 } 1876 1877 var data = DomData.get(elem); // We need a place to store all our handler data 1878 1879 if (!data.handlers) { 1880 data.handlers = {}; 1881 } 1882 1883 if (!data.handlers[type]) { 1884 data.handlers[type] = []; 1885 } 1886 1887 if (!fn.guid) { 1888 fn.guid = newGUID(); 1889 } 1890 1891 data.handlers[type].push(fn); 1892 1893 if (!data.dispatcher) { 1894 data.disabled = false; 1895 1896 data.dispatcher = function (event, hash) { 1897 if (data.disabled) { 1898 return; 1899 } 1900 1901 event = fixEvent(event); 1902 var handlers = data.handlers[event.type]; 1903 1904 if (handlers) { 1905 // Copy handlers so if handlers are added/removed during the process it doesn't throw everything off. 1906 var handlersCopy = handlers.slice(0); 1907 1908 for (var m = 0, n = handlersCopy.length; m < n; m++) { 1909 if (event.isImmediatePropagationStopped()) { 1910 break; 1911 } else { 1912 try { 1913 handlersCopy[m].call(elem, event, hash); 1914 } catch (e) { 1915 log.error(e); 1916 } 1917 } 1918 } 1919 } 1920 }; 1921 } 1922 1923 if (data.handlers[type].length === 1) { 1924 if (elem.addEventListener) { 1925 var options = false; 1926 1927 if (supportsPassive() && passiveEvents.indexOf(type) > -1) { 1928 options = { 1929 passive: true 1930 }; 1931 } 1932 1933 elem.addEventListener(type, data.dispatcher, options); 1934 } else if (elem.attachEvent) { 1935 elem.attachEvent('on' + type, data.dispatcher); 1936 } 1937 } 1938 } 1939 /** 1940 * Removes event listeners from an element 1941 * 1942 * @param {Element|Object} elem 1943 * Object to remove listeners from. 1944 * 1945 * @param {string|string[]} [type] 1946 * Type of listener to remove. Don't include to remove all events from element. 1947 * 1948 * @param {EventTarget~EventListener} [fn] 1949 * Specific listener to remove. Don't include to remove listeners for an event 1950 * type. 1951 */ 1952 1953 function off(elem, type, fn) { 1954 // Don't want to add a cache object through getElData if not needed 1955 if (!DomData.has(elem)) { 1956 return; 1957 } 1958 1959 var data = DomData.get(elem); // If no events exist, nothing to unbind 1960 1961 if (!data.handlers) { 1962 return; 1963 } 1964 1965 if (Array.isArray(type)) { 1966 return _handleMultipleEvents(off, elem, type, fn); 1967 } // Utility function 1968 1969 1970 var removeType = function removeType(el, t) { 1971 data.handlers[t] = []; 1972 1973 _cleanUpEvents(el, t); 1974 }; // Are we removing all bound events? 1975 1976 1977 if (type === undefined) { 1978 for (var t in data.handlers) { 1979 if (Object.prototype.hasOwnProperty.call(data.handlers || {}, t)) { 1980 removeType(elem, t); 1981 } 1982 } 1983 1984 return; 1985 } 1986 1987 var handlers = data.handlers[type]; // If no handlers exist, nothing to unbind 1988 1989 if (!handlers) { 1990 return; 1991 } // If no listener was provided, remove all listeners for type 1992 1993 1994 if (!fn) { 1995 removeType(elem, type); 1996 return; 1997 } // We're only removing a single handler 1998 1999 2000 if (fn.guid) { 2001 for (var n = 0; n < handlers.length; n++) { 2002 if (handlers[n].guid === fn.guid) { 2003 handlers.splice(n--, 1); 2004 } 2005 } 2006 } 2007 2008 _cleanUpEvents(elem, type); 2009 } 2010 /** 2011 * Trigger an event for an element 2012 * 2013 * @param {Element|Object} elem 2014 * Element to trigger an event on 2015 * 2016 * @param {EventTarget~Event|string} event 2017 * A string (the type) or an event object with a type attribute 2018 * 2019 * @param {Object} [hash] 2020 * data hash to pass along with the event 2021 * 2022 * @return {boolean|undefined} 2023 * Returns the opposite of `defaultPrevented` if default was 2024 * prevented. Otherwise, returns `undefined` 2025 */ 2026 2027 function trigger(elem, event, hash) { 2028 // Fetches element data and a reference to the parent (for bubbling). 2029 // Don't want to add a data object to cache for every parent, 2030 // so checking hasElData first. 2031 var elemData = DomData.has(elem) ? DomData.get(elem) : {}; 2032 var parent = elem.parentNode || elem.ownerDocument; // type = event.type || event, 2033 // handler; 2034 // If an event name was passed as a string, creates an event out of it 2035 2036 if (typeof event === 'string') { 2037 event = { 2038 type: event, 2039 target: elem 2040 }; 2041 } else if (!event.target) { 2042 event.target = elem; 2043 } // Normalizes the event properties. 2044 2045 2046 event = fixEvent(event); // If the passed element has a dispatcher, executes the established handlers. 2047 2048 if (elemData.dispatcher) { 2049 elemData.dispatcher.call(elem, event, hash); 2050 } // Unless explicitly stopped or the event does not bubble (e.g. media events) 2051 // recursively calls this function to bubble the event up the DOM. 2052 2053 2054 if (parent && !event.isPropagationStopped() && event.bubbles === true) { 2055 trigger.call(null, parent, event, hash); // If at the top of the DOM, triggers the default action unless disabled. 2056 } else if (!parent && !event.defaultPrevented && event.target && event.target[event.type]) { 2057 if (!DomData.has(event.target)) { 2058 DomData.set(event.target, {}); 2059 } 2060 2061 var targetData = DomData.get(event.target); // Checks if the target has a default action for this event. 2062 2063 if (event.target[event.type]) { 2064 // Temporarily disables event dispatching on the target as we have already executed the handler. 2065 targetData.disabled = true; // Executes the default action. 2066 2067 if (typeof event.target[event.type] === 'function') { 2068 event.target[event.type](); 2069 } // Re-enables event dispatching. 2070 2071 2072 targetData.disabled = false; 2073 } 2074 } // Inform the triggerer if the default was prevented by returning false 2075 2076 2077 return !event.defaultPrevented; 2078 } 2079 /** 2080 * Trigger a listener only once for an event. 2081 * 2082 * @param {Element|Object} elem 2083 * Element or object to bind to. 2084 * 2085 * @param {string|string[]} type 2086 * Name/type of event 2087 * 2088 * @param {Event~EventListener} fn 2089 * Event listener function 2090 */ 2091 2092 function one(elem, type, fn) { 2093 if (Array.isArray(type)) { 2094 return _handleMultipleEvents(one, elem, type, fn); 2095 } 2096 2097 var func = function func() { 2098 off(elem, type, func); 2099 fn.apply(this, arguments); 2100 }; // copy the guid to the new function so it can removed using the original function's ID 2101 2102 2103 func.guid = fn.guid = fn.guid || newGUID(); 2104 on(elem, type, func); 2105 } 2106 /** 2107 * Trigger a listener only once and then turn if off for all 2108 * configured events 2109 * 2110 * @param {Element|Object} elem 2111 * Element or object to bind to. 2112 * 2113 * @param {string|string[]} type 2114 * Name/type of event 2115 * 2116 * @param {Event~EventListener} fn 2117 * Event listener function 2118 */ 2119 2120 function any(elem, type, fn) { 2121 var func = function func() { 2122 off(elem, type, func); 2123 fn.apply(this, arguments); 2124 }; // copy the guid to the new function so it can removed using the original function's ID 2125 2126 2127 func.guid = fn.guid = fn.guid || newGUID(); // multiple ons, but one off for everything 2128 2129 on(elem, type, func); 2130 } 2131 2132 var Events = /*#__PURE__*/Object.freeze({ 2133 fixEvent: fixEvent, 2134 on: on, 2135 off: off, 2136 trigger: trigger, 2137 one: one, 2138 any: any 2139 }); 2140 2141 /** 2142 * @file fn.js 2143 * @module fn 2144 */ 2145 var UPDATE_REFRESH_INTERVAL = 30; 2146 /** 2147 * Bind (a.k.a proxy or context). A simple method for changing the context of 2148 * a function. 2149 * 2150 * It also stores a unique id on the function so it can be easily removed from 2151 * events. 2152 * 2153 * @function 2154 * @param {Mixed} context 2155 * The object to bind as scope. 2156 * 2157 * @param {Function} fn 2158 * The function to be bound to a scope. 2159 * 2160 * @param {number} [uid] 2161 * An optional unique ID for the function to be set 2162 * 2163 * @return {Function} 2164 * The new function that will be bound into the context given 2165 */ 2166 2167 var bind = function bind(context, fn, uid) { 2168 // Make sure the function has a unique ID 2169 if (!fn.guid) { 2170 fn.guid = newGUID(); 2171 } // Create the new function that changes the context 2172 2173 2174 var bound = fn.bind(context); // Allow for the ability to individualize this function 2175 // Needed in the case where multiple objects might share the same prototype 2176 // IF both items add an event listener with the same function, then you try to remove just one 2177 // it will remove both because they both have the same guid. 2178 // when using this, you need to use the bind method when you remove the listener as well. 2179 // currently used in text tracks 2180 2181 bound.guid = uid ? uid + '_' + fn.guid : fn.guid; 2182 return bound; 2183 }; 2184 /** 2185 * Wraps the given function, `fn`, with a new function that only invokes `fn` 2186 * at most once per every `wait` milliseconds. 2187 * 2188 * @function 2189 * @param {Function} fn 2190 * The function to be throttled. 2191 * 2192 * @param {number} wait 2193 * The number of milliseconds by which to throttle. 2194 * 2195 * @return {Function} 2196 */ 2197 2198 var throttle = function throttle(fn, wait) { 2199 var last = window$1.performance.now(); 2200 2201 var throttled = function throttled() { 2202 var now = window$1.performance.now(); 2203 2204 if (now - last >= wait) { 2205 fn.apply(void 0, arguments); 2206 last = now; 2207 } 2208 }; 2209 2210 return throttled; 2211 }; 2212 /** 2213 * Creates a debounced function that delays invoking `func` until after `wait` 2214 * milliseconds have elapsed since the last time the debounced function was 2215 * invoked. 2216 * 2217 * Inspired by lodash and underscore implementations. 2218 * 2219 * @function 2220 * @param {Function} func 2221 * The function to wrap with debounce behavior. 2222 * 2223 * @param {number} wait 2224 * The number of milliseconds to wait after the last invocation. 2225 * 2226 * @param {boolean} [immediate] 2227 * Whether or not to invoke the function immediately upon creation. 2228 * 2229 * @param {Object} [context=window] 2230 * The "context" in which the debounced function should debounce. For 2231 * example, if this function should be tied to a Video.js player, 2232 * the player can be passed here. Alternatively, defaults to the 2233 * global `window` object. 2234 * 2235 * @return {Function} 2236 * A debounced function. 2237 */ 2238 2239 var debounce = function debounce(func, wait, immediate, context) { 2240 if (context === void 0) { 2241 context = window$1; 2242 } 2243 2244 var timeout; 2245 2246 var cancel = function cancel() { 2247 context.clearTimeout(timeout); 2248 timeout = null; 2249 }; 2250 /* eslint-disable consistent-this */ 2251 2252 2253 var debounced = function debounced() { 2254 var self = this; 2255 var args = arguments; 2256 2257 var _later = function later() { 2258 timeout = null; 2259 _later = null; 2260 2261 if (!immediate) { 2262 func.apply(self, args); 2263 } 2264 }; 2265 2266 if (!timeout && immediate) { 2267 func.apply(self, args); 2268 } 2269 2270 context.clearTimeout(timeout); 2271 timeout = context.setTimeout(_later, wait); 2272 }; 2273 /* eslint-enable consistent-this */ 2274 2275 2276 debounced.cancel = cancel; 2277 return debounced; 2278 }; 2279 2280 /** 2281 * @file src/js/event-target.js 2282 */ 2283 /** 2284 * `EventTarget` is a class that can have the same API as the DOM `EventTarget`. It 2285 * adds shorthand functions that wrap around lengthy functions. For example: 2286 * the `on` function is a wrapper around `addEventListener`. 2287 * 2288 * @see [EventTarget Spec]{@link https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventTarget} 2289 * @class EventTarget 2290 */ 2291 2292 var EventTarget = function EventTarget() {}; 2293 /** 2294 * A Custom DOM event. 2295 * 2296 * @typedef {Object} EventTarget~Event 2297 * @see [Properties]{@link https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent} 2298 */ 2299 2300 /** 2301 * All event listeners should follow the following format. 2302 * 2303 * @callback EventTarget~EventListener 2304 * @this {EventTarget} 2305 * 2306 * @param {EventTarget~Event} event 2307 * the event that triggered this function 2308 * 2309 * @param {Object} [hash] 2310 * hash of data sent during the event 2311 */ 2312 2313 /** 2314 * An object containing event names as keys and booleans as values. 2315 * 2316 * > NOTE: If an event name is set to a true value here {@link EventTarget#trigger} 2317 * will have extra functionality. See that function for more information. 2318 * 2319 * @property EventTarget.prototype.allowedEvents_ 2320 * @private 2321 */ 2322 2323 2324 EventTarget.prototype.allowedEvents_ = {}; 2325 /** 2326 * Adds an `event listener` to an instance of an `EventTarget`. An `event listener` is a 2327 * function that will get called when an event with a certain name gets triggered. 2328 * 2329 * @param {string|string[]} type 2330 * An event name or an array of event names. 2331 * 2332 * @param {EventTarget~EventListener} fn 2333 * The function to call with `EventTarget`s 2334 */ 2335 2336 EventTarget.prototype.on = function (type, fn) { 2337 // Remove the addEventListener alias before calling Events.on 2338 // so we don't get into an infinite type loop 2339 var ael = this.addEventListener; 2340 2341 this.addEventListener = function () {}; 2342 2343 on(this, type, fn); 2344 this.addEventListener = ael; 2345 }; 2346 /** 2347 * An alias of {@link EventTarget#on}. Allows `EventTarget` to mimic 2348 * the standard DOM API. 2349 * 2350 * @function 2351 * @see {@link EventTarget#on} 2352 */ 2353 2354 2355 EventTarget.prototype.addEventListener = EventTarget.prototype.on; 2356 /** 2357 * Removes an `event listener` for a specific event from an instance of `EventTarget`. 2358 * This makes it so that the `event listener` will no longer get called when the 2359 * named event happens. 2360 * 2361 * @param {string|string[]} type 2362 * An event name or an array of event names. 2363 * 2364 * @param {EventTarget~EventListener} fn 2365 * The function to remove. 2366 */ 2367 2368 EventTarget.prototype.off = function (type, fn) { 2369 off(this, type, fn); 2370 }; 2371 /** 2372 * An alias of {@link EventTarget#off}. Allows `EventTarget` to mimic 2373 * the standard DOM API. 2374 * 2375 * @function 2376 * @see {@link EventTarget#off} 2377 */ 2378 2379 2380 EventTarget.prototype.removeEventListener = EventTarget.prototype.off; 2381 /** 2382 * This function will add an `event listener` that gets triggered only once. After the 2383 * first trigger it will get removed. This is like adding an `event listener` 2384 * with {@link EventTarget#on} that calls {@link EventTarget#off} on itself. 2385 * 2386 * @param {string|string[]} type 2387 * An event name or an array of event names. 2388 * 2389 * @param {EventTarget~EventListener} fn 2390 * The function to be called once for each event name. 2391 */ 2392 2393 EventTarget.prototype.one = function (type, fn) { 2394 // Remove the addEventListener aliasing Events.on 2395 // so we don't get into an infinite type loop 2396 var ael = this.addEventListener; 2397 2398 this.addEventListener = function () {}; 2399 2400 one(this, type, fn); 2401 this.addEventListener = ael; 2402 }; 2403 2404 EventTarget.prototype.any = function (type, fn) { 2405 // Remove the addEventListener aliasing Events.on 2406 // so we don't get into an infinite type loop 2407 var ael = this.addEventListener; 2408 2409 this.addEventListener = function () {}; 2410 2411 any(this, type, fn); 2412 this.addEventListener = ael; 2413 }; 2414 /** 2415 * This function causes an event to happen. This will then cause any `event listeners` 2416 * that are waiting for that event, to get called. If there are no `event listeners` 2417 * for an event then nothing will happen. 2418 * 2419 * If the name of the `Event` that is being triggered is in `EventTarget.allowedEvents_`. 2420 * Trigger will also call the `on` + `uppercaseEventName` function. 2421 * 2422 * Example: 2423 * 'click' is in `EventTarget.allowedEvents_`, so, trigger will attempt to call 2424 * `onClick` if it exists. 2425 * 2426 * @param {string|EventTarget~Event|Object} event 2427 * The name of the event, an `Event`, or an object with a key of type set to 2428 * an event name. 2429 */ 2430 2431 2432 EventTarget.prototype.trigger = function (event) { 2433 var type = event.type || event; // deprecation 2434 // In a future version we should default target to `this` 2435 // similar to how we default the target to `elem` in 2436 // `Events.trigger`. Right now the default `target` will be 2437 // `document` due to the `Event.fixEvent` call. 2438 2439 if (typeof event === 'string') { 2440 event = { 2441 type: type 2442 }; 2443 } 2444 2445 event = fixEvent(event); 2446 2447 if (this.allowedEvents_[type] && this['on' + type]) { 2448 this['on' + type](event); 2449 } 2450 2451 trigger(this, event); 2452 }; 2453 /** 2454 * An alias of {@link EventTarget#trigger}. Allows `EventTarget` to mimic 2455 * the standard DOM API. 2456 * 2457 * @function 2458 * @see {@link EventTarget#trigger} 2459 */ 2460 2461 2462 EventTarget.prototype.dispatchEvent = EventTarget.prototype.trigger; 2463 var EVENT_MAP; 2464 2465 EventTarget.prototype.queueTrigger = function (event) { 2466 var _this = this; 2467 2468 // only set up EVENT_MAP if it'll be used 2469 if (!EVENT_MAP) { 2470 EVENT_MAP = new Map(); 2471 } 2472 2473 var type = event.type || event; 2474 var map = EVENT_MAP.get(this); 2475 2476 if (!map) { 2477 map = new Map(); 2478 EVENT_MAP.set(this, map); 2479 } 2480 2481 var oldTimeout = map.get(type); 2482 map["delete"](type); 2483 window$1.clearTimeout(oldTimeout); 2484 var timeout = window$1.setTimeout(function () { 2485 // if we cleared out all timeouts for the current target, delete its map 2486 if (map.size === 0) { 2487 map = null; 2488 EVENT_MAP["delete"](_this); 2489 } 2490 2491 _this.trigger(event); 2492 }, 0); 2493 map.set(type, timeout); 2494 }; 2495 2496 /** 2497 * @file mixins/evented.js 2498 * @module evented 2499 */ 2500 /** 2501 * Returns whether or not an object has had the evented mixin applied. 2502 * 2503 * @param {Object} object 2504 * An object to test. 2505 * 2506 * @return {boolean} 2507 * Whether or not the object appears to be evented. 2508 */ 2509 2510 var isEvented = function isEvented(object) { 2511 return object instanceof EventTarget || !!object.eventBusEl_ && ['on', 'one', 'off', 'trigger'].every(function (k) { 2512 return typeof object[k] === 'function'; 2513 }); 2514 }; 2515 /** 2516 * Adds a callback to run after the evented mixin applied. 2517 * 2518 * @param {Object} object 2519 * An object to Add 2520 * @param {Function} callback 2521 * The callback to run. 2522 */ 2523 2524 2525 var addEventedCallback = function addEventedCallback(target, callback) { 2526 if (isEvented(target)) { 2527 callback(); 2528 } else { 2529 if (!target.eventedCallbacks) { 2530 target.eventedCallbacks = []; 2531 } 2532 2533 target.eventedCallbacks.push(callback); 2534 } 2535 }; 2536 /** 2537 * Whether a value is a valid event type - non-empty string or array. 2538 * 2539 * @private 2540 * @param {string|Array} type 2541 * The type value to test. 2542 * 2543 * @return {boolean} 2544 * Whether or not the type is a valid event type. 2545 */ 2546 2547 2548 var isValidEventType = function isValidEventType(type) { 2549 return (// The regex here verifies that the `type` contains at least one non- 2550 // whitespace character. 2551 typeof type === 'string' && /\S/.test(type) || Array.isArray(type) && !!type.length 2552 ); 2553 }; 2554 /** 2555 * Validates a value to determine if it is a valid event target. Throws if not. 2556 * 2557 * @private 2558 * @throws {Error} 2559 * If the target does not appear to be a valid event target. 2560 * 2561 * @param {Object} target 2562 * The object to test. 2563 */ 2564 2565
vendor: 6,489 bytes, lines 2566-2759
2566 var validateTarget = function validateTarget(target) { 2567 if (!target.nodeName && !isEvented(target)) { 2568 throw new Error('Invalid target; must be a DOM node or evented object.'); 2569 } 2570 }; 2571 /** 2572 * Validates a value to determine if it is a valid event target. Throws if not. 2573 * 2574 * @private 2575 * @throws {Error} 2576 * If the type does not appear to be a valid event type. 2577 * 2578 * @param {string|Array} type 2579 * The type to test. 2580 */ 2581 2582 2583 var validateEventType = function validateEventType(type) { 2584 if (!isValidEventType(type)) { 2585 throw new Error('Invalid event type; must be a non-empty string or array.'); 2586 } 2587 }; 2588 /** 2589 * Validates a value to determine if it is a valid listener. Throws if not. 2590 * 2591 * @private 2592 * @throws {Error} 2593 * If the listener is not a function. 2594 * 2595 * @param {Function} listener 2596 * The listener to test. 2597 */ 2598 2599 2600 var validateListener = function validateListener(listener) { 2601 if (typeof listener !== 'function') { 2602 throw new Error('Invalid listener; must be a function.'); 2603 } 2604 }; 2605 /** 2606 * Takes an array of arguments given to `on()` or `one()`, validates them, and 2607 * normalizes them into an object. 2608 * 2609 * @private 2610 * @param {Object} self 2611 * The evented object on which `on()` or `one()` was called. This 2612 * object will be bound as the `this` value for the listener. 2613 * 2614 * @param {Array} args 2615 * An array of arguments passed to `on()` or `one()`. 2616 * 2617 * @return {Object} 2618 * An object containing useful values for `on()` or `one()` calls. 2619 */ 2620 2621 2622 var normalizeListenArgs = function normalizeListenArgs(self, args) { 2623 // If the number of arguments is less than 3, the target is always the 2624 // evented object itself. 2625 var isTargetingSelf = args.length < 3 || args[0] === self || args[0] === self.eventBusEl_; 2626 var target; 2627 var type; 2628 var listener; 2629 2630 if (isTargetingSelf) { 2631 target = self.eventBusEl_; // Deal with cases where we got 3 arguments, but we are still listening to 2632 // the evented object itself. 2633 2634 if (args.length >= 3) { 2635 args.shift(); 2636 } 2637 2638 type = args[0]; 2639 listener = args[1]; 2640 } else { 2641 target = args[0]; 2642 type = args[1]; 2643 listener = args[2]; 2644 } 2645 2646 validateTarget(target); 2647 validateEventType(type); 2648 validateListener(listener); 2649 listener = bind(self, listener); 2650 return { 2651 isTargetingSelf: isTargetingSelf, 2652 target: target, 2653 type: type, 2654 listener: listener 2655 }; 2656 }; 2657 /** 2658 * Adds the listener to the event type(s) on the target, normalizing for 2659 * the type of target. 2660 * 2661 * @private 2662 * @param {Element|Object} target 2663 * A DOM node or evented object. 2664 * 2665 * @param {string} method 2666 * The event binding method to use ("on" or "one"). 2667 * 2668 * @param {string|Array} type 2669 * One or more event type(s). 2670 * 2671 * @param {Function} listener 2672 * A listener function. 2673 */ 2674 2675 2676 var listen = function listen(target, method, type, listener) { 2677 validateTarget(target); 2678 2679 if (target.nodeName) { 2680 Events[method](target, type, listener); 2681 } else { 2682 target[method](type, listener); 2683 } 2684 }; 2685 /** 2686 * Contains methods that provide event capabilities to an object which is passed 2687 * to {@link module:evented|evented}. 2688 * 2689 * @mixin EventedMixin 2690 */ 2691 2692 2693 var EventedMixin = { 2694 /** 2695 * Add a listener to an event (or events) on this object or another evented 2696 * object. 2697 * 2698 * @param {string|Array|Element|Object} targetOrType 2699 * If this is a string or array, it represents the event type(s) 2700 * that will trigger the listener. 2701 * 2702 * Another evented object can be passed here instead, which will 2703 * cause the listener to listen for events on _that_ object. 2704 * 2705 * In either case, the listener's `this` value will be bound to 2706 * this object. 2707 * 2708 * @param {string|Array|Function} typeOrListener 2709 * If the first argument was a string or array, this should be the 2710 * listener function. Otherwise, this is a string or array of event 2711 * type(s). 2712 * 2713 * @param {Function} [listener] 2714 * If the first argument was another evented object, this will be 2715 * the listener function. 2716 */ 2717 on: function on() { 2718 var _this = this; 2719 2720 for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { 2721 args[_key] = arguments[_key]; 2722 } 2723 2724 var _normalizeListenArgs = normalizeListenArgs(this, args), 2725 isTargetingSelf = _normalizeListenArgs.isTargetingSelf, 2726 target = _normalizeListenArgs.target, 2727 type = _normalizeListenArgs.type, 2728 listener = _normalizeListenArgs.listener; 2729 2730 listen(target, 'on', type, listener); // If this object is listening to another evented object. 2731 2732 if (!isTargetingSelf) { 2733 // If this object is disposed, remove the listener. 2734 var removeListenerOnDispose = function removeListenerOnDispose() { 2735 return _this.off(target, type, listener); 2736 }; // Use the same function ID as the listener so we can remove it later it 2737 // using the ID of the original listener. 2738 2739 2740 removeListenerOnDispose.guid = listener.guid; // Add a listener to the target's dispose event as well. This ensures 2741 // that if the target is disposed BEFORE this object, we remove the 2742 // removal listener that was just added. Otherwise, we create a memory leak. 2743 2744 var removeRemoverOnTargetDispose = function removeRemoverOnTargetDispose() { 2745 return _this.off('dispose', removeListenerOnDispose); 2746 }; // Use the same function ID as the listener so we can remove it later 2747 // it using the ID of the original listener. 2748 2749 2750 removeRemoverOnTargetDispose.guid = listener.guid; 2751 listen(this, 'on', 'dispose', removeListenerOnDispose); 2752 listen(target, 'on', 'dispose', removeRemoverOnTargetDispose); 2753 } 2754 }, 2755 2756 /** 2757 * Add a listener to an event (or events) on this object or another evented 2758 * object. The listener will be called once per event and then removed. 2759 *
2760 * @param {string|Array|Element|Object} targetOrType 2761 * If this is a string or array, it represents the event type(s) 2762 * that will trigger the listener. 2763 * 2764 * Another evented object can be passed here instead, which will 2765 * cause the listener to listen for events on _that_ object. 2766 * 2767 * In either case, the listener's `this` value will be bound to 2768 * this object. 2769 * 2770 * @param {string|Array|Function} typeOrListener 2771 * If the first argument was a string or array, this should be the 2772 * listener function. Otherwise, this is a string or array of event 2773 * type(s). 2774 * 2775 * @param {Function} [listener] 2776 * If the first argument was another evented object, this will be 2777 * the listener function. 2778 */ 2779 one: function one() { 2780 var _this2 = this; 2781 2782 for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { 2783 args[_key2] = arguments[_key2]; 2784 } 2785 2786 var _normalizeListenArgs2 = normalizeListenArgs(this, args), 2787 isTargetingSelf = _normalizeListenArgs2.isTargetingSelf, 2788 target = _normalizeListenArgs2.target, 2789 type = _normalizeListenArgs2.type, 2790 listener = _normalizeListenArgs2.listener; // Targeting this evented object. 2791 2792 2793 if (isTargetingSelf) { 2794 listen(target, 'one', type, listener); // Targeting another evented object. 2795 } else { 2796 // TODO: This wrapper is incorrect! It should only 2797 // remove the wrapper for the event type that called it. 2798 // Instead all listners are removed on the first trigger! 2799 // see https://github.com/videojs/video.js/issues/5962 2800 var wrapper = function wrapper() { 2801 _this2.off(target, type, wrapper); 2802 2803 for (var _len3 = arguments.length, largs = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { 2804 largs[_key3] = arguments[_key3]; 2805 } 2806 2807 listener.apply(null, largs); 2808 }; // Use the same function ID as the listener so we can remove it later 2809 // it using the ID of the original listener. 2810 2811 2812 wrapper.guid = listener.guid; 2813 listen(target, 'one', type, wrapper); 2814 } 2815 }, 2816 2817 /** 2818 * Add a listener to an event (or events) on this object or another evented 2819 * object. The listener will only be called once for the first event that is triggered 2820 * then removed. 2821 * 2822 * @param {string|Array|Element|Object} targetOrType 2823 * If this is a string or array, it represents the event type(s) 2824 * that will trigger the listener. 2825 * 2826 * Another evented object can be passed here instead, which will 2827 * cause the listener to listen for events on _that_ object. 2828 * 2829 * In either case, the listener's `this` value will be bound to 2830 * this object. 2831 * 2832 * @param {string|Array|Function} typeOrListener 2833 * If the first argument was a string or array, this should be the 2834 * listener function. Otherwise, this is a string or array of event 2835 * type(s). 2836 * 2837 * @param {Function} [listener] 2838 * If the first argument was another evented object, this will be 2839 * the listener function. 2840 */ 2841 any: function any() { 2842 var _this3 = this; 2843 2844 for (var _len4 = arguments.length, args = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) { 2845 args[_key4] = arguments[_key4]; 2846 } 2847 2848 var _normalizeListenArgs3 = normalizeListenArgs(this, args), 2849 isTargetingSelf = _normalizeListenArgs3.isTargetingSelf, 2850 target = _normalizeListenArgs3.target, 2851 type = _normalizeListenArgs3.type, 2852 listener = _normalizeListenArgs3.listener; // Targeting this evented object. 2853 2854 2855 if (isTargetingSelf) { 2856 listen(target, 'any', type, listener); // Targeting another evented object. 2857 } else { 2858 var wrapper = function wrapper() { 2859 _this3.off(target, type, wrapper); 2860 2861 for (var _len5 = arguments.length, largs = new Array(_len5), _key5 = 0; _key5 < _len5; _key5++) { 2862 largs[_key5] = arguments[_key5]; 2863 } 2864 2865 listener.apply(null, largs); 2866 }; // Use the same function ID as the listener so we can remove it later 2867 // it using the ID of the original listener. 2868 2869 2870 wrapper.guid = listener.guid; 2871 listen(target, 'any', type, wrapper); 2872 } 2873 }, 2874 2875 /** 2876 * Removes listener(s) from event(s) on an evented object. 2877 *
2878 * @param {string|Array|Element|Object} [targetOrType] 2879 * If this is a string or array, it represents the event type(s). 2880 * 2881 * Another evented object can be passed here instead, in which case 2882 * ALL 3 arguments are _required_. 2883 * 2884 * @param {string|Array|Function} [typeOrListener] 2885 * If the first argument was a string or array, this may be the 2886 * listener function. Otherwise, this is a string or array of event 2887 * type(s). 2888 * 2889 * @param {Function} [listener] 2890 * If the first argument was another evented object, this will be 2891 * the listener function; otherwise, _all_ listeners bound to the 2892 * event type(s) will be removed. 2893 */ 2894 off: function off$1(targetOrType, typeOrListener, listener) { 2895 // Targeting this evented object. 2896 if (!targetOrType || isValidEventType(targetOrType)) { 2897 off(this.eventBusEl_, targetOrType, typeOrListener); // Targeting another evented object. 2898 } else { 2899 var target = targetOrType; 2900 var type = typeOrListener; // Fail fast and in a meaningful way! 2901 2902 validateTarget(target); 2903 validateEventType(type); 2904 validateListener(listener); // Ensure there's at least a guid, even if the function hasn't been used 2905 2906 listener = bind(this, listener); // Remove the dispose listener on this evented object, which was given 2907 // the same guid as the event listener in on(). 2908 2909 this.off('dispose', listener); 2910 2911 if (target.nodeName) { 2912 off(target, type, listener); 2913 off(target, 'dispose', listener); 2914 } else if (isEvented(target)) { 2915 target.off(type, listener); 2916 target.off('dispose', listener); 2917 } 2918 } 2919 }, 2920 2921 /** 2922 * Fire an event on this evented object, causing its listeners to be called. 2923 * 2924 * @param {string|Object} event 2925 * An event type or an object with a type property. 2926 * 2927 * @param {Object} [hash] 2928 * An additional object to pass along to listeners. 2929 * 2930 * @return {boolean} 2931 * Whether or not the default behavior was prevented. 2932 */ 2933 trigger: function trigger$1(event, hash) { 2934 return trigger(this.eventBusEl_, event, hash); 2935 } 2936 }; 2937 /** 2938 * Applies {@link module:evented~EventedMixin|EventedMixin} to a target object. 2939 * 2940 * @param {Object} target 2941 * The object to which to add event methods. 2942 * 2943 * @param {Object} [options={}] 2944 * Options for customizing the mixin behavior. 2945 * 2946 * @param {string} [options.eventBusKey] 2947 * By default, adds a `eventBusEl_` DOM element to the target object, 2948 * which is used as an event bus. If the target object already has a 2949 * DOM element that should be used, pass its key here. 2950 * 2951 * @return {Object} 2952 * The target object. 2953 */ 2954 2955 function evented(target, options) { 2956 if (options === void 0) { 2957 options = {}; 2958 } 2959 2960 var _options = options, 2961 eventBusKey = _options.eventBusKey; // Set or create the eventBusEl_. 2962 2963 if (eventBusKey) { 2964 if (!target[eventBusKey].nodeName) { 2965 throw new Error("The eventBusKey \"" + eventBusKey + "\" does not refer to an element."); 2966 } 2967 2968 target.eventBusEl_ = target[eventBusKey]; 2969 } else { 2970 target.eventBusEl_ = createEl('span', { 2971 className: 'vjs-event-bus' 2972 }); 2973 } 2974 2975 assign(target, EventedMixin); 2976 2977 if (target.eventedCallbacks) { 2978 target.eventedCallbacks.forEach(function (callback) { 2979 callback(); 2980 }); 2981 } // When any evented object is disposed, it removes all its listeners. 2982 2983 2984 target.on('dispose', function () { 2985 target.off(); 2986 window$1.setTimeout(function () { 2987 target.eventBusEl_ = null; 2988 }, 0); 2989 }); 2990 return target; 2991 } 2992 2993 /** 2994 * @file mixins/stateful.js 2995 * @module stateful 2996 */ 2997 /** 2998 * Contains methods that provide statefulness to an object which is passed 2999 * to {@link module:stateful}. 3000 * 3001 * @mixin StatefulMixin 3002 */ 3003 3004 var StatefulMixin = { 3005 /** 3006 * A hash containing arbitrary keys and values representing the state of 3007 * the object. 3008 * 3009 * @type {Object} 3010 */ 3011 state: {}, 3012 3013 /** 3014 * Set the state of an object by mutating its 3015 * {@link module:stateful~StatefulMixin.state|state} object in place. 3016 * 3017 * @fires module:stateful~StatefulMixin#statechanged 3018 * @param {Object|Function} stateUpdates 3019 * A new set of properties to shallow-merge into the plugin state. 3020 * Can be a plain object or a function returning a plain object. 3021 * 3022 * @return {Object|undefined}
vendor: 88,523 bytes, lines 3023-5787
3023 * An object containing changes that occurred. If no changes 3024 * occurred, returns `undefined`. 3025 */ 3026 setState: function setState(stateUpdates) { 3027 var _this = this; 3028 3029 // Support providing the `stateUpdates` state as a function. 3030 if (typeof stateUpdates === 'function') { 3031 stateUpdates = stateUpdates(); 3032 } 3033 3034 var changes; 3035 each(stateUpdates, function (value, key) { 3036 // Record the change if the value is different from what's in the 3037 // current state. 3038 if (_this.state[key] !== value) { 3039 changes = changes || {}; 3040 changes[key] = { 3041 from: _this.state[key], 3042 to: value 3043 }; 3044 } 3045 3046 _this.state[key] = value; 3047 }); // Only trigger "statechange" if there were changes AND we have a trigger 3048 // function. This allows us to not require that the target object be an 3049 // evented object. 3050 3051 if (changes && isEvented(this)) { 3052 /** 3053 * An event triggered on an object that is both 3054 * {@link module:stateful|stateful} and {@link module:evented|evented} 3055 * indicating that its state has changed. 3056 * 3057 * @event module:stateful~StatefulMixin#statechanged 3058 * @type {Object} 3059 * @property {Object} changes 3060 * A hash containing the properties that were changed and 3061 * the values they were changed `from` and `to`. 3062 */ 3063 this.trigger({ 3064 changes: changes, 3065 type: 'statechanged' 3066 }); 3067 } 3068 3069 return changes; 3070 } 3071 }; 3072 /** 3073 * Applies {@link module:stateful~StatefulMixin|StatefulMixin} to a target 3074 * object. 3075 * 3076 * If the target object is {@link module:evented|evented} and has a 3077 * `handleStateChanged` method, that method will be automatically bound to the 3078 * `statechanged` event on itself. 3079 * 3080 * @param {Object} target 3081 * The object to be made stateful. 3082 * 3083 * @param {Object} [defaultState] 3084 * A default set of properties to populate the newly-stateful object's 3085 * `state` property. 3086 * 3087 * @return {Object} 3088 * Returns the `target`. 3089 */ 3090 3091 function stateful(target, defaultState) { 3092 assign(target, StatefulMixin); // This happens after the mixing-in because we need to replace the `state` 3093 // added in that step. 3094 3095 target.state = assign({}, target.state, defaultState); // Auto-bind the `handleStateChanged` method of the target object if it exists. 3096 3097 if (typeof target.handleStateChanged === 'function' && isEvented(target)) { 3098 target.on('statechanged', target.handleStateChanged); 3099 } 3100 3101 return target; 3102 } 3103 3104 /** 3105 * @file string-cases.js 3106 * @module to-lower-case 3107 */ 3108 3109 /** 3110 * Lowercase the first letter of a string. 3111 * 3112 * @param {string} string 3113 * String to be lowercased 3114 * 3115 * @return {string} 3116 * The string with a lowercased first letter 3117 */ 3118 var toLowerCase = function toLowerCase(string) { 3119 if (typeof string !== 'string') { 3120 return string; 3121 } 3122 3123 return string.replace(/./, function (w) { 3124 return w.toLowerCase(); 3125 }); 3126 }; 3127 /** 3128 * Uppercase the first letter of a string. 3129 * 3130 * @param {string} string 3131 * String to be uppercased 3132 * 3133 * @return {string} 3134 * The string with an uppercased first letter 3135 */ 3136 3137 var toTitleCase = function toTitleCase(string) { 3138 if (typeof string !== 'string') { 3139 return string; 3140 } 3141 3142 return string.replace(/./, function (w) { 3143 return w.toUpperCase(); 3144 }); 3145 }; 3146 /** 3147 * Compares the TitleCase versions of the two strings for equality. 3148 * 3149 * @param {string} str1 3150 * The first string to compare 3151 * 3152 * @param {string} str2 3153 * The second string to compare 3154 * 3155 * @return {boolean} 3156 * Whether the TitleCase versions of the strings are equal 3157 */ 3158 3159 var titleCaseEquals = function titleCaseEquals(str1, str2) { 3160 return toTitleCase(str1) === toTitleCase(str2); 3161 }; 3162 3163 /** 3164 * @file merge-options.js 3165 * @module merge-options 3166 */ 3167 /** 3168 * Merge two objects recursively. 3169 * 3170 * Performs a deep merge like 3171 * {@link https://lodash.com/docs/4.17.10#merge|lodash.merge}, but only merges 3172 * plain objects (not arrays, elements, or anything else). 3173 * 3174 * Non-plain object values will be copied directly from the right-most 3175 * argument. 3176 * 3177 * @static 3178 * @param {Object[]} sources 3179 * One or more objects to merge into a new object. 3180 * 3181 * @return {Object} 3182 * A new object that is the merged result of all sources. 3183 */ 3184 3185 function mergeOptions() { 3186 var result = {}; 3187 3188 for (var _len = arguments.length, sources = new Array(_len), _key = 0; _key < _len; _key++) { 3189 sources[_key] = arguments[_key]; 3190 } 3191 3192 sources.forEach(function (source) { 3193 if (!source) { 3194 return; 3195 } 3196 3197 each(source, function (value, key) { 3198 if (!isPlain(value)) { 3199 result[key] = value; 3200 return; 3201 } 3202 3203 if (!isPlain(result[key])) { 3204 result[key] = {}; 3205 } 3206 3207 result[key] = mergeOptions(result[key], value); 3208 }); 3209 }); 3210 return result; 3211 } 3212 3213 /** 3214 * Player Component - Base class for all UI objects 3215 * 3216 * @file component.js 3217 */ 3218 /** 3219 * Base class for all UI Components. 3220 * Components are UI objects which represent both a javascript object and an element 3221 * in the DOM. They can be children of other components, and can have 3222 * children themselves. 3223 * 3224 * Components can also use methods from {@link EventTarget} 3225 */ 3226 3227 var Component = 3228 /*#__PURE__*/ 3229 function () { 3230 /** 3231 * A callback that is called when a component is ready. Does not have any 3232 * paramters and any callback value will be ignored. 3233 * 3234 * @callback Component~ReadyCallback 3235 * @this Component 3236 */ 3237 3238 /** 3239 * Creates an instance of this class. 3240 * 3241 * @param {Player} player 3242 * The `Player` that this class should be attached to. 3243 * 3244 * @param {Object} [options] 3245 * The key/value store of player options. 3246 * 3247 * @param {Object[]} [options.children] 3248 * An array of children objects to intialize this component with. Children objects have 3249 * a name property that will be used if more than one component of the same type needs to be 3250 * added. 3251 * 3252 * @param {Component~ReadyCallback} [ready] 3253 * Function that gets called when the `Component` is ready. 3254 */ 3255 function Component(player, options, ready) { 3256 // The component might be the player itself and we can't pass `this` to super 3257 if (!player && this.play) { 3258 this.player_ = player = this; // eslint-disable-line 3259 } else { 3260 this.player_ = player; 3261 } 3262 3263 this.isDisposed_ = false; // Hold the reference to the parent component via `addChild` method 3264 3265 this.parentComponent_ = null; // Make a copy of prototype.options_ to protect against overriding defaults 3266 3267 this.options_ = mergeOptions({}, this.options_); // Updated options with supplied options 3268 3269 options = this.options_ = mergeOptions(this.options_, options); // Get ID from options or options element if one is supplied 3270 3271 this.id_ = options.id || options.el && options.el.id; // If there was no ID from the options, generate one 3272 3273 if (!this.id_) { 3274 // Don't require the player ID function in the case of mock players 3275 var id = player && player.id && player.id() || 'no_player'; 3276 this.id_ = id + "_component_" + newGUID(); 3277 } 3278 3279 this.name_ = options.name || null; // Create element if one wasn't provided in options 3280 3281 if (options.el) { 3282 this.el_ = options.el; 3283 } else if (options.createEl !== false) { 3284 this.el_ = this.createEl(); 3285 } // if evented is anything except false, we want to mixin in evented 3286 3287 3288 if (options.evented !== false) { 3289 // Make this an evented object and use `el_`, if available, as its event bus 3290 evented(this, { 3291 eventBusKey: this.el_ ? 'el_' : null 3292 }); 3293 } 3294 3295 stateful(this, this.constructor.defaultState); 3296 this.children_ = []; 3297 this.childIndex_ = {}; 3298 this.childNameIndex_ = {}; 3299 var SetSham; 3300 3301 if (!window$1.Set) { 3302 SetSham = 3303 /*#__PURE__*/ 3304 function () { 3305 function SetSham() { 3306 this.set_ = {}; 3307 } 3308 3309 var _proto2 = SetSham.prototype; 3310 3311 _proto2.has = function has(key) { 3312 return key in this.set_; 3313 }; 3314 3315 _proto2["delete"] = function _delete(key) { 3316 var has = this.has(key); 3317 delete this.set_[key]; 3318 return has; 3319 }; 3320 3321 _proto2.add = function add(key) { 3322 this.set_[key] = 1; 3323 return this; 3324 }; 3325 3326 _proto2.forEach = function forEach(callback, thisArg) { 3327 for (var key in this.set_) { 3328 callback.call(thisArg, key, key, this); 3329 } 3330 }; 3331 3332 return SetSham; 3333 }(); 3334 } 3335 3336 this.setTimeoutIds_ = window$1.Set ? new Set() : new SetSham(); 3337 this.setIntervalIds_ = window$1.Set ? new Set() : new SetSham(); 3338 this.rafIds_ = window$1.Set ? new Set() : new SetSham(); 3339 this.clearingTimersOnDispose_ = false; // Add any child components in options 3340 3341 if (options.initChildren !== false) { 3342 this.initChildren(); 3343 } 3344 3345 this.ready(ready); // Don't want to trigger ready here or it will before init is actually 3346 // finished for all children that run this constructor 3347 3348 if (options.reportTouchActivity !== false) { 3349 this.enableTouchActivity(); 3350 } 3351 } 3352 /** 3353 * Dispose of the `Component` and all child components. 3354 * 3355 * @fires Component#dispose 3356 */ 3357 3358 3359 var _proto = Component.prototype; 3360 3361 _proto.dispose = function dispose() { 3362 // Bail out if the component has already been disposed. 3363 if (this.isDisposed_) { 3364 return; 3365 } 3366 /** 3367 * Triggered when a `Component` is disposed. 3368 * 3369 * @event Component#dispose 3370 * @type {EventTarget~Event} 3371 * 3372 * @property {boolean} [bubbles=false] 3373 * set to false so that the dispose event does not 3374 * bubble up 3375 */ 3376 3377 3378 this.trigger({ 3379 type: 'dispose', 3380 bubbles: false 3381 }); 3382 this.isDisposed_ = true; // Dispose all children. 3383 3384 if (this.children_) { 3385 for (var i = this.children_.length - 1; i >= 0; i--) { 3386 if (this.children_[i].dispose) { 3387 this.children_[i].dispose(); 3388 } 3389 } 3390 } // Delete child references 3391 3392 3393 this.children_ = null; 3394 this.childIndex_ = null; 3395 this.childNameIndex_ = null; 3396 this.parentComponent_ = null; 3397 3398 if (this.el_) { 3399 // Remove element from DOM 3400 if (this.el_.parentNode) { 3401 this.el_.parentNode.removeChild(this.el_); 3402 } 3403 3404 if (DomData.has(this.el_)) { 3405 DomData["delete"](this.el_); 3406 } 3407 3408 this.el_ = null; 3409 } // remove reference to the player after disposing of the element 3410 3411 3412 this.player_ = null; 3413 } 3414 /** 3415 * Determine whether or not this component has been disposed. 3416 * 3417 * @return {boolean} 3418 * If the component has been disposed, will be `true`. Otherwise, `false`. 3419 */ 3420 ; 3421 3422 _proto.isDisposed = function isDisposed() { 3423 return Boolean(this.isDisposed_); 3424 } 3425 /** 3426 * Return the {@link Player} that the `Component` has attached to. 3427 * 3428 * @return {Player} 3429 * The player that this `Component` has attached to. 3430 */ 3431 ; 3432 3433 _proto.player = function player() { 3434 return this.player_; 3435 } 3436 /** 3437 * Deep merge of options objects with new options. 3438 * > Note: When both `obj` and `options` contain properties whose values are objects. 3439 * The two properties get merged using {@link module:mergeOptions} 3440 * 3441 * @param {Object} obj 3442 * The object that contains new options. 3443 * 3444 * @return {Object} 3445 * A new object of `this.options_` and `obj` merged together. 3446 */ 3447 ; 3448 3449 _proto.options = function options(obj) { 3450 if (!obj) { 3451 return this.options_; 3452 } 3453 3454 this.options_ = mergeOptions(this.options_, obj); 3455 return this.options_; 3456 } 3457 /** 3458 * Get the `Component`s DOM element 3459 * 3460 * @return {Element} 3461 * The DOM element for this `Component`. 3462 */ 3463 ; 3464 3465 _proto.el = function el() { 3466 return this.el_; 3467 } 3468 /** 3469 * Create the `Component`s DOM element. 3470 * 3471 * @param {string} [tagName] 3472 * Element's DOM node type. e.g. 'div' 3473 * 3474 * @param {Object} [properties] 3475 * An object of properties that should be set. 3476 * 3477 * @param {Object} [attributes] 3478 * An object of attributes that should be set. 3479 * 3480 * @return {Element} 3481 * The element that gets created. 3482 */ 3483 ; 3484 3485 _proto.createEl = function createEl$1(tagName, properties, attributes) { 3486 return createEl(tagName, properties, attributes); 3487 } 3488 /** 3489 * Localize a string given the string in english. 3490 * 3491 * If tokens are provided, it'll try and run a simple token replacement on the provided string. 3492 * The tokens it looks for look like `{1}` with the index being 1-indexed into the tokens array. 3493 * 3494 * If a `defaultValue` is provided, it'll use that over `string`, 3495 * if a value isn't found in provided language files. 3496 * This is useful if you want to have a descriptive key for token replacement 3497 * but have a succinct localized string and not require `en.json` to be included. 3498 * 3499 * Currently, it is used for the progress bar timing. 3500 * ```js 3501 * { 3502 * "progress bar timing: currentTime={1} duration={2}": "{1} of {2}" 3503 * } 3504 * ``` 3505 * It is then used like so: 3506 * ```js 3507 * this.localize('progress bar timing: currentTime={1} duration{2}', 3508 * [this.player_.currentTime(), this.player_.duration()], 3509 * '{1} of {2}'); 3510 * ``` 3511 * 3512 * Which outputs something like: `01:23 of 24:56`. 3513 * 3514 * 3515 * @param {string} string 3516 * The string to localize and the key to lookup in the language files. 3517 * @param {string[]} [tokens] 3518 * If the current item has token replacements, provide the tokens here. 3519 * @param {string} [defaultValue] 3520 * Defaults to `string`. Can be a default value to use for token replacement 3521 * if the lookup key is needed to be separate. 3522 * 3523 * @return {string} 3524 * The localized string or if no localization exists the english string. 3525 */ 3526 ; 3527 3528 _proto.localize = function localize(string, tokens, defaultValue) { 3529 if (defaultValue === void 0) { 3530 defaultValue = string; 3531 } 3532 3533 var code = this.player_.language && this.player_.language(); 3534 var languages = this.player_.languages && this.player_.languages(); 3535 var language = languages && languages[code]; 3536 var primaryCode = code && code.split('-')[0]; 3537 var primaryLang = languages && languages[primaryCode]; 3538 var localizedString = defaultValue; 3539 3540 if (language && language[string]) { 3541 localizedString = language[string]; 3542 } else if (primaryLang && primaryLang[string]) { 3543 localizedString = primaryLang[string]; 3544 } 3545 3546 if (tokens) { 3547 localizedString = localizedString.replace(/\{(\d+)\}/g, function (match, index) { 3548 var value = tokens[index - 1]; 3549 var ret = value; 3550 3551 if (typeof value === 'undefined') { 3552 ret = match; 3553 } 3554 3555 return ret; 3556 }); 3557 } 3558 3559 return localizedString; 3560 } 3561 /** 3562 * Return the `Component`s DOM element. This is where children get inserted. 3563 * This will usually be the the same as the element returned in {@link Component#el}. 3564 * 3565 * @return {Element} 3566 * The content element for this `Component`. 3567 */ 3568 ; 3569 3570 _proto.contentEl = function contentEl() { 3571 return this.contentEl_ || this.el_; 3572 } 3573 /** 3574 * Get this `Component`s ID 3575 * 3576 * @return {string} 3577 * The id of this `Component` 3578 */ 3579 ; 3580 3581 _proto.id = function id() { 3582 return this.id_; 3583 } 3584 /** 3585 * Get the `Component`s name. The name gets used to reference the `Component` 3586 * and is set during registration. 3587 * 3588 * @return {string} 3589 * The name of this `Component`. 3590 */ 3591 ; 3592 3593 _proto.name = function name() { 3594 return this.name_; 3595 } 3596 /** 3597 * Get an array of all child components 3598 * 3599 * @return {Array} 3600 * The children 3601 */ 3602 ; 3603 3604 _proto.children = function children() { 3605 return this.children_; 3606 } 3607 /** 3608 * Returns the child `Component` with the given `id`. 3609 * 3610 * @param {string} id 3611 * The id of the child `Component` to get. 3612 * 3613 * @return {Component|undefined} 3614 * The child `Component` with the given `id` or undefined. 3615 */ 3616 ; 3617 3618 _proto.getChildById = function getChildById(id) { 3619 return this.childIndex_[id]; 3620 } 3621 /** 3622 * Returns the child `Component` with the given `name`. 3623 * 3624 * @param {string} name 3625 * The name of the child `Component` to get. 3626 * 3627 * @return {Component|undefined} 3628 * The child `Component` with the given `name` or undefined. 3629 */ 3630 ; 3631 3632 _proto.getChild = function getChild(name) { 3633 if (!name) { 3634 return; 3635 } 3636 3637 return this.childNameIndex_[name]; 3638 } 3639 /** 3640 * Add a child `Component` inside the current `Component`. 3641 * 3642 * 3643 * @param {string|Component} child 3644 * The name or instance of a child to add. 3645 * 3646 * @param {Object} [options={}] 3647 * The key/value store of options that will get passed to children of 3648 * the child. 3649 * 3650 * @param {number} [index=this.children_.length] 3651 * The index to attempt to add a child into. 3652 * 3653 * @return {Component} 3654 * The `Component` that gets added as a child. When using a string the 3655 * `Component` will get created by this process. 3656 */ 3657 ; 3658 3659 _proto.addChild = function addChild(child, options, index) { 3660 if (options === void 0) { 3661 options = {}; 3662 } 3663 3664 if (index === void 0) { 3665 index = this.children_.length; 3666 } 3667 3668 var component; 3669 var componentName; // If child is a string, create component with options 3670 3671 if (typeof child === 'string') { 3672 componentName = toTitleCase(child); 3673 var componentClassName = options.componentClass || componentName; // Set name through options 3674 3675 options.name = componentName; // Create a new object & element for this controls set 3676 // If there's no .player_, this is a player 3677 3678 var ComponentClass = Component.getComponent(componentClassName); 3679 3680 if (!ComponentClass) { 3681 throw new Error("Component " + componentClassName + " does not exist"); 3682 } // data stored directly on the videojs object may be 3683 // misidentified as a component to retain 3684 // backwards-compatibility with 4.x. check to make sure the 3685 // component class can be instantiated. 3686 3687 3688 if (typeof ComponentClass !== 'function') { 3689 return null; 3690 } 3691 3692 component = new ComponentClass(this.player_ || this, options); // child is a component instance 3693 } else { 3694 component = child; 3695 } 3696 3697 if (component.parentComponent_) { 3698 component.parentComponent_.removeChild(component); 3699 } 3700 3701 this.children_.splice(index, 0, component); 3702 component.parentComponent_ = this; 3703 3704 if (typeof component.id === 'function') { 3705 this.childIndex_[component.id()] = component; 3706 } // If a name wasn't used to create the component, check if we can use the 3707 // name function of the component 3708 3709 3710 componentName = componentName || component.name && toTitleCase(component.name()); 3711 3712 if (componentName) { 3713 this.childNameIndex_[componentName] = component; 3714 this.childNameIndex_[toLowerCase(componentName)] = component; 3715 } // Add the UI object's element to the container div (box) 3716 // Having an element is not required 3717 3718 3719 if (typeof component.el === 'function' && component.el()) { 3720 // If inserting before a component, insert before that component's element 3721 var refNode = null; 3722 3723 if (this.children_[index + 1] && this.children_[index + 1].el_) { 3724 refNode = this.children_[index + 1].el_; 3725 } 3726 3727 this.contentEl().insertBefore(component.el(), refNode); 3728 } // Return so it can stored on parent object if desired. 3729 3730 3731 return component; 3732 } 3733 /** 3734 * Remove a child `Component` from this `Component`s list of children. Also removes 3735 * the child `Component`s element from this `Component`s element. 3736 * 3737 * @param {Component} component 3738 * The child `Component` to remove. 3739 */ 3740 ; 3741 3742 _proto.removeChild = function removeChild(component) { 3743 if (typeof component === 'string') { 3744 component = this.getChild(component); 3745 } 3746 3747 if (!component || !this.children_) { 3748 return; 3749 } 3750 3751 var childFound = false; 3752 3753 for (var i = this.children_.length - 1; i >= 0; i--) { 3754 if (this.children_[i] === component) { 3755 childFound = true; 3756 this.children_.splice(i, 1); 3757 break; 3758 } 3759 } 3760 3761 if (!childFound) { 3762 return; 3763 } 3764 3765 component.parentComponent_ = null; 3766 this.childIndex_[component.id()] = null; 3767 this.childNameIndex_[toTitleCase(component.name())] = null; 3768 this.childNameIndex_[toLowerCase(component.name())] = null; 3769 var compEl = component.el(); 3770 3771 if (compEl && compEl.parentNode === this.contentEl()) { 3772 this.contentEl().removeChild(component.el()); 3773 } 3774 } 3775 /** 3776 * Add and initialize default child `Component`s based upon options. 3777 */ 3778 ; 3779 3780 _proto.initChildren = function initChildren() { 3781 var _this = this; 3782 3783 var children = this.options_.children; 3784 3785 if (children) { 3786 // `this` is `parent` 3787 var parentOptions = this.options_; 3788 3789 var handleAdd = function handleAdd(child) { 3790 var name = child.name; 3791 var opts = child.opts; // Allow options for children to be set at the parent options 3792 // e.g. videojs(id, { controlBar: false }); 3793 // instead of videojs(id, { children: { controlBar: false }); 3794 3795 if (parentOptions[name] !== undefined) { 3796 opts = parentOptions[name]; 3797 } // Allow for disabling default components 3798 // e.g. options['children']['posterImage'] = false 3799 3800 3801 if (opts === false) { 3802 return; 3803 } // Allow options to be passed as a simple boolean if no configuration 3804 // is necessary. 3805 3806 3807 if (opts === true) { 3808 opts = {}; 3809 } // We also want to pass the original player options 3810 // to each component as well so they don't need to 3811 // reach back into the player for options later. 3812 3813 3814 opts.playerOptions = _this.options_.playerOptions; // Create and add the child component. 3815 // Add a direct reference to the child by name on the parent instance. 3816 // If two of the same component are used, different names should be supplied 3817 // for each 3818 3819 var newChild = _this.addChild(name, opts); 3820 3821 if (newChild) { 3822 _this[name] = newChild; 3823 } 3824 }; // Allow for an array of children details to passed in the options 3825 3826 3827 var workingChildren; 3828 var Tech = Component.getComponent('Tech'); 3829 3830 if (Array.isArray(children)) { 3831 workingChildren = children; 3832 } else { 3833 workingChildren = Object.keys(children); 3834 } 3835 3836 workingChildren // children that are in this.options_ but also in workingChildren would 3837 // give us extra children we do not want. So, we want to filter them out. 3838 .concat(Object.keys(this.options_).filter(function (child) { 3839 return !workingChildren.some(function (wchild) { 3840 if (typeof wchild === 'string') { 3841 return child === wchild; 3842 } 3843 3844 return child === wchild.name; 3845 }); 3846 })).map(function (child) { 3847 var name; 3848 var opts; 3849 3850 if (typeof child === 'string') { 3851 name = child; 3852 opts = children[name] || _this.options_[name] || {}; 3853 } else { 3854 name = child.name; 3855 opts = child; 3856 } 3857 3858 return { 3859 name: name, 3860 opts: opts 3861 }; 3862 }).filter(function (child) { 3863 // we have to make sure that child.name isn't in the techOrder since 3864 // techs are registerd as Components but can't aren't compatible 3865 // See https://github.com/videojs/video.js/issues/2772 3866 var c = Component.getComponent(child.opts.componentClass || toTitleCase(child.name)); 3867 return c && !Tech.isTech(c); 3868 }).forEach(handleAdd); 3869 } 3870 } 3871 /** 3872 * Builds the default DOM class name. Should be overriden by sub-components. 3873 * 3874 * @return {string} 3875 * The DOM class name for this object. 3876 * 3877 * @abstract 3878 */ 3879 ; 3880 3881 _proto.buildCSSClass = function buildCSSClass() { 3882 // Child classes can include a function that does: 3883 // return 'CLASS NAME' + this._super(); 3884 return ''; 3885 } 3886 /** 3887 * Bind a listener to the component's ready state. 3888 * Different from event listeners in that if the ready event has already happened 3889 * it will trigger the function immediately. 3890 * 3891 * @return {Component} 3892 * Returns itself; method can be chained. 3893 */ 3894 ; 3895 3896 _proto.ready = function ready(fn, sync) { 3897 if (sync === void 0) { 3898 sync = false; 3899 } 3900 3901 if (!fn) { 3902 return; 3903 } 3904 3905 if (!this.isReady_) { 3906 this.readyQueue_ = this.readyQueue_ || []; 3907 this.readyQueue_.push(fn); 3908 return; 3909 } 3910 3911 if (sync) { 3912 fn.call(this); 3913 } else { 3914 // Call the function asynchronously by default for consistency 3915 this.setTimeout(fn, 1); 3916 } 3917 } 3918 /** 3919 * Trigger all the ready listeners for this `Component`. 3920 * 3921 * @fires Component#ready 3922 */ 3923 ; 3924 3925 _proto.triggerReady = function triggerReady() { 3926 this.isReady_ = true; // Ensure ready is triggered asynchronously 3927 3928 this.setTimeout(function () { 3929 var readyQueue = this.readyQueue_; // Reset Ready Queue 3930 3931 this.readyQueue_ = []; 3932 3933 if (readyQueue && readyQueue.length > 0) { 3934 readyQueue.forEach(function (fn) { 3935 fn.call(this); 3936 }, this); 3937 } // Allow for using event listeners also 3938 3939 /** 3940 * Triggered when a `Component` is ready. 3941 * 3942 * @event Component#ready 3943 * @type {EventTarget~Event} 3944 */ 3945 3946 3947 this.trigger('ready'); 3948 }, 1); 3949 } 3950 /** 3951 * Find a single DOM element matching a `selector`. This can be within the `Component`s 3952 * `contentEl()` or another custom context. 3953 * 3954 * @param {string} selector 3955 * A valid CSS selector, which will be passed to `querySelector`. 3956 * 3957 * @param {Element|string} [context=this.contentEl()] 3958 * A DOM element within which to query. Can also be a selector string in 3959 * which case the first matching element will get used as context. If 3960 * missing `this.contentEl()` gets used. If `this.contentEl()` returns 3961 * nothing it falls back to `document`. 3962 * 3963 * @return {Element|null} 3964 * the dom element that was found, or null 3965 * 3966 * @see [Information on CSS Selectors](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors) 3967 */ 3968 ; 3969 3970 _proto.$ = function $$1(selector, context) { 3971 return $(selector, context || this.contentEl()); 3972 } 3973 /** 3974 * Finds all DOM element matching a `selector`. This can be within the `Component`s 3975 * `contentEl()` or another custom context. 3976 * 3977 * @param {string} selector 3978 * A valid CSS selector, which will be passed to `querySelectorAll`. 3979 * 3980 * @param {Element|string} [context=this.contentEl()] 3981 * A DOM element within which to query. Can also be a selector string in 3982 * which case the first matching element will get used as context. If 3983 * missing `this.contentEl()` gets used. If `this.contentEl()` returns 3984 * nothing it falls back to `document`. 3985 * 3986 * @return {NodeList} 3987 * a list of dom elements that were found 3988 * 3989 * @see [Information on CSS Selectors](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors) 3990 */ 3991 ; 3992 3993 _proto.$$ = function $$$1(selector, context) { 3994 return $$(selector, context || this.contentEl()); 3995 } 3996 /** 3997 * Check if a component's element has a CSS class name. 3998 * 3999 * @param {string} classToCheck 4000 * CSS class name to check. 4001 * 4002 * @return {boolean} 4003 * - True if the `Component` has the class. 4004 * - False if the `Component` does not have the class` 4005 */ 4006 ; 4007 4008 _proto.hasClass = function hasClass$1(classToCheck) { 4009 return hasClass(this.el_, classToCheck); 4010 } 4011 /** 4012 * Add a CSS class name to the `Component`s element. 4013 * 4014 * @param {string} classToAdd 4015 * CSS class name to add 4016 */ 4017 ; 4018 4019 _proto.addClass = function addClass$1(classToAdd) { 4020 addClass(this.el_, classToAdd); 4021 } 4022 /** 4023 * Remove a CSS class name from the `Component`s element. 4024 * 4025 * @param {string} classToRemove 4026 * CSS class name to remove 4027 */ 4028 ; 4029 4030 _proto.removeClass = function removeClass$1(classToRemove) { 4031 removeClass(this.el_, classToRemove); 4032 } 4033 /** 4034 * Add or remove a CSS class name from the component's element. 4035 * - `classToToggle` gets added when {@link Component#hasClass} would return false. 4036 * - `classToToggle` gets removed when {@link Component#hasClass} would return true. 4037 * 4038 * @param {string} classToToggle 4039 * The class to add or remove based on (@link Component#hasClass} 4040 * 4041 * @param {boolean|Dom~predicate} [predicate] 4042 * An {@link Dom~predicate} function or a boolean 4043 */ 4044 ; 4045 4046 _proto.toggleClass = function toggleClass$1(classToToggle, predicate) { 4047 toggleClass(this.el_, classToToggle, predicate); 4048 } 4049 /** 4050 * Show the `Component`s element if it is hidden by removing the 4051 * 'vjs-hidden' class name from it. 4052 */ 4053 ; 4054 4055 _proto.show = function show() { 4056 this.removeClass('vjs-hidden'); 4057 } 4058 /** 4059 * Hide the `Component`s element if it is currently showing by adding the 4060 * 'vjs-hidden` class name to it. 4061 */ 4062 ; 4063 4064 _proto.hide = function hide() { 4065 this.addClass('vjs-hidden'); 4066 } 4067 /** 4068 * Lock a `Component`s element in its visible state by adding the 'vjs-lock-showing' 4069 * class name to it. Used during fadeIn/fadeOut. 4070 * 4071 * @private 4072 */ 4073 ; 4074 4075 _proto.lockShowing = function lockShowing() { 4076 this.addClass('vjs-lock-showing'); 4077 } 4078 /** 4079 * Unlock a `Component`s element from its visible state by removing the 'vjs-lock-showing' 4080 * class name from it. Used during fadeIn/fadeOut. 4081 * 4082 * @private 4083 */ 4084 ; 4085 4086 _proto.unlockShowing = function unlockShowing() { 4087 this.removeClass('vjs-lock-showing'); 4088 } 4089 /** 4090 * Get the value of an attribute on the `Component`s element. 4091 * 4092 * @param {string} attribute 4093 * Name of the attribute to get the value from. 4094 * 4095 * @return {string|null} 4096 * - The value of the attribute that was asked for. 4097 * - Can be an empty string on some browsers if the attribute does not exist 4098 * or has no value 4099 * - Most browsers will return null if the attibute does not exist or has 4100 * no value. 4101 * 4102 * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute} 4103 */ 4104 ; 4105 4106 _proto.getAttribute = function getAttribute$1(attribute) { 4107 return getAttribute(this.el_, attribute); 4108 } 4109 /** 4110 * Set the value of an attribute on the `Component`'s element 4111 * 4112 * @param {string} attribute 4113 * Name of the attribute to set. 4114 * 4115 * @param {string} value 4116 * Value to set the attribute to. 4117 * 4118 * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute} 4119 */ 4120 ; 4121 4122 _proto.setAttribute = function setAttribute$1(attribute, value) { 4123 setAttribute(this.el_, attribute, value); 4124 } 4125 /** 4126 * Remove an attribute from the `Component`s element. 4127 * 4128 * @param {string} attribute 4129 * Name of the attribute to remove. 4130 * 4131 * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/removeAttribute} 4132 */ 4133 ; 4134 4135 _proto.removeAttribute = function removeAttribute$1(attribute) { 4136 removeAttribute(this.el_, attribute); 4137 } 4138 /** 4139 * Get or set the width of the component based upon the CSS styles. 4140 * See {@link Component#dimension} for more detailed information. 4141 * 4142 * @param {number|string} [num] 4143 * The width that you want to set postfixed with '%', 'px' or nothing. 4144 * 4145 * @param {boolean} [skipListeners] 4146 * Skip the componentresize event trigger 4147 * 4148 * @return {number|string} 4149 * The width when getting, zero if there is no width. Can be a string 4150 * postpixed with '%' or 'px'. 4151 */ 4152 ; 4153 4154 _proto.width = function width(num, skipListeners) { 4155 return this.dimension('width', num, skipListeners); 4156 } 4157 /** 4158 * Get or set the height of the component based upon the CSS styles. 4159 * See {@link Component#dimension} for more detailed information. 4160 * 4161 * @param {number|string} [num] 4162 * The height that you want to set postfixed with '%', 'px' or nothing. 4163 * 4164 * @param {boolean} [skipListeners] 4165 * Skip the componentresize event trigger 4166 * 4167 * @return {number|string} 4168 * The width when getting, zero if there is no width. Can be a string 4169 * postpixed with '%' or 'px'. 4170 */ 4171 ; 4172 4173 _proto.height = function height(num, skipListeners) { 4174 return this.dimension('height', num, skipListeners); 4175 } 4176 /** 4177 * Set both the width and height of the `Component` element at the same time. 4178 * 4179 * @param {number|string} width 4180 * Width to set the `Component`s element to. 4181 * 4182 * @param {number|string} height 4183 * Height to set the `Component`s element to. 4184 */ 4185 ; 4186 4187 _proto.dimensions = function dimensions(width, height) { 4188 // Skip componentresize listeners on width for optimization 4189 this.width(width, true); 4190 this.height(height); 4191 } 4192 /** 4193 * Get or set width or height of the `Component` element. This is the shared code 4194 * for the {@link Component#width} and {@link Component#height}. 4195 * 4196 * Things to know: 4197 * - If the width or height in an number this will return the number postfixed with 'px'. 4198 * - If the width/height is a percent this will return the percent postfixed with '%' 4199 * - Hidden elements have a width of 0 with `window.getComputedStyle`. This function 4200 * defaults to the `Component`s `style.width` and falls back to `window.getComputedStyle`. 4201 * See [this]{@link http://www.foliotek.com/devblog/getting-the-width-of-a-hidden-element-with-jquery-using-width/} 4202 * for more information 4203 * - If you want the computed style of the component, use {@link Component#currentWidth} 4204 * and {@link {Component#currentHeight} 4205 * 4206 * @fires Component#componentresize 4207 * 4208 * @param {string} widthOrHeight 4209 8 'width' or 'height' 4210 * 4211 * @param {number|string} [num] 4212 8 New dimension 4213 * 4214 * @param {boolean} [skipListeners] 4215 * Skip componentresize event trigger 4216 * 4217 * @return {number} 4218 * The dimension when getting or 0 if unset 4219 */ 4220 ; 4221 4222 _proto.dimension = function dimension(widthOrHeight, num, skipListeners) { 4223 if (num !== undefined) { 4224 // Set to zero if null or literally NaN (NaN !== NaN) 4225 if (num === null || num !== num) { 4226 num = 0; 4227 } // Check if using css width/height (% or px) and adjust 4228 4229 4230 if (('' + num).indexOf('%') !== -1 || ('' + num).indexOf('px') !== -1) { 4231 this.el_.style[widthOrHeight] = num; 4232 } else if (num === 'auto') { 4233 this.el_.style[widthOrHeight] = ''; 4234 } else { 4235 this.el_.style[widthOrHeight] = num + 'px'; 4236 } // skipListeners allows us to avoid triggering the resize event when setting both width and height 4237 4238 4239 if (!skipListeners) { 4240 /** 4241 * Triggered when a component is resized. 4242 * 4243 * @event Component#componentresize 4244 * @type {EventTarget~Event} 4245 */ 4246 this.trigger('componentresize'); 4247 } 4248 4249 return; 4250 } // Not setting a value, so getting it 4251 // Make sure element exists 4252 4253 4254 if (!this.el_) { 4255 return 0; 4256 } // Get dimension value from style 4257 4258 4259 var val = this.el_.style[widthOrHeight]; 4260 var pxIndex = val.indexOf('px'); 4261 4262 if (pxIndex !== -1) { 4263 // Return the pixel value with no 'px' 4264 return parseInt(val.slice(0, pxIndex), 10); 4265 } // No px so using % or no style was set, so falling back to offsetWidth/height 4266 // If component has display:none, offset will return 0 4267 // TODO: handle display:none and no dimension style using px 4268 4269 4270 return parseInt(this.el_['offset' + toTitleCase(widthOrHeight)], 10); 4271 } 4272 /** 4273 * Get the computed width or the height of the component's element. 4274 * 4275 * Uses `window.getComputedStyle`. 4276 * 4277 * @param {string} widthOrHeight 4278 * A string containing 'width' or 'height'. Whichever one you want to get. 4279 * 4280 * @return {number} 4281 * The dimension that gets asked for or 0 if nothing was set 4282 * for that dimension. 4283 */ 4284 ; 4285 4286 _proto.currentDimension = function currentDimension(widthOrHeight) { 4287 var computedWidthOrHeight = 0; 4288 4289 if (widthOrHeight !== 'width' && widthOrHeight !== 'height') { 4290 throw new Error('currentDimension only accepts width or height value'); 4291 } 4292 4293 computedWidthOrHeight = computedStyle(this.el_, widthOrHeight); // remove 'px' from variable and parse as integer 4294 4295 computedWidthOrHeight = parseFloat(computedWidthOrHeight); // if the computed value is still 0, it's possible that the browser is lying 4296 // and we want to check the offset values. 4297 // This code also runs wherever getComputedStyle doesn't exist. 4298 4299 if (computedWidthOrHeight === 0 || isNaN(computedWidthOrHeight)) { 4300 var rule = "offset" + toTitleCase(widthOrHeight); 4301 computedWidthOrHeight = this.el_[rule]; 4302 } 4303 4304 return computedWidthOrHeight; 4305 } 4306 /** 4307 * An object that contains width and height values of the `Component`s 4308 * computed style. Uses `window.getComputedStyle`. 4309 * 4310 * @typedef {Object} Component~DimensionObject 4311 * 4312 * @property {number} width 4313 * The width of the `Component`s computed style. 4314 * 4315 * @property {number} height 4316 * The height of the `Component`s computed style. 4317 */ 4318 4319 /** 4320 * Get an object that contains computed width and height values of the 4321 * component's element. 4322 * 4323 * Uses `window.getComputedStyle`. 4324 * 4325 * @return {Component~DimensionObject} 4326 * The computed dimensions of the component's element. 4327 */ 4328 ; 4329 4330 _proto.currentDimensions = function currentDimensions() { 4331 return { 4332 width: this.currentDimension('width'), 4333 height: this.currentDimension('height') 4334 }; 4335 } 4336 /** 4337 * Get the computed width of the component's element. 4338 * 4339 * Uses `window.getComputedStyle`. 4340 * 4341 * @return {number} 4342 * The computed width of the component's element. 4343 */ 4344 ; 4345 4346 _proto.currentWidth = function currentWidth() { 4347 return this.currentDimension('width'); 4348 } 4349 /** 4350 * Get the computed height of the component's element. 4351 * 4352 * Uses `window.getComputedStyle`. 4353 * 4354 * @return {number} 4355 * The computed height of the component's element. 4356 */ 4357 ; 4358 4359 _proto.currentHeight = function currentHeight() { 4360 return this.currentDimension('height'); 4361 } 4362 /** 4363 * Set the focus to this component 4364 */ 4365 ; 4366 4367 _proto.focus = function focus() { 4368 this.el_.focus(); 4369 } 4370 /** 4371 * Remove the focus from this component 4372 */ 4373 ; 4374 4375 _proto.blur = function blur() { 4376 this.el_.blur(); 4377 } 4378 /** 4379 * When this Component receives a `keydown` event which it does not process, 4380 * it passes the event to the Player for handling. 4381 * 4382 * @param {EventTarget~Event} event 4383 * The `keydown` event that caused this function to be called. 4384 */ 4385 ; 4386 4387 _proto.handleKeyDown = function handleKeyDown(event) { 4388 if (this.player_) { 4389 // We only stop propagation here because we want unhandled events to fall 4390 // back to the browser. 4391 event.stopPropagation(); 4392 this.player_.handleKeyDown(event); 4393 } 4394 } 4395 /** 4396 * Many components used to have a `handleKeyPress` method, which was poorly 4397 * named because it listened to a `keydown` event. This method name now 4398 * delegates to `handleKeyDown`. This means anyone calling `handleKeyPress` 4399 * will not see their method calls stop working. 4400 * 4401 * @param {EventTarget~Event} event 4402 * The event that caused this function to be called. 4403 */ 4404 ; 4405 4406 _proto.handleKeyPress = function handleKeyPress(event) { 4407 this.handleKeyDown(event); 4408 } 4409 /** 4410 * Emit a 'tap' events when touch event support gets detected. This gets used to 4411 * support toggling the controls through a tap on the video. They get enabled 4412 * because every sub-component would have extra overhead otherwise. 4413 * 4414 * @private 4415 * @fires Component#tap 4416 * @listens Component#touchstart 4417 * @listens Component#touchmove 4418 * @listens Component#touchleave 4419 * @listens Component#touchcancel 4420 * @listens Component#touchend 4421 */ 4422 ; 4423 4424 _proto.emitTapEvents = function emitTapEvents() { 4425 // Track the start time so we can determine how long the touch lasted 4426 var touchStart = 0; 4427 var firstTouch = null; // Maximum movement allowed during a touch event to still be considered a tap 4428 // Other popular libs use anywhere from 2 (hammer.js) to 15, 4429 // so 10 seems like a nice, round number. 4430 4431 var tapMovementThreshold = 10; // The maximum length a touch can be while still being considered a tap 4432 4433 var touchTimeThreshold = 200; 4434 var couldBeTap; 4435 this.on('touchstart', function (event) { 4436 // If more than one finger, don't consider treating this as a click 4437 if (event.touches.length === 1) { 4438 // Copy pageX/pageY from the object 4439 firstTouch = { 4440 pageX: event.touches[0].pageX, 4441 pageY: event.touches[0].pageY 4442 }; // Record start time so we can detect a tap vs. "touch and hold" 4443 4444 touchStart = window$1.performance.now(); // Reset couldBeTap tracking 4445 4446 couldBeTap = true; 4447 } 4448 }); 4449 this.on('touchmove', function (event) { 4450 // If more than one finger, don't consider treating this as a click 4451 if (event.touches.length > 1) { 4452 couldBeTap = false; 4453 } else if (firstTouch) { 4454 // Some devices will throw touchmoves for all but the slightest of taps. 4455 // So, if we moved only a small distance, this could still be a tap 4456 var xdiff = event.touches[0].pageX - firstTouch.pageX; 4457 var ydiff = event.touches[0].pageY - firstTouch.pageY; 4458 var touchDistance = Math.sqrt(xdiff * xdiff + ydiff * ydiff); 4459 4460 if (touchDistance > tapMovementThreshold) { 4461 couldBeTap = false; 4462 } 4463 } 4464 }); 4465 4466 var noTap = function noTap() { 4467 couldBeTap = false; 4468 }; // TODO: Listen to the original target. http://youtu.be/DujfpXOKUp8?t=13m8s 4469 4470 4471 this.on('touchleave', noTap); 4472 this.on('touchcancel', noTap); // When the touch ends, measure how long it took and trigger the appropriate 4473 // event 4474 4475 this.on('touchend', function (event) { 4476 firstTouch = null; // Proceed only if the touchmove/leave/cancel event didn't happen 4477 4478 if (couldBeTap === true) { 4479 // Measure how long the touch lasted 4480 var touchTime = window$1.performance.now() - touchStart; // Make sure the touch was less than the threshold to be considered a tap 4481 4482 if (touchTime < touchTimeThreshold) { 4483 // Don't let browser turn this into a click 4484 event.preventDefault(); 4485 /** 4486 * Triggered when a `Component` is tapped. 4487 * 4488 * @event Component#tap 4489 * @type {EventTarget~Event} 4490 */ 4491 4492 this.trigger('tap'); // It may be good to copy the touchend event object and change the 4493 // type to tap, if the other event properties aren't exact after 4494 // Events.fixEvent runs (e.g. event.target) 4495 } 4496 } 4497 }); 4498 } 4499 /** 4500 * This function reports user activity whenever touch events happen. This can get 4501 * turned off by any sub-components that wants touch events to act another way. 4502 * 4503 * Report user touch activity when touch events occur. User activity gets used to 4504 * determine when controls should show/hide. It is simple when it comes to mouse 4505 * events, because any mouse event should show the controls. So we capture mouse 4506 * events that bubble up to the player and report activity when that happens. 4507 * With touch events it isn't as easy as `touchstart` and `touchend` toggle player 4508 * controls. So touch events can't help us at the player level either. 4509 * 4510 * User activity gets checked asynchronously. So what could happen is a tap event 4511 * on the video turns the controls off. Then the `touchend` event bubbles up to 4512 * the player. Which, if it reported user activity, would turn the controls right 4513 * back on. We also don't want to completely block touch events from bubbling up. 4514 * Furthermore a `touchmove` event and anything other than a tap, should not turn 4515 * controls back on. 4516 * 4517 * @listens Component#touchstart 4518 * @listens Component#touchmove 4519 * @listens Component#touchend 4520 * @listens Component#touchcancel 4521 */ 4522 ; 4523 4524 _proto.enableTouchActivity = function enableTouchActivity() { 4525 // Don't continue if the root player doesn't support reporting user activity 4526 if (!this.player() || !this.player().reportUserActivity) { 4527 return; 4528 } // listener for reporting that the user is active 4529 4530 4531 var report = bind(this.player(), this.player().reportUserActivity); 4532 var touchHolding; 4533 this.on('touchstart', function () { 4534 report(); // For as long as the they are touching the device or have their mouse down, 4535 // we consider them active even if they're not moving their finger or mouse. 4536 // So we want to continue to update that they are active 4537 4538 this.clearInterval(touchHolding); // report at the same interval as activityCheck 4539 4540 touchHolding = this.setInterval(report, 250); 4541 }); 4542 4543 var touchEnd = function touchEnd(event) { 4544 report(); // stop the interval that maintains activity if the touch is holding 4545 4546 this.clearInterval(touchHolding); 4547 }; 4548 4549 this.on('touchmove', report); 4550 this.on('touchend', touchEnd); 4551 this.on('touchcancel', touchEnd); 4552 } 4553 /** 4554 * A callback that has no parameters and is bound into `Component`s context. 4555 * 4556 * @callback Component~GenericCallback 4557 * @this Component 4558 */ 4559 4560 /** 4561 * Creates a function that runs after an `x` millisecond timeout. This function is a 4562 * wrapper around `window.setTimeout`. There are a few reasons to use this one 4563 * instead though: 4564 * 1. It gets cleared via {@link Component#clearTimeout} when 4565 * {@link Component#dispose} gets called. 4566 * 2. The function callback will gets turned into a {@link Component~GenericCallback} 4567 * 4568 * > Note: You can't use `window.clearTimeout` on the id returned by this function. This 4569 * will cause its dispose listener not to get cleaned up! Please use 4570 * {@link Component#clearTimeout} or {@link Component#dispose} instead. 4571 * 4572 * @param {Component~GenericCallback} fn 4573 * The function that will be run after `timeout`. 4574 * 4575 * @param {number} timeout 4576 * Timeout in milliseconds to delay before executing the specified function. 4577 * 4578 * @return {number} 4579 * Returns a timeout ID that gets used to identify the timeout. It can also 4580 * get used in {@link Component#clearTimeout} to clear the timeout that 4581 * was set. 4582 * 4583 * @listens Component#dispose 4584 * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setTimeout} 4585 */ 4586 ; 4587 4588 _proto.setTimeout = function setTimeout(fn, timeout) { 4589 var _this2 = this; 4590 4591 // declare as variables so they are properly available in timeout function 4592 // eslint-disable-next-line 4593 var timeoutId; 4594 fn = bind(this, fn); 4595 this.clearTimersOnDispose_(); 4596 timeoutId = window$1.setTimeout(function () { 4597 if (_this2.setTimeoutIds_.has(timeoutId)) { 4598 _this2.setTimeoutIds_["delete"](timeoutId); 4599 } 4600 4601 fn(); 4602 }, timeout); 4603 this.setTimeoutIds_.add(timeoutId); 4604 return timeoutId; 4605 } 4606 /** 4607 * Clears a timeout that gets created via `window.setTimeout` or 4608 * {@link Component#setTimeout}. If you set a timeout via {@link Component#setTimeout} 4609 * use this function instead of `window.clearTimout`. If you don't your dispose 4610 * listener will not get cleaned up until {@link Component#dispose}! 4611 * 4612 * @param {number} timeoutId 4613 * The id of the timeout to clear. The return value of 4614 * {@link Component#setTimeout} or `window.setTimeout`. 4615 * 4616 * @return {number} 4617 * Returns the timeout id that was cleared. 4618 * 4619 * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearTimeout} 4620 */ 4621 ; 4622 4623 _proto.clearTimeout = function clearTimeout(timeoutId) { 4624 if (this.setTimeoutIds_.has(timeoutId)) { 4625 this.setTimeoutIds_["delete"](timeoutId); 4626 window$1.clearTimeout(timeoutId); 4627 } 4628 4629 return timeoutId; 4630 } 4631 /** 4632 * Creates a function that gets run every `x` milliseconds. This function is a wrapper 4633 * around `window.setInterval`. There are a few reasons to use this one instead though. 4634 * 1. It gets cleared via {@link Component#clearInterval} when 4635 * {@link Component#dispose} gets called. 4636 * 2. The function callback will be a {@link Component~GenericCallback} 4637 * 4638 * @param {Component~GenericCallback} fn 4639 * The function to run every `x` seconds. 4640 * 4641 * @param {number} interval 4642 * Execute the specified function every `x` milliseconds. 4643 * 4644 * @return {number} 4645 * Returns an id that can be used to identify the interval. It can also be be used in 4646 * {@link Component#clearInterval} to clear the interval. 4647 * 4648 * @listens Component#dispose 4649 * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setInterval} 4650 */ 4651 ; 4652 4653 _proto.setInterval = function setInterval(fn, interval) { 4654 fn = bind(this, fn); 4655 this.clearTimersOnDispose_(); 4656 var intervalId = window$1.setInterval(fn, interval); 4657 this.setIntervalIds_.add(intervalId); 4658 return intervalId; 4659 } 4660 /** 4661 * Clears an interval that gets created via `window.setInterval` or 4662 * {@link Component#setInterval}. If you set an inteval via {@link Component#setInterval} 4663 * use this function instead of `window.clearInterval`. If you don't your dispose 4664 * listener will not get cleaned up until {@link Component#dispose}! 4665 * 4666 * @param {number} intervalId 4667 * The id of the interval to clear. The return value of 4668 * {@link Component#setInterval} or `window.setInterval`. 4669 * 4670 * @return {number} 4671 * Returns the interval id that was cleared. 4672 * 4673 * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearInterval} 4674 */ 4675 ; 4676 4677 _proto.clearInterval = function clearInterval(intervalId) { 4678 if (this.setIntervalIds_.has(intervalId)) { 4679 this.setIntervalIds_["delete"](intervalId); 4680 window$1.clearInterval(intervalId); 4681 } 4682 4683 return intervalId; 4684 } 4685 /** 4686 * Queues up a callback to be passed to requestAnimationFrame (rAF), but 4687 * with a few extra bonuses: 4688 * 4689 * - Supports browsers that do not support rAF by falling back to 4690 * {@link Component#setTimeout}. 4691 * 4692 * - The callback is turned into a {@link Component~GenericCallback} (i.e. 4693 * bound to the component). 4694 * 4695 * - Automatic cancellation of the rAF callback is handled if the component 4696 * is disposed before it is called. 4697 * 4698 * @param {Component~GenericCallback} fn 4699 * A function that will be bound to this component and executed just 4700 * before the browser's next repaint. 4701 * 4702 * @return {number} 4703 * Returns an rAF ID that gets used to identify the timeout. It can 4704 * also be used in {@link Component#cancelAnimationFrame} to cancel 4705 * the animation frame callback. 4706 * 4707 * @listens Component#dispose 4708 * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame} 4709 */ 4710 ; 4711 4712 _proto.requestAnimationFrame = function requestAnimationFrame(fn) { 4713 var _this3 = this; 4714 4715 // Fall back to using a timer. 4716 if (!this.supportsRaf_) { 4717 return this.setTimeout(fn, 1000 / 60); 4718 } 4719 4720 this.clearTimersOnDispose_(); // declare as variables so they are properly available in rAF function 4721 // eslint-disable-next-line 4722 4723 var id; 4724 fn = bind(this, fn); 4725 id = window$1.requestAnimationFrame(function () { 4726 if (_this3.rafIds_.has(id)) { 4727 _this3.rafIds_["delete"](id); 4728 } 4729 4730 fn(); 4731 }); 4732 this.rafIds_.add(id); 4733 return id; 4734 } 4735 /** 4736 * Cancels a queued callback passed to {@link Component#requestAnimationFrame} 4737 * (rAF). 4738 * 4739 * If you queue an rAF callback via {@link Component#requestAnimationFrame}, 4740 * use this function instead of `window.cancelAnimationFrame`. If you don't, 4741 * your dispose listener will not get cleaned up until {@link Component#dispose}! 4742 * 4743 * @param {number} id 4744 * The rAF ID to clear. The return value of {@link Component#requestAnimationFrame}. 4745 * 4746 * @return {number} 4747 * Returns the rAF ID that was cleared. 4748 * 4749 * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/cancelAnimationFrame} 4750 */ 4751 ; 4752 4753 _proto.cancelAnimationFrame = function cancelAnimationFrame(id) { 4754 // Fall back to using a timer. 4755 if (!this.supportsRaf_) { 4756 return this.clearTimeout(id); 4757 } 4758 4759 if (this.rafIds_.has(id)) { 4760 this.rafIds_["delete"](id); 4761 window$1.cancelAnimationFrame(id); 4762 } 4763 4764 return id; 4765 } 4766 /** 4767 * A function to setup `requestAnimationFrame`, `setTimeout`, 4768 * and `setInterval`, clearing on dispose. 4769 * 4770 * > Previously each timer added and removed dispose listeners on it's own. 4771 * For better performance it was decided to batch them all, and use `Set`s 4772 * to track outstanding timer ids. 4773 * 4774 * @private 4775 */ 4776 ; 4777 4778 _proto.clearTimersOnDispose_ = function clearTimersOnDispose_() { 4779 var _this4 = this; 4780 4781 if (this.clearingTimersOnDispose_) { 4782 return; 4783 } 4784 4785 this.clearingTimersOnDispose_ = true; 4786 this.one('dispose', function () { 4787 [['rafIds_', 'cancelAnimationFrame'], ['setTimeoutIds_', 'clearTimeout'], ['setIntervalIds_', 'clearInterval']].forEach(function (_ref) { 4788 var idName = _ref[0], 4789 cancelName = _ref[1]; 4790 4791 _this4[idName].forEach(_this4[cancelName], _this4); 4792 }); 4793 _this4.clearingTimersOnDispose_ = false; 4794 }); 4795 } 4796 /** 4797 * Register a `Component` with `videojs` given the name and the component. 4798 * 4799 * > NOTE: {@link Tech}s should not be registered as a `Component`. {@link Tech}s 4800 * should be registered using {@link Tech.registerTech} or 4801 * {@link videojs:videojs.registerTech}. 4802 * 4803 * > NOTE: This function can also be seen on videojs as 4804 * {@link videojs:videojs.registerComponent}. 4805 * 4806 * @param {string} name 4807 * The name of the `Component` to register. 4808 * 4809 * @param {Component} ComponentToRegister 4810 * The `Component` class to register. 4811 * 4812 * @return {Component} 4813 * The `Component` that was registered. 4814 */ 4815 ; 4816 4817 Component.registerComponent = function registerComponent(name, ComponentToRegister) { 4818 if (typeof name !== 'string' || !name) { 4819 throw new Error("Illegal component name, \"" + name + "\"; must be a non-empty string."); 4820 } 4821 4822 var Tech = Component.getComponent('Tech'); // We need to make sure this check is only done if Tech has been registered. 4823 4824 var isTech = Tech && Tech.isTech(ComponentToRegister); 4825 var isComp = Component === ComponentToRegister || Component.prototype.isPrototypeOf(ComponentToRegister.prototype); 4826 4827 if (isTech || !isComp) { 4828 var reason; 4829 4830 if (isTech) { 4831 reason = 'techs must be registered using Tech.registerTech()'; 4832 } else { 4833 reason = 'must be a Component subclass'; 4834 } 4835 4836 throw new Error("Illegal component, \"" + name + "\"; " + reason + "."); 4837 } 4838 4839 name = toTitleCase(name); 4840 4841 if (!Component.components_) { 4842 Component.components_ = {}; 4843 } 4844 4845 var Player = Component.getComponent('Player'); 4846 4847 if (name === 'Player' && Player && Player.players) { 4848 var players = Player.players; 4849 var playerNames = Object.keys(players); // If we have players that were disposed, then their name will still be 4850 // in Players.players. So, we must loop through and verify that the value 4851 // for each item is not null. This allows registration of the Player component 4852 // after all players have been disposed or before any were created. 4853 4854 if (players && playerNames.length > 0 && playerNames.map(function (pname) { 4855 return players[pname]; 4856 }).every(Boolean)) { 4857 throw new Error('Can not register Player component after player has been created.'); 4858 } 4859 } 4860 4861 Component.components_[name] = ComponentToRegister; 4862 Component.components_[toLowerCase(name)] = ComponentToRegister; 4863 return ComponentToRegister; 4864 } 4865 /** 4866 * Get a `Component` based on the name it was registered with. 4867 * 4868 * @param {string} name 4869 * The Name of the component to get. 4870 * 4871 * @return {Component} 4872 * The `Component` that got registered under the given name. 4873 * 4874 * @deprecated In `videojs` 6 this will not return `Component`s that were not 4875 * registered using {@link Component.registerComponent}. Currently we 4876 * check the global `videojs` object for a `Component` name and 4877 * return that if it exists. 4878 */ 4879 ; 4880 4881 Component.getComponent = function getComponent(name) { 4882 if (!name || !Component.components_) { 4883 return; 4884 } 4885 4886 return Component.components_[name]; 4887 }; 4888 4889 return Component; 4890 }(); 4891 /** 4892 * Whether or not this component supports `requestAnimationFrame`. 4893 * 4894 * This is exposed primarily for testing purposes. 4895 * 4896 * @private 4897 * @type {Boolean} 4898 */ 4899 4900 4901 Component.prototype.supportsRaf_ = typeof window$1.requestAnimationFrame === 'function' && typeof window$1.cancelAnimationFrame === 'function'; 4902 Component.registerComponent('Component', Component); 4903 4904 function _assertThisInitialized(self) { 4905 if (self === void 0) { 4906 throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); 4907 } 4908 4909 return self; 4910 } 4911 4912 var assertThisInitialized = _assertThisInitialized; 4913 4914 function _inheritsLoose(subClass, superClass) { 4915 subClass.prototype = Object.create(superClass.prototype); 4916 subClass.prototype.constructor = subClass; 4917 subClass.__proto__ = superClass; 4918 } 4919 4920 var inheritsLoose = _inheritsLoose; 4921 4922 /** 4923 * @file browser.js 4924 * @module browser 4925 */ 4926 var USER_AGENT = window$1.navigator && window$1.navigator.userAgent || ''; 4927 var webkitVersionMap = /AppleWebKit\/([\d.]+)/i.exec(USER_AGENT); 4928 var appleWebkitVersion = webkitVersionMap ? parseFloat(webkitVersionMap.pop()) : null; 4929 /** 4930 * Whether or not this device is an iPod. 4931 * 4932 * @static 4933 * @const 4934 * @type {Boolean} 4935 */ 4936 4937 var IS_IPOD = /iPod/i.test(USER_AGENT); 4938 /** 4939 * The detected iOS version - or `null`. 4940 * 4941 * @static 4942 * @const 4943 * @type {string|null} 4944 */ 4945 4946 var IOS_VERSION = function () { 4947 var match = USER_AGENT.match(/OS (\d+)_/i); 4948 4949 if (match && match[1]) { 4950 return match[1]; 4951 } 4952 4953 return null; 4954 }(); 4955 /** 4956 * Whether or not this is an Android device. 4957 * 4958 * @static 4959 * @const 4960 * @type {Boolean} 4961 */ 4962 4963 var IS_ANDROID = /Android/i.test(USER_AGENT); 4964 /** 4965 * The detected Android version - or `null`. 4966 * 4967 * @static 4968 * @const 4969 * @type {number|string|null} 4970 */ 4971 4972 var ANDROID_VERSION = function () { 4973 // This matches Android Major.Minor.Patch versions 4974 // ANDROID_VERSION is Major.Minor as a Number, if Minor isn't available, then only Major is returned 4975 var match = USER_AGENT.match(/Android (\d+)(?:\.(\d+))?(?:\.(\d+))*/i); 4976 4977 if (!match) { 4978 return null; 4979 } 4980 4981 var major = match[1] && parseFloat(match[1]); 4982 var minor = match[2] && parseFloat(match[2]); 4983 4984 if (major && minor) { 4985 return parseFloat(match[1] + '.' + match[2]); 4986 } else if (major) { 4987 return major; 4988 } 4989 4990 return null; 4991 }(); 4992 /** 4993 * Whether or not this is a native Android browser. 4994 * 4995 * @static 4996 * @const 4997 * @type {Boolean} 4998 */ 4999 5000 var IS_NATIVE_ANDROID = IS_ANDROID && ANDROID_VERSION < 5 && appleWebkitVersion < 537; 5001 /** 5002 * Whether or not this is Mozilla Firefox. 5003 * 5004 * @static 5005 * @const 5006 * @type {Boolean} 5007 */ 5008 5009 var IS_FIREFOX = /Firefox/i.test(USER_AGENT); 5010 /** 5011 * Whether or not this is Microsoft Edge. 5012 * 5013 * @static 5014 * @const 5015 * @type {Boolean} 5016 */ 5017 5018 var IS_EDGE = /Edge/i.test(USER_AGENT); 5019 /** 5020 * Whether or not this is Google Chrome. 5021 * 5022 * This will also be `true` for Chrome on iOS, which will have different support 5023 * as it is actually Safari under the hood. 5024 * 5025 * @static 5026 * @const 5027 * @type {Boolean} 5028 */ 5029 5030 var IS_CHROME = !IS_EDGE && (/Chrome/i.test(USER_AGENT) || /CriOS/i.test(USER_AGENT)); 5031 /** 5032 * The detected Google Chrome version - or `null`. 5033 * 5034 * @static 5035 * @const 5036 * @type {number|null} 5037 */ 5038 5039 var CHROME_VERSION = function () { 5040 var match = USER_AGENT.match(/(Chrome|CriOS)\/(\d+)/); 5041 5042 if (match && match[2]) { 5043 return parseFloat(match[2]); 5044 } 5045 5046 return null; 5047 }(); 5048 /** 5049 * The detected Internet Explorer version - or `null`. 5050 * 5051 * @static 5052 * @const 5053 * @type {number|null} 5054 */ 5055 5056 var IE_VERSION = function () { 5057 var result = /MSIE\s(\d+)\.\d/.exec(USER_AGENT); 5058 var version = result && parseFloat(result[1]); 5059 5060 if (!version && /Trident\/7.0/i.test(USER_AGENT) && /rv:11.0/.test(USER_AGENT)) { 5061 // IE 11 has a different user agent string than other IE versions 5062 version = 11.0; 5063 } 5064 5065 return version; 5066 }(); 5067 /** 5068 * Whether or not this is desktop Safari. 5069 * 5070 * @static 5071 * @const 5072 * @type {Boolean} 5073 */ 5074 5075 var IS_SAFARI = /Safari/i.test(USER_AGENT) && !IS_CHROME && !IS_ANDROID && !IS_EDGE; 5076 /** 5077 * Whether or not this is a Windows machine. 5078 * 5079 * @static 5080 * @const 5081 * @type {Boolean} 5082 */ 5083 5084 var IS_WINDOWS = /Windows/i.test(USER_AGENT); 5085 /** 5086 * Whether or not this device is touch-enabled. 5087 * 5088 * @static 5089 * @const 5090 * @type {Boolean} 5091 */ 5092 5093 var TOUCH_ENABLED = isReal() && ('ontouchstart' in window$1 || window$1.navigator.maxTouchPoints || window$1.DocumentTouch && window$1.document instanceof window$1.DocumentTouch); 5094 /** 5095 * Whether or not this device is an iPad. 5096 * 5097 * @static 5098 * @const 5099 * @type {Boolean} 5100 */ 5101 5102 var IS_IPAD = /iPad/i.test(USER_AGENT) || IS_SAFARI && TOUCH_ENABLED && !/iPhone/i.test(USER_AGENT); 5103 /** 5104 * Whether or not this device is an iPhone. 5105 * 5106 * @static 5107 * @const 5108 * @type {Boolean} 5109 */ 5110 // The Facebook app's UIWebView identifies as both an iPhone and iPad, so 5111 // to identify iPhones, we need to exclude iPads. 5112 // http://artsy.github.io/blog/2012/10/18/the-perils-of-ios-user-agent-sniffing/ 5113 5114 var IS_IPHONE = /iPhone/i.test(USER_AGENT) && !IS_IPAD; 5115 /** 5116 * Whether or not this is an iOS device. 5117 * 5118 * @static 5119 * @const 5120 * @type {Boolean} 5121 */ 5122 5123 var IS_IOS = IS_IPHONE || IS_IPAD || IS_IPOD; 5124 /** 5125 * Whether or not this is any flavor of Safari - including iOS. 5126 * 5127 * @static 5128 * @const 5129 * @type {Boolean} 5130 */ 5131 5132 var IS_ANY_SAFARI = (IS_SAFARI || IS_IOS) && !IS_CHROME; 5133 5134 var browser = /*#__PURE__*/Object.freeze({ 5135 IS_IPOD: IS_IPOD, 5136 IOS_VERSION: IOS_VERSION, 5137 IS_ANDROID: IS_ANDROID, 5138 ANDROID_VERSION: ANDROID_VERSION, 5139 IS_NATIVE_ANDROID: IS_NATIVE_ANDROID, 5140 IS_FIREFOX: IS_FIREFOX, 5141 IS_EDGE: IS_EDGE, 5142 IS_CHROME: IS_CHROME, 5143 CHROME_VERSION: CHROME_VERSION, 5144 IE_VERSION: IE_VERSION, 5145 IS_SAFARI: IS_SAFARI, 5146 IS_WINDOWS: IS_WINDOWS, 5147 TOUCH_ENABLED: TOUCH_ENABLED, 5148 IS_IPAD: IS_IPAD, 5149 IS_IPHONE: IS_IPHONE, 5150 IS_IOS: IS_IOS, 5151 IS_ANY_SAFARI: IS_ANY_SAFARI 5152 }); 5153 5154 /** 5155 * @file time-ranges.js 5156 * @module time-ranges 5157 */ 5158 5159 /** 5160 * Returns the time for the specified index at the start or end 5161 * of a TimeRange object. 5162 * 5163 * @typedef {Function} TimeRangeIndex 5164 * 5165 * @param {number} [index=0] 5166 * The range number to return the time for. 5167 * 5168 * @return {number} 5169 * The time offset at the specified index. 5170 * 5171 * @deprecated The index argument must be provided. 5172 * In the future, leaving it out will throw an error. 5173 */ 5174 5175 /** 5176 * An object that contains ranges of time. 5177 * 5178 * @typedef {Object} TimeRange 5179 * 5180 * @property {number} length 5181 * The number of time ranges represented by this object. 5182 * 5183 * @property {module:time-ranges~TimeRangeIndex} start 5184 * Returns the time offset at which a specified time range begins. 5185 * 5186 * @property {module:time-ranges~TimeRangeIndex} end 5187 * Returns the time offset at which a specified time range ends. 5188 * 5189 * @see https://developer.mozilla.org/en-US/docs/Web/API/TimeRanges 5190 */ 5191 5192 /** 5193 * Check if any of the time ranges are over the maximum index. 5194 * 5195 * @private 5196 * @param {string} fnName 5197 * The function name to use for logging 5198 * 5199 * @param {number} index 5200 * The index to check 5201 * 5202 * @param {number} maxIndex 5203 * The maximum possible index 5204 * 5205 * @throws {Error} if the timeRanges provided are over the maxIndex 5206 */ 5207 function rangeCheck(fnName, index, maxIndex) { 5208 if (typeof index !== 'number' || index < 0 || index > maxIndex) { 5209 throw new Error("Failed to execute '" + fnName + "' on 'TimeRanges': The index provided (" + index + ") is non-numeric or out of bounds (0-" + maxIndex + ")."); 5210 } 5211 } 5212 /** 5213 * Get the time for the specified index at the start or end 5214 * of a TimeRange object. 5215 * 5216 * @private 5217 * @param {string} fnName 5218 * The function name to use for logging 5219 * 5220 * @param {string} valueIndex 5221 * The property that should be used to get the time. should be 5222 * 'start' or 'end' 5223 * 5224 * @param {Array} ranges 5225 * An array of time ranges 5226 * 5227 * @param {Array} [rangeIndex=0] 5228 * The index to start the search at 5229 * 5230 * @return {number} 5231 * The time that offset at the specified index. 5232 * 5233 * @deprecated rangeIndex must be set to a value, in the future this will throw an error. 5234 * @throws {Error} if rangeIndex is more than the length of ranges 5235 */ 5236 5237 5238 function getRange(fnName, valueIndex, ranges, rangeIndex) { 5239 rangeCheck(fnName, rangeIndex, ranges.length - 1); 5240 return ranges[rangeIndex][valueIndex]; 5241 } 5242 /** 5243 * Create a time range object given ranges of time. 5244 * 5245 * @private 5246 * @param {Array} [ranges] 5247 * An array of time ranges. 5248 */ 5249 5250 5251 function createTimeRangesObj(ranges) { 5252 if (ranges === undefined || ranges.length === 0) { 5253 return { 5254 length: 0, 5255 start: function start() { 5256 throw new Error('This TimeRanges object is empty'); 5257 }, 5258 end: function end() { 5259 throw new Error('This TimeRanges object is empty'); 5260 } 5261 }; 5262 } 5263 5264 return { 5265 length: ranges.length, 5266 start: getRange.bind(null, 'start', 0, ranges), 5267 end: getRange.bind(null, 'end', 1, ranges) 5268 }; 5269 } 5270 /** 5271 * Create a `TimeRange` object which mimics an 5272 * {@link https://developer.mozilla.org/en-US/docs/Web/API/TimeRanges|HTML5 TimeRanges instance}. 5273 * 5274 * @param {number|Array[]} start 5275 * The start of a single range (a number) or an array of ranges (an 5276 * array of arrays of two numbers each). 5277 * 5278 * @param {number} end 5279 * The end of a single range. Cannot be used with the array form of 5280 * the `start` argument. 5281 */ 5282 5283 5284 function createTimeRanges(start, end) { 5285 if (Array.isArray(start)) { 5286 return createTimeRangesObj(start); 5287 } else if (start === undefined || end === undefined) { 5288 return createTimeRangesObj(); 5289 } 5290 5291 return createTimeRangesObj([[start, end]]); 5292 } 5293 5294 /** 5295 * @file buffer.js 5296 * @module buffer 5297 */ 5298 /** 5299 * Compute the percentage of the media that has been buffered. 5300 * 5301 * @param {TimeRange} buffered 5302 * The current `TimeRange` object representing buffered time ranges 5303 * 5304 * @param {number} duration 5305 * Total duration of the media 5306 * 5307 * @return {number} 5308 * Percent buffered of the total duration in decimal form. 5309 */ 5310 5311 function bufferedPercent(buffered, duration) { 5312 var bufferedDuration = 0; 5313 var start; 5314 var end; 5315 5316 if (!duration) { 5317 return 0; 5318 } 5319 5320 if (!buffered || !buffered.length) { 5321 buffered = createTimeRanges(0, 0); 5322 } 5323 5324 for (var i = 0; i < buffered.length; i++) { 5325 start = buffered.start(i); 5326 end = buffered.end(i); // buffered end can be bigger than duration by a very small fraction 5327 5328 if (end > duration) { 5329 end = duration; 5330 } 5331 5332 bufferedDuration += end - start; 5333 } 5334 5335 return bufferedDuration / duration; 5336 } 5337 5338 /** 5339 * @file fullscreen-api.js 5340 * @module fullscreen-api 5341 * @private 5342 */ 5343 /** 5344 * Store the browser-specific methods for the fullscreen API. 5345 * 5346 * @type {Object} 5347 * @see [Specification]{@link https://fullscreen.spec.whatwg.org} 5348 * @see [Map Approach From Screenfull.js]{@link https://github.com/sindresorhus/screenfull.js} 5349 */ 5350 5351 var FullscreenApi = { 5352 prefixed: true 5353 }; // browser API methods 5354 5355 var apiMap = [['requestFullscreen', 'exitFullscreen', 'fullscreenElement', 'fullscreenEnabled', 'fullscreenchange', 'fullscreenerror', 'fullscreen'], // WebKit 5356 ['webkitRequestFullscreen', 'webkitExitFullscreen', 'webkitFullscreenElement', 'webkitFullscreenEnabled', 'webkitfullscreenchange', 'webkitfullscreenerror', '-webkit-full-screen'], // Mozilla 5357 ['mozRequestFullScreen', 'mozCancelFullScreen', 'mozFullScreenElement', 'mozFullScreenEnabled', 'mozfullscreenchange', 'mozfullscreenerror', '-moz-full-screen'], // Microsoft 5358 ['msRequestFullscreen', 'msExitFullscreen', 'msFullscreenElement', 'msFullscreenEnabled', 'MSFullscreenChange', 'MSFullscreenError', '-ms-fullscreen']]; 5359 var specApi = apiMap[0]; 5360 var browserApi; // determine the supported set of functions 5361 5362 for (var i = 0; i < apiMap.length; i++) { 5363 // check for exitFullscreen function 5364 if (apiMap[i][1] in document) { 5365 browserApi = apiMap[i]; 5366 break; 5367 } 5368 } // map the browser API names to the spec API names 5369 5370 5371 if (browserApi) { 5372 for (var _i = 0; _i < browserApi.length; _i++) { 5373 FullscreenApi[specApi[_i]] = browserApi[_i]; 5374 } 5375 5376 FullscreenApi.prefixed = browserApi[0] !== specApi[0]; 5377 } 5378 5379 /** 5380 * @file media-error.js 5381 */ 5382 /** 5383 * A Custom `MediaError` class which mimics the standard HTML5 `MediaError` class. 5384 * 5385 * @param {number|string|Object|MediaError} value 5386 * This can be of multiple types: 5387 * - number: should be a standard error code 5388 * - string: an error message (the code will be 0) 5389 * - Object: arbitrary properties 5390 * - `MediaError` (native): used to populate a video.js `MediaError` object 5391 * - `MediaError` (video.js): will return itself if it's already a 5392 * video.js `MediaError` object. 5393 * 5394 * @see [MediaError Spec]{@link https://dev.w3.org/html5/spec-author-view/video.html#mediaerror} 5395 * @see [Encrypted MediaError Spec]{@link https://www.w3.org/TR/2013/WD-encrypted-media-20130510/#error-codes} 5396 * 5397 * @class MediaError 5398 */ 5399 5400 function MediaError(value) { 5401 // Allow redundant calls to this constructor to avoid having `instanceof` 5402 // checks peppered around the code. 5403 if (value instanceof MediaError) { 5404 return value; 5405 } 5406 5407 if (typeof value === 'number') { 5408 this.code = value; 5409 } else if (typeof value === 'string') { 5410 // default code is zero, so this is a custom error 5411 this.message = value; 5412 } else if (isObject(value)) { 5413 // We assign the `code` property manually because native `MediaError` objects 5414 // do not expose it as an own/enumerable property of the object. 5415 if (typeof value.code === 'number') { 5416 this.code = value.code; 5417 } 5418 5419 assign(this, value); 5420 } 5421 5422 if (!this.message) { 5423 this.message = MediaError.defaultMessages[this.code] || ''; 5424 } 5425 } 5426 /** 5427 * The error code that refers two one of the defined `MediaError` types 5428 * 5429 * @type {Number} 5430 */ 5431 5432 5433 MediaError.prototype.code = 0; 5434 /** 5435 * An optional message that to show with the error. Message is not part of the HTML5 5436 * video spec but allows for more informative custom errors. 5437 * 5438 * @type {String} 5439 */ 5440 5441 MediaError.prototype.message = ''; 5442 /** 5443 * An optional status code that can be set by plugins to allow even more detail about 5444 * the error. For example a plugin might provide a specific HTTP status code and an 5445 * error message for that code. Then when the plugin gets that error this class will 5446 * know how to display an error message for it. This allows a custom message to show 5447 * up on the `Player` error overlay. 5448 * 5449 * @type {Array} 5450 */ 5451 5452 MediaError.prototype.status = null; 5453 /** 5454 * Errors indexed by the W3C standard. The order **CANNOT CHANGE**! See the 5455 * specification listed under {@link MediaError} for more information. 5456 * 5457 * @enum {array} 5458 * @readonly 5459 * @property {string} 0 - MEDIA_ERR_CUSTOM 5460 * @property {string} 1 - MEDIA_ERR_ABORTED 5461 * @property {string} 2 - MEDIA_ERR_NETWORK 5462 * @property {string} 3 - MEDIA_ERR_DECODE 5463 * @property {string} 4 - MEDIA_ERR_SRC_NOT_SUPPORTED 5464 * @property {string} 5 - MEDIA_ERR_ENCRYPTED 5465 */ 5466 5467 MediaError.errorTypes = ['MEDIA_ERR_CUSTOM', 'MEDIA_ERR_ABORTED', 'MEDIA_ERR_NETWORK', 'MEDIA_ERR_DECODE', 'MEDIA_ERR_SRC_NOT_SUPPORTED', 'MEDIA_ERR_ENCRYPTED']; 5468 /** 5469 * The default `MediaError` messages based on the {@link MediaError.errorTypes}. 5470 * 5471 * @type {Array} 5472 * @constant 5473 */ 5474 5475 MediaError.defaultMessages = { 5476 1: 'You aborted the media playback', 5477 2: 'A network error caused the media download to fail part-way.', 5478 3: 'The media playback was aborted due to a corruption problem or because the media used features your browser did not support.', 5479 4: 'The media could not be loaded, either because the server or network failed or because the format is not supported.', 5480 5: 'The media is encrypted and we do not have the keys to decrypt it.' 5481 }; // Add types as properties on MediaError 5482 // e.g. MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED = 4; 5483 5484 for (var errNum = 0; errNum < MediaError.errorTypes.length; errNum++) { 5485 MediaError[MediaError.errorTypes[errNum]] = errNum; // values should be accessible on both the class and instance 5486 5487 MediaError.prototype[MediaError.errorTypes[errNum]] = errNum; 5488 } // jsdocs for instance/static members added above 5489 5490 var tuple = SafeParseTuple; 5491 5492 function SafeParseTuple(obj, reviver) { 5493 var json; 5494 var error = null; 5495 5496 try { 5497 json = JSON.parse(obj, reviver); 5498 } catch (err) { 5499 error = err; 5500 } 5501 5502 return [error, json]; 5503 } 5504 5505 /** 5506 * Returns whether an object is `Promise`-like (i.e. has a `then` method). 5507 * 5508 * @param {Object} value 5509 * An object that may or may not be `Promise`-like. 5510 * 5511 * @return {boolean} 5512 * Whether or not the object is `Promise`-like. 5513 */ 5514 function isPromise(value) { 5515 return value !== undefined && value !== null && typeof value.then === 'function'; 5516 } 5517 /** 5518 * Silence a Promise-like object. 5519 * 5520 * This is useful for avoiding non-harmful, but potentially confusing "uncaught 5521 * play promise" rejection error messages. 5522 * 5523 * @param {Object} value 5524 * An object that may or may not be `Promise`-like. 5525 */ 5526 5527 function silencePromise(value) { 5528 if (isPromise(value)) { 5529 value.then(null, function (e) {}); 5530 } 5531 } 5532 5533 /** 5534 * @file text-track-list-converter.js Utilities for capturing text track state and 5535 * re-creating tracks based on a capture. 5536 * 5537 * @module text-track-list-converter 5538 */ 5539 5540 /** 5541 * Examine a single {@link TextTrack} and return a JSON-compatible javascript object that 5542 * represents the {@link TextTrack}'s state. 5543 * 5544 * @param {TextTrack} track 5545 * The text track to query. 5546 * 5547 * @return {Object} 5548 * A serializable javascript representation of the TextTrack. 5549 * @private 5550 */ 5551 var trackToJson_ = function trackToJson_(track) { 5552 var ret = ['kind', 'label', 'language', 'id', 'inBandMetadataTrackDispatchType', 'mode', 'src'].reduce(function (acc, prop, i) { 5553 if (track[prop]) { 5554 acc[prop] = track[prop]; 5555 } 5556 5557 return acc; 5558 }, { 5559 cues: track.cues && Array.prototype.map.call(track.cues, function (cue) { 5560 return { 5561 startTime: cue.startTime, 5562 endTime: cue.endTime, 5563 text: cue.text, 5564 id: cue.id 5565 }; 5566 }) 5567 }); 5568 return ret; 5569 }; 5570 /** 5571 * Examine a {@link Tech} and return a JSON-compatible javascript array that represents the 5572 * state of all {@link TextTrack}s currently configured. The return array is compatible with 5573 * {@link text-track-list-converter:jsonToTextTracks}. 5574 * 5575 * @param {Tech} tech 5576 * The tech object to query 5577 * 5578 * @return {Array} 5579 * A serializable javascript representation of the {@link Tech}s 5580 * {@link TextTrackList}. 5581 */ 5582 5583 5584 var textTracksToJson = function textTracksToJson(tech) { 5585 var trackEls = tech.$$('track'); 5586 var trackObjs = Array.prototype.map.call(trackEls, function (t) { 5587 return t.track; 5588 }); 5589 var tracks = Array.prototype.map.call(trackEls, function (trackEl) { 5590 var json = trackToJson_(trackEl.track); 5591 5592 if (trackEl.src) { 5593 json.src = trackEl.src; 5594 } 5595 5596 return json; 5597 }); 5598 return tracks.concat(Array.prototype.filter.call(tech.textTracks(), function (track) { 5599 return trackObjs.indexOf(track) === -1; 5600 }).map(trackToJson_)); 5601 }; 5602 /** 5603 * Create a set of remote {@link TextTrack}s on a {@link Tech} based on an array of javascript 5604 * object {@link TextTrack} representations. 5605 * 5606 * @param {Array} json 5607 * An array of `TextTrack` representation objects, like those that would be 5608 * produced by `textTracksToJson`. 5609 * 5610 * @param {Tech} tech 5611 * The `Tech` to create the `TextTrack`s on. 5612 */ 5613 5614 5615 var jsonToTextTracks = function jsonToTextTracks(json, tech) { 5616 json.forEach(function (track) { 5617 var addedTrack = tech.addRemoteTextTrack(track).track; 5618 5619 if (!track.src && track.cues) { 5620 track.cues.forEach(function (cue) { 5621 return addedTrack.addCue(cue); 5622 }); 5623 } 5624 }); 5625 return tech.textTracks(); 5626 }; 5627 5628 var textTrackConverter = { 5629 textTracksToJson: textTracksToJson, 5630 jsonToTextTracks: jsonToTextTracks, 5631 trackToJson_: trackToJson_ 5632 }; 5633 5634 var keycode = createCommonjsModule(function (module, exports) { 5635 // Source: http://jsfiddle.net/vWx8V/ 5636 // http://stackoverflow.com/questions/5603195/full-list-of-javascript-keycodes 5637 5638 /** 5639 * Conenience method returns corresponding value for given keyName or keyCode. 5640 * 5641 * @param {Mixed} keyCode {Number} or keyName {String} 5642 * @return {Mixed} 5643 * @api public 5644 */ 5645 function keyCode(searchInput) { 5646 // Keyboard Events 5647 if (searchInput && 'object' === typeof searchInput) { 5648 var hasKeyCode = searchInput.which || searchInput.keyCode || searchInput.charCode; 5649 if (hasKeyCode) searchInput = hasKeyCode; 5650 } // Numbers 5651 5652 5653 if ('number' === typeof searchInput) return names[searchInput]; // Everything else (cast to string) 5654 5655 var search = String(searchInput); // check codes 5656 5657 var foundNamedKey = codes[search.toLowerCase()]; 5658 if (foundNamedKey) return foundNamedKey; // check aliases 5659 5660 var foundNamedKey = aliases[search.toLowerCase()]; 5661 if (foundNamedKey) return foundNamedKey; // weird character? 5662 5663 if (search.length === 1) return search.charCodeAt(0); 5664 return undefined; 5665 } 5666 /** 5667 * Compares a keyboard event with a given keyCode or keyName. 5668 * 5669 * @param {Event} event Keyboard event that should be tested 5670 * @param {Mixed} keyCode {Number} or keyName {String} 5671 * @return {Boolean} 5672 * @api public 5673 */ 5674 5675 5676 keyCode.isEventKey = function isEventKey(event, nameOrCode) { 5677 if (event && 'object' === typeof event) { 5678 var keyCode = event.which || event.keyCode || event.charCode; 5679 5680 if (keyCode === null || keyCode === undefined) { 5681 return false; 5682 } 5683 5684 if (typeof nameOrCode === 'string') { 5685 // check codes 5686 var foundNamedKey = codes[nameOrCode.toLowerCase()]; 5687 5688 if (foundNamedKey) { 5689 return foundNamedKey === keyCode; 5690 } // check aliases 5691 5692 5693 var foundNamedKey = aliases[nameOrCode.toLowerCase()]; 5694 5695 if (foundNamedKey) { 5696 return foundNamedKey === keyCode; 5697 } 5698 } else if (typeof nameOrCode === 'number') { 5699 return nameOrCode === keyCode; 5700 } 5701 5702 return false; 5703 } 5704 }; 5705 5706 exports = module.exports = keyCode; 5707 /** 5708 * Get by name 5709 * 5710 * exports.code['enter'] // => 13 5711 */ 5712 5713 var codes = exports.code = exports.codes = { 5714 'backspace': 8, 5715 'tab': 9, 5716 'enter': 13, 5717 'shift': 16, 5718 'ctrl': 17, 5719 'alt': 18, 5720 'pause/break': 19, 5721 'caps lock': 20, 5722 'esc': 27, 5723 'space': 32, 5724 'page up': 33, 5725 'page down': 34, 5726 'end': 35, 5727 'home': 36, 5728 'left': 37, 5729 'up': 38, 5730 'right': 39, 5731 'down': 40, 5732 'insert': 45, 5733 'delete': 46, 5734 'command': 91, 5735 'left command': 91, 5736 'right command': 93, 5737 'numpad *': 106, 5738 'numpad +': 107, 5739 'numpad -': 109, 5740 'numpad .': 110, 5741 'numpad /': 111, 5742 'num lock': 144, 5743 'scroll lock': 145, 5744 'my computer': 182, 5745 'my calculator': 183, 5746 ';': 186, 5747 '=': 187, 5748 ',': 188, 5749 '-': 189, 5750 '.': 190, 5751 '/': 191, 5752 '`': 192, 5753 '[': 219, 5754 '\\': 220, 5755 ']': 221, 5756 "'": 222 // Helper aliases 5757 5758 }; 5759 var aliases = exports.aliases = { 5760 'windows': 91, 5761 'â§': 16, 5762 'â¥': 18, 5763 'â': 17, 5764 'â': 91, 5765 'ctl': 17, 5766 'control': 17, 5767 'option': 18, 5768 'pause': 19, 5769 'break': 19, 5770 'caps': 20, 5771 'return': 13, 5772 'escape': 27, 5773 'spc': 32, 5774 'spacebar': 32, 5775 'pgup': 33, 5776 'pgdn': 34, 5777 'ins': 45, 5778 'del': 46, 5779 'cmd': 91 5780 /*! 5781 * Programatically add the following 5782 */ 5783 // lower case chars 5784 5785 }; 5786 5787 for (i = 97; i < 123; i++) {
5788 codes[String.fromCharCode(i)] = i - 32; 5789 } // numbers 5790 5791 5792 for (var i = 48; i < 58; i++) { 5793 codes[i - 48] = i; 5794 } // function keys 5795 5796 5797 for (i = 1; i < 13; i++) { 5798 codes['f' + i] = i + 111; 5799 } // numpad keys 5800 5801 5802 for (i = 0; i < 10; i++) { 5803 codes['numpad ' + i] = i + 96; 5804 } 5805 /** 5806 * Get by code 5807 * 5808 * exports.name[13] // => 'Enter' 5809 */ 5810 5811 5812 var names = exports.names = exports.title = {}; // title for backward compat 5813 // Create reverse mapping 5814 5815 for (i in codes) { 5816 names[codes[i]] = i; 5817 } // Add aliases 5818 5819 5820 for (var alias in aliases) { 5821 codes[alias] = aliases[alias]; 5822 } 5823 }); 5824 var keycode_1 = keycode.code; 5825 var keycode_2 = keycode.codes; 5826 var keycode_3 = keycode.aliases; 5827 var keycode_4 = keycode.names; 5828 var keycode_5 = keycode.title; 5829 5830 var MODAL_CLASS_NAME = 'vjs-modal-dialog'; 5831 /** 5832 * The `ModalDialog` displays over the video and its controls, which blocks 5833 * interaction with the player until it is closed. 5834 * 5835 * Modal dialogs include a "Close" button and will close when that button 5836 * is activated - or when ESC is pressed anywhere. 5837 * 5838 * @extends Component 5839 */ 5840 5841 var ModalDialog = 5842 /*#__PURE__*/ 5843 function (_Component) { 5844 inheritsLoose(ModalDialog, _Component); 5845 5846 /** 5847 * Create an instance of this class. 5848 * 5849 * @param {Player} player 5850 * The `Player` that this class should be attached to. 5851 * 5852 * @param {Object} [options] 5853 * The key/value store of player options. 5854 * 5855 * @param {Mixed} [options.content=undefined] 5856 * Provide customized content for this modal. 5857 * 5858 * @param {string} [options.description] 5859 * A text description for the modal, primarily for accessibility. 5860 * 5861 * @param {boolean} [options.fillAlways=false] 5862 * Normally, modals are automatically filled only the first time 5863 * they open. This tells the modal to refresh its content 5864 * every time it opens. 5865 * 5866 * @param {string} [options.label] 5867 * A text label for the modal, primarily for accessibility. 5868 * 5869 * @param {boolean} [options.pauseOnOpen=true] 5870 * If `true`, playback will will be paused if playing when 5871 * the modal opens, and resumed when it closes. 5872 * 5873 * @param {boolean} [options.temporary=true] 5874 * If `true`, the modal can only be opened once; it will be 5875 * disposed as soon as it's closed. 5876 * 5877 * @param {boolean} [options.uncloseable=false] 5878 * If `true`, the user will not be able to close the modal 5879 * through the UI in the normal ways. Programmatic closing is 5880 * still possible. 5881 */ 5882 function ModalDialog(player, options) { 5883 var _this; 5884 5885 _this = _Component.call(this, player, options) || this; 5886 _this.opened_ = _this.hasBeenOpened_ = _this.hasBeenFilled_ = false; 5887 5888 _this.closeable(!_this.options_.uncloseable); 5889 5890 _this.content(_this.options_.content); // Make sure the contentEl is defined AFTER any children are initialized 5891 // because we only want the contents of the modal in the contentEl 5892 // (not the UI elements like the close button). 5893 5894 5895 _this.contentEl_ = createEl('div', { 5896 className: MODAL_CLASS_NAME + "-content" 5897 }, { 5898 role: 'document' 5899 }); 5900 _this.descEl_ = createEl('p', { 5901 className: MODAL_CLASS_NAME + "-description vjs-control-text", 5902 id: _this.el().getAttribute('aria-describedby') 5903 }); 5904 textContent(_this.descEl_, _this.description()); 5905 5906 _this.el_.appendChild(_this.descEl_); 5907 5908 _this.el_.appendChild(_this.contentEl_); 5909 5910 return _this; 5911 } 5912 /** 5913 * Create the `ModalDialog`'s DOM element 5914 * 5915 * @return {Element} 5916 * The DOM element that gets created. 5917 */ 5918 5919 5920 var _proto = ModalDialog.prototype; 5921 5922 _proto.createEl = function createEl() { 5923 return _Component.prototype.createEl.call(this, 'div', { 5924 className: this.buildCSSClass(), 5925 tabIndex: -1 5926 }, { 5927 'aria-describedby': this.id() + "_description", 5928 'aria-hidden': 'true', 5929 'aria-label': this.label(), 5930 'role': 'dialog' 5931 }); 5932 }; 5933 5934 _proto.dispose = function dispose() { 5935 this.contentEl_ = null; 5936 this.descEl_ = null; 5937 this.previouslyActiveEl_ = null; 5938 5939 _Component.prototype.dispose.call(this); 5940 } 5941 /** 5942 * Builds the default DOM `className`. 5943 * 5944 * @return {string} 5945 * The DOM `className` for this object. 5946 */ 5947 ; 5948 5949 _proto.buildCSSClass = function buildCSSClass() { 5950 return MODAL_CLASS_NAME + " vjs-hidden " + _Component.prototype.buildCSSClass.call(this); 5951 } 5952 /**
vendor: 37,023 bytes, lines 5953-7331
5953 * Returns the label string for this modal. Primarily used for accessibility. 5954 * 5955 * @return {string} 5956 * the localized or raw label of this modal. 5957 */ 5958 ; 5959 5960 _proto.label = function label() { 5961 return this.localize(this.options_.label || 'Modal Window'); 5962 } 5963 /** 5964 * Returns the description string for this modal. Primarily used for 5965 * accessibility. 5966 * 5967 * @return {string} 5968 * The localized or raw description of this modal. 5969 */ 5970 ; 5971 5972 _proto.description = function description() { 5973 var desc = this.options_.description || this.localize('This is a modal window.'); // Append a universal closeability message if the modal is closeable. 5974 5975 if (this.closeable()) { 5976 desc += ' ' + this.localize('This modal can be closed by pressing the Escape key or activating the close button.'); 5977 } 5978 5979 return desc; 5980 } 5981 /** 5982 * Opens the modal. 5983 * 5984 * @fires ModalDialog#beforemodalopen 5985 * @fires ModalDialog#modalopen 5986 */ 5987 ; 5988 5989 _proto.open = function open() { 5990 if (!this.opened_) { 5991 var player = this.player(); 5992 /** 5993 * Fired just before a `ModalDialog` is opened. 5994 * 5995 * @event ModalDialog#beforemodalopen 5996 * @type {EventTarget~Event} 5997 */ 5998 5999 this.trigger('beforemodalopen'); 6000 this.opened_ = true; // Fill content if the modal has never opened before and 6001 // never been filled. 6002 6003 if (this.options_.fillAlways || !this.hasBeenOpened_ && !this.hasBeenFilled_) { 6004 this.fill(); 6005 } // If the player was playing, pause it and take note of its previously 6006 // playing state. 6007 6008 6009 this.wasPlaying_ = !player.paused(); 6010 6011 if (this.options_.pauseOnOpen && this.wasPlaying_) { 6012 player.pause(); 6013 } 6014 6015 this.on('keydown', this.handleKeyDown); // Hide controls and note if they were enabled. 6016 6017 this.hadControls_ = player.controls(); 6018 player.controls(false); 6019 this.show(); 6020 this.conditionalFocus_(); 6021 this.el().setAttribute('aria-hidden', 'false'); 6022 /** 6023 * Fired just after a `ModalDialog` is opened. 6024 * 6025 * @event ModalDialog#modalopen 6026 * @type {EventTarget~Event} 6027 */ 6028 6029 this.trigger('modalopen'); 6030 this.hasBeenOpened_ = true; 6031 } 6032 } 6033 /** 6034 * If the `ModalDialog` is currently open or closed. 6035 * 6036 * @param {boolean} [value] 6037 * If given, it will open (`true`) or close (`false`) the modal. 6038 * 6039 * @return {boolean} 6040 * the current open state of the modaldialog 6041 */ 6042 ; 6043 6044 _proto.opened = function opened(value) { 6045 if (typeof value === 'boolean') { 6046 this[value ? 'open' : 'close'](); 6047 } 6048 6049 return this.opened_; 6050 } 6051 /** 6052 * Closes the modal, does nothing if the `ModalDialog` is 6053 * not open. 6054 * 6055 * @fires ModalDialog#beforemodalclose 6056 * @fires ModalDialog#modalclose 6057 */ 6058 ; 6059 6060 _proto.close = function close() { 6061 if (!this.opened_) { 6062 return; 6063 } 6064 6065 var player = this.player(); 6066 /** 6067 * Fired just before a `ModalDialog` is closed. 6068 * 6069 * @event ModalDialog#beforemodalclose 6070 * @type {EventTarget~Event} 6071 */ 6072 6073 this.trigger('beforemodalclose'); 6074 this.opened_ = false; 6075 6076 if (this.wasPlaying_ && this.options_.pauseOnOpen) { 6077 player.play(); 6078 } 6079 6080 this.off('keydown', this.handleKeyDown); 6081 6082 if (this.hadControls_) { 6083 player.controls(true); 6084 } 6085 6086 this.hide(); 6087 this.el().setAttribute('aria-hidden', 'true'); 6088 /** 6089 * Fired just after a `ModalDialog` is closed. 6090 * 6091 * @event ModalDialog#modalclose 6092 * @type {EventTarget~Event} 6093 */ 6094 6095 this.trigger('modalclose'); 6096 this.conditionalBlur_(); 6097 6098 if (this.options_.temporary) { 6099 this.dispose(); 6100 } 6101 } 6102 /** 6103 * Check to see if the `ModalDialog` is closeable via the UI. 6104 * 6105 * @param {boolean} [value] 6106 * If given as a boolean, it will set the `closeable` option. 6107 * 6108 * @return {boolean} 6109 * Returns the final value of the closable option. 6110 */ 6111 ; 6112 6113 _proto.closeable = function closeable(value) { 6114 if (typeof value === 'boolean') { 6115 var closeable = this.closeable_ = !!value; 6116 var close = this.getChild('closeButton'); // If this is being made closeable and has no close button, add one. 6117 6118 if (closeable && !close) { 6119 // The close button should be a child of the modal - not its 6120 // content element, so temporarily change the content element. 6121 var temp = this.contentEl_; 6122 this.contentEl_ = this.el_; 6123 close = this.addChild('closeButton', { 6124 controlText: 'Close Modal Dialog' 6125 }); 6126 this.contentEl_ = temp; 6127 this.on(close, 'close', this.close); 6128 } // If this is being made uncloseable and has a close button, remove it. 6129 6130 6131 if (!closeable && close) { 6132 this.off(close, 'close', this.close); 6133 this.removeChild(close); 6134 close.dispose(); 6135 } 6136 } 6137 6138 return this.closeable_; 6139 } 6140 /** 6141 * Fill the modal's content element with the modal's "content" option. 6142 * The content element will be emptied before this change takes place. 6143 */ 6144 ; 6145 6146 _proto.fill = function fill() { 6147 this.fillWith(this.content()); 6148 } 6149 /** 6150 * Fill the modal's content element with arbitrary content. 6151 * The content element will be emptied before this change takes place. 6152 * 6153 * @fires ModalDialog#beforemodalfill 6154 * @fires ModalDialog#modalfill 6155 * 6156 * @param {Mixed} [content] 6157 * The same rules apply to this as apply to the `content` option. 6158 */ 6159 ; 6160 6161 _proto.fillWith = function fillWith(content) { 6162 var contentEl = this.contentEl(); 6163 var parentEl = contentEl.parentNode; 6164 var nextSiblingEl = contentEl.nextSibling; 6165 /** 6166 * Fired just before a `ModalDialog` is filled with content. 6167 * 6168 * @event ModalDialog#beforemodalfill 6169 * @type {EventTarget~Event} 6170 */ 6171 6172 this.trigger('beforemodalfill'); 6173 this.hasBeenFilled_ = true; // Detach the content element from the DOM before performing 6174 // manipulation to avoid modifying the live DOM multiple times. 6175 6176 parentEl.removeChild(contentEl); 6177 this.empty(); 6178 insertContent(contentEl, content); 6179 /** 6180 * Fired just after a `ModalDialog` is filled with content. 6181 * 6182 * @event ModalDialog#modalfill 6183 * @type {EventTarget~Event} 6184 */ 6185 6186 this.trigger('modalfill'); // Re-inject the re-filled content element. 6187 6188 if (nextSiblingEl) { 6189 parentEl.insertBefore(contentEl, nextSiblingEl); 6190 } else { 6191 parentEl.appendChild(contentEl); 6192 } // make sure that the close button is last in the dialog DOM 6193 6194 6195 var closeButton = this.getChild('closeButton'); 6196 6197 if (closeButton) { 6198 parentEl.appendChild(closeButton.el_); 6199 } 6200 } 6201 /** 6202 * Empties the content element. This happens anytime the modal is filled. 6203 * 6204 * @fires ModalDialog#beforemodalempty 6205 * @fires ModalDialog#modalempty 6206 */ 6207 ; 6208 6209 _proto.empty = function empty() { 6210 /** 6211 * Fired just before a `ModalDialog` is emptied. 6212 * 6213 * @event ModalDialog#beforemodalempty 6214 * @type {EventTarget~Event} 6215 */ 6216 this.trigger('beforemodalempty'); 6217 emptyEl(this.contentEl()); 6218 /** 6219 * Fired just after a `ModalDialog` is emptied. 6220 * 6221 * @event ModalDialog#modalempty 6222 * @type {EventTarget~Event} 6223 */ 6224 6225 this.trigger('modalempty'); 6226 } 6227 /** 6228 * Gets or sets the modal content, which gets normalized before being 6229 * rendered into the DOM. 6230 * 6231 * This does not update the DOM or fill the modal, but it is called during 6232 * that process. 6233 * 6234 * @param {Mixed} [value] 6235 * If defined, sets the internal content value to be used on the 6236 * next call(s) to `fill`. This value is normalized before being 6237 * inserted. To "clear" the internal content value, pass `null`. 6238 * 6239 * @return {Mixed} 6240 * The current content of the modal dialog 6241 */ 6242 ; 6243 6244 _proto.content = function content(value) { 6245 if (typeof value !== 'undefined') { 6246 this.content_ = value; 6247 } 6248 6249 return this.content_; 6250 } 6251 /** 6252 * conditionally focus the modal dialog if focus was previously on the player. 6253 * 6254 * @private 6255 */ 6256 ; 6257 6258 _proto.conditionalFocus_ = function conditionalFocus_() { 6259 var activeEl = document.activeElement; 6260 var playerEl = this.player_.el_; 6261 this.previouslyActiveEl_ = null; 6262 6263 if (playerEl.contains(activeEl) || playerEl === activeEl) { 6264 this.previouslyActiveEl_ = activeEl; 6265 this.focus(); 6266 } 6267 } 6268 /** 6269 * conditionally blur the element and refocus the last focused element 6270 * 6271 * @private 6272 */ 6273 ; 6274 6275 _proto.conditionalBlur_ = function conditionalBlur_() { 6276 if (this.previouslyActiveEl_) { 6277 this.previouslyActiveEl_.focus(); 6278 this.previouslyActiveEl_ = null; 6279 } 6280 } 6281 /** 6282 * Keydown handler. Attached when modal is focused. 6283 * 6284 * @listens keydown 6285 */ 6286 ; 6287 6288 _proto.handleKeyDown = function handleKeyDown(event) { 6289 // Do not allow keydowns to reach out of the modal dialog. 6290 event.stopPropagation(); 6291 6292 if (keycode.isEventKey(event, 'Escape') && this.closeable()) { 6293 event.preventDefault(); 6294 this.close(); 6295 return; 6296 } // exit early if it isn't a tab key 6297 6298 6299 if (!keycode.isEventKey(event, 'Tab')) { 6300 return; 6301 } 6302 6303 var focusableEls = this.focusableEls_(); 6304 var activeEl = this.el_.querySelector(':focus'); 6305 var focusIndex; 6306 6307 for (var i = 0; i < focusableEls.length; i++) { 6308 if (activeEl === focusableEls[i]) { 6309 focusIndex = i; 6310 break; 6311 } 6312 } 6313 6314 if (document.activeElement === this.el_) { 6315 focusIndex = 0; 6316 } 6317 6318 if (event.shiftKey && focusIndex === 0) { 6319 focusableEls[focusableEls.length - 1].focus(); 6320 event.preventDefault(); 6321 } else if (!event.shiftKey && focusIndex === focusableEls.length - 1) { 6322 focusableEls[0].focus(); 6323 event.preventDefault(); 6324 } 6325 } 6326 /** 6327 * get all focusable elements 6328 * 6329 * @private 6330 */ 6331 ; 6332 6333 _proto.focusableEls_ = function focusableEls_() { 6334 var allChildren = this.el_.querySelectorAll('*'); 6335 return Array.prototype.filter.call(allChildren, function (child) { 6336 return (child instanceof window$1.HTMLAnchorElement || child instanceof window$1.HTMLAreaElement) && child.hasAttribute('href') || (child instanceof window$1.HTMLInputElement || child instanceof window$1.HTMLSelectElement || child instanceof window$1.HTMLTextAreaElement || child instanceof window$1.HTMLButtonElement) && !child.hasAttribute('disabled') || child instanceof window$1.HTMLIFrameElement || child instanceof window$1.HTMLObjectElement || child instanceof window$1.HTMLEmbedElement || child.hasAttribute('tabindex') && child.getAttribute('tabindex') !== -1 || child.hasAttribute('contenteditable'); 6337 }); 6338 }; 6339 6340 return ModalDialog; 6341 }(Component); 6342 /** 6343 * Default options for `ModalDialog` default options. 6344 * 6345 * @type {Object} 6346 * @private 6347 */ 6348 6349 6350 ModalDialog.prototype.options_ = { 6351 pauseOnOpen: true, 6352 temporary: true 6353 }; 6354 Component.registerComponent('ModalDialog', ModalDialog); 6355 6356 /** 6357 * Common functionaliy between {@link TextTrackList}, {@link AudioTrackList}, and 6358 * {@link VideoTrackList} 6359 * 6360 * @extends EventTarget 6361 */ 6362 6363 var TrackList = 6364 /*#__PURE__*/ 6365 function (_EventTarget) { 6366 inheritsLoose(TrackList, _EventTarget); 6367 6368 /** 6369 * Create an instance of this class 6370 * 6371 * @param {Track[]} tracks 6372 * A list of tracks to initialize the list with. 6373 * 6374 * @abstract 6375 */ 6376 function TrackList(tracks) { 6377 var _this; 6378 6379 if (tracks === void 0) { 6380 tracks = []; 6381 } 6382 6383 _this = _EventTarget.call(this) || this; 6384 _this.tracks_ = []; 6385 /** 6386 * @memberof TrackList 6387 * @member {number} length 6388 * The current number of `Track`s in the this Trackist. 6389 * @instance 6390 */ 6391 6392 Object.defineProperty(assertThisInitialized(_this), 'length', { 6393 get: function get() { 6394 return this.tracks_.length; 6395 } 6396 }); 6397 6398 for (var i = 0; i < tracks.length; i++) { 6399 _this.addTrack(tracks[i]); 6400 } 6401 6402 return _this; 6403 } 6404 /** 6405 * Add a {@link Track} to the `TrackList` 6406 * 6407 * @param {Track} track 6408 * The audio, video, or text track to add to the list. 6409 * 6410 * @fires TrackList#addtrack 6411 */ 6412 6413 6414 var _proto = TrackList.prototype; 6415 6416 _proto.addTrack = function addTrack(track) { 6417 var index = this.tracks_.length; 6418 6419 if (!('' + index in this)) { 6420 Object.defineProperty(this, index, { 6421 get: function get() { 6422 return this.tracks_[index]; 6423 } 6424 }); 6425 } // Do not add duplicate tracks 6426 6427 6428 if (this.tracks_.indexOf(track) === -1) { 6429 this.tracks_.push(track); 6430 /** 6431 * Triggered when a track is added to a track list. 6432 * 6433 * @event TrackList#addtrack 6434 * @type {EventTarget~Event} 6435 * @property {Track} track 6436 * A reference to track that was added. 6437 */ 6438 6439 this.trigger({ 6440 track: track, 6441 type: 'addtrack', 6442 target: this 6443 }); 6444 } 6445 } 6446 /** 6447 * Remove a {@link Track} from the `TrackList` 6448 * 6449 * @param {Track} rtrack 6450 * The audio, video, or text track to remove from the list. 6451 * 6452 * @fires TrackList#removetrack 6453 */ 6454 ; 6455 6456 _proto.removeTrack = function removeTrack(rtrack) { 6457 var track; 6458 6459 for (var i = 0, l = this.length; i < l; i++) { 6460 if (this[i] === rtrack) { 6461 track = this[i]; 6462 6463 if (track.off) { 6464 track.off(); 6465 } 6466 6467 this.tracks_.splice(i, 1); 6468 break; 6469 } 6470 } 6471 6472 if (!track) { 6473 return; 6474 } 6475 /** 6476 * Triggered when a track is removed from track list. 6477 * 6478 * @event TrackList#removetrack 6479 * @type {EventTarget~Event} 6480 * @property {Track} track 6481 * A reference to track that was removed. 6482 */ 6483 6484 6485 this.trigger({ 6486 track: track, 6487 type: 'removetrack', 6488 target: this 6489 }); 6490 } 6491 /** 6492 * Get a Track from the TrackList by a tracks id 6493 * 6494 * @param {string} id - the id of the track to get 6495 * @method getTrackById 6496 * @return {Track} 6497 * @private 6498 */ 6499 ; 6500 6501 _proto.getTrackById = function getTrackById(id) { 6502 var result = null; 6503 6504 for (var i = 0, l = this.length; i < l; i++) { 6505 var track = this[i]; 6506 6507 if (track.id === id) { 6508 result = track; 6509 break; 6510 } 6511 } 6512 6513 return result; 6514 }; 6515 6516 return TrackList; 6517 }(EventTarget); 6518 /** 6519 * Triggered when a different track is selected/enabled. 6520 * 6521 * @event TrackList#change 6522 * @type {EventTarget~Event} 6523 */ 6524 6525 /** 6526 * Events that can be called with on + eventName. See {@link EventHandler}. 6527 * 6528 * @property {Object} TrackList#allowedEvents_ 6529 * @private 6530 */ 6531 6532 6533 TrackList.prototype.allowedEvents_ = { 6534 change: 'change', 6535 addtrack: 'addtrack', 6536 removetrack: 'removetrack' 6537 }; // emulate attribute EventHandler support to allow for feature detection 6538 6539 for (var event in TrackList.prototype.allowedEvents_) { 6540 TrackList.prototype['on' + event] = null; 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 {AudioTrack} track 6551 * The track to skip 6552 * 6553 * @private 6554 */ 6555 6556 var disableOthers = function disableOthers(list, track) { 6557 for (var i = 0; i < list.length; i++) { 6558 if (!Object.keys(list[i]).length || track.id === list[i].id) { 6559 continue; 6560 } // another audio track is enabled, disable it 6561 6562 6563 list[i].enabled = false; 6564 } 6565 }; 6566 /** 6567 * The current list of {@link AudioTrack} for a media file. 6568 * 6569 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotracklist} 6570 * @extends TrackList 6571 */ 6572 6573 6574 var AudioTrackList = 6575 /*#__PURE__*/ 6576 function (_TrackList) { 6577 inheritsLoose(AudioTrackList, _TrackList); 6578 6579 /** 6580 * Create an instance of this class. 6581 * 6582 * @param {AudioTrack[]} [tracks=[]] 6583 * A list of `AudioTrack` to instantiate the list with. 6584 */ 6585 function AudioTrackList(tracks) { 6586 var _this; 6587 6588 if (tracks === void 0) { 6589 tracks = []; 6590 } 6591 6592 // make sure only 1 track is enabled 6593 // sorted from last index to first index 6594 for (var i = tracks.length - 1; i >= 0; i--) { 6595 if (tracks[i].enabled) { 6596 disableOthers(tracks, tracks[i]); 6597 break; 6598 } 6599 } 6600 6601 _this = _TrackList.call(this, tracks) || this; 6602 _this.changing_ = false; 6603 return _this; 6604 } 6605 /** 6606 * Add an {@link AudioTrack} to the `AudioTrackList`. 6607 * 6608 * @param {AudioTrack} track 6609 * The AudioTrack to add to the list 6610 * 6611 * @fires TrackList#addtrack 6612 */ 6613 6614 6615 var _proto = AudioTrackList.prototype; 6616 6617 _proto.addTrack = function addTrack(track) { 6618 var _this2 = this; 6619 6620 if (track.enabled) { 6621 disableOthers(this, track); 6622 } 6623 6624 _TrackList.prototype.addTrack.call(this, track); // native tracks don't have this 6625 6626 6627 if (!track.addEventListener) { 6628 return; 6629 } 6630 6631 track.enabledChange_ = function () { 6632 // when we are disabling other tracks (since we don't support 6633 // more than one track at a time) we will set changing_ 6634 // to true so that we don't trigger additional change events 6635 if (_this2.changing_) { 6636 return; 6637 } 6638 6639 _this2.changing_ = true; 6640 disableOthers(_this2, track); 6641 _this2.changing_ = false; 6642 6643 _this2.trigger('change'); 6644 }; 6645 /** 6646 * @listens AudioTrack#enabledchange 6647 * @fires TrackList#change 6648 */ 6649 6650 6651 track.addEventListener('enabledchange', track.enabledChange_); 6652 }; 6653 6654 _proto.removeTrack = function removeTrack(rtrack) { 6655 _TrackList.prototype.removeTrack.call(this, rtrack); 6656 6657 if (rtrack.removeEventListener && rtrack.enabledChange_) { 6658 rtrack.removeEventListener('enabledchange', rtrack.enabledChange_); 6659 rtrack.enabledChange_ = null; 6660 } 6661 }; 6662 6663 return AudioTrackList; 6664 }(TrackList); 6665 6666 /** 6667 * Un-select all other {@link VideoTrack}s that are selected. 6668 * 6669 * @param {VideoTrackList} list 6670 * list to work on 6671 * 6672 * @param {VideoTrack} track 6673 * The track to skip 6674 * 6675 * @private 6676 */ 6677 6678 var disableOthers$1 = function disableOthers(list, track) { 6679 for (var i = 0; i < list.length; i++) { 6680 if (!Object.keys(list[i]).length || track.id === list[i].id) { 6681 continue; 6682 } // another video track is enabled, disable it 6683 6684 6685 list[i].selected = false; 6686 } 6687 }; 6688 /** 6689 * The current list of {@link VideoTrack} for a video. 6690 * 6691 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#videotracklist} 6692 * @extends TrackList 6693 */ 6694 6695 6696 var VideoTrackList = 6697 /*#__PURE__*/ 6698 function (_TrackList) { 6699 inheritsLoose(VideoTrackList, _TrackList); 6700 6701 /** 6702 * Create an instance of this class. 6703 * 6704 * @param {VideoTrack[]} [tracks=[]] 6705 * A list of `VideoTrack` to instantiate the list with. 6706 */ 6707 function VideoTrackList(tracks) { 6708 var _this; 6709 6710 if (tracks === void 0) { 6711 tracks = []; 6712 } 6713 6714 // make sure only 1 track is enabled 6715 // sorted from last index to first index 6716 for (var i = tracks.length - 1; i >= 0; i--) { 6717 if (tracks[i].selected) { 6718 disableOthers$1(tracks, tracks[i]); 6719 break; 6720 } 6721 } 6722 6723 _this = _TrackList.call(this, tracks) || this; 6724 _this.changing_ = false; 6725 /** 6726 * @member {number} VideoTrackList#selectedIndex 6727 * The current index of the selected {@link VideoTrack`}. 6728 */ 6729 6730 Object.defineProperty(assertThisInitialized(_this), 'selectedIndex', { 6731 get: function get() { 6732 for (var _i = 0; _i < this.length; _i++) { 6733 if (this[_i].selected) { 6734 return _i; 6735 } 6736 } 6737 6738 return -1; 6739 }, 6740 set: function set() {} 6741 }); 6742 return _this; 6743 } 6744 /** 6745 * Add a {@link VideoTrack} to the `VideoTrackList`. 6746 * 6747 * @param {VideoTrack} track 6748 * The VideoTrack to add to the list 6749 * 6750 * @fires TrackList#addtrack 6751 */ 6752 6753 6754 var _proto = VideoTrackList.prototype; 6755 6756 _proto.addTrack = function addTrack(track) { 6757 var _this2 = this; 6758 6759 if (track.selected) { 6760 disableOthers$1(this, track); 6761 } 6762 6763 _TrackList.prototype.addTrack.call(this, track); // native tracks don't have this 6764 6765 6766 if (!track.addEventListener) { 6767 return; 6768 } 6769 6770 track.selectedChange_ = function () { 6771 if (_this2.changing_) { 6772 return; 6773 } 6774 6775 _this2.changing_ = true; 6776 disableOthers$1(_this2, track); 6777 _this2.changing_ = false; 6778 6779 _this2.trigger('change'); 6780 }; 6781 /** 6782 * @listens VideoTrack#selectedchange 6783 * @fires TrackList#change 6784 */ 6785 6786 6787 track.addEventListener('selectedchange', track.selectedChange_); 6788 }; 6789 6790 _proto.removeTrack = function removeTrack(rtrack) { 6791 _TrackList.prototype.removeTrack.call(this, rtrack); 6792 6793 if (rtrack.removeEventListener && rtrack.selectedChange_) { 6794 rtrack.removeEventListener('selectedchange', rtrack.selectedChange_); 6795 rtrack.selectedChange_ = null; 6796 } 6797 }; 6798 6799 return VideoTrackList; 6800 }(TrackList); 6801 6802 /** 6803 * The current list of {@link TextTrack} for a media file. 6804 * 6805 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttracklist} 6806 * @extends TrackList 6807 */ 6808 6809 var TextTrackList = 6810 /*#__PURE__*/ 6811 function (_TrackList) { 6812 inheritsLoose(TextTrackList, _TrackList); 6813 6814 function TextTrackList() { 6815 return _TrackList.apply(this, arguments) || this; 6816 } 6817 6818 var _proto = TextTrackList.prototype; 6819 6820 /** 6821 * Add a {@link TextTrack} to the `TextTrackList` 6822 * 6823 * @param {TextTrack} track 6824 * The text track to add to the list. 6825 * 6826 * @fires TrackList#addtrack 6827 */ 6828 _proto.addTrack = function addTrack(track) { 6829 var _this = this; 6830 6831 _TrackList.prototype.addTrack.call(this, track); 6832 6833 if (!this.queueChange_) { 6834 this.queueChange_ = function () { 6835 return _this.queueTrigger('change'); 6836 }; 6837 } 6838 6839 if (!this.triggerSelectedlanguagechange) { 6840 this.triggerSelectedlanguagechange_ = function () { 6841 return _this.trigger('selectedlanguagechange'); 6842 }; 6843 } 6844 /** 6845 * @listens TextTrack#modechange 6846 * @fires TrackList#change 6847 */ 6848 6849 6850 track.addEventListener('modechange', this.queueChange_); 6851 var nonLanguageTextTrackKind = ['metadata', 'chapters']; 6852 6853 if (nonLanguageTextTrackKind.indexOf(track.kind) === -1) { 6854 track.addEventListener('modechange', this.triggerSelectedlanguagechange_); 6855 } 6856 }; 6857 6858 _proto.removeTrack = function removeTrack(rtrack) { 6859 _TrackList.prototype.removeTrack.call(this, rtrack); // manually remove the event handlers we added 6860 6861 6862 if (rtrack.removeEventListener) { 6863 if (this.queueChange_) { 6864 rtrack.removeEventListener('modechange', this.queueChange_); 6865 } 6866 6867 if (this.selectedlanguagechange_) { 6868 rtrack.removeEventListener('modechange', this.triggerSelectedlanguagechange_); 6869 } 6870 } 6871 }; 6872 6873 return TextTrackList; 6874 }(TrackList); 6875 6876 /** 6877 * @file html-track-element-list.js 6878 */ 6879 6880 /** 6881 * The current list of {@link HtmlTrackElement}s. 6882 */ 6883 var HtmlTrackElementList = 6884 /*#__PURE__*/ 6885 function () { 6886 /** 6887 * Create an instance of this class. 6888 * 6889 * @param {HtmlTrackElement[]} [tracks=[]] 6890 * A list of `HtmlTrackElement` to instantiate the list with. 6891 */ 6892 function HtmlTrackElementList(trackElements) { 6893 if (trackElements === void 0) { 6894 trackElements = []; 6895 } 6896 6897 this.trackElements_ = []; 6898 /** 6899 * @memberof HtmlTrackElementList 6900 * @member {number} length 6901 * The current number of `Track`s in the this Trackist. 6902 * @instance 6903 */ 6904 6905 Object.defineProperty(this, 'length', { 6906 get: function get() { 6907 return this.trackElements_.length; 6908 } 6909 }); 6910 6911 for (var i = 0, length = trackElements.length; i < length; i++) { 6912 this.addTrackElement_(trackElements[i]); 6913 } 6914 } 6915 /** 6916 * Add an {@link HtmlTrackElement} to the `HtmlTrackElementList` 6917 * 6918 * @param {HtmlTrackElement} trackElement 6919 * The track element to add to the list. 6920 * 6921 * @private 6922 */ 6923 6924 6925 var _proto = HtmlTrackElementList.prototype; 6926 6927 _proto.addTrackElement_ = function addTrackElement_(trackElement) { 6928 var index = this.trackElements_.length; 6929 6930 if (!('' + index in this)) { 6931 Object.defineProperty(this, index, { 6932 get: function get() { 6933 return this.trackElements_[index]; 6934 } 6935 }); 6936 } // Do not add duplicate elements 6937 6938 6939 if (this.trackElements_.indexOf(trackElement) === -1) { 6940 this.trackElements_.push(trackElement); 6941 } 6942 } 6943 /** 6944 * Get an {@link HtmlTrackElement} from the `HtmlTrackElementList` given an 6945 * {@link TextTrack}. 6946 * 6947 * @param {TextTrack} track 6948 * The track associated with a track element. 6949 * 6950 * @return {HtmlTrackElement|undefined} 6951 * The track element that was found or undefined. 6952 * 6953 * @private 6954 */ 6955 ; 6956 6957 _proto.getTrackElementByTrack_ = function getTrackElementByTrack_(track) { 6958 var trackElement_; 6959 6960 for (var i = 0, length = this.trackElements_.length; i < length; i++) { 6961 if (track === this.trackElements_[i].track) { 6962 trackElement_ = this.trackElements_[i]; 6963 break; 6964 } 6965 } 6966 6967 return trackElement_; 6968 } 6969 /** 6970 * Remove a {@link HtmlTrackElement} from the `HtmlTrackElementList` 6971 * 6972 * @param {HtmlTrackElement} trackElement 6973 * The track element to remove from the list. 6974 * 6975 * @private 6976 */ 6977 ; 6978 6979 _proto.removeTrackElement_ = function removeTrackElement_(trackElement) { 6980 for (var i = 0, length = this.trackElements_.length; i < length; i++) { 6981 if (trackElement === this.trackElements_[i]) { 6982 if (this.trackElements_[i].track && typeof this.trackElements_[i].track.off === 'function') { 6983 this.trackElements_[i].track.off(); 6984 } 6985 6986 if (typeof this.trackElements_[i].off === 'function') { 6987 this.trackElements_[i].off(); 6988 } 6989 6990 this.trackElements_.splice(i, 1); 6991 break; 6992 } 6993 } 6994 }; 6995 6996 return HtmlTrackElementList; 6997 }(); 6998 6999 /** 7000 * @file text-track-cue-list.js 7001 */ 7002 7003 /** 7004 * @typedef {Object} TextTrackCueList~TextTrackCue 7005 * 7006 * @property {string} id 7007 * The unique id for this text track cue 7008 * 7009 * @property {number} startTime 7010 * The start time for this text track cue 7011 * 7012 * @property {number} endTime 7013 * The end time for this text track cue 7014 * 7015 * @property {boolean} pauseOnExit 7016 * Pause when the end time is reached if true. 7017 * 7018 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackcue} 7019 */ 7020 7021 /** 7022 * A List of TextTrackCues. 7023 * 7024 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackcuelist} 7025 */ 7026 var TextTrackCueList = 7027 /*#__PURE__*/ 7028 function () { 7029 /** 7030 * Create an instance of this class.. 7031 * 7032 * @param {Array} cues 7033 * A list of cues to be initialized with 7034 */ 7035 function TextTrackCueList(cues) { 7036 TextTrackCueList.prototype.setCues_.call(this, cues); 7037 /** 7038 * @memberof TextTrackCueList 7039 * @member {number} length 7040 * The current number of `TextTrackCue`s in the TextTrackCueList. 7041 * @instance 7042 */ 7043 7044 Object.defineProperty(this, 'length', { 7045 get: function get() { 7046 return this.length_; 7047 } 7048 }); 7049 } 7050 /** 7051 * A setter for cues in this list. Creates getters 7052 * an an index for the cues. 7053 * 7054 * @param {Array} cues 7055 * An array of cues to set 7056 * 7057 * @private 7058 */ 7059 7060 7061 var _proto = TextTrackCueList.prototype; 7062 7063 _proto.setCues_ = function setCues_(cues) { 7064 var oldLength = this.length || 0; 7065 var i = 0; 7066 var l = cues.length; 7067 this.cues_ = cues; 7068 this.length_ = cues.length; 7069 7070 var defineProp = function defineProp(index) { 7071 if (!('' + index in this)) { 7072 Object.defineProperty(this, '' + index, { 7073 get: function get() { 7074 return this.cues_[index]; 7075 } 7076 }); 7077 } 7078 }; 7079 7080 if (oldLength < l) { 7081 i = oldLength; 7082 7083 for (; i < l; i++) { 7084 defineProp.call(this, i); 7085 } 7086 } 7087 } 7088 /** 7089 * Get a `TextTrackCue` that is currently in the `TextTrackCueList` by id. 7090 * 7091 * @param {string} id 7092 * The id of the cue that should be searched for. 7093 * 7094 * @return {TextTrackCueList~TextTrackCue|null} 7095 * A single cue or null if none was found. 7096 */ 7097 ; 7098 7099 _proto.getCueById = function getCueById(id) { 7100 var result = null; 7101 7102 for (var i = 0, l = this.length; i < l; i++) { 7103 var cue = this[i]; 7104 7105 if (cue.id === id) { 7106 result = cue; 7107 break; 7108 } 7109 } 7110 7111 return result; 7112 }; 7113 7114 return TextTrackCueList; 7115 }(); 7116 7117 /** 7118 * @file track-kinds.js 7119 */ 7120 7121 /** 7122 * All possible `VideoTrackKind`s 7123 * 7124 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-videotrack-kind 7125 * @typedef VideoTrack~Kind 7126 * @enum 7127 */ 7128 var VideoTrackKind = { 7129 alternative: 'alternative', 7130 captions: 'captions', 7131 main: 'main', 7132 sign: 'sign', 7133 subtitles: 'subtitles', 7134 commentary: 'commentary' 7135 }; 7136 /** 7137 * All possible `AudioTrackKind`s 7138 * 7139 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-audiotrack-kind 7140 * @typedef AudioTrack~Kind 7141 * @enum 7142 */ 7143 7144 var AudioTrackKind = { 7145 'alternative': 'alternative', 7146 'descriptions': 'descriptions', 7147 'main': 'main', 7148 'main-desc': 'main-desc', 7149 'translation': 'translation', 7150 'commentary': 'commentary' 7151 }; 7152 /** 7153 * All possible `TextTrackKind`s 7154 * 7155 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-texttrack-kind 7156 * @typedef TextTrack~Kind 7157 * @enum 7158 */ 7159 7160 var TextTrackKind = { 7161 subtitles: 'subtitles', 7162 captions: 'captions', 7163 descriptions: 'descriptions', 7164 chapters: 'chapters', 7165 metadata: 'metadata' 7166 }; 7167 /** 7168 * All possible `TextTrackMode`s 7169 * 7170 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackmode 7171 * @typedef TextTrack~Mode 7172 * @enum 7173 */ 7174 7175 var TextTrackMode = { 7176 disabled: 'disabled', 7177 hidden: 'hidden', 7178 showing: 'showing' 7179 }; 7180 7181 /** 7182 * A Track class that contains all of the common functionality for {@link AudioTrack}, 7183 * {@link VideoTrack}, and {@link TextTrack}. 7184 * 7185 * > Note: This class should not be used directly 7186 * 7187 * @see {@link https://html.spec.whatwg.org/multipage/embedded-content.html} 7188 * @extends EventTarget 7189 * @abstract 7190 */ 7191 7192 var Track = 7193 /*#__PURE__*/ 7194 function (_EventTarget) { 7195 inheritsLoose(Track, _EventTarget); 7196 7197 /** 7198 * Create an instance of this class. 7199 * 7200 * @param {Object} [options={}] 7201 * Object of option names and values 7202 * 7203 * @param {string} [options.kind=''] 7204 * A valid kind for the track type you are creating. 7205 * 7206 * @param {string} [options.id='vjs_track_' + Guid.newGUID()] 7207 * A unique id for this AudioTrack. 7208 * 7209 * @param {string} [options.label=''] 7210 * The menu label for this track. 7211 * 7212 * @param {string} [options.language=''] 7213 * A valid two character language code. 7214 * 7215 * @abstract 7216 */ 7217 function Track(options) { 7218 var _this; 7219 7220 if (options === void 0) { 7221 options = {}; 7222 } 7223 7224 _this = _EventTarget.call(this) || this; 7225 var trackProps = { 7226 id: options.id || 'vjs_track_' + newGUID(), 7227 kind: options.kind || '', 7228 label: options.label || '', 7229 language: options.language || '' 7230 }; 7231 /** 7232 * @memberof Track 7233 * @member {string} id 7234 * The id of this track. Cannot be changed after creation. 7235 * @instance 7236 * 7237 * @readonly 7238 */ 7239 7240 /** 7241 * @memberof Track 7242 * @member {string} kind 7243 * The kind of track that this is. Cannot be changed after creation. 7244 * @instance 7245 * 7246 * @readonly 7247 */ 7248 7249 /** 7250 * @memberof Track 7251 * @member {string} label 7252 * The label of this track. Cannot be changed after creation. 7253 * @instance 7254 * 7255 * @readonly 7256 */ 7257 7258 /** 7259 * @memberof Track 7260 * @member {string} language 7261 * The two letter language code for this track. Cannot be changed after 7262 * creation. 7263 * @instance 7264 * 7265 * @readonly 7266 */ 7267 7268 var _loop = function _loop(key) { 7269 Object.defineProperty(assertThisInitialized(_this), key, { 7270 get: function get() { 7271 return trackProps[key]; 7272 }, 7273 set: function set() {} 7274 }); 7275 }; 7276 7277 for (var key in trackProps) { 7278 _loop(key); 7279 } 7280 7281 return _this; 7282 } 7283 7284 return Track; 7285 }(EventTarget); 7286 7287 /** 7288 * @file url.js 7289 * @module url 7290 */ 7291 /** 7292 * @typedef {Object} url:URLObject 7293 * 7294 * @property {string} protocol 7295 * The protocol of the url that was parsed. 7296 * 7297 * @property {string} hostname 7298 * The hostname of the url that was parsed. 7299 * 7300 * @property {string} port 7301 * The port of the url that was parsed. 7302 * 7303 * @property {string} pathname 7304 * The pathname of the url that was parsed. 7305 * 7306 * @property {string} search 7307 * The search query of the url that was parsed. 7308 * 7309 * @property {string} hash 7310 * The hash of the url that was parsed. 7311 * 7312 * @property {string} host 7313 * The host of the url that was parsed. 7314 */ 7315 7316 /** 7317 * Resolve and parse the elements of a URL. 7318 * 7319 * @function 7320 * @param {String} url 7321 * The url to parse 7322 * 7323 * @return {url:URLObject} 7324 * An object of url details 7325 */ 7326 7327 var parseUrl = function parseUrl(url) { 7328 var props = ['protocol', 'hostname', 'port', 'pathname', 'search', 'hash', 'host']; // add the url to an anchor and let the browser parse the URL 7329 7330 var a = document.createElement('a'); 7331 a.href = url; // IE8 (and 9?) Fix
7332 // ie8 doesn't parse the URL correctly until the anchor is actually 7333 // added to the body, and an innerHTML is needed to trigger the parsing 7334 7335 var addToBody = a.host === '' && a.protocol !== 'file:'; 7336 var div; 7337 7338 if (addToBody) { 7339 div = document.createElement('div'); 7340 div.innerHTML = "<a href=\"" + url + "\"></a>"; 7341 a = div.firstChild; // prevent the div from affecting layout 7342 7343 div.setAttribute('style', 'display:none; position:absolute;'); 7344 document.body.appendChild(div); 7345 } // Copy the specific URL properties to a new object 7346 // This is also needed for IE8 because the anchor loses its 7347 // properties when it's removed from the dom 7348 7349 7350 var details = {}; 7351 7352 for (var i = 0; i < props.length; i++) { 7353 details[props[i]] = a[props[i]]; 7354 } // IE9 adds the port to the host property unlike everyone else. If 7355 // a port identifier is added for standard ports, strip it. 7356 7357 7358 if (details.protocol === 'http:') { 7359 details.host = details.host.replace(/:80$/, ''); 7360 } 7361 7362 if (details.protocol === 'https:') { 7363 details.host = details.host.replace(/:443$/, ''); 7364 } 7365 7366 if (!details.protocol) { 7367 details.protocol = window$1.location.protocol; 7368 } 7369 7370 if (addToBody) { 7371 document.body.removeChild(div); 7372 } 7373 7374 return details; 7375 }; 7376 /** 7377 * Get absolute version of relative URL. Used to tell Flash the correct URL. 7378 * 7379 * @function 7380 * @param {string} url 7381 * URL to make absolute 7382 * 7383 * @return {string} 7384 * Absolute URL 7385 * 7386 * @see http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue 7387 */ 7388 7389 var getAbsoluteURL = function getAbsoluteURL(url) { 7390 // Check if absolute URL 7391 if (!url.match(/^https?:\/\//)) { 7392 // Convert to absolute URL. Flash hosted off-site needs an absolute URL. 7393 var div = document.createElement('div'); 7394 div.innerHTML = "<a href=\"" + url + "\">x</a>"; 7395 url = div.firstChild.href; 7396 } 7397 7398 return url; 7399 }; 7400 /** 7401 * Returns the extension of the passed file name. It will return an empty string 7402 * if passed an invalid path. 7403 * 7404 * @function 7405 * @param {string} path 7406 * The fileName path like '/path/to/file.mp4' 7407 * 7408 * @return {string} 7409 * The extension in lower case or an empty string if no 7410 * extension could be found. 7411 */ 7412 7413 var getFileExtension = function getFileExtension(path) { 7414 if (typeof path === 'string') { 7415 var splitPathRe = /^(\/?)([\s\S]*?)((?:\.{1,2}|[^\/]+?)(\.([^\.\/\?]+)))(?:[\/]*|[\?].*)$/; 7416 var pathParts = splitPathRe.exec(path); 7417 7418 if (pathParts) { 7419 return pathParts.pop().toLowerCase(); 7420 } 7421 } 7422 7423 return ''; 7424 }; 7425 /** 7426 * Returns whether the url passed is a cross domain request or not. 7427 * 7428 * @function 7429 * @param {string} url 7430 * The url to check. 7431 * 7432 * @param {Object} [winLoc] 7433 * the domain to check the url against, defaults to window.location 7434 * 7435 * @param {string} [winLoc.protocol] 7436 * The window location protocol defaults to window.location.protocol 7437 * 7438 * @param {string} [winLoc.host] 7439 * The window location host defaults to window.location.host 7440 * 7441 * @return {boolean} 7442 * Whether it is a cross domain request or not. 7443 */ 7444 7445 var isCrossOrigin = function isCrossOrigin(url, winLoc) { 7446 if (winLoc === void 0) { 7447 winLoc = window$1.location; 7448 } 7449 7450 var urlInfo = parseUrl(url); // IE8 protocol relative urls will return ':' for protocol 7451 7452 var srcProtocol = urlInfo.protocol === ':' ? winLoc.protocol : urlInfo.protocol; // Check if url is for another domain/origin 7453 // IE8 doesn't know location.origin, so we won't rely on it here 7454 7455 var crossOrigin = srcProtocol + urlInfo.host !== winLoc.protocol + winLoc.host; 7456 return crossOrigin; 7457 }; 7458 7459 var Url = /*#__PURE__*/Object.freeze({ 7460 parseUrl: parseUrl, 7461 getAbsoluteURL: getAbsoluteURL, 7462 getFileExtension: getFileExtension, 7463 isCrossOrigin: isCrossOrigin 7464 }); 7465 7466 var isFunction_1 = isFunction; 7467 var toString$1 = Object.prototype.toString; 7468 7469 function isFunction(fn) { 7470 var string = toString$1.call(fn); 7471 return string === '[object Function]' || typeof fn === 'function' && string !== '[object RegExp]' || typeof window !== 'undefined' && ( // IE8 and below 7472 fn === window.setTimeout || fn === window.alert || fn === window.confirm || fn === window.prompt); 7473 } 7474 7475 /** 7476 * @license 7477 * slighly modified parse-headers 2.0.2 <https://github.com/kesla/parse-headers/> 7478 * Copyright (c) 2014 David Björklund 7479 * Available under the MIT license 7480 * <https://github.com/kesla/parse-headers/blob/master/LICENCE> 7481 */ 7482 7483 7484 var parseHeaders = function parseHeaders(headers) { 7485 var result = {}; 7486 7487 if (!headers) { 7488 return result; 7489 } 7490 7491 headers.trim().split('\n').forEach(function (row) { 7492 var index = row.indexOf(':'); 7493 var key = row.slice(0, index).trim().toLowerCase(); 7494 var value = row.slice(index + 1).trim(); 7495 7496 if (typeof result[key] === 'undefined') { 7497 result[key] = value; 7498 } else if (Array.isArray(result[key])) { 7499 result[key].push(value); 7500 } else { 7501 result[key] = [result[key], value]; 7502 } 7503 }); 7504 return result; 7505 }; 7506 7507 var xhr = createXHR; // Allow use of default import syntax in TypeScript 7508 7509 var default_1 = createXHR; 7510 createXHR.XMLHttpRequest = window$1.XMLHttpRequest || noop; 7511 createXHR.XDomainRequest = "withCredentials" in new createXHR.XMLHttpRequest() ? createXHR.XMLHttpRequest : window$1.XDomainRequest;
vendor: 29,830 bytes, lines 7512-8548
7512 forEachArray(["get", "put", "post", "patch", "head", "delete"], function (method) { 7513 createXHR[method === "delete" ? "del" : method] = function (uri, options, callback) { 7514 options = initParams(uri, options, callback); 7515 options.method = method.toUpperCase(); 7516 return _createXHR(options); 7517 }; 7518 }); 7519 7520 function forEachArray(array, iterator) { 7521 for (var i = 0; i < array.length; i++) { 7522 iterator(array[i]); 7523 } 7524 } 7525 7526 function isEmpty(obj) { 7527 for (var i in obj) { 7528 if (obj.hasOwnProperty(i)) return false; 7529 } 7530 7531 return true; 7532 } 7533 7534 function initParams(uri, options, callback) { 7535 var params = uri; 7536 7537 if (isFunction_1(options)) { 7538 callback = options; 7539 7540 if (typeof uri === "string") { 7541 params = { 7542 uri: uri 7543 }; 7544 } 7545 } else { 7546 params = _extends_1({}, options, { 7547 uri: uri 7548 }); 7549 } 7550 7551 params.callback = callback; 7552 return params; 7553 } 7554 7555 function createXHR(uri, options, callback) { 7556 options = initParams(uri, options, callback); 7557 return _createXHR(options); 7558 } 7559 7560 function _createXHR(options) { 7561 if (typeof options.callback === "undefined") { 7562 throw new Error("callback argument missing"); 7563 } 7564 7565 var called = false; 7566 7567 var callback = function cbOnce(err, response, body) { 7568 if (!called) { 7569 called = true; 7570 options.callback(err, response, body); 7571 } 7572 }; 7573 7574 function readystatechange() { 7575 if (xhr.readyState === 4) { 7576 setTimeout(loadFunc, 0); 7577 } 7578 } 7579 7580 function getBody() { 7581 // Chrome with requestType=blob throws errors arround when even testing access to responseText 7582 var body = undefined; 7583 7584 if (xhr.response) { 7585 body = xhr.response; 7586 } else { 7587 body = xhr.responseText || getXml(xhr); 7588 } 7589 7590 if (isJson) { 7591 try { 7592 body = JSON.parse(body); 7593 } catch (e) {} 7594 } 7595 7596 return body; 7597 } 7598 7599 function errorFunc(evt) { 7600 clearTimeout(timeoutTimer); 7601 7602 if (!(evt instanceof Error)) { 7603 evt = new Error("" + (evt || "Unknown XMLHttpRequest Error")); 7604 } 7605 7606 evt.statusCode = 0; 7607 return callback(evt, failureResponse); 7608 } // will load the data & process the response in a special response object 7609 7610 7611 function loadFunc() { 7612 if (aborted) return; 7613 var status; 7614 clearTimeout(timeoutTimer); 7615 7616 if (options.useXDR && xhr.status === undefined) { 7617 //IE8 CORS GET successful response doesn't have a status field, but body is fine 7618 status = 200; 7619 } else { 7620 status = xhr.status === 1223 ? 204 : xhr.status; 7621 } 7622 7623 var response = failureResponse; 7624 var err = null; 7625 7626 if (status !== 0) { 7627 response = { 7628 body: getBody(), 7629 statusCode: status, 7630 method: method, 7631 headers: {}, 7632 url: uri, 7633 rawRequest: xhr 7634 }; 7635 7636 if (xhr.getAllResponseHeaders) { 7637 //remember xhr can in fact be XDR for CORS in IE 7638 response.headers = parseHeaders(xhr.getAllResponseHeaders()); 7639 } 7640 } else { 7641 err = new Error("Internal XMLHttpRequest Error"); 7642 } 7643 7644 return callback(err, response, response.body); 7645 } 7646 7647 var xhr = options.xhr || null; 7648 7649 if (!xhr) { 7650 if (options.cors || options.useXDR) { 7651 xhr = new createXHR.XDomainRequest(); 7652 } else { 7653 xhr = new createXHR.XMLHttpRequest(); 7654 } 7655 } 7656 7657 var key; 7658 var aborted; 7659 var uri = xhr.url = options.uri || options.url; 7660 var method = xhr.method = options.method || "GET"; 7661 var body = options.body || options.data; 7662 var headers = xhr.headers = options.headers || {}; 7663 var sync = !!options.sync; 7664 var isJson = false; 7665 var timeoutTimer; 7666 var failureResponse = { 7667 body: undefined, 7668 headers: {}, 7669 statusCode: 0, 7670 method: method, 7671 url: uri, 7672 rawRequest: xhr 7673 }; 7674 7675 if ("json" in options && options.json !== false) { 7676 isJson = true; 7677 headers["accept"] || headers["Accept"] || (headers["Accept"] = "application/json"); //Don't override existing accept header declared by user 7678 7679 if (method !== "GET" && method !== "HEAD") { 7680 headers["content-type"] || headers["Content-Type"] || (headers["Content-Type"] = "application/json"); //Don't override existing accept header declared by user 7681 7682 body = JSON.stringify(options.json === true ? body : options.json); 7683 } 7684 } 7685 7686 xhr.onreadystatechange = readystatechange; 7687 xhr.onload = loadFunc; 7688 xhr.onerror = errorFunc; // IE9 must have onprogress be set to a unique function. 7689 7690 xhr.onprogress = function () {// IE must die 7691 }; 7692 7693 xhr.onabort = function () { 7694 aborted = true; 7695 }; 7696 7697 xhr.ontimeout = errorFunc; 7698 xhr.open(method, uri, !sync, options.username, options.password); //has to be after open 7699 7700 if (!sync) { 7701 xhr.withCredentials = !!options.withCredentials; 7702 } // Cannot set timeout with sync request 7703 // not setting timeout on the xhr object, because of old webkits etc. not handling that correctly 7704 // both npm's request and jquery 1.x use this kind of timeout, so this is being consistent 7705 7706 7707 if (!sync && options.timeout > 0) { 7708 timeoutTimer = setTimeout(function () { 7709 if (aborted) return; 7710 aborted = true; //IE9 may still call readystatechange 7711 7712 xhr.abort("timeout"); 7713 var e = new Error("XMLHttpRequest timeout"); 7714 e.code = "ETIMEDOUT"; 7715 errorFunc(e); 7716 }, options.timeout); 7717 } 7718 7719 if (xhr.setRequestHeader) { 7720 for (key in headers) { 7721 if (headers.hasOwnProperty(key)) { 7722 xhr.setRequestHeader(key, headers[key]); 7723 } 7724 } 7725 } else if (options.headers && !isEmpty(options.headers)) { 7726 throw new Error("Headers cannot be set on an XDomainRequest object"); 7727 } 7728 7729 if ("responseType" in options) { 7730 xhr.responseType = options.responseType; 7731 } 7732 7733 if ("beforeSend" in options && typeof options.beforeSend === "function") { 7734 options.beforeSend(xhr); 7735 } // Microsoft Edge browser sends "undefined" when send is called with undefined value. 7736 // XMLHttpRequest spec says to pass null as body to indicate no body 7737 // See https://github.com/naugtur/xhr/issues/100. 7738 7739 7740 xhr.send(body || null); 7741 return xhr; 7742 } 7743 7744 function getXml(xhr) { 7745 // xhr.responseXML will throw Exception "InvalidStateError" or "DOMException" 7746 // See https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseXML. 7747 try { 7748 if (xhr.responseType === "document") { 7749 return xhr.responseXML; 7750 } 7751 7752 var firefoxBugTakenEffect = xhr.responseXML && xhr.responseXML.documentElement.nodeName === "parsererror"; 7753 7754 if (xhr.responseType === "" && !firefoxBugTakenEffect) { 7755 return xhr.responseXML; 7756 } 7757 } catch (e) {} 7758 7759 return null; 7760 } 7761 7762 function noop() {} 7763 xhr["default"] = default_1; 7764 7765 /** 7766 * Takes a webvtt file contents and parses it into cues 7767 * 7768 * @param {string} srcContent 7769 * webVTT file contents 7770 * 7771 * @param {TextTrack} track 7772 * TextTrack to add cues to. Cues come from the srcContent. 7773 * 7774 * @private 7775 */ 7776 7777 var parseCues = function parseCues(srcContent, track) { 7778 var parser = new window$1.WebVTT.Parser(window$1, window$1.vttjs, window$1.WebVTT.StringDecoder()); 7779 var errors = []; 7780 7781 parser.oncue = function (cue) { 7782 track.addCue(cue); 7783 }; 7784 7785 parser.onparsingerror = function (error) { 7786 errors.push(error); 7787 }; 7788 7789 parser.onflush = function () { 7790 track.trigger({ 7791 type: 'loadeddata', 7792 target: track 7793 }); 7794 }; 7795 7796 parser.parse(srcContent); 7797 7798 if (errors.length > 0) { 7799 if (window$1.console && window$1.console.groupCollapsed) { 7800 window$1.console.groupCollapsed("Text Track parsing errors for " + track.src); 7801 } 7802 7803 errors.forEach(function (error) { 7804 return log.error(error); 7805 }); 7806 7807 if (window$1.console && window$1.console.groupEnd) { 7808 window$1.console.groupEnd(); 7809 } 7810 } 7811 7812 parser.flush(); 7813 }; 7814 /** 7815 * Load a `TextTrack` from a specified url. 7816 * 7817 * @param {string} src 7818 * Url to load track from. 7819 * 7820 * @param {TextTrack} track 7821 * Track to add cues to. Comes from the content at the end of `url`. 7822 * 7823 * @private 7824 */ 7825 7826 7827 var loadTrack = function loadTrack(src, track) { 7828 var opts = { 7829 uri: src 7830 }; 7831 var crossOrigin = isCrossOrigin(src); 7832 7833 if (crossOrigin) { 7834 opts.cors = crossOrigin; 7835 } 7836 7837 xhr(opts, bind(this, function (err, response, responseBody) { 7838 if (err) { 7839 return log.error(err, response); 7840 } 7841 7842 track.loaded_ = true; // Make sure that vttjs has loaded, otherwise, wait till it finished loading 7843 // NOTE: this is only used for the alt/video.novtt.js build 7844 7845 if (typeof window$1.WebVTT !== 'function') { 7846 if (track.tech_) { 7847 // to prevent use before define eslint error, we define loadHandler 7848 // as a let here 7849 track.tech_.any(['vttjsloaded', 'vttjserror'], function (event) { 7850 if (event.type === 'vttjserror') { 7851 log.error("vttjs failed to load, stopping trying to process " + track.src); 7852 return; 7853 } 7854 7855 return parseCues(responseBody, track); 7856 }); 7857 } 7858 } else { 7859 parseCues(responseBody, track); 7860 } 7861 })); 7862 }; 7863 /** 7864 * A representation of a single `TextTrack`. 7865 * 7866 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrack} 7867 * @extends Track 7868 */ 7869 7870 7871 var TextTrack = 7872 /*#__PURE__*/ 7873 function (_Track) { 7874 inheritsLoose(TextTrack, _Track); 7875 7876 /** 7877 * Create an instance of this class. 7878 * 7879 * @param {Object} options={} 7880 * Object of option names and values 7881 * 7882 * @param {Tech} options.tech 7883 * A reference to the tech that owns this TextTrack. 7884 * 7885 * @param {TextTrack~Kind} [options.kind='subtitles'] 7886 * A valid text track kind. 7887 * 7888 * @param {TextTrack~Mode} [options.mode='disabled'] 7889 * A valid text track mode. 7890 * 7891 * @param {string} [options.id='vjs_track_' + Guid.newGUID()] 7892 * A unique id for this TextTrack. 7893 * 7894 * @param {string} [options.label=''] 7895 * The menu label for this track. 7896 * 7897 * @param {string} [options.language=''] 7898 * A valid two character language code. 7899 * 7900 * @param {string} [options.srclang=''] 7901 * A valid two character language code. An alternative, but deprioritized 7902 * version of `options.language` 7903 * 7904 * @param {string} [options.src] 7905 * A url to TextTrack cues. 7906 * 7907 * @param {boolean} [options.default] 7908 * If this track should default to on or off. 7909 */ 7910 function TextTrack(options) { 7911 var _this; 7912 7913 if (options === void 0) { 7914 options = {}; 7915 } 7916 7917 if (!options.tech) { 7918 throw new Error('A tech was not provided.'); 7919 } 7920 7921 var settings = mergeOptions(options, { 7922 kind: TextTrackKind[options.kind] || 'subtitles', 7923 language: options.language || options.srclang || '' 7924 }); 7925 var mode = TextTrackMode[settings.mode] || 'disabled'; 7926 var default_ = settings["default"]; 7927 7928 if (settings.kind === 'metadata' || settings.kind === 'chapters') { 7929 mode = 'hidden'; 7930 } 7931 7932 _this = _Track.call(this, settings) || this; 7933 _this.tech_ = settings.tech; 7934 _this.cues_ = []; 7935 _this.activeCues_ = []; 7936 _this.preload_ = _this.tech_.preloadTextTracks !== false; 7937 var cues = new TextTrackCueList(_this.cues_); 7938 var activeCues = new TextTrackCueList(_this.activeCues_); 7939 var changed = false; 7940 var timeupdateHandler = bind(assertThisInitialized(_this), function () { 7941 // Accessing this.activeCues for the side-effects of updating itself 7942 // due to its nature as a getter function. Do not remove or cues will 7943 // stop updating! 7944 // Use the setter to prevent deletion from uglify (pure_getters rule) 7945 this.activeCues = this.activeCues; 7946 7947 if (changed) { 7948 this.trigger('cuechange'); 7949 changed = false; 7950 } 7951 }); 7952 7953 if (mode !== 'disabled') { 7954 _this.tech_.ready(function () { 7955 _this.tech_.on('timeupdate', timeupdateHandler); 7956 }, true); 7957 } 7958 7959 Object.defineProperties(assertThisInitialized(_this), { 7960 /** 7961 * @memberof TextTrack 7962 * @member {boolean} default 7963 * If this track was set to be on or off by default. Cannot be changed after 7964 * creation. 7965 * @instance 7966 * 7967 * @readonly 7968 */ 7969 "default": { 7970 get: function get() { 7971 return default_; 7972 }, 7973 set: function set() {} 7974 }, 7975 7976 /** 7977 * @memberof TextTrack 7978 * @member {string} mode 7979 * Set the mode of this TextTrack to a valid {@link TextTrack~Mode}. Will 7980 * not be set if setting to an invalid mode. 7981 * @instance 7982 * 7983 * @fires TextTrack#modechange 7984 */ 7985 mode: { 7986 get: function get() { 7987 return mode; 7988 }, 7989 set: function set(newMode) { 7990 var _this2 = this; 7991 7992 if (!TextTrackMode[newMode]) { 7993 return; 7994 } 7995 7996 mode = newMode; 7997 7998 if (!this.preload_ && mode !== 'disabled' && this.cues.length === 0) { 7999 // On-demand load. 8000 loadTrack(this.src, this); 8001 } 8002 8003 if (mode !== 'disabled') { 8004 this.tech_.ready(function () { 8005 _this2.tech_.on('timeupdate', timeupdateHandler); 8006 }, true); 8007 } else { 8008 this.tech_.off('timeupdate', timeupdateHandler); 8009 } 8010 /** 8011 * An event that fires when mode changes on this track. This allows 8012 * the TextTrackList that holds this track to act accordingly. 8013 * 8014 * > Note: This is not part of the spec! 8015 * 8016 * @event TextTrack#modechange 8017 * @type {EventTarget~Event} 8018 */ 8019 8020 8021 this.trigger('modechange'); 8022 } 8023 }, 8024 8025 /** 8026 * @memberof TextTrack 8027 * @member {TextTrackCueList} cues 8028 * The text track cue list for this TextTrack. 8029 * @instance 8030 */ 8031 cues: { 8032 get: function get() { 8033 if (!this.loaded_) { 8034 return null; 8035 } 8036 8037 return cues; 8038 }, 8039 set: function set() {} 8040 }, 8041 8042 /** 8043 * @memberof TextTrack 8044 * @member {TextTrackCueList} activeCues 8045 * The list text track cues that are currently active for this TextTrack. 8046 * @instance 8047 */ 8048 activeCues: { 8049 get: function get() { 8050 if (!this.loaded_) { 8051 return null; 8052 } // nothing to do 8053 8054 8055 if (this.cues.length === 0) { 8056 return activeCues; 8057 } 8058 8059 var ct = this.tech_.currentTime(); 8060 var active = []; 8061 8062 for (var i = 0, l = this.cues.length; i < l; i++) { 8063 var cue = this.cues[i]; 8064 8065 if (cue.startTime <= ct && cue.endTime >= ct) { 8066 active.push(cue); 8067 } else if (cue.startTime === cue.endTime && cue.startTime <= ct && cue.startTime + 0.5 >= ct) { 8068 active.push(cue); 8069 } 8070 } 8071 8072 changed = false; 8073 8074 if (active.length !== this.activeCues_.length) { 8075 changed = true; 8076 } else { 8077 for (var _i = 0; _i < active.length; _i++) { 8078 if (this.activeCues_.indexOf(active[_i]) === -1) { 8079 changed = true; 8080 } 8081 } 8082 } 8083 8084 this.activeCues_ = active; 8085 activeCues.setCues_(this.activeCues_); 8086 return activeCues; 8087 }, 8088 // /!\ Keep this setter empty (see the timeupdate handler above) 8089 set: function set() {} 8090 } 8091 }); 8092 8093 if (settings.src) { 8094 _this.src = settings.src; 8095 8096 if (!_this.preload_) { 8097 // Tracks will load on-demand. 8098 // Act like we're loaded for other purposes. 8099 _this.loaded_ = true; 8100 } 8101 8102 if (_this.preload_ || default_ || settings.kind !== 'subtitles' && settings.kind !== 'captions') { 8103 loadTrack(_this.src, assertThisInitialized(_this)); 8104 } 8105 } else { 8106 _this.loaded_ = true; 8107 } 8108 8109 return _this; 8110 } 8111 /** 8112 * Add a cue to the internal list of cues. 8113 * 8114 * @param {TextTrack~Cue} cue 8115 * The cue to add to our internal list 8116 */ 8117 8118 8119 var _proto = TextTrack.prototype; 8120 8121 _proto.addCue = function addCue(originalCue) { 8122 var cue = originalCue; 8123 8124 if (window$1.vttjs && !(originalCue instanceof window$1.vttjs.VTTCue)) { 8125 cue = new window$1.vttjs.VTTCue(originalCue.startTime, originalCue.endTime, originalCue.text); 8126 8127 for (var prop in originalCue) { 8128 if (!(prop in cue)) { 8129 cue[prop] = originalCue[prop]; 8130 } 8131 } // make sure that `id` is copied over 8132 8133 8134 cue.id = originalCue.id; 8135 cue.originalCue_ = originalCue; 8136 } 8137 8138 var tracks = this.tech_.textTracks(); 8139 8140 for (var i = 0; i < tracks.length; i++) { 8141 if (tracks[i] !== this) { 8142 tracks[i].removeCue(cue); 8143 } 8144 } 8145 8146 this.cues_.push(cue); 8147 this.cues.setCues_(this.cues_); 8148 } 8149 /** 8150 * Remove a cue from our internal list 8151 * 8152 * @param {TextTrack~Cue} removeCue 8153 * The cue to remove from our internal list 8154 */ 8155 ; 8156 8157 _proto.removeCue = function removeCue(_removeCue) { 8158 var i = this.cues_.length; 8159 8160 while (i--) { 8161 var cue = this.cues_[i]; 8162 8163 if (cue === _removeCue || cue.originalCue_ && cue.originalCue_ === _removeCue) { 8164 this.cues_.splice(i, 1); 8165 this.cues.setCues_(this.cues_); 8166 break; 8167 } 8168 } 8169 }; 8170 8171 return TextTrack; 8172 }(Track); 8173 /** 8174 * cuechange - One or more cues in the track have become active or stopped being active. 8175 */ 8176 8177 8178 TextTrack.prototype.allowedEvents_ = { 8179 cuechange: 'cuechange' 8180 }; 8181 8182 /** 8183 * A representation of a single `AudioTrack`. If it is part of an {@link AudioTrackList} 8184 * only one `AudioTrack` in the list will be enabled at a time. 8185 * 8186 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotrack} 8187 * @extends Track 8188 */ 8189 8190 var AudioTrack = 8191 /*#__PURE__*/ 8192 function (_Track) { 8193 inheritsLoose(AudioTrack, _Track); 8194 8195 /** 8196 * Create an instance of this class. 8197 * 8198 * @param {Object} [options={}] 8199 * Object of option names and values 8200 * 8201 * @param {AudioTrack~Kind} [options.kind=''] 8202 * A valid audio track kind 8203 * 8204 * @param {string} [options.id='vjs_track_' + Guid.newGUID()] 8205 * A unique id for this AudioTrack. 8206 * 8207 * @param {string} [options.label=''] 8208 * The menu label for this track. 8209 * 8210 * @param {string} [options.language=''] 8211 * A valid two character language code. 8212 * 8213 * @param {boolean} [options.enabled] 8214 * If this track is the one that is currently playing. If this track is part of 8215 * an {@link AudioTrackList}, only one {@link AudioTrack} will be enabled. 8216 */ 8217 function AudioTrack(options) { 8218 var _this; 8219 8220 if (options === void 0) { 8221 options = {}; 8222 } 8223 8224 var settings = mergeOptions(options, { 8225 kind: AudioTrackKind[options.kind] || '' 8226 }); 8227 _this = _Track.call(this, settings) || this; 8228 var enabled = false; 8229 /** 8230 * @memberof AudioTrack 8231 * @member {boolean} enabled 8232 * If this `AudioTrack` is enabled or not. When setting this will 8233 * fire {@link AudioTrack#enabledchange} if the state of enabled is changed. 8234 * @instance 8235 * 8236 * @fires VideoTrack#selectedchange 8237 */ 8238 8239 Object.defineProperty(assertThisInitialized(_this), 'enabled', { 8240 get: function get() { 8241 return enabled; 8242 }, 8243 set: function set(newEnabled) { 8244 // an invalid or unchanged value 8245 if (typeof newEnabled !== 'boolean' || newEnabled === enabled) { 8246 return; 8247 } 8248 8249 enabled = newEnabled; 8250 /** 8251 * An event that fires when enabled changes on this track. This allows 8252 * the AudioTrackList that holds this track to act accordingly. 8253 * 8254 * > Note: This is not part of the spec! Native tracks will do 8255 * this internally without an event. 8256 * 8257 * @event AudioTrack#enabledchange 8258 * @type {EventTarget~Event} 8259 */ 8260 8261 this.trigger('enabledchange'); 8262 } 8263 }); // if the user sets this track to selected then 8264 // set selected to that true value otherwise 8265 // we keep it false 8266 8267 if (settings.enabled) { 8268 _this.enabled = settings.enabled; 8269 } 8270 8271 _this.loaded_ = true; 8272 return _this; 8273 } 8274 8275 return AudioTrack; 8276 }(Track); 8277 8278 /** 8279 * A representation of a single `VideoTrack`. 8280 * 8281 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#videotrack} 8282 * @extends Track 8283 */ 8284 8285 var VideoTrack = 8286 /*#__PURE__*/ 8287 function (_Track) { 8288 inheritsLoose(VideoTrack, _Track); 8289 8290 /** 8291 * Create an instance of this class. 8292 * 8293 * @param {Object} [options={}] 8294 * Object of option names and values 8295 * 8296 * @param {string} [options.kind=''] 8297 * A valid {@link VideoTrack~Kind} 8298 * 8299 * @param {string} [options.id='vjs_track_' + Guid.newGUID()] 8300 * A unique id for this AudioTrack. 8301 * 8302 * @param {string} [options.label=''] 8303 * The menu label for this track. 8304 * 8305 * @param {string} [options.language=''] 8306 * A valid two character language code. 8307 * 8308 * @param {boolean} [options.selected] 8309 * If this track is the one that is currently playing. 8310 */ 8311 function VideoTrack(options) { 8312 var _this; 8313 8314 if (options === void 0) { 8315 options = {}; 8316 } 8317 8318 var settings = mergeOptions(options, { 8319 kind: VideoTrackKind[options.kind] || '' 8320 }); 8321 _this = _Track.call(this, settings) || this; 8322 var selected = false; 8323 /** 8324 * @memberof VideoTrack 8325 * @member {boolean} selected 8326 * If this `VideoTrack` is selected or not. When setting this will 8327 * fire {@link VideoTrack#selectedchange} if the state of selected changed. 8328 * @instance 8329 * 8330 * @fires VideoTrack#selectedchange 8331 */ 8332 8333 Object.defineProperty(assertThisInitialized(_this), 'selected', { 8334 get: function get() { 8335 return selected; 8336 }, 8337 set: function set(newSelected) { 8338 // an invalid or unchanged value 8339 if (typeof newSelected !== 'boolean' || newSelected === selected) { 8340 return; 8341 } 8342 8343 selected = newSelected; 8344 /** 8345 * An event that fires when selected changes on this track. This allows 8346 * the VideoTrackList that holds this track to act accordingly. 8347 * 8348 * > Note: This is not part of the spec! Native tracks will do 8349 * this internally without an event. 8350 * 8351 * @event VideoTrack#selectedchange 8352 * @type {EventTarget~Event} 8353 */ 8354 8355 this.trigger('selectedchange'); 8356 } 8357 }); // if the user sets this track to selected then 8358 // set selected to that true value otherwise 8359 // we keep it false 8360 8361 if (settings.selected) { 8362 _this.selected = settings.selected; 8363 } 8364 8365 return _this; 8366 } 8367 8368 return VideoTrack; 8369 }(Track); 8370 8371 /** 8372 * @memberof HTMLTrackElement 8373 * @typedef {HTMLTrackElement~ReadyState} 8374 * @enum {number} 8375 */ 8376 8377 var NONE = 0; 8378 var LOADING = 1; 8379 var LOADED = 2; 8380 var ERROR = 3; 8381 /** 8382 * A single track represented in the DOM. 8383 * 8384 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#htmltrackelement} 8385 * @extends EventTarget 8386 */ 8387 8388 var HTMLTrackElement = 8389 /*#__PURE__*/ 8390 function (_EventTarget) { 8391 inheritsLoose(HTMLTrackElement, _EventTarget); 8392 8393 /** 8394 * Create an instance of this class. 8395 * 8396 * @param {Object} options={} 8397 * Object of option names and values 8398 * 8399 * @param {Tech} options.tech 8400 * A reference to the tech that owns this HTMLTrackElement. 8401 * 8402 * @param {TextTrack~Kind} [options.kind='subtitles'] 8403 * A valid text track kind. 8404 * 8405 * @param {TextTrack~Mode} [options.mode='disabled'] 8406 * A valid text track mode. 8407 * 8408 * @param {string} [options.id='vjs_track_' + Guid.newGUID()] 8409 * A unique id for this TextTrack. 8410 * 8411 * @param {string} [options.label=''] 8412 * The menu label for this track. 8413 * 8414 * @param {string} [options.language=''] 8415 * A valid two character language code. 8416 * 8417 * @param {string} [options.srclang=''] 8418 * A valid two character language code. An alternative, but deprioritized 8419 * vesion of `options.language` 8420 * 8421 * @param {string} [options.src] 8422 * A url to TextTrack cues. 8423 * 8424 * @param {boolean} [options.default] 8425 * If this track should default to on or off. 8426 */ 8427 function HTMLTrackElement(options) { 8428 var _this; 8429 8430 if (options === void 0) { 8431 options = {}; 8432 } 8433 8434 _this = _EventTarget.call(this) || this; 8435 var readyState; 8436 var track = new TextTrack(options); 8437 _this.kind = track.kind; 8438 _this.src = track.src; 8439 _this.srclang = track.language; 8440 _this.label = track.label; 8441 _this["default"] = track["default"]; 8442 Object.defineProperties(assertThisInitialized(_this), { 8443 /** 8444 * @memberof HTMLTrackElement 8445 * @member {HTMLTrackElement~ReadyState} readyState 8446 * The current ready state of the track element. 8447 * @instance 8448 */ 8449 readyState: { 8450 get: function get() { 8451 return readyState; 8452 } 8453 }, 8454 8455 /** 8456 * @memberof HTMLTrackElement 8457 * @member {TextTrack} track 8458 * The underlying TextTrack object. 8459 * @instance 8460 * 8461 */ 8462 track: { 8463 get: function get() { 8464 return track; 8465 } 8466 } 8467 }); 8468 readyState = NONE; 8469 /** 8470 * @listens TextTrack#loadeddata 8471 * @fires HTMLTrackElement#load 8472 */ 8473 8474 track.addEventListener('loadeddata', function () { 8475 readyState = LOADED; 8476 8477 _this.trigger({ 8478 type: 'load', 8479 target: assertThisInitialized(_this) 8480 }); 8481 }); 8482 return _this; 8483 } 8484 8485 return HTMLTrackElement; 8486 }(EventTarget); 8487 8488 HTMLTrackElement.prototype.allowedEvents_ = { 8489 load: 'load' 8490 }; 8491 HTMLTrackElement.NONE = NONE; 8492 HTMLTrackElement.LOADING = LOADING; 8493 HTMLTrackElement.LOADED = LOADED; 8494 HTMLTrackElement.ERROR = ERROR; 8495 8496 /* 8497 * This file contains all track properties that are used in 8498 * player.js, tech.js, html5.js and possibly other techs in the future. 8499 */ 8500 8501 var NORMAL = { 8502 audio: { 8503 ListClass: AudioTrackList, 8504 TrackClass: AudioTrack, 8505 capitalName: 'Audio' 8506 }, 8507 video: { 8508 ListClass: VideoTrackList, 8509 TrackClass: VideoTrack, 8510 capitalName: 'Video' 8511 }, 8512 text: { 8513 ListClass: TextTrackList, 8514 TrackClass: TextTrack, 8515 capitalName: 'Text' 8516 } 8517 }; 8518 Object.keys(NORMAL).forEach(function (type) { 8519 NORMAL[type].getterName = type + "Tracks"; 8520 NORMAL[type].privateName = type + "Tracks_"; 8521 }); 8522 var REMOTE = { 8523 remoteText: { 8524 ListClass: TextTrackList, 8525 TrackClass: TextTrack, 8526 capitalName: 'RemoteText', 8527 getterName: 'remoteTextTracks', 8528 privateName: 'remoteTextTracks_' 8529 }, 8530 remoteTextEl: { 8531 ListClass: HtmlTrackElementList, 8532 TrackClass: HTMLTrackElement, 8533 capitalName: 'RemoteTextTrackEls', 8534 getterName: 'remoteTextTrackEls', 8535 privateName: 'remoteTextTrackEls_' 8536 } 8537 }; 8538 8539 var ALL = _extends_1({}, NORMAL, REMOTE); 8540 8541 REMOTE.names = Object.keys(REMOTE); 8542 NORMAL.names = Object.keys(NORMAL); 8543 ALL.names = [].concat(REMOTE.names).concat(NORMAL.names); 8544 8545 /** 8546 * Copyright 2013 vtt.js Contributors 8547 * 8548 * Licensed under the Apache License, Version 2.0 (the "License");
8549 * you may not use this file except in compliance with the License. 8550 * You may obtain a copy of the License at 8551 * 8552 * http://www.apache.org/licenses/LICENSE-2.0 8553 * 8554 * Unless required by applicable law or agreed to in writing, software 8555 * distributed under the License is distributed on an "AS IS" BASIS, 8556 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8557 * See the License for the specific language governing permissions and 8558 * limitations under the License. 8559 */ 8560 8561 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 8562 8563 /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ 8564 8565 var _objCreate = Object.create || function () { 8566 function F() {} 8567 8568 return function (o) { 8569 if (arguments.length !== 1) { 8570 throw new Error('Object.create shim only accepts one parameter.'); 8571 } 8572 8573 F.prototype = o; 8574 return new F(); 8575 }; 8576 }(); // Creates a new ParserError object from an errorData object. The errorData 8577 // object should have default code and message properties. The default message 8578 // property can be overriden by passing in a message parameter. 8579 // See ParsingError.Errors below for acceptable errors. 8580 8581 8582 function ParsingError(errorData, message) { 8583 this.name = "ParsingError"; 8584 this.code = errorData.code; 8585 this.message = message || errorData.message; 8586 } 8587 8588 ParsingError.prototype = _objCreate(Error.prototype); 8589 ParsingError.prototype.constructor = ParsingError; // ParsingError metadata for acceptable ParsingErrors. 8590 8591 ParsingError.Errors = { 8592 BadSignature: { 8593 code: 0, 8594 message: "Malformed WebVTT signature." 8595 }, 8596 BadTimeStamp: { 8597 code: 1, 8598 message: "Malformed time stamp." 8599 } 8600 }; // Try to parse input as a time stamp. 8601 8602 function parseTimeStamp(input) { 8603 function computeSeconds(h, m, s, f) { 8604 return (h | 0) * 3600 + (m | 0) * 60 + (s | 0) + (f | 0) / 1000; 8605 } 8606 8607 var m = input.match(/^(\d+):(\d{1,2})(:\d{1,2})?\.(\d{3})/); 8608 8609 if (!m) { 8610 return null; 8611 } 8612 8613 if (m[3]) { 8614 // Timestamp takes the form of [hours]:[minutes]:[seconds].[milliseconds] 8615 return computeSeconds(m[1], m[2], m[3].replace(":", ""), m[4]); 8616 } else if (m[1] > 59) { 8617 // Timestamp takes the form of [hours]:[minutes].[milliseconds] 8618 // First position is hours as it's over 59. 8619 return computeSeconds(m[1], m[2], 0, m[4]); 8620 } else { 8621 // Timestamp takes the form of [minutes]:[seconds].[milliseconds] 8622 return computeSeconds(0, m[1], m[2], m[4]); 8623 } 8624 } // A settings object holds key/value pairs and will ignore anything but the first 8625 // assignment to a specific key. 8626 8627 8628 function Settings() { 8629 this.values = _objCreate(null); 8630 } 8631 8632 Settings.prototype = { 8633 // Only accept the first assignment to any key. 8634 set: function set(k, v) { 8635 if (!this.get(k) && v !== "") { 8636 this.values[k] = v; 8637 } 8638 }, 8639 // Return the value for a key, or a default value. 8640 // If 'defaultKey' is passed then 'dflt' is assumed to be an object with 8641 // a number of possible default values as properties where 'defaultKey' is 8642 // the key of the property that will be chosen; otherwise it's assumed to be 8643 // a single value. 8644 get: function get(k, dflt, defaultKey) { 8645 if (defaultKey) { 8646 return this.has(k) ? this.values[k] : dflt[defaultKey]; 8647 } 8648 8649 return this.has(k) ? this.values[k] : dflt; 8650 }, 8651 // Check whether we have a value for a key. 8652 has: function has(k) { 8653 return k in this.values; 8654 }, 8655 // Accept a setting if its one of the given alternatives. 8656 alt: function alt(k, v, a) { 8657 for (var n = 0; n < a.length; ++n) { 8658 if (v === a[n]) { 8659 this.set(k, v); 8660 break; 8661 } 8662 } 8663 }, 8664 // Accept a setting if its a valid (signed) integer. 8665 integer: function integer(k, v) { 8666 if (/^-?\d+$/.test(v)) { 8667 // integer 8668 this.set(k, parseInt(v, 10)); 8669 } 8670 }, 8671 // Accept a setting if its a valid percentage. 8672 percent: function percent(k, v) { 8673 var m; 8674 8675 if (m = v.match(/^([\d]{1,3})(\.[\d]*)?%$/)) { 8676 v = parseFloat(v); 8677 8678 if (v >= 0 && v <= 100) { 8679 this.set(k, v); 8680 return true; 8681 } 8682 } 8683 8684 return false;
vendor: 5,650 bytes, lines 8685-8886
8685 } 8686 }; // Helper function to parse input into groups separated by 'groupDelim', and 8687 // interprete each group as a key/value pair separated by 'keyValueDelim'. 8688 8689 function parseOptions(input, callback, keyValueDelim, groupDelim) { 8690 var groups = groupDelim ? input.split(groupDelim) : [input]; 8691 8692 for (var i in groups) { 8693 if (typeof groups[i] !== "string") { 8694 continue; 8695 } 8696 8697 var kv = groups[i].split(keyValueDelim); 8698 8699 if (kv.length !== 2) { 8700 continue; 8701 } 8702 8703 var k = kv[0]; 8704 var v = kv[1]; 8705 callback(k, v); 8706 } 8707 } 8708 8709 function parseCue(input, cue, regionList) { 8710 // Remember the original input if we need to throw an error. 8711 var oInput = input; // 4.1 WebVTT timestamp 8712 8713 function consumeTimeStamp() { 8714 var ts = parseTimeStamp(input); 8715 8716 if (ts === null) { 8717 throw new ParsingError(ParsingError.Errors.BadTimeStamp, "Malformed timestamp: " + oInput); 8718 } // Remove time stamp from input. 8719 8720 8721 input = input.replace(/^[^\sa-zA-Z-]+/, ""); 8722 return ts; 8723 } // 4.4.2 WebVTT cue settings 8724 8725 8726 function consumeCueSettings(input, cue) { 8727 var settings = new Settings(); 8728 parseOptions(input, function (k, v) { 8729 switch (k) { 8730 case "region": 8731 // Find the last region we parsed with the same region id. 8732 for (var i = regionList.length - 1; i >= 0; i--) { 8733 if (regionList[i].id === v) { 8734 settings.set(k, regionList[i].region); 8735 break; 8736 } 8737 } 8738 8739 break; 8740 8741 case "vertical": 8742 settings.alt(k, v, ["rl", "lr"]); 8743 break; 8744 8745 case "line": 8746 var vals = v.split(","), 8747 vals0 = vals[0]; 8748 settings.integer(k, vals0); 8749 settings.percent(k, vals0) ? settings.set("snapToLines", false) : null; 8750 settings.alt(k, vals0, ["auto"]); 8751 8752 if (vals.length === 2) { 8753 settings.alt("lineAlign", vals[1], ["start", "center", "end"]); 8754 } 8755 8756 break; 8757 8758 case "position": 8759 vals = v.split(","); 8760 settings.percent(k, vals[0]); 8761 8762 if (vals.length === 2) { 8763 settings.alt("positionAlign", vals[1], ["start", "center", "end"]); 8764 } 8765 8766 break; 8767 8768 case "size": 8769 settings.percent(k, v); 8770 break; 8771 8772 case "align": 8773 settings.alt(k, v, ["start", "center", "end", "left", "right"]); 8774 break; 8775 } 8776 }, /:/, /\s/); // Apply default values for any missing fields. 8777 8778 cue.region = settings.get("region", null); 8779 cue.vertical = settings.get("vertical", ""); 8780 8781 try { 8782 cue.line = settings.get("line", "auto"); 8783 } catch (e) {} 8784 8785 cue.lineAlign = settings.get("lineAlign", "start"); 8786 cue.snapToLines = settings.get("snapToLines", true); 8787 cue.size = settings.get("size", 100); // Safari still uses the old middle value and won't accept center 8788 8789 try { 8790 cue.align = settings.get("align", "center"); 8791 } catch (e) { 8792 cue.align = settings.get("align", "middle"); 8793 } 8794 8795 try { 8796 cue.position = settings.get("position", "auto"); 8797 } catch (e) { 8798 cue.position = settings.get("position", { 8799 start: 0, 8800 left: 0, 8801 center: 50, 8802 middle: 50, 8803 end: 100, 8804 right: 100 8805 }, cue.align); 8806 } 8807 8808 cue.positionAlign = settings.get("positionAlign", { 8809 start: "start", 8810 left: "start", 8811 center: "center", 8812 middle: "center", 8813 end: "end", 8814 right: "end" 8815 }, cue.align); 8816 } 8817 8818 function skipWhitespace() { 8819 input = input.replace(/^\s+/, ""); 8820 } // 4.1 WebVTT cue timings. 8821 8822 8823 skipWhitespace(); 8824 cue.startTime = consumeTimeStamp(); // (1) collect cue start time 8825 8826 skipWhitespace(); 8827 8828 if (input.substr(0, 3) !== "-->") { 8829 // (3) next characters must match "-->" 8830 throw new ParsingError(ParsingError.Errors.BadTimeStamp, "Malformed time stamp (time stamps must be separated by '-->'): " + oInput); 8831 } 8832 8833 input = input.substr(3); 8834 skipWhitespace(); 8835 cue.endTime = consumeTimeStamp(); // (5) collect cue end time 8836 // 4.1 WebVTT cue settings list. 8837 8838 skipWhitespace(); 8839 consumeCueSettings(input, cue); 8840 } 8841 8842 var TEXTAREA_ELEMENT = document.createElement("textarea"); 8843 var TAG_NAME = { 8844 c: "span", 8845 i: "i", 8846 b: "b", 8847 u: "u", 8848 ruby: "ruby", 8849 rt: "rt", 8850 v: "span", 8851 lang: "span" 8852 }; // 5.1 default text color 8853 // 5.2 default text background color is equivalent to text color with bg_ prefix 8854 8855 var DEFAULT_COLOR_CLASS = { 8856 white: 'rgba(255,255,255,1)', 8857 lime: 'rgba(0,255,0,1)', 8858 cyan: 'rgba(0,255,255,1)', 8859 red: 'rgba(255,0,0,1)', 8860 yellow: 'rgba(255,255,0,1)', 8861 magenta: 'rgba(255,0,255,1)', 8862 blue: 'rgba(0,0,255,1)', 8863 black: 'rgba(0,0,0,1)' 8864 }; 8865 var TAG_ANNOTATION = { 8866 v: "title", 8867 lang: "lang" 8868 }; 8869 var NEEDS_PARENT = { 8870 rt: "ruby" 8871 }; // Parse content into a document fragment. 8872 8873 function parseContent(window, input) { 8874 function nextToken() { 8875 // Check for end-of-string. 8876 if (!input) { 8877 return null; 8878 } // Consume 'n' characters from the input. 8879 8880 8881 function consume(result) { 8882 input = input.substr(result.length); 8883 return result; 8884 } 8885 8886 var m = input.match(/^([^<]*)(<[^>
vendor: 23,858 bytes, lines 8886-9546
8886]*>?)?/); // If there is some text before the next tag, return it, otherwise return 8887 // the tag. 8888 8889 return consume(m[1] ? m[1] : m[2]); 8890 } 8891 8892 function unescape(s) { 8893 TEXTAREA_ELEMENT.innerHTML = s; 8894 s = TEXTAREA_ELEMENT.textContent; 8895 TEXTAREA_ELEMENT.textContent = ""; 8896 return s; 8897 } 8898 8899 function shouldAdd(current, element) { 8900 return !NEEDS_PARENT[element.localName] || NEEDS_PARENT[element.localName] === current.localName; 8901 } // Create an element for this tag. 8902 8903 8904 function createElement(type, annotation) { 8905 var tagName = TAG_NAME[type]; 8906 8907 if (!tagName) { 8908 return null; 8909 } 8910 8911 var element = window.document.createElement(tagName); 8912 var name = TAG_ANNOTATION[type]; 8913 8914 if (name && annotation) { 8915 element[name] = annotation.trim(); 8916 } 8917 8918 return element; 8919 } 8920 8921 var rootDiv = window.document.createElement("div"), 8922 current = rootDiv, 8923 t, 8924 tagStack = []; 8925 8926 while ((t = nextToken()) !== null) { 8927 if (t[0] === '<') { 8928 if (t[1] === "/") { 8929 // If the closing tag matches, move back up to the parent node. 8930 if (tagStack.length && tagStack[tagStack.length - 1] === t.substr(2).replace(">", "")) { 8931 tagStack.pop(); 8932 current = current.parentNode; 8933 } // Otherwise just ignore the end tag. 8934 8935 8936 continue; 8937 } 8938 8939 var ts = parseTimeStamp(t.substr(1, t.length - 2)); 8940 var node; 8941 8942 if (ts) { 8943 // Timestamps are lead nodes as well. 8944 node = window.document.createProcessingInstruction("timestamp", ts); 8945 current.appendChild(node); 8946 continue; 8947 } 8948 8949 var m = t.match(/^<([^.\s/0-9>]+)(\.[^\s\\>]+)?([^>\\]+)?(\\?)>?$/); // If we can't parse the tag, skip to the next tag. 8950 8951 if (!m) { 8952 continue; 8953 } // Try to construct an element, and ignore the tag if we couldn't. 8954 8955 8956 node = createElement(m[1], m[3]); 8957 8958 if (!node) { 8959 continue; 8960 } // Determine if the tag should be added based on the context of where it 8961 // is placed in the cuetext. 8962 8963 8964 if (!shouldAdd(current, node)) { 8965 continue; 8966 } // Set the class list (as a list of classes, separated by space). 8967 8968 8969 if (m[2]) { 8970 var classes = m[2].split('.'); 8971 classes.forEach(function (cl) { 8972 var bgColor = /^bg_/.test(cl); // slice out `bg_` if it's a background color 8973 8974 var colorName = bgColor ? cl.slice(3) : cl; 8975 8976 if (DEFAULT_COLOR_CLASS.hasOwnProperty(colorName)) { 8977 var propName = bgColor ? 'background-color' : 'color'; 8978 var propValue = DEFAULT_COLOR_CLASS[colorName]; 8979 node.style[propName] = propValue; 8980 } 8981 }); 8982 node.className = classes.join(' '); 8983 } // Append the node to the current node, and enter the scope of the new 8984 // node. 8985 8986 8987 tagStack.push(m[1]); 8988 current.appendChild(node); 8989 current = node; 8990 continue; 8991 } // Text nodes are leaf nodes. 8992 8993 8994 current.appendChild(window.document.createTextNode(unescape(t))); 8995 } 8996 8997 return rootDiv; 8998 } // This is a list of all the Unicode characters that have a strong 8999 // right-to-left category. What this means is that these characters are 9000 // written right-to-left for sure. It was generated by pulling all the strong 9001 // right-to-left characters out of the Unicode data table. That table can 9002 // found at: http://www.unicode.org/Public/UNIDATA/UnicodeData.txt 9003 9004 9005 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]]; 9006 9007 function isStrongRTLChar(charCode) { 9008 for (var i = 0; i < strongRTLRanges.length; i++) { 9009 var currentRange = strongRTLRanges[i]; 9010 9011 if (charCode >= currentRange[0] && charCode <= currentRange[1]) { 9012 return true; 9013 } 9014 } 9015 9016 return false; 9017 } 9018 9019 function determineBidi(cueDiv) { 9020 var nodeStack = [], 9021 text = "", 9022 charCode; 9023 9024 if (!cueDiv || !cueDiv.childNodes) { 9025 return "ltr"; 9026 } 9027 9028 function pushNodes(nodeStack, node) { 9029 for (var i = node.childNodes.length - 1; i >= 0; i--) { 9030 nodeStack.push(node.childNodes[i]); 9031 } 9032 } 9033 9034 function nextTextNode(nodeStack) { 9035 if (!nodeStack || !nodeStack.length) { 9036 return null; 9037 } 9038 9039 var node = nodeStack.pop(), 9040 text = node.textContent || node.innerText; 9041 9042 if (text) { 9043 // TODO: This should match all unicode type B characters (paragraph 9044 // separator characters). See issue #115. 9045 var m = text.match(/^.*(\n|\r)/); 9046 9047 if (m) { 9048 nodeStack.length = 0; 9049 return m[0]; 9050 } 9051 9052 return text; 9053 } 9054 9055 if (node.tagName === "ruby") { 9056 return nextTextNode(nodeStack); 9057 } 9058 9059 if (node.childNodes) { 9060 pushNodes(nodeStack, node); 9061 return nextTextNode(nodeStack); 9062 } 9063 } 9064 9065 pushNodes(nodeStack, cueDiv); 9066 9067 while (text = nextTextNode(nodeStack)) { 9068 for (var i = 0; i < text.length; i++) { 9069 charCode = text.charCodeAt(i); 9070 9071 if (isStrongRTLChar(charCode)) { 9072 return "rtl"; 9073 } 9074 } 9075 } 9076 9077 return "ltr"; 9078 } 9079 9080 function computeLinePos(cue) { 9081 if (typeof cue.line === "number" && (cue.snapToLines || cue.line >= 0 && cue.line <= 100)) { 9082 return cue.line; 9083 } 9084 9085 if (!cue.track || !cue.track.textTrackList || !cue.track.textTrackList.mediaElement) { 9086 return -1; 9087 } 9088 9089 var track = cue.track, 9090 trackList = track.textTrackList, 9091 count = 0; 9092 9093 for (var i = 0; i < trackList.length && trackList[i] !== track; i++) { 9094 if (trackList[i].mode === "showing") { 9095 count++; 9096 } 9097 } 9098 9099 return ++count * -1; 9100 } 9101 9102 function StyleBox() {} // Apply styles to a div. If there is no div passed then it defaults to the 9103 // div on 'this'. 9104 9105 9106 StyleBox.prototype.applyStyles = function (styles, div) { 9107 div = div || this.div; 9108 9109 for (var prop in styles) { 9110 if (styles.hasOwnProperty(prop)) { 9111 div.style[prop] = styles[prop]; 9112 } 9113 } 9114 }; 9115 9116 StyleBox.prototype.formatStyle = function (val, unit) { 9117 return val === 0 ? 0 : val + unit; 9118 }; // Constructs the computed display state of the cue (a div). Places the div 9119 // into the overlay which should be a block level element (usually a div). 9120 9121 9122 function CueStyleBox(window, cue, styleOptions) { 9123 StyleBox.call(this); 9124 this.cue = cue; // Parse our cue's text into a DOM tree rooted at 'cueDiv'. This div will 9125 // have inline positioning and will function as the cue background box. 9126 9127 this.cueDiv = parseContent(window, cue.text); 9128 var styles = { 9129 color: "rgba(255, 255, 255, 1)", 9130 backgroundColor: "rgba(0, 0, 0, 0.8)", 9131 position: "relative", 9132 left: 0, 9133 right: 0, 9134 top: 0, 9135 bottom: 0, 9136 display: "inline", 9137 writingMode: cue.vertical === "" ? "horizontal-tb" : cue.vertical === "lr" ? "vertical-lr" : "vertical-rl", 9138 unicodeBidi: "plaintext" 9139 }; 9140 this.applyStyles(styles, this.cueDiv); // Create an absolutely positioned div that will be used to position the cue 9141 // div. Note, all WebVTT cue-setting alignments are equivalent to the CSS 9142 // mirrors of them except middle instead of center on Safari. 9143 9144 this.div = window.document.createElement("div"); 9145 styles = { 9146 direction: determineBidi(this.cueDiv), 9147 writingMode: cue.vertical === "" ? "horizontal-tb" : cue.vertical === "lr" ? "vertical-lr" : "vertical-rl", 9148 unicodeBidi: "plaintext", 9149 textAlign: cue.align === "middle" ? "center" : cue.align, 9150 font: styleOptions.font, 9151 whiteSpace: "pre-line", 9152 position: "absolute" 9153 }; 9154 this.applyStyles(styles); 9155 this.div.appendChild(this.cueDiv); // Calculate the distance from the reference edge of the viewport to the text 9156 // position of the cue box. The reference edge will be resolved later when 9157 // the box orientation styles are applied. 9158 9159 var textPos = 0; 9160 9161 switch (cue.positionAlign) { 9162 case "start": 9163 textPos = cue.position; 9164 break; 9165 9166 case "center": 9167 textPos = cue.position - cue.size / 2; 9168 break; 9169 9170 case "end": 9171 textPos = cue.position - cue.size; 9172 break; 9173 } // Horizontal box orientation; textPos is the distance from the left edge of the 9174 // area to the left edge of the box and cue.size is the distance extending to 9175 // the right from there. 9176 9177 9178 if (cue.vertical === "") { 9179 this.applyStyles({ 9180 left: this.formatStyle(textPos, "%"), 9181 width: this.formatStyle(cue.size, "%") 9182 }); // Vertical box orientation; textPos is the distance from the top edge of the 9183 // area to the top edge of the box and cue.size is the height extending 9184 // downwards from there. 9185 } else { 9186 this.applyStyles({ 9187 top: this.formatStyle(textPos, "%"), 9188 height: this.formatStyle(cue.size, "%") 9189 }); 9190 } 9191 9192 this.move = function (box) { 9193 this.applyStyles({ 9194 top: this.formatStyle(box.top, "px"), 9195 bottom: this.formatStyle(box.bottom, "px"), 9196 left: this.formatStyle(box.left, "px"), 9197 right: this.formatStyle(box.right, "px"), 9198 height: this.formatStyle(box.height, "px"), 9199 width: this.formatStyle(box.width, "px") 9200 }); 9201 }; 9202 } 9203 9204 CueStyleBox.prototype = _objCreate(StyleBox.prototype); 9205 CueStyleBox.prototype.constructor = CueStyleBox; // Represents the co-ordinates of an Element in a way that we can easily 9206 // compute things with such as if it overlaps or intersects with another Element. 9207 // Can initialize it with either a StyleBox or another BoxPosition. 9208 9209 function BoxPosition(obj) { 9210 // Either a BoxPosition was passed in and we need to copy it, or a StyleBox 9211 // was passed in and we need to copy the results of 'getBoundingClientRect' 9212 // as the object returned is readonly. All co-ordinate values are in reference 9213 // to the viewport origin (top left). 9214 var lh, height, width, top; 9215 9216 if (obj.div) { 9217 height = obj.div.offsetHeight; 9218 width = obj.div.offsetWidth; 9219 top = obj.div.offsetTop; 9220 var rects = (rects = obj.div.childNodes) && (rects = rects[0]) && rects.getClientRects && rects.getClientRects(); 9221 obj = obj.div.getBoundingClientRect(); // In certain cases the outter div will be slightly larger then the sum of 9222 // the inner div's lines. This could be due to bold text, etc, on some platforms. 9223 // In this case we should get the average line height and use that. This will 9224 // result in the desired behaviour. 9225 9226 lh = rects ? Math.max(rects[0] && rects[0].height || 0, obj.height / rects.length) : 0; 9227 } 9228 9229 this.left = obj.left; 9230 this.right = obj.right; 9231 this.top = obj.top || top; 9232 this.height = obj.height || height; 9233 this.bottom = obj.bottom || top + (obj.height || height); 9234 this.width = obj.width || width; 9235 this.lineHeight = lh !== undefined ? lh : obj.lineHeight; 9236 } // Move the box along a particular axis. Optionally pass in an amount to move 9237 // the box. If no amount is passed then the default is the line height of the 9238 // box. 9239 9240 9241 BoxPosition.prototype.move = function (axis, toMove) { 9242 toMove = toMove !== undefined ? toMove : this.lineHeight; 9243 9244 switch (axis) { 9245 case "+x": 9246 this.left += toMove; 9247 this.right += toMove; 9248 break; 9249 9250 case "-x": 9251 this.left -= toMove; 9252 this.right -= toMove; 9253 break; 9254 9255 case "+y": 9256 this.top += toMove; 9257 this.bottom += toMove; 9258 break; 9259 9260 case "-y": 9261 this.top -= toMove; 9262 this.bottom -= toMove; 9263 break; 9264 } 9265 }; // Check if this box overlaps another box, b2. 9266 9267 9268 BoxPosition.prototype.overlaps = function (b2) { 9269 return this.left < b2.right && this.right > b2.left && this.top < b2.bottom && this.bottom > b2.top; 9270 }; // Check if this box overlaps any other boxes in boxes. 9271 9272 9273 BoxPosition.prototype.overlapsAny = function (boxes) { 9274 for (var i = 0; i < boxes.length; i++) { 9275 if (this.overlaps(boxes[i])) { 9276 return true; 9277 } 9278 } 9279 9280 return false; 9281 }; // Check if this box is within another box. 9282 9283 9284 BoxPosition.prototype.within = function (container) { 9285 return this.top >= container.top && this.bottom <= container.bottom && this.left >= container.left && this.right <= container.right; 9286 }; // Check if this box is entirely within the container or it is overlapping 9287 // on the edge opposite of the axis direction passed. For example, if "+x" is 9288 // passed and the box is overlapping on the left edge of the container, then 9289 // return true. 9290 9291 9292 BoxPosition.prototype.overlapsOppositeAxis = function (container, axis) { 9293 switch (axis) { 9294 case "+x": 9295 return this.left < container.left; 9296 9297 case "-x": 9298 return this.right > container.right; 9299 9300 case "+y": 9301 return this.top < container.top; 9302 9303 case "-y": 9304 return this.bottom > container.bottom; 9305 } 9306 }; // Find the percentage of the area that this box is overlapping with another 9307 // box. 9308 9309 9310 BoxPosition.prototype.intersectPercentage = function (b2) { 9311 var x = Math.max(0, Math.min(this.right, b2.right) - Math.max(this.left, b2.left)), 9312 y = Math.max(0, Math.min(this.bottom, b2.bottom) - Math.max(this.top, b2.top)), 9313 intersectArea = x * y; 9314 return intersectArea / (this.height * this.width); 9315 }; // Convert the positions from this box to CSS compatible positions using 9316 // the reference container's positions. This has to be done because this 9317 // box's positions are in reference to the viewport origin, whereas, CSS 9318 // values are in referecne to their respective edges. 9319 9320 9321 BoxPosition.prototype.toCSSCompatValues = function (reference) { 9322 return { 9323 top: this.top - reference.top, 9324 bottom: reference.bottom - this.bottom, 9325 left: this.left - reference.left, 9326 right: reference.right - this.right, 9327 height: this.height, 9328 width: this.width 9329 }; 9330 }; // Get an object that represents the box's position without anything extra. 9331 // Can pass a StyleBox, HTMLElement, or another BoxPositon. 9332 9333 9334 BoxPosition.getSimpleBoxPosition = function (obj) { 9335 var height = obj.div ? obj.div.offsetHeight : obj.tagName ? obj.offsetHeight : 0; 9336 var width = obj.div ? obj.div.offsetWidth : obj.tagName ? obj.offsetWidth : 0; 9337 var top = obj.div ? obj.div.offsetTop : obj.tagName ? obj.offsetTop : 0; 9338 obj = obj.div ? obj.div.getBoundingClientRect() : obj.tagName ? obj.getBoundingClientRect() : obj; 9339 var ret = { 9340 left: obj.left, 9341 right: obj.right, 9342 top: obj.top || top, 9343 height: obj.height || height, 9344 bottom: obj.bottom || top + (obj.height || height), 9345 width: obj.width || width 9346 }; 9347 return ret; 9348 }; // Move a StyleBox to its specified, or next best, position. The containerBox 9349 // is the box that contains the StyleBox, such as a div. boxPositions are 9350 // a list of other boxes that the styleBox can't overlap with. 9351 9352 9353 function moveBoxToLinePosition(window, styleBox, containerBox, boxPositions) { 9354 // Find the best position for a cue box, b, on the video. The axis parameter 9355 // is a list of axis, the order of which, it will move the box along. For example: 9356 // Passing ["+x", "-x"] will move the box first along the x axis in the positive 9357 // direction. If it doesn't find a good position for it there it will then move 9358 // it along the x axis in the negative direction. 9359 function findBestPosition(b, axis) { 9360 var bestPosition, 9361 specifiedPosition = new BoxPosition(b), 9362 percentage = 1; // Highest possible so the first thing we get is better. 9363 9364 for (var i = 0; i < axis.length; i++) { 9365 while (b.overlapsOppositeAxis(containerBox, axis[i]) || b.within(containerBox) && b.overlapsAny(boxPositions)) { 9366 b.move(axis[i]); 9367 } // We found a spot where we aren't overlapping anything. This is our 9368 // best position. 9369 9370 9371 if (b.within(containerBox)) { 9372 return b; 9373 } 9374 9375 var p = b.intersectPercentage(containerBox); // If we're outside the container box less then we were on our last try 9376 // then remember this position as the best position. 9377 9378 if (percentage > p) { 9379 bestPosition = new BoxPosition(b); 9380 percentage = p; 9381 } // Reset the box position to the specified position. 9382 9383 9384 b = new BoxPosition(specifiedPosition); 9385 } 9386 9387 return bestPosition || specifiedPosition; 9388 } 9389 9390 var boxPosition = new BoxPosition(styleBox), 9391 cue = styleBox.cue, 9392 linePos = computeLinePos(cue), 9393 axis = []; // If we have a line number to align the cue to. 9394 9395 if (cue.snapToLines) { 9396 var size; 9397 9398 switch (cue.vertical) { 9399 case "": 9400 axis = ["+y", "-y"]; 9401 size = "height"; 9402 break; 9403 9404 case "rl": 9405 axis = ["+x", "-x"]; 9406 size = "width"; 9407 break; 9408 9409 case "lr": 9410 axis = ["-x", "+x"]; 9411 size = "width"; 9412 break; 9413 } 9414 9415 var step = boxPosition.lineHeight, 9416 position = step * Math.round(linePos), 9417 maxPosition = containerBox[size] + step, 9418 initialAxis = axis[0]; // If the specified intial position is greater then the max position then 9419 // clamp the box to the amount of steps it would take for the box to 9420 // reach the max position. 9421 9422 if (Math.abs(position) > maxPosition) { 9423 position = position < 0 ? -1 : 1; 9424 position *= Math.ceil(maxPosition / step) * step; 9425 } // If computed line position returns negative then line numbers are 9426 // relative to the bottom of the video instead of the top. Therefore, we 9427 // need to increase our initial position by the length or width of the 9428 // video, depending on the writing direction, and reverse our axis directions. 9429 9430 9431 if (linePos < 0) { 9432 position += cue.vertical === "" ? containerBox.height : containerBox.width; 9433 axis = axis.reverse(); 9434 } // Move the box to the specified position. This may not be its best 9435 // position. 9436 9437 9438 boxPosition.move(initialAxis, position); 9439 } else { 9440 // If we have a percentage line value for the cue. 9441 var calculatedPercentage = boxPosition.lineHeight / containerBox.height * 100; 9442 9443 switch (cue.lineAlign) { 9444 case "center": 9445 linePos -= calculatedPercentage / 2; 9446 break; 9447 9448 case "end": 9449 linePos -= calculatedPercentage; 9450 break; 9451 } // Apply initial line position to the cue box. 9452 9453 9454 switch (cue.vertical) { 9455 case "": 9456 styleBox.applyStyles({ 9457 top: styleBox.formatStyle(linePos, "%") 9458 }); 9459 break; 9460 9461 case "rl": 9462 styleBox.applyStyles({ 9463 left: styleBox.formatStyle(linePos, "%") 9464 }); 9465 break; 9466 9467 case "lr": 9468 styleBox.applyStyles({ 9469 right: styleBox.formatStyle(linePos, "%") 9470 }); 9471 break; 9472 } 9473 9474 axis = ["+y", "-x", "+x", "-y"]; // Get the box position again after we've applied the specified positioning 9475 // to it. 9476 9477 boxPosition = new BoxPosition(styleBox); 9478 } 9479 9480 var bestPosition = findBestPosition(boxPosition, axis); 9481 styleBox.move(bestPosition.toCSSCompatValues(containerBox)); 9482 } 9483 9484 function WebVTT$1() {} // Nothing 9485 // Helper to allow strings to be decoded instead of the default binary utf8 data. 9486 9487 9488 WebVTT$1.StringDecoder = function () { 9489 return { 9490 decode: function decode(data) { 9491 if (!data) { 9492 return ""; 9493 } 9494 9495 if (typeof data !== "string") { 9496 throw new Error("Error - expected string data."); 9497 } 9498 9499 return decodeURIComponent(encodeURIComponent(data)); 9500 } 9501 }; 9502 }; 9503 9504 WebVTT$1.convertCueToDOMTree = function (window, cuetext) { 9505 if (!window || !cuetext) { 9506 return null; 9507 } 9508 9509 return parseContent(window, cuetext); 9510 }; 9511 9512 var FONT_SIZE_PERCENT = 0.05; 9513 var FONT_STYLE = "sans-serif"; 9514 var CUE_BACKGROUND_PADDING = "1.5%"; // Runs the processing model over the cues and regions passed to it. 9515 // @param overlay A block level element (usually a div) that the computed cues 9516 // and regions will be placed into. 9517 9518 WebVTT$1.processCues = function (window, cues, overlay) { 9519 if (!window || !cues || !overlay) { 9520 return null; 9521 } // Remove all previous children. 9522 9523 9524 while (overlay.firstChild) { 9525 overlay.removeChild(overlay.firstChild); 9526 } 9527 9528 var paddedOverlay = window.document.createElement("div"); 9529 paddedOverlay.style.position = "absolute"; 9530 paddedOverlay.style.left = "0"; 9531 paddedOverlay.style.right = "0"; 9532 paddedOverlay.style.top = "0"; 9533 paddedOverlay.style.bottom = "0"; 9534 paddedOverlay.style.margin = CUE_BACKGROUND_PADDING; 9535 overlay.appendChild(paddedOverlay); // Determine if we need to compute the display states of the cues. This could 9536 // be the case if a cue's state has been changed since the last computation or 9537 // if it has not been computed yet. 9538 9539 function shouldCompute(cues) { 9540 for (var i = 0; i < cues.length; i++) { 9541 if (cues[i].hasBeenReset || !cues[i].displayState) { 9542 return true; 9543 } 9544 } 9545 9546 return false;
vendor: 7,094 bytes, lines 9547-9776
9547 } // We don't need to recompute the cues' display states. Just reuse them. 9548 9549 9550 if (!shouldCompute(cues)) { 9551 for (var i = 0; i < cues.length; i++) { 9552 paddedOverlay.appendChild(cues[i].displayState); 9553 } 9554 9555 return; 9556 } 9557 9558 var boxPositions = [], 9559 containerBox = BoxPosition.getSimpleBoxPosition(paddedOverlay), 9560 fontSize = Math.round(containerBox.height * FONT_SIZE_PERCENT * 100) / 100; 9561 var styleOptions = { 9562 font: fontSize + "px " + FONT_STYLE 9563 }; 9564 9565 (function () { 9566 var styleBox, cue; 9567 9568 for (var i = 0; i < cues.length; i++) { 9569 cue = cues[i]; // Compute the intial position and styles of the cue div. 9570 9571 styleBox = new CueStyleBox(window, cue, styleOptions); 9572 paddedOverlay.appendChild(styleBox.div); // Move the cue div to it's correct line position. 9573 9574 moveBoxToLinePosition(window, styleBox, containerBox, boxPositions); // Remember the computed div so that we don't have to recompute it later 9575 // if we don't have too. 9576 9577 cue.displayState = styleBox.div; 9578 boxPositions.push(BoxPosition.getSimpleBoxPosition(styleBox)); 9579 } 9580 })(); 9581 }; 9582 9583 WebVTT$1.Parser = function (window, vttjs, decoder) { 9584 if (!decoder) { 9585 decoder = vttjs; 9586 vttjs = {}; 9587 } 9588 9589 if (!vttjs) { 9590 vttjs = {}; 9591 } 9592 9593 this.window = window; 9594 this.vttjs = vttjs; 9595 this.state = "INITIAL"; 9596 this.buffer = ""; 9597 this.decoder = decoder || new TextDecoder("utf8"); 9598 this.regionList = []; 9599 }; 9600 9601 WebVTT$1.Parser.prototype = { 9602 // If the error is a ParsingError then report it to the consumer if 9603 // possible. If it's not a ParsingError then throw it like normal. 9604 reportOrThrowError: function reportOrThrowError(e) { 9605 if (e instanceof ParsingError) { 9606 this.onparsingerror && this.onparsingerror(e); 9607 } else { 9608 throw e; 9609 } 9610 }, 9611 parse: function parse(data) { 9612 var self = this; // If there is no data then we won't decode it, but will just try to parse 9613 // whatever is in buffer already. This may occur in circumstances, for 9614 // example when flush() is called. 9615 9616 if (data) { 9617 // Try to decode the data that we received. 9618 self.buffer += self.decoder.decode(data, { 9619 stream: true 9620 }); 9621 } 9622 9623 function collectNextLine() { 9624 var buffer = self.buffer; 9625 var pos = 0; 9626 9627 while (pos < buffer.length && buffer[pos] !== '\r' && buffer[pos] !== '\n') { 9628 ++pos; 9629 } 9630 9631 var line = buffer.substr(0, pos); // Advance the buffer early in case we fail below. 9632 9633 if (buffer[pos] === '\r') { 9634 ++pos; 9635 } 9636 9637 if (buffer[pos] === '\n') { 9638 ++pos; 9639 } 9640 9641 self.buffer = buffer.substr(pos); 9642 return line; 9643 } // 3.4 WebVTT region and WebVTT region settings syntax 9644 9645 9646 function parseRegion(input) { 9647 var settings = new Settings(); 9648 parseOptions(input, function (k, v) { 9649 switch (k) { 9650 case "id": 9651 settings.set(k, v); 9652 break; 9653 9654 case "width": 9655 settings.percent(k, v); 9656 break; 9657 9658 case "lines": 9659 settings.integer(k, v); 9660 break; 9661 9662 case "regionanchor": 9663 case "viewportanchor": 9664 var xy = v.split(','); 9665 9666 if (xy.length !== 2) { 9667 break; 9668 } // We have to make sure both x and y parse, so use a temporary 9669 // settings object here. 9670 9671 9672 var anchor = new Settings(); 9673 anchor.percent("x", xy[0]); 9674 anchor.percent("y", xy[1]); 9675 9676 if (!anchor.has("x") || !anchor.has("y")) { 9677 break; 9678 } 9679 9680 settings.set(k + "X", anchor.get("x")); 9681 settings.set(k + "Y", anchor.get("y")); 9682 break; 9683 9684 case "scroll": 9685 settings.alt(k, v, ["up"]); 9686 break; 9687 } 9688 }, /=/, /\s/); // Create the region, using default values for any values that were not 9689 // specified. 9690 9691 if (settings.has("id")) { 9692 var region = new (self.vttjs.VTTRegion || self.window.VTTRegion)(); 9693 region.width = settings.get("width", 100); 9694 region.lines = settings.get("lines", 3); 9695 region.regionAnchorX = settings.get("regionanchorX", 0); 9696 region.regionAnchorY = settings.get("regionanchorY", 100); 9697 region.viewportAnchorX = settings.get("viewportanchorX", 0); 9698 region.viewportAnchorY = settings.get("viewportanchorY", 100); 9699 region.scroll = settings.get("scroll", ""); // Register the region. 9700 9701 self.onregion && self.onregion(region); // Remember the VTTRegion for later in case we parse any VTTCues that 9702 // reference it. 9703 9704 self.regionList.push({ 9705 id: settings.get("id"), 9706 region: region 9707 }); 9708 } 9709 } // draft-pantos-http-live-streaming-20 9710 // https://tools.ietf.org/html/draft-pantos-http-live-streaming-20#section-3.5 9711 // 3.5 WebVTT 9712 9713 9714 function parseTimestampMap(input) { 9715 var settings = new Settings(); 9716 parseOptions(input, function (k, v) { 9717 switch (k) { 9718 case "MPEGT": 9719 settings.integer(k + 'S', v); 9720 break; 9721 9722 case "LOCA": 9723 settings.set(k + 'L', parseTimeStamp(v)); 9724 break; 9725 } 9726 }, /[^\d]:/, /,/); 9727 self.ontimestampmap && self.ontimestampmap({ 9728 "MPEGTS": settings.get("MPEGTS"), 9729 "LOCAL": settings.get("LOCAL") 9730 }); 9731 } // 3.2 WebVTT metadata header syntax 9732 9733 9734 function parseHeader(input) { 9735 if (input.match(/X-TIMESTAMP-MAP/)) { 9736 // This line contains HLS X-TIMESTAMP-MAP metadata 9737 parseOptions(input, function (k, v) { 9738 switch (k) { 9739 case "X-TIMESTAMP-MAP": 9740 parseTimestampMap(v); 9741 break; 9742 } 9743 }, /=/); 9744 } else { 9745 parseOptions(input, function (k, v) { 9746 switch (k) { 9747 case "Region": 9748 // 3.3 WebVTT region metadata header syntax 9749 parseRegion(v); 9750 break; 9751 } 9752 }, /:/); 9753 } 9754 } // 5.1 WebVTT file parsing. 9755 9756 9757 try { 9758 var line; 9759 9760 if (self.state === "INITIAL") { 9761 // We can't start parsing until we have the first line. 9762 if (!/\r\n|\n/.test(self.buffer)) { 9763 return this; 9764 } 9765 9766 line = collectNextLine(); 9767 var m = line.match(/^WEBVTT([ \t].*)?$/); 9768 9769 if (!m || !m[0]) { 9770 throw new ParsingError(ParsingError.Errors.BadSignature); 9771 } 9772 9773 self.state = "HEADER"; 9774 } 9775 9776 var alreadyCollectedLine = false;
vendor: 4,922 bytes, lines 9777-9933
9777 9778 while (self.buffer) { 9779 // We can't parse a line until we have the full line. 9780 if (!/\r\n|\n/.test(self.buffer)) { 9781 return this; 9782 } 9783 9784 if (!alreadyCollectedLine) { 9785 line = collectNextLine(); 9786 } else { 9787 alreadyCollectedLine = false; 9788 } 9789 9790 switch (self.state) { 9791 case "HEADER": 9792 // 13-18 - Allow a header (metadata) under the WEBVTT line. 9793 if (/:/.test(line)) { 9794 parseHeader(line); 9795 } else if (!line) { 9796 // An empty line terminates the header and starts the body (cues). 9797 self.state = "ID"; 9798 } 9799 9800 continue; 9801 9802 case "NOTE": 9803 // Ignore NOTE blocks. 9804 if (!line) { 9805 self.state = "ID"; 9806 } 9807 9808 continue; 9809 9810 case "ID": 9811 // Check for the start of NOTE blocks. 9812 if (/^NOTE($|[ \t])/.test(line)) { 9813 self.state = "NOTE"; 9814 break; 9815 } // 19-29 - Allow any number of line terminators, then initialize new cue values. 9816 9817 9818 if (!line) { 9819 continue; 9820 } 9821 9822 self.cue = new (self.vttjs.VTTCue || self.window.VTTCue)(0, 0, ""); // Safari still uses the old middle value and won't accept center 9823 9824 try { 9825 self.cue.align = "center"; 9826 } catch (e) { 9827 self.cue.align = "middle"; 9828 } 9829 9830 self.state = "CUE"; // 30-39 - Check if self line contains an optional identifier or timing data. 9831 9832 if (line.indexOf("-->") === -1) { 9833 self.cue.id = line; 9834 continue; 9835 } 9836 9837 // Process line as start of a cue. 9838 9839 /*falls through*/ 9840 9841 case "CUE": 9842 // 40 - Collect cue timings and settings. 9843 try { 9844 parseCue(line, self.cue, self.regionList); 9845 } catch (e) { 9846 self.reportOrThrowError(e); // In case of an error ignore rest of the cue. 9847 9848 self.cue = null; 9849 self.state = "BADCUE"; 9850 continue; 9851 } 9852 9853 self.state = "CUETEXT"; 9854 continue; 9855 9856 case "CUETEXT": 9857 var hasSubstring = line.indexOf("-->") !== -1; // 34 - If we have an empty line then report the cue. 9858 // 35 - If we have the special substring '-->' then report the cue, 9859 // but do not collect the line as we need to process the current 9860 // one as a new cue. 9861 9862 if (!line || hasSubstring && (alreadyCollectedLine = true)) { 9863 // We are done parsing self cue. 9864 self.oncue && self.oncue(self.cue); 9865 self.cue = null; 9866 self.state = "ID"; 9867 continue; 9868 } 9869 9870 if (self.cue.text) { 9871 self.cue.text += "\n"; 9872 } 9873 9874 self.cue.text += line.replace(/\u2028/g, '\n').replace(/u2029/g, '\n'); 9875 continue; 9876 9877 case "BADCUE": 9878 // BADCUE 9879 // 54-62 - Collect and discard the remaining cue. 9880 if (!line) { 9881 self.state = "ID"; 9882 } 9883 9884 continue; 9885 } 9886 } 9887 } catch (e) { 9888 self.reportOrThrowError(e); // If we are currently parsing a cue, report what we have. 9889 9890 if (self.state === "CUETEXT" && self.cue && self.oncue) { 9891 self.oncue(self.cue); 9892 } 9893 9894 self.cue = null; // Enter BADWEBVTT state if header was not parsed correctly otherwise 9895 // another exception occurred so enter BADCUE state. 9896 9897 self.state = self.state === "INITIAL" ? "BADWEBVTT" : "BADCUE"; 9898 } 9899 9900 return this; 9901 }, 9902 flush: function flush() { 9903 var self = this; 9904 9905 try { 9906 // Finish decoding the stream. 9907 self.buffer += self.decoder.decode(); // Synthesize the end of the current cue or region. 9908 9909 if (self.cue || self.state === "HEADER") { 9910 self.buffer += "\n\n"; 9911 self.parse(); 9912 } // If we've flushed, parsed, and we're still on the INITIAL state then 9913 // that means we don't have enough of the stream to parse the first 9914 // line. 9915 9916 9917 if (self.state === "INITIAL") { 9918 throw new ParsingError(ParsingError.Errors.BadSignature); 9919 } 9920 } catch (e) { 9921 self.reportOrThrowError(e); 9922 } 9923 9924 self.onflush && self.onflush(); 9925 return this; 9926 } 9927 }; 9928 var vtt = WebVTT$1; 9929 9930 /** 9931 * Copyright 2013 vtt.js Contributors 9932 * 9933 * Licensed under the Apache License, Version 2.0 (the "License");
9934 * you may not use this file except in compliance with the License. 9935 * You may obtain a copy of the License at 9936 * 9937 * http://www.apache.org/licenses/LICENSE-2.0 9938 * 9939 * Unless required by applicable law or agreed to in writing, software 9940 * distributed under the License is distributed on an "AS IS" BASIS, 9941 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9942 * See the License for the specific language governing permissions and 9943 * limitations under the License. 9944 */ 9945 var autoKeyword = "auto"; 9946 var directionSetting = { 9947 "": 1, 9948 "lr": 1, 9949 "rl": 1 9950 }; 9951 var alignSetting = { 9952 "start": 1, 9953 "center": 1, 9954 "end": 1, 9955 "left": 1, 9956 "right": 1, 9957 "auto": 1, 9958 "line-left": 1, 9959 "line-right": 1 9960 }; 9961 9962 function findDirectionSetting(value) { 9963 if (typeof value !== "string") { 9964 return false; 9965 } 9966 9967 var dir = directionSetting[value.toLowerCase()]; 9968 return dir ? value.toLowerCase() : false; 9969 } 9970 9971 function findAlignSetting(value) { 9972 if (typeof value !== "string") { 9973 return false; 9974 } 9975 9976 var align = alignSetting[value.toLowerCase()]; 9977 return align ? value.toLowerCase() : false; 9978 } 9979 9980 function VTTCue(startTime, endTime, text) { 9981 /** 9982 * Shim implementation specific properties. These properties are not in 9983 * the spec. 9984 */ 9985 // Lets us know when the VTTCue's data has changed in such a way that we need 9986 // to recompute its display state. This lets us compute its display state 9987 // lazily. 9988 this.hasBeenReset = false; 9989 /** 9990 * VTTCue and TextTrackCue properties 9991 * http://dev.w3.org/html5/webvtt/#vttcue-interface 9992 */ 9993 9994 var _id = ""; 9995 var _pauseOnExit = false; 9996 var _startTime = startTime; 9997 var _endTime = endTime; 9998 var _text = text; 9999 var _region = null; 10000 var _vertical = ""; 10001 var _snapToLines = true; 10002 var _line = "auto"; 10003 var _lineAlign = "start"; 10004 var _position = "auto"; 10005 var _positionAlign = "auto"; 10006 var _size = 100; 10007 var _align = "center"; 10008 Object.defineProperties(this, { 10009 "id": { 10010 enumerable: true, 10011 get: function get() { 10012 return _id; 10013 }, 10014 set: function set(value) { 10015 _id = "" + value; 10016 } 10017 }, 10018 "pauseOnExit": { 10019 enumerable: true, 10020 get: function get() { 10021 return _pauseOnExit; 10022 }, 10023 set: function set(value) { 10024 _pauseOnExit = !!value; 10025 } 10026 }, 10027 "startTime": { 10028 enumerable: true, 10029 get: function get() { 10030 return _startTime; 10031 }, 10032 set: function set(value) { 10033 if (typeof value !== "number") { 10034 throw new TypeError("Start time must be set to a number."); 10035 } 10036 10037 _startTime = value; 10038 this.hasBeenReset = true; 10039 } 10040 }, 10041 "endTime": { 10042 enumerable: true, 10043 get: function get() { 10044 return _endTime; 10045 }, 10046 set: function set(value) { 10047 if (typeof value !== "number") { 10048 throw new TypeError("End time must be set to a number."); 10049 } 10050 10051 _endTime = value; 10052 this.hasBeenReset = true; 10053 } 10054 }, 10055 "text": { 10056 enumerable: true, 10057 get: function get() { 10058 return _text; 10059 }, 10060 set: function set(value) { 10061 _text = "" + value; 10062 this.hasBeenReset = true; 10063 } 10064 }, 10065 "region": { 10066 enumerable: true, 10067 get: function get() { 10068 return _region; 10069 }, 10070 set: function set(value) { 10071 _region = value; 10072 this.hasBeenReset = true; 10073 } 10074 }, 10075 "vertical": { 10076 enumerable: true, 10077 get: function get() { 10078 return _vertical; 10079 }, 10080 set: function set(value) { 10081 var setting = findDirectionSetting(value); // Have to check for false because the setting an be an empty string. 10082 10083 if (setting === false) {
vendor: 4,233 bytes, lines 10084-10233
10084 throw new SyntaxError("Vertical: an invalid or illegal direction string was specified."); 10085 } 10086 10087 _vertical = setting; 10088 this.hasBeenReset = true; 10089 } 10090 }, 10091 "snapToLines": { 10092 enumerable: true, 10093 get: function get() { 10094 return _snapToLines; 10095 }, 10096 set: function set(value) { 10097 _snapToLines = !!value; 10098 this.hasBeenReset = true; 10099 } 10100 }, 10101 "line": { 10102 enumerable: true, 10103 get: function get() { 10104 return _line; 10105 }, 10106 set: function set(value) { 10107 if (typeof value !== "number" && value !== autoKeyword) { 10108 throw new SyntaxError("Line: an invalid number or illegal string was specified."); 10109 } 10110 10111 _line = value; 10112 this.hasBeenReset = true; 10113 } 10114 }, 10115 "lineAlign": { 10116 enumerable: true, 10117 get: function get() { 10118 return _lineAlign; 10119 }, 10120 set: function set(value) { 10121 var setting = findAlignSetting(value); 10122 10123 if (!setting) { 10124 console.warn("lineAlign: an invalid or illegal string was specified."); 10125 } else { 10126 _lineAlign = setting; 10127 this.hasBeenReset = true; 10128 } 10129 } 10130 }, 10131 "position": { 10132 enumerable: true, 10133 get: function get() { 10134 return _position; 10135 }, 10136 set: function set(value) { 10137 if (value < 0 || value > 100) { 10138 throw new Error("Position must be between 0 and 100."); 10139 } 10140 10141 _position = value; 10142 this.hasBeenReset = true; 10143 } 10144 }, 10145 "positionAlign": { 10146 enumerable: true, 10147 get: function get() { 10148 return _positionAlign; 10149 }, 10150 set: function set(value) { 10151 var setting = findAlignSetting(value); 10152 10153 if (!setting) { 10154 console.warn("positionAlign: an invalid or illegal string was specified."); 10155 } else { 10156 _positionAlign = setting; 10157 this.hasBeenReset = true; 10158 } 10159 } 10160 }, 10161 "size": { 10162 enumerable: true, 10163 get: function get() { 10164 return _size; 10165 }, 10166 set: function set(value) { 10167 if (value < 0 || value > 100) { 10168 throw new Error("Size must be between 0 and 100."); 10169 } 10170 10171 _size = value; 10172 this.hasBeenReset = true; 10173 } 10174 }, 10175 "align": { 10176 enumerable: true, 10177 get: function get() { 10178 return _align; 10179 }, 10180 set: function set(value) { 10181 var setting = findAlignSetting(value); 10182 10183 if (!setting) { 10184 throw new SyntaxError("align: an invalid or illegal alignment string was specified."); 10185 } 10186 10187 _align = setting; 10188 this.hasBeenReset = true; 10189 } 10190 } 10191 }); 10192 /** 10193 * Other <track> spec defined properties 10194 */ 10195 // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-video-element.html#text-track-cue-display-state 10196 10197 this.displayState = undefined; 10198 } 10199 /** 10200 * VTTCue methods 10201 */ 10202 10203 10204 VTTCue.prototype.getCueAsHTML = function () { 10205 // Assume WebVTT.convertCueToDOMTree is on the global. 10206 return WebVTT.convertCueToDOMTree(window, this.text); 10207 }; 10208 10209 var vttcue = VTTCue; 10210 10211 /** 10212 * Copyright 2013 vtt.js Contributors 10213 * 10214 * Licensed under the Apache License, Version 2.0 (the "License"); 10215 * you may not use this file except in compliance with the License. 10216 * You may obtain a copy of the License at 10217 * 10218 * http://www.apache.org/licenses/LICENSE-2.0 10219 * 10220 * Unless required by applicable law or agreed to in writing, software 10221 * distributed under the License is distributed on an "AS IS" BASIS, 10222 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10223 * See the License for the specific language governing permissions and 10224 * limitations under the License. 10225 */ 10226 var scrollSetting = { 10227 "": true, 10228 "up": true 10229 }; 10230 10231 function findScrollSetting(value) { 10232 if (typeof value !== "string") { 10233 return false;
10234 } 10235 10236 var scroll = scrollSetting[value.toLowerCase()]; 10237 return scroll ? value.toLowerCase() : false; 10238 } 10239 10240 function isValidPercentValue(value) { 10241 return typeof value === "number" && value >= 0 && value <= 100; 10242 } // VTTRegion shim http://dev.w3.org/html5/webvtt/#vttregion-interface 10243 10244 10245 function VTTRegion() { 10246 var _width = 100; 10247 var _lines = 3; 10248 var _regionAnchorX = 0; 10249 var _regionAnchorY = 100; 10250 var _viewportAnchorX = 0; 10251 var _viewportAnchorY = 100; 10252 var _scroll = ""; 10253 Object.defineProperties(this, { 10254 "width": { 10255 enumerable: true, 10256 get: function get() { 10257 return _width; 10258 }, 10259 set: function set(value) { 10260 if (!isValidPercentValue(value)) { 10261 throw new Error("Width must be between 0 and 100."); 10262 } 10263 10264 _width = value; 10265 } 10266 }, 10267 "lines": { 10268 enumerable: true, 10269 get: function get() { 10270 return _lines; 10271 }, 10272 set: function set(value) { 10273 if (typeof value !== "number") { 10274 throw new TypeError("Lines must be set to a number."); 10275 } 10276 10277 _lines = value; 10278 } 10279 }, 10280 "regionAnchorY": { 10281 enumerable: true, 10282 get: function get() { 10283 return _regionAnchorY; 10284 }, 10285 set: function set(value) { 10286 if (!isValidPercentValue(value)) { 10287 throw new Error("RegionAnchorX must be between 0 and 100."); 10288 } 10289 10290 _regionAnchorY = value; 10291 } 10292 }, 10293 "regionAnchorX": { 10294 enumerable: true, 10295 get: function get() { 10296 return _regionAnchorX; 10297 }, 10298 set: function set(value) { 10299 if (!isValidPercentValue(value)) { 10300 throw new Error("RegionAnchorY must be between 0 and 100."); 10301 } 10302 10303 _regionAnchorX = value; 10304 } 10305 }, 10306 "viewportAnchorY": { 10307 enumerable: true, 10308 get: function get() { 10309 return _viewportAnchorY; 10310 }, 10311 set: function set(value) { 10312 if (!isValidPercentValue(value)) { 10313 throw new Error("ViewportAnchorY must be between 0 and 100."); 10314 } 10315 10316 _viewportAnchorY = value; 10317 } 10318 }, 10319 "viewportAnchorX": { 10320 enumerable: true, 10321 get: function get() { 10322 return _viewportAnchorX; 10323 }, 10324 set: function set(value) { 10325 if (!isValidPercentValue(value)) { 10326 throw new Error("ViewportAnchorX must be between 0 and 100."); 10327 } 10328 10329 _viewportAnchorX = value; 10330 } 10331 }, 10332 "scroll": { 10333 enumerable: true, 10334 get: function get() { 10335 return _scroll; 10336 }, 10337 set: function set(value) { 10338 var setting = findScrollSetting(value); // Have to check for false as an empty string is a legal value. 10339 10340 if (setting === false) { 10341 console.warn("Scroll: an invalid or illegal string was specified."); 10342 } else { 10343 _scroll = setting; 10344 } 10345 } 10346 } 10347 }); 10348 } 10349 10350 var vttregion = VTTRegion; 10351 10352 var browserIndex = createCommonjsModule(function (module) { 10353 /** 10354 * Copyright 2013 vtt.js Contributors 10355 * 10356 * Licensed under the Apache License, Version 2.0 (the "License"); 10357 * you may not use this file except in compliance with the License. 10358 * You may obtain a copy of the License at 10359 * 10360 * http://www.apache.org/licenses/LICENSE-2.0 10361 * 10362 * Unless required by applicable law or agreed to in writing, software 10363 * distributed under the License is distributed on an "AS IS" BASIS, 10364 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10365 * See the License for the specific language governing permissions and 10366 * limitations under the License. 10367 */ 10368 // Default exports for Node. Export the extended versions of VTTCue and 10369 // VTTRegion in Node since we likely want the capability to convert back and 10370 // forth between JSON. If we don't then it's not that big of a deal since we're 10371 // off browser. 10372 var vttjs = module.exports = { 10373 WebVTT: vtt, 10374 VTTCue: vttcue, 10375 VTTRegion: vttregion 10376 }; 10377 window$1.vttjs = vttjs; 10378 window$1.WebVTT = vttjs.WebVTT; 10379 var cueShim = vttjs.VTTCue; 10380 var regionShim = vttjs.VTTRegion; 10381 var nativeVTTCue = window$1.VTTCue; 10382 var nativeVTTRegion = window$1.VTTRegion; 10383 10384 vttjs.shim = function () { 10385 window$1.VTTCue = cueShim; 10386 window$1.VTTRegion = regionShim; 10387 }; 10388 10389 vttjs.restore = function () { 10390 window$1.VTTCue = nativeVTTCue; 10391 window$1.VTTRegion = nativeVTTRegion; 10392 }; 10393 10394 if (!window$1.VTTCue) { 10395 vttjs.shim(); 10396 } 10397 }); 10398 var browserIndex_1 = browserIndex.WebVTT; 10399 var browserIndex_2 = browserIndex.VTTCue; 10400 var browserIndex_3 = browserIndex.VTTRegion; 10401 10402 /** 10403 * An Object containing a structure like: `{src: 'url', type: 'mimetype'}` or string 10404 * that just contains the src url alone. 10405 * * `var SourceObject = {src: 'http://ex.com/video.mp4', type: 'video/mp4'};` 10406 * `var SourceString = 'http://example.com/some-video.mp4';` 10407 * 10408 * @typedef {Object|string} Tech~SourceObject 10409 * 10410 * @property {string} src 10411 * The url to the source 10412 * 10413 * @property {string} type 10414 * The mime type of the source 10415 */ 10416 10417 /** 10418 * A function used by {@link Tech} to create a new {@link TextTrack}. 10419 * 10420 * @private 10421 * 10422 * @param {Tech} self 10423 * An instance of the Tech class. 10424 * 10425 * @param {string} kind 10426 * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata) 10427 * 10428 * @param {string} [label] 10429 * Label to identify the text track 10430 * 10431 * @param {string} [language] 10432 * Two letter language abbreviation 10433 * 10434 * @param {Object} [options={}] 10435 * An object with additional text track options 10436 * 10437 * @return {TextTrack} 10438 * The text track that was created. 10439 */ 10440 10441 function createTrackHelper(self, kind, label, language, options) { 10442 if (options === void 0) { 10443 options = {}; 10444 } 10445 10446 var tracks = self.textTracks(); 10447 options.kind = kind; 10448 10449 if (label) { 10450 options.label = label; 10451 } 10452 10453 if (language) { 10454 options.language = language; 10455 } 10456 10457 options.tech = self; 10458 var track = new ALL.text.TrackClass(options); 10459 tracks.addTrack(track); 10460 return track; 10461 } 10462 /** 10463 * This is the base class for media playback technology controllers, such as 10464 * {@link Flash} and {@link HTML5} 10465 * 10466 * @extends Component 10467 */ 10468 10469 10470 var Tech = 10471 /*#__PURE__*/ 10472 function (_Component) { 10473 inheritsLoose(Tech, _Component); 10474 10475 /** 10476 * Create an instance of this Tech. 10477 * 10478 * @param {Object} [options] 10479 * The key/value store of player options. 10480 * 10481 * @param {Component~ReadyCallback} ready 10482 * Callback function to call when the `HTML5` Tech is ready. 10483 */ 10484 function Tech(options, ready) { 10485 var _this; 10486 10487 if (options === void 0) { 10488 options = {}; 10489 } 10490 10491 if (ready === void 0) { 10492 ready = function ready() {}; 10493 } 10494 10495 // we don't want the tech to report user activity automatically.
vendor: 38,994 bytes, lines 10496-11793
10496 // This is done manually in addControlsListeners 10497 options.reportTouchActivity = false; 10498 _this = _Component.call(this, null, options, ready) || this; // keep track of whether the current source has played at all to 10499 // implement a very limited played() 10500 10501 _this.hasStarted_ = false; 10502 10503 _this.on('playing', function () { 10504 this.hasStarted_ = true; 10505 }); 10506 10507 _this.on('loadstart', function () { 10508 this.hasStarted_ = false; 10509 }); 10510 10511 ALL.names.forEach(function (name) { 10512 var props = ALL[name]; 10513 10514 if (options && options[props.getterName]) { 10515 _this[props.privateName] = options[props.getterName]; 10516 } 10517 }); // Manually track progress in cases where the browser/flash player doesn't report it. 10518 10519 if (!_this.featuresProgressEvents) { 10520 _this.manualProgressOn(); 10521 } // Manually track timeupdates in cases where the browser/flash player doesn't report it. 10522 10523 10524 if (!_this.featuresTimeupdateEvents) { 10525 _this.manualTimeUpdatesOn(); 10526 } 10527 10528 ['Text', 'Audio', 'Video'].forEach(function (track) { 10529 if (options["native" + track + "Tracks"] === false) { 10530 _this["featuresNative" + track + "Tracks"] = false; 10531 } 10532 }); 10533 10534 if (options.nativeCaptions === false || options.nativeTextTracks === false) { 10535 _this.featuresNativeTextTracks = false; 10536 } else if (options.nativeCaptions === true || options.nativeTextTracks === true) { 10537 _this.featuresNativeTextTracks = true; 10538 } 10539 10540 if (!_this.featuresNativeTextTracks) { 10541 _this.emulateTextTracks(); 10542 } 10543 10544 _this.preloadTextTracks = options.preloadTextTracks !== false; 10545 _this.autoRemoteTextTracks_ = new ALL.text.ListClass(); 10546 10547 _this.initTrackListeners(); // Turn on component tap events only if not using native controls 10548 10549 10550 if (!options.nativeControlsForTouch) { 10551 _this.emitTapEvents(); 10552 } 10553 10554 if (_this.constructor) { 10555 _this.name_ = _this.constructor.name || 'Unknown Tech'; 10556 } 10557 10558 return _this; 10559 } 10560 /** 10561 * A special function to trigger source set in a way that will allow player 10562 * to re-trigger if the player or tech are not ready yet. 10563 * 10564 * @fires Tech#sourceset 10565 * @param {string} src The source string at the time of the source changing. 10566 */ 10567 10568 10569 var _proto = Tech.prototype; 10570 10571 _proto.triggerSourceset = function triggerSourceset(src) { 10572 var _this2 = this; 10573 10574 if (!this.isReady_) { 10575 // on initial ready we have to trigger source set 10576 // 1ms after ready so that player can watch for it. 10577 this.one('ready', function () { 10578 return _this2.setTimeout(function () { 10579 return _this2.triggerSourceset(src); 10580 }, 1); 10581 }); 10582 } 10583 /** 10584 * Fired when the source is set on the tech causing the media element 10585 * to reload. 10586 * 10587 * @see {@link Player#event:sourceset} 10588 * @event Tech#sourceset 10589 * @type {EventTarget~Event} 10590 */ 10591 10592 10593 this.trigger({ 10594 src: src, 10595 type: 'sourceset' 10596 }); 10597 } 10598 /* Fallbacks for unsupported event types 10599 ================================================================================ */ 10600 10601 /** 10602 * Polyfill the `progress` event for browsers that don't support it natively. 10603 * 10604 * @see {@link Tech#trackProgress} 10605 */ 10606 ; 10607 10608 _proto.manualProgressOn = function manualProgressOn() { 10609 this.on('durationchange', this.onDurationChange); 10610 this.manualProgress = true; // Trigger progress watching when a source begins loading 10611 10612 this.one('ready', this.trackProgress); 10613 } 10614 /** 10615 * Turn off the polyfill for `progress` events that was created in 10616 * {@link Tech#manualProgressOn} 10617 */ 10618 ; 10619 10620 _proto.manualProgressOff = function manualProgressOff() { 10621 this.manualProgress = false; 10622 this.stopTrackingProgress(); 10623 this.off('durationchange', this.onDurationChange); 10624 } 10625 /** 10626 * This is used to trigger a `progress` event when the buffered percent changes. It 10627 * sets an interval function that will be called every 500 milliseconds to check if the 10628 * buffer end percent has changed. 10629 * 10630 * > This function is called by {@link Tech#manualProgressOn} 10631 * 10632 * @param {EventTarget~Event} event 10633 * The `ready` event that caused this to run. 10634 * 10635 * @listens Tech#ready 10636 * @fires Tech#progress 10637 */ 10638 ; 10639 10640 _proto.trackProgress = function trackProgress(event) { 10641 this.stopTrackingProgress(); 10642 this.progressInterval = this.setInterval(bind(this, function () { 10643 // Don't trigger unless buffered amount is greater than last time 10644 var numBufferedPercent = this.bufferedPercent(); 10645 10646 if (this.bufferedPercent_ !== numBufferedPercent) { 10647 /** 10648 * See {@link Player#progress} 10649 * 10650 * @event Tech#progress 10651 * @type {EventTarget~Event} 10652 */ 10653 this.trigger('progress'); 10654 } 10655 10656 this.bufferedPercent_ = numBufferedPercent; 10657 10658 if (numBufferedPercent === 1) { 10659 this.stopTrackingProgress(); 10660 } 10661 }), 500); 10662 } 10663 /** 10664 * Update our internal duration on a `durationchange` event by calling 10665 * {@link Tech#duration}. 10666 * 10667 * @param {EventTarget~Event} event 10668 * The `durationchange` event that caused this to run. 10669 * 10670 * @listens Tech#durationchange 10671 */ 10672 ; 10673 10674 _proto.onDurationChange = function onDurationChange(event) { 10675 this.duration_ = this.duration(); 10676 } 10677 /** 10678 * Get and create a `TimeRange` object for buffering. 10679 * 10680 * @return {TimeRange} 10681 * The time range object that was created. 10682 */ 10683 ; 10684 10685 _proto.buffered = function buffered() { 10686 return createTimeRanges(0, 0); 10687 } 10688 /** 10689 * Get the percentage of the current video that is currently buffered. 10690 * 10691 * @return {number} 10692 * A number from 0 to 1 that represents the decimal percentage of the 10693 * video that is buffered. 10694 * 10695 */ 10696 ; 10697 10698 _proto.bufferedPercent = function bufferedPercent$1() { 10699 return bufferedPercent(this.buffered(), this.duration_); 10700 } 10701 /** 10702 * Turn off the polyfill for `progress` events that was created in 10703 * {@link Tech#manualProgressOn} 10704 * Stop manually tracking progress events by clearing the interval that was set in 10705 * {@link Tech#trackProgress}. 10706 */ 10707 ; 10708 10709 _proto.stopTrackingProgress = function stopTrackingProgress() { 10710 this.clearInterval(this.progressInterval); 10711 } 10712 /** 10713 * Polyfill the `timeupdate` event for browsers that don't support it. 10714 * 10715 * @see {@link Tech#trackCurrentTime} 10716 */ 10717 ; 10718 10719 _proto.manualTimeUpdatesOn = function manualTimeUpdatesOn() { 10720 this.manualTimeUpdates = true; 10721 this.on('play', this.trackCurrentTime); 10722 this.on('pause', this.stopTrackingCurrentTime); 10723 } 10724 /** 10725 * Turn off the polyfill for `timeupdate` events that was created in 10726 * {@link Tech#manualTimeUpdatesOn} 10727 */ 10728 ; 10729 10730 _proto.manualTimeUpdatesOff = function manualTimeUpdatesOff() { 10731 this.manualTimeUpdates = false; 10732 this.stopTrackingCurrentTime(); 10733 this.off('play', this.trackCurrentTime); 10734 this.off('pause', this.stopTrackingCurrentTime); 10735 } 10736 /** 10737 * Sets up an interval function to track current time and trigger `timeupdate` every 10738 * 250 milliseconds. 10739 * 10740 * @listens Tech#play 10741 * @triggers Tech#timeupdate 10742 */ 10743 ; 10744 10745 _proto.trackCurrentTime = function trackCurrentTime() { 10746 if (this.currentTimeInterval) { 10747 this.stopTrackingCurrentTime(); 10748 } 10749 10750 this.currentTimeInterval = this.setInterval(function () { 10751 /** 10752 * Triggered at an interval of 250ms to indicated that time is passing in the video. 10753 * 10754 * @event Tech#timeupdate 10755 * @type {EventTarget~Event} 10756 */ 10757 this.trigger({ 10758 type: 'timeupdate', 10759 target: this, 10760 manuallyTriggered: true 10761 }); // 42 = 24 fps // 250 is what Webkit uses // FF uses 15 10762 }, 250); 10763 } 10764 /** 10765 * Stop the interval function created in {@link Tech#trackCurrentTime} so that the 10766 * `timeupdate` event is no longer triggered. 10767 * 10768 * @listens {Tech#pause} 10769 */ 10770 ; 10771 10772 _proto.stopTrackingCurrentTime = function stopTrackingCurrentTime() { 10773 this.clearInterval(this.currentTimeInterval); // #1002 - if the video ends right before the next timeupdate would happen, 10774 // the progress bar won't make it all the way to the end 10775 10776 this.trigger({ 10777 type: 'timeupdate', 10778 target: this, 10779 manuallyTriggered: true 10780 }); 10781 } 10782 /** 10783 * Turn off all event polyfills, clear the `Tech`s {@link AudioTrackList}, 10784 * {@link VideoTrackList}, and {@link TextTrackList}, and dispose of this Tech. 10785 * 10786 * @fires Component#dispose 10787 */ 10788 ; 10789 10790 _proto.dispose = function dispose() { 10791 // clear out all tracks because we can't reuse them between techs 10792 this.clearTracks(NORMAL.names); // Turn off any manual progress or timeupdate tracking 10793 10794 if (this.manualProgress) { 10795 this.manualProgressOff(); 10796 } 10797 10798 if (this.manualTimeUpdates) { 10799 this.manualTimeUpdatesOff(); 10800 } 10801 10802 _Component.prototype.dispose.call(this); 10803 } 10804 /** 10805 * Clear out a single `TrackList` or an array of `TrackLists` given their names. 10806 * 10807 * > Note: Techs without source handlers should call this between sources for `video` 10808 * & `audio` tracks. You don't want to use them between tracks! 10809 * 10810 * @param {string[]|string} types 10811 * TrackList names to clear, valid names are `video`, `audio`, and 10812 * `text`. 10813 */ 10814 ; 10815 10816 _proto.clearTracks = function clearTracks(types) { 10817 var _this3 = this; 10818 10819 types = [].concat(types); // clear out all tracks because we can't reuse them between techs 10820 10821 types.forEach(function (type) { 10822 var list = _this3[type + "Tracks"]() || []; 10823 var i = list.length; 10824 10825 while (i--) { 10826 var track = list[i]; 10827 10828 if (type === 'text') { 10829 _this3.removeRemoteTextTrack(track); 10830 } 10831 10832 list.removeTrack(track); 10833 } 10834 }); 10835 } 10836 /** 10837 * Remove any TextTracks added via addRemoteTextTrack that are 10838 * flagged for automatic garbage collection 10839 */ 10840 ; 10841 10842 _proto.cleanupAutoTextTracks = function cleanupAutoTextTracks() { 10843 var list = this.autoRemoteTextTracks_ || []; 10844 var i = list.length; 10845 10846 while (i--) { 10847 var track = list[i]; 10848 this.removeRemoteTextTrack(track); 10849 } 10850 } 10851 /** 10852 * Reset the tech, which will removes all sources and reset the internal readyState. 10853 * 10854 * @abstract 10855 */ 10856 ; 10857 10858 _proto.reset = function reset() {} 10859 /** 10860 * Get or set an error on the Tech. 10861 * 10862 * @param {MediaError} [err] 10863 * Error to set on the Tech 10864 * 10865 * @return {MediaError|null} 10866 * The current error object on the tech, or null if there isn't one. 10867 */ 10868 ; 10869 10870 _proto.error = function error(err) { 10871 if (err !== undefined) { 10872 this.error_ = new MediaError(err); 10873 this.trigger('error'); 10874 } 10875 10876 return this.error_; 10877 } 10878 /** 10879 * Returns the `TimeRange`s that have been played through for the current source. 10880 * 10881 * > NOTE: This implementation is incomplete. It does not track the played `TimeRange`. 10882 * It only checks whether the source has played at all or not. 10883 * 10884 * @return {TimeRange} 10885 * - A single time range if this video has played 10886 * - An empty set of ranges if not. 10887 */ 10888 ; 10889 10890 _proto.played = function played() { 10891 if (this.hasStarted_) { 10892 return createTimeRanges(0, 0); 10893 } 10894 10895 return createTimeRanges(); 10896 } 10897 /** 10898 * Causes a manual time update to occur if {@link Tech#manualTimeUpdatesOn} was 10899 * previously called. 10900 * 10901 * @fires Tech#timeupdate 10902 */ 10903 ; 10904 10905 _proto.setCurrentTime = function setCurrentTime() { 10906 // improve the accuracy of manual timeupdates 10907 if (this.manualTimeUpdates) { 10908 /** 10909 * A manual `timeupdate` event. 10910 * 10911 * @event Tech#timeupdate 10912 * @type {EventTarget~Event} 10913 */ 10914 this.trigger({ 10915 type: 'timeupdate', 10916 target: this, 10917 manuallyTriggered: true 10918 }); 10919 } 10920 } 10921 /** 10922 * Turn on listeners for {@link VideoTrackList}, {@link {AudioTrackList}, and 10923 * {@link TextTrackList} events. 10924 * 10925 * This adds {@link EventTarget~EventListeners} for `addtrack`, and `removetrack`. 10926 * 10927 * @fires Tech#audiotrackchange 10928 * @fires Tech#videotrackchange 10929 * @fires Tech#texttrackchange 10930 */ 10931 ; 10932 10933 _proto.initTrackListeners = function initTrackListeners() { 10934 var _this4 = this; 10935 10936 /** 10937 * Triggered when tracks are added or removed on the Tech {@link AudioTrackList} 10938 * 10939 * @event Tech#audiotrackchange 10940 * @type {EventTarget~Event} 10941 */ 10942 10943 /** 10944 * Triggered when tracks are added or removed on the Tech {@link VideoTrackList} 10945 * 10946 * @event Tech#videotrackchange 10947 * @type {EventTarget~Event} 10948 */ 10949 10950 /** 10951 * Triggered when tracks are added or removed on the Tech {@link TextTrackList} 10952 * 10953 * @event Tech#texttrackchange 10954 * @type {EventTarget~Event} 10955 */ 10956 NORMAL.names.forEach(function (name) { 10957 var props = NORMAL[name]; 10958 10959 var trackListChanges = function trackListChanges() { 10960 _this4.trigger(name + "trackchange"); 10961 }; 10962 10963 var tracks = _this4[props.getterName](); 10964 10965 tracks.addEventListener('removetrack', trackListChanges); 10966 tracks.addEventListener('addtrack', trackListChanges); 10967 10968 _this4.on('dispose', function () { 10969 tracks.removeEventListener('removetrack', trackListChanges); 10970 tracks.removeEventListener('addtrack', trackListChanges); 10971 }); 10972 }); 10973 } 10974 /** 10975 * Emulate TextTracks using vtt.js if necessary 10976 * 10977 * @fires Tech#vttjsloaded 10978 * @fires Tech#vttjserror 10979 */ 10980 ; 10981 10982 _proto.addWebVttScript_ = function addWebVttScript_() { 10983 var _this5 = this; 10984 10985 if (window$1.WebVTT) { 10986 return; 10987 } // Initially, Tech.el_ is a child of a dummy-div wait until the Component system 10988 // signals that the Tech is ready at which point Tech.el_ is part of the DOM 10989 // before inserting the WebVTT script 10990 10991 10992 if (document.body.contains(this.el())) { 10993 // load via require if available and vtt.js script location was not passed in 10994 // as an option. novtt builds will turn the above require call into an empty object 10995 // which will cause this if check to always fail. 10996 if (!this.options_['vtt.js'] && isPlain(browserIndex) && Object.keys(browserIndex).length > 0) { 10997 this.trigger('vttjsloaded'); 10998 return; 10999 } // load vtt.js via the script location option or the cdn of no location was 11000 // passed in 11001 11002 11003 var script = document.createElement('script'); 11004 script.src = this.options_['vtt.js'] || 'https://vjs.zencdn.net/vttjs/0.14.1/vtt.min.js'; 11005 11006 script.onload = function () { 11007 /** 11008 * Fired when vtt.js is loaded. 11009 * 11010 * @event Tech#vttjsloaded 11011 * @type {EventTarget~Event} 11012 */ 11013 _this5.trigger('vttjsloaded'); 11014 }; 11015 11016 script.onerror = function () { 11017 /** 11018 * Fired when vtt.js was not loaded due to an error 11019 * 11020 * @event Tech#vttjsloaded 11021 * @type {EventTarget~Event} 11022 */ 11023 _this5.trigger('vttjserror'); 11024 }; 11025 11026 this.on('dispose', function () { 11027 script.onload = null; 11028 script.onerror = null; 11029 }); // but have not loaded yet and we set it to true before the inject so that 11030 // we don't overwrite the injected window.WebVTT if it loads right away 11031 11032 window$1.WebVTT = true; 11033 this.el().parentNode.appendChild(script); 11034 } else { 11035 this.ready(this.addWebVttScript_); 11036 } 11037 } 11038 /** 11039 * Emulate texttracks 11040 * 11041 */ 11042 ; 11043 11044 _proto.emulateTextTracks = function emulateTextTracks() { 11045 var _this6 = this; 11046 11047 var tracks = this.textTracks(); 11048 var remoteTracks = this.remoteTextTracks(); 11049 11050 var handleAddTrack = function handleAddTrack(e) { 11051 return tracks.addTrack(e.track); 11052 }; 11053 11054 var handleRemoveTrack = function handleRemoveTrack(e) { 11055 return tracks.removeTrack(e.track); 11056 }; 11057 11058 remoteTracks.on('addtrack', handleAddTrack); 11059 remoteTracks.on('removetrack', handleRemoveTrack); 11060 this.addWebVttScript_(); 11061 11062 var updateDisplay = function updateDisplay() { 11063 return _this6.trigger('texttrackchange'); 11064 }; 11065 11066 var textTracksChanges = function textTracksChanges() { 11067 updateDisplay(); 11068 11069 for (var i = 0; i < tracks.length; i++) { 11070 var track = tracks[i]; 11071 track.removeEventListener('cuechange', updateDisplay); 11072 11073 if (track.mode === 'showing') { 11074 track.addEventListener('cuechange', updateDisplay); 11075 } 11076 } 11077 }; 11078 11079 textTracksChanges(); 11080 tracks.addEventListener('change', textTracksChanges); 11081 tracks.addEventListener('addtrack', textTracksChanges); 11082 tracks.addEventListener('removetrack', textTracksChanges); 11083 this.on('dispose', function () { 11084 remoteTracks.off('addtrack', handleAddTrack); 11085 remoteTracks.off('removetrack', handleRemoveTrack); 11086 tracks.removeEventListener('change', textTracksChanges); 11087 tracks.removeEventListener('addtrack', textTracksChanges); 11088 tracks.removeEventListener('removetrack', textTracksChanges); 11089 11090 for (var i = 0; i < tracks.length; i++) { 11091 var track = tracks[i]; 11092 track.removeEventListener('cuechange', updateDisplay); 11093 } 11094 }); 11095 } 11096 /** 11097 * Create and returns a remote {@link TextTrack} object. 11098 * 11099 * @param {string} kind 11100 * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata) 11101 * 11102 * @param {string} [label] 11103 * Label to identify the text track 11104 * 11105 * @param {string} [language] 11106 * Two letter language abbreviation 11107 * 11108 * @return {TextTrack} 11109 * The TextTrack that gets created. 11110 */ 11111 ; 11112 11113 _proto.addTextTrack = function addTextTrack(kind, label, language) { 11114 if (!kind) { 11115 throw new Error('TextTrack kind is required but was not provided'); 11116 } 11117 11118 return createTrackHelper(this, kind, label, language); 11119 } 11120 /** 11121 * Create an emulated TextTrack for use by addRemoteTextTrack 11122 * 11123 * This is intended to be overridden by classes that inherit from 11124 * Tech in order to create native or custom TextTracks. 11125 * 11126 * @param {Object} options 11127 * The object should contain the options to initialize the TextTrack with. 11128 * 11129 * @param {string} [options.kind] 11130 * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata). 11131 * 11132 * @param {string} [options.label]. 11133 * Label to identify the text track 11134 * 11135 * @param {string} [options.language] 11136 * Two letter language abbreviation. 11137 * 11138 * @return {HTMLTrackElement} 11139 * The track element that gets created. 11140 */ 11141 ; 11142 11143 _proto.createRemoteTextTrack = function createRemoteTextTrack(options) { 11144 var track = mergeOptions(options, { 11145 tech: this 11146 }); 11147 return new REMOTE.remoteTextEl.TrackClass(track); 11148 } 11149 /** 11150 * Creates a remote text track object and returns an html track element. 11151 * 11152 * > Note: This can be an emulated {@link HTMLTrackElement} or a native one. 11153 * 11154 * @param {Object} options 11155 * See {@link Tech#createRemoteTextTrack} for more detailed properties. 11156 * 11157 * @param {boolean} [manualCleanup=true] 11158 * - When false: the TextTrack will be automatically removed from the video 11159 * element whenever the source changes 11160 * - When True: The TextTrack will have to be cleaned up manually 11161 * 11162 * @return {HTMLTrackElement} 11163 * An Html Track Element. 11164 * 11165 * @deprecated The default functionality for this function will be equivalent 11166 * to "manualCleanup=false" in the future. The manualCleanup parameter will 11167 * also be removed. 11168 */ 11169 ; 11170 11171 _proto.addRemoteTextTrack = function addRemoteTextTrack(options, manualCleanup) { 11172 var _this7 = this; 11173 11174 if (options === void 0) { 11175 options = {}; 11176 } 11177 11178 var htmlTrackElement = this.createRemoteTextTrack(options); 11179 11180 if (manualCleanup !== true && manualCleanup !== false) { 11181 // deprecation warning 11182 log.warn('Calling addRemoteTextTrack without explicitly setting the "manualCleanup" parameter to `true` is deprecated and default to `false` in future version of video.js'); 11183 manualCleanup = true; 11184 } // store HTMLTrackElement and TextTrack to remote list 11185 11186 11187 this.remoteTextTrackEls().addTrackElement_(htmlTrackElement); 11188 this.remoteTextTracks().addTrack(htmlTrackElement.track); 11189 11190 if (manualCleanup !== true) { 11191 // create the TextTrackList if it doesn't exist 11192 this.ready(function () { 11193 return _this7.autoRemoteTextTracks_.addTrack(htmlTrackElement.track); 11194 }); 11195 } 11196 11197 return htmlTrackElement; 11198 } 11199 /** 11200 * Remove a remote text track from the remote `TextTrackList`. 11201 * 11202 * @param {TextTrack} track 11203 * `TextTrack` to remove from the `TextTrackList` 11204 */ 11205 ; 11206 11207 _proto.removeRemoteTextTrack = function removeRemoteTextTrack(track) { 11208 var trackElement = this.remoteTextTrackEls().getTrackElementByTrack_(track); // remove HTMLTrackElement and TextTrack from remote list 11209 11210 this.remoteTextTrackEls().removeTrackElement_(trackElement); 11211 this.remoteTextTracks().removeTrack(track); 11212 this.autoRemoteTextTracks_.removeTrack(track); 11213 } 11214 /** 11215 * Gets available media playback quality metrics as specified by the W3C's Media 11216 * Playback Quality API. 11217 * 11218 * @see [Spec]{@link https://wicg.github.io/media-playback-quality} 11219 * 11220 * @return {Object} 11221 * An object with supported media playback quality metrics 11222 * 11223 * @abstract 11224 */ 11225 ; 11226 11227 _proto.getVideoPlaybackQuality = function getVideoPlaybackQuality() { 11228 return {}; 11229 } 11230 /** 11231 * Attempt to create a floating video window always on top of other windows 11232 * so that users may continue consuming media while they interact with other 11233 * content sites, or applications on their device. 11234 * 11235 * @see [Spec]{@link https://wicg.github.io/picture-in-picture} 11236 * 11237 * @return {Promise|undefined} 11238 * A promise with a Picture-in-Picture window if the browser supports 11239 * Promises (or one was passed in as an option). It returns undefined 11240 * otherwise. 11241 * 11242 * @abstract 11243 */ 11244 ; 11245 11246 _proto.requestPictureInPicture = function requestPictureInPicture() { 11247 var PromiseClass = this.options_.Promise || window$1.Promise; 11248 11249 if (PromiseClass) { 11250 return PromiseClass.reject(); 11251 } 11252 } 11253 /** 11254 * A method to set a poster from a `Tech`. 11255 * 11256 * @abstract 11257 */ 11258 ; 11259 11260 _proto.setPoster = function setPoster() {} 11261 /** 11262 * A method to check for the presence of the 'playsinline' <video> attribute. 11263 * 11264 * @abstract 11265 */ 11266 ; 11267 11268 _proto.playsinline = function playsinline() {} 11269 /** 11270 * A method to set or unset the 'playsinline' <video> attribute. 11271 * 11272 * @abstract 11273 */ 11274 ; 11275 11276 _proto.setPlaysinline = function setPlaysinline() {} 11277 /** 11278 * Attempt to force override of native audio tracks. 11279 * 11280 * @param {boolean} override - If set to true native audio will be overridden, 11281 * otherwise native audio will potentially be used. 11282 * 11283 * @abstract 11284 */ 11285 ; 11286 11287 _proto.overrideNativeAudioTracks = function overrideNativeAudioTracks() {} 11288 /** 11289 * Attempt to force override of native video tracks. 11290 * 11291 * @param {boolean} override - If set to true native video will be overridden, 11292 * otherwise native video will potentially be used. 11293 * 11294 * @abstract 11295 */ 11296 ; 11297 11298 _proto.overrideNativeVideoTracks = function overrideNativeVideoTracks() {} 11299 /* 11300 * Check if the tech can support the given mime-type. 11301 * 11302 * The base tech does not support any type, but source handlers might 11303 * overwrite this. 11304 * 11305 * @param {string} type 11306 * The mimetype to check for support 11307 * 11308 * @return {string} 11309 * 'probably', 'maybe', or empty string 11310 * 11311 * @see [Spec]{@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/canPlayType} 11312 * 11313 * @abstract 11314 */ 11315 ; 11316 11317 _proto.canPlayType = function canPlayType() { 11318 return ''; 11319 } 11320 /** 11321 * Check if the type is supported by this tech. 11322 * 11323 * The base tech does not support any type, but source handlers might 11324 * overwrite this. 11325 * 11326 * @param {string} type 11327 * The media type to check 11328 * @return {string} Returns the native video element's response 11329 */ 11330 ; 11331 11332 Tech.canPlayType = function canPlayType() { 11333 return ''; 11334 } 11335 /** 11336 * Check if the tech can support the given source 11337 * 11338 * @param {Object} srcObj 11339 * The source object 11340 * @param {Object} options 11341 * The options passed to the tech 11342 * @return {string} 'probably', 'maybe', or '' (empty string) 11343 */ 11344 ; 11345 11346 Tech.canPlaySource = function canPlaySource(srcObj, options) { 11347 return Tech.canPlayType(srcObj.type); 11348 } 11349 /* 11350 * Return whether the argument is a Tech or not. 11351 * Can be passed either a Class like `Html5` or a instance like `player.tech_` 11352 * 11353 * @param {Object} component 11354 * The item to check 11355 * 11356 * @return {boolean} 11357 * Whether it is a tech or not 11358 * - True if it is a tech 11359 * - False if it is not 11360 */ 11361 ; 11362 11363 Tech.isTech = function isTech(component) { 11364 return component.prototype instanceof Tech || component instanceof Tech || component === Tech; 11365 } 11366 /** 11367 * Registers a `Tech` into a shared list for videojs. 11368 * 11369 * @param {string} name 11370 * Name of the `Tech` to register. 11371 * 11372 * @param {Object} tech 11373 * The `Tech` class to register. 11374 */ 11375 ; 11376 11377 Tech.registerTech = function registerTech(name, tech) { 11378 if (!Tech.techs_) { 11379 Tech.techs_ = {}; 11380 } 11381 11382 if (!Tech.isTech(tech)) { 11383 throw new Error("Tech " + name + " must be a Tech"); 11384 } 11385 11386 if (!Tech.canPlayType) { 11387 throw new Error('Techs must have a static canPlayType method on them'); 11388 } 11389 11390 if (!Tech.canPlaySource) { 11391 throw new Error('Techs must have a static canPlaySource method on them'); 11392 } 11393 11394 name = toTitleCase(name); 11395 Tech.techs_[name] = tech; 11396 Tech.techs_[toLowerCase(name)] = tech; 11397 11398 if (name !== 'Tech') { 11399 // camel case the techName for use in techOrder 11400 Tech.defaultTechOrder_.push(name); 11401 } 11402 11403 return tech; 11404 } 11405 /** 11406 * Get a `Tech` from the shared list by name. 11407 * 11408 * @param {string} name 11409 * `camelCase` or `TitleCase` name of the Tech to get 11410 * 11411 * @return {Tech|undefined} 11412 * The `Tech` or undefined if there was no tech with the name requested. 11413 */ 11414 ; 11415 11416 Tech.getTech = function getTech(name) { 11417 if (!name) { 11418 return; 11419 } 11420 11421 if (Tech.techs_ && Tech.techs_[name]) { 11422 return Tech.techs_[name]; 11423 } 11424 11425 name = toTitleCase(name); 11426 11427 if (window$1 && window$1.videojs && window$1.videojs[name]) { 11428 log.warn("The " + name + " tech was added to the videojs object when it should be registered using videojs.registerTech(name, tech)"); 11429 return window$1.videojs[name]; 11430 } 11431 }; 11432 11433 return Tech; 11434 }(Component); 11435 /** 11436 * Get the {@link VideoTrackList} 11437 * 11438 * @returns {VideoTrackList} 11439 * @method Tech.prototype.videoTracks 11440 */ 11441 11442 /** 11443 * Get the {@link AudioTrackList} 11444 * 11445 * @returns {AudioTrackList} 11446 * @method Tech.prototype.audioTracks 11447 */ 11448 11449 /** 11450 * Get the {@link TextTrackList} 11451 * 11452 * @returns {TextTrackList} 11453 * @method Tech.prototype.textTracks 11454 */ 11455 11456 /** 11457 * Get the remote element {@link TextTrackList} 11458 * 11459 * @returns {TextTrackList} 11460 * @method Tech.prototype.remoteTextTracks 11461 */ 11462 11463 /** 11464 * Get the remote element {@link HtmlTrackElementList} 11465 * 11466 * @returns {HtmlTrackElementList} 11467 * @method Tech.prototype.remoteTextTrackEls 11468 */ 11469 11470 11471 ALL.names.forEach(function (name) { 11472 var props = ALL[name]; 11473 11474 Tech.prototype[props.getterName] = function () { 11475 this[props.privateName] = this[props.privateName] || new props.ListClass(); 11476 return this[props.privateName]; 11477 }; 11478 }); 11479 /** 11480 * List of associated text tracks 11481 * 11482 * @type {TextTrackList} 11483 * @private 11484 * @property Tech#textTracks_ 11485 */ 11486 11487 /** 11488 * List of associated audio tracks. 11489 * 11490 * @type {AudioTrackList} 11491 * @private 11492 * @property Tech#audioTracks_ 11493 */ 11494 11495 /** 11496 * List of associated video tracks. 11497 * 11498 * @type {VideoTrackList} 11499 * @private 11500 * @property Tech#videoTracks_ 11501 */ 11502 11503 /** 11504 * Boolean indicating whether the `Tech` supports volume control. 11505 * 11506 * @type {boolean} 11507 * @default 11508 */ 11509 11510 Tech.prototype.featuresVolumeControl = true; 11511 /** 11512 * Boolean indicating whether the `Tech` supports muting volume. 11513 * 11514 * @type {bolean} 11515 * @default 11516 */ 11517 11518 Tech.prototype.featuresMuteControl = true; 11519 /** 11520 * Boolean indicating whether the `Tech` supports fullscreen resize control. 11521 * Resizing plugins using request fullscreen reloads the plugin 11522 * 11523 * @type {boolean} 11524 * @default 11525 */ 11526 11527 Tech.prototype.featuresFullscreenResize = false; 11528 /** 11529 * Boolean indicating whether the `Tech` supports changing the speed at which the video 11530 * plays. Examples: 11531 * - Set player to play 2x (twice) as fast 11532 * - Set player to play 0.5x (half) as fast 11533 * 11534 * @type {boolean} 11535 * @default 11536 */ 11537 11538 Tech.prototype.featuresPlaybackRate = false; 11539 /** 11540 * Boolean indicating whether the `Tech` supports the `progress` event. This is currently 11541 * not triggered by video-js-swf. This will be used to determine if 11542 * {@link Tech#manualProgressOn} should be called. 11543 * 11544 * @type {boolean} 11545 * @default 11546 */ 11547 11548 Tech.prototype.featuresProgressEvents = false; 11549 /** 11550 * Boolean indicating whether the `Tech` supports the `sourceset` event. 11551 * 11552 * A tech should set this to `true` and then use {@link Tech#triggerSourceset} 11553 * to trigger a {@link Tech#event:sourceset} at the earliest time after getting 11554 * a new source. 11555 * 11556 * @type {boolean} 11557 * @default 11558 */ 11559 11560 Tech.prototype.featuresSourceset = false; 11561 /** 11562 * Boolean indicating whether the `Tech` supports the `timeupdate` event. This is currently 11563 * not triggered by video-js-swf. This will be used to determine if 11564 * {@link Tech#manualTimeUpdates} should be called. 11565 * 11566 * @type {boolean} 11567 * @default 11568 */ 11569 11570 Tech.prototype.featuresTimeupdateEvents = false; 11571 /** 11572 * Boolean indicating whether the `Tech` supports the native `TextTrack`s. 11573 * This will help us integrate with native `TextTrack`s if the browser supports them. 11574 * 11575 * @type {boolean} 11576 * @default 11577 */ 11578 11579 Tech.prototype.featuresNativeTextTracks = false; 11580 /** 11581 * A functional mixin for techs that want to use the Source Handler pattern. 11582 * Source handlers are scripts for handling specific formats. 11583 * The source handler pattern is used for adaptive formats (HLS, DASH) that 11584 * manually load video data and feed it into a Source Buffer (Media Source Extensions) 11585 * Example: `Tech.withSourceHandlers.call(MyTech);` 11586 * 11587 * @param {Tech} _Tech 11588 * The tech to add source handler functions to. 11589 * 11590 * @mixes Tech~SourceHandlerAdditions 11591 */ 11592 11593 Tech.withSourceHandlers = function (_Tech) { 11594 /** 11595 * Register a source handler 11596 * 11597 * @param {Function} handler 11598 * The source handler class 11599 * 11600 * @param {number} [index] 11601 * Register it at the following index 11602 */ 11603 _Tech.registerSourceHandler = function (handler, index) { 11604 var handlers = _Tech.sourceHandlers; 11605 11606 if (!handlers) { 11607 handlers = _Tech.sourceHandlers = []; 11608 } 11609 11610 if (index === undefined) { 11611 // add to the end of the list 11612 index = handlers.length; 11613 } 11614 11615 handlers.splice(index, 0, handler); 11616 }; 11617 /** 11618 * Check if the tech can support the given type. Also checks the 11619 * Techs sourceHandlers. 11620 * 11621 * @param {string} type 11622 * The mimetype to check. 11623 * 11624 * @return {string} 11625 * 'probably', 'maybe', or '' (empty string) 11626 */ 11627 11628 11629 _Tech.canPlayType = function (type) { 11630 var handlers = _Tech.sourceHandlers || []; 11631 var can; 11632 11633 for (var i = 0; i < handlers.length; i++) { 11634 can = handlers[i].canPlayType(type); 11635 11636 if (can) { 11637 return can; 11638 } 11639 } 11640 11641 return ''; 11642 }; 11643 /** 11644 * Returns the first source handler that supports the source. 11645 * 11646 * TODO: Answer question: should 'probably' be prioritized over 'maybe' 11647 * 11648 * @param {Tech~SourceObject} source 11649 * The source object 11650 * 11651 * @param {Object} options 11652 * The options passed to the tech 11653 * 11654 * @return {SourceHandler|null} 11655 * The first source handler that supports the source or null if 11656 * no SourceHandler supports the source 11657 */ 11658 11659 11660 _Tech.selectSourceHandler = function (source, options) { 11661 var handlers = _Tech.sourceHandlers || []; 11662 var can; 11663 11664 for (var i = 0; i < handlers.length; i++) { 11665 can = handlers[i].canHandleSource(source, options); 11666 11667 if (can) { 11668 return handlers[i]; 11669 } 11670 } 11671 11672 return null; 11673 }; 11674 /** 11675 * Check if the tech can support the given source. 11676 * 11677 * @param {Tech~SourceObject} srcObj 11678 * The source object 11679 * 11680 * @param {Object} options 11681 * The options passed to the tech 11682 * 11683 * @return {string} 11684 * 'probably', 'maybe', or '' (empty string) 11685 */ 11686 11687 11688 _Tech.canPlaySource = function (srcObj, options) { 11689 var sh = _Tech.selectSourceHandler(srcObj, options); 11690 11691 if (sh) { 11692 return sh.canHandleSource(srcObj, options); 11693 } 11694 11695 return ''; 11696 }; 11697 /** 11698 * When using a source handler, prefer its implementation of 11699 * any function normally provided by the tech. 11700 */ 11701 11702 11703 var deferrable = ['seekable', 'seeking', 'duration']; 11704 /** 11705 * A wrapper around {@link Tech#seekable} that will call a `SourceHandler`s seekable 11706 * function if it exists, with a fallback to the Techs seekable function. 11707 * 11708 * @method _Tech.seekable 11709 */ 11710 11711 /** 11712 * A wrapper around {@link Tech#duration} that will call a `SourceHandler`s duration 11713 * function if it exists, otherwise it will fallback to the techs duration function. 11714 * 11715 * @method _Tech.duration 11716 */ 11717 11718 deferrable.forEach(function (fnName) { 11719 var originalFn = this[fnName]; 11720 11721 if (typeof originalFn !== 'function') { 11722 return; 11723 } 11724 11725 this[fnName] = function () { 11726 if (this.sourceHandler_ && this.sourceHandler_[fnName]) { 11727 return this.sourceHandler_[fnName].apply(this.sourceHandler_, arguments); 11728 } 11729 11730 return originalFn.apply(this, arguments); 11731 }; 11732 }, _Tech.prototype); 11733 /** 11734 * Create a function for setting the source using a source object 11735 * and source handlers. 11736 * Should never be called unless a source handler was found. 11737 * 11738 * @param {Tech~SourceObject} source 11739 * A source object with src and type keys 11740 */ 11741 11742 _Tech.prototype.setSource = function (source) { 11743 var sh = _Tech.selectSourceHandler(source, this.options_); 11744 11745 if (!sh) { 11746 // Fall back to a native source hander when unsupported sources are 11747 // deliberately set 11748 if (_Tech.nativeSourceHandler) { 11749 sh = _Tech.nativeSourceHandler; 11750 } else { 11751 log.error('No source handler found for the current source.'); 11752 } 11753 } // Dispose any existing source handler 11754 11755 11756 this.disposeSourceHandler(); 11757 this.off('dispose', this.disposeSourceHandler); 11758 11759 if (sh !== _Tech.nativeSourceHandler) { 11760 this.currentSource_ = source; 11761 } 11762 11763 this.sourceHandler_ = sh.handleSource(source, this, this.options_); 11764 this.one('dispose', this.disposeSourceHandler); 11765 }; 11766 /** 11767 * Clean up any existing SourceHandlers and listeners when the Tech is disposed. 11768 * 11769 * @listens Tech#dispose 11770 */ 11771 11772 11773 _Tech.prototype.disposeSourceHandler = function () { 11774 // if we have a source and get another one 11775 // then we are loading something new 11776 // than clear all of our current tracks 11777 if (this.currentSource_) { 11778 this.clearTracks(['audio', 'video']); 11779 this.currentSource_ = null; 11780 } // always clean up auto-text tracks 11781 11782 11783 this.cleanupAutoTextTracks(); 11784 11785 if (this.sourceHandler_) { 11786 if (this.sourceHandler_.dispose) { 11787 this.sourceHandler_.dispose(); 11788 } 11789 11790 this.sourceHandler_ = null; 11791 } 11792 }; 11793 };
vendor: 4,490 bytes, lines 11793-11943
11793 // The base Tech class needs to be registered as a Component. It is the only 11794 // Tech that can be registered as a Component. 11795 11796 11797 Component.registerComponent('Tech', Tech); 11798 Tech.registerTech('Tech', Tech); 11799 /** 11800 * A list of techs that should be added to techOrder on Players 11801 * 11802 * @private 11803 */ 11804 11805 Tech.defaultTechOrder_ = []; 11806 11807 /** 11808 * @file middleware.js 11809 * @module middleware 11810 */ 11811 var middlewares = {}; 11812 var middlewareInstances = {}; 11813 var TERMINATOR = {}; 11814 /** 11815 * A middleware object is a plain JavaScript object that has methods that 11816 * match the {@link Tech} methods found in the lists of allowed 11817 * {@link module:middleware.allowedGetters|getters}, 11818 * {@link module:middleware.allowedSetters|setters}, and 11819 * {@link module:middleware.allowedMediators|mediators}. 11820 * 11821 * @typedef {Object} MiddlewareObject 11822 */ 11823 11824 /** 11825 * A middleware factory function that should return a 11826 * {@link module:middleware~MiddlewareObject|MiddlewareObject}. 11827 * 11828 * This factory will be called for each player when needed, with the player 11829 * passed in as an argument. 11830 * 11831 * @callback MiddlewareFactory 11832 * @param {Player} player 11833 * A Video.js player. 11834 */ 11835 11836 /** 11837 * Define a middleware that the player should use by way of a factory function 11838 * that returns a middleware object. 11839 * 11840 * @param {string} type 11841 * The MIME type to match or `"*"` for all MIME types. 11842 * 11843 * @param {MiddlewareFactory} middleware 11844 * A middleware factory function that will be executed for 11845 * matching types. 11846 */ 11847 11848 function use(type, middleware) { 11849 middlewares[type] = middlewares[type] || []; 11850 middlewares[type].push(middleware); 11851 } 11852 /** 11853 * Asynchronously sets a source using middleware by recursing through any 11854 * matching middlewares and calling `setSource` on each, passing along the 11855 * previous returned value each time. 11856 * 11857 * @param {Player} player 11858 * A {@link Player} instance. 11859 * 11860 * @param {Tech~SourceObject} src 11861 * A source object. 11862 * 11863 * @param {Function} 11864 * The next middleware to run. 11865 */ 11866 11867 function setSource(player, src, next) { 11868 player.setTimeout(function () { 11869 return setSourceHelper(src, middlewares[src.type], next, player); 11870 }, 1); 11871 } 11872 /** 11873 * When the tech is set, passes the tech to each middleware's `setTech` method. 11874 * 11875 * @param {Object[]} middleware 11876 * An array of middleware instances. 11877 * 11878 * @param {Tech} tech 11879 * A Video.js tech. 11880 */ 11881 11882 function setTech(middleware, tech) { 11883 middleware.forEach(function (mw) { 11884 return mw.setTech && mw.setTech(tech); 11885 }); 11886 } 11887 /** 11888 * Calls a getter on the tech first, through each middleware 11889 * from right to left to the player. 11890 * 11891 * @param {Object[]} middleware 11892 * An array of middleware instances. 11893 * 11894 * @param {Tech} tech 11895 * The current tech. 11896 * 11897 * @param {string} method 11898 * A method name. 11899 * 11900 * @return {Mixed} 11901 * The final value from the tech after middleware has intercepted it. 11902 */ 11903 11904 function get(middleware, tech, method) { 11905 return middleware.reduceRight(middlewareIterator(method), tech[method]()); 11906 } 11907 /** 11908 * Takes the argument given to the player and calls the setter method on each 11909 * middleware from left to right to the tech. 11910 * 11911 * @param {Object[]} middleware 11912 * An array of middleware instances. 11913 * 11914 * @param {Tech} tech 11915 * The current tech. 11916 * 11917 * @param {string} method 11918 * A method name. 11919 * 11920 * @param {Mixed} arg 11921 * The value to set on the tech. 11922 * 11923 * @return {Mixed} 11924 * The return value of the `method` of the `tech`. 11925 */ 11926 11927 function set(middleware, tech, method, arg) { 11928 return tech[method](middleware.reduce(middlewareIterator(method), arg)); 11929 } 11930 /** 11931 * Takes the argument given to the player and calls the `call` version of the 11932 * method on each middleware from left to right. 11933 * 11934 * Then, call the passed in method on the tech and return the result unchanged 11935 * back to the player, through middleware, this time from right to left. 11936 * 11937 * @param {Object[]} middleware 11938 * An array of middleware instances. 11939 * 11940 * @param {Tech} tech 11941 * The current tech. 11942 * 11943 * @param {string} method
vendor: 191,679 bytes, lines 11944-18483
11944 * A method name. 11945 * 11946 * @param {Mixed} arg 11947 * The value to set on the tech. 11948 * 11949 * @return {Mixed} 11950 * The return value of the `method` of the `tech`, regardless of the 11951 * return values of middlewares. 11952 */ 11953 11954 function mediate(middleware, tech, method, arg) { 11955 if (arg === void 0) { 11956 arg = null; 11957 } 11958 11959 var callMethod = 'call' + toTitleCase(method); 11960 var middlewareValue = middleware.reduce(middlewareIterator(callMethod), arg); 11961 var terminated = middlewareValue === TERMINATOR; // deprecated. The `null` return value should instead return TERMINATOR to 11962 // prevent confusion if a techs method actually returns null. 11963 11964 var returnValue = terminated ? null : tech[method](middlewareValue); 11965 executeRight(middleware, method, returnValue, terminated); 11966 return returnValue; 11967 } 11968 /** 11969 * Enumeration of allowed getters where the keys are method names. 11970 * 11971 * @type {Object} 11972 */ 11973 11974 var allowedGetters = { 11975 buffered: 1, 11976 currentTime: 1, 11977 duration: 1, 11978 muted: 1, 11979 played: 1, 11980 paused: 1, 11981 seekable: 1, 11982 volume: 1 11983 }; 11984 /** 11985 * Enumeration of allowed setters where the keys are method names. 11986 * 11987 * @type {Object} 11988 */ 11989 11990 var allowedSetters = { 11991 setCurrentTime: 1, 11992 setMuted: 1, 11993 setVolume: 1 11994 }; 11995 /** 11996 * Enumeration of allowed mediators where the keys are method names. 11997 * 11998 * @type {Object} 11999 */ 12000 12001 var allowedMediators = { 12002 play: 1, 12003 pause: 1 12004 }; 12005 12006 function middlewareIterator(method) { 12007 return function (value, mw) { 12008 // if the previous middleware terminated, pass along the termination 12009 if (value === TERMINATOR) { 12010 return TERMINATOR; 12011 } 12012 12013 if (mw[method]) { 12014 return mw[method](value); 12015 } 12016 12017 return value; 12018 }; 12019 } 12020 12021 function executeRight(mws, method, value, terminated) { 12022 for (var i = mws.length - 1; i >= 0; i--) { 12023 var mw = mws[i]; 12024 12025 if (mw[method]) { 12026 mw[method](terminated, value); 12027 } 12028 } 12029 } 12030 /** 12031 * Clear the middleware cache for a player. 12032 * 12033 * @param {Player} player 12034 * A {@link Player} instance. 12035 */ 12036 12037 12038 function clearCacheForPlayer(player) { 12039 middlewareInstances[player.id()] = null; 12040 } 12041 /** 12042 * { 12043 * [playerId]: [[mwFactory, mwInstance], ...] 12044 * } 12045 * 12046 * @private 12047 */ 12048 12049 function getOrCreateFactory(player, mwFactory) { 12050 var mws = middlewareInstances[player.id()]; 12051 var mw = null; 12052 12053 if (mws === undefined || mws === null) { 12054 mw = mwFactory(player); 12055 middlewareInstances[player.id()] = [[mwFactory, mw]]; 12056 return mw; 12057 } 12058 12059 for (var i = 0; i < mws.length; i++) { 12060 var _mws$i = mws[i], 12061 mwf = _mws$i[0], 12062 mwi = _mws$i[1]; 12063 12064 if (mwf !== mwFactory) { 12065 continue; 12066 } 12067 12068 mw = mwi; 12069 } 12070 12071 if (mw === null) { 12072 mw = mwFactory(player); 12073 mws.push([mwFactory, mw]); 12074 } 12075 12076 return mw; 12077 } 12078 12079 function setSourceHelper(src, middleware, next, player, acc, lastRun) { 12080 if (src === void 0) { 12081 src = {}; 12082 } 12083 12084 if (middleware === void 0) { 12085 middleware = []; 12086 } 12087 12088 if (acc === void 0) { 12089 acc = []; 12090 } 12091 12092 if (lastRun === void 0) { 12093 lastRun = false; 12094 } 12095 12096 var _middleware = middleware, 12097 mwFactory = _middleware[0], 12098 mwrest = _middleware.slice(1); // if mwFactory is a string, then we're at a fork in the road 12099 12100 12101 if (typeof mwFactory === 'string') { 12102 setSourceHelper(src, middlewares[mwFactory], next, player, acc, lastRun); // if we have an mwFactory, call it with the player to get the mw, 12103 // then call the mw's setSource method 12104 } else if (mwFactory) { 12105 var mw = getOrCreateFactory(player, mwFactory); // if setSource isn't present, implicitly select this middleware 12106 12107 if (!mw.setSource) { 12108 acc.push(mw); 12109 return setSourceHelper(src, mwrest, next, player, acc, lastRun); 12110 } 12111 12112 mw.setSource(assign({}, src), function (err, _src) { 12113 // something happened, try the next middleware on the current level 12114 // make sure to use the old src 12115 if (err) { 12116 return setSourceHelper(src, mwrest, next, player, acc, lastRun); 12117 } // we've succeeded, now we need to go deeper 12118 12119 12120 acc.push(mw); // if it's the same type, continue down the current chain 12121 // otherwise, we want to go down the new chain 12122 12123 setSourceHelper(_src, src.type === _src.type ? mwrest : middlewares[_src.type], next, player, acc, lastRun); 12124 }); 12125 } else if (mwrest.length) { 12126 setSourceHelper(src, mwrest, next, player, acc, lastRun); 12127 } else if (lastRun) { 12128 next(src, acc); 12129 } else { 12130 setSourceHelper(src, middlewares['*'], next, player, acc, true); 12131 } 12132 } 12133 12134 /** 12135 * Mimetypes 12136 * 12137 * @see http://hul.harvard.edu/ois/////systems/wax/wax-public-help/mimetypes.htm 12138 * @typedef Mimetypes~Kind 12139 * @enum 12140 */ 12141 12142 var MimetypesKind = { 12143 opus: 'video/ogg', 12144 ogv: 'video/ogg', 12145 mp4: 'video/mp4', 12146 mov: 'video/mp4', 12147 m4v: 'video/mp4', 12148 mkv: 'video/x-matroska', 12149 m4a: 'audio/mp4', 12150 mp3: 'audio/mpeg', 12151 aac: 'audio/aac', 12152 oga: 'audio/ogg', 12153 m3u8: 'application/x-mpegURL', 12154 jpg: 'image/jpeg', 12155 jpeg: 'image/jpeg', 12156 gif: 'image/gif', 12157 png: 'image/png', 12158 svg: 'image/svg+xml', 12159 webp: 'image/webp' 12160 }; 12161 /** 12162 * Get the mimetype of a given src url if possible 12163 * 12164 * @param {string} src 12165 * The url to the src 12166 * 12167 * @return {string} 12168 * return the mimetype if it was known or empty string otherwise 12169 */ 12170 12171 var getMimetype = function getMimetype(src) { 12172 if (src === void 0) { 12173 src = ''; 12174 } 12175 12176 var ext = getFileExtension(src); 12177 var mimetype = MimetypesKind[ext.toLowerCase()]; 12178 return mimetype || ''; 12179 }; 12180 /** 12181 * Find the mime type of a given source string if possible. Uses the player 12182 * source cache. 12183 * 12184 * @param {Player} player 12185 * The player object 12186 * 12187 * @param {string} src 12188 * The source string 12189 * 12190 * @return {string} 12191 * The type that was found 12192 */ 12193 12194 var findMimetype = function findMimetype(player, src) { 12195 if (!src) { 12196 return ''; 12197 } // 1. check for the type in the `source` cache 12198 12199 12200 if (player.cache_.source.src === src && player.cache_.source.type) { 12201 return player.cache_.source.type; 12202 } // 2. see if we have this source in our `currentSources` cache 12203 12204 12205 var matchingSources = player.cache_.sources.filter(function (s) { 12206 return s.src === src; 12207 }); 12208 12209 if (matchingSources.length) { 12210 return matchingSources[0].type; 12211 } // 3. look for the src url in source elements and use the type there 12212 12213 12214 var sources = player.$$('source'); 12215 12216 for (var i = 0; i < sources.length; i++) { 12217 var s = sources[i]; 12218 12219 if (s.type && s.src && s.src === src) { 12220 return s.type; 12221 } 12222 } // 4. finally fallback to our list of mime types based on src url extension 12223 12224 12225 return getMimetype(src); 12226 }; 12227 12228 /** 12229 * @module filter-source 12230 */ 12231 /** 12232 * Filter out single bad source objects or multiple source objects in an 12233 * array. Also flattens nested source object arrays into a 1 dimensional 12234 * array of source objects. 12235 * 12236 * @param {Tech~SourceObject|Tech~SourceObject[]} src 12237 * The src object to filter 12238 * 12239 * @return {Tech~SourceObject[]} 12240 * An array of sourceobjects containing only valid sources 12241 * 12242 * @private 12243 */ 12244 12245 var filterSource = function filterSource(src) { 12246 // traverse array 12247 if (Array.isArray(src)) { 12248 var newsrc = []; 12249 src.forEach(function (srcobj) { 12250 srcobj = filterSource(srcobj); 12251 12252 if (Array.isArray(srcobj)) { 12253 newsrc = newsrc.concat(srcobj); 12254 } else if (isObject(srcobj)) { 12255 newsrc.push(srcobj); 12256 } 12257 }); 12258 src = newsrc; 12259 } else if (typeof src === 'string' && src.trim()) { 12260 // convert string into object 12261 src = [fixSource({ 12262 src: src 12263 })]; 12264 } else if (isObject(src) && typeof src.src === 'string' && src.src && src.src.trim()) { 12265 // src is already valid 12266 src = [fixSource(src)]; 12267 } else { 12268 // invalid source, turn it into an empty array 12269 src = []; 12270 } 12271 12272 return src; 12273 }; 12274 /** 12275 * Checks src mimetype, adding it when possible 12276 * 12277 * @param {Tech~SourceObject} src 12278 * The src object to check 12279 * @return {Tech~SourceObject} 12280 * src Object with known type 12281 */ 12282 12283 12284 function fixSource(src) { 12285 if (!src.type) { 12286 var mimetype = getMimetype(src.src); 12287 12288 if (mimetype) { 12289 src.type = mimetype; 12290 } 12291 } 12292 12293 return src; 12294 } 12295 12296 /** 12297 * The `MediaLoader` is the `Component` that decides which playback technology to load 12298 * when a player is initialized. 12299 * 12300 * @extends Component 12301 */ 12302 12303 var MediaLoader = 12304 /*#__PURE__*/ 12305 function (_Component) { 12306 inheritsLoose(MediaLoader, _Component); 12307 12308 /** 12309 * Create an instance of this class. 12310 * 12311 * @param {Player} player 12312 * The `Player` that this class should attach to. 12313 * 12314 * @param {Object} [options] 12315 * The key/value store of player options. 12316 * 12317 * @param {Component~ReadyCallback} [ready] 12318 * The function that is run when this component is ready. 12319 */ 12320 function MediaLoader(player, options, ready) { 12321 var _this; 12322 12323 // MediaLoader has no element 12324 var options_ = mergeOptions({ 12325 createEl: false 12326 }, options); 12327 _this = _Component.call(this, player, options_, ready) || this; // If there are no sources when the player is initialized, 12328 // load the first supported playback technology. 12329 12330 if (!options.playerOptions.sources || options.playerOptions.sources.length === 0) { 12331 for (var i = 0, j = options.playerOptions.techOrder; i < j.length; i++) { 12332 var techName = toTitleCase(j[i]); 12333 var tech = Tech.getTech(techName); // Support old behavior of techs being registered as components. 12334 // Remove once that deprecated behavior is removed. 12335 12336 if (!techName) { 12337 tech = Component.getComponent(techName); 12338 } // Check if the browser supports this technology 12339 12340 12341 if (tech && tech.isSupported()) { 12342 player.loadTech_(techName); 12343 break; 12344 } 12345 } 12346 } else { 12347 // Loop through playback technologies (HTML5, Flash) and check for support. 12348 // Then load the best source. 12349 // A few assumptions here: 12350 // All playback technologies respect preload false. 12351 player.src(options.playerOptions.sources); 12352 } 12353 12354 return _this; 12355 } 12356 12357 return MediaLoader; 12358 }(Component); 12359 12360 Component.registerComponent('MediaLoader', MediaLoader); 12361 12362 /** 12363 * Component which is clickable or keyboard actionable, but is not a 12364 * native HTML button. 12365 * 12366 * @extends Component 12367 */ 12368 12369 var ClickableComponent = 12370 /*#__PURE__*/ 12371 function (_Component) { 12372 inheritsLoose(ClickableComponent, _Component); 12373 12374 /** 12375 * Creates an instance of this class. 12376 * 12377 * @param {Player} player 12378 * The `Player` that this class should be attached to. 12379 * 12380 * @param {Object} [options] 12381 * The key/value store of player options. 12382 * 12383 * @param {function} [options.clickHandler] 12384 * The function to call when the button is clicked / activated 12385 */ 12386 function ClickableComponent(player, options) { 12387 var _this; 12388 12389 _this = _Component.call(this, player, options) || this; 12390 12391 _this.emitTapEvents(); 12392 12393 _this.enable(); 12394 12395 return _this; 12396 } 12397 /** 12398 * Create the `ClickableComponent`s DOM element. 12399 * 12400 * @param {string} [tag=div] 12401 * The element's node type. 12402 * 12403 * @param {Object} [props={}] 12404 * An object of properties that should be set on the element. 12405 * 12406 * @param {Object} [attributes={}] 12407 * An object of attributes that should be set on the element. 12408 * 12409 * @return {Element} 12410 * The element that gets created. 12411 */ 12412 12413 12414 var _proto = ClickableComponent.prototype; 12415 12416 _proto.createEl = function createEl(tag, props, attributes) { 12417 if (tag === void 0) { 12418 tag = 'div'; 12419 } 12420 12421 if (props === void 0) { 12422 props = {}; 12423 } 12424 12425 if (attributes === void 0) { 12426 attributes = {}; 12427 } 12428 12429 props = assign({ 12430 innerHTML: '<span aria-hidden="true" class="vjs-icon-placeholder"></span>', 12431 className: this.buildCSSClass(), 12432 tabIndex: 0 12433 }, props); 12434 12435 if (tag === 'button') { 12436 log.error("Creating a ClickableComponent with an HTML element of " + tag + " is not supported; use a Button instead."); 12437 } // Add ARIA attributes for clickable element which is not a native HTML button 12438 12439 12440 attributes = assign({ 12441 role: 'button' 12442 }, attributes); 12443 this.tabIndex_ = props.tabIndex; 12444 12445 var el = _Component.prototype.createEl.call(this, tag, props, attributes); 12446 12447 this.createControlTextEl(el); 12448 return el; 12449 }; 12450 12451 _proto.dispose = function dispose() { 12452 // remove controlTextEl_ on dispose 12453 this.controlTextEl_ = null; 12454 12455 _Component.prototype.dispose.call(this); 12456 } 12457 /** 12458 * Create a control text element on this `ClickableComponent` 12459 * 12460 * @param {Element} [el] 12461 * Parent element for the control text. 12462 * 12463 * @return {Element} 12464 * The control text element that gets created. 12465 */ 12466 ; 12467 12468 _proto.createControlTextEl = function createControlTextEl(el) { 12469 this.controlTextEl_ = createEl('span', { 12470 className: 'vjs-control-text' 12471 }, { 12472 // let the screen reader user know that the text of the element may change 12473 'aria-live': 'polite' 12474 }); 12475 12476 if (el) { 12477 el.appendChild(this.controlTextEl_); 12478 } 12479 12480 this.controlText(this.controlText_, el); 12481 return this.controlTextEl_; 12482 } 12483 /** 12484 * Get or set the localize text to use for the controls on the `ClickableComponent`. 12485 * 12486 * @param {string} [text] 12487 * Control text for element. 12488 * 12489 * @param {Element} [el=this.el()] 12490 * Element to set the title on. 12491 * 12492 * @return {string} 12493 * - The control text when getting 12494 */ 12495 ; 12496 12497 _proto.controlText = function controlText(text, el) { 12498 if (el === void 0) { 12499 el = this.el(); 12500 } 12501 12502 if (text === undefined) { 12503 return this.controlText_ || 'Need Text'; 12504 } 12505 12506 var localizedText = this.localize(text); 12507 this.controlText_ = text; 12508 textContent(this.controlTextEl_, localizedText); 12509 12510 if (!this.nonIconControl) { 12511 // Set title attribute if only an icon is shown 12512 el.setAttribute('title', localizedText); 12513 } 12514 } 12515 /** 12516 * Builds the default DOM `className`. 12517 * 12518 * @return {string} 12519 * The DOM `className` for this object. 12520 */ 12521 ; 12522 12523 _proto.buildCSSClass = function buildCSSClass() { 12524 return "vjs-control vjs-button " + _Component.prototype.buildCSSClass.call(this); 12525 } 12526 /** 12527 * Enable this `ClickableComponent` 12528 */ 12529 ; 12530 12531 _proto.enable = function enable() { 12532 if (!this.enabled_) { 12533 this.enabled_ = true; 12534 this.removeClass('vjs-disabled'); 12535 this.el_.setAttribute('aria-disabled', 'false'); 12536 12537 if (typeof this.tabIndex_ !== 'undefined') { 12538 this.el_.setAttribute('tabIndex', this.tabIndex_); 12539 } 12540 12541 this.on(['tap', 'click'], this.handleClick); 12542 this.on('keydown', this.handleKeyDown); 12543 } 12544 } 12545 /** 12546 * Disable this `ClickableComponent` 12547 */ 12548 ; 12549 12550 _proto.disable = function disable() { 12551 this.enabled_ = false; 12552 this.addClass('vjs-disabled'); 12553 this.el_.setAttribute('aria-disabled', 'true'); 12554 12555 if (typeof this.tabIndex_ !== 'undefined') { 12556 this.el_.removeAttribute('tabIndex'); 12557 } 12558 12559 this.off('mouseover', this.handleMouseOver); 12560 this.off('mouseout', this.handleMouseOut); 12561 this.off(['tap', 'click'], this.handleClick); 12562 this.off('keydown', this.handleKeyDown); 12563 } 12564 /** 12565 * Event handler that is called when a `ClickableComponent` receives a 12566 * `click` or `tap` event. 12567 * 12568 * @param {EventTarget~Event} event 12569 * The `tap` or `click` event that caused this function to be called. 12570 * 12571 * @listens tap 12572 * @listens click 12573 * @abstract 12574 */ 12575 ; 12576 12577 _proto.handleClick = function handleClick(event) { 12578 if (this.options_.clickHandler) { 12579 this.options_.clickHandler.call(this, arguments); 12580 } 12581 } 12582 /** 12583 * Event handler that is called when a `ClickableComponent` receives a 12584 * `keydown` event. 12585 * 12586 * By default, if the key is Space or Enter, it will trigger a `click` event. 12587 * 12588 * @param {EventTarget~Event} event 12589 * The `keydown` event that caused this function to be called. 12590 * 12591 * @listens keydown 12592 */ 12593 ; 12594 12595 _proto.handleKeyDown = function handleKeyDown(event) { 12596 // Support Space or Enter key operation to fire a click event. Also, 12597 // prevent the event from propagating through the DOM and triggering 12598 // Player hotkeys. 12599 if (keycode.isEventKey(event, 'Space') || keycode.isEventKey(event, 'Enter')) { 12600 event.preventDefault(); 12601 event.stopPropagation(); 12602 this.trigger('click'); 12603 } else { 12604 // Pass keypress handling up for unsupported keys 12605 _Component.prototype.handleKeyDown.call(this, event); 12606 } 12607 }; 12608 12609 return ClickableComponent; 12610 }(Component); 12611 12612 Component.registerComponent('ClickableComponent', ClickableComponent); 12613 12614 /** 12615 * A `ClickableComponent` that handles showing the poster image for the player. 12616 * 12617 * @extends ClickableComponent 12618 */ 12619 12620 var PosterImage = 12621 /*#__PURE__*/ 12622 function (_ClickableComponent) { 12623 inheritsLoose(PosterImage, _ClickableComponent); 12624 12625 /** 12626 * Create an instance of this class. 12627 * 12628 * @param {Player} player 12629 * The `Player` that this class should attach to. 12630 * 12631 * @param {Object} [options] 12632 * The key/value store of player options. 12633 */ 12634 function PosterImage(player, options) { 12635 var _this; 12636 12637 _this = _ClickableComponent.call(this, player, options) || this; 12638 12639 _this.update(); 12640 12641 player.on('posterchange', bind(assertThisInitialized(_this), _this.update)); 12642 return _this; 12643 } 12644 /** 12645 * Clean up and dispose of the `PosterImage`. 12646 */ 12647 12648 12649 var _proto = PosterImage.prototype; 12650 12651 _proto.dispose = function dispose() { 12652 this.player().off('posterchange', this.update); 12653 12654 _ClickableComponent.prototype.dispose.call(this); 12655 } 12656 /** 12657 * Create the `PosterImage`s DOM element. 12658 * 12659 * @return {Element} 12660 * The element that gets created. 12661 */ 12662 ; 12663 12664 _proto.createEl = function createEl$1() { 12665 var el = createEl('div', { 12666 className: 'vjs-poster', 12667 // Don't want poster to be tabbable. 12668 tabIndex: -1 12669 }); 12670 return el; 12671 } 12672 /** 12673 * An {@link EventTarget~EventListener} for {@link Player#posterchange} events. 12674 * 12675 * @listens Player#posterchange 12676 * 12677 * @param {EventTarget~Event} [event] 12678 * The `Player#posterchange` event that triggered this function. 12679 */ 12680 ; 12681 12682 _proto.update = function update(event) { 12683 var url = this.player().poster(); 12684 this.setSrc(url); // If there's no poster source we should display:none on this component 12685 // so it's not still clickable or right-clickable 12686 12687 if (url) { 12688 this.show(); 12689 } else { 12690 this.hide(); 12691 } 12692 } 12693 /** 12694 * Set the source of the `PosterImage` depending on the display method. 12695 * 12696 * @param {string} url 12697 * The URL to the source for the `PosterImage`. 12698 */ 12699 ; 12700 12701 _proto.setSrc = function setSrc(url) { 12702 var backgroundImage = ''; // Any falsy value should stay as an empty string, otherwise 12703 // this will throw an extra error 12704 12705 if (url) { 12706 backgroundImage = "url(\"" + url + "\")"; 12707 } 12708 12709 this.el_.style.backgroundImage = backgroundImage; 12710 } 12711 /** 12712 * An {@link EventTarget~EventListener} for clicks on the `PosterImage`. See 12713 * {@link ClickableComponent#handleClick} for instances where this will be triggered. 12714 * 12715 * @listens tap 12716 * @listens click 12717 * @listens keydown 12718 * 12719 * @param {EventTarget~Event} event 12720 + The `click`, `tap` or `keydown` event that caused this function to be called. 12721 */ 12722 ; 12723 12724 _proto.handleClick = function handleClick(event) { 12725 // We don't want a click to trigger playback when controls are disabled 12726 if (!this.player_.controls()) { 12727 return; 12728 } 12729 12730 if (this.player_.tech(true)) { 12731 this.player_.tech(true).focus(); 12732 } 12733 12734 if (this.player_.paused()) { 12735 silencePromise(this.player_.play()); 12736 } else { 12737 this.player_.pause(); 12738 } 12739 }; 12740 12741 return PosterImage; 12742 }(ClickableComponent); 12743 12744 Component.registerComponent('PosterImage', PosterImage); 12745 12746 var darkGray = '#222'; 12747 var lightGray = '#ccc'; 12748 var fontMap = { 12749 monospace: 'monospace', 12750 sansSerif: 'sans-serif', 12751 serif: 'serif', 12752 monospaceSansSerif: '"Andale Mono", "Lucida Console", monospace', 12753 monospaceSerif: '"Courier New", monospace', 12754 proportionalSansSerif: 'sans-serif', 12755 proportionalSerif: 'serif', 12756 casual: '"Comic Sans MS", Impact, fantasy', 12757 script: '"Monotype Corsiva", cursive', 12758 smallcaps: '"Andale Mono", "Lucida Console", monospace, sans-serif' 12759 }; 12760 /** 12761 * Construct an rgba color from a given hex color code. 12762 * 12763 * @param {number} color 12764 * Hex number for color, like #f0e or #f604e2. 12765 * 12766 * @param {number} opacity 12767 * Value for opacity, 0.0 - 1.0. 12768 * 12769 * @return {string} 12770 * The rgba color that was created, like 'rgba(255, 0, 0, 0.3)'. 12771 */ 12772 12773 function constructColor(color, opacity) { 12774 var hex; 12775 12776 if (color.length === 4) { 12777 // color looks like "#f0e" 12778 hex = color[1] + color[1] + color[2] + color[2] + color[3] + color[3]; 12779 } else if (color.length === 7) { 12780 // color looks like "#f604e2" 12781 hex = color.slice(1); 12782 } else { 12783 throw new Error('Invalid color code provided, ' + color + '; must be formatted as e.g. #f0e or #f604e2.'); 12784 } 12785 12786 return 'rgba(' + parseInt(hex.slice(0, 2), 16) + ',' + parseInt(hex.slice(2, 4), 16) + ',' + parseInt(hex.slice(4, 6), 16) + ',' + opacity + ')'; 12787 } 12788 /** 12789 * Try to update the style of a DOM element. Some style changes will throw an error, 12790 * particularly in IE8. Those should be noops. 12791 * 12792 * @param {Element} el 12793 * The DOM element to be styled. 12794 * 12795 * @param {string} style 12796 * The CSS property on the element that should be styled. 12797 * 12798 * @param {string} rule 12799 * The style rule that should be applied to the property. 12800 * 12801 * @private 12802 */ 12803 12804 function tryUpdateStyle(el, style, rule) { 12805 try { 12806 el.style[style] = rule; 12807 } catch (e) { 12808 // Satisfies linter. 12809 return; 12810 } 12811 } 12812 /** 12813 * The component for displaying text track cues. 12814 * 12815 * @extends Component 12816 */ 12817 12818 12819 var TextTrackDisplay = 12820 /*#__PURE__*/ 12821 function (_Component) { 12822 inheritsLoose(TextTrackDisplay, _Component); 12823 12824 /** 12825 * Creates an instance of this class. 12826 * 12827 * @param {Player} player 12828 * The `Player` that this class should be attached to. 12829 * 12830 * @param {Object} [options] 12831 * The key/value store of player options. 12832 * 12833 * @param {Component~ReadyCallback} [ready] 12834 * The function to call when `TextTrackDisplay` is ready. 12835 */ 12836 function TextTrackDisplay(player, options, ready) { 12837 var _this; 12838 12839 _this = _Component.call(this, player, options, ready) || this; 12840 var updateDisplayHandler = bind(assertThisInitialized(_this), _this.updateDisplay); 12841 player.on('loadstart', bind(assertThisInitialized(_this), _this.toggleDisplay)); 12842 player.on('texttrackchange', updateDisplayHandler); 12843 player.on('loadedmetadata', bind(assertThisInitialized(_this), _this.preselectTrack)); // This used to be called during player init, but was causing an error 12844 // if a track should show by default and the display hadn't loaded yet. 12845 // Should probably be moved to an external track loader when we support 12846 // tracks that don't need a display. 12847 12848 player.ready(bind(assertThisInitialized(_this), function () { 12849 if (player.tech_ && player.tech_.featuresNativeTextTracks) { 12850 this.hide(); 12851 return; 12852 } 12853 12854 player.on('fullscreenchange', updateDisplayHandler); 12855 player.on('playerresize', updateDisplayHandler); 12856 window$1.addEventListener('orientationchange', updateDisplayHandler); 12857 player.on('dispose', function () { 12858 return window$1.removeEventListener('orientationchange', updateDisplayHandler); 12859 }); 12860 var tracks = this.options_.playerOptions.tracks || []; 12861 12862 for (var i = 0; i < tracks.length; i++) { 12863 this.player_.addRemoteTextTrack(tracks[i], true); 12864 } 12865 12866 this.preselectTrack(); 12867 })); 12868 return _this; 12869 } 12870 /** 12871 * Preselect a track following this precedence: 12872 * - matches the previously selected {@link TextTrack}'s language and kind 12873 * - matches the previously selected {@link TextTrack}'s language only 12874 * - is the first default captions track 12875 * - is the first default descriptions track 12876 * 12877 * @listens Player#loadstart 12878 */ 12879 12880 12881 var _proto = TextTrackDisplay.prototype; 12882 12883 _proto.preselectTrack = function preselectTrack() { 12884 var modes = { 12885 captions: 1, 12886 subtitles: 1 12887 }; 12888 var trackList = this.player_.textTracks(); 12889 var userPref = this.player_.cache_.selectedLanguage; 12890 var firstDesc; 12891 var firstCaptions; 12892 var preferredTrack; 12893 12894 for (var i = 0; i < trackList.length; i++) { 12895 var track = trackList[i]; 12896 12897 if (userPref && userPref.enabled && userPref.language && userPref.language === track.language && track.kind in modes) { 12898 // Always choose the track that matches both language and kind 12899 if (track.kind === userPref.kind) { 12900 preferredTrack = track; // or choose the first track that matches language 12901 } else if (!preferredTrack) { 12902 preferredTrack = track; 12903 } // clear everything if offTextTrackMenuItem was clicked 12904 12905 } else if (userPref && !userPref.enabled) { 12906 preferredTrack = null; 12907 firstDesc = null; 12908 firstCaptions = null; 12909 } else if (track["default"]) { 12910 if (track.kind === 'descriptions' && !firstDesc) { 12911 firstDesc = track; 12912 } else if (track.kind in modes && !firstCaptions) { 12913 firstCaptions = track; 12914 } 12915 } 12916 } // The preferredTrack matches the user preference and takes 12917 // precedence over all the other tracks. 12918 // So, display the preferredTrack before the first default track 12919 // and the subtitles/captions track before the descriptions track 12920 12921 12922 if (preferredTrack) { 12923 preferredTrack.mode = 'showing'; 12924 } else if (firstCaptions) { 12925 firstCaptions.mode = 'showing'; 12926 } else if (firstDesc) { 12927 firstDesc.mode = 'showing'; 12928 } 12929 } 12930 /** 12931 * Turn display of {@link TextTrack}'s from the current state into the other state. 12932 * There are only two states: 12933 * - 'shown' 12934 * - 'hidden' 12935 * 12936 * @listens Player#loadstart 12937 */ 12938 ; 12939 12940 _proto.toggleDisplay = function toggleDisplay() { 12941 if (this.player_.tech_ && this.player_.tech_.featuresNativeTextTracks) { 12942 this.hide(); 12943 } else { 12944 this.show(); 12945 } 12946 } 12947 /** 12948 * Create the {@link Component}'s DOM element. 12949 * 12950 * @return {Element} 12951 * The element that was created. 12952 */ 12953 ; 12954 12955 _proto.createEl = function createEl() { 12956 return _Component.prototype.createEl.call(this, 'div', { 12957 className: 'vjs-text-track-display' 12958 }, { 12959 'aria-live': 'off', 12960 'aria-atomic': 'true' 12961 }); 12962 } 12963 /** 12964 * Clear all displayed {@link TextTrack}s. 12965 */ 12966 ; 12967 12968 _proto.clearDisplay = function clearDisplay() { 12969 if (typeof window$1.WebVTT === 'function') { 12970 window$1.WebVTT.processCues(window$1, [], this.el_); 12971 } 12972 } 12973 /** 12974 * Update the displayed TextTrack when a either a {@link Player#texttrackchange} or 12975 * a {@link Player#fullscreenchange} is fired. 12976 * 12977 * @listens Player#texttrackchange 12978 * @listens Player#fullscreenchange 12979 */ 12980 ; 12981 12982 _proto.updateDisplay = function updateDisplay() { 12983 var tracks = this.player_.textTracks(); 12984 var allowMultipleShowingTracks = this.options_.allowMultipleShowingTracks; 12985 this.clearDisplay(); 12986 12987 if (allowMultipleShowingTracks) { 12988 var showingTracks = []; 12989 12990 for (var _i = 0; _i < tracks.length; ++_i) { 12991 var track = tracks[_i]; 12992 12993 if (track.mode !== 'showing') { 12994 continue; 12995 } 12996 12997 showingTracks.push(track); 12998 } 12999 13000 this.updateForTrack(showingTracks); 13001 return; 13002 } // Track display prioritization model: if multiple tracks are 'showing', 13003 // display the first 'subtitles' or 'captions' track which is 'showing', 13004 // otherwise display the first 'descriptions' track which is 'showing' 13005 13006 13007 var descriptionsTrack = null; 13008 var captionsSubtitlesTrack = null; 13009 var i = tracks.length; 13010 13011 while (i--) { 13012 var _track = tracks[i]; 13013 13014 if (_track.mode === 'showing') { 13015 if (_track.kind === 'descriptions') { 13016 descriptionsTrack = _track; 13017 } else { 13018 captionsSubtitlesTrack = _track; 13019 } 13020 } 13021 } 13022 13023 if (captionsSubtitlesTrack) { 13024 if (this.getAttribute('aria-live') !== 'off') { 13025 this.setAttribute('aria-live', 'off'); 13026 } 13027 13028 this.updateForTrack(captionsSubtitlesTrack); 13029 } else if (descriptionsTrack) { 13030 if (this.getAttribute('aria-live') !== 'assertive') { 13031 this.setAttribute('aria-live', 'assertive'); 13032 } 13033 13034 this.updateForTrack(descriptionsTrack); 13035 } 13036 } 13037 /** 13038 * Style {@Link TextTrack} activeCues according to {@Link TextTrackSettings}. 13039 * 13040 * @param {TextTrack} track 13041 * Text track object containing active cues to style. 13042 */ 13043 ; 13044 13045 _proto.updateDisplayState = function updateDisplayState(track) { 13046 var overrides = this.player_.textTrackSettings.getValues(); 13047 var cues = track.activeCues; 13048 var i = cues.length; 13049 13050 while (i--) { 13051 var cue = cues[i]; 13052 13053 if (!cue) { 13054 continue; 13055 } 13056 13057 var cueDiv = cue.displayState; 13058 13059 if (overrides.color) { 13060 cueDiv.firstChild.style.color = overrides.color; 13061 } 13062 13063 if (overrides.textOpacity) { 13064 tryUpdateStyle(cueDiv.firstChild, 'color', constructColor(overrides.color || '#fff', overrides.textOpacity)); 13065 } 13066 13067 if (overrides.backgroundColor) { 13068 cueDiv.firstChild.style.backgroundColor = overrides.backgroundColor; 13069 } 13070 13071 if (overrides.backgroundOpacity) { 13072 tryUpdateStyle(cueDiv.firstChild, 'backgroundColor', constructColor(overrides.backgroundColor || '#000', overrides.backgroundOpacity)); 13073 } 13074 13075 if (overrides.windowColor) { 13076 if (overrides.windowOpacity) { 13077 tryUpdateStyle(cueDiv, 'backgroundColor', constructColor(overrides.windowColor, overrides.windowOpacity)); 13078 } else { 13079 cueDiv.style.backgroundColor = overrides.windowColor; 13080 } 13081 } 13082 13083 if (overrides.edgeStyle) { 13084 if (overrides.edgeStyle === 'dropshadow') { 13085 cueDiv.firstChild.style.textShadow = "2px 2px 3px " + darkGray + ", 2px 2px 4px " + darkGray + ", 2px 2px 5px " + darkGray; 13086 } else if (overrides.edgeStyle === 'raised') { 13087 cueDiv.firstChild.style.textShadow = "1px 1px " + darkGray + ", 2px 2px " + darkGray + ", 3px 3px " + darkGray; 13088 } else if (overrides.edgeStyle === 'depressed') { 13089 cueDiv.firstChild.style.textShadow = "1px 1px " + lightGray + ", 0 1px " + lightGray + ", -1px -1px " + darkGray + ", 0 -1px " + darkGray; 13090 } else if (overrides.edgeStyle === 'uniform') { 13091 cueDiv.firstChild.style.textShadow = "0 0 4px " + darkGray + ", 0 0 4px " + darkGray + ", 0 0 4px " + darkGray + ", 0 0 4px " + darkGray; 13092 } 13093 } 13094 13095 if (overrides.fontPercent && overrides.fontPercent !== 1) { 13096 var fontSize = window$1.parseFloat(cueDiv.style.fontSize); 13097 cueDiv.style.fontSize = fontSize * overrides.fontPercent + 'px'; 13098 cueDiv.style.height = 'auto'; 13099 cueDiv.style.top = 'auto'; 13100 cueDiv.style.bottom = '2px'; 13101 } 13102 13103 if (overrides.fontFamily && overrides.fontFamily !== 'default') { 13104 if (overrides.fontFamily === 'small-caps') { 13105 cueDiv.firstChild.style.fontVariant = 'small-caps'; 13106 } else { 13107 cueDiv.firstChild.style.fontFamily = fontMap[overrides.fontFamily]; 13108 } 13109 } 13110 } 13111 } 13112 /** 13113 * Add an {@link TextTrack} to to the {@link Tech}s {@link TextTrackList}. 13114 * 13115 * @param {TextTrack|TextTrack[]} tracks 13116 * Text track object or text track array to be added to the list. 13117 */ 13118 ; 13119 13120 _proto.updateForTrack = function updateForTrack(tracks) { 13121 if (!Array.isArray(tracks)) { 13122 tracks = [tracks]; 13123 } 13124 13125 if (typeof window$1.WebVTT !== 'function' || tracks.every(function (track) { 13126 return !track.activeCues; 13127 })) { 13128 return; 13129 } 13130 13131 var cues = []; // push all active track cues 13132 13133 for (var i = 0; i < tracks.length; ++i) { 13134 var track = tracks[i]; 13135 13136 for (var j = 0; j < track.activeCues.length; ++j) { 13137 cues.push(track.activeCues[j]); 13138 } 13139 } // removes all cues before it processes new ones 13140 13141 13142 window$1.WebVTT.processCues(window$1, cues, this.el_); // add unique class to each language text track & add settings styling if necessary 13143 13144 for (var _i2 = 0; _i2 < tracks.length; ++_i2) { 13145 var _track2 = tracks[_i2]; 13146 13147 for (var _j = 0; _j < _track2.activeCues.length; ++_j) { 13148 var cueEl = _track2.activeCues[_j].displayState; 13149 addClass(cueEl, 'vjs-text-track-cue'); 13150 addClass(cueEl, 'vjs-text-track-cue-' + (_track2.language ? _track2.language : _i2)); 13151 } 13152 13153 if (this.player_.textTrackSettings) { 13154 this.updateDisplayState(_track2); 13155 } 13156 } 13157 }; 13158 13159 return TextTrackDisplay; 13160 }(Component); 13161 13162 Component.registerComponent('TextTrackDisplay', TextTrackDisplay); 13163 13164 /** 13165 * A loading spinner for use during waiting/loading events. 13166 * 13167 * @extends Component 13168 */ 13169 13170 var LoadingSpinner = 13171 /*#__PURE__*/ 13172 function (_Component) { 13173 inheritsLoose(LoadingSpinner, _Component); 13174 13175 function LoadingSpinner() { 13176 return _Component.apply(this, arguments) || this; 13177 } 13178 13179 var _proto = LoadingSpinner.prototype; 13180 13181 /** 13182 * Create the `LoadingSpinner`s DOM element. 13183 * 13184 * @return {Element} 13185 * The dom element that gets created. 13186 */ 13187 _proto.createEl = function createEl$1() { 13188 var isAudio = this.player_.isAudio(); 13189 var playerType = this.localize(isAudio ? 'Audio Player' : 'Video Player'); 13190 var controlText = createEl('span', { 13191 className: 'vjs-control-text', 13192 innerHTML: this.localize('{1} is loading.', [playerType]) 13193 }); 13194 13195 var el = _Component.prototype.createEl.call(this, 'div', { 13196 className: 'vjs-loading-spinner', 13197 dir: 'ltr' 13198 }); 13199 13200 el.appendChild(controlText); 13201 return el; 13202 }; 13203 13204 return LoadingSpinner; 13205 }(Component); 13206 13207 Component.registerComponent('LoadingSpinner', LoadingSpinner); 13208 13209 /** 13210 * Base class for all buttons. 13211 * 13212 * @extends ClickableComponent 13213 */ 13214 13215 var Button = 13216 /*#__PURE__*/ 13217 function (_ClickableComponent) { 13218 inheritsLoose(Button, _ClickableComponent); 13219 13220 function Button() { 13221 return _ClickableComponent.apply(this, arguments) || this; 13222 } 13223 13224 var _proto = Button.prototype; 13225 13226 /** 13227 * Create the `Button`s DOM element. 13228 * 13229 * @param {string} [tag="button"] 13230 * The element's node type. This argument is IGNORED: no matter what 13231 * is passed, it will always create a `button` element. 13232 * 13233 * @param {Object} [props={}] 13234 * An object of properties that should be set on the element. 13235 * 13236 * @param {Object} [attributes={}] 13237 * An object of attributes that should be set on the element. 13238 * 13239 * @return {Element} 13240 * The element that gets created. 13241 */ 13242 _proto.createEl = function createEl(tag, props, attributes) { 13243 if (props === void 0) { 13244 props = {}; 13245 } 13246 13247 if (attributes === void 0) { 13248 attributes = {}; 13249 } 13250 13251 tag = 'button'; 13252 props = assign({ 13253 innerHTML: '<span aria-hidden="true" class="vjs-icon-placeholder"></span>', 13254 className: this.buildCSSClass() 13255 }, props); // Add attributes for button element 13256 13257 attributes = assign({ 13258 // Necessary since the default button type is "submit" 13259 type: 'button' 13260 }, attributes); 13261 var el = Component.prototype.createEl.call(this, tag, props, attributes); 13262 this.createControlTextEl(el); 13263 return el; 13264 } 13265 /** 13266 * Add a child `Component` inside of this `Button`. 13267 * 13268 * @param {string|Component} child 13269 * The name or instance of a child to add. 13270 * 13271 * @param {Object} [options={}] 13272 * The key/value store of options that will get passed to children of 13273 * the child. 13274 * 13275 * @return {Component} 13276 * The `Component` that gets added as a child. When using a string the 13277 * `Component` will get created by this process. 13278 * 13279 * @deprecated since version 5 13280 */ 13281 ; 13282 13283 _proto.addChild = function addChild(child, options) { 13284 if (options === void 0) { 13285 options = {}; 13286 } 13287 13288 var className = this.constructor.name; 13289 log.warn("Adding an actionable (user controllable) child to a Button (" + className + ") is not supported; use a ClickableComponent instead."); // Avoid the error message generated by ClickableComponent's addChild method 13290 13291 return Component.prototype.addChild.call(this, child, options); 13292 } 13293 /** 13294 * Enable the `Button` element so that it can be activated or clicked. Use this with 13295 * {@link Button#disable}. 13296 */ 13297 ; 13298 13299 _proto.enable = function enable() { 13300 _ClickableComponent.prototype.enable.call(this); 13301 13302 this.el_.removeAttribute('disabled'); 13303 } 13304 /** 13305 * Disable the `Button` element so that it cannot be activated or clicked. Use this with 13306 * {@link Button#enable}. 13307 */ 13308 ; 13309 13310 _proto.disable = function disable() { 13311 _ClickableComponent.prototype.disable.call(this); 13312 13313 this.el_.setAttribute('disabled', 'disabled'); 13314 } 13315 /** 13316 * This gets called when a `Button` has focus and `keydown` is triggered via a key 13317 * press. 13318 * 13319 * @param {EventTarget~Event} event 13320 * The event that caused this function to get called. 13321 * 13322 * @listens keydown 13323 */ 13324 ; 13325 13326 _proto.handleKeyDown = function handleKeyDown(event) { 13327 // Ignore Space or Enter key operation, which is handled by the browser for 13328 // a button - though not for its super class, ClickableComponent. Also, 13329 // prevent the event from propagating through the DOM and triggering Player 13330 // hotkeys. We do not preventDefault here because we _want_ the browser to 13331 // handle it. 13332 if (keycode.isEventKey(event, 'Space') || keycode.isEventKey(event, 'Enter')) { 13333 event.stopPropagation(); 13334 return; 13335 } // Pass keypress handling up for unsupported keys 13336 13337 13338 _ClickableComponent.prototype.handleKeyDown.call(this, event); 13339 }; 13340 13341 return Button; 13342 }(ClickableComponent); 13343 13344 Component.registerComponent('Button', Button); 13345 13346 /** 13347 * The initial play button that shows before the video has played. The hiding of the 13348 * `BigPlayButton` get done via CSS and `Player` states. 13349 * 13350 * @extends Button 13351 */ 13352 13353 var BigPlayButton = 13354 /*#__PURE__*/ 13355 function (_Button) { 13356 inheritsLoose(BigPlayButton, _Button); 13357 13358 function BigPlayButton(player, options) { 13359 var _this; 13360 13361 _this = _Button.call(this, player, options) || this; 13362 _this.mouseused_ = false; 13363 13364 _this.on('mousedown', _this.handleMouseDown); 13365 13366 return _this; 13367 } 13368 /** 13369 * Builds the default DOM `className`. 13370 * 13371 * @return {string} 13372 * The DOM `className` for this object. Always returns 'vjs-big-play-button'. 13373 */ 13374 13375 13376 var _proto = BigPlayButton.prototype; 13377 13378 _proto.buildCSSClass = function buildCSSClass() { 13379 return 'vjs-big-play-button'; 13380 } 13381 /** 13382 * This gets called when a `BigPlayButton` "clicked". See {@link ClickableComponent} 13383 * for more detailed information on what a click can be. 13384 * 13385 * @param {EventTarget~Event} event 13386 * The `keydown`, `tap`, or `click` event that caused this function to be 13387 * called. 13388 * 13389 * @listens tap 13390 * @listens click 13391 */ 13392 ; 13393 13394 _proto.handleClick = function handleClick(event) { 13395 var playPromise = this.player_.play(); // exit early if clicked via the mouse 13396 13397 if (this.mouseused_ && event.clientX && event.clientY) { 13398 var sourceIsEncrypted = this.player_.usingPlugin('eme') && this.player_.eme.sessions && this.player_.eme.sessions.length > 0; 13399 silencePromise(playPromise); 13400 13401 if (this.player_.tech(true) && // We've observed a bug in IE and Edge when playing back DRM content where 13402 // calling .focus() on the video element causes the video to go black, 13403 // so we avoid it in that specific case 13404 !((IE_VERSION || IS_EDGE) && sourceIsEncrypted)) { 13405 this.player_.tech(true).focus(); 13406 } 13407 13408 return; 13409 } 13410 13411 var cb = this.player_.getChild('controlBar'); 13412 var playToggle = cb && cb.getChild('playToggle'); 13413 13414 if (!playToggle) { 13415 this.player_.tech(true).focus(); 13416 return; 13417 } 13418 13419 var playFocus = function playFocus() { 13420 return playToggle.focus(); 13421 }; 13422 13423 if (isPromise(playPromise)) { 13424 playPromise.then(playFocus, function () {}); 13425 } else { 13426 this.setTimeout(playFocus, 1); 13427 } 13428 }; 13429 13430 _proto.handleKeyDown = function handleKeyDown(event) { 13431 this.mouseused_ = false; 13432 13433 _Button.prototype.handleKeyDown.call(this, event); 13434 }; 13435 13436 _proto.handleMouseDown = function handleMouseDown(event) { 13437 this.mouseused_ = true; 13438 }; 13439 13440 return BigPlayButton; 13441 }(Button); 13442 /** 13443 * The text that should display over the `BigPlayButton`s controls. Added to for localization. 13444 * 13445 * @type {string} 13446 * @private 13447 */ 13448 13449 13450 BigPlayButton.prototype.controlText_ = 'Play Video'; 13451 Component.registerComponent('BigPlayButton', BigPlayButton); 13452 13453 /** 13454 * The `CloseButton` is a `{@link Button}` that fires a `close` event when 13455 * it gets clicked. 13456 * 13457 * @extends Button 13458 */ 13459 13460 var CloseButton = 13461 /*#__PURE__*/ 13462 function (_Button) { 13463 inheritsLoose(CloseButton, _Button); 13464 13465 /** 13466 * Creates an instance of the this class. 13467 * 13468 * @param {Player} player 13469 * The `Player` that this class should be attached to. 13470 * 13471 * @param {Object} [options] 13472 * The key/value store of player options. 13473 */ 13474 function CloseButton(player, options) { 13475 var _this; 13476 13477 _this = _Button.call(this, player, options) || this; 13478 13479 _this.controlText(options && options.controlText || _this.localize('Close')); 13480 13481 return _this; 13482 } 13483 /** 13484 * Builds the default DOM `className`. 13485 * 13486 * @return {string} 13487 * The DOM `className` for this object. 13488 */ 13489 13490 13491 var _proto = CloseButton.prototype; 13492 13493 _proto.buildCSSClass = function buildCSSClass() { 13494 return "vjs-close-button " + _Button.prototype.buildCSSClass.call(this); 13495 } 13496 /** 13497 * This gets called when a `CloseButton` gets clicked. See 13498 * {@link ClickableComponent#handleClick} for more information on when 13499 * this will be triggered 13500 * 13501 * @param {EventTarget~Event} event 13502 * The `keydown`, `tap`, or `click` event that caused this function to be 13503 * called. 13504 * 13505 * @listens tap 13506 * @listens click 13507 * @fires CloseButton#close 13508 */ 13509 ; 13510 13511 _proto.handleClick = function handleClick(event) { 13512 /** 13513 * Triggered when the a `CloseButton` is clicked. 13514 * 13515 * @event CloseButton#close 13516 * @type {EventTarget~Event} 13517 * 13518 * @property {boolean} [bubbles=false] 13519 * set to false so that the close event does not 13520 * bubble up to parents if there is no listener 13521 */ 13522 this.trigger({ 13523 type: 'close', 13524 bubbles: false 13525 }); 13526 } 13527 /** 13528 * Event handler that is called when a `CloseButton` receives a 13529 * `keydown` event. 13530 * 13531 * By default, if the key is Esc, it will trigger a `click` event. 13532 * 13533 * @param {EventTarget~Event} event 13534 * The `keydown` event that caused this function to be called. 13535 * 13536 * @listens keydown 13537 */ 13538 ; 13539 13540 _proto.handleKeyDown = function handleKeyDown(event) { 13541 // Esc button will trigger `click` event 13542 if (keycode.isEventKey(event, 'Esc')) { 13543 event.preventDefault(); 13544 event.stopPropagation(); 13545 this.trigger('click'); 13546 } else { 13547 // Pass keypress handling up for unsupported keys 13548 _Button.prototype.handleKeyDown.call(this, event); 13549 } 13550 }; 13551 13552 return CloseButton; 13553 }(Button); 13554 13555 Component.registerComponent('CloseButton', CloseButton); 13556 13557 /** 13558 * Button to toggle between play and pause. 13559 * 13560 * @extends Button 13561 */ 13562 13563 var PlayToggle = 13564 /*#__PURE__*/ 13565 function (_Button) { 13566 inheritsLoose(PlayToggle, _Button); 13567 13568 /** 13569 * Creates an instance of this class. 13570 * 13571 * @param {Player} player 13572 * The `Player` that this class should be attached to. 13573 * 13574 * @param {Object} [options={}] 13575 * The key/value store of player options. 13576 */ 13577 function PlayToggle(player, options) { 13578 var _this; 13579 13580 if (options === void 0) { 13581 options = {}; 13582 } 13583 13584 _this = _Button.call(this, player, options) || this; // show or hide replay icon 13585 13586 options.replay = options.replay === undefined || options.replay; 13587 13588 _this.on(player, 'play', _this.handlePlay); 13589 13590 _this.on(player, 'pause', _this.handlePause); 13591 13592 if (options.replay) { 13593 _this.on(player, 'ended', _this.handleEnded); 13594 } 13595 13596 return _this; 13597 } 13598 /** 13599 * Builds the default DOM `className`. 13600 * 13601 * @return {string} 13602 * The DOM `className` for this object. 13603 */ 13604 13605 13606 var _proto = PlayToggle.prototype; 13607 13608 _proto.buildCSSClass = function buildCSSClass() { 13609 return "vjs-play-control " + _Button.prototype.buildCSSClass.call(this); 13610 } 13611 /** 13612 * This gets called when an `PlayToggle` is "clicked". See 13613 * {@link ClickableComponent} for more detailed information on what a click can be. 13614 * 13615 * @param {EventTarget~Event} [event] 13616 * The `keydown`, `tap`, or `click` event that caused this function to be 13617 * called. 13618 * 13619 * @listens tap 13620 * @listens click 13621 */ 13622 ; 13623 13624 _proto.handleClick = function handleClick(event) { 13625 if (this.player_.paused()) { 13626 this.player_.play(); 13627 } else { 13628 this.player_.pause(); 13629 } 13630 } 13631 /** 13632 * This gets called once after the video has ended and the user seeks so that 13633 * we can change the replay button back to a play button. 13634 * 13635 * @param {EventTarget~Event} [event] 13636 * The event that caused this function to run. 13637 * 13638 * @listens Player#seeked 13639 */ 13640 ; 13641 13642 _proto.handleSeeked = function handleSeeked(event) { 13643 this.removeClass('vjs-ended'); 13644 13645 if (this.player_.paused()) { 13646 this.handlePause(event); 13647 } else { 13648 this.handlePlay(event); 13649 } 13650 } 13651 /** 13652 * Add the vjs-playing class to the element so it can change appearance. 13653 * 13654 * @param {EventTarget~Event} [event] 13655 * The event that caused this function to run. 13656 * 13657 * @listens Player#play 13658 */ 13659 ; 13660 13661 _proto.handlePlay = function handlePlay(event) { 13662 this.removeClass('vjs-ended'); 13663 this.removeClass('vjs-paused'); 13664 this.addClass('vjs-playing'); // change the button text to "Pause" 13665 13666 this.controlText('Pause'); 13667 } 13668 /** 13669 * Add the vjs-paused class to the element so it can change appearance. 13670 * 13671 * @param {EventTarget~Event} [event] 13672 * The event that caused this function to run. 13673 * 13674 * @listens Player#pause 13675 */ 13676 ; 13677 13678 _proto.handlePause = function handlePause(event) { 13679 this.removeClass('vjs-playing'); 13680 this.addClass('vjs-paused'); // change the button text to "Play" 13681 13682 this.controlText('Play'); 13683 } 13684 /** 13685 * Add the vjs-ended class to the element so it can change appearance 13686 * 13687 * @param {EventTarget~Event} [event] 13688 * The event that caused this function to run. 13689 * 13690 * @listens Player#ended 13691 */ 13692 ; 13693 13694 _proto.handleEnded = function handleEnded(event) { 13695 this.removeClass('vjs-playing'); 13696 this.addClass('vjs-ended'); // change the button text to "Replay" 13697 13698 this.controlText('Replay'); // on the next seek remove the replay button 13699 13700 this.one(this.player_, 'seeked', this.handleSeeked); 13701 }; 13702 13703 return PlayToggle; 13704 }(Button); 13705 /** 13706 * The text that should display over the `PlayToggle`s controls. Added for localization. 13707 * 13708 * @type {string} 13709 * @private 13710 */ 13711 13712 13713 PlayToggle.prototype.controlText_ = 'Play'; 13714 Component.registerComponent('PlayToggle', PlayToggle); 13715 13716 /** 13717 * @file format-time.js 13718 * @module format-time 13719 */ 13720 13721 /** 13722 * Format seconds as a time string, H:MM:SS or M:SS. Supplying a guide (in 13723 * seconds) will force a number of leading zeros to cover the length of the 13724 * guide. 13725 * 13726 * @private 13727 * @param {number} seconds 13728 * Number of seconds to be turned into a string 13729 * 13730 * @param {number} guide 13731 * Number (in seconds) to model the string after 13732 * 13733 * @return {string} 13734 * Time formatted as H:MM:SS or M:SS 13735 */ 13736 var defaultImplementation = function defaultImplementation(seconds, guide) { 13737 seconds = seconds < 0 ? 0 : seconds; 13738 var s = Math.floor(seconds % 60); 13739 var m = Math.floor(seconds / 60 % 60); 13740 var h = Math.floor(seconds / 3600); 13741 var gm = Math.floor(guide / 60 % 60); 13742 var gh = Math.floor(guide / 3600); // handle invalid times 13743 13744 if (isNaN(seconds) || seconds === Infinity) { 13745 // '-' is false for all relational operators (e.g. <, >=) so this setting 13746 // will add the minimum number of fields specified by the guide 13747 h = m = s = '-'; 13748 } // Check if we need to show hours 13749 13750 13751 h = h > 0 || gh > 0 ? h + ':' : ''; // If hours are showing, we may need to add a leading zero. 13752 // Always show at least one digit of minutes. 13753 13754 m = ((h || gm >= 10) && m < 10 ? '0' + m : m) + ':'; // Check if leading zero is need for seconds 13755 13756 s = s < 10 ? '0' + s : s; 13757 return h + m + s; 13758 }; // Internal pointer to the current implementation. 13759 13760 13761 var implementation = defaultImplementation; 13762 /** 13763 * Replaces the default formatTime implementation with a custom implementation. 13764 * 13765 * @param {Function} customImplementation 13766 * A function which will be used in place of the default formatTime 13767 * implementation. Will receive the current time in seconds and the 13768 * guide (in seconds) as arguments. 13769 */ 13770 13771 function setFormatTime(customImplementation) { 13772 implementation = customImplementation; 13773 } 13774 /** 13775 * Resets formatTime to the default implementation. 13776 */ 13777 13778 function resetFormatTime() { 13779 implementation = defaultImplementation; 13780 } 13781 /** 13782 * Delegates to either the default time formatting function or a custom 13783 * function supplied via `setFormatTime`. 13784 * 13785 * Formats seconds as a time string (H:MM:SS or M:SS). Supplying a 13786 * guide (in seconds) will force a number of leading zeros to cover the 13787 * length of the guide. 13788 * 13789 * @static 13790 * @example formatTime(125, 600) === "02:05" 13791 * @param {number} seconds 13792 * Number of seconds to be turned into a string 13793 * 13794 * @param {number} guide 13795 * Number (in seconds) to model the string after 13796 * 13797 * @return {string} 13798 * Time formatted as H:MM:SS or M:SS 13799 */ 13800 13801 function formatTime(seconds, guide) { 13802 if (guide === void 0) { 13803 guide = seconds; 13804 } 13805 13806 return implementation(seconds, guide); 13807 } 13808 13809 /** 13810 * Displays time information about the video 13811 * 13812 * @extends Component 13813 */ 13814 13815 var TimeDisplay = 13816 /*#__PURE__*/ 13817 function (_Component) { 13818 inheritsLoose(TimeDisplay, _Component); 13819 13820 /** 13821 * Creates an instance of this class. 13822 * 13823 * @param {Player} player 13824 * The `Player` that this class should be attached to. 13825 * 13826 * @param {Object} [options] 13827 * The key/value store of player options. 13828 */ 13829 function TimeDisplay(player, options) { 13830 var _this; 13831 13832 _this = _Component.call(this, player, options) || this; 13833 13834 _this.on(player, ['timeupdate', 'ended'], _this.updateContent); 13835 13836 _this.updateTextNode_(); 13837 13838 return _this; 13839 } 13840 /** 13841 * Create the `Component`'s DOM element 13842 * 13843 * @return {Element} 13844 * The element that was created. 13845 */ 13846 13847 13848 var _proto = TimeDisplay.prototype; 13849 13850 _proto.createEl = function createEl$1() { 13851 var className = this.buildCSSClass(); 13852 13853 var el = _Component.prototype.createEl.call(this, 'div', { 13854 className: className + " vjs-time-control vjs-control", 13855 innerHTML: "<span class=\"vjs-control-text\" role=\"presentation\">" + this.localize(this.labelText_) + "\xA0</span>" 13856 }); 13857 13858 this.contentEl_ = createEl('span', { 13859 className: className + "-display" 13860 }, { 13861 // tell screen readers not to automatically read the time as it changes 13862 'aria-live': 'off', 13863 // span elements have no implicit role, but some screen readers (notably VoiceOver) 13864 // treat them as a break between items in the DOM when using arrow keys 13865 // (or left-to-right swipes on iOS) to read contents of a page. Using 13866 // role='presentation' causes VoiceOver to NOT treat this span as a break. 13867 'role': 'presentation' 13868 }); 13869 el.appendChild(this.contentEl_); 13870 return el; 13871 }; 13872 13873 _proto.dispose = function dispose() { 13874 this.contentEl_ = null; 13875 this.textNode_ = null; 13876 13877 _Component.prototype.dispose.call(this); 13878 } 13879 /** 13880 * Updates the time display text node with a new time 13881 * 13882 * @param {number} [time=0] the time to update to 13883 * 13884 * @private 13885 */ 13886 ; 13887 13888 _proto.updateTextNode_ = function updateTextNode_(time) { 13889 var _this2 = this; 13890 13891 if (time === void 0) { 13892 time = 0; 13893 } 13894 13895 time = formatTime(time); 13896 13897 if (this.formattedTime_ === time) { 13898 return; 13899 } 13900 13901 this.formattedTime_ = time; 13902 this.requestAnimationFrame(function () { 13903 if (!_this2.contentEl_) { 13904 return; 13905 } 13906 13907 var oldNode = _this2.textNode_; 13908 _this2.textNode_ = document.createTextNode(_this2.formattedTime_); 13909 13910 if (!_this2.textNode_) { 13911 return; 13912 } 13913 13914 if (oldNode) { 13915 _this2.contentEl_.replaceChild(_this2.textNode_, oldNode); 13916 } else { 13917 _this2.contentEl_.appendChild(_this2.textNode_); 13918 } 13919 }); 13920 } 13921 /** 13922 * To be filled out in the child class, should update the displayed time 13923 * in accordance with the fact that the current time has changed. 13924 * 13925 * @param {EventTarget~Event} [event] 13926 * The `timeupdate` event that caused this to run. 13927 * 13928 * @listens Player#timeupdate 13929 */ 13930 ; 13931 13932 _proto.updateContent = function updateContent(event) {}; 13933 13934 return TimeDisplay; 13935 }(Component); 13936 /** 13937 * The text that is added to the `TimeDisplay` for screen reader users. 13938 * 13939 * @type {string} 13940 * @private 13941 */ 13942 13943 13944 TimeDisplay.prototype.labelText_ = 'Time'; 13945 /** 13946 * The text that should display over the `TimeDisplay`s controls. Added to for localization. 13947 * 13948 * @type {string} 13949 * @private 13950 * 13951 * @deprecated in v7; controlText_ is not used in non-active display Components 13952 */ 13953 13954 TimeDisplay.prototype.controlText_ = 'Time'; 13955 Component.registerComponent('TimeDisplay', TimeDisplay); 13956 13957 /** 13958 * Displays the current time 13959 * 13960 * @extends Component 13961 */ 13962 13963 var CurrentTimeDisplay = 13964 /*#__PURE__*/ 13965 function (_TimeDisplay) { 13966 inheritsLoose(CurrentTimeDisplay, _TimeDisplay); 13967 13968 function CurrentTimeDisplay() { 13969 return _TimeDisplay.apply(this, arguments) || this; 13970 } 13971 13972 var _proto = CurrentTimeDisplay.prototype; 13973 13974 /** 13975 * Builds the default DOM `className`. 13976 * 13977 * @return {string} 13978 * The DOM `className` for this object. 13979 */ 13980 _proto.buildCSSClass = function buildCSSClass() { 13981 return 'vjs-current-time'; 13982 } 13983 /** 13984 * Update current time display 13985 * 13986 * @param {EventTarget~Event} [event] 13987 * The `timeupdate` event that caused this function to run. 13988 * 13989 * @listens Player#timeupdate 13990 */ 13991 ; 13992 13993 _proto.updateContent = function updateContent(event) { 13994 // Allows for smooth scrubbing, when player can't keep up. 13995 var time; 13996 13997 if (this.player_.ended()) { 13998 time = this.player_.duration(); 13999 } else { 14000 time = this.player_.scrubbing() ? this.player_.getCache().currentTime : this.player_.currentTime(); 14001 } 14002 14003 this.updateTextNode_(time); 14004 }; 14005 14006 return CurrentTimeDisplay; 14007 }(TimeDisplay); 14008 /** 14009 * The text that is added to the `CurrentTimeDisplay` for screen reader users. 14010 * 14011 * @type {string} 14012 * @private 14013 */ 14014 14015 14016 CurrentTimeDisplay.prototype.labelText_ = 'Current Time'; 14017 /** 14018 * The text that should display over the `CurrentTimeDisplay`s controls. Added to for localization. 14019 * 14020 * @type {string} 14021 * @private 14022 * 14023 * @deprecated in v7; controlText_ is not used in non-active display Components 14024 */ 14025 14026 CurrentTimeDisplay.prototype.controlText_ = 'Current Time'; 14027 Component.registerComponent('CurrentTimeDisplay', CurrentTimeDisplay); 14028 14029 /** 14030 * Displays the duration 14031 * 14032 * @extends Component 14033 */ 14034 14035 var DurationDisplay = 14036 /*#__PURE__*/ 14037 function (_TimeDisplay) { 14038 inheritsLoose(DurationDisplay, _TimeDisplay); 14039 14040 /** 14041 * Creates an instance of this class. 14042 * 14043 * @param {Player} player 14044 * The `Player` that this class should be attached to. 14045 * 14046 * @param {Object} [options] 14047 * The key/value store of player options. 14048 */ 14049 function DurationDisplay(player, options) { 14050 var _this; 14051 14052 _this = _TimeDisplay.call(this, player, options) || this; // we do not want to/need to throttle duration changes, 14053 // as they should always display the changed duration as 14054 // it has changed 14055 14056 _this.on(player, 'durationchange', _this.updateContent); // Listen to loadstart because the player duration is reset when a new media element is loaded, 14057 // but the durationchange on the user agent will not fire. 14058 // @see [Spec]{@link https://www.w3.org/TR/2011/WD-html5-20110113/video.html#media-element-load-algorithm} 14059 14060 14061 _this.on(player, 'loadstart', _this.updateContent); // Also listen for timeupdate (in the parent) and loadedmetadata because removing those 14062 // listeners could have broken dependent applications/libraries. These 14063 // can likely be removed for 7.0. 14064 14065 14066 _this.on(player, 'loadedmetadata', _this.updateContent); 14067 14068 return _this; 14069 } 14070 /** 14071 * Builds the default DOM `className`. 14072 * 14073 * @return {string} 14074 * The DOM `className` for this object. 14075 */ 14076 14077 14078 var _proto = DurationDisplay.prototype; 14079 14080 _proto.buildCSSClass = function buildCSSClass() { 14081 return 'vjs-duration'; 14082 } 14083 /** 14084 * Update duration time display. 14085 * 14086 * @param {EventTarget~Event} [event] 14087 * The `durationchange`, `timeupdate`, or `loadedmetadata` event that caused 14088 * this function to be called. 14089 * 14090 * @listens Player#durationchange 14091 * @listens Player#timeupdate 14092 * @listens Player#loadedmetadata 14093 */ 14094 ; 14095 14096 _proto.updateContent = function updateContent(event) { 14097 var duration = this.player_.duration(); 14098 this.updateTextNode_(duration); 14099 }; 14100 14101 return DurationDisplay; 14102 }(TimeDisplay); 14103 /** 14104 * The text that is added to the `DurationDisplay` for screen reader users. 14105 * 14106 * @type {string} 14107 * @private 14108 */ 14109 14110 14111 DurationDisplay.prototype.labelText_ = 'Duration'; 14112 /** 14113 * The text that should display over the `DurationDisplay`s controls. Added to for localization. 14114 * 14115 * @type {string} 14116 * @private 14117 * 14118 * @deprecated in v7; controlText_ is not used in non-active display Components 14119 */ 14120 14121 DurationDisplay.prototype.controlText_ = 'Duration'; 14122 Component.registerComponent('DurationDisplay', DurationDisplay); 14123 14124 /** 14125 * The separator between the current time and duration. 14126 * Can be hidden if it's not needed in the design. 14127 * 14128 * @extends Component 14129 */ 14130 14131 var TimeDivider = 14132 /*#__PURE__*/ 14133 function (_Component) { 14134 inheritsLoose(TimeDivider, _Component); 14135 14136 function TimeDivider() { 14137 return _Component.apply(this, arguments) || this; 14138 } 14139 14140 var _proto = TimeDivider.prototype; 14141 14142 /** 14143 * Create the component's DOM element 14144 * 14145 * @return {Element} 14146 * The element that was created. 14147 */ 14148 _proto.createEl = function createEl() { 14149 return _Component.prototype.createEl.call(this, 'div', { 14150 className: 'vjs-time-control vjs-time-divider', 14151 innerHTML: '<div><span>/</span></div>' 14152 }, { 14153 // this element and its contents can be hidden from assistive techs since 14154 // it is made extraneous by the announcement of the control text 14155 // for the current time and duration displays 14156 'aria-hidden': true 14157 }); 14158 }; 14159 14160 return TimeDivider; 14161 }(Component); 14162 14163 Component.registerComponent('TimeDivider', TimeDivider); 14164 14165 /** 14166 * Displays the time left in the video 14167 * 14168 * @extends Component 14169 */ 14170 14171 var RemainingTimeDisplay = 14172 /*#__PURE__*/ 14173 function (_TimeDisplay) { 14174 inheritsLoose(RemainingTimeDisplay, _TimeDisplay); 14175 14176 /** 14177 * Creates an instance of this class. 14178 * 14179 * @param {Player} player 14180 * The `Player` that this class should be attached to. 14181 * 14182 * @param {Object} [options] 14183 * The key/value store of player options. 14184 */ 14185 function RemainingTimeDisplay(player, options) { 14186 var _this; 14187 14188 _this = _TimeDisplay.call(this, player, options) || this; 14189 14190 _this.on(player, 'durationchange', _this.updateContent); 14191 14192 return _this; 14193 } 14194 /** 14195 * Builds the default DOM `className`. 14196 * 14197 * @return {string} 14198 * The DOM `className` for this object. 14199 */ 14200 14201 14202 var _proto = RemainingTimeDisplay.prototype; 14203 14204 _proto.buildCSSClass = function buildCSSClass() { 14205 return 'vjs-remaining-time'; 14206 } 14207 /** 14208 * Create the `Component`'s DOM element with the "minus" characted prepend to the time 14209 * 14210 * @return {Element} 14211 * The element that was created. 14212 */ 14213 ; 14214 14215 _proto.createEl = function createEl$1() { 14216 var el = _TimeDisplay.prototype.createEl.call(this); 14217 14218 el.insertBefore(createEl('span', {}, { 14219 'aria-hidden': true 14220 }, '-'), this.contentEl_); 14221 return el; 14222 } 14223 /** 14224 * Update remaining time display. 14225 * 14226 * @param {EventTarget~Event} [event] 14227 * The `timeupdate` or `durationchange` event that caused this to run. 14228 * 14229 * @listens Player#timeupdate 14230 * @listens Player#durationchange 14231 */ 14232 ; 14233 14234 _proto.updateContent = function updateContent(event) { 14235 if (typeof this.player_.duration() !== 'number') { 14236 return; 14237 } 14238 14239 var time; // @deprecated We should only use remainingTimeDisplay 14240 // as of video.js 7 14241 14242 if (this.player_.ended()) { 14243 time = 0; 14244 } else if (this.player_.remainingTimeDisplay) { 14245 time = this.player_.remainingTimeDisplay(); 14246 } else { 14247 time = this.player_.remainingTime(); 14248 } 14249 14250 this.updateTextNode_(time); 14251 }; 14252 14253 return RemainingTimeDisplay; 14254 }(TimeDisplay); 14255 /** 14256 * The text that is added to the `RemainingTimeDisplay` for screen reader users. 14257 * 14258 * @type {string} 14259 * @private 14260 */ 14261 14262 14263 RemainingTimeDisplay.prototype.labelText_ = 'Remaining Time'; 14264 /** 14265 * The text that should display over the `RemainingTimeDisplay`s controls. Added to for localization. 14266 * 14267 * @type {string} 14268 * @private 14269 * 14270 * @deprecated in v7; controlText_ is not used in non-active display Components 14271 */ 14272 14273 RemainingTimeDisplay.prototype.controlText_ = 'Remaining Time'; 14274 Component.registerComponent('RemainingTimeDisplay', RemainingTimeDisplay); 14275 14276 /** 14277 * Displays the live indicator when duration is Infinity. 14278 * 14279 * @extends Component 14280 */ 14281 14282 var LiveDisplay = 14283 /*#__PURE__*/ 14284 function (_Component) { 14285 inheritsLoose(LiveDisplay, _Component); 14286 14287 /** 14288 * Creates an instance of this class. 14289 * 14290 * @param {Player} player 14291 * The `Player` that this class should be attached to. 14292 * 14293 * @param {Object} [options] 14294 * The key/value store of player options. 14295 */ 14296 function LiveDisplay(player, options) { 14297 var _this; 14298 14299 _this = _Component.call(this, player, options) || this; 14300 14301 _this.updateShowing(); 14302 14303 _this.on(_this.player(), 'durationchange', _this.updateShowing); 14304 14305 return _this; 14306 } 14307 /** 14308 * Create the `Component`'s DOM element 14309 * 14310 * @return {Element} 14311 * The element that was created. 14312 */ 14313 14314 14315 var _proto = LiveDisplay.prototype; 14316 14317 _proto.createEl = function createEl$1() { 14318 var el = _Component.prototype.createEl.call(this, 'div', { 14319 className: 'vjs-live-control vjs-control' 14320 }); 14321 14322 this.contentEl_ = createEl('div', { 14323 className: 'vjs-live-display', 14324 innerHTML: "<span class=\"vjs-control-text\">" + this.localize('Stream Type') + "\xA0</span>" + this.localize('LIVE') 14325 }, { 14326 'aria-live': 'off' 14327 }); 14328 el.appendChild(this.contentEl_); 14329 return el; 14330 }; 14331 14332 _proto.dispose = function dispose() { 14333 this.contentEl_ = null; 14334 14335 _Component.prototype.dispose.call(this); 14336 } 14337 /** 14338 * Check the duration to see if the LiveDisplay should be showing or not. Then show/hide 14339 * it accordingly 14340 * 14341 * @param {EventTarget~Event} [event] 14342 * The {@link Player#durationchange} event that caused this function to run. 14343 * 14344 * @listens Player#durationchange 14345 */ 14346 ; 14347 14348 _proto.updateShowing = function updateShowing(event) { 14349 if (this.player().duration() === Infinity) { 14350 this.show(); 14351 } else { 14352 this.hide(); 14353 } 14354 }; 14355 14356 return LiveDisplay; 14357 }(Component); 14358 14359 Component.registerComponent('LiveDisplay', LiveDisplay); 14360 14361 /** 14362 * Displays the live indicator when duration is Infinity. 14363 * 14364 * @extends Component 14365 */ 14366 14367 var SeekToLive = 14368 /*#__PURE__*/ 14369 function (_Button) { 14370 inheritsLoose(SeekToLive, _Button); 14371 14372 /** 14373 * Creates an instance of this class. 14374 * 14375 * @param {Player} player 14376 * The `Player` that this class should be attached to. 14377 * 14378 * @param {Object} [options] 14379 * The key/value store of player options. 14380 */ 14381 function SeekToLive(player, options) { 14382 var _this; 14383 14384 _this = _Button.call(this, player, options) || this; 14385 14386 _this.updateLiveEdgeStatus(); 14387 14388 if (_this.player_.liveTracker) { 14389 _this.on(_this.player_.liveTracker, 'liveedgechange', _this.updateLiveEdgeStatus); 14390 } 14391 14392 return _this; 14393 } 14394 /** 14395 * Create the `Component`'s DOM element 14396 * 14397 * @return {Element} 14398 * The element that was created. 14399 */ 14400 14401 14402 var _proto = SeekToLive.prototype; 14403 14404 _proto.createEl = function createEl$1() { 14405 var el = _Button.prototype.createEl.call(this, 'button', { 14406 className: 'vjs-seek-to-live-control vjs-control' 14407 }); 14408 14409 this.textEl_ = createEl('span', { 14410 className: 'vjs-seek-to-live-text', 14411 innerHTML: this.localize('LIVE') 14412 }, { 14413 'aria-hidden': 'true' 14414 }); 14415 el.appendChild(this.textEl_); 14416 return el; 14417 } 14418 /** 14419 * Update the state of this button if we are at the live edge 14420 * or not 14421 */ 14422 ; 14423 14424 _proto.updateLiveEdgeStatus = function updateLiveEdgeStatus(e) { 14425 // default to live edge 14426 if (!this.player_.liveTracker || this.player_.liveTracker.atLiveEdge()) { 14427 this.setAttribute('aria-disabled', true); 14428 this.addClass('vjs-at-live-edge'); 14429 this.controlText('Seek to live, currently playing live'); 14430 } else { 14431 this.setAttribute('aria-disabled', false); 14432 this.removeClass('vjs-at-live-edge'); 14433 this.controlText('Seek to live, currently behind live'); 14434 } 14435 } 14436 /** 14437 * On click bring us as near to the live point as possible. 14438 * This requires that we wait for the next `live-seekable-change` 14439 * event which will happen every segment length seconds. 14440 */ 14441 ; 14442 14443 _proto.handleClick = function handleClick() { 14444 this.player_.liveTracker.seekToLiveEdge(); 14445 } 14446 /** 14447 * Dispose of the element and stop tracking 14448 */ 14449 ; 14450 14451 _proto.dispose = function dispose() { 14452 if (this.player_.liveTracker) { 14453 this.off(this.player_.liveTracker, 'liveedgechange', this.updateLiveEdgeStatus); 14454 } 14455 14456 this.textEl_ = null; 14457 14458 _Button.prototype.dispose.call(this); 14459 }; 14460 14461 return SeekToLive; 14462 }(Button); 14463 14464 SeekToLive.prototype.controlText_ = 'Seek to live, currently playing live'; 14465 Component.registerComponent('SeekToLive', SeekToLive); 14466 14467 /** 14468 * Keep a number between a min and a max value 14469 * 14470 * @param {number} number 14471 * The number to clamp 14472 * 14473 * @param {number} min 14474 * The minimum value 14475 * @param {number} max 14476 * The maximum value 14477 * 14478 * @return {number} 14479 * the clamped number 14480 */ 14481 var clamp = function clamp(number, min, max) { 14482 number = Number(number); 14483 return Math.min(max, Math.max(min, isNaN(number) ? min : number)); 14484 }; 14485 14486 /** 14487 * The base functionality for a slider. Can be vertical or horizontal. 14488 * For instance the volume bar or the seek bar on a video is a slider. 14489 * 14490 * @extends Component 14491 */ 14492 14493 var Slider = 14494 /*#__PURE__*/ 14495 function (_Component) { 14496 inheritsLoose(Slider, _Component); 14497 14498 /** 14499 * Create an instance of this class 14500 * 14501 * @param {Player} player 14502 * The `Player` that this class should be attached to. 14503 * 14504 * @param {Object} [options] 14505 * The key/value store of player options. 14506 */ 14507 function Slider(player, options) { 14508 var _this; 14509 14510 _this = _Component.call(this, player, options) || this; // Set property names to bar to match with the child Slider class is looking for 14511 14512 _this.bar = _this.getChild(_this.options_.barName); // Set a horizontal or vertical class on the slider depending on the slider type 14513 14514 _this.vertical(!!_this.options_.vertical); 14515 14516 _this.enable(); 14517 14518 return _this; 14519 } 14520 /** 14521 * Are controls are currently enabled for this slider or not. 14522 * 14523 * @return {boolean} 14524 * true if controls are enabled, false otherwise 14525 */ 14526 14527 14528 var _proto = Slider.prototype; 14529 14530 _proto.enabled = function enabled() { 14531 return this.enabled_; 14532 } 14533 /** 14534 * Enable controls for this slider if they are disabled 14535 */ 14536 ; 14537 14538 _proto.enable = function enable() { 14539 if (this.enabled()) { 14540 return; 14541 } 14542 14543 this.on('mousedown', this.handleMouseDown); 14544 this.on('touchstart', this.handleMouseDown); 14545 this.on('keydown', this.handleKeyDown); 14546 this.on('click', this.handleClick); // TODO: deprecated, controlsvisible does not seem to be fired 14547 14548 this.on(this.player_, 'controlsvisible', this.update); 14549 14550 if (this.playerEvent) { 14551 this.on(this.player_, this.playerEvent, this.update); 14552 } 14553 14554 this.removeClass('disabled'); 14555 this.setAttribute('tabindex', 0); 14556 this.enabled_ = true; 14557 } 14558 /** 14559 * Disable controls for this slider if they are enabled 14560 */ 14561 ; 14562 14563 _proto.disable = function disable() { 14564 if (!this.enabled()) { 14565 return; 14566 } 14567 14568 var doc = this.bar.el_.ownerDocument; 14569 this.off('mousedown', this.handleMouseDown); 14570 this.off('touchstart', this.handleMouseDown); 14571 this.off('keydown', this.handleKeyDown); 14572 this.off('click', this.handleClick); 14573 this.off(this.player_, 'controlsvisible', this.update); 14574 this.off(doc, 'mousemove', this.handleMouseMove); 14575 this.off(doc, 'mouseup', this.handleMouseUp); 14576 this.off(doc, 'touchmove', this.handleMouseMove); 14577 this.off(doc, 'touchend', this.handleMouseUp); 14578 this.removeAttribute('tabindex'); 14579 this.addClass('disabled'); 14580 14581 if (this.playerEvent) { 14582 this.off(this.player_, this.playerEvent, this.update); 14583 } 14584 14585 this.enabled_ = false; 14586 } 14587 /** 14588 * Create the `Slider`s DOM element. 14589 * 14590 * @param {string} type 14591 * Type of element to create. 14592 * 14593 * @param {Object} [props={}] 14594 * List of properties in Object form. 14595 * 14596 * @param {Object} [attributes={}] 14597 * list of attributes in Object form. 14598 * 14599 * @return {Element} 14600 * The element that gets created. 14601 */ 14602 ; 14603 14604 _proto.createEl = function createEl(type, props, attributes) { 14605 if (props === void 0) { 14606 props = {}; 14607 } 14608 14609 if (attributes === void 0) { 14610 attributes = {}; 14611 } 14612 14613 // Add the slider element class to all sub classes 14614 props.className = props.className + ' vjs-slider'; 14615 props = assign({ 14616 tabIndex: 0 14617 }, props); 14618 attributes = assign({ 14619 'role': 'slider', 14620 'aria-valuenow': 0, 14621 'aria-valuemin': 0, 14622 'aria-valuemax': 100, 14623 'tabIndex': 0 14624 }, attributes); 14625 return _Component.prototype.createEl.call(this, type, props, attributes); 14626 } 14627 /** 14628 * Handle `mousedown` or `touchstart` events on the `Slider`. 14629 * 14630 * @param {EventTarget~Event} event 14631 * `mousedown` or `touchstart` event that triggered this function 14632 * 14633 * @listens mousedown 14634 * @listens touchstart 14635 * @fires Slider#slideractive 14636 */ 14637 ; 14638 14639 _proto.handleMouseDown = function handleMouseDown(event) { 14640 var doc = this.bar.el_.ownerDocument; 14641 14642 if (event.type === 'mousedown') { 14643 event.preventDefault(); 14644 } // Do not call preventDefault() on touchstart in Chrome 14645 // to avoid console warnings. Use a 'touch-action: none' style 14646 // instead to prevent unintented scrolling. 14647 // https://developers.google.com/web/updates/2017/01/scrolling-intervention 14648 14649 14650 if (event.type === 'touchstart' && !IS_CHROME) { 14651 event.preventDefault(); 14652 } 14653 14654 blockTextSelection(); 14655 this.addClass('vjs-sliding'); 14656 /** 14657 * Triggered when the slider is in an active state 14658 * 14659 * @event Slider#slideractive 14660 * @type {EventTarget~Event} 14661 */ 14662 14663 this.trigger('slideractive'); 14664 this.on(doc, 'mousemove', this.handleMouseMove); 14665 this.on(doc, 'mouseup', this.handleMouseUp); 14666 this.on(doc, 'touchmove', this.handleMouseMove); 14667 this.on(doc, 'touchend', this.handleMouseUp); 14668 this.handleMouseMove(event); 14669 } 14670 /** 14671 * Handle the `mousemove`, `touchmove`, and `mousedown` events on this `Slider`. 14672 * The `mousemove` and `touchmove` events will only only trigger this function during 14673 * `mousedown` and `touchstart`. This is due to {@link Slider#handleMouseDown} and 14674 * {@link Slider#handleMouseUp}. 14675 * 14676 * @param {EventTarget~Event} event 14677 * `mousedown`, `mousemove`, `touchstart`, or `touchmove` event that triggered 14678 * this function 14679 * 14680 * @listens mousemove 14681 * @listens touchmove 14682 */ 14683 ; 14684 14685 _proto.handleMouseMove = function handleMouseMove(event) {} 14686 /** 14687 * Handle `mouseup` or `touchend` events on the `Slider`. 14688 * 14689 * @param {EventTarget~Event} event 14690 * `mouseup` or `touchend` event that triggered this function. 14691 * 14692 * @listens touchend 14693 * @listens mouseup 14694 * @fires Slider#sliderinactive 14695 */ 14696 ; 14697 14698 _proto.handleMouseUp = function handleMouseUp() { 14699 var doc = this.bar.el_.ownerDocument; 14700 unblockTextSelection(); 14701 this.removeClass('vjs-sliding'); 14702 /** 14703 * Triggered when the slider is no longer in an active state. 14704 * 14705 * @event Slider#sliderinactive 14706 * @type {EventTarget~Event} 14707 */ 14708 14709 this.trigger('sliderinactive'); 14710 this.off(doc, 'mousemove', this.handleMouseMove); 14711 this.off(doc, 'mouseup', this.handleMouseUp); 14712 this.off(doc, 'touchmove', this.handleMouseMove); 14713 this.off(doc, 'touchend', this.handleMouseUp); 14714 this.update(); 14715 } 14716 /** 14717 * Update the progress bar of the `Slider`. 14718 * 14719 * @return {number} 14720 * The percentage of progress the progress bar represents as a 14721 * number from 0 to 1. 14722 */ 14723 ; 14724 14725 _proto.update = function update() { 14726 var _this2 = this; 14727 14728 // In VolumeBar init we have a setTimeout for update that pops and update 14729 // to the end of the execution stack. The player is destroyed before then 14730 // update will cause an error 14731 // If there's no bar... 14732 if (!this.el_ || !this.bar) { 14733 return; 14734 } // clamp progress between 0 and 1 14735 // and only round to four decimal places, as we round to two below 14736 14737 14738 var progress = this.getProgress(); 14739 14740 if (progress === this.progress_) { 14741 return; 14742 } 14743 14744 this.progress_ = progress; 14745 this.requestAnimationFrame(function () { 14746 // Set the new bar width or height 14747 var sizeKey = _this2.vertical() ? 'height' : 'width'; // Convert to a percentage for css value 14748 14749 _this2.bar.el().style[sizeKey] = (progress * 100).toFixed(2) + '%'; 14750 }); 14751 return progress; 14752 } 14753 /** 14754 * Get the percentage of the bar that should be filled 14755 * but clamped and rounded. 14756 * 14757 * @return {number} 14758 * percentage filled that the slider is 14759 */ 14760 ; 14761 14762 _proto.getProgress = function getProgress() { 14763 return clamp(this.getPercent(), 0, 1).toFixed(4); 14764 } 14765 /** 14766 * Calculate distance for slider 14767 * 14768 * @param {EventTarget~Event} event 14769 * The event that caused this function to run. 14770 * 14771 * @return {number} 14772 * The current position of the Slider. 14773 * - position.x for vertical `Slider`s 14774 * - position.y for horizontal `Slider`s 14775 */ 14776 ; 14777 14778 _proto.calculateDistance = function calculateDistance(event) { 14779 var position = getPointerPosition(this.el_, event); 14780 14781 if (this.vertical()) { 14782 return position.y; 14783 } 14784 14785 return position.x; 14786 } 14787 /** 14788 * Handle a `keydown` event on the `Slider`. Watches for left, rigth, up, and down 14789 * arrow keys. This function will only be called when the slider has focus. See 14790 * {@link Slider#handleFocus} and {@link Slider#handleBlur}. 14791 * 14792 * @param {EventTarget~Event} event 14793 * the `keydown` event that caused this function to run. 14794 * 14795 * @listens keydown 14796 */ 14797 ; 14798 14799 _proto.handleKeyDown = function handleKeyDown(event) { 14800 // Left and Down Arrows 14801 if (keycode.isEventKey(event, 'Left') || keycode.isEventKey(event, 'Down')) { 14802 event.preventDefault(); 14803 event.stopPropagation(); 14804 this.stepBack(); // Up and Right Arrows 14805 } else if (keycode.isEventKey(event, 'Right') || keycode.isEventKey(event, 'Up')) { 14806 event.preventDefault(); 14807 event.stopPropagation(); 14808 this.stepForward(); 14809 } else { 14810 // Pass keydown handling up for unsupported keys 14811 _Component.prototype.handleKeyDown.call(this, event); 14812 } 14813 } 14814 /** 14815 * Listener for click events on slider, used to prevent clicks 14816 * from bubbling up to parent elements like button menus. 14817 * 14818 * @param {Object} event 14819 * Event that caused this object to run 14820 */ 14821 ; 14822 14823 _proto.handleClick = function handleClick(event) { 14824 event.stopPropagation(); 14825 event.preventDefault(); 14826 } 14827 /** 14828 * Get/set if slider is horizontal for vertical 14829 * 14830 * @param {boolean} [bool] 14831 * - true if slider is vertical, 14832 * - false is horizontal 14833 * 14834 * @return {boolean} 14835 * - true if slider is vertical, and getting 14836 * - false if the slider is horizontal, and getting 14837 */ 14838 ; 14839 14840 _proto.vertical = function vertical(bool) { 14841 if (bool === undefined) { 14842 return this.vertical_ || false; 14843 } 14844 14845 this.vertical_ = !!bool; 14846 14847 if (this.vertical_) { 14848 this.addClass('vjs-slider-vertical'); 14849 } else { 14850 this.addClass('vjs-slider-horizontal'); 14851 } 14852 }; 14853 14854 return Slider; 14855 }(Component); 14856 14857 Component.registerComponent('Slider', Slider); 14858 14859 var percentify = function percentify(time, end) { 14860 return clamp(time / end * 100, 0, 100).toFixed(2) + '%'; 14861 }; 14862 /** 14863 * Shows loading progress 14864 * 14865 * @extends Component 14866 */ 14867 14868 14869 var LoadProgressBar = 14870 /*#__PURE__*/ 14871 function (_Component) { 14872 inheritsLoose(LoadProgressBar, _Component); 14873 14874 /** 14875 * Creates an instance of this class. 14876 * 14877 * @param {Player} player 14878 * The `Player` that this class should be attached to. 14879 * 14880 * @param {Object} [options] 14881 * The key/value store of player options. 14882 */ 14883 function LoadProgressBar(player, options) { 14884 var _this; 14885 14886 _this = _Component.call(this, player, options) || this; 14887 _this.partEls_ = []; 14888 14889 _this.on(player, 'progress', _this.update); 14890 14891 return _this; 14892 } 14893 /** 14894 * Create the `Component`'s DOM element 14895 * 14896 * @return {Element} 14897 * The element that was created. 14898 */ 14899 14900 14901 var _proto = LoadProgressBar.prototype; 14902 14903 _proto.createEl = function createEl$1() { 14904 var el = _Component.prototype.createEl.call(this, 'div', { 14905 className: 'vjs-load-progress' 14906 }); 14907 14908 var wrapper = createEl('span', { 14909 className: 'vjs-control-text' 14910 }); 14911 var loadedText = createEl('span', { 14912 textContent: this.localize('Loaded') 14913 }); 14914 var separator = document.createTextNode(': '); 14915 this.percentageEl_ = createEl('span', { 14916 className: 'vjs-control-text-loaded-percentage', 14917 textContent: '0%' 14918 }); 14919 el.appendChild(wrapper); 14920 wrapper.appendChild(loadedText); 14921 wrapper.appendChild(separator); 14922 wrapper.appendChild(this.percentageEl_); 14923 return el; 14924 }; 14925 14926 _proto.dispose = function dispose() { 14927 this.partEls_ = null; 14928 this.percentageEl_ = null; 14929 14930 _Component.prototype.dispose.call(this); 14931 } 14932 /** 14933 * Update progress bar 14934 * 14935 * @param {EventTarget~Event} [event] 14936 * The `progress` event that caused this function to run. 14937 * 14938 * @listens Player#progress 14939 */ 14940 ; 14941 14942 _proto.update = function update(event) { 14943 var _this2 = this; 14944 14945 this.requestAnimationFrame(function () { 14946 var liveTracker = _this2.player_.liveTracker; 14947 14948 var buffered = _this2.player_.buffered(); 14949 14950 var duration = liveTracker && liveTracker.isLive() ? liveTracker.seekableEnd() : _this2.player_.duration(); 14951 14952 var bufferedEnd = _this2.player_.bufferedEnd(); 14953 14954 var children = _this2.partEls_; 14955 var percent = percentify(bufferedEnd, duration); 14956 14957 if (_this2.percent_ !== percent) { 14958 // update the width of the progress bar 14959 _this2.el_.style.width = percent; // update the control-text 14960 14961 textContent(_this2.percentageEl_, percent); 14962 _this2.percent_ = percent; 14963 } // add child elements to represent the individual buffered time ranges 14964 14965 14966 for (var i = 0; i < buffered.length; i++) { 14967 var start = buffered.start(i); 14968 var end = buffered.end(i); 14969 var part = children[i]; 14970 14971 if (!part) { 14972 part = _this2.el_.appendChild(createEl()); 14973 children[i] = part; 14974 } // only update if changed 14975 14976 14977 if (part.dataset.start === start && part.dataset.end === end) { 14978 continue; 14979 } 14980 14981 part.dataset.start = start; 14982 part.dataset.end = end; // set the percent based on the width of the progress bar (bufferedEnd) 14983 14984 part.style.left = percentify(start, bufferedEnd); 14985 part.style.width = percentify(end - start, bufferedEnd); 14986 } // remove unused buffered range elements 14987 14988 14989 for (var _i = children.length; _i > buffered.length; _i--) { 14990 _this2.el_.removeChild(children[_i - 1]); 14991 } 14992 14993 children.length = buffered.length; 14994 }); 14995 }; 14996 14997 return LoadProgressBar; 14998 }(Component); 14999 15000 Component.registerComponent('LoadProgressBar', LoadProgressBar); 15001 15002 /** 15003 * Time tooltips display a time above the progress bar. 15004 * 15005 * @extends Component 15006 */ 15007 15008 var TimeTooltip = 15009 /*#__PURE__*/ 15010 function (_Component) { 15011 inheritsLoose(TimeTooltip, _Component); 15012 15013 /** 15014 * Creates an instance of this class. 15015 * 15016 * @param {Player} player 15017 * The {@link Player} that this class should be attached to. 15018 * 15019 * @param {Object} [options] 15020 * The key/value store of player options. 15021 */ 15022 function TimeTooltip(player, options) { 15023 var _this; 15024 15025 _this = _Component.call(this, player, options) || this; 15026 _this.update = throttle(bind(assertThisInitialized(_this), _this.update), UPDATE_REFRESH_INTERVAL); 15027 return _this; 15028 } 15029 /** 15030 * Create the time tooltip DOM element 15031 * 15032 * @return {Element} 15033 * The element that was created. 15034 */ 15035 15036 15037 var _proto = TimeTooltip.prototype; 15038 15039 _proto.createEl = function createEl() { 15040 return _Component.prototype.createEl.call(this, 'div', { 15041 className: 'vjs-time-tooltip' 15042 }, { 15043 'aria-hidden': 'true' 15044 }); 15045 } 15046 /** 15047 * Updates the position of the time tooltip relative to the `SeekBar`. 15048 * 15049 * @param {Object} seekBarRect 15050 * The `ClientRect` for the {@link SeekBar} element. 15051 * 15052 * @param {number} seekBarPoint 15053 * A number from 0 to 1, representing a horizontal reference point 15054 * from the left edge of the {@link SeekBar} 15055 */ 15056 ; 15057 15058 _proto.update = function update(seekBarRect, seekBarPoint, content) { 15059 var tooltipRect = getBoundingClientRect(this.el_); 15060 var playerRect = getBoundingClientRect(this.player_.el()); 15061 var seekBarPointPx = seekBarRect.width * seekBarPoint; // do nothing if either rect isn't available 15062 // for example, if the player isn't in the DOM for testing 15063 15064 if (!playerRect || !tooltipRect) { 15065 return; 15066 } // This is the space left of the `seekBarPoint` available within the bounds 15067 // of the player. We calculate any gap between the left edge of the player 15068 // and the left edge of the `SeekBar` and add the number of pixels in the 15069 // `SeekBar` before hitting the `seekBarPoint` 15070 15071 15072 var spaceLeftOfPoint = seekBarRect.left - playerRect.left + seekBarPointPx; // This is the space right of the `seekBarPoint` available within the bounds 15073 // of the player. We calculate the number of pixels from the `seekBarPoint` 15074 // to the right edge of the `SeekBar` and add to that any gap between the 15075 // right edge of the `SeekBar` and the player. 15076 15077 var spaceRightOfPoint = seekBarRect.width - seekBarPointPx + (playerRect.right - seekBarRect.right); // This is the number of pixels by which the tooltip will need to be pulled 15078 // further to the right to center it over the `seekBarPoint`. 15079 15080 var pullTooltipBy = tooltipRect.width / 2; // Adjust the `pullTooltipBy` distance to the left or right depending on 15081 // the results of the space calculations above. 15082 15083 if (spaceLeftOfPoint < pullTooltipBy) { 15084 pullTooltipBy += pullTooltipBy - spaceLeftOfPoint; 15085 } else if (spaceRightOfPoint < pullTooltipBy) { 15086 pullTooltipBy = spaceRightOfPoint; 15087 } // Due to the imprecision of decimal/ratio based calculations and varying 15088 // rounding behaviors, there are cases where the spacing adjustment is off 15089 // by a pixel or two. This adds insurance to these calculations. 15090 15091 15092 if (pullTooltipBy < 0) { 15093 pullTooltipBy = 0; 15094 } else if (pullTooltipBy > tooltipRect.width) { 15095 pullTooltipBy = tooltipRect.width; 15096 } 15097 15098 this.el_.style.right = "-" + pullTooltipBy + "px"; 15099 this.write(content); 15100 } 15101 /** 15102 * Write the time to the tooltip DOM element. 15103 * 15104 * @param {string} content 15105 * The formatted time for the tooltip. 15106 */ 15107 ; 15108 15109 _proto.write = function write(content) { 15110 textContent(this.el_, content); 15111 } 15112 /** 15113 * Updates the position of the time tooltip relative to the `SeekBar`. 15114 * 15115 * @param {Object} seekBarRect 15116 * The `ClientRect` for the {@link SeekBar} element. 15117 * 15118 * @param {number} seekBarPoint 15119 * A number from 0 to 1, representing a horizontal reference point 15120 * from the left edge of the {@link SeekBar} 15121 * 15122 * @param {number} time 15123 * The time to update the tooltip to, not used during live playback 15124 * 15125 * @param {Function} cb 15126 * A function that will be called during the request animation frame 15127 * for tooltips that need to do additional animations from the default 15128 */ 15129 ; 15130 15131 _proto.updateTime = function updateTime(seekBarRect, seekBarPoint, time, cb) { 15132 var _this2 = this; 15133 15134 // If there is an existing rAF ID, cancel it so we don't over-queue. 15135 if (this.rafId_) { 15136 this.cancelAnimationFrame(this.rafId_); 15137 } 15138 15139 this.rafId_ = this.requestAnimationFrame(function () { 15140 var content; 15141 15142 var duration = _this2.player_.duration(); 15143 15144 if (_this2.player_.liveTracker && _this2.player_.liveTracker.isLive()) { 15145 var liveWindow = _this2.player_.liveTracker.liveWindow(); 15146 15147 var secondsBehind = liveWindow - seekBarPoint * liveWindow; 15148 content = (secondsBehind < 1 ? '' : '-') + formatTime(secondsBehind, liveWindow); 15149 } else { 15150 content = formatTime(time, duration); 15151 } 15152 15153 _this2.update(seekBarRect, seekBarPoint, content); 15154 15155 if (cb) { 15156 cb(); 15157 } 15158 }); 15159 }; 15160 15161 return TimeTooltip; 15162 }(Component); 15163 15164 Component.registerComponent('TimeTooltip', TimeTooltip); 15165 15166 /** 15167 * Used by {@link SeekBar} to display media playback progress as part of the 15168 * {@link ProgressControl}. 15169 * 15170 * @extends Component 15171 */ 15172 15173 var PlayProgressBar = 15174 /*#__PURE__*/ 15175 function (_Component) { 15176 inheritsLoose(PlayProgressBar, _Component); 15177 15178 /** 15179 * Creates an instance of this class. 15180 * 15181 * @param {Player} player 15182 * The {@link Player} that this class should be attached to. 15183 * 15184 * @param {Object} [options] 15185 * The key/value store of player options. 15186 */ 15187 function PlayProgressBar(player, options) { 15188 var _this; 15189 15190 _this = _Component.call(this, player, options) || this; 15191 _this.update = throttle(bind(assertThisInitialized(_this), _this.update), UPDATE_REFRESH_INTERVAL); 15192 return _this; 15193 } 15194 /** 15195 * Create the the DOM element for this class. 15196 * 15197 * @return {Element} 15198 * The element that was created. 15199 */ 15200 15201 15202 var _proto = PlayProgressBar.prototype; 15203 15204 _proto.createEl = function createEl() { 15205 return _Component.prototype.createEl.call(this, 'div', { 15206 className: 'vjs-play-progress vjs-slider-bar' 15207 }, { 15208 'aria-hidden': 'true' 15209 }); 15210 } 15211 /** 15212 * Enqueues updates to its own DOM as well as the DOM of its 15213 * {@link TimeTooltip} child. 15214 * 15215 * @param {Object} seekBarRect 15216 * The `ClientRect` for the {@link SeekBar} element. 15217 * 15218 * @param {number} seekBarPoint 15219 * A number from 0 to 1, representing a horizontal reference point 15220 * from the left edge of the {@link SeekBar} 15221 */ 15222 ; 15223 15224 _proto.update = function update(seekBarRect, seekBarPoint) { 15225 var timeTooltip = this.getChild('timeTooltip'); 15226 15227 if (!timeTooltip) { 15228 return; 15229 } 15230 15231 var time = this.player_.scrubbing() ? this.player_.getCache().currentTime : this.player_.currentTime(); 15232 timeTooltip.updateTime(seekBarRect, seekBarPoint, time); 15233 }; 15234 15235 return PlayProgressBar; 15236 }(Component); 15237 /** 15238 * Default options for {@link PlayProgressBar}. 15239 * 15240 * @type {Object} 15241 * @private 15242 */ 15243 15244 15245 PlayProgressBar.prototype.options_ = { 15246 children: [] 15247 }; // Time tooltips should not be added to a player on mobile devices 15248 15249 if (!IS_IOS && !IS_ANDROID) { 15250 PlayProgressBar.prototype.options_.children.push('timeTooltip'); 15251 } 15252 15253 Component.registerComponent('PlayProgressBar', PlayProgressBar); 15254 15255 /** 15256 * The {@link MouseTimeDisplay} component tracks mouse movement over the 15257 * {@link ProgressControl}. It displays an indicator and a {@link TimeTooltip} 15258 * indicating the time which is represented by a given point in the 15259 * {@link ProgressControl}. 15260 * 15261 * @extends Component 15262 */ 15263 15264 var MouseTimeDisplay = 15265 /*#__PURE__*/ 15266 function (_Component) { 15267 inheritsLoose(MouseTimeDisplay, _Component); 15268 15269 /** 15270 * Creates an instance of this class. 15271 * 15272 * @param {Player} player 15273 * The {@link Player} that this class should be attached to. 15274 * 15275 * @param {Object} [options] 15276 * The key/value store of player options. 15277 */ 15278 function MouseTimeDisplay(player, options) { 15279 var _this; 15280 15281 _this = _Component.call(this, player, options) || this; 15282 _this.update = throttle(bind(assertThisInitialized(_this), _this.update), UPDATE_REFRESH_INTERVAL); 15283 return _this; 15284 } 15285 /** 15286 * Create the DOM element for this class. 15287 * 15288 * @return {Element} 15289 * The element that was created. 15290 */ 15291 15292 15293 var _proto = MouseTimeDisplay.prototype; 15294 15295 _proto.createEl = function createEl() { 15296 return _Component.prototype.createEl.call(this, 'div', { 15297 className: 'vjs-mouse-display' 15298 }); 15299 } 15300 /** 15301 * Enqueues updates to its own DOM as well as the DOM of its 15302 * {@link TimeTooltip} child. 15303 * 15304 * @param {Object} seekBarRect 15305 * The `ClientRect` for the {@link SeekBar} element. 15306 * 15307 * @param {number} seekBarPoint 15308 * A number from 0 to 1, representing a horizontal reference point 15309 * from the left edge of the {@link SeekBar} 15310 */ 15311 ; 15312 15313 _proto.update = function update(seekBarRect, seekBarPoint) { 15314 var _this2 = this; 15315 15316 var time = seekBarPoint * this.player_.duration(); 15317 this.getChild('timeTooltip').updateTime(seekBarRect, seekBarPoint, time, function () { 15318 _this2.el_.style.left = seekBarRect.width * seekBarPoint + "px"; 15319 }); 15320 }; 15321 15322 return MouseTimeDisplay; 15323 }(Component); 15324 /** 15325 * Default options for `MouseTimeDisplay` 15326 * 15327 * @type {Object} 15328 * @private 15329 */ 15330 15331 15332 MouseTimeDisplay.prototype.options_ = { 15333 children: ['timeTooltip'] 15334 }; 15335 Component.registerComponent('MouseTimeDisplay', MouseTimeDisplay); 15336 15337 var STEP_SECONDS = 5; // The multiplier of STEP_SECONDS that PgUp/PgDown move the timeline. 15338 15339 var PAGE_KEY_MULTIPLIER = 12; // The interval at which the bar should update as it progresses. 15340 15341 var UPDATE_REFRESH_INTERVAL$1 = 30; 15342 /** 15343 * Seek bar and container for the progress bars. Uses {@link PlayProgressBar} 15344 * as its `bar`. 15345 * 15346 * @extends Slider 15347 */ 15348 15349 var SeekBar = 15350 /*#__PURE__*/ 15351 function (_Slider) { 15352 inheritsLoose(SeekBar, _Slider); 15353 15354 /** 15355 * Creates an instance of this class. 15356 * 15357 * @param {Player} player 15358 * The `Player` that this class should be attached to. 15359 * 15360 * @param {Object} [options] 15361 * The key/value store of player options. 15362 */ 15363 function SeekBar(player, options) { 15364 var _this; 15365 15366 _this = _Slider.call(this, player, options) || this; 15367 15368 _this.setEventHandlers_(); 15369 15370 return _this; 15371 } 15372 /** 15373 * Sets the event handlers 15374 * 15375 * @private 15376 */ 15377 15378 15379 var _proto = SeekBar.prototype; 15380 15381 _proto.setEventHandlers_ = function setEventHandlers_() { 15382 this.update_ = bind(this, this.update); 15383 this.update = throttle(this.update_, UPDATE_REFRESH_INTERVAL$1); 15384 this.on(this.player_, ['ended', 'durationchange', 'timeupdate'], this.update); 15385 15386 if (this.player_.liveTracker) { 15387 this.on(this.player_.liveTracker, 'liveedgechange', this.update); 15388 } // when playing, let's ensure we smoothly update the play progress bar 15389 // via an interval 15390 15391 15392 this.updateInterval = null; 15393 this.on(this.player_, ['playing'], this.enableInterval_); 15394 this.on(this.player_, ['ended', 'pause', 'waiting'], this.disableInterval_); // we don't need to update the play progress if the document is hidden, 15395 // also, this causes the CPU to spike and eventually crash the page on IE11. 15396 15397 if ('hidden' in document && 'visibilityState' in document) { 15398 this.on(document, 'visibilitychange', this.toggleVisibility_); 15399 } 15400 }; 15401 15402 _proto.toggleVisibility_ = function toggleVisibility_(e) { 15403 if (document.hidden) { 15404 this.disableInterval_(e); 15405 } else { 15406 this.enableInterval_(); // we just switched back to the page and someone may be looking, so, update ASAP 15407 15408 this.update(); 15409 } 15410 }; 15411 15412 _proto.enableInterval_ = function enableInterval_() { 15413 if (this.updateInterval) { 15414 return; 15415 } 15416 15417 this.updateInterval = this.setInterval(this.update, UPDATE_REFRESH_INTERVAL$1); 15418 }; 15419 15420 _proto.disableInterval_ = function disableInterval_(e) { 15421 if (this.player_.liveTracker && this.player_.liveTracker.isLive() && e.type !== 'ended') { 15422 return; 15423 } 15424 15425 if (!this.updateInterval) { 15426 return; 15427 } 15428 15429 this.clearInterval(this.updateInterval); 15430 this.updateInterval = null; 15431 } 15432 /** 15433 * Create the `Component`'s DOM element 15434 * 15435 * @return {Element} 15436 * The element that was created. 15437 */ 15438 ; 15439 15440 _proto.createEl = function createEl() { 15441 return _Slider.prototype.createEl.call(this, 'div', { 15442 className: 'vjs-progress-holder' 15443 }, { 15444 'aria-label': this.localize('Progress Bar') 15445 }); 15446 } 15447 /** 15448 * This function updates the play progress bar and accessibility 15449 * attributes to whatever is passed in. 15450 * 15451 * @param {EventTarget~Event} [event] 15452 * The `timeupdate` or `ended` event that caused this to run. 15453 * 15454 * @listens Player#timeupdate 15455 * 15456 * @return {number} 15457 * The current percent at a number from 0-1 15458 */ 15459 ; 15460 15461 _proto.update = function update(event) { 15462 var _this2 = this; 15463 15464 var percent = _Slider.prototype.update.call(this); 15465 15466 this.requestAnimationFrame(function () { 15467 var currentTime = _this2.player_.ended() ? _this2.player_.duration() : _this2.getCurrentTime_(); 15468 var liveTracker = _this2.player_.liveTracker; 15469 15470 var duration = _this2.player_.duration(); 15471 15472 if (liveTracker && liveTracker.isLive()) { 15473 duration = _this2.player_.liveTracker.liveCurrentTime(); 15474 } 15475 15476 if (_this2.percent_ !== percent) { 15477 // machine readable value of progress bar (percentage complete) 15478 _this2.el_.setAttribute('aria-valuenow', (percent * 100).toFixed(2)); 15479 15480 _this2.percent_ = percent; 15481 } 15482 15483 if (_this2.currentTime_ !== currentTime || _this2.duration_ !== duration) { 15484 // human readable value of progress bar (time complete) 15485 _this2.el_.setAttribute('aria-valuetext', _this2.localize('progress bar timing: currentTime={1} duration={2}', [formatTime(currentTime, duration), formatTime(duration, duration)], '{1} of {2}')); 15486 15487 _this2.currentTime_ = currentTime; 15488 _this2.duration_ = duration; 15489 } 15490 }); 15491 return percent; 15492 } 15493 /** 15494 * Get the value of current time but allows for smooth scrubbing, 15495 * when player can't keep up. 15496 * 15497 * @return {number} 15498 * The current time value to display 15499 * 15500 * @private 15501 */ 15502 ; 15503 15504 _proto.getCurrentTime_ = function getCurrentTime_() { 15505 return this.player_.scrubbing() ? this.player_.getCache().currentTime : this.player_.currentTime(); 15506 } 15507 /** 15508 * Get the percentage of media played so far. 15509 * 15510 * @return {number} 15511 * The percentage of media played so far (0 to 1). 15512 */ 15513 ; 15514 15515 _proto.getPercent = function getPercent() { 15516 var currentTime = this.getCurrentTime_(); 15517 var percent; 15518 var liveTracker = this.player_.liveTracker; 15519 15520 if (liveTracker && liveTracker.isLive()) { 15521 percent = (currentTime - liveTracker.seekableStart()) / liveTracker.liveWindow(); // prevent the percent from changing at the live edge 15522 15523 if (liveTracker.atLiveEdge()) { 15524 percent = 1; 15525 } 15526 } else { 15527 percent = currentTime / this.player_.duration(); 15528 } 15529 15530 return percent; 15531 } 15532 /** 15533 * Handle mouse down on seek bar 15534 * 15535 * @param {EventTarget~Event} event 15536 * The `mousedown` event that caused this to run. 15537 * 15538 * @listens mousedown 15539 */ 15540 ; 15541 15542 _proto.handleMouseDown = function handleMouseDown(event) { 15543 if (!isSingleLeftClick(event)) { 15544 return; 15545 } // Stop event propagation to prevent double fire in progress-control.js 15546 15547 15548 event.stopPropagation(); 15549 this.player_.scrubbing(true); 15550 this.videoWasPlaying = !this.player_.paused(); 15551 this.player_.pause(); 15552 15553 _Slider.prototype.handleMouseDown.call(this, event); 15554 } 15555 /** 15556 * Handle mouse move on seek bar 15557 * 15558 * @param {EventTarget~Event} event 15559 * The `mousemove` event that caused this to run. 15560 * 15561 * @listens mousemove 15562 */ 15563 ; 15564 15565 _proto.handleMouseMove = function handleMouseMove(event) { 15566 if (!isSingleLeftClick(event)) { 15567 return; 15568 } 15569 15570 var newTime; 15571 var distance = this.calculateDistance(event); 15572 var liveTracker = this.player_.liveTracker; 15573 15574 if (!liveTracker || !liveTracker.isLive()) { 15575 newTime = distance * this.player_.duration(); // Don't let video end while scrubbing. 15576 15577 if (newTime === this.player_.duration()) { 15578 newTime = newTime - 0.1; 15579 } 15580 } else { 15581 var seekableStart = liveTracker.seekableStart(); 15582 var seekableEnd = liveTracker.liveCurrentTime(); 15583 newTime = seekableStart + distance * liveTracker.liveWindow(); // Don't let video end while scrubbing. 15584 15585 if (newTime >= seekableEnd) { 15586 newTime = seekableEnd; 15587 } // Compensate for precision differences so that currentTime is not less 15588 // than seekable start 15589 15590 15591 if (newTime <= seekableStart) { 15592 newTime = seekableStart + 0.1; 15593 } // On android seekableEnd can be Infinity sometimes, 15594 // this will cause newTime to be Infinity, which is 15595 // not a valid currentTime. 15596 15597 15598 if (newTime === Infinity) { 15599 return; 15600 } 15601 } // Set new time (tell player to seek to new time) 15602 15603 15604 this.player_.currentTime(newTime); 15605 }; 15606 15607 _proto.enable = function enable() { 15608 _Slider.prototype.enable.call(this); 15609 15610 var mouseTimeDisplay = this.getChild('mouseTimeDisplay'); 15611 15612 if (!mouseTimeDisplay) { 15613 return; 15614 } 15615 15616 mouseTimeDisplay.show(); 15617 }; 15618 15619 _proto.disable = function disable() { 15620 _Slider.prototype.disable.call(this); 15621 15622 var mouseTimeDisplay = this.getChild('mouseTimeDisplay'); 15623 15624 if (!mouseTimeDisplay) { 15625 return; 15626 } 15627 15628 mouseTimeDisplay.hide(); 15629 } 15630 /** 15631 * Handle mouse up on seek bar 15632 * 15633 * @param {EventTarget~Event} event 15634 * The `mouseup` event that caused this to run. 15635 * 15636 * @listens mouseup 15637 */ 15638 ; 15639 15640 _proto.handleMouseUp = function handleMouseUp(event) { 15641 _Slider.prototype.handleMouseUp.call(this, event); // Stop event propagation to prevent double fire in progress-control.js 15642 15643 15644 if (event) { 15645 event.stopPropagation(); 15646 } 15647 15648 this.player_.scrubbing(false); 15649 /** 15650 * Trigger timeupdate because we're done seeking and the time has changed. 15651 * This is particularly useful for if the player is paused to time the time displays. 15652 * 15653 * @event Tech#timeupdate 15654 * @type {EventTarget~Event} 15655 */ 15656 15657 this.player_.trigger({ 15658 type: 'timeupdate', 15659 target: this, 15660 manuallyTriggered: true 15661 }); 15662 15663 if (this.videoWasPlaying) { 15664 silencePromise(this.player_.play()); 15665 } else { 15666 // We're done seeking and the time has changed. 15667 // If the player is paused, make sure we display the correct time on the seek bar. 15668 this.update_(); 15669 } 15670 } 15671 /** 15672 * Move more quickly fast forward for keyboard-only users 15673 */ 15674 ; 15675 15676 _proto.stepForward = function stepForward() { 15677 this.player_.currentTime(this.player_.currentTime() + STEP_SECONDS); 15678 } 15679 /** 15680 * Move more quickly rewind for keyboard-only users 15681 */ 15682 ; 15683 15684 _proto.stepBack = function stepBack() { 15685 this.player_.currentTime(this.player_.currentTime() - STEP_SECONDS); 15686 } 15687 /** 15688 * Toggles the playback state of the player 15689 * This gets called when enter or space is used on the seekbar 15690 * 15691 * @param {EventTarget~Event} event 15692 * The `keydown` event that caused this function to be called 15693 * 15694 */ 15695 ; 15696 15697 _proto.handleAction = function handleAction(event) { 15698 if (this.player_.paused()) { 15699 this.player_.play(); 15700 } else { 15701 this.player_.pause(); 15702 } 15703 } 15704 /** 15705 * Called when this SeekBar has focus and a key gets pressed down. 15706 * Supports the following keys: 15707 * 15708 * Space or Enter key fire a click event 15709 * Home key moves to start of the timeline 15710 * End key moves to end of the timeline 15711 * Digit "0" through "9" keys move to 0%, 10% ... 80%, 90% of the timeline 15712 * PageDown key moves back a larger step than ArrowDown 15713 * PageUp key moves forward a large step 15714 * 15715 * @param {EventTarget~Event} event 15716 * The `keydown` event that caused this function to be called. 15717 * 15718 * @listens keydown 15719 */ 15720 ; 15721 15722 _proto.handleKeyDown = function handleKeyDown(event) { 15723 if (keycode.isEventKey(event, 'Space') || keycode.isEventKey(event, 'Enter')) { 15724 event.preventDefault(); 15725 event.stopPropagation(); 15726 this.handleAction(event); 15727 } else if (keycode.isEventKey(event, 'Home')) { 15728 event.preventDefault(); 15729 event.stopPropagation(); 15730 this.player_.currentTime(0); 15731 } else if (keycode.isEventKey(event, 'End')) { 15732 event.preventDefault(); 15733 event.stopPropagation(); 15734 this.player_.currentTime(this.player_.duration()); 15735 } else if (/^[0-9]$/.test(keycode(event))) { 15736 event.preventDefault(); 15737 event.stopPropagation(); 15738 var gotoFraction = (keycode.codes[keycode(event)] - keycode.codes['0']) * 10.0 / 100.0; 15739 this.player_.currentTime(this.player_.duration() * gotoFraction); 15740 } else if (keycode.isEventKey(event, 'PgDn')) { 15741 event.preventDefault(); 15742 event.stopPropagation(); 15743 this.player_.currentTime(this.player_.currentTime() - STEP_SECONDS * PAGE_KEY_MULTIPLIER); 15744 } else if (keycode.isEventKey(event, 'PgUp')) { 15745 event.preventDefault(); 15746 event.stopPropagation(); 15747 this.player_.currentTime(this.player_.currentTime() + STEP_SECONDS * PAGE_KEY_MULTIPLIER); 15748 } else { 15749 // Pass keydown handling up for unsupported keys 15750 _Slider.prototype.handleKeyDown.call(this, event); 15751 } 15752 }; 15753 15754 return SeekBar; 15755 }(Slider); 15756 /** 15757 * Default options for the `SeekBar` 15758 * 15759 * @type {Object} 15760 * @private 15761 */ 15762 15763 15764 SeekBar.prototype.options_ = { 15765 children: ['loadProgressBar', 'playProgressBar'], 15766 barName: 'playProgressBar' 15767 }; // MouseTimeDisplay tooltips should not be added to a player on mobile devices 15768 15769 if (!IS_IOS && !IS_ANDROID) { 15770 SeekBar.prototype.options_.children.splice(1, 0, 'mouseTimeDisplay'); 15771 } 15772 15773 Component.registerComponent('SeekBar', SeekBar); 15774 15775 /** 15776 * The Progress Control component contains the seek bar, load progress, 15777 * and play progress. 15778 * 15779 * @extends Component 15780 */ 15781 15782 var ProgressControl = 15783 /*#__PURE__*/ 15784 function (_Component) { 15785 inheritsLoose(ProgressControl, _Component); 15786 15787 /** 15788 * Creates an instance of this class. 15789 * 15790 * @param {Player} player 15791 * The `Player` that this class should be attached to. 15792 * 15793 * @param {Object} [options] 15794 * The key/value store of player options. 15795 */ 15796 function ProgressControl(player, options) { 15797 var _this; 15798 15799 _this = _Component.call(this, player, options) || this; 15800 _this.handleMouseMove = throttle(bind(assertThisInitialized(_this), _this.handleMouseMove), UPDATE_REFRESH_INTERVAL); 15801 _this.throttledHandleMouseSeek = throttle(bind(assertThisInitialized(_this), _this.handleMouseSeek), UPDATE_REFRESH_INTERVAL); 15802 15803 _this.enable(); 15804 15805 return _this; 15806 } 15807 /** 15808 * Create the `Component`'s DOM element 15809 * 15810 * @return {Element} 15811 * The element that was created. 15812 */ 15813 15814 15815 var _proto = ProgressControl.prototype; 15816 15817 _proto.createEl = function createEl() { 15818 return _Component.prototype.createEl.call(this, 'div', { 15819 className: 'vjs-progress-control vjs-control' 15820 }); 15821 } 15822 /** 15823 * When the mouse moves over the `ProgressControl`, the pointer position 15824 * gets passed down to the `MouseTimeDisplay` component. 15825 * 15826 * @param {EventTarget~Event} event 15827 * The `mousemove` event that caused this function to run. 15828 * 15829 * @listen mousemove 15830 */ 15831 ; 15832 15833 _proto.handleMouseMove = function handleMouseMove(event) { 15834 var seekBar = this.getChild('seekBar'); 15835 15836 if (!seekBar) { 15837 return; 15838 } 15839 15840 var playProgressBar = seekBar.getChild('playProgressBar'); 15841 var mouseTimeDisplay = seekBar.getChild('mouseTimeDisplay'); 15842 15843 if (!playProgressBar && !mouseTimeDisplay) { 15844 return; 15845 } 15846 15847 var seekBarEl = seekBar.el(); 15848 var seekBarRect = getBoundingClientRect(seekBarEl); 15849 var seekBarPoint = getPointerPosition(seekBarEl, event).x; // The default skin has a gap on either side of the `SeekBar`. This means 15850 // that it's possible to trigger this behavior outside the boundaries of 15851 // the `SeekBar`. This ensures we stay within it at all times. 15852 15853 seekBarPoint = clamp(0, 1, seekBarPoint); 15854 15855 if (mouseTimeDisplay) { 15856 mouseTimeDisplay.update(seekBarRect, seekBarPoint); 15857 } 15858 15859 if (playProgressBar) { 15860 playProgressBar.update(seekBarRect, seekBar.getProgress()); 15861 } 15862 } 15863 /** 15864 * A throttled version of the {@link ProgressControl#handleMouseSeek} listener. 15865 * 15866 * @method ProgressControl#throttledHandleMouseSeek 15867 * @param {EventTarget~Event} event 15868 * The `mousemove` event that caused this function to run. 15869 * 15870 * @listen mousemove 15871 * @listen touchmove 15872 */ 15873 15874 /** 15875 * Handle `mousemove` or `touchmove` events on the `ProgressControl`. 15876 * 15877 * @param {EventTarget~Event} event 15878 * `mousedown` or `touchstart` event that triggered this function 15879 * 15880 * @listens mousemove 15881 * @listens touchmove 15882 */ 15883 ; 15884 15885 _proto.handleMouseSeek = function handleMouseSeek(event) { 15886 var seekBar = this.getChild('seekBar'); 15887 15888 if (seekBar) { 15889 seekBar.handleMouseMove(event); 15890 } 15891 } 15892 /** 15893 * Are controls are currently enabled for this progress control. 15894 * 15895 * @return {boolean} 15896 * true if controls are enabled, false otherwise 15897 */ 15898 ; 15899 15900 _proto.enabled = function enabled() { 15901 return this.enabled_; 15902 } 15903 /** 15904 * Disable all controls on the progress control and its children 15905 */ 15906 ; 15907 15908 _proto.disable = function disable() { 15909 this.children().forEach(function (child) { 15910 return child.disable && child.disable(); 15911 }); 15912 15913 if (!this.enabled()) { 15914 return; 15915 } 15916 15917 this.off(['mousedown', 'touchstart'], this.handleMouseDown); 15918 this.off(this.el_, 'mousemove', this.handleMouseMove); 15919 this.handleMouseUp(); 15920 this.addClass('disabled'); 15921 this.enabled_ = false; 15922 } 15923 /** 15924 * Enable all controls on the progress control and its children 15925 */ 15926 ; 15927 15928 _proto.enable = function enable() { 15929 this.children().forEach(function (child) { 15930 return child.enable && child.enable(); 15931 }); 15932 15933 if (this.enabled()) { 15934 return; 15935 } 15936 15937 this.on(['mousedown', 'touchstart'], this.handleMouseDown); 15938 this.on(this.el_, 'mousemove', this.handleMouseMove); 15939 this.removeClass('disabled'); 15940 this.enabled_ = true; 15941 } 15942 /** 15943 * Handle `mousedown` or `touchstart` events on the `ProgressControl`. 15944 * 15945 * @param {EventTarget~Event} event 15946 * `mousedown` or `touchstart` event that triggered this function 15947 * 15948 * @listens mousedown 15949 * @listens touchstart 15950 */ 15951 ; 15952 15953 _proto.handleMouseDown = function handleMouseDown(event) { 15954 var doc = this.el_.ownerDocument; 15955 var seekBar = this.getChild('seekBar'); 15956 15957 if (seekBar) { 15958 seekBar.handleMouseDown(event); 15959 } 15960 15961 this.on(doc, 'mousemove', this.throttledHandleMouseSeek); 15962 this.on(doc, 'touchmove', this.throttledHandleMouseSeek); 15963 this.on(doc, 'mouseup', this.handleMouseUp); 15964 this.on(doc, 'touchend', this.handleMouseUp); 15965 } 15966 /** 15967 * Handle `mouseup` or `touchend` events on the `ProgressControl`. 15968 * 15969 * @param {EventTarget~Event} event 15970 * `mouseup` or `touchend` event that triggered this function. 15971 * 15972 * @listens touchend 15973 * @listens mouseup 15974 */ 15975 ; 15976 15977 _proto.handleMouseUp = function handleMouseUp(event) { 15978 var doc = this.el_.ownerDocument; 15979 var seekBar = this.getChild('seekBar'); 15980 15981 if (seekBar) { 15982 seekBar.handleMouseUp(event); 15983 } 15984 15985 this.off(doc, 'mousemove', this.throttledHandleMouseSeek); 15986 this.off(doc, 'touchmove', this.throttledHandleMouseSeek); 15987 this.off(doc, 'mouseup', this.handleMouseUp); 15988 this.off(doc, 'touchend', this.handleMouseUp); 15989 }; 15990 15991 return ProgressControl; 15992 }(Component); 15993 /** 15994 * Default options for `ProgressControl` 15995 * 15996 * @type {Object} 15997 * @private 15998 */ 15999 16000 16001 ProgressControl.prototype.options_ = { 16002 children: ['seekBar'] 16003 }; 16004 Component.registerComponent('ProgressControl', ProgressControl); 16005 16006 /** 16007 * Toggle Picture-in-Picture mode 16008 * 16009 * @extends Button 16010 */ 16011 16012 var PictureInPictureToggle = 16013 /*#__PURE__*/ 16014 function (_Button) { 16015 inheritsLoose(PictureInPictureToggle, _Button); 16016 16017 /** 16018 * Creates an instance of this class. 16019 * 16020 * @param {Player} player 16021 * The `Player` that this class should be attached to. 16022 * 16023 * @param {Object} [options] 16024 * The key/value store of player options. 16025 * 16026 * @listens Player#enterpictureinpicture 16027 * @listens Player#leavepictureinpicture 16028 */ 16029 function PictureInPictureToggle(player, options) { 16030 var _this; 16031 16032 _this = _Button.call(this, player, options) || this; 16033 16034 _this.on(player, ['enterpictureinpicture', 'leavepictureinpicture'], _this.handlePictureInPictureChange); // TODO: Activate button on player loadedmetadata event. 16035 // TODO: Deactivate button on player emptied event. 16036 // TODO: Deactivate button if disablepictureinpicture attribute is present. 16037 16038 16039 if (!document.pictureInPictureEnabled) { 16040 _this.disable(); 16041 } 16042 16043 return _this; 16044 } 16045 /** 16046 * Builds the default DOM `className`. 16047 * 16048 * @return {string} 16049 * The DOM `className` for this object. 16050 */ 16051 16052 16053 var _proto = PictureInPictureToggle.prototype; 16054 16055 _proto.buildCSSClass = function buildCSSClass() { 16056 return "vjs-picture-in-picture-control " + _Button.prototype.buildCSSClass.call(this); 16057 } 16058 /** 16059 * Handles enterpictureinpicture and leavepictureinpicture on the player and change control text accordingly. 16060 * 16061 * @param {EventTarget~Event} [event] 16062 * The {@link Player#enterpictureinpicture} or {@link Player#leavepictureinpicture} event that caused this function to be 16063 * called. 16064 * 16065 * @listens Player#enterpictureinpicture 16066 * @listens Player#leavepictureinpicture 16067 */ 16068 ; 16069 16070 _proto.handlePictureInPictureChange = function handlePictureInPictureChange(event) { 16071 if (this.player_.isInPictureInPicture()) { 16072 this.controlText('Exit Picture-in-Picture'); 16073 } else { 16074 this.controlText('Picture-in-Picture'); 16075 } 16076 } 16077 /** 16078 * This gets called when an `PictureInPictureToggle` is "clicked". See 16079 * {@link ClickableComponent} for more detailed information on what a click can be. 16080 * 16081 * @param {EventTarget~Event} [event] 16082 * The `keydown`, `tap`, or `click` event that caused this function to be 16083 * called. 16084 * 16085 * @listens tap 16086 * @listens click 16087 */ 16088 ; 16089 16090 _proto.handleClick = function handleClick(event) { 16091 if (!this.player_.isInPictureInPicture()) { 16092 this.player_.requestPictureInPicture(); 16093 } else { 16094 this.player_.exitPictureInPicture(); 16095 } 16096 }; 16097 16098 return PictureInPictureToggle; 16099 }(Button); 16100 /** 16101 * The text that should display over the `PictureInPictureToggle`s controls. Added for localization. 16102 * 16103 * @type {string} 16104 * @private 16105 */ 16106 16107 16108 PictureInPictureToggle.prototype.controlText_ = 'Picture-in-Picture'; 16109 Component.registerComponent('PictureInPictureToggle', PictureInPictureToggle); 16110 16111 /** 16112 * Toggle fullscreen video 16113 * 16114 * @extends Button 16115 */ 16116 16117 var FullscreenToggle = 16118 /*#__PURE__*/ 16119 function (_Button) { 16120 inheritsLoose(FullscreenToggle, _Button); 16121 16122 /** 16123 * Creates an instance of this class. 16124 * 16125 * @param {Player} player 16126 * The `Player` that this class should be attached to. 16127 * 16128 * @param {Object} [options] 16129 * The key/value store of player options. 16130 */ 16131 function FullscreenToggle(player, options) { 16132 var _this; 16133 16134 _this = _Button.call(this, player, options) || this; 16135 16136 _this.on(player, 'fullscreenchange', _this.handleFullscreenChange); 16137 16138 if (document[player.fsApi_.fullscreenEnabled] === false) { 16139 _this.disable(); 16140 } 16141 16142 return _this; 16143 } 16144 /** 16145 * Builds the default DOM `className`. 16146 * 16147 * @return {string} 16148 * The DOM `className` for this object. 16149 */ 16150 16151 16152 var _proto = FullscreenToggle.prototype; 16153 16154 _proto.buildCSSClass = function buildCSSClass() { 16155 return "vjs-fullscreen-control " + _Button.prototype.buildCSSClass.call(this); 16156 } 16157 /** 16158 * Handles fullscreenchange on the player and change control text accordingly. 16159 * 16160 * @param {EventTarget~Event} [event] 16161 * The {@link Player#fullscreenchange} event that caused this function to be 16162 * called. 16163 * 16164 * @listens Player#fullscreenchange 16165 */ 16166 ; 16167 16168 _proto.handleFullscreenChange = function handleFullscreenChange(event) { 16169 if (this.player_.isFullscreen()) { 16170 this.controlText('Non-Fullscreen'); 16171 } else { 16172 this.controlText('Fullscreen'); 16173 } 16174 } 16175 /** 16176 * This gets called when an `FullscreenToggle` is "clicked". See 16177 * {@link ClickableComponent} for more detailed information on what a click can be. 16178 * 16179 * @param {EventTarget~Event} [event] 16180 * The `keydown`, `tap`, or `click` event that caused this function to be 16181 * called. 16182 * 16183 * @listens tap 16184 * @listens click 16185 */ 16186 ; 16187 16188 _proto.handleClick = function handleClick(event) { 16189 if (!this.player_.isFullscreen()) { 16190 this.player_.requestFullscreen(); 16191 } else { 16192 this.player_.exitFullscreen(); 16193 } 16194 }; 16195 16196 return FullscreenToggle; 16197 }(Button); 16198 /** 16199 * The text that should display over the `FullscreenToggle`s controls. Added for localization. 16200 * 16201 * @type {string} 16202 * @private 16203 */ 16204 16205 16206 FullscreenToggle.prototype.controlText_ = 'Fullscreen'; 16207 Component.registerComponent('FullscreenToggle', FullscreenToggle); 16208 16209 /** 16210 * Check if volume control is supported and if it isn't hide the 16211 * `Component` that was passed using the `vjs-hidden` class. 16212 * 16213 * @param {Component} self 16214 * The component that should be hidden if volume is unsupported 16215 * 16216 * @param {Player} player 16217 * A reference to the player 16218 * 16219 * @private 16220 */ 16221 var checkVolumeSupport = function checkVolumeSupport(self, player) { 16222 // hide volume controls when they're not supported by the current tech 16223 if (player.tech_ && !player.tech_.featuresVolumeControl) { 16224 self.addClass('vjs-hidden'); 16225 } 16226 16227 self.on(player, 'loadstart', function () { 16228 if (!player.tech_.featuresVolumeControl) { 16229 self.addClass('vjs-hidden'); 16230 } else { 16231 self.removeClass('vjs-hidden'); 16232 } 16233 }); 16234 }; 16235 16236 /** 16237 * Shows volume level 16238 * 16239 * @extends Component 16240 */ 16241 16242 var VolumeLevel = 16243 /*#__PURE__*/ 16244 function (_Component) { 16245 inheritsLoose(VolumeLevel, _Component); 16246 16247 function VolumeLevel() { 16248 return _Component.apply(this, arguments) || this; 16249 } 16250 16251 var _proto = VolumeLevel.prototype; 16252 16253 /** 16254 * Create the `Component`'s DOM element 16255 * 16256 * @return {Element} 16257 * The element that was created. 16258 */ 16259 _proto.createEl = function createEl() { 16260 return _Component.prototype.createEl.call(this, 'div', { 16261 className: 'vjs-volume-level', 16262 innerHTML: '<span class="vjs-control-text"></span>' 16263 }); 16264 }; 16265 16266 return VolumeLevel; 16267 }(Component); 16268 16269 Component.registerComponent('VolumeLevel', VolumeLevel); 16270 16271 /** 16272 * The bar that contains the volume level and can be clicked on to adjust the level 16273 * 16274 * @extends Slider 16275 */ 16276 16277 var VolumeBar = 16278 /*#__PURE__*/ 16279 function (_Slider) { 16280 inheritsLoose(VolumeBar, _Slider); 16281 16282 /** 16283 * Creates an instance of this class. 16284 * 16285 * @param {Player} player 16286 * The `Player` that this class should be attached to. 16287 * 16288 * @param {Object} [options] 16289 * The key/value store of player options. 16290 */ 16291 function VolumeBar(player, options) { 16292 var _this; 16293 16294 _this = _Slider.call(this, player, options) || this; 16295 16296 _this.on('slideractive', _this.updateLastVolume_); 16297 16298 _this.on(player, 'volumechange', _this.updateARIAAttributes); 16299 16300 player.ready(function () { 16301 return _this.updateARIAAttributes(); 16302 }); 16303 return _this; 16304 } 16305 /** 16306 * Create the `Component`'s DOM element 16307 * 16308 * @return {Element} 16309 * The element that was created. 16310 */ 16311 16312 16313 var _proto = VolumeBar.prototype; 16314 16315 _proto.createEl = function createEl() { 16316 return _Slider.prototype.createEl.call(this, 'div', { 16317 className: 'vjs-volume-bar vjs-slider-bar' 16318 }, { 16319 'aria-label': this.localize('Volume Level'), 16320 'aria-live': 'polite' 16321 }); 16322 } 16323 /** 16324 * Handle mouse down on volume bar 16325 * 16326 * @param {EventTarget~Event} event 16327 * The `mousedown` event that caused this to run. 16328 * 16329 * @listens mousedown 16330 */ 16331 ; 16332 16333 _proto.handleMouseDown = function handleMouseDown(event) { 16334 if (!isSingleLeftClick(event)) { 16335 return; 16336 } 16337 16338 _Slider.prototype.handleMouseDown.call(this, event); 16339 } 16340 /** 16341 * Handle movement events on the {@link VolumeMenuButton}. 16342 * 16343 * @param {EventTarget~Event} event 16344 * The event that caused this function to run. 16345 * 16346 * @listens mousemove 16347 */ 16348 ; 16349 16350 _proto.handleMouseMove = function handleMouseMove(event) { 16351 if (!isSingleLeftClick(event)) { 16352 return; 16353 } 16354 16355 this.checkMuted(); 16356 this.player_.volume(this.calculateDistance(event)); 16357 } 16358 /** 16359 * If the player is muted unmute it. 16360 */ 16361 ; 16362 16363 _proto.checkMuted = function checkMuted() { 16364 if (this.player_.muted()) { 16365 this.player_.muted(false); 16366 } 16367 } 16368 /** 16369 * Get percent of volume level 16370 * 16371 * @return {number} 16372 * Volume level percent as a decimal number. 16373 */ 16374 ; 16375 16376 _proto.getPercent = function getPercent() { 16377 if (this.player_.muted()) { 16378 return 0; 16379 } 16380 16381 return this.player_.volume(); 16382 } 16383 /** 16384 * Increase volume level for keyboard users 16385 */ 16386 ; 16387 16388 _proto.stepForward = function stepForward() { 16389 this.checkMuted(); 16390 this.player_.volume(this.player_.volume() + 0.1); 16391 } 16392 /** 16393 * Decrease volume level for keyboard users 16394 */ 16395 ; 16396 16397 _proto.stepBack = function stepBack() { 16398 this.checkMuted(); 16399 this.player_.volume(this.player_.volume() - 0.1); 16400 } 16401 /** 16402 * Update ARIA accessibility attributes 16403 * 16404 * @param {EventTarget~Event} [event] 16405 * The `volumechange` event that caused this function to run. 16406 * 16407 * @listens Player#volumechange 16408 */ 16409 ; 16410 16411 _proto.updateARIAAttributes = function updateARIAAttributes(event) { 16412 var ariaValue = this.player_.muted() ? 0 : this.volumeAsPercentage_(); 16413 this.el_.setAttribute('aria-valuenow', ariaValue); 16414 this.el_.setAttribute('aria-valuetext', ariaValue + '%'); 16415 } 16416 /** 16417 * Returns the current value of the player volume as a percentage 16418 * 16419 * @private 16420 */ 16421 ; 16422 16423 _proto.volumeAsPercentage_ = function volumeAsPercentage_() { 16424 return Math.round(this.player_.volume() * 100); 16425 } 16426 /** 16427 * When user starts dragging the VolumeBar, store the volume and listen for 16428 * the end of the drag. When the drag ends, if the volume was set to zero, 16429 * set lastVolume to the stored volume. 16430 * 16431 * @listens slideractive 16432 * @private 16433 */ 16434 ; 16435 16436 _proto.updateLastVolume_ = function updateLastVolume_() { 16437 var _this2 = this; 16438 16439 var volumeBeforeDrag = this.player_.volume(); 16440 this.one('sliderinactive', function () { 16441 if (_this2.player_.volume() === 0) { 16442 _this2.player_.lastVolume_(volumeBeforeDrag); 16443 } 16444 }); 16445 }; 16446 16447 return VolumeBar; 16448 }(Slider); 16449 /** 16450 * Default options for the `VolumeBar` 16451 * 16452 * @type {Object} 16453 * @private 16454 */ 16455 16456 16457 VolumeBar.prototype.options_ = { 16458 children: ['volumeLevel'], 16459 barName: 'volumeLevel' 16460 }; 16461 /** 16462 * Call the update event for this Slider when this event happens on the player. 16463 * 16464 * @type {string} 16465 */ 16466 16467 VolumeBar.prototype.playerEvent = 'volumechange'; 16468 Component.registerComponent('VolumeBar', VolumeBar); 16469 16470 /** 16471 * The component for controlling the volume level 16472 * 16473 * @extends Component 16474 */ 16475 16476 var VolumeControl = 16477 /*#__PURE__*/ 16478 function (_Component) { 16479 inheritsLoose(VolumeControl, _Component); 16480 16481 /** 16482 * Creates an instance of this class. 16483 * 16484 * @param {Player} player 16485 * The `Player` that this class should be attached to. 16486 * 16487 * @param {Object} [options={}] 16488 * The key/value store of player options. 16489 */ 16490 function VolumeControl(player, options) { 16491 var _this; 16492 16493 if (options === void 0) { 16494 options = {}; 16495 } 16496 16497 options.vertical = options.vertical || false; // Pass the vertical option down to the VolumeBar if 16498 // the VolumeBar is turned on. 16499 16500 if (typeof options.volumeBar === 'undefined' || isPlain(options.volumeBar)) { 16501 options.volumeBar = options.volumeBar || {}; 16502 options.volumeBar.vertical = options.vertical; 16503 } 16504 16505 _this = _Component.call(this, player, options) || this; // hide this control if volume support is missing 16506 16507 checkVolumeSupport(assertThisInitialized(_this), player); 16508 _this.throttledHandleMouseMove = throttle(bind(assertThisInitialized(_this), _this.handleMouseMove), UPDATE_REFRESH_INTERVAL); 16509 16510 _this.on('mousedown', _this.handleMouseDown); 16511 16512 _this.on('touchstart', _this.handleMouseDown); // while the slider is active (the mouse has been pressed down and 16513 // is dragging) or in focus we do not want to hide the VolumeBar 16514 16515 16516 _this.on(_this.volumeBar, ['focus', 'slideractive'], function () { 16517 _this.volumeBar.addClass('vjs-slider-active'); 16518 16519 _this.addClass('vjs-slider-active'); 16520 16521 _this.trigger('slideractive'); 16522 }); 16523 16524 _this.on(_this.volumeBar, ['blur', 'sliderinactive'], function () { 16525 _this.volumeBar.removeClass('vjs-slider-active'); 16526 16527 _this.removeClass('vjs-slider-active'); 16528 16529 _this.trigger('sliderinactive'); 16530 }); 16531 16532 return _this; 16533 } 16534 /** 16535 * Create the `Component`'s DOM element 16536 * 16537 * @return {Element} 16538 * The element that was created. 16539 */ 16540 16541 16542 var _proto = VolumeControl.prototype; 16543 16544 _proto.createEl = function createEl() { 16545 var orientationClass = 'vjs-volume-horizontal'; 16546 16547 if (this.options_.vertical) { 16548 orientationClass = 'vjs-volume-vertical'; 16549 } 16550 16551 return _Component.prototype.createEl.call(this, 'div', { 16552 className: "vjs-volume-control vjs-control " + orientationClass 16553 }); 16554 } 16555 /** 16556 * Handle `mousedown` or `touchstart` events on the `VolumeControl`. 16557 * 16558 * @param {EventTarget~Event} event 16559 * `mousedown` or `touchstart` event that triggered this function 16560 * 16561 * @listens mousedown 16562 * @listens touchstart 16563 */ 16564 ; 16565 16566 _proto.handleMouseDown = function handleMouseDown(event) { 16567 var doc = this.el_.ownerDocument; 16568 this.on(doc, 'mousemove', this.throttledHandleMouseMove); 16569 this.on(doc, 'touchmove', this.throttledHandleMouseMove); 16570 this.on(doc, 'mouseup', this.handleMouseUp); 16571 this.on(doc, 'touchend', this.handleMouseUp); 16572 } 16573 /** 16574 * Handle `mouseup` or `touchend` events on the `VolumeControl`. 16575 * 16576 * @param {EventTarget~Event} event 16577 * `mouseup` or `touchend` event that triggered this function. 16578 * 16579 * @listens touchend 16580 * @listens mouseup 16581 */ 16582 ; 16583 16584 _proto.handleMouseUp = function handleMouseUp(event) { 16585 var doc = this.el_.ownerDocument; 16586 this.off(doc, 'mousemove', this.throttledHandleMouseMove); 16587 this.off(doc, 'touchmove', this.throttledHandleMouseMove); 16588 this.off(doc, 'mouseup', this.handleMouseUp); 16589 this.off(doc, 'touchend', this.handleMouseUp); 16590 } 16591 /** 16592 * Handle `mousedown` or `touchstart` events on the `VolumeControl`. 16593 * 16594 * @param {EventTarget~Event} event 16595 * `mousedown` or `touchstart` event that triggered this function 16596 * 16597 * @listens mousedown 16598 * @listens touchstart 16599 */ 16600 ; 16601 16602 _proto.handleMouseMove = function handleMouseMove(event) { 16603 this.volumeBar.handleMouseMove(event); 16604 }; 16605 16606 return VolumeControl; 16607 }(Component); 16608 /** 16609 * Default options for the `VolumeControl` 16610 * 16611 * @type {Object} 16612 * @private 16613 */ 16614 16615 16616 VolumeControl.prototype.options_ = { 16617 children: ['volumeBar'] 16618 }; 16619 Component.registerComponent('VolumeControl', VolumeControl); 16620 16621 /** 16622 * Check if muting volume is supported and if it isn't hide the mute toggle 16623 * button. 16624 * 16625 * @param {Component} self 16626 * A reference to the mute toggle button 16627 * 16628 * @param {Player} player 16629 * A reference to the player 16630 * 16631 * @private 16632 */ 16633 var checkMuteSupport = function checkMuteSupport(self, player) { 16634 // hide mute toggle button if it's not supported by the current tech 16635 if (player.tech_ && !player.tech_.featuresMuteControl) { 16636 self.addClass('vjs-hidden'); 16637 } 16638 16639 self.on(player, 'loadstart', function () { 16640 if (!player.tech_.featuresMuteControl) { 16641 self.addClass('vjs-hidden'); 16642 } else { 16643 self.removeClass('vjs-hidden'); 16644 } 16645 }); 16646 }; 16647 16648 /** 16649 * A button component for muting the audio. 16650 * 16651 * @extends Button 16652 */ 16653 16654 var MuteToggle = 16655 /*#__PURE__*/ 16656 function (_Button) { 16657 inheritsLoose(MuteToggle, _Button); 16658 16659 /** 16660 * Creates an instance of this class. 16661 * 16662 * @param {Player} player 16663 * The `Player` that this class should be attached to. 16664 * 16665 * @param {Object} [options] 16666 * The key/value store of player options. 16667 */ 16668 function MuteToggle(player, options) { 16669 var _this; 16670 16671 _this = _Button.call(this, player, options) || this; // hide this control if volume support is missing 16672 16673 checkMuteSupport(assertThisInitialized(_this), player); 16674 16675 _this.on(player, ['loadstart', 'volumechange'], _this.update); 16676 16677 return _this; 16678 } 16679 /** 16680 * Builds the default DOM `className`. 16681 * 16682 * @return {string} 16683 * The DOM `className` for this object. 16684 */ 16685 16686 16687 var _proto = MuteToggle.prototype; 16688 16689 _proto.buildCSSClass = function buildCSSClass() { 16690 return "vjs-mute-control " + _Button.prototype.buildCSSClass.call(this); 16691 } 16692 /** 16693 * This gets called when an `MuteToggle` is "clicked". See 16694 * {@link ClickableComponent} for more detailed information on what a click can be. 16695 * 16696 * @param {EventTarget~Event} [event] 16697 * The `keydown`, `tap`, or `click` event that caused this function to be 16698 * called. 16699 * 16700 * @listens tap 16701 * @listens click 16702 */ 16703 ; 16704 16705 _proto.handleClick = function handleClick(event) { 16706 var vol = this.player_.volume(); 16707 var lastVolume = this.player_.lastVolume_(); 16708 16709 if (vol === 0) { 16710 var volumeToSet = lastVolume < 0.1 ? 0.1 : lastVolume; 16711 this.player_.volume(volumeToSet); 16712 this.player_.muted(false); 16713 } else { 16714 this.player_.muted(this.player_.muted() ? false : true); 16715 } 16716 } 16717 /** 16718 * Update the `MuteToggle` button based on the state of `volume` and `muted` 16719 * on the player. 16720 * 16721 * @param {EventTarget~Event} [event] 16722 * The {@link Player#loadstart} event if this function was called 16723 * through an event. 16724 * 16725 * @listens Player#loadstart 16726 * @listens Player#volumechange 16727 */ 16728 ; 16729 16730 _proto.update = function update(event) { 16731 this.updateIcon_(); 16732 this.updateControlText_(); 16733 } 16734 /** 16735 * Update the appearance of the `MuteToggle` icon. 16736 * 16737 * Possible states (given `level` variable below): 16738 * - 0: crossed out 16739 * - 1: zero bars of volume 16740 * - 2: one bar of volume 16741 * - 3: two bars of volume 16742 * 16743 * @private 16744 */ 16745 ; 16746 16747 _proto.updateIcon_ = function updateIcon_() { 16748 var vol = this.player_.volume(); 16749 var level = 3; // in iOS when a player is loaded with muted attribute 16750 // and volume is changed with a native mute button 16751 // we want to make sure muted state is updated 16752 16753 if (IS_IOS && this.player_.tech_ && this.player_.tech_.el_) { 16754 this.player_.muted(this.player_.tech_.el_.muted); 16755 } 16756 16757 if (vol === 0 || this.player_.muted()) { 16758 level = 0; 16759 } else if (vol < 0.33) { 16760 level = 1; 16761 } else if (vol < 0.67) { 16762 level = 2; 16763 } // TODO improve muted icon classes 16764 16765 16766 for (var i = 0; i < 4; i++) { 16767 removeClass(this.el_, "vjs-vol-" + i); 16768 } 16769 16770 addClass(this.el_, "vjs-vol-" + level); 16771 } 16772 /** 16773 * If `muted` has changed on the player, update the control text 16774 * (`title` attribute on `vjs-mute-control` element and content of 16775 * `vjs-control-text` element). 16776 * 16777 * @private 16778 */ 16779 ; 16780 16781 _proto.updateControlText_ = function updateControlText_() { 16782 var soundOff = this.player_.muted() || this.player_.volume() === 0; 16783 var text = soundOff ? 'Unmute' : 'Mute'; 16784 16785 if (this.controlText() !== text) { 16786 this.controlText(text); 16787 } 16788 }; 16789 16790 return MuteToggle; 16791 }(Button); 16792 /** 16793 * The text that should display over the `MuteToggle`s controls. Added for localization. 16794 * 16795 * @type {string} 16796 * @private 16797 */ 16798 16799 16800 MuteToggle.prototype.controlText_ = 'Mute'; 16801 Component.registerComponent('MuteToggle', MuteToggle); 16802 16803 /** 16804 * A Component to contain the MuteToggle and VolumeControl so that 16805 * they can work together. 16806 * 16807 * @extends Component 16808 */ 16809 16810 var VolumePanel = 16811 /*#__PURE__*/ 16812 function (_Component) { 16813 inheritsLoose(VolumePanel, _Component); 16814 16815 /** 16816 * Creates an instance of this class. 16817 * 16818 * @param {Player} player 16819 * The `Player` that this class should be attached to. 16820 * 16821 * @param {Object} [options={}] 16822 * The key/value store of player options. 16823 */ 16824 function VolumePanel(player, options) { 16825 var _this; 16826 16827 if (options === void 0) { 16828 options = {}; 16829 } 16830 16831 if (typeof options.inline !== 'undefined') { 16832 options.inline = options.inline; 16833 } else { 16834 options.inline = true; 16835 } // pass the inline option down to the VolumeControl as vertical if 16836 // the VolumeControl is on. 16837 16838 16839 if (typeof options.volumeControl === 'undefined' || isPlain(options.volumeControl)) { 16840 options.volumeControl = options.volumeControl || {}; 16841 options.volumeControl.vertical = !options.inline; 16842 } 16843 16844 _this = _Component.call(this, player, options) || this; 16845 16846 _this.on(player, ['loadstart'], _this.volumePanelState_); 16847 16848 _this.on(_this.muteToggle, 'keyup', _this.handleKeyPress); 16849 16850 _this.on(_this.volumeControl, 'keyup', _this.handleVolumeControlKeyUp); 16851 16852 _this.on('keydown', _this.handleKeyPress); 16853 16854 _this.on('mouseover', _this.handleMouseOver); 16855 16856 _this.on('mouseout', _this.handleMouseOut); // while the slider is active (the mouse has been pressed down and 16857 // is dragging) we do not want to hide the VolumeBar 16858 16859 16860 _this.on(_this.volumeControl, ['slideractive'], _this.sliderActive_); 16861 16862 _this.on(_this.volumeControl, ['sliderinactive'], _this.sliderInactive_); 16863 16864 return _this; 16865 } 16866 /** 16867 * Add vjs-slider-active class to the VolumePanel 16868 * 16869 * @listens VolumeControl#slideractive 16870 * @private 16871 */ 16872 16873 16874 var _proto = VolumePanel.prototype; 16875 16876 _proto.sliderActive_ = function sliderActive_() { 16877 this.addClass('vjs-slider-active'); 16878 } 16879 /** 16880 * Removes vjs-slider-active class to the VolumePanel 16881 * 16882 * @listens VolumeControl#sliderinactive 16883 * @private 16884 */ 16885 ; 16886 16887 _proto.sliderInactive_ = function sliderInactive_() { 16888 this.removeClass('vjs-slider-active'); 16889 } 16890 /** 16891 * Adds vjs-hidden or vjs-mute-toggle-only to the VolumePanel 16892 * depending on MuteToggle and VolumeControl state 16893 * 16894 * @listens Player#loadstart 16895 * @private 16896 */ 16897 ; 16898 16899 _proto.volumePanelState_ = function volumePanelState_() { 16900 // hide volume panel if neither volume control or mute toggle 16901 // are displayed 16902 if (this.volumeControl.hasClass('vjs-hidden') && this.muteToggle.hasClass('vjs-hidden')) { 16903 this.addClass('vjs-hidden'); 16904 } // if only mute toggle is visible we don't want 16905 // volume panel expanding when hovered or active 16906 16907 16908 if (this.volumeControl.hasClass('vjs-hidden') && !this.muteToggle.hasClass('vjs-hidden')) { 16909 this.addClass('vjs-mute-toggle-only'); 16910 } 16911 } 16912 /** 16913 * Create the `Component`'s DOM element 16914 * 16915 * @return {Element} 16916 * The element that was created. 16917 */ 16918 ; 16919 16920 _proto.createEl = function createEl() { 16921 var orientationClass = 'vjs-volume-panel-horizontal'; 16922 16923 if (!this.options_.inline) { 16924 orientationClass = 'vjs-volume-panel-vertical'; 16925 } 16926 16927 return _Component.prototype.createEl.call(this, 'div', { 16928 className: "vjs-volume-panel vjs-control " + orientationClass 16929 }); 16930 } 16931 /** 16932 * Dispose of the `volume-panel` and all child components. 16933 */ 16934 ; 16935 16936 _proto.dispose = function dispose() { 16937 this.handleMouseOut(); 16938 16939 _Component.prototype.dispose.call(this); 16940 } 16941 /** 16942 * Handles `keyup` events on the `VolumeControl`, looking for ESC, which closes 16943 * the volume panel and sets focus on `MuteToggle`. 16944 * 16945 * @param {EventTarget~Event} event 16946 * The `keyup` event that caused this function to be called. 16947 * 16948 * @listens keyup 16949 */ 16950 ; 16951 16952 _proto.handleVolumeControlKeyUp = function handleVolumeControlKeyUp(event) { 16953 if (keycode.isEventKey(event, 'Esc')) { 16954 this.muteToggle.focus(); 16955 } 16956 } 16957 /** 16958 * This gets called when a `VolumePanel` gains hover via a `mouseover` event. 16959 * Turns on listening for `mouseover` event. When they happen it 16960 * calls `this.handleMouseOver`. 16961 * 16962 * @param {EventTarget~Event} event 16963 * The `mouseover` event that caused this function to be called. 16964 * 16965 * @listens mouseover 16966 */ 16967 ; 16968 16969 _proto.handleMouseOver = function handleMouseOver(event) { 16970 this.addClass('vjs-hover'); 16971 on(document, 'keyup', bind(this, this.handleKeyPress)); 16972 } 16973 /** 16974 * This gets called when a `VolumePanel` gains hover via a `mouseout` event. 16975 * Turns on listening for `mouseout` event. When they happen it 16976 * calls `this.handleMouseOut`. 16977 * 16978 * @param {EventTarget~Event} event 16979 * The `mouseout` event that caused this function to be called. 16980 * 16981 * @listens mouseout 16982 */ 16983 ; 16984 16985 _proto.handleMouseOut = function handleMouseOut(event) { 16986 this.removeClass('vjs-hover'); 16987 off(document, 'keyup', bind(this, this.handleKeyPress)); 16988 } 16989 /** 16990 * Handles `keyup` event on the document or `keydown` event on the `VolumePanel`, 16991 * looking for ESC, which hides the `VolumeControl`. 16992 * 16993 * @param {EventTarget~Event} event 16994 * The keypress that triggered this event. 16995 * 16996 * @listens keydown | keyup 16997 */ 16998 ; 16999 17000 _proto.handleKeyPress = function handleKeyPress(event) { 17001 if (keycode.isEventKey(event, 'Esc')) { 17002 this.handleMouseOut(); 17003 } 17004 }; 17005 17006 return VolumePanel; 17007 }(Component); 17008 /** 17009 * Default options for the `VolumeControl` 17010 * 17011 * @type {Object} 17012 * @private 17013 */ 17014 17015 17016 VolumePanel.prototype.options_ = { 17017 children: ['muteToggle', 'volumeControl'] 17018 }; 17019 Component.registerComponent('VolumePanel', VolumePanel); 17020 17021 /** 17022 * The Menu component is used to build popup menus, including subtitle and 17023 * captions selection menus. 17024 * 17025 * @extends Component 17026 */ 17027 17028 var Menu = 17029 /*#__PURE__*/ 17030 function (_Component) { 17031 inheritsLoose(Menu, _Component); 17032 17033 /** 17034 * Create an instance of this class. 17035 * 17036 * @param {Player} player 17037 * the player that this component should attach to 17038 * 17039 * @param {Object} [options] 17040 * Object of option names and values 17041 * 17042 */ 17043 function Menu(player, options) { 17044 var _this; 17045 17046 _this = _Component.call(this, player, options) || this; 17047 17048 if (options) { 17049 _this.menuButton_ = options.menuButton; 17050 } 17051 17052 _this.focusedChild_ = -1; 17053 17054 _this.on('keydown', _this.handleKeyDown); // All the menu item instances share the same blur handler provided by the menu container. 17055 17056 17057 _this.boundHandleBlur_ = bind(assertThisInitialized(_this), _this.handleBlur); 17058 _this.boundHandleTapClick_ = bind(assertThisInitialized(_this), _this.handleTapClick); 17059 return _this; 17060 } 17061 /** 17062 * Add event listeners to the {@link MenuItem}. 17063 * 17064 * @param {Object} component 17065 * The instance of the `MenuItem` to add listeners to. 17066 * 17067 */ 17068 17069 17070 var _proto = Menu.prototype; 17071 17072 _proto.addEventListenerForItem = function addEventListenerForItem(component) { 17073 if (!(component instanceof Component)) { 17074 return; 17075 } 17076 17077 this.on(component, 'blur', this.boundHandleBlur_); 17078 this.on(component, ['tap', 'click'], this.boundHandleTapClick_); 17079 } 17080 /** 17081 * Remove event listeners from the {@link MenuItem}. 17082 * 17083 * @param {Object} component 17084 * The instance of the `MenuItem` to remove listeners. 17085 * 17086 */ 17087 ; 17088 17089 _proto.removeEventListenerForItem = function removeEventListenerForItem(component) { 17090 if (!(component instanceof Component)) { 17091 return; 17092 } 17093 17094 this.off(component, 'blur', this.boundHandleBlur_); 17095 this.off(component, ['tap', 'click'], this.boundHandleTapClick_); 17096 } 17097 /** 17098 * This method will be called indirectly when the component has been added 17099 * before the component adds to the new menu instance by `addItem`. 17100 * In this case, the original menu instance will remove the component 17101 * by calling `removeChild`. 17102 * 17103 * @param {Object} component 17104 * The instance of the `MenuItem` 17105 */ 17106 ; 17107 17108 _proto.removeChild = function removeChild(component) { 17109 if (typeof component === 'string') { 17110 component = this.getChild(component); 17111 } 17112 17113 this.removeEventListenerForItem(component); 17114 17115 _Component.prototype.removeChild.call(this, component); 17116 } 17117 /** 17118 * Add a {@link MenuItem} to the menu. 17119 * 17120 * @param {Object|string} component 17121 * The name or instance of the `MenuItem` to add. 17122 * 17123 */ 17124 ; 17125 17126 _proto.addItem = function addItem(component) { 17127 var childComponent = this.addChild(component); 17128 17129 if (childComponent) { 17130 this.addEventListenerForItem(childComponent); 17131 } 17132 } 17133 /** 17134 * Create the `Menu`s DOM element. 17135 * 17136 * @return {Element} 17137 * the element that was created 17138 */ 17139 ; 17140 17141 _proto.createEl = function createEl$1() { 17142 var contentElType = this.options_.contentElType || 'ul'; 17143 this.contentEl_ = createEl(contentElType, { 17144 className: 'vjs-menu-content' 17145 }); 17146 this.contentEl_.setAttribute('role', 'menu'); 17147 17148 var el = _Component.prototype.createEl.call(this, 'div', { 17149 append: this.contentEl_, 17150 className: 'vjs-menu' 17151 }); 17152 17153 el.appendChild(this.contentEl_); // Prevent clicks from bubbling up. Needed for Menu Buttons, 17154 // where a click on the parent is significant 17155 17156 on(el, 'click', function (event) { 17157 event.preventDefault(); 17158 event.stopImmediatePropagation(); 17159 }); 17160 return el; 17161 }; 17162 17163 _proto.dispose = function dispose() { 17164 this.contentEl_ = null; 17165 this.boundHandleBlur_ = null; 17166 this.boundHandleTapClick_ = null; 17167 17168 _Component.prototype.dispose.call(this); 17169 } 17170 /** 17171 * Called when a `MenuItem` loses focus. 17172 * 17173 * @param {EventTarget~Event} event 17174 * The `blur` event that caused this function to be called. 17175 * 17176 * @listens blur 17177 */ 17178 ; 17179 17180 _proto.handleBlur = function handleBlur(event) { 17181 var relatedTarget = event.relatedTarget || document.activeElement; // Close menu popup when a user clicks outside the menu 17182 17183 if (!this.children().some(function (element) { 17184 return element.el() === relatedTarget; 17185 })) { 17186 var btn = this.menuButton_; 17187 17188 if (btn && btn.buttonPressed_ && relatedTarget !== btn.el().firstChild) { 17189 btn.unpressButton(); 17190 } 17191 } 17192 } 17193 /** 17194 * Called when a `MenuItem` gets clicked or tapped. 17195 * 17196 * @param {EventTarget~Event} event 17197 * The `click` or `tap` event that caused this function to be called. 17198 * 17199 * @listens click,tap 17200 */ 17201 ; 17202 17203 _proto.handleTapClick = function handleTapClick(event) { 17204 // Unpress the associated MenuButton, and move focus back to it 17205 if (this.menuButton_) { 17206 this.menuButton_.unpressButton(); 17207 var childComponents = this.children(); 17208 17209 if (!Array.isArray(childComponents)) { 17210 return; 17211 } 17212 17213 var foundComponent = childComponents.filter(function (component) { 17214 return component.el() === event.target; 17215 })[0]; 17216 17217 if (!foundComponent) { 17218 return; 17219 } // don't focus menu button if item is a caption settings item 17220 // because focus will move elsewhere 17221 17222 17223 if (foundComponent.name() !== 'CaptionSettingsMenuItem') { 17224 this.menuButton_.focus(); 17225 } 17226 } 17227 } 17228 /** 17229 * Handle a `keydown` event on this menu. This listener is added in the constructor. 17230 * 17231 * @param {EventTarget~Event} event 17232 * A `keydown` event that happened on the menu. 17233 * 17234 * @listens keydown 17235 */ 17236 ; 17237 17238 _proto.handleKeyDown = function handleKeyDown(event) { 17239 // Left and Down Arrows 17240 if (keycode.isEventKey(event, 'Left') || keycode.isEventKey(event, 'Down')) { 17241 event.preventDefault(); 17242 event.stopPropagation(); 17243 this.stepForward(); // Up and Right Arrows 17244 } else if (keycode.isEventKey(event, 'Right') || keycode.isEventKey(event, 'Up')) { 17245 event.preventDefault(); 17246 event.stopPropagation(); 17247 this.stepBack(); 17248 } 17249 } 17250 /** 17251 * Move to next (lower) menu item for keyboard users. 17252 */ 17253 ; 17254 17255 _proto.stepForward = function stepForward() { 17256 var stepChild = 0; 17257 17258 if (this.focusedChild_ !== undefined) { 17259 stepChild = this.focusedChild_ + 1; 17260 } 17261 17262 this.focus(stepChild); 17263 } 17264 /** 17265 * Move to previous (higher) menu item for keyboard users. 17266 */ 17267 ; 17268 17269 _proto.stepBack = function stepBack() { 17270 var stepChild = 0; 17271 17272 if (this.focusedChild_ !== undefined) { 17273 stepChild = this.focusedChild_ - 1; 17274 } 17275 17276 this.focus(stepChild); 17277 } 17278 /** 17279 * Set focus on a {@link MenuItem} in the `Menu`. 17280 * 17281 * @param {Object|string} [item=0] 17282 * Index of child item set focus on. 17283 */ 17284 ; 17285 17286 _proto.focus = function focus(item) { 17287 if (item === void 0) { 17288 item = 0; 17289 } 17290 17291 var children = this.children().slice(); 17292 var haveTitle = children.length && children[0].className && /vjs-menu-title/.test(children[0].className); 17293 17294 if (haveTitle) { 17295 children.shift(); 17296 } 17297 17298 if (children.length > 0) { 17299 if (item < 0) { 17300 item = 0; 17301 } else if (item >= children.length) { 17302 item = children.length - 1; 17303 } 17304 17305 this.focusedChild_ = item; 17306 children[item].el_.focus(); 17307 } 17308 }; 17309 17310 return Menu; 17311 }(Component); 17312 17313 Component.registerComponent('Menu', Menu); 17314 17315 /** 17316 * A `MenuButton` class for any popup {@link Menu}. 17317 * 17318 * @extends Component 17319 */ 17320 17321 var MenuButton = 17322 /*#__PURE__*/ 17323 function (_Component) { 17324 inheritsLoose(MenuButton, _Component); 17325 17326 /** 17327 * Creates an instance of this class. 17328 * 17329 * @param {Player} player 17330 * The `Player` that this class should be attached to. 17331 * 17332 * @param {Object} [options={}] 17333 * The key/value store of player options. 17334 */ 17335 function MenuButton(player, options) { 17336 var _this; 17337 17338 if (options === void 0) { 17339 options = {}; 17340 } 17341 17342 _this = _Component.call(this, player, options) || this; 17343 _this.menuButton_ = new Button(player, options); 17344 17345 _this.menuButton_.controlText(_this.controlText_); 17346 17347 _this.menuButton_.el_.setAttribute('aria-haspopup', 'true'); // Add buildCSSClass values to the button, not the wrapper 17348 17349 17350 var buttonClass = Button.prototype.buildCSSClass(); 17351 _this.menuButton_.el_.className = _this.buildCSSClass() + ' ' + buttonClass; 17352 17353 _this.menuButton_.removeClass('vjs-control'); 17354 17355 _this.addChild(_this.menuButton_); 17356 17357 _this.update(); 17358 17359 _this.enabled_ = true; 17360 17361 _this.on(_this.menuButton_, 'tap', _this.handleClick); 17362 17363 _this.on(_this.menuButton_, 'click', _this.handleClick); 17364 17365 _this.on(_this.menuButton_, 'keydown', _this.handleKeyDown); 17366 17367 _this.on(_this.menuButton_, 'mouseenter', function () { 17368 _this.addClass('vjs-hover'); 17369 17370 _this.menu.show(); 17371 17372 on(document, 'keyup', bind(assertThisInitialized(_this), _this.handleMenuKeyUp)); 17373 }); 17374 17375 _this.on('mouseleave', _this.handleMouseLeave); 17376 17377 _this.on('keydown', _this.handleSubmenuKeyDown); 17378 17379 return _this; 17380 } 17381 /** 17382 * Update the menu based on the current state of its items. 17383 */ 17384 17385 17386 var _proto = MenuButton.prototype; 17387 17388 _proto.update = function update() { 17389 var menu = this.createMenu(); 17390 17391 if (this.menu) { 17392 this.menu.dispose(); 17393 this.removeChild(this.menu); 17394 } 17395 17396 this.menu = menu; 17397 this.addChild(menu); 17398 /** 17399 * Track the state of the menu button 17400 * 17401 * @type {Boolean} 17402 * @private 17403 */ 17404 17405 this.buttonPressed_ = false; 17406 this.menuButton_.el_.setAttribute('aria-expanded', 'false'); 17407 17408 if (this.items && this.items.length <= this.hideThreshold_) { 17409 this.hide(); 17410 } else { 17411 this.show(); 17412 } 17413 } 17414 /** 17415 * Create the menu and add all items to it. 17416 * 17417 * @return {Menu} 17418 * The constructed menu 17419 */ 17420 ; 17421 17422 _proto.createMenu = function createMenu() { 17423 var menu = new Menu(this.player_, { 17424 menuButton: this 17425 }); 17426 /** 17427 * Hide the menu if the number of items is less than or equal to this threshold. This defaults 17428 * to 0 and whenever we add items which can be hidden to the menu we'll increment it. We list 17429 * it here because every time we run `createMenu` we need to reset the value. 17430 * 17431 * @protected 17432 * @type {Number} 17433 */ 17434 17435 this.hideThreshold_ = 0; // Add a title list item to the top 17436 17437 if (this.options_.title) { 17438 var titleEl = createEl('li', { 17439 className: 'vjs-menu-title', 17440 innerHTML: toTitleCase(this.options_.title), 17441 tabIndex: -1 17442 }); 17443 this.hideThreshold_ += 1; 17444 var titleComponent = new Component(this.player_, { 17445 el: titleEl 17446 }); 17447 menu.addItem(titleComponent); 17448 } 17449 17450 this.items = this.createItems(); 17451 17452 if (this.items) { 17453 // Add menu items to the menu 17454 for (var i = 0; i < this.items.length; i++) { 17455 menu.addItem(this.items[i]); 17456 } 17457 } 17458 17459 return menu; 17460 } 17461 /** 17462 * Create the list of menu items. Specific to each subclass. 17463 * 17464 * @abstract 17465 */ 17466 ; 17467 17468 _proto.createItems = function createItems() {} 17469 /** 17470 * Create the `MenuButtons`s DOM element. 17471 * 17472 * @return {Element} 17473 * The element that gets created. 17474 */ 17475 ; 17476 17477 _proto.createEl = function createEl() { 17478 return _Component.prototype.createEl.call(this, 'div', { 17479 className: this.buildWrapperCSSClass() 17480 }, {}); 17481 } 17482 /** 17483 * Allow sub components to stack CSS class names for the wrapper element 17484 * 17485 * @return {string} 17486 * The constructed wrapper DOM `className` 17487 */ 17488 ; 17489 17490 _proto.buildWrapperCSSClass = function buildWrapperCSSClass() { 17491 var menuButtonClass = 'vjs-menu-button'; // If the inline option is passed, we want to use different styles altogether. 17492 17493 if (this.options_.inline === true) { 17494 menuButtonClass += '-inline'; 17495 } else { 17496 menuButtonClass += '-popup'; 17497 } // TODO: Fix the CSS so that this isn't necessary 17498 17499 17500 var buttonClass = Button.prototype.buildCSSClass(); 17501 return "vjs-menu-button " + menuButtonClass + " " + buttonClass + " " + _Component.prototype.buildCSSClass.call(this); 17502 } 17503 /** 17504 * Builds the default DOM `className`. 17505 * 17506 * @return {string} 17507 * The DOM `className` for this object. 17508 */ 17509 ; 17510 17511 _proto.buildCSSClass = function buildCSSClass() { 17512 var menuButtonClass = 'vjs-menu-button'; // If the inline option is passed, we want to use different styles altogether. 17513 17514 if (this.options_.inline === true) { 17515 menuButtonClass += '-inline'; 17516 } else { 17517 menuButtonClass += '-popup'; 17518 } 17519 17520 return "vjs-menu-button " + menuButtonClass + " " + _Component.prototype.buildCSSClass.call(this); 17521 } 17522 /** 17523 * Get or set the localized control text that will be used for accessibility. 17524 * 17525 * > NOTE: This will come from the internal `menuButton_` element. 17526 * 17527 * @param {string} [text] 17528 * Control text for element. 17529 * 17530 * @param {Element} [el=this.menuButton_.el()] 17531 * Element to set the title on. 17532 * 17533 * @return {string} 17534 * - The control text when getting 17535 */ 17536 ; 17537 17538 _proto.controlText = function controlText(text, el) { 17539 if (el === void 0) { 17540 el = this.menuButton_.el(); 17541 } 17542 17543 return this.menuButton_.controlText(text, el); 17544 } 17545 /** 17546 * Dispose of the `menu-button` and all child components. 17547 */ 17548 ; 17549 17550 _proto.dispose = function dispose() { 17551 this.handleMouseLeave(); 17552 17553 _Component.prototype.dispose.call(this); 17554 } 17555 /** 17556 * Handle a click on a `MenuButton`. 17557 * See {@link ClickableComponent#handleClick} for instances where this is called. 17558 * 17559 * @param {EventTarget~Event} event 17560 * The `keydown`, `tap`, or `click` event that caused this function to be 17561 * called. 17562 * 17563 * @listens tap 17564 * @listens click 17565 */ 17566 ; 17567 17568 _proto.handleClick = function handleClick(event) { 17569 if (this.buttonPressed_) { 17570 this.unpressButton(); 17571 } else { 17572 this.pressButton(); 17573 } 17574 } 17575 /** 17576 * Handle `mouseleave` for `MenuButton`. 17577 * 17578 * @param {EventTarget~Event} event 17579 * The `mouseleave` event that caused this function to be called. 17580 * 17581 * @listens mouseleave 17582 */ 17583 ; 17584 17585 _proto.handleMouseLeave = function handleMouseLeave(event) { 17586 this.removeClass('vjs-hover'); 17587 off(document, 'keyup', bind(this, this.handleMenuKeyUp)); 17588 } 17589 /** 17590 * Set the focus to the actual button, not to this element 17591 */ 17592 ; 17593 17594 _proto.focus = function focus() { 17595 this.menuButton_.focus(); 17596 } 17597 /** 17598 * Remove the focus from the actual button, not this element 17599 */ 17600 ; 17601 17602 _proto.blur = function blur() { 17603 this.menuButton_.blur(); 17604 } 17605 /** 17606 * Handle tab, escape, down arrow, and up arrow keys for `MenuButton`. See 17607 * {@link ClickableComponent#handleKeyDown} for instances where this is called. 17608 * 17609 * @param {EventTarget~Event} event 17610 * The `keydown` event that caused this function to be called. 17611 * 17612 * @listens keydown 17613 */ 17614 ; 17615 17616 _proto.handleKeyDown = function handleKeyDown(event) { 17617 // Escape or Tab unpress the 'button' 17618 if (keycode.isEventKey(event, 'Esc') || keycode.isEventKey(event, 'Tab')) { 17619 if (this.buttonPressed_) { 17620 this.unpressButton(); 17621 } // Don't preventDefault for Tab key - we still want to lose focus 17622 17623 17624 if (!keycode.isEventKey(event, 'Tab')) { 17625 event.preventDefault(); // Set focus back to the menu button's button 17626 17627 this.menuButton_.focus(); 17628 } // Up Arrow or Down Arrow also 'press' the button to open the menu 17629 17630 } else if (keycode.isEventKey(event, 'Up') || keycode.isEventKey(event, 'Down')) { 17631 if (!this.buttonPressed_) { 17632 event.preventDefault(); 17633 this.pressButton(); 17634 } 17635 } 17636 } 17637 /** 17638 * Handle a `keyup` event on a `MenuButton`. The listener for this is added in 17639 * the constructor. 17640 * 17641 * @param {EventTarget~Event} event 17642 * Key press event 17643 * 17644 * @listens keyup 17645 */ 17646 ; 17647 17648 _proto.handleMenuKeyUp = function handleMenuKeyUp(event) { 17649 // Escape hides popup menu 17650 if (keycode.isEventKey(event, 'Esc') || keycode.isEventKey(event, 'Tab')) { 17651 this.removeClass('vjs-hover'); 17652 } 17653 } 17654 /** 17655 * This method name now delegates to `handleSubmenuKeyDown`. This means 17656 * anyone calling `handleSubmenuKeyPress` will not see their method calls 17657 * stop working. 17658 * 17659 * @param {EventTarget~Event} event 17660 * The event that caused this function to be called. 17661 */ 17662 ; 17663 17664 _proto.handleSubmenuKeyPress = function handleSubmenuKeyPress(event) { 17665 this.handleSubmenuKeyDown(event); 17666 } 17667 /** 17668 * Handle a `keydown` event on a sub-menu. The listener for this is added in 17669 * the constructor. 17670 * 17671 * @param {EventTarget~Event} event 17672 * Key press event 17673 * 17674 * @listens keydown 17675 */ 17676 ; 17677 17678 _proto.handleSubmenuKeyDown = function handleSubmenuKeyDown(event) { 17679 // Escape or Tab unpress the 'button' 17680 if (keycode.isEventKey(event, 'Esc') || keycode.isEventKey(event, 'Tab')) { 17681 if (this.buttonPressed_) { 17682 this.unpressButton(); 17683 } // Don't preventDefault for Tab key - we still want to lose focus 17684 17685 17686 if (!keycode.isEventKey(event, 'Tab')) { 17687 event.preventDefault(); // Set focus back to the menu button's button 17688 17689 this.menuButton_.focus(); 17690 } 17691 } 17692 } 17693 /** 17694 * Put the current `MenuButton` into a pressed state. 17695 */ 17696 ; 17697 17698 _proto.pressButton = function pressButton() { 17699 if (this.enabled_) { 17700 this.buttonPressed_ = true; 17701 this.menu.show(); 17702 this.menu.lockShowing(); 17703 this.menuButton_.el_.setAttribute('aria-expanded', 'true'); // set the focus into the submenu, except on iOS where it is resulting in 17704 // undesired scrolling behavior when the player is in an iframe 17705 17706 if (IS_IOS && isInFrame()) { 17707 // Return early so that the menu isn't focused 17708 return; 17709 } 17710 17711 this.menu.focus(); 17712 } 17713 } 17714 /** 17715 * Take the current `MenuButton` out of a pressed state. 17716 */ 17717 ; 17718 17719 _proto.unpressButton = function unpressButton() { 17720 if (this.enabled_) { 17721 this.buttonPressed_ = false; 17722 this.menu.unlockShowing(); 17723 this.menu.hide(); 17724 this.menuButton_.el_.setAttribute('aria-expanded', 'false'); 17725 } 17726 } 17727 /** 17728 * Disable the `MenuButton`. Don't allow it to be clicked. 17729 */ 17730 ; 17731 17732 _proto.disable = function disable() { 17733 this.unpressButton(); 17734 this.enabled_ = false; 17735 this.addClass('vjs-disabled'); 17736 this.menuButton_.disable(); 17737 } 17738 /** 17739 * Enable the `MenuButton`. Allow it to be clicked. 17740 */ 17741 ; 17742 17743 _proto.enable = function enable() { 17744 this.enabled_ = true; 17745 this.removeClass('vjs-disabled'); 17746 this.menuButton_.enable(); 17747 }; 17748 17749 return MenuButton; 17750 }(Component); 17751 17752 Component.registerComponent('MenuButton', MenuButton); 17753 17754 /** 17755 * The base class for buttons that toggle specific track types (e.g. subtitles). 17756 * 17757 * @extends MenuButton 17758 */ 17759 17760 var TrackButton = 17761 /*#__PURE__*/ 17762 function (_MenuButton) { 17763 inheritsLoose(TrackButton, _MenuButton); 17764 17765 /** 17766 * Creates an instance of this class. 17767 * 17768 * @param {Player} player 17769 * The `Player` that this class should be attached to. 17770 * 17771 * @param {Object} [options] 17772 * The key/value store of player options. 17773 */ 17774 function TrackButton(player, options) { 17775 var _this; 17776 17777 var tracks = options.tracks; 17778 _this = _MenuButton.call(this, player, options) || this; 17779 17780 if (_this.items.length <= 1) { 17781 _this.hide(); 17782 } 17783 17784 if (!tracks) { 17785 return assertThisInitialized(_this); 17786 } 17787 17788 var updateHandler = bind(assertThisInitialized(_this), _this.update); 17789 tracks.addEventListener('removetrack', updateHandler); 17790 tracks.addEventListener('addtrack', updateHandler); 17791 17792 _this.player_.on('ready', updateHandler); 17793 17794 _this.player_.on('dispose', function () { 17795 tracks.removeEventListener('removetrack', updateHandler); 17796 tracks.removeEventListener('addtrack', updateHandler); 17797 }); 17798 17799 return _this; 17800 } 17801 17802 return TrackButton; 17803 }(MenuButton); 17804 17805 Component.registerComponent('TrackButton', TrackButton); 17806 17807 /** 17808 * @file menu-keys.js 17809 */ 17810 17811 /** 17812 * All keys used for operation of a menu (`MenuButton`, `Menu`, and `MenuItem`) 17813 * Note that 'Enter' and 'Space' are not included here (otherwise they would 17814 * prevent the `MenuButton` and `MenuItem` from being keyboard-clickable) 17815 * @typedef MenuKeys 17816 * @array 17817 */ 17818 var MenuKeys = ['Tab', 'Esc', 'Up', 'Down', 'Right', 'Left']; 17819 17820 /** 17821 * The component for a menu item. `<li>` 17822 * 17823 * @extends ClickableComponent 17824 */ 17825 17826 var MenuItem = 17827 /*#__PURE__*/ 17828 function (_ClickableComponent) { 17829 inheritsLoose(MenuItem, _ClickableComponent); 17830 17831 /** 17832 * Creates an instance of the this class. 17833 * 17834 * @param {Player} player 17835 * The `Player` that this class should be attached to. 17836 * 17837 * @param {Object} [options={}] 17838 * The key/value store of player options. 17839 * 17840 */ 17841 function MenuItem(player, options) { 17842 var _this; 17843 17844 _this = _ClickableComponent.call(this, player, options) || this; 17845 _this.selectable = options.selectable; 17846 _this.isSelected_ = options.selected || false; 17847 _this.multiSelectable = options.multiSelectable; 17848 17849 _this.selected(_this.isSelected_); 17850 17851 if (_this.selectable) { 17852 if (_this.multiSelectable) { 17853 _this.el_.setAttribute('role', 'menuitemcheckbox'); 17854 } else { 17855 _this.el_.setAttribute('role', 'menuitemradio'); 17856 } 17857 } else { 17858 _this.el_.setAttribute('role', 'menuitem'); 17859 } 17860 17861 return _this; 17862 } 17863 /** 17864 * Create the `MenuItem's DOM element 17865 * 17866 * @param {string} [type=li] 17867 * Element's node type, not actually used, always set to `li`. 17868 * 17869 * @param {Object} [props={}] 17870 * An object of properties that should be set on the element 17871 * 17872 * @param {Object} [attrs={}] 17873 * An object of attributes that should be set on the element 17874 * 17875 * @return {Element} 17876 * The element that gets created. 17877 */ 17878 17879 17880 var _proto = MenuItem.prototype; 17881 17882 _proto.createEl = function createEl(type, props, attrs) { 17883 // The control is textual, not just an icon 17884 this.nonIconControl = true; 17885 return _ClickableComponent.prototype.createEl.call(this, 'li', assign({ 17886 className: 'vjs-menu-item', 17887 innerHTML: "<span class=\"vjs-menu-item-text\">" + this.localize(this.options_.label) + "</span>", 17888 tabIndex: -1 17889 }, props), attrs); 17890 } 17891 /** 17892 * Ignore keys which are used by the menu, but pass any other ones up. See 17893 * {@link ClickableComponent#handleKeyDown} for instances where this is called. 17894 * 17895 * @param {EventTarget~Event} event 17896 * The `keydown` event that caused this function to be called. 17897 * 17898 * @listens keydown 17899 */ 17900 ; 17901 17902 _proto.handleKeyDown = function handleKeyDown(event) { 17903 if (!MenuKeys.some(function (key) { 17904 return keycode.isEventKey(event, key); 17905 })) { 17906 // Pass keydown handling up for unused keys 17907 _ClickableComponent.prototype.handleKeyDown.call(this, event); 17908 } 17909 } 17910 /** 17911 * Any click on a `MenuItem` puts it into the selected state. 17912 * See {@link ClickableComponent#handleClick} for instances where this is called. 17913 * 17914 * @param {EventTarget~Event} event 17915 * The `keydown`, `tap`, or `click` event that caused this function to be 17916 * called. 17917 * 17918 * @listens tap 17919 * @listens click 17920 */ 17921 ; 17922 17923 _proto.handleClick = function handleClick(event) { 17924 this.selected(true); 17925 } 17926 /** 17927 * Set the state for this menu item as selected or not. 17928 * 17929 * @param {boolean} selected 17930 * if the menu item is selected or not 17931 */ 17932 ; 17933 17934 _proto.selected = function selected(_selected) { 17935 if (this.selectable) { 17936 if (_selected) { 17937 this.addClass('vjs-selected'); 17938 this.el_.setAttribute('aria-checked', 'true'); // aria-checked isn't fully supported by browsers/screen readers, 17939 // so indicate selected state to screen reader in the control text. 17940 17941 this.controlText(', selected'); 17942 this.isSelected_ = true; 17943 } else { 17944 this.removeClass('vjs-selected'); 17945 this.el_.setAttribute('aria-checked', 'false'); // Indicate un-selected state to screen reader 17946 17947 this.controlText(''); 17948 this.isSelected_ = false; 17949 } 17950 } 17951 }; 17952 17953 return MenuItem; 17954 }(ClickableComponent); 17955 17956 Component.registerComponent('MenuItem', MenuItem); 17957 17958 /** 17959 * The specific menu item type for selecting a language within a text track kind 17960 * 17961 * @extends MenuItem 17962 */ 17963 17964 var TextTrackMenuItem = 17965 /*#__PURE__*/ 17966 function (_MenuItem) { 17967 inheritsLoose(TextTrackMenuItem, _MenuItem); 17968 17969 /** 17970 * Creates an instance of this class. 17971 * 17972 * @param {Player} player 17973 * The `Player` that this class should be attached to. 17974 * 17975 * @param {Object} [options] 17976 * The key/value store of player options. 17977 */ 17978 function TextTrackMenuItem(player, options) { 17979 var _this; 17980 17981 var track = options.track; 17982 var tracks = player.textTracks(); // Modify options for parent MenuItem class's init. 17983 17984 options.label = track.label || track.language || 'Unknown'; 17985 options.selected = track.mode === 'showing'; 17986 _this = _MenuItem.call(this, player, options) || this; 17987 _this.track = track; // Determine the relevant kind(s) of tracks for this component and filter 17988 // out empty kinds. 17989 17990 _this.kinds = (options.kinds || [options.kind || _this.track.kind]).filter(Boolean); 17991 17992 var changeHandler = function changeHandler() { 17993 for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { 17994 args[_key] = arguments[_key]; 17995 } 17996 17997 _this.handleTracksChange.apply(assertThisInitialized(_this), args); 17998 }; 17999 18000 var selectedLanguageChangeHandler = function selectedLanguageChangeHandler() { 18001 for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { 18002 args[_key2] = arguments[_key2]; 18003 } 18004 18005 _this.handleSelectedLanguageChange.apply(assertThisInitialized(_this), args); 18006 }; 18007 18008 player.on(['loadstart', 'texttrackchange'], changeHandler); 18009 tracks.addEventListener('change', changeHandler); 18010 tracks.addEventListener('selectedlanguagechange', selectedLanguageChangeHandler); 18011 18012 _this.on('dispose', function () { 18013 player.off(['loadstart', 'texttrackchange'], changeHandler); 18014 tracks.removeEventListener('change', changeHandler); 18015 tracks.removeEventListener('selectedlanguagechange', selectedLanguageChangeHandler); 18016 }); // iOS7 doesn't dispatch change events to TextTrackLists when an 18017 // associated track's mode changes. Without something like 18018 // Object.observe() (also not present on iOS7), it's not 18019 // possible to detect changes to the mode attribute and polyfill 18020 // the change event. As a poor substitute, we manually dispatch 18021 // change events whenever the controls modify the mode. 18022 18023 18024 if (tracks.onchange === undefined) { 18025 var event; 18026 18027 _this.on(['tap', 'click'], function () { 18028 if (typeof window$1.Event !== 'object') { 18029 // Android 2.3 throws an Illegal Constructor error for window.Event 18030 try { 18031 event = new window$1.Event('change'); 18032 } catch (err) {// continue regardless of error 18033 } 18034 } 18035 18036 if (!event) { 18037 event = document.createEvent('Event'); 18038 event.initEvent('change', true, true); 18039 } 18040 18041 tracks.dispatchEvent(event); 18042 }); 18043 } // set the default state based on current tracks 18044 18045 18046 _this.handleTracksChange(); 18047 18048 return _this; 18049 } 18050 /** 18051 * This gets called when an `TextTrackMenuItem` is "clicked". See 18052 * {@link ClickableComponent} for more detailed information on what a click can be. 18053 * 18054 * @param {EventTarget~Event} event 18055 * The `keydown`, `tap`, or `click` event that caused this function to be 18056 * called. 18057 * 18058 * @listens tap 18059 * @listens click 18060 */ 18061 18062 18063 var _proto = TextTrackMenuItem.prototype; 18064 18065 _proto.handleClick = function handleClick(event) { 18066 var referenceTrack = this.track; 18067 var tracks = this.player_.textTracks(); 18068 18069 _MenuItem.prototype.handleClick.call(this, event); 18070 18071 if (!tracks) { 18072 return; 18073 } 18074 18075 for (var i = 0; i < tracks.length; i++) { 18076 var track = tracks[i]; // If the track from the text tracks list is not of the right kind, 18077 // skip it. We do not want to affect tracks of incompatible kind(s). 18078 18079 if (this.kinds.indexOf(track.kind) === -1) { 18080 continue; 18081 } // If this text track is the component's track and it is not showing, 18082 // set it to showing. 18083 18084 18085 if (track === referenceTrack) { 18086 if (track.mode !== 'showing') { 18087 track.mode = 'showing'; 18088 } // If this text track is not the component's track and it is not 18089 // disabled, set it to disabled. 18090 18091 } else if (track.mode !== 'disabled') { 18092 track.mode = 'disabled'; 18093 } 18094 } 18095 } 18096 /** 18097 * Handle text track list change 18098 * 18099 * @param {EventTarget~Event} event 18100 * The `change` event that caused this function to be called. 18101 * 18102 * @listens TextTrackList#change 18103 */ 18104 ; 18105 18106 _proto.handleTracksChange = function handleTracksChange(event) { 18107 var shouldBeSelected = this.track.mode === 'showing'; // Prevent redundant selected() calls because they may cause 18108 // screen readers to read the appended control text unnecessarily 18109 18110 if (shouldBeSelected !== this.isSelected_) { 18111 this.selected(shouldBeSelected); 18112 } 18113 }; 18114 18115 _proto.handleSelectedLanguageChange = function handleSelectedLanguageChange(event) { 18116 if (this.track.mode === 'showing') { 18117 var selectedLanguage = this.player_.cache_.selectedLanguage; // Don't replace the kind of track across the same language 18118 18119 if (selectedLanguage && selectedLanguage.enabled && selectedLanguage.language === this.track.language && selectedLanguage.kind !== this.track.kind) { 18120 return; 18121 } 18122 18123 this.player_.cache_.selectedLanguage = { 18124 enabled: true, 18125 language: this.track.language, 18126 kind: this.track.kind 18127 }; 18128 } 18129 }; 18130 18131 _proto.dispose = function dispose() { 18132 // remove reference to track object on dispose 18133 this.track = null; 18134 18135 _MenuItem.prototype.dispose.call(this); 18136 }; 18137 18138 return TextTrackMenuItem; 18139 }(MenuItem); 18140 18141 Component.registerComponent('TextTrackMenuItem', TextTrackMenuItem); 18142 18143 /** 18144 * A special menu item for turning of a specific type of text track 18145 * 18146 * @extends TextTrackMenuItem 18147 */ 18148 18149 var OffTextTrackMenuItem = 18150 /*#__PURE__*/ 18151 function (_TextTrackMenuItem) { 18152 inheritsLoose(OffTextTrackMenuItem, _TextTrackMenuItem); 18153 18154 /** 18155 * Creates an instance of this class. 18156 * 18157 * @param {Player} player 18158 * The `Player` that this class should be attached to. 18159 * 18160 * @param {Object} [options] 18161 * The key/value store of player options. 18162 */ 18163 function OffTextTrackMenuItem(player, options) { 18164 // Create pseudo track info 18165 // Requires options['kind'] 18166 options.track = { 18167 player: player, 18168 // it is no longer necessary to store `kind` or `kinds` on the track itself 18169 // since they are now stored in the `kinds` property of all instances of 18170 // TextTrackMenuItem, but this will remain for backwards compatibility 18171 kind: options.kind, 18172 kinds: options.kinds, 18173 "default": false, 18174 mode: 'disabled' 18175 }; 18176 18177 if (!options.kinds) { 18178 options.kinds = [options.kind]; 18179 } 18180 18181 if (options.label) { 18182 options.track.label = options.label; 18183 } else { 18184 options.track.label = options.kinds.join(' and ') + ' off'; 18185 } // MenuItem is selectable 18186 18187 18188 options.selectable = true; // MenuItem is NOT multiSelectable (i.e. only one can be marked "selected" at a time) 18189 18190 options.multiSelectable = false; 18191 return _TextTrackMenuItem.call(this, player, options) || this; 18192 } 18193 /** 18194 * Handle text track change 18195 * 18196 * @param {EventTarget~Event} event 18197 * The event that caused this function to run 18198 */ 18199 18200 18201 var _proto = OffTextTrackMenuItem.prototype; 18202 18203 _proto.handleTracksChange = function handleTracksChange(event) { 18204 var tracks = this.player().textTracks(); 18205 var shouldBeSelected = true; 18206 18207 for (var i = 0, l = tracks.length; i < l; i++) { 18208 var track = tracks[i]; 18209 18210 if (this.options_.kinds.indexOf(track.kind) > -1 && track.mode === 'showing') { 18211 shouldBeSelected = false; 18212 break; 18213 } 18214 } // Prevent redundant selected() calls because they may cause 18215 // screen readers to read the appended control text unnecessarily 18216 18217 18218 if (shouldBeSelected !== this.isSelected_) { 18219 this.selected(shouldBeSelected); 18220 } 18221 }; 18222 18223 _proto.handleSelectedLanguageChange = function handleSelectedLanguageChange(event) { 18224 var tracks = this.player().textTracks(); 18225 var allHidden = true; 18226 18227 for (var i = 0, l = tracks.length; i < l; i++) { 18228 var track = tracks[i]; 18229 18230 if (['captions', 'descriptions', 'subtitles'].indexOf(track.kind) > -1 && track.mode === 'showing') { 18231 allHidden = false; 18232 break; 18233 } 18234 } 18235 18236 if (allHidden) { 18237 this.player_.cache_.selectedLanguage = { 18238 enabled: false 18239 }; 18240 } 18241 }; 18242 18243 return OffTextTrackMenuItem; 18244 }(TextTrackMenuItem); 18245 18246 Component.registerComponent('OffTextTrackMenuItem', OffTextTrackMenuItem); 18247 18248 /** 18249 * The base class for buttons that toggle specific text track types (e.g. subtitles) 18250 * 18251 * @extends MenuButton 18252 */ 18253 18254 var TextTrackButton = 18255 /*#__PURE__*/ 18256 function (_TrackButton) { 18257 inheritsLoose(TextTrackButton, _TrackButton); 18258 18259 /** 18260 * Creates an instance of this class. 18261 * 18262 * @param {Player} player 18263 * The `Player` that this class should be attached to. 18264 * 18265 * @param {Object} [options={}] 18266 * The key/value store of player options. 18267 */ 18268 function TextTrackButton(player, options) { 18269 if (options === void 0) { 18270 options = {}; 18271 } 18272 18273 options.tracks = player.textTracks(); 18274 return _TrackButton.call(this, player, options) || this; 18275 } 18276 /** 18277 * Create a menu item for each text track 18278 * 18279 * @param {TextTrackMenuItem[]} [items=[]] 18280 * Existing array of items to use during creation 18281 * 18282 * @return {TextTrackMenuItem[]} 18283 * Array of menu items that were created 18284 */ 18285 18286 18287 var _proto = TextTrackButton.prototype; 18288 18289 _proto.createItems = function createItems(items, TrackMenuItem) { 18290 if (items === void 0) { 18291 items = []; 18292 } 18293 18294 if (TrackMenuItem === void 0) { 18295 TrackMenuItem = TextTrackMenuItem; 18296 } 18297 18298 // Label is an override for the [track] off label 18299 // USed to localise captions/subtitles 18300 var label; 18301 18302 if (this.label_) { 18303 label = this.label_ + " off"; 18304 } // Add an OFF menu item to turn all tracks off 18305 18306 18307 items.push(new OffTextTrackMenuItem(this.player_, { 18308 kinds: this.kinds_, 18309 kind: this.kind_, 18310 label: label 18311 })); 18312 this.hideThreshold_ += 1; 18313 var tracks = this.player_.textTracks(); 18314 18315 if (!Array.isArray(this.kinds_)) { 18316 this.kinds_ = [this.kind_]; 18317 } 18318 18319 for (var i = 0; i < tracks.length; i++) { 18320 var track = tracks[i]; // only add tracks that are of an appropriate kind and have a label 18321 18322 if (this.kinds_.indexOf(track.kind) > -1) { 18323 var item = new TrackMenuItem(this.player_, { 18324 track: track, 18325 kinds: this.kinds_, 18326 kind: this.kind_, 18327 // MenuItem is selectable 18328 selectable: true, 18329 // MenuItem is NOT multiSelectable (i.e. only one can be marked "selected" at a time) 18330 multiSelectable: false 18331 }); 18332 item.addClass("vjs-" + track.kind + "-menu-item"); 18333 items.push(item); 18334 } 18335 } 18336 18337 return items; 18338 }; 18339 18340 return TextTrackButton; 18341 }(TrackButton); 18342 18343 Component.registerComponent('TextTrackButton', TextTrackButton); 18344 18345 /** 18346 * The chapter track menu item 18347 * 18348 * @extends MenuItem 18349 */ 18350 18351 var ChaptersTrackMenuItem = 18352 /*#__PURE__*/ 18353 function (_MenuItem) { 18354 inheritsLoose(ChaptersTrackMenuItem, _MenuItem); 18355 18356 /** 18357 * Creates an instance of this class. 18358 * 18359 * @param {Player} player 18360 * The `Player` that this class should be attached to. 18361 * 18362 * @param {Object} [options] 18363 * The key/value store of player options. 18364 */ 18365 function ChaptersTrackMenuItem(player, options) { 18366 var _this; 18367 18368 var track = options.track; 18369 var cue = options.cue; 18370 var currentTime = player.currentTime(); // Modify options for parent MenuItem class's init. 18371 18372 options.selectable = true; 18373 options.multiSelectable = false; 18374 options.label = cue.text; 18375 options.selected = cue.startTime <= currentTime && currentTime < cue.endTime; 18376 _this = _MenuItem.call(this, player, options) || this; 18377 _this.track = track; 18378 _this.cue = cue; 18379 track.addEventListener('cuechange', bind(assertThisInitialized(_this), _this.update)); 18380 return _this; 18381 } 18382 /** 18383 * This gets called when an `ChaptersTrackMenuItem` is "clicked". See 18384 * {@link ClickableComponent} for more detailed information on what a click can be. 18385 * 18386 * @param {EventTarget~Event} [event] 18387 * The `keydown`, `tap`, or `click` event that caused this function to be 18388 * called. 18389 * 18390 * @listens tap 18391 * @listens click 18392 */ 18393 18394 18395 var _proto = ChaptersTrackMenuItem.prototype; 18396 18397 _proto.handleClick = function handleClick(event) { 18398 _MenuItem.prototype.handleClick.call(this); 18399 18400 this.player_.currentTime(this.cue.startTime); 18401 this.update(this.cue.startTime); 18402 } 18403 /** 18404 * Update chapter menu item 18405 * 18406 * @param {EventTarget~Event} [event] 18407 * The `cuechange` event that caused this function to run. 18408 * 18409 * @listens TextTrack#cuechange 18410 */ 18411 ; 18412 18413 _proto.update = function update(event) { 18414 var cue = this.cue; 18415 var currentTime = this.player_.currentTime(); // vjs.log(currentTime, cue.startTime); 18416 18417 this.selected(cue.startTime <= currentTime && currentTime < cue.endTime); 18418 }; 18419 18420 return ChaptersTrackMenuItem; 18421 }(MenuItem); 18422 18423 Component.registerComponent('ChaptersTrackMenuItem', ChaptersTrackMenuItem); 18424 18425 /** 18426 * The button component for toggling and selecting chapters 18427 * Chapters act much differently than other text tracks 18428 * Cues are navigation vs. other tracks of alternative languages 18429 * 18430 * @extends TextTrackButton 18431 */ 18432 18433 var ChaptersButton = 18434 /*#__PURE__*/ 18435 function (_TextTrackButton) { 18436 inheritsLoose(ChaptersButton, _TextTrackButton); 18437 18438 /** 18439 * Creates an instance of this class. 18440 * 18441 * @param {Player} player 18442 * The `Player` that this class should be attached to. 18443 * 18444 * @param {Object} [options] 18445 * The key/value store of player options. 18446 * 18447 * @param {Component~ReadyCallback} [ready] 18448 * The function to call when this function is ready. 18449 */ 18450 function ChaptersButton(player, options, ready) { 18451 return _TextTrackButton.call(this, player, options, ready) || this; 18452 } 18453 /** 18454 * Builds the default DOM `className`. 18455 * 18456 * @return {string} 18457 * The DOM `className` for this object. 18458 */ 18459 18460 18461 var _proto = ChaptersButton.prototype; 18462 18463 _proto.buildCSSClass = function buildCSSClass() { 18464 return "vjs-chapters-button " + _TextTrackButton.prototype.buildCSSClass.call(this); 18465 }; 18466 18467 _proto.buildWrapperCSSClass = function buildWrapperCSSClass() { 18468 return "vjs-chapters-button " + _TextTrackButton.prototype.buildWrapperCSSClass.call(this); 18469 } 18470 /** 18471 * Update the menu based on the current state of its items. 18472 * 18473 * @param {EventTarget~Event} [event] 18474 * An event that triggered this function to run. 18475 * 18476 * @listens TextTrackList#addtrack 18477 * @listens TextTrackList#removetrack 18478 * @listens TextTrackList#change 18479 */ 18480 ; 18481 18482 _proto.update = function update(event) { 18483 if (!this.track_ || event && (event.type === 'addtrack' || event.type === 'removetrack')) {
vendor: 1,553 bytes, lines 18484-18537
18484 this.setTrack(this.findChaptersTrack()); 18485 } 18486 18487 _TextTrackButton.prototype.update.call(this); 18488 } 18489 /** 18490 * Set the currently selected track for the chapters button. 18491 * 18492 * @param {TextTrack} track 18493 * The new track to select. Nothing will change if this is the currently selected 18494 * track. 18495 */ 18496 ; 18497 18498 _proto.setTrack = function setTrack(track) { 18499 if (this.track_ === track) { 18500 return; 18501 } 18502 18503 if (!this.updateHandler_) { 18504 this.updateHandler_ = this.update.bind(this); 18505 } // here this.track_ refers to the old track instance 18506 18507 18508 if (this.track_) { 18509 var remoteTextTrackEl = this.player_.remoteTextTrackEls().getTrackElementByTrack_(this.track_); 18510 18511 if (remoteTextTrackEl) { 18512 remoteTextTrackEl.removeEventListener('load', this.updateHandler_); 18513 } 18514 18515 this.track_ = null; 18516 } 18517 18518 this.track_ = track; // here this.track_ refers to the new track instance 18519 18520 if (this.track_) { 18521 this.track_.mode = 'hidden'; 18522 18523 var _remoteTextTrackEl = this.player_.remoteTextTrackEls().getTrackElementByTrack_(this.track_); 18524 18525 if (_remoteTextTrackEl) { 18526 _remoteTextTrackEl.addEventListener('load', this.updateHandler_); 18527 } 18528 } 18529 } 18530 /** 18531 * Find the track object that is currently in use by this ChaptersButton 18532 * 18533 * @return {TextTrack|undefined} 18534 * The current track or undefined if none was found. 18535 */ 18536 ; 18537
vendor: 16,174 bytes, lines 18538-19115
18538 _proto.findChaptersTrack = function findChaptersTrack() { 18539 var tracks = this.player_.textTracks() || []; 18540 18541 for (var i = tracks.length - 1; i >= 0; i--) { 18542 // We will always choose the last track as our chaptersTrack 18543 var track = tracks[i]; 18544 18545 if (track.kind === this.kind_) { 18546 return track; 18547 } 18548 } 18549 } 18550 /** 18551 * Get the caption for the ChaptersButton based on the track label. This will also 18552 * use the current tracks localized kind as a fallback if a label does not exist. 18553 * 18554 * @return {string} 18555 * The tracks current label or the localized track kind. 18556 */ 18557 ; 18558 18559 _proto.getMenuCaption = function getMenuCaption() { 18560 if (this.track_ && this.track_.label) { 18561 return this.track_.label; 18562 } 18563 18564 return this.localize(toTitleCase(this.kind_)); 18565 } 18566 /** 18567 * Create menu from chapter track 18568 * 18569 * @return {Menu} 18570 * New menu for the chapter buttons 18571 */ 18572 ; 18573 18574 _proto.createMenu = function createMenu() { 18575 this.options_.title = this.getMenuCaption(); 18576 return _TextTrackButton.prototype.createMenu.call(this); 18577 } 18578 /** 18579 * Create a menu item for each text track 18580 * 18581 * @return {TextTrackMenuItem[]} 18582 * Array of menu items 18583 */ 18584 ; 18585 18586 _proto.createItems = function createItems() { 18587 var items = []; 18588 18589 if (!this.track_) { 18590 return items; 18591 } 18592 18593 var cues = this.track_.cues; 18594 18595 if (!cues) { 18596 return items; 18597 } 18598 18599 for (var i = 0, l = cues.length; i < l; i++) { 18600 var cue = cues[i]; 18601 var mi = new ChaptersTrackMenuItem(this.player_, { 18602 track: this.track_, 18603 cue: cue 18604 }); 18605 items.push(mi); 18606 } 18607 18608 return items; 18609 }; 18610 18611 return ChaptersButton; 18612 }(TextTrackButton); 18613 /** 18614 * `kind` of TextTrack to look for to associate it with this menu. 18615 * 18616 * @type {string} 18617 * @private 18618 */ 18619 18620 18621 ChaptersButton.prototype.kind_ = 'chapters'; 18622 /** 18623 * The text that should display over the `ChaptersButton`s controls. Added for localization. 18624 * 18625 * @type {string} 18626 * @private 18627 */ 18628 18629 ChaptersButton.prototype.controlText_ = 'Chapters'; 18630 Component.registerComponent('ChaptersButton', ChaptersButton); 18631 18632 /** 18633 * The button component for toggling and selecting descriptions 18634 * 18635 * @extends TextTrackButton 18636 */ 18637 18638 var DescriptionsButton = 18639 /*#__PURE__*/ 18640 function (_TextTrackButton) { 18641 inheritsLoose(DescriptionsButton, _TextTrackButton); 18642 18643 /** 18644 * Creates an instance of this class. 18645 * 18646 * @param {Player} player 18647 * The `Player` that this class should be attached to. 18648 * 18649 * @param {Object} [options] 18650 * The key/value store of player options. 18651 * 18652 * @param {Component~ReadyCallback} [ready] 18653 * The function to call when this component is ready. 18654 */ 18655 function DescriptionsButton(player, options, ready) { 18656 var _this; 18657 18658 _this = _TextTrackButton.call(this, player, options, ready) || this; 18659 var tracks = player.textTracks(); 18660 var changeHandler = bind(assertThisInitialized(_this), _this.handleTracksChange); 18661 tracks.addEventListener('change', changeHandler); 18662 18663 _this.on('dispose', function () { 18664 tracks.removeEventListener('change', changeHandler); 18665 }); 18666 18667 return _this; 18668 } 18669 /** 18670 * Handle text track change 18671 * 18672 * @param {EventTarget~Event} event 18673 * The event that caused this function to run 18674 * 18675 * @listens TextTrackList#change 18676 */ 18677 18678 18679 var _proto = DescriptionsButton.prototype; 18680 18681 _proto.handleTracksChange = function handleTracksChange(event) { 18682 var tracks = this.player().textTracks(); 18683 var disabled = false; // Check whether a track of a different kind is showing 18684 18685 for (var i = 0, l = tracks.length; i < l; i++) { 18686 var track = tracks[i]; 18687 18688 if (track.kind !== this.kind_ && track.mode === 'showing') { 18689 disabled = true; 18690 break; 18691 } 18692 } // If another track is showing, disable this menu button 18693 18694 18695 if (disabled) { 18696 this.disable(); 18697 } else { 18698 this.enable(); 18699 } 18700 } 18701 /** 18702 * Builds the default DOM `className`. 18703 * 18704 * @return {string} 18705 * The DOM `className` for this object. 18706 */ 18707 ; 18708 18709 _proto.buildCSSClass = function buildCSSClass() { 18710 return "vjs-descriptions-button " + _TextTrackButton.prototype.buildCSSClass.call(this); 18711 }; 18712 18713 _proto.buildWrapperCSSClass = function buildWrapperCSSClass() { 18714 return "vjs-descriptions-button " + _TextTrackButton.prototype.buildWrapperCSSClass.call(this); 18715 }; 18716 18717 return DescriptionsButton; 18718 }(TextTrackButton); 18719 /** 18720 * `kind` of TextTrack to look for to associate it with this menu. 18721 * 18722 * @type {string} 18723 * @private 18724 */ 18725 18726 18727 DescriptionsButton.prototype.kind_ = 'descriptions'; 18728 /** 18729 * The text that should display over the `DescriptionsButton`s controls. Added for localization. 18730 * 18731 * @type {string} 18732 * @private 18733 */ 18734 18735 DescriptionsButton.prototype.controlText_ = 'Descriptions'; 18736 Component.registerComponent('DescriptionsButton', DescriptionsButton); 18737 18738 /** 18739 * The button component for toggling and selecting subtitles 18740 * 18741 * @extends TextTrackButton 18742 */ 18743 18744 var SubtitlesButton = 18745 /*#__PURE__*/ 18746 function (_TextTrackButton) { 18747 inheritsLoose(SubtitlesButton, _TextTrackButton); 18748 18749 /** 18750 * Creates an instance of this class. 18751 * 18752 * @param {Player} player 18753 * The `Player` that this class should be attached to. 18754 * 18755 * @param {Object} [options] 18756 * The key/value store of player options. 18757 * 18758 * @param {Component~ReadyCallback} [ready] 18759 * The function to call when this component is ready. 18760 */ 18761 function SubtitlesButton(player, options, ready) { 18762 return _TextTrackButton.call(this, player, options, ready) || this; 18763 } 18764 /** 18765 * Builds the default DOM `className`. 18766 * 18767 * @return {string} 18768 * The DOM `className` for this object. 18769 */ 18770 18771 18772 var _proto = SubtitlesButton.prototype; 18773 18774 _proto.buildCSSClass = function buildCSSClass() { 18775 return "vjs-subtitles-button " + _TextTrackButton.prototype.buildCSSClass.call(this); 18776 }; 18777 18778 _proto.buildWrapperCSSClass = function buildWrapperCSSClass() { 18779 return "vjs-subtitles-button " + _TextTrackButton.prototype.buildWrapperCSSClass.call(this); 18780 }; 18781 18782 return SubtitlesButton; 18783 }(TextTrackButton); 18784 /** 18785 * `kind` of TextTrack to look for to associate it with this menu. 18786 * 18787 * @type {string} 18788 * @private 18789 */ 18790 18791 18792 SubtitlesButton.prototype.kind_ = 'subtitles'; 18793 /** 18794 * The text that should display over the `SubtitlesButton`s controls. Added for localization. 18795 * 18796 * @type {string} 18797 * @private 18798 */ 18799 18800 SubtitlesButton.prototype.controlText_ = 'Subtitles'; 18801 Component.registerComponent('SubtitlesButton', SubtitlesButton); 18802 18803 /** 18804 * The menu item for caption track settings menu 18805 * 18806 * @extends TextTrackMenuItem 18807 */ 18808 18809 var CaptionSettingsMenuItem = 18810 /*#__PURE__*/ 18811 function (_TextTrackMenuItem) { 18812 inheritsLoose(CaptionSettingsMenuItem, _TextTrackMenuItem); 18813 18814 /** 18815 * Creates an instance of this class. 18816 * 18817 * @param {Player} player 18818 * The `Player` that this class should be attached to. 18819 * 18820 * @param {Object} [options] 18821 * The key/value store of player options. 18822 */ 18823 function CaptionSettingsMenuItem(player, options) { 18824 var _this; 18825 18826 options.track = { 18827 player: player, 18828 kind: options.kind, 18829 label: options.kind + ' settings', 18830 selectable: false, 18831 "default": false, 18832 mode: 'disabled' 18833 }; // CaptionSettingsMenuItem has no concept of 'selected' 18834 18835 options.selectable = false; 18836 options.name = 'CaptionSettingsMenuItem'; 18837 _this = _TextTrackMenuItem.call(this, player, options) || this; 18838 18839 _this.addClass('vjs-texttrack-settings'); 18840 18841 _this.controlText(', opens ' + options.kind + ' settings dialog'); 18842 18843 return _this; 18844 } 18845 /** 18846 * This gets called when an `CaptionSettingsMenuItem` is "clicked". See 18847 * {@link ClickableComponent} for more detailed information on what a click can be. 18848 * 18849 * @param {EventTarget~Event} [event] 18850 * The `keydown`, `tap`, or `click` event that caused this function to be 18851 * called. 18852 * 18853 * @listens tap 18854 * @listens click 18855 */ 18856 18857 18858 var _proto = CaptionSettingsMenuItem.prototype; 18859 18860 _proto.handleClick = function handleClick(event) { 18861 this.player().getChild('textTrackSettings').open(); 18862 }; 18863 18864 return CaptionSettingsMenuItem; 18865 }(TextTrackMenuItem); 18866 18867 Component.registerComponent('CaptionSettingsMenuItem', CaptionSettingsMenuItem); 18868 18869 /** 18870 * The button component for toggling and selecting captions 18871 * 18872 * @extends TextTrackButton 18873 */ 18874 18875 var CaptionsButton = 18876 /*#__PURE__*/ 18877 function (_TextTrackButton) { 18878 inheritsLoose(CaptionsButton, _TextTrackButton); 18879 18880 /** 18881 * Creates an instance of this class. 18882 * 18883 * @param {Player} player 18884 * The `Player` that this class should be attached to. 18885 * 18886 * @param {Object} [options] 18887 * The key/value store of player options. 18888 * 18889 * @param {Component~ReadyCallback} [ready] 18890 * The function to call when this component is ready. 18891 */ 18892 function CaptionsButton(player, options, ready) { 18893 return _TextTrackButton.call(this, player, options, ready) || this; 18894 } 18895 /** 18896 * Builds the default DOM `className`. 18897 * 18898 * @return {string} 18899 * The DOM `className` for this object. 18900 */ 18901 18902 18903 var _proto = CaptionsButton.prototype; 18904 18905 _proto.buildCSSClass = function buildCSSClass() { 18906 return "vjs-captions-button " + _TextTrackButton.prototype.buildCSSClass.call(this); 18907 }; 18908 18909 _proto.buildWrapperCSSClass = function buildWrapperCSSClass() { 18910 return "vjs-captions-button " + _TextTrackButton.prototype.buildWrapperCSSClass.call(this); 18911 } 18912 /** 18913 * Create caption menu items 18914 * 18915 * @return {CaptionSettingsMenuItem[]} 18916 * The array of current menu items. 18917 */ 18918 ; 18919 18920 _proto.createItems = function createItems() { 18921 var items = []; 18922 18923 if (!(this.player().tech_ && this.player().tech_.featuresNativeTextTracks) && this.player().getChild('textTrackSettings')) { 18924 items.push(new CaptionSettingsMenuItem(this.player_, { 18925 kind: this.kind_ 18926 })); 18927 this.hideThreshold_ += 1; 18928 } 18929 18930 return _TextTrackButton.prototype.createItems.call(this, items); 18931 }; 18932 18933 return CaptionsButton; 18934 }(TextTrackButton); 18935 /** 18936 * `kind` of TextTrack to look for to associate it with this menu. 18937 * 18938 * @type {string} 18939 * @private 18940 */ 18941 18942 18943 CaptionsButton.prototype.kind_ = 'captions'; 18944 /** 18945 * The text that should display over the `CaptionsButton`s controls. Added for localization. 18946 * 18947 * @type {string} 18948 * @private 18949 */ 18950 18951 CaptionsButton.prototype.controlText_ = 'Captions'; 18952 Component.registerComponent('CaptionsButton', CaptionsButton); 18953 18954 /** 18955 * SubsCapsMenuItem has an [cc] icon to distinguish captions from subtitles 18956 * in the SubsCapsMenu. 18957 * 18958 * @extends TextTrackMenuItem 18959 */ 18960 18961 var SubsCapsMenuItem = 18962 /*#__PURE__*/ 18963 function (_TextTrackMenuItem) { 18964 inheritsLoose(SubsCapsMenuItem, _TextTrackMenuItem); 18965 18966 function SubsCapsMenuItem() { 18967 return _TextTrackMenuItem.apply(this, arguments) || this; 18968 } 18969 18970 var _proto = SubsCapsMenuItem.prototype; 18971 18972 _proto.createEl = function createEl(type, props, attrs) { 18973 var innerHTML = "<span class=\"vjs-menu-item-text\">" + this.localize(this.options_.label); 18974 18975 if (this.options_.track.kind === 'captions') { 18976 innerHTML += "\n <span aria-hidden=\"true\" class=\"vjs-icon-placeholder\"></span>\n <span class=\"vjs-control-text\"> " + this.localize('Captions') + "</span>\n "; 18977 } 18978 18979 innerHTML += '</span>'; 18980 18981 var el = _TextTrackMenuItem.prototype.createEl.call(this, type, assign({ 18982 innerHTML: innerHTML 18983 }, props), attrs); 18984 18985 return el; 18986 }; 18987 18988 return SubsCapsMenuItem; 18989 }(TextTrackMenuItem); 18990 18991 Component.registerComponent('SubsCapsMenuItem', SubsCapsMenuItem); 18992 18993 /** 18994 * The button component for toggling and selecting captions and/or subtitles 18995 * 18996 * @extends TextTrackButton 18997 */ 18998 18999 var SubsCapsButton = 19000 /*#__PURE__*/ 19001 function (_TextTrackButton) { 19002 inheritsLoose(SubsCapsButton, _TextTrackButton); 19003 19004 function SubsCapsButton(player, options) { 19005 var _this; 19006 19007 if (options === void 0) { 19008 options = {}; 19009 } 19010 19011 _this = _TextTrackButton.call(this, player, options) || this; // Although North America uses "captions" in most cases for 19012 // "captions and subtitles" other locales use "subtitles" 19013 19014 _this.label_ = 'subtitles'; 19015 19016 if (['en', 'en-us', 'en-ca', 'fr-ca'].indexOf(_this.player_.language_) > -1) { 19017 _this.label_ = 'captions'; 19018 } 19019 19020 _this.menuButton_.controlText(toTitleCase(_this.label_)); 19021 19022 return _this; 19023 } 19024 /** 19025 * Builds the default DOM `className`. 19026 * 19027 * @return {string} 19028 * The DOM `className` for this object. 19029 */ 19030 19031 19032 var _proto = SubsCapsButton.prototype; 19033 19034 _proto.buildCSSClass = function buildCSSClass() { 19035 return "vjs-subs-caps-button " + _TextTrackButton.prototype.buildCSSClass.call(this); 19036 }; 19037 19038 _proto.buildWrapperCSSClass = function buildWrapperCSSClass() { 19039 return "vjs-subs-caps-button " + _TextTrackButton.prototype.buildWrapperCSSClass.call(this); 19040 } 19041 /** 19042 * Create caption/subtitles menu items 19043 * 19044 * @return {CaptionSettingsMenuItem[]} 19045 * The array of current menu items. 19046 */ 19047 ; 19048 19049 _proto.createItems = function createItems() { 19050 var items = []; 19051 19052 if (!(this.player().tech_ && this.player().tech_.featuresNativeTextTracks) && this.player().getChild('textTrackSettings')) { 19053 items.push(new CaptionSettingsMenuItem(this.player_, { 19054 kind: this.label_ 19055 })); 19056 this.hideThreshold_ += 1; 19057 } 19058 19059 items = _TextTrackButton.prototype.createItems.call(this, items, SubsCapsMenuItem); 19060 return items; 19061 }; 19062 19063 return SubsCapsButton; 19064 }(TextTrackButton); 19065 /** 19066 * `kind`s of TextTrack to look for to associate it with this menu. 19067 * 19068 * @type {array} 19069 * @private 19070 */ 19071 19072 19073 SubsCapsButton.prototype.kinds_ = ['captions', 'subtitles']; 19074 /** 19075 * The text that should display over the `SubsCapsButton`s controls. 19076 * 19077 * 19078 * @type {string} 19079 * @private 19080 */ 19081 19082 SubsCapsButton.prototype.controlText_ = 'Subtitles'; 19083 Component.registerComponent('SubsCapsButton', SubsCapsButton); 19084 19085 /** 19086 * An {@link AudioTrack} {@link MenuItem} 19087 * 19088 * @extends MenuItem 19089 */ 19090 19091 var AudioTrackMenuItem = 19092 /*#__PURE__*/ 19093 function (_MenuItem) { 19094 inheritsLoose(AudioTrackMenuItem, _MenuItem); 19095 19096 /** 19097 * Creates an instance of this class. 19098 * 19099 * @param {Player} player 19100 * The `Player` that this class should be attached to. 19101 * 19102 * @param {Object} [options] 19103 * The key/value store of player options. 19104 */ 19105 function AudioTrackMenuItem(player, options) { 19106 var _this; 19107 19108 var track = options.track; 19109 var tracks = player.audioTracks(); // Modify options for parent MenuItem class's init. 19110 19111 options.label = track.label || track.language || 'Unknown'; 19112 options.selected = track.enabled; 19113 _this = _MenuItem.call(this, player, options) || this; 19114 _this.track = track; 19115
vendor: 5,857 bytes, lines 19116-19315
19116 _this.addClass("vjs-" + track.kind + "-menu-item"); 19117 19118 var changeHandler = function changeHandler() { 19119 for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { 19120 args[_key] = arguments[_key]; 19121 } 19122 19123 _this.handleTracksChange.apply(assertThisInitialized(_this), args); 19124 }; 19125 19126 tracks.addEventListener('change', changeHandler); 19127 19128 _this.on('dispose', function () { 19129 tracks.removeEventListener('change', changeHandler); 19130 }); 19131 19132 return _this; 19133 } 19134 19135 var _proto = AudioTrackMenuItem.prototype; 19136 19137 _proto.createEl = function createEl(type, props, attrs) { 19138 var innerHTML = "<span class=\"vjs-menu-item-text\">" + this.localize(this.options_.label); 19139 19140 if (this.options_.track.kind === 'main-desc') { 19141 innerHTML += "\n <span aria-hidden=\"true\" class=\"vjs-icon-placeholder\"></span>\n <span class=\"vjs-control-text\"> " + this.localize('Descriptions') + "</span>\n "; 19142 } 19143 19144 innerHTML += '</span>'; 19145 19146 var el = _MenuItem.prototype.createEl.call(this, type, assign({ 19147 innerHTML: innerHTML 19148 }, props), attrs); 19149 19150 return el; 19151 } 19152 /** 19153 * This gets called when an `AudioTrackMenuItem is "clicked". See {@link ClickableComponent} 19154 * for more detailed information on what a click can be. 19155 * 19156 * @param {EventTarget~Event} [event] 19157 * The `keydown`, `tap`, or `click` event that caused this function to be 19158 * called. 19159 * 19160 * @listens tap 19161 * @listens click 19162 */ 19163 ; 19164 19165 _proto.handleClick = function handleClick(event) { 19166 var tracks = this.player_.audioTracks(); 19167 19168 _MenuItem.prototype.handleClick.call(this, event); 19169 19170 for (var i = 0; i < tracks.length; i++) { 19171 var track = tracks[i]; 19172 track.enabled = track === this.track; 19173 } 19174 } 19175 /** 19176 * Handle any {@link AudioTrack} change. 19177 * 19178 * @param {EventTarget~Event} [event] 19179 * The {@link AudioTrackList#change} event that caused this to run. 19180 * 19181 * @listens AudioTrackList#change 19182 */ 19183 ; 19184 19185 _proto.handleTracksChange = function handleTracksChange(event) { 19186 this.selected(this.track.enabled); 19187 }; 19188 19189 return AudioTrackMenuItem; 19190 }(MenuItem); 19191 19192 Component.registerComponent('AudioTrackMenuItem', AudioTrackMenuItem); 19193 19194 /** 19195 * The base class for buttons that toggle specific {@link AudioTrack} types. 19196 * 19197 * @extends TrackButton 19198 */ 19199 19200 var AudioTrackButton = 19201 /*#__PURE__*/ 19202 function (_TrackButton) { 19203 inheritsLoose(AudioTrackButton, _TrackButton); 19204 19205 /** 19206 * Creates an instance of this class. 19207 * 19208 * @param {Player} player 19209 * The `Player` that this class should be attached to. 19210 * 19211 * @param {Object} [options={}] 19212 * The key/value store of player options. 19213 */ 19214 function AudioTrackButton(player, options) { 19215 if (options === void 0) { 19216 options = {}; 19217 } 19218 19219 options.tracks = player.audioTracks(); 19220 return _TrackButton.call(this, player, options) || this; 19221 } 19222 /** 19223 * Builds the default DOM `className`. 19224 * 19225 * @return {string} 19226 * The DOM `className` for this object. 19227 */ 19228 19229 19230 var _proto = AudioTrackButton.prototype; 19231 19232 _proto.buildCSSClass = function buildCSSClass() { 19233 return "vjs-audio-button " + _TrackButton.prototype.buildCSSClass.call(this); 19234 }; 19235 19236 _proto.buildWrapperCSSClass = function buildWrapperCSSClass() { 19237 return "vjs-audio-button " + _TrackButton.prototype.buildWrapperCSSClass.call(this); 19238 } 19239 /** 19240 * Create a menu item for each audio track 19241 * 19242 * @param {AudioTrackMenuItem[]} [items=[]] 19243 * An array of existing menu items to use. 19244 * 19245 * @return {AudioTrackMenuItem[]} 19246 * An array of menu items 19247 */ 19248 ; 19249 19250 _proto.createItems = function createItems(items) { 19251 if (items === void 0) { 19252 items = []; 19253 } 19254 19255 // if there's only one audio track, there no point in showing it 19256 this.hideThreshold_ = 1; 19257 var tracks = this.player_.audioTracks(); 19258 19259 for (var i = 0; i < tracks.length; i++) { 19260 var track = tracks[i]; 19261 items.push(new AudioTrackMenuItem(this.player_, { 19262 track: track, 19263 // MenuItem is selectable 19264 selectable: true, 19265 // MenuItem is NOT multiSelectable (i.e. only one can be marked "selected" at a time) 19266 multiSelectable: false 19267 })); 19268 } 19269 19270 return items; 19271 }; 19272 19273 return AudioTrackButton; 19274 }(TrackButton); 19275 /** 19276 * The text that should display over the `AudioTrackButton`s controls. Added for localization. 19277 * 19278 * @type {string} 19279 * @private 19280 */ 19281 19282 19283 AudioTrackButton.prototype.controlText_ = 'Audio Track'; 19284 Component.registerComponent('AudioTrackButton', AudioTrackButton); 19285 19286 /** 19287 * The specific menu item type for selecting a playback rate. 19288 * 19289 * @extends MenuItem 19290 */ 19291 19292 var PlaybackRateMenuItem = 19293 /*#__PURE__*/ 19294 function (_MenuItem) { 19295 inheritsLoose(PlaybackRateMenuItem, _MenuItem); 19296 19297 /** 19298 * Creates an instance of this class. 19299 * 19300 * @param {Player} player 19301 * The `Player` that this class should be attached to. 19302 * 19303 * @param {Object} [options] 19304 * The key/value store of player options. 19305 */ 19306 function PlaybackRateMenuItem(player, options) { 19307 var _this; 19308 19309 var label = options.rate; 19310 var rate = parseFloat(label, 10); // Modify options for parent MenuItem class's init. 19311 19312 options.label = label; 19313 options.selected = rate === 1; 19314 options.selectable = true; 19315 options.multiSelectable = false;
vendor: 4,544 bytes, lines 19316-19482
19316 _this = _MenuItem.call(this, player, options) || this; 19317 _this.label = label; 19318 _this.rate = rate; 19319 19320 _this.on(player, 'ratechange', _this.update); 19321 19322 return _this; 19323 } 19324 /** 19325 * This gets called when an `PlaybackRateMenuItem` is "clicked". See 19326 * {@link ClickableComponent} for more detailed information on what a click can be. 19327 * 19328 * @param {EventTarget~Event} [event] 19329 * The `keydown`, `tap`, or `click` event that caused this function to be 19330 * called. 19331 * 19332 * @listens tap 19333 * @listens click 19334 */ 19335 19336 19337 var _proto = PlaybackRateMenuItem.prototype; 19338 19339 _proto.handleClick = function handleClick(event) { 19340 _MenuItem.prototype.handleClick.call(this); 19341 19342 this.player().playbackRate(this.rate); 19343 } 19344 /** 19345 * Update the PlaybackRateMenuItem when the playbackrate changes. 19346 * 19347 * @param {EventTarget~Event} [event] 19348 * The `ratechange` event that caused this function to run. 19349 * 19350 * @listens Player#ratechange 19351 */ 19352 ; 19353 19354 _proto.update = function update(event) { 19355 this.selected(this.player().playbackRate() === this.rate); 19356 }; 19357 19358 return PlaybackRateMenuItem; 19359 }(MenuItem); 19360 /** 19361 * The text that should display over the `PlaybackRateMenuItem`s controls. Added for localization. 19362 * 19363 * @type {string} 19364 * @private 19365 */ 19366 19367 19368 PlaybackRateMenuItem.prototype.contentElType = 'button'; 19369 Component.registerComponent('PlaybackRateMenuItem', PlaybackRateMenuItem); 19370 19371 /** 19372 * The component for controlling the playback rate. 19373 * 19374 * @extends MenuButton 19375 */ 19376 19377 var PlaybackRateMenuButton = 19378 /*#__PURE__*/ 19379 function (_MenuButton) { 19380 inheritsLoose(PlaybackRateMenuButton, _MenuButton); 19381 19382 /** 19383 * Creates an instance of this class. 19384 * 19385 * @param {Player} player 19386 * The `Player` that this class should be attached to. 19387 * 19388 * @param {Object} [options] 19389 * The key/value store of player options. 19390 */ 19391 function PlaybackRateMenuButton(player, options) { 19392 var _this; 19393 19394 _this = _MenuButton.call(this, player, options) || this; 19395 19396 _this.updateVisibility(); 19397 19398 _this.updateLabel(); 19399 19400 _this.on(player, 'loadstart', _this.updateVisibility); 19401 19402 _this.on(player, 'ratechange', _this.updateLabel); 19403 19404 return _this; 19405 } 19406 /** 19407 * Create the `Component`'s DOM element 19408 * 19409 * @return {Element} 19410 * The element that was created. 19411 */ 19412 19413 19414 var _proto = PlaybackRateMenuButton.prototype; 19415 19416 _proto.createEl = function createEl$1() { 19417 var el = _MenuButton.prototype.createEl.call(this); 19418 19419 this.labelEl_ = createEl('div', { 19420 className: 'vjs-playback-rate-value', 19421 innerHTML: '1x' 19422 }); 19423 el.appendChild(this.labelEl_); 19424 return el; 19425 }; 19426 19427 _proto.dispose = function dispose() { 19428 this.labelEl_ = null; 19429 19430 _MenuButton.prototype.dispose.call(this); 19431 } 19432 /** 19433 * Builds the default DOM `className`. 19434 * 19435 * @return {string} 19436 * The DOM `className` for this object. 19437 */ 19438 ; 19439 19440 _proto.buildCSSClass = function buildCSSClass() { 19441 return "vjs-playback-rate " + _MenuButton.prototype.buildCSSClass.call(this); 19442 }; 19443 19444 _proto.buildWrapperCSSClass = function buildWrapperCSSClass() { 19445 return "vjs-playback-rate " + _MenuButton.prototype.buildWrapperCSSClass.call(this); 19446 } 19447 /** 19448 * Create the playback rate menu 19449 * 19450 * @return {Menu} 19451 * Menu object populated with {@link PlaybackRateMenuItem}s 19452 */ 19453 ; 19454 19455 _proto.createMenu = function createMenu() { 19456 var menu = new Menu(this.player()); 19457 var rates = this.playbackRates(); 19458 19459 if (rates) { 19460 for (var i = rates.length - 1; i >= 0; i--) { 19461 menu.addChild(new PlaybackRateMenuItem(this.player(), { 19462 rate: rates[i] + 'x' 19463 })); 19464 } 19465 } 19466 19467 return menu; 19468 } 19469 /** 19470 * Updates ARIA accessibility attributes 19471 */ 19472 ; 19473 19474 _proto.updateARIAAttributes = function updateARIAAttributes() { 19475 // Current playback rate 19476 this.el().setAttribute('aria-valuenow', this.player().playbackRate()); 19477 } 19478 /** 19479 * This gets called when an `PlaybackRateMenuButton` is "clicked". See 19480 * {@link ClickableComponent} for more detailed information on what a click can be. 19481 * 19482 * @param {EventTarget~Event} [event]
vendor: 5,076 bytes, lines 19483-19669
19483 * The `keydown`, `tap`, or `click` event that caused this function to be 19484 * called. 19485 * 19486 * @listens tap 19487 * @listens click 19488 */ 19489 ; 19490 19491 _proto.handleClick = function handleClick(event) { 19492 // select next rate option 19493 var currentRate = this.player().playbackRate(); 19494 var rates = this.playbackRates(); // this will select first one if the last one currently selected 19495 19496 var newRate = rates[0]; 19497 19498 for (var i = 0; i < rates.length; i++) { 19499 if (rates[i] > currentRate) { 19500 newRate = rates[i]; 19501 break; 19502 } 19503 } 19504 19505 this.player().playbackRate(newRate); 19506 } 19507 /** 19508 * Get possible playback rates 19509 * 19510 * @return {Array} 19511 * All possible playback rates 19512 */ 19513 ; 19514 19515 _proto.playbackRates = function playbackRates() { 19516 return this.options_.playbackRates || this.options_.playerOptions && this.options_.playerOptions.playbackRates; 19517 } 19518 /** 19519 * Get whether playback rates is supported by the tech 19520 * and an array of playback rates exists 19521 * 19522 * @return {boolean} 19523 * Whether changing playback rate is supported 19524 */ 19525 ; 19526 19527 _proto.playbackRateSupported = function playbackRateSupported() { 19528 return this.player().tech_ && this.player().tech_.featuresPlaybackRate && this.playbackRates() && this.playbackRates().length > 0; 19529 } 19530 /** 19531 * Hide playback rate controls when they're no playback rate options to select 19532 * 19533 * @param {EventTarget~Event} [event] 19534 * The event that caused this function to run. 19535 * 19536 * @listens Player#loadstart 19537 */ 19538 ; 19539 19540 _proto.updateVisibility = function updateVisibility(event) { 19541 if (this.playbackRateSupported()) { 19542 this.removeClass('vjs-hidden'); 19543 } else { 19544 this.addClass('vjs-hidden'); 19545 } 19546 } 19547 /** 19548 * Update button label when rate changed 19549 * 19550 * @param {EventTarget~Event} [event] 19551 * The event that caused this function to run. 19552 * 19553 * @listens Player#ratechange 19554 */ 19555 ; 19556 19557 _proto.updateLabel = function updateLabel(event) { 19558 if (this.playbackRateSupported()) { 19559 this.labelEl_.innerHTML = this.player().playbackRate() + 'x'; 19560 } 19561 }; 19562 19563 return PlaybackRateMenuButton; 19564 }(MenuButton); 19565 /** 19566 * The text that should display over the `FullscreenToggle`s controls. Added for localization. 19567 * 19568 * @type {string} 19569 * @private 19570 */ 19571 19572 19573 PlaybackRateMenuButton.prototype.controlText_ = 'Playback Rate'; 19574 Component.registerComponent('PlaybackRateMenuButton', PlaybackRateMenuButton); 19575 19576 /** 19577 * Just an empty spacer element that can be used as an append point for plugins, etc. 19578 * Also can be used to create space between elements when necessary. 19579 * 19580 * @extends Component 19581 */ 19582 19583 var Spacer = 19584 /*#__PURE__*/ 19585 function (_Component) { 19586 inheritsLoose(Spacer, _Component); 19587 19588 function Spacer() { 19589 return _Component.apply(this, arguments) || this; 19590 } 19591 19592 var _proto = Spacer.prototype; 19593 19594 /** 19595 * Builds the default DOM `className`. 19596 * 19597 * @return {string} 19598 * The DOM `className` for this object. 19599 */ 19600 _proto.buildCSSClass = function buildCSSClass() { 19601 return "vjs-spacer " + _Component.prototype.buildCSSClass.call(this); 19602 } 19603 /** 19604 * Create the `Component`'s DOM element 19605 * 19606 * @return {Element} 19607 * The element that was created. 19608 */ 19609 ; 19610 19611 _proto.createEl = function createEl() { 19612 return _Component.prototype.createEl.call(this, 'div', { 19613 className: this.buildCSSClass() 19614 }); 19615 }; 19616 19617 return Spacer; 19618 }(Component); 19619 19620 Component.registerComponent('Spacer', Spacer); 19621 19622 /** 19623 * Spacer specifically meant to be used as an insertion point for new plugins, etc. 19624 * 19625 * @extends Spacer 19626 */ 19627 19628 var CustomControlSpacer = 19629 /*#__PURE__*/ 19630 function (_Spacer) { 19631 inheritsLoose(CustomControlSpacer, _Spacer); 19632 19633 function CustomControlSpacer() { 19634 return _Spacer.apply(this, arguments) || this; 19635 } 19636 19637 var _proto = CustomControlSpacer.prototype; 19638 19639 /** 19640 * Builds the default DOM `className`. 19641 * 19642 * @return {string} 19643 * The DOM `className` for this object. 19644 */ 19645 _proto.buildCSSClass = function buildCSSClass() { 19646 return "vjs-custom-control-spacer " + _Spacer.prototype.buildCSSClass.call(this); 19647 } 19648 /** 19649 * Create the `Component`'s DOM element 19650 * 19651 * @return {Element} 19652 * The element that was created. 19653 */ 19654 ; 19655 19656 _proto.createEl = function createEl() { 19657 var el = _Spacer.prototype.createEl.call(this, { 19658 className: this.buildCSSClass() 19659 }); // No-flex/table-cell mode requires there be some content 19660 // in the cell to fill the remaining space of the table. 19661 19662 19663 el.innerHTML = "\xA0"; 19664 return el; 19665 }; 19666 19667 return CustomControlSpacer; 19668 }(Spacer); 19669
vendor: 24,726 bytes, lines 19670-20450
19670 Component.registerComponent('CustomControlSpacer', CustomControlSpacer); 19671 19672 /** 19673 * Container of main controls. 19674 * 19675 * @extends Component 19676 */ 19677 19678 var ControlBar = 19679 /*#__PURE__*/ 19680 function (_Component) { 19681 inheritsLoose(ControlBar, _Component); 19682 19683 function ControlBar() { 19684 return _Component.apply(this, arguments) || this; 19685 } 19686 19687 var _proto = ControlBar.prototype; 19688 19689 /** 19690 * Create the `Component`'s DOM element 19691 * 19692 * @return {Element} 19693 * The element that was created. 19694 */ 19695 _proto.createEl = function createEl() { 19696 return _Component.prototype.createEl.call(this, 'div', { 19697 className: 'vjs-control-bar', 19698 dir: 'ltr' 19699 }); 19700 }; 19701 19702 return ControlBar; 19703 }(Component); 19704 /** 19705 * Default options for `ControlBar` 19706 * 19707 * @type {Object} 19708 * @private 19709 */ 19710 19711 19712 ControlBar.prototype.options_ = { 19713 children: ['playToggle', 'volumePanel', 'currentTimeDisplay', 'timeDivider', 'durationDisplay', 'progressControl', 'liveDisplay', 'seekToLive', 'remainingTimeDisplay', 'customControlSpacer', 'playbackRateMenuButton', 'chaptersButton', 'descriptionsButton', 'subsCapsButton', 'audioTrackButton', 'fullscreenToggle'] 19714 }; 19715 19716 if ('exitPictureInPicture' in document) { 19717 ControlBar.prototype.options_.children.splice(ControlBar.prototype.options_.children.length - 1, 0, 'pictureInPictureToggle'); 19718 } 19719 19720 Component.registerComponent('ControlBar', ControlBar); 19721 19722 /** 19723 * A display that indicates an error has occurred. This means that the video 19724 * is unplayable. 19725 * 19726 * @extends ModalDialog 19727 */ 19728 19729 var ErrorDisplay = 19730 /*#__PURE__*/ 19731 function (_ModalDialog) { 19732 inheritsLoose(ErrorDisplay, _ModalDialog); 19733 19734 /** 19735 * Creates an instance of this class. 19736 * 19737 * @param {Player} player 19738 * The `Player` that this class should be attached to. 19739 * 19740 * @param {Object} [options] 19741 * The key/value store of player options. 19742 */ 19743 function ErrorDisplay(player, options) { 19744 var _this; 19745 19746 _this = _ModalDialog.call(this, player, options) || this; 19747 19748 _this.on(player, 'error', _this.open); 19749 19750 return _this; 19751 } 19752 /** 19753 * Builds the default DOM `className`. 19754 * 19755 * @return {string} 19756 * The DOM `className` for this object. 19757 * 19758 * @deprecated Since version 5. 19759 */ 19760 19761 19762 var _proto = ErrorDisplay.prototype; 19763 19764 _proto.buildCSSClass = function buildCSSClass() { 19765 return "vjs-error-display " + _ModalDialog.prototype.buildCSSClass.call(this); 19766 } 19767 /** 19768 * Gets the localized error message based on the `Player`s error. 19769 * 19770 * @return {string} 19771 * The `Player`s error message localized or an empty string. 19772 */ 19773 ; 19774 19775 _proto.content = function content() { 19776 var error = this.player().error(); 19777 return error ? this.localize(error.message) : ''; 19778 }; 19779 19780 return ErrorDisplay; 19781 }(ModalDialog); 19782 /** 19783 * The default options for an `ErrorDisplay`. 19784 * 19785 * @private 19786 */ 19787 19788 19789 ErrorDisplay.prototype.options_ = _extends_1({}, ModalDialog.prototype.options_, { 19790 pauseOnOpen: false, 19791 fillAlways: true, 19792 temporary: false, 19793 uncloseable: true 19794 }); 19795 Component.registerComponent('ErrorDisplay', ErrorDisplay); 19796 19797 var LOCAL_STORAGE_KEY = 'vjs-text-track-settings'; 19798 var COLOR_BLACK = ['#000', 'Black']; 19799 var COLOR_BLUE = ['#00F', 'Blue']; 19800 var COLOR_CYAN = ['#0FF', 'Cyan']; 19801 var COLOR_GREEN = ['#0F0', 'Green']; 19802 var COLOR_MAGENTA = ['#F0F', 'Magenta']; 19803 var COLOR_RED = ['#F00', 'Red']; 19804 var COLOR_WHITE = ['#FFF', 'White']; 19805 var COLOR_YELLOW = ['#FF0', 'Yellow']; 19806 var OPACITY_OPAQUE = ['1', 'Opaque']; 19807 var OPACITY_SEMI = ['0.5', 'Semi-Transparent']; 19808 var OPACITY_TRANS = ['0', 'Transparent']; // Configuration for the various <select> elements in the DOM of this component. 19809 // 19810 // Possible keys include: 19811 // 19812 // `default`: 19813 // The default option index. Only needs to be provided if not zero. 19814 // `parser`: 19815 // A function which is used to parse the value from the selected option in 19816 // a customized way. 19817 // `selector`: 19818 // The selector used to find the associated <select> element. 19819 19820 var selectConfigs = { 19821 backgroundColor: { 19822 selector: '.vjs-bg-color > select', 19823 id: 'captions-background-color-%s', 19824 label: 'Color', 19825 options: [COLOR_BLACK, COLOR_WHITE, COLOR_RED, COLOR_GREEN, COLOR_BLUE, COLOR_YELLOW, COLOR_MAGENTA, COLOR_CYAN] 19826 }, 19827 backgroundOpacity: { 19828 selector: '.vjs-bg-opacity > select', 19829 id: 'captions-background-opacity-%s', 19830 label: 'Transparency', 19831 options: [OPACITY_OPAQUE, OPACITY_SEMI, OPACITY_TRANS] 19832 }, 19833 color: { 19834 selector: '.vjs-fg-color > select', 19835 id: 'captions-foreground-color-%s', 19836 label: 'Color', 19837 options: [COLOR_WHITE, COLOR_BLACK, COLOR_RED, COLOR_GREEN, COLOR_BLUE, COLOR_YELLOW, COLOR_MAGENTA, COLOR_CYAN] 19838 }, 19839 edgeStyle: { 19840 selector: '.vjs-edge-style > select', 19841 id: '%s', 19842 label: 'Text Edge Style', 19843 options: [['none', 'None'], ['raised', 'Raised'], ['depressed', 'Depressed'], ['uniform', 'Uniform'], ['dropshadow', 'Dropshadow']] 19844 }, 19845 fontFamily: { 19846 selector: '.vjs-font-family > select', 19847 id: 'captions-font-family-%s', 19848 label: 'Font Family', 19849 options: [['proportionalSansSerif', 'Proportional Sans-Serif'], ['monospaceSansSerif', 'Monospace Sans-Serif'], ['proportionalSerif', 'Proportional Serif'], ['monospaceSerif', 'Monospace Serif'], ['casual', 'Casual'], ['script', 'Script'], ['small-caps', 'Small Caps']] 19850 }, 19851 fontPercent: { 19852 selector: '.vjs-font-percent > select', 19853 id: 'captions-font-size-%s', 19854 label: 'Font Size', 19855 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%']], 19856 "default": 2, 19857 parser: function parser(v) { 19858 return v === '1.00' ? null : Number(v); 19859 } 19860 }, 19861 textOpacity: { 19862 selector: '.vjs-text-opacity > select', 19863 id: 'captions-foreground-opacity-%s', 19864 label: 'Transparency', 19865 options: [OPACITY_OPAQUE, OPACITY_SEMI] 19866 }, 19867 // Options for this object are defined below. 19868 windowColor: { 19869 selector: '.vjs-window-color > select', 19870 id: 'captions-window-color-%s', 19871 label: 'Color' 19872 }, 19873 // Options for this object are defined below. 19874 windowOpacity: { 19875 selector: '.vjs-window-opacity > select', 19876 id: 'captions-window-opacity-%s', 19877 label: 'Transparency', 19878 options: [OPACITY_TRANS, OPACITY_SEMI, OPACITY_OPAQUE] 19879 } 19880 }; 19881 selectConfigs.windowColor.options = selectConfigs.backgroundColor.options; 19882 /** 19883 * Get the actual value of an option. 19884 * 19885 * @param {string} value 19886 * The value to get 19887 * 19888 * @param {Function} [parser] 19889 * Optional function to adjust the value. 19890 * 19891 * @return {Mixed} 19892 * - Will be `undefined` if no value exists 19893 * - Will be `undefined` if the given value is "none". 19894 * - Will be the actual value otherwise. 19895 * 19896 * @private 19897 */ 19898 19899 function parseOptionValue(value, parser) { 19900 if (parser) { 19901 value = parser(value); 19902 } 19903 19904 if (value && value !== 'none') { 19905 return value; 19906 } 19907 } 19908 /** 19909 * Gets the value of the selected <option> element within a <select> element. 19910 * 19911 * @param {Element} el 19912 * the element to look in 19913 * 19914 * @param {Function} [parser] 19915 * Optional function to adjust the value. 19916 * 19917 * @return {Mixed} 19918 * - Will be `undefined` if no value exists 19919 * - Will be `undefined` if the given value is "none". 19920 * - Will be the actual value otherwise. 19921 * 19922 * @private 19923 */ 19924 19925 19926 function getSelectedOptionValue(el, parser) { 19927 var value = el.options[el.options.selectedIndex].value; 19928 return parseOptionValue(value, parser); 19929 } 19930 /** 19931 * Sets the selected <option> element within a <select> element based on a 19932 * given value. 19933 * 19934 * @param {Element} el 19935 * The element to look in. 19936 * 19937 * @param {string} value 19938 * the property to look on. 19939 * 19940 * @param {Function} [parser] 19941 * Optional function to adjust the value before comparing. 19942 * 19943 * @private 19944 */ 19945 19946 19947 function setSelectedOption(el, value, parser) { 19948 if (!value) { 19949 return; 19950 } 19951 19952 for (var i = 0; i < el.options.length; i++) { 19953 if (parseOptionValue(el.options[i].value, parser) === value) { 19954 el.selectedIndex = i; 19955 break; 19956 } 19957 } 19958 } 19959 /** 19960 * Manipulate Text Tracks settings. 19961 * 19962 * @extends ModalDialog 19963 */ 19964 19965 19966 var TextTrackSettings = 19967 /*#__PURE__*/ 19968 function (_ModalDialog) { 19969 inheritsLoose(TextTrackSettings, _ModalDialog); 19970 19971 /** 19972 * Creates an instance of this class. 19973 * 19974 * @param {Player} player 19975 * The `Player` that this class should be attached to. 19976 * 19977 * @param {Object} [options] 19978 * The key/value store of player options. 19979 */ 19980 function TextTrackSettings(player, options) { 19981 var _this; 19982 19983 options.temporary = false; 19984 _this = _ModalDialog.call(this, player, options) || this; 19985 _this.updateDisplay = bind(assertThisInitialized(_this), _this.updateDisplay); // fill the modal and pretend we have opened it 19986 19987 _this.fill(); 19988 19989 _this.hasBeenOpened_ = _this.hasBeenFilled_ = true; 19990 _this.endDialog = createEl('p', { 19991 className: 'vjs-control-text', 19992 textContent: _this.localize('End of dialog window.') 19993 }); 19994 19995 _this.el().appendChild(_this.endDialog); 19996 19997 _this.setDefaults(); // Grab `persistTextTrackSettings` from the player options if not passed in child options 19998 19999 20000 if (options.persistTextTrackSettings === undefined) { 20001 _this.options_.persistTextTrackSettings = _this.options_.playerOptions.persistTextTrackSettings; 20002 } 20003 20004 _this.on(_this.$('.vjs-done-button'), 'click', function () { 20005 _this.saveSettings(); 20006 20007 _this.close(); 20008 }); 20009 20010 _this.on(_this.$('.vjs-default-button'), 'click', function () { 20011 _this.setDefaults(); 20012 20013 _this.updateDisplay(); 20014 }); 20015 20016 each(selectConfigs, function (config) { 20017 _this.on(_this.$(config.selector), 'change', _this.updateDisplay); 20018 }); 20019 20020 if (_this.options_.persistTextTrackSettings) { 20021 _this.restoreSettings(); 20022 } 20023 20024 return _this; 20025 } 20026 20027 var _proto = TextTrackSettings.prototype; 20028 20029 _proto.dispose = function dispose() { 20030 this.endDialog = null; 20031 20032 _ModalDialog.prototype.dispose.call(this); 20033 } 20034 /** 20035 * Create a <select> element with configured options. 20036 * 20037 * @param {string} key 20038 * Configuration key to use during creation. 20039 * 20040 * @return {string} 20041 * An HTML string. 20042 * 20043 * @private 20044 */ 20045 ; 20046 20047 _proto.createElSelect_ = function createElSelect_(key, legendId, type) { 20048 var _this2 = this; 20049 20050 if (legendId === void 0) { 20051 legendId = ''; 20052 } 20053 20054 if (type === void 0) { 20055 type = 'label'; 20056 } 20057 20058 var config = selectConfigs[key]; 20059 var id = config.id.replace('%s', this.id_); 20060 var selectLabelledbyIds = [legendId, id].join(' ').trim(); 20061 return ["<" + type + " id=\"" + id + "\" class=\"" + (type === 'label' ? 'vjs-label' : '') + "\">", this.localize(config.label), "</" + type + ">", "<select aria-labelledby=\"" + selectLabelledbyIds + "\">"].concat(config.options.map(function (o) { 20062 var optionId = id + '-' + o[1].replace(/\W+/g, ''); 20063 return ["<option id=\"" + optionId + "\" value=\"" + o[0] + "\" ", "aria-labelledby=\"" + selectLabelledbyIds + " " + optionId + "\">", _this2.localize(o[1]), '</option>'].join(''); 20064 })).concat('</select>').join(''); 20065 } 20066 /** 20067 * Create foreground color element for the component 20068 * 20069 * @return {string} 20070 * An HTML string. 20071 * 20072 * @private 20073 */ 20074 ; 20075 20076 _proto.createElFgColor_ = function createElFgColor_() { 20077 var legendId = "captions-text-legend-" + this.id_; 20078 return ['<fieldset class="vjs-fg-color vjs-track-setting">', "<legend id=\"" + legendId + "\">", this.localize('Text'), '</legend>', this.createElSelect_('color', legendId), '<span class="vjs-text-opacity vjs-opacity">', this.createElSelect_('textOpacity', legendId), '</span>', '</fieldset>'].join(''); 20079 } 20080 /** 20081 * Create background color element for the component 20082 * 20083 * @return {string} 20084 * An HTML string. 20085 * 20086 * @private 20087 */ 20088 ; 20089 20090 _proto.createElBgColor_ = function createElBgColor_() { 20091 var legendId = "captions-background-" + this.id_; 20092 return ['<fieldset class="vjs-bg-color vjs-track-setting">', "<legend id=\"" + legendId + "\">", this.localize('Background'), '</legend>', this.createElSelect_('backgroundColor', legendId), '<span class="vjs-bg-opacity vjs-opacity">', this.createElSelect_('backgroundOpacity', legendId), '</span>', '</fieldset>'].join(''); 20093 } 20094 /** 20095 * Create window color element for the component 20096 * 20097 * @return {string} 20098 * An HTML string. 20099 * 20100 * @private 20101 */ 20102 ; 20103 20104 _proto.createElWinColor_ = function createElWinColor_() { 20105 var legendId = "captions-window-" + this.id_; 20106 return ['<fieldset class="vjs-window-color vjs-track-setting">', "<legend id=\"" + legendId + "\">", this.localize('Window'), '</legend>', this.createElSelect_('windowColor', legendId), '<span class="vjs-window-opacity vjs-opacity">', this.createElSelect_('windowOpacity', legendId), '</span>', '</fieldset>'].join(''); 20107 } 20108 /** 20109 * Create color elements for the component 20110 * 20111 * @return {Element} 20112 * The element that was created 20113 * 20114 * @private 20115 */ 20116 ; 20117 20118 _proto.createElColors_ = function createElColors_() { 20119 return createEl('div', { 20120 className: 'vjs-track-settings-colors', 20121 innerHTML: [this.createElFgColor_(), this.createElBgColor_(), this.createElWinColor_()].join('') 20122 }); 20123 } 20124 /** 20125 * Create font elements for the component 20126 * 20127 * @return {Element} 20128 * The element that was created. 20129 * 20130 * @private 20131 */ 20132 ; 20133 20134 _proto.createElFont_ = function createElFont_() { 20135 return createEl('div', { 20136 className: 'vjs-track-settings-font', 20137 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('') 20138 }); 20139 } 20140 /** 20141 * Create controls for the component 20142 * 20143 * @return {Element} 20144 * The element that was created. 20145 * 20146 * @private 20147 */ 20148 ; 20149 20150 _proto.createElControls_ = function createElControls_() { 20151 var defaultsDescription = this.localize('restore all settings to the default values'); 20152 return createEl('div', { 20153 className: 'vjs-track-settings-controls', 20154 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('') 20155 }); 20156 }; 20157 20158 _proto.content = function content() { 20159 return [this.createElColors_(), this.createElFont_(), this.createElControls_()]; 20160 }; 20161 20162 _proto.label = function label() { 20163 return this.localize('Caption Settings Dialog'); 20164 }; 20165 20166 _proto.description = function description() { 20167 return this.localize('Beginning of dialog window. Escape will cancel and close the window.'); 20168 }; 20169 20170 _proto.buildCSSClass = function buildCSSClass() { 20171 return _ModalDialog.prototype.buildCSSClass.call(this) + ' vjs-text-track-settings'; 20172 } 20173 /** 20174 * Gets an object of text track settings (or null). 20175 * 20176 * @return {Object} 20177 * An object with config values parsed from the DOM or localStorage. 20178 */ 20179 ; 20180 20181 _proto.getValues = function getValues() { 20182 var _this3 = this; 20183 20184 return reduce(selectConfigs, function (accum, config, key) { 20185 var value = getSelectedOptionValue(_this3.$(config.selector), config.parser); 20186 20187 if (value !== undefined) { 20188 accum[key] = value; 20189 } 20190 20191 return accum; 20192 }, {}); 20193 } 20194 /** 20195 * Sets text track settings from an object of values. 20196 * 20197 * @param {Object} values 20198 * An object with config values parsed from the DOM or localStorage. 20199 */ 20200 ; 20201 20202 _proto.setValues = function setValues(values) { 20203 var _this4 = this; 20204 20205 each(selectConfigs, function (config, key) { 20206 setSelectedOption(_this4.$(config.selector), values[key], config.parser); 20207 }); 20208 } 20209 /** 20210 * Sets all `<select>` elements to their default values. 20211 */ 20212 ; 20213 20214 _proto.setDefaults = function setDefaults() { 20215 var _this5 = this; 20216 20217 each(selectConfigs, function (config) { 20218 var index = config.hasOwnProperty('default') ? config["default"] : 0; 20219 _this5.$(config.selector).selectedIndex = index; 20220 }); 20221 } 20222 /** 20223 * Restore texttrack settings from localStorage 20224 */ 20225 ; 20226 20227 _proto.restoreSettings = function restoreSettings() { 20228 var values; 20229 20230 try { 20231 values = JSON.parse(window$1.localStorage.getItem(LOCAL_STORAGE_KEY)); 20232 } catch (err) { 20233 log.warn(err); 20234 } 20235 20236 if (values) { 20237 this.setValues(values); 20238 } 20239 } 20240 /** 20241 * Save text track settings to localStorage 20242 */ 20243 ; 20244 20245 _proto.saveSettings = function saveSettings() { 20246 if (!this.options_.persistTextTrackSettings) { 20247 return; 20248 } 20249 20250 var values = this.getValues(); 20251 20252 try { 20253 if (Object.keys(values).length) { 20254 window$1.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(values)); 20255 } else { 20256 window$1.localStorage.removeItem(LOCAL_STORAGE_KEY); 20257 } 20258 } catch (err) { 20259 log.warn(err); 20260 } 20261 } 20262 /** 20263 * Update display of text track settings 20264 */ 20265 ; 20266 20267 _proto.updateDisplay = function updateDisplay() { 20268 var ttDisplay = this.player_.getChild('textTrackDisplay'); 20269 20270 if (ttDisplay) { 20271 ttDisplay.updateDisplay(); 20272 } 20273 } 20274 /** 20275 * conditionally blur the element and refocus the captions button 20276 * 20277 * @private 20278 */ 20279 ; 20280 20281 _proto.conditionalBlur_ = function conditionalBlur_() { 20282 this.previouslyActiveEl_ = null; 20283 var cb = this.player_.controlBar; 20284 var subsCapsBtn = cb && cb.subsCapsButton; 20285 var ccBtn = cb && cb.captionsButton; 20286 20287 if (subsCapsBtn) { 20288 subsCapsBtn.focus(); 20289 } else if (ccBtn) { 20290 ccBtn.focus(); 20291 } 20292 }; 20293 20294 return TextTrackSettings; 20295 }(ModalDialog); 20296 20297 Component.registerComponent('TextTrackSettings', TextTrackSettings); 20298 20299 /** 20300 * A Resize Manager. It is in charge of triggering `playerresize` on the player in the right conditions. 20301 * 20302 * 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}. 20303 * 20304 * If the ResizeObserver is available natively, it will be used. A polyfill can be passed in as an option. 20305 * If a `playerresize` event is not needed, the ResizeManager component can be removed from the player, see the example below. 20306 * @example <caption>How to disable the resize manager</caption> 20307 * const player = videojs('#vid', { 20308 * resizeManager: false 20309 * }); 20310 * 20311 * @see {@link https://wicg.github.io/ResizeObserver/|ResizeObserver specification} 20312 * 20313 * @extends Component 20314 */ 20315 20316 var ResizeManager = 20317 /*#__PURE__*/ 20318 function (_Component) { 20319 inheritsLoose(ResizeManager, _Component); 20320 20321 /** 20322 * Create the ResizeManager. 20323 * 20324 * @param {Object} player 20325 * The `Player` that this class should be attached to. 20326 * 20327 * @param {Object} [options] 20328 * The key/value store of ResizeManager options. 20329 * 20330 * @param {Object} [options.ResizeObserver] 20331 * A polyfill for ResizeObserver can be passed in here. 20332 * If this is set to null it will ignore the native ResizeObserver and fall back to the iframe fallback. 20333 */ 20334 function ResizeManager(player, options) { 20335 var _this; 20336 20337 var RESIZE_OBSERVER_AVAILABLE = options.ResizeObserver || window$1.ResizeObserver; // if `null` was passed, we want to disable the ResizeObserver 20338 20339 if (options.ResizeObserver === null) { 20340 RESIZE_OBSERVER_AVAILABLE = false; 20341 } // Only create an element when ResizeObserver isn't available 20342 20343 20344 var options_ = mergeOptions({ 20345 createEl: !RESIZE_OBSERVER_AVAILABLE, 20346 reportTouchActivity: false 20347 }, options); 20348 _this = _Component.call(this, player, options_) || this; 20349 _this.ResizeObserver = options.ResizeObserver || window$1.ResizeObserver; 20350 _this.loadListener_ = null; 20351 _this.resizeObserver_ = null; 20352 _this.debouncedHandler_ = debounce(function () { 20353 _this.resizeHandler(); 20354 }, 100, false, assertThisInitialized(_this)); 20355 20356 if (RESIZE_OBSERVER_AVAILABLE) { 20357 _this.resizeObserver_ = new _this.ResizeObserver(_this.debouncedHandler_); 20358 20359 _this.resizeObserver_.observe(player.el()); 20360 } else { 20361 _this.loadListener_ = function () { 20362 if (!_this.el_ || !_this.el_.contentWindow) { 20363 return; 20364 } 20365 20366 var debouncedHandler_ = _this.debouncedHandler_; 20367 20368 var unloadListener_ = _this.unloadListener_ = function () { 20369 off(this, 'resize', debouncedHandler_); 20370 off(this, 'unload', unloadListener_); 20371 unloadListener_ = null; 20372 }; // safari and edge can unload the iframe before resizemanager dispose 20373 // we have to dispose of event handlers correctly before that happens 20374 20375 20376 on(_this.el_.contentWindow, 'unload', unloadListener_); 20377 on(_this.el_.contentWindow, 'resize', debouncedHandler_); 20378 }; 20379 20380 _this.one('load', _this.loadListener_); 20381 } 20382 20383 return _this; 20384 } 20385 20386 var _proto = ResizeManager.prototype; 20387 20388 _proto.createEl = function createEl() { 20389 return _Component.prototype.createEl.call(this, 'iframe', { 20390 className: 'vjs-resize-manager', 20391 tabIndex: -1 20392 }, { 20393 'aria-hidden': 'true' 20394 }); 20395 } 20396 /** 20397 * Called when a resize is triggered on the iframe or a resize is observed via the ResizeObserver 20398 * 20399 * @fires Player#playerresize 20400 */ 20401 ; 20402 20403 _proto.resizeHandler = function resizeHandler() { 20404 /** 20405 * Called when the player size has changed 20406 * 20407 * @event Player#playerresize 20408 * @type {EventTarget~Event} 20409 */ 20410 // make sure player is still around to trigger 20411 // prevents this from causing an error after dispose 20412 if (!this.player_ || !this.player_.trigger) { 20413 return; 20414 } 20415 20416 this.player_.trigger('playerresize'); 20417 }; 20418 20419 _proto.dispose = function dispose() { 20420 if (this.debouncedHandler_) { 20421 this.debouncedHandler_.cancel(); 20422 } 20423 20424 if (this.resizeObserver_) { 20425 if (this.player_.el()) { 20426 this.resizeObserver_.unobserve(this.player_.el()); 20427 } 20428 20429 this.resizeObserver_.disconnect(); 20430 } 20431 20432 if (this.loadListener_) { 20433 this.off('load', this.loadListener_); 20434 } 20435 20436 if (this.el_ && this.el_.contentWindow && this.unloadListener_) { 20437 this.unloadListener_.call(this.el_.contentWindow); 20438 } 20439 20440 this.ResizeObserver = null; 20441 this.resizeObserver = null; 20442 this.debouncedHandler_ = null; 20443 this.loadListener_ = null; 20444 20445 _Component.prototype.dispose.call(this); 20446 }; 20447 20448 return ResizeManager; 20449 }(Component); 20450
20451 Component.registerComponent('ResizeManager', ResizeManager); 20452 20453 /** 20454 * Computes the median of an array. 20455 * 20456 * @param {number[]} arr 20457 * Input array of numbers. 20458 * 20459 * @return {number} 20460 * Median value. 20461 */ 20462 var median = function median(arr) { 20463 var mid = Math.floor(arr.length / 2); 20464 var sortedList = [].concat(arr).sort(function (a, b) { 20465 return a - b; 20466 }); 20467 return arr.length % 2 !== 0 ? sortedList[mid] : (sortedList[mid - 1] + sortedList[mid]) / 2; 20468 }; 20469 20470 /* track when we are at the live edge, and other helpers for live playback */ 20471 20472 var LiveTracker = 20473 /*#__PURE__*/ 20474 function (_Component) { 20475 inheritsLoose(LiveTracker, _Component); 20476 20477 function LiveTracker(player, options) { 20478 var _this; 20479 20480 // LiveTracker does not need an element 20481 var options_ = mergeOptions({ 20482 createEl: false 20483 }, options); 20484 _this = _Component.call(this, player, options_) || this; 20485 20486 _this.reset_(); 20487 20488 _this.on(_this.player_, 'durationchange', _this.handleDurationchange); // we don't need to track live playback if the document is hidden, 20489 // also, tracking when the document is hidden can 20490 // cause the CPU to spike and eventually crash the page on IE11. 20491 20492 20493 if (IE_VERSION && 'hidden' in document && 'visibilityState' in document) { 20494 _this.on(document, 'visibilitychange', _this.handleVisibilityChange); 20495 } 20496 20497 return _this; 20498 } 20499 20500 var _proto = LiveTracker.prototype; 20501 20502 _proto.handleVisibilityChange = function handleVisibilityChange() { 20503 if (this.player_.duration() !== Infinity) { 20504 return; 20505 } 20506 20507 if (document.hidden) { 20508 this.stopTracking(); 20509 } else { 20510 this.startTracking(); 20511 } 20512 }; 20513 20514 _proto.isBehind_ = function isBehind_() { 20515 // don't report that we are behind until a timeupdate has been seen 20516 if (!this.timeupdateSeen_) { 20517 return false; 20518 } 20519 20520 var liveCurrentTime = this.liveCurrentTime(); 20521 var currentTime = this.player_.currentTime(); // the live edge window is the amount of seconds away from live 20522 // that a player can be, but still be considered live. 20523 // we add 0.07 because the live tracking happens every 30ms 20524 // and we want some wiggle room for short segment live playback 20525 20526 var liveEdgeWindow = this.seekableIncrement_ * 2 + 0.07; // on Android liveCurrentTime can bee Infinity, because seekableEnd 20527 // can be Infinity, so we handle that case. 20528 20529 return liveCurrentTime !== Infinity && liveCurrentTime - liveEdgeWindow >= currentTime; 20530 } // all the functionality for tracking when seek end changes 20531 // and for tracking how far past seek end we should be 20532 ; 20533 20534 _proto.trackLive_ = function trackLive_() { 20535 this.pastSeekEnd_ = this.pastSeekEnd_; 20536 var seekable = this.player_.seekable(); // skip undefined seekable 20537 20538 if (!seekable || !seekable.length) { 20539 return; 20540 } 20541 20542 var newSeekEnd = this.seekableEnd(); // we can only tell if we are behind live, when seekable changes 20543 // once we detect that seekable has changed we check the new seek 20544 // end against current time, with a fudge value of half a second. 20545 20546 if (newSeekEnd !== this.lastSeekEnd_) { 20547 if (this.lastSeekEnd_) { 20548 // we try to get the best fit value for the seeking increment 20549 // variable from the last 12 values. 20550 this.seekableIncrementList_ = this.seekableIncrementList_.slice(-11); 20551 this.seekableIncrementList_.push(Math.abs(newSeekEnd - this.lastSeekEnd_)); 20552 20553 if (this.seekableIncrementList_.length > 3) { 20554 this.seekableIncrement_ = median(this.seekableIncrementList_); 20555 } 20556 } 20557 20558 this.pastSeekEnd_ = 0; 20559 this.lastSeekEnd_ = newSeekEnd; 20560 this.trigger('seekableendchange'); 20561 } // we should reset pastSeekEnd when the value 20562 // is much higher than seeking increment. 20563 20564 20565 if (this.pastSeekEnd() > this.seekableIncrement_ * 1.5) { 20566 this.pastSeekEnd_ = 0; 20567 } else { 20568 this.pastSeekEnd_ = this.pastSeekEnd() + 0.03; 20569 } 20570 20571 if (this.isBehind_() !== this.behindLiveEdge()) { 20572 this.behindLiveEdge_ = this.isBehind_(); 20573 this.trigger('liveedgechange'); 20574 } 20575 } 20576 /** 20577 * handle a durationchange event on the player 20578 * and start/stop tracking accordingly. 20579 */ 20580 ; 20581 20582 _proto.handleDurationchange = function handleDurationchange() { 20583 if (this.player_.duration() === Infinity) { 20584 this.startTracking(); 20585 } else { 20586 this.stopTracking(); 20587 } 20588 } 20589 /** 20590 * start tracking live playback 20591 */ 20592 ; 20593 20594 _proto.startTracking = function startTracking() { 20595 var _this2 = this; 20596 20597 if (this.isTracking()) { 20598 return; 20599 } // If we haven't seen a timeupdate, we need to check whether playback 20600 // began before this component started tracking. This can happen commonly 20601 // when using autoplay. 20602 20603 20604 if (!this.timeupdateSeen_) { 20605 this.timeupdateSeen_ = this.player_.hasStarted(); 20606 } 20607 20608 this.trackingInterval_ = this.setInterval(this.trackLive_, 30); 20609 this.trackLive_(); 20610 this.on(this.player_, 'play', this.trackLive_); 20611 this.on(this.player_, 'pause', this.trackLive_); // this is to prevent showing that we are not live 20612 // before a video starts to play 20613 20614 if (!this.timeupdateSeen_) { 20615 this.one(this.player_, 'play', this.handlePlay); 20616 20617 this.handleTimeupdate = function () { 20618 _this2.timeupdateSeen_ = true; 20619 _this2.handleTimeupdate = null; 20620 }; 20621 20622 this.one(this.player_, 'timeupdate', this.handleTimeupdate); 20623 } 20624 }; 20625 20626 _proto.handlePlay = function handlePlay() { 20627 this.one(this.player_, 'timeupdate', this.seekToLiveEdge); 20628 } 20629 /** 20630 * Stop tracking, and set all internal variables to 20631 * their initial value. 20632 */ 20633 ; 20634 20635 _proto.reset_ = function reset_() { 20636 this.pastSeekEnd_ = 0; 20637 this.lastSeekEnd_ = null; 20638 this.behindLiveEdge_ = null; 20639 this.timeupdateSeen_ = false;
vendor: 28,696 bytes, lines 20640-21515
20640 this.clearInterval(this.trackingInterval_); 20641 this.trackingInterval_ = null; 20642 this.seekableIncrement_ = 12; 20643 this.seekableIncrementList_ = []; 20644 this.off(this.player_, 'play', this.trackLive_); 20645 this.off(this.player_, 'pause', this.trackLive_); 20646 this.off(this.player_, 'play', this.handlePlay); 20647 this.off(this.player_, 'timeupdate', this.seekToLiveEdge); 20648 20649 if (this.handleTimeupdate) { 20650 this.off(this.player_, 'timeupdate', this.handleTimeupdate); 20651 this.handleTimeupdate = null; 20652 } 20653 } 20654 /** 20655 * stop tracking live playback 20656 */ 20657 ; 20658 20659 _proto.stopTracking = function stopTracking() { 20660 if (!this.isTracking()) { 20661 return; 20662 } 20663 20664 this.reset_(); 20665 } 20666 /** 20667 * A helper to get the player seekable end 20668 * so that we don't have to null check everywhere 20669 */ 20670 ; 20671 20672 _proto.seekableEnd = function seekableEnd() { 20673 var seekable = this.player_.seekable(); 20674 var seekableEnds = []; 20675 var i = seekable ? seekable.length : 0; 20676 20677 while (i--) { 20678 seekableEnds.push(seekable.end(i)); 20679 } // grab the furthest seekable end after sorting, or if there are none 20680 // default to Infinity 20681 20682 20683 return seekableEnds.length ? seekableEnds.sort()[seekableEnds.length - 1] : Infinity; 20684 } 20685 /** 20686 * A helper to get the player seekable start 20687 * so that we don't have to null check everywhere 20688 */ 20689 ; 20690 20691 _proto.seekableStart = function seekableStart() { 20692 var seekable = this.player_.seekable(); 20693 var seekableStarts = []; 20694 var i = seekable ? seekable.length : 0; 20695 20696 while (i--) { 20697 seekableStarts.push(seekable.start(i)); 20698 } // grab the first seekable start after sorting, or if there are none 20699 // default to 0 20700 20701 20702 return seekableStarts.length ? seekableStarts.sort()[0] : 0; 20703 } 20704 /** 20705 * Get the live time window 20706 */ 20707 ; 20708 20709 _proto.liveWindow = function liveWindow() { 20710 var liveCurrentTime = this.liveCurrentTime(); 20711 20712 if (liveCurrentTime === Infinity) { 20713 return Infinity; 20714 } 20715 20716 return liveCurrentTime - this.seekableStart(); 20717 } 20718 /** 20719 * Determines if the player is live, only checks if this component 20720 * is tracking live playback or not 20721 */ 20722 ; 20723 20724 _proto.isLive = function isLive() { 20725 return this.isTracking(); 20726 } 20727 /** 20728 * Determines if currentTime is at the live edge and won't fall behind 20729 * on each seekableendchange 20730 */ 20731 ; 20732 20733 _proto.atLiveEdge = function atLiveEdge() { 20734 return !this.behindLiveEdge(); 20735 } 20736 /** 20737 * get what we expect the live current time to be 20738 */ 20739 ; 20740 20741 _proto.liveCurrentTime = function liveCurrentTime() { 20742 return this.pastSeekEnd() + this.seekableEnd(); 20743 } 20744 /** 20745 * Returns how far past seek end we expect current time to be 20746 */ 20747 ; 20748 20749 _proto.pastSeekEnd = function pastSeekEnd() { 20750 return this.pastSeekEnd_; 20751 } 20752 /** 20753 * If we are currently behind the live edge, aka currentTime will be 20754 * behind on a seekableendchange 20755 */ 20756 ; 20757 20758 _proto.behindLiveEdge = function behindLiveEdge() { 20759 return this.behindLiveEdge_; 20760 }; 20761 20762 _proto.isTracking = function isTracking() { 20763 return typeof this.trackingInterval_ === 'number'; 20764 } 20765 /** 20766 * Seek to the live edge if we are behind the live edge 20767 */ 20768 ; 20769 20770 _proto.seekToLiveEdge = function seekToLiveEdge() { 20771 if (this.atLiveEdge()) { 20772 return; 20773 } 20774 20775 this.player_.currentTime(this.liveCurrentTime()); 20776 20777 if (this.player_.paused()) { 20778 this.player_.play(); 20779 } 20780 }; 20781 20782 _proto.dispose = function dispose() { 20783 this.stopTracking(); 20784 20785 _Component.prototype.dispose.call(this); 20786 }; 20787 20788 return LiveTracker; 20789 }(Component); 20790 20791 Component.registerComponent('LiveTracker', LiveTracker); 20792 20793 /** 20794 * This function is used to fire a sourceset when there is something 20795 * similar to `mediaEl.load()` being called. It will try to find the source via 20796 * the `src` attribute and then the `<source>` elements. It will then fire `sourceset` 20797 * with the source that was found or empty string if we cannot know. If it cannot 20798 * find a source then `sourceset` will not be fired. 20799 * 20800 * @param {Html5} tech 20801 * The tech object that sourceset was setup on 20802 * 20803 * @return {boolean} 20804 * returns false if the sourceset was not fired and true otherwise. 20805 */ 20806 20807 var sourcesetLoad = function sourcesetLoad(tech) { 20808 var el = tech.el(); // if `el.src` is set, that source will be loaded. 20809 20810 if (el.hasAttribute('src')) { 20811 tech.triggerSourceset(el.src); 20812 return true; 20813 } 20814 /** 20815 * Since there isn't a src property on the media element, source elements will be used for 20816 * implementing the source selection algorithm. This happens asynchronously and 20817 * for most cases were there is more than one source we cannot tell what source will 20818 * be loaded, without re-implementing the source selection algorithm. At this time we are not 20819 * going to do that. There are three special cases that we do handle here though: 20820 * 20821 * 1. If there are no sources, do not fire `sourceset`. 20822 * 2. If there is only one `<source>` with a `src` property/attribute that is our `src` 20823 * 3. If there is more than one `<source>` but all of them have the same `src` url. 20824 * That will be our src. 20825 */ 20826 20827 20828 var sources = tech.$$('source'); 20829 var srcUrls = []; 20830 var src = ''; // if there are no sources, do not fire sourceset 20831 20832 if (!sources.length) { 20833 return false; 20834 } // only count valid/non-duplicate source elements 20835 20836 20837 for (var i = 0; i < sources.length; i++) { 20838 var url = sources[i].src; 20839 20840 if (url && srcUrls.indexOf(url) === -1) { 20841 srcUrls.push(url); 20842 } 20843 } // there were no valid sources 20844 20845 20846 if (!srcUrls.length) { 20847 return false; 20848 } // there is only one valid source element url 20849 // use that 20850 20851 20852 if (srcUrls.length === 1) { 20853 src = srcUrls[0]; 20854 } 20855 20856 tech.triggerSourceset(src); 20857 return true; 20858 }; 20859 /** 20860 * our implementation of an `innerHTML` descriptor for browsers 20861 * that do not have one. 20862 */ 20863 20864 20865 var innerHTMLDescriptorPolyfill = Object.defineProperty({}, 'innerHTML', { 20866 get: function get() { 20867 return this.cloneNode(true).innerHTML; 20868 }, 20869 set: function set(v) { 20870 // make a dummy node to use innerHTML on 20871 var dummy = document.createElement(this.nodeName.toLowerCase()); // set innerHTML to the value provided 20872 20873 dummy.innerHTML = v; // make a document fragment to hold the nodes from dummy 20874 20875 var docFrag = document.createDocumentFragment(); // copy all of the nodes created by the innerHTML on dummy 20876 // to the document fragment 20877 20878 while (dummy.childNodes.length) { 20879 docFrag.appendChild(dummy.childNodes[0]); 20880 } // remove content 20881 20882 20883 this.innerText = ''; // now we add all of that html in one by appending the 20884 // document fragment. This is how innerHTML does it. 20885 20886 window$1.Element.prototype.appendChild.call(this, docFrag); // then return the result that innerHTML's setter would 20887 20888 return this.innerHTML; 20889 } 20890 }); 20891 /** 20892 * Get a property descriptor given a list of priorities and the 20893 * property to get. 20894 */ 20895 20896 var getDescriptor = function getDescriptor(priority, prop) { 20897 var descriptor = {}; 20898 20899 for (var i = 0; i < priority.length; i++) { 20900 descriptor = Object.getOwnPropertyDescriptor(priority[i], prop); 20901 20902 if (descriptor && descriptor.set && descriptor.get) { 20903 break; 20904 } 20905 } 20906 20907 descriptor.enumerable = true; 20908 descriptor.configurable = true; 20909 return descriptor; 20910 }; 20911 20912 var getInnerHTMLDescriptor = function getInnerHTMLDescriptor(tech) { 20913 return getDescriptor([tech.el(), window$1.HTMLMediaElement.prototype, window$1.Element.prototype, innerHTMLDescriptorPolyfill], 'innerHTML'); 20914 }; 20915 /** 20916 * Patches browser internal functions so that we can tell synchronously 20917 * if a `<source>` was appended to the media element. For some reason this 20918 * causes a `sourceset` if the the media element is ready and has no source. 20919 * This happens when: 20920 * - The page has just loaded and the media element does not have a source. 20921 * - The media element was emptied of all sources, then `load()` was called. 20922 * 20923 * It does this by patching the following functions/properties when they are supported: 20924 * 20925 * - `append()` - can be used to add a `<source>` element to the media element 20926 * - `appendChild()` - can be used to add a `<source>` element to the media element 20927 * - `insertAdjacentHTML()` - can be used to add a `<source>` element to the media element 20928 * - `innerHTML` - can be used to add a `<source>` element to the media element 20929 * 20930 * @param {Html5} tech 20931 * The tech object that sourceset is being setup on. 20932 */ 20933 20934 20935 var firstSourceWatch = function firstSourceWatch(tech) { 20936 var el = tech.el(); // make sure firstSourceWatch isn't setup twice. 20937 20938 if (el.resetSourceWatch_) { 20939 return; 20940 } 20941 20942 var old = {}; 20943 var innerDescriptor = getInnerHTMLDescriptor(tech); 20944 20945 var appendWrapper = function appendWrapper(appendFn) { 20946 return function () { 20947 for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { 20948 args[_key] = arguments[_key]; 20949 } 20950 20951 var retval = appendFn.apply(el, args); 20952 sourcesetLoad(tech); 20953 return retval; 20954 }; 20955 }; 20956 20957 ['append', 'appendChild', 'insertAdjacentHTML'].forEach(function (k) { 20958 if (!el[k]) { 20959 return; 20960 } // store the old function 20961 20962 20963 old[k] = el[k]; // call the old function with a sourceset if a source 20964 // was loaded 20965 20966 el[k] = appendWrapper(old[k]); 20967 }); 20968 Object.defineProperty(el, 'innerHTML', mergeOptions(innerDescriptor, { 20969 set: appendWrapper(innerDescriptor.set) 20970 })); 20971 20972 el.resetSourceWatch_ = function () { 20973 el.resetSourceWatch_ = null; 20974 Object.keys(old).forEach(function (k) { 20975 el[k] = old[k]; 20976 }); 20977 Object.defineProperty(el, 'innerHTML', innerDescriptor); 20978 }; // on the first sourceset, we need to revert our changes 20979 20980 20981 tech.one('sourceset', el.resetSourceWatch_); 20982 }; 20983 /** 20984 * our implementation of a `src` descriptor for browsers 20985 * that do not have one. 20986 */ 20987 20988 20989 var srcDescriptorPolyfill = Object.defineProperty({}, 'src', { 20990 get: function get() { 20991 if (this.hasAttribute('src')) { 20992 return getAbsoluteURL(window$1.Element.prototype.getAttribute.call(this, 'src')); 20993 } 20994 20995 return ''; 20996 }, 20997 set: function set(v) { 20998 window$1.Element.prototype.setAttribute.call(this, 'src', v); 20999 return v; 21000 } 21001 }); 21002 21003 var getSrcDescriptor = function getSrcDescriptor(tech) { 21004 return getDescriptor([tech.el(), window$1.HTMLMediaElement.prototype, srcDescriptorPolyfill], 'src'); 21005 }; 21006 /** 21007 * setup `sourceset` handling on the `Html5` tech. This function 21008 * patches the following element properties/functions: 21009 * 21010 * - `src` - to determine when `src` is set 21011 * - `setAttribute()` - to determine when `src` is set 21012 * - `load()` - this re-triggers the source selection algorithm, and can 21013 * cause a sourceset. 21014 * 21015 * If there is no source when we are adding `sourceset` support or during a `load()` 21016 * we also patch the functions listed in `firstSourceWatch`. 21017 * 21018 * @param {Html5} tech 21019 * The tech to patch 21020 */ 21021 21022 21023 var setupSourceset = function setupSourceset(tech) { 21024 if (!tech.featuresSourceset) { 21025 return; 21026 } 21027 21028 var el = tech.el(); // make sure sourceset isn't setup twice. 21029 21030 if (el.resetSourceset_) { 21031 return; 21032 } 21033 21034 var srcDescriptor = getSrcDescriptor(tech); 21035 var oldSetAttribute = el.setAttribute; 21036 var oldLoad = el.load; 21037 Object.defineProperty(el, 'src', mergeOptions(srcDescriptor, { 21038 set: function set(v) { 21039 var retval = srcDescriptor.set.call(el, v); // we use the getter here to get the actual value set on src 21040 21041 tech.triggerSourceset(el.src); 21042 return retval; 21043 } 21044 })); 21045 21046 el.setAttribute = function (n, v) { 21047 var retval = oldSetAttribute.call(el, n, v); 21048 21049 if (/src/i.test(n)) { 21050 tech.triggerSourceset(el.src); 21051 } 21052 21053 return retval; 21054 }; 21055 21056 el.load = function () { 21057 var retval = oldLoad.call(el); // if load was called, but there was no source to fire 21058 // sourceset on. We have to watch for a source append 21059 // as that can trigger a `sourceset` when the media element 21060 // has no source 21061 21062 if (!sourcesetLoad(tech)) { 21063 tech.triggerSourceset(''); 21064 firstSourceWatch(tech); 21065 } 21066 21067 return retval; 21068 }; 21069 21070 if (el.currentSrc) { 21071 tech.triggerSourceset(el.currentSrc); 21072 } else if (!sourcesetLoad(tech)) { 21073 firstSourceWatch(tech); 21074 } 21075 21076 el.resetSourceset_ = function () { 21077 el.resetSourceset_ = null; 21078 el.load = oldLoad; 21079 el.setAttribute = oldSetAttribute; 21080 Object.defineProperty(el, 'src', srcDescriptor); 21081 21082 if (el.resetSourceWatch_) { 21083 el.resetSourceWatch_(); 21084 } 21085 }; 21086 }; 21087 21088 /** 21089 * Object.defineProperty but "lazy", which means that the value is only set after 21090 * it retrieved the first time, rather than being set right away. 21091 * 21092 * @param {Object} obj the object to set the property on 21093 * @param {string} key the key for the property to set 21094 * @param {Function} getValue the function used to get the value when it is needed. 21095 * @param {boolean} setter wether a setter shoould be allowed or not 21096 */ 21097 var defineLazyProperty = function defineLazyProperty(obj, key, getValue, setter) { 21098 if (setter === void 0) { 21099 setter = true; 21100 } 21101 21102 var set = function set(value) { 21103 return Object.defineProperty(obj, key, { 21104 value: value, 21105 enumerable: true, 21106 writable: true 21107 }); 21108 }; 21109 21110 var options = { 21111 configurable: true, 21112 enumerable: true, 21113 get: function get() { 21114 var value = getValue(); 21115 set(value); 21116 return value; 21117 } 21118 }; 21119 21120 if (setter) { 21121 options.set = set; 21122 } 21123 21124 return Object.defineProperty(obj, key, options); 21125 }; 21126 21127 /** 21128 * HTML5 Media Controller - Wrapper for HTML5 Media API 21129 * 21130 * @mixes Tech~SourceHandlerAdditions 21131 * @extends Tech 21132 */ 21133 21134 var Html5 = 21135 /*#__PURE__*/ 21136 function (_Tech) { 21137 inheritsLoose(Html5, _Tech); 21138 21139 /** 21140 * Create an instance of this Tech. 21141 * 21142 * @param {Object} [options] 21143 * The key/value store of player options. 21144 * 21145 * @param {Component~ReadyCallback} ready 21146 * Callback function to call when the `HTML5` Tech is ready. 21147 */ 21148 function Html5(options, ready) { 21149 var _this; 21150 21151 _this = _Tech.call(this, options, ready) || this; 21152 var source = options.source; 21153 var crossoriginTracks = false; // Set the source if one is provided 21154 // 1) Check if the source is new (if not, we want to keep the original so playback isn't interrupted) 21155 // 2) Check to see if the network state of the tag was failed at init, and if so, reset the source 21156 // anyway so the error gets fired. 21157 21158 if (source && (_this.el_.currentSrc !== source.src || options.tag && options.tag.initNetworkState_ === 3)) { 21159 _this.setSource(source); 21160 } else { 21161 _this.handleLateInit_(_this.el_); 21162 } // setup sourceset after late sourceset/init 21163 21164 21165 if (options.enableSourceset) { 21166 _this.setupSourcesetHandling_(); 21167 } 21168 21169 if (_this.el_.hasChildNodes()) { 21170 var nodes = _this.el_.childNodes; 21171 var nodesLength = nodes.length; 21172 var removeNodes = []; 21173 21174 while (nodesLength--) { 21175 var node = nodes[nodesLength]; 21176 var nodeName = node.nodeName.toLowerCase(); 21177 21178 if (nodeName === 'track') { 21179 if (!_this.featuresNativeTextTracks) { 21180 // Empty video tag tracks so the built-in player doesn't use them also. 21181 // This may not be fast enough to stop HTML5 browsers from reading the tags 21182 // so we'll need to turn off any default tracks if we're manually doing 21183 // captions and subtitles. videoElement.textTracks 21184 removeNodes.push(node); 21185 } else { 21186 // store HTMLTrackElement and TextTrack to remote list 21187 _this.remoteTextTrackEls().addTrackElement_(node); 21188 21189 _this.remoteTextTracks().addTrack(node.track); 21190 21191 _this.textTracks().addTrack(node.track); 21192 21193 if (!crossoriginTracks && !_this.el_.hasAttribute('crossorigin') && isCrossOrigin(node.src)) { 21194 crossoriginTracks = true; 21195 } 21196 } 21197 } 21198 } 21199 21200 for (var i = 0; i < removeNodes.length; i++) { 21201 _this.el_.removeChild(removeNodes[i]); 21202 } 21203 } 21204 21205 _this.proxyNativeTracks_(); 21206 21207 if (_this.featuresNativeTextTracks && crossoriginTracks) { 21208 log.warn('Text Tracks are being loaded from another origin but the crossorigin attribute isn\'t used.\n' + 'This may prevent text tracks from loading.'); 21209 } // prevent iOS Safari from disabling metadata text tracks during native playback 21210 21211 21212 _this.restoreMetadataTracksInIOSNativePlayer_(); // Determine if native controls should be used 21213 // Our goal should be to get the custom controls on mobile solid everywhere 21214 // so we can remove this all together. Right now this will block custom 21215 // controls on touch enabled laptops like the Chrome Pixel 21216 21217 21218 if ((TOUCH_ENABLED || IS_IPHONE || IS_NATIVE_ANDROID) && options.nativeControlsForTouch === true) { 21219 _this.setControls(true); 21220 } // on iOS, we want to proxy `webkitbeginfullscreen` and `webkitendfullscreen` 21221 // into a `fullscreenchange` event 21222 21223 21224 _this.proxyWebkitFullscreen_(); 21225 21226 _this.triggerReady(); 21227 21228 return _this; 21229 } 21230 /** 21231 * Dispose of `HTML5` media element and remove all tracks. 21232 */ 21233 21234 21235 var _proto = Html5.prototype; 21236 21237 _proto.dispose = function dispose() { 21238 if (this.el_ && this.el_.resetSourceset_) { 21239 this.el_.resetSourceset_(); 21240 } 21241 21242 Html5.disposeMediaElement(this.el_); 21243 this.options_ = null; // tech will handle clearing of the emulated track list 21244 21245 _Tech.prototype.dispose.call(this); 21246 } 21247 /** 21248 * Modify the media element so that we can detect when 21249 * the source is changed. Fires `sourceset` just after the source has changed 21250 */ 21251 ; 21252 21253 _proto.setupSourcesetHandling_ = function setupSourcesetHandling_() { 21254 setupSourceset(this); 21255 } 21256 /** 21257 * When a captions track is enabled in the iOS Safari native player, all other 21258 * tracks are disabled (including metadata tracks), which nulls all of their 21259 * associated cue points. This will restore metadata tracks to their pre-fullscreen 21260 * state in those cases so that cue points are not needlessly lost. 21261 * 21262 * @private 21263 */ 21264 ; 21265 21266 _proto.restoreMetadataTracksInIOSNativePlayer_ = function restoreMetadataTracksInIOSNativePlayer_() { 21267 var textTracks = this.textTracks(); 21268 var metadataTracksPreFullscreenState; // captures a snapshot of every metadata track's current state 21269 21270 var takeMetadataTrackSnapshot = function takeMetadataTrackSnapshot() { 21271 metadataTracksPreFullscreenState = []; 21272 21273 for (var i = 0; i < textTracks.length; i++) { 21274 var track = textTracks[i]; 21275 21276 if (track.kind === 'metadata') { 21277 metadataTracksPreFullscreenState.push({ 21278 track: track, 21279 storedMode: track.mode 21280 }); 21281 } 21282 } 21283 }; // snapshot each metadata track's initial state, and update the snapshot 21284 // each time there is a track 'change' event 21285 21286 21287 takeMetadataTrackSnapshot(); 21288 textTracks.addEventListener('change', takeMetadataTrackSnapshot); 21289 this.on('dispose', function () { 21290 return textTracks.removeEventListener('change', takeMetadataTrackSnapshot); 21291 }); 21292 21293 var restoreTrackMode = function restoreTrackMode() { 21294 for (var i = 0; i < metadataTracksPreFullscreenState.length; i++) { 21295 var storedTrack = metadataTracksPreFullscreenState[i]; 21296 21297 if (storedTrack.track.mode === 'disabled' && storedTrack.track.mode !== storedTrack.storedMode) { 21298 storedTrack.track.mode = storedTrack.storedMode; 21299 } 21300 } // we only want this handler to be executed on the first 'change' event 21301 21302 21303 textTracks.removeEventListener('change', restoreTrackMode); 21304 }; // when we enter fullscreen playback, stop updating the snapshot and 21305 // restore all track modes to their pre-fullscreen state 21306 21307 21308 this.on('webkitbeginfullscreen', function () { 21309 textTracks.removeEventListener('change', takeMetadataTrackSnapshot); // remove the listener before adding it just in case it wasn't previously removed 21310 21311 textTracks.removeEventListener('change', restoreTrackMode); 21312 textTracks.addEventListener('change', restoreTrackMode); 21313 }); // start updating the snapshot again after leaving fullscreen 21314 21315 this.on('webkitendfullscreen', function () { 21316 // remove the listener before adding it just in case it wasn't previously removed 21317 textTracks.removeEventListener('change', takeMetadataTrackSnapshot); 21318 textTracks.addEventListener('change', takeMetadataTrackSnapshot); // remove the restoreTrackMode handler in case it wasn't triggered during fullscreen playback 21319 21320 textTracks.removeEventListener('change', restoreTrackMode); 21321 }); 21322 } 21323 /** 21324 * Attempt to force override of tracks for the given type 21325 * 21326 * @param {string} type - Track type to override, possible values include 'Audio', 21327 * 'Video', and 'Text'. 21328 * @param {boolean} override - If set to true native audio/video will be overridden, 21329 * otherwise native audio/video will potentially be used. 21330 * @private 21331 */ 21332 ; 21333 21334 _proto.overrideNative_ = function overrideNative_(type, override) { 21335 var _this2 = this; 21336 21337 // If there is no behavioral change don't add/remove listeners 21338 if (override !== this["featuresNative" + type + "Tracks"]) { 21339 return; 21340 } 21341 21342 var lowerCaseType = type.toLowerCase(); 21343 21344 if (this[lowerCaseType + "TracksListeners_"]) { 21345 Object.keys(this[lowerCaseType + "TracksListeners_"]).forEach(function (eventName) { 21346 var elTracks = _this2.el()[lowerCaseType + "Tracks"]; 21347 21348 elTracks.removeEventListener(eventName, _this2[lowerCaseType + "TracksListeners_"][eventName]); 21349 }); 21350 } 21351 21352 this["featuresNative" + type + "Tracks"] = !override; 21353 this[lowerCaseType + "TracksListeners_"] = null; 21354 this.proxyNativeTracksForType_(lowerCaseType); 21355 } 21356 /** 21357 * Attempt to force override of native audio tracks. 21358 * 21359 * @param {boolean} override - If set to true native audio will be overridden, 21360 * otherwise native audio will potentially be used. 21361 */ 21362 ; 21363 21364 _proto.overrideNativeAudioTracks = function overrideNativeAudioTracks(override) { 21365 this.overrideNative_('Audio', override); 21366 } 21367 /** 21368 * Attempt to force override of native video tracks. 21369 * 21370 * @param {boolean} override - If set to true native video will be overridden, 21371 * otherwise native video will potentially be used. 21372 */ 21373 ; 21374 21375 _proto.overrideNativeVideoTracks = function overrideNativeVideoTracks(override) { 21376 this.overrideNative_('Video', override); 21377 } 21378 /** 21379 * Proxy native track list events for the given type to our track 21380 * lists if the browser we are playing in supports that type of track list. 21381 * 21382 * @param {string} name - Track type; values include 'audio', 'video', and 'text' 21383 * @private 21384 */ 21385 ; 21386 21387 _proto.proxyNativeTracksForType_ = function proxyNativeTracksForType_(name) { 21388 var _this3 = this; 21389 21390 var props = NORMAL[name]; 21391 var elTracks = this.el()[props.getterName]; 21392 var techTracks = this[props.getterName](); 21393 21394 if (!this["featuresNative" + props.capitalName + "Tracks"] || !elTracks || !elTracks.addEventListener) { 21395 return; 21396 } 21397 21398 var listeners = { 21399 change: function change(e) { 21400 techTracks.trigger({ 21401 type: 'change', 21402 target: techTracks, 21403 currentTarget: techTracks, 21404 srcElement: techTracks 21405 }); 21406 }, 21407 addtrack: function addtrack(e) { 21408 techTracks.addTrack(e.track); 21409 }, 21410 removetrack: function removetrack(e) { 21411 techTracks.removeTrack(e.track); 21412 } 21413 }; 21414 21415 var removeOldTracks = function removeOldTracks() { 21416 var removeTracks = []; 21417 21418 for (var i = 0; i < techTracks.length; i++) { 21419 var found = false; 21420 21421 for (var j = 0; j < elTracks.length; j++) { 21422 if (elTracks[j] === techTracks[i]) { 21423 found = true; 21424 break; 21425 } 21426 } 21427 21428 if (!found) { 21429 removeTracks.push(techTracks[i]); 21430 } 21431 } 21432 21433 while (removeTracks.length) { 21434 techTracks.removeTrack(removeTracks.shift()); 21435 } 21436 }; 21437 21438 this[props.getterName + 'Listeners_'] = listeners; 21439 Object.keys(listeners).forEach(function (eventName) { 21440 var listener = listeners[eventName]; 21441 elTracks.addEventListener(eventName, listener); 21442 21443 _this3.on('dispose', function (e) { 21444 return elTracks.removeEventListener(eventName, listener); 21445 }); 21446 }); // Remove (native) tracks that are not used anymore 21447 21448 this.on('loadstart', removeOldTracks); 21449 this.on('dispose', function (e) { 21450 return _this3.off('loadstart', removeOldTracks); 21451 }); 21452 } 21453 /** 21454 * Proxy all native track list events to our track lists if the browser we are playing 21455 * in supports that type of track list. 21456 * 21457 * @private 21458 */ 21459 ; 21460 21461 _proto.proxyNativeTracks_ = function proxyNativeTracks_() { 21462 var _this4 = this; 21463 21464 NORMAL.names.forEach(function (name) { 21465 _this4.proxyNativeTracksForType_(name); 21466 }); 21467 } 21468 /** 21469 * Create the `Html5` Tech's DOM element. 21470 * 21471 * @return {Element} 21472 * The element that gets created. 21473 */ 21474 ; 21475 21476 _proto.createEl = function createEl() { 21477 var el = this.options_.tag; // Check if this browser supports moving the element into the box. 21478 // On the iPhone video will break if you move the element, 21479 // So we have to create a brand new element. 21480 // If we ingested the player div, we do not need to move the media element. 21481 21482 if (!el || !(this.options_.playerElIngest || this.movingMediaElementInDOM)) { 21483 // If the original tag is still there, clone and remove it. 21484 if (el) { 21485 var clone = el.cloneNode(true); 21486 21487 if (el.parentNode) { 21488 el.parentNode.insertBefore(clone, el); 21489 } 21490 21491 Html5.disposeMediaElement(el); 21492 el = clone; 21493 } else { 21494 el = document.createElement('video'); // determine if native controls should be used 21495 21496 var tagAttributes = this.options_.tag && getAttributes(this.options_.tag); 21497 var attributes = mergeOptions({}, tagAttributes); 21498 21499 if (!TOUCH_ENABLED || this.options_.nativeControlsForTouch !== true) { 21500 delete attributes.controls; 21501 } 21502 21503 setAttributes(el, assign(attributes, { 21504 id: this.options_.techId, 21505 "class": 'vjs-tech' 21506 })); 21507 } 21508 21509 el.playerId = this.options_.playerId; 21510 } 21511 21512 if (typeof this.options_.preload !== 'undefined') { 21513 setAttribute(el, 'preload', this.options_.preload); 21514 } // Update specific tag settings, in case they were overridden 21515 // `autoplay` has to be *last* so that `muted` and `playsinline` are present
vendor: 8,072 bytes, lines 21516-21760
21516 // when iOS/Safari or other browsers attempt to autoplay. 21517 21518 21519 var settingsAttrs = ['loop', 'muted', 'playsinline', 'autoplay']; 21520 21521 for (var i = 0; i < settingsAttrs.length; i++) { 21522 var attr = settingsAttrs[i]; 21523 var value = this.options_[attr]; 21524 21525 if (typeof value !== 'undefined') { 21526 if (value) { 21527 setAttribute(el, attr, attr); 21528 } else { 21529 removeAttribute(el, attr); 21530 } 21531 21532 el[attr] = value; 21533 } 21534 } 21535 21536 return el; 21537 } 21538 /** 21539 * This will be triggered if the loadstart event has already fired, before videojs was 21540 * ready. Two known examples of when this can happen are: 21541 * 1. If we're loading the playback object after it has started loading 21542 * 2. The media is already playing the (often with autoplay on) then 21543 * 21544 * This function will fire another loadstart so that videojs can catchup. 21545 * 21546 * @fires Tech#loadstart 21547 * 21548 * @return {undefined} 21549 * returns nothing. 21550 */ 21551 ; 21552 21553 _proto.handleLateInit_ = function handleLateInit_(el) { 21554 if (el.networkState === 0 || el.networkState === 3) { 21555 // The video element hasn't started loading the source yet 21556 // or didn't find a source 21557 return; 21558 } 21559 21560 if (el.readyState === 0) { 21561 // NetworkState is set synchronously BUT loadstart is fired at the 21562 // end of the current stack, usually before setInterval(fn, 0). 21563 // So at this point we know loadstart may have already fired or is 21564 // about to fire, and either way the player hasn't seen it yet. 21565 // We don't want to fire loadstart prematurely here and cause a 21566 // double loadstart so we'll wait and see if it happens between now 21567 // and the next loop, and fire it if not. 21568 // HOWEVER, we also want to make sure it fires before loadedmetadata 21569 // which could also happen between now and the next loop, so we'll 21570 // watch for that also. 21571 var loadstartFired = false; 21572 21573 var setLoadstartFired = function setLoadstartFired() { 21574 loadstartFired = true; 21575 }; 21576 21577 this.on('loadstart', setLoadstartFired); 21578 21579 var triggerLoadstart = function triggerLoadstart() { 21580 // We did miss the original loadstart. Make sure the player 21581 // sees loadstart before loadedmetadata 21582 if (!loadstartFired) { 21583 this.trigger('loadstart'); 21584 } 21585 }; 21586 21587 this.on('loadedmetadata', triggerLoadstart); 21588 this.ready(function () { 21589 this.off('loadstart', setLoadstartFired); 21590 this.off('loadedmetadata', triggerLoadstart); 21591 21592 if (!loadstartFired) { 21593 // We did miss the original native loadstart. Fire it now. 21594 this.trigger('loadstart'); 21595 } 21596 }); 21597 return; 21598 } // From here on we know that loadstart already fired and we missed it. 21599 // The other readyState events aren't as much of a problem if we double 21600 // them, so not going to go to as much trouble as loadstart to prevent 21601 // that unless we find reason to. 21602 21603 21604 var eventsToTrigger = ['loadstart']; // loadedmetadata: newly equal to HAVE_METADATA (1) or greater 21605 21606 eventsToTrigger.push('loadedmetadata'); // loadeddata: newly increased to HAVE_CURRENT_DATA (2) or greater 21607 21608 if (el.readyState >= 2) { 21609 eventsToTrigger.push('loadeddata'); 21610 } // canplay: newly increased to HAVE_FUTURE_DATA (3) or greater 21611 21612 21613 if (el.readyState >= 3) { 21614 eventsToTrigger.push('canplay'); 21615 } // canplaythrough: newly equal to HAVE_ENOUGH_DATA (4) 21616 21617 21618 if (el.readyState >= 4) { 21619 eventsToTrigger.push('canplaythrough'); 21620 } // We still need to give the player time to add event listeners 21621 21622 21623 this.ready(function () { 21624 eventsToTrigger.forEach(function (type) { 21625 this.trigger(type); 21626 }, this); 21627 }); 21628 } 21629 /** 21630 * Set current time for the `HTML5` tech. 21631 * 21632 * @param {number} seconds 21633 * Set the current time of the media to this. 21634 */ 21635 ; 21636 21637 _proto.setCurrentTime = function setCurrentTime(seconds) { 21638 try { 21639 this.el_.currentTime = seconds; 21640 } catch (e) { 21641 log(e, 'Video is not ready. (Video.js)'); // this.warning(VideoJS.warnings.videoNotReady); 21642 } 21643 } 21644 /** 21645 * Get the current duration of the HTML5 media element. 21646 * 21647 * @return {number} 21648 * The duration of the media or 0 if there is no duration. 21649 */ 21650 ; 21651 21652 _proto.duration = function duration() { 21653 var _this5 = this; 21654 21655 // Android Chrome will report duration as Infinity for VOD HLS until after 21656 // playback has started, which triggers the live display erroneously. 21657 // Return NaN if playback has not started and trigger a durationupdate once 21658 // the duration can be reliably known. 21659 if (this.el_.duration === Infinity && IS_ANDROID && IS_CHROME && this.el_.currentTime === 0) { 21660 // Wait for the first `timeupdate` with currentTime > 0 - there may be 21661 // several with 0 21662 var checkProgress = function checkProgress() { 21663 if (_this5.el_.currentTime > 0) { 21664 // Trigger durationchange for genuinely live video 21665 if (_this5.el_.duration === Infinity) { 21666 _this5.trigger('durationchange'); 21667 } 21668 21669 _this5.off('timeupdate', checkProgress); 21670 } 21671 }; 21672 21673 this.on('timeupdate', checkProgress); 21674 return NaN; 21675 } 21676 21677 return this.el_.duration || NaN; 21678 } 21679 /** 21680 * Get the current width of the HTML5 media element. 21681 * 21682 * @return {number} 21683 * The width of the HTML5 media element. 21684 */ 21685 ; 21686 21687 _proto.width = function width() { 21688 return this.el_.offsetWidth; 21689 } 21690 /** 21691 * Get the current height of the HTML5 media element. 21692 * 21693 * @return {number} 21694 * The height of the HTML5 media element. 21695 */ 21696 ; 21697 21698 _proto.height = function height() { 21699 return this.el_.offsetHeight; 21700 } 21701 /** 21702 * Proxy iOS `webkitbeginfullscreen` and `webkitendfullscreen` into 21703 * `fullscreenchange` event. 21704 * 21705 * @private 21706 * @fires fullscreenchange 21707 * @listens webkitendfullscreen 21708 * @listens webkitbeginfullscreen 21709 * @listens webkitbeginfullscreen 21710 */ 21711 ; 21712 21713 _proto.proxyWebkitFullscreen_ = function proxyWebkitFullscreen_() { 21714 var _this6 = this; 21715 21716 if (!('webkitDisplayingFullscreen' in this.el_)) { 21717 return; 21718 } 21719 21720 var endFn = function endFn() { 21721 this.trigger('fullscreenchange', { 21722 isFullscreen: false 21723 }); 21724 }; 21725 21726 var beginFn = function beginFn() { 21727 if ('webkitPresentationMode' in this.el_ && this.el_.webkitPresentationMode !== 'picture-in-picture') { 21728 this.one('webkitendfullscreen', endFn); 21729 this.trigger('fullscreenchange', { 21730 isFullscreen: true 21731 }); 21732 } 21733 }; 21734 21735 this.on('webkitbeginfullscreen', beginFn); 21736 this.on('dispose', function () { 21737 _this6.off('webkitbeginfullscreen', beginFn); 21738 21739 _this6.off('webkitendfullscreen', endFn); 21740 }); 21741 } 21742 /** 21743 * Check if fullscreen is supported on the current playback device. 21744 * 21745 * @return {boolean} 21746 * - True if fullscreen is supported. 21747 * - False if fullscreen is not supported. 21748 */ 21749 ; 21750 21751 _proto.supportsFullScreen = function supportsFullScreen() { 21752 if (typeof this.el_.webkitEnterFullScreen === 'function') { 21753 var userAgent = window$1.navigator && window$1.navigator.userAgent || ''; // Seems to be broken in Chromium/Chrome && Safari in Leopard 21754 21755 if (/Android/.test(userAgent) || !/Chrome|Mac OS X 10.5/.test(userAgent)) { 21756 return true; 21757 } 21758 } 21759 21760 return false;
vendor: 33,393 bytes, lines 21761-22754
21761 } 21762 /** 21763 * Request that the `HTML5` Tech enter fullscreen. 21764 */ 21765 ; 21766 21767 _proto.enterFullScreen = function enterFullScreen() { 21768 var video = this.el_; 21769 21770 if (video.paused && video.networkState <= video.HAVE_METADATA) { 21771 // attempt to prime the video element for programmatic access 21772 // this isn't necessary on the desktop but shouldn't hurt 21773 this.el_.play(); // playing and pausing synchronously during the transition to fullscreen 21774 // can get iOS ~6.1 devices into a play/pause loop 21775 21776 this.setTimeout(function () { 21777 video.pause(); 21778 video.webkitEnterFullScreen(); 21779 }, 0); 21780 } else { 21781 video.webkitEnterFullScreen(); 21782 } 21783 } 21784 /** 21785 * Request that the `HTML5` Tech exit fullscreen. 21786 */ 21787 ; 21788 21789 _proto.exitFullScreen = function exitFullScreen() { 21790 this.el_.webkitExitFullScreen(); 21791 } 21792 /** 21793 * Create a floating video window always on top of other windows so that users may 21794 * continue consuming media while they interact with other content sites, or 21795 * applications on their device. 21796 * 21797 * @see [Spec]{@link https://wicg.github.io/picture-in-picture} 21798 * 21799 * @return {Promise} 21800 * A promise with a Picture-in-Picture window. 21801 */ 21802 ; 21803 21804 _proto.requestPictureInPicture = function requestPictureInPicture() { 21805 return this.el_.requestPictureInPicture(); 21806 } 21807 /** 21808 * A getter/setter for the `Html5` Tech's source object. 21809 * > Note: Please use {@link Html5#setSource} 21810 * 21811 * @param {Tech~SourceObject} [src] 21812 * The source object you want to set on the `HTML5` techs element. 21813 * 21814 * @return {Tech~SourceObject|undefined} 21815 * - The current source object when a source is not passed in. 21816 * - undefined when setting 21817 * 21818 * @deprecated Since version 5. 21819 */ 21820 ; 21821 21822 _proto.src = function src(_src) { 21823 if (_src === undefined) { 21824 return this.el_.src; 21825 } // Setting src through `src` instead of `setSrc` will be deprecated 21826 21827 21828 this.setSrc(_src); 21829 } 21830 /** 21831 * Reset the tech by removing all sources and then calling 21832 * {@link Html5.resetMediaElement}. 21833 */ 21834 ; 21835 21836 _proto.reset = function reset() { 21837 Html5.resetMediaElement(this.el_); 21838 } 21839 /** 21840 * Get the current source on the `HTML5` Tech. Falls back to returning the source from 21841 * the HTML5 media element. 21842 * 21843 * @return {Tech~SourceObject} 21844 * The current source object from the HTML5 tech. With a fallback to the 21845 * elements source. 21846 */ 21847 ; 21848 21849 _proto.currentSrc = function currentSrc() { 21850 if (this.currentSource_) { 21851 return this.currentSource_.src; 21852 } 21853 21854 return this.el_.currentSrc; 21855 } 21856 /** 21857 * Set controls attribute for the HTML5 media Element. 21858 * 21859 * @param {string} val 21860 * Value to set the controls attribute to 21861 */ 21862 ; 21863 21864 _proto.setControls = function setControls(val) { 21865 this.el_.controls = !!val; 21866 } 21867 /** 21868 * Create and returns a remote {@link TextTrack} object. 21869 * 21870 * @param {string} kind 21871 * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata) 21872 * 21873 * @param {string} [label] 21874 * Label to identify the text track 21875 * 21876 * @param {string} [language] 21877 * Two letter language abbreviation 21878 * 21879 * @return {TextTrack} 21880 * The TextTrack that gets created. 21881 */ 21882 ; 21883 21884 _proto.addTextTrack = function addTextTrack(kind, label, language) { 21885 if (!this.featuresNativeTextTracks) { 21886 return _Tech.prototype.addTextTrack.call(this, kind, label, language); 21887 } 21888 21889 return this.el_.addTextTrack(kind, label, language); 21890 } 21891 /** 21892 * Creates either native TextTrack or an emulated TextTrack depending 21893 * on the value of `featuresNativeTextTracks` 21894 * 21895 * @param {Object} options 21896 * The object should contain the options to initialize the TextTrack with. 21897 * 21898 * @param {string} [options.kind] 21899 * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata). 21900 * 21901 * @param {string} [options.label] 21902 * Label to identify the text track 21903 * 21904 * @param {string} [options.language] 21905 * Two letter language abbreviation. 21906 * 21907 * @param {boolean} [options.default] 21908 * Default this track to on. 21909 * 21910 * @param {string} [options.id] 21911 * The internal id to assign this track. 21912 * 21913 * @param {string} [options.src] 21914 * A source url for the track. 21915 * 21916 * @return {HTMLTrackElement} 21917 * The track element that gets created. 21918 */ 21919 ; 21920 21921 _proto.createRemoteTextTrack = function createRemoteTextTrack(options) { 21922 if (!this.featuresNativeTextTracks) { 21923 return _Tech.prototype.createRemoteTextTrack.call(this, options); 21924 } 21925 21926 var htmlTrackElement = document.createElement('track'); 21927 21928 if (options.kind) { 21929 htmlTrackElement.kind = options.kind; 21930 } 21931 21932 if (options.label) { 21933 htmlTrackElement.label = options.label; 21934 } 21935 21936 if (options.language || options.srclang) { 21937 htmlTrackElement.srclang = options.language || options.srclang; 21938 } 21939 21940 if (options["default"]) { 21941 htmlTrackElement["default"] = options["default"]; 21942 } 21943 21944 if (options.id) { 21945 htmlTrackElement.id = options.id; 21946 } 21947 21948 if (options.src) { 21949 htmlTrackElement.src = options.src; 21950 } 21951 21952 return htmlTrackElement; 21953 } 21954 /** 21955 * Creates a remote text track object and returns an html track element. 21956 * 21957 * @param {Object} options The object should contain values for 21958 * kind, language, label, and src (location of the WebVTT file) 21959 * @param {boolean} [manualCleanup=true] if set to false, the TextTrack will be 21960 * automatically removed from the video element whenever the source changes 21961 * @return {HTMLTrackElement} An Html Track Element. 21962 * This can be an emulated {@link HTMLTrackElement} or a native one. 21963 * @deprecated The default value of the "manualCleanup" parameter will default 21964 * to "false" in upcoming versions of Video.js 21965 */ 21966 ; 21967 21968 _proto.addRemoteTextTrack = function addRemoteTextTrack(options, manualCleanup) { 21969 var htmlTrackElement = _Tech.prototype.addRemoteTextTrack.call(this, options, manualCleanup); 21970 21971 if (this.featuresNativeTextTracks) { 21972 this.el().appendChild(htmlTrackElement); 21973 } 21974 21975 return htmlTrackElement; 21976 } 21977 /** 21978 * Remove remote `TextTrack` from `TextTrackList` object 21979 * 21980 * @param {TextTrack} track 21981 * `TextTrack` object to remove 21982 */ 21983 ; 21984 21985 _proto.removeRemoteTextTrack = function removeRemoteTextTrack(track) { 21986 _Tech.prototype.removeRemoteTextTrack.call(this, track); 21987 21988 if (this.featuresNativeTextTracks) { 21989 var tracks = this.$$('track'); 21990 var i = tracks.length; 21991 21992 while (i--) { 21993 if (track === tracks[i] || track === tracks[i].track) { 21994 this.el().removeChild(tracks[i]); 21995 } 21996 } 21997 } 21998 } 21999 /** 22000 * Gets available media playback quality metrics as specified by the W3C's Media 22001 * Playback Quality API. 22002 * 22003 * @see [Spec]{@link https://wicg.github.io/media-playback-quality} 22004 * 22005 * @return {Object} 22006 * An object with supported media playback quality metrics 22007 */ 22008 ; 22009 22010 _proto.getVideoPlaybackQuality = function getVideoPlaybackQuality() { 22011 if (typeof this.el().getVideoPlaybackQuality === 'function') { 22012 return this.el().getVideoPlaybackQuality(); 22013 } 22014 22015 var videoPlaybackQuality = {}; 22016 22017 if (typeof this.el().webkitDroppedFrameCount !== 'undefined' && typeof this.el().webkitDecodedFrameCount !== 'undefined') { 22018 videoPlaybackQuality.droppedVideoFrames = this.el().webkitDroppedFrameCount; 22019 videoPlaybackQuality.totalVideoFrames = this.el().webkitDecodedFrameCount; 22020 } 22021 22022 if (window$1.performance && typeof window$1.performance.now === 'function') { 22023 videoPlaybackQuality.creationTime = window$1.performance.now(); 22024 } else if (window$1.performance && window$1.performance.timing && typeof window$1.performance.timing.navigationStart === 'number') { 22025 videoPlaybackQuality.creationTime = window$1.Date.now() - window$1.performance.timing.navigationStart; 22026 } 22027 22028 return videoPlaybackQuality; 22029 }; 22030 22031 return Html5; 22032 }(Tech); 22033 /* HTML5 Support Testing ---------------------------------------------------- */ 22034 22035 /** 22036 * Element for testing browser HTML5 media capabilities 22037 * 22038 * @type {Element} 22039 * @constant 22040 * @private 22041 */ 22042 22043 22044 defineLazyProperty(Html5, 'TEST_VID', function () { 22045 if (!isReal()) { 22046 return; 22047 } 22048 22049 var video = document.createElement('video'); 22050 var track = document.createElement('track'); 22051 track.kind = 'captions'; 22052 track.srclang = 'en'; 22053 track.label = 'English'; 22054 video.appendChild(track); 22055 return video; 22056 }); 22057 /** 22058 * Check if HTML5 media is supported by this browser/device. 22059 * 22060 * @return {boolean} 22061 * - True if HTML5 media is supported. 22062 * - False if HTML5 media is not supported. 22063 */ 22064 22065 Html5.isSupported = function () { 22066 // IE with no Media Player is a LIAR! (#984) 22067 try { 22068 Html5.TEST_VID.volume = 0.5; 22069 } catch (e) { 22070 return false; 22071 } 22072 22073 return !!(Html5.TEST_VID && Html5.TEST_VID.canPlayType); 22074 }; 22075 /** 22076 * Check if the tech can support the given type 22077 * 22078 * @param {string} type 22079 * The mimetype to check 22080 * @return {string} 'probably', 'maybe', or '' (empty string) 22081 */ 22082 22083 22084 Html5.canPlayType = function (type) { 22085 return Html5.TEST_VID.canPlayType(type); 22086 }; 22087 /** 22088 * Check if the tech can support the given source 22089 * 22090 * @param {Object} srcObj 22091 * The source object 22092 * @param {Object} options 22093 * The options passed to the tech 22094 * @return {string} 'probably', 'maybe', or '' (empty string) 22095 */ 22096 22097 22098 Html5.canPlaySource = function (srcObj, options) { 22099 return Html5.canPlayType(srcObj.type); 22100 }; 22101 /** 22102 * Check if the volume can be changed in this browser/device. 22103 * Volume cannot be changed in a lot of mobile devices. 22104 * Specifically, it can't be changed from 1 on iOS. 22105 * 22106 * @return {boolean} 22107 * - True if volume can be controlled 22108 * - False otherwise 22109 */ 22110 22111 22112 Html5.canControlVolume = function () { 22113 // IE will error if Windows Media Player not installed #3315 22114 try { 22115 var volume = Html5.TEST_VID.volume; 22116 Html5.TEST_VID.volume = volume / 2 + 0.1; 22117 return volume !== Html5.TEST_VID.volume; 22118 } catch (e) { 22119 return false; 22120 } 22121 }; 22122 /** 22123 * Check if the volume can be muted in this browser/device. 22124 * Some devices, e.g. iOS, don't allow changing volume 22125 * but permits muting/unmuting. 22126 * 22127 * @return {bolean} 22128 * - True if volume can be muted 22129 * - False otherwise 22130 */ 22131 22132 22133 Html5.canMuteVolume = function () { 22134 try { 22135 var muted = Html5.TEST_VID.muted; // in some versions of iOS muted property doesn't always 22136 // work, so we want to set both property and attribute 22137 22138 Html5.TEST_VID.muted = !muted; 22139 22140 if (Html5.TEST_VID.muted) { 22141 setAttribute(Html5.TEST_VID, 'muted', 'muted'); 22142 } else { 22143 removeAttribute(Html5.TEST_VID, 'muted', 'muted'); 22144 } 22145 22146 return muted !== Html5.TEST_VID.muted; 22147 } catch (e) { 22148 return false; 22149 } 22150 }; 22151 /** 22152 * Check if the playback rate can be changed in this browser/device. 22153 * 22154 * @return {boolean} 22155 * - True if playback rate can be controlled 22156 * - False otherwise 22157 */ 22158 22159 22160 Html5.canControlPlaybackRate = function () { 22161 // Playback rate API is implemented in Android Chrome, but doesn't do anything 22162 // https://github.com/videojs/video.js/issues/3180 22163 if (IS_ANDROID && IS_CHROME && CHROME_VERSION < 58) { 22164 return false; 22165 } // IE will error if Windows Media Player not installed #3315 22166 22167 22168 try { 22169 var playbackRate = Html5.TEST_VID.playbackRate; 22170 Html5.TEST_VID.playbackRate = playbackRate / 2 + 0.1; 22171 return playbackRate !== Html5.TEST_VID.playbackRate; 22172 } catch (e) { 22173 return false; 22174 } 22175 }; 22176 /** 22177 * Check if we can override a video/audio elements attributes, with 22178 * Object.defineProperty. 22179 * 22180 * @return {boolean} 22181 * - True if builtin attributes can be overridden 22182 * - False otherwise 22183 */ 22184 22185 22186 Html5.canOverrideAttributes = function () { 22187 // if we cannot overwrite the src/innerHTML property, there is no support 22188 // iOS 7 safari for instance cannot do this. 22189 try { 22190 var noop = function noop() {}; 22191 22192 Object.defineProperty(document.createElement('video'), 'src', { 22193 get: noop, 22194 set: noop 22195 }); 22196 Object.defineProperty(document.createElement('audio'), 'src', { 22197 get: noop, 22198 set: noop 22199 }); 22200 Object.defineProperty(document.createElement('video'), 'innerHTML', { 22201 get: noop, 22202 set: noop 22203 }); 22204 Object.defineProperty(document.createElement('audio'), 'innerHTML', { 22205 get: noop, 22206 set: noop 22207 }); 22208 } catch (e) { 22209 return false; 22210 } 22211 22212 return true; 22213 }; 22214 /** 22215 * Check to see if native `TextTrack`s are supported by this browser/device. 22216 * 22217 * @return {boolean} 22218 * - True if native `TextTrack`s are supported. 22219 * - False otherwise 22220 */ 22221 22222 22223 Html5.supportsNativeTextTracks = function () { 22224 return IS_ANY_SAFARI || IS_IOS && IS_CHROME; 22225 }; 22226 /** 22227 * Check to see if native `VideoTrack`s are supported by this browser/device 22228 * 22229 * @return {boolean} 22230 * - True if native `VideoTrack`s are supported. 22231 * - False otherwise 22232 */ 22233 22234 22235 Html5.supportsNativeVideoTracks = function () { 22236 return !!(Html5.TEST_VID && Html5.TEST_VID.videoTracks); 22237 }; 22238 /** 22239 * Check to see if native `AudioTrack`s are supported by this browser/device 22240 * 22241 * @return {boolean} 22242 * - True if native `AudioTrack`s are supported. 22243 * - False otherwise 22244 */ 22245 22246 22247 Html5.supportsNativeAudioTracks = function () { 22248 return !!(Html5.TEST_VID && Html5.TEST_VID.audioTracks); 22249 }; 22250 /** 22251 * An array of events available on the Html5 tech. 22252 * 22253 * @private 22254 * @type {Array} 22255 */ 22256 22257 22258 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']; 22259 /** 22260 * Boolean indicating whether the `Tech` supports volume control. 22261 * 22262 * @type {boolean} 22263 * @default {@link Html5.canControlVolume} 22264 */ 22265 22266 /** 22267 * Boolean indicating whether the `Tech` supports muting volume. 22268 * 22269 * @type {bolean} 22270 * @default {@link Html5.canMuteVolume} 22271 */ 22272 22273 /** 22274 * Boolean indicating whether the `Tech` supports changing the speed at which the media 22275 * plays. Examples: 22276 * - Set player to play 2x (twice) as fast 22277 * - Set player to play 0.5x (half) as fast 22278 * 22279 * @type {boolean} 22280 * @default {@link Html5.canControlPlaybackRate} 22281 */ 22282 22283 /** 22284 * Boolean indicating whether the `Tech` supports the `sourceset` event. 22285 * 22286 * @type {boolean} 22287 * @default 22288 */ 22289 22290 /** 22291 * Boolean indicating whether the `HTML5` tech currently supports native `TextTrack`s. 22292 * 22293 * @type {boolean} 22294 * @default {@link Html5.supportsNativeTextTracks} 22295 */ 22296 22297 /** 22298 * Boolean indicating whether the `HTML5` tech currently supports native `VideoTrack`s. 22299 * 22300 * @type {boolean} 22301 * @default {@link Html5.supportsNativeVideoTracks} 22302 */ 22303 22304 /** 22305 * Boolean indicating whether the `HTML5` tech currently supports native `AudioTrack`s. 22306 * 22307 * @type {boolean} 22308 * @default {@link Html5.supportsNativeAudioTracks} 22309 */ 22310 22311 [['featuresVolumeControl', 'canControlVolume'], ['featuresMuteControl', 'canMuteVolume'], ['featuresPlaybackRate', 'canControlPlaybackRate'], ['featuresSourceset', 'canOverrideAttributes'], ['featuresNativeTextTracks', 'supportsNativeTextTracks'], ['featuresNativeVideoTracks', 'supportsNativeVideoTracks'], ['featuresNativeAudioTracks', 'supportsNativeAudioTracks']].forEach(function (_ref) { 22312 var key = _ref[0], 22313 fn = _ref[1]; 22314 defineLazyProperty(Html5.prototype, key, function () { 22315 return Html5[fn](); 22316 }, true); 22317 }); 22318 /** 22319 * Boolean indicating whether the `HTML5` tech currently supports the media element 22320 * moving in the DOM. iOS breaks if you move the media element, so this is set this to 22321 * false there. Everywhere else this should be true. 22322 * 22323 * @type {boolean} 22324 * @default 22325 */ 22326 22327 Html5.prototype.movingMediaElementInDOM = !IS_IOS; // TODO: Previous comment: No longer appears to be used. Can probably be removed. 22328 // Is this true? 22329 22330 /** 22331 * Boolean indicating whether the `HTML5` tech currently supports automatic media resize 22332 * when going into fullscreen. 22333 * 22334 * @type {boolean} 22335 * @default 22336 */ 22337 22338 Html5.prototype.featuresFullscreenResize = true; 22339 /** 22340 * Boolean indicating whether the `HTML5` tech currently supports the progress event. 22341 * If this is false, manual `progress` events will be triggered instead. 22342 * 22343 * @type {boolean} 22344 * @default 22345 */ 22346 22347 Html5.prototype.featuresProgressEvents = true; 22348 /** 22349 * Boolean indicating whether the `HTML5` tech currently supports the timeupdate event. 22350 * If this is false, manual `timeupdate` events will be triggered instead. 22351 * 22352 * @default 22353 */ 22354 22355 Html5.prototype.featuresTimeupdateEvents = true; // HTML5 Feature detection and Device Fixes --------------------------------- // 22356 22357 var canPlayType; 22358 22359 Html5.patchCanPlayType = function () { 22360 // Android 4.0 and above can play HLS to some extent but it reports being unable to do so 22361 // Firefox and Chrome report correctly 22362 if (ANDROID_VERSION >= 4.0 && !IS_FIREFOX && !IS_CHROME) { 22363 canPlayType = Html5.TEST_VID && Html5.TEST_VID.constructor.prototype.canPlayType; 22364 22365 Html5.TEST_VID.constructor.prototype.canPlayType = function (type) { 22366 var mpegurlRE = /^application\/(?:x-|vnd\.apple\.)mpegurl/i; 22367 22368 if (type && mpegurlRE.test(type)) { 22369 return 'maybe'; 22370 } 22371 22372 return canPlayType.call(this, type); 22373 }; 22374 } 22375 }; 22376 22377 Html5.unpatchCanPlayType = function () { 22378 var r = Html5.TEST_VID.constructor.prototype.canPlayType; 22379 22380 if (canPlayType) { 22381 Html5.TEST_VID.constructor.prototype.canPlayType = canPlayType; 22382 } 22383 22384 return r; 22385 }; // by default, patch the media element 22386 22387 22388 Html5.patchCanPlayType(); 22389 22390 Html5.disposeMediaElement = function (el) { 22391 if (!el) { 22392 return; 22393 } 22394 22395 if (el.parentNode) { 22396 el.parentNode.removeChild(el); 22397 } // remove any child track or source nodes to prevent their loading 22398 22399 22400 while (el.hasChildNodes()) { 22401 el.removeChild(el.firstChild); 22402 } // remove any src reference. not setting `src=''` because that causes a warning 22403 // in firefox 22404 22405 22406 el.removeAttribute('src'); // force the media element to update its loading state by calling load() 22407 // however IE on Windows 7N has a bug that throws an error so need a try/catch (#793) 22408 22409 if (typeof el.load === 'function') { 22410 // wrapping in an iife so it's not deoptimized (#1060#discussion_r10324473) 22411 (function () { 22412 try { 22413 el.load(); 22414 } catch (e) {// not supported 22415 } 22416 })(); 22417 } 22418 }; 22419 22420 Html5.resetMediaElement = function (el) { 22421 if (!el) { 22422 return; 22423 } 22424 22425 var sources = el.querySelectorAll('source'); 22426 var i = sources.length; 22427 22428 while (i--) { 22429 el.removeChild(sources[i]); 22430 } // remove any src reference. 22431 // not setting `src=''` because that throws an error 22432 22433 22434 el.removeAttribute('src'); 22435 22436 if (typeof el.load === 'function') { 22437 // wrapping in an iife so it's not deoptimized (#1060#discussion_r10324473) 22438 (function () { 22439 try { 22440 el.load(); 22441 } catch (e) {// satisfy linter 22442 } 22443 })(); 22444 } 22445 }; 22446 /* Native HTML5 element property wrapping ----------------------------------- */ 22447 // Wrap native boolean attributes with getters that check both property and attribute 22448 // The list is as followed: 22449 // muted, defaultMuted, autoplay, controls, loop, playsinline 22450 22451 22452 [ 22453 /** 22454 * Get the value of `muted` from the media element. `muted` indicates 22455 * that the volume for the media should be set to silent. This does not actually change 22456 * the `volume` attribute. 22457 * 22458 * @method Html5#muted 22459 * @return {boolean} 22460 * - True if the value of `volume` should be ignored and the audio set to silent. 22461 * - False if the value of `volume` should be used. 22462 * 22463 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-muted} 22464 */ 22465 'muted', 22466 /** 22467 * Get the value of `defaultMuted` from the media element. `defaultMuted` indicates 22468 * whether the media should start muted or not. Only changes the default state of the 22469 * media. `muted` and `defaultMuted` can have different values. {@link Html5#muted} indicates the 22470 * current state. 22471 * 22472 * @method Html5#defaultMuted 22473 * @return {boolean} 22474 * - The value of `defaultMuted` from the media element. 22475 * - True indicates that the media should start muted. 22476 * - False indicates that the media should not start muted 22477 * 22478 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultmuted} 22479 */ 22480 'defaultMuted', 22481 /** 22482 * Get the value of `autoplay` from the media element. `autoplay` indicates 22483 * that the media should start to play as soon as the page is ready. 22484 * 22485 * @method Html5#autoplay 22486 * @return {boolean} 22487 * - The value of `autoplay` from the media element. 22488 * - True indicates that the media should start as soon as the page loads. 22489 * - False indicates that the media should not start as soon as the page loads. 22490 * 22491 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-autoplay} 22492 */ 22493 'autoplay', 22494 /** 22495 * Get the value of `controls` from the media element. `controls` indicates 22496 * whether the native media controls should be shown or hidden. 22497 * 22498 * @method Html5#controls 22499 * @return {boolean} 22500 * - The value of `controls` from the media element. 22501 * - True indicates that native controls should be showing. 22502 * - False indicates that native controls should be hidden. 22503 * 22504 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-controls} 22505 */ 22506 'controls', 22507 /** 22508 * Get the value of `loop` from the media element. `loop` indicates 22509 * that the media should return to the start of the media and continue playing once 22510 * it reaches the end. 22511 * 22512 * @method Html5#loop 22513 * @return {boolean} 22514 * - The value of `loop` from the media element. 22515 * - True indicates that playback should seek back to start once 22516 * the end of a media is reached. 22517 * - False indicates that playback should not loop back to the start when the 22518 * end of the media is reached. 22519 * 22520 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-loop} 22521 */ 22522 'loop', 22523 /** 22524 * Get the value of `playsinline` from the media element. `playsinline` indicates 22525 * to the browser that non-fullscreen playback is preferred when fullscreen 22526 * playback is the native default, such as in iOS Safari. 22527 * 22528 * @method Html5#playsinline 22529 * @return {boolean} 22530 * - The value of `playsinline` from the media element. 22531 * - True indicates that the media should play inline. 22532 * - False indicates that the media should not play inline. 22533 * 22534 * @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline} 22535 */ 22536 'playsinline'].forEach(function (prop) { 22537 Html5.prototype[prop] = function () { 22538 return this.el_[prop] || this.el_.hasAttribute(prop); 22539 }; 22540 }); // Wrap native boolean attributes with setters that set both property and attribute 22541 // The list is as followed: 22542 // setMuted, setDefaultMuted, setAutoplay, setLoop, setPlaysinline 22543 // setControls is special-cased above 22544 22545 [ 22546 /** 22547 * Set the value of `muted` on the media element. `muted` indicates that the current 22548 * audio level should be silent. 22549 * 22550 * @method Html5#setMuted 22551 * @param {boolean} muted 22552 * - True if the audio should be set to silent 22553 * - False otherwise 22554 * 22555 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-muted} 22556 */ 22557 'muted', 22558 /** 22559 * Set the value of `defaultMuted` on the media element. `defaultMuted` indicates that the current 22560 * audio level should be silent, but will only effect the muted level on intial playback.. 22561 * 22562 * @method Html5.prototype.setDefaultMuted 22563 * @param {boolean} defaultMuted 22564 * - True if the audio should be set to silent 22565 * - False otherwise 22566 * 22567 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultmuted} 22568 */ 22569 'defaultMuted', 22570 /** 22571 * Set the value of `autoplay` on the media element. `autoplay` indicates 22572 * that the media should start to play as soon as the page is ready. 22573 * 22574 * @method Html5#setAutoplay 22575 * @param {boolean} autoplay 22576 * - True indicates that the media should start as soon as the page loads. 22577 * - False indicates that the media should not start as soon as the page loads. 22578 * 22579 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-autoplay} 22580 */ 22581 'autoplay', 22582 /** 22583 * Set the value of `loop` on the media element. `loop` indicates 22584 * that the media should return to the start of the media and continue playing once 22585 * it reaches the end. 22586 * 22587 * @method Html5#setLoop 22588 * @param {boolean} loop 22589 * - True indicates that playback should seek back to start once 22590 * the end of a media is reached. 22591 * - False indicates that playback should not loop back to the start when the 22592 * end of the media is reached. 22593 * 22594 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-loop} 22595 */ 22596 'loop', 22597 /** 22598 * Set the value of `playsinline` from the media element. `playsinline` indicates 22599 * to the browser that non-fullscreen playback is preferred when fullscreen 22600 * playback is the native default, such as in iOS Safari. 22601 * 22602 * @method Html5#setPlaysinline 22603 * @param {boolean} playsinline 22604 * - True indicates that the media should play inline. 22605 * - False indicates that the media should not play inline. 22606 * 22607 * @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline} 22608 */ 22609 'playsinline'].forEach(function (prop) { 22610 Html5.prototype['set' + toTitleCase(prop)] = function (v) { 22611 this.el_[prop] = v; 22612 22613 if (v) { 22614 this.el_.setAttribute(prop, prop); 22615 } else { 22616 this.el_.removeAttribute(prop); 22617 } 22618 }; 22619 }); // Wrap native properties with a getter 22620 // The list is as followed 22621 // paused, currentTime, buffered, volume, poster, preload, error, seeking 22622 // seekable, ended, playbackRate, defaultPlaybackRate, played, networkState 22623 // readyState, videoWidth, videoHeight 22624 22625 [ 22626 /** 22627 * Get the value of `paused` from the media element. `paused` indicates whether the media element 22628 * is currently paused or not. 22629 * 22630 * @method Html5#paused 22631 * @return {boolean} 22632 * The value of `paused` from the media element. 22633 * 22634 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-paused} 22635 */ 22636 'paused', 22637 /** 22638 * Get the value of `currentTime` from the media element. `currentTime` indicates 22639 * the current second that the media is at in playback. 22640 * 22641 * @method Html5#currentTime 22642 * @return {number} 22643 * The value of `currentTime` from the media element. 22644 * 22645 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-currenttime} 22646 */ 22647 'currentTime', 22648 /** 22649 * Get the value of `buffered` from the media element. `buffered` is a `TimeRange` 22650 * object that represents the parts of the media that are already downloaded and 22651 * available for playback. 22652 * 22653 * @method Html5#buffered 22654 * @return {TimeRange} 22655 * The value of `buffered` from the media element. 22656 * 22657 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-buffered} 22658 */ 22659 'buffered', 22660 /** 22661 * Get the value of `volume` from the media element. `volume` indicates 22662 * the current playback volume of audio for a media. `volume` will be a value from 0 22663 * (silent) to 1 (loudest and default). 22664 * 22665 * @method Html5#volume 22666 * @return {number} 22667 * The value of `volume` from the media element. Value will be between 0-1. 22668 * 22669 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-a-volume} 22670 */ 22671 'volume', 22672 /** 22673 * Get the value of `poster` from the media element. `poster` indicates 22674 * that the url of an image file that can/will be shown when no media data is available. 22675 * 22676 * @method Html5#poster 22677 * @return {string} 22678 * The value of `poster` from the media element. Value will be a url to an 22679 * image. 22680 * 22681 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-video-poster} 22682 */ 22683 'poster', 22684 /** 22685 * Get the value of `preload` from the media element. `preload` indicates 22686 * what should download before the media is interacted with. It can have the following 22687 * values: 22688 * - none: nothing should be downloaded 22689 * - metadata: poster and the first few frames of the media may be downloaded to get 22690 * media dimensions and other metadata 22691 * - auto: allow the media and metadata for the media to be downloaded before 22692 * interaction 22693 * 22694 * @method Html5#preload 22695 * @return {string} 22696 * The value of `preload` from the media element. Will be 'none', 'metadata', 22697 * or 'auto'. 22698 * 22699 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-preload} 22700 */ 22701 'preload', 22702 /** 22703 * Get the value of the `error` from the media element. `error` indicates any 22704 * MediaError that may have occurred during playback. If error returns null there is no 22705 * current error. 22706 * 22707 * @method Html5#error 22708 * @return {MediaError|null} 22709 * The value of `error` from the media element. Will be `MediaError` if there 22710 * is a current error and null otherwise. 22711 * 22712 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-error} 22713 */ 22714 'error', 22715 /** 22716 * Get the value of `seeking` from the media element. `seeking` indicates whether the 22717 * media is currently seeking to a new position or not. 22718 * 22719 * @method Html5#seeking 22720 * @return {boolean} 22721 * - The value of `seeking` from the media element. 22722 * - True indicates that the media is currently seeking to a new position. 22723 * - False indicates that the media is not seeking to a new position at this time. 22724 * 22725 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-seeking} 22726 */ 22727 'seeking', 22728 /** 22729 * Get the value of `seekable` from the media element. `seekable` returns a 22730 * `TimeRange` object indicating ranges of time that can currently be `seeked` to. 22731 * 22732 * @method Html5#seekable 22733 * @return {TimeRange} 22734 * The value of `seekable` from the media element. A `TimeRange` object 22735 * indicating the current ranges of time that can be seeked to. 22736 * 22737 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-seekable} 22738 */ 22739 'seekable', 22740 /** 22741 * Get the value of `ended` from the media element. `ended` indicates whether 22742 * the media has reached the end or not. 22743 * 22744 * @method Html5#ended 22745 * @return {boolean} 22746 * - The value of `ended` from the media element. 22747 * - True indicates that the media has ended. 22748 * - False indicates that the media has not ended. 22749 * 22750 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-ended} 22751 */ 22752 'ended', 22753 /** 22754 * Get the value of `playbackRate` from the media element. `playbackRate` indicates
22755 * the rate at which the media is currently playing back. Examples: 22756 * - if playbackRate is set to 2, media will play twice as fast. 22757 * - if playbackRate is set to 0.5, media will play half as fast. 22758 * 22759 * @method Html5#playbackRate 22760 * @return {number} 22761 * The value of `playbackRate` from the media element. A number indicating 22762 * the current playback speed of the media, where 1 is normal speed. 22763 * 22764 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate} 22765 */ 22766 'playbackRate', 22767 /** 22768 * Get the value of `defaultPlaybackRate` from the media element. `defaultPlaybackRate` indicates 22769 * the rate at which the media is currently playing back. This value will not indicate the current 22770 * `playbackRate` after playback has started, use {@link Html5#playbackRate} for that. 22771 * 22772 * Examples: 22773 * - if defaultPlaybackRate is set to 2, media will play twice as fast. 22774 * - if defaultPlaybackRate is set to 0.5, media will play half as fast. 22775 * 22776 * @method Html5.prototype.defaultPlaybackRate 22777 * @return {number} 22778 * The value of `defaultPlaybackRate` from the media element. A number indicating 22779 * the current playback speed of the media, where 1 is normal speed. 22780 * 22781 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate} 22782 */ 22783 'defaultPlaybackRate', 22784 /** 22785 * Get the value of `played` from the media element. `played` returns a `TimeRange` 22786 * object representing points in the media timeline that have been played. 22787 * 22788 * @method Html5#played 22789 * @return {TimeRange} 22790 * The value of `played` from the media element. A `TimeRange` object indicating 22791 * the ranges of time that have been played. 22792 * 22793 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-played} 22794 */ 22795 'played', 22796 /** 22797 * Get the value of `networkState` from the media element. `networkState` indicates 22798 * the current network state. It returns an enumeration from the following list: 22799 * - 0: NETWORK_EMPTY 22800 * - 1: NETWORK_IDLE 22801 * - 2: NETWORK_LOADING 22802 * - 3: NETWORK_NO_SOURCE 22803 * 22804 * @method Html5#networkState 22805 * @return {number} 22806 * The value of `networkState` from the media element. This will be a number 22807 * from the list in the description. 22808 * 22809 * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-networkstate} 22810 */ 22811 'networkState', 22812 /** 22813 * Get the value of `readyState` from the media element. `readyState` indicates 22814 * the current state of the media element. It returns an enumeration from the 22815 * following list: 22816 * - 0: HAVE_NOTHING 22817 * - 1: HAVE_METADATA 22818 * - 2: HAVE_CURRENT_DATA 22819 * - 3: HAVE_FUTURE_DATA 22820 * - 4: HAVE_ENOUGH_DATA 22821 * 22822 * @method Html5#readyState 22823 * @return {number} 22824 * The value of `readyState` from the media element. This will be a number 22825 * from the list in the description. 22826 * 22827 * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#ready-states} 22828 */ 22829 'readyState', 22830 /** 22831 * Get the value of `videoWidth` from the video element. `videoWidth` indicates 22832 * the current width of the video in css pixels. 22833 * 22834 * @method Html5#videoWidth 22835 * @return {number} 22836 * The value of `videoWidth` from the video element. This will be a number 22837 * in css pixels. 22838 * 22839 * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-video-videowidth} 22840 */ 22841 'videoWidth', 22842 /** 22843 * Get the value of `videoHeight` from the video element. `videoHeight` indicates 22844 * the current height of the video in css pixels. 22845 * 22846 * @method Html5#videoHeight 22847 * @return {number} 22848 * The value of `videoHeight` from the video element. This will be a number 22849 * in css pixels. 22850 * 22851 * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-video-videowidth} 22852 */ 22853 'videoHeight'].forEach(function (prop) { 22854 Html5.prototype[prop] = function () { 22855 return this.el_[prop]; 22856 }; 22857 }); // Wrap native properties with a setter in this format: 22858 // set + toTitleCase(name) 22859 // The list is as follows: 22860 // setVolume, setSrc, setPoster, setPreload, setPlaybackRate, setDefaultPlaybackRate 22861 22862 [ 22863 /** 22864 * Set the value of `volume` on the media element. `volume` indicates the current 22865 * audio level as a percentage in decimal form. This means that 1 is 100%, 0.5 is 50%, and 22866 * so on. 22867 * 22868 * @method Html5#setVolume 22869 * @param {number} percentAsDecimal 22870 * The volume percent as a decimal. Valid range is from 0-1. 22871 * 22872 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-a-volume} 22873 */ 22874 'volume', 22875 /** 22876 * Set the value of `src` on the media element. `src` indicates the current 22877 * {@link Tech~SourceObject} for the media. 22878 * 22879 * @method Html5#setSrc 22880 * @param {Tech~SourceObject} src 22881 * The source object to set as the current source. 22882 * 22883 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-src} 22884 */ 22885 'src', 22886 /** 22887 * Set the value of `poster` on the media element. `poster` is the url to 22888 * an image file that can/will be shown when no media data is available. 22889 * 22890 * @method Html5#setPoster 22891 * @param {string} poster 22892 * The url to an image that should be used as the `poster` for the media 22893 * element. 22894 * 22895 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-poster} 22896 */ 22897 'poster', 22898 /** 22899 * Set the value of `preload` on the media element. `preload` indicates 22900 * what should download before the media is interacted with. It can have the following 22901 * values: 22902 * - none: nothing should be downloaded 22903 * - metadata: poster and the first few frames of the media may be downloaded to get 22904 * media dimensions and other metadata 22905 * - auto: allow the media and metadata for the media to be downloaded before 22906 * interaction 22907 * 22908 * @method Html5#setPreload 22909 * @param {string} preload 22910 * The value of `preload` to set on the media element. Must be 'none', 'metadata', 22911 * or 'auto'. 22912 * 22913 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-preload} 22914 */ 22915 'preload', 22916 /** 22917 * Set the value of `playbackRate` on the media element. `playbackRate` indicates 22918 * the rate at which the media should play back. Examples: 22919 * - if playbackRate is set to 2, media will play twice as fast. 22920 * - if playbackRate is set to 0.5, media will play half as fast. 22921 * 22922 * @method Html5#setPlaybackRate 22923 * @return {number} 22924 * The value of `playbackRate` from the media element. A number indicating 22925 * the current playback speed of the media, where 1 is normal speed. 22926 * 22927 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate} 22928 */ 22929 'playbackRate', 22930 /** 22931 * Set the value of `defaultPlaybackRate` on the media element. `defaultPlaybackRate` indicates 22932 * the rate at which the media should play back upon initial startup. Changing this value 22933 * after a video has started will do nothing. Instead you should used {@link Html5#setPlaybackRate}. 22934 * 22935 * Example Values: 22936 * - if playbackRate is set to 2, media will play twice as fast. 22937 * - if playbackRate is set to 0.5, media will play half as fast. 22938 * 22939 * @method Html5.prototype.setDefaultPlaybackRate 22940 * @return {number} 22941 * The value of `defaultPlaybackRate` from the media element. A number indicating 22942 * the current playback speed of the media, where 1 is normal speed. 22943 * 22944 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultplaybackrate} 22945 */ 22946 'defaultPlaybackRate'].forEach(function (prop) { 22947 Html5.prototype['set' + toTitleCase(prop)] = function (v) { 22948 this.el_[prop] = v; 22949 }; 22950 }); // wrap native functions with a function 22951 // The list is as follows: 22952 // pause, load, play 22953 22954 [ 22955 /** 22956 * A wrapper around the media elements `pause` function. This will call the `HTML5` 22957 * media elements `pause` function. 22958 * 22959 * @method Html5#pause 22960 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-pause} 22961 */ 22962 'pause', 22963 /** 22964 * A wrapper around the media elements `load` function. This will call the `HTML5`s 22965 * media element `load` function. 22966 * 22967 * @method Html5#load 22968 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-load} 22969 */ 22970 'load', 22971 /** 22972 * A wrapper around the media elements `play` function. This will call the `HTML5`s 22973 * media element `play` function. 22974 * 22975 * @method Html5#play 22976 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-play} 22977 */ 22978 'play'].forEach(function (prop) { 22979 Html5.prototype[prop] = function () { 22980 return this.el_[prop](); 22981 }; 22982 }); 22983 Tech.withSourceHandlers(Html5); 22984 /** 22985 * Native source handler for Html5, simply passes the source to the media element. 22986 * 22987 * @property {Tech~SourceObject} source 22988 * The source object 22989 * 22990 * @property {Html5} tech 22991 * The instance of the HTML5 tech. 22992 */ 22993 22994 Html5.nativeSourceHandler = {}; 22995 /** 22996 * Check if the media element can play the given mime type. 22997 * 22998 * @param {string} type 22999 * The mimetype to check 23000 * 23001 * @return {string} 23002 * 'probably', 'maybe', or '' (empty string) 23003 */ 23004 23005 Html5.nativeSourceHandler.canPlayType = function (type) { 23006 // IE without MediaPlayer throws an error (#519) 23007 try {
23008 return Html5.TEST_VID.canPlayType(type); 23009 } catch (e) { 23010 return ''; 23011 } 23012 }; 23013 /** 23014 * Check if the media element can handle a source natively. 23015 * 23016 * @param {Tech~SourceObject} source 23017 * The source object 23018 * 23019 * @param {Object} [options] 23020 * Options to be passed to the tech. 23021 * 23022 * @return {string} 23023 * 'probably', 'maybe', or '' (empty string). 23024 */ 23025 23026 23027 Html5.nativeSourceHandler.canHandleSource = function (source, options) { 23028 // If a type was provided we should rely on that 23029 if (source.type) { 23030 return Html5.nativeSourceHandler.canPlayType(source.type); // If no type, fall back to checking 'video/[EXTENSION]' 23031 } else if (source.src) { 23032 var ext = getFileExtension(source.src); 23033 return Html5.nativeSourceHandler.canPlayType("video/" + ext); 23034 } 23035 23036 return ''; 23037 }; 23038 /** 23039 * Pass the source to the native media element. 23040 * 23041 * @param {Tech~SourceObject} source 23042 * The source object 23043 * 23044 * @param {Html5} tech 23045 * The instance of the Html5 tech 23046 * 23047 * @param {Object} [options] 23048 * The options to pass to the source 23049 */ 23050 23051 23052 Html5.nativeSourceHandler.handleSource = function (source, tech, options) { 23053 tech.setSrc(source.src); 23054 }; 23055 /** 23056 * A noop for the native dispose function, as cleanup is not needed. 23057 */ 23058 23059 23060 Html5.nativeSourceHandler.dispose = function () {}; // Register the native source handler 23061 23062 23063 Html5.registerSourceHandler(Html5.nativeSourceHandler); 23064 Tech.registerTech('Html5', Html5); 23065 23066 // on the player when they happen 23067 23068 var TECH_EVENTS_RETRIGGER = [ 23069 /** 23070 * Fired while the user agent is downloading media data. 23071 * 23072 * @event Player#progress 23073 * @type {EventTarget~Event} 23074 */ 23075 23076 /** 23077 * Retrigger the `progress` event that was triggered by the {@link Tech}. 23078 * 23079 * @private 23080 * @method Player#handleTechProgress_ 23081 * @fires Player#progress 23082 * @listens Tech#progress 23083 */ 23084 'progress', 23085 /** 23086 * Fires when the loading of an audio/video is aborted. 23087 * 23088 * @event Player#abort 23089 * @type {EventTarget~Event} 23090 */ 23091 23092 /** 23093 * Retrigger the `abort` event that was triggered by the {@link Tech}. 23094 * 23095 * @private 23096 * @method Player#handleTechAbort_ 23097 * @fires Player#abort 23098 * @listens Tech#abort 23099 */ 23100 'abort', 23101 /** 23102 * Fires when the browser is intentionally not getting media data. 23103 * 23104 * @event Player#suspend 23105 * @type {EventTarget~Event} 23106 */ 23107 23108 /** 23109 * Retrigger the `suspend` event that was triggered by the {@link Tech}. 23110 * 23111 * @private 23112 * @method Player#handleTechSuspend_ 23113 * @fires Player#suspend 23114 * @listens Tech#suspend 23115 */ 23116 'suspend', 23117 /** 23118 * Fires when the current playlist is empty. 23119 * 23120 * @event Player#emptied 23121 * @type {EventTarget~Event} 23122 */ 23123 23124 /** 23125 * Retrigger the `emptied` event that was triggered by the {@link Tech}. 23126 * 23127 * @private 23128 * @method Player#handleTechEmptied_ 23129 * @fires Player#emptied 23130 * @listens Tech#emptied 23131 */ 23132 'emptied', 23133 /** 23134 * Fires when the browser is trying to get media data, but data is not available. 23135 * 23136 * @event Player#stalled 23137 * @type {EventTarget~Event} 23138 */ 23139 23140 /** 23141 * Retrigger the `stalled` event that was triggered by the {@link Tech}. 23142 * 23143 * @private 23144 * @method Player#handleTechStalled_ 23145 * @fires Player#stalled 23146 * @listens Tech#stalled 23147 */ 23148 'stalled', 23149 /** 23150 * Fires when the browser has loaded meta data for the audio/video. 23151 * 23152 * @event Player#loadedmetadata 23153 * @type {EventTarget~Event} 23154 */ 23155 23156 /** 23157 * Retrigger the `stalled` event that was triggered by the {@link Tech}. 23158 * 23159 * @private 23160 * @method Player#handleTechLoadedmetadata_ 23161 * @fires Player#loadedmetadata 23162 * @listens Tech#loadedmetadata 23163 */ 23164 'loadedmetadata', 23165 /** 23166 * Fires when the browser has loaded the current frame of the audio/video. 23167 * 23168 * @event Player#loadeddata 23169 * @type {event} 23170 */ 23171 23172 /** 23173 * Retrigger the `loadeddata` event that was triggered by the {@link Tech}. 23174 * 23175 * @private 23176 * @method Player#handleTechLoaddeddata_ 23177 * @fires Player#loadeddata 23178 * @listens Tech#loadeddata 23179 */ 23180 'loadeddata', 23181 /** 23182 * Fires when the current playback position has changed. 23183 * 23184 * @event Player#timeupdate 23185 * @type {event} 23186 */ 23187 23188 /** 23189 * Retrigger the `timeupdate` event that was triggered by the {@link Tech}. 23190 * 23191 * @private 23192 * @method Player#handleTechTimeUpdate_ 23193 * @fires Player#timeupdate 23194 * @listens Tech#timeupdate 23195 */ 23196 'timeupdate', 23197 /** 23198 * Fires when the video's intrinsic dimensions change 23199 * 23200 * @event Player#resize 23201 * @type {event} 23202 */ 23203 23204 /** 23205 * Retrigger the `resize` event that was triggered by the {@link Tech}. 23206 * 23207 * @private 23208 * @method Player#handleTechResize_ 23209 * @fires Player#resize 23210 * @listens Tech#resize 23211 */ 23212 'resize', 23213 /** 23214 * Fires when the volume has been changed 23215 * 23216 * @event Player#volumechange 23217 * @type {event} 23218 */ 23219 23220 /** 23221 * Retrigger the `volumechange` event that was triggered by the {@link Tech}. 23222 * 23223 * @private 23224 * @method Player#handleTechVolumechange_ 23225 * @fires Player#volumechange 23226 * @listens Tech#volumechange 23227 */ 23228 'volumechange', 23229 /** 23230 * Fires when the text track has been changed 23231 * 23232 * @event Player#texttrackchange 23233 * @type {event} 23234 */ 23235 23236 /** 23237 * Retrigger the `texttrackchange` event that was triggered by the {@link Tech}. 23238 * 23239 * @private 23240 * @method Player#handleTechTexttrackchange_ 23241 * @fires Player#texttrackchange 23242 * @listens Tech#texttrackchange 23243 */ 23244 'texttrackchange']; // events to queue when playback rate is zero 23245 // this is a hash for the sole purpose of mapping non-camel-cased event names 23246 // to camel-cased function names 23247 23248 var TECH_EVENTS_QUEUE = {
vendor: 23,312 bytes, lines 23249-23945
23249 canplay: 'CanPlay', 23250 canplaythrough: 'CanPlayThrough', 23251 playing: 'Playing', 23252 seeked: 'Seeked' 23253 }; 23254 var BREAKPOINT_ORDER = ['tiny', 'xsmall', 'small', 'medium', 'large', 'xlarge', 'huge']; 23255 var BREAKPOINT_CLASSES = {}; // grep: vjs-layout-tiny 23256 // grep: vjs-layout-x-small 23257 // grep: vjs-layout-small 23258 // grep: vjs-layout-medium 23259 // grep: vjs-layout-large 23260 // grep: vjs-layout-x-large 23261 // grep: vjs-layout-huge 23262 23263 BREAKPOINT_ORDER.forEach(function (k) { 23264 var v = k.charAt(0) === 'x' ? "x-" + k.substring(1) : k; 23265 BREAKPOINT_CLASSES[k] = "vjs-layout-" + v; 23266 }); 23267 var DEFAULT_BREAKPOINTS = { 23268 tiny: 210, 23269 xsmall: 320, 23270 small: 425, 23271 medium: 768, 23272 large: 1440, 23273 xlarge: 2560, 23274 huge: Infinity 23275 }; 23276 /** 23277 * An instance of the `Player` class is created when any of the Video.js setup methods 23278 * are used to initialize a video. 23279 * 23280 * After an instance has been created it can be accessed globally in two ways: 23281 * 1. By calling `videojs('example_video_1');` 23282 * 2. By using it directly via `videojs.players.example_video_1;` 23283 * 23284 * @extends Component 23285 */ 23286 23287 var Player = 23288 /*#__PURE__*/ 23289 function (_Component) { 23290 inheritsLoose(Player, _Component); 23291 23292 /** 23293 * Create an instance of this class. 23294 * 23295 * @param {Element} tag 23296 * The original video DOM element used for configuring options. 23297 * 23298 * @param {Object} [options] 23299 * Object of option names and values. 23300 * 23301 * @param {Component~ReadyCallback} [ready] 23302 * Ready callback function. 23303 */ 23304 function Player(tag, options, ready) { 23305 var _this; 23306 23307 // Make sure tag ID exists 23308 tag.id = tag.id || options.id || "vjs_video_" + newGUID(); // Set Options 23309 // The options argument overrides options set in the video tag 23310 // which overrides globally set options. 23311 // This latter part coincides with the load order 23312 // (tag must exist before Player) 23313 23314 options = assign(Player.getTagSettings(tag), options); // Delay the initialization of children because we need to set up 23315 // player properties first, and can't use `this` before `super()` 23316 23317 options.initChildren = false; // Same with creating the element 23318 23319 options.createEl = false; // don't auto mixin the evented mixin 23320 23321 options.evented = false; // we don't want the player to report touch activity on itself 23322 // see enableTouchActivity in Component 23323 23324 options.reportTouchActivity = false; // If language is not set, get the closest lang attribute 23325 23326 if (!options.language) { 23327 if (typeof tag.closest === 'function') { 23328 var closest = tag.closest('[lang]'); 23329 23330 if (closest && closest.getAttribute) { 23331 options.language = closest.getAttribute('lang'); 23332 } 23333 } else { 23334 var element = tag; 23335 23336 while (element && element.nodeType === 1) { 23337 if (getAttributes(element).hasOwnProperty('lang')) { 23338 options.language = element.getAttribute('lang'); 23339 break; 23340 } 23341 23342 element = element.parentNode; 23343 } 23344 } 23345 } // Run base component initializing with new options 23346 23347 23348 _this = _Component.call(this, null, options, ready) || this; // Create bound methods for document listeners. 23349 23350 _this.boundDocumentFullscreenChange_ = bind(assertThisInitialized(_this), _this.documentFullscreenChange_); 23351 _this.boundFullWindowOnEscKey_ = bind(assertThisInitialized(_this), _this.fullWindowOnEscKey); // create logger 23352 23353 _this.log = createLogger$1(_this.id_); // Hold our own reference to fullscreen api so it can be mocked in tests 23354 23355 _this.fsApi_ = FullscreenApi; // Tracks when a tech changes the poster 23356 23357 _this.isPosterFromTech_ = false; // Holds callback info that gets queued when playback rate is zero 23358 // and a seek is happening 23359 23360 _this.queuedCallbacks_ = []; // Turn off API access because we're loading a new tech that might load asynchronously 23361 23362 _this.isReady_ = false; // Init state hasStarted_ 23363 23364 _this.hasStarted_ = false; // Init state userActive_ 23365 23366 _this.userActive_ = false; // if the global option object was accidentally blown away by 23367 // someone, bail early with an informative error 23368 23369 if (!_this.options_ || !_this.options_.techOrder || !_this.options_.techOrder.length) { 23370 throw new Error('No techOrder specified. Did you overwrite ' + 'videojs.options instead of just changing the ' + 'properties you want to override?'); 23371 } // Store the original tag used to set options 23372 23373 23374 _this.tag = tag; // Store the tag attributes used to restore html5 element 23375 23376 _this.tagAttributes = tag && getAttributes(tag); // Update current language 23377 23378 _this.language(_this.options_.language); // Update Supported Languages 23379 23380 23381 if (options.languages) { 23382 // Normalise player option languages to lowercase 23383 var languagesToLower = {}; 23384 Object.getOwnPropertyNames(options.languages).forEach(function (name) { 23385 languagesToLower[name.toLowerCase()] = options.languages[name]; 23386 }); 23387 _this.languages_ = languagesToLower; 23388 } else { 23389 _this.languages_ = Player.prototype.options_.languages; 23390 } 23391 23392 _this.resetCache_(); // Set poster 23393 23394 23395 _this.poster_ = options.poster || ''; // Set controls 23396 23397 _this.controls_ = !!options.controls; // Original tag settings stored in options 23398 // now remove immediately so native controls don't flash. 23399 // May be turned back on by HTML5 tech if nativeControlsForTouch is true 23400 23401 tag.controls = false; 23402 tag.removeAttribute('controls'); 23403 _this.changingSrc_ = false; 23404 _this.playCallbacks_ = []; 23405 _this.playTerminatedQueue_ = []; // the attribute overrides the option 23406 23407 if (tag.hasAttribute('autoplay')) { 23408 _this.autoplay(true); 23409 } else { 23410 // otherwise use the setter to validate and 23411 // set the correct value. 23412 _this.autoplay(_this.options_.autoplay); 23413 } // check plugins 23414 23415 23416 if (options.plugins) { 23417 Object.keys(options.plugins).forEach(function (name) { 23418 if (typeof _this[name] !== 'function') { 23419 throw new Error("plugin \"" + name + "\" does not exist"); 23420 } 23421 }); 23422 } 23423 /* 23424 * Store the internal state of scrubbing 23425 * 23426 * @private 23427 * @return {Boolean} True if the user is scrubbing 23428 */ 23429 23430 23431 _this.scrubbing_ = false; 23432 _this.el_ = _this.createEl(); // Make this an evented object and use `el_` as its event bus. 23433 23434 evented(assertThisInitialized(_this), { 23435 eventBusKey: 'el_' 23436 }); 23437 23438 if (_this.fluid_) { 23439 _this.on('playerreset', _this.updateStyleEl_); 23440 } // We also want to pass the original player options to each component and plugin 23441 // as well so they don't need to reach back into the player for options later. 23442 // We also need to do another copy of this.options_ so we don't end up with 23443 // an infinite loop. 23444 23445 23446 var playerOptionsCopy = mergeOptions(_this.options_); // Load plugins 23447 23448 if (options.plugins) { 23449 Object.keys(options.plugins).forEach(function (name) { 23450 _this[name](options.plugins[name]); 23451 }); 23452 } 23453 23454 _this.options_.playerOptions = playerOptionsCopy; 23455 _this.middleware_ = []; 23456 23457 _this.initChildren(); // Set isAudio based on whether or not an audio tag was used 23458 23459 23460 _this.isAudio(tag.nodeName.toLowerCase() === 'audio'); // Update controls className. Can't do this when the controls are initially 23461 // set because the element doesn't exist yet. 23462 23463 23464 if (_this.controls()) { 23465 _this.addClass('vjs-controls-enabled'); 23466 } else { 23467 _this.addClass('vjs-controls-disabled'); 23468 } // Set ARIA label and region role depending on player type 23469 23470 23471 _this.el_.setAttribute('role', 'region'); 23472 23473 if (_this.isAudio()) { 23474 _this.el_.setAttribute('aria-label', _this.localize('Audio Player')); 23475 } else { 23476 _this.el_.setAttribute('aria-label', _this.localize('Video Player')); 23477 } 23478 23479 if (_this.isAudio()) { 23480 _this.addClass('vjs-audio'); 23481 } 23482 23483 if (_this.flexNotSupported_()) { 23484 _this.addClass('vjs-no-flex'); 23485 } // TODO: Make this smarter. Toggle user state between touching/mousing 23486 // using events, since devices can have both touch and mouse events. 23487 // TODO: Make this check be performed again when the window switches between monitors 23488 // (See https://github.com/videojs/video.js/issues/5683) 23489 23490 23491 if (TOUCH_ENABLED) { 23492 _this.addClass('vjs-touch-enabled'); 23493 } // iOS Safari has broken hover handling 23494 23495 23496 if (!IS_IOS) { 23497 _this.addClass('vjs-workinghover'); 23498 } // Make player easily findable by ID 23499 23500 23501 Player.players[_this.id_] = assertThisInitialized(_this); // Add a major version class to aid css in plugins 23502 23503 var majorVersion = version.split('.')[0]; 23504 23505 _this.addClass("vjs-v" + majorVersion); // When the player is first initialized, trigger activity so components 23506 // like the control bar show themselves if needed 23507 23508 23509 _this.userActive(true); 23510 23511 _this.reportUserActivity(); 23512 23513 _this.one('play', _this.listenForUserActivity_); 23514 23515 _this.on('stageclick', _this.handleStageClick_); 23516 23517 _this.on('keydown', _this.handleKeyDown); 23518 23519 _this.breakpoints(_this.options_.breakpoints); 23520 23521 _this.responsive(_this.options_.responsive); 23522 23523 return _this; 23524 } 23525 /** 23526 * Destroys the video player and does any necessary cleanup. 23527 * 23528 * This is especially helpful if you are dynamically adding and removing videos 23529 * to/from the DOM. 23530 * 23531 * @fires Player#dispose 23532 */ 23533 23534 23535 var _proto = Player.prototype; 23536 23537 _proto.dispose = function dispose() { 23538 var _this2 = this; 23539 23540 /** 23541 * Called when the player is being disposed of. 23542 * 23543 * @event Player#dispose 23544 * @type {EventTarget~Event} 23545 */ 23546 this.trigger('dispose'); // prevent dispose from being called twice 23547 23548 this.off('dispose'); // Make sure all player-specific document listeners are unbound. This is 23549 23550 off(document, this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_); 23551 off(document, 'keydown', this.boundFullWindowOnEscKey_); 23552 23553 if (this.styleEl_ && this.styleEl_.parentNode) { 23554 this.styleEl_.parentNode.removeChild(this.styleEl_); 23555 this.styleEl_ = null; 23556 } // Kill reference to this player 23557 23558 23559 Player.players[this.id_] = null; 23560 23561 if (this.tag && this.tag.player) { 23562 this.tag.player = null; 23563 } 23564 23565 if (this.el_ && this.el_.player) { 23566 this.el_.player = null; 23567 } 23568 23569 if (this.tech_) { 23570 this.tech_.dispose(); 23571 this.isPosterFromTech_ = false; 23572 this.poster_ = ''; 23573 } 23574 23575 if (this.playerElIngest_) { 23576 this.playerElIngest_ = null; 23577 } 23578 23579 if (this.tag) { 23580 this.tag = null; 23581 } 23582 23583 clearCacheForPlayer(this); // remove all event handlers for track lists 23584 // all tracks and track listeners are removed on 23585 // tech dispose 23586 23587 ALL.names.forEach(function (name) { 23588 var props = ALL[name]; 23589 23590 var list = _this2[props.getterName](); // if it is not a native list 23591 // we have to manually remove event listeners 23592 23593 23594 if (list && list.off) { 23595 list.off(); 23596 } 23597 }); // the actual .el_ is removed here 23598 23599 _Component.prototype.dispose.call(this); 23600 } 23601 /** 23602 * Create the `Player`'s DOM element. 23603 * 23604 * @return {Element} 23605 * The DOM element that gets created. 23606 */ 23607 ; 23608 23609 _proto.createEl = function createEl() { 23610 var tag = this.tag; 23611 var el; 23612 var playerElIngest = this.playerElIngest_ = tag.parentNode && tag.parentNode.hasAttribute && tag.parentNode.hasAttribute('data-vjs-player'); 23613 var divEmbed = this.tag.tagName.toLowerCase() === 'video-js'; 23614 23615 if (playerElIngest) { 23616 el = this.el_ = tag.parentNode; 23617 } else if (!divEmbed) { 23618 el = this.el_ = _Component.prototype.createEl.call(this, 'div'); 23619 } // Copy over all the attributes from the tag, including ID and class 23620 // ID will now reference player box, not the video tag 23621 23622 23623 var attrs = getAttributes(tag); 23624 23625 if (divEmbed) { 23626 el = this.el_ = tag; 23627 tag = this.tag = document.createElement('video'); 23628 23629 while (el.children.length) { 23630 tag.appendChild(el.firstChild); 23631 } 23632 23633 if (!hasClass(el, 'video-js')) { 23634 addClass(el, 'video-js'); 23635 } 23636 23637 el.appendChild(tag); 23638 playerElIngest = this.playerElIngest_ = el; // move properties over from our custom `video-js` element 23639 // to our new `video` element. This will move things like 23640 // `src` or `controls` that were set via js before the player 23641 // was initialized. 23642 23643 Object.keys(el).forEach(function (k) { 23644 try { 23645 tag[k] = el[k]; 23646 } catch (e) {// we got a a property like outerHTML which we can't actually copy, ignore it 23647 } 23648 }); 23649 } // set tabindex to -1 to remove the video element from the focus order 23650 23651 23652 tag.setAttribute('tabindex', '-1'); 23653 attrs.tabindex = '-1'; // Workaround for #4583 (JAWS+IE doesn't announce BPB or play button), and 23654 // for the same issue with Chrome (on Windows) with JAWS. 23655 // See https://github.com/FreedomScientific/VFO-standards-support/issues/78 23656 // Note that we can't detect if JAWS is being used, but this ARIA attribute 23657 // doesn't change behavior of IE11 or Chrome if JAWS is not being used 23658 23659 if (IE_VERSION || IS_CHROME && IS_WINDOWS) { 23660 tag.setAttribute('role', 'application'); 23661 attrs.role = 'application'; 23662 } // Remove width/height attrs from tag so CSS can make it 100% width/height 23663 23664 23665 tag.removeAttribute('width'); 23666 tag.removeAttribute('height'); 23667 23668 if ('width' in attrs) { 23669 delete attrs.width; 23670 } 23671 23672 if ('height' in attrs) { 23673 delete attrs.height; 23674 } 23675 23676 Object.getOwnPropertyNames(attrs).forEach(function (attr) { 23677 // don't copy over the class attribute to the player element when we're in a div embed 23678 // the class is already set up properly in the divEmbed case 23679 // and we want to make sure that the `video-js` class doesn't get lost 23680 if (!(divEmbed && attr === 'class')) { 23681 el.setAttribute(attr, attrs[attr]); 23682 } 23683 23684 if (divEmbed) { 23685 tag.setAttribute(attr, attrs[attr]); 23686 } 23687 }); // Update tag id/class for use as HTML5 playback tech 23688 // Might think we should do this after embedding in container so .vjs-tech class 23689 // doesn't flash 100% width/height, but class only applies with .video-js parent 23690 23691 tag.playerId = tag.id; 23692 tag.id += '_html5_api'; 23693 tag.className = 'vjs-tech'; // Make player findable on elements 23694 23695 tag.player = el.player = this; // Default state of video is paused 23696 23697 this.addClass('vjs-paused'); // Add a style element in the player that we'll use to set the width/height 23698 // of the player in a way that's still overrideable by CSS, just like the 23699 // video element 23700 23701 if (window$1.VIDEOJS_NO_DYNAMIC_STYLE !== true) { 23702 this.styleEl_ = createStyleElement('vjs-styles-dimensions'); 23703 var defaultsStyleEl = $('.vjs-styles-defaults'); 23704 var head = $('head'); 23705 head.insertBefore(this.styleEl_, defaultsStyleEl ? defaultsStyleEl.nextSibling : head.firstChild); 23706 } 23707 23708 this.fill_ = false; 23709 this.fluid_ = false; // Pass in the width/height/aspectRatio options which will update the style el 23710 23711 this.width(this.options_.width); 23712 this.height(this.options_.height); 23713 this.fill(this.options_.fill); 23714 this.fluid(this.options_.fluid); 23715 this.aspectRatio(this.options_.aspectRatio); // Hide any links within the video/audio tag, 23716 // because IE doesn't hide them completely from screen readers. 23717 23718 var links = tag.getElementsByTagName('a'); 23719 23720 for (var i = 0; i < links.length; i++) { 23721 var linkEl = links.item(i); 23722 addClass(linkEl, 'vjs-hidden'); 23723 linkEl.setAttribute('hidden', 'hidden'); 23724 } // insertElFirst seems to cause the networkState to flicker from 3 to 2, so 23725 // keep track of the original for later so we can know if the source originally failed 23726 23727 23728 tag.initNetworkState_ = tag.networkState; // Wrap video tag in div (el/box) container 23729 23730 if (tag.parentNode && !playerElIngest) { 23731 tag.parentNode.insertBefore(el, tag); 23732 } // insert the tag as the first child of the player element 23733 // then manually add it to the children array so that this.addChild 23734 // will work properly for other components 23735 // 23736 // Breaks iPhone, fixed in HTML5 setup. 23737 23738 23739 prependTo(tag, el); 23740 this.children_.unshift(tag); // Set lang attr on player to ensure CSS :lang() in consistent with player 23741 // if it's been set to something different to the doc 23742 23743 this.el_.setAttribute('lang', this.language_); 23744 this.el_ = el; 23745 return el; 23746 } 23747 /** 23748 * A getter/setter for the `Player`'s width. Returns the player's configured value. 23749 * To get the current width use `currentWidth()`. 23750 * 23751 * @param {number} [value] 23752 * The value to set the `Player`'s width to. 23753 * 23754 * @return {number} 23755 * The current width of the `Player` when getting. 23756 */ 23757 ; 23758 23759 _proto.width = function width(value) { 23760 return this.dimension('width', value); 23761 } 23762 /** 23763 * A getter/setter for the `Player`'s height. Returns the player's configured value. 23764 * To get the current height use `currentheight()`. 23765 * 23766 * @param {number} [value] 23767 * The value to set the `Player`'s heigth to. 23768 * 23769 * @return {number} 23770 * The current height of the `Player` when getting. 23771 */ 23772 ; 23773 23774 _proto.height = function height(value) { 23775 return this.dimension('height', value); 23776 } 23777 /** 23778 * A getter/setter for the `Player`'s width & height. 23779 * 23780 * @param {string} dimension 23781 * This string can be: 23782 * - 'width' 23783 * - 'height' 23784 * 23785 * @param {number} [value] 23786 * Value for dimension specified in the first argument. 23787 * 23788 * @return {number} 23789 * The dimension arguments value when getting (width/height). 23790 */ 23791 ; 23792 23793 _proto.dimension = function dimension(_dimension, value) { 23794 var privDimension = _dimension + '_'; 23795 23796 if (value === undefined) { 23797 return this[privDimension] || 0; 23798 } 23799 23800 if (value === '' || value === 'auto') { 23801 // If an empty string is given, reset the dimension to be automatic 23802 this[privDimension] = undefined; 23803 this.updateStyleEl_(); 23804 return; 23805 } 23806 23807 var parsedVal = parseFloat(value); 23808 23809 if (isNaN(parsedVal)) { 23810 log.error("Improper value \"" + value + "\" supplied for for " + _dimension); 23811 return; 23812 } 23813 23814 this[privDimension] = parsedVal; 23815 this.updateStyleEl_(); 23816 } 23817 /** 23818 * A getter/setter/toggler for the vjs-fluid `className` on the `Player`. 23819 * 23820 * Turning this on will turn off fill mode. 23821 * 23822 * @param {boolean} [bool] 23823 * - A value of true adds the class. 23824 * - A value of false removes the class. 23825 * - No value will be a getter. 23826 * 23827 * @return {boolean|undefined} 23828 * - The value of fluid when getting. 23829 * - `undefined` when setting. 23830 */ 23831 ; 23832 23833 _proto.fluid = function fluid(bool) { 23834 if (bool === undefined) { 23835 return !!this.fluid_; 23836 } 23837 23838 this.fluid_ = !!bool; 23839 23840 if (isEvented(this)) { 23841 this.off('playerreset', this.updateStyleEl_); 23842 } 23843 23844 if (bool) { 23845 this.addClass('vjs-fluid'); 23846 this.fill(false); 23847 addEventedCallback(function () { 23848 this.on('playerreset', this.updateStyleEl_); 23849 }); 23850 } else { 23851 this.removeClass('vjs-fluid'); 23852 } 23853 23854 this.updateStyleEl_(); 23855 } 23856 /** 23857 * A getter/setter/toggler for the vjs-fill `className` on the `Player`. 23858 * 23859 * Turning this on will turn off fluid mode. 23860 * 23861 * @param {boolean} [bool] 23862 * - A value of true adds the class. 23863 * - A value of false removes the class. 23864 * - No value will be a getter. 23865 * 23866 * @return {boolean|undefined} 23867 * - The value of fluid when getting. 23868 * - `undefined` when setting. 23869 */ 23870 ; 23871 23872 _proto.fill = function fill(bool) { 23873 if (bool === undefined) { 23874 return !!this.fill_; 23875 } 23876 23877 this.fill_ = !!bool; 23878 23879 if (bool) { 23880 this.addClass('vjs-fill'); 23881 this.fluid(false); 23882 } else { 23883 this.removeClass('vjs-fill'); 23884 } 23885 } 23886 /** 23887 * Get/Set the aspect ratio 23888 * 23889 * @param {string} [ratio] 23890 * Aspect ratio for player 23891 * 23892 * @return {string|undefined} 23893 * returns the current aspect ratio when getting 23894 */ 23895 23896 /** 23897 * A getter/setter for the `Player`'s aspect ratio. 23898 * 23899 * @param {string} [ratio] 23900 * The value to set the `Player's aspect ratio to. 23901 * 23902 * @return {string|undefined} 23903 * - The current aspect ratio of the `Player` when getting. 23904 * - undefined when setting 23905 */ 23906 ; 23907 23908 _proto.aspectRatio = function aspectRatio(ratio) { 23909 if (ratio === undefined) { 23910 return this.aspectRatio_; 23911 } // Check for width:height format 23912 23913 23914 if (!/^\d+\:\d+$/.test(ratio)) { 23915 throw new Error('Improper value supplied for aspect ratio. The format should be width:height, for example 16:9.'); 23916 } 23917 23918 this.aspectRatio_ = ratio; // We're assuming if you set an aspect ratio you want fluid mode, 23919 // because in fixed mode you could calculate width and height yourself. 23920 23921 this.fluid(true); 23922 this.updateStyleEl_(); 23923 } 23924 /** 23925 * Update styles of the `Player` element (height, width and aspect ratio). 23926 * 23927 * @private 23928 * @listens Tech#loadedmetadata 23929 */ 23930 ; 23931 23932 _proto.updateStyleEl_ = function updateStyleEl_() { 23933 if (window$1.VIDEOJS_NO_DYNAMIC_STYLE === true) { 23934 var _width = typeof this.width_ === 'number' ? this.width_ : this.options_.width; 23935 23936 var _height = typeof this.height_ === 'number' ? this.height_ : this.options_.height; 23937 23938 var techEl = this.tech_ && this.tech_.el(); 23939 23940 if (techEl) { 23941 if (_width >= 0) { 23942 techEl.width = _width; 23943 } 23944 23945 if (_height >= 0) {
vendor: 8,710 bytes, lines 23946-24155
23946 techEl.height = _height; 23947 } 23948 } 23949 23950 return; 23951 } 23952 23953 var width; 23954 var height; 23955 var aspectRatio; 23956 var idClass; // The aspect ratio is either used directly or to calculate width and height. 23957 23958 if (this.aspectRatio_ !== undefined && this.aspectRatio_ !== 'auto') { 23959 // Use any aspectRatio that's been specifically set 23960 aspectRatio = this.aspectRatio_; 23961 } else if (this.videoWidth() > 0) { 23962 // Otherwise try to get the aspect ratio from the video metadata 23963 aspectRatio = this.videoWidth() + ':' + this.videoHeight(); 23964 } else { 23965 // Or use a default. The video element's is 2:1, but 16:9 is more common. 23966 aspectRatio = '16:9'; 23967 } // Get the ratio as a decimal we can use to calculate dimensions 23968 23969 23970 var ratioParts = aspectRatio.split(':'); 23971 var ratioMultiplier = ratioParts[1] / ratioParts[0]; 23972 23973 if (this.width_ !== undefined) { 23974 // Use any width that's been specifically set 23975 width = this.width_; 23976 } else if (this.height_ !== undefined) { 23977 // Or calulate the width from the aspect ratio if a height has been set 23978 width = this.height_ / ratioMultiplier; 23979 } else { 23980 // Or use the video's metadata, or use the video el's default of 300 23981 width = this.videoWidth() || 300; 23982 } 23983 23984 if (this.height_ !== undefined) { 23985 // Use any height that's been specifically set 23986 height = this.height_; 23987 } else { 23988 // Otherwise calculate the height from the ratio and the width 23989 height = width * ratioMultiplier; 23990 } // Ensure the CSS class is valid by starting with an alpha character 23991 23992 23993 if (/^[^a-zA-Z]/.test(this.id())) { 23994 idClass = 'dimensions-' + this.id(); 23995 } else { 23996 idClass = this.id() + '-dimensions'; 23997 } // Ensure the right class is still on the player for the style element 23998 23999 24000 this.addClass(idClass); 24001 setTextContent(this.styleEl_, "\n ." + idClass + " {\n width: " + width + "px;\n height: " + height + "px;\n }\n\n ." + idClass + ".vjs-fluid {\n padding-top: " + ratioMultiplier * 100 + "%;\n }\n "); 24002 } 24003 /** 24004 * Load/Create an instance of playback {@link Tech} including element 24005 * and API methods. Then append the `Tech` element in `Player` as a child. 24006 * 24007 * @param {string} techName 24008 * name of the playback technology 24009 * 24010 * @param {string} source 24011 * video source 24012 * 24013 * @private 24014 */ 24015 ; 24016 24017 _proto.loadTech_ = function loadTech_(techName, source) { 24018 var _this3 = this; 24019 24020 // Pause and remove current playback technology 24021 if (this.tech_) { 24022 this.unloadTech_(); 24023 } 24024 24025 var titleTechName = toTitleCase(techName); 24026 var camelTechName = techName.charAt(0).toLowerCase() + techName.slice(1); // get rid of the HTML5 video tag as soon as we are using another tech 24027 24028 if (titleTechName !== 'Html5' && this.tag) { 24029 Tech.getTech('Html5').disposeMediaElement(this.tag); 24030 this.tag.player = null; 24031 this.tag = null; 24032 } 24033 24034 this.techName_ = titleTechName; // Turn off API access because we're loading a new tech that might load asynchronously 24035 24036 this.isReady_ = false; // if autoplay is a string we pass false to the tech 24037 // because the player is going to handle autoplay on `loadstart` 24038 24039 var autoplay = typeof this.autoplay() === 'string' ? false : this.autoplay(); // Grab tech-specific options from player options and add source and parent element to use. 24040 24041 var techOptions = { 24042 source: source, 24043 autoplay: autoplay, 24044 'nativeControlsForTouch': this.options_.nativeControlsForTouch, 24045 'playerId': this.id(), 24046 'techId': this.id() + "_" + camelTechName + "_api", 24047 'playsinline': this.options_.playsinline, 24048 'preload': this.options_.preload, 24049 'loop': this.options_.loop, 24050 'muted': this.options_.muted, 24051 'poster': this.poster(), 24052 'language': this.language(), 24053 'playerElIngest': this.playerElIngest_ || false, 24054 'vtt.js': this.options_['vtt.js'], 24055 'canOverridePoster': !!this.options_.techCanOverridePoster, 24056 'enableSourceset': this.options_.enableSourceset, 24057 'Promise': this.options_.Promise 24058 }; 24059 ALL.names.forEach(function (name) { 24060 var props = ALL[name]; 24061 techOptions[props.getterName] = _this3[props.privateName]; 24062 }); 24063 assign(techOptions, this.options_[titleTechName]); 24064 assign(techOptions, this.options_[camelTechName]); 24065 assign(techOptions, this.options_[techName.toLowerCase()]); 24066 24067 if (this.tag) { 24068 techOptions.tag = this.tag; 24069 } 24070 24071 if (source && source.src === this.cache_.src && this.cache_.currentTime > 0) { 24072 techOptions.startTime = this.cache_.currentTime; 24073 } // Initialize tech instance 24074 24075 24076 var TechClass = Tech.getTech(techName); 24077 24078 if (!TechClass) { 24079 throw new Error("No Tech named '" + titleTechName + "' exists! '" + titleTechName + "' should be registered using videojs.registerTech()'"); 24080 } 24081 24082 this.tech_ = new TechClass(techOptions); // player.triggerReady is always async, so don't need this to be async 24083 24084 this.tech_.ready(bind(this, this.handleTechReady_), true); 24085 textTrackConverter.jsonToTextTracks(this.textTracksJson_ || [], this.tech_); // Listen to all HTML5-defined events and trigger them on the player 24086 24087 TECH_EVENTS_RETRIGGER.forEach(function (event) { 24088 _this3.on(_this3.tech_, event, _this3["handleTech" + toTitleCase(event) + "_"]); 24089 }); 24090 Object.keys(TECH_EVENTS_QUEUE).forEach(function (event) { 24091 _this3.on(_this3.tech_, event, function (eventObj) { 24092 if (_this3.tech_.playbackRate() === 0 && _this3.tech_.seeking()) { 24093 _this3.queuedCallbacks_.push({ 24094 callback: _this3["handleTech" + TECH_EVENTS_QUEUE[event] + "_"].bind(_this3), 24095 event: eventObj 24096 }); 24097 24098 return; 24099 } 24100 24101 _this3["handleTech" + TECH_EVENTS_QUEUE[event] + "_"](eventObj); 24102 }); 24103 }); 24104 this.on(this.tech_, 'loadstart', this.handleTechLoadStart_); 24105 this.on(this.tech_, 'sourceset', this.handleTechSourceset_); 24106 this.on(this.tech_, 'waiting', this.handleTechWaiting_); 24107 this.on(this.tech_, 'ended', this.handleTechEnded_); 24108 this.on(this.tech_, 'seeking', this.handleTechSeeking_); 24109 this.on(this.tech_, 'play', this.handleTechPlay_); 24110 this.on(this.tech_, 'firstplay', this.handleTechFirstPlay_); 24111 this.on(this.tech_, 'pause', this.handleTechPause_); 24112 this.on(this.tech_, 'durationchange', this.handleTechDurationChange_); 24113 this.on(this.tech_, 'fullscreenchange', this.handleTechFullscreenChange_); 24114 this.on(this.tech_, 'enterpictureinpicture', this.handleTechEnterPictureInPicture_); 24115 this.on(this.tech_, 'leavepictureinpicture', this.handleTechLeavePictureInPicture_); 24116 this.on(this.tech_, 'error', this.handleTechError_); 24117 this.on(this.tech_, 'loadedmetadata', this.updateStyleEl_); 24118 this.on(this.tech_, 'posterchange', this.handleTechPosterChange_); 24119 this.on(this.tech_, 'textdata', this.handleTechTextData_); 24120 this.on(this.tech_, 'ratechange', this.handleTechRateChange_); 24121 this.usingNativeControls(this.techGet_('controls')); 24122 24123 if (this.controls() && !this.usingNativeControls()) { 24124 this.addTechControlsListeners_(); 24125 } // Add the tech element in the DOM if it was not already there 24126 // Make sure to not insert the original video element if using Html5 24127 24128 24129 if (this.tech_.el().parentNode !== this.el() && (titleTechName !== 'Html5' || !this.tag)) { 24130 prependTo(this.tech_.el(), this.el()); 24131 } // Get rid of the original video tag reference after the first tech is loaded 24132 24133 24134 if (this.tag) { 24135 this.tag.player = null; 24136 this.tag = null; 24137 } 24138 } 24139 /** 24140 * Unload and dispose of the current playback {@link Tech}. 24141 * 24142 * @private 24143 */ 24144 ; 24145 24146 _proto.unloadTech_ = function unloadTech_() { 24147 var _this4 = this; 24148 24149 // Save the current text tracks so that we can reuse the same text tracks with the next tech 24150 ALL.names.forEach(function (name) { 24151 var props = ALL[name]; 24152 _this4[props.privateName] = _this4[props.getterName](); 24153 }); 24154 this.textTracksJson_ = textTrackConverter.textTracksToJson(this.tech_); 24155 this.isReady_ = false;
vendor: 10,042 bytes, lines 24156-24442
24156 this.tech_.dispose(); 24157 this.tech_ = false; 24158 24159 if (this.isPosterFromTech_) { 24160 this.poster_ = ''; 24161 this.trigger('posterchange'); 24162 } 24163 24164 this.isPosterFromTech_ = false; 24165 } 24166 /** 24167 * Return a reference to the current {@link Tech}. 24168 * It will print a warning by default about the danger of using the tech directly 24169 * but any argument that is passed in will silence the warning. 24170 * 24171 * @param {*} [safety] 24172 * Anything passed in to silence the warning 24173 * 24174 * @return {Tech} 24175 * The Tech 24176 */ 24177 ; 24178 24179 _proto.tech = function tech(safety) { 24180 if (safety === undefined) { 24181 log.warn('Using the tech directly can be dangerous. I hope you know what you\'re doing.\n' + 'See https://github.com/videojs/video.js/issues/2617 for more info.\n'); 24182 } 24183 24184 return this.tech_; 24185 } 24186 /** 24187 * Set up click and touch listeners for the playback element 24188 * 24189 * - On desktops: a click on the video itself will toggle playback 24190 * - On mobile devices: a click on the video toggles controls 24191 * which is done by toggling the user state between active and 24192 * inactive 24193 * - A tap can signal that a user has become active or has become inactive 24194 * e.g. a quick tap on an iPhone movie should reveal the controls. Another 24195 * quick tap should hide them again (signaling the user is in an inactive 24196 * viewing state) 24197 * - In addition to this, we still want the user to be considered inactive after 24198 * a few seconds of inactivity. 24199 * 24200 * > Note: the only part of iOS interaction we can't mimic with this setup 24201 * is a touch and hold on the video element counting as activity in order to 24202 * keep the controls showing, but that shouldn't be an issue. A touch and hold 24203 * on any controls will still keep the user active 24204 * 24205 * @private 24206 */ 24207 ; 24208 24209 _proto.addTechControlsListeners_ = function addTechControlsListeners_() { 24210 // Make sure to remove all the previous listeners in case we are called multiple times. 24211 this.removeTechControlsListeners_(); // Some browsers (Chrome & IE) don't trigger a click on a flash swf, but do 24212 // trigger mousedown/up. 24213 // http://stackoverflow.com/questions/1444562/javascript-onclick-event-over-flash-object 24214 // Any touch events are set to block the mousedown event from happening 24215 24216 this.on(this.tech_, 'mouseup', this.handleTechClick_); 24217 this.on(this.tech_, 'dblclick', this.handleTechDoubleClick_); // If the controls were hidden we don't want that to change without a tap event 24218 // so we'll check if the controls were already showing before reporting user 24219 // activity 24220 24221 this.on(this.tech_, 'touchstart', this.handleTechTouchStart_); 24222 this.on(this.tech_, 'touchmove', this.handleTechTouchMove_); 24223 this.on(this.tech_, 'touchend', this.handleTechTouchEnd_); // The tap listener needs to come after the touchend listener because the tap 24224 // listener cancels out any reportedUserActivity when setting userActive(false) 24225 24226 this.on(this.tech_, 'tap', this.handleTechTap_); 24227 } 24228 /** 24229 * Remove the listeners used for click and tap controls. This is needed for 24230 * toggling to controls disabled, where a tap/touch should do nothing. 24231 * 24232 * @private 24233 */ 24234 ; 24235 24236 _proto.removeTechControlsListeners_ = function removeTechControlsListeners_() { 24237 // We don't want to just use `this.off()` because there might be other needed 24238 // listeners added by techs that extend this. 24239 this.off(this.tech_, 'tap', this.handleTechTap_); 24240 this.off(this.tech_, 'touchstart', this.handleTechTouchStart_); 24241 this.off(this.tech_, 'touchmove', this.handleTechTouchMove_); 24242 this.off(this.tech_, 'touchend', this.handleTechTouchEnd_); 24243 this.off(this.tech_, 'mouseup', this.handleTechClick_); 24244 this.off(this.tech_, 'dblclick', this.handleTechDoubleClick_); 24245 } 24246 /** 24247 * Player waits for the tech to be ready 24248 * 24249 * @private 24250 */ 24251 ; 24252 24253 _proto.handleTechReady_ = function handleTechReady_() { 24254 this.triggerReady(); // Keep the same volume as before 24255 24256 if (this.cache_.volume) { 24257 this.techCall_('setVolume', this.cache_.volume); 24258 } // Look if the tech found a higher resolution poster while loading 24259 24260 24261 this.handleTechPosterChange_(); // Update the duration if available 24262 24263 this.handleTechDurationChange_(); 24264 } 24265 /** 24266 * Retrigger the `loadstart` event that was triggered by the {@link Tech}. This 24267 * function will also trigger {@link Player#firstplay} if it is the first loadstart 24268 * for a video. 24269 * 24270 * @fires Player#loadstart 24271 * @fires Player#firstplay 24272 * @listens Tech#loadstart 24273 * @private 24274 */ 24275 ; 24276 24277 _proto.handleTechLoadStart_ = function handleTechLoadStart_() { 24278 // TODO: Update to use `emptied` event instead. See #1277. 24279 this.removeClass('vjs-ended'); 24280 this.removeClass('vjs-seeking'); // reset the error state 24281 24282 this.error(null); // Update the duration 24283 24284 this.handleTechDurationChange_(); // If it's already playing we want to trigger a firstplay event now. 24285 // The firstplay event relies on both the play and loadstart events 24286 // which can happen in any order for a new source 24287 24288 if (!this.paused()) { 24289 /** 24290 * Fired when the user agent begins looking for media data 24291 * 24292 * @event Player#loadstart 24293 * @type {EventTarget~Event} 24294 */ 24295 this.trigger('loadstart'); 24296 this.trigger('firstplay'); 24297 } else { 24298 // reset the hasStarted state 24299 this.hasStarted(false); 24300 this.trigger('loadstart'); 24301 } // autoplay happens after loadstart for the browser, 24302 // so we mimic that behavior 24303 24304 24305 this.manualAutoplay_(this.autoplay()); 24306 } 24307 /** 24308 * Handle autoplay string values, rather than the typical boolean 24309 * values that should be handled by the tech. Note that this is not 24310 * part of any specification. Valid values and what they do can be 24311 * found on the autoplay getter at Player#autoplay() 24312 */ 24313 ; 24314 24315 _proto.manualAutoplay_ = function manualAutoplay_(type) { 24316 var _this5 = this; 24317 24318 if (!this.tech_ || typeof type !== 'string') { 24319 return; 24320 } 24321 24322 var muted = function muted() { 24323 var previouslyMuted = _this5.muted(); 24324 24325 _this5.muted(true); 24326 24327 var restoreMuted = function restoreMuted() { 24328 _this5.muted(previouslyMuted); 24329 }; // restore muted on play terminatation 24330 24331 24332 _this5.playTerminatedQueue_.push(restoreMuted); 24333 24334 var mutedPromise = _this5.play(); 24335 24336 if (!isPromise(mutedPromise)) { 24337 return; 24338 } 24339 24340 return mutedPromise["catch"](restoreMuted); 24341 }; 24342 24343 var promise; // if muted defaults to true 24344 // the only thing we can do is call play 24345 24346 if (type === 'any' && this.muted() !== true) { 24347 promise = this.play(); 24348 24349 if (isPromise(promise)) { 24350 promise = promise["catch"](muted); 24351 } 24352 } else if (type === 'muted' && this.muted() !== true) { 24353 promise = muted(); 24354 } else { 24355 promise = this.play(); 24356 } 24357 24358 if (!isPromise(promise)) { 24359 return; 24360 } 24361 24362 return promise.then(function () { 24363 _this5.trigger({ 24364 type: 'autoplay-success', 24365 autoplay: type 24366 }); 24367 })["catch"](function (e) { 24368 _this5.trigger({ 24369 type: 'autoplay-failure', 24370 autoplay: type 24371 }); 24372 }); 24373 } 24374 /** 24375 * Update the internal source caches so that we return the correct source from 24376 * `src()`, `currentSource()`, and `currentSources()`. 24377 * 24378 * > Note: `currentSources` will not be updated if the source that is passed in exists 24379 * in the current `currentSources` cache. 24380 * 24381 * 24382 * @param {Tech~SourceObject} srcObj 24383 * A string or object source to update our caches to. 24384 */ 24385 ; 24386 24387 _proto.updateSourceCaches_ = function updateSourceCaches_(srcObj) { 24388 if (srcObj === void 0) { 24389 srcObj = ''; 24390 } 24391 24392 var src = srcObj; 24393 var type = ''; 24394 24395 if (typeof src !== 'string') { 24396 src = srcObj.src; 24397 type = srcObj.type; 24398 } // make sure all the caches are set to default values 24399 // to prevent null checking 24400 24401 24402 this.cache_.source = this.cache_.source || {}; 24403 this.cache_.sources = this.cache_.sources || []; // try to get the type of the src that was passed in 24404 24405 if (src && !type) { 24406 type = findMimetype(this, src); 24407 } // update `currentSource` cache always 24408 24409 24410 this.cache_.source = mergeOptions({}, srcObj, { 24411 src: src, 24412 type: type 24413 }); 24414 var matchingSources = this.cache_.sources.filter(function (s) { 24415 return s.src && s.src === src; 24416 }); 24417 var sourceElSources = []; 24418 var sourceEls = this.$$('source'); 24419 var matchingSourceEls = []; 24420 24421 for (var i = 0; i < sourceEls.length; i++) { 24422 var sourceObj = getAttributes(sourceEls[i]); 24423 sourceElSources.push(sourceObj); 24424 24425 if (sourceObj.src && sourceObj.src === src) { 24426 matchingSourceEls.push(sourceObj.src); 24427 } 24428 } // if we have matching source els but not matching sources 24429 // the current source cache is not up to date 24430 24431 24432 if (matchingSourceEls.length && !matchingSources.length) { 24433 this.cache_.sources = sourceElSources; // if we don't have matching source or source els set the 24434 // sources cache to the `currentSource` cache 24435 } else if (!matchingSources.length) { 24436 this.cache_.sources = [this.cache_.source]; 24437 } // update the tech `src` cache 24438 24439 24440 this.cache_.src = src; 24441 } 24442 /**
24443 * *EXPERIMENTAL* Fired when the source is set or changed on the {@link Tech} 24444 * causing the media element to reload. 24445 * 24446 * It will fire for the initial source and each subsequent source. 24447 * This event is a custom event from Video.js and is triggered by the {@link Tech}. 24448 * 24449 * The event object for this event contains a `src` property that will contain the source 24450 * that was available when the event was triggered. This is generally only necessary if Video.js 24451 * is switching techs while the source was being changed. 24452 * 24453 * It is also fired when `load` is called on the player (or media element) 24454 * because the {@link https://html.spec.whatwg.org/multipage/media.html#dom-media-load|specification for `load`} 24455 * says that the resource selection algorithm needs to be aborted and restarted. 24456 * In this case, it is very likely that the `src` property will be set to the 24457 * empty string `""` to indicate we do not know what the source will be but 24458 * that it is changing. 24459 * 24460 * *This event is currently still experimental and may change in minor releases.* 24461 * __To use this, pass `enableSourceset` option to the player.__ 24462 * 24463 * @event Player#sourceset 24464 * @type {EventTarget~Event} 24465 * @prop {string} src 24466 * The source url available when the `sourceset` was triggered. 24467 * It will be an empty string if we cannot know what the source is 24468 * but know that the source will change. 24469 */ 24470 24471 /** 24472 * Retrigger the `sourceset` event that was triggered by the {@link Tech}. 24473 * 24474 * @fires Player#sourceset 24475 * @listens Tech#sourceset 24476 * @private 24477 */ 24478 ; 24479 24480 _proto.handleTechSourceset_ = function handleTechSourceset_(event) { 24481 var _this6 = this; 24482 24483 // only update the source cache when the source 24484 // was not updated using the player api 24485 if (!this.changingSrc_) { 24486 var updateSourceCaches = function updateSourceCaches(src) { 24487 return _this6.updateSourceCaches_(src); 24488 }; 24489 24490 var playerSrc = this.currentSource().src; 24491 var eventSrc = event.src; // if we have a playerSrc that is not a blob, and a tech src that is a blob 24492 24493 if (playerSrc && !/^blob:/.test(playerSrc) && /^blob:/.test(eventSrc)) { 24494 // if both the tech source and the player source were updated we assume 24495 // something like @videojs/http-streaming did the sourceset and skip updating the source cache. 24496 if (!this.lastSource_ || this.lastSource_.tech !== eventSrc && this.lastSource_.player !== playerSrc) { 24497 updateSourceCaches = function updateSourceCaches() {}; 24498 } 24499 } // update the source to the intial source right away 24500 // in some cases this will be empty string 24501 24502 24503 updateSourceCaches(eventSrc); // if the `sourceset` `src` was an empty string 24504 // wait for a `loadstart` to update the cache to `currentSrc`. 24505 // If a sourceset happens before a `loadstart`, we reset the state 24506 24507 if (!event.src) { 24508 this.tech_.any(['sourceset', 'loadstart'], function (e) { 24509 // if a sourceset happens before a `loadstart` there 24510 // is nothing to do as this `handleTechSourceset_` 24511 // will be called again and this will be handled there. 24512 if (e.type === 'sourceset') { 24513 return; 24514 } 24515 24516 var techSrc = _this6.techGet('currentSrc'); 24517 24518 _this6.lastSource_.tech = techSrc; 24519 24520 _this6.updateSourceCaches_(techSrc); 24521 }); 24522 } 24523 } 24524 24525 this.lastSource_ = { 24526 player: this.currentSource().src, 24527 tech: event.src 24528 }; 24529 this.trigger({ 24530 src: event.src, 24531 type: 'sourceset' 24532 }); 24533 } 24534 /** 24535 * Add/remove the vjs-has-started class 24536 * 24537 * @fires Player#firstplay 24538 * 24539 * @param {boolean} request 24540 * - true: adds the class 24541 * - false: remove the class 24542 * 24543 * @return {boolean} 24544 * the boolean value of hasStarted_ 24545 */ 24546 ; 24547 24548 _proto.hasStarted = function hasStarted(request) { 24549 if (request === undefined) { 24550 // act as getter, if we have no request to change 24551 return this.hasStarted_; 24552 } 24553 24554 if (request === this.hasStarted_) { 24555 return; 24556 } 24557 24558 this.hasStarted_ = request; 24559 24560 if (this.hasStarted_) { 24561 this.addClass('vjs-has-started'); 24562 this.trigger('firstplay'); 24563 } else { 24564 this.removeClass('vjs-has-started'); 24565 } 24566 } 24567 /** 24568 * Fired whenever the media begins or resumes playback 24569 * 24570 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-play} 24571 * @fires Player#play 24572 * @listens Tech#play 24573 * @private 24574 */ 24575 ; 24576 24577 _proto.handleTechPlay_ = function handleTechPlay_() { 24578 this.removeClass('vjs-ended'); 24579 this.removeClass('vjs-paused'); 24580 this.addClass('vjs-playing'); // hide the poster when the user hits play 24581 24582 this.hasStarted(true); 24583 /** 24584 * Triggered whenever an {@link Tech#play} event happens. Indicates that 24585 * playback has started or resumed. 24586 * 24587 * @event Player#play 24588 * @type {EventTarget~Event} 24589 */ 24590 24591 this.trigger('play'); 24592 } 24593 /** 24594 * Retrigger the `ratechange` event that was triggered by the {@link Tech}. 24595 * 24596 * If there were any events queued while the playback rate was zero, fire 24597 * those events now. 24598 * 24599 * @private 24600 * @method Player#handleTechRateChange_ 24601 * @fires Player#ratechange 24602 * @listens Tech#ratechange 24603 */ 24604 ; 24605 24606 _proto.handleTechRateChange_ = function handleTechRateChange_() { 24607 if (this.tech_.playbackRate() > 0 && this.cache_.lastPlaybackRate === 0) { 24608 this.queuedCallbacks_.forEach(function (queued) { 24609 return queued.callback(queued.event); 24610 }); 24611 this.queuedCallbacks_ = []; 24612 } 24613 24614 this.cache_.lastPlaybackRate = this.tech_.playbackRate(); 24615 /** 24616 * Fires when the playing speed of the audio/video is changed 24617 * 24618 * @event Player#ratechange 24619 * @type {event} 24620 */ 24621 24622 this.trigger('ratechange'); 24623 } 24624 /** 24625 * Retrigger the `waiting` event that was triggered by the {@link Tech}. 24626 * 24627 * @fires Player#waiting 24628 * @listens Tech#waiting 24629 * @private 24630 */ 24631 ; 24632 24633 _proto.handleTechWaiting_ = function handleTechWaiting_() { 24634 var _this7 = this; 24635 24636 this.addClass('vjs-waiting'); 24637 /** 24638 * A readyState change on the DOM element has caused playback to stop. 24639 * 24640 * @event Player#waiting 24641 * @type {EventTarget~Event} 24642 */ 24643 24644 this.trigger('waiting'); // Browsers may emit a timeupdate event after a waiting event. In order to prevent 24645 // premature removal of the waiting class, wait for the time to change. 24646 24647 var timeWhenWaiting = this.currentTime(); 24648 24649 var timeUpdateListener = function timeUpdateListener() { 24650 if (timeWhenWaiting !== _this7.currentTime()) { 24651 _this7.removeClass('vjs-waiting'); 24652 24653 _this7.off('timeupdate', timeUpdateListener); 24654 } 24655 }; 24656 24657 this.on('timeupdate', timeUpdateListener); 24658 } 24659 /** 24660 * Retrigger the `canplay` event that was triggered by the {@link Tech}. 24661 * > Note: This is not consistent between browsers. See #1351 24662 * 24663 * @fires Player#canplay 24664 * @listens Tech#canplay 24665 * @private 24666 */ 24667 ; 24668 24669 _proto.handleTechCanPlay_ = function handleTechCanPlay_() { 24670 this.removeClass('vjs-waiting'); 24671 /** 24672 * The media has a readyState of HAVE_FUTURE_DATA or greater. 24673 * 24674 * @event Player#canplay 24675 * @type {EventTarget~Event} 24676 */ 24677 24678 this.trigger('canplay'); 24679 } 24680 /** 24681 * Retrigger the `canplaythrough` event that was triggered by the {@link Tech}. 24682 * 24683 * @fires Player#canplaythrough 24684 * @listens Tech#canplaythrough 24685 * @private 24686 */ 24687 ; 24688 24689 _proto.handleTechCanPlayThrough_ = function handleTechCanPlayThrough_() { 24690 this.removeClass('vjs-waiting'); 24691 /** 24692 * The media has a readyState of HAVE_ENOUGH_DATA or greater. This means that the 24693 * entire media file can be played without buffering. 24694 * 24695 * @event Player#canplaythrough 24696 * @type {EventTarget~Event} 24697 */ 24698 24699 this.trigger('canplaythrough'); 24700 } 24701 /** 24702 * Retrigger the `playing` event that was triggered by the {@link Tech}. 24703 * 24704 * @fires Player#playing 24705 * @listens Tech#playing 24706 * @private 24707 */ 24708 ; 24709 24710 _proto.handleTechPlaying_ = function handleTechPlaying_() { 24711 this.removeClass('vjs-waiting'); 24712 /** 24713 * The media is no longer blocked from playback, and has started playing. 24714 * 24715 * @event Player#playing 24716 * @type {EventTarget~Event} 24717 */ 24718 24719 this.trigger('playing'); 24720 } 24721 /** 24722 * Retrigger the `seeking` event that was triggered by the {@link Tech}. 24723 * 24724 * @fires Player#seeking 24725 * @listens Tech#seeking 24726 * @private 24727 */ 24728 ; 24729 24730 _proto.handleTechSeeking_ = function handleTechSeeking_() { 24731 this.addClass('vjs-seeking'); 24732 /** 24733 * Fired whenever the player is jumping to a new time 24734 * 24735 * @event Player#seeking 24736 * @type {EventTarget~Event} 24737 */ 24738 24739 this.trigger('seeking'); 24740 } 24741 /** 24742 * Retrigger the `seeked` event that was triggered by the {@link Tech}. 24743 * 24744 * @fires Player#seeked 24745 * @listens Tech#seeked 24746 * @private 24747 */ 24748 ; 24749 24750 _proto.handleTechSeeked_ = function handleTechSeeked_() { 24751 this.removeClass('vjs-seeking'); 24752 this.removeClass('vjs-ended'); 24753 /** 24754 * Fired when the player has finished jumping to a new time 24755 * 24756 * @event Player#seeked 24757 * @type {EventTarget~Event} 24758 */ 24759 24760 this.trigger('seeked'); 24761 } 24762 /** 24763 * Retrigger the `firstplay` event that was triggered by the {@link Tech}. 24764 * 24765 * @fires Player#firstplay 24766 * @listens Tech#firstplay 24767 * @deprecated As of 6.0 firstplay event is deprecated. 24768 * As of 6.0 passing the `starttime` option to the player and the firstplay event are deprecated. 24769 * @private 24770 */ 24771 ; 24772 24773 _proto.handleTechFirstPlay_ = function handleTechFirstPlay_() { 24774 // If the first starttime attribute is specified 24775 // then we will start at the given offset in seconds 24776 if (this.options_.starttime) { 24777 log.warn('Passing the `starttime` option to the player will be deprecated
vendor: 26,018 bytes, lines 24777-25653
24777in 6.0'); 24778 this.currentTime(this.options_.starttime); 24779 } 24780 24781 this.addClass('vjs-has-started'); 24782 /** 24783 * Fired the first time a video is played. Not part of the HLS spec, and this is 24784 * probably not the best implementation yet, so use sparingly. If you don't have a 24785 * reason to prevent playback, use `myPlayer.one('play');` instead. 24786 * 24787 * @event Player#firstplay 24788 * @deprecated As of 6.0 firstplay event is deprecated. 24789 * @type {EventTarget~Event} 24790 */ 24791 24792 this.trigger('firstplay'); 24793 } 24794 /** 24795 * Retrigger the `pause` event that was triggered by the {@link Tech}. 24796 * 24797 * @fires Player#pause 24798 * @listens Tech#pause 24799 * @private 24800 */ 24801 ; 24802 24803 _proto.handleTechPause_ = function handleTechPause_() { 24804 this.removeClass('vjs-playing'); 24805 this.addClass('vjs-paused'); 24806 /** 24807 * Fired whenever the media has been paused 24808 * 24809 * @event Player#pause 24810 * @type {EventTarget~Event} 24811 */ 24812 24813 this.trigger('pause'); 24814 } 24815 /** 24816 * Retrigger the `ended` event that was triggered by the {@link Tech}. 24817 * 24818 * @fires Player#ended 24819 * @listens Tech#ended 24820 * @private 24821 */ 24822 ; 24823 24824 _proto.handleTechEnded_ = function handleTechEnded_() { 24825 this.addClass('vjs-ended'); 24826 24827 if (this.options_.loop) { 24828 this.currentTime(0); 24829 this.play(); 24830 } else if (!this.paused()) { 24831 this.pause(); 24832 } 24833 /** 24834 * Fired when the end of the media resource is reached (currentTime == duration) 24835 * 24836 * @event Player#ended 24837 * @type {EventTarget~Event} 24838 */ 24839 24840 24841 this.trigger('ended'); 24842 } 24843 /** 24844 * Fired when the duration of the media resource is first known or changed 24845 * 24846 * @listens Tech#durationchange 24847 * @private 24848 */ 24849 ; 24850 24851 _proto.handleTechDurationChange_ = function handleTechDurationChange_() { 24852 this.duration(this.techGet_('duration')); 24853 } 24854 /** 24855 * Handle a click on the media element to play/pause 24856 * 24857 * @param {EventTarget~Event} event 24858 * the event that caused this function to trigger 24859 * 24860 * @listens Tech#mouseup 24861 * @private 24862 */ 24863 ; 24864 24865 _proto.handleTechClick_ = function handleTechClick_(event) { 24866 if (!isSingleLeftClick(event)) { 24867 return; 24868 } // When controls are disabled a click should not toggle playback because 24869 // the click is considered a control 24870 24871 24872 if (!this.controls_) { 24873 return; 24874 } 24875 24876 if (this.paused()) { 24877 silencePromise(this.play()); 24878 } else { 24879 this.pause(); 24880 } 24881 } 24882 /** 24883 * Handle a double-click on the media element to enter/exit fullscreen 24884 * 24885 * @param {EventTarget~Event} event 24886 * the event that caused this function to trigger 24887 * 24888 * @listens Tech#dblclick 24889 * @private 24890 */ 24891 ; 24892 24893 _proto.handleTechDoubleClick_ = function handleTechDoubleClick_(event) { 24894 if (!this.controls_) { 24895 return; 24896 } // we do not want to toggle fullscreen state 24897 // when double-clicking inside a control bar or a modal 24898 24899 24900 var inAllowedEls = Array.prototype.some.call(this.$$('.vjs-control-bar, .vjs-modal-dialog'), function (el) { 24901 return el.contains(event.target); 24902 }); 24903 24904 if (!inAllowedEls) { 24905 /* 24906 * options.userActions.doubleClick 24907 * 24908 * If `undefined` or `true`, double-click toggles fullscreen if controls are present 24909 * Set to `false` to disable double-click handling 24910 * Set to a function to substitute an external double-click handler 24911 */ 24912 if (this.options_ === undefined || this.options_.userActions === undefined || this.options_.userActions.doubleClick === undefined || this.options_.userActions.doubleClick !== false) { 24913 if (this.options_ !== undefined && this.options_.userActions !== undefined && typeof this.options_.userActions.doubleClick === 'function') { 24914 this.options_.userActions.doubleClick.call(this, event); 24915 } else if (this.isFullscreen()) { 24916 this.exitFullscreen(); 24917 } else { 24918 this.requestFullscreen(); 24919 } 24920 } 24921 } 24922 } 24923 /** 24924 * Handle a tap on the media element. It will toggle the user 24925 * activity state, which hides and shows the controls. 24926 * 24927 * @listens Tech#tap 24928 * @private 24929 */ 24930 ; 24931 24932 _proto.handleTechTap_ = function handleTechTap_() { 24933 this.userActive(!this.userActive()); 24934 } 24935 /** 24936 * Handle touch to start 24937 * 24938 * @listens Tech#touchstart 24939 * @private 24940 */ 24941 ; 24942 24943 _proto.handleTechTouchStart_ = function handleTechTouchStart_() { 24944 this.userWasActive = this.userActive(); 24945 } 24946 /** 24947 * Handle touch to move 24948 * 24949 * @listens Tech#touchmove 24950 * @private 24951 */ 24952 ; 24953 24954 _proto.handleTechTouchMove_ = function handleTechTouchMove_() { 24955 if (this.userWasActive) { 24956 this.reportUserActivity(); 24957 } 24958 } 24959 /** 24960 * Handle touch to end 24961 * 24962 * @param {EventTarget~Event} event 24963 * the touchend event that triggered 24964 * this function 24965 * 24966 * @listens Tech#touchend 24967 * @private 24968 */ 24969 ; 24970 24971 _proto.handleTechTouchEnd_ = function handleTechTouchEnd_(event) { 24972 // Stop the mouse events from also happening 24973 event.preventDefault(); 24974 } 24975 /** 24976 * native click events on the SWF aren't triggered on IE11, Win8.1RT 24977 * use stageclick events triggered from inside the SWF instead 24978 * 24979 * @private 24980 * @listens stageclick 24981 */ 24982 ; 24983 24984 _proto.handleStageClick_ = function handleStageClick_() { 24985 this.reportUserActivity(); 24986 } 24987 /** 24988 * @private 24989 */ 24990 ; 24991 24992 _proto.toggleFullscreenClass_ = function toggleFullscreenClass_() { 24993 if (this.isFullscreen()) { 24994 this.addClass('vjs-fullscreen'); 24995 } else { 24996 this.removeClass('vjs-fullscreen'); 24997 } 24998 } 24999 /** 25000 * when the document fschange event triggers it calls this 25001 */ 25002 ; 25003 25004 _proto.documentFullscreenChange_ = function documentFullscreenChange_(e) { 25005 var el = this.el(); 25006 var isFs = document[this.fsApi_.fullscreenElement] === el; 25007 25008 if (!isFs && el.matches) { 25009 isFs = el.matches(':' + this.fsApi_.fullscreen); 25010 } else if (!isFs && el.msMatchesSelector) { 25011 isFs = el.msMatchesSelector(':' + this.fsApi_.fullscreen); 25012 } 25013 25014 this.isFullscreen(isFs); // If cancelling fullscreen, remove event listener. 25015 25016 if (this.isFullscreen() === false) { 25017 off(document, this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_); 25018 } 25019 25020 if (this.fsApi_.prefixed) { 25021 /** 25022 * @event Player#fullscreenchange 25023 * @type {EventTarget~Event} 25024 */ 25025 this.trigger('fullscreenchange'); 25026 } 25027 } 25028 /** 25029 * Handle Tech Fullscreen Change 25030 * 25031 * @param {EventTarget~Event} event 25032 * the fullscreenchange event that triggered this function 25033 * 25034 * @param {Object} data 25035 * the data that was sent with the event 25036 * 25037 * @private 25038 * @listens Tech#fullscreenchange 25039 * @fires Player#fullscreenchange 25040 */ 25041 ; 25042 25043 _proto.handleTechFullscreenChange_ = function handleTechFullscreenChange_(event, data) { 25044 if (data) { 25045 this.isFullscreen(data.isFullscreen); 25046 } 25047 /** 25048 * Fired when going in and out of fullscreen. 25049 * 25050 * @event Player#fullscreenchange 25051 * @type {EventTarget~Event} 25052 */ 25053 25054 25055 this.trigger('fullscreenchange'); 25056 } 25057 /** 25058 * @private 25059 */ 25060 ; 25061 25062 _proto.togglePictureInPictureClass_ = function togglePictureInPictureClass_() { 25063 if (this.isInPictureInPicture()) { 25064 this.addClass('vjs-picture-in-picture'); 25065 } else { 25066 this.removeClass('vjs-picture-in-picture'); 25067 } 25068 } 25069 /** 25070 * Handle Tech Enter Picture-in-Picture. 25071 * 25072 * @param {EventTarget~Event} event 25073 * the enterpictureinpicture event that triggered this function 25074 * 25075 * @private 25076 * @listens Tech#enterpictureinpicture 25077 */ 25078 ; 25079 25080 _proto.handleTechEnterPictureInPicture_ = function handleTechEnterPictureInPicture_(event) { 25081 this.isInPictureInPicture(true); 25082 } 25083 /** 25084 * Handle Tech Leave Picture-in-Picture. 25085 * 25086 * @param {EventTarget~Event} event 25087 * the leavepictureinpicture event that triggered this function 25088 * 25089 * @private 25090 * @listens Tech#leavepictureinpicture 25091 */ 25092 ; 25093 25094 _proto.handleTechLeavePictureInPicture_ = function handleTechLeavePictureInPicture_(event) { 25095 this.isInPictureInPicture(false); 25096 } 25097 /** 25098 * Fires when an error occurred during the loading of an audio/video. 25099 * 25100 * @private 25101 * @listens Tech#error 25102 */ 25103 ; 25104 25105 _proto.handleTechError_ = function handleTechError_() { 25106 var error = this.tech_.error(); 25107 this.error(error); 25108 } 25109 /** 25110 * Retrigger the `textdata` event that was triggered by the {@link Tech}. 25111 * 25112 * @fires Player#textdata 25113 * @listens Tech#textdata 25114 * @private 25115 */ 25116 ; 25117 25118 _proto.handleTechTextData_ = function handleTechTextData_() { 25119 var data = null; 25120 25121 if (arguments.length > 1) { 25122 data = arguments[1]; 25123 } 25124 /** 25125 * Fires when we get a textdata event from tech 25126 * 25127 * @event Player#textdata 25128 * @type {EventTarget~Event} 25129 */ 25130 25131 25132 this.trigger('textdata', data); 25133 } 25134 /** 25135 * Get object for cached values. 25136 * 25137 * @return {Object} 25138 * get the current object cache 25139 */ 25140 ; 25141 25142 _proto.getCache = function getCache() { 25143 return this.cache_; 25144 } 25145 /** 25146 * Resets the internal cache object. 25147 * 25148 * Using this function outside the player constructor or reset method may 25149 * have unintended side-effects. 25150 * 25151 * @private 25152 */ 25153 ; 25154 25155 _proto.resetCache_ = function resetCache_() { 25156 this.cache_ = { 25157 // Right now, the currentTime is not _really_ cached because it is always 25158 // retrieved from the tech (see: currentTime). However, for completeness, 25159 // we set it to zero here to ensure that if we do start actually caching 25160 // it, we reset it along with everything else. 25161 currentTime: 0, 25162 inactivityTimeout: this.options_.inactivityTimeout, 25163 duration: NaN, 25164 lastVolume: 1, 25165 lastPlaybackRate: this.defaultPlaybackRate(), 25166 media: null, 25167 src: '', 25168 source: {}, 25169 sources: [], 25170 volume: 1 25171 }; 25172 } 25173 /** 25174 * Pass values to the playback tech 25175 * 25176 * @param {string} [method] 25177 * the method to call 25178 * 25179 * @param {Object} arg 25180 * the argument to pass 25181 * 25182 * @private 25183 */ 25184 ; 25185 25186 _proto.techCall_ = function techCall_(method, arg) { 25187 // If it's not ready yet, call method when it is 25188 this.ready(function () { 25189 if (method in allowedSetters) { 25190 return set(this.middleware_, this.tech_, method, arg); 25191 } else if (method in allowedMediators) { 25192 return mediate(this.middleware_, this.tech_, method, arg); 25193 } 25194 25195 try { 25196 if (this.tech_) { 25197 this.tech_[method](arg); 25198 } 25199 } catch (e) { 25200 log(e); 25201 throw e; 25202 } 25203 }, true); 25204 } 25205 /** 25206 * Get calls can't wait for the tech, and sometimes don't need to. 25207 * 25208 * @param {string} method 25209 * Tech method 25210 * 25211 * @return {Function|undefined} 25212 * the method or undefined 25213 * 25214 * @private 25215 */ 25216 ; 25217 25218 _proto.techGet_ = function techGet_(method) { 25219 if (!this.tech_ || !this.tech_.isReady_) { 25220 return; 25221 } 25222 25223 if (method in allowedGetters) { 25224 return get(this.middleware_, this.tech_, method); 25225 } else if (method in allowedMediators) { 25226 return mediate(this.middleware_, this.tech_, method); 25227 } // Flash likes to die and reload when you hide or reposition it. 25228 // In these cases the object methods go away and we get errors. 25229 // When that happens we'll catch the errors and inform tech that it's not ready any more. 25230 25231 25232 try { 25233 return this.tech_[method](); 25234 } catch (e) { 25235 // When building additional tech libs, an expected method may not be defined yet 25236 if (this.tech_[method] === undefined) { 25237 log("Video.js: " + method + " method not defined for " + this.techName_ + " playback technology.", e); 25238 throw e; 25239 } // When a method isn't available on the object it throws a TypeError 25240 25241 25242 if (e.name === 'TypeError') { 25243 log("Video.js: " + method + " unavailable on " + this.techName_ + " playback technology element.", e); 25244 this.tech_.isReady_ = false; 25245 throw e; 25246 } // If error unknown, just log and throw 25247 25248 25249 log(e); 25250 throw e; 25251 } 25252 } 25253 /** 25254 * Attempt to begin playback at the first opportunity. 25255 * 25256 * @return {Promise|undefined} 25257 * Returns a promise if the browser supports Promises (or one 25258 * was passed in as an option). This promise will be resolved on 25259 * the return value of play. If this is undefined it will fulfill the 25260 * promise chain otherwise the promise chain will be fulfilled when 25261 * the promise from play is fulfilled. 25262 */ 25263 ; 25264 25265 _proto.play = function play() { 25266 var _this8 = this; 25267 25268 var PromiseClass = this.options_.Promise || window$1.Promise; 25269 25270 if (PromiseClass) { 25271 return new PromiseClass(function (resolve) { 25272 _this8.play_(resolve); 25273 }); 25274 } 25275 25276 return this.play_(); 25277 } 25278 /** 25279 * The actual logic for play, takes a callback that will be resolved on the 25280 * return value of play. This allows us to resolve to the play promise if there 25281 * is one on modern browsers. 25282 * 25283 * @private 25284 * @param {Function} [callback] 25285 * The callback that should be called when the techs play is actually called 25286 */ 25287 ; 25288 25289 _proto.play_ = function play_(callback) { 25290 var _this9 = this; 25291 25292 if (callback === void 0) { 25293 callback = silencePromise; 25294 } 25295 25296 this.playCallbacks_.push(callback); 25297 var isSrcReady = Boolean(!this.changingSrc_ && (this.src() || this.currentSrc())); // treat calls to play_ somewhat like the `one` event function 25298 25299 if (this.waitToPlay_) { 25300 this.off(['ready', 'loadstart'], this.waitToPlay_); 25301 this.waitToPlay_ = null; 25302 } // if the player/tech is not ready or the src itself is not ready 25303 // queue up a call to play on `ready` or `loadstart` 25304 25305 25306 if (!this.isReady_ || !isSrcReady) { 25307 this.waitToPlay_ = function (e) { 25308 _this9.play_(); 25309 }; 25310 25311 this.one(['ready', 'loadstart'], this.waitToPlay_); // if we are in Safari, there is a high chance that loadstart will trigger after the gesture timeperiod 25312 // in that case, we need to prime the video element by calling load so it'll be ready in time 25313 25314 if (!isSrcReady && (IS_ANY_SAFARI || IS_IOS)) { 25315 this.load(); 25316 } 25317 25318 return; 25319 } // If the player/tech is ready and we have a source, we can attempt playback. 25320 25321 25322 var val = this.techGet_('play'); // play was terminated if the returned value is null 25323 25324 if (val === null) { 25325 this.runPlayTerminatedQueue_(); 25326 } else { 25327 this.runPlayCallbacks_(val); 25328 } 25329 } 25330 /** 25331 * These functions will be run when if play is terminated. If play 25332 * runPlayCallbacks_ is run these function will not be run. This allows us 25333 * to differenciate between a terminated play and an actual call to play. 25334 */ 25335 ; 25336 25337 _proto.runPlayTerminatedQueue_ = function runPlayTerminatedQueue_() { 25338 var queue = this.playTerminatedQueue_.slice(0); 25339 this.playTerminatedQueue_ = []; 25340 queue.forEach(function (q) { 25341 q(); 25342 }); 25343 } 25344 /** 25345 * When a callback to play is delayed we have to run these 25346 * callbacks when play is actually called on the tech. This function 25347 * runs the callbacks that were delayed and accepts the return value 25348 * from the tech. 25349 * 25350 * @param {undefined|Promise} val 25351 * The return value from the tech. 25352 */ 25353 ; 25354 25355 _proto.runPlayCallbacks_ = function runPlayCallbacks_(val) { 25356 var callbacks = this.playCallbacks_.slice(0); 25357 this.playCallbacks_ = []; // clear play terminatedQueue since we finished a real play 25358 25359 this.playTerminatedQueue_ = []; 25360 callbacks.forEach(function (cb) { 25361 cb(val); 25362 }); 25363 } 25364 /** 25365 * Pause the video playback 25366 * 25367 * @return {Player} 25368 * A reference to the player object this function was called on 25369 */ 25370 ; 25371 25372 _proto.pause = function pause() { 25373 this.techCall_('pause'); 25374 } 25375 /** 25376 * Check if the player is paused or has yet to play 25377 * 25378 * @return {boolean} 25379 * - false: if the media is currently playing 25380 * - true: if media is not currently playing 25381 */ 25382 ; 25383 25384 _proto.paused = function paused() { 25385 // The initial state of paused should be true (in Safari it's actually false) 25386 return this.techGet_('paused') === false ? false : true; 25387 } 25388 /** 25389 * Get a TimeRange object representing the current ranges of time that the user 25390 * has played. 25391 * 25392 * @return {TimeRange} 25393 * A time range object that represents all the increments of time that have 25394 * been played. 25395 */ 25396 ; 25397 25398 _proto.played = function played() { 25399 return this.techGet_('played') || createTimeRanges(0, 0); 25400 } 25401 /** 25402 * Returns whether or not the user is "scrubbing". Scrubbing is 25403 * when the user has clicked the progress bar handle and is 25404 * dragging it along the progress bar. 25405 * 25406 * @param {boolean} [isScrubbing] 25407 * whether the user is or is not scrubbing 25408 * 25409 * @return {boolean} 25410 * The value of scrubbing when getting 25411 */ 25412 ; 25413 25414 _proto.scrubbing = function scrubbing(isScrubbing) { 25415 if (typeof isScrubbing === 'undefined') { 25416 return this.scrubbing_; 25417 } 25418 25419 this.scrubbing_ = !!isScrubbing; 25420 25421 if (isScrubbing) { 25422 this.addClass('vjs-scrubbing'); 25423 } else { 25424 this.removeClass('vjs-scrubbing'); 25425 } 25426 } 25427 /** 25428 * Get or set the current time (in seconds) 25429 * 25430 * @param {number|string} [seconds] 25431 * The time to seek to in seconds 25432 * 25433 * @return {number} 25434 * - the current time in seconds when getting 25435 */ 25436 ; 25437 25438 _proto.currentTime = function currentTime(seconds) { 25439 if (typeof seconds !== 'undefined') { 25440 if (seconds < 0) { 25441 seconds = 0; 25442 } 25443 25444 this.techCall_('setCurrentTime', seconds); 25445 return; 25446 } // cache last currentTime and return. default to 0 seconds 25447 // 25448 // Caching the currentTime is meant to prevent a massive amount of reads on the tech's 25449 // currentTime when scrubbing, but may not provide much performance benefit afterall. 25450 // Should be tested. Also something has to read the actual current time or the cache will 25451 // never get updated. 25452 25453 25454 this.cache_.currentTime = this.techGet_('currentTime') || 0; 25455 return this.cache_.currentTime; 25456 } 25457 /** 25458 * Normally gets the length in time of the video in seconds; 25459 * in all but the rarest use cases an argument will NOT be passed to the method 25460 * 25461 * > **NOTE**: The video must have started loading before the duration can be 25462 * known, and in the case of Flash, may not be known until the video starts 25463 * playing. 25464 * 25465 * @fires Player#durationchange 25466 * 25467 * @param {number} [seconds] 25468 * The duration of the video to set in seconds 25469 * 25470 * @return {number} 25471 * - The duration of the video in seconds when getting 25472 */ 25473 ; 25474 25475 _proto.duration = function duration(seconds) { 25476 if (seconds === undefined) { 25477 // return NaN if the duration is not known 25478 return this.cache_.duration !== undefined ? this.cache_.duration : NaN; 25479 } 25480 25481 seconds = parseFloat(seconds); // Standardize on Infinity for signaling video is live 25482 25483 if (seconds < 0) { 25484 seconds = Infinity; 25485 } 25486 25487 if (seconds !== this.cache_.duration) { 25488 // Cache the last set value for optimized scrubbing (esp. Flash) 25489 this.cache_.duration = seconds; 25490 25491 if (seconds === Infinity) { 25492 this.addClass('vjs-live'); 25493 25494 if (this.options_.liveui && this.player_.liveTracker) { 25495 this.addClass('vjs-liveui'); 25496 } 25497 } else { 25498 this.removeClass('vjs-live'); 25499 this.removeClass('vjs-liveui'); 25500 } 25501 25502 if (!isNaN(seconds)) { 25503 // Do not fire durationchange unless the duration value is known. 25504 // @see [Spec]{@link https://www.w3.org/TR/2011/WD-html5-20110113/video.html#media-element-load-algorithm} 25505 25506 /** 25507 * @event Player#durationchange 25508 * @type {EventTarget~Event} 25509 */ 25510 this.trigger('durationchange'); 25511 } 25512 } 25513 } 25514 /** 25515 * Calculates how much time is left in the video. Not part 25516 * of the native video API. 25517 * 25518 * @return {number} 25519 * The time remaining in seconds 25520 */ 25521 ; 25522 25523 _proto.remainingTime = function remainingTime() { 25524 return this.duration() - this.currentTime(); 25525 } 25526 /** 25527 * A remaining time function that is intented to be used when 25528 * the time is to be displayed directly to the user. 25529 * 25530 * @return {number} 25531 * The rounded time remaining in seconds 25532 */ 25533 ; 25534 25535 _proto.remainingTimeDisplay = function remainingTimeDisplay() { 25536 return Math.floor(this.duration()) - Math.floor(this.currentTime()); 25537 } // 25538 // Kind of like an array of portions of the video that have been downloaded. 25539 25540 /** 25541 * Get a TimeRange object with an array of the times of the video 25542 * that have been downloaded. If you just want the percent of the 25543 * video that's been downloaded, use bufferedPercent. 25544 * 25545 * @see [Buffered Spec]{@link http://dev.w3.org/html5/spec/video.html#dom-media-buffered} 25546 * 25547 * @return {TimeRange} 25548 * A mock TimeRange object (following HTML spec) 25549 */ 25550 ; 25551 25552 _proto.buffered = function buffered() { 25553 var buffered = this.techGet_('buffered'); 25554 25555 if (!buffered || !buffered.length) { 25556 buffered = createTimeRanges(0, 0); 25557 } 25558 25559 return buffered; 25560 } 25561 /** 25562 * Get the percent (as a decimal) of the video that's been downloaded. 25563 * This method is not a part of the native HTML video API. 25564 * 25565 * @return {number} 25566 * A decimal between 0 and 1 representing the percent 25567 * that is buffered 0 being 0% and 1 being 100% 25568 */ 25569 ; 25570 25571 _proto.bufferedPercent = function bufferedPercent$1() { 25572 return bufferedPercent(this.buffered(), this.duration()); 25573 } 25574 /** 25575 * Get the ending time of the last buffered time range 25576 * This is used in the progress bar to encapsulate all time ranges. 25577 * 25578 * @return {number} 25579 * The end of the last buffered time range 25580 */ 25581 ; 25582 25583 _proto.bufferedEnd = function bufferedEnd() { 25584 var buffered = this.buffered(); 25585 var duration = this.duration(); 25586 var end = buffered.end(buffered.length - 1); 25587 25588 if (end > duration) { 25589 end = duration; 25590 } 25591 25592 return end; 25593 } 25594 /** 25595 * Get or set the current volume of the media 25596 * 25597 * @param {number} [percentAsDecimal] 25598 * The new volume as a decimal percent: 25599 * - 0 is muted/0%/off 25600 * - 1.0 is 100%/full 25601 * - 0.5 is half volume or 50% 25602 * 25603 * @return {number} 25604 * The current volume as a percent when getting 25605 */ 25606 ; 25607 25608 _proto.volume = function volume(percentAsDecimal) { 25609 var vol; 25610 25611 if (percentAsDecimal !== undefined) { 25612 // Force value to between 0 and 1 25613 vol = Math.max(0, Math.min(1, parseFloat(percentAsDecimal))); 25614 this.cache_.volume = vol; 25615 this.techCall_('setVolume', vol); 25616 25617 if (vol > 0) { 25618 this.lastVolume_(vol); 25619 } 25620 25621 return; 25622 } // Default to 1 when returning current volume. 25623 25624 25625 vol = parseFloat(this.techGet_('volume')); 25626 return isNaN(vol) ? 1 : vol; 25627 } 25628 /** 25629 * Get the current muted state, or turn mute on or off 25630 * 25631 * @param {boolean} [muted] 25632 * - true to mute 25633 * - false to unmute 25634 * 25635 * @return {boolean} 25636 * - true if mute is on and getting 25637 * - false if mute is off and getting 25638 */ 25639 ; 25640 25641 _proto.muted = function muted(_muted) { 25642 if (_muted !== undefined) { 25643 this.techCall_('setMuted', _muted); 25644 return; 25645 } 25646 25647 return this.techGet_('muted') || false; 25648 } 25649 /** 25650 * Get the current defaultMuted state, or turn defaultMuted on or off. defaultMuted 25651 * indicates the state of muted on initial playback. 25652 * 25653 * ```js
25654 * var myPlayer = videojs('some-player-id'); 25655 * 25656 * myPlayer.src("http://www.example.com/path/to/video.mp4"); 25657 * 25658 * // get, should be false 25659 * console.log(myPlayer.defaultMuted()); 25660 * // set to true 25661 * myPlayer.defaultMuted(true); 25662 * // get should be true 25663 * console.log(myPlayer.defaultMuted()); 25664 * ``` 25665 * 25666 * @param {boolean} [defaultMuted] 25667 * - true to mute 25668 * - false to unmute 25669 * 25670 * @return {boolean|Player} 25671 * - true if defaultMuted is on and getting 25672 * - false if defaultMuted is off and getting 25673 * - A reference to the current player when setting 25674 */ 25675 ; 25676 25677 _proto.defaultMuted = function defaultMuted(_defaultMuted) { 25678 if (_defaultMuted !== undefined) { 25679 return this.techCall_('setDefaultMuted', _defaultMuted); 25680 } 25681 25682 return this.techGet_('defaultMuted') || false; 25683 } 25684 /** 25685 * Get the last volume, or set it 25686 * 25687 * @param {number} [percentAsDecimal] 25688 * The new last volume as a decimal percent: 25689 * - 0 is muted/0%/off 25690 * - 1.0 is 100%/full 25691 * - 0.5 is half volume or 50% 25692 * 25693 * @return {number} 25694 * the current value of lastVolume as a percent when getting 25695 * 25696 * @private 25697 */ 25698 ; 25699 25700 _proto.lastVolume_ = function lastVolume_(percentAsDecimal) { 25701 if (percentAsDecimal !== undefined && percentAsDecimal !== 0) { 25702 this.cache_.lastVolume = percentAsDecimal; 25703 return; 25704 } 25705 25706 return this.cache_.lastVolume; 25707 } 25708 /** 25709 * Check if current tech can support native fullscreen 25710 * (e.g. with built in controls like iOS, so not our flash swf) 25711 * 25712 * @return {boolean} 25713 * if native fullscreen is supported 25714 */ 25715 ; 25716 25717 _proto.supportsFullScreen = function supportsFullScreen() { 25718 return this.techGet_('supportsFullScreen') || false; 25719 } 25720 /** 25721 * Check if the player is in fullscreen mode or tell the player that it 25722 * is or is not in fullscreen mode. 25723 * 25724 * > NOTE: As of the latest HTML5 spec, isFullscreen is no longer an official 25725 * property and instead document.fullscreenElement is used. But isFullscreen is 25726 * still a valuable property for internal player workings. 25727 * 25728 * @param {boolean} [isFS] 25729 * Set the players current fullscreen state 25730 * 25731 * @return {boolean} 25732 * - true if fullscreen is on and getting 25733 * - false if fullscreen is off and getting 25734 */ 25735 ; 25736 25737 _proto.isFullscreen = function isFullscreen(isFS) { 25738 if (isFS !== undefined) { 25739 this.isFullscreen_ = !!isFS; 25740 this.toggleFullscreenClass_(); 25741 return; 25742 } 25743 25744 return !!this.isFullscreen_; 25745 } 25746 /** 25747 * Increase the size of the video to full screen 25748 * In some browsers, full screen is not supported natively, so it enters 25749 * "full window mode", where the video fills the browser window. 25750 * In browsers and devices that support native full screen, sometimes the 25751 * browser's default controls will be shown, and not the Video.js custom skin. 25752 * This includes most mobile devices (iOS, Android) and older versions of 25753 * Safari. 25754 * 25755 * @param {Object} [fullscreenOptions] 25756 * Override the player fullscreen options 25757 * 25758 * @fires Player#fullscreenchange 25759 */ 25760 ; 25761 25762 _proto.requestFullscreen = function requestFullscreen(fullscreenOptions) { 25763 var fsOptions; 25764 this.isFullscreen(true); 25765 25766 if (this.fsApi_.requestFullscreen) { 25767 // the browser supports going fullscreen at the element level so we can 25768 // take the controls fullscreen as well as the video 25769 // Trigger fullscreenchange event after change 25770 // We have to specifically add this each time, and remove 25771 // when canceling fullscreen. Otherwise if there's multiple 25772 // players on a page, they would all be reacting to the same fullscreen 25773 // events 25774 on(document, this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_); // only pass FullscreenOptions to requestFullscreen if it isn't prefixed 25775 25776 if (!this.fsApi_.prefixed) { 25777 fsOptions = this.options_.fullscreen && this.options_.fullscreen.options || {}; 25778 25779 if (fullscreenOptions !== undefined) { 25780 fsOptions = fullscreenOptions; 25781 } 25782 } 25783 25784 silencePromise(this.el_[this.fsApi_.requestFullscreen](fsOptions)); 25785 } else if (this.tech_.supportsFullScreen()) { 25786 // we can't take the video.js controls fullscreen but we can go fullscreen 25787 // with native controls 25788 this.techCall_('enterFullScreen'); 25789 } else { 25790 // fullscreen isn't supported so we'll just stretch the video element to 25791 // fill the viewport 25792 this.enterFullWindow(); 25793 /** 25794 * @event Player#fullscreenchange 25795 * @type {EventTarget~Event} 25796 */ 25797 25798 this.trigger('fullscreenchange'); 25799 } 25800 } 25801 /** 25802 * Return the video to its normal size after having been in full screen mode 25803 * 25804 * @fires Player#fullscreenchange 25805 */ 25806 ; 25807 25808 _proto.exitFullscreen = function exitFullscreen() { 25809 this.isFullscreen(false); // Check for browser element fullscreen support 25810 25811 if (this.fsApi_.requestFullscreen) { 25812 silencePromise(document[this.fsApi_.exitFullscreen]()); 25813 } else if (this.tech_.supportsFullScreen()) { 25814 this.techCall_('exitFullScreen'); 25815 } else { 25816 this.exitFullWindow(); 25817 /** 25818 * @event Player#fullscreenchange 25819 * @type {EventTarget~Event} 25820 */ 25821 25822 this.trigger('fullscreenchange'); 25823 } 25824 } 25825 /** 25826 * When fullscreen isn't supported we can stretch the 25827 * video container to as wide as the browser will let us. 25828 * 25829 * @fires Player#enterFullWindow 25830 */ 25831 ; 25832 25833 _proto.enterFullWindow = function enterFullWindow() { 25834 this.isFullWindow = true; // Storing original doc overflow value to return to when fullscreen is off 25835 25836 this.docOrigOverflow = document.documentElement.style.overflow; // Add listener for esc key to exit fullscreen 25837 25838 on(document, 'keydown', this.boundFullWindowOnEscKey_); // Hide any scroll bars 25839 25840 document.documentElement.style.overflow = 'hidden'; // Apply fullscreen styles 25841 25842 addClass(document.body, 'vjs-full-window'); 25843 /** 25844 * @event Player#enterFullWindow 25845 * @type {EventTarget~Event} 25846 */ 25847 25848 this.trigger('enterFullWindow'); 25849 } 25850 /** 25851 * Check for call to either exit full window or 25852 * full screen on ESC key 25853 * 25854 * @param {string} event 25855 * Event to check for key press 25856 */ 25857 ; 25858 25859 _proto.fullWindowOnEscKey = function fullWindowOnEscKey(event) { 25860 if (keycode.isEventKey(event, 'Esc')) { 25861 if (this.isFullscreen() === true) { 25862 this.exitFullscreen(); 25863 } else { 25864 this.exitFullWindow(); 25865 } 25866 } 25867 } 25868 /** 25869 * Exit full window 25870 * 25871 * @fires Player#exitFullWindow 25872 */ 25873 ; 25874 25875 _proto.exitFullWindow = function exitFullWindow() { 25876 this.isFullWindow = false;
vendor: 9,198 bytes, lines 25877-26137
25877 off(document, 'keydown', this.boundFullWindowOnEscKey_); // Unhide scroll bars. 25878 25879 document.documentElement.style.overflow = this.docOrigOverflow; // Remove fullscreen styles 25880 25881 removeClass(document.body, 'vjs-full-window'); // Resize the box, controller, and poster to original sizes 25882 // this.positionAll(); 25883 25884 /** 25885 * @event Player#exitFullWindow 25886 * @type {EventTarget~Event} 25887 */ 25888 25889 this.trigger('exitFullWindow'); 25890 } 25891 /** 25892 * Check if the player is in Picture-in-Picture mode or tell the player that it 25893 * is or is not in Picture-in-Picture mode. 25894 * 25895 * @param {boolean} [isPiP] 25896 * Set the players current Picture-in-Picture state 25897 * 25898 * @return {boolean} 25899 * - true if Picture-in-Picture is on and getting 25900 * - false if Picture-in-Picture is off and getting 25901 */ 25902 ; 25903 25904 _proto.isInPictureInPicture = function isInPictureInPicture(isPiP) { 25905 if (isPiP !== undefined) { 25906 this.isInPictureInPicture_ = !!isPiP; 25907 this.togglePictureInPictureClass_(); 25908 return; 25909 } 25910 25911 return !!this.isInPictureInPicture_; 25912 } 25913 /** 25914 * Create a floating video window always on top of other windows so that users may 25915 * continue consuming media while they interact with other content sites, or 25916 * applications on their device. 25917 * 25918 * @see [Spec]{@link https://wicg.github.io/picture-in-picture} 25919 * 25920 * @fires Player#enterpictureinpicture 25921 * 25922 * @return {Promise} 25923 * A promise with a Picture-in-Picture window. 25924 */ 25925 ; 25926 25927 _proto.requestPictureInPicture = function requestPictureInPicture() { 25928 if ('pictureInPictureEnabled' in document) { 25929 /** 25930 * This event fires when the player enters picture in picture mode 25931 * 25932 * @event Player#enterpictureinpicture 25933 * @type {EventTarget~Event} 25934 */ 25935 return this.techGet_('requestPictureInPicture'); 25936 } 25937 } 25938 /** 25939 * Exit Picture-in-Picture mode. 25940 * 25941 * @see [Spec]{@link https://wicg.github.io/picture-in-picture} 25942 * 25943 * @fires Player#leavepictureinpicture 25944 * 25945 * @return {Promise} 25946 * A promise. 25947 */ 25948 ; 25949 25950 _proto.exitPictureInPicture = function exitPictureInPicture() { 25951 if ('pictureInPictureEnabled' in document) { 25952 /** 25953 * This event fires when the player leaves picture in picture mode 25954 * 25955 * @event Player#leavepictureinpicture 25956 * @type {EventTarget~Event} 25957 */ 25958 return document.exitPictureInPicture(); 25959 } 25960 } 25961 /** 25962 * Called when this Player has focus and a key gets pressed down, or when 25963 * any Component of this player receives a key press that it doesn't handle. 25964 * This allows player-wide hotkeys (either as defined below, or optionally 25965 * by an external function). 25966 * 25967 * @param {EventTarget~Event} event 25968 * The `keydown` event that caused this function to be called. 25969 * 25970 * @listens keydown 25971 */ 25972 ; 25973 25974 _proto.handleKeyDown = function handleKeyDown(event) { 25975 var userActions = this.options_.userActions; // Bail out if hotkeys are not configured. 25976 25977 if (!userActions || !userActions.hotkeys) { 25978 return; 25979 } // Function that determines whether or not to exclude an element from 25980 // hotkeys handling. 25981 25982 25983 var excludeElement = function excludeElement(el) { 25984 var tagName = el.tagName.toLowerCase(); // The first and easiest test is for `contenteditable` elements. 25985 25986 if (el.isContentEditable) { 25987 return true; 25988 } // Inputs matching these types will still trigger hotkey handling as 25989 // they are not text inputs. 25990 25991 25992 var allowedInputTypes = ['button', 'checkbox', 'hidden', 'radio', 'reset', 'submit']; 25993 25994 if (tagName === 'input') { 25995 return allowedInputTypes.indexOf(el.type) === -1; 25996 } // The final test is by tag name. These tags will be excluded entirely. 25997 25998 25999 var excludedTags = ['textarea']; 26000 return excludedTags.indexOf(tagName) !== -1; 26001 }; // Bail out if the user is focused on an interactive form element. 26002 26003 26004 if (excludeElement(this.el_.ownerDocument.activeElement)) { 26005 return; 26006 } 26007 26008 if (typeof userActions.hotkeys === 'function') { 26009 userActions.hotkeys.call(this, event); 26010 } else { 26011 this.handleHotkeys(event); 26012 } 26013 } 26014 /** 26015 * Called when this Player receives a hotkey keydown event. 26016 * Supported player-wide hotkeys are: 26017 * 26018 * f - toggle fullscreen 26019 * m - toggle mute 26020 * k or Space - toggle play/pause 26021 * 26022 * @param {EventTarget~Event} event 26023 * The `keydown` event that caused this function to be called. 26024 */ 26025 ; 26026 26027 _proto.handleHotkeys = function handleHotkeys(event) { 26028 var hotkeys = this.options_.userActions ? this.options_.userActions.hotkeys : {}; // set fullscreenKey, muteKey, playPauseKey from `hotkeys`, use defaults if not set 26029 26030 var _hotkeys$fullscreenKe = hotkeys.fullscreenKey, 26031 fullscreenKey = _hotkeys$fullscreenKe === void 0 ? function (keydownEvent) { 26032 return keycode.isEventKey(keydownEvent, 'f'); 26033 } : _hotkeys$fullscreenKe, 26034 _hotkeys$muteKey = hotkeys.muteKey, 26035 muteKey = _hotkeys$muteKey === void 0 ? function (keydownEvent) { 26036 return keycode.isEventKey(keydownEvent, 'm'); 26037 } : _hotkeys$muteKey, 26038 _hotkeys$playPauseKey = hotkeys.playPauseKey, 26039 playPauseKey = _hotkeys$playPauseKey === void 0 ? function (keydownEvent) { 26040 return keycode.isEventKey(keydownEvent, 'k') || keycode.isEventKey(keydownEvent, 'Space'); 26041 } : _hotkeys$playPauseKey; 26042 26043 if (fullscreenKey.call(this, event)) { 26044 event.preventDefault(); 26045 event.stopPropagation(); 26046 var FSToggle = Component.getComponent('FullscreenToggle'); 26047 26048 if (document[this.fsApi_.fullscreenEnabled] !== false) { 26049 FSToggle.prototype.handleClick.call(this, event); 26050 } 26051 } else if (muteKey.call(this, event)) { 26052 event.preventDefault(); 26053 event.stopPropagation(); 26054 var MuteToggle = Component.getComponent('MuteToggle'); 26055 MuteToggle.prototype.handleClick.call(this, event); 26056 } else if (playPauseKey.call(this, event)) { 26057 event.preventDefault(); 26058 event.stopPropagation(); 26059 var PlayToggle = Component.getComponent('PlayToggle'); 26060 PlayToggle.prototype.handleClick.call(this, event); 26061 } 26062 } 26063 /** 26064 * Check whether the player can play a given mimetype 26065 * 26066 * @see https://www.w3.org/TR/2011/WD-html5-20110113/video.html#dom-navigator-canplaytype 26067 * 26068 * @param {string} type 26069 * The mimetype to check 26070 * 26071 * @return {string} 26072 * 'probably', 'maybe', or '' (empty string) 26073 */ 26074 ; 26075 26076 _proto.canPlayType = function canPlayType(type) { 26077 var can; // Loop through each playback technology in the options order 26078 26079 for (var i = 0, j = this.options_.techOrder; i < j.length; i++) { 26080 var techName = j[i]; 26081 var tech = Tech.getTech(techName); // Support old behavior of techs being registered as components. 26082 // Remove once that deprecated behavior is removed. 26083 26084 if (!tech) { 26085 tech = Component.getComponent(techName); 26086 } // Check if the current tech is defined before continuing 26087 26088 26089 if (!tech) { 26090 log.error("The \"" + techName + "\" tech is undefined. Skipped browser support check for that tech."); 26091 continue; 26092 } // Check if the browser supports this technology 26093 26094 26095 if (tech.isSupported()) { 26096 can = tech.canPlayType(type); 26097 26098 if (can) { 26099 return can; 26100 } 26101 } 26102 } 26103 26104 return ''; 26105 } 26106 /** 26107 * Select source based on tech-order or source-order 26108 * Uses source-order selection if `options.sourceOrder` is truthy. Otherwise, 26109 * defaults to tech-order selection 26110 * 26111 * @param {Array} sources 26112 * The sources for a media asset 26113 * 26114 * @return {Object|boolean} 26115 * Object of source and tech order or false 26116 */ 26117 ; 26118 26119 _proto.selectSource = function selectSource(sources) { 26120 var _this10 = this; 26121 26122 // Get only the techs specified in `techOrder` that exist and are supported by the 26123 // current platform 26124 var techs = this.options_.techOrder.map(function (techName) { 26125 return [techName, Tech.getTech(techName)]; 26126 }).filter(function (_ref) { 26127 var techName = _ref[0], 26128 tech = _ref[1]; 26129 26130 // Check if the current tech is defined before continuing 26131 if (tech) { 26132 // Check if the browser supports this technology 26133 return tech.isSupported(); 26134 } 26135 26136 log.error("The \"" + techName + "\" tech is undefined. Skipped browser support check for that tech."); 26137 return false;
vendor: 5,368 bytes, lines 26138-26297
26138 }); // Iterate over each `innerArray` element once per `outerArray` element and execute 26139 // `tester` with both. If `tester` returns a non-falsy value, exit early and return 26140 // that value. 26141 26142 var findFirstPassingTechSourcePair = function findFirstPassingTechSourcePair(outerArray, innerArray, tester) { 26143 var found; 26144 outerArray.some(function (outerChoice) { 26145 return innerArray.some(function (innerChoice) { 26146 found = tester(outerChoice, innerChoice); 26147 26148 if (found) { 26149 return true; 26150 } 26151 }); 26152 }); 26153 return found; 26154 }; 26155 26156 var foundSourceAndTech; 26157 26158 var flip = function flip(fn) { 26159 return function (a, b) { 26160 return fn(b, a); 26161 }; 26162 }; 26163 26164 var finder = function finder(_ref2, source) { 26165 var techName = _ref2[0], 26166 tech = _ref2[1]; 26167 26168 if (tech.canPlaySource(source, _this10.options_[techName.toLowerCase()])) { 26169 return { 26170 source: source, 26171 tech: techName 26172 }; 26173 } 26174 }; // Depending on the truthiness of `options.sourceOrder`, we swap the order of techs and sources 26175 // to select from them based on their priority. 26176 26177 26178 if (this.options_.sourceOrder) { 26179 // Source-first ordering 26180 foundSourceAndTech = findFirstPassingTechSourcePair(sources, techs, flip(finder)); 26181 } else { 26182 // Tech-first ordering 26183 foundSourceAndTech = findFirstPassingTechSourcePair(techs, sources, finder); 26184 } 26185 26186 return foundSourceAndTech || false; 26187 } 26188 /** 26189 * Get or set the video source. 26190 * 26191 * @param {Tech~SourceObject|Tech~SourceObject[]|string} [source] 26192 * A SourceObject, an array of SourceObjects, or a string referencing 26193 * a URL to a media source. It is _highly recommended_ that an object 26194 * or array of objects is used here, so that source selection 26195 * algorithms can take the `type` into account. 26196 * 26197 * If not provided, this method acts as a getter. 26198 * 26199 * @return {string|undefined} 26200 * If the `source` argument is missing, returns the current source 26201 * URL. Otherwise, returns nothing/undefined. 26202 */ 26203 ; 26204 26205 _proto.src = function src(source) { 26206 var _this11 = this; 26207 26208 // getter usage 26209 if (typeof source === 'undefined') { 26210 return this.cache_.src || ''; 26211 } // filter out invalid sources and turn our source into 26212 // an array of source objects 26213 26214 26215 var sources = filterSource(source); // if a source was passed in then it is invalid because 26216 // it was filtered to a zero length Array. So we have to 26217 // show an error 26218 26219 if (!sources.length) { 26220 this.setTimeout(function () { 26221 this.error({ 26222 code: 4, 26223 message: this.localize(this.options_.notSupportedMessage) 26224 }); 26225 }, 0); 26226 return; 26227 } // intial sources 26228 26229 26230 this.changingSrc_ = true; 26231 this.cache_.sources = sources; 26232 this.updateSourceCaches_(sources[0]); // middlewareSource is the source after it has been changed by middleware 26233 26234 setSource(this, sources[0], function (middlewareSource, mws) { 26235 _this11.middleware_ = mws; // since sourceSet is async we have to update the cache again after we select a source since 26236 // the source that is selected could be out of order from the cache update above this callback. 26237 26238 _this11.cache_.sources = sources; 26239 26240 _this11.updateSourceCaches_(middlewareSource); 26241 26242 var err = _this11.src_(middlewareSource); 26243 26244 if (err) { 26245 if (sources.length > 1) { 26246 return _this11.src(sources.slice(1)); 26247 } 26248 26249 _this11.changingSrc_ = false; // We need to wrap this in a timeout to give folks a chance to add error event handlers 26250 26251 _this11.setTimeout(function () { 26252 this.error({ 26253 code: 4, 26254 message: this.localize(this.options_.notSupportedMessage) 26255 }); 26256 }, 0); // we could not find an appropriate tech, but let's still notify the delegate that this is it 26257 // this needs a better comment about why this is needed 26258 26259 26260 _this11.triggerReady(); 26261 26262 return; 26263 } 26264 26265 setTech(mws, _this11.tech_); 26266 }); 26267 } 26268 /** 26269 * Set the source object on the tech, returns a boolean that indicates whether 26270 * there is a tech that can play the source or not 26271 * 26272 * @param {Tech~SourceObject} source 26273 * The source object to set on the Tech 26274 * 26275 * @return {boolean} 26276 * - True if there is no Tech to playback this source 26277 * - False otherwise 26278 * 26279 * @private 26280 */ 26281 ; 26282 26283 _proto.src_ = function src_(source) { 26284 var _this12 = this; 26285 26286 var sourceTech = this.selectSource([source]); 26287 26288 if (!sourceTech) { 26289 return true; 26290 } 26291 26292 if (!titleCaseEquals(sourceTech.tech, this.techName_)) { 26293 this.changingSrc_ = true; // load this technology with the chosen source 26294 26295 this.loadTech_(sourceTech.tech, sourceTech.source); 26296 this.tech_.ready(function () { 26297 _this12.changingSrc_ = false;
vendor: 37,911 bytes, lines 26298-27480
26298 }); 26299 return false; 26300 } // wait until the tech is ready to set the source 26301 // and set it synchronously if possible (#2326) 26302 26303 26304 this.ready(function () { 26305 // The setSource tech method was added with source handlers 26306 // so older techs won't support it 26307 // We need to check the direct prototype for the case where subclasses 26308 // of the tech do not support source handlers 26309 if (this.tech_.constructor.prototype.hasOwnProperty('setSource')) { 26310 this.techCall_('setSource', source); 26311 } else { 26312 this.techCall_('src', source.src); 26313 } 26314 26315 this.changingSrc_ = false; 26316 }, true); 26317 return false; 26318 } 26319 /** 26320 * Begin loading the src data. 26321 */ 26322 ; 26323 26324 _proto.load = function load() { 26325 this.techCall_('load'); 26326 } 26327 /** 26328 * Reset the player. Loads the first tech in the techOrder, 26329 * removes all the text tracks in the existing `tech`, 26330 * and calls `reset` on the `tech`. 26331 */ 26332 ; 26333 26334 _proto.reset = function reset() { 26335 var _this13 = this; 26336 26337 var PromiseClass = this.options_.Promise || window$1.Promise; 26338 26339 if (this.paused() || !PromiseClass) { 26340 this.doReset_(); 26341 } else { 26342 var playPromise = this.play(); 26343 silencePromise(playPromise.then(function () { 26344 return _this13.doReset_(); 26345 })); 26346 } 26347 }; 26348 26349 _proto.doReset_ = function doReset_() { 26350 if (this.tech_) { 26351 this.tech_.clearTracks('text'); 26352 } 26353 26354 this.resetCache_(); 26355 this.poster(''); 26356 this.loadTech_(this.options_.techOrder[0], null); 26357 this.techCall_('reset'); 26358 this.resetControlBarUI_(); 26359 26360 if (isEvented(this)) { 26361 this.trigger('playerreset'); 26362 } 26363 } 26364 /** 26365 * Reset Control Bar's UI by calling sub-methods that reset 26366 * all of Control Bar's components 26367 */ 26368 ; 26369 26370 _proto.resetControlBarUI_ = function resetControlBarUI_() { 26371 this.resetProgressBar_(); 26372 this.resetPlaybackRate_(); 26373 this.resetVolumeBar_(); 26374 } 26375 /** 26376 * Reset tech's progress so progress bar is reset in the UI 26377 */ 26378 ; 26379 26380 _proto.resetProgressBar_ = function resetProgressBar_() { 26381 this.currentTime(0); 26382 var _this$controlBar = this.controlBar, 26383 durationDisplay = _this$controlBar.durationDisplay, 26384 remainingTimeDisplay = _this$controlBar.remainingTimeDisplay; 26385 26386 if (durationDisplay) { 26387 durationDisplay.updateContent(); 26388 } 26389 26390 if (remainingTimeDisplay) { 26391 remainingTimeDisplay.updateContent(); 26392 } 26393 } 26394 /** 26395 * Reset Playback ratio 26396 */ 26397 ; 26398 26399 _proto.resetPlaybackRate_ = function resetPlaybackRate_() { 26400 this.playbackRate(this.defaultPlaybackRate()); 26401 this.handleTechRateChange_(); 26402 } 26403 /** 26404 * Reset Volume bar 26405 */ 26406 ; 26407 26408 _proto.resetVolumeBar_ = function resetVolumeBar_() { 26409 this.volume(1.0); 26410 this.trigger('volumechange'); 26411 } 26412 /** 26413 * Returns all of the current source objects. 26414 * 26415 * @return {Tech~SourceObject[]} 26416 * The current source objects 26417 */ 26418 ; 26419 26420 _proto.currentSources = function currentSources() { 26421 var source = this.currentSource(); 26422 var sources = []; // assume `{}` or `{ src }` 26423 26424 if (Object.keys(source).length !== 0) { 26425 sources.push(source); 26426 } 26427 26428 return this.cache_.sources || sources; 26429 } 26430 /** 26431 * Returns the current source object. 26432 * 26433 * @return {Tech~SourceObject} 26434 * The current source object 26435 */ 26436 ; 26437 26438 _proto.currentSource = function currentSource() { 26439 return this.cache_.source || {}; 26440 } 26441 /** 26442 * Returns the fully qualified URL of the current source value e.g. http://mysite.com/video.mp4 26443 * Can be used in conjunction with `currentType` to assist in rebuilding the current source object. 26444 * 26445 * @return {string} 26446 * The current source 26447 */ 26448 ; 26449 26450 _proto.currentSrc = function currentSrc() { 26451 return this.currentSource() && this.currentSource().src || ''; 26452 } 26453 /** 26454 * Get the current source type e.g. video/mp4 26455 * This can allow you rebuild the current source object so that you could load the same 26456 * source and tech later 26457 * 26458 * @return {string} 26459 * The source MIME type 26460 */ 26461 ; 26462 26463 _proto.currentType = function currentType() { 26464 return this.currentSource() && this.currentSource().type || ''; 26465 } 26466 /** 26467 * Get or set the preload attribute 26468 * 26469 * @param {boolean} [value] 26470 * - true means that we should preload 26471 * - false means that we should not preload 26472 * 26473 * @return {string} 26474 * The preload attribute value when getting 26475 */ 26476 ; 26477 26478 _proto.preload = function preload(value) { 26479 if (value !== undefined) { 26480 this.techCall_('setPreload', value); 26481 this.options_.preload = value; 26482 return; 26483 } 26484 26485 return this.techGet_('preload'); 26486 } 26487 /** 26488 * Get or set the autoplay option. When this is a boolean it will 26489 * modify the attribute on the tech. When this is a string the attribute on 26490 * the tech will be removed and `Player` will handle autoplay on loadstarts. 26491 * 26492 * @param {boolean|string} [value] 26493 * - true: autoplay using the browser behavior 26494 * - false: do not autoplay 26495 * - 'play': call play() on every loadstart 26496 * - 'muted': call muted() then play() on every loadstart 26497 * - 'any': call play() on every loadstart. if that fails call muted() then play(). 26498 * - *: values other than those listed here will be set `autoplay` to true 26499 * 26500 * @return {boolean|string} 26501 * The current value of autoplay when getting 26502 */ 26503 ; 26504 26505 _proto.autoplay = function autoplay(value) { 26506 // getter usage 26507 if (value === undefined) { 26508 return this.options_.autoplay || false; 26509 } 26510 26511 var techAutoplay; // if the value is a valid string set it to that 26512 26513 if (typeof value === 'string' && /(any|play|muted)/.test(value)) { 26514 this.options_.autoplay = value; 26515 this.manualAutoplay_(value); 26516 techAutoplay = false; // any falsy value sets autoplay to false in the browser, 26517 // lets do the same 26518 } else if (!value) { 26519 this.options_.autoplay = false; // any other value (ie truthy) sets autoplay to true 26520 } else { 26521 this.options_.autoplay = true; 26522 } 26523 26524 techAutoplay = typeof techAutoplay === 'undefined' ? this.options_.autoplay : techAutoplay; // if we don't have a tech then we do not queue up 26525 // a setAutoplay call on tech ready. We do this because the 26526 // autoplay option will be passed in the constructor and we 26527 // do not need to set it twice 26528 26529 if (this.tech_) { 26530 this.techCall_('setAutoplay', techAutoplay); 26531 } 26532 } 26533 /** 26534 * Set or unset the playsinline attribute. 26535 * Playsinline tells the browser that non-fullscreen playback is preferred. 26536 * 26537 * @param {boolean} [value] 26538 * - true means that we should try to play inline by default 26539 * - false means that we should use the browser's default playback mode, 26540 * which in most cases is inline. iOS Safari is a notable exception 26541 * and plays fullscreen by default. 26542 * 26543 * @return {string|Player} 26544 * - the current value of playsinline 26545 * - the player when setting 26546 * 26547 * @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline} 26548 */ 26549 ; 26550 26551 _proto.playsinline = function playsinline(value) { 26552 if (value !== undefined) { 26553 this.techCall_('setPlaysinline', value); 26554 this.options_.playsinline = value; 26555 return this; 26556 } 26557 26558 return this.techGet_('playsinline'); 26559 } 26560 /** 26561 * Get or set the loop attribute on the video element. 26562 * 26563 * @param {boolean} [value] 26564 * - true means that we should loop the video 26565 * - false means that we should not loop the video 26566 * 26567 * @return {boolean} 26568 * The current value of loop when getting 26569 */ 26570 ; 26571 26572 _proto.loop = function loop(value) { 26573 if (value !== undefined) { 26574 this.techCall_('setLoop', value); 26575 this.options_.loop = value; 26576 return; 26577 } 26578 26579 return this.techGet_('loop'); 26580 } 26581 /** 26582 * Get or set the poster image source url 26583 * 26584 * @fires Player#posterchange 26585 * 26586 * @param {string} [src] 26587 * Poster image source URL 26588 * 26589 * @return {string} 26590 * The current value of poster when getting 26591 */ 26592 ; 26593 26594 _proto.poster = function poster(src) { 26595 if (src === undefined) { 26596 return this.poster_; 26597 } // The correct way to remove a poster is to set as an empty string 26598 // other falsey values will throw errors 26599 26600 26601 if (!src) { 26602 src = ''; 26603 } 26604 26605 if (src === this.poster_) { 26606 return; 26607 } // update the internal poster variable 26608 26609 26610 this.poster_ = src; // update the tech's poster 26611 26612 this.techCall_('setPoster', src); 26613 this.isPosterFromTech_ = false; // alert components that the poster has been set 26614 26615 /** 26616 * This event fires when the poster image is changed on the player. 26617 * 26618 * @event Player#posterchange 26619 * @type {EventTarget~Event} 26620 */ 26621 26622 this.trigger('posterchange'); 26623 } 26624 /** 26625 * Some techs (e.g. YouTube) can provide a poster source in an 26626 * asynchronous way. We want the poster component to use this 26627 * poster source so that it covers up the tech's controls. 26628 * (YouTube's play button). However we only want to use this 26629 * source if the player user hasn't set a poster through 26630 * the normal APIs. 26631 * 26632 * @fires Player#posterchange 26633 * @listens Tech#posterchange 26634 * @private 26635 */ 26636 ; 26637 26638 _proto.handleTechPosterChange_ = function handleTechPosterChange_() { 26639 if ((!this.poster_ || this.options_.techCanOverridePoster) && this.tech_ && this.tech_.poster) { 26640 var newPoster = this.tech_.poster() || ''; 26641 26642 if (newPoster !== this.poster_) { 26643 this.poster_ = newPoster; 26644 this.isPosterFromTech_ = true; // Let components know the poster has changed 26645 26646 this.trigger('posterchange'); 26647 } 26648 } 26649 } 26650 /** 26651 * Get or set whether or not the controls are showing. 26652 * 26653 * @fires Player#controlsenabled 26654 * 26655 * @param {boolean} [bool] 26656 * - true to turn controls on 26657 * - false to turn controls off 26658 * 26659 * @return {boolean} 26660 * The current value of controls when getting 26661 */ 26662 ; 26663 26664 _proto.controls = function controls(bool) { 26665 if (bool === undefined) { 26666 return !!this.controls_; 26667 } 26668 26669 bool = !!bool; // Don't trigger a change event unless it actually changed 26670 26671 if (this.controls_ === bool) { 26672 return; 26673 } 26674 26675 this.controls_ = bool; 26676 26677 if (this.usingNativeControls()) { 26678 this.techCall_('setControls', bool); 26679 } 26680 26681 if (this.controls_) { 26682 this.removeClass('vjs-controls-disabled'); 26683 this.addClass('vjs-controls-enabled'); 26684 /** 26685 * @event Player#controlsenabled 26686 * @type {EventTarget~Event} 26687 */ 26688 26689 this.trigger('controlsenabled'); 26690 26691 if (!this.usingNativeControls()) { 26692 this.addTechControlsListeners_(); 26693 } 26694 } else { 26695 this.removeClass('vjs-controls-enabled'); 26696 this.addClass('vjs-controls-disabled'); 26697 /** 26698 * @event Player#controlsdisabled 26699 * @type {EventTarget~Event} 26700 */ 26701 26702 this.trigger('controlsdisabled'); 26703 26704 if (!this.usingNativeControls()) { 26705 this.removeTechControlsListeners_(); 26706 } 26707 } 26708 } 26709 /** 26710 * Toggle native controls on/off. Native controls are the controls built into 26711 * devices (e.g. default iPhone controls), Flash, or other techs 26712 * (e.g. Vimeo Controls) 26713 * **This should only be set by the current tech, because only the tech knows 26714 * if it can support native controls** 26715 * 26716 * @fires Player#usingnativecontrols 26717 * @fires Player#usingcustomcontrols 26718 * 26719 * @param {boolean} [bool] 26720 * - true to turn native controls on 26721 * - false to turn native controls off 26722 * 26723 * @return {boolean} 26724 * The current value of native controls when getting 26725 */ 26726 ; 26727 26728 _proto.usingNativeControls = function usingNativeControls(bool) { 26729 if (bool === undefined) { 26730 return !!this.usingNativeControls_; 26731 } 26732 26733 bool = !!bool; // Don't trigger a change event unless it actually changed 26734 26735 if (this.usingNativeControls_ === bool) { 26736 return; 26737 } 26738 26739 this.usingNativeControls_ = bool; 26740 26741 if (this.usingNativeControls_) { 26742 this.addClass('vjs-using-native-controls'); 26743 /** 26744 * player is using the native device controls 26745 * 26746 * @event Player#usingnativecontrols 26747 * @type {EventTarget~Event} 26748 */ 26749 26750 this.trigger('usingnativecontrols'); 26751 } else { 26752 this.removeClass('vjs-using-native-controls'); 26753 /** 26754 * player is using the custom HTML controls 26755 * 26756 * @event Player#usingcustomcontrols 26757 * @type {EventTarget~Event} 26758 */ 26759 26760 this.trigger('usingcustomcontrols'); 26761 } 26762 } 26763 /** 26764 * Set or get the current MediaError 26765 * 26766 * @fires Player#error 26767 * 26768 * @param {MediaError|string|number} [err] 26769 * A MediaError or a string/number to be turned 26770 * into a MediaError 26771 * 26772 * @return {MediaError|null} 26773 * The current MediaError when getting (or null) 26774 */ 26775 ; 26776 26777 _proto.error = function error(err) { 26778 if (err === undefined) { 26779 return this.error_ || null; 26780 } // Suppress the first error message for no compatible source until 26781 // user interaction 26782 26783 26784 if (this.options_.suppressNotSupportedError && err && err.code === 4) { 26785 var triggerSuppressedError = function triggerSuppressedError() { 26786 this.error(err); 26787 }; 26788 26789 this.options_.suppressNotSupportedError = false; 26790 this.any(['click', 'touchstart'], triggerSuppressedError); 26791 this.one('loadstart', function () { 26792 this.off(['click', 'touchstart'], triggerSuppressedError); 26793 }); 26794 return; 26795 } // restoring to default 26796 26797 26798 if (err === null) { 26799 this.error_ = err; 26800 this.removeClass('vjs-error'); 26801 26802 if (this.errorDisplay) { 26803 this.errorDisplay.close(); 26804 } 26805 26806 return; 26807 } 26808 26809 this.error_ = new MediaError(err); // add the vjs-error classname to the player 26810 26811 this.addClass('vjs-error'); // log the name of the error type and any message 26812 // IE11 logs "[object object]" and required you to expand message to see error object 26813 26814 log.error("(CODE:" + this.error_.code + " " + MediaError.errorTypes[this.error_.code] + ")", this.error_.message, this.error_); 26815 /** 26816 * @event Player#error 26817 * @type {EventTarget~Event} 26818 */ 26819 26820 this.trigger('error'); 26821 return; 26822 } 26823 /** 26824 * Report user activity 26825 * 26826 * @param {Object} event 26827 * Event object 26828 */ 26829 ; 26830 26831 _proto.reportUserActivity = function reportUserActivity(event) { 26832 this.userActivity_ = true; 26833 } 26834 /** 26835 * Get/set if user is active 26836 * 26837 * @fires Player#useractive 26838 * @fires Player#userinactive 26839 * 26840 * @param {boolean} [bool] 26841 * - true if the user is active 26842 * - false if the user is inactive 26843 * 26844 * @return {boolean} 26845 * The current value of userActive when getting 26846 */ 26847 ; 26848 26849 _proto.userActive = function userActive(bool) { 26850 if (bool === undefined) { 26851 return this.userActive_; 26852 } 26853 26854 bool = !!bool; 26855 26856 if (bool === this.userActive_) { 26857 return; 26858 } 26859 26860 this.userActive_ = bool; 26861 26862 if (this.userActive_) { 26863 this.userActivity_ = true; 26864 this.removeClass('vjs-user-inactive'); 26865 this.addClass('vjs-user-active'); 26866 /** 26867 * @event Player#useractive 26868 * @type {EventTarget~Event} 26869 */ 26870 26871 this.trigger('useractive'); 26872 return; 26873 } // Chrome/Safari/IE have bugs where when you change the cursor it can 26874 // trigger a mousemove event. This causes an issue when you're hiding 26875 // the cursor when the user is inactive, and a mousemove signals user 26876 // activity. Making it impossible to go into inactive mode. Specifically 26877 // this happens in fullscreen when we really need to hide the cursor. 26878 // 26879 // When this gets resolved in ALL browsers it can be removed 26880 // https://code.google.com/p/chromium/issues/detail?id=103041 26881 26882 26883 if (this.tech_) { 26884 this.tech_.one('mousemove', function (e) { 26885 e.stopPropagation(); 26886 e.preventDefault(); 26887 }); 26888 } 26889 26890 this.userActivity_ = false; 26891 this.removeClass('vjs-user-active'); 26892 this.addClass('vjs-user-inactive'); 26893 /** 26894 * @event Player#userinactive 26895 * @type {EventTarget~Event} 26896 */ 26897 26898 this.trigger('userinactive'); 26899 } 26900 /** 26901 * Listen for user activity based on timeout value 26902 * 26903 * @private 26904 */ 26905 ; 26906 26907 _proto.listenForUserActivity_ = function listenForUserActivity_() { 26908 var mouseInProgress; 26909 var lastMoveX; 26910 var lastMoveY; 26911 var handleActivity = bind(this, this.reportUserActivity); 26912 26913 var handleMouseMove = function handleMouseMove(e) { 26914 // #1068 - Prevent mousemove spamming 26915 // Chrome Bug: https://code.google.com/p/chromium/issues/detail?id=366970 26916 if (e.screenX !== lastMoveX || e.screenY !== lastMoveY) { 26917 lastMoveX = e.screenX; 26918 lastMoveY = e.screenY; 26919 handleActivity(); 26920 } 26921 }; 26922 26923 var handleMouseDown = function handleMouseDown() { 26924 handleActivity(); // For as long as the they are touching the device or have their mouse down, 26925 // we consider them active even if they're not moving their finger or mouse. 26926 // So we want to continue to update that they are active 26927 26928 this.clearInterval(mouseInProgress); // Setting userActivity=true now and setting the interval to the same time 26929 // as the activityCheck interval (250) should ensure we never miss the 26930 // next activityCheck 26931 26932 mouseInProgress = this.setInterval(handleActivity, 250); 26933 }; 26934 26935 var handleMouseUp = function handleMouseUp(event) { 26936 handleActivity(); // Stop the interval that maintains activity if the mouse/touch is down 26937 26938 this.clearInterval(mouseInProgress); 26939 }; // Any mouse movement will be considered user activity 26940 26941 26942 this.on('mousedown', handleMouseDown); 26943 this.on('mousemove', handleMouseMove); 26944 this.on('mouseup', handleMouseUp); 26945 var controlBar = this.getChild('controlBar'); // Fixes bug on Android & iOS where when tapping progressBar (when control bar is displayed) 26946 // controlBar would no longer be hidden by default timeout. 26947 26948 if (controlBar && !IS_IOS && !IS_ANDROID) { 26949 controlBar.on('mouseenter', function (event) { 26950 this.player().cache_.inactivityTimeout = this.player().options_.inactivityTimeout; 26951 this.player().options_.inactivityTimeout = 0; 26952 }); 26953 controlBar.on('mouseleave', function (event) { 26954 this.player().options_.inactivityTimeout = this.player().cache_.inactivityTimeout; 26955 }); 26956 } // Listen for keyboard navigation 26957 // Shouldn't need to use inProgress interval because of key repeat 26958 26959 26960 this.on('keydown', handleActivity); 26961 this.on('keyup', handleActivity); // Run an interval every 250 milliseconds instead of stuffing everything into 26962 // the mousemove/touchmove function itself, to prevent performance degradation. 26963 // `this.reportUserActivity` simply sets this.userActivity_ to true, which 26964 // then gets picked up by this loop 26965 // http://ejohn.org/blog/learning-from-twitter/ 26966 26967 var inactivityTimeout; 26968 this.setInterval(function () { 26969 // Check to see if mouse/touch activity has happened 26970 if (!this.userActivity_) { 26971 return; 26972 } // Reset the activity tracker 26973 26974 26975 this.userActivity_ = false; // If the user state was inactive, set the state to active 26976 26977 this.userActive(true); // Clear any existing inactivity timeout to start the timer over 26978 26979 this.clearTimeout(inactivityTimeout); 26980 var timeout = this.options_.inactivityTimeout; 26981 26982 if (timeout <= 0) { 26983 return; 26984 } // In <timeout> milliseconds, if no more activity has occurred the 26985 // user will be considered inactive 26986 26987 26988 inactivityTimeout = this.setTimeout(function () { 26989 // Protect against the case where the inactivityTimeout can trigger just 26990 // before the next user activity is picked up by the activity check loop 26991 // causing a flicker 26992 if (!this.userActivity_) { 26993 this.userActive(false); 26994 } 26995 }, timeout); 26996 }, 250); 26997 } 26998 /** 26999 * Gets or sets the current playback rate. A playback rate of 27000 * 1.0 represents normal speed and 0.5 would indicate half-speed 27001 * playback, for instance. 27002 * 27003 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-playbackrate 27004 * 27005 * @param {number} [rate] 27006 * New playback rate to set. 27007 * 27008 * @return {number} 27009 * The current playback rate when getting or 1.0 27010 */ 27011 ; 27012 27013 _proto.playbackRate = function playbackRate(rate) { 27014 if (rate !== undefined) { 27015 // NOTE: this.cache_.lastPlaybackRate is set from the tech handler 27016 // that is registered above 27017 this.techCall_('setPlaybackRate', rate); 27018 return; 27019 } 27020 27021 if (this.tech_ && this.tech_.featuresPlaybackRate) { 27022 return this.cache_.lastPlaybackRate || this.techGet_('playbackRate'); 27023 } 27024 27025 return 1.0; 27026 } 27027 /** 27028 * Gets or sets the current default playback rate. A default playback rate of 27029 * 1.0 represents normal speed and 0.5 would indicate half-speed playback, for instance. 27030 * defaultPlaybackRate will only represent what the initial playbackRate of a video was, not 27031 * not the current playbackRate. 27032 * 27033 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-defaultplaybackrate 27034 * 27035 * @param {number} [rate] 27036 * New default playback rate to set. 27037 * 27038 * @return {number|Player} 27039 * - The default playback rate when getting or 1.0 27040 * - the player when setting 27041 */ 27042 ; 27043 27044 _proto.defaultPlaybackRate = function defaultPlaybackRate(rate) { 27045 if (rate !== undefined) { 27046 return this.techCall_('setDefaultPlaybackRate', rate); 27047 } 27048 27049 if (this.tech_ && this.tech_.featuresPlaybackRate) { 27050 return this.techGet_('defaultPlaybackRate'); 27051 } 27052 27053 return 1.0; 27054 } 27055 /** 27056 * Gets or sets the audio flag 27057 * 27058 * @param {boolean} bool 27059 * - true signals that this is an audio player 27060 * - false signals that this is not an audio player 27061 * 27062 * @return {boolean} 27063 * The current value of isAudio when getting 27064 */ 27065 ; 27066 27067 _proto.isAudio = function isAudio(bool) { 27068 if (bool !== undefined) { 27069 this.isAudio_ = !!bool; 27070 return; 27071 } 27072 27073 return !!this.isAudio_; 27074 } 27075 /** 27076 * A helper method for adding a {@link TextTrack} to our 27077 * {@link TextTrackList}. 27078 * 27079 * In addition to the W3C settings we allow adding additional info through options. 27080 * 27081 * @see http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-addtexttrack 27082 * 27083 * @param {string} [kind] 27084 * the kind of TextTrack you are adding 27085 * 27086 * @param {string} [label] 27087 * the label to give the TextTrack label 27088 * 27089 * @param {string} [language] 27090 * the language to set on the TextTrack 27091 * 27092 * @return {TextTrack|undefined} 27093 * the TextTrack that was added or undefined 27094 * if there is no tech 27095 */ 27096 ; 27097 27098 _proto.addTextTrack = function addTextTrack(kind, label, language) { 27099 if (this.tech_) { 27100 return this.tech_.addTextTrack(kind, label, language); 27101 } 27102 } 27103 /** 27104 * Create a remote {@link TextTrack} and an {@link HTMLTrackElement}. It will 27105 * automatically removed from the video element whenever the source changes, unless 27106 * manualCleanup is set to false. 27107 * 27108 * @param {Object} options 27109 * Options to pass to {@link HTMLTrackElement} during creation. See 27110 * {@link HTMLTrackElement} for object properties that you should use. 27111 * 27112 * @param {boolean} [manualCleanup=true] if set to false, the TextTrack will be 27113 * 27114 * @return {HtmlTrackElement} 27115 * the HTMLTrackElement that was created and added 27116 * to the HtmlTrackElementList and the remote 27117 * TextTrackList 27118 * 27119 * @deprecated The default value of the "manualCleanup" parameter will default 27120 * to "false" in upcoming versions of Video.js 27121 */ 27122 ; 27123 27124 _proto.addRemoteTextTrack = function addRemoteTextTrack(options, manualCleanup) { 27125 if (this.tech_) { 27126 return this.tech_.addRemoteTextTrack(options, manualCleanup); 27127 } 27128 } 27129 /** 27130 * Remove a remote {@link TextTrack} from the respective 27131 * {@link TextTrackList} and {@link HtmlTrackElementList}. 27132 * 27133 * @param {Object} track 27134 * Remote {@link TextTrack} to remove 27135 * 27136 * @return {undefined} 27137 * does not return anything 27138 */ 27139 ; 27140 27141 _proto.removeRemoteTextTrack = function removeRemoteTextTrack(obj) { 27142 if (obj === void 0) { 27143 obj = {}; 27144 } 27145 27146 var _obj = obj, 27147 track = _obj.track; 27148 27149 if (!track) { 27150 track = obj; 27151 } // destructure the input into an object with a track argument, defaulting to arguments[0] 27152 // default the whole argument to an empty object if nothing was passed in 27153 27154 27155 if (this.tech_) { 27156 return this.tech_.removeRemoteTextTrack(track); 27157 } 27158 } 27159 /** 27160 * Gets available media playback quality metrics as specified by the W3C's Media 27161 * Playback Quality API. 27162 * 27163 * @see [Spec]{@link https://wicg.github.io/media-playback-quality} 27164 * 27165 * @return {Object|undefined} 27166 * An object with supported media playback quality metrics or undefined if there 27167 * is no tech or the tech does not support it. 27168 */ 27169 ; 27170 27171 _proto.getVideoPlaybackQuality = function getVideoPlaybackQuality() { 27172 return this.techGet_('getVideoPlaybackQuality'); 27173 } 27174 /** 27175 * Get video width 27176 * 27177 * @return {number} 27178 * current video width 27179 */ 27180 ; 27181 27182 _proto.videoWidth = function videoWidth() { 27183 return this.tech_ && this.tech_.videoWidth && this.tech_.videoWidth() || 0; 27184 } 27185 /** 27186 * Get video height 27187 * 27188 * @return {number} 27189 * current video height 27190 */ 27191 ; 27192 27193 _proto.videoHeight = function videoHeight() { 27194 return this.tech_ && this.tech_.videoHeight && this.tech_.videoHeight() || 0; 27195 } 27196 /** 27197 * The player's language code 27198 * NOTE: The language should be set in the player options if you want the 27199 * the controls to be built with a specific language. Changing the language 27200 * later will not update controls text. 27201 * 27202 * @param {string} [code] 27203 * the language code to set the player to 27204 * 27205 * @return {string} 27206 * The current language code when getting 27207 */ 27208 ; 27209 27210 _proto.language = function language(code) { 27211 if (code === undefined) { 27212 return this.language_; 27213 } 27214 27215 this.language_ = String(code).toLowerCase(); 27216 } 27217 /** 27218 * Get the player's language dictionary 27219 * Merge every time, because a newly added plugin might call videojs.addLanguage() at any time 27220 * Languages specified directly in the player options have precedence 27221 * 27222 * @return {Array} 27223 * An array of of supported languages 27224 */ 27225 ; 27226 27227 _proto.languages = function languages() { 27228 return mergeOptions(Player.prototype.options_.languages, this.languages_); 27229 } 27230 /** 27231 * returns a JavaScript object reperesenting the current track 27232 * information. **DOES not return it as JSON** 27233 * 27234 * @return {Object} 27235 * Object representing the current of track info 27236 */ 27237 ; 27238 27239 _proto.toJSON = function toJSON() { 27240 var options = mergeOptions(this.options_); 27241 var tracks = options.tracks; 27242 options.tracks = []; 27243 27244 for (var i = 0; i < tracks.length; i++) { 27245 var track = tracks[i]; // deep merge tracks and null out player so no circular references 27246 27247 track = mergeOptions(track); 27248 track.player = undefined; 27249 options.tracks[i] = track; 27250 } 27251 27252 return options; 27253 } 27254 /** 27255 * Creates a simple modal dialog (an instance of the {@link ModalDialog} 27256 * component) that immediately overlays the player with arbitrary 27257 * content and removes itself when closed. 27258 * 27259 * @param {string|Function|Element|Array|null} content 27260 * Same as {@link ModalDialog#content}'s param of the same name. 27261 * The most straight-forward usage is to provide a string or DOM 27262 * element. 27263 * 27264 * @param {Object} [options] 27265 * Extra options which will be passed on to the {@link ModalDialog}. 27266 * 27267 * @return {ModalDialog} 27268 * the {@link ModalDialog} that was created 27269 */ 27270 ; 27271 27272 _proto.createModal = function createModal(content, options) { 27273 var _this14 = this; 27274 27275 options = options || {}; 27276 options.content = content || ''; 27277 var modal = new ModalDialog(this, options); 27278 this.addChild(modal); 27279 modal.on('dispose', function () { 27280 _this14.removeChild(modal); 27281 }); 27282 modal.open(); 27283 return modal; 27284 } 27285 /** 27286 * Change breakpoint classes when the player resizes. 27287 * 27288 * @private 27289 */ 27290 ; 27291 27292 _proto.updateCurrentBreakpoint_ = function updateCurrentBreakpoint_() { 27293 if (!this.responsive()) { 27294 return; 27295 } 27296 27297 var currentBreakpoint = this.currentBreakpoint(); 27298 var currentWidth = this.currentWidth(); 27299 27300 for (var i = 0; i < BREAKPOINT_ORDER.length; i++) { 27301 var candidateBreakpoint = BREAKPOINT_ORDER[i]; 27302 var maxWidth = this.breakpoints_[candidateBreakpoint]; 27303 27304 if (currentWidth <= maxWidth) { 27305 // The current breakpoint did not change, nothing to do. 27306 if (currentBreakpoint === candidateBreakpoint) { 27307 return; 27308 } // Only remove a class if there is a current breakpoint. 27309 27310 27311 if (currentBreakpoint) { 27312 this.removeClass(BREAKPOINT_CLASSES[currentBreakpoint]); 27313 } 27314 27315 this.addClass(BREAKPOINT_CLASSES[candidateBreakpoint]); 27316 this.breakpoint_ = candidateBreakpoint; 27317 break; 27318 } 27319 } 27320 } 27321 /** 27322 * Removes the current breakpoint. 27323 * 27324 * @private 27325 */ 27326 ; 27327 27328 _proto.removeCurrentBreakpoint_ = function removeCurrentBreakpoint_() { 27329 var className = this.currentBreakpointClass(); 27330 this.breakpoint_ = ''; 27331 27332 if (className) { 27333 this.removeClass(className); 27334 } 27335 } 27336 /** 27337 * Get or set breakpoints on the player. 27338 * 27339 * Calling this method with an object or `true` will remove any previous 27340 * custom breakpoints and start from the defaults again. 27341 * 27342 * @param {Object|boolean} [breakpoints] 27343 * If an object is given, it can be used to provide custom 27344 * breakpoints. If `true` is given, will set default breakpoints. 27345 * If this argument is not given, will simply return the current 27346 * breakpoints. 27347 * 27348 * @param {number} [breakpoints.tiny] 27349 * The maximum width for the "vjs-layout-tiny" class. 27350 * 27351 * @param {number} [breakpoints.xsmall] 27352 * The maximum width for the "vjs-layout-x-small" class. 27353 * 27354 * @param {number} [breakpoints.small] 27355 * The maximum width for the "vjs-layout-small" class. 27356 * 27357 * @param {number} [breakpoints.medium] 27358 * The maximum width for the "vjs-layout-medium" class. 27359 * 27360 * @param {number} [breakpoints.large] 27361 * The maximum width for the "vjs-layout-large" class. 27362 * 27363 * @param {number} [breakpoints.xlarge] 27364 * The maximum width for the "vjs-layout-x-large" class. 27365 * 27366 * @param {number} [breakpoints.huge] 27367 * The maximum width for the "vjs-layout-huge" class. 27368 * 27369 * @return {Object} 27370 * An object mapping breakpoint names to maximum width values. 27371 */ 27372 ; 27373 27374 _proto.breakpoints = function breakpoints(_breakpoints) { 27375 // Used as a getter. 27376 if (_breakpoints === undefined) { 27377 return assign(this.breakpoints_); 27378 } 27379 27380 this.breakpoint_ = ''; 27381 this.breakpoints_ = assign({}, DEFAULT_BREAKPOINTS, _breakpoints); // When breakpoint definitions change, we need to update the currently 27382 // selected breakpoint. 27383 27384 this.updateCurrentBreakpoint_(); // Clone the breakpoints before returning. 27385 27386 return assign(this.breakpoints_); 27387 } 27388 /** 27389 * Get or set a flag indicating whether or not this player should adjust 27390 * its UI based on its dimensions. 27391 * 27392 * @param {boolean} value 27393 * Should be `true` if the player should adjust its UI based on its 27394 * dimensions; otherwise, should be `false`. 27395 * 27396 * @return {boolean} 27397 * Will be `true` if this player should adjust its UI based on its 27398 * dimensions; otherwise, will be `false`. 27399 */ 27400 ; 27401 27402 _proto.responsive = function responsive(value) { 27403 // Used as a getter. 27404 if (value === undefined) { 27405 return this.responsive_; 27406 } 27407 27408 value = Boolean(value); 27409 var current = this.responsive_; // Nothing changed. 27410 27411 if (value === current) { 27412 return; 27413 } // The value actually changed, set it. 27414 27415 27416 this.responsive_ = value; // Start listening for breakpoints and set the initial breakpoint if the 27417 // player is now responsive. 27418 27419 if (value) { 27420 this.on('playerresize', this.updateCurrentBreakpoint_); 27421 this.updateCurrentBreakpoint_(); // Stop listening for breakpoints if the player is no longer responsive. 27422 } else { 27423 this.off('playerresize', this.updateCurrentBreakpoint_); 27424 this.removeCurrentBreakpoint_(); 27425 } 27426 27427 return value; 27428 } 27429 /** 27430 * Get current breakpoint name, if any. 27431 * 27432 * @return {string} 27433 * If there is currently a breakpoint set, returns a the key from the 27434 * breakpoints object matching it. Otherwise, returns an empty string. 27435 */ 27436 ; 27437 27438 _proto.currentBreakpoint = function currentBreakpoint() { 27439 return this.breakpoint_; 27440 } 27441 /** 27442 * Get the current breakpoint class name. 27443 * 27444 * @return {string} 27445 * The matching class name (e.g. `"vjs-layout-tiny"` or 27446 * `"vjs-layout-large"`) for the current breakpoint. Empty string if 27447 * there is no current breakpoint. 27448 */ 27449 ; 27450 27451 _proto.currentBreakpointClass = function currentBreakpointClass() { 27452 return BREAKPOINT_CLASSES[this.breakpoint_] || ''; 27453 } 27454 /** 27455 * An object that describes a single piece of media. 27456 * 27457 * Properties that are not part of this type description will be retained; so, 27458 * this can be viewed as a generic metadata storage mechanism as well. 27459 * 27460 * @see {@link https://wicg.github.io/mediasession/#the-mediametadata-interface} 27461 * @typedef {Object} Player~MediaObject 27462 * 27463 * @property {string} [album] 27464 * Unused, except if this object is passed to the `MediaSession` 27465 * API. 27466 * 27467 * @property {string} [artist] 27468 * Unused, except if this object is passed to the `MediaSession` 27469 * API. 27470 * 27471 * @property {Object[]} [artwork] 27472 * Unused, except if this object is passed to the `MediaSession` 27473 * API. If not specified, will be populated via the `poster`, if 27474 * available. 27475 * 27476 * @property {string} [poster] 27477 * URL to an image that will display before playback. 27478 * 27479 * @property {Tech~SourceObject|Tech~SourceObject[]|string} [src] 27480 * A single source object, an array of source objects, or a string
27481 * referencing a URL to a media source. It is _highly recommended_ 27482 * that an object or array of objects is used here, so that source 27483 * selection algorithms can take the `type` into account. 27484 * 27485 * @property {string} [title] 27486 * Unused, except if this object is passed to the `MediaSession` 27487 * API. 27488 * 27489 * @property {Object[]} [textTracks] 27490 * An array of objects to be used to create text tracks, following 27491 * the {@link https://www.w3.org/TR/html50/embedded-content-0.html#the-track-element|native track element format}. 27492 * For ease of removal, these will be created as "remote" text 27493 * tracks and set to automatically clean up on source changes. 27494 * 27495 * These objects may have properties like `src`, `kind`, `label`, 27496 * and `language`, see {@link Tech#createRemoteTextTrack}. 27497 */ 27498 27499 /** 27500 * Populate the player using a {@link Player~MediaObject|MediaObject}. 27501 * 27502 * @param {Player~MediaObject} media 27503 * A media object. 27504 * 27505 * @param {Function} ready 27506 * A callback to be called when the player is ready. 27507 */ 27508 ; 27509 27510 _proto.loadMedia = function loadMedia(media, ready) { 27511 var _this15 = this; 27512 27513 if (!media || typeof media !== 'object') { 27514 return; 27515 } 27516 27517 this.reset(); // Clone the media object so it cannot be mutated from outside. 27518 27519 this.cache_.media = mergeOptions(media); 27520 var _this$cache_$media = this.cache_.media, 27521 artwork = _this$cache_$media.artwork, 27522 poster = _this$cache_$media.poster, 27523 src = _this$cache_$media.src, 27524 textTracks = _this$cache_$media.textTracks; // If `artwork` is not given, create it using `poster`. 27525 27526 if (!artwork && poster) { 27527 this.cache_.media.artwork = [{ 27528 src: poster, 27529 type: getMimetype(poster) 27530 }]; 27531 } 27532 27533 if (src) { 27534 this.src(src); 27535 } 27536 27537 if (poster) { 27538 this.poster(poster); 27539 } 27540 27541 if (Array.isArray(textTracks)) { 27542 textTracks.forEach(function (tt) { 27543 return _this15.addRemoteTextTrack(tt, false); 27544 }); 27545 } 27546 27547 this.ready(ready); 27548 } 27549 /** 27550 * Get a clone of the current {@link Player~MediaObject} for this player. 27551 * 27552 * If the `loadMedia` method has not been used, will attempt to return a 27553 * {@link Player~MediaObject} based on the current state of the player. 27554 * 27555 * @return {Player~MediaObject} 27556 */ 27557 ; 27558 27559 _proto.getMedia = function getMedia() { 27560 if (!this.cache_.media) { 27561 var poster = this.poster(); 27562 var src = this.currentSources(); 27563 var textTracks = Array.prototype.map.call(this.remoteTextTracks(), function (tt) { 27564 return { 27565 kind: tt.kind, 27566 label: tt.label, 27567 language: tt.language, 27568 src: tt.src 27569 }; 27570 }); 27571 var media = { 27572 src: src, 27573 textTracks: textTracks 27574 }; 27575 27576 if (poster) { 27577 media.poster = poster; 27578 media.artwork = [{ 27579 src: media.poster, 27580 type: getMimetype(media.poster) 27581 }]; 27582 } 27583 27584 return media; 27585 } 27586 27587 return mergeOptions(this.cache_.media); 27588 } 27589 /** 27590 * Gets tag settings 27591 * 27592 * @param {Element} tag 27593 * The player tag 27594 * 27595 * @return {Object} 27596 * An object containing all of the settings 27597 * for a player tag 27598 */ 27599 ; 27600 27601 Player.getTagSettings = function getTagSettings(tag) { 27602 var baseOptions = { 27603 sources: [], 27604 tracks: [] 27605 }; 27606 var tagOptions = getAttributes(tag); 27607 var dataSetup = tagOptions['data-setup']; 27608 27609 if (hasClass(tag, 'vjs-fill')) { 27610 tagOptions.fill = true; 27611 } 27612 27613 if (hasClass(tag, 'vjs-fluid')) { 27614 tagOptions.fluid = true; 27615 } // Check if data-setup attr exists. 27616 27617 27618 if (dataSetup !== null) { 27619 // Parse options JSON 27620 // If empty string, make it a parsable json object. 27621 var _safeParseTuple = tuple(dataSetup || '{}'), 27622 err = _safeParseTuple[0], 27623 data = _safeParseTuple[1]; 27624 27625 if (err) { 27626 log.error(err); 27627 } 27628 27629 assign(tagOptions, data); 27630 } 27631 27632 assign(baseOptions, tagOptions); // Get tag children settings 27633 27634 if (tag.hasChildNodes()) { 27635 var children = tag.childNodes; 27636 27637 for (var i = 0, j = children.length; i < j; i++) { 27638 var child = children[i]; // Change case needed: http://ejohn.org/blog/nodename-case-sensitivity/ 27639 27640 var childName = child.nodeName.toLowerCase(); 27641 27642 if (childName === 'source') { 27643 baseOptions.sources.push(getAttributes(child)); 27644 } else if (childName === 'track') { 27645 baseOptions.tracks.push(getAttributes(child)); 27646 } 27647 } 27648 } 27649 27650 return baseOptions; 27651 } 27652 /** 27653 * Determine whether or not flexbox is supported 27654 * 27655 * @return {boolean} 27656 * - true if flexbox is supported 27657 * - false if flexbox is not supported 27658 */ 27659 ; 27660 27661 _proto.flexNotSupported_ = function flexNotSupported_() { 27662 var elem = document.createElement('i'); // Note: We don't actually use flexBasis (or flexOrder), but it's one of the more 27663 // common flex features that we can rely on when checking for flex support. 27664 27665 return !('flexBasis' in elem.style || 'webkitFlexBasis' in elem.style || 'mozFlexBasis' in elem.style || 'msFlexBasis' in elem.style || // IE10-specific (2012 flex spec), available for completeness 27666 'msFlexOrder' in elem.style); 27667 }; 27668 27669 return Player; 27670 }(Component); 27671 /** 27672 * Get the {@link VideoTrackList} 27673 * @link https://html.spec.whatwg.org/multipage/embedded-content.html#videotracklist 27674 * 27675 * @return {VideoTrackList} 27676 * the current video track list 27677 * 27678 * @method Player.prototype.videoTracks 27679 */ 27680 27681 /** 27682 * Get the {@link AudioTrackList} 27683 * @link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotracklist 27684 * 27685 * @return {AudioTrackList} 27686 * the current audio track list 27687 * 27688 * @method Player.prototype.audioTracks 27689 */ 27690 27691 /** 27692 * Get the {@link TextTrackList} 27693 * 27694 * @link http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-texttracks 27695 * 27696 * @return {TextTrackList} 27697 * the current text track list 27698 * 27699 * @method Player.prototype.textTracks 27700 */ 27701 27702 /** 27703 * Get the remote {@link TextTrackList} 27704 * 27705 * @return {TextTrackList} 27706 * The current remote text track list 27707 * 27708 * @method Player.prototype.remoteTextTracks 27709 */ 27710 27711 /** 27712 * Get the remote {@link HtmlTrackElementList} tracks. 27713 * 27714 * @return {HtmlTrackElementList} 27715 * The current remote text track element list 27716 * 27717 * @method Player.prototype.remoteTextTrackEls 27718 */ 27719 27720
vendor: 4,869 bytes, lines 27721-27853
27721 ALL.names.forEach(function (name) { 27722 var props = ALL[name]; 27723 27724 Player.prototype[props.getterName] = function () { 27725 if (this.tech_) { 27726 return this.tech_[props.getterName](); 27727 } // if we have not yet loadTech_, we create {video,audio,text}Tracks_ 27728 // these will be passed to the tech during loading 27729 27730 27731 this[props.privateName] = this[props.privateName] || new props.ListClass(); 27732 return this[props.privateName]; 27733 }; 27734 }); 27735 /** 27736 * Global enumeration of players. 27737 * 27738 * The keys are the player IDs and the values are either the {@link Player} 27739 * instance or `null` for disposed players. 27740 * 27741 * @type {Object} 27742 */ 27743 27744 Player.players = {}; 27745 var navigator = window$1.navigator; 27746 /* 27747 * Player instance options, surfaced using options 27748 * options = Player.prototype.options_ 27749 * Make changes in options, not here. 27750 * 27751 * @type {Object} 27752 * @private 27753 */ 27754 27755 Player.prototype.options_ = { 27756 // Default order of fallback technology 27757 techOrder: Tech.defaultTechOrder_, 27758 html5: {}, 27759 flash: {}, 27760 // default inactivity timeout 27761 inactivityTimeout: 2000, 27762 // default playback rates 27763 playbackRates: [], 27764 // Add playback rate selection by adding rates 27765 // 'playbackRates': [0.5, 1, 1.5, 2], 27766 liveui: false, 27767 // Included control sets 27768 children: ['mediaLoader', 'posterImage', 'textTrackDisplay', 'loadingSpinner', 'bigPlayButton', 'liveTracker', 'controlBar', 'errorDisplay', 'textTrackSettings', 'resizeManager'], 27769 language: navigator && (navigator.languages && navigator.languages[0] || navigator.userLanguage || navigator.language) || 'en', 27770 // locales and their language translations 27771 languages: {}, 27772 // Default message to show when a video cannot be played. 27773 notSupportedMessage: 'No compatible source was found for this media.', 27774 fullscreen: { 27775 options: { 27776 navigationUI: 'hide' 27777 } 27778 }, 27779 breakpoints: {}, 27780 responsive: false 27781 }; 27782 [ 27783 /** 27784 * Returns whether or not the player is in the "ended" state. 27785 * 27786 * @return {Boolean} True if the player is in the ended state, false if not. 27787 * @method Player#ended 27788 */ 27789 'ended', 27790 /** 27791 * Returns whether or not the player is in the "seeking" state. 27792 * 27793 * @return {Boolean} True if the player is in the seeking state, false if not. 27794 * @method Player#seeking 27795 */ 27796 'seeking', 27797 /** 27798 * Returns the TimeRanges of the media that are currently available 27799 * for seeking to. 27800 * 27801 * @return {TimeRanges} the seekable intervals of the media timeline 27802 * @method Player#seekable 27803 */ 27804 'seekable', 27805 /** 27806 * Returns the current state of network activity for the element, from 27807 * the codes in the list below. 27808 * - NETWORK_EMPTY (numeric value 0) 27809 * The element has not yet been initialised. All attributes are in 27810 * their initial states. 27811 * - NETWORK_IDLE (numeric value 1) 27812 * The element's resource selection algorithm is active and has 27813 * selected a resource, but it is not actually using the network at 27814 * this time. 27815 * - NETWORK_LOADING (numeric value 2) 27816 * The user agent is actively trying to download data. 27817 * - NETWORK_NO_SOURCE (numeric value 3) 27818 * The element's resource selection algorithm is active, but it has 27819 * not yet found a resource to use. 27820 * 27821 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#network-states 27822 * @return {number} the current network activity state 27823 * @method Player#networkState 27824 */ 27825 'networkState', 27826 /** 27827 * Returns a value that expresses the current state of the element 27828 * with respect to rendering the current playback position, from the 27829 * codes in the list below. 27830 * - HAVE_NOTHING (numeric value 0) 27831 * No information regarding the media resource is available. 27832 * - HAVE_METADATA (numeric value 1) 27833 * Enough of the resource has been obtained that the duration of the 27834 * resource is available. 27835 * - HAVE_CURRENT_DATA (numeric value 2) 27836 * Data for the immediate current playback position is available. 27837 * - HAVE_FUTURE_DATA (numeric value 3) 27838 * Data for the immediate current playback position is available, as 27839 * well as enough data for the user agent to advance the current 27840 * playback position in the direction of playback. 27841 * - HAVE_ENOUGH_DATA (numeric value 4) 27842 * The user agent estimates that enough data is available for 27843 * playback to proceed uninterrupted. 27844 * 27845 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-readystate 27846 * @return {number} the current playback rendering state 27847 * @method Player#readyState 27848 */ 27849 'readyState'].forEach(function (fn) { 27850 Player.prototype[fn] = function () { 27851 return this.techGet_(fn); 27852 }; 27853 });
27854 TECH_EVENTS_RETRIGGER.forEach(function (event) { 27855 Player.prototype["handleTech" + toTitleCase(event) + "_"] = function () { 27856 return this.trigger(event); 27857 }; 27858 }); 27859 /** 27860 * Fired when the player has initial duration and dimension information 27861 * 27862 * @event Player#loadedmetadata 27863 * @type {EventTarget~Event} 27864 */ 27865 27866 /** 27867 * Fired when the player has downloaded data at the current playback position 27868 * 27869 * @event Player#loadeddata 27870 * @type {EventTarget~Event} 27871 */ 27872 27873 /** 27874 * Fired when the current playback position has changed * 27875 * During playback this is fired every 15-250 milliseconds, depending on the 27876 * playback technology in use. 27877 * 27878 * @event Player#timeupdate 27879 * @type {EventTarget~Event} 27880 */ 27881 27882 /** 27883 * Fired when the volume changes 27884 * 27885 * @event Player#volumechange 27886 * @type {EventTarget~Event} 27887 */ 27888 27889 /** 27890 * Reports whether or not a player has a plugin available. 27891 * 27892 * This does not report whether or not the plugin has ever been initialized 27893 * on this player. For that, [usingPlugin]{@link Player#usingPlugin}. 27894 * 27895 * @method Player#hasPlugin 27896 * @param {string} name 27897 * The name of a plugin. 27898 * 27899 * @return {boolean} 27900 * Whether or not this player has the requested plugin available. 27901 */ 27902 27903 /** 27904 * Reports whether or not a player is using a plugin by name. 27905 * 27906 * For basic plugins, this only reports whether the plugin has _ever_ been 27907 * initialized on this player. 27908 * 27909 * @method Player#usingPlugin 27910 * @param {string} name 27911 * The name of a plugin. 27912 * 27913 * @return {boolean} 27914 * Whether or not this player is using the requested plugin. 27915 */ 27916 27917 Component.registerComponent('Player', Player); 27918 27919 var setPrototypeOf = createCommonjsModule(function (module) { 27920 function _setPrototypeOf(o, p) { 27921 module.exports = _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { 27922 o.__proto__ = p; 27923 return o; 27924 }; 27925 27926 return _setPrototypeOf(o, p); 27927 } 27928 27929 module.exports = _setPrototypeOf; 27930 }); 27931 27932 var construct = createCommonjsModule(function (module) { 27933 function isNativeReflectConstruct() { 27934 if (typeof Reflect === "undefined" || !Reflect.construct) return false; 27935 if (Reflect.construct.sham) return false; 27936 if (typeof Proxy === "function") return true; 27937 27938 try { 27939 Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); 27940 return true; 27941 } catch (e) { 27942 return false; 27943 } 27944 } 27945 27946 function _construct(Parent, args, Class) { 27947 if (isNativeReflectConstruct()) { 27948 module.exports = _construct = Reflect.construct; 27949 } else { 27950 module.exports = _construct = function _construct(Parent, args, Class) { 27951 var a = [null]; 27952 a.push.apply(a, args); 27953 var Constructor = Function.bind.apply(Parent, a); 27954 var instance = new Constructor(); 27955 if (Class) setPrototypeOf(instance, Class.prototype); 27956 return instance; 27957 }; 27958 } 27959 27960 return _construct.apply(null, arguments); 27961 } 27962 27963 module.exports = _construct; 27964 }); 27965 27966 /** 27967 * The base plugin name. 27968 * 27969 * @private 27970 * @constant 27971 * @type {string} 27972 */ 27973 27974 var BASE_PLUGIN_NAME = 'plugin'; 27975 /** 27976 * The key on which a player's active plugins cache is stored. 27977 * 27978 * @private 27979 * @constant 27980 * @type {string} 27981 */ 27982 27983 var PLUGIN_CACHE_KEY = 'activePlugins_'; 27984 /** 27985 * Stores registered plugins in a private space. 27986 * 27987 * @private 27988 * @type {Object} 27989 */ 27990 27991 var pluginStorage = {}; 27992 /** 27993 * Reports whether or not a plugin has been registered. 27994 * 27995 * @private 27996 * @param {string} name 27997 * The name of a plugin. 27998 * 27999 * @return {boolean} 28000 * Whether or not the plugin has been registered. 28001 */ 28002 28003 var pluginExists = function pluginExists(name) { 28004 return pluginStorage.hasOwnProperty(name); 28005 }; 28006 /** 28007 * Get a single registered plugin by name. 28008 * 28009 * @private 28010 * @param {string} name 28011 * The name of a plugin. 28012 * 28013 * @return {Function|undefined} 28014 * The plugin (or undefined). 28015 */ 28016 28017 28018 var getPlugin = function getPlugin(name) { 28019 return pluginExists(name) ? pluginStorage[name] : undefined; 28020 }; 28021 /** 28022 * Marks a plugin as "active" on a player. 28023 * 28024 * Also, ensures that the player has an object for tracking active plugins. 28025 * 28026 * @private 28027 * @param {Player} player 28028 * A Video.js player instance. 28029 * 28030 * @param {string} name 28031 * The name of a plugin. 28032 */ 28033 28034 28035 var markPluginAsActive = function markPluginAsActive(player, name) { 28036 player[PLUGIN_CACHE_KEY] = player[PLUGIN_CACHE_KEY] || {}; 28037 player[PLUGIN_CACHE_KEY][name] = true; 28038 }; 28039 /** 28040 * Triggers a pair of plugin setup events. 28041 * 28042 * @private 28043 * @param {Player} player 28044 * A Video.js player instance. 28045 * 28046 * @param {Plugin~PluginEventHash} hash 28047 * A plugin event hash. 28048 * 28049 * @param {boolean} [before] 28050 * If true, prefixes the event name with "before". In other words, 28051 * use this to trigger "beforepluginsetup" instead of "pluginsetup". 28052 */ 28053 28054 28055 var triggerSetupEvent = function triggerSetupEvent(player, hash, before) { 28056 var eventName = (before ? 'before' : '') + 'pluginsetup'; 28057 player.trigger(eventName, hash); 28058 player.trigger(eventName + ':' + hash.name, hash); 28059 }; 28060 /** 28061 * Takes a basic plugin function and returns a wrapper function which marks 28062 * on the player that the plugin has been activated. 28063 * 28064 * @private 28065 * @param {string} name 28066 * The name of the plugin. 28067 * 28068 * @param {Function} plugin 28069 * The basic plugin. 28070 * 28071 * @return {Function} 28072 * A wrapper function for the given plugin. 28073 */ 28074 28075 28076 var createBasicPlugin = function createBasicPlugin(name, plugin) { 28077 var basicPluginWrapper = function basicPluginWrapper() { 28078 // We trigger the "beforepluginsetup" and "pluginsetup" events on the player 28079 // regardless, but we want the hash to be consistent with the hash provided 28080 // for advanced plugins. 28081 // 28082 // The only potentially counter-intuitive thing here is the `instance` in 28083 // the "pluginsetup" event is the value returned by the `plugin` function. 28084 triggerSetupEvent(this, { 28085 name: name, 28086 plugin: plugin, 28087 instance: null 28088 }, true); 28089 var instance = plugin.apply(this, arguments); 28090 markPluginAsActive(this, name); 28091 triggerSetupEvent(this, { 28092 name: name, 28093 plugin: plugin, 28094 instance: instance 28095 }); 28096 return instance; 28097 }; 28098
vendor: 9,187 bytes, lines 28099-28380
28099 Object.keys(plugin).forEach(function (prop) { 28100 basicPluginWrapper[prop] = plugin[prop]; 28101 }); 28102 return basicPluginWrapper; 28103 }; 28104 /** 28105 * Takes a plugin sub-class and returns a factory function for generating 28106 * instances of it. 28107 * 28108 * This factory function will replace itself with an instance of the requested 28109 * sub-class of Plugin. 28110 * 28111 * @private 28112 * @param {string} name 28113 * The name of the plugin. 28114 * 28115 * @param {Plugin} PluginSubClass 28116 * The advanced plugin. 28117 * 28118 * @return {Function} 28119 */ 28120 28121 28122 var createPluginFactory = function createPluginFactory(name, PluginSubClass) { 28123 // Add a `name` property to the plugin prototype so that each plugin can 28124 // refer to itself by name. 28125 PluginSubClass.prototype.name = name; 28126 return function () { 28127 triggerSetupEvent(this, { 28128 name: name, 28129 plugin: PluginSubClass, 28130 instance: null 28131 }, true); 28132 28133 for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { 28134 args[_key] = arguments[_key]; 28135 } 28136 28137 var instance = construct(PluginSubClass, [this].concat(args)); // The plugin is replaced by a function that returns the current instance. 28138 28139 28140 this[name] = function () { 28141 return instance; 28142 }; 28143 28144 triggerSetupEvent(this, instance.getEventHash()); 28145 return instance; 28146 }; 28147 }; 28148 /** 28149 * Parent class for all advanced plugins. 28150 * 28151 * @mixes module:evented~EventedMixin 28152 * @mixes module:stateful~StatefulMixin 28153 * @fires Player#beforepluginsetup 28154 * @fires Player#beforepluginsetup:$name 28155 * @fires Player#pluginsetup 28156 * @fires Player#pluginsetup:$name 28157 * @listens Player#dispose 28158 * @throws {Error} 28159 * If attempting to instantiate the base {@link Plugin} class 28160 * directly instead of via a sub-class. 28161 */ 28162 28163 28164 var Plugin = 28165 /*#__PURE__*/ 28166 function () { 28167 /** 28168 * Creates an instance of this class. 28169 * 28170 * Sub-classes should call `super` to ensure plugins are properly initialized. 28171 * 28172 * @param {Player} player 28173 * A Video.js player instance. 28174 */ 28175 function Plugin(player) { 28176 if (this.constructor === Plugin) { 28177 throw new Error('Plugin must be sub-classed; not directly instantiated.'); 28178 } 28179 28180 this.player = player; // Make this object evented, but remove the added `trigger` method so we 28181 // use the prototype version instead. 28182 28183 evented(this); 28184 delete this.trigger; 28185 stateful(this, this.constructor.defaultState); 28186 markPluginAsActive(player, this.name); // Auto-bind the dispose method so we can use it as a listener and unbind 28187 // it later easily. 28188 28189 this.dispose = bind(this, this.dispose); // If the player is disposed, dispose the plugin. 28190 28191 player.on('dispose', this.dispose); 28192 } 28193 /** 28194 * Get the version of the plugin that was set on <pluginName>.VERSION 28195 */ 28196 28197 28198 var _proto = Plugin.prototype; 28199 28200 _proto.version = function version() { 28201 return this.constructor.VERSION; 28202 } 28203 /** 28204 * Each event triggered by plugins includes a hash of additional data with 28205 * conventional properties. 28206 * 28207 * This returns that object or mutates an existing hash. 28208 * 28209 * @param {Object} [hash={}] 28210 * An object to be used as event an event hash. 28211 * 28212 * @return {Plugin~PluginEventHash} 28213 * An event hash object with provided properties mixed-in. 28214 */ 28215 ; 28216 28217 _proto.getEventHash = function getEventHash(hash) { 28218 if (hash === void 0) { 28219 hash = {}; 28220 } 28221 28222 hash.name = this.name; 28223 hash.plugin = this.constructor; 28224 hash.instance = this; 28225 return hash; 28226 } 28227 /** 28228 * Triggers an event on the plugin object and overrides 28229 * {@link module:evented~EventedMixin.trigger|EventedMixin.trigger}. 28230 * 28231 * @param {string|Object} event 28232 * An event type or an object with a type property. 28233 * 28234 * @param {Object} [hash={}] 28235 * Additional data hash to merge with a 28236 * {@link Plugin~PluginEventHash|PluginEventHash}. 28237 * 28238 * @return {boolean} 28239 * Whether or not default was prevented. 28240 */ 28241 ; 28242 28243 _proto.trigger = function trigger$1(event, hash) { 28244 if (hash === void 0) { 28245 hash = {}; 28246 } 28247 28248 return trigger(this.eventBusEl_, event, this.getEventHash(hash)); 28249 } 28250 /** 28251 * Handles "statechanged" events on the plugin. No-op by default, override by 28252 * subclassing. 28253 * 28254 * @abstract 28255 * @param {Event} e 28256 * An event object provided by a "statechanged" event. 28257 * 28258 * @param {Object} e.changes 28259 * An object describing changes that occurred with the "statechanged" 28260 * event. 28261 */ 28262 ; 28263 28264 _proto.handleStateChanged = function handleStateChanged(e) {} 28265 /** 28266 * Disposes a plugin. 28267 * 28268 * Subclasses can override this if they want, but for the sake of safety, 28269 * it's probably best to subscribe the "dispose" event. 28270 * 28271 * @fires Plugin#dispose 28272 */ 28273 ; 28274 28275 _proto.dispose = function dispose() { 28276 var name = this.name, 28277 player = this.player; 28278 /** 28279 * Signals that a advanced plugin is about to be disposed. 28280 * 28281 * @event Plugin#dispose 28282 * @type {EventTarget~Event} 28283 */ 28284 28285 this.trigger('dispose'); 28286 this.off(); 28287 player.off('dispose', this.dispose); // Eliminate any possible sources of leaking memory by clearing up 28288 // references between the player and the plugin instance and nulling out 28289 // the plugin's state and replacing methods with a function that throws. 28290 28291 player[PLUGIN_CACHE_KEY][name] = false; 28292 this.player = this.state = null; // Finally, replace the plugin name on the player with a new factory 28293 // function, so that the plugin is ready to be set up again. 28294 28295 player[name] = createPluginFactory(name, pluginStorage[name]); 28296 } 28297 /** 28298 * Determines if a plugin is a basic plugin (i.e. not a sub-class of `Plugin`). 28299 * 28300 * @param {string|Function} plugin 28301 * If a string, matches the name of a plugin. If a function, will be 28302 * tested directly. 28303 * 28304 * @return {boolean} 28305 * Whether or not a plugin is a basic plugin. 28306 */ 28307 ; 28308 28309 Plugin.isBasic = function isBasic(plugin) { 28310 var p = typeof plugin === 'string' ? getPlugin(plugin) : plugin; 28311 return typeof p === 'function' && !Plugin.prototype.isPrototypeOf(p.prototype); 28312 } 28313 /** 28314 * Register a Video.js plugin. 28315 * 28316 * @param {string} name 28317 * The name of the plugin to be registered. Must be a string and 28318 * must not match an existing plugin or a method on the `Player` 28319 * prototype. 28320 * 28321 * @param {Function} plugin 28322 * A sub-class of `Plugin` or a function for basic plugins. 28323 * 28324 * @return {Function} 28325 * For advanced plugins, a factory function for that plugin. For 28326 * basic plugins, a wrapper function that initializes the plugin. 28327 */ 28328 ; 28329 28330 Plugin.registerPlugin = function registerPlugin(name, plugin) { 28331 if (typeof name !== 'string') { 28332 throw new Error("Illegal plugin name, \"" + name + "\", must be a string, was " + typeof name + "."); 28333 } 28334 28335 if (pluginExists(name)) { 28336 log.warn("A plugin named \"" + name + "\" already exists. You may want to avoid re-registering plugins!"); 28337 } else if (Player.prototype.hasOwnProperty(name)) { 28338 throw new Error("Illegal plugin name, \"" + name + "\", cannot share a name with an existing player method!"); 28339 } 28340 28341 if (typeof plugin !== 'function') { 28342 throw new Error("Illegal plugin for \"" + name + "\", must be a function, was " + typeof plugin + "."); 28343 } 28344 28345 pluginStorage[name] = plugin; // Add a player prototype method for all sub-classed plugins (but not for 28346 // the base Plugin class). 28347 28348 if (name !== BASE_PLUGIN_NAME) { 28349 if (Plugin.isBasic(plugin)) { 28350 Player.prototype[name] = createBasicPlugin(name, plugin); 28351 } else { 28352 Player.prototype[name] = createPluginFactory(name, plugin); 28353 } 28354 } 28355 28356 return plugin; 28357 } 28358 /** 28359 * De-register a Video.js plugin. 28360 * 28361 * @param {string} name 28362 * The name of the plugin to be de-registered. Must be a string that 28363 * matches an existing plugin. 28364 * 28365 * @throws {Error} 28366 * If an attempt is made to de-register the base plugin. 28367 */ 28368 ; 28369 28370 Plugin.deregisterPlugin = function deregisterPlugin(name) { 28371 if (name === BASE_PLUGIN_NAME) { 28372 throw new Error('Cannot de-register base plugin.'); 28373 } 28374 28375 if (pluginExists(name)) { 28376 delete pluginStorage[name]; 28377 delete Player.prototype[name]; 28378 } 28379 } 28380 /**
28381 * Gets an object containing multiple Video.js plugins. 28382 * 28383 * @param {Array} [names] 28384 * If provided, should be an array of plugin names. Defaults to _all_ 28385 * plugin names. 28386 * 28387 * @return {Object|undefined} 28388 * An object containing plugin(s) associated with their name(s) or 28389 * `undefined` if no matching plugins exist). 28390 */ 28391 ; 28392 28393 Plugin.getPlugins = function getPlugins(names) { 28394 if (names === void 0) { 28395 names = Object.keys(pluginStorage); 28396 } 28397 28398 var result; 28399 names.forEach(function (name) { 28400 var plugin = getPlugin(name); 28401 28402 if (plugin) { 28403 result = result || {}; 28404 result[name] = plugin; 28405 } 28406 }); 28407 return result; 28408 } 28409 /** 28410 * Gets a plugin's version, if available 28411 * 28412 * @param {string} name 28413 * The name of a plugin. 28414 * 28415 * @return {string} 28416 * The plugin's version or an empty string. 28417 */ 28418 ; 28419 28420 Plugin.getPluginVersion = function getPluginVersion(name) { 28421 var plugin = getPlugin(name); 28422 return plugin && plugin.VERSION || ''; 28423 }; 28424 28425 return Plugin; 28426 }(); 28427 /** 28428 * Gets a plugin by name if it exists. 28429 * 28430 * @static 28431 * @method getPlugin 28432 * @memberOf Plugin 28433 * @param {string} name 28434 * The name of a plugin. 28435 * 28436 * @returns {Function|undefined} 28437 * The plugin (or `undefined`). 28438 */ 28439 28440 28441 Plugin.getPlugin = getPlugin; 28442 /** 28443 * The name of the base plugin class as it is registered. 28444 * 28445 * @type {string} 28446 */ 28447 28448 Plugin.BASE_PLUGIN_NAME = BASE_PLUGIN_NAME; 28449 Plugin.registerPlugin(BASE_PLUGIN_NAME, Plugin); 28450 /** 28451 * Documented in player.js 28452 * 28453 * @ignore 28454 */ 28455 28456 Player.prototype.usingPlugin = function (name) { 28457 return !!this[PLUGIN_CACHE_KEY] && this[PLUGIN_CACHE_KEY][name] === true; 28458 }; 28459 /** 28460 * Documented in player.js 28461 * 28462 * @ignore 28463 */ 28464 28465 28466 Player.prototype.hasPlugin = function (name) { 28467 return !!pluginExists(name); 28468 }; 28469 /** 28470 * Signals that a plugin is about to be set up on a player. 28471 * 28472 * @event Player#beforepluginsetup 28473 * @type {Plugin~PluginEventHash} 28474 */ 28475 28476 /** 28477 * Signals that a plugin is about to be set up on a player - by name. The name 28478 * is the name of the plugin. 28479 * 28480 * @event Player#beforepluginsetup:$name 28481 * @type {Plugin~PluginEventHash} 28482 */ 28483 28484 /** 28485 * Signals that a plugin has just been set up on a player. 28486 * 28487 * @event Player#pluginsetup 28488 * @type {Plugin~PluginEventHash} 28489 */ 28490 28491 /** 28492 * Signals that a plugin has just been set up on a player - by name. The name 28493 * is the name of the plugin. 28494 * 28495 * @event Player#pluginsetup:$name 28496 * @type {Plugin~PluginEventHash} 28497 */ 28498 28499 /** 28500 * @typedef {Object} Plugin~PluginEventHash 28501 * 28502 * @property {string} instance 28503 * For basic plugins, the return value of the plugin function. For 28504 * advanced plugins, the plugin instance on which the event is fired. 28505 * 28506 * @property {string} name 28507 * The name of the plugin. 28508 * 28509 * @property {string} plugin 28510 * For basic plugins, the plugin function. For advanced plugins, the 28511 * plugin class/constructor. 28512 */ 28513 28514 function _inherits(subClass, superClass) { 28515 if (typeof superClass !== "function" && superClass !== null) { 28516 throw new TypeError("Super expression must either be null or a function"); 28517 } 28518 28519 subClass.prototype = Object.create(superClass && superClass.prototype, { 28520 constructor: { 28521 value: subClass, 28522 writable: true, 28523 configurable: true 28524 } 28525 }); 28526 if (superClass) setPrototypeOf(subClass, superClass); 28527 } 28528 28529 var inherits = _inherits; 28530 28531 /** 28532 * @file extend.js 28533 * @module extend 28534 */ 28535 /** 28536 * Used to subclass an existing class by emulating ES subclassing using the 28537 * `extends` keyword. 28538 * 28539 * @function 28540 * @example 28541 * var MyComponent = videojs.extend(videojs.getComponent('Component'), { 28542 * myCustomMethod: function() { 28543 * // Do things in my method. 28544 * } 28545 * }); 28546 * 28547 * @param {Function} superClass 28548 * The class to inherit from 28549 * 28550 * @param {Object} [subClassMethods={}] 28551 * Methods of the new class 28552 * 28553 * @return {Function} 28554 * The new class with subClassMethods that inherited superClass. 28555 */ 28556 28557 var extend = function extend(superClass, subClassMethods) { 28558 if (subClassMethods === void 0) { 28559 subClassMethods = {}; 28560 } 28561 28562 var subClass = function subClass() { 28563 superClass.apply(this, arguments); 28564 }; 28565 28566 var methods = {}; 28567 28568 if (typeof subClassMethods === 'object') { 28569 if (subClassMethods.constructor !== Object.prototype.constructor) { 28570 subClass = subClassMethods.constructor; 28571 } 28572 28573 methods = subClassMethods; 28574 } else if (typeof subClassMethods === 'function') { 28575 subClass = subClassMethods; 28576 } 28577 28578 inherits(subClass, superClass); // this is needed for backward-compatibility and node compatibility. 28579 28580 28581 if (superClass) { 28582 subClass.super_ = superClass; 28583 } // Extend subObj's prototype with functions and other properties from props 28584 28585 28586 for (var name in methods) { 28587 if (methods.hasOwnProperty(name)) { 28588 subClass.prototype[name] = methods[name]; 28589 } 28590 } 28591 28592 return subClass; 28593 }; 28594 28595 /** 28596 * @file video.js 28597 * @module videojs 28598 */ 28599 /** 28600 * Normalize an `id` value by trimming off a leading `#` 28601 * 28602 * @private 28603 * @param {string} id 28604 * A string, maybe with a leading `#`. 28605 * 28606 * @return {string} 28607 * The string, without any leading `#`. 28608 */ 28609 28610 var normalizeId = function normalizeId(id) { 28611 return id.indexOf('#') === 0 ? id.slice(1) : id; 28612 }; 28613 /** 28614 * The `videojs()` function doubles as the main function for users to create a 28615 * {@link Player} instance as well as the main library namespace. 28616 * 28617 * It can also be used as a getter for a pre-existing {@link Player} instance. 28618 * However, we _strongly_ recommend using `videojs.getPlayer()` for this 28619 * purpose because it avoids any potential for unintended initialization. 28620 * 28621 * Due to [limitations](https://github.com/jsdoc3/jsdoc/issues/955#issuecomment-313829149) 28622 * of our JSDoc template, we cannot properly document this as both a function 28623 * and a namespace, so its function signature is documented here. 28624 * 28625 * #### Arguments 28626 * ##### id 28627 * string|Element, **required** 28628 * 28629 * Video element or video element ID. 28630 * 28631 * ##### options 28632 * Object, optional 28633 * 28634 * Options object for providing settings. 28635 * See: [Options Guide](https://docs.videojs.com/tutorial-options.html). 28636 * 28637 * ##### ready 28638 * {@link Component~ReadyCallback}, optional 28639 * 28640 * A function to be called when the {@link Player} and {@link Tech} are ready. 28641 * 28642 * #### Return Value 28643 * 28644 * The `videojs()` function returns a {@link Player} instance. 28645 * 28646 * @namespace 28647 * 28648 * @borrows AudioTrack as AudioTrack 28649 * @borrows Component.getComponent as getComponent 28650 * @borrows module:computed-style~computedStyle as computedStyle 28651 * @borrows module:events.on as on 28652 * @borrows module:events.one as one 28653 * @borrows module:events.off as off 28654 * @borrows module:events.trigger as trigger 28655 * @borrows EventTarget as EventTarget 28656 * @borrows module:extend~extend as extend 28657 * @borrows module:fn.bind as bind 28658 * @borrows module:format-time.formatTime as formatTime 28659 * @borrows module:format-time.resetFormatTime as resetFormatTime 28660 * @borrows module:format-time.setFormatTime as setFormatTime 28661 * @borrows module:merge-options.mergeOptions as mergeOptions 28662 * @borrows module:middleware.use as use 28663 * @borrows Player.players as players 28664 * @borrows Plugin.registerPlugin as registerPlugin 28665 * @borrows Plugin.deregisterPlugin as deregisterPlugin 28666 * @borrows Plugin.getPlugins as getPlugins 28667 * @borrows Plugin.getPlugin as getPlugin 28668 * @borrows Plugin.getPluginVersion as getPluginVersion 28669 * @borrows Tech.getTech as getTech 28670 * @borrows Tech.registerTech as registerTech 28671 * @borrows TextTrack as TextTrack 28672 * @borrows module:time-ranges.createTimeRanges as createTimeRange 28673 * @borrows module:time-ranges.createTimeRanges as createTimeRanges 28674 * @borrows module:url.isCrossOrigin as isCrossOrigin 28675 * @borrows module:url.parseUrl as parseUrl 28676 * @borrows VideoTrack as VideoTrack 28677 * 28678 * @param {string|Element}
28678 id 28679 * Video element or video element ID. 28680 * 28681 * @param {Object} [options] 28682 * Options object for providing settings. 28683 * See: [Options Guide](https://docs.videojs.com/tutorial-options.html). 28684 * 28685 * @param {Component~ReadyCallback} [ready] 28686 * A function to be called when the {@link Player} and {@link Tech} are 28687 * ready. 28688 * 28689 * @return {Player} 28690 * The `videojs()` function returns a {@link Player|Player} instance. 28691 */ 28692 28693 28694 function videojs$1(id, options, ready) { 28695 var player = videojs$1.getPlayer(id); 28696 28697 if (player) { 28698 if (options) { 28699 log.warn("Player \"" + id + "\" is already initialised. Options will not be applied."); 28700 } 28701 28702 if (ready) { 28703 player.ready(ready); 28704 } 28705 28706 return player; 28707 } 28708 28709 var el = typeof id === 'string' ? $('#' + normalizeId(id)) : id; 28710 28711 if (!isEl(el)) { 28712 throw new TypeError('The element or ID supplied is not valid. (videojs)'); 28713 } // document.body.contains(el) will only check if el is contained within that one document. 28714 // This causes problems for elements in iframes. 28715 // Instead, use the element's ownerDocument instead of the global document. 28716 // This will make sure that the element is indeed in the dom of that document. 28717 // Additionally, check that the document in question has a default view. 28718 // If the document is no longer attached to the dom, the defaultView of the document will be null. 28719 28720 28721 if (!el.ownerDocument.defaultView || !el.ownerDocument.body.contains(el)) { 28722 log.warn('The element supplied is not included in the DOM'); 28723 } 28724 28725 options = options || {}; 28726 videojs$1.hooks('beforesetup').forEach(function (hookFunction) { 28727 var opts = hookFunction(el, mergeOptions(options)); 28728 28729 if (!isObject(opts) || Array.isArray(opts)) { 28730 log.error('please return an object in beforesetup hooks'); 28731 return; 28732 } 28733 28734 options = mergeOptions(options, opts); 28735 }); // We get the current "Player" component here in case an integration has 28736 // replaced it with a custom player. 28737 28738 var PlayerComponent = Component.getComponent('Player'); 28739 player = new PlayerComponent(el, options, ready); 28740 videojs$1.hooks('setup').forEach(function (hookFunction) { 28741 return hookFunction(player); 28742 }); 28743 return player; 28744 } 28745 /** 28746 * An Object that contains lifecycle hooks as keys which point to an array 28747 * of functions that are run when a lifecycle is triggered 28748 * 28749 * @private 28750 */ 28751 28752 28753 videojs$1.hooks_ = {}; 28754 /** 28755 * Get a list of hooks for a specific lifecycle 28756 * 28757 * @param {string} type 28758 * the lifecyle to get hooks from 28759 * 28760 * @param {Function|Function[]} [fn] 28761 * Optionally add a hook (or hooks) to the lifecycle that your are getting. 28762 * 28763 * @return {Array} 28764 * an array of hooks, or an empty array if there are none. 28765 */ 28766 28767 videojs$1.hooks = function (type, fn) { 28768 videojs$1.hooks_[type] = videojs$1.hooks_[type] || []; 28769 28770 if (fn) { 28771 videojs$1.hooks_[type] = videojs$1.hooks_[type].concat(fn); 28772 } 28773 28774 return videojs$1.hooks_[type]; 28775 }; 28776 /** 28777 * Add a function hook to a specific videojs lifecycle. 28778 * 28779 * @param {string} type 28780 * the lifecycle to hook the function to. 28781 * 28782 * @param {Function|Function[]} 28783 * The function or array of functions to attach. 28784 */ 28785 28786 28787 videojs$1.hook = function (type, fn) { 28788 videojs$1.hooks(type, fn); 28789 }; 28790 /** 28791 * Add a function hook that will only run once to a specific videojs lifecycle. 28792 * 28793 * @param {string} type 28794 * the lifecycle to hook the function to. 28795 * 28796 * @param {Function|Function[]} 28797 * The function or array of functions to attach. 28798 */ 28799 28800 28801 videojs$1.hookOnce = function (type, fn) { 28802 videojs$1.hooks(type, [].concat(fn).map(function (original) { 28803 var wrapper = function wrapper() { 28804 videojs$1.removeHook(type, wrapper); 28805 return original.apply(void 0, arguments); 28806 }; 28807 28808 return wrapper; 28809 })); 28810 }; 28811 /** 28812 * Remove a hook from a specific videojs lifecycle. 28813 * 28814 * @param {string} type 28815 * the lifecycle that the function hooked to 28816 * 28817 * @param {Function} fn 28818 * The hooked function to remove 28819 * 28820 * @return {boolean} 28821 * The function that was removed or undef 28822 */ 28823 28824
vendor: 9,104 bytes, lines 28825-29121
28825 videojs$1.removeHook = function (type, fn) { 28826 var index = videojs$1.hooks(type).indexOf(fn); 28827 28828 if (index <= -1) { 28829 return false; 28830 } 28831 28832 videojs$1.hooks_[type] = videojs$1.hooks_[type].slice(); 28833 videojs$1.hooks_[type].splice(index, 1); 28834 return true; 28835 }; // Add default styles 28836 28837 28838 if (window$1.VIDEOJS_NO_DYNAMIC_STYLE !== true && isReal()) { 28839 var style = $('.vjs-styles-defaults'); 28840 28841 if (!style) { 28842 style = createStyleElement('vjs-styles-defaults'); 28843 var head = $('head'); 28844 28845 if (head) { 28846 head.insertBefore(style, head.firstChild); 28847 } 28848 28849 setTextContent(style, "\n .video-js {\n width: 300px;\n height: 150px;\n }\n\n .vjs-fluid {\n padding-top: 56.25%\n }\n "); 28850 } 28851 } // Run Auto-load players 28852 // You have to wait at least once in case this script is loaded after your 28853 // video in the DOM (weird behavior only with minified version) 28854 28855 28856 autoSetupTimeout(1, videojs$1); 28857 /** 28858 * Current Video.js version. Follows [semantic versioning](https://semver.org/). 28859 * 28860 * @type {string} 28861 */ 28862 28863 videojs$1.VERSION = version; 28864 /** 28865 * The global options object. These are the settings that take effect 28866 * if no overrides are specified when the player is created. 28867 * 28868 * @type {Object} 28869 */ 28870 28871 videojs$1.options = Player.prototype.options_; 28872 /** 28873 * Get an object with the currently created players, keyed by player ID 28874 * 28875 * @return {Object} 28876 * The created players 28877 */ 28878 28879 videojs$1.getPlayers = function () { 28880 return Player.players; 28881 }; 28882 /** 28883 * Get a single player based on an ID or DOM element. 28884 * 28885 * This is useful if you want to check if an element or ID has an associated 28886 * Video.js player, but not create one if it doesn't. 28887 * 28888 * @param {string|Element} id 28889 * An HTML element - `<video>`, `<audio>`, or `<video-js>` - 28890 * or a string matching the `id` of such an element. 28891 * 28892 * @return {Player|undefined} 28893 * A player instance or `undefined` if there is no player instance 28894 * matching the argument. 28895 */ 28896 28897 28898 videojs$1.getPlayer = function (id) { 28899 var players = Player.players; 28900 var tag; 28901 28902 if (typeof id === 'string') { 28903 var nId = normalizeId(id); 28904 var player = players[nId]; 28905 28906 if (player) { 28907 return player; 28908 } 28909 28910 tag = $('#' + nId); 28911 } else { 28912 tag = id; 28913 } 28914 28915 if (isEl(tag)) { 28916 var _tag = tag, 28917 _player = _tag.player, 28918 playerId = _tag.playerId; // Element may have a `player` property referring to an already created 28919 // player instance. If so, return that. 28920 28921 if (_player || players[playerId]) { 28922 return _player || players[playerId]; 28923 } 28924 } 28925 }; 28926 /** 28927 * Returns an array of all current players. 28928 * 28929 * @return {Array} 28930 * An array of all players. The array will be in the order that 28931 * `Object.keys` provides, which could potentially vary between 28932 * JavaScript engines. 28933 * 28934 */ 28935 28936 28937 videojs$1.getAllPlayers = function () { 28938 return (// Disposed players leave a key with a `null` value, so we need to make sure 28939 // we filter those out. 28940 Object.keys(Player.players).map(function (k) { 28941 return Player.players[k]; 28942 }).filter(Boolean) 28943 ); 28944 }; 28945 28946 videojs$1.players = Player.players; 28947 videojs$1.getComponent = Component.getComponent; 28948 /** 28949 * Register a component so it can referred to by name. Used when adding to other 28950 * components, either through addChild `component.addChild('myComponent')` or through 28951 * default children options `{ children: ['myComponent'] }`. 28952 * 28953 * > NOTE: You could also just initialize the component before adding. 28954 * `component.addChild(new MyComponent());` 28955 * 28956 * @param {string} name 28957 * The class name of the component 28958 * 28959 * @param {Component} comp 28960 * The component class 28961 * 28962 * @return {Component} 28963 * The newly registered component 28964 */ 28965 28966 videojs$1.registerComponent = function (name, comp) { 28967 if (Tech.isTech(comp)) { 28968 log.warn("The " + name + " tech was registered as a component. It should instead be registered using videojs.registerTech(name, tech)"); 28969 } 28970 28971 Component.registerComponent.call(Component, name, comp); 28972 }; 28973 28974 videojs$1.getTech = Tech.getTech; 28975 videojs$1.registerTech = Tech.registerTech; 28976 videojs$1.use = use; 28977 /** 28978 * An object that can be returned by a middleware to signify 28979 * that the middleware is being terminated. 28980 * 28981 * @type {object} 28982 * @property {object} middleware.TERMINATOR 28983 */ 28984 28985 Object.defineProperty(videojs$1, 'middleware', { 28986 value: {}, 28987 writeable: false, 28988 enumerable: true 28989 }); 28990 Object.defineProperty(videojs$1.middleware, 'TERMINATOR', { 28991 value: TERMINATOR, 28992 writeable: false, 28993 enumerable: true 28994 }); 28995 /** 28996 * A reference to the {@link module:browser|browser utility module} as an object. 28997 * 28998 * @type {Object} 28999 * @see {@link module:browser|browser} 29000 */ 29001 29002 videojs$1.browser = browser; 29003 /** 29004 * Use {@link module:browser.TOUCH_ENABLED|browser.TOUCH_ENABLED} instead; only 29005 * included for backward-compatibility with 4.x. 29006 * 29007 * @deprecated Since version 5.0, use {@link module:browser.TOUCH_ENABLED|browser.TOUCH_ENABLED instead. 29008 * @type {boolean} 29009 */ 29010 29011 videojs$1.TOUCH_ENABLED = TOUCH_ENABLED; 29012 videojs$1.extend = extend; 29013 videojs$1.mergeOptions = mergeOptions; 29014 videojs$1.bind = bind; 29015 videojs$1.registerPlugin = Plugin.registerPlugin; 29016 videojs$1.deregisterPlugin = Plugin.deregisterPlugin; 29017 /** 29018 * Deprecated method to register a plugin with Video.js 29019 * 29020 * @deprecated videojs.plugin() is deprecated; use videojs.registerPlugin() instead 29021 * 29022 * @param {string} name 29023 * The plugin name 29024 * 29025 * @param {Plugin|Function} plugin 29026 * The plugin sub-class or function 29027 */ 29028 29029 videojs$1.plugin = function (name, plugin) { 29030 log.warn('videojs.plugin() is deprecated; use videojs.registerPlugin() instead'); 29031 return Plugin.registerPlugin(name, plugin); 29032 }; 29033 29034 videojs$1.getPlugins = Plugin.getPlugins; 29035 videojs$1.getPlugin = Plugin.getPlugin; 29036 videojs$1.getPluginVersion = Plugin.getPluginVersion; 29037 /** 29038 * Adding languages so that they're available to all players. 29039 * Example: `videojs.addLanguage('es', { 'Hello': 'Hola' });` 29040 * 29041 * @param {string} code 29042 * The language code or dictionary property 29043 * 29044 * @param {Object} data 29045 * The data values to be translated 29046 * 29047 * @return {Object} 29048 * The resulting language dictionary object 29049 */ 29050 29051 videojs$1.addLanguage = function (code, data) { 29052 var _mergeOptions; 29053 29054 code = ('' + code).toLowerCase(); 29055 videojs$1.options.languages = mergeOptions(videojs$1.options.languages, (_mergeOptions = {}, _mergeOptions[code] = data, _mergeOptions)); 29056 return videojs$1.options.languages[code]; 29057 }; 29058 /** 29059 * A reference to the {@link module:log|log utility module} as an object. 29060 * 29061 * @type {Function} 29062 * @see {@link module:log|log} 29063 */ 29064 29065 29066 videojs$1.log = log; 29067 videojs$1.createLogger = createLogger$1; 29068 videojs$1.createTimeRange = videojs$1.createTimeRanges = createTimeRanges; 29069 videojs$1.formatTime = formatTime; 29070 videojs$1.setFormatTime = setFormatTime; 29071 videojs$1.resetFormatTime = resetFormatTime; 29072 videojs$1.parseUrl = parseUrl; 29073 videojs$1.isCrossOrigin = isCrossOrigin; 29074 videojs$1.EventTarget = EventTarget; 29075 videojs$1.on = on; 29076 videojs$1.one = one; 29077 videojs$1.off = off; 29078 videojs$1.trigger = trigger; 29079 /** 29080 * A cross-browser XMLHttpRequest wrapper. 29081 * 29082 * @function 29083 * @param {Object} options 29084 * Settings for the request. 29085 * 29086 * @return {XMLHttpRequest|XDomainRequest} 29087 * The request object. 29088 * 29089 * @see https://github.com/Raynos/xhr 29090 */ 29091 29092 videojs$1.xhr = xhr; 29093 videojs$1.TextTrack = TextTrack; 29094 videojs$1.AudioTrack = AudioTrack; 29095 videojs$1.VideoTrack = VideoTrack; 29096 ['isEl', 'isTextNode', 'createEl', 'hasClass', 'addClass', 'removeClass', 'toggleClass', 'setAttributes', 'getAttributes', 'emptyEl', 'appendContent', 'insertContent'].forEach(function (k) { 29097 videojs$1[k] = function () { 29098 log.warn("videojs." + k + "() is deprecated; use videojs.dom." + k + "() instead"); 29099 return Dom[k].apply(null, arguments); 29100 }; 29101 }); 29102 videojs$1.computedStyle = computedStyle; 29103 /** 29104 * A reference to the {@link module:dom|DOM utility module} as an object. 29105 * 29106 * @type {Object} 29107 * @see {@link module:dom|dom} 29108 */ 29109 29110 videojs$1.dom = Dom; 29111 /** 29112 * A reference to the {@link module:url|URL utility module} as an object. 29113 * 29114 * @type {Object} 29115 * @see {@link module:url|url} 29116 */ 29117 29118 videojs$1.url = Url; 29119 videojs$1.defineLazyProperty = defineLazyProperty; 29120 29121 var urlToolkit = createCommonjsModule(function (module, exports
vendor: 4,749 bytes, lines 29121-29228
29121) { 29122 // see https://tools.ietf.org/html/rfc1808 29123 29124 /* jshint ignore:start */ 29125 (function (root) { 29126 /* jshint ignore:end */ 29127 var URL_REGEX = /^((?:[a-zA-Z0-9+\-.]+:)?)(\/\/[^\/?#]*)?((?:[^\/\?#]*\/)*.*?)??(;.*?)?(\?.*?)?(#.*?)?$/; 29128 var FIRST_SEGMENT_REGEX = /^([^\/?#]*)(.*)$/; 29129 var SLASH_DOT_REGEX = /(?:\/|^)\.(?=\/)/g; 29130 var SLASH_DOT_DOT_REGEX = /(?:\/|^)\.\.\/(?!\.\.\/).*?(?=\/)/g; 29131 var URLToolkit = { 29132 // jshint ignore:line 29133 // If opts.alwaysNormalize is true then the path will always be normalized even when it starts with / or // 29134 // E.g 29135 // With opts.alwaysNormalize = false (default, spec compliant) 29136 // http://a.com/b/cd + /e/f/../g => http://a.com/e/f/../g 29137 // With opts.alwaysNormalize = true (not spec compliant) 29138 // http://a.com/b/cd + /e/f/../g => http://a.com/e/g 29139 buildAbsoluteURL: function buildAbsoluteURL(baseURL, relativeURL, opts) { 29140 opts = opts || {}; // remove any remaining space and CRLF 29141 29142 baseURL = baseURL.trim(); 29143 relativeURL = relativeURL.trim(); 29144 29145 if (!relativeURL) { 29146 // 2a) If the embedded URL is entirely empty, it inherits the 29147 // entire base URL (i.e., is set equal to the base URL) 29148 // and we are done. 29149 if (!opts.alwaysNormalize) { 29150 return baseURL; 29151 } 29152 29153 var basePartsForNormalise = URLToolkit.parseURL(baseURL); 29154 29155 if (!basePartsForNormalise) { 29156 throw new Error('Error trying to parse base URL.'); 29157 } 29158 29159 basePartsForNormalise.path = URLToolkit.normalizePath(basePartsForNormalise.path); 29160 return URLToolkit.buildURLFromParts(basePartsForNormalise); 29161 } 29162 29163 var relativeParts = URLToolkit.parseURL(relativeURL); 29164 29165 if (!relativeParts) { 29166 throw new Error('Error trying to parse relative URL.'); 29167 } 29168 29169 if (relativeParts.scheme) { 29170 // 2b) If the embedded URL starts with a scheme name, it is 29171 // interpreted as an absolute URL and we are done. 29172 if (!opts.alwaysNormalize) { 29173 return relativeURL; 29174 } 29175 29176 relativeParts.path = URLToolkit.normalizePath(relativeParts.path); 29177 return URLToolkit.buildURLFromParts(relativeParts); 29178 } 29179 29180 var baseParts = URLToolkit.parseURL(baseURL); 29181 29182 if (!baseParts) { 29183 throw new Error('Error trying to parse base URL.'); 29184 } 29185 29186 if (!baseParts.netLoc && baseParts.path && baseParts.path[0] !== '/') { 29187 // If netLoc missing and path doesn't start with '/', assume everthing before the first '/' is the netLoc 29188 // This causes 'example.com/a' to be handled as '//example.com/a' instead of '/example.com/a' 29189 var pathParts = FIRST_SEGMENT_REGEX.exec(baseParts.path); 29190 baseParts.netLoc = pathParts[1]; 29191 baseParts.path = pathParts[2]; 29192 } 29193 29194 if (baseParts.netLoc && !baseParts.path) { 29195 baseParts.path = '/'; 29196 } 29197 29198 var builtParts = { 29199 // 2c) Otherwise, the embedded URL inherits the scheme of 29200 // the base URL. 29201 scheme: baseParts.scheme, 29202 netLoc: relativeParts.netLoc, 29203 path: null, 29204 params: relativeParts.params, 29205 query: relativeParts.query, 29206 fragment: relativeParts.fragment 29207 }; 29208 29209 if (!relativeParts.netLoc) { 29210 // 3) If the embedded URL's <net_loc> is non-empty, we skip to 29211 // Step 7. Otherwise, the embedded URL inherits the <net_loc> 29212 // (if any) of the base URL. 29213 builtParts.netLoc = baseParts.netLoc; // 4) If the embedded URL path is preceded by a slash "/", the 29214 // path is not relative and we skip to Step 7. 29215 29216 if (relativeParts.path[0] !== '/') { 29217 if (!relativeParts.path) { 29218 // 5) If the embedded URL path is empty (and not preceded by a 29219 // slash), then the embedded URL inherits the base URL path 29220 builtParts.path = baseParts.path; // 5a) if the embedded URL's <params> is non-empty, we skip to 29221 // step 7; otherwise, it inherits the <params> of the base 29222 // URL (if any) and 29223 29224 if (!relativeParts.params) { 29225 builtParts.params = baseParts.params; // 5b) if the embedded URL's <query> is non-empty, we skip to 29226 // step 7; otherwise, it inherits the <query> of the base 29227 // URL (if any) and we skip to step 7. 29228
29229 if (!relativeParts.query) { 29230 builtParts.query = baseParts.query; 29231 } 29232 } 29233 } else { 29234 // 6) The last segment of the base URL's path (anything 29235 // following the rightmost slash "/", or the entire path if no 29236 // slash is present) is removed and the embedded URL's path is 29237 // appended in its place. 29238 var baseURLPath = baseParts.path; 29239 var newPath = baseURLPath.substring(0, baseURLPath.lastIndexOf('/') + 1) + relativeParts.path; 29240 builtParts.path = URLToolkit.normalizePath(newPath); 29241 } 29242 } 29243 } 29244 29245 if (builtParts.path === null) { 29246 builtParts.path = opts.alwaysNormalize ? URLToolkit.normalizePath(relativeParts.path) : relativeParts.path; 29247 } 29248 29249 return URLToolkit.buildURLFromParts(builtParts); 29250 }, 29251 parseURL: function parseURL(url) { 29252 var parts = URL_REGEX.exec(url); 29253 29254 if (!parts) { 29255 return null; 29256 } 29257 29258 return { 29259 scheme: parts[1] || '', 29260 netLoc: parts[2] || '', 29261 path: parts[3] || '', 29262 params: parts[4] || '', 29263 query: parts[5] || '', 29264 fragment: parts[6] || '' 29265 }; 29266 }, 29267 normalizePath: function normalizePath(path) { 29268 // The following operations are 29269 // then applied, in order, to the new path: 29270 // 6a) All occurrences of "./", where "." is a complete path 29271 // segment, are removed. 29272 // 6b) If the path ends with "." as a complete path segment, 29273 // that "." is removed. 29274 path = path.split('').reverse().join('').replace(SLASH_DOT_REGEX, ''); // 6c) All occurrences of "<segment>/../", where <segment> is a 29275 // complete path segment not equal to "..", are removed. 29276 // Removal of these path segments is performed iteratively, 29277 // removing the leftmost matching pattern on each iteration, 29278 // until no matching pattern remains. 29279 // 6d) If the path ends with "<segment>/..", where <segment> is a 29280 // complete path segment not equal to "..", that 29281 // "<segment>/.." is removed. 29282 29283 while (path.length !== (path = path.replace(SLASH_DOT_DOT_REGEX, '')).length) {} // jshint ignore:line 29284 29285 29286 return path.split('').reverse().join(''); 29287 }, 29288 buildURLFromParts: function buildURLFromParts(parts) { 29289 return parts.scheme + parts.netLoc + parts.path + parts.params + parts.query + parts.fragment; 29290 } 29291 }; 29292 /* jshint ignore:start */ 29293 29294 module.exports = URLToolkit; 29295 })(); 29296 /* jshint ignore:end */ 29297 29298 }); 29299 29300 /*! @name m3u8-parser @version 4.4.0 @license Apache-2.0 */ 29301 29302 function _extends() { 29303 _extends = Object.assign || function (target) { 29304 for (var i = 1; i < arguments.length; i++) { 29305 var source = arguments[i]; 29306 29307 for (var key in source) { 29308 if (Object.prototype.hasOwnProperty.call(source, key)) { 29309 target[key] = source[key]; 29310 } 29311 } 29312 } 29313 29314 return target; 29315 }; 29316 29317 return _extends.apply(this, arguments); 29318 } 29319 29320 function _inheritsLoose$1(subClass, superClass) { 29321 subClass.prototype = Object.create(superClass.prototype); 29322 subClass.prototype.constructor = subClass; 29323 subClass.__proto__ = superClass; 29324 } 29325 29326 function _assertThisInitialized$1(self) { 29327 if (self === void 0) { 29328 throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); 29329 } 29330 29331 return self; 29332 } 29333 /** 29334 * @file stream.js 29335 */ 29336 29337 /** 29338 * A lightweight readable stream implementation that handles event dispatching. 29339 * 29340 * @class Stream 29341 */ 29342 29343 29344 var Stream = 29345 /*#__PURE__*/ 29346 function () { 29347 function Stream() { 29348 this.listeners = {}; 29349 } 29350 /** 29351 * Add a listener for a specified event type. 29352 * 29353 * @param {string} type the event name 29354 * @param {Function} listener the callback to be invoked when an event of 29355 * the specified type occurs 29356 */ 29357 29358 29359 var _proto = Stream.prototype; 29360 29361 _proto.on = function on(type, listener) { 29362 if (!this.listeners[type]) { 29363 this.listeners[type] = []; 29364 } 29365 29366 this.listeners[type].push(listener); 29367 } 29368 /** 29369 * Remove a listener for a specified event type. 29370 * 29371 * @param {string} type the event name 29372 * @param {Function} listener a function previously registered for this 29373 * type of event through `on` 29374 * @return {boolean} if we could turn it off or not 29375 */ 29376 ; 29377 29378 _proto.off = function off(type, listener) { 29379 if (!this.listeners[type]) { 29380 return false;
vendor: 40,557 bytes, lines 29381-30723
29381 } 29382 29383 var index = this.listeners[type].indexOf(listener); 29384 this.listeners[type].splice(index, 1); 29385 return index > -1; 29386 } 29387 /** 29388 * Trigger an event of the specified type on this stream. Any additional 29389 * arguments to this function are passed as parameters to event listeners. 29390 * 29391 * @param {string} type the event name 29392 */ 29393 ; 29394 29395 _proto.trigger = function trigger(type) { 29396 var callbacks = this.listeners[type]; 29397 var i; 29398 var length; 29399 var args; 29400 29401 if (!callbacks) { 29402 return; 29403 } // Slicing the arguments on every invocation of this method 29404 // can add a significant amount of overhead. Avoid the 29405 // intermediate object creation for the common case of a 29406 // single callback argument 29407 29408 29409 if (arguments.length === 2) { 29410 length = callbacks.length; 29411 29412 for (i = 0; i < length; ++i) { 29413 callbacks[i].call(this, arguments[1]); 29414 } 29415 } else { 29416 args = Array.prototype.slice.call(arguments, 1); 29417 length = callbacks.length; 29418 29419 for (i = 0; i < length; ++i) { 29420 callbacks[i].apply(this, args); 29421 } 29422 } 29423 } 29424 /** 29425 * Destroys the stream and cleans up. 29426 */ 29427 ; 29428 29429 _proto.dispose = function dispose() { 29430 this.listeners = {}; 29431 } 29432 /** 29433 * Forwards all `data` events on this stream to the destination stream. The 29434 * destination stream should provide a method `push` to receive the data 29435 * events as they arrive. 29436 * 29437 * @param {Stream} destination the stream that will receive all `data` events 29438 * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options 29439 */ 29440 ; 29441 29442 _proto.pipe = function pipe(destination) { 29443 this.on('data', function (data) { 29444 destination.push(data); 29445 }); 29446 }; 29447 29448 return Stream; 29449 }(); 29450 /** 29451 * A stream that buffers string input and generates a `data` event for each 29452 * line. 29453 * 29454 * @class LineStream 29455 * @extends Stream 29456 */ 29457 29458 29459 var LineStream = 29460 /*#__PURE__*/ 29461 function (_Stream) { 29462 _inheritsLoose$1(LineStream, _Stream); 29463 29464 function LineStream() { 29465 var _this; 29466 29467 _this = _Stream.call(this) || this; 29468 _this.buffer = ''; 29469 return _this; 29470 } 29471 /** 29472 * Add new data to be parsed. 29473 * 29474 * @param {string} data the text to process 29475 */ 29476 29477 29478 var _proto = LineStream.prototype; 29479 29480 _proto.push = function push(data) { 29481 var nextNewline; 29482 this.buffer += data; 29483 nextNewline = this.buffer.indexOf('\n'); 29484 29485 for (; nextNewline > -1; nextNewline = this.buffer.indexOf('\n')) { 29486 this.trigger('data', this.buffer.substring(0, nextNewline)); 29487 this.buffer = this.buffer.substring(nextNewline + 1); 29488 } 29489 }; 29490 29491 return LineStream; 29492 }(Stream); 29493 /** 29494 * "forgiving" attribute list psuedo-grammar: 29495 * attributes -> keyvalue (',' keyvalue)* 29496 * keyvalue -> key '=' value 29497 * key -> [^=]* 29498 * value -> '"' [^"]* '"' | [^,]* 29499 */ 29500 29501 29502 var attributeSeparator = function attributeSeparator() { 29503 var key = '[^=]*'; 29504 var value = '"[^"]*"|[^,]*'; 29505 var keyvalue = '(?:' + key + ')=(?:' + value + ')'; 29506 return new RegExp('(?:^|,)(' + keyvalue + ')'); 29507 }; 29508 /** 29509 * Parse attributes from a line given the separator 29510 * 29511 * @param {string} attributes the attribute line to parse 29512 */ 29513 29514 29515 var parseAttributes = function parseAttributes(attributes) { 29516 // split the string using attributes as the separator 29517 var attrs = attributes.split(attributeSeparator()); 29518 var result = {}; 29519 var i = attrs.length; 29520 var attr; 29521 29522 while (i--) { 29523 // filter out unmatched portions of the string 29524 if (attrs[i] === '') { 29525 continue; 29526 } // split the key and value 29527 29528 29529 attr = /([^=]*)=(.*)/.exec(attrs[i]).slice(1); // trim whitespace and remove optional quotes around the value 29530 29531 attr[0] = attr[0].replace(/^\s+|\s+$/g, ''); 29532 attr[1] = attr[1].replace(/^\s+|\s+$/g, ''); 29533 attr[1] = attr[1].replace(/^['"](.*)['"]$/g, '$1'); 29534 result[attr[0]] = attr[1]; 29535 } 29536 29537 return result; 29538 }; 29539 /** 29540 * A line-level M3U8 parser event stream. It expects to receive input one 29541 * line at a time and performs a context-free parse of its contents. A stream 29542 * interpretation of a manifest can be useful if the manifest is expected to 29543 * be too large to fit comfortably into memory or the entirety of the input 29544 * is not immediately available. Otherwise, it's probably much easier to work 29545 * with a regular `Parser` object. 29546 * 29547 * Produces `data` events with an object that captures the parser's 29548 * interpretation of the input. That object has a property `tag` that is one 29549 * of `uri`, `comment`, or `tag`. URIs only have a single additional 29550 * property, `line`, which captures the entirety of the input without 29551 * interpretation. Comments similarly have a single additional property 29552 * `text` which is the input without the leading `#`. 29553 * 29554 * Tags always have a property `tagType` which is the lower-cased version of 29555 * the M3U8 directive without the `#EXT` or `#EXT-X-` prefix. For instance, 29556 * `#EXT-X-MEDIA-SEQUENCE` becomes `media-sequence` when parsed. Unrecognized 29557 * tags are given the tag type `unknown` and a single additional property 29558 * `data` with the remainder of the input. 29559 * 29560 * @class ParseStream 29561 * @extends Stream 29562 */ 29563 29564 29565 var ParseStream = 29566 /*#__PURE__*/ 29567 function (_Stream) { 29568 _inheritsLoose$1(ParseStream, _Stream); 29569 29570 function ParseStream() { 29571 var _this; 29572 29573 _this = _Stream.call(this) || this; 29574 _this.customParsers = []; 29575 _this.tagMappers = []; 29576 return _this; 29577 } 29578 /** 29579 * Parses an additional line of input. 29580 * 29581 * @param {string} line a single line of an M3U8 file to parse 29582 */ 29583 29584 29585 var _proto = ParseStream.prototype; 29586 29587 _proto.push = function push(line) { 29588 var _this2 = this; 29589 29590 var match; 29591 var event; // strip whitespace 29592 29593 line = line.trim(); 29594 29595 if (line.length === 0) { 29596 // ignore empty lines 29597 return; 29598 } // URIs 29599 29600 29601 if (line[0] !== '#') { 29602 this.trigger('data', { 29603 type: 'uri', 29604 uri: line 29605 }); 29606 return; 29607 } // map tags 29608 29609 29610 var newLines = this.tagMappers.reduce(function (acc, mapper) { 29611 var mappedLine = mapper(line); // skip if unchanged 29612 29613 if (mappedLine === line) { 29614 return acc; 29615 } 29616 29617 return acc.concat([mappedLine]); 29618 }, [line]); 29619 newLines.forEach(function (newLine) { 29620 for (var i = 0; i < _this2.customParsers.length; i++) { 29621 if (_this2.customParsers[i].call(_this2, newLine)) { 29622 return; 29623 } 29624 } // Comments 29625 29626 29627 if (newLine.indexOf('#EXT') !== 0) { 29628 _this2.trigger('data', { 29629 type: 'comment', 29630 text: newLine.slice(1) 29631 }); 29632 29633 return; 29634 } // strip off any carriage returns here so the regex matching 29635 // doesn't have to account for them. 29636 29637 29638 newLine = newLine.replace('\r', ''); // Tags 29639 29640 match = /^#EXTM3U/.exec(newLine); 29641 29642 if (match) { 29643 _this2.trigger('data', { 29644 type: 'tag', 29645 tagType: 'm3u' 29646 }); 29647 29648 return; 29649 } 29650 29651 match = /^#EXTINF:?([0-9\.]*)?,?(.*)?$/.exec(newLine); 29652 29653 if (match) { 29654 event = { 29655 type: 'tag', 29656 tagType: 'inf' 29657 }; 29658 29659 if (match[1]) { 29660 event.duration = parseFloat(match[1]); 29661 } 29662 29663 if (match[2]) { 29664 event.title = match[2]; 29665 } 29666 29667 _this2.trigger('data', event); 29668 29669 return; 29670 } 29671 29672 match = /^#EXT-X-TARGETDURATION:?([0-9.]*)?/.exec(newLine); 29673 29674 if (match) { 29675 event = { 29676 type: 'tag', 29677 tagType: 'targetduration' 29678 }; 29679 29680 if (match[1]) { 29681 event.duration = parseInt(match[1], 10); 29682 } 29683 29684 _this2.trigger('data', event); 29685 29686 return; 29687 } 29688 29689 match = /^#ZEN-TOTAL-DURATION:?([0-9.]*)?/.exec(newLine); 29690 29691 if (match) { 29692 event = { 29693 type: 'tag', 29694 tagType: 'totalduration' 29695 }; 29696 29697 if (match[1]) { 29698 event.duration = parseInt(match[1], 10); 29699 } 29700 29701 _this2.trigger('data', event); 29702 29703 return; 29704 } 29705 29706 match = /^#EXT-X-VERSION:?([0-9.]*)?/.exec(newLine); 29707 29708 if (match) { 29709 event = { 29710 type: 'tag', 29711 tagType: 'version' 29712 }; 29713 29714 if (match[1]) { 29715 event.version = parseInt(match[1], 10); 29716 } 29717 29718 _this2.trigger('data', event); 29719 29720 return; 29721 } 29722 29723 match = /^#EXT-X-MEDIA-SEQUENCE:?(\-?[0-9.]*)?/.exec(newLine); 29724 29725 if (match) { 29726 event = { 29727 type: 'tag', 29728 tagType: 'media-sequence' 29729 }; 29730 29731 if (match[1]) { 29732 event.number = parseInt(match[1], 10); 29733 } 29734 29735 _this2.trigger('data', event); 29736 29737 return; 29738 } 29739 29740 match = /^#EXT-X-DISCONTINUITY-SEQUENCE:?(\-?[0-9.]*)?/.exec(newLine); 29741 29742 if (match) { 29743 event = { 29744 type: 'tag', 29745 tagType: 'discontinuity-sequence' 29746 }; 29747 29748 if (match[1]) { 29749 event.number = parseInt(match[1], 10); 29750 } 29751 29752 _this2.trigger('data', event); 29753 29754 return; 29755 } 29756 29757 match = /^#EXT-X-PLAYLIST-TYPE:?(.*)?$/.exec(newLine); 29758 29759 if (match) { 29760 event = { 29761 type: 'tag', 29762 tagType: 'playlist-type' 29763 }; 29764 29765 if (match[1]) { 29766 event.playlistType = match[1]; 29767 } 29768 29769 _this2.trigger('data', event); 29770 29771 return; 29772 } 29773 29774 match = /^#EXT-X-BYTERANGE:?([0-9.]*)?@?([0-9.]*)?/.exec(newLine); 29775 29776 if (match) { 29777 event = { 29778 type: 'tag', 29779 tagType: 'byterange' 29780 }; 29781 29782 if (match[1]) { 29783 event.length = parseInt(match[1], 10); 29784 } 29785 29786 if (match[2]) { 29787 event.offset = parseInt(match[2], 10); 29788 } 29789 29790 _this2.trigger('data', event); 29791 29792 return; 29793 } 29794 29795 match = /^#EXT-X-ALLOW-CACHE:?(YES|NO)?/.exec(newLine); 29796 29797 if (match) { 29798 event = { 29799 type: 'tag', 29800 tagType: 'allow-cache' 29801 }; 29802 29803 if (match[1]) { 29804 event.allowed = !/NO/.test(match[1]); 29805 } 29806 29807 _this2.trigger('data', event); 29808 29809 return; 29810 } 29811 29812 match = /^#EXT-X-MAP:?(.*)$/.exec(newLine); 29813 29814 if (match) { 29815 event = { 29816 type: 'tag', 29817 tagType: 'map' 29818 }; 29819 29820 if (match[1]) { 29821 var attributes = parseAttributes(match[1]); 29822 29823 if (attributes.URI) { 29824 event.uri = attributes.URI; 29825 } 29826 29827 if (attributes.BYTERANGE) { 29828 var _attributes$BYTERANGE = attributes.BYTERANGE.split('@'), 29829 length = _attributes$BYTERANGE[0], 29830 offset = _attributes$BYTERANGE[1]; 29831 29832 event.byterange = {}; 29833 29834 if (length) { 29835 event.byterange.length = parseInt(length, 10); 29836 } 29837 29838 if (offset) { 29839 event.byterange.offset = parseInt(offset, 10); 29840 } 29841 } 29842 } 29843 29844 _this2.trigger('data', event); 29845 29846 return; 29847 } 29848 29849 match = /^#EXT-X-STREAM-INF:?(.*)$/.exec(newLine); 29850 29851 if (match) { 29852 event = { 29853 type: 'tag', 29854 tagType: 'stream-inf' 29855 }; 29856 29857 if (match[1]) { 29858 event.attributes = parseAttributes(match[1]); 29859 29860 if (event.attributes.RESOLUTION) { 29861 var split = event.attributes.RESOLUTION.split('x'); 29862 var resolution = {}; 29863 29864 if (split[0]) { 29865 resolution.width = parseInt(split[0], 10); 29866 } 29867 29868 if (split[1]) { 29869 resolution.height = parseInt(split[1], 10); 29870 } 29871 29872 event.attributes.RESOLUTION = resolution; 29873 } 29874 29875 if (event.attributes.BANDWIDTH) { 29876 event.attributes.BANDWIDTH = parseInt(event.attributes.BANDWIDTH, 10); 29877 } 29878 29879 if (event.attributes['PROGRAM-ID']) { 29880 event.attributes['PROGRAM-ID'] = parseInt(event.attributes['PROGRAM-ID'], 10); 29881 } 29882 } 29883 29884 _this2.trigger('data', event); 29885 29886 return; 29887 } 29888 29889 match = /^#EXT-X-MEDIA:?(.*)$/.exec(newLine); 29890 29891 if (match) { 29892 event = { 29893 type: 'tag', 29894 tagType: 'media' 29895 }; 29896 29897 if (match[1]) { 29898 event.attributes = parseAttributes(match[1]); 29899 } 29900 29901 _this2.trigger('data', event); 29902 29903 return; 29904 } 29905 29906 match = /^#EXT-X-ENDLIST/.exec(newLine); 29907 29908 if (match) { 29909 _this2.trigger('data', { 29910 type: 'tag', 29911 tagType: 'endlist' 29912 }); 29913 29914 return; 29915 } 29916 29917 match = /^#EXT-X-DISCONTINUITY/.exec(newLine); 29918 29919 if (match) { 29920 _this2.trigger('data', { 29921 type: 'tag', 29922 tagType: 'discontinuity' 29923 }); 29924 29925 return; 29926 } 29927 29928 match = /^#EXT-X-PROGRAM-DATE-TIME:?(.*)$/.exec(newLine); 29929 29930 if (match) { 29931 event = { 29932 type: 'tag', 29933 tagType: 'program-date-time' 29934 }; 29935 29936 if (match[1]) { 29937 event.dateTimeString = match[1]; 29938 event.dateTimeObject = new Date(match[1]); 29939 } 29940 29941 _this2.trigger('data', event); 29942 29943 return; 29944 } 29945 29946 match = /^#EXT-X-KEY:?(.*)$/.exec(newLine); 29947 29948 if (match) { 29949 event = { 29950 type: 'tag', 29951 tagType: 'key' 29952 }; 29953 29954 if (match[1]) { 29955 event.attributes = parseAttributes(match[1]); // parse the IV string into a Uint32Array 29956 29957 if (event.attributes.IV) { 29958 if (event.attributes.IV.substring(0, 2).toLowerCase() === '0x') { 29959 event.attributes.IV = event.attributes.IV.substring(2); 29960 } 29961 29962 event.attributes.IV = event.attributes.IV.match(/.{8}/g); 29963 event.attributes.IV[0] = parseInt(event.attributes.IV[0], 16); 29964 event.attributes.IV[1] = parseInt(event.attributes.IV[1], 16); 29965 event.attributes.IV[2] = parseInt(event.attributes.IV[2], 16); 29966 event.attributes.IV[3] = parseInt(event.attributes.IV[3], 16); 29967 event.attributes.IV = new Uint32Array(event.attributes.IV); 29968 } 29969 } 29970 29971 _this2.trigger('data', event); 29972 29973 return; 29974 } 29975 29976 match = /^#EXT-X-START:?(.*)$/.exec(newLine); 29977 29978 if (match) { 29979 event = { 29980 type: 'tag', 29981 tagType: 'start' 29982 }; 29983 29984 if (match[1]) { 29985 event.attributes = parseAttributes(match[1]); 29986 event.attributes['TIME-OFFSET'] = parseFloat(event.attributes['TIME-OFFSET']); 29987 event.attributes.PRECISE = /YES/.test(event.attributes.PRECISE); 29988 } 29989 29990 _this2.trigger('data', event); 29991 29992 return; 29993 } 29994 29995 match = /^#EXT-X-CUE-OUT-CONT:?(.*)?$/.exec(newLine); 29996 29997 if (match) { 29998 event = { 29999 type: 'tag', 30000 tagType: 'cue-out-cont' 30001 }; 30002 30003 if (match[1]) { 30004 event.data = match[1]; 30005 } else { 30006 event.data = ''; 30007 } 30008 30009 _this2.trigger('data', event); 30010 30011 return; 30012 } 30013 30014 match = /^#EXT-X-CUE-OUT:?(.*)?$/.exec(newLine); 30015 30016 if (match) { 30017 event = { 30018 type: 'tag', 30019 tagType: 'cue-out' 30020 }; 30021 30022 if (match[1]) { 30023 event.data = match[1]; 30024 } else { 30025 event.data = ''; 30026 } 30027 30028 _this2.trigger('data', event); 30029 30030 return; 30031 } 30032 30033 match = /^#EXT-X-CUE-IN:?(.*)?$/.exec(newLine); 30034 30035 if (match) { 30036 event = { 30037 type: 'tag', 30038 tagType: 'cue-in' 30039 }; 30040 30041 if (match[1]) { 30042 event.data = match[1]; 30043 } else { 30044 event.data = ''; 30045 } 30046 30047 _this2.trigger('data', event); 30048 30049 return; 30050 } // unknown tag type 30051 30052 30053 _this2.trigger('data', { 30054 type: 'tag', 30055 data: newLine.slice(4) 30056 }); 30057 }); 30058 } 30059 /** 30060 * Add a parser for custom headers 30061 * 30062 * @param {Object} options a map of options for the added parser 30063 * @param {RegExp} options.expression a regular expression to match the custom header 30064 * @param {string} options.customType the custom type to register to the output 30065 * @param {Function} [options.dataParser] function to parse the line into an object 30066 * @param {boolean} [options.segment] should tag data be attached to the segment object 30067 */ 30068 ; 30069 30070 _proto.addParser = function addParser(_ref) { 30071 var _this3 = this; 30072 30073 var expression = _ref.expression, 30074 customType = _ref.customType, 30075 dataParser = _ref.dataParser, 30076 segment = _ref.segment; 30077 30078 if (typeof dataParser !== 'function') { 30079 dataParser = function dataParser(line) { 30080 return line; 30081 }; 30082 } 30083 30084 this.customParsers.push(function (line) { 30085 var match = expression.exec(line); 30086 30087 if (match) { 30088 _this3.trigger('data', { 30089 type: 'custom', 30090 data: dataParser(line), 30091 customType: customType, 30092 segment: segment 30093 }); 30094 30095 return true; 30096 } 30097 }); 30098 } 30099 /** 30100 * Add a custom header mapper 30101 * 30102 * @param {Object} options 30103 * @param {RegExp} options.expression a regular expression to match the custom header 30104 * @param {Function} options.map function to translate tag into a different tag 30105 */ 30106 ; 30107 30108 _proto.addTagMapper = function addTagMapper(_ref2) { 30109 var expression = _ref2.expression, 30110 map = _ref2.map; 30111 30112 var mapFn = function mapFn(line) { 30113 if (expression.test(line)) { 30114 return map(line); 30115 } 30116 30117 return line; 30118 }; 30119 30120 this.tagMappers.push(mapFn); 30121 }; 30122 30123 return ParseStream; 30124 }(Stream); 30125 30126 function decodeB64ToUint8Array(b64Text) { 30127 var decodedString = window$1.atob(b64Text || ''); 30128 var array = new Uint8Array(decodedString.length); 30129 30130 for (var i = 0; i < decodedString.length; i++) { 30131 array[i] = decodedString.charCodeAt(i); 30132 } 30133 30134 return array; 30135 } 30136 /** 30137 * A parser for M3U8 files. The current interpretation of the input is 30138 * exposed as a property `manifest` on parser objects. It's just two lines to 30139 * create and parse a manifest once you have the contents available as a string: 30140 * 30141 * ```js 30142 * var parser = new m3u8.Parser(); 30143 * parser.push(xhr.responseText); 30144 * ``` 30145 * 30146 * New input can later be applied to update the manifest object by calling 30147 * `push` again. 30148 * 30149 * The parser attempts to create a usable manifest object even if the 30150 * underlying input is somewhat nonsensical. It emits `info` and `warning` 30151 * events during the parse if it encounters input that seems invalid or 30152 * requires some property of the manifest object to be defaulted. 30153 * 30154 * @class Parser 30155 * @extends Stream 30156 */ 30157 30158 30159 var Parser = 30160 /*#__PURE__*/ 30161 function (_Stream) { 30162 _inheritsLoose$1(Parser, _Stream); 30163 30164 function Parser() { 30165 var _this; 30166 30167 _this = _Stream.call(this) || this; 30168 _this.lineStream = new LineStream(); 30169 _this.parseStream = new ParseStream(); 30170 30171 _this.lineStream.pipe(_this.parseStream); 30172 /* eslint-disable consistent-this */ 30173 30174 30175 var self = _assertThisInitialized$1(_this); 30176 /* eslint-enable consistent-this */ 30177 30178 30179 var uris = []; 30180 var currentUri = {}; // if specified, the active EXT-X-MAP definition 30181 30182 var currentMap; // if specified, the active decryption key 30183 30184 var _key; 30185 30186 var noop = function noop() {}; 30187 30188 var defaultMediaGroups = { 30189 'AUDIO': {}, 30190 'VIDEO': {}, 30191 'CLOSED-CAPTIONS': {}, 30192 'SUBTITLES': {} 30193 }; // This is the Widevine UUID from DASH IF IOP. The same exact string is 30194 // used in MPDs with Widevine encrypted streams. 30195 30196 var widevineUuid = 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed'; // group segments into numbered timelines delineated by discontinuities 30197 30198 var currentTimeline = 0; // the manifest is empty until the parse stream begins delivering data 30199 30200 _this.manifest = { 30201 allowCache: true, 30202 discontinuityStarts: [], 30203 segments: [] 30204 }; // update the manifest with the m3u8 entry from the parse stream 30205 30206 _this.parseStream.on('data', function (entry) { 30207 var mediaGroup; 30208 var rendition; 30209 ({ 30210 tag: function tag() { 30211 // switch based on the tag type 30212 (({ 30213 'allow-cache': function allowCache() { 30214 this.manifest.allowCache = entry.allowed; 30215 30216 if (!('allowed' in entry)) { 30217 this.trigger('info', { 30218 message: 'defaulting allowCache to YES' 30219 }); 30220 this.manifest.allowCache = true; 30221 } 30222 }, 30223 byterange: function byterange() { 30224 var byterange = {}; 30225 30226 if ('length' in entry) { 30227 currentUri.byterange = byterange; 30228 byterange.length = entry.length; 30229 30230 if (!('offset' in entry)) { 30231 this.trigger('info', { 30232 message: 'defaulting offset to zero' 30233 }); 30234 entry.offset = 0; 30235 } 30236 } 30237 30238 if ('offset' in entry) { 30239 currentUri.byterange = byterange; 30240 byterange.offset = entry.offset; 30241 } 30242 }, 30243 endlist: function endlist() { 30244 this.manifest.endList = true; 30245 }, 30246 inf: function inf() { 30247 if (!('mediaSequence' in this.manifest)) { 30248 this.manifest.mediaSequence = 0; 30249 this.trigger('info', { 30250 message: 'defaulting media sequence to zero' 30251 }); 30252 } 30253 30254 if (!('discontinuitySequence' in this.manifest)) { 30255 this.manifest.discontinuitySequence = 0; 30256 this.trigger('info', { 30257 message: 'defaulting discontinuity sequence to zero' 30258 }); 30259 } 30260 30261 if (entry.duration > 0) { 30262 currentUri.duration = entry.duration; 30263 } 30264 30265 if (entry.duration === 0) { 30266 currentUri.duration = 0.01; 30267 this.trigger('info', { 30268 message: 'updating zero segment duration to a small value' 30269 }); 30270 } 30271 30272 this.manifest.segments = uris; 30273 }, 30274 key: function key() { 30275 if (!entry.attributes) { 30276 this.trigger('warn', { 30277 message: 'ignoring key declaration without attribute list' 30278 }); 30279 return; 30280 } // clear the active encryption key 30281 30282 30283 if (entry.attributes.METHOD === 'NONE') { 30284 _key = null; 30285 return; 30286 } 30287 30288 if (!entry.attributes.URI) { 30289 this.trigger('warn', { 30290 message: 'ignoring key declaration without URI' 30291 }); 30292 return; 30293 } // check if the content is encrypted for Widevine 30294 // Widevine/HLS spec: https://storage.googleapis.com/wvdocs/Widevine_DRM_HLS.pdf 30295 30296 30297 if (entry.attributes.KEYFORMAT === widevineUuid) { 30298 var VALID_METHODS = ['SAMPLE-AES', 'SAMPLE-AES-CTR', 'SAMPLE-AES-CENC']; 30299 30300 if (VALID_METHODS.indexOf(entry.attributes.METHOD) === -1) { 30301 this.trigger('warn', { 30302 message: 'invalid key method provided for Widevine' 30303 }); 30304 return; 30305 } 30306 30307 if (entry.attributes.METHOD === 'SAMPLE-AES-CENC') { 30308 this.trigger('warn', { 30309 message: 'SAMPLE-AES-CENC is deprecated, please use SAMPLE-AES-CTR instead' 30310 }); 30311 } 30312 30313 if (entry.attributes.URI.substring(0, 23) !== 'data:text/plain;base64,') { 30314 this.trigger('warn', { 30315 message: 'invalid key URI provided for Widevine' 30316 }); 30317 return; 30318 } 30319 30320 if (!(entry.attributes.KEYID && entry.attributes.KEYID.substring(0, 2) === '0x')) { 30321 this.trigger('warn', { 30322 message: 'invalid key ID provided for Widevine' 30323 }); 30324 return; 30325 } // if Widevine key attributes are valid, store them as `contentProtection` 30326 // on the manifest to emulate Widevine tag structure in a DASH mpd 30327 30328 30329 this.manifest.contentProtection = { 30330 'com.widevine.alpha': { 30331 attributes: { 30332 schemeIdUri: entry.attributes.KEYFORMAT, 30333 // remove '0x' from the key id string 30334 keyId: entry.attributes.KEYID.substring(2) 30335 }, 30336 // decode the base64-encoded PSSH box 30337 pssh: decodeB64ToUint8Array(entry.attributes.URI.split(',')[1]) 30338 } 30339 }; 30340 return; 30341 } 30342 30343 if (!entry.attributes.METHOD) { 30344 this.trigger('warn', { 30345 message: 'defaulting key method to AES-128' 30346 }); 30347 } // setup an encryption key for upcoming segments 30348 30349 30350 _key = { 30351 method: entry.attributes.METHOD || 'AES-128', 30352 uri: entry.attributes.URI 30353 }; 30354 30355 if (typeof entry.attributes.IV !== 'undefined') { 30356 _key.iv = entry.attributes.IV; 30357 } 30358 }, 30359 'media-sequence': function mediaSequence() { 30360 if (!isFinite(entry.number)) { 30361 this.trigger('warn', { 30362 message: 'ignoring invalid media sequence: ' + entry.number 30363 }); 30364 return; 30365 } 30366 30367 this.manifest.mediaSequence = entry.number; 30368 }, 30369 'discontinuity-sequence': function discontinuitySequence() { 30370 if (!isFinite(entry.number)) { 30371 this.trigger('warn', { 30372 message: 'ignoring invalid discontinuity sequence: ' + entry.number 30373 }); 30374 return; 30375 } 30376 30377 this.manifest.discontinuitySequence = entry.number; 30378 currentTimeline = entry.number; 30379 }, 30380 'playlist-type': function playlistType() { 30381 if (!/VOD|EVENT/.test(entry.playlistType)) { 30382 this.trigger('warn', { 30383 message: 'ignoring unknown playlist type: ' + entry.playlist 30384 }); 30385 return; 30386 } 30387 30388 this.manifest.playlistType = entry.playlistType; 30389 }, 30390 map: function map() { 30391 currentMap = {}; 30392 30393 if (entry.uri) { 30394 currentMap.uri = entry.uri; 30395 } 30396 30397 if (entry.byterange) { 30398 currentMap.byterange = entry.byterange; 30399 } 30400 }, 30401 'stream-inf': function streamInf() { 30402 this.manifest.playlists = uris; 30403 this.manifest.mediaGroups = this.manifest.mediaGroups || defaultMediaGroups; 30404 30405 if (!entry.attributes) { 30406 this.trigger('warn', { 30407 message: 'ignoring empty stream-inf attributes' 30408 }); 30409 return; 30410 } 30411 30412 if (!currentUri.attributes) { 30413 currentUri.attributes = {}; 30414 } 30415 30416 _extends(currentUri.attributes, entry.attributes); 30417 }, 30418 media: function media() { 30419 this.manifest.mediaGroups = this.manifest.mediaGroups || defaultMediaGroups; 30420 30421 if (!(entry.attributes && entry.attributes.TYPE && entry.attributes['GROUP-ID'] && entry.attributes.NAME)) { 30422 this.trigger('warn', { 30423 message: 'ignoring incomplete or missing media group' 30424 }); 30425 return; 30426 } // find the media group, creating defaults as necessary 30427 30428 30429 var mediaGroupType = this.manifest.mediaGroups[entry.attributes.TYPE]; 30430 mediaGroupType[entry.attributes['GROUP-ID']] = mediaGroupType[entry.attributes['GROUP-ID']] || {}; 30431 mediaGroup = mediaGroupType[entry.attributes['GROUP-ID']]; // collect the rendition metadata 30432 30433 rendition = { 30434 "default": /yes/i.test(entry.attributes.DEFAULT) 30435 }; 30436 30437 if (rendition["default"]) { 30438 rendition.autoselect = true; 30439 } else { 30440 rendition.autoselect = /yes/i.test(entry.attributes.AUTOSELECT); 30441 } 30442 30443 if (entry.attributes.LANGUAGE) { 30444 rendition.language = entry.attributes.LANGUAGE; 30445 } 30446 30447 if (entry.attributes.URI) { 30448 rendition.uri = entry.attributes.URI; 30449 } 30450 30451 if (entry.attributes['INSTREAM-ID']) { 30452 rendition.instreamId = entry.attributes['INSTREAM-ID']; 30453 } 30454 30455 if (entry.attributes.CHARACTERISTICS) { 30456 rendition.characteristics = entry.attributes.CHARACTERISTICS; 30457 } 30458 30459 if (entry.attributes.FORCED) { 30460 rendition.forced = /yes/i.test(entry.attributes.FORCED); 30461 } // insert the new rendition 30462 30463 30464 mediaGroup[entry.attributes.NAME] = rendition; 30465 }, 30466 discontinuity: function discontinuity() { 30467 currentTimeline += 1; 30468 currentUri.discontinuity = true; 30469 this.manifest.discontinuityStarts.push(uris.length); 30470 }, 30471 'program-date-time': function programDateTime() { 30472 if (typeof this.manifest.dateTimeString === 'undefined') { 30473 // PROGRAM-DATE-TIME is a media-segment tag, but for backwards 30474 // compatibility, we add the first occurence of the PROGRAM-DATE-TIME tag 30475 // to the manifest object 30476 // TODO: Consider removing this in future major version 30477 this.manifest.dateTimeString = entry.dateTimeString; 30478 this.manifest.dateTimeObject = entry.dateTimeObject; 30479 } 30480 30481 currentUri.dateTimeString = entry.dateTimeString; 30482 currentUri.dateTimeObject = entry.dateTimeObject; 30483 }, 30484 targetduration: function targetduration() { 30485 if (!isFinite(entry.duration) || entry.duration < 0) { 30486 this.trigger('warn', { 30487 message: 'ignoring invalid target duration: ' + entry.duration 30488 }); 30489 return; 30490 } 30491 30492 this.manifest.targetDuration = entry.duration; 30493 }, 30494 totalduration: function totalduration() { 30495 if (!isFinite(entry.duration) || entry.duration < 0) { 30496 this.trigger('warn', { 30497 message: 'ignoring invalid total duration: ' + entry.duration 30498 }); 30499 return; 30500 } 30501 30502 this.manifest.totalDuration = entry.duration; 30503 }, 30504 start: function start() { 30505 if (!entry.attributes || isNaN(entry.attributes['TIME-OFFSET'])) { 30506 this.trigger('warn', { 30507 message: 'ignoring start declaration without appropriate attribute list' 30508 }); 30509 return; 30510 } 30511 30512 this.manifest.start = { 30513 timeOffset: entry.attributes['TIME-OFFSET'], 30514 precise: entry.attributes.PRECISE 30515 }; 30516 }, 30517 'cue-out': function cueOut() { 30518 currentUri.cueOut = entry.data; 30519 }, 30520 'cue-out-cont': function cueOutCont() { 30521 currentUri.cueOutCont = entry.data; 30522 }, 30523 'cue-in': function cueIn() { 30524 currentUri.cueIn = entry.data; 30525 } 30526 })[entry.tagType] || noop).call(self); 30527 }, 30528 uri: function uri() { 30529 currentUri.uri = entry.uri; 30530 uris.push(currentUri); // if no explicit duration was declared, use the target duration 30531 30532 if (this.manifest.targetDuration && !('duration' in currentUri)) { 30533 this.trigger('warn', { 30534 message: 'defaulting segment duration to the target duration' 30535 }); 30536 currentUri.duration = this.manifest.targetDuration; 30537 } // annotate with encryption information, if necessary 30538 30539 30540 if (_key) { 30541 currentUri.key = _key; 30542 } 30543 30544 currentUri.timeline = currentTimeline; // annotate with initialization segment information, if necessary 30545 30546 if (currentMap) { 30547 currentUri.map = currentMap; 30548 } // prepare for the next URI 30549 30550 30551 currentUri = {}; 30552 }, 30553 comment: function comment() {// comments are not important for playback 30554 }, 30555 custom: function custom() { 30556 // if this is segment-level data attach the output to the segment 30557 if (entry.segment) { 30558 currentUri.custom = currentUri.custom || {}; 30559 currentUri.custom[entry.customType] = entry.data; // if this is manifest-level data attach to the top level manifest object 30560 } else { 30561 this.manifest.custom = this.manifest.custom || {}; 30562 this.manifest.custom[entry.customType] = entry.data; 30563 } 30564 } 30565 })[entry.type].call(self); 30566 }); 30567 30568 return _this; 30569 } 30570 /** 30571 * Parse the input string and update the manifest object. 30572 * 30573 * @param {string} chunk a potentially incomplete portion of the manifest 30574 */ 30575 30576 30577 var _proto = Parser.prototype; 30578 30579 _proto.push = function push(chunk) { 30580 this.lineStream.push(chunk); 30581 } 30582 /** 30583 * Flush any remaining input. This can be handy if the last line of an M3U8 30584 * manifest did not contain a trailing newline but the file has been 30585 * completely received. 30586 */ 30587 ; 30588 30589 _proto.end = function end() { 30590 // flush any buffered input 30591 this.lineStream.push('\n'); 30592 } 30593 /** 30594 * Add an additional parser for non-standard tags 30595 * 30596 * @param {Object} options a map of options for the added parser 30597 * @param {RegExp} options.expression a regular expression to match the custom header 30598 * @param {string} options.type the type to register to the output 30599 * @param {Function} [options.dataParser] function to parse the line into an object 30600 * @param {boolean} [options.segment] should tag data be attached to the segment object 30601 */ 30602 ; 30603 30604 _proto.addParser = function addParser(options) { 30605 this.parseStream.addParser(options); 30606 } 30607 /** 30608 * Add a custom header mapper 30609 * 30610 * @param {Object} options 30611 * @param {RegExp} options.expression a regular expression to match the custom header 30612 * @param {Function} options.map function to translate tag into a different tag 30613 */ 30614 ; 30615 30616 _proto.addTagMapper = function addTagMapper(options) { 30617 this.parseStream.addTagMapper(options); 30618 }; 30619 30620 return Parser; 30621 }(Stream); 30622 30623 /*! @name mpd-parser @version 0.8.1 @license Apache-2.0 */ 30624 30625 var isObject$1 = function isObject(obj) { 30626 return !!obj && typeof obj === 'object'; 30627 }; 30628 30629 var merge = function merge() { 30630 for (var _len = arguments.length, objects = new Array(_len), _key = 0; _key < _len; _key++) { 30631 objects[_key] = arguments[_key]; 30632 } 30633 30634 return objects.reduce(function (result, source) { 30635 Object.keys(source).forEach(function (key) { 30636 if (Array.isArray(result[key]) && Array.isArray(source[key])) { 30637 result[key] = result[key].concat(source[key]); 30638 } else if (isObject$1(result[key]) && isObject$1(source[key])) { 30639 result[key] = merge(result[key], source[key]); 30640 } else { 30641 result[key] = source[key]; 30642 } 30643 }); 30644 return result; 30645 }, {}); 30646 }; 30647 30648 var values = function values(o) { 30649 return Object.keys(o).map(function (k) { 30650 return o[k]; 30651 }); 30652 }; 30653 30654 var range = function range(start, end) { 30655 var result = []; 30656 30657 for (var i = start; i < end; i++) { 30658 result.push(i); 30659 } 30660 30661 return result; 30662 }; 30663 30664 var flatten = function flatten(lists) { 30665 return lists.reduce(function (x, y) { 30666 return x.concat(y); 30667 }, []); 30668 }; 30669 30670 var from = function from(list) { 30671 if (!list.length) { 30672 return []; 30673 } 30674 30675 var result = []; 30676 30677 for (var i = 0; i < list.length; i++) { 30678 result.push(list[i]); 30679 } 30680 30681 return result; 30682 }; 30683 30684 var findIndexes = function findIndexes(l, key) { 30685 return l.reduce(function (a, e, i) { 30686 if (e[key]) { 30687 a.push(i); 30688 } 30689 30690 return a; 30691 }, []); 30692 }; 30693 30694 var errors = { 30695 INVALID_NUMBER_OF_PERIOD: 'INVALID_NUMBER_OF_PERIOD', 30696 DASH_EMPTY_MANIFEST: 'DASH_EMPTY_MANIFEST', 30697 DASH_INVALID_XML: 'DASH_INVALID_XML', 30698 NO_BASE_URL: 'NO_BASE_URL', 30699 MISSING_SEGMENT_INFORMATION: 'MISSING_SEGMENT_INFORMATION', 30700 SEGMENT_TIME_UNSPECIFIED: 'SEGMENT_TIME_UNSPECIFIED', 30701 UNSUPPORTED_UTC_TIMING_SCHEME: 'UNSUPPORTED_UTC_TIMING_SCHEME' 30702 }; 30703 30704 function createCommonjsModule$1(fn, module) { 30705 return module = { 30706 exports: {} 30707 }, fn(module, module.exports), module.exports; 30708 } 30709 30710 var urlToolkit$1 = createCommonjsModule$1(function (module, exports) { 30711 // see https://tools.ietf.org/html/rfc1808 30712 30713 /* jshint ignore:start */ 30714 (function (root) { 30715 /* jshint ignore:end */ 30716 var URL_REGEX = /^((?:[a-zA-Z0-9+\-.]+:)?)(\/\/[^\/?#]*)?((?:[^\/\?#]*\/)*.*?)??(;.*?)?(\?.*?)?(#.*?)?$/; 30717 var FIRST_SEGMENT_REGEX = /^([^\/?#]*)(.*)$/; 30718 var SLASH_DOT_REGEX = /(?:\/|^)\.(?=\/)/g; 30719 var SLASH_DOT_DOT_REGEX = /(?:\/|^)\.\.\/(?!\.\.\/).*?(?=\/)/g; 30720 var URLToolkit = { 30721 // jshint ignore:line 30722 // If opts.alwaysNormalize is true then the path will always be normalized even when it starts with / or // 30723 // E.g
vendor: 4,132 bytes, lines 30724-30817
30724 // With opts.alwaysNormalize = false (default, spec compliant) 30725 // http://a.com/b/cd + /e/f/../g => http://a.com/e/f/../g 30726 // With opts.alwaysNormalize = true (not spec compliant) 30727 // http://a.com/b/cd + /e/f/../g => http://a.com/e/g 30728 buildAbsoluteURL: function buildAbsoluteURL(baseURL, relativeURL, opts) { 30729 opts = opts || {}; // remove any remaining space and CRLF 30730 30731 baseURL = baseURL.trim(); 30732 relativeURL = relativeURL.trim(); 30733 30734 if (!relativeURL) { 30735 // 2a) If the embedded URL is entirely empty, it inherits the 30736 // entire base URL (i.e., is set equal to the base URL) 30737 // and we are done. 30738 if (!opts.alwaysNormalize) { 30739 return baseURL; 30740 } 30741 30742 var basePartsForNormalise = URLToolkit.parseURL(baseURL); 30743 30744 if (!basePartsForNormalise) { 30745 throw new Error('Error trying to parse base URL.'); 30746 } 30747 30748 basePartsForNormalise.path = URLToolkit.normalizePath(basePartsForNormalise.path); 30749 return URLToolkit.buildURLFromParts(basePartsForNormalise); 30750 } 30751 30752 var relativeParts = URLToolkit.parseURL(relativeURL); 30753 30754 if (!relativeParts) { 30755 throw new Error('Error trying to parse relative URL.'); 30756 } 30757 30758 if (relativeParts.scheme) { 30759 // 2b) If the embedded URL starts with a scheme name, it is 30760 // interpreted as an absolute URL and we are done. 30761 if (!opts.alwaysNormalize) { 30762 return relativeURL; 30763 } 30764 30765 relativeParts.path = URLToolkit.normalizePath(relativeParts.path); 30766 return URLToolkit.buildURLFromParts(relativeParts); 30767 } 30768 30769 var baseParts = URLToolkit.parseURL(baseURL); 30770 30771 if (!baseParts) { 30772 throw new Error('Error trying to parse base URL.'); 30773 } 30774 30775 if (!baseParts.netLoc && baseParts.path && baseParts.path[0] !== '/') { 30776 // If netLoc missing and path doesn't start with '/', assume everthing before the first '/' is the netLoc 30777 // This causes 'example.com/a' to be handled as '//example.com/a' instead of '/example.com/a' 30778 var pathParts = FIRST_SEGMENT_REGEX.exec(baseParts.path); 30779 baseParts.netLoc = pathParts[1]; 30780 baseParts.path = pathParts[2]; 30781 } 30782 30783 if (baseParts.netLoc && !baseParts.path) { 30784 baseParts.path = '/'; 30785 } 30786 30787 var builtParts = { 30788 // 2c) Otherwise, the embedded URL inherits the scheme of 30789 // the base URL. 30790 scheme: baseParts.scheme, 30791 netLoc: relativeParts.netLoc, 30792 path: null, 30793 params: relativeParts.params, 30794 query: relativeParts.query, 30795 fragment: relativeParts.fragment 30796 }; 30797 30798 if (!relativeParts.netLoc) { 30799 // 3) If the embedded URL's <net_loc> is non-empty, we skip to 30800 // Step 7. Otherwise, the embedded URL inherits the <net_loc> 30801 // (if any) of the base URL. 30802 builtParts.netLoc = baseParts.netLoc; // 4) If the embedded URL path is preceded by a slash "/", the 30803 // path is not relative and we skip to Step 7. 30804 30805 if (relativeParts.path[0] !== '/') { 30806 if (!relativeParts.path) { 30807 // 5) If the embedded URL path is empty (and not preceded by a 30808 // slash), then the embedded URL inherits the base URL path 30809 builtParts.path = baseParts.path; // 5a) if the embedded URL's <params> is non-empty, we skip to 30810 // step 7; otherwise, it inherits the <params> of the base 30811 // URL (if any) and 30812 30813 if (!relativeParts.params) { 30814 builtParts.params = baseParts.params; // 5b) if the embedded URL's <query> is non-empty, we skip to 30815 // step 7; otherwise, it inherits the <query> of the base 30816 // URL (if any) and we skip to step 7. 30817
vendor: 4,112 bytes, lines 30818-30920
30818 if (!relativeParts.query) { 30819 builtParts.query = baseParts.query; 30820 } 30821 } 30822 } else { 30823 // 6) The last segment of the base URL's path (anything 30824 // following the rightmost slash "/", or the entire path if no 30825 // slash is present) is removed and the embedded URL's path is 30826 // appended in its place. 30827 var baseURLPath = baseParts.path; 30828 var newPath = baseURLPath.substring(0, baseURLPath.lastIndexOf('/') + 1) + relativeParts.path; 30829 builtParts.path = URLToolkit.normalizePath(newPath); 30830 } 30831 } 30832 } 30833 30834 if (builtParts.path === null) { 30835 builtParts.path = opts.alwaysNormalize ? URLToolkit.normalizePath(relativeParts.path) : relativeParts.path; 30836 } 30837 30838 return URLToolkit.buildURLFromParts(builtParts); 30839 }, 30840 parseURL: function parseURL(url) { 30841 var parts = URL_REGEX.exec(url); 30842 30843 if (!parts) { 30844 return null; 30845 } 30846 30847 return { 30848 scheme: parts[1] || '', 30849 netLoc: parts[2] || '', 30850 path: parts[3] || '', 30851 params: parts[4] || '', 30852 query: parts[5] || '', 30853 fragment: parts[6] || '' 30854 }; 30855 }, 30856 normalizePath: function normalizePath(path) { 30857 // The following operations are 30858 // then applied, in order, to the new path: 30859 // 6a) All occurrences of "./", where "." is a complete path 30860 // segment, are removed. 30861 // 6b) If the path ends with "." as a complete path segment, 30862 // that "." is removed. 30863 path = path.split('').reverse().join('').replace(SLASH_DOT_REGEX, ''); // 6c) All occurrences of "<segment>/../", where <segment> is a 30864 // complete path segment not equal to "..", are removed. 30865 // Removal of these path segments is performed iteratively, 30866 // removing the leftmost matching pattern on each iteration, 30867 // until no matching pattern remains. 30868 // 6d) If the path ends with "<segment>/..", where <segment> is a 30869 // complete path segment not equal to "..", that 30870 // "<segment>/.." is removed. 30871 30872 while (path.length !== (path = path.replace(SLASH_DOT_DOT_REGEX, '')).length) {} // jshint ignore:line 30873 30874 30875 return path.split('').reverse().join(''); 30876 }, 30877 buildURLFromParts: function buildURLFromParts(parts) { 30878 return parts.scheme + parts.netLoc + parts.path + parts.params + parts.query + parts.fragment; 30879 } 30880 }; 30881 /* jshint ignore:start */ 30882 30883 module.exports = URLToolkit; 30884 })(); 30885 /* jshint ignore:end */ 30886 30887 }); 30888 30889 var resolveUrl = function resolveUrl(baseUrl, relativeUrl) { 30890 // return early if we don't need to resolve 30891 if (/^[a-z]+:/i.test(relativeUrl)) { 30892 return relativeUrl; 30893 } // if the base URL is relative then combine with the current location 30894 30895 30896 if (!/\/\//i.test(baseUrl)) { 30897 baseUrl = urlToolkit$1.buildAbsoluteURL(window$1.location.href, baseUrl); 30898 } 30899 30900 return urlToolkit$1.buildAbsoluteURL(baseUrl, relativeUrl); 30901 }; 30902 /** 30903 * @typedef {Object} SingleUri 30904 * @property {string} uri - relative location of segment 30905 * @property {string} resolvedUri - resolved location of segment 30906 * @property {Object} byterange - Object containing information on how to make byte range 30907 * requests following byte-range-spec per RFC2616. 30908 * @property {String} byterange.length - length of range request 30909 * @property {String} byterange.offset - byte offset of range request 30910 * 30911 * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.1 30912 */ 30913 30914 /** 30915 * Converts a URLType node (5.3.9.2.3 Table 13) to a segment object 30916 * that conforms to how m3u8-parser is structured 30917 * 30918 * @see https://github.com/videojs/m3u8-parser 30919 * 30920 * @param {string} baseUrl - baseUrl provided by <BaseUrl>
30920 nodes 30921 * @param {string} source - source url for segment 30922 * @param {string} range - optional range used for range calls, 30923 * follows RFC 2616, Clause 14.35.1 30924 * @return {SingleUri} full segment information transformed into a format similar 30925 * to m3u8-parser 30926 */ 30927 30928 30929 var urlTypeToSegment = function urlTypeToSegment(_ref) { 30930 var _ref$baseUrl = _ref.baseUrl, 30931 baseUrl = _ref$baseUrl === void 0 ? '' : _ref$baseUrl, 30932 _ref$source = _ref.source, 30933 source = _ref$source === void 0 ? '' : _ref$source, 30934 _ref$range = _ref.range, 30935 range = _ref$range === void 0 ? '' : _ref$range, 30936 _ref$indexRange = _ref.indexRange, 30937 indexRange = _ref$indexRange === void 0 ? '' : _ref$indexRange; 30938 var segment = { 30939 uri: source, 30940 resolvedUri: resolveUrl(baseUrl || '', source) 30941 }; 30942 30943 if (range || indexRange) { 30944 var rangeStr = range ? range : indexRange; 30945 var ranges = rangeStr.split('-'); 30946 var startRange = parseInt(ranges[0], 10); 30947 var endRange = parseInt(ranges[1], 10); // byterange should be inclusive according to 30948 // RFC 2616, Clause 14.35.1 30949 30950 segment.byterange = { 30951 length: endRange - startRange + 1, 30952 offset: startRange 30953 }; 30954 } 30955 30956 return segment; 30957 }; 30958 30959 var byteRangeToString = function byteRangeToString(byterange) { 30960 // `endRange` is one less than `offset + length` because the HTTP range 30961 // header uses inclusive ranges 30962 var endRange = byterange.offset + byterange.length - 1; 30963 return byterange.offset + "-" + endRange; 30964 }; 30965 /** 30966 * Functions for calculating the range of available segments in static and dynamic 30967 * manifests. 30968 */ 30969 30970 30971 var segmentRange = { 30972 /** 30973 * Returns the entire range of available segments for a static MPD 30974 * 30975 * @param {Object} attributes 30976 * Inheritied MPD attributes 30977 * @return {{ start: number, end: number }} 30978 * The start and end numbers for available segments 30979 */ 30980 "static": function _static(attributes) { 30981 var duration = attributes.duration, 30982 _attributes$timescale = attributes.timescale, 30983 timescale = _attributes$timescale === void 0 ? 1 : _attributes$timescale, 30984 sourceDuration = attributes.sourceDuration; 30985 return { 30986 start: 0, 30987 end: Math.ceil(sourceDuration / (duration / timescale)) 30988 }; 30989 }, 30990 30991 /** 30992 * Returns the current live window range of available segments for a dynamic MPD 30993 * 30994 * @param {Object} attributes 30995 * Inheritied MPD attributes 30996 * @return {{ start: number, end: number }} 30997 * The start and end numbers for available segments 30998 */ 30999 dynamic: function dynamic(attributes) { 31000 var NOW = attributes.NOW, 31001 clientOffset = attributes.clientOffset, 31002 availabilityStartTime = attributes.availabilityStartTime, 31003 _attributes$timescale2 = attributes.timescale, 31004 timescale = _attributes$timescale2 === void 0 ? 1 : _attributes$timescale2, 31005 duration = attributes.duration, 31006 _attributes$start = attributes.start, 31007 start = _attributes$start === void 0 ? 0 : _attributes$start, 31008 _attributes$minimumUp = attributes.minimumUpdatePeriod, 31009 minimumUpdatePeriod = _attributes$minimumUp === void 0 ? 0 : _attributes$minimumUp, 31010 _attributes$timeShift = attributes.timeShiftBufferDepth, 31011 timeShiftBufferDepth = _attributes$timeShift === void 0 ? Infinity : _attributes$timeShift; 31012 var now = (NOW + clientOffset) / 1000; 31013 var periodStartWC = availabilityStartTime + start; 31014 var periodEndWC = now + minimumUpdatePeriod; 31015 var periodDuration = periodEndWC - periodStartWC; 31016 var segmentCount = Math.ceil(periodDuration * timescale / duration); 31017 var availableStart = Math.floor((now - periodStartWC - timeShiftBufferDepth) * timescale / duration); 31018 var availableEnd = Math.floor((now - periodStartWC) * timescale / duration); 31019 return { 31020 start: Math.max(0, availableStart), 31021 end: Math.min(segmentCount, availableEnd) 31022 }; 31023 } 31024 }; 31025 /** 31026 * Maps a range of numbers to objects with information needed to build the corresponding 31027 * segment list 31028 * 31029 * @name toSegmentsCallback 31030 * @function 31031 * @param {number} number 31032 * Number of the segment 31033 * @param {number} index 31034 * Index of the number in the range list 31035 * @return {{ number: Number, duration: Number, timeline: Number, time: Number }} 31036 * Object with segment timing and duration info 31037 */ 31038 31039 /** 31040 * Returns a callback for Array.prototype.map for mapping a range of numbers to 31041 * information needed to build the segment list. 31042 * 31043 * @param {Object} attributes 31044 * Inherited MPD attributes 31045 * @return {toSegmentsCallback} 31046 * Callback map function 31047 */ 31048 31049 var toSegments = function toSegments(attributes) { 31050 return function (number, index) { 31051 var duration = attributes.duration, 31052 _attributes$timescale3 = attributes.timescale, 31053 timescale = _attributes$timescale3 === void 0 ? 1 : _attributes$timescale3, 31054 periodIndex = attributes.periodIndex, 31055 _attributes$startNumb = attributes.startNumber, 31056 startNumber = _attributes$startNumb === void 0 ? 1 : _attributes$startNumb;
31057 return { 31058 number: startNumber + number, 31059 duration: duration / timescale, 31060 timeline: periodIndex, 31061 time: index * duration 31062 }; 31063 }; 31064 }; 31065 /** 31066 * Returns a list of objects containing segment timing and duration info used for 31067 * building the list of segments. This uses the @duration attribute specified 31068 * in the MPD manifest to derive the range of segments. 31069 * 31070 * @param {Object} attributes 31071 * Inherited MPD attributes 31072 * @return {{number: number, duration: number, time: number, timeline: number}[]} 31073 * List of Objects with segment timing and duration info 31074 */ 31075 31076 31077 var parseByDuration = function parseByDuration(attributes) { 31078 var _attributes$type = attributes.type, 31079 type = _attributes$type === void 0 ? 'static' : _attributes$type, 31080 duration = attributes.duration, 31081 _attributes$timescale4 = attributes.timescale, 31082 timescale = _attributes$timescale4 === void 0 ? 1 : _attributes$timescale4, 31083 sourceDuration = attributes.sourceDuration; 31084 31085 var _segmentRange$type = segmentRange[type](attributes), 31086 start = _segmentRange$type.start, 31087 end = _segmentRange$type.end; 31088 31089 var segments = range(start, end).map(toSegments(attributes)); 31090 31091 if (type === 'static') { 31092 var index = segments.length - 1; // final segment may be less than full segment duration 31093 31094 segments[index].duration = sourceDuration - duration / timescale * index; 31095 } 31096 31097 return segments; 31098 }; 31099 /** 31100 * Translates SegmentBase into a set of segments. 31101 * (DASH SPEC Section 5.3.9.3.2) contains a set of <SegmentURL> nodes. Each 31102 * node should be translated into segment. 31103 * 31104 * @param {Object} attributes 31105 * Object containing all inherited attributes from parent elements with attribute 31106 * names as keys 31107 * @return {Object.<Array>} list of segments 31108 */ 31109 31110 31111 var segmentsFromBase = function segmentsFromBase(attributes) { 31112 var baseUrl = attributes.baseUrl, 31113 _attributes$initializ = attributes.initialization, 31114 initialization = _attributes$initializ === void 0 ? {} : _attributes$initializ, 31115 sourceDuration = attributes.sourceDuration, 31116 _attributes$timescale = attributes.timescale, 31117 timescale = _attributes$timescale === void 0 ? 1 : _attributes$timescale, 31118 _attributes$indexRang = attributes.indexRange, 31119 indexRange = _attributes$indexRang === void 0 ? '' : _attributes$indexRang, 31120 duration = attributes.duration; // base url is required for SegmentBase to work, per spec (Section 5.3.9.2.1) 31121 31122 if (!baseUrl) { 31123 throw new Error(errors.NO_BASE_URL); 31124 } 31125 31126 var initSegment = urlTypeToSegment({ 31127 baseUrl: baseUrl, 31128 source: initialization.sourceURL, 31129 range: initialization.range 31130 }); 31131 var segment = urlTypeToSegment({ 31132 baseUrl: baseUrl, 31133 source: baseUrl, 31134 indexRange: indexRange 31135 }); 31136 segment.map = initSegment; // If there is a duration, use it, otherwise use the given duration of the source 31137 // (since SegmentBase is only for one total segment) 31138 31139 if (duration) { 31140 var segmentTimeInfo = parseByDuration(attributes); 31141 31142 if (segmentTimeInfo.length) { 31143 segment.duration = segmentTimeInfo[0].duration; 31144 segment.timeline = segmentTimeInfo[0].timeline; 31145 } 31146 } else if (sourceDuration) { 31147 segment.duration = sourceDuration / timescale; 31148 segment.timeline = 0; 31149 } // This is used for mediaSequence 31150 31151 31152 segment.number = 0; 31153 return [segment]; 31154 }; 31155 /** 31156 * Given a playlist, a sidx box, and a baseUrl, update the segment list of the playlist 31157 * according to the sidx information given. 31158 * 31159 * playlist.sidx has metadadata about the sidx where-as the sidx param 31160 * is the parsed sidx box itself. 31161 * 31162 * @param {Object} playlist the playlist to update the sidx information for 31163 * @param {Object} sidx the parsed sidx box 31164 * @return {Object} the playlist object with the updated sidx information 31165 */ 31166 31167 31168 var addSegmentsToPlaylist = function addSegmentsToPlaylist(playlist, sidx, baseUrl) { 31169 // Retain init segment information 31170 var initSegment = playlist.sidx.map ? playlist.sidx.map : null; // Retain source duration from initial master manifest parsing 31171 31172 var sourceDuration = playlist.sidx.duration; // Retain source timeline 31173 31174 var timeline = playlist.timeline || 0; 31175 var sidxByteRange = playlist.sidx.byterange; 31176 var sidxEnd = sidxByteRange.offset + sidxByteRange.length; // Retain timescale of the parsed sidx 31177 31178 var timescale = sidx.timescale; // referenceType 1 refers to other sidx boxes 31179 31180 var mediaReferences = sidx.references.filter(function (r) { 31181 return r.referenceType !== 1; 31182 }); 31183 var segments = []; // firstOffset is the offset from the end of the sidx box 31184 31185 var startIndex = sidxEnd + sidx.firstOffset; 31186 31187 for (var i = 0; i < mediaReferences.length; i++) { 31188 var reference = sidx.references[i]; // size of the referenced (sub)segment 31189 31190 var size = reference.referencedSize; // duration of the referenced (sub)segment, in the timescale 31191 // this will be converted to seconds when generating segments 31192 31193 var duration = reference.subsegmentDuration; // should be an inclusive range 31194 31195 var endIndex = startIndex + size - 1; 31196 var indexRange = startIndex + "-" + endIndex; 31197 var attributes = { 31198 baseUrl: baseUrl, 31199 timescale: timescale, 31200 timeline: timeline, 31201 // this is used in parseByDuration 31202 periodIndex: timeline, 31203 duration: duration, 31204 sourceDuration: sourceDuration, 31205 indexRange: indexRange 31206 }; 31207 var segment = segmentsFromBase(attributes)[0]; 31208 31209 if (initSegment) { 31210 segment.map = initSegment; 31211 } 31212 31213 segments.push(segment); 31214 startIndex += size; 31215 } 31216 31217 playlist.segments = segments; 31218 return playlist; 31219 }; 31220 31221 var mergeDiscontiguousPlaylists = function mergeDiscontiguousPlaylists(playlists) { 31222 var mergedPlaylists = values(playlists.reduce(function (acc, playlist) { 31223 // assuming playlist IDs are the same across periods 31224 // TODO: handle multiperiod where representation sets are not the same 31225 // across periods 31226 var name = playlist.attributes.id + (playlist.attributes.lang || ''); // Periods after first 31227 31228 if (acc[name]) { 31229 var _acc$name$segments; // first segment of subsequent periods signal a discontinuity 31230 31231 31232 if (playlist.segments[0]) { 31233 playlist.segments[0].discontinuity = true; 31234 } 31235 31236 (_acc$name$segments = acc[name].segments).push.apply(_acc$name$segments, playlist.segments); // bubble up contentProtection, this assumes all DRM content 31237 // has the same contentProtection 31238 31239 31240 if (playlist.attributes.contentProtection) { 31241 acc[name].attributes.contentProtection = playlist.attributes.contentProtection; 31242 } 31243 } else { 31244 // first Period 31245 acc[name] = playlist; 31246 } 31247 31248 return acc; 31249 }, {})); 31250 return mergedPlaylists.map(function (playlist) { 31251 playlist.discontinuityStarts = findIndexes(playlist.segments, 'discontinuity'); 31252 return playlist; 31253 }); 31254 }; 31255 31256 var addSegmentInfoFromSidx = function addSegmentInfoFromSidx(playlists, sidxMapping) { 31257 if (sidxMapping === void 0) { 31258 sidxMapping = {}; 31259 } 31260 31261 if (!Object.keys(sidxMapping).length) { 31262 return playlists; 31263 } 31264 31265 for (var i in playlists) { 31266 var playlist = playlists[i]; 31267 31268 if (!playlist.sidx) { 31269 continue; 31270 } 31271 31272 var sidxKey = playlist.sidx.uri + '-' + byteRangeToString(playlist.sidx.byterange); 31273 var sidxMatch = sidxMapping[sidxKey] && sidxMapping[sidxKey].sidx; 31274 31275 if (playlist.sidx && sidxMatch) { 31276 addSegmentsToPlaylist(playlist, sidxMatch, playlist.sidx.resolvedUri); 31277 } 31278 } 31279 31280 return playlists; 31281 }; 31282 31283 var formatAudioPlaylist = function formatAudioPlaylist(_ref) { 31284 var _attributes; 31285 31286 var attributes = _ref.attributes, 31287 segments = _ref.segments, 31288 sidx = _ref.sidx; 31289 var playlist = { 31290 attributes: (_attributes = { 31291 NAME: attributes.id, 31292 BANDWIDTH: attributes.bandwidth, 31293 CODECS: attributes.codecs 31294 }, _attributes['PROGRAM-ID'] = 1, _attributes), 31295 uri: '', 31296 endList: (attributes.type || 'static') === 'static', 31297 timeline: attributes.periodIndex, 31298 resolvedUri: '', 31299 targetDuration: attributes.duration, 31300 segments: segments, 31301 mediaSequence: segments.length ? segments[0].number : 1 31302 }; 31303 31304 if (attributes.contentProtection) { 31305 playlist.contentProtection = attributes.contentProtection; 31306 } 31307 31308 if (sidx) { 31309 playlist.sidx = sidx; 31310 } 31311 31312 return playlist; 31313 }; 31314 31315 var formatVttPlaylist = function formatVttPlaylist(_ref2) { 31316 var _attributes2; 31317 31318 var attributes = _ref2.attributes, 31319 segments = _ref2.segments; 31320 31321 if (typeof segments === 'undefined') { 31322 // vtt tracks may use single file in BaseURL 31323 segments = [{ 31324 uri: attributes.baseUrl, 31325 timeline: attributes.periodIndex, 31326 resolvedUri: attributes.baseUrl || '', 31327 duration: attributes.sourceDuration, 31328 number: 0 31329 }]; // targetDuration should be the same duration as the only segment 31330 31331 attributes.duration = attributes.sourceDuration; 31332 } 31333 31334 return { 31335 attributes: (_attributes2 = { 31336 NAME: attributes.id, 31337 BANDWIDTH: attributes.bandwidth 31338 }, _attributes2['PROGRAM-ID'] = 1, _attributes2), 31339 uri: '', 31340 endList: (attributes.type || 'static') === 'static', 31341 timeline: attributes.periodIndex, 31342 resolvedUri: attributes.baseUrl || '', 31343 targetDuration: attributes.duration, 31344 segments: segments, 31345 mediaSequence: segments.length ? segments[0].number : 1 31346 }; 31347 }; 31348 31349 var organizeAudioPlaylists = function organizeAudioPlaylists(playlists, sidxMapping) { 31350 if (sidxMapping === void 0) { 31351 sidxMapping = {}; 31352 } 31353 31354 var mainPlaylist; 31355 var formattedPlaylists = playlists.reduce(function (a, playlist) { 31356 var role = playlist.attributes.role && playlist.attributes.role.value || ''; 31357 var language = playlist.attributes.lang || ''; 31358 var label = 'main'; 31359 31360 if (language) { 31361 var roleLabel = role ? " (" + role + ")" : ''; 31362 label = "" + playlist.attributes.lang + roleLabel; 31363 } // skip if we already have the highest quality audio for a language 31364 31365 31366 if (a[label] && a[label].playlists[0].attributes.BANDWIDTH > playlist.attributes.bandwidth) { 31367 return a; 31368 } 31369 31370 a[label] = { 31371 language: language, 31372 autoselect: true, 31373 "default": role === 'main', 31374 playlists: addSegmentInfoFromSidx([formatAudioPlaylist(playlist)], sidxMapping), 31375 uri: '' 31376 }; 31377 31378 if (typeof mainPlaylist === 'undefined' && role === 'main') { 31379 mainPlaylist = playlist; 31380 mainPlaylist["default"] = true; 31381 } 31382 31383 return a; 31384 }, {}); // if no playlists have role "main", mark the first as main 31385 31386 if (!mainPlaylist) { 31387 var firstLabel = Object.keys(formattedPlaylists)[0]; 31388 formattedPlaylists[firstLabel]["default"] = true; 31389 } 31390 31391 return formattedPlaylists; 31392 }; 31393 31394 var organizeVttPlaylists = function organizeVttPlaylists(playlists, sidxMapping) { 31395 if (sidxMapping === void 0) { 31396 sidxMapping = {}; 31397 } 31398 31399 return playlists.reduce(function (a, playlist) { 31400 var label = playlist.attributes.lang || 'text'; // skip if we already have subtitles 31401 31402 if (a[label]) { 31403 return a; 31404 } 31405 31406 a[label] = { 31407 language: label, 31408 "default": false, 31409 autoselect: false, 31410 playlists: addSegmentInfoFromSidx([formatVttPlaylist(playlist)], sidxMapping), 31411 uri: '' 31412 }; 31413 return a; 31414 }, {}); 31415 }; 31416 31417 var formatVideoPlaylist = function formatVideoPlaylist(_ref3) { 31418 var _attributes3; 31419 31420 var attributes = _ref3.attributes, 31421 segments = _ref3.segments, 31422 sidx = _ref3.sidx; 31423 var playlist = { 31424 attributes: (_attributes3 = { 31425 NAME: attributes.id, 31426 AUDIO: 'audio', 31427 SUBTITLES: 'subs', 31428 RESOLUTION: { 31429 width: attributes.width, 31430 height: attributes.height 31431 }, 31432 CODECS: attributes.codecs, 31433 BANDWIDTH: attributes.bandwidth 31434 }, _attributes3['PROGRAM-ID'] = 1, _attributes3), 31435 uri: '', 31436 endList: (attributes.type || 'static') === 'static', 31437 timeline: attributes.periodIndex, 31438 resolvedUri: '', 31439 targetDuration: attributes.duration, 31440 segments: segments, 31441 mediaSequence: segments.length ? segments[0].number : 1 31442 }; 31443 31444 if (attributes.contentProtection) { 31445 playlist.contentProtection = attributes.contentProtection; 31446 } 31447 31448 if (sidx) { 31449 playlist.sidx = sidx; 31450 } 31451 31452 return playlist; 31453 }; 31454 31455 var toM3u8 = function toM3u8(dashPlaylists, sidxMapping) { 31456 var _mediaGroups; 31457 31458 if (sidxMapping === void 0) { 31459 sidxMapping = {}; 31460 } 31461 31462 if (!dashPlaylists.length) { 31463 return {}; 31464 } // grab all master attributes 31465 31466 31467 var _dashPlaylists$0$attr = dashPlaylists[0].attributes, 31468 duration = _dashPlaylists$0$attr.sourceDuration, 31469 _dashPlaylists$0$attr2 = _dashPlaylists$0$attr.minimumUpdatePeriod, 31470 minimumUpdatePeriod = _dashPlaylists$0$attr2 === void 0 ? 0 : _dashPlaylists$0$attr2; 31471 31472 var videoOnly = function videoOnly(_ref4) { 31473 var attributes = _ref4.attributes; 31474 return attributes.mimeType === 'video/mp4' || attributes.contentType === 'video'; 31475 }; 31476 31477 var audioOnly = function audioOnly(_ref5) { 31478 var attributes = _ref5.attributes; 31479 return attributes.mimeType === 'audio/mp4' || attributes.contentType === 'audio'; 31480 }; 31481 31482 var vttOnly = function vttOnly(_ref6) { 31483 var attributes = _ref6.attributes; 31484 return attributes.mimeType === 'text/vtt' || attributes.contentType === 'text'; 31485 }; 31486 31487 var videoPlaylists = mergeDiscontiguousPlaylists(dashPlaylists.filter(videoOnly)).map(formatVideoPlaylist); 31488 var audioPlaylists = mergeDiscontiguousPlaylists(dashPlaylists.filter(audioOnly)); 31489 var vttPlaylists = dashPlaylists.filter(vttOnly); 31490 var master = { 31491 allowCache: true, 31492 discontinuityStarts: [], 31493 segments: [], 31494 endList: true, 31495 mediaGroups: (_mediaGroups = { 31496 AUDIO: {}, 31497 VIDEO: {} 31498 }, _mediaGroups['CLOSED-CAPTIONS'] = {}, _mediaGroups.SUBTITLES = {}, _mediaGroups), 31499 uri: '', 31500 duration: duration, 31501 playlists: addSegmentInfoFromSidx(videoPlaylists, sidxMapping), 31502 minimumUpdatePeriod: minimumUpdatePeriod * 1000 31503 }; 31504 31505 if (audioPlaylists.length) { 31506 master.mediaGroups.AUDIO.audio = organizeAudioPlaylists(audioPlaylists, sidxMapping); 31507 } 31508 31509 if (vttPlaylists.length) { 31510 master.mediaGroups.SUBTITLES.subs = organizeVttPlaylists(vttPlaylists, sidxMapping); 31511 } 31512 31513 return master; 31514 }; 31515 /** 31516 * Calculates the R (repetition) value for a live stream (for the final segment 31517 * in a manifest where the r value is negative 1) 31518 * 31519 * @param {Object} attributes 31520 * Object containing all inherited attributes from parent elements with attribute 31521 * names as keys 31522 * @param {number} time 31523 * current time (typically the total time up until the final segment) 31524 * @param {number} duration 31525 * duration property for the given <S /> 31526 * 31527 * @return {number} 31528 * R value to reach the end of the given period 31529 */ 31530 31531 31532 var getLiveRValue = function getLiveRValue(attributes, time, duration) { 31533 var NOW = attributes.NOW, 31534 clientOffset = attributes.clientOffset, 31535 availabilityStartTime = attributes.availabilityStartTime, 31536 _attributes$timescale = attributes.timescale, 31537 timescale = _attributes$timescale === void 0 ? 1 : _attributes$timescale, 31538 _attributes$start = attributes.start, 31539 start = _attributes$start === void 0 ? 0 : _attributes$start, 31540
vendor: 11,178 bytes, lines 31540-31837
31540 _attributes$minimumUp = attributes.minimumUpdatePeriod, 31541 minimumUpdatePeriod = _attributes$minimumUp === void 0 ? 0 : _attributes$minimumUp; 31542 var now = (NOW + clientOffset) / 1000; 31543 var periodStartWC = availabilityStartTime + start; 31544 var periodEndWC = now + minimumUpdatePeriod; 31545 var periodDuration = periodEndWC - periodStartWC; 31546 return Math.ceil((periodDuration * timescale - time) / duration); 31547 }; 31548 /** 31549 * Uses information provided by SegmentTemplate.SegmentTimeline to determine segment 31550 * timing and duration 31551 * 31552 * @param {Object} attributes 31553 * Object containing all inherited attributes from parent elements with attribute 31554 * names as keys 31555 * @param {Object[]} segmentTimeline 31556 * List of objects representing the attributes of each S element contained within 31557 * 31558 * @return {{number: number, duration: number, time: number, timeline: number}[]} 31559 * List of Objects with segment timing and duration info 31560 */ 31561 31562 31563 var parseByTimeline = function parseByTimeline(attributes, segmentTimeline) { 31564 var _attributes$type = attributes.type, 31565 type = _attributes$type === void 0 ? 'static' : _attributes$type, 31566 _attributes$minimumUp2 = attributes.minimumUpdatePeriod, 31567 minimumUpdatePeriod = _attributes$minimumUp2 === void 0 ? 0 : _attributes$minimumUp2, 31568 _attributes$media = attributes.media, 31569 media = _attributes$media === void 0 ? '' : _attributes$media, 31570 sourceDuration = attributes.sourceDuration, 31571 _attributes$timescale2 = attributes.timescale, 31572 timescale = _attributes$timescale2 === void 0 ? 1 : _attributes$timescale2, 31573 _attributes$startNumb = attributes.startNumber, 31574 startNumber = _attributes$startNumb === void 0 ? 1 : _attributes$startNumb, 31575 timeline = attributes.periodIndex; 31576 var segments = []; 31577 var time = -1; 31578 31579 for (var sIndex = 0; sIndex < segmentTimeline.length; sIndex++) { 31580 var S = segmentTimeline[sIndex]; 31581 var duration = S.d; 31582 var repeat = S.r || 0; 31583 var segmentTime = S.t || 0; 31584 31585 if (time < 0) { 31586 // first segment 31587 time = segmentTime; 31588 } 31589 31590 if (segmentTime && segmentTime > time) { 31591 // discontinuity 31592 // TODO: How to handle this type of discontinuity 31593 // timeline++ here would treat it like HLS discontuity and content would 31594 // get appended without gap 31595 // E.G. 31596 // <S t="0" d="1" /> 31597 // <S d="1" /> 31598 // <S d="1" /> 31599 // <S t="5" d="1" /> 31600 // would have $Time$ values of [0, 1, 2, 5] 31601 // should this be appened at time positions [0, 1, 2, 3],(#EXT-X-DISCONTINUITY) 31602 // or [0, 1, 2, gap, gap, 5]? (#EXT-X-GAP) 31603 // does the value of sourceDuration consider this when calculating arbitrary 31604 // negative @r repeat value? 31605 // E.G. Same elements as above with this added at the end 31606 // <S d="1" r="-1" /> 31607 // with a sourceDuration of 10 31608 // Would the 2 gaps be included in the time duration calculations resulting in 31609 // 8 segments with $Time$ values of [0, 1, 2, 5, 6, 7, 8, 9] or 10 segments 31610 // with $Time$ values of [0, 1, 2, 5, 6, 7, 8, 9, 10, 11] ? 31611 time = segmentTime; 31612 } 31613 31614 var count = void 0; 31615 31616 if (repeat < 0) { 31617 var nextS = sIndex + 1; 31618 31619 if (nextS === segmentTimeline.length) { 31620 // last segment 31621 if (type === 'dynamic' && minimumUpdatePeriod > 0 && media.indexOf('$Number$') > 0) { 31622 count = getLiveRValue(attributes, time, duration); 31623 } else { 31624 // TODO: This may be incorrect depending on conclusion of TODO above 31625 count = (sourceDuration * timescale - time) / duration; 31626 } 31627 } else { 31628 count = (segmentTimeline[nextS].t - time) / duration; 31629 } 31630 } else { 31631 count = repeat + 1; 31632 } 31633 31634 var end = startNumber + segments.length + count; 31635 var number = startNumber + segments.length; 31636 31637 while (number < end) { 31638 segments.push({ 31639 number: number, 31640 duration: duration / timescale, 31641 time: time, 31642 timeline: timeline 31643 }); 31644 time += duration; 31645 number++; 31646 } 31647 } 31648 31649 return segments; 31650 }; 31651 31652 var identifierPattern = /\$([A-z]*)(?:(%0)([0-9]+)d)?\$/g; 31653 /** 31654 * Replaces template identifiers with corresponding values. To be used as the callback 31655 * for String.prototype.replace 31656 * 31657 * @name replaceCallback 31658 * @function 31659 * @param {string} match 31660 * Entire match of identifier 31661 * @param {string} identifier 31662 * Name of matched identifier 31663 * @param {string} format 31664 * Format tag string. Its presence indicates that padding is expected 31665 * @param {string} width 31666 * Desired length of the replaced value. Values less than this width shall be left 31667 * zero padded 31668 * @return {string} 31669 * Replacement for the matched identifier 31670 */ 31671 31672 /** 31673 * Returns a function to be used as a callback for String.prototype.replace to replace 31674 * template identifiers 31675 * 31676 * @param {Obect} values 31677 * Object containing values that shall be used to replace known identifiers 31678 * @param {number} values.RepresentationID 31679 * Value of the Representation@id attribute 31680 * @param {number} values.Number 31681 * Number of the corresponding segment 31682 * @param {number} values.Bandwidth 31683 * Value of the Representation@bandwidth attribute. 31684 * @param {number} values.Time 31685 * Timestamp value of the corresponding segment 31686 * @return {replaceCallback} 31687 * Callback to be used with String.prototype.replace to replace identifiers 31688 */ 31689 31690 var identifierReplacement = function identifierReplacement(values) { 31691 return function (match, identifier, format, width) { 31692 if (match === '$$') { 31693 // escape sequence 31694 return '$'; 31695 } 31696 31697 if (typeof values[identifier] === 'undefined') { 31698 return match; 31699 } 31700 31701 var value = '' + values[identifier]; 31702 31703 if (identifier === 'RepresentationID') { 31704 // Format tag shall not be present with RepresentationID 31705 return value; 31706 } 31707 31708 if (!format) { 31709 width = 1; 31710 } else { 31711 width = parseInt(width, 10); 31712 } 31713 31714 if (value.length >= width) { 31715 return value; 31716 } 31717 31718 return "" + new Array(width - value.length + 1).join('0') + value; 31719 }; 31720 }; 31721 /** 31722 * Constructs a segment url from a template string 31723 * 31724 * @param {string} url 31725 * Template string to construct url from 31726 * @param {Obect} values 31727 * Object containing values that shall be used to replace known identifiers 31728 * @param {number} values.RepresentationID 31729 * Value of the Representation@id attribute 31730 * @param {number} values.Number 31731 * Number of the corresponding segment 31732 * @param {number} values.Bandwidth 31733 * Value of the Representation@bandwidth attribute. 31734 * @param {number} values.Time 31735 * Timestamp value of the corresponding segment 31736 * @return {string} 31737 * Segment url with identifiers replaced 31738 */ 31739 31740 31741 var constructTemplateUrl = function constructTemplateUrl(url, values) { 31742 return url.replace(identifierPattern, identifierReplacement(values)); 31743 }; 31744 /** 31745 * Generates a list of objects containing timing and duration information about each 31746 * segment needed to generate segment uris and the complete segment object 31747 * 31748 * @param {Object} attributes 31749 * Object containing all inherited attributes from parent elements with attribute 31750 * names as keys 31751 * @param {Object[]|undefined} segmentTimeline 31752 * List of objects representing the attributes of each S element contained within 31753 * the SegmentTimeline element 31754 * @return {{number: number, duration: number, time: number, timeline: number}[]} 31755 * List of Objects with segment timing and duration info 31756 */ 31757 31758 31759 var parseTemplateInfo = function parseTemplateInfo(attributes, segmentTimeline) { 31760 if (!attributes.duration && !segmentTimeline) { 31761 // if neither @duration or SegmentTimeline are present, then there shall be exactly 31762 // one media segment 31763 return [{ 31764 number: attributes.startNumber || 1, 31765 duration: attributes.sourceDuration, 31766 time: 0, 31767 timeline: attributes.periodIndex 31768 }]; 31769 } 31770 31771 if (attributes.duration) { 31772 return parseByDuration(attributes); 31773 } 31774 31775 return parseByTimeline(attributes, segmentTimeline); 31776 }; 31777 /** 31778 * Generates a list of segments using information provided by the SegmentTemplate element 31779 * 31780 * @param {Object} attributes 31781 * Object containing all inherited attributes from parent elements with attribute 31782 * names as keys 31783 * @param {Object[]|undefined} segmentTimeline 31784 * List of objects representing the attributes of each S element contained within 31785 * the SegmentTimeline element 31786 * @return {Object[]} 31787 * List of segment objects 31788 */ 31789 31790 31791 var segmentsFromTemplate = function segmentsFromTemplate(attributes, segmentTimeline) { 31792 var templateValues = { 31793 RepresentationID: attributes.id, 31794 Bandwidth: attributes.bandwidth || 0 31795 }; 31796 var _attributes$initializ = attributes.initialization, 31797 initialization = _attributes$initializ === void 0 ? { 31798 sourceURL: '', 31799 range: '' 31800 } : _attributes$initializ; 31801 var mapSegment = urlTypeToSegment({ 31802 baseUrl: attributes.baseUrl, 31803 source: constructTemplateUrl(initialization.sourceURL, templateValues), 31804 range: initialization.range 31805 }); 31806 var segments = parseTemplateInfo(attributes, segmentTimeline); 31807 return segments.map(function (segment) { 31808 templateValues.Number = segment.number; 31809 templateValues.Time = segment.time; 31810 var uri = constructTemplateUrl(attributes.media || '', templateValues); 31811 return { 31812 uri: uri, 31813 timeline: segment.timeline, 31814 duration: segment.duration, 31815 resolvedUri: resolveUrl(attributes.baseUrl || '', uri), 31816 map: mapSegment, 31817 number: segment.number 31818 }; 31819 }); 31820 }; 31821 /** 31822 * Converts a <SegmentUrl> (of type URLType from the DASH spec 5.3.9.2 Table 14) 31823 * to an object that matches the output of a segment in videojs/mpd-parser 31824 * 31825 * @param {Object} attributes 31826 * Object containing all inherited attributes from parent elements with attribute 31827 * names as keys 31828 * @param {Object} segmentUrl 31829 * <SegmentURL> node to translate into a segment object 31830 * @return {Object} translated segment object 31831 */ 31832 31833 31834 var SegmentURLToSegmentObject = function SegmentURLToSegmentObject(attributes, segmentUrl) { 31835 var baseUrl = attributes.baseUrl, 31836 _attributes$initializ = attributes.initialization, 31837 initialization = _attributes$initializ === void 0 ? {}
31837 : _attributes$initializ; 31838 var initSegment = urlTypeToSegment({ 31839 baseUrl: baseUrl, 31840 source: initialization.sourceURL, 31841 range: initialization.range 31842 }); 31843 var segment = urlTypeToSegment({ 31844 baseUrl: baseUrl, 31845 source: segmentUrl.media, 31846 range: segmentUrl.mediaRange 31847 }); 31848 segment.map = initSegment; 31849 return segment; 31850 }; 31851 /** 31852 * Generates a list of segments using information provided by the SegmentList element 31853 * SegmentList (DASH SPEC Section 5.3.9.3.2) contains a set of <SegmentURL> nodes. Each 31854 * node should be translated into segment. 31855 * 31856 * @param {Object} attributes 31857 * Object containing all inherited attributes from parent elements with attribute 31858 * names as keys 31859 * @param {Object[]|undefined} segmentTimeline 31860 * List of objects representing the attributes of each S element contained within 31861 * the SegmentTimeline element 31862 * @return {Object.<Array>} list of segments 31863 */ 31864 31865 31866 var segmentsFromList = function segmentsFromList(
vendor: 4,561 bytes, lines 31866-31992
31866attributes, segmentTimeline) { 31867 var duration = attributes.duration, 31868 _attributes$segmentUr = attributes.segmentUrls, 31869 segmentUrls = _attributes$segmentUr === void 0 ? [] : _attributes$segmentUr; // Per spec (5.3.9.2.1) no way to determine segment duration OR 31870 // if both SegmentTimeline and @duration are defined, it is outside of spec. 31871 31872 if (!duration && !segmentTimeline || duration && segmentTimeline) { 31873 throw new Error(errors.SEGMENT_TIME_UNSPECIFIED); 31874 } 31875 31876 var segmentUrlMap = segmentUrls.map(function (segmentUrlObject) { 31877 return SegmentURLToSegmentObject(attributes, segmentUrlObject); 31878 }); 31879 var segmentTimeInfo; 31880 31881 if (duration) { 31882 segmentTimeInfo = parseByDuration(attributes); 31883 } 31884 31885 if (segmentTimeline) { 31886 segmentTimeInfo = parseByTimeline(attributes, segmentTimeline); 31887 } 31888 31889 var segments = segmentTimeInfo.map(function (segmentTime, index) { 31890 if (segmentUrlMap[index]) { 31891 var segment = segmentUrlMap[index]; 31892 segment.timeline = segmentTime.timeline; 31893 segment.duration = segmentTime.duration; 31894 segment.number = segmentTime.number; 31895 return segment; 31896 } // Since we're mapping we should get rid of any blank segments (in case 31897 // the given SegmentTimeline is handling for more elements than we have 31898 // SegmentURLs for). 31899 31900 }).filter(function (segment) { 31901 return segment; 31902 }); 31903 return segments; 31904 }; 31905 31906 var generateSegments = function generateSegments(_ref) { 31907 var attributes = _ref.attributes, 31908 segmentInfo = _ref.segmentInfo; 31909 var segmentAttributes; 31910 var segmentsFn; 31911 31912 if (segmentInfo.template) { 31913 segmentsFn = segmentsFromTemplate; 31914 segmentAttributes = merge(attributes, segmentInfo.template); 31915 } else if (segmentInfo.base) { 31916 segmentsFn = segmentsFromBase; 31917 segmentAttributes = merge(attributes, segmentInfo.base); 31918 } else if (segmentInfo.list) { 31919 segmentsFn = segmentsFromList; 31920 segmentAttributes = merge(attributes, segmentInfo.list); 31921 } 31922 31923 var segmentsInfo = { 31924 attributes: attributes 31925 }; 31926 31927 if (!segmentsFn) { 31928 return segmentsInfo; 31929 } 31930 31931 var segments = segmentsFn(segmentAttributes, segmentInfo.timeline); // The @duration attribute will be used to determin the playlist's targetDuration which 31932 // must be in seconds. Since we've generated the segment list, we no longer need 31933 // @duration to be in @timescale units, so we can convert it here. 31934 31935 if (segmentAttributes.duration) { 31936 var _segmentAttributes = segmentAttributes, 31937 duration = _segmentAttributes.duration, 31938 _segmentAttributes$ti = _segmentAttributes.timescale, 31939 timescale = _segmentAttributes$ti === void 0 ? 1 : _segmentAttributes$ti; 31940 segmentAttributes.duration = duration / timescale; 31941 } else if (segments.length) { 31942 // if there is no @duration attribute, use the largest segment duration as 31943 // as target duration 31944 segmentAttributes.duration = segments.reduce(function (max, segment) { 31945 return Math.max(max, Math.ceil(segment.duration)); 31946 }, 0); 31947 } else { 31948 segmentAttributes.duration = 0; 31949 } 31950 31951 segmentsInfo.attributes = segmentAttributes; 31952 segmentsInfo.segments = segments; // This is a sidx box without actual segment information 31953 31954 if (segmentInfo.base && segmentAttributes.indexRange) { 31955 segmentsInfo.sidx = segments[0]; 31956 segmentsInfo.segments = []; 31957 } 31958 31959 return segmentsInfo; 31960 }; 31961 31962 var toPlaylists = function toPlaylists(representations) { 31963 return representations.map(generateSegments); 31964 }; 31965 31966 var findChildren = function findChildren(element, name) { 31967 return from(element.childNodes).filter(function (_ref) { 31968 var tagName = _ref.tagName; 31969 return tagName === name; 31970 }); 31971 }; 31972 31973 var getContent = function getContent(element) { 31974 return element.textContent.trim(); 31975 }; 31976 31977 var parseDuration = function parseDuration(str) { 31978 var SECONDS_IN_YEAR = 365 * 24 * 60 * 60; 31979 var SECONDS_IN_MONTH = 30 * 24 * 60 * 60; 31980 var SECONDS_IN_DAY = 24 * 60 * 60; 31981 var SECONDS_IN_HOUR = 60 * 60; 31982 var SECONDS_IN_MIN = 60; // P10Y10M10DT10H10M10.1S 31983 31984 var durationRegex = /P(?:(\d*)Y)?(?:(\d*)M)?(?:(\d*)D)?(?:T(?:(\d*)H)?(?:(\d*)M)?(?:([\d.]*)S)?)?/; 31985 var match = durationRegex.exec(str); 31986 31987 if (!match) { 31988 return 0; 31989 } 31990 31991 var _match$slice = match.slice(1), 31992 year = _match$slice[0],
vendor: 12,672 bytes, lines 31993-32362
31993 month = _match$slice[1], 31994 day = _match$slice[2], 31995 hour = _match$slice[3], 31996 minute = _match$slice[4], 31997 second = _match$slice[5]; 31998 31999 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); 32000 }; 32001 32002 var parseDate = function parseDate(str) { 32003 // Date format without timezone according to ISO 8601 32004 // YYY-MM-DDThh:mm:ss.ssssss 32005 var dateRegex = /^\d+-\d+-\d+T\d+:\d+:\d+(\.\d+)?$/; // If the date string does not specifiy a timezone, we must specifiy UTC. This is 32006 // expressed by ending with 'Z' 32007 32008 if (dateRegex.test(str)) { 32009 str += 'Z'; 32010 } 32011 32012 return Date.parse(str); 32013 }; 32014 32015 var parsers = { 32016 /** 32017 * Specifies the duration of the entire Media Presentation. Format is a duration string 32018 * as specified in ISO 8601 32019 * 32020 * @param {string} value 32021 * value of attribute as a string 32022 * @return {number} 32023 * The duration in seconds 32024 */ 32025 mediaPresentationDuration: function mediaPresentationDuration(value) { 32026 return parseDuration(value); 32027 }, 32028 32029 /** 32030 * Specifies the Segment availability start time for all Segments referred to in this 32031 * MPD. For a dynamic manifest, it specifies the anchor for the earliest availability 32032 * time. Format is a date string as specified in ISO 8601 32033 * 32034 * @param {string} value 32035 * value of attribute as a string 32036 * @return {number} 32037 * The date as seconds from unix epoch 32038 */ 32039 availabilityStartTime: function availabilityStartTime(value) { 32040 return parseDate(value) / 1000; 32041 }, 32042 32043 /** 32044 * Specifies the smallest period between potential changes to the MPD. Format is a 32045 * duration string as specified in ISO 8601 32046 * 32047 * @param {string} value 32048 * value of attribute as a string 32049 * @return {number} 32050 * The duration in seconds 32051 */ 32052 minimumUpdatePeriod: function minimumUpdatePeriod(value) { 32053 return parseDuration(value); 32054 }, 32055 32056 /** 32057 * Specifies the duration of the smallest time shifting buffer for any Representation 32058 * in the MPD. Format is a duration string as specified in ISO 8601 32059 * 32060 * @param {string} value 32061 * value of attribute as a string 32062 * @return {number} 32063 * The duration in seconds 32064 */ 32065 timeShiftBufferDepth: function timeShiftBufferDepth(value) { 32066 return parseDuration(value); 32067 }, 32068 32069 /** 32070 * Specifies the PeriodStart time of the Period relative to the availabilityStarttime. 32071 * Format is a duration string as specified in ISO 8601 32072 * 32073 * @param {string} value 32074 * value of attribute as a string 32075 * @return {number} 32076 * The duration in seconds 32077 */ 32078 start: function start(value) { 32079 return parseDuration(value); 32080 }, 32081 32082 /** 32083 * Specifies the width of the visual presentation 32084 * 32085 * @param {string} value 32086 * value of attribute as a string 32087 * @return {number} 32088 * The parsed width 32089 */ 32090 width: function width(value) { 32091 return parseInt(value, 10); 32092 }, 32093 32094 /** 32095 * Specifies the height of the visual presentation 32096 * 32097 * @param {string} value 32098 * value of attribute as a string 32099 * @return {number} 32100 * The parsed height 32101 */ 32102 height: function height(value) { 32103 return parseInt(value, 10); 32104 }, 32105 32106 /** 32107 * Specifies the bitrate of the representation 32108 * 32109 * @param {string} value 32110 * value of attribute as a string 32111 * @return {number} 32112 * The parsed bandwidth 32113 */ 32114 bandwidth: function bandwidth(value) { 32115 return parseInt(value, 10); 32116 }, 32117 32118 /** 32119 * Specifies the number of the first Media Segment in this Representation in the Period 32120 * 32121 * @param {string} value 32122 * value of attribute as a string 32123 * @return {number} 32124 * The parsed number 32125 */ 32126 startNumber: function startNumber(value) { 32127 return parseInt(value, 10); 32128 }, 32129 32130 /** 32131 * Specifies the timescale in units per seconds 32132 * 32133 * @param {string} value 32134 * value of attribute as a string 32135 * @return {number} 32136 * The aprsed timescale 32137 */ 32138 timescale: function timescale(value) { 32139 return parseInt(value, 10); 32140 }, 32141 32142 /** 32143 * Specifies the constant approximate Segment duration 32144 * NOTE: The <Period> element also contains an @duration attribute. This duration 32145 * specifies the duration of the Period. This attribute is currently not 32146 * supported by the rest of the parser, however we still check for it to prevent 32147 * errors. 32148 * 32149 * @param {string} value 32150 * value of attribute as a string 32151 * @return {number} 32152 * The parsed duration 32153 */ 32154 duration: function duration(value) { 32155 var parsedValue = parseInt(value, 10); 32156 32157 if (isNaN(parsedValue)) { 32158 return parseDuration(value); 32159 } 32160 32161 return parsedValue; 32162 }, 32163 32164 /** 32165 * Specifies the Segment duration, in units of the value of the @timescale. 32166 * 32167 * @param {string} value 32168 * value of attribute as a string 32169 * @return {number} 32170 * The parsed duration 32171 */ 32172 d: function d(value) { 32173 return parseInt(value, 10); 32174 }, 32175 32176 /** 32177 * Specifies the MPD start time, in @timescale units, the first Segment in the series 32178 * starts relative to the beginning of the Period 32179 * 32180 * @param {string} value 32181 * value of attribute as a string 32182 * @return {number} 32183 * The parsed time 32184 */ 32185 t: function t(value) { 32186 return parseInt(value, 10); 32187 }, 32188 32189 /** 32190 * Specifies the repeat count of the number of following contiguous Segments with the 32191 * same duration expressed by the value of @d 32192 * 32193 * @param {string} value 32194 * value of attribute as a string 32195 * @return {number} 32196 * The parsed number 32197 */ 32198 r: function r(value) { 32199 return parseInt(value, 10); 32200 }, 32201 32202 /** 32203 * Default parser for all other attributes. Acts as a no-op and just returns the value 32204 * as a string 32205 * 32206 * @param {string} value 32207 * value of attribute as a string 32208 * @return {string} 32209 * Unparsed value 32210 */ 32211 DEFAULT: function DEFAULT(value) { 32212 return value; 32213 } 32214 }; 32215 /** 32216 * Gets all the attributes and values of the provided node, parses attributes with known 32217 * types, and returns an object with attribute names mapped to values. 32218 * 32219 * @param {Node} el 32220 * The node to parse attributes from 32221 * @return {Object} 32222 * Object with all attributes of el parsed 32223 */ 32224 32225 var parseAttributes$1 = function parseAttributes(el) { 32226 if (!(el && el.attributes)) { 32227 return {}; 32228 } 32229 32230 return from(el.attributes).reduce(function (a, e) { 32231 var parseFn = parsers[e.name] || parsers.DEFAULT; 32232 a[e.name] = parseFn(e.value); 32233 return a; 32234 }, {}); 32235 }; 32236 32237 function decodeB64ToUint8Array$1(b64Text) { 32238 var decodedString = window$1.atob(b64Text); 32239 var array = new Uint8Array(decodedString.length); 32240 32241 for (var i = 0; i < decodedString.length; i++) { 32242 array[i] = decodedString.charCodeAt(i); 32243 } 32244 32245 return array; 32246 } 32247 32248 var keySystemsMap = { 32249 'urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b': 'org.w3.clearkey', 32250 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed': 'com.widevine.alpha', 32251 'urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95': 'com.microsoft.playready', 32252 'urn:uuid:f239e769-efa3-4850-9c16-a903c6932efb': 'com.adobe.primetime' 32253 }; 32254 /** 32255 * Builds a list of urls that is the product of the reference urls and BaseURL values 32256 * 32257 * @param {string[]} referenceUrls 32258 * List of reference urls to resolve to 32259 * @param {Node[]} baseUrlElements 32260 * List of BaseURL nodes from the mpd 32261 * @return {string[]} 32262 * List of resolved urls 32263 */ 32264 32265 var buildBaseUrls = function buildBaseUrls(referenceUrls, baseUrlElements) { 32266 if (!baseUrlElements.length) { 32267 return referenceUrls; 32268 } 32269 32270 return flatten(referenceUrls.map(function (reference) { 32271 return baseUrlElements.map(function (baseUrlElement) { 32272 return resolveUrl(reference, getContent(baseUrlElement)); 32273 }); 32274 })); 32275 }; 32276 /** 32277 * Contains all Segment information for its containing AdaptationSet 32278 * 32279 * @typedef {Object} SegmentInformation 32280 * @property {Object|undefined} template 32281 * Contains the attributes for the SegmentTemplate node 32282 * @property {Object[]|undefined} timeline 32283 * Contains a list of atrributes for each S node within the SegmentTimeline node 32284 * @property {Object|undefined} list 32285 * Contains the attributes for the SegmentList node 32286 * @property {Object|undefined} base 32287 * Contains the attributes for the SegmentBase node 32288 */ 32289 32290 /** 32291 * Returns all available Segment information contained within the AdaptationSet node 32292 * 32293 * @param {Node} adaptationSet 32294 * The AdaptationSet node to get Segment information from 32295 * @return {SegmentInformation} 32296 * The Segment information contained within the provided AdaptationSet 32297 */ 32298 32299 32300 var getSegmentInformation = function getSegmentInformation(adaptationSet) { 32301 var segmentTemplate = findChildren(adaptationSet, 'SegmentTemplate')[0]; 32302 var segmentList = findChildren(adaptationSet, 'SegmentList')[0]; 32303 var segmentUrls = segmentList && findChildren(segmentList, 'SegmentURL').map(function (s) { 32304 return merge({ 32305 tag: 'SegmentURL' 32306 }, parseAttributes$1(s)); 32307 }); 32308 var segmentBase = findChildren(adaptationSet, 'SegmentBase')[0]; 32309 var segmentTimelineParentNode = segmentList || segmentTemplate; 32310 var segmentTimeline = segmentTimelineParentNode && findChildren(segmentTimelineParentNode, 'SegmentTimeline')[0]; 32311 var segmentInitializationParentNode = segmentList || segmentBase || segmentTemplate; 32312 var segmentInitialization = segmentInitializationParentNode && findChildren(segmentInitializationParentNode, 'Initialization')[0]; // SegmentTemplate is handled slightly differently, since it can have both 32313 // @initialization and an <Initialization> node. @initialization can be templated, 32314 // while the node can have a url and range specified. If the <SegmentTemplate> has 32315 // both @initialization and an <Initialization> subelement we opt to override with 32316 // the node, as this interaction is not defined in the spec. 32317 32318 var template = segmentTemplate && parseAttributes$1(segmentTemplate); 32319 32320 if (template && segmentInitialization) { 32321 template.initialization = segmentInitialization && parseAttributes$1(segmentInitialization); 32322 } else if (template && template.initialization) { 32323 // If it is @initialization we convert it to an object since this is the format that 32324 // later functions will rely on for the initialization segment. This is only valid 32325 // for <SegmentTemplate> 32326 template.initialization = { 32327 sourceURL: template.initialization 32328 }; 32329 } 32330 32331 var segmentInfo = { 32332 template: template, 32333 timeline: segmentTimeline && findChildren(segmentTimeline, 'S').map(function (s) { 32334 return parseAttributes$1(s); 32335 }), 32336 list: segmentList && merge(parseAttributes$1(segmentList), { 32337 segmentUrls: segmentUrls, 32338 initialization: parseAttributes$1(segmentInitialization) 32339 }), 32340 base: segmentBase && merge(parseAttributes$1(segmentBase), { 32341 initialization: parseAttributes$1(segmentInitialization) 32342 }) 32343 }; 32344 Object.keys(segmentInfo).forEach(function (key) { 32345 if (!segmentInfo[key]) { 32346 delete segmentInfo[key]; 32347 } 32348 }); 32349 return segmentInfo; 32350 }; 32351 /** 32352 * Contains Segment information and attributes needed to construct a Playlist object 32353 * from a Representation 32354 * 32355 * @typedef {Object} RepresentationInformation 32356 * @property {SegmentInformation} segmentInfo 32357 * Segment information for this Representation 32358 * @property {Object} attributes 32359 * Inherited attributes for this Representation 32360 */ 32361 32362 /**
32363 * Maps a Representation node to an object containing Segment information and attributes 32364 * 32365 * @name inheritBaseUrlsCallback 32366 * @function 32367 * @param {Node} representation 32368 * Representation node from the mpd 32369 * @return {RepresentationInformation} 32370 * Representation information needed to construct a Playlist object 32371 */ 32372 32373 /** 32374 * Returns a callback for Array.prototype.map for mapping Representation nodes to 32375 * Segment information and attributes using inherited BaseURL nodes. 32376 * 32377 * @param {Object} adaptationSetAttributes 32378 * Contains attributes inherited by the AdaptationSet 32379 * @param {string[]} adaptationSetBaseUrls 32380 * Contains list of resolved base urls inherited by the AdaptationSet 32381 * @param {SegmentInformation} adaptationSetSegmentInfo 32382 * Contains Segment information for the AdaptationSet 32383 * @return {inheritBaseUrlsCallback} 32384 * Callback map function 32385 */ 32386 32387 32388 var inheritBaseUrls = function inheritBaseUrls(adaptationSetAttributes, adaptationSetBaseUrls, adaptationSetSegmentInfo) { 32389 return function (representation) { 32390 var repBaseUrlElements = findChildren(representation, 'BaseURL'); 32391 var repBaseUrls = buildBaseUrls(adaptationSetBaseUrls, repBaseUrlElements); 32392 var attributes = merge(adaptationSetAttributes, parseAttributes$1(representation)); 32393 var representationSegmentInfo = getSegmentInformation(representation); 32394 return repBaseUrls.map(function (baseUrl) { 32395 return { 32396 segmentInfo: merge(adaptationSetSegmentInfo, representationSegmentInfo), 32397 attributes: merge(attributes, { 32398 baseUrl: baseUrl 32399 }) 32400 }; 32401 }); 32402 }; 32403 }; 32404 /** 32405 * Tranforms a series of content protection nodes to
32406 * an object containing pssh data by key system 32407 * 32408 * @param {Node[]} contentProtectionNodes 32409 * Content protection nodes 32410 * @return {Object} 32411 * Object containing pssh data by key system 32412 */ 32413 32414 32415 var generateKeySystemInformation = function generateKeySystemInformation(contentProtectionNodes) { 32416 return contentProtectionNodes.reduce(function (acc, node) { 32417 var attributes = parseAttributes$1(node); 32418 var keySystem = keySystemsMap[attributes.schemeIdUri]; 32419 32420 if (keySystem) { 32421 acc[keySystem] = { 32422 attributes: attributes 32423 }; 32424 var psshNode = findChildren(node, 'cenc:pssh')[0]; 32425 32426 if (psshNode) { 32427 var pssh = getContent(psshNode); 32428 var psshBuffer = pssh && decodeB64ToUint8Array$1(pssh); 32429 acc[keySystem].pssh = psshBuffer; 32430 } 32431 } 32432 32433 return acc; 32434 }, {}); 32435 }; 32436 /** 32437 * Maps an AdaptationSet node to a list of Representation information objects 32438 * 32439 * @name toRepresentationsCallback 32440 * @function 32441 * @param {Node} adaptationSet 32442 * AdaptationSet node from the mpd 32443 * @return {RepresentationInformation[]} 32444 * List of objects containing Representaion information 32445 */ 32446 32447 /** 32448 * Returns a callback for Array.prototype.map for mapping AdaptationSet nodes to a list of 32449 * Representation information objects 32450 * 32451 * @param {Object} periodAttributes 32452 * Contains attributes inherited by the Period 32453 * @param {string[]} periodBaseUrls 32454 * Contains list of resolved base urls inherited by the Period 32455 * @param {string[]} periodSegmentInfo 32456 * Contains Segment Information at the period level 32457 * @return {toRepresentationsCallback} 32458 * Callback map function 32459 */ 32460 32461 32462 var toRepresentations = function toRepresentations(periodAttributes, periodBaseUrls, periodSegmentInfo) { 32463 return function (adaptationSet) { 32464 var adaptationSetAttributes = parseAttributes$1(adaptationSet); 32465 var adaptationSetBaseUrls = buildBaseUrls(periodBaseUrls, findChildren(adaptationSet, 'BaseURL')); 32466 var role = findChildren(adaptationSet, 'Role')[0]; 32467 var roleAttributes = { 32468 role: parseAttributes$1(role) 32469 }; 32470 var attrs = merge(periodAttributes, adaptationSetAttributes, roleAttributes); 32471 var contentProtection = generateKeySystemInformation(findChildren(adaptationSet, 'ContentProtection')); 32472 32473 if (Object.keys(contentProtection).length) { 32474 attrs = merge(attrs, { 32475 contentProtection: contentProtection 32476 }); 32477 } 32478 32479 var segmentInfo = getSegmentInformation(adaptationSet); 32480 var representations = findChildren(adaptationSet, 'Representation'); 32481 var adaptationSetSegmentInfo = merge(periodSegmentInfo, segmentInfo); 32482 return flatten(representations.map(inheritBaseUrls(attrs, adaptationSetBaseUrls, adaptationSetSegmentInfo))); 32483 }; 32484 }; 32485 /** 32486 * Maps an Period node to a list of Representation inforamtion objects for all 32487 * AdaptationSet nodes contained within the Period 32488 * 32489 * @name toAdaptationSetsCallback 32490 * @function 32491 * @param {Node} period 32492 * Period node from the mpd 32493 * @param {number} periodIndex 32494 * Index of the Period within the mpd 32495 * @return {RepresentationInformation[]} 32496 * List of objects containing Representaion information 32497 */ 32498 32499 /** 32500 * Returns a callback for Array.prototype.map for mapping Period nodes to a list of 32501 * Representation information objects 32502 * 32503 * @param {Object} mpdAttributes 32504 * Contains attributes inherited by the mpd 32505 * @param {string[]} mpdBaseUrls 32506 * Contains list of resolved base urls inherited by the mpd 32507 * @return {toAdaptationSetsCallback} 32508 * Callback map function 32509 */ 32510 32511 32512 var toAdaptationSets = function toAdaptationSets(mpdAttributes, mpdBaseUrls) { 32513 return function (period, index) { 32514 var periodBaseUrls = buildBaseUrls(mpdBaseUrls, findChildren(period, 'BaseURL')); 32515 var periodAtt = parseAttributes$1(period); 32516 var parsedPeriodId = parseInt(periodAtt.id, 10); // fallback to mapping index if Period@id is not a number 32517 32518 var periodIndex = window$1.isNaN(parsedPeriodId) ? index : parsedPeriodId; 32519 var periodAttributes = merge(mpdAttributes, { 32520 periodIndex: periodIndex 32521 }); 32522 var adaptationSets = findChildren(period, 'AdaptationSet'); 32523 var periodSegmentInfo = getSegmentInformation(period); 32524 return flatten(adaptationSets.map(toRepresentations(periodAttributes, periodBaseUrls, periodSegmentInfo))); 32525 }; 32526 }; 32527 /** 32528 * Traverses the mpd xml tree to generate a list of Representation information objects 32529 * that have inherited attributes from parent nodes 32530 * 32531 * @param {Node} mpd 32532 * The root node of the mpd 32533 * @param {Object} options 32534 * Available options for inheritAttributes 32535 * @param {string} options.manifestUri 32536 * The uri source of the mpd 32537 * @param {number} options.NOW 32538 * Current time per DASH IOP. Default is current time in ms since epoch 32539 * @param {number} options.clientOffset 32540 * Client time difference from NOW (in milliseconds) 32541 * @return {RepresentationInformation[]} 32542 * List of objects containing Representation information 32543 */ 32544 32545 32546 var inheritAttributes = function inheritAttributes(mpd, options) { 32547 if (options === void 0) { 32548 options = {}; 32549 } 32550 32551 var _options = options, 32552 _options$manifestUri = _options.manifestUri, 32553 manifestUri = _options$manifestUri === void 0 ? '' : _options$manifestUri, 32554 _options$NOW = _options.NOW, 32555 NOW = _options$NOW === void 0 ? Date.now() : _options$NOW, 32556 _options$clientOffset = _options.clientOffset, 32557 clientOffset = _options$clientOffset === void 0 ? 0 : _options$clientOffset;
vendor: 7,205 bytes, lines 32558-32796
32558 var periods = findChildren(mpd, 'Period'); 32559 32560 if (!periods.length) { 32561 throw new Error(errors.INVALID_NUMBER_OF_PERIOD); 32562 } 32563 32564 var mpdAttributes = parseAttributes$1(mpd); 32565 var mpdBaseUrls = buildBaseUrls([manifestUri], findChildren(mpd, 'BaseURL')); 32566 mpdAttributes.sourceDuration = mpdAttributes.mediaPresentationDuration || 0; 32567 mpdAttributes.NOW = NOW; 32568 mpdAttributes.clientOffset = clientOffset; 32569 return flatten(periods.map(toAdaptationSets(mpdAttributes, mpdBaseUrls))); 32570 }; 32571 32572 var stringToMpdXml = function stringToMpdXml(manifestString) { 32573 if (manifestString === '') { 32574 throw new Error(errors.DASH_EMPTY_MANIFEST); 32575 } 32576 32577 var parser = new window$1.DOMParser(); 32578 var xml = parser.parseFromString(manifestString, 'application/xml'); 32579 var mpd = xml && xml.documentElement.tagName === 'MPD' ? xml.documentElement : null; 32580 32581 if (!mpd || mpd && mpd.getElementsByTagName('parsererror').length > 0) { 32582 throw new Error(errors.DASH_INVALID_XML); 32583 } 32584 32585 return mpd; 32586 }; 32587 /** 32588 * Parses the manifest for a UTCTiming node, returning the nodes attributes if found 32589 * 32590 * @param {string} mpd 32591 * XML string of the MPD manifest 32592 * @return {Object|null} 32593 * Attributes of UTCTiming node specified in the manifest. Null if none found 32594 */ 32595 32596 32597 var parseUTCTimingScheme = function parseUTCTimingScheme(mpd) { 32598 var UTCTimingNode = findChildren(mpd, 'UTCTiming')[0]; 32599 32600 if (!UTCTimingNode) { 32601 return null; 32602 } 32603 32604 var attributes = parseAttributes$1(UTCTimingNode); 32605 32606 switch (attributes.schemeIdUri) { 32607 case 'urn:mpeg:dash:utc:http-head:2014': 32608 case 'urn:mpeg:dash:utc:http-head:2012': 32609 attributes.method = 'HEAD'; 32610 break; 32611 32612 case 'urn:mpeg:dash:utc:http-xsdate:2014': 32613 case 'urn:mpeg:dash:utc:http-iso:2014': 32614 case 'urn:mpeg:dash:utc:http-xsdate:2012': 32615 case 'urn:mpeg:dash:utc:http-iso:2012': 32616 attributes.method = 'GET'; 32617 break; 32618 32619 case 'urn:mpeg:dash:utc:direct:2014': 32620 case 'urn:mpeg:dash:utc:direct:2012': 32621 attributes.method = 'DIRECT'; 32622 attributes.value = Date.parse(attributes.value); 32623 break; 32624 32625 case 'urn:mpeg:dash:utc:http-ntp:2014': 32626 case 'urn:mpeg:dash:utc:ntp:2014': 32627 case 'urn:mpeg:dash:utc:sntp:2014': 32628 default: 32629 throw new Error(errors.UNSUPPORTED_UTC_TIMING_SCHEME); 32630 } 32631 32632 return attributes; 32633 }; 32634 32635 var parse = function parse(manifestString, options) { 32636 if (options === void 0) { 32637 options = {}; 32638 } 32639 32640 return toM3u8(toPlaylists(inheritAttributes(stringToMpdXml(manifestString), options)), options.sidxMapping); 32641 }; 32642 /** 32643 * Parses the manifest for a UTCTiming node, returning the nodes attributes if found 32644 * 32645 * @param {string} manifestString 32646 * XML string of the MPD manifest 32647 * @return {Object|null} 32648 * Attributes of UTCTiming node specified in the manifest. Null if none found 32649 */ 32650 32651 32652 var parseUTCTiming = function parseUTCTiming(manifestString) { 32653 return parseUTCTimingScheme(stringToMpdXml(manifestString)); 32654 }; 32655 32656 /** 32657 * mux.js 32658 * 32659 * Copyright (c) Brightcove 32660 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 32661 */ 32662 var toUnsigned = function toUnsigned(value) { 32663 return value >>> 0; 32664 }; 32665 32666 var toHexString = function toHexString(value) { 32667 return ('00' + value.toString(16)).slice(-2); 32668 }; 32669 32670 var bin = { 32671 toUnsigned: toUnsigned, 32672 toHexString: toHexString 32673 }; 32674 32675 var inspectMp4, 32676 _textifyMp, 32677 toUnsigned$1 = bin.toUnsigned, 32678 parseMp4Date = function parseMp4Date(seconds) { 32679 return new Date(seconds * 1000 - 2082844800000); 32680 }, 32681 parseSampleFlags = function parseSampleFlags(flags) { 32682 return { 32683 isLeading: (flags[0] & 0x0c) >>> 2, 32684 dependsOn: flags[0] & 0x03, 32685 isDependedOn: (flags[1] & 0xc0) >>> 6, 32686 hasRedundancy: (flags[1] & 0x30) >>> 4, 32687 paddingValue: (flags[1] & 0x0e) >>> 1, 32688 isNonSyncSample: flags[1] & 0x01, 32689 degradationPriority: flags[2] << 8 | flags[3] 32690 }; 32691 }, 32692 32693 /** 32694 * Returns the string representation of an ASCII encoded four byte buffer. 32695 * @param buffer {Uint8Array} a four-byte buffer to translate 32696 * @return {string} the corresponding string 32697 */ 32698 parseType = function parseType(buffer) { 32699 var result = ''; 32700 result += String.fromCharCode(buffer[0]); 32701 result += String.fromCharCode(buffer[1]); 32702 result += String.fromCharCode(buffer[2]); 32703 result += String.fromCharCode(buffer[3]); 32704 return result; 32705 }, 32706 // Find the data for a box specified by its path 32707 findBox = function findBox(data, path) { 32708 var results = [], 32709 i, 32710 size, 32711 type, 32712 end, 32713 subresults; 32714 32715 if (!path.length) { 32716 // short-circuit the search for empty paths 32717 return null; 32718 } 32719 32720 for (i = 0; i < data.byteLength;) { 32721 size = toUnsigned$1(data[i] << 24 | data[i + 1] << 16 | data[i + 2] << 8 | data[i + 3]); 32722 type = parseType(data.subarray(i + 4, i + 8)); 32723 end = size > 1 ? i + size : data.byteLength; 32724 32725 if (type === path[0]) { 32726 if (path.length === 1) { 32727 // this is the end of the path and we've found the box we were 32728 // looking for 32729 results.push(data.subarray(i + 8, end)); 32730 } else { 32731 // recursively search for the next box along the path 32732 subresults = findBox(data.subarray(i + 8, end), path.slice(1)); 32733 32734 if (subresults.length) { 32735 results = results.concat(subresults); 32736 } 32737 } 32738 } 32739 32740 i = end; 32741 } // we've finished searching all of data 32742 32743 32744 return results; 32745 }, 32746 nalParse = function nalParse(avcStream) { 32747 var avcView = new DataView(avcStream.buffer, avcStream.byteOffset, avcStream.byteLength), 32748 result = [], 32749 i, 32750 length; 32751 32752 for (i = 0; i + 4 < avcStream.length; i += length) { 32753 length = avcView.getUint32(i); 32754 i += 4; // bail if this doesn't appear to be an H264 stream 32755 32756 if (length <= 0) { 32757 result.push('<span style=\'color:red;\'>MALFORMED DATA</span>'); 32758 continue; 32759 } 32760 32761 switch (avcStream[i] & 0x1F) { 32762 case 0x01: 32763 result.push('slice_layer_without_partitioning_rbsp'); 32764 break; 32765 32766 case 0x05: 32767 result.push('slice_layer_without_partitioning_rbsp_idr'); 32768 break; 32769 32770 case 0x06: 32771 result.push('sei_rbsp'); 32772 break; 32773 32774 case 0x07: 32775 result.push('seq_parameter_set_rbsp'); 32776 break; 32777 32778 case 0x08: 32779 result.push('pic_parameter_set_rbsp'); 32780 break; 32781 32782 case 0x09: 32783 result.push('access_unit_delimiter_rbsp'); 32784 break; 32785 32786 default: 32787 result.push('UNKNOWN NAL - ' + avcStream[i] & 0x1F); 32788 break; 32789 } 32790 } 32791 32792 return result; 32793 }, 32794 // registry of handlers for individual mp4 box types 32795 parse$1 = { 32796 // codingname, not a first-class box type. stsd entries share the
vendor: 6,697 bytes, lines 32797-32997
32797 // same format as real boxes so the parsing infrastructure can be 32798 // shared 32799 avc1: function avc1(data) { 32800 var view = new DataView(data.buffer, data.byteOffset, data.byteLength); 32801 return { 32802 dataReferenceIndex: view.getUint16(6), 32803 width: view.getUint16(24), 32804 height: view.getUint16(26), 32805 horizresolution: view.getUint16(28) + view.getUint16(30) / 16, 32806 vertresolution: view.getUint16(32) + view.getUint16(34) / 16, 32807 frameCount: view.getUint16(40), 32808 depth: view.getUint16(74), 32809 config: inspectMp4(data.subarray(78, data.byteLength)) 32810 }; 32811 }, 32812 avcC: function avcC(data) { 32813 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 32814 result = { 32815 configurationVersion: data[0], 32816 avcProfileIndication: data[1], 32817 profileCompatibility: data[2], 32818 avcLevelIndication: data[3], 32819 lengthSizeMinusOne: data[4] & 0x03, 32820 sps: [], 32821 pps: [] 32822 }, 32823 numOfSequenceParameterSets = data[5] & 0x1f, 32824 numOfPictureParameterSets, 32825 nalSize, 32826 offset, 32827 i; // iterate past any SPSs 32828 32829 offset = 6; 32830 32831 for (i = 0; i < numOfSequenceParameterSets; i++) { 32832 nalSize = view.getUint16(offset); 32833 offset += 2; 32834 result.sps.push(new Uint8Array(data.subarray(offset, offset + nalSize))); 32835 offset += nalSize; 32836 } // iterate past any PPSs 32837 32838 32839 numOfPictureParameterSets = data[offset]; 32840 offset++; 32841 32842 for (i = 0; i < numOfPictureParameterSets; i++) { 32843 nalSize = view.getUint16(offset); 32844 offset += 2; 32845 result.pps.push(new Uint8Array(data.subarray(offset, offset + nalSize))); 32846 offset += nalSize; 32847 } 32848 32849 return result; 32850 }, 32851 btrt: function btrt(data) { 32852 var view = new DataView(data.buffer, data.byteOffset, data.byteLength); 32853 return { 32854 bufferSizeDB: view.getUint32(0), 32855 maxBitrate: view.getUint32(4), 32856 avgBitrate: view.getUint32(8) 32857 }; 32858 }, 32859 esds: function esds(data) { 32860 return { 32861 version: data[0], 32862 flags: new Uint8Array(data.subarray(1, 4)), 32863 esId: data[6] << 8 | data[7], 32864 streamPriority: data[8] & 0x1f, 32865 decoderConfig: { 32866 objectProfileIndication: data[11], 32867 streamType: data[12] >>> 2 & 0x3f, 32868 bufferSize: data[13] << 16 | data[14] << 8 | data[15], 32869 maxBitrate: data[16] << 24 | data[17] << 16 | data[18] << 8 | data[19], 32870 avgBitrate: data[20] << 24 | data[21] << 16 | data[22] << 8 | data[23], 32871 decoderConfigDescriptor: { 32872 tag: data[24], 32873 length: data[25], 32874 audioObjectType: data[26] >>> 3 & 0x1f, 32875 samplingFrequencyIndex: (data[26] & 0x07) << 1 | data[27] >>> 7 & 0x01, 32876 channelConfiguration: data[27] >>> 3 & 0x0f 32877 } 32878 } 32879 }; 32880 }, 32881 ftyp: function ftyp(data) { 32882 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 32883 result = { 32884 majorBrand: parseType(data.subarray(0, 4)), 32885 minorVersion: view.getUint32(4), 32886 compatibleBrands: [] 32887 }, 32888 i = 8; 32889 32890 while (i < data.byteLength) { 32891 result.compatibleBrands.push(parseType(data.subarray(i, i + 4))); 32892 i += 4; 32893 } 32894 32895 return result; 32896 }, 32897 dinf: function dinf(data) { 32898 return { 32899 boxes: inspectMp4(data) 32900 }; 32901 }, 32902 dref: function dref(data) { 32903 return { 32904 version: data[0], 32905 flags: new Uint8Array(data.subarray(1, 4)), 32906 dataReferences: inspectMp4(data.subarray(8)) 32907 }; 32908 }, 32909 hdlr: function hdlr(data) { 32910 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 32911 result = { 32912 version: view.getUint8(0), 32913 flags: new Uint8Array(data.subarray(1, 4)), 32914 handlerType: parseType(data.subarray(8, 12)), 32915 name: '' 32916 }, 32917 i = 8; // parse out the name field 32918 32919 for (i = 24; i < data.byteLength; i++) { 32920 if (data[i] === 0x00) { 32921 // the name field is null-terminated 32922 i++; 32923 break; 32924 } 32925 32926 result.name += String.fromCharCode(data[i]); 32927 } // decode UTF-8 to javascript's internal representation 32928 // see http://ecmanaut.blogspot.com/2006/07/encoding-decoding-utf8-in-javascript.html 32929 32930 32931 result.name = decodeURIComponent(escape(result.name)); 32932 return result; 32933 }, 32934 mdat: function mdat(data) { 32935 return { 32936 byteLength: data.byteLength, 32937 nals: nalParse(data) 32938 }; 32939 }, 32940 mdhd: function mdhd(data) { 32941 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 32942 i = 4, 32943 language, 32944 result = { 32945 version: view.getUint8(0), 32946 flags: new Uint8Array(data.subarray(1, 4)), 32947 language: '' 32948 }; 32949 32950 if (result.version === 1) { 32951 i += 4; 32952 result.creationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes 32953 32954 i += 8; 32955 result.modificationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes 32956 32957 i += 4; 32958 result.timescale = view.getUint32(i); 32959 i += 8; 32960 result.duration = view.getUint32(i); // truncating top 4 bytes 32961 } else { 32962 result.creationTime = parseMp4Date(view.getUint32(i)); 32963 i += 4; 32964 result.modificationTime = parseMp4Date(view.getUint32(i)); 32965 i += 4; 32966 result.timescale = view.getUint32(i); 32967 i += 4; 32968 result.duration = view.getUint32(i); 32969 } 32970 32971 i += 4; // language is stored as an ISO-639-2/T code in an array of three 5-bit fields 32972 // each field is the packed difference between its ASCII value and 0x60 32973 32974 language = view.getUint16(i); 32975 result.language += String.fromCharCode((language >> 10) + 0x60); 32976 result.language += String.fromCharCode(((language & 0x03e0) >> 5) + 0x60); 32977 result.language += String.fromCharCode((language & 0x1f) + 0x60); 32978 return result; 32979 }, 32980 mdia: function mdia(data) { 32981 return { 32982 boxes: inspectMp4(data) 32983 }; 32984 }, 32985 mfhd: function mfhd(data) { 32986 return { 32987 version: data[0], 32988 flags: new Uint8Array(data.subarray(1, 4)), 32989 sequenceNumber: data[4] << 24 | data[5] << 16 | data[6] << 8 | data[7] 32990 }; 32991 }, 32992 minf: function minf(data) { 32993 return { 32994 boxes: inspectMp4(data) 32995 }; 32996 }, 32997 // codingname, not a first-class box type. stsd entries share the
vendor: 25,421 bytes, lines 32998-33757
32998 // same format as real boxes so the parsing infrastructure can be 32999 // shared 33000 mp4a: function mp4a(data) { 33001 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 33002 result = { 33003 // 6 bytes reserved 33004 dataReferenceIndex: view.getUint16(6), 33005 // 4 + 4 bytes reserved 33006 channelcount: view.getUint16(16), 33007 samplesize: view.getUint16(18), 33008 // 2 bytes pre_defined 33009 // 2 bytes reserved 33010 samplerate: view.getUint16(24) + view.getUint16(26) / 65536 33011 }; // if there are more bytes to process, assume this is an ISO/IEC 33012 // 14496-14 MP4AudioSampleEntry and parse the ESDBox 33013 33014 if (data.byteLength > 28) { 33015 result.streamDescriptor = inspectMp4(data.subarray(28))[0]; 33016 } 33017 33018 return result; 33019 }, 33020 moof: function moof(data) { 33021 return { 33022 boxes: inspectMp4(data) 33023 }; 33024 }, 33025 moov: function moov(data) { 33026 return { 33027 boxes: inspectMp4(data) 33028 }; 33029 }, 33030 mvex: function mvex(data) { 33031 return { 33032 boxes: inspectMp4(data) 33033 }; 33034 }, 33035 mvhd: function mvhd(data) { 33036 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 33037 i = 4, 33038 result = { 33039 version: view.getUint8(0), 33040 flags: new Uint8Array(data.subarray(1, 4)) 33041 }; 33042 33043 if (result.version === 1) { 33044 i += 4; 33045 result.creationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes 33046 33047 i += 8; 33048 result.modificationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes 33049 33050 i += 4; 33051 result.timescale = view.getUint32(i); 33052 i += 8; 33053 result.duration = view.getUint32(i); // truncating top 4 bytes 33054 } else { 33055 result.creationTime = parseMp4Date(view.getUint32(i)); 33056 i += 4; 33057 result.modificationTime = parseMp4Date(view.getUint32(i)); 33058 i += 4; 33059 result.timescale = view.getUint32(i); 33060 i += 4; 33061 result.duration = view.getUint32(i); 33062 } 33063 33064 i += 4; // convert fixed-point, base 16 back to a number 33065 33066 result.rate = view.getUint16(i) + view.getUint16(i + 2) / 16; 33067 i += 4; 33068 result.volume = view.getUint8(i) + view.getUint8(i + 1) / 8; 33069 i += 2; 33070 i += 2; 33071 i += 2 * 4; 33072 result.matrix = new Uint32Array(data.subarray(i, i + 9 * 4)); 33073 i += 9 * 4; 33074 i += 6 * 4; 33075 result.nextTrackId = view.getUint32(i); 33076 return result; 33077 }, 33078 pdin: function pdin(data) { 33079 var view = new DataView(data.buffer, data.byteOffset, data.byteLength); 33080 return { 33081 version: view.getUint8(0), 33082 flags: new Uint8Array(data.subarray(1, 4)), 33083 rate: view.getUint32(4), 33084 initialDelay: view.getUint32(8) 33085 }; 33086 }, 33087 sdtp: function sdtp(data) { 33088 var result = { 33089 version: data[0], 33090 flags: new Uint8Array(data.subarray(1, 4)), 33091 samples: [] 33092 }, 33093 i; 33094 33095 for (i = 4; i < data.byteLength; i++) { 33096 result.samples.push({ 33097 dependsOn: (data[i] & 0x30) >> 4, 33098 isDependedOn: (data[i] & 0x0c) >> 2, 33099 hasRedundancy: data[i] & 0x03 33100 }); 33101 } 33102 33103 return result; 33104 }, 33105 sidx: function sidx(data) { 33106 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 33107 result = { 33108 version: data[0], 33109 flags: new Uint8Array(data.subarray(1, 4)), 33110 references: [], 33111 referenceId: view.getUint32(4), 33112 timescale: view.getUint32(8), 33113 earliestPresentationTime: view.getUint32(12), 33114 firstOffset: view.getUint32(16) 33115 }, 33116 referenceCount = view.getUint16(22), 33117 i; 33118 33119 for (i = 24; referenceCount; i += 12, referenceCount--) { 33120 result.references.push({ 33121 referenceType: (data[i] & 0x80) >>> 7, 33122 referencedSize: view.getUint32(i) & 0x7FFFFFFF, 33123 subsegmentDuration: view.getUint32(i + 4), 33124 startsWithSap: !!(data[i + 8] & 0x80), 33125 sapType: (data[i + 8] & 0x70) >>> 4, 33126 sapDeltaTime: view.getUint32(i + 8) & 0x0FFFFFFF 33127 }); 33128 } 33129 33130 return result; 33131 }, 33132 smhd: function smhd(data) { 33133 return { 33134 version: data[0], 33135 flags: new Uint8Array(data.subarray(1, 4)), 33136 balance: data[4] + data[5] / 256 33137 }; 33138 }, 33139 stbl: function stbl(data) { 33140 return { 33141 boxes: inspectMp4(data) 33142 }; 33143 }, 33144 stco: function stco(data) { 33145 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 33146 result = { 33147 version: data[0], 33148 flags: new Uint8Array(data.subarray(1, 4)), 33149 chunkOffsets: [] 33150 }, 33151 entryCount = view.getUint32(4), 33152 i; 33153 33154 for (i = 8; entryCount; i += 4, entryCount--) { 33155 result.chunkOffsets.push(view.getUint32(i)); 33156 } 33157 33158 return result; 33159 }, 33160 stsc: function stsc(data) { 33161 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 33162 entryCount = view.getUint32(4), 33163 result = { 33164 version: data[0], 33165 flags: new Uint8Array(data.subarray(1, 4)), 33166 sampleToChunks: [] 33167 }, 33168 i; 33169 33170 for (i = 8; entryCount; i += 12, entryCount--) { 33171 result.sampleToChunks.push({ 33172 firstChunk: view.getUint32(i), 33173 samplesPerChunk: view.getUint32(i + 4), 33174 sampleDescriptionIndex: view.getUint32(i + 8) 33175 }); 33176 } 33177 33178 return result; 33179 }, 33180 stsd: function stsd(data) { 33181 return { 33182 version: data[0], 33183 flags: new Uint8Array(data.subarray(1, 4)), 33184 sampleDescriptions: inspectMp4(data.subarray(8)) 33185 }; 33186 }, 33187 stsz: function stsz(data) { 33188 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 33189 result = { 33190 version: data[0], 33191 flags: new Uint8Array(data.subarray(1, 4)), 33192 sampleSize: view.getUint32(4), 33193 entries: [] 33194 }, 33195 i; 33196 33197 for (i = 12; i < data.byteLength; i += 4) { 33198 result.entries.push(view.getUint32(i)); 33199 } 33200 33201 return result; 33202 }, 33203 stts: function stts(data) { 33204 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 33205 result = { 33206 version: data[0], 33207 flags: new Uint8Array(data.subarray(1, 4)), 33208 timeToSamples: [] 33209 }, 33210 entryCount = view.getUint32(4), 33211 i; 33212 33213 for (i = 8; entryCount; i += 8, entryCount--) { 33214 result.timeToSamples.push({ 33215 sampleCount: view.getUint32(i), 33216 sampleDelta: view.getUint32(i + 4) 33217 }); 33218 } 33219 33220 return result; 33221 }, 33222 styp: function styp(data) { 33223 return parse$1.ftyp(data); 33224 }, 33225 tfdt: function tfdt(data) { 33226 var result = { 33227 version: data[0], 33228 flags: new Uint8Array(data.subarray(1, 4)), 33229 baseMediaDecodeTime: toUnsigned$1(data[4] << 24 | data[5] << 16 | data[6] << 8 | data[7]) 33230 }; 33231 33232 if (result.version === 1) { 33233 result.baseMediaDecodeTime *= Math.pow(2, 32); 33234 result.baseMediaDecodeTime += toUnsigned$1(data[8] << 24 | data[9] << 16 | data[10] << 8 | data[11]); 33235 } 33236 33237 return result; 33238 }, 33239 tfhd: function tfhd(data) { 33240 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 33241 result = { 33242 version: data[0], 33243 flags: new Uint8Array(data.subarray(1, 4)), 33244 trackId: view.getUint32(4) 33245 }, 33246 baseDataOffsetPresent = result.flags[2] & 0x01, 33247 sampleDescriptionIndexPresent = result.flags[2] & 0x02, 33248 defaultSampleDurationPresent = result.flags[2] & 0x08, 33249 defaultSampleSizePresent = result.flags[2] & 0x10, 33250 defaultSampleFlagsPresent = result.flags[2] & 0x20, 33251 durationIsEmpty = result.flags[0] & 0x010000, 33252 defaultBaseIsMoof = result.flags[0] & 0x020000, 33253 i; 33254 i = 8; 33255 33256 if (baseDataOffsetPresent) { 33257 i += 4; // truncate top 4 bytes 33258 // FIXME: should we read the full 64 bits? 33259 33260 result.baseDataOffset = view.getUint32(12); 33261 i += 4; 33262 } 33263 33264 if (sampleDescriptionIndexPresent) { 33265 result.sampleDescriptionIndex = view.getUint32(i); 33266 i += 4; 33267 } 33268 33269 if (defaultSampleDurationPresent) { 33270 result.defaultSampleDuration = view.getUint32(i); 33271 i += 4; 33272 } 33273 33274 if (defaultSampleSizePresent) { 33275 result.defaultSampleSize = view.getUint32(i); 33276 i += 4; 33277 } 33278 33279 if (defaultSampleFlagsPresent) { 33280 result.defaultSampleFlags = view.getUint32(i); 33281 } 33282 33283 if (durationIsEmpty) { 33284 result.durationIsEmpty = true; 33285 } 33286 33287 if (!baseDataOffsetPresent && defaultBaseIsMoof) { 33288 result.baseDataOffsetIsMoof = true; 33289 } 33290 33291 return result; 33292 }, 33293 tkhd: function tkhd(data) { 33294 var view = new DataView(data.buffer, data.byteOffset, data.byteLength), 33295 i = 4, 33296 result = { 33297 version: view.getUint8(0), 33298 flags: new Uint8Array(data.subarray(1, 4)) 33299 }; 33300 33301 if (result.version === 1) { 33302 i += 4; 33303 result.creationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes 33304 33305 i += 8; 33306 result.modificationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes 33307 33308 i += 4; 33309 result.trackId = view.getUint32(i); 33310 i += 4; 33311 i += 8; 33312 result.duration = view.getUint32(i); // truncating top 4 bytes 33313 } else { 33314 result.creationTime = parseMp4Date(view.getUint32(i)); 33315 i += 4; 33316 result.modificationTime = parseMp4Date(view.getUint32(i)); 33317 i += 4; 33318 result.trackId = view.getUint32(i); 33319 i += 4; 33320 i += 4; 33321 result.duration = view.getUint32(i); 33322 } 33323 33324 i += 4; 33325 i += 2 * 4; 33326 result.layer = view.getUint16(i); 33327 i += 2; 33328 result.alternateGroup = view.getUint16(i); 33329 i += 2; // convert fixed-point, base 16 back to a number 33330 33331 result.volume = view.getUint8(i) + view.getUint8(i + 1) / 8; 33332 i += 2; 33333 i += 2; 33334 result.matrix = new Uint32Array(data.subarray(i, i + 9 * 4)); 33335 i += 9 * 4; 33336 result.width = view.getUint16(i) + view.getUint16(i + 2) / 16; 33337 i += 4; 33338 result.height = view.getUint16(i) + view.getUint16(i + 2) / 16; 33339 return result; 33340 }, 33341 traf: function traf(data) { 33342 return { 33343 boxes: inspectMp4(data) 33344 }; 33345 }, 33346 trak: function trak(data) { 33347 return { 33348 boxes: inspectMp4(data) 33349 }; 33350 }, 33351 trex: function trex(data) { 33352 var view = new DataView(data.buffer, data.byteOffset, data.byteLength); 33353 return { 33354 version: data[0], 33355 flags: new Uint8Array(data.subarray(1, 4)), 33356 trackId: view.getUint32(4), 33357 defaultSampleDescriptionIndex: view.getUint32(8), 33358 defaultSampleDuration: view.getUint32(12), 33359 defaultSampleSize: view.getUint32(16), 33360 sampleDependsOn: data[20] & 0x03, 33361 sampleIsDependedOn: (data[21] & 0xc0) >> 6, 33362 sampleHasRedundancy: (data[21] & 0x30) >> 4, 33363 samplePaddingValue: (data[21] & 0x0e) >> 1, 33364 sampleIsDifferenceSample: !!(data[21] & 0x01), 33365 sampleDegradationPriority: view.getUint16(22) 33366 }; 33367 }, 33368 trun: function trun(data) { 33369 var result = { 33370 version: data[0], 33371 flags: new Uint8Array(data.subarray(1, 4)), 33372 samples: [] 33373 }, 33374 view = new DataView(data.buffer, data.byteOffset, data.byteLength), 33375 // Flag interpretation 33376 dataOffsetPresent = result.flags[2] & 0x01, 33377 // compare with 2nd byte of 0x1 33378 firstSampleFlagsPresent = result.flags[2] & 0x04, 33379 // compare with 2nd byte of 0x4 33380 sampleDurationPresent = result.flags[1] & 0x01, 33381 // compare with 2nd byte of 0x100 33382 sampleSizePresent = result.flags[1] & 0x02, 33383 // compare with 2nd byte of 0x200 33384 sampleFlagsPresent = result.flags[1] & 0x04, 33385 // compare with 2nd byte of 0x400 33386 sampleCompositionTimeOffsetPresent = result.flags[1] & 0x08, 33387 // compare with 2nd byte of 0x800 33388 sampleCount = view.getUint32(4), 33389 offset = 8, 33390 sample; 33391 33392 if (dataOffsetPresent) { 33393 // 32 bit signed integer 33394 result.dataOffset = view.getInt32(offset); 33395 offset += 4; 33396 } // Overrides the flags for the first sample only. The order of 33397 // optional values will be: duration, size, compositionTimeOffset 33398 33399 33400 if (firstSampleFlagsPresent && sampleCount) { 33401 sample = { 33402 flags: parseSampleFlags(data.subarray(offset, offset + 4)) 33403 }; 33404 offset += 4; 33405 33406 if (sampleDurationPresent) { 33407 sample.duration = view.getUint32(offset); 33408 offset += 4; 33409 } 33410 33411 if (sampleSizePresent) { 33412 sample.size = view.getUint32(offset); 33413 offset += 4; 33414 } 33415 33416 if (sampleCompositionTimeOffsetPresent) { 33417 // Note: this should be a signed int if version is 1 33418 sample.compositionTimeOffset = view.getUint32(offset); 33419 offset += 4; 33420 } 33421 33422 result.samples.push(sample); 33423 sampleCount--; 33424 } 33425 33426 while (sampleCount--) { 33427 sample = {}; 33428 33429 if (sampleDurationPresent) { 33430 sample.duration = view.getUint32(offset); 33431 offset += 4; 33432 } 33433 33434 if (sampleSizePresent) { 33435 sample.size = view.getUint32(offset); 33436 offset += 4; 33437 } 33438 33439 if (sampleFlagsPresent) { 33440 sample.flags = parseSampleFlags(data.subarray(offset, offset + 4)); 33441 offset += 4; 33442 } 33443 33444 if (sampleCompositionTimeOffsetPresent) { 33445 // Note: this should be a signed int if version is 1 33446 sample.compositionTimeOffset = view.getUint32(offset); 33447 offset += 4; 33448 } 33449 33450 result.samples.push(sample); 33451 } 33452 33453 return result; 33454 }, 33455 'url ': function url(data) { 33456 return { 33457 version: data[0], 33458 flags: new Uint8Array(data.subarray(1, 4)) 33459 }; 33460 }, 33461 vmhd: function vmhd(data) { 33462 var view = new DataView(data.buffer, data.byteOffset, data.byteLength); 33463 return { 33464 version: data[0], 33465 flags: new Uint8Array(data.subarray(1, 4)), 33466 graphicsmode: view.getUint16(4), 33467 opcolor: new Uint16Array([view.getUint16(6), view.getUint16(8), view.getUint16(10)]) 33468 }; 33469 } 33470 }; 33471 /** 33472 * Return a javascript array of box objects parsed from an ISO base 33473 * media file. 33474 * @param data {Uint8Array} the binary data of the media to be inspected 33475 * @return {array} a javascript array of potentially nested box objects 33476 */ 33477 33478 33479 inspectMp4 = function inspectMp4(data) { 33480 var i = 0, 33481 result = [], 33482 view, 33483 size, 33484 type, 33485 end, 33486 box; // Convert data from Uint8Array to ArrayBuffer, to follow Dataview API 33487 33488 var ab = new ArrayBuffer(data.length); 33489 var v = new Uint8Array(ab); 33490 33491 for (var z = 0; z < data.length; ++z) { 33492 v[z] = data[z]; 33493 } 33494 33495 view = new DataView(ab); 33496 33497 while (i < data.byteLength) { 33498 // parse box data 33499 size = view.getUint32(i); 33500 type = parseType(data.subarray(i + 4, i + 8)); 33501 end = size > 1 ? i + size : data.byteLength; // parse type-specific data 33502 33503 box = (parse$1[type] || function (data) { 33504 return { 33505 data: data 33506 }; 33507 })(data.subarray(i + 8, end)); 33508 33509 box.size = size; 33510 box.type = type; // store this box and move to the next 33511 33512 result.push(box); 33513 i = end; 33514 } 33515 33516 return result; 33517 }; 33518 /** 33519 * Returns a textual representation of the javascript represtentation 33520 * of an MP4 file. You can use it as an alternative to 33521 * JSON.stringify() to compare inspected MP4s. 33522 * @param inspectedMp4 {array} the parsed array of boxes in an MP4 33523 * file 33524 * @param depth {number} (optional) the number of ancestor boxes of 33525 * the elements of inspectedMp4. Assumed to be zero if unspecified. 33526 * @return {string} a text representation of the parsed MP4 33527 */ 33528 33529 33530 _textifyMp = function textifyMp4(inspectedMp4, depth) { 33531 var indent; 33532 depth = depth || 0; 33533 indent = new Array(depth * 2 + 1).join(' '); // iterate over all the boxes 33534 33535 return inspectedMp4.map(function (box, index) { 33536 // list the box type first at the current indentation level 33537 return indent + box.type + '\n' + // the type is already included and handle child boxes separately 33538 Object.keys(box).filter(function (key) { 33539 return key !== 'type' && key !== 'boxes'; // output all the box properties 33540 }).map(function (key) { 33541 var prefix = indent + ' ' + key + ': ', 33542 value = box[key]; // print out raw bytes as hexademical 33543 33544 if (value instanceof Uint8Array || value instanceof Uint32Array) { 33545 var bytes = Array.prototype.slice.call(new Uint8Array(value.buffer, value.byteOffset, value.byteLength)).map(function (_byte) { 33546 return ' ' + ('00' + _byte.toString(16)).slice(-2); 33547 }).join('').match(/.{1,24}/g); 33548 33549 if (!bytes) { 33550 return prefix + '<>'; 33551 } 33552 33553 if (bytes.length === 1) { 33554 return prefix + '<' + bytes.join('').slice(1) + '>'; 33555 } 33556 33557 return prefix + '<\n' + bytes.map(function (line) { 33558 return indent + ' ' + line; 33559 }).join('\n') + '\n' + indent + ' >'; 33560 } // stringify generic objects 33561 33562 33563 return prefix + JSON.stringify(value, null, 2).split('\n').map(function (line, index) { 33564 if (index === 0) { 33565 return line; 33566 } 33567 33568 return indent + ' ' + line; 33569 }).join('\n'); 33570 }).join('\n') + ( // recursively textify the child boxes 33571 box.boxes ? '\n' + _textifyMp(box.boxes, depth + 1) : ''); 33572 }).join('\n'); 33573 }; 33574 33575 var mp4Inspector = { 33576 inspect: inspectMp4, 33577 textify: _textifyMp, 33578 parseType: parseType, 33579 findBox: findBox, 33580 parseTraf: parse$1.traf, 33581 parseTfdt: parse$1.tfdt, 33582 parseHdlr: parse$1.hdlr, 33583 parseTfhd: parse$1.tfhd, 33584 parseTrun: parse$1.trun, 33585 parseSidx: parse$1.sidx 33586 }; 33587 33588 var toUnsigned$2 = bin.toUnsigned; 33589 var toHexString$1 = bin.toHexString; 33590 var timescale, startTime, compositionStartTime, getVideoTrackIds, getTracks; 33591 /** 33592 * Parses an MP4 initialization segment and extracts the timescale 33593 * values for any declared tracks. Timescale values indicate the 33594 * number of clock ticks per second to assume for time-based values 33595 * elsewhere in the MP4. 33596 * 33597 * To determine the start time of an MP4, you need two pieces of 33598 * information: the timescale unit and the earliest base media decode 33599 * time. Multiple timescales can be specified within an MP4 but the 33600 * base media decode time is always expressed in the timescale from 33601 * the media header box for the track: 33602 * ``` 33603 * moov > trak > mdia > mdhd.timescale 33604 * ``` 33605 * @param init {Uint8Array} the bytes of the init segment 33606 * @return {object} a hash of track ids to timescale values or null if 33607 * the init segment is malformed. 33608 */ 33609 33610 timescale = function timescale(init) { 33611 var result = {}, 33612 traks = mp4Inspector.findBox(init, ['moov', 'trak']); // mdhd timescale 33613 33614 return traks.reduce(function (result, trak) { 33615 var tkhd, version, index, id, mdhd; 33616 tkhd = mp4Inspector.findBox(trak, ['tkhd'])[0]; 33617 33618 if (!tkhd) { 33619 return null; 33620 } 33621 33622 version = tkhd[0]; 33623 index = version === 0 ? 12 : 20; 33624 id = toUnsigned$2(tkhd[index] << 24 | tkhd[index + 1] << 16 | tkhd[index + 2] << 8 | tkhd[index + 3]); 33625 mdhd = mp4Inspector.findBox(trak, ['mdia', 'mdhd'])[0]; 33626 33627 if (!mdhd) { 33628 return null; 33629 } 33630 33631 version = mdhd[0]; 33632 index = version === 0 ? 12 : 20; 33633 result[id] = toUnsigned$2(mdhd[index] << 24 | mdhd[index + 1] << 16 | mdhd[index + 2] << 8 | mdhd[index + 3]); 33634 return result; 33635 }, result); 33636 }; 33637 /** 33638 * Determine the base media decode start time, in seconds, for an MP4 33639 * fragment. If multiple fragments are specified, the earliest time is 33640 * returned. 33641 * 33642 * The base media decode time can be parsed from track fragment 33643 * metadata: 33644 * ``` 33645 * moof > traf > tfdt.baseMediaDecodeTime 33646 * ``` 33647 * It requires the timescale value from the mdhd to interpret. 33648 * 33649 * @param timescale {object} a hash of track ids to timescale values. 33650 * @return {number} the earliest base media decode start time for the 33651 * fragment, in seconds 33652 */ 33653 33654 33655 startTime = function startTime(timescale, fragment) { 33656 var trafs, baseTimes, result; // we need info from two childrend of each track fragment box 33657 33658 trafs = mp4Inspector.findBox(fragment, ['moof', 'traf']); // determine the start times for each track 33659 33660 baseTimes = [].concat.apply([], trafs.map(function (traf) { 33661 return mp4Inspector.findBox(traf, ['tfhd']).map(function (tfhd) { 33662 var id, scale, baseTime; // get the track id from the tfhd 33663 33664 id = toUnsigned$2(tfhd[4] << 24 | tfhd[5] << 16 | tfhd[6] << 8 | tfhd[7]); // assume a 90kHz clock if no timescale was specified 33665 33666 scale = timescale[id] || 90e3; // get the base media decode time from the tfdt 33667 33668 baseTime = mp4Inspector.findBox(traf, ['tfdt']).map(function (tfdt) { 33669 var version, result; 33670 version = tfdt[0]; 33671 result = toUnsigned$2(tfdt[4] << 24 | tfdt[5] << 16 | tfdt[6] << 8 | tfdt[7]); 33672 33673 if (version === 1) { 33674 result *= Math.pow(2, 32); 33675 result += toUnsigned$2(tfdt[8] << 24 | tfdt[9] << 16 | tfdt[10] << 8 | tfdt[11]); 33676 } 33677 33678 return result; 33679 })[0]; 33680 baseTime = baseTime || Infinity; // convert base time to seconds 33681 33682 return baseTime / scale; 33683 }); 33684 })); // return the minimum 33685 33686 result = Math.min.apply(null, baseTimes); 33687 return isFinite(result) ? result : 0; 33688 }; 33689 /** 33690 * Determine the composition start, in seconds, for an MP4 33691 * fragment. 33692 * 33693 * The composition start time of a fragment can be calculated using the base 33694 * media decode time, composition time offset, and timescale, as follows: 33695 * 33696 * compositionStartTime = (baseMediaDecodeTime + compositionTimeOffset) / timescale 33697 * 33698 * All of the aforementioned information is contained within a media fragment's 33699 * `traf` box, except for timescale info, which comes from the initialization 33700 * segment, so a track id (also contained within a `traf`) is also necessary to 33701 * associate it with a timescale 33702 * 33703 * 33704 * @param timescales {object} - a hash of track ids to timescale values. 33705 * @param fragment {Unit8Array} - the bytes of a media segment 33706 * @return {number} the composition start time for the fragment, in seconds 33707 **/ 33708 33709 33710 compositionStartTime = function compositionStartTime(timescales, fragment) { 33711 var trafBoxes = mp4Inspector.findBox(fragment, ['moof', 'traf']); 33712 var baseMediaDecodeTime = 0; 33713 var compositionTimeOffset = 0; 33714 var trackId; 33715 33716 if (trafBoxes && trafBoxes.length) { 33717 // The spec states that track run samples contained within a `traf` box are contiguous, but 33718 // it does not explicitly state whether the `traf` boxes themselves are contiguous. 33719 // We will assume that they are, so we only need the first to calculate start time. 33720 var parsedTraf = mp4Inspector.parseTraf(trafBoxes[0]); 33721 33722 for (var i = 0; i < parsedTraf.boxes.length; i++) { 33723 if (parsedTraf.boxes[i].type === 'tfhd') { 33724 trackId = parsedTraf.boxes[i].trackId; 33725 } else if (parsedTraf.boxes[i].type === 'tfdt') { 33726 baseMediaDecodeTime = parsedTraf.boxes[i].baseMediaDecodeTime; 33727 } else if (parsedTraf.boxes[i].type === 'trun' && parsedTraf.boxes[i].samples.length) { 33728 compositionTimeOffset = parsedTraf.boxes[i].samples[0].compositionTimeOffset || 0; 33729 } 33730 } 33731 } // Get timescale for this specific track. Assume a 90kHz clock if no timescale was 33732 // specified. 33733 33734 33735 var timescale = timescales[trackId] || 90e3; // return the composition start time, in seconds 33736 33737 return (baseMediaDecodeTime + compositionTimeOffset) / timescale; 33738 }; 33739 /** 33740 * Find the trackIds of the video tracks in this source. 33741 * Found by parsing the Handler Reference and Track Header Boxes: 33742 * moov > trak > mdia > hdlr 33743 * moov > trak > tkhd 33744 * 33745 * @param {Uint8Array} init - The bytes of the init segment for this source 33746 * @return {Number[]} A list of trackIds 33747 * 33748 * @see ISO-BMFF-12/2015, Section 8.4.3 33749 **/ 33750 33751 33752 getVideoTrackIds = function getVideoTrackIds(init) { 33753 var traks = mp4Inspector.findBox(init, ['moov', 'trak']); 33754 var videoTrackIds = []; 33755 traks.forEach(function (trak) { 33756 var hdlrs = mp4Inspector.findBox(trak, ['mdia', 'hdlr']); 33757 var tkhds = mp4Inspector.findBox(trak, ['tkhd']);
vendor: 8,593 bytes, lines 33758-34019
33758 hdlrs.forEach(function (hdlr, index) { 33759 var handlerType = mp4Inspector.parseType(hdlr.subarray(8, 12)); 33760 var tkhd = tkhds[index]; 33761 var view; 33762 var version; 33763 var trackId; 33764 33765 if (handlerType === 'vide') { 33766 view = new DataView(tkhd.buffer, tkhd.byteOffset, tkhd.byteLength); 33767 version = view.getUint8(0); 33768 trackId = version === 0 ? view.getUint32(12) : view.getUint32(20); 33769 videoTrackIds.push(trackId); 33770 } 33771 }); 33772 }); 33773 return videoTrackIds; 33774 }; 33775 /** 33776 * Get all the video, audio, and hint tracks from a non fragmented 33777 * mp4 segment 33778 */ 33779 33780 33781 getTracks = function getTracks(init) { 33782 var traks = mp4Inspector.findBox(init, ['moov', 'trak']); 33783 var tracks = []; 33784 traks.forEach(function (trak) { 33785 var track = {}; 33786 var tkhd = mp4Inspector.findBox(trak, ['tkhd'])[0]; 33787 var view, version; // id 33788 33789 if (tkhd) { 33790 view = new DataView(tkhd.buffer, tkhd.byteOffset, tkhd.byteLength); 33791 version = view.getUint8(0); 33792 track.id = version === 0 ? view.getUint32(12) : view.getUint32(20); 33793 } 33794 33795 var hdlr = mp4Inspector.findBox(trak, ['mdia', 'hdlr'])[0]; // type 33796 33797 if (hdlr) { 33798 var type = mp4Inspector.parseType(hdlr.subarray(8, 12)); 33799 33800 if (type === 'vide') { 33801 track.type = 'video'; 33802 } else if (type === 'soun') { 33803 track.type = 'audio'; 33804 } else { 33805 track.type = type; 33806 } 33807 } // codec 33808 33809 33810 var stsd = mp4Inspector.findBox(trak, ['mdia', 'minf', 'stbl', 'stsd'])[0]; 33811 33812 if (stsd) { 33813 var sampleDescriptions = stsd.subarray(8); // gives the codec type string 33814 33815 track.codec = mp4Inspector.parseType(sampleDescriptions.subarray(4, 8)); 33816 var codecBox = mp4Inspector.findBox(sampleDescriptions, [track.codec])[0]; 33817 var codecConfig, codecConfigType; 33818 33819 if (codecBox) { 33820 // https://tools.ietf.org/html/rfc6381#section-3.3 33821 if (/^[a-z]vc[1-9]$/i.test(track.codec)) { 33822 // we don't need anything but the "config" parameter of the 33823 // avc1 codecBox 33824 codecConfig = codecBox.subarray(78); 33825 codecConfigType = mp4Inspector.parseType(codecConfig.subarray(4, 8)); 33826 33827 if (codecConfigType === 'avcC' && codecConfig.length > 11) { 33828 track.codec += '.'; // left padded with zeroes for single digit hex 33829 // profile idc 33830 33831 track.codec += toHexString$1(codecConfig[9]); // the byte containing the constraint_set flags 33832 33833 track.codec += toHexString$1(codecConfig[10]); // level idc 33834 33835 track.codec += toHexString$1(codecConfig[11]); 33836 } else { 33837 // TODO: show a warning that we couldn't parse the codec 33838 // and are using the default 33839 track.codec = 'avc1.4d400d'; 33840 } 33841 } else if (/^mp4[a,v]$/i.test(track.codec)) { 33842 // we do not need anything but the streamDescriptor of the mp4a codecBox 33843 codecConfig = codecBox.subarray(28); 33844 codecConfigType = mp4Inspector.parseType(codecConfig.subarray(4, 8)); 33845 33846 if (codecConfigType === 'esds' && codecConfig.length > 20 && codecConfig[19] !== 0) { 33847 track.codec += '.' + toHexString$1(codecConfig[19]); // this value is only a single digit 33848 33849 track.codec += '.' + toHexString$1(codecConfig[20] >>> 2 & 0x3f).replace(/^0/, ''); 33850 } else { 33851 // TODO: show a warning that we couldn't parse the codec 33852 // and are using the default 33853 track.codec = 'mp4a.40.2'; 33854 } 33855 } 33856 } 33857 } 33858 33859 var mdhd = mp4Inspector.findBox(trak, ['mdia', 'mdhd'])[0]; 33860 33861 if (mdhd && tkhd) { 33862 var index = version === 0 ? 12 : 20; 33863 track.timescale = toUnsigned$2(mdhd[index] << 24 | mdhd[index + 1] << 16 | mdhd[index + 2] << 8 | mdhd[index + 3]); 33864 } 33865 33866 tracks.push(track); 33867 }); 33868 return tracks; 33869 }; 33870 33871 var probe = { 33872 // export mp4 inspector's findBox and parseType for backwards compatibility 33873 findBox: mp4Inspector.findBox, 33874 parseType: mp4Inspector.parseType, 33875 timescale: timescale, 33876 startTime: startTime, 33877 compositionStartTime: compositionStartTime, 33878 videoTrackIds: getVideoTrackIds, 33879 tracks: getTracks 33880 }; 33881 33882 /** 33883 * mux.js 33884 * 33885 * Copyright (c) Brightcove 33886 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 33887 * 33888 * Reads in-band caption information from a video elementary 33889 * stream. Captions must follow the CEA-708 standard for injection 33890 * into an MPEG-2 transport streams. 33891 * @see https://en.wikipedia.org/wiki/CEA-708 33892 * @see https://www.gpo.gov/fdsys/pkg/CFR-2007-title47-vol1/pdf/CFR-2007-title47-vol1-sec15-119.pdf 33893 */ 33894 // payload type field to indicate how they are to be 33895 // interpreted. CEAS-708 caption content is always transmitted with 33896 // payload type 0x04. 33897 33898 var USER_DATA_REGISTERED_ITU_T_T35 = 4, 33899 RBSP_TRAILING_BITS = 128; 33900 /** 33901 * Parse a supplemental enhancement information (SEI) NAL unit. 33902 * Stops parsing once a message of type ITU T T35 has been found. 33903 * 33904 * @param bytes {Uint8Array} the bytes of a SEI NAL unit 33905 * @return {object} the parsed SEI payload 33906 * @see Rec. ITU-T H.264, 7.3.2.3.1 33907 */ 33908 33909 var parseSei = function parseSei(bytes) { 33910 var i = 0, 33911 result = { 33912 payloadType: -1, 33913 payloadSize: 0 33914 }, 33915 payloadType = 0, 33916 payloadSize = 0; // go through the sei_rbsp parsing each each individual sei_message 33917 33918 while (i < bytes.byteLength) { 33919 // stop once we have hit the end of the sei_rbsp 33920 if (bytes[i] === RBSP_TRAILING_BITS) { 33921 break; 33922 } // Parse payload type 33923 33924 33925 while (bytes[i] === 0xFF) { 33926 payloadType += 255; 33927 i++; 33928 } 33929 33930 payloadType += bytes[i++]; // Parse payload size 33931 33932 while (bytes[i] === 0xFF) { 33933 payloadSize += 255; 33934 i++; 33935 } 33936 33937 payloadSize += bytes[i++]; // this sei_message is a 608/708 caption so save it and break 33938 // there can only ever be one caption message in a frame's sei 33939 33940 if (!result.payload && payloadType === USER_DATA_REGISTERED_ITU_T_T35) { 33941 result.payloadType = payloadType; 33942 result.payloadSize = payloadSize; 33943 result.payload = bytes.subarray(i, i + payloadSize); 33944 break; 33945 } // skip the payload and parse the next message 33946 33947 33948 i += payloadSize; 33949 payloadType = 0; 33950 payloadSize = 0; 33951 } 33952 33953 return result; 33954 }; // see ANSI/SCTE 128-1 (2013), section 8.1 33955 33956 33957 var parseUserData = function parseUserData(sei) { 33958 // itu_t_t35_contry_code must be 181 (United States) for 33959 // captions 33960 if (sei.payload[0] !== 181) { 33961 return null; 33962 } // itu_t_t35_provider_code should be 49 (ATSC) for captions 33963 33964 33965 if ((sei.payload[1] << 8 | sei.payload[2]) !== 49) { 33966 return null; 33967 } // the user_identifier should be "GA94" to indicate ATSC1 data 33968 33969 33970 if (String.fromCharCode(sei.payload[3], sei.payload[4], sei.payload[5], sei.payload[6]) !== 'GA94') { 33971 return null; 33972 } // finally, user_data_type_code should be 0x03 for caption data 33973 33974 33975 if (sei.payload[7] !== 0x03) { 33976 return null; 33977 } // return the user_data_type_structure and strip the trailing 33978 // marker bits 33979 33980 33981 return sei.payload.subarray(8, sei.payload.length - 1); 33982 }; // see CEA-708-D, section 4.4 33983 33984 33985 var parseCaptionPackets = function parseCaptionPackets(pts, userData) { 33986 var results = [], 33987 i, 33988 count, 33989 offset, 33990 data; // if this is just filler, return immediately 33991 33992 if (!(userData[0] & 0x40)) { 33993 return results; 33994 } // parse out the cc_data_1 and cc_data_2 fields 33995 33996 33997 count = userData[0] & 0x1f; 33998 33999 for (i = 0; i < count; i++) { 34000 offset = i * 3; 34001 data = { 34002 type: userData[offset + 2] & 0x03, 34003 pts: pts 34004 }; // capture cc data when cc_valid is 1 34005 34006 if (userData[offset + 2] & 0x04) { 34007 data.ccData = userData[offset + 3] << 8 | userData[offset + 4]; 34008 results.push(data); 34009 } 34010 } 34011 34012 return results; 34013 }; 34014 34015 var discardEmulationPreventionBytes = function discardEmulationPreventionBytes(data) { 34016 var length = data.byteLength, 34017 emulationPreventionBytesPositions = [], 34018 i = 1, 34019 newLength,
vendor: 8,481 bytes, lines 34020-34279
34020 newData; // Find all `Emulation Prevention Bytes` 34021 34022 while (i < length - 2) { 34023 if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0x03) { 34024 emulationPreventionBytesPositions.push(i + 2); 34025 i += 2; 34026 } else { 34027 i++; 34028 } 34029 } // If no Emulation Prevention Bytes were found just return the original 34030 // array 34031 34032 34033 if (emulationPreventionBytesPositions.length === 0) { 34034 return data; 34035 } // Create a new array to hold the NAL unit data 34036 34037 34038 newLength = length - emulationPreventionBytesPositions.length; 34039 newData = new Uint8Array(newLength); 34040 var sourceIndex = 0; 34041 34042 for (i = 0; i < newLength; sourceIndex++, i++) { 34043 if (sourceIndex === emulationPreventionBytesPositions[0]) { 34044 // Skip this byte 34045 sourceIndex++; // Remove this position index 34046 34047 emulationPreventionBytesPositions.shift(); 34048 } 34049 34050 newData[i] = data[sourceIndex]; 34051 } 34052 34053 return newData; 34054 }; // exports 34055 34056 34057 var captionPacketParser = { 34058 parseSei: parseSei, 34059 parseUserData: parseUserData, 34060 parseCaptionPackets: parseCaptionPackets, 34061 discardEmulationPreventionBytes: discardEmulationPreventionBytes, 34062 USER_DATA_REGISTERED_ITU_T_T35: USER_DATA_REGISTERED_ITU_T_T35 34063 }; 34064 34065 /** 34066 * mux.js 34067 * 34068 * Copyright (c) Brightcove 34069 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 34070 * 34071 * A lightweight readable stream implemention that handles event dispatching. 34072 * Objects that inherit from streams should call init in their constructors. 34073 */ 34074 34075 var Stream$1 = function Stream() { 34076 this.init = function () { 34077 var listeners = {}; 34078 /** 34079 * Add a listener for a specified event type. 34080 * @param type {string} the event name 34081 * @param listener {function} the callback to be invoked when an event of 34082 * the specified type occurs 34083 */ 34084 34085 this.on = function (type, listener) { 34086 if (!listeners[type]) { 34087 listeners[type] = []; 34088 } 34089 34090 listeners[type] = listeners[type].concat(listener); 34091 }; 34092 /** 34093 * Remove a listener for a specified event type. 34094 * @param type {string} the event name 34095 * @param listener {function} a function previously registered for this 34096 * type of event through `on` 34097 */ 34098 34099 34100 this.off = function (type, listener) { 34101 var index; 34102 34103 if (!listeners[type]) { 34104 return false; 34105 } 34106 34107 index = listeners[type].indexOf(listener); 34108 listeners[type] = listeners[type].slice(); 34109 listeners[type].splice(index, 1); 34110 return index > -1; 34111 }; 34112 /** 34113 * Trigger an event of the specified type on this stream. Any additional 34114 * arguments to this function are passed as parameters to event listeners. 34115 * @param type {string} the event name 34116 */ 34117 34118 34119 this.trigger = function (type) { 34120 var callbacks, i, length, args; 34121 callbacks = listeners[type]; 34122 34123 if (!callbacks) { 34124 return; 34125 } // Slicing the arguments on every invocation of this method 34126 // can add a significant amount of overhead. Avoid the 34127 // intermediate object creation for the common case of a 34128 // single callback argument 34129 34130 34131 if (arguments.length === 2) { 34132 length = callbacks.length; 34133 34134 for (i = 0; i < length; ++i) { 34135 callbacks[i].call(this, arguments[1]); 34136 } 34137 } else { 34138 args = []; 34139 i = arguments.length; 34140 34141 for (i = 1; i < arguments.length; ++i) { 34142 args.push(arguments[i]); 34143 } 34144 34145 length = callbacks.length; 34146 34147 for (i = 0; i < length; ++i) { 34148 callbacks[i].apply(this, args); 34149 } 34150 } 34151 }; 34152 /** 34153 * Destroys the stream and cleans up. 34154 */ 34155 34156 34157 this.dispose = function () { 34158 listeners = {}; 34159 }; 34160 }; 34161 }; 34162 /** 34163 * Forwards all `data` events on this stream to the destination stream. The 34164 * destination stream should provide a method `push` to receive the data 34165 * events as they arrive. 34166 * @param destination {stream} the stream that will receive all `data` events 34167 * @param autoFlush {boolean} if false, we will not call `flush` on the destination 34168 * when the current stream emits a 'done' event 34169 * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options 34170 */ 34171 34172 34173 Stream$1.prototype.pipe = function (destination) { 34174 this.on('data', function (data) { 34175 destination.push(data); 34176 }); 34177 this.on('done', function (flushSource) { 34178 destination.flush(flushSource); 34179 }); 34180 this.on('partialdone', function (flushSource) { 34181 destination.partialFlush(flushSource); 34182 }); 34183 this.on('endedtimeline', function (flushSource) { 34184 destination.endTimeline(flushSource); 34185 }); 34186 this.on('reset', function (flushSource) { 34187 destination.reset(flushSource); 34188 }); 34189 return destination; 34190 }; // Default stream functions that are expected to be overridden to perform 34191 // actual work. These are provided by the prototype as a sort of no-op 34192 // implementation so that we don't have to check for their existence in the 34193 // `pipe` function above. 34194 34195 34196 Stream$1.prototype.push = function (data) { 34197 this.trigger('data', data); 34198 }; 34199 34200 Stream$1.prototype.flush = function (flushSource) { 34201 this.trigger('done', flushSource); 34202 }; 34203 34204 Stream$1.prototype.partialFlush = function (flushSource) { 34205 this.trigger('partialdone', flushSource); 34206 }; 34207 34208 Stream$1.prototype.endTimeline = function (flushSource) { 34209 this.trigger('endedtimeline', flushSource); 34210 }; 34211 34212 Stream$1.prototype.reset = function (flushSource) { 34213 this.trigger('reset', flushSource); 34214 }; 34215 34216 var stream = Stream$1; 34217 34218 // Link To Transport 34219 // ----------------- 34220 34221 34222 var CaptionStream = function CaptionStream() { 34223 CaptionStream.prototype.init.call(this); 34224 this.captionPackets_ = []; 34225 this.ccStreams_ = [new Cea608Stream(0, 0), // eslint-disable-line no-use-before-define 34226 new Cea608Stream(0, 1), // eslint-disable-line no-use-before-define 34227 new Cea608Stream(1, 0), // eslint-disable-line no-use-before-define 34228 new Cea608Stream(1, 1) // eslint-disable-line no-use-before-define 34229 ]; 34230 this.reset(); // forward data and done events from CCs to this CaptionStream 34231 34232 this.ccStreams_.forEach(function (cc) { 34233 cc.on('data', this.trigger.bind(this, 'data')); 34234 cc.on('partialdone', this.trigger.bind(this, 'partialdone')); 34235 cc.on('done', this.trigger.bind(this, 'done')); 34236 }, this); 34237 }; 34238 34239 CaptionStream.prototype = new stream(); 34240 34241 CaptionStream.prototype.push = function (event) { 34242 var sei, userData, newCaptionPackets; // only examine SEI NALs 34243 34244 if (event.nalUnitType !== 'sei_rbsp') { 34245 return; 34246 } // parse the sei 34247 34248 34249 sei = captionPacketParser.parseSei(event.escapedRBSP); // ignore everything but user_data_registered_itu_t_t35 34250 34251 if (sei.payloadType !== captionPacketParser.USER_DATA_REGISTERED_ITU_T_T35) { 34252 return; 34253 } // parse out the user data payload 34254 34255 34256 userData = captionPacketParser.parseUserData(sei); // ignore unrecognized userData 34257 34258 if (!userData) { 34259 return; 34260 } // Sometimes, the same segment # will be downloaded twice. To stop the 34261 // caption data from being processed twice, we track the latest dts we've 34262 // received and ignore everything with a dts before that. However, since 34263 // data for a specific dts can be split across packets on either side of 34264 // a segment boundary, we need to make sure we *don't* ignore the packets 34265 // from the *next* segment that have dts === this.latestDts_. By constantly 34266 // tracking the number of packets received with dts === this.latestDts_, we 34267 // know how many should be ignored once we start receiving duplicates. 34268 34269 34270 if (event.dts < this.latestDts_) { 34271 // We've started getting older data, so set the flag. 34272 this.ignoreNextEqualDts_ = true; 34273 return; 34274 } else if (event.dts === this.latestDts_ && this.ignoreNextEqualDts_) { 34275 this.numSameDts_--; 34276 34277 if (!this.numSameDts_) { 34278 // We've received the last duplicate packet, time to start processing again 34279 this.ignoreNextEqualDts_ = false;
vendor: 6,649 bytes, lines 34280-34526
34280 } 34281 34282 return; 34283 } // parse out CC data packets and save them for later 34284 34285 34286 newCaptionPackets = captionPacketParser.parseCaptionPackets(event.pts, userData); 34287 this.captionPackets_ = this.captionPackets_.concat(newCaptionPackets); 34288 34289 if (this.latestDts_ !== event.dts) { 34290 this.numSameDts_ = 0; 34291 } 34292 34293 this.numSameDts_++; 34294 this.latestDts_ = event.dts; 34295 }; 34296 34297 CaptionStream.prototype.flushCCStreams = function (flushType) { 34298 this.ccStreams_.forEach(function (cc) { 34299 return flushType === 'flush' ? cc.flush() : cc.partialFlush(); 34300 }, this); 34301 }; 34302 34303 CaptionStream.prototype.flushStream = function (flushType) { 34304 // make sure we actually parsed captions before proceeding 34305 if (!this.captionPackets_.length) { 34306 this.flushCCStreams(flushType); 34307 return; 34308 } // In Chrome, the Array#sort function is not stable so add a 34309 // presortIndex that we can use to ensure we get a stable-sort 34310 34311 34312 this.captionPackets_.forEach(function (elem, idx) { 34313 elem.presortIndex = idx; 34314 }); // sort caption byte-pairs based on their PTS values 34315 34316 this.captionPackets_.sort(function (a, b) { 34317 if (a.pts === b.pts) { 34318 return a.presortIndex - b.presortIndex; 34319 } 34320 34321 return a.pts - b.pts; 34322 }); 34323 this.captionPackets_.forEach(function (packet) { 34324 if (packet.type < 2) { 34325 // Dispatch packet to the right Cea608Stream 34326 this.dispatchCea608Packet(packet); 34327 } // this is where an 'else' would go for a dispatching packets 34328 // to a theoretical Cea708Stream that handles SERVICEn data 34329 34330 }, this); 34331 this.captionPackets_.length = 0; 34332 this.flushCCStreams(flushType); 34333 }; 34334 34335 CaptionStream.prototype.flush = function () { 34336 return this.flushStream('flush'); 34337 }; // Only called if handling partial data 34338 34339 34340 CaptionStream.prototype.partialFlush = function () { 34341 return this.flushStream('partialFlush'); 34342 }; 34343 34344 CaptionStream.prototype.reset = function () { 34345 this.latestDts_ = null; 34346 this.ignoreNextEqualDts_ = false; 34347 this.numSameDts_ = 0; 34348 this.activeCea608Channel_ = [null, null]; 34349 this.ccStreams_.forEach(function (ccStream) { 34350 ccStream.reset(); 34351 }); 34352 }; // From the CEA-608 spec: 34353 34354 /* 34355 * When XDS sub-packets are interleaved with other services, the end of each sub-packet shall be followed 34356 * by a control pair to change to a different service. When any of the control codes from 0x10 to 0x1F is 34357 * used to begin a control code pair, it indicates the return to captioning or Text data. The control code pair 34358 * and subsequent data should then be processed according to the FCC rules. It may be necessary for the 34359 * line 21 data encoder to automatically insert a control code pair (i.e. RCL, RU2, RU3, RU4, RDC, or RTD) 34360 * to switch to captioning or Text. 34361 */ 34362 // With that in mind, we ignore any data between an XDS control code and a 34363 // subsequent closed-captioning control code. 34364 34365 34366 CaptionStream.prototype.dispatchCea608Packet = function (packet) { 34367 // NOTE: packet.type is the CEA608 field 34368 if (this.setsTextOrXDSActive(packet)) { 34369 this.activeCea608Channel_[packet.type] = null; 34370 } else if (this.setsChannel1Active(packet)) { 34371 this.activeCea608Channel_[packet.type] = 0; 34372 } else if (this.setsChannel2Active(packet)) { 34373 this.activeCea608Channel_[packet.type] = 1; 34374 } 34375 34376 if (this.activeCea608Channel_[packet.type] === null) { 34377 // If we haven't received anything to set the active channel, or the 34378 // packets are Text/XDS data, discard the data; we don't want jumbled 34379 // captions 34380 return; 34381 } 34382 34383 this.ccStreams_[(packet.type << 1) + this.activeCea608Channel_[packet.type]].push(packet); 34384 }; 34385 34386 CaptionStream.prototype.setsChannel1Active = function (packet) { 34387 return (packet.ccData & 0x7800) === 0x1000; 34388 }; 34389 34390 CaptionStream.prototype.setsChannel2Active = function (packet) { 34391 return (packet.ccData & 0x7800) === 0x1800; 34392 }; 34393 34394 CaptionStream.prototype.setsTextOrXDSActive = function (packet) { 34395 return (packet.ccData & 0x7100) === 0x0100 || (packet.ccData & 0x78fe) === 0x102a || (packet.ccData & 0x78fe) === 0x182a; 34396 }; // ---------------------- 34397 // Session to Application 34398 // ---------------------- 34399 // This hash maps non-ASCII, special, and extended character codes to their 34400 // proper Unicode equivalent. The first keys that are only a single byte 34401 // are the non-standard ASCII characters, which simply map the CEA608 byte 34402 // to the standard ASCII/Unicode. The two-byte keys that follow are the CEA608 34403 // character codes, but have their MSB bitmasked with 0x03 so that a lookup 34404 // can be performed regardless of the field and data channel on which the 34405 // character code was received. 34406 34407 34408 var CHARACTER_TRANSLATION = { 34409 0x2a: 0xe1, 34410 // á 34411 0x5c: 0xe9, 34412 // é 34413 0x5e: 0xed, 34414 // à 34415 0x5f: 0xf3, 34416 // ó 34417 0x60: 0xfa, 34418 // ú 34419 0x7b: 0xe7, 34420 // ç 34421 0x7c: 0xf7, 34422 // ÷ 34423 0x7d: 0xd1, 34424 // à 34425 0x7e: 0xf1, 34426 // ñ 34427 0x7f: 0x2588, 34428 // â 34429 0x0130: 0xae, 34430 // ® 34431 0x0131: 0xb0, 34432 // ° 34433 0x0132: 0xbd, 34434 // ½ 34435 0x0133: 0xbf, 34436 // ¿ 34437 0x0134: 0x2122, 34438 // ⢠34439 0x0135: 0xa2, 34440 // ¢ 34441 0x0136: 0xa3, 34442 // £ 34443 0x0137: 0x266a, 34444 // ⪠34445 0x0138: 0xe0, 34446 // à 34447 0x0139: 0xa0, 34448 // 34449 0x013a: 0xe8, 34450 // è 34451 0x013b: 0xe2, 34452 // â 34453 0x013c: 0xea, 34454 // ê 34455 0x013d: 0xee, 34456 // î 34457 0x013e: 0xf4, 34458 // ô 34459 0x013f: 0xfb, 34460 // û 34461 0x0220: 0xc1, 34462 // à 34463 0x0221: 0xc9, 34464 // à 34465 0x0222: 0xd3, 34466 // à 34467 0x0223: 0xda, 34468 // à 34469 0x0224: 0xdc, 34470 // à 34471 0x0225: 0xfc, 34472 // ü 34473 0x0226: 0x2018, 34474 // â 34475 0x0227: 0xa1, 34476 // ¡ 34477 0x0228: 0x2a, 34478 // * 34479 0x0229: 0x27, 34480 // ' 34481 0x022a: 0x2014, 34482 // â 34483 0x022b: 0xa9, 34484 // © 34485 0x022c: 0x2120, 34486 // â 34487 0x022d: 0x2022, 34488 // ⢠34489 0x022e: 0x201c, 34490 // â 34491 0x022f: 0x201d, 34492 // â 34493 0x0230: 0xc0, 34494 // à 34495 0x0231: 0xc2, 34496 // à 34497 0x0232: 0xc7, 34498 // à 34499 0x0233: 0xc8, 34500 // à 34501 0x0234: 0xca, 34502 // à 34503 0x0235: 0xcb, 34504 // à 34505 0x0236: 0xeb, 34506 // ë 34507 0x0237: 0xce, 34508 // à 34509 0x0238: 0xcf, 34510 // à 34511 0x0239: 0xef, 34512 // ï 34513 0x023a: 0xd4, 34514 // à 34515 0x023b: 0xd9, 34516 // à 34517 0x023c: 0xf9, 34518 // ù 34519 0x023d: 0xdb, 34520 // à 34521 0x023e: 0xab, 34522 // « 34523 0x023f: 0xbb, 34524 // » 34525 0x0320: 0xc3, 34526 // Ã
vendor: 18,966 bytes, lines 34527-35059
34527 0x0321: 0xe3, 34528 // ã 34529 0x0322: 0xcd, 34530 // à 34531 0x0323: 0xcc, 34532 // à 34533 0x0324: 0xec, 34534 // ì 34535 0x0325: 0xd2, 34536 // à 34537 0x0326: 0xf2, 34538 // ò 34539 0x0327: 0xd5, 34540 // à 34541 0x0328: 0xf5, 34542 // õ 34543 0x0329: 0x7b, 34544 // { 34545 0x032a: 0x7d, 34546 // } 34547 0x032b: 0x5c, 34548 // \ 34549 0x032c: 0x5e, 34550 // ^ 34551 0x032d: 0x5f, 34552 // _ 34553 0x032e: 0x7c, 34554 // | 34555 0x032f: 0x7e, 34556 // ~ 34557 0x0330: 0xc4, 34558 // à 34559 0x0331: 0xe4, 34560 // ä 34561 0x0332: 0xd6, 34562 // à 34563 0x0333: 0xf6, 34564 // ö 34565 0x0334: 0xdf, 34566 // à 34567 0x0335: 0xa5, 34568 // Â¥ 34569 0x0336: 0xa4, 34570 // ¤ 34571 0x0337: 0x2502, 34572 // â 34573 0x0338: 0xc5, 34574 // à 34575 0x0339: 0xe5, 34576 // Ã¥ 34577 0x033a: 0xd8, 34578 // à 34579 0x033b: 0xf8, 34580 // ø 34581 0x033c: 0x250c, 34582 // â 34583 0x033d: 0x2510, 34584 // â 34585 0x033e: 0x2514, 34586 // â 34587 0x033f: 0x2518 // â 34588 34589 }; 34590 34591 var getCharFromCode = function getCharFromCode(code) { 34592 if (code === null) { 34593 return ''; 34594 } 34595 34596 code = CHARACTER_TRANSLATION[code] || code; 34597 return String.fromCharCode(code); 34598 }; // the index of the last row in a CEA-608 display buffer 34599 34600 34601 var BOTTOM_ROW = 14; // This array is used for mapping PACs -> row #, since there's no way of 34602 // getting it through bit logic. 34603 34604 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 34605 // cells. The "bottom" row is the last element in the outer array. 34606 34607 var createDisplayBuffer = function createDisplayBuffer() { 34608 var result = [], 34609 i = BOTTOM_ROW + 1; 34610 34611 while (i--) { 34612 result.push(''); 34613 } 34614 34615 return result; 34616 }; 34617 34618 var Cea608Stream = function Cea608Stream(field, dataChannel) { 34619 Cea608Stream.prototype.init.call(this); 34620 this.field_ = field || 0; 34621 this.dataChannel_ = dataChannel || 0; 34622 this.name_ = 'CC' + ((this.field_ << 1 | this.dataChannel_) + 1); 34623 this.setConstants(); 34624 this.reset(); 34625 34626 this.push = function (packet) { 34627 var data, swap, char0, char1, text; // remove the parity bits 34628 34629 data = packet.ccData & 0x7f7f; // ignore duplicate control codes; the spec demands they're sent twice 34630 34631 if (data === this.lastControlCode_) { 34632 this.lastControlCode_ = null; 34633 return; 34634 } // Store control codes 34635 34636 34637 if ((data & 0xf000) === 0x1000) { 34638 this.lastControlCode_ = data; 34639 } else if (data !== this.PADDING_) { 34640 this.lastControlCode_ = null; 34641 } 34642 34643 char0 = data >>> 8; 34644 char1 = data & 0xff; 34645 34646 if (data === this.PADDING_) { 34647 return; 34648 } else if (data === this.RESUME_CAPTION_LOADING_) { 34649 this.mode_ = 'popOn'; 34650 } else if (data === this.END_OF_CAPTION_) { 34651 // If an EOC is received while in paint-on mode, the displayed caption 34652 // text should be swapped to non-displayed memory as if it was a pop-on 34653 // caption. Because of that, we should explicitly switch back to pop-on 34654 // mode 34655 this.mode_ = 'popOn'; 34656 this.clearFormatting(packet.pts); // if a caption was being displayed, it's gone now 34657 34658 this.flushDisplayed(packet.pts); // flip memory 34659 34660 swap = this.displayed_; 34661 this.displayed_ = this.nonDisplayed_; 34662 this.nonDisplayed_ = swap; // start measuring the time to display the caption 34663 34664 this.startPts_ = packet.pts; 34665 } else if (data === this.ROLL_UP_2_ROWS_) { 34666 this.rollUpRows_ = 2; 34667 this.setRollUp(packet.pts); 34668 } else if (data === this.ROLL_UP_3_ROWS_) { 34669 this.rollUpRows_ = 3; 34670 this.setRollUp(packet.pts); 34671 } else if (data === this.ROLL_UP_4_ROWS_) { 34672 this.rollUpRows_ = 4; 34673 this.setRollUp(packet.pts); 34674 } else if (data === this.CARRIAGE_RETURN_) { 34675 this.clearFormatting(packet.pts); 34676 this.flushDisplayed(packet.pts); 34677 this.shiftRowsUp_(); 34678 this.startPts_ = packet.pts; 34679 } else if (data === this.BACKSPACE_) { 34680 if (this.mode_ === 'popOn') { 34681 this.nonDisplayed_[this.row_] = this.nonDisplayed_[this.row_].slice(0, -1); 34682 } else { 34683 this.displayed_[this.row_] = this.displayed_[this.row_].slice(0, -1); 34684 } 34685 } else if (data === this.ERASE_DISPLAYED_MEMORY_) { 34686 this.flushDisplayed(packet.pts); 34687 this.displayed_ = createDisplayBuffer(); 34688 } else if (data === this.ERASE_NON_DISPLAYED_MEMORY_) { 34689 this.nonDisplayed_ = createDisplayBuffer(); 34690 } else if (data === this.RESUME_DIRECT_CAPTIONING_) { 34691 if (this.mode_ !== 'paintOn') { 34692 // NOTE: This should be removed when proper caption positioning is 34693 // implemented 34694 this.flushDisplayed(packet.pts); 34695 this.displayed_ = createDisplayBuffer(); 34696 } 34697 34698 this.mode_ = 'paintOn'; 34699 this.startPts_ = packet.pts; // Append special characters to caption text 34700 } else if (this.isSpecialCharacter(char0, char1)) { 34701 // Bitmask char0 so that we can apply character transformations 34702 // regardless of field and data channel. 34703 // Then byte-shift to the left and OR with char1 so we can pass the 34704 // entire character code to `getCharFromCode`. 34705 char0 = (char0 & 0x03) << 8; 34706 text = getCharFromCode(char0 | char1); 34707 this[this.mode_](packet.pts, text); 34708 this.column_++; // Append extended characters to caption text 34709 } else if (this.isExtCharacter(char0, char1)) { 34710 // Extended characters always follow their "non-extended" equivalents. 34711 // IE if a "è" is desired, you'll always receive "eè"; non-compliant 34712 // decoders are supposed to drop the "è", while compliant decoders 34713 // backspace the "e" and insert "è". 34714 // Delete the previous character 34715 if (this.mode_ === 'popOn') { 34716 this.nonDisplayed_[this.row_] = this.nonDisplayed_[this.row_].slice(0, -1); 34717 } else { 34718 this.displayed_[this.row_] = this.displayed_[this.row_].slice(0, -1); 34719 } // Bitmask char0 so that we can apply character transformations 34720 // regardless of field and data channel. 34721 // Then byte-shift to the left and OR with char1 so we can pass the 34722 // entire character code to `getCharFromCode`. 34723 34724 34725 char0 = (char0 & 0x03) << 8; 34726 text = getCharFromCode(char0 | char1); 34727 this[this.mode_](packet.pts, text); 34728 this.column_++; // Process mid-row codes 34729 } else if (this.isMidRowCode(char0, char1)) { 34730 // Attributes are not additive, so clear all formatting 34731 this.clearFormatting(packet.pts); // According to the standard, mid-row codes 34732 // should be replaced with spaces, so add one now 34733 34734 this[this.mode_](packet.pts, ' '); 34735 this.column_++; 34736 34737 if ((char1 & 0xe) === 0xe) { 34738 this.addFormatting(packet.pts, ['i']); 34739 } 34740 34741 if ((char1 & 0x1) === 0x1) { 34742 this.addFormatting(packet.pts, ['u']); 34743 } // Detect offset control codes and adjust cursor 34744 34745 } else if (this.isOffsetControlCode(char0, char1)) { 34746 // Cursor position is set by indent PAC (see below) in 4-column 34747 // increments, with an additional offset code of 1-3 to reach any 34748 // of the 32 columns specified by CEA-608. So all we need to do 34749 // here is increment the column cursor by the given offset. 34750 this.column_ += char1 & 0x03; // Detect PACs (Preamble Address Codes) 34751 } else if (this.isPAC(char0, char1)) { 34752 // There's no logic for PAC -> row mapping, so we have to just 34753 // find the row code in an array and use its index :( 34754 var row = ROWS.indexOf(data & 0x1f20); // Configure the caption window if we're in roll-up mode 34755 34756 if (this.mode_ === 'rollUp') { 34757 // This implies that the base row is incorrectly set. 34758 // As per the recommendation in CEA-608(Base Row Implementation), defer to the number 34759 // of roll-up rows set. 34760 if (row - this.rollUpRows_ + 1 < 0) { 34761 row = this.rollUpRows_ - 1; 34762 } 34763 34764 this.setRollUp(packet.pts, row); 34765 } 34766 34767 if (row !== this.row_) { 34768 // formatting is only persistent for current row 34769 this.clearFormatting(packet.pts); 34770 this.row_ = row; 34771 } // All PACs can apply underline, so detect and apply 34772 // (All odd-numbered second bytes set underline) 34773 34774 34775 if (char1 & 0x1 && this.formatting_.indexOf('u') === -1) { 34776 this.addFormatting(packet.pts, ['u']); 34777 } 34778 34779 if ((data & 0x10) === 0x10) { 34780 // We've got an indent level code. Each successive even number 34781 // increments the column cursor by 4, so we can get the desired 34782 // column position by bit-shifting to the right (to get n/2) 34783 // and multiplying by 4. 34784 this.column_ = ((data & 0xe) >> 1) * 4; 34785 } 34786 34787 if (this.isColorPAC(char1)) { 34788 // it's a color code, though we only support white, which 34789 // can be either normal or italicized. white italics can be 34790 // either 0x4e or 0x6e depending on the row, so we just 34791 // bitwise-and with 0xe to see if italics should be turned on 34792 if ((char1 & 0xe) === 0xe) { 34793 this.addFormatting(packet.pts, ['i']); 34794 } 34795 } // We have a normal character in char0, and possibly one in char1 34796 34797 } else if (this.isNormalChar(char0)) { 34798 if (char1 === 0x00) { 34799 char1 = null; 34800 } 34801 34802 text = getCharFromCode(char0); 34803 text += getCharFromCode(char1); 34804 this[this.mode_](packet.pts, text); 34805 this.column_ += text.length; 34806 } // finish data processing 34807 34808 }; 34809 }; 34810 34811 Cea608Stream.prototype = new stream(); // Trigger a cue point that captures the current state of the 34812 // display buffer 34813 34814 Cea608Stream.prototype.flushDisplayed = function (pts) { 34815 var content = this.displayed_ // remove spaces from the start and end of the string 34816 .map(function (row) { 34817 try { 34818 return row.trim(); 34819 } catch (e) { 34820 // Ordinarily, this shouldn't happen. However, caption 34821 // parsing errors should not throw exceptions and 34822 // break playback. 34823 // eslint-disable-next-line no-console 34824 console.error('Skipping malformed caption.'); 34825 return ''; 34826 } 34827 }) // combine all text rows to display in one cue 34828 .join('\n') // and remove blank rows from the start and end, but not the middle 34829 .replace(/^\n+|\n+$/g, ''); 34830 34831 if (content.length) { 34832 this.trigger('data', { 34833 startPts: this.startPts_, 34834 endPts: pts, 34835 text: content, 34836 stream: this.name_ 34837 }); 34838 } 34839 }; 34840 /** 34841 * Zero out the data, used for startup and on seek 34842 */ 34843 34844 34845 Cea608Stream.prototype.reset = function () { 34846 this.mode_ = 'popOn'; // When in roll-up mode, the index of the last row that will 34847 // actually display captions. If a caption is shifted to a row 34848 // with a lower index than this, it is cleared from the display 34849 // buffer 34850 34851 this.topRow_ = 0; 34852 this.startPts_ = 0; 34853 this.displayed_ = createDisplayBuffer(); 34854 this.nonDisplayed_ = createDisplayBuffer(); 34855 this.lastControlCode_ = null; // Track row and column for proper line-breaking and spacing 34856 34857 this.column_ = 0; 34858 this.row_ = BOTTOM_ROW; 34859 this.rollUpRows_ = 2; // This variable holds currently-applied formatting 34860 34861 this.formatting_ = []; 34862 }; 34863 /** 34864 * Sets up control code and related constants for this instance 34865 */ 34866 34867 34868 Cea608Stream.prototype.setConstants = function () { 34869 // The following attributes have these uses: 34870 // ext_ : char0 for mid-row codes, and the base for extended 34871 // chars (ext_+0, ext_+1, and ext_+2 are char0s for 34872 // extended codes) 34873 // control_: char0 for control codes, except byte-shifted to the 34874 // left so that we can do this.control_ | CONTROL_CODE 34875 // offset_: char0 for tab offset codes 34876 // 34877 // It's also worth noting that control codes, and _only_ control codes, 34878 // differ between field 1 and field2. Field 2 control codes are always 34879 // their field 1 value plus 1. That's why there's the "| field" on the 34880 // control value. 34881 if (this.dataChannel_ === 0) { 34882 this.BASE_ = 0x10; 34883 this.EXT_ = 0x11; 34884 this.CONTROL_ = (0x14 | this.field_) << 8; 34885 this.OFFSET_ = 0x17; 34886 } else if (this.dataChannel_ === 1) { 34887 this.BASE_ = 0x18; 34888 this.EXT_ = 0x19; 34889 this.CONTROL_ = (0x1c | this.field_) << 8; 34890 this.OFFSET_ = 0x1f; 34891 } // Constants for the LSByte command codes recognized by Cea608Stream. This 34892 // list is not exhaustive. For a more comprehensive listing and semantics see 34893 // http://www.gpo.gov/fdsys/pkg/CFR-2010-title47-vol1/pdf/CFR-2010-title47-vol1-sec15-119.pdf 34894 // Padding 34895 34896 34897 this.PADDING_ = 0x0000; // Pop-on Mode 34898 34899 this.RESUME_CAPTION_LOADING_ = this.CONTROL_ | 0x20; 34900 this.END_OF_CAPTION_ = this.CONTROL_ | 0x2f; // Roll-up Mode 34901 34902 this.ROLL_UP_2_ROWS_ = this.CONTROL_ | 0x25; 34903 this.ROLL_UP_3_ROWS_ = this.CONTROL_ | 0x26; 34904 this.ROLL_UP_4_ROWS_ = this.CONTROL_ | 0x27; 34905 this.CARRIAGE_RETURN_ = this.CONTROL_ | 0x2d; // paint-on mode 34906 34907 this.RESUME_DIRECT_CAPTIONING_ = this.CONTROL_ | 0x29; // Erasure 34908 34909 this.BACKSPACE_ = this.CONTROL_ | 0x21; 34910 this.ERASE_DISPLAYED_MEMORY_ = this.CONTROL_ | 0x2c; 34911 this.ERASE_NON_DISPLAYED_MEMORY_ = this.CONTROL_ | 0x2e; 34912 }; 34913 /** 34914 * Detects if the 2-byte packet data is a special character 34915 * 34916 * Special characters have a second byte in the range 0x30 to 0x3f, 34917 * with the first byte being 0x11 (for data channel 1) or 0x19 (for 34918 * data channel 2). 34919 * 34920 * @param {Integer} char0 The first byte 34921 * @param {Integer} char1 The second byte 34922 * @return {Boolean} Whether the 2 bytes are an special character 34923 */ 34924 34925 34926 Cea608Stream.prototype.isSpecialCharacter = function (char0, char1) { 34927 return char0 === this.EXT_ && char1 >= 0x30 && char1 <= 0x3f; 34928 }; 34929 /** 34930 * Detects if the 2-byte packet data is an extended character 34931 * 34932 * Extended characters have a second byte in the range 0x20 to 0x3f, 34933 * with the first byte being 0x12 or 0x13 (for data channel 1) or 34934 * 0x1a or 0x1b (for data channel 2). 34935 * 34936 * @param {Integer} char0 The first byte 34937 * @param {Integer} char1 The second byte 34938 * @return {Boolean} Whether the 2 bytes are an extended character 34939 */ 34940 34941 34942 Cea608Stream.prototype.isExtCharacter = function (char0, char1) { 34943 return (char0 === this.EXT_ + 1 || char0 === this.EXT_ + 2) && char1 >= 0x20 && char1 <= 0x3f; 34944 }; 34945 /** 34946 * Detects if the 2-byte packet is a mid-row code 34947 * 34948 * Mid-row codes have a second byte in the range 0x20 to 0x2f, with 34949 * the first byte being 0x11 (for data channel 1) or 0x19 (for data 34950 * channel 2). 34951 * 34952 * @param {Integer} char0 The first byte 34953 * @param {Integer} char1 The second byte 34954 * @return {Boolean} Whether the 2 bytes are a mid-row code 34955 */ 34956 34957 34958 Cea608Stream.prototype.isMidRowCode = function (char0, char1) { 34959 return char0 === this.EXT_ && char1 >= 0x20 && char1 <= 0x2f; 34960 }; 34961 /** 34962 * Detects if the 2-byte packet is an offset control code 34963 * 34964 * Offset control codes have a second byte in the range 0x21 to 0x23, 34965 * with the first byte being 0x17 (for data channel 1) or 0x1f (for 34966 * data channel 2). 34967 * 34968 * @param {Integer} char0 The first byte 34969 * @param {Integer} char1 The second byte 34970 * @return {Boolean} Whether the 2 bytes are an offset control code 34971 */ 34972 34973 34974 Cea608Stream.prototype.isOffsetControlCode = function (char0, char1) { 34975 return char0 === this.OFFSET_ && char1 >= 0x21 && char1 <= 0x23; 34976 }; 34977 /** 34978 * Detects if the 2-byte packet is a Preamble Address Code 34979 * 34980 * PACs have a first byte in the range 0x10 to 0x17 (for data channel 1) 34981 * or 0x18 to 0x1f (for data channel 2), with the second byte in the 34982 * range 0x40 to 0x7f. 34983 * 34984 * @param {Integer} char0 The first byte 34985 * @param {Integer} char1 The second byte 34986 * @return {Boolean} Whether the 2 bytes are a PAC 34987 */ 34988 34989 34990 Cea608Stream.prototype.isPAC = function (char0, char1) { 34991 return char0 >= this.BASE_ && char0 < this.BASE_ + 8 && char1 >= 0x40 && char1 <= 0x7f; 34992 }; 34993 /** 34994 * Detects if a packet's second byte is in the range of a PAC color code 34995 * 34996 * PAC color codes have the second byte be in the range 0x40 to 0x4f, or 34997 * 0x60 to 0x6f. 34998 * 34999 * @param {Integer} char1 The second byte 35000 * @return {Boolean} Whether the byte is a color PAC 35001 */ 35002 35003 35004 Cea608Stream.prototype.isColorPAC = function (char1) { 35005 return char1 >= 0x40 && char1 <= 0x4f || char1 >= 0x60 && char1 <= 0x7f; 35006 }; 35007 /** 35008 * Detects if a single byte is in the range of a normal character 35009 * 35010 * Normal text bytes are in the range 0x20 to 0x7f. 35011 * 35012 * @param {Integer} char The byte 35013 * @return {Boolean} Whether the byte is a normal character 35014 */ 35015 35016 35017 Cea608Stream.prototype.isNormalChar = function (_char) { 35018 return _char >= 0x20 && _char <= 0x7f; 35019 }; 35020 /** 35021 * Configures roll-up 35022 * 35023 * @param {Integer} pts Current PTS 35024 * @param {Integer} newBaseRow Used by PACs to slide the current window to 35025 * a new position 35026 */ 35027 35028 35029 Cea608Stream.prototype.setRollUp = function (pts, newBaseRow) { 35030 // Reset the base row to the bottom row when switching modes 35031 if (this.mode_ !== 'rollUp') { 35032 this.row_ = BOTTOM_ROW; 35033 this.mode_ = 'rollUp'; // Spec says to wipe memories when switching to roll-up 35034 35035 this.flushDisplayed(pts); 35036 this.nonDisplayed_ = createDisplayBuffer(); 35037 this.displayed_ = createDisplayBuffer(); 35038 } 35039 35040 if (newBaseRow !== undefined && newBaseRow !== this.row_) { 35041 // move currently displayed captions (up or down) to the new base row 35042 for (var i = 0; i < this.rollUpRows_; i++) { 35043 this.displayed_[newBaseRow - i] = this.displayed_[this.row_ - i]; 35044 this.displayed_[this.row_ - i] = ''; 35045 } 35046 } 35047 35048 if (newBaseRow === undefined) { 35049 newBaseRow = this.row_; 35050 } 35051 35052 this.topRow_ = newBaseRow - this.rollUpRows_ + 1; 35053 }; // Adds the opening HTML tag for the passed character to the caption text, 35054 // and keeps track of it for later closing 35055 35056 35057 Cea608Stream.prototype.addFormatting = function (pts, format) { 35058 this.formatting_ = this.formatting_.concat(format); 35059 var text = format.reduce(function (text, format) {
vendor: 2,998 bytes, lines 35060-35160
35060 return text + '<' + format + '>'; 35061 }, ''); 35062 this[this.mode_](pts, text); 35063 }; // Adds HTML closing tags for current formatting to caption text and 35064 // clears remembered formatting 35065 35066 35067 Cea608Stream.prototype.clearFormatting = function (pts) { 35068 if (!this.formatting_.length) { 35069 return; 35070 } 35071 35072 var text = this.formatting_.reverse().reduce(function (text, format) { 35073 return text + '</' + format + '>'; 35074 }, ''); 35075 this.formatting_ = []; 35076 this[this.mode_](pts, text); 35077 }; // Mode Implementations 35078 35079 35080 Cea608Stream.prototype.popOn = function (pts, text) { 35081 var baseRow = this.nonDisplayed_[this.row_]; // buffer characters 35082 35083 baseRow += text; 35084 this.nonDisplayed_[this.row_] = baseRow; 35085 }; 35086 35087 Cea608Stream.prototype.rollUp = function (pts, text) { 35088 var baseRow = this.displayed_[this.row_]; 35089 baseRow += text; 35090 this.displayed_[this.row_] = baseRow; 35091 }; 35092 35093 Cea608Stream.prototype.shiftRowsUp_ = function () { 35094 var i; // clear out inactive rows 35095 35096 for (i = 0; i < this.topRow_; i++) { 35097 this.displayed_[i] = ''; 35098 } 35099 35100 for (i = this.row_ + 1; i < BOTTOM_ROW + 1; i++) { 35101 this.displayed_[i] = ''; 35102 } // shift displayed rows up 35103 35104 35105 for (i = this.topRow_; i < this.row_; i++) { 35106 this.displayed_[i] = this.displayed_[i + 1]; 35107 } // clear out the bottom row 35108 35109 35110 this.displayed_[this.row_] = ''; 35111 }; 35112 35113 Cea608Stream.prototype.paintOn = function (pts, text) { 35114 var baseRow = this.displayed_[this.row_]; 35115 baseRow += text; 35116 this.displayed_[this.row_] = baseRow; 35117 }; // exports 35118 35119 35120 var captionStream = { 35121 CaptionStream: CaptionStream, 35122 Cea608Stream: Cea608Stream 35123 }; 35124 35125 var discardEmulationPreventionBytes$1 = captionPacketParser.discardEmulationPreventionBytes; 35126 var CaptionStream$1 = captionStream.CaptionStream; 35127 /** 35128 * Maps an offset in the mdat to a sample based on the the size of the samples. 35129 * Assumes that `parseSamples` has been called first. 35130 * 35131 * @param {Number} offset - The offset into the mdat 35132 * @param {Object[]} samples - An array of samples, parsed using `parseSamples` 35133 * @return {?Object} The matching sample, or null if no match was found. 35134 * 35135 * @see ISO-BMFF-12/2015, Section 8.8.8 35136 **/ 35137 35138 var mapToSample = function mapToSample(offset, samples) { 35139 var approximateOffset = offset; 35140 35141 for (var i = 0; i < samples.length; i++) { 35142 var sample = samples[i]; 35143 35144 if (approximateOffset < sample.size) { 35145 return sample; 35146 } 35147 35148 approximateOffset -= sample.size; 35149 } 35150 35151 return null; 35152 }; 35153 /** 35154 * Finds SEI nal units contained in a Media Data Box. 35155 * Assumes that `parseSamples` has been called first. 35156 * 35157 * @param {Uint8Array} avcStream - The bytes of the mdat 35158 * @param {Object[]} samples - The samples parsed out by `parseSamples` 35159 * @param {Number} trackId - The trackId of this video track 35160 * @return {Object[]}
vendor: 4,298 bytes, lines 35160-35285
35160 seiNals - the parsed SEI NALUs found. 35161 * The contents of the seiNal should match what is expected by 35162 * CaptionStream.push (nalUnitType, size, data, escapedRBSP, pts, dts) 35163 * 35164 * @see ISO-BMFF-12/2015, Section 8.1.1 35165 * @see Rec. ITU-T H.264, 7.3.2.3.1 35166 **/ 35167 35168 35169 var findSeiNals = function findSeiNals(avcStream, samples, trackId) { 35170 var avcView = new DataView(avcStream.buffer, avcStream.byteOffset, avcStream.byteLength), 35171 result = [], 35172 seiNal, 35173 i, 35174 length, 35175 lastMatchedSample; 35176 35177 for (i = 0; i + 4 < avcStream.length; i += length) { 35178 length = avcView.getUint32(i); 35179 i += 4; // Bail if this doesn't appear to be an H264 stream 35180 35181 if (length <= 0) { 35182 continue; 35183 } 35184 35185 switch (avcStream[i] & 0x1F) { 35186 case 0x06: 35187 var data = avcStream.subarray(i + 1, i + 1 + length); 35188 var matchingSample = mapToSample(i, samples); 35189 seiNal = { 35190 nalUnitType: 'sei_rbsp', 35191 size: length, 35192 data: data, 35193 escapedRBSP: discardEmulationPreventionBytes$1(data), 35194 trackId: trackId 35195 }; 35196 35197 if (matchingSample) { 35198 seiNal.pts = matchingSample.pts; 35199 seiNal.dts = matchingSample.dts; 35200 lastMatchedSample = matchingSample; 35201 } else { 35202 // If a matching sample cannot be found, use the last 35203 // sample's values as they should be as close as possible 35204 seiNal.pts = lastMatchedSample.pts; 35205 seiNal.dts = lastMatchedSample.dts; 35206 } 35207 35208 result.push(seiNal); 35209 break; 35210 35211 default: 35212 break; 35213 } 35214 } 35215 35216 return result; 35217 }; 35218 /** 35219 * Parses sample information out of Track Run Boxes and calculates 35220 * the absolute presentation and decode timestamps of each sample. 35221 * 35222 * @param {Array<Uint8Array>} truns - The Trun Run boxes to be parsed 35223 * @param {Number} baseMediaDecodeTime - base media decode time from tfdt 35224 @see ISO-BMFF-12/2015, Section 8.8.12 35225 * @param {Object} tfhd - The parsed Track Fragment Header 35226 * @see inspect.parseTfhd 35227 * @return {Object[]} the parsed samples 35228 * 35229 * @see ISO-BMFF-12/2015, Section 8.8.8 35230 **/ 35231 35232 35233 var parseSamples = function parseSamples(truns, baseMediaDecodeTime, tfhd) { 35234 var currentDts = baseMediaDecodeTime; 35235 var defaultSampleDuration = tfhd.defaultSampleDuration || 0; 35236 var defaultSampleSize = tfhd.defaultSampleSize || 0; 35237 var trackId = tfhd.trackId; 35238 var allSamples = []; 35239 truns.forEach(function (trun) { 35240 // Note: We currently do not parse the sample table as well 35241 // as the trun. It's possible some sources will require this. 35242 // moov > trak > mdia > minf > stbl 35243 var trackRun = mp4Inspector.parseTrun(trun); 35244 var samples = trackRun.samples; 35245 samples.forEach(function (sample) { 35246 if (sample.duration === undefined) { 35247 sample.duration = defaultSampleDuration; 35248 } 35249 35250 if (sample.size === undefined) { 35251 sample.size = defaultSampleSize; 35252 } 35253 35254 sample.trackId = trackId; 35255 sample.dts = currentDts; 35256 35257 if (sample.compositionTimeOffset === undefined) { 35258 sample.compositionTimeOffset = 0; 35259 } 35260 35261 sample.pts = currentDts + sample.compositionTimeOffset; 35262 currentDts += sample.duration; 35263 }); 35264 allSamples = allSamples.concat(samples); 35265 }); 35266 return allSamples; 35267 }; 35268 /** 35269 * Parses out caption nals from an FMP4 segment's video tracks. 35270 * 35271 * @param {Uint8Array} segment - The bytes of a single segment 35272 * @param {Number} videoTrackId - The trackId of a video track in the segment 35273 * @return {Object.<Number, Object[]>} A mapping of video trackId to 35274 * a list of seiNals found in that track 35275 **/ 35276 35277 35278 var parseCaptionNals = function parseCaptionNals(segment, videoTrackId) { 35279 // To get the samples 35280 var trafs = probe.findBox(segment, ['moof', 'traf']); // To get SEI NAL units 35281 35282 var mdats = probe.findBox(segment, ['mdat']); 35283 var captionNals = {}; 35284 var mdatTrafPairs = []; // Pair up each traf with a mdat as moofs and mdats are in pairs 35285
vendor: 3,771 bytes, lines 35286-35388
35286 mdats.forEach(function (mdat, index) { 35287 var matchingTraf = trafs[index]; 35288 mdatTrafPairs.push({ 35289 mdat: mdat, 35290 traf: matchingTraf 35291 }); 35292 }); 35293 mdatTrafPairs.forEach(function (pair) { 35294 var mdat = pair.mdat; 35295 var traf = pair.traf; 35296 var tfhd = probe.findBox(traf, ['tfhd']); // Exactly 1 tfhd per traf 35297 35298 var headerInfo = mp4Inspector.parseTfhd(tfhd[0]); 35299 var trackId = headerInfo.trackId; 35300 var tfdt = probe.findBox(traf, ['tfdt']); // Either 0 or 1 tfdt per traf 35301 35302 var baseMediaDecodeTime = tfdt.length > 0 ? mp4Inspector.parseTfdt(tfdt[0]).baseMediaDecodeTime : 0; 35303 var truns = probe.findBox(traf, ['trun']); 35304 var samples; 35305 var seiNals; // Only parse video data for the chosen video track 35306 35307 if (videoTrackId === trackId && truns.length > 0) { 35308 samples = parseSamples(truns, baseMediaDecodeTime, headerInfo); 35309 seiNals = findSeiNals(mdat, samples, trackId); 35310 35311 if (!captionNals[trackId]) { 35312 captionNals[trackId] = []; 35313 } 35314 35315 captionNals[trackId] = captionNals[trackId].concat(seiNals); 35316 } 35317 }); 35318 return captionNals; 35319 }; 35320 /** 35321 * Parses out inband captions from an MP4 container and returns 35322 * caption objects that can be used by WebVTT and the TextTrack API. 35323 * @see https://developer.mozilla.org/en-US/docs/Web/API/VTTCue 35324 * @see https://developer.mozilla.org/en-US/docs/Web/API/TextTrack 35325 * Assumes that `probe.getVideoTrackIds` and `probe.timescale` have been called first 35326 * 35327 * @param {Uint8Array} segment - The fmp4 segment containing embedded captions 35328 * @param {Number} trackId - The id of the video track to parse 35329 * @param {Number} timescale - The timescale for the video track from the init segment 35330 * 35331 * @return {?Object[]} parsedCaptions - A list of captions or null if no video tracks 35332 * @return {Number} parsedCaptions[].startTime - The time to show the caption in seconds 35333 * @return {Number} parsedCaptions[].endTime - The time to stop showing the caption in seconds 35334 * @return {String} parsedCaptions[].text - The visible content of the caption 35335 **/ 35336 35337 35338 var parseEmbeddedCaptions = function parseEmbeddedCaptions(segment, trackId, timescale) { 35339 var seiNals; // the ISO-BMFF spec says that trackId can't be zero, but there's some broken content out there 35340 35341 if (trackId === null) { 35342 return null; 35343 } 35344 35345 seiNals = parseCaptionNals(segment, trackId); 35346 return { 35347 seiNals: seiNals[trackId], 35348 timescale: timescale 35349 }; 35350 }; 35351 /** 35352 * Converts SEI NALUs into captions that can be used by video.js 35353 **/ 35354 35355 35356 var CaptionParser = function CaptionParser() { 35357 var isInitialized = false; 35358 var captionStream; // Stores segments seen before trackId and timescale are set 35359 35360 var segmentCache; // Stores video track ID of the track being parsed 35361 35362 var trackId; // Stores the timescale of the track being parsed 35363 35364 var timescale; // Stores captions parsed so far 35365 35366 var parsedCaptions; // Stores whether we are receiving partial data or not 35367 35368 var parsingPartial; 35369 /** 35370 * A method to indicate whether a CaptionParser has been initalized 35371 * @returns {Boolean} 35372 **/ 35373 35374 this.isInitialized = function () { 35375 return isInitialized; 35376 }; 35377 /** 35378 * Initializes the underlying CaptionStream, SEI NAL parsing 35379 * and management, and caption collection 35380 **/ 35381 35382 35383 this.init = function (options) { 35384 captionStream = new CaptionStream$1(); 35385 isInitialized = true; 35386 parsingPartial = options ? options.isPartial : false; // Collect dispatched captions 35387 35388 captionStream.on('data', function (event) {
vendor: 4,550 bytes, lines 35389-35538
35389 // Convert to seconds in the source's timescale 35390 event.startTime = event.startPts / timescale; 35391 event.endTime = event.endPts / timescale; 35392 parsedCaptions.captions.push(event); 35393 parsedCaptions.captionStreams[event.stream] = true; 35394 }); 35395 }; 35396 /** 35397 * Determines if a new video track will be selected 35398 * or if the timescale changed 35399 * @return {Boolean} 35400 **/ 35401 35402 35403 this.isNewInit = function (videoTrackIds, timescales) { 35404 if (videoTrackIds && videoTrackIds.length === 0 || timescales && typeof timescales === 'object' && Object.keys(timescales).length === 0) { 35405 return false; 35406 } 35407 35408 return trackId !== videoTrackIds[0] || timescale !== timescales[trackId]; 35409 }; 35410 /** 35411 * Parses out SEI captions and interacts with underlying 35412 * CaptionStream to return dispatched captions 35413 * 35414 * @param {Uint8Array} segment - The fmp4 segment containing embedded captions 35415 * @param {Number[]} videoTrackIds - A list of video tracks found in the init segment 35416 * @param {Object.<Number, Number>} timescales - The timescales found in the init segment 35417 * @see parseEmbeddedCaptions 35418 * @see m2ts/caption-stream.js 35419 **/ 35420 35421 35422 this.parse = function (segment, videoTrackIds, timescales) { 35423 var parsedData; 35424 35425 if (!this.isInitialized()) { 35426 return null; // This is not likely to be a video segment 35427 } else if (!videoTrackIds || !timescales) { 35428 return null; 35429 } else if (this.isNewInit(videoTrackIds, timescales)) { 35430 // Use the first video track only as there is no 35431 // mechanism to switch to other video tracks 35432 trackId = videoTrackIds[0]; 35433 timescale = timescales[trackId]; // If an init segment has not been seen yet, hold onto segment 35434 // data until we have one. 35435 // the ISO-BMFF spec says that trackId can't be zero, but there's some broken content out there 35436 } else if (trackId === null || !timescale) { 35437 segmentCache.push(segment); 35438 return null; 35439 } // Now that a timescale and trackId is set, parse cached segments 35440 35441 35442 while (segmentCache.length > 0) { 35443 var cachedSegment = segmentCache.shift(); 35444 this.parse(cachedSegment, videoTrackIds, timescales); 35445 } 35446 35447 parsedData = parseEmbeddedCaptions(segment, trackId, timescale); 35448 35449 if (parsedData === null || !parsedData.seiNals) { 35450 return null; 35451 } 35452 35453 this.pushNals(parsedData.seiNals); // Force the parsed captions to be dispatched 35454 35455 this.flushStream(); 35456 return parsedCaptions; 35457 }; 35458 /** 35459 * Pushes SEI NALUs onto CaptionStream 35460 * @param {Object[]} nals - A list of SEI nals parsed using `parseCaptionNals` 35461 * Assumes that `parseCaptionNals` has been called first 35462 * @see m2ts/caption-stream.js 35463 **/ 35464 35465 35466 this.pushNals = function (nals) { 35467 if (!this.isInitialized() || !nals || nals.length === 0) { 35468 return null; 35469 } 35470 35471 nals.forEach(function (nal) { 35472 captionStream.push(nal); 35473 }); 35474 }; 35475 /** 35476 * Flushes underlying CaptionStream to dispatch processed, displayable captions 35477 * @see m2ts/caption-stream.js 35478 **/ 35479 35480 35481 this.flushStream = function () { 35482 if (!this.isInitialized()) { 35483 return null; 35484 } 35485 35486 if (!parsingPartial) { 35487 captionStream.flush(); 35488 } else { 35489 captionStream.partialFlush(); 35490 } 35491 }; 35492 /** 35493 * Reset caption buckets for new data 35494 **/ 35495 35496 35497 this.clearParsedCaptions = function () { 35498 parsedCaptions.captions = []; 35499 parsedCaptions.captionStreams = {}; 35500 }; 35501 /** 35502 * Resets underlying CaptionStream 35503 * @see m2ts/caption-stream.js 35504 **/ 35505 35506 35507 this.resetCaptionStream = function () { 35508 if (!this.isInitialized()) { 35509 return null; 35510 } 35511 35512 captionStream.reset(); 35513 }; 35514 /** 35515 * Convenience method to clear all captions flushed from the 35516 * CaptionStream and still being parsed 35517 * @see m2ts/caption-stream.js 35518 **/ 35519 35520 35521 this.clearAllCaptions = function () { 35522 this.clearParsedCaptions(); 35523 this.resetCaptionStream(); 35524 }; 35525 /** 35526 * Reset caption parser 35527 **/ 35528 35529 35530 this.reset = function () { 35531 segmentCache = []; 35532 trackId = null; 35533 timescale = null; 35534 35535 if (!parsedCaptions) { 35536 parsedCaptions = { 35537 captions: [], 35538 // CC1, CC2, CC3, CC4
vendor: 7,393 bytes, lines 35539-35772
35539 captionStreams: {} 35540 }; 35541 } else { 35542 this.clearParsedCaptions(); 35543 } 35544 35545 this.resetCaptionStream(); 35546 }; 35547 35548 this.reset(); 35549 }; 35550 35551 var captionParser = CaptionParser; 35552 35553 /** 35554 * mux.js 35555 * 35556 * Copyright (c) Brightcove 35557 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 35558 */ 35559 35560 var streamTypes = { 35561 H264_STREAM_TYPE: 0x1B, 35562 ADTS_STREAM_TYPE: 0x0F, 35563 METADATA_STREAM_TYPE: 0x15 35564 }; 35565 35566 var MAX_TS = 8589934592; 35567 var RO_THRESH = 4294967296; 35568 var TYPE_SHARED = 'shared'; 35569 35570 var handleRollover = function handleRollover(value, reference) { 35571 var direction = 1; 35572 35573 if (value > reference) { 35574 // If the current timestamp value is greater than our reference timestamp and we detect a 35575 // timestamp rollover, this means the roll over is happening in the opposite direction. 35576 // Example scenario: Enter a long stream/video just after a rollover occurred. The reference 35577 // point will be set to a small number, e.g. 1. The user then seeks backwards over the 35578 // rollover point. In loading this segment, the timestamp values will be very large, 35579 // e.g. 2^33 - 1. Since this comes before the data we loaded previously, we want to adjust 35580 // the time stamp to be `value - 2^33`. 35581 direction = -1; 35582 } // Note: A seek forwards or back that is greater than the RO_THRESH (2^32, ~13 hours) will 35583 // cause an incorrect adjustment. 35584 35585 35586 while (Math.abs(reference - value) > RO_THRESH) { 35587 value += direction * MAX_TS; 35588 } 35589 35590 return value; 35591 }; 35592 35593 var TimestampRolloverStream = function TimestampRolloverStream(type) { 35594 var lastDTS, referenceDTS; 35595 TimestampRolloverStream.prototype.init.call(this); // The "shared" type is used in cases where a stream will contain muxed 35596 // video and audio. We could use `undefined` here, but having a string 35597 // makes debugging a little clearer. 35598 35599 this.type_ = type || TYPE_SHARED; 35600 35601 this.push = function (data) { 35602 // Any "shared" rollover streams will accept _all_ data. Otherwise, 35603 // streams will only accept data that matches their type. 35604 if (this.type_ !== TYPE_SHARED && data.type !== this.type_) { 35605 return; 35606 } 35607 35608 if (referenceDTS === undefined) { 35609 referenceDTS = data.dts; 35610 } 35611 35612 data.dts = handleRollover(data.dts, referenceDTS); 35613 data.pts = handleRollover(data.pts, referenceDTS); 35614 lastDTS = data.dts; 35615 this.trigger('data', data); 35616 }; 35617 35618 this.flush = function () { 35619 referenceDTS = lastDTS; 35620 this.trigger('done'); 35621 }; 35622 35623 this.endTimeline = function () { 35624 this.flush(); 35625 this.trigger('endedtimeline'); 35626 }; 35627 35628 this.discontinuity = function () { 35629 referenceDTS = void 0; 35630 lastDTS = void 0; 35631 }; 35632 35633 this.reset = function () { 35634 this.discontinuity(); 35635 this.trigger('reset'); 35636 }; 35637 }; 35638 35639 TimestampRolloverStream.prototype = new stream(); 35640 var timestampRolloverStream = { 35641 TimestampRolloverStream: TimestampRolloverStream, 35642 handleRollover: handleRollover 35643 }; 35644 35645 var parsePid = function parsePid(packet) { 35646 var pid = packet[1] & 0x1f; 35647 pid <<= 8; 35648 pid |= packet[2]; 35649 return pid; 35650 }; 35651 35652 var parsePayloadUnitStartIndicator = function parsePayloadUnitStartIndicator(packet) { 35653 return !!(packet[1] & 0x40); 35654 }; 35655 35656 var parseAdaptionField = function parseAdaptionField(packet) { 35657 var offset = 0; // if an adaption field is present, its length is specified by the 35658 // fifth byte of the TS packet header. The adaptation field is 35659 // used to add stuffing to PES packets that don't fill a complete 35660 // TS packet, and to specify some forms of timing and control data 35661 // that we do not currently use. 35662 35663 if ((packet[3] & 0x30) >>> 4 > 0x01) { 35664 offset += packet[4] + 1; 35665 } 35666 35667 return offset; 35668 }; 35669 35670 var parseType$1 = function parseType(packet, pmtPid) { 35671 var pid = parsePid(packet); 35672 35673 if (pid === 0) { 35674 return 'pat'; 35675 } else if (pid === pmtPid) { 35676 return 'pmt'; 35677 } else if (pmtPid) { 35678 return 'pes'; 35679 } 35680 35681 return null; 35682 }; 35683 35684 var parsePat = function parsePat(packet) { 35685 var pusi = parsePayloadUnitStartIndicator(packet); 35686 var offset = 4 + parseAdaptionField(packet); 35687 35688 if (pusi) { 35689 offset += packet[offset] + 1; 35690 } 35691 35692 return (packet[offset + 10] & 0x1f) << 8 | packet[offset + 11]; 35693 }; 35694 35695 var parsePmt = function parsePmt(packet) { 35696 var programMapTable = {}; 35697 var pusi = parsePayloadUnitStartIndicator(packet); 35698 var payloadOffset = 4 + parseAdaptionField(packet); 35699 35700 if (pusi) { 35701 payloadOffset += packet[payloadOffset] + 1; 35702 } // PMTs can be sent ahead of the time when they should actually 35703 // take effect. We don't believe this should ever be the case 35704 // for HLS but we'll ignore "forward" PMT declarations if we see 35705 // them. Future PMT declarations have the current_next_indicator 35706 // set to zero. 35707 35708 35709 if (!(packet[payloadOffset + 5] & 0x01)) { 35710 return; 35711 } 35712 35713 var sectionLength, tableEnd, programInfoLength; // the mapping table ends at the end of the current section 35714 35715 sectionLength = (packet[payloadOffset + 1] & 0x0f) << 8 | packet[payloadOffset + 2]; 35716 tableEnd = 3 + sectionLength - 4; // to determine where the table is, we have to figure out how 35717 // long the program info descriptors are 35718 35719 programInfoLength = (packet[payloadOffset + 10] & 0x0f) << 8 | packet[payloadOffset + 11]; // advance the offset to the first entry in the mapping table 35720 35721 var offset = 12 + programInfoLength; 35722 35723 while (offset < tableEnd) { 35724 var i = payloadOffset + offset; // add an entry that maps the elementary_pid to the stream_type 35725 35726 programMapTable[(packet[i + 1] & 0x1F) << 8 | packet[i + 2]] = packet[i]; // move to the next table entry 35727 // skip past the elementary stream descriptors, if present 35728 35729 offset += ((packet[i + 3] & 0x0F) << 8 | packet[i + 4]) + 5; 35730 } 35731 35732 return programMapTable; 35733 }; 35734 35735 var parsePesType = function parsePesType(packet, programMapTable) { 35736 var pid = parsePid(packet); 35737 var type = programMapTable[pid]; 35738 35739 switch (type) { 35740 case streamTypes.H264_STREAM_TYPE: 35741 return 'video'; 35742 35743 case streamTypes.ADTS_STREAM_TYPE: 35744 return 'audio'; 35745 35746 case streamTypes.METADATA_STREAM_TYPE: 35747 return 'timed-metadata'; 35748 35749 default: 35750 return null; 35751 } 35752 }; 35753 35754 var parsePesTime = function parsePesTime(packet) { 35755 var pusi = parsePayloadUnitStartIndicator(packet); 35756 35757 if (!pusi) { 35758 return null; 35759 } 35760 35761 var offset = 4 + parseAdaptionField(packet); 35762 35763 if (offset >= packet.byteLength) { 35764 // From the H 222.0 MPEG-TS spec 35765 // "For transport stream packets carrying PES packets, stuffing is needed when there 35766 // is insufficient PES packet data to completely fill the transport stream packet 35767 // payload bytes. Stuffing is accomplished by defining an adaptation field longer than 35768 // the sum of the lengths of the data elements in it, so that the payload bytes 35769 // remaining after the adaptation field exactly accommodates the available PES packet 35770 // data." 35771 // 35772 // If the offset is >
vendor: 5,828 bytes, lines 35772-35949
35772= the length of the packet, then the packet contains no data 35773 // and instead is just adaption field stuffing bytes 35774 return null; 35775 } 35776 35777 var pes = null; 35778 var ptsDtsFlags; // PES packets may be annotated with a PTS value, or a PTS value 35779 // and a DTS value. Determine what combination of values is 35780 // available to work with. 35781 35782 ptsDtsFlags = packet[offset + 7]; // PTS and DTS are normally stored as a 33-bit number. Javascript 35783 // performs all bitwise operations on 32-bit integers but javascript 35784 // supports a much greater range (52-bits) of integer using standard 35785 // mathematical operations. 35786 // We construct a 31-bit value using bitwise operators over the 31 35787 // most significant bits and then multiply by 4 (equal to a left-shift 35788 // of 2) before we add the final 2 least significant bits of the 35789 // timestamp (equal to an OR.) 35790 35791 if (ptsDtsFlags & 0xC0) { 35792 pes = {}; // the PTS and DTS are not written out directly. For information 35793 // on how they are encoded, see 35794 // http://dvd.sourceforge.net/dvdinfo/pes-hdr.html 35795 35796 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; 35797 pes.pts *= 4; // Left shift by 2 35798 35799 pes.pts += (packet[offset + 13] & 0x06) >>> 1; // OR by the two LSBs 35800 35801 pes.dts = pes.pts; 35802 35803 if (ptsDtsFlags & 0x40) { 35804 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; 35805 pes.dts *= 4; // Left shift by 2 35806 35807 pes.dts += (packet[offset + 18] & 0x06) >>> 1; // OR by the two LSBs 35808 } 35809 } 35810 35811 return pes; 35812 }; 35813 35814 var parseNalUnitType = function parseNalUnitType(type) { 35815 switch (type) { 35816 case 0x05: 35817 return 'slice_layer_without_partitioning_rbsp_idr'; 35818 35819 case 0x06: 35820 return 'sei_rbsp'; 35821 35822 case 0x07: 35823 return 'seq_parameter_set_rbsp'; 35824 35825 case 0x08: 35826 return 'pic_parameter_set_rbsp'; 35827 35828 case 0x09: 35829 return 'access_unit_delimiter_rbsp'; 35830 35831 default: 35832 return null; 35833 } 35834 }; 35835 35836 var videoPacketContainsKeyFrame = function videoPacketContainsKeyFrame(packet) { 35837 var offset = 4 + parseAdaptionField(packet); 35838 var frameBuffer = packet.subarray(offset); 35839 var frameI = 0; 35840 var frameSyncPoint = 0; 35841 var foundKeyFrame = false; 35842 var nalType; // advance the sync point to a NAL start, if necessary 35843 35844 for (; frameSyncPoint < frameBuffer.byteLength - 3; frameSyncPoint++) { 35845 if (frameBuffer[frameSyncPoint + 2] === 1) { 35846 // the sync point is properly aligned 35847 frameI = frameSyncPoint + 5; 35848 break; 35849 } 35850 } 35851 35852 while (frameI < frameBuffer.byteLength) { 35853 // look at the current byte to determine if we've hit the end of 35854 // a NAL unit boundary 35855 switch (frameBuffer[frameI]) { 35856 case 0: 35857 // skip past non-sync sequences 35858 if (frameBuffer[frameI - 1] !== 0) { 35859 frameI += 2; 35860 break; 35861 } else if (frameBuffer[frameI - 2] !== 0) { 35862 frameI++; 35863 break; 35864 } 35865 35866 if (frameSyncPoint + 3 !== frameI - 2) { 35867 nalType = parseNalUnitType(frameBuffer[frameSyncPoint + 3] & 0x1f); 35868 35869 if (nalType === 'slice_layer_without_partitioning_rbsp_idr') { 35870 foundKeyFrame = true; 35871 } 35872 } // drop trailing zeroes 35873 35874 35875 do { 35876 frameI++; 35877 } while (frameBuffer[frameI] !== 1 && frameI < frameBuffer.length); 35878 35879 frameSyncPoint = frameI - 2; 35880 frameI += 3; 35881 break; 35882 35883 case 1: 35884 // skip past non-sync sequences 35885 if (frameBuffer[frameI - 1] !== 0 || frameBuffer[frameI - 2] !== 0) { 35886 frameI += 3; 35887 break; 35888 } 35889 35890 nalType = parseNalUnitType(frameBuffer[frameSyncPoint + 3] & 0x1f); 35891 35892 if (nalType === 'slice_layer_without_partitioning_rbsp_idr') { 35893 foundKeyFrame = true; 35894 } 35895 35896 frameSyncPoint = frameI - 2; 35897 frameI += 3; 35898 break; 35899 35900 default: 35901 // the current byte isn't a one or zero, so it cannot be part 35902 // of a sync sequence 35903 frameI += 3; 35904 break; 35905 } 35906 } 35907 35908 frameBuffer = frameBuffer.subarray(frameSyncPoint); 35909 frameI -= frameSyncPoint; 35910 frameSyncPoint = 0; // parse the final nal 35911 35912 if (frameBuffer && frameBuffer.byteLength > 3) { 35913 nalType = parseNalUnitType(frameBuffer[frameSyncPoint + 3] & 0x1f); 35914 35915 if (nalType === 'slice_layer_without_partitioning_rbsp_idr') { 35916 foundKeyFrame = true; 35917 } 35918 } 35919 35920 return foundKeyFrame; 35921 }; 35922 35923 var probe$1 = { 35924 parseType: parseType$1, 35925 parsePat: parsePat, 35926 parsePmt: parsePmt, 35927 parsePayloadUnitStartIndicator: parsePayloadUnitStartIndicator, 35928 parsePesType: parsePesType, 35929 parsePesTime: parsePesTime, 35930 videoPacketContainsKeyFrame: videoPacketContainsKeyFrame 35931 }; 35932 35933 /** 35934 * mux.js 35935 * 35936 * Copyright (c) Brightcove 35937 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 35938 * 35939 * Utilities to detect basic properties and metadata about Aac data. 35940 */ 35941 35942 var ADTS_SAMPLING_FREQUENCIES = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350]; 35943 35944 var isLikelyAacData = function isLikelyAacData(data) { 35945 if (data[0] === 'I'.charCodeAt(0) && data[1] === 'D'.charCodeAt(0) && data[2] === '3'.charCodeAt(0)) { 35946 return true; 35947 } 35948 35949 return false;
vendor: 5,626 bytes, lines 35950-36127
35950 }; 35951 35952 var parseSyncSafeInteger = function parseSyncSafeInteger(data) { 35953 return data[0] << 21 | data[1] << 14 | data[2] << 7 | data[3]; 35954 }; // return a percent-encoded representation of the specified byte range 35955 // @see http://en.wikipedia.org/wiki/Percent-encoding 35956 35957 35958 var percentEncode = function percentEncode(bytes, start, end) { 35959 var i, 35960 result = ''; 35961 35962 for (i = start; i < end; i++) { 35963 result += '%' + ('00' + bytes[i].toString(16)).slice(-2); 35964 } 35965 35966 return result; 35967 }; // return the string representation of the specified byte range, 35968 // interpreted as ISO-8859-1. 35969 35970 35971 var parseIso88591 = function parseIso88591(bytes, start, end) { 35972 return unescape(percentEncode(bytes, start, end)); // jshint ignore:line 35973 }; 35974 35975 var parseId3TagSize = function parseId3TagSize(header, byteIndex) { 35976 var returnSize = header[byteIndex + 6] << 21 | header[byteIndex + 7] << 14 | header[byteIndex + 8] << 7 | header[byteIndex + 9], 35977 flags = header[byteIndex + 5], 35978 footerPresent = (flags & 16) >> 4; 35979 35980 if (footerPresent) { 35981 return returnSize + 20; 35982 } 35983 35984 return returnSize + 10; 35985 }; 35986 35987 var parseAdtsSize = function parseAdtsSize(header, byteIndex) { 35988 var lowThree = (header[byteIndex + 5] & 0xE0) >> 5, 35989 middle = header[byteIndex + 4] << 3, 35990 highTwo = header[byteIndex + 3] & 0x3 << 11; 35991 return highTwo | middle | lowThree; 35992 }; 35993 35994 var parseType$2 = function parseType(header, byteIndex) { 35995 if (header[byteIndex] === 'I'.charCodeAt(0) && header[byteIndex + 1] === 'D'.charCodeAt(0) && header[byteIndex + 2] === '3'.charCodeAt(0)) { 35996 return 'timed-metadata'; 35997 } else if (header[byteIndex] & 0xff === 0xff && (header[byteIndex + 1] & 0xf0) === 0xf0) { 35998 return 'audio'; 35999 } 36000 36001 return null; 36002 }; 36003 36004 var parseSampleRate = function parseSampleRate(packet) { 36005 var i = 0; 36006 36007 while (i + 5 < packet.length) { 36008 if (packet[i] !== 0xFF || (packet[i + 1] & 0xF6) !== 0xF0) { 36009 // If a valid header was not found, jump one forward and attempt to 36010 // find a valid ADTS header starting at the next byte 36011 i++; 36012 continue; 36013 } 36014 36015 return ADTS_SAMPLING_FREQUENCIES[(packet[i + 2] & 0x3c) >>> 2]; 36016 } 36017 36018 return null; 36019 }; 36020 36021 var parseAacTimestamp = function parseAacTimestamp(packet) { 36022 var frameStart, frameSize, frame, frameHeader; // find the start of the first frame and the end of the tag 36023 36024 frameStart = 10; 36025 36026 if (packet[5] & 0x40) { 36027 // advance the frame start past the extended header 36028 frameStart += 4; // header size field 36029 36030 frameStart += parseSyncSafeInteger(packet.subarray(10, 14)); 36031 } // parse one or more ID3 frames 36032 // http://id3.org/id3v2.3.0#ID3v2_frame_overview 36033 36034 36035 do { 36036 // determine the number of bytes in this frame 36037 frameSize = parseSyncSafeInteger(packet.subarray(frameStart + 4, frameStart + 8)); 36038 36039 if (frameSize < 1) { 36040 return null; 36041 } 36042 36043 frameHeader = String.fromCharCode(packet[frameStart], packet[frameStart + 1], packet[frameStart + 2], packet[frameStart + 3]); 36044 36045 if (frameHeader === 'PRIV') { 36046 frame = packet.subarray(frameStart + 10, frameStart + frameSize + 10); 36047 36048 for (var i = 0; i < frame.byteLength; i++) { 36049 if (frame[i] === 0) { 36050 var owner = parseIso88591(frame, 0, i); 36051 36052 if (owner === 'com.apple.streaming.transportStreamTimestamp') { 36053 var d = frame.subarray(i + 1); 36054 var size = (d[3] & 0x01) << 30 | d[4] << 22 | d[5] << 14 | d[6] << 6 | d[7] >>> 2; 36055 size *= 4; 36056 size += d[7] & 0x03; 36057 return size; 36058 } 36059 36060 break; 36061 } 36062 } 36063 } 36064 36065 frameStart += 10; // advance past the frame header 36066 36067 frameStart += frameSize; // advance past the frame body 36068 } while (frameStart < packet.byteLength); 36069 36070 return null; 36071 }; 36072 36073 var utils = { 36074 isLikelyAacData: isLikelyAacData, 36075 parseId3TagSize: parseId3TagSize, 36076 parseAdtsSize: parseAdtsSize, 36077 parseType: parseType$2, 36078 parseSampleRate: parseSampleRate, 36079 parseAacTimestamp: parseAacTimestamp 36080 }; 36081 36082 /** 36083 * mux.js 36084 * 36085 * Copyright (c) Brightcove 36086 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 36087 */ 36088 var ONE_SECOND_IN_TS = 90000, 36089 // 90kHz clock 36090 secondsToVideoTs, 36091 secondsToAudioTs, 36092 videoTsToSeconds, 36093 audioTsToSeconds, 36094 audioTsToVideoTs, 36095 videoTsToAudioTs, 36096 metadataTsToSeconds; 36097 36098 secondsToVideoTs = function secondsToVideoTs(seconds) { 36099 return seconds * ONE_SECOND_IN_TS; 36100 }; 36101 36102 secondsToAudioTs = function secondsToAudioTs(seconds, sampleRate) { 36103 return seconds * sampleRate; 36104 }; 36105 36106 videoTsToSeconds = function videoTsToSeconds(timestamp) { 36107 return timestamp / ONE_SECOND_IN_TS; 36108 }; 36109 36110 audioTsToSeconds = function audioTsToSeconds(timestamp, sampleRate) { 36111 return timestamp / sampleRate; 36112 }; 36113 36114 audioTsToVideoTs = function audioTsToVideoTs(timestamp, sampleRate) { 36115 return secondsToVideoTs(audioTsToSeconds(timestamp, sampleRate)); 36116 }; 36117 36118 videoTsToAudioTs = function videoTsToAudioTs(timestamp, sampleRate) { 36119 return secondsToAudioTs(videoTsToSeconds(timestamp), sampleRate); 36120 }; 36121 /** 36122 * Adjust ID3 tag or caption timing information by the timeline pts values 36123 * (if keepOriginalTimestamps is false) and convert to seconds 36124 */ 36125 36126 36127 metadataTsToSeconds = function metadataTsToSeconds(timestamp, timelineStartPts,
vendor: 4,162 bytes, lines 36127-36267
36127keepOriginalTimestamps) { 36128 return videoTsToSeconds(keepOriginalTimestamps ? timestamp : timestamp - timelineStartPts); 36129 }; 36130 36131 var clock = { 36132 ONE_SECOND_IN_TS: ONE_SECOND_IN_TS, 36133 secondsToVideoTs: secondsToVideoTs, 36134 secondsToAudioTs: secondsToAudioTs, 36135 videoTsToSeconds: videoTsToSeconds, 36136 audioTsToSeconds: audioTsToSeconds, 36137 audioTsToVideoTs: audioTsToVideoTs, 36138 videoTsToAudioTs: videoTsToAudioTs, 36139 metadataTsToSeconds: metadataTsToSeconds 36140 }; 36141 36142 var handleRollover$1 = timestampRolloverStream.handleRollover; 36143 var probe$2 = {}; 36144 probe$2.ts = probe$1; 36145 probe$2.aac = utils; 36146 var ONE_SECOND_IN_TS$1 = clock.ONE_SECOND_IN_TS; 36147 var MP2T_PACKET_LENGTH = 188, 36148 // bytes 36149 SYNC_BYTE = 0x47; 36150 /** 36151 * walks through segment data looking for pat and pmt packets to parse out 36152 * program map table information 36153 */ 36154 36155 var parsePsi_ = function parsePsi_(bytes, pmt) { 36156 var startIndex = 0, 36157 endIndex = MP2T_PACKET_LENGTH, 36158 packet, 36159 type; 36160 36161 while (endIndex < bytes.byteLength) { 36162 // Look for a pair of start and end sync bytes in the data.. 36163 if (bytes[startIndex] === SYNC_BYTE && bytes[endIndex] === SYNC_BYTE) { 36164 // We found a packet 36165 packet = bytes.subarray(startIndex, endIndex); 36166 type = probe$2.ts.parseType(packet, pmt.pid); 36167 36168 switch (type) { 36169 case 'pat': 36170 if (!pmt.pid) { 36171 pmt.pid = probe$2.ts.parsePat(packet); 36172 } 36173 36174 break; 36175 36176 case 'pmt': 36177 if (!pmt.table) { 36178 pmt.table = probe$2.ts.parsePmt(packet); 36179 } 36180 36181 break; 36182 36183 default: 36184 break; 36185 } // Found the pat and pmt, we can stop walking the segment 36186 36187 36188 if (pmt.pid && pmt.table) { 36189 return; 36190 } 36191 36192 startIndex += MP2T_PACKET_LENGTH; 36193 endIndex += MP2T_PACKET_LENGTH; 36194 continue; 36195 } // If we get here, we have somehow become de-synchronized and we need to step 36196 // forward one byte at a time until we find a pair of sync bytes that denote 36197 // a packet 36198 36199 36200 startIndex++; 36201 endIndex++; 36202 } 36203 }; 36204 /** 36205 * walks through the segment data from the start and end to get timing information 36206 * for the first and last audio pes packets 36207 */ 36208 36209 36210 var parseAudioPes_ = function parseAudioPes_(bytes, pmt, result) { 36211 var startIndex = 0, 36212 endIndex = MP2T_PACKET_LENGTH, 36213 packet, 36214 type, 36215 pesType, 36216 pusi, 36217 parsed; 36218 var endLoop = false; // Start walking from start of segment to get first audio packet 36219 36220 while (endIndex <= bytes.byteLength) { 36221 // Look for a pair of start and end sync bytes in the data.. 36222 if (bytes[startIndex] === SYNC_BYTE && (bytes[endIndex] === SYNC_BYTE || endIndex === bytes.byteLength)) { 36223 // We found a packet 36224 packet = bytes.subarray(startIndex, endIndex); 36225 type = probe$2.ts.parseType(packet, pmt.pid); 36226 36227 switch (type) { 36228 case 'pes': 36229 pesType = probe$2.ts.parsePesType(packet, pmt.table); 36230 pusi = probe$2.ts.parsePayloadUnitStartIndicator(packet); 36231 36232 if (pesType === 'audio' && pusi) { 36233 parsed = probe$2.ts.parsePesTime(packet); 36234 36235 if (parsed) { 36236 parsed.type = 'audio'; 36237 result.audio.push(parsed); 36238 endLoop = true; 36239 } 36240 } 36241 36242 break; 36243 36244 default: 36245 break; 36246 } 36247 36248 if (endLoop) { 36249 break; 36250 } 36251 36252 startIndex += MP2T_PACKET_LENGTH; 36253 endIndex += MP2T_PACKET_LENGTH; 36254 continue; 36255 } // If we get here, we have somehow become de-synchronized and we need to step 36256 // forward one byte at a time until we find a pair of sync bytes that denote 36257 // a packet 36258 36259 36260 startIndex++; 36261 endIndex++; 36262 } // Start walking from end of segment to get last audio packet 36263 36264 36265 endIndex = bytes.byteLength; 36266 startIndex = endIndex - MP2T_PACKET_LENGTH; 36267 endLoop = false;
vendor: 5,143 bytes, lines 36268-36420
36268 36269 while (startIndex >= 0) { 36270 // Look for a pair of start and end sync bytes in the data.. 36271 if (bytes[startIndex] === SYNC_BYTE && (bytes[endIndex] === SYNC_BYTE || endIndex === bytes.byteLength)) { 36272 // We found a packet 36273 packet = bytes.subarray(startIndex, endIndex); 36274 type = probe$2.ts.parseType(packet, pmt.pid); 36275 36276 switch (type) { 36277 case 'pes': 36278 pesType = probe$2.ts.parsePesType(packet, pmt.table); 36279 pusi = probe$2.ts.parsePayloadUnitStartIndicator(packet); 36280 36281 if (pesType === 'audio' && pusi) { 36282 parsed = probe$2.ts.parsePesTime(packet); 36283 36284 if (parsed) { 36285 parsed.type = 'audio'; 36286 result.audio.push(parsed); 36287 endLoop = true; 36288 } 36289 } 36290 36291 break; 36292 36293 default: 36294 break; 36295 } 36296 36297 if (endLoop) { 36298 break; 36299 } 36300 36301 startIndex -= MP2T_PACKET_LENGTH; 36302 endIndex -= MP2T_PACKET_LENGTH; 36303 continue; 36304 } // If we get here, we have somehow become de-synchronized and we need to step 36305 // forward one byte at a time until we find a pair of sync bytes that denote 36306 // a packet 36307 36308 36309 startIndex--; 36310 endIndex--; 36311 } 36312 }; 36313 /** 36314 * walks through the segment data from the start and end to get timing information 36315 * for the first and last video pes packets as well as timing information for the first 36316 * key frame. 36317 */ 36318 36319 36320 var parseVideoPes_ = function parseVideoPes_(bytes, pmt, result) { 36321 var startIndex = 0, 36322 endIndex = MP2T_PACKET_LENGTH, 36323 packet, 36324 type, 36325 pesType, 36326 pusi, 36327 parsed, 36328 frame, 36329 i, 36330 pes; 36331 var endLoop = false; 36332 var currentFrame = { 36333 data: [], 36334 size: 0 36335 }; // Start walking from start of segment to get first video packet 36336 36337 while (endIndex < bytes.byteLength) { 36338 // Look for a pair of start and end sync bytes in the data.. 36339 if (bytes[startIndex] === SYNC_BYTE && bytes[endIndex] === SYNC_BYTE) { 36340 // We found a packet 36341 packet = bytes.subarray(startIndex, endIndex); 36342 type = probe$2.ts.parseType(packet, pmt.pid); 36343 36344 switch (type) { 36345 case 'pes': 36346 pesType = probe$2.ts.parsePesType(packet, pmt.table); 36347 pusi = probe$2.ts.parsePayloadUnitStartIndicator(packet); 36348 36349 if (pesType === 'video') { 36350 if (pusi && !endLoop) { 36351 parsed = probe$2.ts.parsePesTime(packet); 36352 36353 if (parsed) { 36354 parsed.type = 'video'; 36355 result.video.push(parsed); 36356 endLoop = true; 36357 } 36358 } 36359 36360 if (!result.firstKeyFrame) { 36361 if (pusi) { 36362 if (currentFrame.size !== 0) { 36363 frame = new Uint8Array(currentFrame.size); 36364 i = 0; 36365 36366 while (currentFrame.data.length) { 36367 pes = currentFrame.data.shift(); 36368 frame.set(pes, i); 36369 i += pes.byteLength; 36370 } 36371 36372 if (probe$2.ts.videoPacketContainsKeyFrame(frame)) { 36373 var firstKeyFrame = probe$2.ts.parsePesTime(frame); // PTS/DTS may not be available. Simply *not* setting 36374 // the keyframe seems to work fine with HLS playback 36375 // and definitely preferable to a crash with TypeError... 36376 36377 if (firstKeyFrame) { 36378 result.firstKeyFrame = firstKeyFrame; 36379 result.firstKeyFrame.type = 'video'; 36380 } else { 36381 // eslint-disable-next-line 36382 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.'); 36383 } 36384 } 36385 36386 currentFrame.size = 0; 36387 } 36388 } 36389 36390 currentFrame.data.push(packet); 36391 currentFrame.size += packet.byteLength; 36392 } 36393 } 36394 36395 break; 36396 36397 default: 36398 break; 36399 } 36400 36401 if (endLoop && result.firstKeyFrame) { 36402 break; 36403 } 36404 36405 startIndex += MP2T_PACKET_LENGTH; 36406 endIndex += MP2T_PACKET_LENGTH; 36407 continue; 36408 } // If we get here, we have somehow become de-synchronized and we need to step 36409 // forward one byte at a time until we find a pair of sync bytes that denote 36410 // a packet 36411 36412 36413 startIndex++; 36414 endIndex++; 36415 } // Start walking from end of segment to get last video packet 36416 36417 36418 endIndex = bytes.byteLength; 36419 startIndex = endIndex - MP2T_PACKET_LENGTH; 36420 endLoop = false;
vendor: 42,857 bytes, lines 36421-37735
36421 36422 while (startIndex >= 0) { 36423 // Look for a pair of start and end sync bytes in the data.. 36424 if (bytes[startIndex] === SYNC_BYTE && bytes[endIndex] === SYNC_BYTE) { 36425 // We found a packet 36426 packet = bytes.subarray(startIndex, endIndex); 36427 type = probe$2.ts.parseType(packet, pmt.pid); 36428 36429 switch (type) { 36430 case 'pes': 36431 pesType = probe$2.ts.parsePesType(packet, pmt.table); 36432 pusi = probe$2.ts.parsePayloadUnitStartIndicator(packet); 36433 36434 if (pesType === 'video' && pusi) { 36435 parsed = probe$2.ts.parsePesTime(packet); 36436 36437 if (parsed) { 36438 parsed.type = 'video'; 36439 result.video.push(parsed); 36440 endLoop = true; 36441 } 36442 } 36443 36444 break; 36445 36446 default: 36447 break; 36448 } 36449 36450 if (endLoop) { 36451 break; 36452 } 36453 36454 startIndex -= MP2T_PACKET_LENGTH; 36455 endIndex -= MP2T_PACKET_LENGTH; 36456 continue; 36457 } // If we get here, we have somehow become de-synchronized and we need to step 36458 // forward one byte at a time until we find a pair of sync bytes that denote 36459 // a packet 36460 36461 36462 startIndex--; 36463 endIndex--; 36464 } 36465 }; 36466 /** 36467 * Adjusts the timestamp information for the segment to account for 36468 * rollover and convert to seconds based on pes packet timescale (90khz clock) 36469 */ 36470 36471 36472 var adjustTimestamp_ = function adjustTimestamp_(segmentInfo, baseTimestamp) { 36473 if (segmentInfo.audio && segmentInfo.audio.length) { 36474 var audioBaseTimestamp = baseTimestamp; 36475 36476 if (typeof audioBaseTimestamp === 'undefined') { 36477 audioBaseTimestamp = segmentInfo.audio[0].dts; 36478 } 36479 36480 segmentInfo.audio.forEach(function (info) { 36481 info.dts = handleRollover$1(info.dts, audioBaseTimestamp); 36482 info.pts = handleRollover$1(info.pts, audioBaseTimestamp); // time in seconds 36483 36484 info.dtsTime = info.dts / ONE_SECOND_IN_TS$1; 36485 info.ptsTime = info.pts / ONE_SECOND_IN_TS$1; 36486 }); 36487 } 36488 36489 if (segmentInfo.video && segmentInfo.video.length) { 36490 var videoBaseTimestamp = baseTimestamp; 36491 36492 if (typeof videoBaseTimestamp === 'undefined') { 36493 videoBaseTimestamp = segmentInfo.video[0].dts; 36494 } 36495 36496 segmentInfo.video.forEach(function (info) { 36497 info.dts = handleRollover$1(info.dts, videoBaseTimestamp); 36498 info.pts = handleRollover$1(info.pts, videoBaseTimestamp); // time in seconds 36499 36500 info.dtsTime = info.dts / ONE_SECOND_IN_TS$1; 36501 info.ptsTime = info.pts / ONE_SECOND_IN_TS$1; 36502 }); 36503 36504 if (segmentInfo.firstKeyFrame) { 36505 var frame = segmentInfo.firstKeyFrame; 36506 frame.dts = handleRollover$1(frame.dts, videoBaseTimestamp); 36507 frame.pts = handleRollover$1(frame.pts, videoBaseTimestamp); // time in seconds 36508 36509 frame.dtsTime = frame.dts / ONE_SECOND_IN_TS$1; 36510 frame.ptsTime = frame.dts / ONE_SECOND_IN_TS$1; 36511 } 36512 } 36513 }; 36514 /** 36515 * inspects the aac data stream for start and end time information 36516 */ 36517 36518 36519 var inspectAac_ = function inspectAac_(bytes) { 36520 var endLoop = false, 36521 audioCount = 0, 36522 sampleRate = null, 36523 timestamp = null, 36524 frameSize = 0, 36525 byteIndex = 0, 36526 packet; 36527 36528 while (bytes.length - byteIndex >= 3) { 36529 var type = probe$2.aac.parseType(bytes, byteIndex); 36530 36531 switch (type) { 36532 case 'timed-metadata': 36533 // Exit early because we don't have enough to parse 36534 // the ID3 tag header 36535 if (bytes.length - byteIndex < 10) { 36536 endLoop = true; 36537 break; 36538 } 36539 36540 frameSize = probe$2.aac.parseId3TagSize(bytes, byteIndex); // Exit early if we don't have enough in the buffer 36541 // to emit a full packet 36542 36543 if (frameSize > bytes.length) { 36544 endLoop = true; 36545 break; 36546 } 36547 36548 if (timestamp === null) { 36549 packet = bytes.subarray(byteIndex, byteIndex + frameSize); 36550 timestamp = probe$2.aac.parseAacTimestamp(packet); 36551 } 36552 36553 byteIndex += frameSize; 36554 break; 36555 36556 case 'audio': 36557 // Exit early because we don't have enough to parse 36558 // the ADTS frame header 36559 if (bytes.length - byteIndex < 7) { 36560 endLoop = true; 36561 break; 36562 } 36563 36564 frameSize = probe$2.aac.parseAdtsSize(bytes, byteIndex); // Exit early if we don't have enough in the buffer 36565 // to emit a full packet 36566 36567 if (frameSize > bytes.length) { 36568 endLoop = true; 36569 break; 36570 } 36571 36572 if (sampleRate === null) { 36573 packet = bytes.subarray(byteIndex, byteIndex + frameSize); 36574 sampleRate = probe$2.aac.parseSampleRate(packet); 36575 } 36576 36577 audioCount++; 36578 byteIndex += frameSize; 36579 break; 36580 36581 default: 36582 byteIndex++; 36583 break; 36584 } 36585 36586 if (endLoop) { 36587 return null; 36588 } 36589 } 36590 36591 if (sampleRate === null || timestamp === null) { 36592 return null; 36593 } 36594 36595 var audioTimescale = ONE_SECOND_IN_TS$1 / sampleRate; 36596 var result = { 36597 audio: [{ 36598 type: 'audio', 36599 dts: timestamp, 36600 pts: timestamp 36601 }, { 36602 type: 'audio', 36603 dts: timestamp + audioCount * 1024 * audioTimescale, 36604 pts: timestamp + audioCount * 1024 * audioTimescale 36605 }] 36606 }; 36607 return result; 36608 }; 36609 /** 36610 * inspects the transport stream segment data for start and end time information 36611 * of the audio and video tracks (when present) as well as the first key frame's 36612 * start time. 36613 */ 36614 36615 36616 var inspectTs_ = function inspectTs_(bytes) { 36617 var pmt = { 36618 pid: null, 36619 table: null 36620 }; 36621 var result = {}; 36622 parsePsi_(bytes, pmt); 36623 36624 for (var pid in pmt.table) { 36625 if (pmt.table.hasOwnProperty(pid)) { 36626 var type = pmt.table[pid]; 36627 36628 switch (type) { 36629 case streamTypes.H264_STREAM_TYPE: 36630 result.video = []; 36631 parseVideoPes_(bytes, pmt, result); 36632 36633 if (result.video.length === 0) { 36634 delete result.video; 36635 } 36636 36637 break; 36638 36639 case streamTypes.ADTS_STREAM_TYPE: 36640 result.audio = []; 36641 parseAudioPes_(bytes, pmt, result); 36642 36643 if (result.audio.length === 0) { 36644 delete result.audio; 36645 } 36646 36647 break; 36648 36649 default: 36650 break; 36651 } 36652 } 36653 } 36654 36655 return result; 36656 }; 36657 /** 36658 * Inspects segment byte data and returns an object with start and end timing information 36659 * 36660 * @param {Uint8Array} bytes The segment byte data 36661 * @param {Number} baseTimestamp Relative reference timestamp used when adjusting frame 36662 * timestamps for rollover. This value must be in 90khz clock. 36663 * @return {Object} Object containing start and end frame timing info of segment. 36664 */ 36665 36666 36667 var inspect = function inspect(bytes, baseTimestamp) { 36668 var isAacData = probe$2.aac.isLikelyAacData(bytes); 36669 var result; 36670 36671 if (isAacData) { 36672 result = inspectAac_(bytes); 36673 } else { 36674 result = inspectTs_(bytes); 36675 } 36676 36677 if (!result || !result.audio && !result.video) { 36678 return null; 36679 } 36680 36681 adjustTimestamp_(result, baseTimestamp); 36682 return result; 36683 }; 36684 36685 var tsInspector = { 36686 inspect: inspect, 36687 parseAudioPes_: parseAudioPes_ 36688 }; 36689 36690 /* 36691 * pkcs7.pad 36692 * https://github.com/brightcove/pkcs7 36693 * 36694 * Copyright (c) 2014 Brightcove 36695 * Licensed under the apache2 license. 36696 */ 36697 /** 36698 * Returns the subarray of a Uint8Array without PKCS#7 padding. 36699 * @param padded {Uint8Array} unencrypted bytes that have been padded 36700 * @return {Uint8Array} the unpadded bytes 36701 * @see http://tools.ietf.org/html/rfc5652 36702 */ 36703 36704 function unpad(padded) { 36705 return padded.subarray(0, padded.byteLength - padded[padded.byteLength - 1]); 36706 } 36707 36708 var classCallCheck = function classCallCheck(instance, Constructor) { 36709 if (!(instance instanceof Constructor)) { 36710 throw new TypeError("Cannot call a class as a function"); 36711 } 36712 }; 36713 36714 var createClass = function () { 36715 function defineProperties(target, props) { 36716 for (var i = 0; i < props.length; i++) { 36717 var descriptor = props[i]; 36718 descriptor.enumerable = descriptor.enumerable || false; 36719 descriptor.configurable = true; 36720 if ("value" in descriptor) descriptor.writable = true; 36721 Object.defineProperty(target, descriptor.key, descriptor); 36722 } 36723 } 36724 36725 return function (Constructor, protoProps, staticProps) { 36726 if (protoProps) defineProperties(Constructor.prototype, protoProps); 36727 if (staticProps) defineProperties(Constructor, staticProps); 36728 return Constructor; 36729 }; 36730 }(); 36731 36732 var inherits$1 = function inherits(subClass, superClass) { 36733 if (typeof superClass !== "function" && superClass !== null) { 36734 throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); 36735 } 36736 36737 subClass.prototype = Object.create(superClass && superClass.prototype, { 36738 constructor: { 36739 value: subClass, 36740 enumerable: false, 36741 writable: true, 36742 configurable: true 36743 } 36744 }); 36745 if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; 36746 }; 36747 36748 var possibleConstructorReturn = function possibleConstructorReturn(self, call) { 36749 if (!self) { 36750 throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); 36751 } 36752 36753 return call && (typeof call === "object" || typeof call === "function") ? call : self; 36754 }; 36755 /** 36756 * @file aes.js 36757 * 36758 * This file contains an adaptation of the AES decryption algorithm 36759 * from the Standford Javascript Cryptography Library. That work is 36760 * covered by the following copyright and permissions notice: 36761 * 36762 * Copyright 2009-2010 Emily Stark, Mike Hamburg, Dan Boneh. 36763 * All rights reserved. 36764 * 36765 * Redistribution and use in source and binary forms, with or without 36766 * modification, are permitted provided that the following conditions are 36767 * met: 36768 * 36769 * 1. Redistributions of source code must retain the above copyright 36770 * notice, this list of conditions and the following disclaimer. 36771 * 36772 * 2. Redistributions in binary form must reproduce the above 36773 * copyright notice, this list of conditions and the following 36774 * disclaimer in the documentation and/or other materials provided 36775 * with the distribution. 36776 * 36777 * THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR 36778 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 36779 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 36780 * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> OR CONTRIBUTORS BE 36781 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 36782 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 36783 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 36784 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 36785 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 36786 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN 36787 * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 36788 * 36789 * The views and conclusions contained in the software and documentation 36790 * are those of the authors and should not be interpreted as representing 36791 * official policies, either expressed or implied, of the authors. 36792 */ 36793 36794 /** 36795 * Expand the S-box tables. 36796 * 36797 * @private 36798 */ 36799 36800 36801 var precompute = function precompute() { 36802 var tables = [[[], [], [], [], []], [[], [], [], [], []]]; 36803 var encTable = tables[0]; 36804 var decTable = tables[1]; 36805 var sbox = encTable[4]; 36806 var sboxInv = decTable[4]; 36807 var i = void 0; 36808 var x = void 0; 36809 var xInv = void 0; 36810 var d = []; 36811 var th = []; 36812 var x2 = void 0; 36813 var x4 = void 0; 36814 var x8 = void 0; 36815 var s = void 0; 36816 var tEnc = void 0; 36817 var tDec = void 0; // Compute double and third tables 36818 36819 for (i = 0; i < 256; i++) { 36820 th[(d[i] = i << 1 ^ (i >> 7) * 283) ^ i] = i; 36821 } 36822 36823 for (x = xInv = 0; !sbox[x]; x ^= x2 || 1, xInv = th[xInv] || 1) { 36824 // Compute sbox 36825 s = xInv ^ xInv << 1 ^ xInv << 2 ^ xInv << 3 ^ xInv << 4; 36826 s = s >> 8 ^ s & 255 ^ 99; 36827 sbox[x] = s; 36828 sboxInv[s] = x; // Compute MixColumns 36829 36830 x8 = d[x4 = d[x2 = d[x]]]; 36831 tDec = x8 * 0x1010101 ^ x4 * 0x10001 ^ x2 * 0x101 ^ x * 0x1010100; 36832 tEnc = d[s] * 0x101 ^ s * 0x1010100; 36833 36834 for (i = 0; i < 4; i++) { 36835 encTable[i][x] = tEnc = tEnc << 24 ^ tEnc >>> 8; 36836 decTable[i][s] = tDec = tDec << 24 ^ tDec >>> 8; 36837 } 36838 } // Compactify. Considerable speedup on Firefox. 36839 36840 36841 for (i = 0; i < 5; i++) { 36842 encTable[i] = encTable[i].slice(0); 36843 decTable[i] = decTable[i].slice(0); 36844 } 36845 36846 return tables; 36847 }; 36848 36849 var aesTables = null; 36850 /** 36851 * Schedule out an AES key for both encryption and decryption. This 36852 * is a low-level class. Use a cipher mode to do bulk encryption. 36853 * 36854 * @class AES 36855 * @param key {Array} The key as an array of 4, 6 or 8 words. 36856 */ 36857 36858 var AES = function () { 36859 function AES(key) { 36860 classCallCheck(this, AES); 36861 /** 36862 * The expanded S-box and inverse S-box tables. These will be computed 36863 * on the client so that we don't have to send them down the wire. 36864 * 36865 * There are two tables, _tables[0] is for encryption and 36866 * _tables[1] is for decryption. 36867 * 36868 * The first 4 sub-tables are the expanded S-box with MixColumns. The 36869 * last (_tables[01][4]) is the S-box itself. 36870 * 36871 * @private 36872 */ 36873 // if we have yet to precompute the S-box tables 36874 // do so now 36875 36876 if (!aesTables) { 36877 aesTables = precompute(); 36878 } // then make a copy of that object for use 36879 36880 36881 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()]]; 36882 var i = void 0; 36883 var j = void 0; 36884 var tmp = void 0; 36885 var encKey = void 0; 36886 var decKey = void 0; 36887 var sbox = this._tables[0][4]; 36888 var decTable = this._tables[1]; 36889 var keyLen = key.length; 36890 var rcon = 1; 36891 36892 if (keyLen !== 4 && keyLen !== 6 && keyLen !== 8) { 36893 throw new Error('Invalid aes key size'); 36894 } 36895 36896 encKey = key.slice(0); 36897 decKey = []; 36898 this._key = [encKey, decKey]; // schedule encryption keys 36899 36900 for (i = keyLen; i < 4 * keyLen + 28; i++) { 36901 tmp = encKey[i - 1]; // apply sbox 36902 36903 if (i % keyLen === 0 || keyLen === 8 && i % keyLen === 4) { 36904 tmp = sbox[tmp >>> 24] << 24 ^ sbox[tmp >> 16 & 255] << 16 ^ sbox[tmp >> 8 & 255] << 8 ^ sbox[tmp & 255]; // shift rows and add rcon 36905 36906 if (i % keyLen === 0) { 36907 tmp = tmp << 8 ^ tmp >>> 24 ^ rcon << 24; 36908 rcon = rcon << 1 ^ (rcon >> 7) * 283; 36909 } 36910 } 36911 36912 encKey[i] = encKey[i - keyLen] ^ tmp; 36913 } // schedule decryption keys 36914 36915 36916 for (j = 0; i; j++, i--) { 36917 tmp = encKey[j & 3 ? i : i - 4]; 36918 36919 if (i <= 4 || j < 4) { 36920 decKey[j] = tmp; 36921 } else { 36922 decKey[j] = decTable[0][sbox[tmp >>> 24]] ^ decTable[1][sbox[tmp >> 16 & 255]] ^ decTable[2][sbox[tmp >> 8 & 255]] ^ decTable[3][sbox[tmp & 255]]; 36923 } 36924 } 36925 } 36926 /** 36927 * Decrypt 16 bytes, specified as four 32-bit words. 36928 * 36929 * @param {Number} encrypted0 the first word to decrypt 36930 * @param {Number} encrypted1 the second word to decrypt 36931 * @param {Number} encrypted2 the third word to decrypt 36932 * @param {Number} encrypted3 the fourth word to decrypt 36933 * @param {Int32Array} out the array to write the decrypted words 36934 * into 36935 * @param {Number} offset the offset into the output array to start 36936 * writing results 36937 * @return {Array} The plaintext. 36938 */ 36939 36940 36941 AES.prototype.decrypt = function decrypt(encrypted0, encrypted1, encrypted2, encrypted3, out, offset) { 36942 var key = this._key[1]; // state variables a,b,c,d are loaded with pre-whitened data 36943 36944 var a = encrypted0 ^ key[0]; 36945 var b = encrypted3 ^ key[1]; 36946 var c = encrypted2 ^ key[2]; 36947 var d = encrypted1 ^ key[3]; 36948 var a2 = void 0; 36949 var b2 = void 0; 36950 var c2 = void 0; // key.length === 2 ? 36951 36952 var nInnerRounds = key.length / 4 - 2; 36953 var i = void 0; 36954 var kIndex = 4; 36955 var table = this._tables[1]; // load up the tables 36956 36957 var table0 = table[0]; 36958 var table1 = table[1]; 36959 var table2 = table[2]; 36960 var table3 = table[3]; 36961 var sbox = table[4]; // Inner rounds. Cribbed from OpenSSL. 36962 36963 for (i = 0; i < nInnerRounds; i++) { 36964 a2 = table0[a >>> 24] ^ table1[b >> 16 & 255] ^ table2[c >> 8 & 255] ^ table3[d & 255] ^ key[kIndex]; 36965 b2 = table0[b >>> 24] ^ table1[c >> 16 & 255] ^ table2[d >> 8 & 255] ^ table3[a & 255] ^ key[kIndex + 1]; 36966 c2 = table0[c >>> 24] ^ table1[d >> 16 & 255] ^ table2[a >> 8 & 255] ^ table3[b & 255] ^ key[kIndex + 2]; 36967 d = table0[d >>> 24] ^ table1[a >> 16 & 255] ^ table2[b >> 8 & 255] ^ table3[c & 255] ^ key[kIndex + 3]; 36968 kIndex += 4; 36969 a = a2; 36970 b = b2; 36971 c = c2; 36972 } // Last round. 36973 36974 36975 for (i = 0; i < 4; i++) { 36976 out[(3 & -i) + offset] = sbox[a >>> 24] << 24 ^ sbox[b >> 16 & 255] << 16 ^ sbox[c >> 8 & 255] << 8 ^ sbox[d & 255] ^ key[kIndex++]; 36977 a2 = a; 36978 a = b; 36979 b = c; 36980 c = d; 36981 d = a2; 36982 } 36983 }; 36984 36985 return AES; 36986 }(); 36987 /** 36988 * @file stream.js 36989 */ 36990 36991 /** 36992 * A lightweight readable stream implemention that handles event dispatching. 36993 * 36994 * @class Stream 36995 */ 36996 36997 36998 var Stream$2 = function () { 36999 function Stream() { 37000 classCallCheck(this, Stream); 37001 this.listeners = {}; 37002 } 37003 /** 37004 * Add a listener for a specified event type. 37005 * 37006 * @param {String} type the event name 37007 * @param {Function} listener the callback to be invoked when an event of 37008 * the specified type occurs 37009 */ 37010 37011 37012 Stream.prototype.on = function on(type, listener) { 37013 if (!this.listeners[type]) { 37014 this.listeners[type] = []; 37015 } 37016 37017 this.listeners[type].push(listener); 37018 }; 37019 /** 37020 * Remove a listener for a specified event type. 37021 * 37022 * @param {String} type the event name 37023 * @param {Function} listener a function previously registered for this 37024 * type of event through `on` 37025 * @return {Boolean} if we could turn it off or not 37026 */ 37027 37028 37029 Stream.prototype.off = function off(type, listener) { 37030 if (!this.listeners[type]) { 37031 return false; 37032 } 37033 37034 var index = this.listeners[type].indexOf(listener); 37035 this.listeners[type].splice(index, 1); 37036 return index > -1; 37037 }; 37038 /** 37039 * Trigger an event of the specified type on this stream. Any additional 37040 * arguments to this function are passed as parameters to event listeners. 37041 * 37042 * @param {String} type the event name 37043 */ 37044 37045 37046 Stream.prototype.trigger = function trigger(type) { 37047 var callbacks = this.listeners[type]; 37048 37049 if (!callbacks) { 37050 return; 37051 } // Slicing the arguments on every invocation of this method 37052 // can add a significant amount of overhead. Avoid the 37053 // intermediate object creation for the common case of a 37054 // single callback argument 37055 37056 37057 if (arguments.length === 2) { 37058 var length = callbacks.length; 37059 37060 for (var i = 0; i < length; ++i) { 37061 callbacks[i].call(this, arguments[1]); 37062 } 37063 } else { 37064 var args = Array.prototype.slice.call(arguments, 1); 37065 var _length = callbacks.length; 37066 37067 for (var _i = 0; _i < _length; ++_i) { 37068 callbacks[_i].apply(this, args); 37069 } 37070 } 37071 }; 37072 /** 37073 * Destroys the stream and cleans up. 37074 */ 37075 37076 37077 Stream.prototype.dispose = function dispose() { 37078 this.listeners = {}; 37079 }; 37080 /** 37081 * Forwards all `data` events on this stream to the destination stream. The 37082 * destination stream should provide a method `push` to receive the data 37083 * events as they arrive. 37084 * 37085 * @param {Stream} destination the stream that will receive all `data` events 37086 * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options 37087 */ 37088 37089 37090 Stream.prototype.pipe = function pipe(destination) { 37091 this.on('data', function (data) { 37092 destination.push(data); 37093 }); 37094 }; 37095 37096 return Stream; 37097 }(); 37098 /** 37099 * @file async-stream.js 37100 */ 37101 37102 /** 37103 * A wrapper around the Stream class to use setTiemout 37104 * and run stream "jobs" Asynchronously 37105 * 37106 * @class AsyncStream 37107 * @extends Stream 37108 */ 37109 37110 37111 var AsyncStream = function (_Stream) { 37112 inherits$1(AsyncStream, _Stream); 37113 37114 function AsyncStream() { 37115 classCallCheck(this, AsyncStream); 37116 37117 var _this = possibleConstructorReturn(this, _Stream.call(this, Stream$2)); 37118 37119 _this.jobs = []; 37120 _this.delay = 1; 37121 _this.timeout_ = null; 37122 return _this; 37123 } 37124 /** 37125 * process an async job 37126 * 37127 * @private 37128 */ 37129 37130 37131 AsyncStream.prototype.processJob_ = function processJob_() { 37132 this.jobs.shift()(); 37133 37134 if (this.jobs.length) { 37135 this.timeout_ = setTimeout(this.processJob_.bind(this), this.delay); 37136 } else { 37137 this.timeout_ = null; 37138 } 37139 }; 37140 /** 37141 * push a job into the stream 37142 * 37143 * @param {Function} job the job to push into the stream 37144 */ 37145 37146 37147 AsyncStream.prototype.push = function push(job) { 37148 this.jobs.push(job); 37149 37150 if (!this.timeout_) { 37151 this.timeout_ = setTimeout(this.processJob_.bind(this), this.delay); 37152 } 37153 }; 37154 37155 return AsyncStream; 37156 }(Stream$2); 37157 /** 37158 * @file decrypter.js 37159 * 37160 * An asynchronous implementation of AES-128 CBC decryption with 37161 * PKCS#7 padding. 37162 */ 37163 37164 /** 37165 * Convert network-order (big-endian) bytes into their little-endian 37166 * representation. 37167 */ 37168 37169 37170 var ntoh = function ntoh(word) { 37171 return word << 24 | (word & 0xff00) << 8 | (word & 0xff0000) >> 8 | word >>> 24; 37172 }; 37173 /** 37174 * Decrypt bytes using AES-128 with CBC and PKCS#7 padding. 37175 * 37176 * @param {Uint8Array} encrypted the encrypted bytes 37177 * @param {Uint32Array} key the bytes of the decryption key 37178 * @param {Uint32Array} initVector the initialization vector (IV) to 37179 * use for the first round of CBC. 37180 * @return {Uint8Array} the decrypted bytes 37181 * 37182 * @see http://en.wikipedia.org/wiki/Advanced_Encryption_Standard 37183 * @see http://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_Block_Chaining_.28CBC.29 37184 * @see https://tools.ietf.org/html/rfc2315 37185 */ 37186 37187 37188 var decrypt = function decrypt(encrypted, key, initVector) { 37189 // word-level access to the encrypted bytes 37190 var encrypted32 = new Int32Array(encrypted.buffer, encrypted.byteOffset, encrypted.byteLength >> 2); 37191 var decipher = new AES(Array.prototype.slice.call(key)); // byte and word-level access for the decrypted output 37192 37193 var decrypted = new Uint8Array(encrypted.byteLength); 37194 var decrypted32 = new Int32Array(decrypted.buffer); // temporary variables for working with the IV, encrypted, and 37195 // decrypted data 37196 37197 var init0 = void 0; 37198 var init1 = void 0; 37199 var init2 = void 0; 37200 var init3 = void 0; 37201 var encrypted0 = void 0; 37202 var encrypted1 = void 0; 37203 var encrypted2 = void 0; 37204 var encrypted3 = void 0; // iteration variable 37205 37206 var wordIx = void 0; // pull out the words of the IV to ensure we don't modify the 37207 // passed-in reference and easier access 37208 37209 init0 = initVector[0]; 37210 init1 = initVector[1]; 37211 init2 = initVector[2]; 37212 init3 = initVector[3]; // decrypt four word sequences, applying cipher-block chaining (CBC) 37213 // to each decrypted block 37214 37215 for (wordIx = 0; wordIx < encrypted32.length; wordIx += 4) { 37216 // convert big-endian (network order) words into little-endian 37217 // (javascript order) 37218 encrypted0 = ntoh(encrypted32[wordIx]); 37219 encrypted1 = ntoh(encrypted32[wordIx + 1]); 37220 encrypted2 = ntoh(encrypted32[wordIx + 2]); 37221 encrypted3 = ntoh(encrypted32[wordIx + 3]); // decrypt the block 37222 37223 decipher.decrypt(encrypted0, encrypted1, encrypted2, encrypted3, decrypted32, wordIx); // XOR with the IV, and restore network byte-order to obtain the 37224 // plaintext 37225 37226 decrypted32[wordIx] = ntoh(decrypted32[wordIx] ^ init0); 37227 decrypted32[wordIx + 1] = ntoh(decrypted32[wordIx + 1] ^ init1); 37228 decrypted32[wordIx + 2] = ntoh(decrypted32[wordIx + 2] ^ init2); 37229 decrypted32[wordIx + 3] = ntoh(decrypted32[wordIx + 3] ^ init3); // setup the IV for the next round 37230 37231 init0 = encrypted0; 37232 init1 = encrypted1; 37233 init2 = encrypted2; 37234 init3 = encrypted3; 37235 } 37236 37237 return decrypted; 37238 }; 37239 /** 37240 * The `Decrypter` class that manages decryption of AES 37241 * data through `AsyncStream` objects and the `decrypt` 37242 * function 37243 * 37244 * @param {Uint8Array} encrypted the encrypted bytes 37245 * @param {Uint32Array} key the bytes of the decryption key 37246 * @param {Uint32Array} initVector the initialization vector (IV) to 37247 * @param {Function} done the function to run when done 37248 * @class Decrypter 37249 */ 37250 37251 37252 var Decrypter = function () { 37253 function Decrypter(encrypted, key, initVector, done) { 37254 classCallCheck(this, Decrypter); 37255 var step = Decrypter.STEP; 37256 var encrypted32 = new Int32Array(encrypted.buffer); 37257 var decrypted = new Uint8Array(encrypted.byteLength); 37258 var i = 0; 37259 this.asyncStream_ = new AsyncStream(); // split up the encryption job and do the individual chunks asynchronously 37260 37261 this.asyncStream_.push(this.decryptChunk_(encrypted32.subarray(i, i + step), key, initVector, decrypted)); 37262 37263 for (i = step; i < encrypted32.length; i += step) { 37264 initVector = new Uint32Array([ntoh(encrypted32[i - 4]), ntoh(encrypted32[i - 3]), ntoh(encrypted32[i - 2]), ntoh(encrypted32[i - 1])]); 37265 this.asyncStream_.push(this.decryptChunk_(encrypted32.subarray(i, i + step), key, initVector, decrypted)); 37266 } // invoke the done() callback when everything is finished 37267 37268 37269 this.asyncStream_.push(function () { 37270 // remove pkcs#7 padding from the decrypted bytes 37271 done(null, unpad(decrypted)); 37272 }); 37273 } 37274 /** 37275 * a getter for step the maximum number of bytes to process at one time 37276 * 37277 * @return {Number} the value of step 32000 37278 */ 37279 37280 /** 37281 * @private 37282 */ 37283 37284 37285 Decrypter.prototype.decryptChunk_ = function decryptChunk_(encrypted, key, initVector, decrypted) { 37286 return function () { 37287 var bytes = decrypt(encrypted, key, initVector); 37288 decrypted.set(bytes, encrypted.byteOffset); 37289 }; 37290 }; 37291 37292 createClass(Decrypter, null, [{ 37293 key: 'STEP', 37294 get: function get$$1() { 37295 // 4 * 8000; 37296 return 32000; 37297 } 37298 }]); 37299 return Decrypter; 37300 }(); 37301 37302 /** 37303 * @videojs/http-streaming 37304 * @version 1.11.2 37305 * @copyright 2019 Brightcove, Inc 37306 * @license Apache-2.0 37307 */ 37308 /** 37309 * @file resolve-url.js - Handling how URLs are resolved and manipulated 37310 */ 37311 37312 var resolveUrl$1 = function resolveUrl(baseURL, relativeURL) { 37313 // return early if we don't need to resolve 37314 if (/^[a-z]+:/i.test(relativeURL)) { 37315 return relativeURL; 37316 } // if the base URL is relative then combine with the current location 37317 37318 37319 if (!/\/\//i.test(baseURL)) { 37320 baseURL = urlToolkit.buildAbsoluteURL(window$1.location.href, baseURL); 37321 } 37322 37323 return urlToolkit.buildAbsoluteURL(baseURL, relativeURL); 37324 }; 37325 /** 37326 * Checks whether xhr request was redirected and returns correct url depending 37327 * on `handleManifestRedirects` option 37328 * 37329 * @api private 37330 * 37331 * @param {String} url - an url being requested 37332 * @param {XMLHttpRequest} req - xhr request result 37333 * 37334 * @return {String} 37335 */ 37336 37337 37338 var resolveManifestRedirect = function resolveManifestRedirect(handleManifestRedirect, url, req) { 37339 // To understand how the responseURL below is set and generated: 37340 // - https://fetch.spec.whatwg.org/#concept-response-url 37341 // - https://fetch.spec.whatwg.org/#atomic-http-redirect-handling 37342 if (handleManifestRedirect && req.responseURL && url !== req.responseURL) { 37343 return req.responseURL; 37344 } 37345 37346 return url; 37347 }; 37348 37349 var classCallCheck$1 = function classCallCheck(instance, Constructor) { 37350 if (!(instance instanceof Constructor)) { 37351 throw new TypeError("Cannot call a class as a function"); 37352 } 37353 }; 37354 37355 var createClass$1 = function () { 37356 function defineProperties(target, props) { 37357 for (var i = 0; i < props.length; i++) { 37358 var descriptor = props[i]; 37359 descriptor.enumerable = descriptor.enumerable || false; 37360 descriptor.configurable = true; 37361 if ("value" in descriptor) descriptor.writable = true; 37362 Object.defineProperty(target, descriptor.key, descriptor); 37363 } 37364 } 37365 37366 return function (Constructor, protoProps, staticProps) { 37367 if (protoProps) defineProperties(Constructor.prototype, protoProps); 37368 if (staticProps) defineProperties(Constructor, staticProps); 37369 return Constructor; 37370 }; 37371 }(); 37372 37373 var get$1 = function get(object, property, receiver) { 37374 if (object === null) object = Function.prototype; 37375 var desc = Object.getOwnPropertyDescriptor(object, property); 37376 37377 if (desc === undefined) { 37378 var parent = Object.getPrototypeOf(object); 37379 37380 if (parent === null) { 37381 return undefined; 37382 } else { 37383 return get(parent, property, receiver); 37384 } 37385 } else if ("value" in desc) { 37386 return desc.value; 37387 } else { 37388 var getter = desc.get; 37389 37390 if (getter === undefined) { 37391 return undefined; 37392 } 37393 37394 return getter.call(receiver); 37395 } 37396 }; 37397 37398 var inherits$2 = function inherits(subClass, superClass) { 37399 if (typeof superClass !== "function" && superClass !== null) { 37400 throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); 37401 } 37402 37403 subClass.prototype = Object.create(superClass && superClass.prototype, { 37404 constructor: { 37405 value: subClass, 37406 enumerable: false, 37407 writable: true, 37408 configurable: true 37409 } 37410 }); 37411 if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; 37412 }; 37413 37414 var possibleConstructorReturn$1 = function possibleConstructorReturn(self, call) { 37415 if (!self) { 37416 throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); 37417 } 37418 37419 return call && (typeof call === "object" || typeof call === "function") ? call : self; 37420 }; 37421 37422 var slicedToArray = function () { 37423 function sliceIterator(arr, i) { 37424 var _arr = []; 37425 var _n = true; 37426 var _d = false; 37427 var _e = undefined; 37428 37429 try { 37430 for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { 37431 _arr.push(_s.value); 37432 37433 if (i && _arr.length === i) break; 37434 } 37435 } catch (err) { 37436 _d = true; 37437 _e = err; 37438 } finally { 37439 try { 37440 if (!_n && _i["return"]) _i["return"](); 37441 } finally { 37442 if (_d) throw _e; 37443 } 37444 } 37445 37446 return _arr; 37447 } 37448 37449 return function (arr, i) { 37450 if (Array.isArray(arr)) { 37451 return arr; 37452 } else if (Symbol.iterator in Object(arr)) { 37453 return sliceIterator(arr, i); 37454 } else { 37455 throw new TypeError("Invalid attempt to destructure non-iterable instance"); 37456 } 37457 }; 37458 }(); 37459 /** 37460 * @file playlist-loader.js 37461 * 37462 * A state machine that manages the loading, caching, and updating of 37463 * M3U8 playlists. 37464 * 37465 */ 37466 37467 37468 var mergeOptions$1 = videojs$1.mergeOptions, 37469 EventTarget$1 = videojs$1.EventTarget, 37470 log$1 = videojs$1.log; 37471 /** 37472 * Loops through all supported media groups in master and calls the provided 37473 * callback for each group 37474 * 37475 * @param {Object} master 37476 * The parsed master manifest object 37477 * @param {Function} callback 37478 * Callback to call for each media group 37479 */ 37480 37481 var forEachMediaGroup = function forEachMediaGroup(master, callback) { 37482 ['AUDIO', 'SUBTITLES'].forEach(function (mediaType) { 37483 for (var groupKey in master.mediaGroups[mediaType]) { 37484 for (var labelKey in master.mediaGroups[mediaType][groupKey]) { 37485 var mediaProperties = master.mediaGroups[mediaType][groupKey][labelKey]; 37486 callback(mediaProperties, mediaType, groupKey, labelKey); 37487 } 37488 } 37489 }); 37490 }; 37491 /** 37492 * Returns a new array of segments that is the result of merging 37493 * properties from an older list of segments onto an updated 37494 * list. No properties on the updated playlist will be overridden. 37495 * 37496 * @param {Array} original the outdated list of segments 37497 * @param {Array} update the updated list of segments 37498 * @param {Number=} offset the index of the first update 37499 * segment in the original segment list. For non-live playlists, 37500 * this should always be zero and does not need to be 37501 * specified. For live playlists, it should be the difference 37502 * between the media sequence numbers in the original and updated 37503 * playlists. 37504 * @return a list of merged segment objects 37505 */ 37506 37507 37508 var updateSegments = function updateSegments(original, update, offset) { 37509 var result = update.slice(); 37510 offset = offset || 0; 37511 var length = Math.min(original.length, update.length + offset); 37512 37513 for (var i = offset; i < length; i++) { 37514 result[i - offset] = mergeOptions$1(original[i], result[i - offset]); 37515 } 37516 37517 return result; 37518 }; 37519 37520 var resolveSegmentUris = function resolveSegmentUris(segment, baseUri) { 37521 if (!segment.resolvedUri) { 37522 segment.resolvedUri = resolveUrl$1(baseUri, segment.uri); 37523 } 37524 37525 if (segment.key && !segment.key.resolvedUri) { 37526 segment.key.resolvedUri = resolveUrl$1(baseUri, segment.key.uri); 37527 } 37528 37529 if (segment.map && !segment.map.resolvedUri) { 37530 segment.map.resolvedUri = resolveUrl$1(baseUri, segment.map.uri); 37531 } 37532 }; 37533 /** 37534 * Returns a new master playlist that is the result of merging an 37535 * updated media playlist into the original version. If the 37536 * updated media playlist does not match any of the playlist 37537 * entries in the original master playlist, null is returned. 37538 * 37539 * @param {Object} master a parsed master M3U8 object 37540 * @param {Object} media a parsed media M3U8 object 37541 * @return {Object} a new object that represents the original 37542 * master playlist with the updated media playlist merged in, or 37543 * null if the merge produced no change. 37544 */ 37545 37546 37547 var updateMaster = function updateMaster(master, media) { 37548 var result = mergeOptions$1(master, {}); 37549 var playlist = result.playlists[media.uri]; 37550 37551 if (!playlist) { 37552 return null; 37553 } // consider the playlist unchanged if the number of segments is equal, the media 37554 // sequence number is unchanged, and this playlist hasn't become the end of the playlist 37555 37556 37557 if (playlist.segments && media.segments && playlist.segments.length === media.segments.length && playlist.endList === media.endList && playlist.mediaSequence === media.mediaSequence) { 37558 return null; 37559 } 37560 37561 var mergedPlaylist = mergeOptions$1(playlist, media); // if the update could overlap existing segment information, merge the two segment lists 37562 37563 if (playlist.segments) { 37564 mergedPlaylist.segments = updateSegments(playlist.segments, media.segments, media.mediaSequence - playlist.mediaSequence); 37565 } // resolve any segment URIs to prevent us from having to do it later 37566 37567 37568 mergedPlaylist.segments.forEach(function (segment) { 37569 resolveSegmentUris(segment, mergedPlaylist.resolvedUri); 37570 }); // TODO Right now in the playlists array there are two references to each playlist, one 37571 // that is referenced by index, and one by URI. The index reference may no longer be 37572 // necessary. 37573 37574 for (var i = 0; i < result.playlists.length; i++) { 37575 if (result.playlists[i].uri === media.uri) { 37576 result.playlists[i] = mergedPlaylist; 37577 } 37578 } 37579 37580 result.playlists[media.uri] = mergedPlaylist; 37581 return result; 37582 }; 37583 37584 var setupMediaPlaylists = function setupMediaPlaylists(master) { 37585 // setup by-URI lookups and resolve media playlist URIs 37586 var i = master.playlists.length; 37587 37588 while (i--) { 37589 var playlist = master.playlists[i]; 37590 master.playlists[playlist.uri] = playlist; 37591 playlist.resolvedUri = resolveUrl$1(master.uri, playlist.uri); 37592 playlist.id = i; 37593 37594 if (!playlist.attributes) { 37595 // Although the spec states an #EXT-X-STREAM-INF tag MUST have a 37596 // BANDWIDTH attribute, we can play the stream without it. This means a poorly 37597 // formatted master playlist may not have an attribute list. An attributes 37598 // property is added here to prevent undefined references when we encounter 37599 // this scenario. 37600 playlist.attributes = {}; 37601 log$1.warn('Invalid playlist STREAM-INF detected. Missing BANDWIDTH attribute.'); 37602 } 37603 } 37604 }; 37605 37606 var resolveMediaGroupUris = function resolveMediaGroupUris(master) { 37607 forEachMediaGroup(master, function (properties) { 37608 if (properties.uri) { 37609 properties.resolvedUri = resolveUrl$1(master.uri, properties.uri); 37610 } 37611 }); 37612 }; 37613 /** 37614 * Calculates the time to wait before refreshing a live playlist 37615 * 37616 * @param {Object} media 37617 * The current media 37618 * @param {Boolean} update 37619 * True if there were any updates from the last refresh, false otherwise 37620 * @return {Number} 37621 * The time in ms to wait before refreshing the live playlist 37622 */ 37623 37624 37625 var refreshDelay = function refreshDelay(media, update) { 37626 var lastSegment = media.segments[media.segments.length - 1]; 37627 var delay = void 0; 37628 37629 if (update && lastSegment && lastSegment.duration) { 37630 delay = lastSegment.duration * 1000; 37631 } else { 37632 // if the playlist is unchanged since the last reload or last segment duration 37633 // cannot be determined, try again after half the target duration 37634 delay = (media.targetDuration || 10) * 500; 37635 } 37636 37637 return delay; 37638 }; 37639 /** 37640 * Load a playlist from a remote location 37641 * 37642 * @class PlaylistLoader 37643 * @extends Stream 37644 * @param {String} srcUrl the url to start with 37645 * @param {Boolean} withCredentials the withCredentials xhr option 37646 * @constructor 37647 */ 37648 37649 37650 var PlaylistLoader = function (_EventTarget) { 37651 inherits$2(PlaylistLoader, _EventTarget); 37652 37653 function PlaylistLoader(srcUrl, hls) { 37654 var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 37655 classCallCheck$1(this, PlaylistLoader); 37656 37657 var _this = possibleConstructorReturn$1(this, (PlaylistLoader.__proto__ || Object.getPrototypeOf(PlaylistLoader)).call(this)); 37658 37659 var _options$withCredenti = options.withCredentials, 37660 withCredentials = _options$withCredenti === undefined ? false : _options$withCredenti, 37661 _options$handleManife = options.handleManifestRedirects, 37662 handleManifestRedirects = _options$handleManife === undefined ? false : _options$handleManife; 37663 _this.srcUrl = srcUrl; 37664 _this.hls_ = hls; 37665 _this.withCredentials = withCredentials; 37666 _this.handleManifestRedirects = handleManifestRedirects; 37667 var hlsOptions = hls.options_; 37668 _this.customTagParsers = hlsOptions && hlsOptions.customTagParsers || []; 37669 _this.customTagMappers = hlsOptions && hlsOptions.customTagMappers || []; 37670 37671 if (!_this.srcUrl) { 37672 throw new Error('A non-empty playlist URL is required'); 37673 } // initialize the loader state 37674 37675 37676 _this.state = 'HAVE_NOTHING'; // live playlist staleness timeout 37677 37678 _this.on('mediaupdatetimeout', function () { 37679 if (_this.state !== 'HAVE_METADATA') { 37680 // only refresh the media playlist if no other activity is going on 37681 return; 37682 } 37683 37684 _this.state = 'HAVE_CURRENT_METADATA'; 37685 _this.request = _this.hls_.xhr({ 37686 uri: resolveUrl$1(_this.master.uri, _this.media().uri), 37687 withCredentials: _this.withCredentials 37688 }, function (error, req) { 37689 // disposed 37690 if (!_this.request) { 37691 return; 37692 } 37693 37694 if (error) { 37695 return _this.playlistRequestError(_this.request, _this.media().uri, 'HAVE_METADATA'); 37696 } 37697 37698 _this.haveMetadata(_this.request, _this.media().uri); 37699 }); 37700 }); 37701 37702 return _this; 37703 } 37704 37705 createClass$1(PlaylistLoader, [{ 37706 key: 'playlistRequestError', 37707 value: function playlistRequestError(xhr, url, startingState) { 37708 // any in-flight request is now finished 37709 this.request = null; 37710 37711 if (startingState) { 37712 this.state = startingState; 37713 } 37714 37715 this.error = { 37716 playlist: this.master.playlists[url], 37717 status: xhr.status, 37718 message: 'HLS playlist request error at URL: ' + url + '.', 37719 responseText: xhr.responseText, 37720 code: xhr.status >= 500 ? 4 : 2 37721 }; 37722 this.trigger('error'); 37723 } // update the playlist loader's state in response to a new or 37724 // updated playlist. 37725 37726 }, { 37727 key: 'haveMetadata', 37728 value: function haveMetadata(xhr, url) { 37729 var _this2 = this; // any in-flight request is now finished 37730 37731 37732 this.request = null; 37733 this.state = 'HAVE_METADATA'; 37734 var parser = new Parser(); // adding custom tag parsers 37735
vendor: 5,589 bytes, lines 37736-37900
37736 this.customTagParsers.forEach(function (customParser) { 37737 return parser.addParser(customParser); 37738 }); // adding custom tag mappers 37739 37740 this.customTagMappers.forEach(function (mapper) { 37741 return parser.addTagMapper(mapper); 37742 }); 37743 parser.push(xhr.responseText); 37744 parser.end(); 37745 parser.manifest.uri = url; // m3u8-parser does not attach an attributes property to media playlists so make 37746 // sure that the property is attached to avoid undefined reference errors 37747 37748 parser.manifest.attributes = parser.manifest.attributes || {}; // merge this playlist into the master 37749 37750 var update = updateMaster(this.master, parser.manifest); 37751 this.targetDuration = parser.manifest.targetDuration; 37752 37753 if (update) { 37754 this.master = update; 37755 this.media_ = this.master.playlists[parser.manifest.uri]; 37756 } else { 37757 this.trigger('playlistunchanged'); 37758 } // refresh live playlists after a target duration passes 37759 37760 37761 if (!this.media().endList) { 37762 window$1.clearTimeout(this.mediaUpdateTimeout); 37763 this.mediaUpdateTimeout = window$1.setTimeout(function () { 37764 _this2.trigger('mediaupdatetimeout'); 37765 }, refreshDelay(this.media(), !!update)); 37766 } 37767 37768 this.trigger('loadedplaylist'); 37769 } 37770 /** 37771 * Abort any outstanding work and clean up. 37772 */ 37773 37774 }, { 37775 key: 'dispose', 37776 value: function dispose() { 37777 this.stopRequest(); 37778 window$1.clearTimeout(this.mediaUpdateTimeout); 37779 window$1.clearTimeout(this.finalRenditionTimeout); 37780 } 37781 }, { 37782 key: 'stopRequest', 37783 value: function stopRequest() { 37784 if (this.request) { 37785 var oldRequest = this.request; 37786 this.request = null; 37787 oldRequest.onreadystatechange = null; 37788 oldRequest.abort(); 37789 } 37790 } 37791 /** 37792 * When called without any arguments, returns the currently 37793 * active media playlist. When called with a single argument, 37794 * triggers the playlist loader to asynchronously switch to the 37795 * specified media playlist. Calling this method while the 37796 * loader is in the HAVE_NOTHING causes an error to be emitted 37797 * but otherwise has no effect. 37798 * 37799 * @param {Object=} playlist the parsed media playlist 37800 * object to switch to 37801 * @param {Boolean=} is this the last available playlist 37802 * 37803 * @return {Playlist} the current loaded media 37804 */ 37805 37806 }, { 37807 key: 'media', 37808 value: function media(playlist, isFinalRendition) { 37809 var _this3 = this; // getter 37810 37811 37812 if (!playlist) { 37813 return this.media_; 37814 } // setter 37815 37816 37817 if (this.state === 'HAVE_NOTHING') { 37818 throw new Error('Cannot switch media playlist from ' + this.state); 37819 } // find the playlist object if the target playlist has been 37820 // specified by URI 37821 37822 37823 if (typeof playlist === 'string') { 37824 if (!this.master.playlists[playlist]) { 37825 throw new Error('Unknown playlist URI: ' + playlist); 37826 } 37827 37828 playlist = this.master.playlists[playlist]; 37829 } 37830 37831 window$1.clearTimeout(this.finalRenditionTimeout); 37832 37833 if (isFinalRendition) { 37834 var delay = playlist.targetDuration / 2 * 1000 || 5 * 1000; 37835 this.finalRenditionTimeout = window$1.setTimeout(this.media.bind(this, playlist, false), delay); 37836 return; 37837 } 37838 37839 var startingState = this.state; 37840 var mediaChange = !this.media_ || playlist.uri !== this.media_.uri; // switch to fully loaded playlists immediately 37841 37842 if (this.master.playlists[playlist.uri].endList) { 37843 // abort outstanding playlist requests 37844 if (this.request) { 37845 this.request.onreadystatechange = null; 37846 this.request.abort(); 37847 this.request = null; 37848 } 37849 37850 this.state = 'HAVE_METADATA'; 37851 this.media_ = playlist; // trigger media change if the active media has been updated 37852 37853 if (mediaChange) { 37854 this.trigger('mediachanging'); 37855 this.trigger('mediachange'); 37856 } 37857 37858 return; 37859 } // switching to the active playlist is a no-op 37860 37861 37862 if (!mediaChange) { 37863 return; 37864 } 37865 37866 this.state = 'SWITCHING_MEDIA'; // there is already an outstanding playlist request 37867 37868 if (this.request) { 37869 if (playlist.resolvedUri === this.request.url) { 37870 // requesting to switch to the same playlist multiple times 37871 // has no effect after the first 37872 return; 37873 } 37874 37875 this.request.onreadystatechange = null; 37876 this.request.abort(); 37877 this.request = null; 37878 } // request the new playlist 37879 37880 37881 if (this.media_) { 37882 this.trigger('mediachanging'); 37883 } 37884 37885 this.request = this.hls_.xhr({ 37886 uri: playlist.resolvedUri, 37887 withCredentials: this.withCredentials 37888 }, function (error, req) { 37889 // disposed 37890 if (!_this3.request) { 37891 return; 37892 } 37893 37894 playlist.resolvedUri = resolveManifestRedirect(_this3.handleManifestRedirects, playlist.resolvedUri, req); 37895 37896 if (error) { 37897 return _this3.playlistRequestError(_this3.request, playlist.uri, startingState); 37898 } 37899 37900 _this3.haveMetadata(req, playlist.uri);
vendor: 29,940 bytes, lines 37900-38875
37900 // fire loadedmetadata the first time a media playlist is loaded 37901 37902 37903 if (startingState === 'HAVE_MASTER') { 37904 _this3.trigger('loadedmetadata'); 37905 } else { 37906 _this3.trigger('mediachange'); 37907 } 37908 }); 37909 } 37910 /** 37911 * pause loading of the playlist 37912 */ 37913 37914 }, { 37915 key: 'pause', 37916 value: function pause() { 37917 this.stopRequest(); 37918 window$1.clearTimeout(this.mediaUpdateTimeout); 37919 37920 if (this.state === 'HAVE_NOTHING') { 37921 // If we pause the loader before any data has been retrieved, its as if we never 37922 // started, so reset to an unstarted state. 37923 this.started = false; 37924 } // Need to restore state now that no activity is happening 37925 37926 37927 if (this.state === 'SWITCHING_MEDIA') { 37928 // if the loader was in the process of switching media, it should either return to 37929 // HAVE_MASTER or HAVE_METADATA depending on if the loader has loaded a media 37930 // playlist yet. This is determined by the existence of loader.media_ 37931 if (this.media_) { 37932 this.state = 'HAVE_METADATA'; 37933 } else { 37934 this.state = 'HAVE_MASTER'; 37935 } 37936 } else if (this.state === 'HAVE_CURRENT_METADATA') { 37937 this.state = 'HAVE_METADATA'; 37938 } 37939 } 37940 /** 37941 * start loading of the playlist 37942 */ 37943 37944 }, { 37945 key: 'load', 37946 value: function load(isFinalRendition) { 37947 var _this4 = this; 37948 37949 window$1.clearTimeout(this.mediaUpdateTimeout); 37950 var media = this.media(); 37951 37952 if (isFinalRendition) { 37953 var delay = media ? media.targetDuration / 2 * 1000 : 5 * 1000; 37954 this.mediaUpdateTimeout = window$1.setTimeout(function () { 37955 return _this4.load(); 37956 }, delay); 37957 return; 37958 } 37959 37960 if (!this.started) { 37961 this.start(); 37962 return; 37963 } 37964 37965 if (media && !media.endList) { 37966 this.trigger('mediaupdatetimeout'); 37967 } else { 37968 this.trigger('loadedplaylist'); 37969 } 37970 } 37971 /** 37972 * start loading of the playlist 37973 */ 37974 37975 }, { 37976 key: 'start', 37977 value: function start() { 37978 var _this5 = this; 37979 37980 this.started = true; // request the specified URL 37981 37982 this.request = this.hls_.xhr({ 37983 uri: this.srcUrl, 37984 withCredentials: this.withCredentials 37985 }, function (error, req) { 37986 // disposed 37987 if (!_this5.request) { 37988 return; 37989 } // clear the loader's request reference 37990 37991 37992 _this5.request = null; 37993 37994 if (error) { 37995 _this5.error = { 37996 status: req.status, 37997 message: 'HLS playlist request error at URL: ' + _this5.srcUrl + '.', 37998 responseText: req.responseText, 37999 // MEDIA_ERR_NETWORK 38000 code: 2 38001 }; 38002 38003 if (_this5.state === 'HAVE_NOTHING') { 38004 _this5.started = false; 38005 } 38006 38007 return _this5.trigger('error'); 38008 } 38009 38010 var parser = new Parser(); // adding custom tag parsers 38011 38012 _this5.customTagParsers.forEach(function (customParser) { 38013 return parser.addParser(customParser); 38014 }); // adding custom tag mappers 38015 38016 38017 _this5.customTagMappers.forEach(function (mapper) { 38018 return parser.addTagMapper(mapper); 38019 }); 38020 38021 parser.push(req.responseText); 38022 parser.end(); 38023 _this5.state = 'HAVE_MASTER'; 38024 _this5.srcUrl = resolveManifestRedirect(_this5.handleManifestRedirects, _this5.srcUrl, req); 38025 parser.manifest.uri = _this5.srcUrl; // loaded a master playlist 38026 38027 if (parser.manifest.playlists) { 38028 _this5.master = parser.manifest; 38029 setupMediaPlaylists(_this5.master); 38030 resolveMediaGroupUris(_this5.master); 38031 38032 _this5.trigger('loadedplaylist'); 38033 38034 if (!_this5.request) { 38035 // no media playlist was specifically selected so start 38036 // from the first listed one 38037 _this5.media(parser.manifest.playlists[0]); 38038 } 38039 38040 return; 38041 } // loaded a media playlist 38042 // infer a master playlist if none was previously requested 38043 38044 38045 _this5.master = { 38046 mediaGroups: { 38047 'AUDIO': {}, 38048 'VIDEO': {}, 38049 'CLOSED-CAPTIONS': {}, 38050 'SUBTITLES': {} 38051 }, 38052 uri: window$1.location.href, 38053 playlists: [{ 38054 uri: _this5.srcUrl, 38055 id: 0, 38056 resolvedUri: _this5.srcUrl, 38057 // m3u8-parser does not attach an attributes property to media playlists so make 38058 // sure that the property is attached to avoid undefined reference errors 38059 attributes: {} 38060 }] 38061 }; 38062 _this5.master.playlists[_this5.srcUrl] = _this5.master.playlists[0]; 38063 38064 _this5.haveMetadata(req, _this5.srcUrl); 38065 38066 return _this5.trigger('loadedmetadata'); 38067 }); 38068 } 38069 }]); 38070 return PlaylistLoader; 38071 }(EventTarget$1); 38072 /** 38073 * @file playlist.js 38074 * 38075 * Playlist related utilities. 38076 */ 38077 38078 38079 var createTimeRange = videojs$1.createTimeRange; 38080 /** 38081 * walk backward until we find a duration we can use 38082 * or return a failure 38083 * 38084 * @param {Playlist} playlist the playlist to walk through 38085 * @param {Number} endSequence the mediaSequence to stop walking on 38086 */ 38087 38088 var backwardDuration = function backwardDuration(playlist, endSequence) { 38089 var result = 0; 38090 var i = endSequence - playlist.mediaSequence; // if a start time is available for segment immediately following 38091 // the interval, use it 38092 38093 var segment = playlist.segments[i]; // Walk backward until we find the latest segment with timeline 38094 // information that is earlier than endSequence 38095 38096 if (segment) { 38097 if (typeof segment.start !== 'undefined') { 38098 return { 38099 result: segment.start, 38100 precise: true 38101 }; 38102 } 38103 38104 if (typeof segment.end !== 'undefined') { 38105 return { 38106 result: segment.end - segment.duration, 38107 precise: true 38108 }; 38109 } 38110 } 38111 38112 while (i--) { 38113 segment = playlist.segments[i]; 38114 38115 if (typeof segment.end !== 'undefined') { 38116 return { 38117 result: result + segment.end, 38118 precise: true 38119 }; 38120 } 38121 38122 result += segment.duration; 38123 38124 if (typeof segment.start !== 'undefined') { 38125 return { 38126 result: result + segment.start, 38127 precise: true 38128 }; 38129 } 38130 } 38131 38132 return { 38133 result: result, 38134 precise: false 38135 }; 38136 }; 38137 /** 38138 * walk forward until we find a duration we can use 38139 * or return a failure 38140 * 38141 * @param {Playlist} playlist the playlist to walk through 38142 * @param {Number} endSequence the mediaSequence to stop walking on 38143 */ 38144 38145 38146 var forwardDuration = function forwardDuration(playlist, endSequence) { 38147 var result = 0; 38148 var segment = void 0; 38149 var i = endSequence - playlist.mediaSequence; // Walk forward until we find the earliest segment with timeline 38150 // information 38151 38152 for (; i < playlist.segments.length; i++) { 38153 segment = playlist.segments[i]; 38154 38155 if (typeof segment.start !== 'undefined') { 38156 return { 38157 result: segment.start - result, 38158 precise: true 38159 }; 38160 } 38161 38162 result += segment.duration; 38163 38164 if (typeof segment.end !== 'undefined') { 38165 return { 38166 result: segment.end - result, 38167 precise: true 38168 }; 38169 } 38170 } // indicate we didn't find a useful duration estimate 38171 38172 38173 return { 38174 result: -1, 38175 precise: false 38176 }; 38177 }; 38178 /** 38179 * Calculate the media duration from the segments associated with a 38180 * playlist. The duration of a subinterval of the available segments 38181 * may be calculated by specifying an end index. 38182 * 38183 * @param {Object} playlist a media playlist object 38184 * @param {Number=} endSequence an exclusive upper boundary 38185 * for the playlist. Defaults to playlist length. 38186 * @param {Number} expired the amount of time that has dropped 38187 * off the front of the playlist in a live scenario 38188 * @return {Number} the duration between the first available segment 38189 * and end index. 38190 */ 38191 38192 38193 var intervalDuration = function intervalDuration(playlist, endSequence, expired) { 38194 var backward = void 0; 38195 var forward = void 0; 38196 38197 if (typeof endSequence === 'undefined') { 38198 endSequence = playlist.mediaSequence + playlist.segments.length; 38199 } 38200 38201 if (endSequence < playlist.mediaSequence) { 38202 return 0; 38203 } // do a backward walk to estimate the duration 38204 38205 38206 backward = backwardDuration(playlist, endSequence); 38207 38208 if (backward.precise) { 38209 // if we were able to base our duration estimate on timing 38210 // information provided directly from the Media Source, return 38211 // it 38212 return backward.result; 38213 } // walk forward to see if a precise duration estimate can be made 38214 // that way 38215 38216 38217 forward = forwardDuration(playlist, endSequence); 38218 38219 if (forward.precise) { 38220 // we found a segment that has been buffered and so it's 38221 // position is known precisely 38222 return forward.result; 38223 } // return the less-precise, playlist-based duration estimate 38224 38225 38226 return backward.result + expired; 38227 }; 38228 /** 38229 * Calculates the duration of a playlist. If a start and end index 38230 * are specified, the duration will be for the subset of the media 38231 * timeline between those two indices. The total duration for live 38232 * playlists is always Infinity. 38233 * 38234 * @param {Object} playlist a media playlist object 38235 * @param {Number=} endSequence an exclusive upper 38236 * boundary for the playlist. Defaults to the playlist media 38237 * sequence number plus its length. 38238 * @param {Number=} expired the amount of time that has 38239 * dropped off the front of the playlist in a live scenario 38240 * @return {Number} the duration between the start index and end 38241 * index. 38242 */ 38243 38244 38245 var duration = function duration(playlist, endSequence, expired) { 38246 if (!playlist) { 38247 return 0; 38248 } 38249 38250 if (typeof expired !== 'number') { 38251 expired = 0; 38252 } // if a slice of the total duration is not requested, use 38253 // playlist-level duration indicators when they're present 38254 38255 38256 if (typeof endSequence === 'undefined') { 38257 // if present, use the duration specified in the playlist 38258 if (playlist.totalDuration) { 38259 return playlist.totalDuration; 38260 } // duration should be Infinity for live playlists 38261 38262 38263 if (!playlist.endList) { 38264 return window$1.Infinity; 38265 } 38266 } // calculate the total duration based on the segment durations 38267 38268 38269 return intervalDuration(playlist, endSequence, expired); 38270 }; 38271 /** 38272 * Calculate the time between two indexes in the current playlist 38273 * neight the start- nor the end-index need to be within the current 38274 * playlist in which case, the targetDuration of the playlist is used 38275 * to approximate the durations of the segments 38276 * 38277 * @param {Object} playlist a media playlist object 38278 * @param {Number} startIndex 38279 * @param {Number} endIndex 38280 * @return {Number} the number of seconds between startIndex and endIndex 38281 */ 38282 38283 38284 var sumDurations = function sumDurations(playlist, startIndex, endIndex) { 38285 var durations = 0; 38286 38287 if (startIndex > endIndex) { 38288 var _ref = [endIndex, startIndex]; 38289 startIndex = _ref[0]; 38290 endIndex = _ref[1]; 38291 } 38292 38293 if (startIndex < 0) { 38294 for (var i = startIndex; i < Math.min(0, endIndex); i++) { 38295 durations += playlist.targetDuration; 38296 } 38297 38298 startIndex = 0; 38299 } 38300 38301 for (var _i = startIndex; _i < endIndex; _i++) { 38302 durations += playlist.segments[_i].duration; 38303 } 38304 38305 return durations; 38306 }; 38307 /** 38308 * Determines the media index of the segment corresponding to the safe edge of the live 38309 * window which is the duration of the last segment plus 2 target durations from the end 38310 * of the playlist. 38311 * 38312 * @param {Object} playlist 38313 * a media playlist object 38314 * @return {Number} 38315 * The media index of the segment at the safe live point. 0 if there is no "safe" 38316 * point. 38317 * @function safeLiveIndex 38318 */ 38319 38320 38321 var safeLiveIndex = function safeLiveIndex(playlist) { 38322 if (!playlist.segments.length) { 38323 return 0; 38324 } 38325 38326 var i = playlist.segments.length - 1; 38327 var distanceFromEnd = playlist.segments[i].duration || playlist.targetDuration; 38328 var safeDistance = distanceFromEnd + playlist.targetDuration * 2; 38329 38330 while (i--) { 38331 distanceFromEnd += playlist.segments[i].duration; 38332 38333 if (distanceFromEnd >= safeDistance) { 38334 break; 38335 } 38336 } 38337 38338 return Math.max(0, i); 38339 }; 38340 /** 38341 * Calculates the playlist end time 38342 * 38343 * @param {Object} playlist a media playlist object 38344 * @param {Number=} expired the amount of time that has 38345 * dropped off the front of the playlist in a live scenario 38346 * @param {Boolean|false} useSafeLiveEnd a boolean value indicating whether or not the 38347 * playlist end calculation should consider the safe live end 38348 * (truncate the playlist end by three segments). This is normally 38349 * used for calculating the end of the playlist's seekable range. 38350 * @returns {Number} the end time of playlist 38351 * @function playlistEnd 38352 */ 38353 38354 38355 var playlistEnd = function playlistEnd(playlist, expired, useSafeLiveEnd) { 38356 if (!playlist || !playlist.segments) { 38357 return null; 38358 } 38359 38360 if (playlist.endList) { 38361 return duration(playlist); 38362 } 38363 38364 if (expired === null) { 38365 return null; 38366 } 38367 38368 expired = expired || 0; 38369 var endSequence = useSafeLiveEnd ? safeLiveIndex(playlist) : playlist.segments.length; 38370 return intervalDuration(playlist, playlist.mediaSequence + endSequence, expired); 38371 }; 38372 /** 38373 * Calculates the interval of time that is currently seekable in a 38374 * playlist. The returned time ranges are relative to the earliest 38375 * moment in the specified playlist that is still available. A full 38376 * seekable implementation for live streams would need to offset 38377 * these values by the duration of content that has expired from the 38378 * stream. 38379 * 38380 * @param {Object} playlist a media playlist object 38381 * dropped off the front of the playlist in a live scenario 38382 * @param {Number=} expired the amount of time that has 38383 * dropped off the front of the playlist in a live scenario 38384 * @return {TimeRanges} the periods of time that are valid targets 38385 * for seeking 38386 */ 38387 38388 38389 var seekable = function seekable(playlist, expired) { 38390 var useSafeLiveEnd = true; 38391 var seekableStart = expired || 0; 38392 var seekableEnd = playlistEnd(playlist, expired, useSafeLiveEnd); 38393 38394 if (seekableEnd === null) { 38395 return createTimeRange(); 38396 } 38397 38398 return createTimeRange(seekableStart, seekableEnd); 38399 }; 38400 38401 var isWholeNumber = function isWholeNumber(num) { 38402 return num - Math.floor(num) === 0; 38403 }; 38404 38405 var roundSignificantDigit = function roundSignificantDigit(increment, num) { 38406 // If we have a whole number, just add 1 to it 38407 if (isWholeNumber(num)) { 38408 return num + increment * 0.1; 38409 } 38410 38411 var numDecimalDigits = num.toString().split('.')[1].length; 38412 38413 for (var i = 1; i <= numDecimalDigits; i++) { 38414 var scale = Math.pow(10, i); 38415 var temp = num * scale; 38416 38417 if (isWholeNumber(temp) || i === numDecimalDigits) { 38418 return (temp + increment) / scale; 38419 } 38420 } 38421 }; 38422 38423 var ceilLeastSignificantDigit = roundSignificantDigit.bind(null, 1); 38424 var floorLeastSignificantDigit = roundSignificantDigit.bind(null, -1); 38425 /** 38426 * Determine the index and estimated starting time of the segment that 38427 * contains a specified playback position in a media playlist. 38428 * 38429 * @param {Object} playlist the media playlist to query 38430 * @param {Number} currentTime The number of seconds since the earliest 38431 * possible position to determine the containing segment for 38432 * @param {Number} startIndex 38433 * @param {Number} startTime 38434 * @return {Object} 38435 */ 38436 38437 var getMediaInfoForTime = function getMediaInfoForTime(playlist, currentTime, startIndex, startTime) { 38438 var i = void 0; 38439 var segment = void 0; 38440 var numSegments = playlist.segments.length; 38441 var time = currentTime - startTime; 38442 38443 if (time < 0) { 38444 // Walk backward from startIndex in the playlist, adding durations 38445 // until we find a segment that contains `time` and return it 38446 if (startIndex > 0) { 38447 for (i = startIndex - 1; i >= 0; i--) { 38448 segment = playlist.segments[i]; 38449 time += floorLeastSignificantDigit(segment.duration); 38450 38451 if (time > 0) { 38452 return { 38453 mediaIndex: i, 38454 startTime: startTime - sumDurations(playlist, startIndex, i) 38455 }; 38456 } 38457 } 38458 } // We were unable to find a good segment within the playlist 38459 // so select the first segment 38460 38461 38462 return { 38463 mediaIndex: 0, 38464 startTime: currentTime 38465 }; 38466 } // When startIndex is negative, we first walk forward to first segment 38467 // adding target durations. If we "run out of time" before getting to 38468 // the first segment, return the first segment 38469 38470 38471 if (startIndex < 0) { 38472 for (i = startIndex; i < 0; i++) { 38473 time -= playlist.targetDuration; 38474 38475 if (time < 0) { 38476 return { 38477 mediaIndex: 0, 38478 startTime: currentTime 38479 }; 38480 } 38481 } 38482 38483 startIndex = 0; 38484 } // Walk forward from startIndex in the playlist, subtracting durations 38485 // until we find a segment that contains `time` and return it 38486 38487 38488 for (i = startIndex; i < numSegments; i++) { 38489 segment = playlist.segments[i]; 38490 time -= ceilLeastSignificantDigit(segment.duration); 38491 38492 if (time < 0) { 38493 return { 38494 mediaIndex: i, 38495 startTime: startTime + sumDurations(playlist, startIndex, i) 38496 }; 38497 } 38498 } // We are out of possible candidates so load the last one... 38499 38500 38501 return { 38502 mediaIndex: numSegments - 1, 38503 startTime: currentTime 38504 }; 38505 }; 38506 /** 38507 * Check whether the playlist is blacklisted or not. 38508 * 38509 * @param {Object} playlist the media playlist object 38510 * @return {boolean} whether the playlist is blacklisted or not 38511 * @function isBlacklisted 38512 */ 38513 38514 38515 var isBlacklisted = function isBlacklisted(playlist) { 38516 return playlist.excludeUntil && playlist.excludeUntil > Date.now(); 38517 }; 38518 /** 38519 * Check whether the playlist is compatible with current playback configuration or has 38520 * been blacklisted permanently for being incompatible. 38521 * 38522 * @param {Object} playlist the media playlist object 38523 * @return {boolean} whether the playlist is incompatible or not 38524 * @function isIncompatible 38525 */ 38526 38527 38528 var isIncompatible = function isIncompatible(playlist) { 38529 return playlist.excludeUntil && playlist.excludeUntil === Infinity; 38530 }; 38531 /** 38532 * Check whether the playlist is enabled or not. 38533 * 38534 * @param {Object} playlist the media playlist object 38535 * @return {boolean} whether the playlist is enabled or not 38536 * @function isEnabled 38537 */ 38538 38539 38540 var isEnabled = function isEnabled(playlist) { 38541 var blacklisted = isBlacklisted(playlist); 38542 return !playlist.disabled && !blacklisted; 38543 }; 38544 /** 38545 * Check whether the playlist has been manually disabled through the representations api. 38546 * 38547 * @param {Object} playlist the media playlist object 38548 * @return {boolean} whether the playlist is disabled manually or not 38549 * @function isDisabled 38550 */ 38551 38552 38553 var isDisabled = function isDisabled(playlist) { 38554 return playlist.disabled; 38555 }; 38556 /** 38557 * Returns whether the current playlist is an AES encrypted HLS stream 38558 * 38559 * @return {Boolean} true if it's an AES encrypted HLS stream 38560 */ 38561 38562 38563 var isAes = function isAes(media) { 38564 for (var i = 0; i < media.segments.length; i++) { 38565 if (media.segments[i].key) { 38566 return true; 38567 } 38568 } 38569 38570 return false; 38571 }; 38572 /** 38573 * Returns whether the current playlist contains fMP4 38574 * 38575 * @return {Boolean} true if the playlist contains fMP4 38576 */ 38577 38578 38579 var isFmp4 = function isFmp4(media) { 38580 for (var i = 0; i < media.segments.length; i++) { 38581 if (media.segments[i].map) { 38582 return true; 38583 } 38584 } 38585 38586 return false; 38587 }; 38588 /** 38589 * Checks if the playlist has a value for the specified attribute 38590 * 38591 * @param {String} attr 38592 * Attribute to check for 38593 * @param {Object} playlist 38594 * The media playlist object 38595 * @return {Boolean} 38596 * Whether the playlist contains a value for the attribute or not 38597 * @function hasAttribute 38598 */ 38599 38600 38601 var hasAttribute = function hasAttribute(attr, playlist) { 38602 return playlist.attributes && playlist.attributes[attr]; 38603 }; 38604 /** 38605 * Estimates the time required to complete a segment download from the specified playlist 38606 * 38607 * @param {Number} segmentDuration 38608 * Duration of requested segment 38609 * @param {Number} bandwidth 38610 * Current measured bandwidth of the player 38611 * @param {Object} playlist 38612 * The media playlist object 38613 * @param {Number=} bytesReceived 38614 * Number of bytes already received for the request. Defaults to 0 38615 * @return {Number|NaN} 38616 * The estimated time to request the segment. NaN if bandwidth information for 38617 * the given playlist is unavailable 38618 * @function estimateSegmentRequestTime 38619 */ 38620 38621 38622 var estimateSegmentRequestTime = function estimateSegmentRequestTime(segmentDuration, bandwidth, playlist) { 38623 var bytesReceived = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0; 38624 38625 if (!hasAttribute('BANDWIDTH', playlist)) { 38626 return NaN; 38627 } 38628 38629 var size = segmentDuration * playlist.attributes.BANDWIDTH; 38630 return (size - bytesReceived * 8) / bandwidth; 38631 }; 38632 /* 38633 * Returns whether the current playlist is the lowest rendition 38634 * 38635 * @return {Boolean} true if on lowest rendition 38636 */ 38637 38638 38639 var isLowestEnabledRendition = function isLowestEnabledRendition(master, media) { 38640 if (master.playlists.length === 1) { 38641 return true; 38642 } 38643 38644 var currentBandwidth = media.attributes.BANDWIDTH || Number.MAX_VALUE; 38645 return master.playlists.filter(function (playlist) { 38646 if (!isEnabled(playlist)) { 38647 return false; 38648 } 38649 38650 return (playlist.attributes.BANDWIDTH || 0) < currentBandwidth; 38651 }).length === 0; 38652 }; // exports 38653 38654 38655 var Playlist = { 38656 duration: duration, 38657 seekable: seekable, 38658 safeLiveIndex: safeLiveIndex, 38659 getMediaInfoForTime: getMediaInfoForTime, 38660 isEnabled: isEnabled, 38661 isDisabled: isDisabled, 38662 isBlacklisted: isBlacklisted, 38663 isIncompatible: isIncompatible, 38664 playlistEnd: playlistEnd, 38665 isAes: isAes, 38666 isFmp4: isFmp4, 38667 hasAttribute: hasAttribute, 38668 estimateSegmentRequestTime: estimateSegmentRequestTime, 38669 isLowestEnabledRendition: isLowestEnabledRendition 38670 }; 38671 /** 38672 * @file xhr.js 38673 */ 38674 38675 var videojsXHR = videojs$1.xhr, 38676 mergeOptions$1$1 = videojs$1.mergeOptions; 38677 38678 var xhrFactory = function xhrFactory() { 38679 var xhr = function XhrFunction(options, callback) { 38680 // Add a default timeout for all hls requests 38681 options = mergeOptions$1$1({ 38682 timeout: 45e3 38683 }, options); // Allow an optional user-specified function to modify the option 38684 // object before we construct the xhr request 38685 38686 var beforeRequest = XhrFunction.beforeRequest || videojs$1.Hls.xhr.beforeRequest; 38687 38688 if (beforeRequest && typeof beforeRequest === 'function') { 38689 var newOptions = beforeRequest(options); 38690 38691 if (newOptions) { 38692 options = newOptions; 38693 } 38694 } 38695 38696 var request = videojsXHR(options, function (error, response) { 38697 var reqResponse = request.response; 38698 38699 if (!error && reqResponse) { 38700 request.responseTime = Date.now(); 38701 request.roundTripTime = request.responseTime - request.requestTime; 38702 request.bytesReceived = reqResponse.byteLength || reqResponse.length; 38703 38704 if (!request.bandwidth) { 38705 request.bandwidth = Math.floor(request.bytesReceived / request.roundTripTime * 8 * 1000); 38706 } 38707 } 38708 38709 if (response.headers) { 38710 request.responseHeaders = response.headers; 38711 } // videojs.xhr now uses a specific code on the error 38712 // object to signal that a request has timed out instead 38713 // of setting a boolean on the request object 38714 38715 38716 if (error && error.code === 'ETIMEDOUT') { 38717 request.timedout = true; 38718 } // videojs.xhr no longer considers status codes outside of 200 and 0 38719 // (for file uris) to be errors, but the old XHR did, so emulate that 38720 // behavior. Status 206 may be used in response to byterange requests. 38721 38722 38723 if (!error && !request.aborted && response.statusCode !== 200 && response.statusCode !== 206 && response.statusCode !== 0) { 38724 error = new Error('XHR Failed with a response of: ' + (request && (reqResponse || request.responseText))); 38725 } 38726 38727 callback(error, request); 38728 }); 38729 var originalAbort = request.abort; 38730 38731 request.abort = function () { 38732 request.aborted = true; 38733 return originalAbort.apply(request, arguments); 38734 }; 38735 38736 request.uri = options.uri; 38737 request.requestTime = Date.now(); 38738 return request; 38739 }; 38740 38741 return xhr; 38742 }; 38743 /** 38744 * Turns segment byterange into a string suitable for use in 38745 * HTTP Range requests 38746 * 38747 * @param {Object} byterange - an object with two values defining the start and end 38748 * of a byte-range 38749 */ 38750 38751 38752 var byterangeStr = function byterangeStr(byterange) { 38753 var byterangeStart = void 0; 38754 var byterangeEnd = void 0; // `byterangeEnd` is one less than `offset + length` because the HTTP range 38755 // header uses inclusive ranges 38756 38757 byterangeEnd = byterange.offset + byterange.length - 1; 38758 byterangeStart = byterange.offset; 38759 return 'bytes=' + byterangeStart + '-' + byterangeEnd; 38760 }; 38761 /** 38762 * Defines headers for use in the xhr request for a particular segment. 38763 * 38764 * @param {Object} segment - a simplified copy of the segmentInfo object 38765 * from SegmentLoader 38766 */ 38767 38768 38769 var segmentXhrHeaders = function segmentXhrHeaders(segment) { 38770 var headers = {}; 38771 38772 if (segment.byterange) { 38773 headers.Range = byterangeStr(segment.byterange); 38774 } 38775 38776 return headers; 38777 }; 38778 /** 38779 * @file bin-utils.js 38780 */ 38781 38782 /** 38783 * convert a TimeRange to text 38784 * 38785 * @param {TimeRange} range the timerange to use for conversion 38786 * @param {Number} i the iterator on the range to convert 38787 */ 38788 38789 38790 var textRange = function textRange(range, i) { 38791 return range.start(i) + '-' + range.end(i); 38792 }; 38793 /** 38794 * format a number as hex string 38795 * 38796 * @param {Number} e The number 38797 * @param {Number} i the iterator 38798 */ 38799 38800 38801 var formatHexString = function formatHexString(e, i) { 38802 var value = e.toString(16); 38803 return '00'.substring(0, 2 - value.length) + value + (i % 2 ? ' ' : ''); 38804 }; 38805 38806 var formatAsciiString = function formatAsciiString(e) { 38807 if (e >= 0x20 && e < 0x7e) { 38808 return String.fromCharCode(e); 38809 } 38810 38811 return '.'; 38812 }; 38813 /** 38814 * Creates an object for sending to a web worker modifying properties that are TypedArrays 38815 * into a new object with seperated properties for the buffer, byteOffset, and byteLength. 38816 * 38817 * @param {Object} message 38818 * Object of properties and values to send to the web worker 38819 * @return {Object} 38820 * Modified message with TypedArray values expanded 38821 * @function createTransferableMessage 38822 */ 38823 38824 38825 var createTransferableMessage = function createTransferableMessage(message) { 38826 var transferable = {}; 38827 Object.keys(message).forEach(function (key) { 38828 var value = message[key]; 38829 38830 if (ArrayBuffer.isView(value)) { 38831 transferable[key] = { 38832 bytes: value.buffer, 38833 byteOffset: value.byteOffset, 38834 byteLength: value.byteLength 38835 }; 38836 } else { 38837 transferable[key] = value; 38838 } 38839 }); 38840 return transferable; 38841 }; 38842 /** 38843 * Returns a unique string identifier for a media initialization 38844 * segment. 38845 */ 38846 38847 38848 var initSegmentId = function initSegmentId(initSegment) { 38849 var byterange = initSegment.byterange || { 38850 length: Infinity, 38851 offset: 0 38852 }; 38853 return [byterange.length, byterange.offset, initSegment.resolvedUri].join(','); 38854 }; 38855 /** 38856 * Returns a unique string identifier for a media segment key. 38857 */ 38858 38859 38860 var segmentKeyId = function segmentKeyId(key) { 38861 return key.resolvedUri; 38862 }; 38863 /** 38864 * utils to help dump binary data to the console 38865 */ 38866 38867 38868 var hexDump = function hexDump(data) { 38869 var bytes = Array.prototype.slice.call(data); 38870 var step = 16; 38871 var result = ''; 38872 var hex = void 0; 38873 var ascii = void 0; 38874 38875 for (var j = 0;
vendor: 19,733 bytes, lines 38875-39435
38875 j < bytes.length / step; j++) { 38876 hex = bytes.slice(j * step, j * step + step).map(formatHexString).join(''); 38877 ascii = bytes.slice(j * step, j * step + step).map(formatAsciiString).join(''); 38878 result += hex + ' ' + ascii + '\n'; 38879 } 38880 38881 return result; 38882 }; 38883 38884 var tagDump = function tagDump(_ref) { 38885 var bytes = _ref.bytes; 38886 return hexDump(bytes); 38887 }; 38888 38889 var textRanges = function textRanges(ranges) { 38890 var result = ''; 38891 var i = void 0; 38892 38893 for (i = 0; i < ranges.length; i++) { 38894 result += textRange(ranges, i) + ' '; 38895 } 38896 38897 return result; 38898 }; 38899 38900 var utils$1 = 38901 /*#__PURE__*/ 38902 Object.freeze({ 38903 createTransferableMessage: createTransferableMessage, 38904 initSegmentId: initSegmentId, 38905 segmentKeyId: segmentKeyId, 38906 hexDump: hexDump, 38907 tagDump: tagDump, 38908 textRanges: textRanges 38909 }); // TODO handle fmp4 case where the timing info is accurate and doesn't involve transmux 38910 // Add 25% to the segment duration to account for small discrepencies in segment timing. 38911 // 25% was arbitrarily chosen, and may need to be refined over time. 38912 38913 var SEGMENT_END_FUDGE_PERCENT = 0.25; 38914 /** 38915 * Converts a player time (any time that can be gotten/set from player.currentTime(), 38916 * e.g., any time within player.seekable().start(0) to player.seekable().end(0)) to a 38917 * program time (any time referencing the real world (e.g., EXT-X-PROGRAM-DATE-TIME)). 38918 * 38919 * The containing segment is required as the EXT-X-PROGRAM-DATE-TIME serves as an "anchor 38920 * point" (a point where we have a mapping from program time to player time, with player 38921 * time being the post transmux start of the segment). 38922 * 38923 * For more details, see [this doc](../../docs/program-time-from-player-time.md). 38924 * 38925 * @param {Number} playerTime the player time 38926 * @param {Object} segment the segment which contains the player time 38927 * @return {Date} program time 38928 */ 38929 38930 var playerTimeToProgramTime = function playerTimeToProgramTime(playerTime, segment) { 38931 if (!segment.dateTimeObject) { 38932 // Can't convert without an "anchor point" for the program time (i.e., a time that can 38933 // be used to map the start of a segment with a real world time). 38934 return null; 38935 } 38936 38937 var transmuxerPrependedSeconds = segment.videoTimingInfo.transmuxerPrependedSeconds; 38938 var transmuxedStart = segment.videoTimingInfo.transmuxedPresentationStart; // get the start of the content from before old content is prepended 38939 38940 var startOfSegment = transmuxedStart + transmuxerPrependedSeconds; 38941 var offsetFromSegmentStart = playerTime - startOfSegment; 38942 return new Date(segment.dateTimeObject.getTime() + offsetFromSegmentStart * 1000); 38943 }; 38944 38945 var originalSegmentVideoDuration = function originalSegmentVideoDuration(videoTimingInfo) { 38946 return videoTimingInfo.transmuxedPresentationEnd - videoTimingInfo.transmuxedPresentationStart - videoTimingInfo.transmuxerPrependedSeconds; 38947 }; 38948 /** 38949 * Finds a segment that contains the time requested given as an ISO-8601 string. The 38950 * returned segment might be an estimate or an accurate match. 38951 * 38952 * @param {String} programTime The ISO-8601 programTime to find a match for 38953 * @param {Object} playlist A playlist object to search within 38954 */ 38955 38956 38957 var findSegmentForProgramTime = function findSegmentForProgramTime(programTime, playlist) { 38958 // Assumptions: 38959 // - verifyProgramDateTimeTags has already been run 38960 // - live streams have been started 38961 var dateTimeObject = void 0; 38962 38963 try { 38964 dateTimeObject = new Date(programTime); 38965 } catch (e) { 38966 return null; 38967 } 38968 38969 if (!playlist || !playlist.segments || playlist.segments.length === 0) { 38970 return null; 38971 } 38972 38973 var segment = playlist.segments[0]; 38974 38975 if (dateTimeObject < segment.dateTimeObject) { 38976 // Requested time is before stream start. 38977 return null; 38978 } 38979 38980 for (var i = 0; i < playlist.segments.length - 1; i++) { 38981 segment = playlist.segments[i]; 38982 var nextSegmentStart = playlist.segments[i + 1].dateTimeObject; 38983 38984 if (dateTimeObject < nextSegmentStart) { 38985 break; 38986 } 38987 } 38988 38989 var lastSegment = playlist.segments[playlist.segments.length - 1]; 38990 var lastSegmentStart = lastSegment.dateTimeObject; 38991 var lastSegmentDuration = lastSegment.videoTimingInfo ? originalSegmentVideoDuration(lastSegment.videoTimingInfo) : lastSegment.duration + lastSegment.duration * SEGMENT_END_FUDGE_PERCENT; 38992 var lastSegmentEnd = new Date(lastSegmentStart.getTime() + lastSegmentDuration * 1000); 38993 38994 if (dateTimeObject > lastSegmentEnd) { 38995 // Beyond the end of the stream, or our best guess of the end of the stream. 38996 return null; 38997 } 38998 38999 if (dateTimeObject > lastSegmentStart) { 39000 segment = lastSegment; 39001 } 39002 39003 return { 39004 segment: segment, 39005 estimatedStart: segment.videoTimingInfo ? segment.videoTimingInfo.transmuxedPresentationStart : Playlist.duration(playlist, playlist.mediaSequence + playlist.segments.indexOf(segment)), 39006 // Although, given that all segments have accurate date time objects, the segment 39007 // selected should be accurate, unless the video has been transmuxed at some point 39008 // (determined by the presence of the videoTimingInfo object), the segment's "player 39009 // time" (the start time in the player) can't be considered accurate. 39010 type: segment.videoTimingInfo ? 'accurate' : 'estimate' 39011 }; 39012 }; 39013 /** 39014 * Finds a segment that contains the given player time(in seconds). 39015 * 39016 * @param {Number} time The player time to find a match for 39017 * @param {Object} playlist A playlist object to search within 39018 */ 39019 39020 39021 var findSegmentForPlayerTime = function findSegmentForPlayerTime(time, playlist) { 39022 // Assumptions: 39023 // - there will always be a segment.duration 39024 // - we can start from zero 39025 // - segments are in time order 39026 if (!playlist || !playlist.segments || playlist.segments.length === 0) { 39027 return null; 39028 } 39029 39030 var segmentEnd = 0; 39031 var segment = void 0; 39032 39033 for (var i = 0; i < playlist.segments.length; i++) { 39034 segment = playlist.segments[i]; // videoTimingInfo is set after the segment is downloaded and transmuxed, and 39035 // should contain the most accurate values we have for the segment's player times. 39036 // 39037 // Use the accurate transmuxedPresentationEnd value if it is available, otherwise fall 39038 // back to an estimate based on the manifest derived (inaccurate) segment.duration, to 39039 // calculate an end value. 39040 39041 segmentEnd = segment.videoTimingInfo ? segment.videoTimingInfo.transmuxedPresentationEnd : segmentEnd + segment.duration; 39042 39043 if (time <= segmentEnd) { 39044 break; 39045 } 39046 } 39047 39048 var lastSegment = playlist.segments[playlist.segments.length - 1]; 39049 39050 if (lastSegment.videoTimingInfo && lastSegment.videoTimingInfo.transmuxedPresentationEnd < time) { 39051 // The time requested is beyond the stream end. 39052 return null; 39053 } 39054 39055 if (time > segmentEnd) { 39056 // The time is within or beyond the last segment. 39057 // 39058 // Check to see if the time is beyond a reasonable guess of the end of the stream. 39059 if (time > segmentEnd + lastSegment.duration * SEGMENT_END_FUDGE_PERCENT) { 39060 // Technically, because the duration value is only an estimate, the time may still 39061 // exist in the last segment, however, there isn't enough information to make even 39062 // a reasonable estimate. 39063 return null; 39064 } 39065 39066 segment = lastSegment; 39067 } 39068 39069 return { 39070 segment: segment, 39071 estimatedStart: segment.videoTimingInfo ? segment.videoTimingInfo.transmuxedPresentationStart : segmentEnd - segment.duration, 39072 // Because videoTimingInfo is only set after transmux, it is the only way to get 39073 // accurate timing values. 39074 type: segment.videoTimingInfo ? 'accurate' : 'estimate' 39075 }; 39076 }; 39077 /** 39078 * Gives the offset of the comparisonTimestamp from the programTime timestamp in seconds. 39079 * If the offset returned is positive, the programTime occurs after the 39080 * comparisonTimestamp. 39081 * If the offset is negative, the programTime occurs before the comparisonTimestamp. 39082 * 39083 * @param {String} comparisonTimeStamp An ISO-8601 timestamp to compare against 39084 * @param {String} programTime The programTime as an ISO-8601 string 39085 * @return {Number} offset 39086 */ 39087 39088 39089 var getOffsetFromTimestamp = function getOffsetFromTimestamp(comparisonTimeStamp, programTime) { 39090 var segmentDateTime = void 0; 39091 var programDateTime = void 0; 39092 39093 try { 39094 segmentDateTime = new Date(comparisonTimeStamp); 39095 programDateTime = new Date(programTime); 39096 } catch (e) {// TODO handle error 39097 } 39098 39099 var segmentTimeEpoch = segmentDateTime.getTime(); 39100 var programTimeEpoch = programDateTime.getTime(); 39101 return (programTimeEpoch - segmentTimeEpoch) / 1000; 39102 }; 39103 /** 39104 * Checks that all segments in this playlist have programDateTime tags. 39105 * 39106 * @param {Object} playlist A playlist object 39107 */ 39108 39109 39110 var verifyProgramDateTimeTags = function verifyProgramDateTimeTags(playlist) { 39111 if (!playlist.segments || playlist.segments.length === 0) { 39112 return false; 39113 } 39114 39115 for (var i = 0; i < playlist.segments.length; i++) { 39116 var segment = playlist.segments[i]; 39117 39118 if (!segment.dateTimeObject) { 39119 return false; 39120 } 39121 } 39122 39123 return true; 39124 }; 39125 /** 39126 * Returns the programTime of the media given a playlist and a playerTime. 39127 * The playlist must have programDateTime tags for a programDateTime tag to be returned. 39128 * If the segments containing the time requested have not been buffered yet, an estimate 39129 * may be returned to the callback. 39130 * 39131 * @param {Object} args 39132 * @param {Object} args.playlist A playlist object to search within 39133 * @param {Number} time A playerTime in seconds 39134 * @param {Function} callback(err, programTime) 39135 * @returns {String} err.message A detailed error message 39136 * @returns {Object} programTime 39137 * @returns {Number} programTime.mediaSeconds The streamTime in seconds 39138 * @returns {String} programTime.programDateTime The programTime as an ISO-8601 String 39139 */ 39140 39141 39142 var getProgramTime = function getProgramTime(_ref) { 39143 var playlist = _ref.playlist, 39144 _ref$time = _ref.time, 39145 time = _ref$time === undefined ? undefined : _ref$time, 39146 callback = _ref.callback; 39147 39148 if (!callback) { 39149 throw new Error('getProgramTime: callback must be provided'); 39150 } 39151 39152 if (!playlist || time === undefined) { 39153 return callback({ 39154 message: 'getProgramTime: playlist and time must be provided' 39155 }); 39156 } 39157 39158 var matchedSegment = findSegmentForPlayerTime(time, playlist); 39159 39160 if (!matchedSegment) { 39161 return callback({ 39162 message: 'valid programTime was not found' 39163 }); 39164 } 39165 39166 if (matchedSegment.type === 'estimate') { 39167 return callback({ 39168 message: 'Accurate programTime could not be determined.' + ' Please seek to e.seekTime and try again', 39169 seekTime: matchedSegment.estimatedStart 39170 }); 39171 } 39172 39173 var programTimeObject = { 39174 mediaSeconds: time 39175 }; 39176 var programTime = playerTimeToProgramTime(time, matchedSegment.segment); 39177 39178 if (programTime) { 39179 programTimeObject.programDateTime = programTime.toISOString(); 39180 } 39181 39182 return callback(null, programTimeObject); 39183 }; 39184 /** 39185 * Seeks in the player to a time that matches the given programTime ISO-8601 string. 39186 * 39187 * @param {Object} args 39188 * @param {String} args.programTime A programTime to seek to as an ISO-8601 String 39189 * @param {Object} args.playlist A playlist to look within 39190 * @param {Number} args.retryCount The number of times to try for an accurate seek. Default is 2. 39191 * @param {Function} args.seekTo A method to perform a seek 39192 * @param {Boolean} args.pauseAfterSeek Whether to end in a paused state after seeking. Default is true. 39193 * @param {Object} args.tech The tech to seek on 39194 * @param {Function} args.callback(err, newTime) A callback to return the new time to 39195 * @returns {String} err.message A detailed error message 39196 * @returns {Number} newTime The exact time that was seeked to in seconds 39197 */ 39198 39199 39200 var seekToProgramTime = function seekToProgramTime(_ref2) { 39201 var programTime = _ref2.programTime, 39202 playlist = _ref2.playlist, 39203 _ref2$retryCount = _ref2.retryCount, 39204 retryCount = _ref2$retryCount === undefined ? 2 : _ref2$retryCount, 39205 seekTo = _ref2.seekTo, 39206 _ref2$pauseAfterSeek = _ref2.pauseAfterSeek, 39207 pauseAfterSeek = _ref2$pauseAfterSeek === undefined ? true : _ref2$pauseAfterSeek, 39208 tech = _ref2.tech, 39209 callback = _ref2.callback; 39210 39211 if (!callback) { 39212 throw new Error('seekToProgramTime: callback must be provided'); 39213 } 39214 39215 if (typeof programTime === 'undefined' || !playlist || !seekTo) { 39216 return callback({ 39217 message: 'seekToProgramTime: programTime, seekTo and playlist must be provided' 39218 }); 39219 } 39220 39221 if (!playlist.endList && !tech.hasStarted_) { 39222 return callback({ 39223 message: 'player must be playing a live stream to start buffering' 39224 }); 39225 } 39226 39227 if (!verifyProgramDateTimeTags(playlist)) { 39228 return callback({ 39229 message: 'programDateTime tags must be provided in the manifest ' + playlist.resolvedUri 39230 }); 39231 } 39232 39233 var matchedSegment = findSegmentForProgramTime(programTime, playlist); // no match 39234 39235 if (!matchedSegment) { 39236 return callback({ 39237 message: programTime + ' was not found in the stream' 39238 }); 39239 } 39240 39241 var segment = matchedSegment.segment; 39242 var mediaOffset = getOffsetFromTimestamp(segment.dateTimeObject, programTime); 39243 39244 if (matchedSegment.type === 'estimate') { 39245 // we've run out of retries 39246 if (retryCount === 0) { 39247 return callback({ 39248 message: programTime + ' is not buffered yet. Try again' 39249 }); 39250 } 39251 39252 seekTo(matchedSegment.estimatedStart + mediaOffset); 39253 tech.one('seeked', function () { 39254 seekToProgramTime({ 39255 programTime: programTime, 39256 playlist: playlist, 39257 retryCount: retryCount - 1, 39258 seekTo: seekTo, 39259 pauseAfterSeek: pauseAfterSeek, 39260 tech: tech, 39261 callback: callback 39262 }); 39263 }); 39264 return; 39265 } // Since the segment.start value is determined from the buffered end or ending time 39266 // of the prior segment, the seekToTime doesn't need to account for any transmuxer 39267 // modifications. 39268 39269 39270 var seekToTime = segment.start + mediaOffset; 39271 39272 var seekedCallback = function seekedCallback() { 39273 return callback(null, tech.currentTime()); 39274 }; // listen for seeked event 39275 39276 39277 tech.one('seeked', seekedCallback); // pause before seeking as video.js will restore this state 39278 39279 if (pauseAfterSeek) { 39280 tech.pause(); 39281 } 39282 39283 seekTo(seekToTime); 39284 }; 39285 /** 39286 * ranges 39287 * 39288 * Utilities for working with TimeRanges. 39289 * 39290 */ 39291 // Fudge factor to account for TimeRanges rounding 39292 39293 39294 var TIME_FUDGE_FACTOR = 1 / 30; // Comparisons between time values such as current time and the end of the buffered range 39295 // can be misleading because of precision differences or when the current media has poorly 39296 // aligned audio and video, which can cause values to be slightly off from what you would 39297 // expect. This value is what we consider to be safe to use in such comparisons to account 39298 // for these scenarios. 39299 39300 var SAFE_TIME_DELTA = TIME_FUDGE_FACTOR * 3; 39301 39302 var filterRanges = function filterRanges(timeRanges, predicate) { 39303 var results = []; 39304 var i = void 0; 39305 39306 if (timeRanges && timeRanges.length) { 39307 // Search for ranges that match the predicate 39308 for (i = 0; i < timeRanges.length; i++) { 39309 if (predicate(timeRanges.start(i), timeRanges.end(i))) { 39310 results.push([timeRanges.start(i), timeRanges.end(i)]); 39311 } 39312 } 39313 } 39314 39315 return videojs$1.createTimeRanges(results); 39316 }; 39317 /** 39318 * Attempts to find the buffered TimeRange that contains the specified 39319 * time. 39320 * @param {TimeRanges} buffered - the TimeRanges object to query 39321 * @param {number} time - the time to filter on. 39322 * @returns {TimeRanges} a new TimeRanges object 39323 */ 39324 39325 39326 var findRange = function findRange(buffered, time) { 39327 return filterRanges(buffered, function (start, end) { 39328 return start - SAFE_TIME_DELTA <= time && end + SAFE_TIME_DELTA >= time; 39329 }); 39330 }; 39331 /** 39332 * Returns the TimeRanges that begin later than the specified time. 39333 * @param {TimeRanges} timeRanges - the TimeRanges object to query 39334 * @param {number} time - the time to filter on. 39335 * @returns {TimeRanges} a new TimeRanges object. 39336 */ 39337 39338 39339 var findNextRange = function findNextRange(timeRanges, time) { 39340 return filterRanges(timeRanges, function (start) { 39341 return start - TIME_FUDGE_FACTOR >= time; 39342 }); 39343 }; 39344 /** 39345 * Returns gaps within a list of TimeRanges 39346 * @param {TimeRanges} buffered - the TimeRanges object 39347 * @return {TimeRanges} a TimeRanges object of gaps 39348 */ 39349 39350 39351 var findGaps = function findGaps(buffered) { 39352 if (buffered.length < 2) { 39353 return videojs$1.createTimeRanges(); 39354 } 39355 39356 var ranges = []; 39357 39358 for (var i = 1; i < buffered.length; i++) { 39359 var start = buffered.end(i - 1); 39360 var end = buffered.start(i); 39361 ranges.push([start, end]); 39362 } 39363 39364 return videojs$1.createTimeRanges(ranges); 39365 }; 39366 /** 39367 * Gets a human readable string for a TimeRange 39368 * 39369 * @param {TimeRange} range 39370 * @returns {String} a human readable string 39371 */ 39372 39373 39374 var printableRange = function printableRange(range) { 39375 var strArr = []; 39376 39377 if (!range || !range.length) { 39378 return ''; 39379 } 39380 39381 for (var i = 0; i < range.length; i++) { 39382 strArr.push(range.start(i) + ' => ' + range.end(i)); 39383 } 39384 39385 return strArr.join(', '); 39386 }; 39387 /** 39388 * Calculates the amount of time left in seconds until the player hits the end of the 39389 * buffer and causes a rebuffer 39390 * 39391 * @param {TimeRange} buffered 39392 * The state of the buffer 39393 * @param {Numnber} currentTime 39394 * The current time of the player 39395 * @param {Number} playbackRate 39396 * The current playback rate of the player. Defaults to 1. 39397 * @return {Number} 39398 * Time until the player has to start rebuffering in seconds. 39399 * @function timeUntilRebuffer 39400 */ 39401 39402 39403 var timeUntilRebuffer = function timeUntilRebuffer(buffered, currentTime) { 39404 var playbackRate = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 1; 39405 var bufferedEnd = buffered.length ? buffered.end(buffered.length - 1) : 0; 39406 return (bufferedEnd - currentTime) / playbackRate; 39407 }; 39408 /** 39409 * Converts a TimeRanges object into an array representation 39410 * @param {TimeRanges} timeRanges 39411 * @returns {Array} 39412 */ 39413 39414 39415 var timeRangesToArray = function timeRangesToArray(timeRanges) { 39416 var timeRangesList = []; 39417 39418 for (var i = 0; i < timeRanges.length; i++) { 39419 timeRangesList.push({ 39420 start: timeRanges.start(i), 39421 end: timeRanges.end(i) 39422 }); 39423 } 39424 39425 return timeRangesList; 39426 }; 39427 /** 39428 * @file create-text-tracks-if-necessary.js 39429 */ 39430 39431 /** 39432 * Create text tracks on video.js if they exist on a segment. 39433 * 39434 * @param {Object} sourceBuffer the VSB or FSB 39435 * @param {Object}
vendor: 17,131 bytes, lines 39435-39964
39435 mediaSource the HTML media source 39436 * @param {Object} segment the segment that may contain the text track 39437 * @private 39438 */ 39439 39440 39441 var createTextTracksIfNecessary = function createTextTracksIfNecessary(sourceBuffer, mediaSource, segment) { 39442 var player = mediaSource.player_; // create an in-band caption track if one is present in the segment 39443 39444 if (segment.captions && segment.captions.length) { 39445 if (!sourceBuffer.inbandTextTracks_) { 39446 sourceBuffer.inbandTextTracks_ = {}; 39447 } 39448 39449 for (var trackId in segment.captionStreams) { 39450 if (!sourceBuffer.inbandTextTracks_[trackId]) { 39451 player.tech_.trigger({ 39452 type: 'usage', 39453 name: 'hls-608' 39454 }); 39455 var track = player.textTracks().getTrackById(trackId); 39456 39457 if (track) { 39458 // Resuse an existing track with a CC# id because this was 39459 // very likely created by videojs-contrib-hls from information 39460 // in the m3u8 for us to use 39461 sourceBuffer.inbandTextTracks_[trackId] = track; 39462 } else { 39463 // Otherwise, create a track with the default `CC#` label and 39464 // without a language 39465 sourceBuffer.inbandTextTracks_[trackId] = player.addRemoteTextTrack({ 39466 kind: 'captions', 39467 id: trackId, 39468 label: trackId 39469 }, false).track; 39470 } 39471 } 39472 } 39473 } 39474 39475 if (segment.metadata && segment.metadata.length && !sourceBuffer.metadataTrack_) { 39476 sourceBuffer.metadataTrack_ = player.addRemoteTextTrack({ 39477 kind: 'metadata', 39478 label: 'Timed Metadata' 39479 }, false).track; 39480 sourceBuffer.metadataTrack_.inBandMetadataTrackDispatchType = segment.metadata.dispatchType; 39481 } 39482 }; 39483 /** 39484 * @file remove-cues-from-track.js 39485 */ 39486 39487 /** 39488 * Remove cues from a track on video.js. 39489 * 39490 * @param {Double} start start of where we should remove the cue 39491 * @param {Double} end end of where the we should remove the cue 39492 * @param {Object} track the text track to remove the cues from 39493 * @private 39494 */ 39495 39496 39497 var removeCuesFromTrack = function removeCuesFromTrack(start, end, track) { 39498 var i = void 0; 39499 var cue = void 0; 39500 39501 if (!track) { 39502 return; 39503 } 39504 39505 if (!track.cues) { 39506 return; 39507 } 39508 39509 i = track.cues.length; 39510 39511 while (i--) { 39512 cue = track.cues[i]; // Remove any overlapping cue 39513 39514 if (cue.startTime <= end && cue.endTime >= start) { 39515 track.removeCue(cue); 39516 } 39517 } 39518 }; 39519 /** 39520 * @file add-text-track-data.js 39521 */ 39522 39523 /** 39524 * Define properties on a cue for backwards compatability, 39525 * but warn the user that the way that they are using it 39526 * is depricated and will be removed at a later date. 39527 * 39528 * @param {Cue} cue the cue to add the properties on 39529 * @private 39530 */ 39531 39532 39533 var deprecateOldCue = function deprecateOldCue(cue) { 39534 Object.defineProperties(cue.frame, { 39535 id: { 39536 get: function get() { 39537 videojs$1.log.warn('cue.frame.id is deprecated. Use cue.value.key instead.'); 39538 return cue.value.key; 39539 } 39540 }, 39541 value: { 39542 get: function get() { 39543 videojs$1.log.warn('cue.frame.value is deprecated. Use cue.value.data instead.'); 39544 return cue.value.data; 39545 } 39546 }, 39547 privateData: { 39548 get: function get() { 39549 videojs$1.log.warn('cue.frame.privateData is deprecated. Use cue.value.data instead.'); 39550 return cue.value.data; 39551 } 39552 } 39553 }); 39554 }; 39555 39556 var durationOfVideo = function durationOfVideo(duration) { 39557 var dur = void 0; 39558 39559 if (isNaN(duration) || Math.abs(duration) === Infinity) { 39560 dur = Number.MAX_VALUE; 39561 } else { 39562 dur = duration; 39563 } 39564 39565 return dur; 39566 }; 39567 /** 39568 * Add text track data to a source handler given the captions and 39569 * metadata from the buffer. 39570 * 39571 * @param {Object} sourceHandler the virtual source buffer 39572 * @param {Array} captionArray an array of caption data 39573 * @param {Array} metadataArray an array of meta data 39574 * @private 39575 */ 39576 39577 39578 var addTextTrackData = function addTextTrackData(sourceHandler, captionArray, metadataArray) { 39579 var Cue = window$1.WebKitDataCue || window$1.VTTCue; 39580 39581 if (captionArray) { 39582 captionArray.forEach(function (caption) { 39583 var track = caption.stream; 39584 this.inbandTextTracks_[track].addCue(new Cue(caption.startTime + this.timestampOffset, caption.endTime + this.timestampOffset, caption.text)); 39585 }, sourceHandler); 39586 } 39587 39588 if (metadataArray) { 39589 var videoDuration = durationOfVideo(sourceHandler.mediaSource_.duration); 39590 metadataArray.forEach(function (metadata) { 39591 var time = metadata.cueTime + this.timestampOffset; // if time isn't a finite number between 0 and Infinity, like NaN, 39592 // ignore this bit of metadata. 39593 // This likely occurs when you have an non-timed ID3 tag like TIT2, 39594 // which is the "Title/Songname/Content description" frame 39595 39596 if (typeof time !== 'number' || window$1.isNaN(time) || time < 0 || !(time < Infinity)) { 39597 return; 39598 } 39599 39600 metadata.frames.forEach(function (frame) { 39601 var cue = new Cue(time, time, frame.value || frame.url || frame.data || ''); 39602 cue.frame = frame; 39603 cue.value = frame; 39604 deprecateOldCue(cue); 39605 this.metadataTrack_.addCue(cue); 39606 }, this); 39607 }, sourceHandler); // Updating the metadeta cues so that 39608 // the endTime of each cue is the startTime of the next cue 39609 // the endTime of last cue is the duration of the video 39610 39611 if (sourceHandler.metadataTrack_ && sourceHandler.metadataTrack_.cues && sourceHandler.metadataTrack_.cues.length) { 39612 var cues = sourceHandler.metadataTrack_.cues; 39613 var cuesArray = []; // Create a copy of the TextTrackCueList... 39614 // ...disregarding cues with a falsey value 39615 39616 for (var i = 0; i < cues.length; i++) { 39617 if (cues[i]) { 39618 cuesArray.push(cues[i]); 39619 } 39620 } // Group cues by their startTime value 39621 39622 39623 var cuesGroupedByStartTime = cuesArray.reduce(function (obj, cue) { 39624 var timeSlot = obj[cue.startTime] || []; 39625 timeSlot.push(cue); 39626 obj[cue.startTime] = timeSlot; 39627 return obj; 39628 }, {}); // Sort startTimes by ascending order 39629 39630 var sortedStartTimes = Object.keys(cuesGroupedByStartTime).sort(function (a, b) { 39631 return Number(a) - Number(b); 39632 }); // Map each cue group's endTime to the next group's startTime 39633 39634 sortedStartTimes.forEach(function (startTime, idx) { 39635 var cueGroup = cuesGroupedByStartTime[startTime]; 39636 var nextTime = Number(sortedStartTimes[idx + 1]) || videoDuration; // Map each cue's endTime the next group's startTime 39637 39638 cueGroup.forEach(function (cue) { 39639 cue.endTime = nextTime; 39640 }); 39641 }); 39642 } 39643 } 39644 }; 39645 39646 var win = typeof window !== 'undefined' ? window : {}, 39647 TARGET = typeof Symbol === 'undefined' ? '__target' : Symbol(), 39648 SCRIPT_TYPE = 'application/javascript', 39649 BlobBuilder = win.BlobBuilder || win.WebKitBlobBuilder || win.MozBlobBuilder || win.MSBlobBuilder, 39650 URL = win.URL || win.webkitURL || URL && URL.msURL, 39651 Worker = win.Worker; 39652 /** 39653 * Returns a wrapper around Web Worker code that is constructible. 39654 * 39655 * @function shimWorker 39656 * 39657 * @param { String } filename The name of the file 39658 * @param { Function } fn Function wrapping the code of the worker 39659 */ 39660 39661 function shimWorker(filename, fn) { 39662 return function ShimWorker(forceFallback) { 39663 var o = this; 39664 39665 if (!fn) { 39666 return new Worker(filename); 39667 } else if (Worker && !forceFallback) { 39668 // Convert the function's inner code to a string to construct the worker 39669 var source = fn.toString().replace(/^function.+?{/, '').slice(0, -1), 39670 objURL = createSourceObject(source); 39671 this[TARGET] = new Worker(objURL); 39672 wrapTerminate(this[TARGET], objURL); 39673 return this[TARGET]; 39674 } else { 39675 var selfShim = { 39676 postMessage: function postMessage(m) { 39677 if (o.onmessage) { 39678 setTimeout(function () { 39679 o.onmessage({ 39680 data: m, 39681 target: selfShim 39682 }); 39683 }); 39684 } 39685 } 39686 }; 39687 fn.call(selfShim); 39688 39689 this.postMessage = function (m) { 39690 setTimeout(function () { 39691 selfShim.onmessage({ 39692 data: m, 39693 target: o 39694 }); 39695 }); 39696 }; 39697 39698 this.isThisThread = true; 39699 } 39700 }; 39701 } // Test Worker capabilities 39702 39703 39704 if (Worker) { 39705 var testWorker, 39706 objURL = createSourceObject('self.onmessage = function () {}'), 39707 testArray = new Uint8Array(1); 39708 39709 try { 39710 testWorker = new Worker(objURL); // Native browser on some Samsung devices throws for transferables, let's detect it 39711 39712 testWorker.postMessage(testArray, [testArray.buffer]); 39713 } catch (e) { 39714 Worker = null; 39715 } finally { 39716 URL.revokeObjectURL(objURL); 39717 39718 if (testWorker) { 39719 testWorker.terminate(); 39720 } 39721 } 39722 } 39723 39724 function createSourceObject(str) { 39725 try { 39726 return URL.createObjectURL(new Blob([str], { 39727 type: SCRIPT_TYPE 39728 })); 39729 } catch (e) { 39730 var blob = new BlobBuilder(); 39731 blob.append(str); 39732 return URL.createObjectURL(blob.getBlob(type)); 39733 } 39734 } 39735 39736 function wrapTerminate(worker, objURL) { 39737 if (!worker || !objURL) return; 39738 var term = worker.terminate; 39739 worker.objURL = objURL; 39740 39741 worker.terminate = function () { 39742 if (worker.objURL) URL.revokeObjectURL(worker.objURL); 39743 term.call(worker); 39744 }; 39745 } 39746 39747 var TransmuxWorker = new shimWorker("./transmuxer-worker.worker.js", function (window, document$$1) { 39748 var self = this; 39749 39750 var transmuxerWorker = function () { 39751 /** 39752 * mux.js 39753 * 39754 * Copyright (c) Brightcove 39755 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 39756 * 39757 * A lightweight readable stream implemention that handles event dispatching. 39758 * Objects that inherit from streams should call init in their constructors. 39759 */ 39760 var Stream = function Stream() { 39761 this.init = function () { 39762 var listeners = {}; 39763 /** 39764 * Add a listener for a specified event type. 39765 * @param type {string} the event name 39766 * @param listener {function} the callback to be invoked when an event of 39767 * the specified type occurs 39768 */ 39769 39770 this.on = function (type, listener) { 39771 if (!listeners[type]) { 39772 listeners[type] = []; 39773 } 39774 39775 listeners[type] = listeners[type].concat(listener); 39776 }; 39777 /** 39778 * Remove a listener for a specified event type. 39779 * @param type {string} the event name 39780 * @param listener {function} a function previously registered for this 39781 * type of event through `on` 39782 */ 39783 39784 39785 this.off = function (type, listener) { 39786 var index; 39787 39788 if (!listeners[type]) { 39789 return false; 39790 } 39791 39792 index = listeners[type].indexOf(listener); 39793 listeners[type] = listeners[type].slice(); 39794 listeners[type].splice(index, 1); 39795 return index > -1; 39796 }; 39797 /** 39798 * Trigger an event of the specified type on this stream. Any additional 39799 * arguments to this function are passed as parameters to event listeners. 39800 * @param type {string} the event name 39801 */ 39802 39803 39804 this.trigger = function (type) { 39805 var callbacks, i, length, args; 39806 callbacks = listeners[type]; 39807 39808 if (!callbacks) { 39809 return; 39810 } // Slicing the arguments on every invocation of this method 39811 // can add a significant amount of overhead. Avoid the 39812 // intermediate object creation for the common case of a 39813 // single callback argument 39814 39815 39816 if (arguments.length === 2) { 39817 length = callbacks.length; 39818 39819 for (i = 0; i < length; ++i) { 39820 callbacks[i].call(this, arguments[1]); 39821 } 39822 } else { 39823 args = []; 39824 i = arguments.length; 39825 39826 for (i = 1; i < arguments.length; ++i) { 39827 args.push(arguments[i]); 39828 } 39829 39830 length = callbacks.length; 39831 39832 for (i = 0; i < length; ++i) { 39833 callbacks[i].apply(this, args); 39834 } 39835 } 39836 }; 39837 /** 39838 * Destroys the stream and cleans up. 39839 */ 39840 39841 39842 this.dispose = function () { 39843 listeners = {}; 39844 }; 39845 }; 39846 }; 39847 /** 39848 * Forwards all `data` events on this stream to the destination stream. The 39849 * destination stream should provide a method `push` to receive the data 39850 * events as they arrive. 39851 * @param destination {stream} the stream that will receive all `data` events 39852 * @param autoFlush {boolean} if false, we will not call `flush` on the destination 39853 * when the current stream emits a 'done' event 39854 * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options 39855 */ 39856 39857 39858 Stream.prototype.pipe = function (destination) { 39859 this.on('data', function (data) { 39860 destination.push(data); 39861 }); 39862 this.on('done', function (flushSource) { 39863 destination.flush(flushSource); 39864 }); 39865 this.on('partialdone', function (flushSource) { 39866 destination.partialFlush(flushSource); 39867 }); 39868 this.on('endedtimeline', function (flushSource) { 39869 destination.endTimeline(flushSource); 39870 }); 39871 this.on('reset', function (flushSource) { 39872 destination.reset(flushSource); 39873 }); 39874 return destination; 39875 }; // Default stream functions that are expected to be overridden to perform 39876 // actual work. These are provided by the prototype as a sort of no-op 39877 // implementation so that we don't have to check for their existence in the 39878 // `pipe` function above. 39879 39880 39881 Stream.prototype.push = function (data) { 39882 this.trigger('data', data); 39883 }; 39884 39885 Stream.prototype.flush = function (flushSource) { 39886 this.trigger('done', flushSource); 39887 }; 39888 39889 Stream.prototype.partialFlush = function (flushSource) { 39890 this.trigger('partialdone', flushSource); 39891 }; 39892 39893 Stream.prototype.endTimeline = function (flushSource) { 39894 this.trigger('endedtimeline', flushSource); 39895 }; 39896 39897 Stream.prototype.reset = function (flushSource) { 39898 this.trigger('reset', flushSource); 39899 }; 39900 39901 var stream = Stream; 39902 /** 39903 * mux.js 39904 * 39905 * Copyright (c) Brightcove 39906 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 39907 * 39908 * Functions that generate fragmented MP4s suitable for use with Media 39909 * Source Extensions. 39910 */ 39911 39912 var UINT32_MAX = Math.pow(2, 32) - 1; 39913 var box, dinf, esds, ftyp, mdat, mfhd, minf, moof, moov, mvex, mvhd, trak, tkhd, mdia, mdhd, hdlr, sdtp, stbl, stsd, traf, trex, trun, types, MAJOR_BRAND, MINOR_VERSION, AVC1_BRAND, VIDEO_HDLR, AUDIO_HDLR, HDLR_TYPES, VMHD, SMHD, DREF, STCO, STSC, STSZ, STTS; // pre-calculate constants 39914 39915 (function () { 39916 var i; 39917 types = { 39918 avc1: [], 39919 // codingname 39920 avcC: [], 39921 btrt: [], 39922 dinf: [], 39923 dref: [], 39924 esds: [], 39925 ftyp: [], 39926 hdlr: [], 39927 mdat: [], 39928 mdhd: [], 39929 mdia: [], 39930 mfhd: [], 39931 minf: [], 39932 moof: [], 39933 moov: [], 39934 mp4a: [], 39935 // codingname 39936 mvex: [], 39937 mvhd: [], 39938 pasp: [], 39939 sdtp: [], 39940 smhd: [], 39941 stbl: [], 39942 stco: [], 39943 stsc: [], 39944 stsd: [], 39945 stsz: [], 39946 stts: [], 39947 styp: [], 39948 tfdt: [], 39949 tfhd: [], 39950 traf: [], 39951 trak: [], 39952 trun: [], 39953 trex: [], 39954 tkhd: [], 39955 vmhd: [] 39956 }; // In environments where Uint8Array is undefined (e.g., IE8), skip set up so that we 39957 // don't throw an error 39958 39959 if (typeof Uint8Array === 'undefined') { 39960 return; 39961 } 39962 39963 for (i in types) { 39964 if (types.hasOwnProperty(i)) {
vendor: 16,384 bytes, lines 39965-40334
39965 types[i] = [i.charCodeAt(0), i.charCodeAt(1), i.charCodeAt(2), i.charCodeAt(3)]; 39966 } 39967 } 39968 39969 MAJOR_BRAND = new Uint8Array(['i'.charCodeAt(0), 's'.charCodeAt(0), 'o'.charCodeAt(0), 'm'.charCodeAt(0)]); 39970 AVC1_BRAND = new Uint8Array(['a'.charCodeAt(0), 'v'.charCodeAt(0), 'c'.charCodeAt(0), '1'.charCodeAt(0)]); 39971 MINOR_VERSION = new Uint8Array([0, 0, 0, 1]); 39972 VIDEO_HDLR = new Uint8Array([0x00, // version 0 39973 0x00, 0x00, 0x00, // flags 39974 0x00, 0x00, 0x00, 0x00, // pre_defined 39975 0x76, 0x69, 0x64, 0x65, // handler_type: 'vide' 39976 0x00, 0x00, 0x00, 0x00, // reserved 39977 0x00, 0x00, 0x00, 0x00, // reserved 39978 0x00, 0x00, 0x00, 0x00, // reserved 39979 0x56, 0x69, 0x64, 0x65, 0x6f, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'VideoHandler' 39980 ]); 39981 AUDIO_HDLR = new Uint8Array([0x00, // version 0 39982 0x00, 0x00, 0x00, // flags 39983 0x00, 0x00, 0x00, 0x00, // pre_defined 39984 0x73, 0x6f, 0x75, 0x6e, // handler_type: 'soun' 39985 0x00, 0x00, 0x00, 0x00, // reserved 39986 0x00, 0x00, 0x00, 0x00, // reserved 39987 0x00, 0x00, 0x00, 0x00, // reserved 39988 0x53, 0x6f, 0x75, 0x6e, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'SoundHandler' 39989 ]); 39990 HDLR_TYPES = { 39991 video: VIDEO_HDLR, 39992 audio: AUDIO_HDLR 39993 }; 39994 DREF = new Uint8Array([0x00, // version 0 39995 0x00, 0x00, 0x00, // flags 39996 0x00, 0x00, 0x00, 0x01, // entry_count 39997 0x00, 0x00, 0x00, 0x0c, // entry_size 39998 0x75, 0x72, 0x6c, 0x20, // 'url' type 39999 0x00, // version 0 40000 0x00, 0x00, 0x01 // entry_flags 40001 ]); 40002 SMHD = new Uint8Array([0x00, // version 40003 0x00, 0x00, 0x00, // flags 40004 0x00, 0x00, // balance, 0 means centered 40005 0x00, 0x00 // reserved 40006 ]); 40007 STCO = new Uint8Array([0x00, // version 40008 0x00, 0x00, 0x00, // flags 40009 0x00, 0x00, 0x00, 0x00 // entry_count 40010 ]); 40011 STSC = STCO; 40012 STSZ = new Uint8Array([0x00, // version 40013 0x00, 0x00, 0x00, // flags 40014 0x00, 0x00, 0x00, 0x00, // sample_size 40015 0x00, 0x00, 0x00, 0x00 // sample_count 40016 ]); 40017 STTS = STCO; 40018 VMHD = new Uint8Array([0x00, // version 40019 0x00, 0x00, 0x01, // flags 40020 0x00, 0x00, // graphicsmode 40021 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // opcolor 40022 ]); 40023 })(); 40024 40025 box = function box(type) { 40026 var payload = [], 40027 size = 0, 40028 i, 40029 result, 40030 view; 40031 40032 for (i = 1; i < arguments.length; i++) { 40033 payload.push(arguments[i]); 40034 } 40035 40036 i = payload.length; // calculate the total size we need to allocate 40037 40038 while (i--) { 40039 size += payload[i].byteLength; 40040 } 40041 40042 result = new Uint8Array(size + 8); 40043 view = new DataView(result.buffer, result.byteOffset, result.byteLength); 40044 view.setUint32(0, result.byteLength); 40045 result.set(type, 4); // copy the payload into the result 40046 40047 for (i = 0, size = 8; i < payload.length; i++) { 40048 result.set(payload[i], size); 40049 size += payload[i].byteLength; 40050 } 40051 40052 return result; 40053 }; 40054 40055 dinf = function dinf() { 40056 return box(types.dinf, box(types.dref, DREF)); 40057 }; 40058 40059 esds = function esds(track) { 40060 return box(types.esds, new Uint8Array([0x00, // version 40061 0x00, 0x00, 0x00, // flags 40062 // ES_Descriptor 40063 0x03, // tag, ES_DescrTag 40064 0x19, // length 40065 0x00, 0x00, // ES_ID 40066 0x00, // streamDependenceFlag, URL_flag, reserved, streamPriority 40067 // DecoderConfigDescriptor 40068 0x04, // tag, DecoderConfigDescrTag 40069 0x11, // length 40070 0x40, // object type 40071 0x15, // streamType 40072 0x00, 0x06, 0x00, // bufferSizeDB 40073 0x00, 0x00, 0xda, 0xc0, // maxBitrate 40074 0x00, 0x00, 0xda, 0xc0, // avgBitrate 40075 // DecoderSpecificInfo 40076 0x05, // tag, DecoderSpecificInfoTag 40077 0x02, // length 40078 // ISO/IEC 14496-3, AudioSpecificConfig 40079 // for samplingFrequencyIndex see ISO/IEC 13818-7:2006, 8.1.3.2.2, Table 35 40080 track.audioobjecttype << 3 | track.samplingfrequencyindex >>> 1, track.samplingfrequencyindex << 7 | track.channelcount << 3, 0x06, 0x01, 0x02 // GASpecificConfig 40081 ])); 40082 }; 40083 40084 ftyp = function ftyp() { 40085 return box(types.ftyp, MAJOR_BRAND, MINOR_VERSION, MAJOR_BRAND, AVC1_BRAND); 40086 }; 40087 40088 hdlr = function hdlr(type) { 40089 return box(types.hdlr, HDLR_TYPES[type]); 40090 }; 40091 40092 mdat = function mdat(data) { 40093 return box(types.mdat, data); 40094 }; 40095 40096 mdhd = function mdhd(track) { 40097 var result = new Uint8Array([0x00, // version 0 40098 0x00, 0x00, 0x00, // flags 40099 0x00, 0x00, 0x00, 0x02, // creation_time 40100 0x00, 0x00, 0x00, 0x03, // modification_time 40101 0x00, 0x01, 0x5f, 0x90, // timescale, 90,000 "ticks" per second 40102 track.duration >>> 24 & 0xFF, track.duration >>> 16 & 0xFF, track.duration >>> 8 & 0xFF, track.duration & 0xFF, // duration 40103 0x55, 0xc4, // 'und' language (undetermined) 40104 0x00, 0x00]); // Use the sample rate from the track metadata, when it is 40105 // defined. The sample rate can be parsed out of an ADTS header, for 40106 // instance. 40107 40108 if (track.samplerate) { 40109 result[12] = track.samplerate >>> 24 & 0xFF; 40110 result[13] = track.samplerate >>> 16 & 0xFF; 40111 result[14] = track.samplerate >>> 8 & 0xFF; 40112 result[15] = track.samplerate & 0xFF; 40113 } 40114 40115 return box(types.mdhd, result); 40116 }; 40117 40118 mdia = function mdia(track) { 40119 return box(types.mdia, mdhd(track), hdlr(track.type), minf(track)); 40120 }; 40121 40122 mfhd = function mfhd(sequenceNumber) { 40123 return box(types.mfhd, new Uint8Array([0x00, 0x00, 0x00, 0x00, // flags 40124 (sequenceNumber & 0xFF000000) >> 24, (sequenceNumber & 0xFF0000) >> 16, (sequenceNumber & 0xFF00) >> 8, sequenceNumber & 0xFF // sequence_number 40125 ])); 40126 }; 40127 40128 minf = function minf(track) { 40129 return box(types.minf, track.type === 'video' ? box(types.vmhd, VMHD) : box(types.smhd, SMHD), dinf(), stbl(track)); 40130 }; 40131 40132 moof = function moof(sequenceNumber, tracks) { 40133 var trackFragments = [], 40134 i = tracks.length; // build traf boxes for each track fragment 40135 40136 while (i--) { 40137 trackFragments[i] = traf(tracks[i]); 40138 } 40139 40140 return box.apply(null, [types.moof, mfhd(sequenceNumber)].concat(trackFragments)); 40141 }; 40142 /** 40143 * Returns a movie box. 40144 * @param tracks {array} the tracks associated with this movie 40145 * @see ISO/IEC 14496-12:2012(E), section 8.2.1 40146 */ 40147 40148 40149 moov = function moov(tracks) { 40150 var i = tracks.length, 40151 boxes = []; 40152 40153 while (i--) { 40154 boxes[i] = trak(tracks[i]); 40155 } 40156 40157 return box.apply(null, [types.moov, mvhd(0xffffffff)].concat(boxes).concat(mvex(tracks))); 40158 }; 40159 40160 mvex = function mvex(tracks) { 40161 var i = tracks.length, 40162 boxes = []; 40163 40164 while (i--) { 40165 boxes[i] = trex(tracks[i]); 40166 } 40167 40168 return box.apply(null, [types.mvex].concat(boxes)); 40169 }; 40170 40171 mvhd = function mvhd(duration) { 40172 var bytes = new Uint8Array([0x00, // version 0 40173 0x00, 0x00, 0x00, // flags 40174 0x00, 0x00, 0x00, 0x01, // creation_time 40175 0x00, 0x00, 0x00, 0x02, // modification_time 40176 0x00, 0x01, 0x5f, 0x90, // timescale, 90,000 "ticks" per second 40177 (duration & 0xFF000000) >> 24, (duration & 0xFF0000) >> 16, (duration & 0xFF00) >> 8, duration & 0xFF, // duration 40178 0x00, 0x01, 0x00, 0x00, // 1.0 rate 40179 0x01, 0x00, // 1.0 volume 40180 0x00, 0x00, // reserved 40181 0x00, 0x00, 0x00, 0x00, // reserved 40182 0x00, 0x00, 0x00, 0x00, // reserved 40183 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, // transformation: unity matrix 40184 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // pre_defined 40185 0xff, 0xff, 0xff, 0xff // next_track_ID 40186 ]); 40187 return box(types.mvhd, bytes); 40188 }; 40189 40190 sdtp = function sdtp(track) { 40191 var samples = track.samples || [], 40192 bytes = new Uint8Array(4 + samples.length), 40193 flags, 40194 i; // leave the full box header (4 bytes) all zero 40195 // write the sample table 40196 40197 for (i = 0; i < samples.length; i++) { 40198 flags = samples[i].flags; 40199 bytes[i + 4] = flags.dependsOn << 4 | flags.isDependedOn << 2 | flags.hasRedundancy; 40200 } 40201 40202 return box(types.sdtp, bytes); 40203 }; 40204 40205 stbl = function stbl(track) { 40206 return box(types.stbl, stsd(track), box(types.stts, STTS), box(types.stsc, STSC), box(types.stsz, STSZ), box(types.stco, STCO)); 40207 }; 40208 40209 (function () { 40210 var videoSample, audioSample; 40211 40212 stsd = function stsd(track) { 40213 return box(types.stsd, new Uint8Array([0x00, // version 0 40214 0x00, 0x00, 0x00, // flags 40215 0x00, 0x00, 0x00, 0x01]), track.type === 'video' ? videoSample(track) : audioSample(track)); 40216 }; 40217 40218 videoSample = function videoSample(track) { 40219 var sps = track.sps || [], 40220 pps = track.pps || [], 40221 sequenceParameterSets = [], 40222 pictureParameterSets = [], 40223 i, 40224 avc1Box; // assemble the SPSs 40225 40226 for (i = 0; i < sps.length; i++) { 40227 sequenceParameterSets.push((sps[i].byteLength & 0xFF00) >>> 8); 40228 sequenceParameterSets.push(sps[i].byteLength & 0xFF); // sequenceParameterSetLength 40229 40230 sequenceParameterSets = sequenceParameterSets.concat(Array.prototype.slice.call(sps[i])); // SPS 40231 } // assemble the PPSs 40232 40233 40234 for (i = 0; i < pps.length; i++) { 40235 pictureParameterSets.push((pps[i].byteLength & 0xFF00) >>> 8); 40236 pictureParameterSets.push(pps[i].byteLength & 0xFF); 40237 pictureParameterSets = pictureParameterSets.concat(Array.prototype.slice.call(pps[i])); 40238 } 40239 40240 avc1Box = [types.avc1, new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // reserved 40241 0x00, 0x01, // data_reference_index 40242 0x00, 0x00, // pre_defined 40243 0x00, 0x00, // reserved 40244 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // pre_defined 40245 (track.width & 0xff00) >> 8, track.width & 0xff, // width 40246 (track.height & 0xff00) >> 8, track.height & 0xff, // height 40247 0x00, 0x48, 0x00, 0x00, // horizresolution 40248 0x00, 0x48, 0x00, 0x00, // vertresolution 40249 0x00, 0x00, 0x00, 0x00, // reserved 40250 0x00, 0x01, // frame_count 40251 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, // compressorname 40252 0x00, 0x18, // depth = 24 40253 0x11, 0x11 // pre_defined = -1 40254 ]), box(types.avcC, new Uint8Array([0x01, // configurationVersion 40255 track.profileIdc, // AVCProfileIndication 40256 track.profileCompatibility, // profile_compatibility 40257 track.levelIdc, // AVCLevelIndication 40258 0xff // lengthSizeMinusOne, hard-coded to 4 bytes 40259 ].concat([sps.length], // numOfSequenceParameterSets 40260 sequenceParameterSets, // "SPS" 40261 [pps.length], // numOfPictureParameterSets 40262 pictureParameterSets // "PPS" 40263 ))), box(types.btrt, new Uint8Array([0x00, 0x1c, 0x9c, 0x80, // bufferSizeDB 40264 0x00, 0x2d, 0xc6, 0xc0, // maxBitrate 40265 0x00, 0x2d, 0xc6, 0xc0 // avgBitrate 40266 ]))]; 40267 40268 if (track.sarRatio) { 40269 var hSpacing = track.sarRatio[0], 40270 vSpacing = track.sarRatio[1]; 40271 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]))); 40272 } 40273 40274 return box.apply(null, avc1Box); 40275 }; 40276 40277 audioSample = function audioSample(track) { 40278 return box(types.mp4a, new Uint8Array([// SampleEntry, ISO/IEC 14496-12 40279 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // reserved 40280 0x00, 0x01, // data_reference_index 40281 // AudioSampleEntry, ISO/IEC 14496-12 40282 0x00, 0x00, 0x00, 0x00, // reserved 40283 0x00, 0x00, 0x00, 0x00, // reserved 40284 (track.channelcount & 0xff00) >> 8, track.channelcount & 0xff, // channelcount 40285 (track.samplesize & 0xff00) >> 8, track.samplesize & 0xff, // samplesize 40286 0x00, 0x00, // pre_defined 40287 0x00, 0x00, // reserved 40288 (track.samplerate & 0xff00) >> 8, track.samplerate & 0xff, 0x00, 0x00 // samplerate, 16.16 40289 // MP4AudioSampleEntry, ISO/IEC 14496-14 40290 ]), esds(track)); 40291 }; 40292 })(); 40293 40294 tkhd = function tkhd(track) { 40295 var result = new Uint8Array([0x00, // version 0 40296 0x00, 0x00, 0x07, // flags 40297 0x00, 0x00, 0x00, 0x00, // creation_time 40298 0x00, 0x00, 0x00, 0x00, // modification_time 40299 (track.id & 0xFF000000) >> 24, (track.id & 0xFF0000) >> 16, (track.id & 0xFF00) >> 8, track.id & 0xFF, // track_ID 40300 0x00, 0x00, 0x00, 0x00, // reserved 40301 (track.duration & 0xFF000000) >> 24, (track.duration & 0xFF0000) >> 16, (track.duration & 0xFF00) >> 8, track.duration & 0xFF, // duration 40302 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // reserved 40303 0x00, 0x00, // layer 40304 0x00, 0x00, // alternate_group 40305 0x01, 0x00, // non-audio track volume 40306 0x00, 0x00, // reserved 40307 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, // transformation: unity matrix 40308 (track.width & 0xFF00) >> 8, track.width & 0xFF, 0x00, 0x00, // width 40309 (track.height & 0xFF00) >> 8, track.height & 0xFF, 0x00, 0x00 // height 40310 ]); 40311 return box(types.tkhd, result); 40312 }; 40313 /** 40314 * Generate a track fragment (traf) box. A traf box collects metadata 40315 * about tracks in a movie fragment (moof) box. 40316 */ 40317 40318 40319 traf = function traf(track) { 40320 var trackFragmentHeader, trackFragmentDecodeTime, trackFragmentRun, sampleDependencyTable, dataOffset, upperWordBaseMediaDecodeTime, lowerWordBaseMediaDecodeTime; 40321 trackFragmentHeader = box(types.tfhd, new Uint8Array([0x00, // version 0 40322 0x00, 0x00, 0x3a, // flags 40323 (track.id & 0xFF000000) >> 24, (track.id & 0xFF0000) >> 16, (track.id & 0xFF00) >> 8, track.id & 0xFF, // track_ID 40324 0x00, 0x00, 0x00, 0x01, // sample_description_index 40325 0x00, 0x00, 0x00, 0x00, // default_sample_duration 40326 0x00, 0x00, 0x00, 0x00, // default_sample_size 40327 0x00, 0x00, 0x00, 0x00 // default_sample_flags 40328 ])); 40329 upperWordBaseMediaDecodeTime = Math.floor(track.baseMediaDecodeTime / (UINT32_MAX + 1)); 40330 lowerWordBaseMediaDecodeTime = Math.floor(track.baseMediaDecodeTime % (UINT32_MAX + 1)); 40331 trackFragmentDecodeTime = box(types.tfdt, new Uint8Array([0x01, // version 1 40332 0x00, 0x00, 0x00, // flags 40333 // baseMediaDecodeTime 40334 upperWordBaseMediaDecodeTime >>> 24 & 0xFF, upperWordBaseMediaDecodeTime >>> 16 & 0xFF, upperWordBaseMediaDecodeTime >>> 8 & 0xFF, upperWordBaseMediaDecodeTime & 0xFF, lowerWordBaseMediaDecodeTime >>> 24 & 0xFF, lowerWordBaseMediaDecodeTime >>> 16 & 0xFF, lowerWordBaseMediaDecodeTime >>> 8 & 0xFF, lowerWordBaseMediaDecodeTime &
vendor: 10,330 bytes, lines 40334-40579
40334 0xFF])); // the data offset specifies the number of bytes from the start of 40335 // the containing moof to the first payload byte of the associated 40336 // mdat 40337 40338 dataOffset = 32 + // tfhd 40339 20 + // tfdt 40340 8 + // traf header 40341 16 + // mfhd 40342 8 + // moof header 40343 8; // mdat header 40344 // audio tracks require less metadata 40345 40346 if (track.type === 'audio') { 40347 trackFragmentRun = trun(track, dataOffset); 40348 return box(types.traf, trackFragmentHeader, trackFragmentDecodeTime, trackFragmentRun); 40349 } // video tracks should contain an independent and disposable samples 40350 // box (sdtp) 40351 // generate one and adjust offsets to match 40352 40353 40354 sampleDependencyTable = sdtp(track); 40355 trackFragmentRun = trun(track, sampleDependencyTable.length + dataOffset); 40356 return box(types.traf, trackFragmentHeader, trackFragmentDecodeTime, trackFragmentRun, sampleDependencyTable); 40357 }; 40358 /** 40359 * Generate a track box. 40360 * @param track {object} a track definition 40361 * @return {Uint8Array} the track box 40362 */ 40363 40364 40365 trak = function trak(track) { 40366 track.duration = track.duration || 0xffffffff; 40367 return box(types.trak, tkhd(track), mdia(track)); 40368 }; 40369 40370 trex = function trex(track) { 40371 var result = new Uint8Array([0x00, // version 0 40372 0x00, 0x00, 0x00, // flags 40373 (track.id & 0xFF000000) >> 24, (track.id & 0xFF0000) >> 16, (track.id & 0xFF00) >> 8, track.id & 0xFF, // track_ID 40374 0x00, 0x00, 0x00, 0x01, // default_sample_description_index 40375 0x00, 0x00, 0x00, 0x00, // default_sample_duration 40376 0x00, 0x00, 0x00, 0x00, // default_sample_size 40377 0x00, 0x01, 0x00, 0x01 // default_sample_flags 40378 ]); // the last two bytes of default_sample_flags is the sample 40379 // degradation priority, a hint about the importance of this sample 40380 // relative to others. Lower the degradation priority for all sample 40381 // types other than video. 40382 40383 if (track.type !== 'video') { 40384 result[result.length - 1] = 0x00; 40385 } 40386 40387 return box(types.trex, result); 40388 }; 40389 40390 (function () { 40391 var audioTrun, videoTrun, trunHeader; // This method assumes all samples are uniform. That is, if a 40392 // duration is present for the first sample, it will be present for 40393 // all subsequent samples. 40394 // see ISO/IEC 14496-12:2012, Section 8.8.8.1 40395 40396 trunHeader = function trunHeader(samples, offset) { 40397 var durationPresent = 0, 40398 sizePresent = 0, 40399 flagsPresent = 0, 40400 compositionTimeOffset = 0; // trun flag constants 40401 40402 if (samples.length) { 40403 if (samples[0].duration !== undefined) { 40404 durationPresent = 0x1; 40405 } 40406 40407 if (samples[0].size !== undefined) { 40408 sizePresent = 0x2; 40409 } 40410 40411 if (samples[0].flags !== undefined) { 40412 flagsPresent = 0x4; 40413 } 40414 40415 if (samples[0].compositionTimeOffset !== undefined) { 40416 compositionTimeOffset = 0x8; 40417 } 40418 } 40419 40420 return [0x00, // version 0 40421 0x00, durationPresent | sizePresent | flagsPresent | compositionTimeOffset, 0x01, // flags 40422 (samples.length & 0xFF000000) >>> 24, (samples.length & 0xFF0000) >>> 16, (samples.length & 0xFF00) >>> 8, samples.length & 0xFF, // sample_count 40423 (offset & 0xFF000000) >>> 24, (offset & 0xFF0000) >>> 16, (offset & 0xFF00) >>> 8, offset & 0xFF // data_offset 40424 ]; 40425 }; 40426 40427 videoTrun = function videoTrun(track, offset) { 40428 var bytes, samples, sample, i; 40429 samples = track.samples || []; 40430 offset += 8 + 12 + 16 * samples.length; 40431 bytes = trunHeader(samples, offset); 40432 40433 for (i = 0; i < samples.length; i++) { 40434 sample = samples[i]; 40435 bytes = bytes.concat([(sample.duration & 0xFF000000) >>> 24, (sample.duration & 0xFF0000) >>> 16, (sample.duration & 0xFF00) >>> 8, sample.duration & 0xFF, // sample_duration 40436 (sample.size & 0xFF000000) >>> 24, (sample.size & 0xFF0000) >>> 16, (sample.size & 0xFF00) >>> 8, sample.size & 0xFF, // sample_size 40437 sample.flags.isLeading << 2 | sample.flags.dependsOn, sample.flags.isDependedOn << 6 | sample.flags.hasRedundancy << 4 | sample.flags.paddingValue << 1 | sample.flags.isNonSyncSample, sample.flags.degradationPriority & 0xF0 << 8, sample.flags.degradationPriority & 0x0F, // sample_flags 40438 (sample.compositionTimeOffset & 0xFF000000) >>> 24, (sample.compositionTimeOffset & 0xFF0000) >>> 16, (sample.compositionTimeOffset & 0xFF00) >>> 8, sample.compositionTimeOffset & 0xFF // sample_composition_time_offset 40439 ]); 40440 } 40441 40442 return box(types.trun, new Uint8Array(bytes)); 40443 }; 40444 40445 audioTrun = function audioTrun(track, offset) { 40446 var bytes, samples, sample, i; 40447 samples = track.samples || []; 40448 offset += 8 + 12 + 8 * samples.length; 40449 bytes = trunHeader(samples, offset); 40450 40451 for (i = 0; i < samples.length; i++) { 40452 sample = samples[i]; 40453 bytes = bytes.concat([(sample.duration & 0xFF000000) >>> 24, (sample.duration & 0xFF0000) >>> 16, (sample.duration & 0xFF00) >>> 8, sample.duration & 0xFF, // sample_duration 40454 (sample.size & 0xFF000000) >>> 24, (sample.size & 0xFF0000) >>> 16, (sample.size & 0xFF00) >>> 8, sample.size & 0xFF]); // sample_size 40455 } 40456 40457 return box(types.trun, new Uint8Array(bytes)); 40458 }; 40459 40460 trun = function trun(track, offset) { 40461 if (track.type === 'audio') { 40462 return audioTrun(track, offset); 40463 } 40464 40465 return videoTrun(track, offset); 40466 }; 40467 })(); 40468 40469 var mp4Generator = { 40470 ftyp: ftyp, 40471 mdat: mdat, 40472 moof: moof, 40473 moov: moov, 40474 initSegment: function initSegment(tracks) { 40475 var fileType = ftyp(), 40476 movie = moov(tracks), 40477 result; 40478 result = new Uint8Array(fileType.byteLength + movie.byteLength); 40479 result.set(fileType); 40480 result.set(movie, fileType.byteLength); 40481 return result; 40482 } 40483 }; 40484 /** 40485 * mux.js 40486 * 40487 * Copyright (c) Brightcove 40488 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 40489 */ 40490 // Convert an array of nal units into an array of frames with each frame being 40491 // composed of the nal units that make up that frame 40492 // Also keep track of cummulative data about the frame from the nal units such 40493 // as the frame duration, starting pts, etc. 40494 40495 var groupNalsIntoFrames = function groupNalsIntoFrames(nalUnits) { 40496 var i, 40497 currentNal, 40498 currentFrame = [], 40499 frames = []; // TODO added for LHLS, make sure this is OK 40500 40501 frames.byteLength = 0; 40502 frames.nalCount = 0; 40503 frames.duration = 0; 40504 currentFrame.byteLength = 0; 40505 40506 for (i = 0; i < nalUnits.length; i++) { 40507 currentNal = nalUnits[i]; // Split on 'aud'-type nal units 40508 40509 if (currentNal.nalUnitType === 'access_unit_delimiter_rbsp') { 40510 // Since the very first nal unit is expected to be an AUD 40511 // only push to the frames array when currentFrame is not empty 40512 if (currentFrame.length) { 40513 currentFrame.duration = currentNal.dts - currentFrame.dts; // TODO added for LHLS, make sure this is OK 40514 40515 frames.byteLength += currentFrame.byteLength; 40516 frames.nalCount += currentFrame.length; 40517 frames.duration += currentFrame.duration; 40518 frames.push(currentFrame); 40519 } 40520 40521 currentFrame = [currentNal]; 40522 currentFrame.byteLength = currentNal.data.byteLength; 40523 currentFrame.pts = currentNal.pts; 40524 currentFrame.dts = currentNal.dts; 40525 } else { 40526 // Specifically flag key frames for ease of use later 40527 if (currentNal.nalUnitType === 'slice_layer_without_partitioning_rbsp_idr') { 40528 currentFrame.keyFrame = true; 40529 } 40530 40531 currentFrame.duration = currentNal.dts - currentFrame.dts; 40532 currentFrame.byteLength += currentNal.data.byteLength; 40533 currentFrame.push(currentNal); 40534 } 40535 } // For the last frame, use the duration of the previous frame if we 40536 // have nothing better to go on 40537 40538 40539 if (frames.length && (!currentFrame.duration || currentFrame.duration <= 0)) { 40540 currentFrame.duration = frames[frames.length - 1].duration; 40541 } // Push the final frame 40542 // TODO added for LHLS, make sure this is OK 40543 40544 40545 frames.byteLength += currentFrame.byteLength; 40546 frames.nalCount += currentFrame.length; 40547 frames.duration += currentFrame.duration; 40548 frames.push(currentFrame); 40549 return frames; 40550 }; // Convert an array of frames into an array of Gop with each Gop being composed 40551 // of the frames that make up that Gop 40552 // Also keep track of cummulative data about the Gop from the frames such as the 40553 // Gop duration, starting pts, etc. 40554 40555 40556 var groupFramesIntoGops = function groupFramesIntoGops(frames) { 40557 var i, 40558 currentFrame, 40559 currentGop = [], 40560 gops = []; // We must pre-set some of the values on the Gop since we 40561 // keep running totals of these values 40562 40563 currentGop.byteLength = 0; 40564 currentGop.nalCount = 0; 40565 currentGop.duration = 0; 40566 currentGop.pts = frames[0].pts; 40567 currentGop.dts = frames[0].dts; // store some metadata about all the Gops 40568 40569 gops.byteLength = 0; 40570 gops.nalCount = 0; 40571 gops.duration = 0; 40572 gops.pts = frames[0].pts; 40573 gops.dts = frames[0].dts; 40574 40575 for (i = 0; i < frames.length; i++) { 40576 currentFrame = frames[i]; 40577 40578 if (currentFrame.keyFrame) { 40579 // Since the very first frame is expected to be an keyframe
vendor: 3,087 bytes, lines 40580-40667
40580 // only push to the gops array when currentGop is not empty 40581 if (currentGop.length) { 40582 gops.push(currentGop); 40583 gops.byteLength += currentGop.byteLength; 40584 gops.nalCount += currentGop.nalCount; 40585 gops.duration += currentGop.duration; 40586 } 40587 40588 currentGop = [currentFrame]; 40589 currentGop.nalCount = currentFrame.length; 40590 currentGop.byteLength = currentFrame.byteLength; 40591 currentGop.pts = currentFrame.pts; 40592 currentGop.dts = currentFrame.dts; 40593 currentGop.duration = currentFrame.duration; 40594 } else { 40595 currentGop.duration += currentFrame.duration; 40596 currentGop.nalCount += currentFrame.length; 40597 currentGop.byteLength += currentFrame.byteLength; 40598 currentGop.push(currentFrame); 40599 } 40600 } 40601 40602 if (gops.length && currentGop.duration <= 0) { 40603 currentGop.duration = gops[gops.length - 1].duration; 40604 } 40605 40606 gops.byteLength += currentGop.byteLength; 40607 gops.nalCount += currentGop.nalCount; 40608 gops.duration += currentGop.duration; // push the final Gop 40609 40610 gops.push(currentGop); 40611 return gops; 40612 }; 40613 /* 40614 * Search for the first keyframe in the GOPs and throw away all frames 40615 * until that keyframe. Then extend the duration of the pulled keyframe 40616 * and pull the PTS and DTS of the keyframe so that it covers the time 40617 * range of the frames that were disposed. 40618 * 40619 * @param {Array} gops video GOPs 40620 * @returns {Array} modified video GOPs 40621 */ 40622 40623 40624 var extendFirstKeyFrame = function extendFirstKeyFrame(gops) { 40625 var currentGop; 40626 40627 if (!gops[0][0].keyFrame && gops.length > 1) { 40628 // Remove the first GOP 40629 currentGop = gops.shift(); 40630 gops.byteLength -= currentGop.byteLength; 40631 gops.nalCount -= currentGop.nalCount; // Extend the first frame of what is now the 40632 // first gop to cover the time period of the 40633 // frames we just removed 40634 40635 gops[0][0].dts = currentGop.dts; 40636 gops[0][0].pts = currentGop.pts; 40637 gops[0][0].duration += currentGop.duration; 40638 } 40639 40640 return gops; 40641 }; 40642 /** 40643 * Default sample object 40644 * see ISO/IEC 14496-12:2012, section 8.6.4.3 40645 */ 40646 40647 40648 var createDefaultSample = function createDefaultSample() { 40649 return { 40650 size: 0, 40651 flags: { 40652 isLeading: 0, 40653 dependsOn: 1, 40654 isDependedOn: 0, 40655 hasRedundancy: 0, 40656 degradationPriority: 0, 40657 isNonSyncSample: 1 40658 } 40659 }; 40660 }; 40661 /* 40662 * Collates information from a video frame into an object for eventual 40663 * entry into an MP4 sample table. 40664 * 40665 * @param {Object} frame the video frame 40666 * @param {Number} dataOffset the byte offset to position the sample 40667 * @return {Object}
vendor: 7,724 bytes, lines 40667-40877
40667 object containing sample table info for a frame 40668 */ 40669 40670 40671 var sampleForFrame = function sampleForFrame(frame, dataOffset) { 40672 var sample = createDefaultSample(); 40673 sample.dataOffset = dataOffset; 40674 sample.compositionTimeOffset = frame.pts - frame.dts; 40675 sample.duration = frame.duration; 40676 sample.size = 4 * frame.length; // Space for nal unit size 40677 40678 sample.size += frame.byteLength; 40679 40680 if (frame.keyFrame) { 40681 sample.flags.dependsOn = 2; 40682 sample.flags.isNonSyncSample = 0; 40683 } 40684 40685 return sample; 40686 }; // generate the track's sample table from an array of gops 40687 40688 40689 var generateSampleTable = function generateSampleTable(gops, baseDataOffset) { 40690 var h, 40691 i, 40692 sample, 40693 currentGop, 40694 currentFrame, 40695 dataOffset = baseDataOffset || 0, 40696 samples = []; 40697 40698 for (h = 0; h < gops.length; h++) { 40699 currentGop = gops[h]; 40700 40701 for (i = 0; i < currentGop.length; i++) { 40702 currentFrame = currentGop[i]; 40703 sample = sampleForFrame(currentFrame, dataOffset); 40704 dataOffset += sample.size; 40705 samples.push(sample); 40706 } 40707 } 40708 40709 return samples; 40710 }; // generate the track's raw mdat data from an array of gops 40711 40712 40713 var concatenateNalData = function concatenateNalData(gops) { 40714 var h, 40715 i, 40716 j, 40717 currentGop, 40718 currentFrame, 40719 currentNal, 40720 dataOffset = 0, 40721 nalsByteLength = gops.byteLength, 40722 numberOfNals = gops.nalCount, 40723 totalByteLength = nalsByteLength + 4 * numberOfNals, 40724 data = new Uint8Array(totalByteLength), 40725 view = new DataView(data.buffer); // For each Gop.. 40726 40727 for (h = 0; h < gops.length; h++) { 40728 currentGop = gops[h]; // For each Frame.. 40729 40730 for (i = 0; i < currentGop.length; i++) { 40731 currentFrame = currentGop[i]; // For each NAL.. 40732 40733 for (j = 0; j < currentFrame.length; j++) { 40734 currentNal = currentFrame[j]; 40735 view.setUint32(dataOffset, currentNal.data.byteLength); 40736 dataOffset += 4; 40737 data.set(currentNal.data, dataOffset); 40738 dataOffset += currentNal.data.byteLength; 40739 } 40740 } 40741 } 40742 40743 return data; 40744 }; // generate the track's sample table from a frame 40745 40746 40747 var generateSampleTableForFrame = function generateSampleTableForFrame(frame, baseDataOffset) { 40748 var sample, 40749 dataOffset = baseDataOffset || 0, 40750 samples = []; 40751 sample = sampleForFrame(frame, dataOffset); 40752 samples.push(sample); 40753 return samples; 40754 }; // generate the track's raw mdat data from a frame 40755 40756 40757 var concatenateNalDataForFrame = function concatenateNalDataForFrame(frame) { 40758 var i, 40759 currentNal, 40760 dataOffset = 0, 40761 nalsByteLength = frame.byteLength, 40762 numberOfNals = frame.length, 40763 totalByteLength = nalsByteLength + 4 * numberOfNals, 40764 data = new Uint8Array(totalByteLength), 40765 view = new DataView(data.buffer); // For each NAL.. 40766 40767 for (i = 0; i < frame.length; i++) { 40768 currentNal = frame[i]; 40769 view.setUint32(dataOffset, currentNal.data.byteLength); 40770 dataOffset += 4; 40771 data.set(currentNal.data, dataOffset); 40772 dataOffset += currentNal.data.byteLength; 40773 } 40774 40775 return data; 40776 }; 40777 40778 var frameUtils = { 40779 groupNalsIntoFrames: groupNalsIntoFrames, 40780 groupFramesIntoGops: groupFramesIntoGops, 40781 extendFirstKeyFrame: extendFirstKeyFrame, 40782 generateSampleTable: generateSampleTable, 40783 concatenateNalData: concatenateNalData, 40784 generateSampleTableForFrame: generateSampleTableForFrame, 40785 concatenateNalDataForFrame: concatenateNalDataForFrame 40786 }; 40787 /** 40788 * mux.js 40789 * 40790 * Copyright (c) Brightcove 40791 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 40792 */ 40793 40794 var highPrefix = [33, 16, 5, 32, 164, 27]; 40795 var lowPrefix = [33, 65, 108, 84, 1, 2, 4, 8, 168, 2, 4, 8, 17, 191, 252]; 40796 40797 var zeroFill = function zeroFill(count) { 40798 var a = []; 40799 40800 while (count--) { 40801 a.push(0); 40802 } 40803 40804 return a; 40805 }; 40806 40807 var makeTable = function makeTable(metaTable) { 40808 return Object.keys(metaTable).reduce(function (obj, key) { 40809 obj[key] = new Uint8Array(metaTable[key].reduce(function (arr, part) { 40810 return arr.concat(part); 40811 }, [])); 40812 return obj; 40813 }, {}); 40814 }; // Frames-of-silence to use for filling in missing AAC frames 40815 40816 40817 var coneOfSilence = { 40818 96000: [highPrefix, [227, 64], zeroFill(154), [56]], 40819 88200: [highPrefix, [231], zeroFill(170), [56]], 40820 64000: [highPrefix, [248, 192], zeroFill(240), [56]], 40821 48000: [highPrefix, [255, 192], zeroFill(268), [55, 148, 128], zeroFill(54), [112]], 40822 44100: [highPrefix, [255, 192], zeroFill(268), [55, 163, 128], zeroFill(84), [112]], 40823 32000: [highPrefix, [255, 192], zeroFill(268), [55, 234], zeroFill(226), [112]], 40824 24000: [highPrefix, [255, 192], zeroFill(268), [55, 255, 128], zeroFill(268), [111, 112], zeroFill(126), [224]], 40825 16000: [highPrefix, [255, 192], zeroFill(268), [55, 255, 128], zeroFill(268), [111, 255], zeroFill(269), [223, 108], zeroFill(195), [1, 192]], 40826 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]], 40827 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]], 40828 8000: [lowPrefix, zeroFill(268), [3, 121, 16], zeroFill(47), [7]] 40829 }; 40830 var silence = makeTable(coneOfSilence); 40831 /** 40832 * mux.js 40833 * 40834 * Copyright (c) Brightcove 40835 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 40836 */ 40837 40838 var ONE_SECOND_IN_TS = 90000, 40839 // 90kHz clock 40840 secondsToVideoTs, 40841 secondsToAudioTs, 40842 videoTsToSeconds, 40843 audioTsToSeconds, 40844 audioTsToVideoTs, 40845 videoTsToAudioTs, 40846 metadataTsToSeconds; 40847 40848 secondsToVideoTs = function secondsToVideoTs(seconds) { 40849 return seconds * ONE_SECOND_IN_TS; 40850 }; 40851 40852 secondsToAudioTs = function secondsToAudioTs(seconds, sampleRate) { 40853 return seconds * sampleRate; 40854 }; 40855 40856 videoTsToSeconds = function videoTsToSeconds(timestamp) { 40857 return timestamp / ONE_SECOND_IN_TS; 40858 }; 40859 40860 audioTsToSeconds = function audioTsToSeconds(timestamp, sampleRate) { 40861 return timestamp / sampleRate; 40862 }; 40863 40864 audioTsToVideoTs = function audioTsToVideoTs(timestamp, sampleRate) { 40865 return secondsToVideoTs(audioTsToSeconds(timestamp, sampleRate)); 40866 }; 40867 40868 videoTsToAudioTs = function videoTsToAudioTs(timestamp, sampleRate) { 40869 return secondsToAudioTs(videoTsToSeconds(timestamp), sampleRate); 40870 }; 40871 /** 40872 * Adjust ID3 tag or caption timing information by the timeline pts values 40873 * (if keepOriginalTimestamps is false) and convert to seconds 40874 */ 40875 40876 40877 metadataTsToSeconds = function metadataTsToSeconds(timestamp, timelineStartPts,
vendor: 4,368 bytes, lines 40877-40989
40877keepOriginalTimestamps) { 40878 return videoTsToSeconds(keepOriginalTimestamps ? timestamp : timestamp - timelineStartPts); 40879 }; 40880 40881 var clock = { 40882 ONE_SECOND_IN_TS: ONE_SECOND_IN_TS, 40883 secondsToVideoTs: secondsToVideoTs, 40884 secondsToAudioTs: secondsToAudioTs, 40885 videoTsToSeconds: videoTsToSeconds, 40886 audioTsToSeconds: audioTsToSeconds, 40887 audioTsToVideoTs: audioTsToVideoTs, 40888 videoTsToAudioTs: videoTsToAudioTs, 40889 metadataTsToSeconds: metadataTsToSeconds 40890 }; 40891 /** 40892 * mux.js 40893 * 40894 * Copyright (c) Brightcove 40895 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 40896 */ 40897 40898 /** 40899 * Sum the `byteLength` properties of the data in each AAC frame 40900 */ 40901 40902 var sumFrameByteLengths = function sumFrameByteLengths(array) { 40903 var i, 40904 currentObj, 40905 sum = 0; // sum the byteLength's all each nal unit in the frame 40906 40907 for (i = 0; i < array.length; i++) { 40908 currentObj = array[i]; 40909 sum += currentObj.data.byteLength; 40910 } 40911 40912 return sum; 40913 }; // Possibly pad (prefix) the audio track with silence if appending this track 40914 // would lead to the introduction of a gap in the audio buffer 40915 40916 40917 var prefixWithSilence = function prefixWithSilence(track, frames, audioAppendStartTs, videoBaseMediaDecodeTime) { 40918 var baseMediaDecodeTimeTs, 40919 frameDuration = 0, 40920 audioGapDuration = 0, 40921 audioFillFrameCount = 0, 40922 audioFillDuration = 0, 40923 silentFrame, 40924 i, 40925 firstFrame; 40926 40927 if (!frames.length) { 40928 return; 40929 } 40930 40931 baseMediaDecodeTimeTs = clock.audioTsToVideoTs(track.baseMediaDecodeTime, track.samplerate); // determine frame clock duration based on sample rate, round up to avoid overfills 40932 40933 frameDuration = Math.ceil(clock.ONE_SECOND_IN_TS / (track.samplerate / 1024)); 40934 40935 if (audioAppendStartTs && videoBaseMediaDecodeTime) { 40936 // insert the shortest possible amount (audio gap or audio to video gap) 40937 audioGapDuration = baseMediaDecodeTimeTs - Math.max(audioAppendStartTs, videoBaseMediaDecodeTime); // number of full frames in the audio gap 40938 40939 audioFillFrameCount = Math.floor(audioGapDuration / frameDuration); 40940 audioFillDuration = audioFillFrameCount * frameDuration; 40941 } // don't attempt to fill gaps smaller than a single frame or larger 40942 // than a half second 40943 40944 40945 if (audioFillFrameCount < 1 || audioFillDuration > clock.ONE_SECOND_IN_TS / 2) { 40946 return; 40947 } 40948 40949 silentFrame = silence[track.samplerate]; 40950 40951 if (!silentFrame) { 40952 // we don't have a silent frame pregenerated for the sample rate, so use a frame 40953 // from the content instead 40954 silentFrame = frames[0].data; 40955 } 40956 40957 for (i = 0; i < audioFillFrameCount; i++) { 40958 firstFrame = frames[0]; 40959 frames.splice(0, 0, { 40960 data: silentFrame, 40961 dts: firstFrame.dts - frameDuration, 40962 pts: firstFrame.pts - frameDuration 40963 }); 40964 } 40965 40966 track.baseMediaDecodeTime -= Math.floor(clock.videoTsToAudioTs(audioFillDuration, track.samplerate)); 40967 }; // If the audio segment extends before the earliest allowed dts 40968 // value, remove AAC frames until starts at or after the earliest 40969 // allowed DTS so that we don't end up with a negative baseMedia- 40970 // DecodeTime for the audio track 40971 40972 40973 var trimAdtsFramesByEarliestDts = function trimAdtsFramesByEarliestDts(adtsFrames, track, earliestAllowedDts) { 40974 if (track.minSegmentDts >= earliestAllowedDts) { 40975 return adtsFrames; 40976 } // We will need to recalculate the earliest segment Dts 40977 40978 40979 track.minSegmentDts = Infinity; 40980 return adtsFrames.filter(function (currentFrame) { 40981 // If this is an allowed frame, keep it and record it's Dts 40982 if (currentFrame.dts >= earliestAllowedDts) { 40983 track.minSegmentDts = Math.min(track.minSegmentDts, currentFrame.dts); 40984 track.minSegmentPts = track.minSegmentDts; 40985 return true; 40986 } // Otherwise, discard it 40987 40988 40989 return false;
vendor: 14,025 bytes, lines 40990-41382
40990 }); 40991 }; // generate the track's raw mdat data from an array of frames 40992 40993 40994 var generateSampleTable$1 = function generateSampleTable(frames) { 40995 var i, 40996 currentFrame, 40997 samples = []; 40998 40999 for (i = 0; i < frames.length; i++) { 41000 currentFrame = frames[i]; 41001 samples.push({ 41002 size: currentFrame.data.byteLength, 41003 duration: 1024 // For AAC audio, all samples contain 1024 samples 41004 41005 }); 41006 } 41007 41008 return samples; 41009 }; // generate the track's sample table from an array of frames 41010 41011 41012 var concatenateFrameData = function concatenateFrameData(frames) { 41013 var i, 41014 currentFrame, 41015 dataOffset = 0, 41016 data = new Uint8Array(sumFrameByteLengths(frames)); 41017 41018 for (i = 0; i < frames.length; i++) { 41019 currentFrame = frames[i]; 41020 data.set(currentFrame.data, dataOffset); 41021 dataOffset += currentFrame.data.byteLength; 41022 } 41023 41024 return data; 41025 }; 41026 41027 var audioFrameUtils = { 41028 prefixWithSilence: prefixWithSilence, 41029 trimAdtsFramesByEarliestDts: trimAdtsFramesByEarliestDts, 41030 generateSampleTable: generateSampleTable$1, 41031 concatenateFrameData: concatenateFrameData 41032 }; 41033 /** 41034 * mux.js 41035 * 41036 * Copyright (c) Brightcove 41037 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 41038 */ 41039 41040 var ONE_SECOND_IN_TS$1 = clock.ONE_SECOND_IN_TS; 41041 /** 41042 * Store information about the start and end of the track and the 41043 * duration for each frame/sample we process in order to calculate 41044 * the baseMediaDecodeTime 41045 */ 41046 41047 var collectDtsInfo = function collectDtsInfo(track, data) { 41048 if (typeof data.pts === 'number') { 41049 if (track.timelineStartInfo.pts === undefined) { 41050 track.timelineStartInfo.pts = data.pts; 41051 } 41052 41053 if (track.minSegmentPts === undefined) { 41054 track.minSegmentPts = data.pts; 41055 } else { 41056 track.minSegmentPts = Math.min(track.minSegmentPts, data.pts); 41057 } 41058 41059 if (track.maxSegmentPts === undefined) { 41060 track.maxSegmentPts = data.pts; 41061 } else { 41062 track.maxSegmentPts = Math.max(track.maxSegmentPts, data.pts); 41063 } 41064 } 41065 41066 if (typeof data.dts === 'number') { 41067 if (track.timelineStartInfo.dts === undefined) { 41068 track.timelineStartInfo.dts = data.dts; 41069 } 41070 41071 if (track.minSegmentDts === undefined) { 41072 track.minSegmentDts = data.dts; 41073 } else { 41074 track.minSegmentDts = Math.min(track.minSegmentDts, data.dts); 41075 } 41076 41077 if (track.maxSegmentDts === undefined) { 41078 track.maxSegmentDts = data.dts; 41079 } else { 41080 track.maxSegmentDts = Math.max(track.maxSegmentDts, data.dts); 41081 } 41082 } 41083 }; 41084 /** 41085 * Clear values used to calculate the baseMediaDecodeTime between 41086 * tracks 41087 */ 41088 41089 41090 var clearDtsInfo = function clearDtsInfo(track) { 41091 delete track.minSegmentDts; 41092 delete track.maxSegmentDts; 41093 delete track.minSegmentPts; 41094 delete track.maxSegmentPts; 41095 }; 41096 /** 41097 * Calculate the track's baseMediaDecodeTime based on the earliest 41098 * DTS the transmuxer has ever seen and the minimum DTS for the 41099 * current track 41100 * @param track {object} track metadata configuration 41101 * @param keepOriginalTimestamps {boolean} If true, keep the timestamps 41102 * in the source; false to adjust the first segment to start at 0. 41103 */ 41104 41105 41106 var calculateTrackBaseMediaDecodeTime = function calculateTrackBaseMediaDecodeTime(track, keepOriginalTimestamps) { 41107 var baseMediaDecodeTime, 41108 scale, 41109 minSegmentDts = track.minSegmentDts; // Optionally adjust the time so the first segment starts at zero. 41110 41111 if (!keepOriginalTimestamps) { 41112 minSegmentDts -= track.timelineStartInfo.dts; 41113 } // track.timelineStartInfo.baseMediaDecodeTime is the location, in time, where 41114 // we want the start of the first segment to be placed 41115 41116 41117 baseMediaDecodeTime = track.timelineStartInfo.baseMediaDecodeTime; // Add to that the distance this segment is from the very first 41118 41119 baseMediaDecodeTime += minSegmentDts; // baseMediaDecodeTime must not become negative 41120 41121 baseMediaDecodeTime = Math.max(0, baseMediaDecodeTime); 41122 41123 if (track.type === 'audio') { 41124 // Audio has a different clock equal to the sampling_rate so we need to 41125 // scale the PTS values into the clock rate of the track 41126 scale = track.samplerate / ONE_SECOND_IN_TS$1; 41127 baseMediaDecodeTime *= scale; 41128 baseMediaDecodeTime = Math.floor(baseMediaDecodeTime); 41129 } 41130 41131 return baseMediaDecodeTime; 41132 }; 41133 41134 var trackDecodeInfo = { 41135 clearDtsInfo: clearDtsInfo, 41136 calculateTrackBaseMediaDecodeTime: calculateTrackBaseMediaDecodeTime, 41137 collectDtsInfo: collectDtsInfo 41138 }; 41139 /** 41140 * mux.js 41141 * 41142 * Copyright (c) Brightcove 41143 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 41144 * 41145 * Reads in-band caption information from a video elementary 41146 * stream. Captions must follow the CEA-708 standard for injection 41147 * into an MPEG-2 transport streams. 41148 * @see https://en.wikipedia.org/wiki/CEA-708 41149 * @see https://www.gpo.gov/fdsys/pkg/CFR-2007-title47-vol1/pdf/CFR-2007-title47-vol1-sec15-119.pdf 41150 */ 41151 // Supplemental enhancement information (SEI) NAL units have a 41152 // payload type field to indicate how they are to be 41153 // interpreted. CEAS-708 caption content is always transmitted with 41154 // payload type 0x04. 41155 41156 var USER_DATA_REGISTERED_ITU_T_T35 = 4, 41157 RBSP_TRAILING_BITS = 128; 41158 /** 41159 * Parse a supplemental enhancement information (SEI) NAL unit. 41160 * Stops parsing once a message of type ITU T T35 has been found. 41161 * 41162 * @param bytes {Uint8Array} the bytes of a SEI NAL unit 41163 * @return {object} the parsed SEI payload 41164 * @see Rec. ITU-T H.264, 7.3.2.3.1 41165 */ 41166 41167 var parseSei = function parseSei(bytes) { 41168 var i = 0, 41169 result = { 41170 payloadType: -1, 41171 payloadSize: 0 41172 }, 41173 payloadType = 0, 41174 payloadSize = 0; // go through the sei_rbsp parsing each each individual sei_message 41175 41176 while (i < bytes.byteLength) { 41177 // stop once we have hit the end of the sei_rbsp 41178 if (bytes[i] === RBSP_TRAILING_BITS) { 41179 break; 41180 } // Parse payload type 41181 41182 41183 while (bytes[i] === 0xFF) { 41184 payloadType += 255; 41185 i++; 41186 } 41187 41188 payloadType += bytes[i++]; // Parse payload size 41189 41190 while (bytes[i] === 0xFF) { 41191 payloadSize += 255; 41192 i++; 41193 } 41194 41195 payloadSize += bytes[i++]; // this sei_message is a 608/708 caption so save it and break 41196 // there can only ever be one caption message in a frame's sei 41197 41198 if (!result.payload && payloadType === USER_DATA_REGISTERED_ITU_T_T35) { 41199 result.payloadType = payloadType; 41200 result.payloadSize = payloadSize; 41201 result.payload = bytes.subarray(i, i + payloadSize); 41202 break; 41203 } // skip the payload and parse the next message 41204 41205 41206 i += payloadSize; 41207 payloadType = 0; 41208 payloadSize = 0; 41209 } 41210 41211 return result; 41212 }; // see ANSI/SCTE 128-1 (2013), section 8.1 41213 41214 41215 var parseUserData = function parseUserData(sei) { 41216 // itu_t_t35_contry_code must be 181 (United States) for 41217 // captions 41218 if (sei.payload[0] !== 181) { 41219 return null; 41220 } // itu_t_t35_provider_code should be 49 (ATSC) for captions 41221 41222 41223 if ((sei.payload[1] << 8 | sei.payload[2]) !== 49) { 41224 return null; 41225 } // the user_identifier should be "GA94" to indicate ATSC1 data 41226 41227 41228 if (String.fromCharCode(sei.payload[3], sei.payload[4], sei.payload[5], sei.payload[6]) !== 'GA94') { 41229 return null; 41230 } // finally, user_data_type_code should be 0x03 for caption data 41231 41232 41233 if (sei.payload[7] !== 0x03) { 41234 return null; 41235 } // return the user_data_type_structure and strip the trailing 41236 // marker bits 41237 41238 41239 return sei.payload.subarray(8, sei.payload.length - 1); 41240 }; // see CEA-708-D, section 4.4 41241 41242 41243 var parseCaptionPackets = function parseCaptionPackets(pts, userData) { 41244 var results = [], 41245 i, 41246 count, 41247 offset, 41248 data; // if this is just filler, return immediately 41249 41250 if (!(userData[0] & 0x40)) { 41251 return results; 41252 } // parse out the cc_data_1 and cc_data_2 fields 41253 41254 41255 count = userData[0] & 0x1f; 41256 41257 for (i = 0; i < count; i++) { 41258 offset = i * 3; 41259 data = { 41260 type: userData[offset + 2] & 0x03, 41261 pts: pts 41262 }; // capture cc data when cc_valid is 1 41263 41264 if (userData[offset + 2] & 0x04) { 41265 data.ccData = userData[offset + 3] << 8 | userData[offset + 4]; 41266 results.push(data); 41267 } 41268 } 41269 41270 return results; 41271 }; 41272 41273 var discardEmulationPreventionBytes = function discardEmulationPreventionBytes(data) { 41274 var length = data.byteLength, 41275 emulationPreventionBytesPositions = [], 41276 i = 1, 41277 newLength, 41278 newData; // Find all `Emulation Prevention Bytes` 41279 41280 while (i < length - 2) { 41281 if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0x03) { 41282 emulationPreventionBytesPositions.push(i + 2); 41283 i += 2; 41284 } else { 41285 i++; 41286 } 41287 } // If no Emulation Prevention Bytes were found just return the original 41288 // array 41289 41290 41291 if (emulationPreventionBytesPositions.length === 0) { 41292 return data; 41293 } // Create a new array to hold the NAL unit data 41294 41295 41296 newLength = length - emulationPreventionBytesPositions.length; 41297 newData = new Uint8Array(newLength); 41298 var sourceIndex = 0; 41299 41300 for (i = 0; i < newLength; sourceIndex++, i++) { 41301 if (sourceIndex === emulationPreventionBytesPositions[0]) { 41302 // Skip this byte 41303 sourceIndex++; // Remove this position index 41304 41305 emulationPreventionBytesPositions.shift(); 41306 } 41307 41308 newData[i] = data[sourceIndex]; 41309 } 41310 41311 return newData; 41312 }; // exports 41313 41314 41315 var captionPacketParser = { 41316 parseSei: parseSei, 41317 parseUserData: parseUserData, 41318 parseCaptionPackets: parseCaptionPackets, 41319 discardEmulationPreventionBytes: discardEmulationPreventionBytes, 41320 USER_DATA_REGISTERED_ITU_T_T35: USER_DATA_REGISTERED_ITU_T_T35 41321 }; // ----------------- 41322 // Link To Transport 41323 // ----------------- 41324 41325 var CaptionStream = function CaptionStream() { 41326 CaptionStream.prototype.init.call(this); 41327 this.captionPackets_ = []; 41328 this.ccStreams_ = [new Cea608Stream(0, 0), // eslint-disable-line no-use-before-define 41329 new Cea608Stream(0, 1), // eslint-disable-line no-use-before-define 41330 new Cea608Stream(1, 0), // eslint-disable-line no-use-before-define 41331 new Cea608Stream(1, 1) // eslint-disable-line no-use-before-define 41332 ]; 41333 this.reset(); // forward data and done events from CCs to this CaptionStream 41334 41335 this.ccStreams_.forEach(function (cc) { 41336 cc.on('data', this.trigger.bind(this, 'data')); 41337 cc.on('partialdone', this.trigger.bind(this, 'partialdone')); 41338 cc.on('done', this.trigger.bind(this, 'done')); 41339 }, this); 41340 }; 41341 41342 CaptionStream.prototype = new stream(); 41343 41344 CaptionStream.prototype.push = function (event) { 41345 var sei, userData, newCaptionPackets; // only examine SEI NALs 41346 41347 if (event.nalUnitType !== 'sei_rbsp') { 41348 return; 41349 } // parse the sei 41350 41351 41352 sei = captionPacketParser.parseSei(event.escapedRBSP); // ignore everything but user_data_registered_itu_t_t35 41353 41354 if (sei.payloadType !== captionPacketParser.USER_DATA_REGISTERED_ITU_T_T35) { 41355 return; 41356 } // parse out the user data payload 41357 41358 41359 userData = captionPacketParser.parseUserData(sei); // ignore unrecognized userData 41360 41361 if (!userData) { 41362 return; 41363 } // Sometimes, the same segment # will be downloaded twice. To stop the 41364 // caption data from being processed twice, we track the latest dts we've 41365 // received and ignore everything with a dts before that. However, since 41366 // data for a specific dts can be split across packets on either side of 41367 // a segment boundary, we need to make sure we *don't* ignore the packets 41368 // from the *next* segment that have dts === this.latestDts_. By constantly 41369 // tracking the number of packets received with dts === this.latestDts_, we 41370 // know how many should be ignored once we start receiving duplicates. 41371 41372 41373 if (event.dts < this.latestDts_) { 41374 // We've started getting older data, so set the flag. 41375 this.ignoreNextEqualDts_ = true; 41376 return; 41377 } else if (event.dts === this.latestDts_ && this.ignoreNextEqualDts_) { 41378 this.numSameDts_--; 41379 41380 if (!this.numSameDts_) { 41381 // We've received the last duplicate packet, time to start processing again 41382 this.ignoreNextEqualDts_ = false;
vendor: 6,377 bytes, lines 41383-41569
41383 } 41384 41385 return; 41386 } // parse out CC data packets and save them for later 41387 41388 41389 newCaptionPackets = captionPacketParser.parseCaptionPackets(event.pts, userData); 41390 this.captionPackets_ = this.captionPackets_.concat(newCaptionPackets); 41391 41392 if (this.latestDts_ !== event.dts) { 41393 this.numSameDts_ = 0; 41394 } 41395 41396 this.numSameDts_++; 41397 this.latestDts_ = event.dts; 41398 }; 41399 41400 CaptionStream.prototype.flushCCStreams = function (flushType) { 41401 this.ccStreams_.forEach(function (cc) { 41402 return flushType === 'flush' ? cc.flush() : cc.partialFlush(); 41403 }, this); 41404 }; 41405 41406 CaptionStream.prototype.flushStream = function (flushType) { 41407 // make sure we actually parsed captions before proceeding 41408 if (!this.captionPackets_.length) { 41409 this.flushCCStreams(flushType); 41410 return; 41411 } // In Chrome, the Array#sort function is not stable so add a 41412 // presortIndex that we can use to ensure we get a stable-sort 41413 41414 41415 this.captionPackets_.forEach(function (elem, idx) { 41416 elem.presortIndex = idx; 41417 }); // sort caption byte-pairs based on their PTS values 41418 41419 this.captionPackets_.sort(function (a, b) { 41420 if (a.pts === b.pts) { 41421 return a.presortIndex - b.presortIndex; 41422 } 41423 41424 return a.pts - b.pts; 41425 }); 41426 this.captionPackets_.forEach(function (packet) { 41427 if (packet.type < 2) { 41428 // Dispatch packet to the right Cea608Stream 41429 this.dispatchCea608Packet(packet); 41430 } // this is where an 'else' would go for a dispatching packets 41431 // to a theoretical Cea708Stream that handles SERVICEn data 41432 41433 }, this); 41434 this.captionPackets_.length = 0; 41435 this.flushCCStreams(flushType); 41436 }; 41437 41438 CaptionStream.prototype.flush = function () { 41439 return this.flushStream('flush'); 41440 }; // Only called if handling partial data 41441 41442 41443 CaptionStream.prototype.partialFlush = function () { 41444 return this.flushStream('partialFlush'); 41445 }; 41446 41447 CaptionStream.prototype.reset = function () { 41448 this.latestDts_ = null; 41449 this.ignoreNextEqualDts_ = false; 41450 this.numSameDts_ = 0; 41451 this.activeCea608Channel_ = [null, null]; 41452 this.ccStreams_.forEach(function (ccStream) { 41453 ccStream.reset(); 41454 }); 41455 }; // From the CEA-608 spec: 41456 41457 /* 41458 * When XDS sub-packets are interleaved with other services, the end of each sub-packet shall be followed 41459 * by a control pair to change to a different service. When any of the control codes from 0x10 to 0x1F is 41460 * used to begin a control code pair, it indicates the return to captioning or Text data. The control code pair 41461 * and subsequent data should then be processed according to the FCC rules. It may be necessary for the 41462 * line 21 data encoder to automatically insert a control code pair (i.e. RCL, RU2, RU3, RU4, RDC, or RTD) 41463 * to switch to captioning or Text. 41464 */ 41465 // With that in mind, we ignore any data between an XDS control code and a 41466 // subsequent closed-captioning control code. 41467 41468 41469 CaptionStream.prototype.dispatchCea608Packet = function (packet) { 41470 // NOTE: packet.type is the CEA608 field 41471 if (this.setsTextOrXDSActive(packet)) { 41472 this.activeCea608Channel_[packet.type] = null; 41473 } else if (this.setsChannel1Active(packet)) { 41474 this.activeCea608Channel_[packet.type] = 0; 41475 } else if (this.setsChannel2Active(packet)) { 41476 this.activeCea608Channel_[packet.type] = 1; 41477 } 41478 41479 if (this.activeCea608Channel_[packet.type] === null) { 41480 // If we haven't received anything to set the active channel, or the 41481 // packets are Text/XDS data, discard the data; we don't want jumbled 41482 // captions 41483 return; 41484 } 41485 41486 this.ccStreams_[(packet.type << 1) + this.activeCea608Channel_[packet.type]].push(packet); 41487 }; 41488 41489 CaptionStream.prototype.setsChannel1Active = function (packet) { 41490 return (packet.ccData & 0x7800) === 0x1000; 41491 }; 41492 41493 CaptionStream.prototype.setsChannel2Active = function (packet) { 41494 return (packet.ccData & 0x7800) === 0x1800; 41495 }; 41496 41497 CaptionStream.prototype.setsTextOrXDSActive = function (packet) { 41498 return (packet.ccData & 0x7100) === 0x0100 || (packet.ccData & 0x78fe) === 0x102a || (packet.ccData & 0x78fe) === 0x182a; 41499 }; // ---------------------- 41500 // Session to Application 41501 // ---------------------- 41502 // This hash maps non-ASCII, special, and extended character codes to their 41503 // proper Unicode equivalent. The first keys that are only a single byte 41504 // are the non-standard ASCII characters, which simply map the CEA608 byte 41505 // to the standard ASCII/Unicode. The two-byte keys that follow are the CEA608 41506 // character codes, but have their MSB bitmasked with 0x03 so that a lookup 41507 // can be performed regardless of the field and data channel on which the 41508 // character code was received. 41509 41510 41511 var CHARACTER_TRANSLATION = { 41512 0x2a: 0xe1, 41513 // á 41514 0x5c: 0xe9, 41515 // é 41516 0x5e: 0xed, 41517 // à 41518 0x5f: 0xf3, 41519 // ó 41520 0x60: 0xfa, 41521 // ú 41522 0x7b: 0xe7, 41523 // ç 41524 0x7c: 0xf7, 41525 // ÷ 41526 0x7d: 0xd1, 41527 // à 41528 0x7e: 0xf1, 41529 // ñ 41530 0x7f: 0x2588, 41531 // â 41532 0x0130: 0xae, 41533 // ® 41534 0x0131: 0xb0, 41535 // ° 41536 0x0132: 0xbd, 41537 // ½ 41538 0x0133: 0xbf, 41539 // ¿ 41540 0x0134: 0x2122, 41541 // ⢠41542 0x0135: 0xa2, 41543 // ¢ 41544 0x0136: 0xa3, 41545 // £ 41546 0x0137: 0x266a, 41547 // ⪠41548 0x0138: 0xe0, 41549 // à 41550 0x0139: 0xa0, 41551 // 41552 0x013a: 0xe8, 41553 // è 41554 0x013b: 0xe2, 41555 // â 41556 0x013c: 0xea, 41557 // ê 41558 0x013d: 0xee, 41559 // î 41560 0x013e: 0xf4, 41561 // ô 41562 0x013f: 0xfb, 41563 // û 41564 0x0220: 0xc1, 41565 // à 41566 0x0221: 0xc9, 41567 // à 41568 0x0222: 0xd3, 41569 // Ã
vendor: 21,954 bytes, lines 41570-42162
41570 0x0223: 0xda, 41571 // à 41572 0x0224: 0xdc, 41573 // à 41574 0x0225: 0xfc, 41575 // ü 41576 0x0226: 0x2018, 41577 // â 41578 0x0227: 0xa1, 41579 // ¡ 41580 0x0228: 0x2a, 41581 // * 41582 0x0229: 0x27, 41583 // ' 41584 0x022a: 0x2014, 41585 // â 41586 0x022b: 0xa9, 41587 // © 41588 0x022c: 0x2120, 41589 // â 41590 0x022d: 0x2022, 41591 // ⢠41592 0x022e: 0x201c, 41593 // â 41594 0x022f: 0x201d, 41595 // â 41596 0x0230: 0xc0, 41597 // à 41598 0x0231: 0xc2, 41599 // à 41600 0x0232: 0xc7, 41601 // à 41602 0x0233: 0xc8, 41603 // à 41604 0x0234: 0xca, 41605 // à 41606 0x0235: 0xcb, 41607 // à 41608 0x0236: 0xeb, 41609 // ë 41610 0x0237: 0xce, 41611 // à 41612 0x0238: 0xcf, 41613 // à 41614 0x0239: 0xef, 41615 // ï 41616 0x023a: 0xd4, 41617 // à 41618 0x023b: 0xd9, 41619 // à 41620 0x023c: 0xf9, 41621 // ù 41622 0x023d: 0xdb, 41623 // à 41624 0x023e: 0xab, 41625 // « 41626 0x023f: 0xbb, 41627 // » 41628 0x0320: 0xc3, 41629 // à 41630 0x0321: 0xe3, 41631 // ã 41632 0x0322: 0xcd, 41633 // à 41634 0x0323: 0xcc, 41635 // à 41636 0x0324: 0xec, 41637 // ì 41638 0x0325: 0xd2, 41639 // à 41640 0x0326: 0xf2, 41641 // ò 41642 0x0327: 0xd5, 41643 // à 41644 0x0328: 0xf5, 41645 // õ 41646 0x0329: 0x7b, 41647 // { 41648 0x032a: 0x7d, 41649 // } 41650 0x032b: 0x5c, 41651 // \ 41652 0x032c: 0x5e, 41653 // ^ 41654 0x032d: 0x5f, 41655 // _ 41656 0x032e: 0x7c, 41657 // | 41658 0x032f: 0x7e, 41659 // ~ 41660 0x0330: 0xc4, 41661 // à 41662 0x0331: 0xe4, 41663 // ä 41664 0x0332: 0xd6, 41665 // à 41666 0x0333: 0xf6, 41667 // ö 41668 0x0334: 0xdf, 41669 // à 41670 0x0335: 0xa5, 41671 // Â¥ 41672 0x0336: 0xa4, 41673 // ¤ 41674 0x0337: 0x2502, 41675 // â 41676 0x0338: 0xc5, 41677 // à 41678 0x0339: 0xe5, 41679 // Ã¥ 41680 0x033a: 0xd8, 41681 // à 41682 0x033b: 0xf8, 41683 // ø 41684 0x033c: 0x250c, 41685 // â 41686 0x033d: 0x2510, 41687 // â 41688 0x033e: 0x2514, 41689 // â 41690 0x033f: 0x2518 // â 41691 41692 }; 41693 41694 var getCharFromCode = function getCharFromCode(code) { 41695 if (code === null) { 41696 return ''; 41697 } 41698 41699 code = CHARACTER_TRANSLATION[code] || code; 41700 return String.fromCharCode(code); 41701 }; // the index of the last row in a CEA-608 display buffer 41702 41703 41704 var BOTTOM_ROW = 14; // This array is used for mapping PACs -> row #, since there's no way of 41705 // getting it through bit logic. 41706 41707 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 41708 // cells. The "bottom" row is the last element in the outer array. 41709 41710 var createDisplayBuffer = function createDisplayBuffer() { 41711 var result = [], 41712 i = BOTTOM_ROW + 1; 41713 41714 while (i--) { 41715 result.push(''); 41716 } 41717 41718 return result; 41719 }; 41720 41721 var Cea608Stream = function Cea608Stream(field, dataChannel) { 41722 Cea608Stream.prototype.init.call(this); 41723 this.field_ = field || 0; 41724 this.dataChannel_ = dataChannel || 0; 41725 this.name_ = 'CC' + ((this.field_ << 1 | this.dataChannel_) + 1); 41726 this.setConstants(); 41727 this.reset(); 41728 41729 this.push = function (packet) { 41730 var data, swap, char0, char1, text; // remove the parity bits 41731 41732 data = packet.ccData & 0x7f7f; // ignore duplicate control codes; the spec demands they're sent twice 41733 41734 if (data === this.lastControlCode_) { 41735 this.lastControlCode_ = null; 41736 return; 41737 } // Store control codes 41738 41739 41740 if ((data & 0xf000) === 0x1000) { 41741 this.lastControlCode_ = data; 41742 } else if (data !== this.PADDING_) { 41743 this.lastControlCode_ = null; 41744 } 41745 41746 char0 = data >>> 8; 41747 char1 = data & 0xff; 41748 41749 if (data === this.PADDING_) { 41750 return; 41751 } else if (data === this.RESUME_CAPTION_LOADING_) { 41752 this.mode_ = 'popOn'; 41753 } else if (data === this.END_OF_CAPTION_) { 41754 // If an EOC is received while in paint-on mode, the displayed caption 41755 // text should be swapped to non-displayed memory as if it was a pop-on 41756 // caption. Because of that, we should explicitly switch back to pop-on 41757 // mode 41758 this.mode_ = 'popOn'; 41759 this.clearFormatting(packet.pts); // if a caption was being displayed, it's gone now 41760 41761 this.flushDisplayed(packet.pts); // flip memory 41762 41763 swap = this.displayed_; 41764 this.displayed_ = this.nonDisplayed_; 41765 this.nonDisplayed_ = swap; // start measuring the time to display the caption 41766 41767 this.startPts_ = packet.pts; 41768 } else if (data === this.ROLL_UP_2_ROWS_) { 41769 this.rollUpRows_ = 2; 41770 this.setRollUp(packet.pts); 41771 } else if (data === this.ROLL_UP_3_ROWS_) { 41772 this.rollUpRows_ = 3; 41773 this.setRollUp(packet.pts); 41774 } else if (data === this.ROLL_UP_4_ROWS_) { 41775 this.rollUpRows_ = 4; 41776 this.setRollUp(packet.pts); 41777 } else if (data === this.CARRIAGE_RETURN_) { 41778 this.clearFormatting(packet.pts); 41779 this.flushDisplayed(packet.pts); 41780 this.shiftRowsUp_(); 41781 this.startPts_ = packet.pts; 41782 } else if (data === this.BACKSPACE_) { 41783 if (this.mode_ === 'popOn') { 41784 this.nonDisplayed_[this.row_] = this.nonDisplayed_[this.row_].slice(0, -1); 41785 } else { 41786 this.displayed_[this.row_] = this.displayed_[this.row_].slice(0, -1); 41787 } 41788 } else if (data === this.ERASE_DISPLAYED_MEMORY_) { 41789 this.flushDisplayed(packet.pts); 41790 this.displayed_ = createDisplayBuffer(); 41791 } else if (data === this.ERASE_NON_DISPLAYED_MEMORY_) { 41792 this.nonDisplayed_ = createDisplayBuffer(); 41793 } else if (data === this.RESUME_DIRECT_CAPTIONING_) { 41794 if (this.mode_ !== 'paintOn') { 41795 // NOTE: This should be removed when proper caption positioning is 41796 // implemented 41797 this.flushDisplayed(packet.pts); 41798 this.displayed_ = createDisplayBuffer(); 41799 } 41800 41801 this.mode_ = 'paintOn'; 41802 this.startPts_ = packet.pts; // Append special characters to caption text 41803 } else if (this.isSpecialCharacter(char0, char1)) { 41804 // Bitmask char0 so that we can apply character transformations 41805 // regardless of field and data channel. 41806 // Then byte-shift to the left and OR with char1 so we can pass the 41807 // entire character code to `getCharFromCode`. 41808 char0 = (char0 & 0x03) << 8; 41809 text = getCharFromCode(char0 | char1); 41810 this[this.mode_](packet.pts, text); 41811 this.column_++; // Append extended characters to caption text 41812 } else if (this.isExtCharacter(char0, char1)) { 41813 // Extended characters always follow their "non-extended" equivalents. 41814 // IE if a "è" is desired, you'll always receive "eè"; non-compliant 41815 // decoders are supposed to drop the "è", while compliant decoders 41816 // backspace the "e" and insert "è". 41817 // Delete the previous character 41818 if (this.mode_ === 'popOn') { 41819 this.nonDisplayed_[this.row_] = this.nonDisplayed_[this.row_].slice(0, -1); 41820 } else { 41821 this.displayed_[this.row_] = this.displayed_[this.row_].slice(0, -1); 41822 } // Bitmask char0 so that we can apply character transformations 41823 // regardless of field and data channel. 41824 // Then byte-shift to the left and OR with char1 so we can pass the 41825 // entire character code to `getCharFromCode`. 41826 41827 41828 char0 = (char0 & 0x03) << 8; 41829 text = getCharFromCode(char0 | char1); 41830 this[this.mode_](packet.pts, text); 41831 this.column_++; // Process mid-row codes 41832 } else if (this.isMidRowCode(char0, char1)) { 41833 // Attributes are not additive, so clear all formatting 41834 this.clearFormatting(packet.pts); // According to the standard, mid-row codes 41835 // should be replaced with spaces, so add one now 41836 41837 this[this.mode_](packet.pts, ' '); 41838 this.column_++; 41839 41840 if ((char1 & 0xe) === 0xe) { 41841 this.addFormatting(packet.pts, ['i']); 41842 } 41843 41844 if ((char1 & 0x1) === 0x1) { 41845 this.addFormatting(packet.pts, ['u']); 41846 } // Detect offset control codes and adjust cursor 41847 41848 } else if (this.isOffsetControlCode(char0, char1)) { 41849 // Cursor position is set by indent PAC (see below) in 4-column 41850 // increments, with an additional offset code of 1-3 to reach any 41851 // of the 32 columns specified by CEA-608. So all we need to do 41852 // here is increment the column cursor by the given offset. 41853 this.column_ += char1 & 0x03; // Detect PACs (Preamble Address Codes) 41854 } else if (this.isPAC(char0, char1)) { 41855 // There's no logic for PAC -> row mapping, so we have to just 41856 // find the row code in an array and use its index :( 41857 var row = ROWS.indexOf(data & 0x1f20); // Configure the caption window if we're in roll-up mode 41858 41859 if (this.mode_ === 'rollUp') { 41860 // This implies that the base row is incorrectly set. 41861 // As per the recommendation in CEA-608(Base Row Implementation), defer to the number 41862 // of roll-up rows set. 41863 if (row - this.rollUpRows_ + 1 < 0) { 41864 row = this.rollUpRows_ - 1; 41865 } 41866 41867 this.setRollUp(packet.pts, row); 41868 } 41869 41870 if (row !== this.row_) { 41871 // formatting is only persistent for current row 41872 this.clearFormatting(packet.pts); 41873 this.row_ = row; 41874 } // All PACs can apply underline, so detect and apply 41875 // (All odd-numbered second bytes set underline) 41876 41877 41878 if (char1 & 0x1 && this.formatting_.indexOf('u') === -1) { 41879 this.addFormatting(packet.pts, ['u']); 41880 } 41881 41882 if ((data & 0x10) === 0x10) { 41883 // We've got an indent level code. Each successive even number 41884 // increments the column cursor by 4, so we can get the desired 41885 // column position by bit-shifting to the right (to get n/2) 41886 // and multiplying by 4. 41887 this.column_ = ((data & 0xe) >> 1) * 4; 41888 } 41889 41890 if (this.isColorPAC(char1)) { 41891 // it's a color code, though we only support white, which 41892 // can be either normal or italicized. white italics can be 41893 // either 0x4e or 0x6e depending on the row, so we just 41894 // bitwise-and with 0xe to see if italics should be turned on 41895 if ((char1 & 0xe) === 0xe) { 41896 this.addFormatting(packet.pts, ['i']); 41897 } 41898 } // We have a normal character in char0, and possibly one in char1 41899 41900 } else if (this.isNormalChar(char0)) { 41901 if (char1 === 0x00) { 41902 char1 = null; 41903 } 41904 41905 text = getCharFromCode(char0); 41906 text += getCharFromCode(char1); 41907 this[this.mode_](packet.pts, text); 41908 this.column_ += text.length; 41909 } // finish data processing 41910 41911 }; 41912 }; 41913 41914 Cea608Stream.prototype = new stream(); // Trigger a cue point that captures the current state of the 41915 // display buffer 41916 41917 Cea608Stream.prototype.flushDisplayed = function (pts) { 41918 var content = this.displayed_ // remove spaces from the start and end of the string 41919 .map(function (row) { 41920 try { 41921 return row.trim(); 41922 } catch (e) { 41923 // Ordinarily, this shouldn't happen. However, caption 41924 // parsing errors should not throw exceptions and 41925 // break playback. 41926 // eslint-disable-next-line no-console 41927 console.error('Skipping malformed caption.'); 41928 return ''; 41929 } 41930 }) // combine all text rows to display in one cue 41931 .join('\n') // and remove blank rows from the start and end, but not the middle 41932 .replace(/^\n+|\n+$/g, ''); 41933 41934 if (content.length) { 41935 this.trigger('data', { 41936 startPts: this.startPts_, 41937 endPts: pts, 41938 text: content, 41939 stream: this.name_ 41940 }); 41941 } 41942 }; 41943 /** 41944 * Zero out the data, used for startup and on seek 41945 */ 41946 41947 41948 Cea608Stream.prototype.reset = function () { 41949 this.mode_ = 'popOn'; // When in roll-up mode, the index of the last row that will 41950 // actually display captions. If a caption is shifted to a row 41951 // with a lower index than this, it is cleared from the display 41952 // buffer 41953 41954 this.topRow_ = 0; 41955 this.startPts_ = 0; 41956 this.displayed_ = createDisplayBuffer(); 41957 this.nonDisplayed_ = createDisplayBuffer(); 41958 this.lastControlCode_ = null; // Track row and column for proper line-breaking and spacing 41959 41960 this.column_ = 0; 41961 this.row_ = BOTTOM_ROW; 41962 this.rollUpRows_ = 2; // This variable holds currently-applied formatting 41963 41964 this.formatting_ = []; 41965 }; 41966 /** 41967 * Sets up control code and related constants for this instance 41968 */ 41969 41970 41971 Cea608Stream.prototype.setConstants = function () { 41972 // The following attributes have these uses: 41973 // ext_ : char0 for mid-row codes, and the base for extended 41974 // chars (ext_+0, ext_+1, and ext_+2 are char0s for 41975 // extended codes) 41976 // control_: char0 for control codes, except byte-shifted to the 41977 // left so that we can do this.control_ | CONTROL_CODE 41978 // offset_: char0 for tab offset codes 41979 // 41980 // It's also worth noting that control codes, and _only_ control codes, 41981 // differ between field 1 and field2. Field 2 control codes are always 41982 // their field 1 value plus 1. That's why there's the "| field" on the 41983 // control value. 41984 if (this.dataChannel_ === 0) { 41985 this.BASE_ = 0x10; 41986 this.EXT_ = 0x11; 41987 this.CONTROL_ = (0x14 | this.field_) << 8; 41988 this.OFFSET_ = 0x17; 41989 } else if (this.dataChannel_ === 1) { 41990 this.BASE_ = 0x18; 41991 this.EXT_ = 0x19; 41992 this.CONTROL_ = (0x1c | this.field_) << 8; 41993 this.OFFSET_ = 0x1f; 41994 } // Constants for the LSByte command codes recognized by Cea608Stream. This 41995 // list is not exhaustive. For a more comprehensive listing and semantics see 41996 // http://www.gpo.gov/fdsys/pkg/CFR-2010-title47-vol1/pdf/CFR-2010-title47-vol1-sec15-119.pdf 41997 // Padding 41998 41999 42000 this.PADDING_ = 0x0000; // Pop-on Mode 42001 42002 this.RESUME_CAPTION_LOADING_ = this.CONTROL_ | 0x20; 42003 this.END_OF_CAPTION_ = this.CONTROL_ | 0x2f; // Roll-up Mode 42004 42005 this.ROLL_UP_2_ROWS_ = this.CONTROL_ | 0x25; 42006 this.ROLL_UP_3_ROWS_ = this.CONTROL_ | 0x26; 42007 this.ROLL_UP_4_ROWS_ = this.CONTROL_ | 0x27; 42008 this.CARRIAGE_RETURN_ = this.CONTROL_ | 0x2d; // paint-on mode 42009 42010 this.RESUME_DIRECT_CAPTIONING_ = this.CONTROL_ | 0x29; // Erasure 42011 42012 this.BACKSPACE_ = this.CONTROL_ | 0x21; 42013 this.ERASE_DISPLAYED_MEMORY_ = this.CONTROL_ | 0x2c; 42014 this.ERASE_NON_DISPLAYED_MEMORY_ = this.CONTROL_ | 0x2e; 42015 }; 42016 /** 42017 * Detects if the 2-byte packet data is a special character 42018 * 42019 * Special characters have a second byte in the range 0x30 to 0x3f, 42020 * with the first byte being 0x11 (for data channel 1) or 0x19 (for 42021 * data channel 2). 42022 * 42023 * @param {Integer} char0 The first byte 42024 * @param {Integer} char1 The second byte 42025 * @return {Boolean} Whether the 2 bytes are an special character 42026 */ 42027 42028 42029 Cea608Stream.prototype.isSpecialCharacter = function (char0, char1) { 42030 return char0 === this.EXT_ && char1 >= 0x30 && char1 <= 0x3f; 42031 }; 42032 /** 42033 * Detects if the 2-byte packet data is an extended character 42034 * 42035 * Extended characters have a second byte in the range 0x20 to 0x3f, 42036 * with the first byte being 0x12 or 0x13 (for data channel 1) or 42037 * 0x1a or 0x1b (for data channel 2). 42038 * 42039 * @param {Integer} char0 The first byte 42040 * @param {Integer} char1 The second byte 42041 * @return {Boolean} Whether the 2 bytes are an extended character 42042 */ 42043 42044 42045 Cea608Stream.prototype.isExtCharacter = function (char0, char1) { 42046 return (char0 === this.EXT_ + 1 || char0 === this.EXT_ + 2) && char1 >= 0x20 && char1 <= 0x3f; 42047 }; 42048 /** 42049 * Detects if the 2-byte packet is a mid-row code 42050 * 42051 * Mid-row codes have a second byte in the range 0x20 to 0x2f, with 42052 * the first byte being 0x11 (for data channel 1) or 0x19 (for data 42053 * channel 2). 42054 * 42055 * @param {Integer} char0 The first byte 42056 * @param {Integer} char1 The second byte 42057 * @return {Boolean} Whether the 2 bytes are a mid-row code 42058 */ 42059 42060 42061 Cea608Stream.prototype.isMidRowCode = function (char0, char1) { 42062 return char0 === this.EXT_ && char1 >= 0x20 && char1 <= 0x2f; 42063 }; 42064 /** 42065 * Detects if the 2-byte packet is an offset control code 42066 * 42067 * Offset control codes have a second byte in the range 0x21 to 0x23, 42068 * with the first byte being 0x17 (for data channel 1) or 0x1f (for 42069 * data channel 2). 42070 * 42071 * @param {Integer} char0 The first byte 42072 * @param {Integer} char1 The second byte 42073 * @return {Boolean} Whether the 2 bytes are an offset control code 42074 */ 42075 42076 42077 Cea608Stream.prototype.isOffsetControlCode = function (char0, char1) { 42078 return char0 === this.OFFSET_ && char1 >= 0x21 && char1 <= 0x23; 42079 }; 42080 /** 42081 * Detects if the 2-byte packet is a Preamble Address Code 42082 * 42083 * PACs have a first byte in the range 0x10 to 0x17 (for data channel 1) 42084 * or 0x18 to 0x1f (for data channel 2), with the second byte in the 42085 * range 0x40 to 0x7f. 42086 * 42087 * @param {Integer} char0 The first byte 42088 * @param {Integer} char1 The second byte 42089 * @return {Boolean} Whether the 2 bytes are a PAC 42090 */ 42091 42092 42093 Cea608Stream.prototype.isPAC = function (char0, char1) { 42094 return char0 >= this.BASE_ && char0 < this.BASE_ + 8 && char1 >= 0x40 && char1 <= 0x7f; 42095 }; 42096 /** 42097 * Detects if a packet's second byte is in the range of a PAC color code 42098 * 42099 * PAC color codes have the second byte be in the range 0x40 to 0x4f, or 42100 * 0x60 to 0x6f. 42101 * 42102 * @param {Integer} char1 The second byte 42103 * @return {Boolean} Whether the byte is a color PAC 42104 */ 42105 42106 42107 Cea608Stream.prototype.isColorPAC = function (char1) { 42108 return char1 >= 0x40 && char1 <= 0x4f || char1 >= 0x60 && char1 <= 0x7f; 42109 }; 42110 /** 42111 * Detects if a single byte is in the range of a normal character 42112 * 42113 * Normal text bytes are in the range 0x20 to 0x7f. 42114 * 42115 * @param {Integer} char The byte 42116 * @return {Boolean} Whether the byte is a normal character 42117 */ 42118 42119 42120 Cea608Stream.prototype.isNormalChar = function (_char) { 42121 return _char >= 0x20 && _char <= 0x7f; 42122 }; 42123 /** 42124 * Configures roll-up 42125 * 42126 * @param {Integer} pts Current PTS 42127 * @param {Integer} newBaseRow Used by PACs to slide the current window to 42128 * a new position 42129 */ 42130 42131 42132 Cea608Stream.prototype.setRollUp = function (pts, newBaseRow) { 42133 // Reset the base row to the bottom row when switching modes 42134 if (this.mode_ !== 'rollUp') { 42135 this.row_ = BOTTOM_ROW; 42136 this.mode_ = 'rollUp'; // Spec says to wipe memories when switching to roll-up 42137 42138 this.flushDisplayed(pts); 42139 this.nonDisplayed_ = createDisplayBuffer(); 42140 this.displayed_ = createDisplayBuffer(); 42141 } 42142 42143 if (newBaseRow !== undefined && newBaseRow !== this.row_) { 42144 // move currently displayed captions (up or down) to the new base row 42145 for (var i = 0; i < this.rollUpRows_; i++) { 42146 this.displayed_[newBaseRow - i] = this.displayed_[this.row_ - i]; 42147 this.displayed_[this.row_ - i] = ''; 42148 } 42149 } 42150 42151 if (newBaseRow === undefined) { 42152 newBaseRow = this.row_; 42153 } 42154 42155 this.topRow_ = newBaseRow - this.rollUpRows_ + 1; 42156 }; // Adds the opening HTML tag for the passed character to the caption text, 42157 // and keeps track of it for later closing 42158 42159 42160 Cea608Stream.prototype.addFormatting = function (pts, format) { 42161 this.formatting_ = this.formatting_.concat(format); 42162 var text = format.reduce(function (text, format) {
vendor: 13,205 bytes, lines 42163-42525
42163 return text + '<' + format + '>'; 42164 }, ''); 42165 this[this.mode_](pts, text); 42166 }; // Adds HTML closing tags for current formatting to caption text and 42167 // clears remembered formatting 42168 42169 42170 Cea608Stream.prototype.clearFormatting = function (pts) { 42171 if (!this.formatting_.length) { 42172 return; 42173 } 42174 42175 var text = this.formatting_.reverse().reduce(function (text, format) { 42176 return text + '</' + format + '>'; 42177 }, ''); 42178 this.formatting_ = []; 42179 this[this.mode_](pts, text); 42180 }; // Mode Implementations 42181 42182 42183 Cea608Stream.prototype.popOn = function (pts, text) { 42184 var baseRow = this.nonDisplayed_[this.row_]; // buffer characters 42185 42186 baseRow += text; 42187 this.nonDisplayed_[this.row_] = baseRow; 42188 }; 42189 42190 Cea608Stream.prototype.rollUp = function (pts, text) { 42191 var baseRow = this.displayed_[this.row_]; 42192 baseRow += text; 42193 this.displayed_[this.row_] = baseRow; 42194 }; 42195 42196 Cea608Stream.prototype.shiftRowsUp_ = function () { 42197 var i; // clear out inactive rows 42198 42199 for (i = 0; i < this.topRow_; i++) { 42200 this.displayed_[i] = ''; 42201 } 42202 42203 for (i = this.row_ + 1; i < BOTTOM_ROW + 1; i++) { 42204 this.displayed_[i] = ''; 42205 } // shift displayed rows up 42206 42207 42208 for (i = this.topRow_; i < this.row_; i++) { 42209 this.displayed_[i] = this.displayed_[i + 1]; 42210 } // clear out the bottom row 42211 42212 42213 this.displayed_[this.row_] = ''; 42214 }; 42215 42216 Cea608Stream.prototype.paintOn = function (pts, text) { 42217 var baseRow = this.displayed_[this.row_]; 42218 baseRow += text; 42219 this.displayed_[this.row_] = baseRow; 42220 }; // exports 42221 42222 42223 var captionStream = { 42224 CaptionStream: CaptionStream, 42225 Cea608Stream: Cea608Stream 42226 }; 42227 /** 42228 * mux.js 42229 * 42230 * Copyright (c) Brightcove 42231 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 42232 */ 42233 42234 var streamTypes = { 42235 H264_STREAM_TYPE: 0x1B, 42236 ADTS_STREAM_TYPE: 0x0F, 42237 METADATA_STREAM_TYPE: 0x15 42238 }; 42239 var MAX_TS = 8589934592; 42240 var RO_THRESH = 4294967296; 42241 var TYPE_SHARED = 'shared'; 42242 42243 var handleRollover = function handleRollover(value, reference) { 42244 var direction = 1; 42245 42246 if (value > reference) { 42247 // If the current timestamp value is greater than our reference timestamp and we detect a 42248 // timestamp rollover, this means the roll over is happening in the opposite direction. 42249 // Example scenario: Enter a long stream/video just after a rollover occurred. The reference 42250 // point will be set to a small number, e.g. 1. The user then seeks backwards over the 42251 // rollover point. In loading this segment, the timestamp values will be very large, 42252 // e.g. 2^33 - 1. Since this comes before the data we loaded previously, we want to adjust 42253 // the time stamp to be `value - 2^33`. 42254 direction = -1; 42255 } // Note: A seek forwards or back that is greater than the RO_THRESH (2^32, ~13 hours) will 42256 // cause an incorrect adjustment. 42257 42258 42259 while (Math.abs(reference - value) > RO_THRESH) { 42260 value += direction * MAX_TS; 42261 } 42262 42263 return value; 42264 }; 42265 42266 var TimestampRolloverStream = function TimestampRolloverStream(type) { 42267 var lastDTS, referenceDTS; 42268 TimestampRolloverStream.prototype.init.call(this); // The "shared" type is used in cases where a stream will contain muxed 42269 // video and audio. We could use `undefined` here, but having a string 42270 // makes debugging a little clearer. 42271 42272 this.type_ = type || TYPE_SHARED; 42273 42274 this.push = function (data) { 42275 // Any "shared" rollover streams will accept _all_ data. Otherwise, 42276 // streams will only accept data that matches their type. 42277 if (this.type_ !== TYPE_SHARED && data.type !== this.type_) { 42278 return; 42279 } 42280 42281 if (referenceDTS === undefined) { 42282 referenceDTS = data.dts; 42283 } 42284 42285 data.dts = handleRollover(data.dts, referenceDTS); 42286 data.pts = handleRollover(data.pts, referenceDTS); 42287 lastDTS = data.dts; 42288 this.trigger('data', data); 42289 }; 42290 42291 this.flush = function () { 42292 referenceDTS = lastDTS; 42293 this.trigger('done'); 42294 }; 42295 42296 this.endTimeline = function () { 42297 this.flush(); 42298 this.trigger('endedtimeline'); 42299 }; 42300 42301 this.discontinuity = function () { 42302 referenceDTS = void 0; 42303 lastDTS = void 0; 42304 }; 42305 42306 this.reset = function () { 42307 this.discontinuity(); 42308 this.trigger('reset'); 42309 }; 42310 }; 42311 42312 TimestampRolloverStream.prototype = new stream(); 42313 var timestampRolloverStream = { 42314 TimestampRolloverStream: TimestampRolloverStream, 42315 handleRollover: handleRollover 42316 }; 42317 42318 var percentEncode = function percentEncode(bytes, start, end) { 42319 var i, 42320 result = ''; 42321 42322 for (i = start; i < end; i++) { 42323 result += '%' + ('00' + bytes[i].toString(16)).slice(-2); 42324 } 42325 42326 return result; 42327 }, 42328 // return the string representation of the specified byte range, 42329 // interpreted as UTf-8. 42330 parseUtf8 = function parseUtf8(bytes, start, end) { 42331 return decodeURIComponent(percentEncode(bytes, start, end)); 42332 }, 42333 // return the string representation of the specified byte range, 42334 // interpreted as ISO-8859-1. 42335 parseIso88591 = function parseIso88591(bytes, start, end) { 42336 return unescape(percentEncode(bytes, start, end)); // jshint ignore:line 42337 }, 42338 parseSyncSafeInteger = function parseSyncSafeInteger(data) { 42339 return data[0] << 21 | data[1] << 14 | data[2] << 7 | data[3]; 42340 }, 42341 tagParsers = { 42342 TXXX: function TXXX(tag) { 42343 var i; 42344 42345 if (tag.data[0] !== 3) { 42346 // ignore frames with unrecognized character encodings 42347 return; 42348 } 42349 42350 for (i = 1; i < tag.data.length; i++) { 42351 if (tag.data[i] === 0) { 42352 // parse the text fields 42353 tag.description = parseUtf8(tag.data, 1, i); // do not include the null terminator in the tag value 42354 42355 tag.value = parseUtf8(tag.data, i + 1, tag.data.length).replace(/\0*$/, ''); 42356 break; 42357 } 42358 } 42359 42360 tag.data = tag.value; 42361 }, 42362 WXXX: function WXXX(tag) { 42363 var i; 42364 42365 if (tag.data[0] !== 3) { 42366 // ignore frames with unrecognized character encodings 42367 return; 42368 } 42369 42370 for (i = 1; i < tag.data.length; i++) { 42371 if (tag.data[i] === 0) { 42372 // parse the description and URL fields 42373 tag.description = parseUtf8(tag.data, 1, i); 42374 tag.url = parseUtf8(tag.data, i + 1, tag.data.length); 42375 break; 42376 } 42377 } 42378 }, 42379 PRIV: function PRIV(tag) { 42380 var i; 42381 42382 for (i = 0; i < tag.data.length; i++) { 42383 if (tag.data[i] === 0) { 42384 // parse the description and URL fields 42385 tag.owner = parseIso88591(tag.data, 0, i); 42386 break; 42387 } 42388 } 42389 42390 tag.privateData = tag.data.subarray(i + 1); 42391 tag.data = tag.privateData; 42392 } 42393 }, 42394 _MetadataStream; 42395 42396 _MetadataStream = function MetadataStream(options) { 42397 var settings = { 42398 debug: !!(options && options.debug), 42399 // the bytes of the program-level descriptor field in MP2T 42400 // see ISO/IEC 13818-1:2013 (E), section 2.6 "Program and 42401 // program element descriptors" 42402 descriptor: options && options.descriptor 42403 }, 42404 // the total size in bytes of the ID3 tag being parsed 42405 tagSize = 0, 42406 // tag data that is not complete enough to be parsed 42407 buffer = [], 42408 // the total number of bytes currently in the buffer 42409 bufferSize = 0, 42410 i; 42411 42412 _MetadataStream.prototype.init.call(this); // calculate the text track in-band metadata track dispatch type 42413 // https://html.spec.whatwg.org/multipage/embedded-content.html#steps-to-expose-a-media-resource-specific-text-track 42414 42415 42416 this.dispatchType = streamTypes.METADATA_STREAM_TYPE.toString(16); 42417 42418 if (settings.descriptor) { 42419 for (i = 0; i < settings.descriptor.length; i++) { 42420 this.dispatchType += ('00' + settings.descriptor[i].toString(16)).slice(-2); 42421 } 42422 } 42423 42424 this.push = function (chunk) { 42425 var tag, frameStart, frameSize, frame, i, frameHeader; 42426 42427 if (chunk.type !== 'timed-metadata') { 42428 return; 42429 } // if data_alignment_indicator is set in the PES header, 42430 // we must have the start of a new ID3 tag. Assume anything 42431 // remaining in the buffer was malformed and throw it out 42432 42433 42434 if (chunk.dataAlignmentIndicator) { 42435 bufferSize = 0; 42436 buffer.length = 0; 42437 } // ignore events that don't look like ID3 data 42438 42439 42440 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))) { 42441 if (settings.debug) { 42442 // eslint-disable-next-line no-console 42443 console.log('Skipping unrecognized metadata packet'); 42444 } 42445 42446 return; 42447 } // add this chunk to the data we've collected so far 42448 42449 42450 buffer.push(chunk); 42451 bufferSize += chunk.data.byteLength; // grab the size of the entire frame from the ID3 header 42452 42453 if (buffer.length === 1) { 42454 // the frame size is transmitted as a 28-bit integer in the 42455 // last four bytes of the ID3 header. 42456 // The most significant bit of each byte is dropped and the 42457 // results concatenated to recover the actual value. 42458 tagSize = parseSyncSafeInteger(chunk.data.subarray(6, 10)); // ID3 reports the tag size excluding the header but it's more 42459 // convenient for our comparisons to include it 42460 42461 tagSize += 10; 42462 } // if the entire frame has not arrived, wait for more data 42463 42464 42465 if (bufferSize < tagSize) { 42466 return; 42467 } // collect the entire frame so it can be parsed 42468 42469 42470 tag = { 42471 data: new Uint8Array(tagSize), 42472 frames: [], 42473 pts: buffer[0].pts, 42474 dts: buffer[0].dts 42475 }; 42476 42477 for (i = 0; i < tagSize;) { 42478 tag.data.set(buffer[0].data.subarray(0, tagSize - i), i); 42479 i += buffer[0].data.byteLength; 42480 bufferSize -= buffer[0].data.byteLength; 42481 buffer.shift(); 42482 } // find the start of the first frame and the end of the tag 42483 42484 42485 frameStart = 10; 42486 42487 if (tag.data[5] & 0x40) { 42488 // advance the frame start past the extended header 42489 frameStart += 4; // header size field 42490 42491 frameStart += parseSyncSafeInteger(tag.data.subarray(10, 14)); // clip any padding off the end 42492 42493 tagSize -= parseSyncSafeInteger(tag.data.subarray(16, 20)); 42494 } // parse one or more ID3 frames 42495 // http://id3.org/id3v2.3.0#ID3v2_frame_overview 42496 42497 42498 do { 42499 // determine the number of bytes in this frame 42500 frameSize = parseSyncSafeInteger(tag.data.subarray(frameStart + 4, frameStart + 8)); 42501 42502 if (frameSize < 1) { 42503 // eslint-disable-next-line no-console 42504 return console.log('Malformed ID3 frame encountered. Skipping metadata parsing.'); 42505 } 42506 42507 frameHeader = String.fromCharCode(tag.data[frameStart], tag.data[frameStart + 1], tag.data[frameStart + 2], tag.data[frameStart + 3]); 42508 frame = { 42509 id: frameHeader, 42510 data: tag.data.subarray(frameStart + 10, frameStart + frameSize + 10) 42511 }; 42512 frame.key = frame.id; 42513 42514 if (tagParsers[frame.id]) { 42515 tagParsers[frame.id](frame); // handle the special PRIV frame used to indicate the start 42516 // time for raw AAC data 42517 42518 if (frame.owner === 'com.apple.streaming.transportStreamTimestamp') { 42519 var d = frame.data, 42520 size = (d[3] & 0x01) << 30 | d[4] << 22 | d[5] << 14 | d[6] << 6 | d[7] >>> 2; 42521 size *= 4; 42522 size += d[7] & 0x03; 42523 frame.timeStamp = size; // in raw AAC, all subsequent data will be timestamped based 42524 // on the value of this frame 42525 // we couldn't have known the appropriate pts and dts before
vendor: 10,312 bytes, lines 42526-42782
42526 // parsing this ID3 tag so set those values now 42527 42528 if (tag.pts === undefined && tag.dts === undefined) { 42529 tag.pts = frame.timeStamp; 42530 tag.dts = frame.timeStamp; 42531 } 42532 42533 this.trigger('timestamp', frame); 42534 } 42535 } 42536 42537 tag.frames.push(frame); 42538 frameStart += 10; // advance past the frame header 42539 42540 frameStart += frameSize; // advance past the frame body 42541 } while (frameStart < tagSize); 42542 42543 this.trigger('data', tag); 42544 }; 42545 }; 42546 42547 _MetadataStream.prototype = new stream(); 42548 var metadataStream = _MetadataStream; 42549 var TimestampRolloverStream$1 = timestampRolloverStream.TimestampRolloverStream; // object types 42550 42551 var _TransportPacketStream, _TransportParseStream, _ElementaryStream; // constants 42552 42553 42554 var MP2T_PACKET_LENGTH = 188, 42555 // bytes 42556 SYNC_BYTE = 0x47; 42557 /** 42558 * Splits an incoming stream of binary data into MPEG-2 Transport 42559 * Stream packets. 42560 */ 42561 42562 _TransportPacketStream = function TransportPacketStream() { 42563 var buffer = new Uint8Array(MP2T_PACKET_LENGTH), 42564 bytesInBuffer = 0; 42565 42566 _TransportPacketStream.prototype.init.call(this); // Deliver new bytes to the stream. 42567 42568 /** 42569 * Split a stream of data into M2TS packets 42570 **/ 42571 42572 42573 this.push = function (bytes) { 42574 var startIndex = 0, 42575 endIndex = MP2T_PACKET_LENGTH, 42576 everything; // If there are bytes remaining from the last segment, prepend them to the 42577 // bytes that were pushed in 42578 42579 if (bytesInBuffer) { 42580 everything = new Uint8Array(bytes.byteLength + bytesInBuffer); 42581 everything.set(buffer.subarray(0, bytesInBuffer)); 42582 everything.set(bytes, bytesInBuffer); 42583 bytesInBuffer = 0; 42584 } else { 42585 everything = bytes; 42586 } // While we have enough data for a packet 42587 42588 42589 while (endIndex < everything.byteLength) { 42590 // Look for a pair of start and end sync bytes in the data.. 42591 if (everything[startIndex] === SYNC_BYTE && everything[endIndex] === SYNC_BYTE) { 42592 // We found a packet so emit it and jump one whole packet forward in 42593 // the stream 42594 this.trigger('data', everything.subarray(startIndex, endIndex)); 42595 startIndex += MP2T_PACKET_LENGTH; 42596 endIndex += MP2T_PACKET_LENGTH; 42597 continue; 42598 } // If we get here, we have somehow become de-synchronized and we need to step 42599 // forward one byte at a time until we find a pair of sync bytes that denote 42600 // a packet 42601 42602 42603 startIndex++; 42604 endIndex++; 42605 } // If there was some data left over at the end of the segment that couldn't 42606 // possibly be a whole packet, keep it because it might be the start of a packet 42607 // that continues in the next segment 42608 42609 42610 if (startIndex < everything.byteLength) { 42611 buffer.set(everything.subarray(startIndex), 0); 42612 bytesInBuffer = everything.byteLength - startIndex; 42613 } 42614 }; 42615 /** 42616 * Passes identified M2TS packets to the TransportParseStream to be parsed 42617 **/ 42618 42619 42620 this.flush = function () { 42621 // If the buffer contains a whole packet when we are being flushed, emit it 42622 // and empty the buffer. Otherwise hold onto the data because it may be 42623 // important for decoding the next segment 42624 if (bytesInBuffer === MP2T_PACKET_LENGTH && buffer[0] === SYNC_BYTE) { 42625 this.trigger('data', buffer); 42626 bytesInBuffer = 0; 42627 } 42628 42629 this.trigger('done'); 42630 }; 42631 42632 this.endTimeline = function () { 42633 this.flush(); 42634 this.trigger('endedtimeline'); 42635 }; 42636 42637 this.reset = function () { 42638 bytesInBuffer = 0; 42639 this.trigger('reset'); 42640 }; 42641 }; 42642 42643 _TransportPacketStream.prototype = new stream(); 42644 /** 42645 * Accepts an MP2T TransportPacketStream and emits data events with parsed 42646 * forms of the individual transport stream packets. 42647 */ 42648 42649 _TransportParseStream = function TransportParseStream() { 42650 var parsePsi, parsePat, parsePmt, self; 42651 42652 _TransportParseStream.prototype.init.call(this); 42653 42654 self = this; 42655 this.packetsWaitingForPmt = []; 42656 this.programMapTable = undefined; 42657 42658 parsePsi = function parsePsi(payload, psi) { 42659 var offset = 0; // PSI packets may be split into multiple sections and those 42660 // sections may be split into multiple packets. If a PSI 42661 // section starts in this packet, the payload_unit_start_indicator 42662 // will be true and the first byte of the payload will indicate 42663 // the offset from the current position to the start of the 42664 // section. 42665 42666 if (psi.payloadUnitStartIndicator) { 42667 offset += payload[offset] + 1; 42668 } 42669 42670 if (psi.type === 'pat') { 42671 parsePat(payload.subarray(offset), psi); 42672 } else { 42673 parsePmt(payload.subarray(offset), psi); 42674 } 42675 }; 42676 42677 parsePat = function parsePat(payload, pat) { 42678 pat.section_number = payload[7]; // eslint-disable-line camelcase 42679 42680 pat.last_section_number = payload[8]; // eslint-disable-line camelcase 42681 // skip the PSI header and parse the first PMT entry 42682 42683 self.pmtPid = (payload[10] & 0x1F) << 8 | payload[11]; 42684 pat.pmtPid = self.pmtPid; 42685 }; 42686 /** 42687 * Parse out the relevant fields of a Program Map Table (PMT). 42688 * @param payload {Uint8Array} the PMT-specific portion of an MP2T 42689 * packet. The first byte in this array should be the table_id 42690 * field. 42691 * @param pmt {object} the object that should be decorated with 42692 * fields parsed from the PMT. 42693 */ 42694 42695 42696 parsePmt = function parsePmt(payload, pmt) { 42697 var sectionLength, tableEnd, programInfoLength, offset; // PMTs can be sent ahead of the time when they should actually 42698 // take effect. We don't believe this should ever be the case 42699 // for HLS but we'll ignore "forward" PMT declarations if we see 42700 // them. Future PMT declarations have the current_next_indicator 42701 // set to zero. 42702 42703 if (!(payload[5] & 0x01)) { 42704 return; 42705 } // overwrite any existing program map table 42706 42707 42708 self.programMapTable = { 42709 video: null, 42710 audio: null, 42711 'timed-metadata': {} 42712 }; // the mapping table ends at the end of the current section 42713 42714 sectionLength = (payload[1] & 0x0f) << 8 | payload[2]; 42715 tableEnd = 3 + sectionLength - 4; // to determine where the table is, we have to figure out how 42716 // long the program info descriptors are 42717 42718 programInfoLength = (payload[10] & 0x0f) << 8 | payload[11]; // advance the offset to the first entry in the mapping table 42719 42720 offset = 12 + programInfoLength; 42721 42722 while (offset < tableEnd) { 42723 var streamType = payload[offset]; 42724 var pid = (payload[offset + 1] & 0x1F) << 8 | payload[offset + 2]; // only map a single elementary_pid for audio and video stream types 42725 // TODO: should this be done for metadata too? for now maintain behavior of 42726 // multiple metadata streams 42727 42728 if (streamType === streamTypes.H264_STREAM_TYPE && self.programMapTable.video === null) { 42729 self.programMapTable.video = pid; 42730 } else if (streamType === streamTypes.ADTS_STREAM_TYPE && self.programMapTable.audio === null) { 42731 self.programMapTable.audio = pid; 42732 } else if (streamType === streamTypes.METADATA_STREAM_TYPE) { 42733 // map pid to stream type for metadata streams 42734 self.programMapTable['timed-metadata'][pid] = streamType; 42735 } // move to the next table entry 42736 // skip past the elementary stream descriptors, if present 42737 42738 42739 offset += ((payload[offset + 3] & 0x0F) << 8 | payload[offset + 4]) + 5; 42740 } // record the map on the packet as well 42741 42742 42743 pmt.programMapTable = self.programMapTable; 42744 }; 42745 /** 42746 * Deliver a new MP2T packet to the next stream in the pipeline. 42747 */ 42748 42749 42750 this.push = function (packet) { 42751 var result = {}, 42752 offset = 4; 42753 result.payloadUnitStartIndicator = !!(packet[1] & 0x40); // pid is a 13-bit field starting at the last bit of packet[1] 42754 42755 result.pid = packet[1] & 0x1f; 42756 result.pid <<= 8; 42757 result.pid |= packet[2]; // if an adaption field is present, its length is specified by the 42758 // fifth byte of the TS packet header. The adaptation field is 42759 // used to add stuffing to PES packets that don't fill a complete 42760 // TS packet, and to specify some forms of timing and control data 42761 // that we do not currently use. 42762 42763 if ((packet[3] & 0x30) >>> 4 > 0x01) { 42764 offset += packet[offset] + 1; 42765 } // parse the rest of the packet based on the type 42766 42767 42768 if (result.pid === 0) { 42769 result.type = 'pat'; 42770 parsePsi(packet.subarray(offset), result); 42771 this.trigger('data', result); 42772 } else if (result.pid === this.pmtPid) { 42773 result.type = 'pmt'; 42774 parsePsi(packet.subarray(offset), result); 42775 this.trigger('data', result); // if there are any packets waiting for a PMT to be found, process them now 42776 42777 while (this.packetsWaitingForPmt.length) { 42778 this.processPes_.apply(this, this.packetsWaitingForPmt.shift()); 42779 } 42780 } else if (this.programMapTable === undefined) { 42781 // When we have not seen a PMT yet, defer further processing of 42782 // PES packets until one has been parsed
vendor: 9,723 bytes, lines 42783-43041
42783 this.packetsWaitingForPmt.push([packet, offset, result]); 42784 } else { 42785 this.processPes_(packet, offset, result); 42786 } 42787 }; 42788 42789 this.processPes_ = function (packet, offset, result) { 42790 // set the appropriate stream type 42791 if (result.pid === this.programMapTable.video) { 42792 result.streamType = streamTypes.H264_STREAM_TYPE; 42793 } else if (result.pid === this.programMapTable.audio) { 42794 result.streamType = streamTypes.ADTS_STREAM_TYPE; 42795 } else { 42796 // if not video or audio, it is timed-metadata or unknown 42797 // if unknown, streamType will be undefined 42798 result.streamType = this.programMapTable['timed-metadata'][result.pid]; 42799 } 42800 42801 result.type = 'pes'; 42802 result.data = packet.subarray(offset); 42803 this.trigger('data', result); 42804 }; 42805 }; 42806 42807 _TransportParseStream.prototype = new stream(); 42808 _TransportParseStream.STREAM_TYPES = { 42809 h264: 0x1b, 42810 adts: 0x0f 42811 }; 42812 /** 42813 * Reconsistutes program elementary stream (PES) packets from parsed 42814 * transport stream packets. That is, if you pipe an 42815 * mp2t.TransportParseStream into a mp2t.ElementaryStream, the output 42816 * events will be events which capture the bytes for individual PES 42817 * packets plus relevant metadata that has been extracted from the 42818 * container. 42819 */ 42820 42821 _ElementaryStream = function ElementaryStream() { 42822 var self = this, 42823 // PES packet fragments 42824 video = { 42825 data: [], 42826 size: 0 42827 }, 42828 audio = { 42829 data: [], 42830 size: 0 42831 }, 42832 timedMetadata = { 42833 data: [], 42834 size: 0 42835 }, 42836 programMapTable, 42837 parsePes = function parsePes(payload, pes) { 42838 var ptsDtsFlags; // get the packet length, this will be 0 for video 42839 42840 pes.packetLength = 6 + (payload[4] << 8 | payload[5]); // find out if this packets starts a new keyframe 42841 42842 pes.dataAlignmentIndicator = (payload[6] & 0x04) !== 0; // PES packets may be annotated with a PTS value, or a PTS value 42843 // and a DTS value. Determine what combination of values is 42844 // available to work with. 42845 42846 ptsDtsFlags = payload[7]; // PTS and DTS are normally stored as a 33-bit number. Javascript 42847 // performs all bitwise operations on 32-bit integers but javascript 42848 // supports a much greater range (52-bits) of integer using standard 42849 // mathematical operations. 42850 // We construct a 31-bit value using bitwise operators over the 31 42851 // most significant bits and then multiply by 4 (equal to a left-shift 42852 // of 2) before we add the final 2 least significant bits of the 42853 // timestamp (equal to an OR.) 42854 42855 if (ptsDtsFlags & 0xC0) { 42856 // the PTS and DTS are not written out directly. For information 42857 // on how they are encoded, see 42858 // http://dvd.sourceforge.net/dvdinfo/pes-hdr.html 42859 pes.pts = (payload[9] & 0x0E) << 27 | (payload[10] & 0xFF) << 20 | (payload[11] & 0xFE) << 12 | (payload[12] & 0xFF) << 5 | (payload[13] & 0xFE) >>> 3; 42860 pes.pts *= 4; // Left shift by 2 42861 42862 pes.pts += (payload[13] & 0x06) >>> 1; // OR by the two LSBs 42863 42864 pes.dts = pes.pts; 42865 42866 if (ptsDtsFlags & 0x40) { 42867 pes.dts = (payload[14] & 0x0E) << 27 | (payload[15] & 0xFF) << 20 | (payload[16] & 0xFE) << 12 | (payload[17] & 0xFF) << 5 | (payload[18] & 0xFE) >>> 3; 42868 pes.dts *= 4; // Left shift by 2 42869 42870 pes.dts += (payload[18] & 0x06) >>> 1; // OR by the two LSBs 42871 } 42872 } // the data section starts immediately after the PES header. 42873 // pes_header_data_length specifies the number of header bytes 42874 // that follow the last byte of the field. 42875 42876 42877 pes.data = payload.subarray(9 + payload[8]); 42878 }, 42879 42880 /** 42881 * Pass completely parsed PES packets to the next stream in the pipeline 42882 **/ 42883 flushStream = function flushStream(stream$$1, type, forceFlush) { 42884 var packetData = new Uint8Array(stream$$1.size), 42885 event = { 42886 type: type 42887 }, 42888 i = 0, 42889 offset = 0, 42890 packetFlushable = false, 42891 fragment; // do nothing if there is not enough buffered data for a complete 42892 // PES header 42893 42894 if (!stream$$1.data.length || stream$$1.size < 9) { 42895 return; 42896 } 42897 42898 event.trackId = stream$$1.data[0].pid; // reassemble the packet 42899 42900 for (i = 0; i < stream$$1.data.length; i++) { 42901 fragment = stream$$1.data[i]; 42902 packetData.set(fragment.data, offset); 42903 offset += fragment.data.byteLength; 42904 } // parse assembled packet's PES header 42905 42906 42907 parsePes(packetData, event); // non-video PES packets MUST have a non-zero PES_packet_length 42908 // check that there is enough stream data to fill the packet 42909 42910 packetFlushable = type === 'video' || event.packetLength <= stream$$1.size; // flush pending packets if the conditions are right 42911 42912 if (forceFlush || packetFlushable) { 42913 stream$$1.size = 0; 42914 stream$$1.data.length = 0; 42915 } // only emit packets that are complete. this is to avoid assembling 42916 // incomplete PES packets due to poor segmentation 42917 42918 42919 if (packetFlushable) { 42920 self.trigger('data', event); 42921 } 42922 }; 42923 42924 _ElementaryStream.prototype.init.call(this); 42925 /** 42926 * Identifies M2TS packet types and parses PES packets using metadata 42927 * parsed from the PMT 42928 **/ 42929 42930 42931 this.push = function (data) { 42932 ({ 42933 pat: function pat() {// we have to wait for the PMT to arrive as well before we 42934 // have any meaningful metadata 42935 }, 42936 pes: function pes() { 42937 var stream$$1, streamType; 42938 42939 switch (data.streamType) { 42940 case streamTypes.H264_STREAM_TYPE: 42941 case streamTypes.H264_STREAM_TYPE: 42942 stream$$1 = video; 42943 streamType = 'video'; 42944 break; 42945 42946 case streamTypes.ADTS_STREAM_TYPE: 42947 stream$$1 = audio; 42948 streamType = 'audio'; 42949 break; 42950 42951 case streamTypes.METADATA_STREAM_TYPE: 42952 stream$$1 = timedMetadata; 42953 streamType = 'timed-metadata'; 42954 break; 42955 42956 default: 42957 // ignore unknown stream types 42958 return; 42959 } // if a new packet is starting, we can flush the completed 42960 // packet 42961 42962 42963 if (data.payloadUnitStartIndicator) { 42964 flushStream(stream$$1, streamType, true); 42965 } // buffer this fragment until we are sure we've received the 42966 // complete payload 42967 42968 42969 stream$$1.data.push(data); 42970 stream$$1.size += data.data.byteLength; 42971 }, 42972 pmt: function pmt() { 42973 var event = { 42974 type: 'metadata', 42975 tracks: [] 42976 }; 42977 programMapTable = data.programMapTable; // translate audio and video streams to tracks 42978 42979 if (programMapTable.video !== null) { 42980 event.tracks.push({ 42981 timelineStartInfo: { 42982 baseMediaDecodeTime: 0 42983 }, 42984 id: +programMapTable.video, 42985 codec: 'avc', 42986 type: 'video' 42987 }); 42988 } 42989 42990 if (programMapTable.audio !== null) { 42991 event.tracks.push({ 42992 timelineStartInfo: { 42993 baseMediaDecodeTime: 0 42994 }, 42995 id: +programMapTable.audio, 42996 codec: 'adts', 42997 type: 'audio' 42998 }); 42999 } 43000 43001 self.trigger('data', event); 43002 } 43003 })[data.type](); 43004 }; 43005 43006 this.reset = function () { 43007 video.size = 0; 43008 video.data.length = 0; 43009 audio.size = 0; 43010 audio.data.length = 0; 43011 this.trigger('reset'); 43012 }; 43013 /** 43014 * Flush any remaining input. Video PES packets may be of variable 43015 * length. Normally, the start of a new video packet can trigger the 43016 * finalization of the previous packet. That is not possible if no 43017 * more video is forthcoming, however. In that case, some other 43018 * mechanism (like the end of the file) has to be employed. When it is 43019 * clear that no additional data is forthcoming, calling this method 43020 * will flush the buffered packets. 43021 */ 43022 43023 43024 this.flushStreams_ = function () { 43025 // !!THIS ORDER IS IMPORTANT!! 43026 // video first then audio 43027 flushStream(video, 'video'); 43028 flushStream(audio, 'audio'); 43029 flushStream(timedMetadata, 'timed-metadata'); 43030 }; 43031 43032 this.flush = function () { 43033 this.flushStreams_(); 43034 this.trigger('done'); 43035 }; 43036 }; 43037 43038 _ElementaryStream.prototype = new stream(); 43039 var m2ts = { 43040 PAT_PID: 0x0000, 43041 MP2T_PACKET_LENGTH: MP2T_PACKET_LENGTH,
vendor: 6,108 bytes, lines 43042-43209
43042 TransportPacketStream: _TransportPacketStream, 43043 TransportParseStream: _TransportParseStream, 43044 ElementaryStream: _ElementaryStream, 43045 TimestampRolloverStream: TimestampRolloverStream$1, 43046 CaptionStream: captionStream.CaptionStream, 43047 Cea608Stream: captionStream.Cea608Stream, 43048 MetadataStream: metadataStream 43049 }; 43050 43051 for (var type in streamTypes) { 43052 if (streamTypes.hasOwnProperty(type)) { 43053 m2ts[type] = streamTypes[type]; 43054 } 43055 } 43056 43057 var m2ts_1 = m2ts; 43058 var ONE_SECOND_IN_TS$2 = clock.ONE_SECOND_IN_TS; 43059 43060 var _AdtsStream; 43061 43062 var ADTS_SAMPLING_FREQUENCIES = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350]; 43063 /* 43064 * Accepts a ElementaryStream and emits data events with parsed 43065 * AAC Audio Frames of the individual packets. Input audio in ADTS 43066 * format is unpacked and re-emitted as AAC frames. 43067 * 43068 * @see http://wiki.multimedia.cx/index.php?title=ADTS 43069 * @see http://wiki.multimedia.cx/?title=Understanding_AAC 43070 */ 43071 43072 _AdtsStream = function AdtsStream(handlePartialSegments) { 43073 var buffer, 43074 frameNum = 0; 43075 43076 _AdtsStream.prototype.init.call(this); 43077 43078 this.push = function (packet) { 43079 var i = 0, 43080 frameLength, 43081 protectionSkipBytes, 43082 frameEnd, 43083 oldBuffer, 43084 sampleCount, 43085 adtsFrameDuration; 43086 43087 if (!handlePartialSegments) { 43088 frameNum = 0; 43089 } 43090 43091 if (packet.type !== 'audio') { 43092 // ignore non-audio data 43093 return; 43094 } // Prepend any data in the buffer to the input data so that we can parse 43095 // aac frames the cross a PES packet boundary 43096 43097 43098 if (buffer) { 43099 oldBuffer = buffer; 43100 buffer = new Uint8Array(oldBuffer.byteLength + packet.data.byteLength); 43101 buffer.set(oldBuffer); 43102 buffer.set(packet.data, oldBuffer.byteLength); 43103 } else { 43104 buffer = packet.data; 43105 } // unpack any ADTS frames which have been fully received 43106 // for details on the ADTS header, see http://wiki.multimedia.cx/index.php?title=ADTS 43107 43108 43109 while (i + 5 < buffer.length) { 43110 // Look for the start of an ADTS header.. 43111 if (buffer[i] !== 0xFF || (buffer[i + 1] & 0xF6) !== 0xF0) { 43112 // If a valid header was not found, jump one forward and attempt to 43113 // find a valid ADTS header starting at the next byte 43114 i++; 43115 continue; 43116 } // The protection skip bit tells us if we have 2 bytes of CRC data at the 43117 // end of the ADTS header 43118 43119 43120 protectionSkipBytes = (~buffer[i + 1] & 0x01) * 2; // Frame length is a 13 bit integer starting 16 bits from the 43121 // end of the sync sequence 43122 43123 frameLength = (buffer[i + 3] & 0x03) << 11 | buffer[i + 4] << 3 | (buffer[i + 5] & 0xe0) >> 5; 43124 sampleCount = ((buffer[i + 6] & 0x03) + 1) * 1024; 43125 adtsFrameDuration = sampleCount * ONE_SECOND_IN_TS$2 / ADTS_SAMPLING_FREQUENCIES[(buffer[i + 2] & 0x3c) >>> 2]; 43126 frameEnd = i + frameLength; // If we don't have enough data to actually finish this ADTS frame, return 43127 // and wait for more data 43128 43129 if (buffer.byteLength < frameEnd) { 43130 return; 43131 } // Otherwise, deliver the complete AAC frame 43132 43133 43134 this.trigger('data', { 43135 pts: packet.pts + frameNum * adtsFrameDuration, 43136 dts: packet.dts + frameNum * adtsFrameDuration, 43137 sampleCount: sampleCount, 43138 audioobjecttype: (buffer[i + 2] >>> 6 & 0x03) + 1, 43139 channelcount: (buffer[i + 2] & 1) << 2 | (buffer[i + 3] & 0xc0) >>> 6, 43140 samplerate: ADTS_SAMPLING_FREQUENCIES[(buffer[i + 2] & 0x3c) >>> 2], 43141 samplingfrequencyindex: (buffer[i + 2] & 0x3c) >>> 2, 43142 // assume ISO/IEC 14496-12 AudioSampleEntry default of 16 43143 samplesize: 16, 43144 data: buffer.subarray(i + 7 + protectionSkipBytes, frameEnd) 43145 }); 43146 frameNum++; // If the buffer is empty, clear it and return 43147 43148 if (buffer.byteLength === frameEnd) { 43149 buffer = undefined; 43150 return; 43151 } // Remove the finished frame from the buffer and start the process again 43152 43153 43154 buffer = buffer.subarray(frameEnd); 43155 } 43156 }; 43157 43158 this.flush = function () { 43159 frameNum = 0; 43160 this.trigger('done'); 43161 }; 43162 43163 this.reset = function () { 43164 buffer = void 0; 43165 this.trigger('reset'); 43166 }; 43167 43168 this.endTimeline = function () { 43169 buffer = void 0; 43170 this.trigger('endedtimeline'); 43171 }; 43172 }; 43173 43174 _AdtsStream.prototype = new stream(); 43175 var adts = _AdtsStream; 43176 /** 43177 * mux.js 43178 * 43179 * Copyright (c) Brightcove 43180 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 43181 */ 43182 43183 var ExpGolomb; 43184 /** 43185 * Parser for exponential Golomb codes, a variable-bitwidth number encoding 43186 * scheme used by h264. 43187 */ 43188 43189 ExpGolomb = function ExpGolomb(workingData) { 43190 var // the number of bytes left to examine in workingData 43191 workingBytesAvailable = workingData.byteLength, 43192 // the current word being examined 43193 workingWord = 0, 43194 // :uint 43195 // the number of bits left to examine in the current word 43196 workingBitsAvailable = 0; // :uint; 43197 // ():uint 43198 43199 this.length = function () { 43200 return 8 * workingBytesAvailable; 43201 }; // ():uint 43202 43203 43204 this.bitsAvailable = function () { 43205 return 8 * workingBytesAvailable + workingBitsAvailable; 43206 }; // ():void 43207 43208 43209 this.loadWord = function () {
vendor: 8,404 bytes, lines 43210-43487
43210 var position = workingData.byteLength - workingBytesAvailable, 43211 workingBytes = new Uint8Array(4), 43212 availableBytes = Math.min(4, workingBytesAvailable); 43213 43214 if (availableBytes === 0) { 43215 throw new Error('no bytes available'); 43216 } 43217 43218 workingBytes.set(workingData.subarray(position, position + availableBytes)); 43219 workingWord = new DataView(workingBytes.buffer).getUint32(0); // track the amount of workingData that has been processed 43220 43221 workingBitsAvailable = availableBytes * 8; 43222 workingBytesAvailable -= availableBytes; 43223 }; // (count:int):void 43224 43225 43226 this.skipBits = function (count) { 43227 var skipBytes; // :int 43228 43229 if (workingBitsAvailable > count) { 43230 workingWord <<= count; 43231 workingBitsAvailable -= count; 43232 } else { 43233 count -= workingBitsAvailable; 43234 skipBytes = Math.floor(count / 8); 43235 count -= skipBytes * 8; 43236 workingBytesAvailable -= skipBytes; 43237 this.loadWord(); 43238 workingWord <<= count; 43239 workingBitsAvailable -= count; 43240 } 43241 }; // (size:int):uint 43242 43243 43244 this.readBits = function (size) { 43245 var bits = Math.min(workingBitsAvailable, size), 43246 // :uint 43247 valu = workingWord >>> 32 - bits; // :uint 43248 // if size > 31, handle error 43249 43250 workingBitsAvailable -= bits; 43251 43252 if (workingBitsAvailable > 0) { 43253 workingWord <<= bits; 43254 } else if (workingBytesAvailable > 0) { 43255 this.loadWord(); 43256 } 43257 43258 bits = size - bits; 43259 43260 if (bits > 0) { 43261 return valu << bits | this.readBits(bits); 43262 } 43263 43264 return valu; 43265 }; // ():uint 43266 43267 43268 this.skipLeadingZeros = function () { 43269 var leadingZeroCount; // :uint 43270 43271 for (leadingZeroCount = 0; leadingZeroCount < workingBitsAvailable; ++leadingZeroCount) { 43272 if ((workingWord & 0x80000000 >>> leadingZeroCount) !== 0) { 43273 // the first bit of working word is 1 43274 workingWord <<= leadingZeroCount; 43275 workingBitsAvailable -= leadingZeroCount; 43276 return leadingZeroCount; 43277 } 43278 } // we exhausted workingWord and still have not found a 1 43279 43280 43281 this.loadWord(); 43282 return leadingZeroCount + this.skipLeadingZeros(); 43283 }; // ():void 43284 43285 43286 this.skipUnsignedExpGolomb = function () { 43287 this.skipBits(1 + this.skipLeadingZeros()); 43288 }; // ():void 43289 43290 43291 this.skipExpGolomb = function () { 43292 this.skipBits(1 + this.skipLeadingZeros()); 43293 }; // ():uint 43294 43295 43296 this.readUnsignedExpGolomb = function () { 43297 var clz = this.skipLeadingZeros(); // :uint 43298 43299 return this.readBits(clz + 1) - 1; 43300 }; // ():int 43301 43302 43303 this.readExpGolomb = function () { 43304 var valu = this.readUnsignedExpGolomb(); // :int 43305 43306 if (0x01 & valu) { 43307 // the number is odd if the low order bit is set 43308 return 1 + valu >>> 1; // add 1 to make it even, and divide by 2 43309 } 43310 43311 return -1 * (valu >>> 1); // divide by two then make it negative 43312 }; // Some convenience functions 43313 // :Boolean 43314 43315 43316 this.readBoolean = function () { 43317 return this.readBits(1) === 1; 43318 }; // ():int 43319 43320 43321 this.readUnsignedByte = function () { 43322 return this.readBits(8); 43323 }; 43324 43325 this.loadWord(); 43326 }; 43327 43328 var expGolomb = ExpGolomb; 43329 43330 var _H264Stream, _NalByteStream; 43331 43332 var PROFILES_WITH_OPTIONAL_SPS_DATA; 43333 /** 43334 * Accepts a NAL unit byte stream and unpacks the embedded NAL units. 43335 */ 43336 43337 _NalByteStream = function NalByteStream() { 43338 var syncPoint = 0, 43339 i, 43340 buffer; 43341 43342 _NalByteStream.prototype.init.call(this); 43343 /* 43344 * Scans a byte stream and triggers a data event with the NAL units found. 43345 * @param {Object} data Event received from H264Stream 43346 * @param {Uint8Array} data.data The h264 byte stream to be scanned 43347 * 43348 * @see H264Stream.push 43349 */ 43350 43351 43352 this.push = function (data) { 43353 var swapBuffer; 43354 43355 if (!buffer) { 43356 buffer = data.data; 43357 } else { 43358 swapBuffer = new Uint8Array(buffer.byteLength + data.data.byteLength); 43359 swapBuffer.set(buffer); 43360 swapBuffer.set(data.data, buffer.byteLength); 43361 buffer = swapBuffer; 43362 } 43363 43364 var len = buffer.byteLength; // Rec. ITU-T H.264, Annex B 43365 // scan for NAL unit boundaries 43366 // a match looks like this: 43367 // 0 0 1 .. NAL .. 0 0 1 43368 // ^ sync point ^ i 43369 // or this: 43370 // 0 0 1 .. NAL .. 0 0 0 43371 // ^ sync point ^ i 43372 // advance the sync point to a NAL start, if necessary 43373 43374 for (; syncPoint < len - 3; syncPoint++) { 43375 if (buffer[syncPoint + 2] === 1) { 43376 // the sync point is properly aligned 43377 i = syncPoint + 5; 43378 break; 43379 } 43380 } 43381 43382 while (i < len) { 43383 // look at the current byte to determine if we've hit the end of 43384 // a NAL unit boundary 43385 switch (buffer[i]) { 43386 case 0: 43387 // skip past non-sync sequences 43388 if (buffer[i - 1] !== 0) { 43389 i += 2; 43390 break; 43391 } else if (buffer[i - 2] !== 0) { 43392 i++; 43393 break; 43394 } // deliver the NAL unit if it isn't empty 43395 43396 43397 if (syncPoint + 3 !== i - 2) { 43398 this.trigger('data', buffer.subarray(syncPoint + 3, i - 2)); 43399 } // drop trailing zeroes 43400 43401 43402 do { 43403 i++; 43404 } while (buffer[i] !== 1 && i < len); 43405 43406 syncPoint = i - 2; 43407 i += 3; 43408 break; 43409 43410 case 1: 43411 // skip past non-sync sequences 43412 if (buffer[i - 1] !== 0 || buffer[i - 2] !== 0) { 43413 i += 3; 43414 break; 43415 } // deliver the NAL unit 43416 43417 43418 this.trigger('data', buffer.subarray(syncPoint + 3, i - 2)); 43419 syncPoint = i - 2; 43420 i += 3; 43421 break; 43422 43423 default: 43424 // the current byte isn't a one or zero, so it cannot be part 43425 // of a sync sequence 43426 i += 3; 43427 break; 43428 } 43429 } // filter out the NAL units that were delivered 43430 43431 43432 buffer = buffer.subarray(syncPoint); 43433 i -= syncPoint; 43434 syncPoint = 0; 43435 }; 43436 43437 this.reset = function () { 43438 buffer = null; 43439 syncPoint = 0; 43440 this.trigger('reset'); 43441 }; 43442 43443 this.flush = function () { 43444 // deliver the last buffered NAL unit 43445 if (buffer && buffer.byteLength > 3) { 43446 this.trigger('data', buffer.subarray(syncPoint + 3)); 43447 } // reset the stream state 43448 43449 43450 buffer = null; 43451 syncPoint = 0; 43452 this.trigger('done'); 43453 }; 43454 43455 this.endTimeline = function () { 43456 this.flush(); 43457 this.trigger('endedtimeline'); 43458 }; 43459 }; 43460 43461 _NalByteStream.prototype = new stream(); // values of profile_idc that indicate additional fields are included in the SPS 43462 // see Recommendation ITU-T H.264 (4/2013), 43463 // 7.3.2.1.1 Sequence parameter set data syntax 43464 43465 PROFILES_WITH_OPTIONAL_SPS_DATA = { 43466 100: true, 43467 110: true, 43468 122: true, 43469 244: true, 43470 44: true, 43471 83: true, 43472 86: true, 43473 118: true, 43474 128: true, 43475 138: true, 43476 139: true, 43477 134: true 43478 }; 43479 /** 43480 * Accepts input from a ElementaryStream and produces H.264 NAL unit data 43481 * events. 43482 */ 43483 43484 _H264Stream = function H264Stream() { 43485 var nalByteStream = new _NalByteStream(), 43486 self, 43487 trackId,
43488 currentPts, 43489 currentDts, 43490 discardEmulationPreventionBytes, 43491 readSequenceParameterSet, 43492 skipScalingList; 43493 43494 _H264Stream.prototype.init.call(this); 43495 43496 self = this; 43497 /* 43498 * Pushes a packet from a stream onto the NalByteStream 43499 * 43500 * @param {Object} packet - A packet received from a stream 43501 * @param {Uint8Array} packet.data - The raw bytes of the packet 43502 * @param {Number} packet.dts - Decode timestamp of the packet 43503 * @param {Number} packet.pts - Presentation timestamp of the packet 43504 * @param {Number} packet.trackId - The id of the h264 track this packet came from 43505 * @param {('video'|'audio')} packet.type - The type of packet 43506 * 43507 */ 43508 43509 this.push = function (packet) { 43510 if (packet.type !== 'video') { 43511 return; 43512 } 43513 43514 trackId = packet.trackId; 43515 currentPts = packet.pts; 43516 currentDts = packet.dts; 43517 nalByteStream.push(packet); 43518 }; 43519 /* 43520 * Identify NAL unit types and pass on the NALU, trackId, presentation and decode timestamps 43521 * for the NALUs to the next stream component. 43522 * Also, preprocess caption and sequence parameter NALUs. 43523 * 43524 * @param {Uint8Array} data - A NAL unit identified by `NalByteStream.push` 43525 * @see NalByteStream.push 43526 */ 43527 43528 43529 nalByteStream.on('data', function (data) { 43530 var event = { 43531 trackId: trackId,
vendor: 12,657 bytes, lines 43532-43901
43532 pts: currentPts, 43533 dts: currentDts, 43534 data: data 43535 }; 43536 43537 switch (data[0] & 0x1f) { 43538 case 0x05: 43539 event.nalUnitType = 'slice_layer_without_partitioning_rbsp_idr'; 43540 break; 43541 43542 case 0x06: 43543 event.nalUnitType = 'sei_rbsp'; 43544 event.escapedRBSP = discardEmulationPreventionBytes(data.subarray(1)); 43545 break; 43546 43547 case 0x07: 43548 event.nalUnitType = 'seq_parameter_set_rbsp'; 43549 event.escapedRBSP = discardEmulationPreventionBytes(data.subarray(1)); 43550 event.config = readSequenceParameterSet(event.escapedRBSP); 43551 break; 43552 43553 case 0x08: 43554 event.nalUnitType = 'pic_parameter_set_rbsp'; 43555 break; 43556 43557 case 0x09: 43558 event.nalUnitType = 'access_unit_delimiter_rbsp'; 43559 break; 43560 43561 default: 43562 break; 43563 } // This triggers data on the H264Stream 43564 43565 43566 self.trigger('data', event); 43567 }); 43568 nalByteStream.on('done', function () { 43569 self.trigger('done'); 43570 }); 43571 nalByteStream.on('partialdone', function () { 43572 self.trigger('partialdone'); 43573 }); 43574 nalByteStream.on('reset', function () { 43575 self.trigger('reset'); 43576 }); 43577 nalByteStream.on('endedtimeline', function () { 43578 self.trigger('endedtimeline'); 43579 }); 43580 43581 this.flush = function () { 43582 nalByteStream.flush(); 43583 }; 43584 43585 this.partialFlush = function () { 43586 nalByteStream.partialFlush(); 43587 }; 43588 43589 this.reset = function () { 43590 nalByteStream.reset(); 43591 }; 43592 43593 this.endTimeline = function () { 43594 nalByteStream.endTimeline(); 43595 }; 43596 /** 43597 * Advance the ExpGolomb decoder past a scaling list. The scaling 43598 * list is optionally transmitted as part of a sequence parameter 43599 * set and is not relevant to transmuxing. 43600 * @param count {number} the number of entries in this scaling list 43601 * @param expGolombDecoder {object} an ExpGolomb pointed to the 43602 * start of a scaling list 43603 * @see Recommendation ITU-T H.264, Section 7.3.2.1.1.1 43604 */ 43605 43606 43607 skipScalingList = function skipScalingList(count, expGolombDecoder) { 43608 var lastScale = 8, 43609 nextScale = 8, 43610 j, 43611 deltaScale; 43612 43613 for (j = 0; j < count; j++) { 43614 if (nextScale !== 0) { 43615 deltaScale = expGolombDecoder.readExpGolomb(); 43616 nextScale = (lastScale + deltaScale + 256) % 256; 43617 } 43618 43619 lastScale = nextScale === 0 ? lastScale : nextScale; 43620 } 43621 }; 43622 /** 43623 * Expunge any "Emulation Prevention" bytes from a "Raw Byte 43624 * Sequence Payload" 43625 * @param data {Uint8Array} the bytes of a RBSP from a NAL 43626 * unit 43627 * @return {Uint8Array} the RBSP without any Emulation 43628 * Prevention Bytes 43629 */ 43630 43631 43632 discardEmulationPreventionBytes = function discardEmulationPreventionBytes(data) { 43633 var length = data.byteLength, 43634 emulationPreventionBytesPositions = [], 43635 i = 1, 43636 newLength, 43637 newData; // Find all `Emulation Prevention Bytes` 43638 43639 while (i < length - 2) { 43640 if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0x03) { 43641 emulationPreventionBytesPositions.push(i + 2); 43642 i += 2; 43643 } else { 43644 i++; 43645 } 43646 } // If no Emulation Prevention Bytes were found just return the original 43647 // array 43648 43649 43650 if (emulationPreventionBytesPositions.length === 0) { 43651 return data; 43652 } // Create a new array to hold the NAL unit data 43653 43654 43655 newLength = length - emulationPreventionBytesPositions.length; 43656 newData = new Uint8Array(newLength); 43657 var sourceIndex = 0; 43658 43659 for (i = 0; i < newLength; sourceIndex++, i++) { 43660 if (sourceIndex === emulationPreventionBytesPositions[0]) { 43661 // Skip this byte 43662 sourceIndex++; // Remove this position index 43663 43664 emulationPreventionBytesPositions.shift(); 43665 } 43666 43667 newData[i] = data[sourceIndex]; 43668 } 43669 43670 return newData; 43671 }; 43672 /** 43673 * Read a sequence parameter set and return some interesting video 43674 * properties. A sequence parameter set is the H264 metadata that 43675 * describes the properties of upcoming video frames. 43676 * @param data {Uint8Array} the bytes of a sequence parameter set 43677 * @return {object} an object with configuration parsed from the 43678 * sequence parameter set, including the dimensions of the 43679 * associated video frames. 43680 */ 43681 43682 43683 readSequenceParameterSet = function readSequenceParameterSet(data) { 43684 var frameCropLeftOffset = 0, 43685 frameCropRightOffset = 0, 43686 frameCropTopOffset = 0, 43687 frameCropBottomOffset = 0, 43688 sarScale = 1, 43689 expGolombDecoder, 43690 profileIdc, 43691 levelIdc, 43692 profileCompatibility, 43693 chromaFormatIdc, 43694 picOrderCntType, 43695 numRefFramesInPicOrderCntCycle, 43696 picWidthInMbsMinus1, 43697 picHeightInMapUnitsMinus1, 43698 frameMbsOnlyFlag, 43699 scalingListCount, 43700 sarRatio, 43701 aspectRatioIdc, 43702 i; 43703 expGolombDecoder = new expGolomb(data); 43704 profileIdc = expGolombDecoder.readUnsignedByte(); // profile_idc 43705 43706 profileCompatibility = expGolombDecoder.readUnsignedByte(); // constraint_set[0-5]_flag 43707 43708 levelIdc = expGolombDecoder.readUnsignedByte(); // level_idc u(8) 43709 43710 expGolombDecoder.skipUnsignedExpGolomb(); // seq_parameter_set_id 43711 // some profiles have more optional data we don't need 43712 43713 if (PROFILES_WITH_OPTIONAL_SPS_DATA[profileIdc]) { 43714 chromaFormatIdc = expGolombDecoder.readUnsignedExpGolomb(); 43715 43716 if (chromaFormatIdc === 3) { 43717 expGolombDecoder.skipBits(1); // separate_colour_plane_flag 43718 } 43719 43720 expGolombDecoder.skipUnsignedExpGolomb(); // bit_depth_luma_minus8 43721 43722 expGolombDecoder.skipUnsignedExpGolomb(); // bit_depth_chroma_minus8 43723 43724 expGolombDecoder.skipBits(1); // qpprime_y_zero_transform_bypass_flag 43725 43726 if (expGolombDecoder.readBoolean()) { 43727 // seq_scaling_matrix_present_flag 43728 scalingListCount = chromaFormatIdc !== 3 ? 8 : 12; 43729 43730 for (i = 0; i < scalingListCount; i++) { 43731 if (expGolombDecoder.readBoolean()) { 43732 // seq_scaling_list_present_flag[ i ] 43733 if (i < 6) { 43734 skipScalingList(16, expGolombDecoder); 43735 } else { 43736 skipScalingList(64, expGolombDecoder); 43737 } 43738 } 43739 } 43740 } 43741 } 43742 43743 expGolombDecoder.skipUnsignedExpGolomb(); // log2_max_frame_num_minus4 43744 43745 picOrderCntType = expGolombDecoder.readUnsignedExpGolomb(); 43746 43747 if (picOrderCntType === 0) { 43748 expGolombDecoder.readUnsignedExpGolomb(); // log2_max_pic_order_cnt_lsb_minus4 43749 } else if (picOrderCntType === 1) { 43750 expGolombDecoder.skipBits(1); // delta_pic_order_always_zero_flag 43751 43752 expGolombDecoder.skipExpGolomb(); // offset_for_non_ref_pic 43753 43754 expGolombDecoder.skipExpGolomb(); // offset_for_top_to_bottom_field 43755 43756 numRefFramesInPicOrderCntCycle = expGolombDecoder.readUnsignedExpGolomb(); 43757 43758 for (i = 0; i < numRefFramesInPicOrderCntCycle; i++) { 43759 expGolombDecoder.skipExpGolomb(); // offset_for_ref_frame[ i ] 43760 } 43761 } 43762 43763 expGolombDecoder.skipUnsignedExpGolomb(); // max_num_ref_frames 43764 43765 expGolombDecoder.skipBits(1); // gaps_in_frame_num_value_allowed_flag 43766 43767 picWidthInMbsMinus1 = expGolombDecoder.readUnsignedExpGolomb(); 43768 picHeightInMapUnitsMinus1 = expGolombDecoder.readUnsignedExpGolomb(); 43769 frameMbsOnlyFlag = expGolombDecoder.readBits(1); 43770 43771 if (frameMbsOnlyFlag === 0) { 43772 expGolombDecoder.skipBits(1); // mb_adaptive_frame_field_flag 43773 } 43774 43775 expGolombDecoder.skipBits(1); // direct_8x8_inference_flag 43776 43777 if (expGolombDecoder.readBoolean()) { 43778 // frame_cropping_flag 43779 frameCropLeftOffset = expGolombDecoder.readUnsignedExpGolomb(); 43780 frameCropRightOffset = expGolombDecoder.readUnsignedExpGolomb(); 43781 frameCropTopOffset = expGolombDecoder.readUnsignedExpGolomb(); 43782 frameCropBottomOffset = expGolombDecoder.readUnsignedExpGolomb(); 43783 } 43784 43785 if (expGolombDecoder.readBoolean()) { 43786 // vui_parameters_present_flag 43787 if (expGolombDecoder.readBoolean()) { 43788 // aspect_ratio_info_present_flag 43789 aspectRatioIdc = expGolombDecoder.readUnsignedByte(); 43790 43791 switch (aspectRatioIdc) { 43792 case 1: 43793 sarRatio = [1, 1]; 43794 break; 43795 43796 case 2: 43797 sarRatio = [12, 11]; 43798 break; 43799 43800 case 3: 43801 sarRatio = [10, 11]; 43802 break; 43803 43804 case 4: 43805 sarRatio = [16, 11]; 43806 break; 43807 43808 case 5: 43809 sarRatio = [40, 33]; 43810 break; 43811 43812 case 6: 43813 sarRatio = [24, 11]; 43814 break; 43815 43816 case 7: 43817 sarRatio = [20, 11]; 43818 break; 43819 43820 case 8: 43821 sarRatio = [32, 11]; 43822 break; 43823 43824 case 9: 43825 sarRatio = [80, 33]; 43826 break; 43827 43828 case 10: 43829 sarRatio = [18, 11]; 43830 break; 43831 43832 case 11: 43833 sarRatio = [15, 11]; 43834 break; 43835 43836 case 12: 43837 sarRatio = [64, 33]; 43838 break; 43839 43840 case 13: 43841 sarRatio = [160, 99]; 43842 break; 43843 43844 case 14: 43845 sarRatio = [4, 3]; 43846 break; 43847 43848 case 15: 43849 sarRatio = [3, 2]; 43850 break; 43851 43852 case 16: 43853 sarRatio = [2, 1]; 43854 break; 43855 43856 case 255: 43857 { 43858 sarRatio = [expGolombDecoder.readUnsignedByte() << 8 | expGolombDecoder.readUnsignedByte(), expGolombDecoder.readUnsignedByte() << 8 | expGolombDecoder.readUnsignedByte()]; 43859 break; 43860 } 43861 } 43862 43863 if (sarRatio) { 43864 sarScale = sarRatio[0] / sarRatio[1]; 43865 } 43866 } 43867 } 43868 43869 return { 43870 profileIdc: profileIdc, 43871 levelIdc: levelIdc, 43872 profileCompatibility: profileCompatibility, 43873 width: Math.ceil(((picWidthInMbsMinus1 + 1) * 16 - frameCropLeftOffset * 2 - frameCropRightOffset * 2) * sarScale), 43874 height: (2 - frameMbsOnlyFlag) * (picHeightInMapUnitsMinus1 + 1) * 16 - frameCropTopOffset * 2 - frameCropBottomOffset * 2, 43875 sarRatio: sarRatio 43876 }; 43877 }; 43878 }; 43879 43880 _H264Stream.prototype = new stream(); 43881 var h264 = { 43882 H264Stream: _H264Stream, 43883 NalByteStream: _NalByteStream 43884 }; 43885 /** 43886 * mux.js 43887 * 43888 * Copyright (c) Brightcove 43889 * Licensed Apache-2.0 https://github.com/videojs/mux.js/blob/master/LICENSE 43890 * 43891 * Utilities to detect basic properties and metadata about Aac data. 43892 */ 43893 43894 var ADTS_SAMPLING_FREQUENCIES$1 = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350]; 43895 43896 var isLikelyAacData = function isLikelyAacData(data) { 43897 if (data[0] === 'I'.charCodeAt(0) && data[1] === 'D'.charCodeAt(0) && data[2] === '3'.charCodeAt(0)) { 43898 return true; 43899 } 43900 43901 return false;
vendor: 7,509 bytes, lines 43902-44108
43902 }; 43903 43904 var parseSyncSafeInteger$1 = function parseSyncSafeInteger(data) { 43905 return data[0] << 21 | data[1] << 14 | data[2] << 7 | data[3]; 43906 }; // return a percent-encoded representation of the specified byte range 43907 // @see http://en.wikipedia.org/wiki/Percent-encoding 43908 43909 43910 var percentEncode$1 = function percentEncode(bytes, start, end) { 43911 var i, 43912 result = ''; 43913 43914 for (i = start; i < end; i++) { 43915 result += '%' + ('00' + bytes[i].toString(16)).slice(-2); 43916 } 43917 43918 return result; 43919 }; // return the string representation of the specified byte range, 43920 // interpreted as ISO-8859-1. 43921 43922 43923 var parseIso88591$1 = function parseIso88591(bytes, start, end) { 43924 return unescape(percentEncode$1(bytes, start, end)); // jshint ignore:line 43925 }; 43926 43927 var parseId3TagSize = function parseId3TagSize(header, byteIndex) { 43928 var returnSize = header[byteIndex + 6] << 21 | header[byteIndex + 7] << 14 | header[byteIndex + 8] << 7 | header[byteIndex + 9], 43929 flags = header[byteIndex + 5], 43930 footerPresent = (flags & 16) >> 4; 43931 43932 if (footerPresent) { 43933 return returnSize + 20; 43934 } 43935 43936 return returnSize + 10; 43937 }; 43938 43939 var parseAdtsSize = function parseAdtsSize(header, byteIndex) { 43940 var lowThree = (header[byteIndex + 5] & 0xE0) >> 5, 43941 middle = header[byteIndex + 4] << 3, 43942 highTwo = header[byteIndex + 3] & 0x3 << 11; 43943 return highTwo | middle | lowThree; 43944 }; 43945 43946 var parseType = function parseType(header, byteIndex) { 43947 if (header[byteIndex] === 'I'.charCodeAt(0) && header[byteIndex + 1] === 'D'.charCodeAt(0) && header[byteIndex + 2] === '3'.charCodeAt(0)) { 43948 return 'timed-metadata'; 43949 } else if (header[byteIndex] & 0xff === 0xff && (header[byteIndex + 1] & 0xf0) === 0xf0) { 43950 return 'audio'; 43951 } 43952 43953 return null; 43954 }; 43955 43956 var parseSampleRate = function parseSampleRate(packet) { 43957 var i = 0; 43958 43959 while (i + 5 < packet.length) { 43960 if (packet[i] !== 0xFF || (packet[i + 1] & 0xF6) !== 0xF0) { 43961 // If a valid header was not found, jump one forward and attempt to 43962 // find a valid ADTS header starting at the next byte 43963 i++; 43964 continue; 43965 } 43966 43967 return ADTS_SAMPLING_FREQUENCIES$1[(packet[i + 2] & 0x3c) >>> 2]; 43968 } 43969 43970 return null; 43971 }; 43972 43973 var parseAacTimestamp = function parseAacTimestamp(packet) { 43974 var frameStart, frameSize, frame, frameHeader; // find the start of the first frame and the end of the tag 43975 43976 frameStart = 10; 43977 43978 if (packet[5] & 0x40) { 43979 // advance the frame start past the extended header 43980 frameStart += 4; // header size field 43981 43982 frameStart += parseSyncSafeInteger$1(packet.subarray(10, 14)); 43983 } // parse one or more ID3 frames 43984 // http://id3.org/id3v2.3.0#ID3v2_frame_overview 43985 43986 43987 do { 43988 // determine the number of bytes in this frame 43989 frameSize = parseSyncSafeInteger$1(packet.subarray(frameStart + 4, frameStart + 8)); 43990 43991 if (frameSize < 1) { 43992 return null; 43993 } 43994 43995 frameHeader = String.fromCharCode(packet[frameStart], packet[frameStart + 1], packet[frameStart + 2], packet[frameStart + 3]); 43996 43997 if (frameHeader === 'PRIV') { 43998 frame = packet.subarray(frameStart + 10, frameStart + frameSize + 10); 43999 44000 for (var i = 0; i < frame.byteLength; i++) { 44001 if (frame[i] === 0) { 44002 var owner = parseIso88591$1(frame, 0, i); 44003 44004 if (owner === 'com.apple.streaming.transportStreamTimestamp') { 44005 var d = frame.subarray(i + 1); 44006 var size = (d[3] & 0x01) << 30 | d[4] << 22 | d[5] << 14 | d[6] << 6 | d[7] >>> 2; 44007 size *= 4; 44008 size += d[7] & 0x03; 44009 return size; 44010 } 44011 44012 break; 44013 } 44014 } 44015 } 44016 44017 frameStart += 10; // advance past the frame header 44018 44019 frameStart += frameSize; // advance past the frame body 44020 } while (frameStart < packet.byteLength); 44021 44022 return null; 44023 }; 44024 44025 var utils = { 44026 isLikelyAacData: isLikelyAacData, 44027 parseId3TagSize: parseId3TagSize, 44028 parseAdtsSize: parseAdtsSize, 44029 parseType: parseType, 44030 parseSampleRate: parseSampleRate, 44031 parseAacTimestamp: parseAacTimestamp 44032 }; // Constants 44033 44034 var _AacStream; 44035 /** 44036 * Splits an incoming stream of binary data into ADTS and ID3 Frames. 44037 */ 44038 44039 44040 _AacStream = function AacStream() { 44041 var everything = new Uint8Array(), 44042 timeStamp = 0; 44043 44044 _AacStream.prototype.init.call(this); 44045 44046 this.setTimestamp = function (timestamp) { 44047 timeStamp = timestamp; 44048 }; 44049 44050 this.push = function (bytes) { 44051 var frameSize = 0, 44052 byteIndex = 0, 44053 bytesLeft, 44054 chunk, 44055 packet, 44056 tempLength; // If there are bytes remaining from the last segment, prepend them to the 44057 // bytes that were pushed in 44058 44059 if (everything.length) { 44060 tempLength = everything.length; 44061 everything = new Uint8Array(bytes.byteLength + tempLength); 44062 everything.set(everything.subarray(0, tempLength)); 44063 everything.set(bytes, tempLength); 44064 } else { 44065 everything = bytes; 44066 } 44067 44068 while (everything.length - byteIndex >= 3) { 44069 if (everything[byteIndex] === 'I'.charCodeAt(0) && everything[byteIndex + 1] === 'D'.charCodeAt(0) && everything[byteIndex + 2] === '3'.charCodeAt(0)) { 44070 // Exit early because we don't have enough to parse 44071 // the ID3 tag header 44072 if (everything.length - byteIndex < 10) { 44073 break; 44074 } // check framesize 44075 44076 44077 frameSize = utils.parseId3TagSize(everything, byteIndex); // Exit early if we don't have enough in the buffer 44078 // to emit a full packet 44079 // Add to byteIndex to support multiple ID3 tags in sequence 44080 44081 if (byteIndex + frameSize > everything.length) { 44082 break; 44083 } 44084 44085 chunk = { 44086 type: 'timed-metadata', 44087 data: everything.subarray(byteIndex, byteIndex + frameSize) 44088 }; 44089 this.trigger('data', chunk); 44090 byteIndex += frameSize; 44091 continue; 44092 } else if ((everything[byteIndex] & 0xff) === 0xff && (everything[byteIndex + 1] & 0xf0) === 0xf0) { 44093 // Exit early because we don't have enough to parse 44094 // the ADTS frame header 44095 if (everything.length - byteIndex < 7) { 44096 break; 44097 } 44098 44099 frameSize = utils.parseAdtsSize(everything, byteIndex); // Exit early if we don't have enough in the buffer 44100 // to emit a full packet 44101 44102 if (byteIndex + frameSize > everything.length) { 44103 break; 44104 } 44105 44106 packet = { 44107 type: 'audio', 44108 data: everything.subarray(byteIndex, byteIndex + frameSize),
vendor: 8,212 bytes, lines 44109-44319
44109 pts: timeStamp, 44110 dts: timeStamp 44111 }; 44112 this.trigger('data', packet); 44113 byteIndex += frameSize; 44114 continue; 44115 } 44116 44117 byteIndex++; 44118 } 44119 44120 bytesLeft = everything.length - byteIndex; 44121 44122 if (bytesLeft > 0) { 44123 everything = everything.subarray(byteIndex); 44124 } else { 44125 everything = new Uint8Array(); 44126 } 44127 }; 44128 44129 this.reset = function () { 44130 everything = new Uint8Array(); 44131 this.trigger('reset'); 44132 }; 44133 44134 this.endTimeline = function () { 44135 everything = new Uint8Array(); 44136 this.trigger('endedtimeline'); 44137 }; 44138 }; 44139 44140 _AacStream.prototype = new stream(); 44141 var aac = _AacStream; 44142 var H264Stream = h264.H264Stream; 44143 var isLikelyAacData$1 = utils.isLikelyAacData; 44144 var ONE_SECOND_IN_TS$3 = clock.ONE_SECOND_IN_TS; // constants 44145 44146 var AUDIO_PROPERTIES = ['audioobjecttype', 'channelcount', 'samplerate', 'samplingfrequencyindex', 'samplesize']; 44147 var VIDEO_PROPERTIES = ['width', 'height', 'profileIdc', 'levelIdc', 'profileCompatibility', 'sarRatio']; // object types 44148 44149 var _VideoSegmentStream, _AudioSegmentStream, _Transmuxer, _CoalesceStream; 44150 /** 44151 * Compare two arrays (even typed) for same-ness 44152 */ 44153 44154 44155 var arrayEquals = function arrayEquals(a, b) { 44156 var i; 44157 44158 if (a.length !== b.length) { 44159 return false; 44160 } // compare the value of each element in the array 44161 44162 44163 for (i = 0; i < a.length; i++) { 44164 if (a[i] !== b[i]) { 44165 return false; 44166 } 44167 } 44168 44169 return true; 44170 }; 44171 44172 var generateVideoSegmentTimingInfo = function generateVideoSegmentTimingInfo(baseMediaDecodeTime, startDts, startPts, endDts, endPts, prependedContentDuration) { 44173 var ptsOffsetFromDts = startPts - startDts, 44174 decodeDuration = endDts - startDts, 44175 presentationDuration = endPts - startPts; // The PTS and DTS values are based on the actual stream times from the segment, 44176 // however, the player time values will reflect a start from the baseMediaDecodeTime. 44177 // In order to provide relevant values for the player times, base timing info on the 44178 // baseMediaDecodeTime and the DTS and PTS durations of the segment. 44179 44180 return { 44181 start: { 44182 dts: baseMediaDecodeTime, 44183 pts: baseMediaDecodeTime + ptsOffsetFromDts 44184 }, 44185 end: { 44186 dts: baseMediaDecodeTime + decodeDuration, 44187 pts: baseMediaDecodeTime + presentationDuration 44188 }, 44189 prependedContentDuration: prependedContentDuration, 44190 baseMediaDecodeTime: baseMediaDecodeTime 44191 }; 44192 }; 44193 /** 44194 * Constructs a single-track, ISO BMFF media segment from AAC data 44195 * events. The output of this stream can be fed to a SourceBuffer 44196 * configured with a suitable initialization segment. 44197 * @param track {object} track metadata configuration 44198 * @param options {object} transmuxer options object 44199 * @param options.keepOriginalTimestamps {boolean} If true, keep the timestamps 44200 * in the source; false to adjust the first segment to start at 0. 44201 */ 44202 44203 44204 _AudioSegmentStream = function AudioSegmentStream(track, options) { 44205 var adtsFrames = [], 44206 sequenceNumber = 0, 44207 earliestAllowedDts = 0, 44208 audioAppendStartTs = 0, 44209 videoBaseMediaDecodeTime = Infinity; 44210 options = options || {}; 44211 44212 _AudioSegmentStream.prototype.init.call(this); 44213 44214 this.push = function (data) { 44215 trackDecodeInfo.collectDtsInfo(track, data); 44216 44217 if (track) { 44218 AUDIO_PROPERTIES.forEach(function (prop) { 44219 track[prop] = data[prop]; 44220 }); 44221 } // buffer audio data until end() is called 44222 44223 44224 adtsFrames.push(data); 44225 }; 44226 44227 this.setEarliestDts = function (earliestDts) { 44228 earliestAllowedDts = earliestDts - track.timelineStartInfo.baseMediaDecodeTime; 44229 }; 44230 44231 this.setVideoBaseMediaDecodeTime = function (baseMediaDecodeTime) { 44232 videoBaseMediaDecodeTime = baseMediaDecodeTime; 44233 }; 44234 44235 this.setAudioAppendStart = function (timestamp) { 44236 audioAppendStartTs = timestamp; 44237 }; 44238 44239 this.flush = function () { 44240 var frames, moof, mdat, boxes, frameDuration; // return early if no audio data has been observed 44241 44242 if (adtsFrames.length === 0) { 44243 this.trigger('done', 'AudioSegmentStream'); 44244 return; 44245 } 44246 44247 frames = audioFrameUtils.trimAdtsFramesByEarliestDts(adtsFrames, track, earliestAllowedDts); 44248 track.baseMediaDecodeTime = trackDecodeInfo.calculateTrackBaseMediaDecodeTime(track, options.keepOriginalTimestamps); 44249 audioFrameUtils.prefixWithSilence(track, frames, audioAppendStartTs, videoBaseMediaDecodeTime); // we have to build the index from byte locations to 44250 // samples (that is, adts frames) in the audio data 44251 44252 track.samples = audioFrameUtils.generateSampleTable(frames); // concatenate the audio data to constuct the mdat 44253 44254 mdat = mp4Generator.mdat(audioFrameUtils.concatenateFrameData(frames)); 44255 adtsFrames = []; 44256 moof = mp4Generator.moof(sequenceNumber, [track]); 44257 boxes = new Uint8Array(moof.byteLength + mdat.byteLength); // bump the sequence number for next time 44258 44259 sequenceNumber++; 44260 boxes.set(moof); 44261 boxes.set(mdat, moof.byteLength); 44262 trackDecodeInfo.clearDtsInfo(track); 44263 frameDuration = Math.ceil(ONE_SECOND_IN_TS$3 * 1024 / track.samplerate); // TODO this check was added to maintain backwards compatibility (particularly with 44264 // tests) on adding the timingInfo event. However, it seems unlikely that there's a 44265 // valid use-case where an init segment/data should be triggered without associated 44266 // frames. Leaving for now, but should be looked into. 44267 44268 if (frames.length) { 44269 this.trigger('timingInfo', { 44270 start: frames[0].dts, 44271 end: frames[0].dts + frames.length * frameDuration 44272 }); 44273 } 44274 44275 this.trigger('data', { 44276 track: track, 44277 boxes: boxes 44278 }); 44279 this.trigger('done', 'AudioSegmentStream'); 44280 }; 44281 44282 this.reset = function () { 44283 trackDecodeInfo.clearDtsInfo(track); 44284 adtsFrames = []; 44285 this.trigger('reset'); 44286 }; 44287 }; 44288 44289 _AudioSegmentStream.prototype = new stream(); 44290 /** 44291 * Constructs a single-track, ISO BMFF media segment from H264 data 44292 * events. The output of this stream can be fed to a SourceBuffer 44293 * configured with a suitable initialization segment. 44294 * @param track {object} track metadata configuration 44295 * @param options {object} transmuxer options object 44296 * @param options.alignGopsAtEnd {boolean} If true, start from the end of the 44297 * gopsToAlignWith list when attempting to align gop pts 44298 * @param options.keepOriginalTimestamps {boolean} If true, keep the timestamps 44299 * in the source; false to adjust the first segment to start at 0. 44300 */ 44301 44302 _VideoSegmentStream = function VideoSegmentStream(track, options) { 44303 var sequenceNumber = 0, 44304 nalUnits = [], 44305 gopsToAlignWith = [], 44306 config, 44307 pps; 44308 options = options || {}; 44309 44310 _VideoSegmentStream.prototype.init.call(this); 44311 44312 delete track.minPTS; 44313 this.gopCache_ = []; 44314 /** 44315 * Constructs a ISO BMFF segment given H264 nalUnits 44316 * @param {Object} nalUnit A data event representing a nalUnit 44317 * @param {String} nalUnit.nalUnitType 44318 * @param {Object} nalUnit.config Properties for a mp4 track 44319 * @param {Uint8Array}
vendor: 12,289 bytes, lines 44319-44625
44319 nalUnit.data The nalUnit bytes 44320 * @see lib/codecs/h264.js 44321 **/ 44322 44323 this.push = function (nalUnit) { 44324 trackDecodeInfo.collectDtsInfo(track, nalUnit); // record the track config 44325 44326 if (nalUnit.nalUnitType === 'seq_parameter_set_rbsp' && !config) { 44327 config = nalUnit.config; 44328 track.sps = [nalUnit.data]; 44329 VIDEO_PROPERTIES.forEach(function (prop) { 44330 track[prop] = config[prop]; 44331 }, this); 44332 } 44333 44334 if (nalUnit.nalUnitType === 'pic_parameter_set_rbsp' && !pps) { 44335 pps = nalUnit.data; 44336 track.pps = [nalUnit.data]; 44337 } // buffer video until flush() is called 44338 44339 44340 nalUnits.push(nalUnit); 44341 }; 44342 /** 44343 * Pass constructed ISO BMFF track and boxes on to the 44344 * next stream in the pipeline 44345 **/ 44346 44347 44348 this.flush = function () { 44349 var frames, 44350 gopForFusion, 44351 gops, 44352 moof, 44353 mdat, 44354 boxes, 44355 prependedContentDuration = 0, 44356 firstGop, 44357 lastGop; // Throw away nalUnits at the start of the byte stream until 44358 // we find the first AUD 44359 44360 while (nalUnits.length) { 44361 if (nalUnits[0].nalUnitType === 'access_unit_delimiter_rbsp') { 44362 break; 44363 } 44364 44365 nalUnits.shift(); 44366 } // Return early if no video data has been observed 44367 44368 44369 if (nalUnits.length === 0) { 44370 this.resetStream_(); 44371 this.trigger('done', 'VideoSegmentStream'); 44372 return; 44373 } // Organize the raw nal-units into arrays that represent 44374 // higher-level constructs such as frames and gops 44375 // (group-of-pictures) 44376 44377 44378 frames = frameUtils.groupNalsIntoFrames(nalUnits); 44379 gops = frameUtils.groupFramesIntoGops(frames); // If the first frame of this fragment is not a keyframe we have 44380 // a problem since MSE (on Chrome) requires a leading keyframe. 44381 // 44382 // We have two approaches to repairing this situation: 44383 // 1) GOP-FUSION: 44384 // This is where we keep track of the GOPS (group-of-pictures) 44385 // from previous fragments and attempt to find one that we can 44386 // prepend to the current fragment in order to create a valid 44387 // fragment. 44388 // 2) KEYFRAME-PULLING: 44389 // Here we search for the first keyframe in the fragment and 44390 // throw away all the frames between the start of the fragment 44391 // and that keyframe. We then extend the duration and pull the 44392 // PTS of the keyframe forward so that it covers the time range 44393 // of the frames that were disposed of. 44394 // 44395 // #1 is far prefereable over #2 which can cause "stuttering" but 44396 // requires more things to be just right. 44397 44398 if (!gops[0][0].keyFrame) { 44399 // Search for a gop for fusion from our gopCache 44400 gopForFusion = this.getGopForFusion_(nalUnits[0], track); 44401 44402 if (gopForFusion) { 44403 // in order to provide more accurate timing information about the segment, save 44404 // the number of seconds prepended to the original segment due to GOP fusion 44405 prependedContentDuration = gopForFusion.duration; 44406 gops.unshift(gopForFusion); // Adjust Gops' metadata to account for the inclusion of the 44407 // new gop at the beginning 44408 44409 gops.byteLength += gopForFusion.byteLength; 44410 gops.nalCount += gopForFusion.nalCount; 44411 gops.pts = gopForFusion.pts; 44412 gops.dts = gopForFusion.dts; 44413 gops.duration += gopForFusion.duration; 44414 } else { 44415 // If we didn't find a candidate gop fall back to keyframe-pulling 44416 gops = frameUtils.extendFirstKeyFrame(gops); 44417 } 44418 } // Trim gops to align with gopsToAlignWith 44419 44420 44421 if (gopsToAlignWith.length) { 44422 var alignedGops; 44423 44424 if (options.alignGopsAtEnd) { 44425 alignedGops = this.alignGopsAtEnd_(gops); 44426 } else { 44427 alignedGops = this.alignGopsAtStart_(gops); 44428 } 44429 44430 if (!alignedGops) { 44431 // save all the nals in the last GOP into the gop cache 44432 this.gopCache_.unshift({ 44433 gop: gops.pop(), 44434 pps: track.pps, 44435 sps: track.sps 44436 }); // Keep a maximum of 6 GOPs in the cache 44437 44438 this.gopCache_.length = Math.min(6, this.gopCache_.length); // Clear nalUnits 44439 44440 nalUnits = []; // return early no gops can be aligned with desired gopsToAlignWith 44441 44442 this.resetStream_(); 44443 this.trigger('done', 'VideoSegmentStream'); 44444 return; 44445 } // Some gops were trimmed. clear dts info so minSegmentDts and pts are correct 44446 // when recalculated before sending off to CoalesceStream 44447 44448 44449 trackDecodeInfo.clearDtsInfo(track); 44450 gops = alignedGops; 44451 } 44452 44453 trackDecodeInfo.collectDtsInfo(track, gops); // First, we have to build the index from byte locations to 44454 // samples (that is, frames) in the video data 44455 44456 track.samples = frameUtils.generateSampleTable(gops); // Concatenate the video data and construct the mdat 44457 44458 mdat = mp4Generator.mdat(frameUtils.concatenateNalData(gops)); 44459 track.baseMediaDecodeTime = trackDecodeInfo.calculateTrackBaseMediaDecodeTime(track, options.keepOriginalTimestamps); 44460 this.trigger('processedGopsInfo', gops.map(function (gop) { 44461 return { 44462 pts: gop.pts, 44463 dts: gop.dts, 44464 byteLength: gop.byteLength 44465 }; 44466 })); 44467 firstGop = gops[0]; 44468 lastGop = gops[gops.length - 1]; 44469 this.trigger('segmentTimingInfo', generateVideoSegmentTimingInfo(track.baseMediaDecodeTime, firstGop.dts, firstGop.pts, lastGop.dts + lastGop.duration, lastGop.pts + lastGop.duration, prependedContentDuration)); 44470 this.trigger('timingInfo', { 44471 start: gops[0].dts, 44472 end: gops[gops.length - 1].dts + gops[gops.length - 1].duration 44473 }); // save all the nals in the last GOP into the gop cache 44474 44475 this.gopCache_.unshift({ 44476 gop: gops.pop(), 44477 pps: track.pps, 44478 sps: track.sps 44479 }); // Keep a maximum of 6 GOPs in the cache 44480 44481 this.gopCache_.length = Math.min(6, this.gopCache_.length); // Clear nalUnits 44482 44483 nalUnits = []; 44484 this.trigger('baseMediaDecodeTime', track.baseMediaDecodeTime); 44485 this.trigger('timelineStartInfo', track.timelineStartInfo); 44486 moof = mp4Generator.moof(sequenceNumber, [track]); // it would be great to allocate this array up front instead of 44487 // throwing away hundreds of media segment fragments 44488 44489 boxes = new Uint8Array(moof.byteLength + mdat.byteLength); // Bump the sequence number for next time 44490 44491 sequenceNumber++; 44492 boxes.set(moof); 44493 boxes.set(mdat, moof.byteLength); 44494 this.trigger('data', { 44495 track: track, 44496 boxes: boxes 44497 }); 44498 this.resetStream_(); // Continue with the flush process now 44499 44500 this.trigger('done', 'VideoSegmentStream'); 44501 }; 44502 44503 this.reset = function () { 44504 this.resetStream_(); 44505 nalUnits = []; 44506 this.gopCache_.length = 0; 44507 gopsToAlignWith.length = 0; 44508 this.trigger('reset'); 44509 }; 44510 44511 this.resetStream_ = function () { 44512 trackDecodeInfo.clearDtsInfo(track); // reset config and pps because they may differ across segments 44513 // for instance, when we are rendition switching 44514 44515 config = undefined; 44516 pps = undefined; 44517 }; // Search for a candidate Gop for gop-fusion from the gop cache and 44518 // return it or return null if no good candidate was found 44519 44520 44521 this.getGopForFusion_ = function (nalUnit) { 44522 var halfSecond = 45000, 44523 // Half-a-second in a 90khz clock 44524 allowableOverlap = 10000, 44525 // About 3 frames @ 30fps 44526 nearestDistance = Infinity, 44527 dtsDistance, 44528 nearestGopObj, 44529 currentGop, 44530 currentGopObj, 44531 i; // Search for the GOP nearest to the beginning of this nal unit 44532 44533 for (i = 0; i < this.gopCache_.length; i++) { 44534 currentGopObj = this.gopCache_[i]; 44535 currentGop = currentGopObj.gop; // Reject Gops with different SPS or PPS 44536 44537 if (!(track.pps && arrayEquals(track.pps[0], currentGopObj.pps[0])) || !(track.sps && arrayEquals(track.sps[0], currentGopObj.sps[0]))) { 44538 continue; 44539 } // Reject Gops that would require a negative baseMediaDecodeTime 44540 44541 44542 if (currentGop.dts < track.timelineStartInfo.dts) { 44543 continue; 44544 } // The distance between the end of the gop and the start of the nalUnit 44545 44546 44547 dtsDistance = nalUnit.dts - currentGop.dts - currentGop.duration; // Only consider GOPS that start before the nal unit and end within 44548 // a half-second of the nal unit 44549 44550 if (dtsDistance >= -allowableOverlap && dtsDistance <= halfSecond) { 44551 // Always use the closest GOP we found if there is more than 44552 // one candidate 44553 if (!nearestGopObj || nearestDistance > dtsDistance) { 44554 nearestGopObj = currentGopObj; 44555 nearestDistance = dtsDistance; 44556 } 44557 } 44558 } 44559 44560 if (nearestGopObj) { 44561 return nearestGopObj.gop; 44562 } 44563 44564 return null; 44565 }; // trim gop list to the first gop found that has a matching pts with a gop in the list 44566 // of gopsToAlignWith starting from the START of the list 44567 44568 44569 this.alignGopsAtStart_ = function (gops) { 44570 var alignIndex, gopIndex, align, gop, byteLength, nalCount, duration, alignedGops; 44571 byteLength = gops.byteLength; 44572 nalCount = gops.nalCount; 44573 duration = gops.duration; 44574 alignIndex = gopIndex = 0; 44575 44576 while (alignIndex < gopsToAlignWith.length && gopIndex < gops.length) { 44577 align = gopsToAlignWith[alignIndex]; 44578 gop = gops[gopIndex]; 44579 44580 if (align.pts === gop.pts) { 44581 break; 44582 } 44583 44584 if (gop.pts > align.pts) { 44585 // this current gop starts after the current gop we want to align on, so increment 44586 // align index 44587 alignIndex++; 44588 continue; 44589 } // current gop starts before the current gop we want to align on. so increment gop 44590 // index 44591 44592 44593 gopIndex++; 44594 byteLength -= gop.byteLength; 44595 nalCount -= gop.nalCount; 44596 duration -= gop.duration; 44597 } 44598 44599 if (gopIndex === 0) { 44600 // no gops to trim 44601 return gops; 44602 } 44603 44604 if (gopIndex === gops.length) { 44605 // all gops trimmed, skip appending all gops 44606 return null; 44607 } 44608 44609 alignedGops = gops.slice(gopIndex); 44610 alignedGops.byteLength = byteLength; 44611 alignedGops.duration = duration; 44612 alignedGops.nalCount = nalCount; 44613 alignedGops.pts = alignedGops[0].pts; 44614 alignedGops.dts = alignedGops[0].dts; 44615 return alignedGops; 44616 }; // trim gop list to the first gop found that has a matching pts with a gop in the list 44617 // of gopsToAlignWith starting from the END of the list 44618 44619 44620 this.alignGopsAtEnd_ = function (gops) { 44621 var alignIndex, gopIndex, align, gop, alignEndIndex, matchFound; 44622 alignIndex = gopsToAlignWith.length - 1; 44623 gopIndex = gops.length - 1; 44624 alignEndIndex = null; 44625 matchFound = false;
vendor: 4,995 bytes, lines 44626-44772
44626 44627 while (alignIndex >= 0 && gopIndex >= 0) { 44628 align = gopsToAlignWith[alignIndex]; 44629 gop = gops[gopIndex]; 44630 44631 if (align.pts === gop.pts) { 44632 matchFound = true; 44633 break; 44634 } 44635 44636 if (align.pts > gop.pts) { 44637 alignIndex--; 44638 continue; 44639 } 44640 44641 if (alignIndex === gopsToAlignWith.length - 1) { 44642 // gop.pts is greater than the last alignment candidate. If no match is found 44643 // by the end of this loop, we still want to append gops that come after this 44644 // point 44645 alignEndIndex = gopIndex; 44646 } 44647 44648 gopIndex--; 44649 } 44650 44651 if (!matchFound && alignEndIndex === null) { 44652 return null; 44653 } 44654 44655 var trimIndex; 44656 44657 if (matchFound) { 44658 trimIndex = gopIndex; 44659 } else { 44660 trimIndex = alignEndIndex; 44661 } 44662 44663 if (trimIndex === 0) { 44664 return gops; 44665 } 44666 44667 var alignedGops = gops.slice(trimIndex); 44668 var metadata = alignedGops.reduce(function (total, gop) { 44669 total.byteLength += gop.byteLength; 44670 total.duration += gop.duration; 44671 total.nalCount += gop.nalCount; 44672 return total; 44673 }, { 44674 byteLength: 0, 44675 duration: 0, 44676 nalCount: 0 44677 }); 44678 alignedGops.byteLength = metadata.byteLength; 44679 alignedGops.duration = metadata.duration; 44680 alignedGops.nalCount = metadata.nalCount; 44681 alignedGops.pts = alignedGops[0].pts; 44682 alignedGops.dts = alignedGops[0].dts; 44683 return alignedGops; 44684 }; 44685 44686 this.alignGopsWith = function (newGopsToAlignWith) { 44687 gopsToAlignWith = newGopsToAlignWith; 44688 }; 44689 }; 44690 44691 _VideoSegmentStream.prototype = new stream(); 44692 /** 44693 * A Stream that can combine multiple streams (ie. audio & video) 44694 * into a single output segment for MSE. Also supports audio-only 44695 * and video-only streams. 44696 * @param options {object} transmuxer options object 44697 * @param options.keepOriginalTimestamps {boolean} If true, keep the timestamps 44698 * in the source; false to adjust the first segment to start at media timeline start. 44699 */ 44700 44701 _CoalesceStream = function CoalesceStream(options, metadataStream) { 44702 // Number of Tracks per output segment 44703 // If greater than 1, we combine multiple 44704 // tracks into a single segment 44705 this.numberOfTracks = 0; 44706 this.metadataStream = metadataStream; 44707 options = options || {}; 44708 44709 if (typeof options.remux !== 'undefined') { 44710 this.remuxTracks = !!options.remux; 44711 } else { 44712 this.remuxTracks = true; 44713 } 44714 44715 if (typeof options.keepOriginalTimestamps === 'boolean') { 44716 this.keepOriginalTimestamps = options.keepOriginalTimestamps; 44717 } else { 44718 this.keepOriginalTimestamps = false; 44719 } 44720 44721 this.pendingTracks = []; 44722 this.videoTrack = null; 44723 this.pendingBoxes = []; 44724 this.pendingCaptions = []; 44725 this.pendingMetadata = []; 44726 this.pendingBytes = 0; 44727 this.emittedTracks = 0; 44728 44729 _CoalesceStream.prototype.init.call(this); // Take output from multiple 44730 44731 44732 this.push = function (output) { 44733 // buffer incoming captions until the associated video segment 44734 // finishes 44735 if (output.text) { 44736 return this.pendingCaptions.push(output); 44737 } // buffer incoming id3 tags until the final flush 44738 44739 44740 if (output.frames) { 44741 return this.pendingMetadata.push(output); 44742 } // Add this track to the list of pending tracks and store 44743 // important information required for the construction of 44744 // the final segment 44745 44746 44747 this.pendingTracks.push(output.track); 44748 this.pendingBytes += output.boxes.byteLength; // TODO: is there an issue for this against chrome? 44749 // We unshift audio and push video because 44750 // as of Chrome 75 when switching from 44751 // one init segment to another if the video 44752 // mdat does not appear after the audio mdat 44753 // only audio will play for the duration of our transmux. 44754 44755 if (output.track.type === 'video') { 44756 this.videoTrack = output.track; 44757 this.pendingBoxes.push(output.boxes); 44758 } 44759 44760 if (output.track.type === 'audio') { 44761 this.audioTrack = output.track; 44762 this.pendingBoxes.unshift(output.boxes); 44763 } 44764 }; 44765 }; 44766 44767 _CoalesceStream.prototype = new stream(); 44768 44769 _CoalesceStream.prototype.flush = function (flushSource) { 44770 var offset = 0, 44771 event = { 44772 captions: [],
vendor: 4,830 bytes, lines 44773-44878
44773 captionStreams: {}, 44774 metadata: [], 44775 info: {} 44776 }, 44777 caption, 44778 id3, 44779 initSegment, 44780 timelineStartPts = 0, 44781 i; 44782 44783 if (this.pendingTracks.length < this.numberOfTracks) { 44784 if (flushSource !== 'VideoSegmentStream' && flushSource !== 'AudioSegmentStream') { 44785 // Return because we haven't received a flush from a data-generating 44786 // portion of the segment (meaning that we have only recieved meta-data 44787 // or captions.) 44788 return; 44789 } else if (this.remuxTracks) { 44790 // Return until we have enough tracks from the pipeline to remux (if we 44791 // are remuxing audio and video into a single MP4) 44792 return; 44793 } else if (this.pendingTracks.length === 0) { 44794 // In the case where we receive a flush without any data having been 44795 // received we consider it an emitted track for the purposes of coalescing 44796 // `done` events. 44797 // We do this for the case where there is an audio and video track in the 44798 // segment but no audio data. (seen in several playlists with alternate 44799 // audio tracks and no audio present in the main TS segments.) 44800 this.emittedTracks++; 44801 44802 if (this.emittedTracks >= this.numberOfTracks) { 44803 this.trigger('done'); 44804 this.emittedTracks = 0; 44805 } 44806 44807 return; 44808 } 44809 } 44810 44811 if (this.videoTrack) { 44812 timelineStartPts = this.videoTrack.timelineStartInfo.pts; 44813 VIDEO_PROPERTIES.forEach(function (prop) { 44814 event.info[prop] = this.videoTrack[prop]; 44815 }, this); 44816 } else if (this.audioTrack) { 44817 timelineStartPts = this.audioTrack.timelineStartInfo.pts; 44818 AUDIO_PROPERTIES.forEach(function (prop) { 44819 event.info[prop] = this.audioTrack[prop]; 44820 }, this); 44821 } 44822 44823 if (this.videoTrack || this.audioTrack) { 44824 if (this.pendingTracks.length === 1) { 44825 event.type = this.pendingTracks[0].type; 44826 } else { 44827 event.type = 'combined'; 44828 } 44829 44830 this.emittedTracks += this.pendingTracks.length; 44831 initSegment = mp4Generator.initSegment(this.pendingTracks); // Create a new typed array to hold the init segment 44832 44833 event.initSegment = new Uint8Array(initSegment.byteLength); // Create an init segment containing a moov 44834 // and track definitions 44835 44836 event.initSegment.set(initSegment); // Create a new typed array to hold the moof+mdats 44837 44838 event.data = new Uint8Array(this.pendingBytes); // Append each moof+mdat (one per track) together 44839 44840 for (i = 0; i < this.pendingBoxes.length; i++) { 44841 event.data.set(this.pendingBoxes[i], offset); 44842 offset += this.pendingBoxes[i].byteLength; 44843 } // Translate caption PTS times into second offsets to match the 44844 // video timeline for the segment, and add track info 44845 44846 44847 for (i = 0; i < this.pendingCaptions.length; i++) { 44848 caption = this.pendingCaptions[i]; 44849 caption.startTime = clock.metadataTsToSeconds(caption.startPts, timelineStartPts, this.keepOriginalTimestamps); 44850 caption.endTime = clock.metadataTsToSeconds(caption.endPts, timelineStartPts, this.keepOriginalTimestamps); 44851 event.captionStreams[caption.stream] = true; 44852 event.captions.push(caption); 44853 } // Translate ID3 frame PTS times into second offsets to match the 44854 // video timeline for the segment 44855 44856 44857 for (i = 0; i < this.pendingMetadata.length; i++) { 44858 id3 = this.pendingMetadata[i]; 44859 id3.cueTime = clock.metadataTsToSeconds(id3.pts, timelineStartPts, this.keepOriginalTimestamps); 44860 event.metadata.push(id3); 44861 } // We add this to every single emitted segment even though we only need 44862 // it for the first 44863 44864 44865 event.metadata.dispatchType = this.metadataStream.dispatchType; // Reset stream state 44866 44867 this.pendingTracks.length = 0; 44868 this.videoTrack = null; 44869 this.pendingBoxes.length = 0; 44870 this.pendingCaptions.length = 0; 44871 this.pendingBytes = 0; 44872 this.pendingMetadata.length = 0; // Emit the built segment 44873 // We include captions and ID3 tags for backwards compatibility, 44874 // ideally we should send only video and audio in the data event 44875 44876 this.trigger('data', event); // Emit each caption to the outside world 44877 // Ideally, this would happen immediately on parsing captions, 44878 // but we need to ensure that video data is sent back first
vendor: 12,292 bytes, lines 44879-45142
44879 // so that caption timing can be adjusted to match video timing 44880 44881 for (i = 0; i < event.captions.length; i++) { 44882 caption = event.captions[i]; 44883 this.trigger('caption', caption); 44884 } // Emit each id3 tag to the outside world 44885 // Ideally, this would happen immediately on parsing the tag, 44886 // but we need to ensure that video data is sent back first 44887 // so that ID3 frame timing can be adjusted to match video timing 44888 44889 44890 for (i = 0; i < event.metadata.length; i++) { 44891 id3 = event.metadata[i]; 44892 this.trigger('id3Frame', id3); 44893 } 44894 } // Only emit `done` if all tracks have been flushed and emitted 44895 44896 44897 if (this.emittedTracks >= this.numberOfTracks) { 44898 this.trigger('done'); 44899 this.emittedTracks = 0; 44900 } 44901 }; 44902 44903 _CoalesceStream.prototype.setRemux = function (val) { 44904 this.remuxTracks = val; 44905 }; 44906 /** 44907 * A Stream that expects MP2T binary data as input and produces 44908 * corresponding media segments, suitable for use with Media Source 44909 * Extension (MSE) implementations that support the ISO BMFF byte 44910 * stream format, like Chrome. 44911 */ 44912 44913 44914 _Transmuxer = function Transmuxer(options) { 44915 var self = this, 44916 hasFlushed = true, 44917 videoTrack, 44918 audioTrack; 44919 44920 _Transmuxer.prototype.init.call(this); 44921 44922 options = options || {}; 44923 this.baseMediaDecodeTime = options.baseMediaDecodeTime || 0; 44924 this.transmuxPipeline_ = {}; 44925 44926 this.setupAacPipeline = function () { 44927 var pipeline = {}; 44928 this.transmuxPipeline_ = pipeline; 44929 pipeline.type = 'aac'; 44930 pipeline.metadataStream = new m2ts_1.MetadataStream(); // set up the parsing pipeline 44931 44932 pipeline.aacStream = new aac(); 44933 pipeline.audioTimestampRolloverStream = new m2ts_1.TimestampRolloverStream('audio'); 44934 pipeline.timedMetadataTimestampRolloverStream = new m2ts_1.TimestampRolloverStream('timed-metadata'); 44935 pipeline.adtsStream = new adts(); 44936 pipeline.coalesceStream = new _CoalesceStream(options, pipeline.metadataStream); 44937 pipeline.headOfPipeline = pipeline.aacStream; 44938 pipeline.aacStream.pipe(pipeline.audioTimestampRolloverStream).pipe(pipeline.adtsStream); 44939 pipeline.aacStream.pipe(pipeline.timedMetadataTimestampRolloverStream).pipe(pipeline.metadataStream).pipe(pipeline.coalesceStream); 44940 pipeline.metadataStream.on('timestamp', function (frame) { 44941 pipeline.aacStream.setTimestamp(frame.timeStamp); 44942 }); 44943 pipeline.aacStream.on('data', function (data) { 44944 if (data.type === 'timed-metadata' && !pipeline.audioSegmentStream) { 44945 audioTrack = audioTrack || { 44946 timelineStartInfo: { 44947 baseMediaDecodeTime: self.baseMediaDecodeTime 44948 }, 44949 codec: 'adts', 44950 type: 'audio' 44951 }; // hook up the audio segment stream to the first track with aac data 44952 44953 pipeline.coalesceStream.numberOfTracks++; 44954 pipeline.audioSegmentStream = new _AudioSegmentStream(audioTrack, options); 44955 pipeline.audioSegmentStream.on('timingInfo', self.trigger.bind(self, 'audioTimingInfo')); // Set up the final part of the audio pipeline 44956 44957 pipeline.adtsStream.pipe(pipeline.audioSegmentStream).pipe(pipeline.coalesceStream); 44958 } // emit pmt info 44959 44960 44961 self.trigger('trackinfo', { 44962 hasAudio: !!audioTrack, 44963 hasVideo: !!videoTrack 44964 }); 44965 }); // Re-emit any data coming from the coalesce stream to the outside world 44966 44967 pipeline.coalesceStream.on('data', this.trigger.bind(this, 'data')); // Let the consumer know we have finished flushing the entire pipeline 44968 44969 pipeline.coalesceStream.on('done', this.trigger.bind(this, 'done')); 44970 }; 44971 44972 this.setupTsPipeline = function () { 44973 var pipeline = {}; 44974 this.transmuxPipeline_ = pipeline; 44975 pipeline.type = 'ts'; 44976 pipeline.metadataStream = new m2ts_1.MetadataStream(); // set up the parsing pipeline 44977 44978 pipeline.packetStream = new m2ts_1.TransportPacketStream(); 44979 pipeline.parseStream = new m2ts_1.TransportParseStream(); 44980 pipeline.elementaryStream = new m2ts_1.ElementaryStream(); 44981 pipeline.timestampRolloverStream = new m2ts_1.TimestampRolloverStream(); 44982 pipeline.adtsStream = new adts(); 44983 pipeline.h264Stream = new H264Stream(); 44984 pipeline.captionStream = new m2ts_1.CaptionStream(); 44985 pipeline.coalesceStream = new _CoalesceStream(options, pipeline.metadataStream); 44986 pipeline.headOfPipeline = pipeline.packetStream; // disassemble MPEG2-TS packets into elementary streams 44987 44988 pipeline.packetStream.pipe(pipeline.parseStream).pipe(pipeline.elementaryStream).pipe(pipeline.timestampRolloverStream); // !!THIS ORDER IS IMPORTANT!! 44989 // demux the streams 44990 44991 pipeline.timestampRolloverStream.pipe(pipeline.h264Stream); 44992 pipeline.timestampRolloverStream.pipe(pipeline.adtsStream); 44993 pipeline.timestampRolloverStream.pipe(pipeline.metadataStream).pipe(pipeline.coalesceStream); // Hook up CEA-608/708 caption stream 44994 44995 pipeline.h264Stream.pipe(pipeline.captionStream).pipe(pipeline.coalesceStream); 44996 pipeline.elementaryStream.on('data', function (data) { 44997 var i; 44998 44999 if (data.type === 'metadata') { 45000 i = data.tracks.length; // scan the tracks listed in the metadata 45001 45002 while (i--) { 45003 if (!videoTrack && data.tracks[i].type === 'video') { 45004 videoTrack = data.tracks[i]; 45005 videoTrack.timelineStartInfo.baseMediaDecodeTime = self.baseMediaDecodeTime; 45006 } else if (!audioTrack && data.tracks[i].type === 'audio') { 45007 audioTrack = data.tracks[i]; 45008 audioTrack.timelineStartInfo.baseMediaDecodeTime = self.baseMediaDecodeTime; 45009 } 45010 } // hook up the video segment stream to the first track with h264 data 45011 45012 45013 if (videoTrack && !pipeline.videoSegmentStream) { 45014 pipeline.coalesceStream.numberOfTracks++; 45015 pipeline.videoSegmentStream = new _VideoSegmentStream(videoTrack, options); 45016 pipeline.videoSegmentStream.on('timelineStartInfo', function (timelineStartInfo) { 45017 // When video emits timelineStartInfo data after a flush, we forward that 45018 // info to the AudioSegmentStream, if it exists, because video timeline 45019 // data takes precedence. 45020 if (audioTrack) { 45021 audioTrack.timelineStartInfo = timelineStartInfo; // On the first segment we trim AAC frames that exist before the 45022 // very earliest DTS we have seen in video because Chrome will 45023 // interpret any video track with a baseMediaDecodeTime that is 45024 // non-zero as a gap. 45025 45026 pipeline.audioSegmentStream.setEarliestDts(timelineStartInfo.dts); 45027 } 45028 }); 45029 pipeline.videoSegmentStream.on('processedGopsInfo', self.trigger.bind(self, 'gopInfo')); 45030 pipeline.videoSegmentStream.on('segmentTimingInfo', self.trigger.bind(self, 'videoSegmentTimingInfo')); 45031 pipeline.videoSegmentStream.on('baseMediaDecodeTime', function (baseMediaDecodeTime) { 45032 if (audioTrack) { 45033 pipeline.audioSegmentStream.setVideoBaseMediaDecodeTime(baseMediaDecodeTime); 45034 } 45035 }); 45036 pipeline.videoSegmentStream.on('timingInfo', self.trigger.bind(self, 'videoTimingInfo')); // Set up the final part of the video pipeline 45037 45038 pipeline.h264Stream.pipe(pipeline.videoSegmentStream).pipe(pipeline.coalesceStream); 45039 } 45040 45041 if (audioTrack && !pipeline.audioSegmentStream) { 45042 // hook up the audio segment stream to the first track with aac data 45043 pipeline.coalesceStream.numberOfTracks++; 45044 pipeline.audioSegmentStream = new _AudioSegmentStream(audioTrack, options); 45045 pipeline.audioSegmentStream.on('timingInfo', self.trigger.bind(self, 'audioTimingInfo')); // Set up the final part of the audio pipeline 45046 45047 pipeline.adtsStream.pipe(pipeline.audioSegmentStream).pipe(pipeline.coalesceStream); 45048 } // emit pmt info 45049 45050 45051 self.trigger('trackinfo', { 45052 hasAudio: !!audioTrack, 45053 hasVideo: !!videoTrack 45054 }); 45055 } 45056 }); // Re-emit any data coming from the coalesce stream to the outside world 45057 45058 pipeline.coalesceStream.on('data', this.trigger.bind(this, 'data')); 45059 pipeline.coalesceStream.on('id3Frame', function (id3Frame) { 45060 id3Frame.dispatchType = pipeline.metadataStream.dispatchType; 45061 self.trigger('id3Frame', id3Frame); 45062 }); 45063 pipeline.coalesceStream.on('caption', this.trigger.bind(this, 'caption')); // Let the consumer know we have finished flushing the entire pipeline 45064 45065 pipeline.coalesceStream.on('done', this.trigger.bind(this, 'done')); 45066 }; // hook up the segment streams once track metadata is delivered 45067 45068 45069 this.setBaseMediaDecodeTime = function (baseMediaDecodeTime) { 45070 var pipeline = this.transmuxPipeline_; 45071 45072 if (!options.keepOriginalTimestamps) { 45073 this.baseMediaDecodeTime = baseMediaDecodeTime; 45074 } 45075 45076 if (audioTrack) { 45077 audioTrack.timelineStartInfo.dts = undefined; 45078 audioTrack.timelineStartInfo.pts = undefined; 45079 trackDecodeInfo.clearDtsInfo(audioTrack); 45080 45081 if (!options.keepOriginalTimestamps) { 45082 audioTrack.timelineStartInfo.baseMediaDecodeTime = baseMediaDecodeTime; 45083 } 45084 45085 if (pipeline.audioTimestampRolloverStream) { 45086 pipeline.audioTimestampRolloverStream.discontinuity(); 45087 } 45088 } 45089 45090 if (videoTrack) { 45091 if (pipeline.videoSegmentStream) { 45092 pipeline.videoSegmentStream.gopCache_ = []; 45093 } 45094 45095 videoTrack.timelineStartInfo.dts = undefined; 45096 videoTrack.timelineStartInfo.pts = undefined; 45097 trackDecodeInfo.clearDtsInfo(videoTrack); 45098 pipeline.captionStream.reset(); 45099 45100 if (!options.keepOriginalTimestamps) { 45101 videoTrack.timelineStartInfo.baseMediaDecodeTime = baseMediaDecodeTime; 45102 } 45103 } 45104 45105 if (pipeline.timestampRolloverStream) { 45106 pipeline.timestampRolloverStream.discontinuity(); 45107 } 45108 }; 45109 45110 this.setAudioAppendStart = function (timestamp) { 45111 if (audioTrack) { 45112 this.transmuxPipeline_.audioSegmentStream.setAudioAppendStart(timestamp); 45113 } 45114 }; 45115 45116 this.setRemux = function (val) { 45117 var pipeline = this.transmuxPipeline_; 45118 options.remux = val; 45119 45120 if (pipeline && pipeline.coalesceStream) { 45121 pipeline.coalesceStream.setRemux(val); 45122 } 45123 }; 45124 45125 this.alignGopsWith = function (gopsToAlignWith) { 45126 if (videoTrack && this.transmuxPipeline_.videoSegmentStream) { 45127 this.transmuxPipeline_.videoSegmentStream.alignGopsWith(gopsToAlignWith); 45128 } 45129 }; // feed incoming data to the front of the parsing pipeline 45130 45131 45132 this.push = function (data) { 45133 if (hasFlushed) { 45134 var isAac = isLikelyAacData$1(data); 45135 45136 if (isAac && this.transmuxPipeline_.type !== 'aac') { 45137 this.setupAacPipeline(); 45138 } else if (!isAac && this.transmuxPipeline_.type !== 'ts') { 45139 this.setupTsPipeline(); 45140 } 45141 45142 hasFlushed = false;
vendor: 34,564 bytes, lines 45143-46172
45143 } 45144 45145 this.transmuxPipeline_.headOfPipeline.push(data); 45146 }; // flush any buffered data 45147 45148 45149 this.flush = function () { 45150 hasFlushed = true; // Start at the top of the pipeline and flush all pending work 45151 45152 this.transmuxPipeline_.headOfPipeline.flush(); 45153 }; 45154 45155 this.endTimeline = function () { 45156 this.transmuxPipeline_.headOfPipeline.endTimeline(); 45157 }; 45158 45159 this.reset = function () { 45160 if (this.transmuxPipeline_.headOfPipeline) { 45161 this.transmuxPipeline_.headOfPipeline.reset(); 45162 } 45163 }; // Caption data has to be reset when seeking outside buffered range 45164 45165 45166 this.resetCaptions = function () { 45167 if (this.transmuxPipeline_.captionStream) { 45168 this.transmuxPipeline_.captionStream.reset(); 45169 } 45170 }; 45171 }; 45172 45173 _Transmuxer.prototype = new stream(); 45174 var transmuxer = { 45175 Transmuxer: _Transmuxer, 45176 VideoSegmentStream: _VideoSegmentStream, 45177 AudioSegmentStream: _AudioSegmentStream, 45178 AUDIO_PROPERTIES: AUDIO_PROPERTIES, 45179 VIDEO_PROPERTIES: VIDEO_PROPERTIES, 45180 // exported for testing 45181 generateVideoSegmentTimingInfo: generateVideoSegmentTimingInfo 45182 }; 45183 45184 var classCallCheck = function classCallCheck(instance, Constructor) { 45185 if (!(instance instanceof Constructor)) { 45186 throw new TypeError("Cannot call a class as a function"); 45187 } 45188 }; 45189 45190 var createClass = function () { 45191 function defineProperties(target, props) { 45192 for (var i = 0; i < props.length; i++) { 45193 var descriptor = props[i]; 45194 descriptor.enumerable = descriptor.enumerable || false; 45195 descriptor.configurable = true; 45196 if ("value" in descriptor) descriptor.writable = true; 45197 Object.defineProperty(target, descriptor.key, descriptor); 45198 } 45199 } 45200 45201 return function (Constructor, protoProps, staticProps) { 45202 if (protoProps) defineProperties(Constructor.prototype, protoProps); 45203 if (staticProps) defineProperties(Constructor, staticProps); 45204 return Constructor; 45205 }; 45206 }(); 45207 /** 45208 * @file transmuxer-worker.js 45209 */ 45210 45211 /** 45212 * Re-emits transmuxer events by converting them into messages to the 45213 * world outside the worker. 45214 * 45215 * @param {Object} transmuxer the transmuxer to wire events on 45216 * @private 45217 */ 45218 45219 45220 var wireTransmuxerEvents = function wireTransmuxerEvents(self, transmuxer$$1) { 45221 transmuxer$$1.on('data', function (segment) { 45222 // transfer ownership of the underlying ArrayBuffer 45223 // instead of doing a copy to save memory 45224 // ArrayBuffers are transferable but generic TypedArrays are not 45225 // @link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers#Passing_data_by_transferring_ownership_(transferable_objects) 45226 var initArray = segment.initSegment; 45227 segment.initSegment = { 45228 data: initArray.buffer, 45229 byteOffset: initArray.byteOffset, 45230 byteLength: initArray.byteLength 45231 }; 45232 var typedArray = segment.data; 45233 segment.data = typedArray.buffer; 45234 self.postMessage({ 45235 action: 'data', 45236 segment: segment, 45237 byteOffset: typedArray.byteOffset, 45238 byteLength: typedArray.byteLength 45239 }, [segment.data]); 45240 }); 45241 45242 if (transmuxer$$1.captionStream) { 45243 transmuxer$$1.captionStream.on('data', function (caption) { 45244 self.postMessage({ 45245 action: 'caption', 45246 data: caption 45247 }); 45248 }); 45249 } 45250 45251 transmuxer$$1.on('done', function (data) { 45252 self.postMessage({ 45253 action: 'done' 45254 }); 45255 }); 45256 transmuxer$$1.on('gopInfo', function (gopInfo) { 45257 self.postMessage({ 45258 action: 'gopInfo', 45259 gopInfo: gopInfo 45260 }); 45261 }); 45262 transmuxer$$1.on('videoSegmentTimingInfo', function (videoSegmentTimingInfo) { 45263 self.postMessage({ 45264 action: 'videoSegmentTimingInfo', 45265 videoSegmentTimingInfo: videoSegmentTimingInfo 45266 }); 45267 }); 45268 }; 45269 /** 45270 * All incoming messages route through this hash. If no function exists 45271 * to handle an incoming message, then we ignore the message. 45272 * 45273 * @class MessageHandlers 45274 * @param {Object} options the options to initialize with 45275 */ 45276 45277 45278 var MessageHandlers = function () { 45279 function MessageHandlers(self, options) { 45280 classCallCheck(this, MessageHandlers); 45281 this.options = options || {}; 45282 this.self = self; 45283 this.init(); 45284 } 45285 /** 45286 * initialize our web worker and wire all the events. 45287 */ 45288 45289 45290 createClass(MessageHandlers, [{ 45291 key: 'init', 45292 value: function init() { 45293 if (this.transmuxer) { 45294 this.transmuxer.dispose(); 45295 } 45296 45297 this.transmuxer = new transmuxer.Transmuxer(this.options); 45298 wireTransmuxerEvents(this.self, this.transmuxer); 45299 } 45300 /** 45301 * Adds data (a ts segment) to the start of the transmuxer pipeline for 45302 * processing. 45303 * 45304 * @param {ArrayBuffer} data data to push into the muxer 45305 */ 45306 45307 }, { 45308 key: 'push', 45309 value: function push(data) { 45310 // Cast array buffer to correct type for transmuxer 45311 var segment = new Uint8Array(data.data, data.byteOffset, data.byteLength); 45312 this.transmuxer.push(segment); 45313 } 45314 /** 45315 * Recreate the transmuxer so that the next segment added via `push` 45316 * start with a fresh transmuxer. 45317 */ 45318 45319 }, { 45320 key: 'reset', 45321 value: function reset() { 45322 this.init(); 45323 } 45324 /** 45325 * Set the value that will be used as the `baseMediaDecodeTime` time for the 45326 * next segment pushed in. Subsequent segments will have their `baseMediaDecodeTime` 45327 * set relative to the first based on the PTS values. 45328 * 45329 * @param {Object} data used to set the timestamp offset in the muxer 45330 */ 45331 45332 }, { 45333 key: 'setTimestampOffset', 45334 value: function setTimestampOffset(data) { 45335 var timestampOffset = data.timestampOffset || 0; 45336 this.transmuxer.setBaseMediaDecodeTime(Math.round(timestampOffset * 90000)); 45337 } 45338 }, { 45339 key: 'setAudioAppendStart', 45340 value: function setAudioAppendStart(data) { 45341 this.transmuxer.setAudioAppendStart(Math.ceil(data.appendStart * 90000)); 45342 } 45343 /** 45344 * Forces the pipeline to finish processing the last segment and emit it's 45345 * results. 45346 * 45347 * @param {Object} data event data, not really used 45348 */ 45349 45350 }, { 45351 key: 'flush', 45352 value: function flush(data) { 45353 this.transmuxer.flush(); 45354 } 45355 }, { 45356 key: 'resetCaptions', 45357 value: function resetCaptions() { 45358 this.transmuxer.resetCaptions(); 45359 } 45360 }, { 45361 key: 'alignGopsWith', 45362 value: function alignGopsWith(data) { 45363 this.transmuxer.alignGopsWith(data.gopsToAlignWith.slice()); 45364 } 45365 }]); 45366 return MessageHandlers; 45367 }(); 45368 /** 45369 * Our web wroker interface so that things can talk to mux.js 45370 * that will be running in a web worker. the scope is passed to this by 45371 * webworkify. 45372 * 45373 * @param {Object} self the scope for the web worker 45374 */ 45375 45376 45377 var TransmuxerWorker = function TransmuxerWorker(self) { 45378 self.onmessage = function (event) { 45379 if (event.data.action === 'init' && event.data.options) { 45380 this.messageHandlers = new MessageHandlers(self, event.data.options); 45381 return; 45382 } 45383 45384 if (!this.messageHandlers) { 45385 this.messageHandlers = new MessageHandlers(self); 45386 } 45387 45388 if (event.data && event.data.action && event.data.action !== 'init') { 45389 if (this.messageHandlers[event.data.action]) { 45390 this.messageHandlers[event.data.action](event.data); 45391 } 45392 } 45393 }; 45394 }; 45395 45396 var transmuxerWorker = new TransmuxerWorker(self); 45397 return transmuxerWorker; 45398 }(); 45399 }); 45400 /** 45401 * @file - codecs.js - Handles tasks regarding codec strings such as translating them to 45402 * codec strings, or translating codec strings into objects that can be examined. 45403 */ 45404 // Default codec parameters if none were provided for video and/or audio 45405 45406 var defaultCodecs = { 45407 videoCodec: 'avc1', 45408 videoObjectTypeIndicator: '.4d400d', 45409 // AAC-LC 45410 audioProfile: '2' 45411 }; 45412 /** 45413 * Replace the old apple-style `avc1.<dd>.<dd>` codec string with the standard 45414 * `avc1.<hhhhhh>` 45415 * 45416 * @param {Array} codecs an array of codec strings to fix 45417 * @return {Array} the translated codec array 45418 * @private 45419 */ 45420 45421 var translateLegacyCodecs = function translateLegacyCodecs(codecs) { 45422 return codecs.map(function (codec) { 45423 return codec.replace(/avc1\.(\d+)\.(\d+)/i, function (orig, profile, avcLevel) { 45424 var profileHex = ('00' + Number(profile).toString(16)).slice(-2); 45425 var avcLevelHex = ('00' + Number(avcLevel).toString(16)).slice(-2); 45426 return 'avc1.' + profileHex + '00' + avcLevelHex; 45427 }); 45428 }); 45429 }; 45430 /** 45431 * Parses a codec string to retrieve the number of codecs specified, 45432 * the video codec and object type indicator, and the audio profile. 45433 */ 45434 45435 45436 var parseCodecs = function parseCodecs() { 45437 var codecs = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; 45438 var result = { 45439 codecCount: 0 45440 }; 45441 var parsed = void 0; 45442 result.codecCount = codecs.split(',').length; 45443 result.codecCount = result.codecCount || 2; // parse the video codec 45444 45445 parsed = /(^|\s|,)+(avc[13])([^ ,]*)/i.exec(codecs); 45446 45447 if (parsed) { 45448 result.videoCodec = parsed[2]; 45449 result.videoObjectTypeIndicator = parsed[3]; 45450 } // parse the last field of the audio codec 45451 45452 45453 result.audioProfile = /(^|\s|,)+mp4a.[0-9A-Fa-f]+\.([0-9A-Fa-f]+)/i.exec(codecs); 45454 result.audioProfile = result.audioProfile && result.audioProfile[2]; 45455 return result; 45456 }; 45457 /** 45458 * Replace codecs in the codec string with the old apple-style `avc1.<dd>.<dd>` to the 45459 * standard `avc1.<hhhhhh>`. 45460 * 45461 * @param codecString {String} the codec string 45462 * @return {String} the codec string with old apple-style codecs replaced 45463 * 45464 * @private 45465 */ 45466 45467 45468 var mapLegacyAvcCodecs = function mapLegacyAvcCodecs(codecString) { 45469 return codecString.replace(/avc1\.(\d+)\.(\d+)/i, function (match) { 45470 return translateLegacyCodecs([match])[0]; 45471 }); 45472 }; 45473 /** 45474 * Build a media mime-type string from a set of parameters 45475 * @param {String} type either 'audio' or 'video' 45476 * @param {String} container either 'mp2t' or 'mp4' 45477 * @param {Array} codecs an array of codec strings to add 45478 * @return {String} a valid media mime-type 45479 */ 45480 45481 45482 var makeMimeTypeString = function makeMimeTypeString(type, container, codecs) { 45483 // The codecs array is filtered so that falsey values are 45484 // dropped and don't cause Array#join to create spurious 45485 // commas 45486 return type + '/' + container + '; codecs="' + codecs.filter(function (c) { 45487 return !!c; 45488 }).join(', ') + '"'; 45489 }; 45490 /** 45491 * Returns the type container based on information in the playlist 45492 * @param {Playlist} media the current media playlist 45493 * @return {String} a valid media container type 45494 */ 45495 45496 45497 var getContainerType = function getContainerType(media) { 45498 // An initialization segment means the media playlist is an iframe 45499 // playlist or is using the mp4 container. We don't currently 45500 // support iframe playlists, so assume this is signalling mp4 45501 // fragments. 45502 if (media.segments && media.segments.length && media.segments[0].map) { 45503 return 'mp4'; 45504 } 45505 45506 return 'mp2t'; 45507 }; 45508 /** 45509 * Returns a set of codec strings parsed from the playlist or the default 45510 * codec strings if no codecs were specified in the playlist 45511 * @param {Playlist} media the current media playlist 45512 * @return {Object} an object with the video and audio codecs 45513 */ 45514 45515 45516 var getCodecs = function getCodecs(media) { 45517 // if the codecs were explicitly specified, use them instead of the 45518 // defaults 45519 var mediaAttributes = media.attributes || {}; 45520 45521 if (mediaAttributes.CODECS) { 45522 return parseCodecs(mediaAttributes.CODECS); 45523 } 45524 45525 return defaultCodecs; 45526 }; 45527 45528 var audioProfileFromDefault = function audioProfileFromDefault(master, audioGroupId) { 45529 if (!master.mediaGroups.AUDIO || !audioGroupId) { 45530 return null; 45531 } 45532 45533 var audioGroup = master.mediaGroups.AUDIO[audioGroupId]; 45534 45535 if (!audioGroup) { 45536 return null; 45537 } 45538 45539 for (var name in audioGroup) { 45540 var audioType = audioGroup[name]; 45541 45542 if (audioType["default"] && audioType.playlists) { 45543 // codec should be the same for all playlists within the audio type 45544 return parseCodecs(audioType.playlists[0].attributes.CODECS).audioProfile; 45545 } 45546 } 45547 45548 return null; 45549 }; 45550 /** 45551 * Calculates the MIME type strings for a working configuration of 45552 * SourceBuffers to play variant streams in a master playlist. If 45553 * there is no possible working configuration, an empty array will be 45554 * returned. 45555 * 45556 * @param master {Object} the m3u8 object for the master playlist 45557 * @param media {Object} the m3u8 object for the variant playlist 45558 * @return {Array} the MIME type strings. If the array has more than 45559 * one entry, the first element should be applied to the video 45560 * SourceBuffer and the second to the audio SourceBuffer. 45561 * 45562 * @private 45563 */ 45564 45565 45566 var mimeTypesForPlaylist = function mimeTypesForPlaylist(master, media) { 45567 var containerType = getContainerType(media); 45568 var codecInfo = getCodecs(media); 45569 var mediaAttributes = media.attributes || {}; // Default condition for a traditional HLS (no demuxed audio/video) 45570 45571 var isMuxed = true; 45572 var isMaat = false; 45573 45574 if (!media) { 45575 // Not enough information 45576 return []; 45577 } 45578 45579 if (master.mediaGroups.AUDIO && mediaAttributes.AUDIO) { 45580 var audioGroup = master.mediaGroups.AUDIO[mediaAttributes.AUDIO]; // Handle the case where we are in a multiple-audio track scenario 45581 45582 if (audioGroup) { 45583 isMaat = true; // Start with the everything demuxed then... 45584 45585 isMuxed = false; // ...check to see if any audio group tracks are muxed (ie. lacking a uri) 45586 45587 for (var groupId in audioGroup) { 45588 // either a uri is present (if the case of HLS and an external playlist), or 45589 // playlists is present (in the case of DASH where we don't have external audio 45590 // playlists) 45591 if (!audioGroup[groupId].uri && !audioGroup[groupId].playlists) { 45592 isMuxed = true; 45593 break; 45594 } 45595 } 45596 } 45597 } // HLS with multiple-audio tracks must always get an audio codec. 45598 // Put another way, there is no way to have a video-only multiple-audio HLS! 45599 45600 45601 if (isMaat && !codecInfo.audioProfile) { 45602 if (!isMuxed) { 45603 // It is possible for codecs to be specified on the audio media group playlist but 45604 // not on the rendition playlist. This is mostly the case for DASH, where audio and 45605 // video are always separate (and separately specified). 45606 codecInfo.audioProfile = audioProfileFromDefault(master, mediaAttributes.AUDIO); 45607 } 45608 45609 if (!codecInfo.audioProfile) { 45610 videojs$1.log.warn('Multiple audio tracks present but no audio codec string is specified. ' + 'Attempting to use the default audio codec (mp4a.40.2)'); 45611 codecInfo.audioProfile = defaultCodecs.audioProfile; 45612 } 45613 } // Generate the final codec strings from the codec object generated above 45614 45615 45616 var codecStrings = {}; 45617 45618 if (codecInfo.videoCodec) { 45619 codecStrings.video = '' + codecInfo.videoCodec + codecInfo.videoObjectTypeIndicator; 45620 } 45621 45622 if (codecInfo.audioProfile) { 45623 codecStrings.audio = 'mp4a.40.' + codecInfo.audioProfile; 45624 } // Finally, make and return an array with proper mime-types depending on 45625 // the configuration 45626 45627 45628 var justAudio = makeMimeTypeString('audio', containerType, [codecStrings.audio]); 45629 var justVideo = makeMimeTypeString('video', containerType, [codecStrings.video]); 45630 var bothVideoAudio = makeMimeTypeString('video', containerType, [codecStrings.video, codecStrings.audio]); 45631 45632 if (isMaat) { 45633 if (!isMuxed && codecStrings.video) { 45634 return [justVideo, justAudio]; 45635 } 45636 45637 if (!isMuxed && !codecStrings.video) { 45638 // There is no muxed content and no video codec string, so this is an audio only 45639 // stream with alternate audio. 45640 return [justAudio, justAudio]; 45641 } // There exists the possiblity that this will return a `video/container` 45642 // mime-type for the first entry in the array even when there is only audio. 45643 // This doesn't appear to be a problem and simplifies the code. 45644 45645 45646 return [bothVideoAudio, justAudio]; 45647 } // If there is no video codec at all, always just return a single 45648 // audio/<container> mime-type 45649 45650 45651 if (!codecStrings.video) { 45652 return [justAudio]; 45653 } // When not using separate audio media groups, audio and video is 45654 // *always* muxed 45655 45656 45657 return [bothVideoAudio]; 45658 }; 45659 /** 45660 * Parse a content type header into a type and parameters 45661 * object 45662 * 45663 * @param {String} type the content type header 45664 * @return {Object} the parsed content-type 45665 * @private 45666 */ 45667 45668 45669 var parseContentType = function parseContentType(type) { 45670 var object = { 45671 type: '', 45672 parameters: {} 45673 }; 45674 var parameters = type.trim().split(';'); // first parameter should always be content-type 45675 45676 object.type = parameters.shift().trim(); 45677 parameters.forEach(function (parameter) { 45678 var pair = parameter.trim().split('='); 45679 45680 if (pair.length > 1) { 45681 var name = pair[0].replace(/"/g, '').trim(); 45682 var value = pair[1].replace(/"/g, '').trim(); 45683 object.parameters[name] = value; 45684 } 45685 }); 45686 return object; 45687 }; 45688 /** 45689 * Check if a codec string refers to an audio codec. 45690 * 45691 * @param {String} codec codec string to check 45692 * @return {Boolean} if this is an audio codec 45693 * @private 45694 */ 45695 45696 45697 var isAudioCodec = function isAudioCodec(codec) { 45698 return /mp4a\.\d+.\d+/i.test(codec); 45699 }; 45700 /** 45701 * Check if a codec string refers to a video codec. 45702 * 45703 * @param {String} codec codec string to check 45704 * @return {Boolean} if this is a video codec 45705 * @private 45706 */ 45707 45708 45709 var isVideoCodec = function isVideoCodec(codec) { 45710 return /avc1\.[\da-f]+/i.test(codec); 45711 }; 45712 /** 45713 * Returns a list of gops in the buffer that have a pts value of 3 seconds or more in 45714 * front of current time. 45715 * 45716 * @param {Array} buffer 45717 * The current buffer of gop information 45718 * @param {Number} currentTime 45719 * The current time 45720 * @param {Double} mapping 45721 * Offset to map display time to stream presentation time 45722 * @return {Array} 45723 * List of gops considered safe to append over 45724 */ 45725 45726 45727 var gopsSafeToAlignWith = function gopsSafeToAlignWith(buffer, currentTime, mapping) { 45728 if (typeof currentTime === 'undefined' || currentTime === null || !buffer.length) { 45729 return []; 45730 } // pts value for current time + 3 seconds to give a bit more wiggle room 45731 45732 45733 var currentTimePts = Math.ceil((currentTime - mapping + 3) * 90000); 45734 var i = void 0; 45735 45736 for (i = 0; i < buffer.length; i++) { 45737 if (buffer[i].pts > currentTimePts) { 45738 break; 45739 } 45740 } 45741 45742 return buffer.slice(i); 45743 }; 45744 /** 45745 * Appends gop information (timing and byteLength) received by the transmuxer for the 45746 * gops appended in the last call to appendBuffer 45747 * 45748 * @param {Array} buffer 45749 * The current buffer of gop information 45750 * @param {Array} gops 45751 * List of new gop information 45752 * @param {boolean} replace 45753 * If true, replace the buffer with the new gop information. If false, append the 45754 * new gop information to the buffer in the right location of time. 45755 * @return {Array} 45756 * Updated list of gop information 45757 */ 45758 45759 45760 var updateGopBuffer = function updateGopBuffer(buffer, gops, replace) { 45761 if (!gops.length) { 45762 return buffer; 45763 } 45764 45765 if (replace) { 45766 // If we are in safe append mode, then completely overwrite the gop buffer 45767 // with the most recent appeneded data. This will make sure that when appending 45768 // future segments, we only try to align with gops that are both ahead of current 45769 // time and in the last segment appended. 45770 return gops.slice(); 45771 } 45772 45773 var start = gops[0].pts; 45774 var i = 0; 45775 45776 for (i; i < buffer.length; i++) { 45777 if (buffer[i].pts >= start) { 45778 break; 45779 } 45780 } 45781 45782 return buffer.slice(0, i).concat(gops); 45783 }; 45784 /** 45785 * Removes gop information in buffer that overlaps with provided start and end 45786 * 45787 * @param {Array} buffer 45788 * The current buffer of gop information 45789 * @param {Double} start 45790 * position to start the remove at 45791 * @param {Double} end 45792 * position to end the remove at 45793 * @param {Double} mapping 45794 * Offset to map display time to stream presentation time 45795 */ 45796 45797 45798 var removeGopBuffer = function removeGopBuffer(buffer, start, end, mapping) { 45799 var startPts = Math.ceil((start - mapping) * 90000); 45800 var endPts = Math.ceil((end - mapping) * 90000); 45801 var updatedBuffer = buffer.slice(); 45802 var i = buffer.length; 45803 45804 while (i--) { 45805 if (buffer[i].pts <= endPts) { 45806 break; 45807 } 45808 } 45809 45810 if (i === -1) { 45811 // no removal because end of remove range is before start of buffer 45812 return updatedBuffer; 45813 } 45814 45815 var j = i + 1; 45816 45817 while (j--) { 45818 if (buffer[j].pts <= startPts) { 45819 break; 45820 } 45821 } // clamp remove range start to 0 index 45822 45823 45824 j = Math.max(j, 0); 45825 updatedBuffer.splice(j, i - j + 1); 45826 return updatedBuffer; 45827 }; 45828 45829 var buffered = function buffered(videoBuffer, audioBuffer, audioDisabled) { 45830 var start = null; 45831 var end = null; 45832 var arity = 0; 45833 var extents = []; 45834 var ranges = []; // neither buffer has been created yet 45835 45836 if (!videoBuffer && !audioBuffer) { 45837 return videojs$1.createTimeRange(); 45838 } // only one buffer is configured 45839 45840 45841 if (!videoBuffer) { 45842 return audioBuffer.buffered; 45843 } 45844 45845 if (!audioBuffer) { 45846 return videoBuffer.buffered; 45847 } // both buffers are configured 45848 45849 45850 if (audioDisabled) { 45851 return videoBuffer.buffered; 45852 } // both buffers are empty 45853 45854 45855 if (videoBuffer.buffered.length === 0 && audioBuffer.buffered.length === 0) { 45856 return videojs$1.createTimeRange(); 45857 } // Handle the case where we have both buffers and create an 45858 // intersection of the two 45859 45860 45861 var videoBuffered = videoBuffer.buffered; 45862 var audioBuffered = audioBuffer.buffered; 45863 var count = videoBuffered.length; // A) Gather up all start and end times 45864 45865 while (count--) { 45866 extents.push({ 45867 time: videoBuffered.start(count), 45868 type: 'start' 45869 }); 45870 extents.push({ 45871 time: videoBuffered.end(count), 45872 type: 'end' 45873 }); 45874 } 45875 45876 count = audioBuffered.length; 45877 45878 while (count--) { 45879 extents.push({ 45880 time: audioBuffered.start(count), 45881 type: 'start' 45882 }); 45883 extents.push({ 45884 time: audioBuffered.end(count), 45885 type: 'end' 45886 }); 45887 } // B) Sort them by time 45888 45889 45890 extents.sort(function (a, b) { 45891 return a.time - b.time; 45892 }); // C) Go along one by one incrementing arity for start and decrementing 45893 // arity for ends 45894 45895 for (count = 0; count < extents.length; count++) { 45896 if (extents[count].type === 'start') { 45897 arity++; // D) If arity is ever incremented to 2 we are entering an 45898 // overlapping range 45899 45900 if (arity === 2) { 45901 start = extents[count].time; 45902 } 45903 } else if (extents[count].type === 'end') { 45904 arity--; // E) If arity is ever decremented to 1 we leaving an 45905 // overlapping range 45906 45907 if (arity === 1) { 45908 end = extents[count].time; 45909 } 45910 } // F) Record overlapping ranges 45911 45912 45913 if (start !== null && end !== null) { 45914 ranges.push([start, end]); 45915 start = null; 45916 end = null; 45917 } 45918 } 45919 45920 return videojs$1.createTimeRanges(ranges); 45921 }; 45922 /** 45923 * @file virtual-source-buffer.js 45924 */ 45925 45926 45927 var ONE_SECOND_IN_TS$2 = 90000; // We create a wrapper around the SourceBuffer so that we can manage the 45928 // state of the `updating` property manually. We have to do this because 45929 // Firefox changes `updating` to false long before triggering `updateend` 45930 // events and that was causing strange problems in videojs-contrib-hls 45931 45932 var makeWrappedSourceBuffer = function makeWrappedSourceBuffer(mediaSource, mimeType) { 45933 var sourceBuffer = mediaSource.addSourceBuffer(mimeType); 45934 var wrapper = Object.create(null); 45935 wrapper.updating = false; 45936 wrapper.realBuffer_ = sourceBuffer; 45937 45938 var _loop = function _loop(key) { 45939 if (typeof sourceBuffer[key] === 'function') { 45940 wrapper[key] = function () { 45941 return sourceBuffer[key].apply(sourceBuffer, arguments); 45942 }; 45943 } else if (typeof wrapper[key] === 'undefined') { 45944 Object.defineProperty(wrapper, key, { 45945 get: function get$$1() { 45946 return sourceBuffer[key]; 45947 }, 45948 set: function set$$1(v) { 45949 return sourceBuffer[key] = v; 45950 } 45951 }); 45952 } 45953 }; 45954 45955 for (var key in sourceBuffer) { 45956 _loop(key); 45957 } 45958 45959 return wrapper; 45960 }; 45961 /** 45962 * VirtualSourceBuffers exist so that we can transmux non native formats 45963 * into a native format, but keep the same api as a native source buffer. 45964 * It creates a transmuxer, that works in its own thread (a web worker) and 45965 * that transmuxer muxes the data into a native format. VirtualSourceBuffer will 45966 * then send all of that data to the naive sourcebuffer so that it is 45967 * indestinguishable from a natively supported format. 45968 * 45969 * @param {HtmlMediaSource} mediaSource the parent mediaSource 45970 * @param {Array} codecs array of codecs that we will be dealing with 45971 * @class VirtualSourceBuffer 45972 * @extends video.js.EventTarget 45973 */ 45974 45975 45976 var VirtualSourceBuffer = function (_videojs$EventTarget) { 45977 inherits$2(VirtualSourceBuffer, _videojs$EventTarget); 45978 45979 function VirtualSourceBuffer(mediaSource, codecs) { 45980 classCallCheck$1(this, VirtualSourceBuffer); 45981 45982 var _this = possibleConstructorReturn$1(this, (VirtualSourceBuffer.__proto__ || Object.getPrototypeOf(VirtualSourceBuffer)).call(this, videojs$1.EventTarget)); 45983 45984 _this.timestampOffset_ = 0; 45985 _this.pendingBuffers_ = []; 45986 _this.bufferUpdating_ = false; 45987 _this.mediaSource_ = mediaSource; 45988 _this.codecs_ = codecs; 45989 _this.audioCodec_ = null; 45990 _this.videoCodec_ = null; 45991 _this.audioDisabled_ = false; 45992 _this.appendAudioInitSegment_ = true; 45993 _this.gopBuffer_ = []; 45994 _this.timeMapping_ = 0; 45995 _this.safeAppend_ = videojs$1.browser.IE_VERSION >= 11; 45996 var options = { 45997 remux: false, 45998 alignGopsAtEnd: _this.safeAppend_ 45999 }; 46000 46001 _this.codecs_.forEach(function (codec) { 46002 if (isAudioCodec(codec)) { 46003 _this.audioCodec_ = codec; 46004 } else if (isVideoCodec(codec)) { 46005 _this.videoCodec_ = codec; 46006 } 46007 }); // append muxed segments to their respective native buffers as 46008 // soon as they are available 46009 46010 46011 _this.transmuxer_ = new TransmuxWorker(); 46012 46013 _this.transmuxer_.postMessage({ 46014 action: 'init', 46015 options: options 46016 }); 46017 46018 _this.transmuxer_.onmessage = function (event) { 46019 if (event.data.action === 'data') { 46020 return _this.data_(event); 46021 } 46022 46023 if (event.data.action === 'done') { 46024 return _this.done_(event); 46025 } 46026 46027 if (event.data.action === 'gopInfo') { 46028 return _this.appendGopInfo_(event); 46029 } 46030 46031 if (event.data.action === 'videoSegmentTimingInfo') { 46032 return _this.videoSegmentTimingInfo_(event.data.videoSegmentTimingInfo); 46033 } 46034 }; // this timestampOffset is a property with the side-effect of resetting 46035 // baseMediaDecodeTime in the transmuxer on the setter 46036 46037 46038 Object.defineProperty(_this, 'timestampOffset', { 46039 get: function get$$1() { 46040 return this.timestampOffset_; 46041 }, 46042 set: function set$$1(val) { 46043 if (typeof val === 'number' && val >= 0) { 46044 this.timestampOffset_ = val; 46045 this.appendAudioInitSegment_ = true; // reset gop buffer on timestampoffset as this signals a change in timeline 46046 46047 this.gopBuffer_.length = 0; 46048 this.timeMapping_ = 0; // We have to tell the transmuxer to set the baseMediaDecodeTime to 46049 // the desired timestampOffset for the next segment 46050 46051 this.transmuxer_.postMessage({ 46052 action: 'setTimestampOffset', 46053 timestampOffset: val 46054 }); 46055 } 46056 } 46057 }); // setting the append window affects both source buffers 46058 46059 Object.defineProperty(_this, 'appendWindowStart', { 46060 get: function get$$1() { 46061 return (this.videoBuffer_ || this.audioBuffer_).appendWindowStart; 46062 }, 46063 set: function set$$1(start) { 46064 if (this.videoBuffer_) { 46065 this.videoBuffer_.appendWindowStart = start; 46066 } 46067 46068 if (this.audioBuffer_) { 46069 this.audioBuffer_.appendWindowStart = start; 46070 } 46071 } 46072 }); // this buffer is "updating" if either of its native buffers are 46073 46074 Object.defineProperty(_this, 'updating', { 46075 get: function get$$1() { 46076 return !!(this.bufferUpdating_ || !this.audioDisabled_ && this.audioBuffer_ && this.audioBuffer_.updating || this.videoBuffer_ && this.videoBuffer_.updating); 46077 } 46078 }); // the buffered property is the intersection of the buffered 46079 // ranges of the native source buffers 46080 46081 Object.defineProperty(_this, 'buffered', { 46082 get: function get$$1() { 46083 return buffered(this.videoBuffer_, this.audioBuffer_, this.audioDisabled_); 46084 } 46085 }); 46086 return _this; 46087 } 46088 /** 46089 * When we get a data event from the transmuxer 46090 * we call this function and handle the data that 46091 * was sent to us 46092 * 46093 * @private 46094 * @param {Event} event the data event from the transmuxer 46095 */ 46096 46097 46098 createClass$1(VirtualSourceBuffer, [{ 46099 key: 'data_', 46100 value: function data_(event) { 46101 var segment = event.data.segment; // Cast ArrayBuffer to TypedArray 46102 46103 segment.data = new Uint8Array(segment.data, event.data.byteOffset, event.data.byteLength); 46104 segment.initSegment = new Uint8Array(segment.initSegment.data, segment.initSegment.byteOffset, segment.initSegment.byteLength); 46105 createTextTracksIfNecessary(this, this.mediaSource_, segment); // Add the segments to the pendingBuffers array 46106 46107 this.pendingBuffers_.push(segment); 46108 return; 46109 } 46110 /** 46111 * When we get a done event from the transmuxer 46112 * we call this function and we process all 46113 * of the pending data that we have been saving in the 46114 * data_ function 46115 * 46116 * @private 46117 * @param {Event} event the done event from the transmuxer 46118 */ 46119 46120 }, { 46121 key: 'done_', 46122 value: function done_(event) { 46123 // Don't process and append data if the mediaSource is closed 46124 if (this.mediaSource_.readyState === 'closed') { 46125 this.pendingBuffers_.length = 0; 46126 return; 46127 } // All buffers should have been flushed from the muxer 46128 // start processing anything we have received 46129 46130 46131 this.processPendingSegments_(); 46132 return; 46133 } 46134 }, { 46135 key: 'videoSegmentTimingInfo_', 46136 value: function videoSegmentTimingInfo_(timingInfo) { 46137 var timingInfoInSeconds = { 46138 start: { 46139 decode: timingInfo.start.dts / ONE_SECOND_IN_TS$2, 46140 presentation: timingInfo.start.pts / ONE_SECOND_IN_TS$2 46141 }, 46142 end: { 46143 decode: timingInfo.end.dts / ONE_SECOND_IN_TS$2, 46144 presentation: timingInfo.end.pts / ONE_SECOND_IN_TS$2 46145 }, 46146 baseMediaDecodeTime: timingInfo.baseMediaDecodeTime / ONE_SECOND_IN_TS$2 46147 }; 46148 46149 if (timingInfo.prependedContentDuration) { 46150 timingInfoInSeconds.prependedContentDuration = timingInfo.prependedContentDuration / ONE_SECOND_IN_TS$2; 46151 } 46152 46153 this.trigger({ 46154 type: 'videoSegmentTimingInfo', 46155 videoSegmentTimingInfo: timingInfoInSeconds 46156 }); 46157 } 46158 /** 46159 * Create our internal native audio/video source buffers and add 46160 * event handlers to them with the following conditions: 46161 * 1. they do not already exist on the mediaSource 46162 * 2. this VSB has a codec for them 46163 * 46164 * @private 46165 */ 46166 46167 }, { 46168 key: 'createRealSourceBuffers_', 46169 value: function createRealSourceBuffers_() { 46170 var _this2 = this; 46171 46172 var types = ['audio', 'video'];
46173 types.forEach(function (type) { 46174 // Don't create a SourceBuffer of this type if we don't have a 46175 // codec for it 46176 if (!_this2[type + 'Codec_']) { 46177 return; 46178 } // Do nothing if a SourceBuffer of this type already exists 46179 46180 46181 if (_this2[type + 'Buffer_']) { 46182 return; 46183 } 46184 46185 var buffer = null; // If the mediasource already has a SourceBuffer for the codec 46186 // use that 46187 46188 if (_this2.mediaSource_[type + 'Buffer_']) { 46189 buffer = _this2.mediaSource_[type + 'Buffer_']; // In multiple audio track cases, the audio source buffer is disabled 46190 // on the main VirtualSourceBuffer by the HTMLMediaSource much earlier 46191 // than createRealSourceBuffers_ is called to create the second 46192 // VirtualSourceBuffer because that happens as a side-effect of 46193 // videojs-contrib-hls starting the audioSegmentLoader. As a result, 46194 // the audioBuffer is essentially "ownerless" and no one will toggle
vendor: 30,121 bytes, lines 46195-47020
46195 // the `updating` state back to false once the `updateend` event is received 46196 // 46197 // Setting `updating` to false manually will work around this 46198 // situation and allow work to continue 46199 46200 buffer.updating = false; 46201 } else { 46202 var codecProperty = type + 'Codec_'; 46203 var mimeType = type + '/mp4;codecs="' + _this2[codecProperty] + '"'; 46204 buffer = makeWrappedSourceBuffer(_this2.mediaSource_.nativeMediaSource_, mimeType); 46205 _this2.mediaSource_[type + 'Buffer_'] = buffer; 46206 } 46207 46208 _this2[type + 'Buffer_'] = buffer; // Wire up the events to the SourceBuffer 46209 46210 ['update', 'updatestart', 'updateend'].forEach(function (event) { 46211 buffer.addEventListener(event, function () { 46212 // if audio is disabled 46213 if (type === 'audio' && _this2.audioDisabled_) { 46214 return; 46215 } 46216 46217 if (event === 'updateend') { 46218 _this2[type + 'Buffer_'].updating = false; 46219 } 46220 46221 var shouldTrigger = types.every(function (t) { 46222 // skip checking audio's updating status if audio 46223 // is not enabled 46224 if (t === 'audio' && _this2.audioDisabled_) { 46225 return true; 46226 } // if the other type is updating we don't trigger 46227 46228 46229 if (type !== t && _this2[t + 'Buffer_'] && _this2[t + 'Buffer_'].updating) { 46230 return false; 46231 } 46232 46233 return true; 46234 }); 46235 46236 if (shouldTrigger) { 46237 return _this2.trigger(event); 46238 } 46239 }); 46240 }); 46241 }); 46242 } 46243 /** 46244 * Emulate the native mediasource function, but our function will 46245 * send all of the proposed segments to the transmuxer so that we 46246 * can transmux them before we append them to our internal 46247 * native source buffers in the correct format. 46248 * 46249 * @link https://developer.mozilla.org/en-US/docs/Web/API/SourceBuffer/appendBuffer 46250 * @param {Uint8Array} segment the segment to append to the buffer 46251 */ 46252 46253 }, { 46254 key: 'appendBuffer', 46255 value: function appendBuffer(segment) { 46256 // Start the internal "updating" state 46257 this.bufferUpdating_ = true; 46258 46259 if (this.audioBuffer_ && this.audioBuffer_.buffered.length) { 46260 var audioBuffered = this.audioBuffer_.buffered; 46261 this.transmuxer_.postMessage({ 46262 action: 'setAudioAppendStart', 46263 appendStart: audioBuffered.end(audioBuffered.length - 1) 46264 }); 46265 } 46266 46267 if (this.videoBuffer_) { 46268 this.transmuxer_.postMessage({ 46269 action: 'alignGopsWith', 46270 gopsToAlignWith: gopsSafeToAlignWith(this.gopBuffer_, this.mediaSource_.player_ ? this.mediaSource_.player_.currentTime() : null, this.timeMapping_) 46271 }); 46272 } 46273 46274 this.transmuxer_.postMessage({ 46275 action: 'push', 46276 // Send the typed-array of data as an ArrayBuffer so that 46277 // it can be sent as a "Transferable" and avoid the costly 46278 // memory copy 46279 data: segment.buffer, 46280 // To recreate the original typed-array, we need information 46281 // about what portion of the ArrayBuffer it was a view into 46282 byteOffset: segment.byteOffset, 46283 byteLength: segment.byteLength 46284 }, [segment.buffer]); 46285 this.transmuxer_.postMessage({ 46286 action: 'flush' 46287 }); 46288 } 46289 /** 46290 * Appends gop information (timing and byteLength) received by the transmuxer for the 46291 * gops appended in the last call to appendBuffer 46292 * 46293 * @param {Event} event 46294 * The gopInfo event from the transmuxer 46295 * @param {Array} event.data.gopInfo 46296 * List of gop info to append 46297 */ 46298 46299 }, { 46300 key: 'appendGopInfo_', 46301 value: function appendGopInfo_(event) { 46302 this.gopBuffer_ = updateGopBuffer(this.gopBuffer_, event.data.gopInfo, this.safeAppend_); 46303 } 46304 /** 46305 * Emulate the native mediasource function and remove parts 46306 * of the buffer from any of our internal buffers that exist 46307 * 46308 * @link https://developer.mozilla.org/en-US/docs/Web/API/SourceBuffer/remove 46309 * @param {Double} start position to start the remove at 46310 * @param {Double} end position to end the remove at 46311 */ 46312 46313 }, { 46314 key: 'remove', 46315 value: function remove(start, end) { 46316 if (this.videoBuffer_) { 46317 this.videoBuffer_.updating = true; 46318 this.videoBuffer_.remove(start, end); 46319 this.gopBuffer_ = removeGopBuffer(this.gopBuffer_, start, end, this.timeMapping_); 46320 } 46321 46322 if (!this.audioDisabled_ && this.audioBuffer_) { 46323 this.audioBuffer_.updating = true; 46324 this.audioBuffer_.remove(start, end); 46325 } // Remove Metadata Cues (id3) 46326 46327 46328 removeCuesFromTrack(start, end, this.metadataTrack_); // Remove Any Captions 46329 46330 if (this.inbandTextTracks_) { 46331 for (var track in this.inbandTextTracks_) { 46332 removeCuesFromTrack(start, end, this.inbandTextTracks_[track]); 46333 } 46334 } 46335 } 46336 /** 46337 * Process any segments that the muxer has output 46338 * Concatenate segments together based on type and append them into 46339 * their respective sourceBuffers 46340 * 46341 * @private 46342 */ 46343 46344 }, { 46345 key: 'processPendingSegments_', 46346 value: function processPendingSegments_() { 46347 var sortedSegments = { 46348 video: { 46349 segments: [], 46350 bytes: 0 46351 }, 46352 audio: { 46353 segments: [], 46354 bytes: 0 46355 }, 46356 captions: [], 46357 metadata: [] 46358 }; 46359 46360 if (!this.pendingBuffers_.length) { 46361 // We are no longer in the internal "updating" state 46362 this.trigger('updateend'); 46363 this.bufferUpdating_ = false; 46364 return; 46365 } // Sort segments into separate video/audio arrays and 46366 // keep track of their total byte lengths 46367 46368 46369 sortedSegments = this.pendingBuffers_.reduce(function (segmentObj, segment) { 46370 var type = segment.type; 46371 var data = segment.data; 46372 var initSegment = segment.initSegment; 46373 segmentObj[type].segments.push(data); 46374 segmentObj[type].bytes += data.byteLength; 46375 segmentObj[type].initSegment = initSegment; // Gather any captions into a single array 46376 46377 if (segment.captions) { 46378 segmentObj.captions = segmentObj.captions.concat(segment.captions); 46379 } 46380 46381 if (segment.info) { 46382 segmentObj[type].info = segment.info; 46383 } // Gather any metadata into a single array 46384 46385 46386 if (segment.metadata) { 46387 segmentObj.metadata = segmentObj.metadata.concat(segment.metadata); 46388 } 46389 46390 return segmentObj; 46391 }, sortedSegments); // Create the real source buffers if they don't exist by now since we 46392 // finally are sure what tracks are contained in the source 46393 46394 if (!this.videoBuffer_ && !this.audioBuffer_) { 46395 // Remove any codecs that may have been specified by default but 46396 // are no longer applicable now 46397 if (sortedSegments.video.bytes === 0) { 46398 this.videoCodec_ = null; 46399 } 46400 46401 if (sortedSegments.audio.bytes === 0) { 46402 this.audioCodec_ = null; 46403 } 46404 46405 this.createRealSourceBuffers_(); 46406 } 46407 46408 if (sortedSegments.audio.info) { 46409 this.mediaSource_.trigger({ 46410 type: 'audioinfo', 46411 info: sortedSegments.audio.info 46412 }); 46413 } 46414 46415 if (sortedSegments.video.info) { 46416 this.mediaSource_.trigger({ 46417 type: 'videoinfo', 46418 info: sortedSegments.video.info 46419 }); 46420 } 46421 46422 if (this.appendAudioInitSegment_) { 46423 if (!this.audioDisabled_ && this.audioBuffer_) { 46424 sortedSegments.audio.segments.unshift(sortedSegments.audio.initSegment); 46425 sortedSegments.audio.bytes += sortedSegments.audio.initSegment.byteLength; 46426 } 46427 46428 this.appendAudioInitSegment_ = false; 46429 } 46430 46431 var triggerUpdateend = false; // Merge multiple video and audio segments into one and append 46432 46433 if (this.videoBuffer_ && sortedSegments.video.bytes) { 46434 sortedSegments.video.segments.unshift(sortedSegments.video.initSegment); 46435 sortedSegments.video.bytes += sortedSegments.video.initSegment.byteLength; 46436 this.concatAndAppendSegments_(sortedSegments.video, this.videoBuffer_); 46437 } else if (this.videoBuffer_ && (this.audioDisabled_ || !this.audioBuffer_)) { 46438 // The transmuxer did not return any bytes of video, meaning it was all trimmed 46439 // for gop alignment. Since we have a video buffer and audio is disabled, updateend 46440 // will never be triggered by this source buffer, which will cause contrib-hls 46441 // to be stuck forever waiting for updateend. If audio is not disabled, updateend 46442 // will be triggered by the audio buffer, which will be sent upwards since the video 46443 // buffer will not be in an updating state. 46444 triggerUpdateend = true; 46445 } // Add text-track data for all 46446 46447 46448 addTextTrackData(this, sortedSegments.captions, sortedSegments.metadata); 46449 46450 if (!this.audioDisabled_ && this.audioBuffer_) { 46451 this.concatAndAppendSegments_(sortedSegments.audio, this.audioBuffer_); 46452 } 46453 46454 this.pendingBuffers_.length = 0; 46455 46456 if (triggerUpdateend) { 46457 this.trigger('updateend'); 46458 } // We are no longer in the internal "updating" state 46459 46460 46461 this.bufferUpdating_ = false; 46462 } 46463 /** 46464 * Combine all segments into a single Uint8Array and then append them 46465 * to the destination buffer 46466 * 46467 * @param {Object} segmentObj 46468 * @param {SourceBuffer} destinationBuffer native source buffer to append data to 46469 * @private 46470 */ 46471 46472 }, { 46473 key: 'concatAndAppendSegments_', 46474 value: function concatAndAppendSegments_(segmentObj, destinationBuffer) { 46475 var offset = 0; 46476 var tempBuffer = void 0; 46477 46478 if (segmentObj.bytes) { 46479 tempBuffer = new Uint8Array(segmentObj.bytes); // Combine the individual segments into one large typed-array 46480 46481 segmentObj.segments.forEach(function (segment) { 46482 tempBuffer.set(segment, offset); 46483 offset += segment.byteLength; 46484 }); 46485 46486 try { 46487 destinationBuffer.updating = true; 46488 destinationBuffer.appendBuffer(tempBuffer); 46489 } catch (error) { 46490 if (this.mediaSource_.player_) { 46491 this.mediaSource_.player_.error({ 46492 code: -3, 46493 type: 'APPEND_BUFFER_ERR', 46494 message: error.message, 46495 originalError: error 46496 }); 46497 } 46498 } 46499 } 46500 } 46501 /** 46502 * Emulate the native mediasource function. abort any soureBuffer 46503 * actions and throw out any un-appended data. 46504 * 46505 * @link https://developer.mozilla.org/en-US/docs/Web/API/SourceBuffer/abort 46506 */ 46507 46508 }, { 46509 key: 'abort', 46510 value: function abort() { 46511 if (this.videoBuffer_) { 46512 this.videoBuffer_.abort(); 46513 } 46514 46515 if (!this.audioDisabled_ && this.audioBuffer_) { 46516 this.audioBuffer_.abort(); 46517 } 46518 46519 if (this.transmuxer_) { 46520 this.transmuxer_.postMessage({ 46521 action: 'reset' 46522 }); 46523 } 46524 46525 this.pendingBuffers_.length = 0; 46526 this.bufferUpdating_ = false; 46527 } 46528 }]); 46529 return VirtualSourceBuffer; 46530 }(videojs$1.EventTarget); 46531 /** 46532 * @file html-media-source.js 46533 */ 46534 46535 /** 46536 * Our MediaSource implementation in HTML, mimics native 46537 * MediaSource where/if possible. 46538 * 46539 * @link https://developer.mozilla.org/en-US/docs/Web/API/MediaSource 46540 * @class HtmlMediaSource 46541 * @extends videojs.EventTarget 46542 */ 46543 46544 46545 var HtmlMediaSource = function (_videojs$EventTarget) { 46546 inherits$2(HtmlMediaSource, _videojs$EventTarget); 46547 46548 function HtmlMediaSource() { 46549 classCallCheck$1(this, HtmlMediaSource); 46550 46551 var _this = possibleConstructorReturn$1(this, (HtmlMediaSource.__proto__ || Object.getPrototypeOf(HtmlMediaSource)).call(this)); 46552 46553 var property = void 0; 46554 _this.nativeMediaSource_ = new window$1.MediaSource(); // delegate to the native MediaSource's methods by default 46555 46556 for (property in _this.nativeMediaSource_) { 46557 if (!(property in HtmlMediaSource.prototype) && typeof _this.nativeMediaSource_[property] === 'function') { 46558 _this[property] = _this.nativeMediaSource_[property].bind(_this.nativeMediaSource_); 46559 } 46560 } // emulate `duration` and `seekable` until seeking can be 46561 // handled uniformly for live streams 46562 // see https://github.com/w3c/media-source/issues/5 46563 46564 46565 _this.duration_ = NaN; 46566 Object.defineProperty(_this, 'duration', { 46567 get: function get$$1() { 46568 if (this.duration_ === Infinity) { 46569 return this.duration_; 46570 } 46571 46572 return this.nativeMediaSource_.duration; 46573 }, 46574 set: function set$$1(duration) { 46575 this.duration_ = duration; 46576 46577 if (duration !== Infinity) { 46578 this.nativeMediaSource_.duration = duration; 46579 return; 46580 } 46581 } 46582 }); 46583 Object.defineProperty(_this, 'seekable', { 46584 get: function get$$1() { 46585 if (this.duration_ === Infinity) { 46586 return videojs$1.createTimeRanges([[0, this.nativeMediaSource_.duration]]); 46587 } 46588 46589 return this.nativeMediaSource_.seekable; 46590 } 46591 }); 46592 Object.defineProperty(_this, 'readyState', { 46593 get: function get$$1() { 46594 return this.nativeMediaSource_.readyState; 46595 } 46596 }); 46597 Object.defineProperty(_this, 'activeSourceBuffers', { 46598 get: function get$$1() { 46599 return this.activeSourceBuffers_; 46600 } 46601 }); // the list of virtual and native SourceBuffers created by this 46602 // MediaSource 46603 46604 _this.sourceBuffers = []; 46605 _this.activeSourceBuffers_ = []; 46606 /** 46607 * update the list of active source buffers based upon various 46608 * imformation from HLS and video.js 46609 * 46610 * @private 46611 */ 46612 46613 _this.updateActiveSourceBuffers_ = function () { 46614 // Retain the reference but empty the array 46615 _this.activeSourceBuffers_.length = 0; // If there is only one source buffer, then it will always be active and audio will 46616 // be disabled based on the codec of the source buffer 46617 46618 if (_this.sourceBuffers.length === 1) { 46619 var sourceBuffer = _this.sourceBuffers[0]; 46620 sourceBuffer.appendAudioInitSegment_ = true; 46621 sourceBuffer.audioDisabled_ = !sourceBuffer.audioCodec_; 46622 46623 _this.activeSourceBuffers_.push(sourceBuffer); 46624 46625 return; 46626 } // There are 2 source buffers, a combined (possibly video only) source buffer and 46627 // and an audio only source buffer. 46628 // By default, the audio in the combined virtual source buffer is enabled 46629 // and the audio-only source buffer (if it exists) is disabled. 46630 46631 46632 var disableCombined = false; 46633 var disableAudioOnly = true; // TODO: maybe we can store the sourcebuffers on the track objects? 46634 // safari may do something like this 46635 46636 for (var i = 0; i < _this.player_.audioTracks().length; i++) { 46637 var track = _this.player_.audioTracks()[i]; 46638 46639 if (track.enabled && track.kind !== 'main') { 46640 // The enabled track is an alternate audio track so disable the audio in 46641 // the combined source buffer and enable the audio-only source buffer. 46642 disableCombined = true; 46643 disableAudioOnly = false; 46644 break; 46645 } 46646 } 46647 46648 _this.sourceBuffers.forEach(function (sourceBuffer, index) { 46649 /* eslinst-disable */ 46650 // TODO once codecs are required, we can switch to using the codecs to determine 46651 // what stream is the video stream, rather than relying on videoTracks 46652 46653 /* eslinst-enable */ 46654 sourceBuffer.appendAudioInitSegment_ = true; 46655 46656 if (sourceBuffer.videoCodec_ && sourceBuffer.audioCodec_) { 46657 // combined 46658 sourceBuffer.audioDisabled_ = disableCombined; 46659 } else if (sourceBuffer.videoCodec_ && !sourceBuffer.audioCodec_) { 46660 // If the "combined" source buffer is video only, then we do not want 46661 // disable the audio-only source buffer (this is mostly for demuxed 46662 // audio and video hls) 46663 sourceBuffer.audioDisabled_ = true; 46664 disableAudioOnly = false; 46665 } else if (!sourceBuffer.videoCodec_ && sourceBuffer.audioCodec_) { 46666 // audio only 46667 // In the case of audio only with alternate audio and disableAudioOnly is true 46668 // this means we want to disable the audio on the alternate audio sourcebuffer 46669 // but not the main "combined" source buffer. The "combined" source buffer is 46670 // always at index 0, so this ensures audio won't be disabled in both source 46671 // buffers. 46672 sourceBuffer.audioDisabled_ = index ? disableAudioOnly : !disableAudioOnly; 46673 46674 if (sourceBuffer.audioDisabled_) { 46675 return; 46676 } 46677 } 46678 46679 _this.activeSourceBuffers_.push(sourceBuffer); 46680 }); 46681 }; 46682 46683 _this.onPlayerMediachange_ = function () { 46684 _this.sourceBuffers.forEach(function (sourceBuffer) { 46685 sourceBuffer.appendAudioInitSegment_ = true; 46686 }); 46687 }; 46688 46689 _this.onHlsReset_ = function () { 46690 _this.sourceBuffers.forEach(function (sourceBuffer) { 46691 if (sourceBuffer.transmuxer_) { 46692 sourceBuffer.transmuxer_.postMessage({ 46693 action: 'resetCaptions' 46694 }); 46695 } 46696 }); 46697 }; 46698 46699 _this.onHlsSegmentTimeMapping_ = function (event) { 46700 _this.sourceBuffers.forEach(function (buffer) { 46701 return buffer.timeMapping_ = event.mapping; 46702 }); 46703 }; // Re-emit MediaSource events on the polyfill 46704 46705 46706 ['sourceopen', 'sourceclose', 'sourceended'].forEach(function (eventName) { 46707 this.nativeMediaSource_.addEventListener(eventName, this.trigger.bind(this)); 46708 }, _this); // capture the associated player when the MediaSource is 46709 // successfully attached 46710 46711 _this.on('sourceopen', function (event) { 46712 // Get the player this MediaSource is attached to 46713 var video = document.querySelector('[src="' + _this.url_ + '"]'); 46714 46715 if (!video) { 46716 return; 46717 } 46718 46719 _this.player_ = videojs$1(video.parentNode); 46720 46721 if (!_this.player_) { 46722 return; 46723 } // hls-reset is fired by videojs.Hls on to the tech after the main SegmentLoader 46724 // resets its state and flushes the buffer 46725 46726 46727 _this.player_.tech_.on('hls-reset', _this.onHlsReset_); // hls-segment-time-mapping is fired by videojs.Hls on to the tech after the main 46728 // SegmentLoader inspects an MTS segment and has an accurate stream to display 46729 // time mapping 46730 46731 46732 _this.player_.tech_.on('hls-segment-time-mapping', _this.onHlsSegmentTimeMapping_); 46733 46734 if (_this.player_.audioTracks && _this.player_.audioTracks()) { 46735 _this.player_.audioTracks().on('change', _this.updateActiveSourceBuffers_); 46736 46737 _this.player_.audioTracks().on('addtrack', _this.updateActiveSourceBuffers_); 46738 46739 _this.player_.audioTracks().on('removetrack', _this.updateActiveSourceBuffers_); 46740 } 46741 46742 _this.player_.on('mediachange', _this.onPlayerMediachange_); 46743 }); 46744 46745 _this.on('sourceended', function (event) { 46746 var duration = durationOfVideo(_this.duration); 46747 46748 for (var i = 0; i < _this.sourceBuffers.length; i++) { 46749 var sourcebuffer = _this.sourceBuffers[i]; 46750 var cues = sourcebuffer.metadataTrack_ && sourcebuffer.metadataTrack_.cues; 46751 46752 if (cues && cues.length) { 46753 cues[cues.length - 1].endTime = duration; 46754 } 46755 } 46756 }); // explicitly terminate any WebWorkers that were created 46757 // by SourceHandlers 46758 46759 46760 _this.on('sourceclose', function (event) { 46761 this.sourceBuffers.forEach(function (sourceBuffer) { 46762 if (sourceBuffer.transmuxer_) { 46763 sourceBuffer.transmuxer_.terminate(); 46764 } 46765 }); 46766 this.sourceBuffers.length = 0; 46767 46768 if (!this.player_) { 46769 return; 46770 } 46771 46772 if (this.player_.audioTracks && this.player_.audioTracks()) { 46773 this.player_.audioTracks().off('change', this.updateActiveSourceBuffers_); 46774 this.player_.audioTracks().off('addtrack', this.updateActiveSourceBuffers_); 46775 this.player_.audioTracks().off('removetrack', this.updateActiveSourceBuffers_); 46776 } // We can only change this if the player hasn't been disposed of yet 46777 // because `off` eventually tries to use the el_ property. If it has 46778 // been disposed of, then don't worry about it because there are no 46779 // event handlers left to unbind anyway 46780 46781 46782 if (this.player_.el_) { 46783 this.player_.off('mediachange', this.onPlayerMediachange_); 46784 } 46785 46786 if (this.player_.tech_ && this.player_.tech_.el_) { 46787 this.player_.tech_.off('hls-reset', this.onHlsReset_); 46788 this.player_.tech_.off('hls-segment-time-mapping', this.onHlsSegmentTimeMapping_); 46789 } 46790 }); 46791 46792 return _this; 46793 } 46794 /** 46795 * Add a range that that can now be seeked to. 46796 * 46797 * @param {Double} start where to start the addition 46798 * @param {Double} end where to end the addition 46799 * @private 46800 */ 46801 46802 46803 createClass$1(HtmlMediaSource, [{ 46804 key: 'addSeekableRange_', 46805 value: function addSeekableRange_(start, end) { 46806 var error = void 0; 46807 46808 if (this.duration !== Infinity) { 46809 error = new Error('MediaSource.addSeekableRange() can only be invoked ' + 'when the duration is Infinity'); 46810 error.name = 'InvalidStateError'; 46811 error.code = 11; 46812 throw error; 46813 } 46814 46815 if (end > this.nativeMediaSource_.duration || isNaN(this.nativeMediaSource_.duration)) { 46816 this.nativeMediaSource_.duration = end; 46817 } 46818 } 46819 /** 46820 * Add a source buffer to the media source. 46821 * 46822 * @link https://developer.mozilla.org/en-US/docs/Web/API/MediaSource/addSourceBuffer 46823 * @param {String} type the content-type of the content 46824 * @return {Object} the created source buffer 46825 */ 46826 46827 }, { 46828 key: 'addSourceBuffer', 46829 value: function addSourceBuffer(type) { 46830 var buffer = void 0; 46831 var parsedType = parseContentType(type); // Create a VirtualSourceBuffer to transmux MPEG-2 transport 46832 // stream segments into fragmented MP4s 46833 46834 if (/^(video|audio)\/mp2t$/i.test(parsedType.type)) { 46835 var codecs = []; 46836 46837 if (parsedType.parameters && parsedType.parameters.codecs) { 46838 codecs = parsedType.parameters.codecs.split(','); 46839 codecs = translateLegacyCodecs(codecs); 46840 codecs = codecs.filter(function (codec) { 46841 return isAudioCodec(codec) || isVideoCodec(codec); 46842 }); 46843 } 46844 46845 if (codecs.length === 0) { 46846 codecs = ['avc1.4d400d', 'mp4a.40.2']; 46847 } 46848 46849 buffer = new VirtualSourceBuffer(this, codecs); 46850 46851 if (this.sourceBuffers.length !== 0) { 46852 // If another VirtualSourceBuffer already exists, then we are creating a 46853 // SourceBuffer for an alternate audio track and therefore we know that 46854 // the source has both an audio and video track. 46855 // That means we should trigger the manual creation of the real 46856 // SourceBuffers instead of waiting for the transmuxer to return data 46857 this.sourceBuffers[0].createRealSourceBuffers_(); 46858 buffer.createRealSourceBuffers_(); // Automatically disable the audio on the first source buffer if 46859 // a second source buffer is ever created 46860 46861 this.sourceBuffers[0].audioDisabled_ = true; 46862 } 46863 } else { 46864 // delegate to the native implementation 46865 buffer = this.nativeMediaSource_.addSourceBuffer(type); 46866 } 46867 46868 this.sourceBuffers.push(buffer); 46869 return buffer; 46870 } 46871 }]); 46872 return HtmlMediaSource; 46873 }(videojs$1.EventTarget); 46874 /** 46875 * @file videojs-contrib-media-sources.js 46876 */ 46877 46878 46879 var urlCount = 0; // ------------ 46880 // Media Source 46881 // ------------ 46882 // store references to the media sources so they can be connected 46883 // to a video element (a swf object) 46884 // TODO: can we store this somewhere local to this module? 46885 46886 videojs$1.mediaSources = {}; 46887 /** 46888 * Provide a method for a swf object to notify JS that a 46889 * media source is now open. 46890 * 46891 * @param {String} msObjectURL string referencing the MSE Object URL 46892 * @param {String} swfId the swf id 46893 */ 46894 46895 var open = function open(msObjectURL, swfId) { 46896 var mediaSource = videojs$1.mediaSources[msObjectURL]; 46897 46898 if (mediaSource) { 46899 mediaSource.trigger({ 46900 type: 'sourceopen', 46901 swfId: swfId 46902 }); 46903 } else { 46904 throw new Error('Media Source not found (Video.js)'); 46905 } 46906 }; 46907 /** 46908 * Check to see if the native MediaSource object exists and supports 46909 * an MP4 container with both H.264 video and AAC-LC audio. 46910 * 46911 * @return {Boolean} if native media sources are supported 46912 */ 46913 46914 46915 var supportsNativeMediaSources = function supportsNativeMediaSources() { 46916 return !!window$1.MediaSource && !!window$1.MediaSource.isTypeSupported && window$1.MediaSource.isTypeSupported('video/mp4;codecs="avc1.4d400d,mp4a.40.2"'); 46917 }; 46918 /** 46919 * An emulation of the MediaSource API so that we can support 46920 * native and non-native functionality. returns an instance of 46921 * HtmlMediaSource. 46922 * 46923 * @link https://developer.mozilla.org/en-US/docs/Web/API/MediaSource/MediaSource 46924 */ 46925 46926 46927 var MediaSource = function MediaSource() { 46928 this.MediaSource = { 46929 open: open, 46930 supportsNativeMediaSources: supportsNativeMediaSources 46931 }; 46932 46933 if (supportsNativeMediaSources()) { 46934 return new HtmlMediaSource(); 46935 } 46936 46937 throw new Error('Cannot use create a virtual MediaSource for this video'); 46938 }; 46939 46940 MediaSource.open = open; 46941 MediaSource.supportsNativeMediaSources = supportsNativeMediaSources; 46942 /** 46943 * A wrapper around the native URL for our MSE object 46944 * implementation, this object is exposed under videojs.URL 46945 * 46946 * @link https://developer.mozilla.org/en-US/docs/Web/API/URL/URL 46947 */ 46948 46949 var URL$1 = { 46950 /** 46951 * A wrapper around the native createObjectURL for our objects. 46952 * This function maps a native or emulated mediaSource to a blob 46953 * url so that it can be loaded into video.js 46954 * 46955 * @link https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL 46956 * @param {MediaSource} object the object to create a blob url to 46957 */ 46958 createObjectURL: function createObjectURL(object) { 46959 var objectUrlPrefix = 'blob:vjs-media-source/'; 46960 var url = void 0; // use the native MediaSource to generate an object URL 46961 46962 if (object instanceof HtmlMediaSource) { 46963 url = window$1.URL.createObjectURL(object.nativeMediaSource_); 46964 object.url_ = url; 46965 return url; 46966 } // if the object isn't an emulated MediaSource, delegate to the 46967 // native implementation 46968 46969 46970 if (!(object instanceof HtmlMediaSource)) { 46971 url = window$1.URL.createObjectURL(object); 46972 object.url_ = url; 46973 return url; 46974 } // build a URL that can be used to map back to the emulated 46975 // MediaSource 46976 46977 46978 url = objectUrlPrefix + urlCount; 46979 urlCount++; // setup the mapping back to object 46980 46981 videojs$1.mediaSources[url] = object; 46982 return url; 46983 } 46984 }; 46985 videojs$1.MediaSource = MediaSource; 46986 videojs$1.URL = URL$1; 46987 var EventTarget$1$1 = videojs$1.EventTarget, 46988 mergeOptions$2 = videojs$1.mergeOptions; 46989 /** 46990 * Returns a new master manifest that is the result of merging an updated master manifest 46991 * into the original version. 46992 * 46993 * @param {Object} oldMaster 46994 * The old parsed mpd object 46995 * @param {Object} newMaster 46996 * The updated parsed mpd object 46997 * @return {Object} 46998 * A new object representing the original master manifest with the updated media 46999 * playlists merged in 47000 */ 47001 47002 var updateMaster$1 = function updateMaster$$1(oldMaster, newMaster) { 47003 var noChanges = void 0; 47004 var update = mergeOptions$2(oldMaster, { 47005 // These are top level properties that can be updated 47006 duration: newMaster.duration, 47007 minimumUpdatePeriod: newMaster.minimumUpdatePeriod 47008 }); // First update the playlists in playlist list 47009 47010 for (var i = 0; i < newMaster.playlists.length; i++) { 47011 var playlistUpdate = updateMaster(update, newMaster.playlists[i]); 47012 47013 if (playlistUpdate) { 47014 update = playlistUpdate; 47015 } else { 47016 noChanges = true; 47017 } 47018 } // Then update media group playlists 47019 47020
47021 forEachMediaGroup(newMaster, function (properties, type, group, label) { 47022 if (properties.playlists && properties.playlists.length) { 47023 var uri = properties.playlists[0].uri; 47024 47025 var _playlistUpdate = updateMaster(update, properties.playlists[0]); 47026 47027 if (_playlistUpdate) { 47028 update = _playlistUpdate; // update the playlist reference within media groups 47029 47030 update.mediaGroups[type][group][label].playlists[0] = update.playlists[uri]; 47031 noChanges = false; 47032 } 47033 } 47034 }); 47035 47036 if (noChanges) { 47037 return null; 47038 } 47039 47040 return update; 47041 }; 47042 47043 var generateSidxKey = function generateSidxKey(sidxInfo) { 47044 // should be non-inclusive 47045 var sidxByteRangeEnd = sidxInfo.byterange.offset + sidxInfo.byterange.length - 1; 47046 return sidxInfo.uri + '-' + sidxInfo.byterange.offset + '-' + sidxByteRangeEnd; 47047 }; // SIDX should be equivalent if the URI and byteranges of the SIDX match. 47048 // If the SIDXs have maps, the two maps should match, 47049 // both `a` and `b` missing SIDXs is considered matching. 47050 // If `a` or `b` but not both have a map, they aren't matching. 47051 47052 47053 var equivalentSidx = function equivalentSidx(a, b) { 47054 var neitherMap = Boolean(!a.map && !b.map); 47055 var equivalentMap = neitherMap || Boolean(a.map && b.map && a.map.byterange.offset === b.map.byterange.offset && a.map.byterange.length === b.map.byterange.length); 47056 return equivalentMap && a.uri === b.uri && a.byterange.offset === b.byterange.offset && a.byterange.length === b.byterange.length; 47057 }; // exported for testing 47058 47059 47060 var compareSidxEntry = function compareSidxEntry(playlists, oldSidxMapping) { 47061 var newSidxMapping = {}; 47062 47063 for (var uri in playlists) { 47064 var playlist = playlists[uri]; 47065 var currentSidxInfo = playlist.sidx; 47066 47067 if (currentSidxInfo) { 47068 var key = generateSidxKey(currentSidxInfo); 47069 47070 if (!oldSidxMapping[key]) { 47071 break; 47072 } 47073 47074 var savedSidxInfo = oldSidxMapping[key].sidxInfo; 47075 47076 if (equivalentSidx(savedSidxInfo, currentSidxInfo)) { 47077 newSidxMapping[key] = oldSidxMapping[key]; 47078 } 47079 } 47080 } 47081 47082 return newSidxMapping; 47083 }; 47084 /** 47085 * A function that filters out changed items as they need to be requested separately. 47086 * 47087 * The method is exported for testing 47088 * 47089 * @param {Object} masterXml the mpd XML 47090 * @param {string} srcUrl the mpd url 47091 * @param {Date} clientOffset a time difference between server and client (passed through and not used) 47092 * @param {Object} oldSidxMapping the SIDX to compare against 47093 */ 47094 47095 47096 var filterChangedSidxMappings = function filterChangedSidxMappings(masterXml, srcUrl, clientOffset, oldSidxMapping) { 47097 // Don't pass current sidx mapping 47098 var master = parse(masterXml, { 47099 manifestUri: srcUrl, 47100 clientOffset: clientOffset 47101 }); 47102 var videoSidx = compareSidxEntry(master.playlists, oldSidxMapping); 47103 var mediaGroupSidx = videoSidx; 47104 forEachMediaGroup(master, function (properties, mediaType, groupKey, labelKey) { 47105 if (properties.playlists && properties.playlists.length) { 47106 var playlists = properties.playlists; 47107 mediaGroupSidx = mergeOptions$2(mediaGroupSidx, compareSidxEntry(playlists, oldSidxMapping)); 47108 } 47109 }); 47110 return mediaGroupSidx; 47111 }; // exported for testing 47112 47113 47114 var requestSidx_ = function requestSidx_(sidxRange, playlist, xhr, options, finishProcessingFn) { 47115 var sidxInfo = { 47116 // resolve the segment URL relative to the playlist 47117 uri: resolveManifestRedirect(options.handleManifestRedirects, sidxRange.resolvedUri), 47118 // resolvedUri: sidxRange.resolvedUri, 47119 byterange: sidxRange.byterange, 47120 // the segment's playlist 47121 playlist: playlist 47122 }; 47123 var sidxRequestOptions = videojs$1.mergeOptions(sidxInfo, { 47124 responseType: 'arraybuffer', 47125 headers: segmentXhrHeaders(sidxInfo) 47126 }); 47127 return xhr(sidxRequestOptions, finishProcessingFn); 47128 }; 47129 47130 var DashPlaylistLoader = function (_EventTarget) { 47131 inherits$2(DashPlaylistLoader, _EventTarget); // DashPlaylistLoader must accept either a src url or a playlist because subsequent 47132 // playlist loader setups from media groups will expect to be able to pass a playlist 47133 // (since there aren't external URLs to media playlists with DASH) 47134 47135 function DashPlaylistLoader(srcUrlOrPlaylist, hls) { 47136 var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 47137 var masterPlaylistLoader = arguments[3]; 47138 classCallCheck$1(this, DashPlaylistLoader); 47139 47140 var _this = possibleConstructorReturn$1(this, (DashPlaylistLoader.__proto__ || Object.getPrototypeOf(DashPlaylistLoader)).call(this)); 47141 47142 var _options$withCredenti = options.withCredentials, 47143 withCredentials = _options$withCredenti === undefined ? false : _options$withCredenti, 47144 _options$handleManife = options.handleManifestRedirects, 47145 handleManifestRedirects = _options$handleManife === undefined ? false : _options$handleManife; 47146 _this.hls_ = hls; 47147 _this.withCredentials = withCredentials; 47148 _this.handleManifestRedirects = handleManifestRedirects; 47149 47150 if (!srcUrlOrPlaylist) { 47151 throw new Error('A non-empty playlist URL or playlist is required'); 47152 } // event naming? 47153 47154 47155 _this.on('minimumUpdatePeriod', function () { 47156 _this.refreshXml_(); 47157 }); // live playlist staleness timeout 47158 47159 47160 _this.on('mediaupdatetimeout', function () { 47161 _this.refreshMedia_(_this.media().uri); 47162 }); 47163 47164 _this.state = 'HAVE_NOTHING'; 47165 _this.loadedPlaylists_ = {}; // initialize the loader state 47166 // The masterPlaylistLoader will be created with a string 47167 47168 if (typeof srcUrlOrPlaylist === 'string') { 47169 _this.srcUrl = srcUrlOrPlaylist; // TODO: reset sidxMapping between period changes 47170 // once multi-period is refactored 47171 47172 _this.sidxMapping_ = {}; 47173 return possibleConstructorReturn$1(_this); 47174 } 47175 47176 _this.setupChildLoader(masterPlaylistLoader, srcUrlOrPlaylist); 47177 47178 return _this; 47179 } 47180 47181 createClass$1(DashPlaylistLoader, [{ 47182 key: 'setupChildLoader', 47183 value: function setupChildLoader(masterPlaylistLoader, playlist) { 47184 this.masterPlaylistLoader_ = masterPlaylistLoader; 47185 this.childPlaylist_ = playlist; 47186 } 47187 }, { 47188 key: 'dispose', 47189 value: function dispose() { 47190 this.stopRequest(); 47191 this.loadedPlaylists_ = {}; 47192 window$1.clearTimeout(this.minimumUpdatePeriodTimeout_); 47193 window$1.clearTimeout(this.mediaRequest_); 47194 window$1.clearTimeout(this.mediaUpdateTimeout); 47195 } 47196 }, { 47197 key: 'hasPendingRequest', 47198 value: function hasPendingRequest() { 47199 return this.request || this.mediaRequest_; 47200 } 47201 }, { 47202 key: 'stopRequest', 47203 value: function stopRequest() { 47204 if (this.request) { 47205 var oldRequest = this.request; 47206 this.request = null; 47207 oldRequest.onreadystatechange = null; 47208 oldRequest.abort(); 47209 } 47210 } 47211 }, { 47212 key: 'sidxRequestFinished_', 47213 value: function sidxRequestFinished_(playlist, master, startingState, doneFn) { 47214 var _this2 = this; 47215 47216 return function (err, request) { 47217 // disposed 47218 if (!_this2.request) { 47219 return; 47220 } // pending request is cleared 47221 47222 47223 _this2.request = null; 47224 47225 if (err) { 47226 _this2.error = { 47227 status: request.status, 47228 message: 'DASH playlist request error at URL: ' + playlist.uri, 47229 response: request.response, 47230 // MEDIA_ERR_NETWORK 47231 code: 2 47232 }; 47233 47234 if (startingState) { 47235 _this2.state = startingState; 47236 } 47237 47238 _this2.trigger('error'); 47239 47240 return doneFn(master, null); 47241 } 47242 47243 var bytes = new Uint8Array(request.response); 47244 var sidx = mp4Inspector.parseSidx(bytes.subarray(8)); 47245 return doneFn(master, sidx); 47246 }; 47247 } 47248 }, { 47249 key: 'media', 47250 value: function media(playlist) { 47251 var _this3 = this; // getter 47252 47253 47254 if (!playlist) { 47255 return this.media_; 47256 } // setter 47257 47258 47259 if (this.state === 'HAVE_NOTHING') { 47260 throw new Error('Cannot switch media playlist from ' + this.state); 47261 } 47262 47263 var startingState = this.state; // find the playlist object if the target playlist has been specified by URI 47264 47265 if (typeof playlist === 'string') { 47266 if (!this.master.playlists[playlist]) { 47267 throw new Error('Unknown playlist URI: ' + playlist); 47268 } 47269 47270 playlist = this.master.playlists[playlist]; 47271 } 47272 47273 var mediaChange = !this.media_ || playlist.uri !== this.media_.uri; // switch to previously loaded playlists immediately 47274 47275 if (mediaChange && this.loadedPlaylists_[playlist.uri] && this.loadedPlaylists_[playlist.uri].endList) { 47276 this.state = 'HAVE_METADATA'; 47277 this.media_ = playlist; // trigger media change if the active media has been updated 47278 47279 if (mediaChange) { 47280 this.trigger('mediachanging'); 47281 this.trigger('mediachange'); 47282 } 47283 47284 return; 47285 } // switching to the active playlist is a no-op 47286 47287 47288 if (!mediaChange) { 47289 return; 47290 } // switching from an already loaded playlist 47291 47292 47293 if (this.media_) { 47294 this.trigger('mediachanging'); 47295 } 47296 47297 if (!playlist.sidx) { 47298 // Continue asynchronously if there is no sidx 47299 // wait one tick to allow haveMaster to run first on a child loader 47300 this.mediaRequest_ = window$1.setTimeout(this.haveMetadata.bind(this, { 47301 startingState: startingState, 47302 playlist: playlist 47303 }), 0); // exit early and don't do sidx work 47304 47305 return; 47306 } // we have sidx mappings 47307 47308 47309 var oldMaster = void 0; 47310 var sidxMapping = void 0; // sidxMapping is used when parsing the masterXml, so store 47311 // it on the masterPlaylistLoader 47312 47313 if (this.masterPlaylistLoader_) { 47314 oldMaster = this.masterPlaylistLoader_.master; 47315 sidxMapping = this.masterPlaylistLoader_.sidxMapping_; 47316 } else { 47317 oldMaster = this.master; 47318 sidxMapping = this.sidxMapping_; 47319 } 47320 47321 var sidxKey = generateSidxKey(playlist.sidx); 47322 sidxMapping[sidxKey] = { 47323 sidxInfo: playlist.sidx 47324 }; 47325 this.request = requestSidx_(playlist.sidx, playlist, this.hls_.xhr, { 47326 handleManifestRedirects: this.handleManifestRedirects 47327 }, this.sidxRequestFinished_(playlist, oldMaster, startingState, function (newMaster, sidx) { 47328 if (!newMaster || !sidx) { 47329 throw new Error('failed to request sidx'); 47330 } // update loader's sidxMapping with parsed sidx box 47331 47332 47333 sidxMapping[sidxKey].sidx = sidx; // everything is ready just continue to haveMetadata 47334 47335 _this3.haveMetadata({ 47336 startingState: startingState, 47337 playlist: newMaster.playlists[playlist.uri] 47338 }); 47339 })); 47340 } 47341 }, { 47342 key: 'haveMetadata', 47343 value: function haveMetadata(_ref) { 47344 var startingState = _ref.startingState, 47345 playlist = _ref.playlist; 47346 this.state = 'HAVE_METADATA'; 47347 this.loadedPlaylists_[playlist.uri] = playlist; 47348 this.mediaRequest_ = null; // This will trigger loadedplaylist 47349 47350 this.refreshMedia_(playlist.uri);
vendor: 10,203 bytes, lines 47350-47637
47350 // fire loadedmetadata the first time a media playlist is loaded 47351 // to resolve setup of media groups 47352 47353 if (startingState === 'HAVE_MASTER') { 47354 this.trigger('loadedmetadata'); 47355 } else { 47356 // trigger media change if the active media has been updated 47357 this.trigger('mediachange'); 47358 } 47359 } 47360 }, { 47361 key: 'pause', 47362 value: function pause() { 47363 this.stopRequest(); 47364 window$1.clearTimeout(this.mediaUpdateTimeout); 47365 window$1.clearTimeout(this.minimumUpdatePeriodTimeout_); 47366 47367 if (this.state === 'HAVE_NOTHING') { 47368 // If we pause the loader before any data has been retrieved, its as if we never 47369 // started, so reset to an unstarted state. 47370 this.started = false; 47371 } 47372 } 47373 }, { 47374 key: 'load', 47375 value: function load(isFinalRendition) { 47376 var _this4 = this; 47377 47378 window$1.clearTimeout(this.mediaUpdateTimeout); 47379 window$1.clearTimeout(this.minimumUpdatePeriodTimeout_); 47380 var media = this.media(); 47381 47382 if (isFinalRendition) { 47383 var delay = media ? media.targetDuration / 2 * 1000 : 5 * 1000; 47384 this.mediaUpdateTimeout = window$1.setTimeout(function () { 47385 return _this4.load(); 47386 }, delay); 47387 return; 47388 } // because the playlists are internal to the manifest, load should either load the 47389 // main manifest, or do nothing but trigger an event 47390 47391 47392 if (!this.started) { 47393 this.start(); 47394 return; 47395 } 47396 47397 this.trigger('loadedplaylist'); 47398 } 47399 /** 47400 * Parses the master xml string and updates playlist uri references 47401 * 47402 * @return {Object} 47403 * The parsed mpd manifest object 47404 */ 47405 47406 }, { 47407 key: 'parseMasterXml', 47408 value: function parseMasterXml() { 47409 var master = parse(this.masterXml_, { 47410 manifestUri: this.srcUrl, 47411 clientOffset: this.clientOffset_, 47412 sidxMapping: this.sidxMapping_ 47413 }); 47414 master.uri = this.srcUrl; // Set up phony URIs for the playlists since we won't have external URIs for DASH 47415 // but reference playlists by their URI throughout the project 47416 // TODO: Should we create the dummy uris in mpd-parser as well (leaning towards yes). 47417 47418 for (var i = 0; i < master.playlists.length; i++) { 47419 var phonyUri = 'placeholder-uri-' + i; 47420 master.playlists[i].uri = phonyUri; // set up by URI references 47421 47422 master.playlists[phonyUri] = master.playlists[i]; 47423 } // set up phony URIs for the media group playlists since we won't have external 47424 // URIs for DASH but reference playlists by their URI throughout the project 47425 47426 47427 forEachMediaGroup(master, function (properties, mediaType, groupKey, labelKey) { 47428 if (properties.playlists && properties.playlists.length) { 47429 var _phonyUri = 'placeholder-uri-' + mediaType + '-' + groupKey + '-' + labelKey; 47430 47431 properties.playlists[0].uri = _phonyUri; // setup URI references 47432 47433 master.playlists[_phonyUri] = properties.playlists[0]; 47434 } 47435 }); 47436 setupMediaPlaylists(master); 47437 resolveMediaGroupUris(master); 47438 return master; 47439 } 47440 }, { 47441 key: 'start', 47442 value: function start() { 47443 var _this5 = this; 47444 47445 this.started = true; // We don't need to request the master manifest again 47446 // Call this asynchronously to match the xhr request behavior below 47447 47448 if (this.masterPlaylistLoader_) { 47449 this.mediaRequest_ = window$1.setTimeout(this.haveMaster_.bind(this), 0); 47450 return; 47451 } // request the specified URL 47452 47453 47454 this.request = this.hls_.xhr({ 47455 uri: this.srcUrl, 47456 withCredentials: this.withCredentials 47457 }, function (error, req) { 47458 // disposed 47459 if (!_this5.request) { 47460 return; 47461 } // clear the loader's request reference 47462 47463 47464 _this5.request = null; 47465 47466 if (error) { 47467 _this5.error = { 47468 status: req.status, 47469 message: 'DASH playlist request error at URL: ' + _this5.srcUrl, 47470 responseText: req.responseText, 47471 // MEDIA_ERR_NETWORK 47472 code: 2 47473 }; 47474 47475 if (_this5.state === 'HAVE_NOTHING') { 47476 _this5.started = false; 47477 } 47478 47479 return _this5.trigger('error'); 47480 } 47481 47482 _this5.masterXml_ = req.responseText; 47483 47484 if (req.responseHeaders && req.responseHeaders.date) { 47485 _this5.masterLoaded_ = Date.parse(req.responseHeaders.date); 47486 } else { 47487 _this5.masterLoaded_ = Date.now(); 47488 } 47489 47490 _this5.srcUrl = resolveManifestRedirect(_this5.handleManifestRedirects, _this5.srcUrl, req); 47491 47492 _this5.syncClientServerClock_(_this5.onClientServerClockSync_.bind(_this5)); 47493 }); 47494 } 47495 /** 47496 * Parses the master xml for UTCTiming node to sync the client clock to the server 47497 * clock. If the UTCTiming node requires a HEAD or GET request, that request is made. 47498 * 47499 * @param {Function} done 47500 * Function to call when clock sync has completed 47501 */ 47502 47503 }, { 47504 key: 'syncClientServerClock_', 47505 value: function syncClientServerClock_(done) { 47506 var _this6 = this; 47507 47508 var utcTiming = parseUTCTiming(this.masterXml_); // No UTCTiming element found in the mpd. Use Date header from mpd request as the 47509 // server clock 47510 47511 if (utcTiming === null) { 47512 this.clientOffset_ = this.masterLoaded_ - Date.now(); 47513 return done(); 47514 } 47515 47516 if (utcTiming.method === 'DIRECT') { 47517 this.clientOffset_ = utcTiming.value - Date.now(); 47518 return done(); 47519 } 47520 47521 this.request = this.hls_.xhr({ 47522 uri: resolveUrl$1(this.srcUrl, utcTiming.value), 47523 method: utcTiming.method, 47524 withCredentials: this.withCredentials 47525 }, function (error, req) { 47526 // disposed 47527 if (!_this6.request) { 47528 return; 47529 } 47530 47531 if (error) { 47532 // sync request failed, fall back to using date header from mpd 47533 // TODO: log warning 47534 _this6.clientOffset_ = _this6.masterLoaded_ - Date.now(); 47535 return done(); 47536 } 47537 47538 var serverTime = void 0; 47539 47540 if (utcTiming.method === 'HEAD') { 47541 if (!req.responseHeaders || !req.responseHeaders.date) { 47542 // expected date header not preset, fall back to using date header from mpd 47543 // TODO: log warning 47544 serverTime = _this6.masterLoaded_; 47545 } else { 47546 serverTime = Date.parse(req.responseHeaders.date); 47547 } 47548 } else { 47549 serverTime = Date.parse(req.responseText); 47550 } 47551 47552 _this6.clientOffset_ = serverTime - Date.now(); 47553 done(); 47554 }); 47555 } 47556 }, { 47557 key: 'haveMaster_', 47558 value: function haveMaster_() { 47559 this.state = 'HAVE_MASTER'; // clear media request 47560 47561 this.mediaRequest_ = null; 47562 47563 if (!this.masterPlaylistLoader_) { 47564 this.master = this.parseMasterXml(); // We have the master playlist at this point, so 47565 // trigger this to allow MasterPlaylistController 47566 // to make an initial playlist selection 47567 47568 this.trigger('loadedplaylist'); 47569 } else if (!this.media_) { 47570 // no media playlist was specifically selected so select 47571 // the one the child playlist loader was created with 47572 this.media(this.childPlaylist_); 47573 } 47574 } 47575 /** 47576 * Handler for after client/server clock synchronization has happened. Sets up 47577 * xml refresh timer if specificed by the manifest. 47578 */ 47579 47580 }, { 47581 key: 'onClientServerClockSync_', 47582 value: function onClientServerClockSync_() { 47583 var _this7 = this; 47584 47585 this.haveMaster_(); 47586 47587 if (!this.hasPendingRequest() && !this.media_) { 47588 this.media(this.master.playlists[0]); 47589 } // TODO: minimumUpdatePeriod can have a value of 0. Currently the manifest will not 47590 // be refreshed when this is the case. The inter-op guide says that when the 47591 // minimumUpdatePeriod is 0, the manifest should outline all currently available 47592 // segments, but future segments may require an update. I think a good solution 47593 // would be to update the manifest at the same rate that the media playlists 47594 // are "refreshed", i.e. every targetDuration. 47595 47596 47597 if (this.master && this.master.minimumUpdatePeriod) { 47598 this.minimumUpdatePeriodTimeout_ = window$1.setTimeout(function () { 47599 _this7.trigger('minimumUpdatePeriod'); 47600 }, this.master.minimumUpdatePeriod); 47601 } 47602 } 47603 /** 47604 * Sends request to refresh the master xml and updates the parsed master manifest 47605 * TODO: Does the client offset need to be recalculated when the xml is refreshed? 47606 */ 47607 47608 }, { 47609 key: 'refreshXml_', 47610 value: function refreshXml_() { 47611 var _this8 = this; // The srcUrl here *may* need to pass through handleManifestsRedirects when 47612 // sidx is implemented 47613 47614 47615 this.request = this.hls_.xhr({ 47616 uri: this.srcUrl, 47617 withCredentials: this.withCredentials 47618 }, function (error, req) { 47619 // disposed 47620 if (!_this8.request) { 47621 return; 47622 } // clear the loader's request reference 47623 47624 47625 _this8.request = null; 47626 47627 if (error) { 47628 _this8.error = { 47629 status: req.status, 47630 message: 'DASH playlist request error at URL: ' + _this8.srcUrl, 47631 responseText: req.responseText, 47632 // MEDIA_ERR_NETWORK 47633 code: 2 47634 }; 47635 47636 if (_this8.state === 'HAVE_NOTHING') { 47637 _this8.started = false;
47638 } 47639 47640 return _this8.trigger('error'); 47641 } 47642 47643 _this8.masterXml_ = req.responseText; // This will filter out updated sidx info from the mapping 47644 47645 _this8.sidxMapping_ = filterChangedSidxMappings(_this8.masterXml_, _this8.srcUrl, _this8.clientOffset_, _this8.sidxMapping_); 47646 47647 var master = _this8.parseMasterXml(); 47648 47649 var updatedMaster = updateMaster$1(_this8.master, master); 47650 47651 var currentSidxInfo = _this8.media().sidx; 47652 47653 if (updatedMaster) { 47654 if (currentSidxInfo) { 47655 var sidxKey = generateSidxKey(currentSidxInfo); // the sidx was updated, so the previous mapping was removed 47656 47657 if (!_this8.sidxMapping_[sidxKey]) { 47658 var playlist = _this8.media(); 47659 47660 _this8.request = requestSidx_(playlist.sidx, playlist, _this8.hls_.xhr, { 47661 handleManifestRedirects: _this8.handleManifestRedirects 47662 }, _this8.sidxRequestFinished_(playlist, master, _this8.state, function (newMaster, sidx) { 47663 if (!newMaster || !sidx) { 47664 throw new Error('failed to request sidx on minimumUpdatePeriod'); 47665 } // update loader's sidxMapping with parsed sidx box 47666 47667 47668 _this8.sidxMapping_[sidxKey].sidx = sidx; 47669 _this8.minimumUpdatePeriodTimeout_ = window$1.setTimeout(function () { 47670 _this8.trigger('minimumUpdatePeriod'); 47671 }, _this8.master.minimumUpdatePeriod); // TODO: do we need to reload the current playlist? 47672 47673 _this8.refreshMedia_(_this8.media().uri); 47674 47675 return; 47676 })); 47677 } 47678 } else { 47679 _this8.master = updatedMaster; 47680 } 47681 } 47682 47683 _this8.minimumUpdatePeriodTimeout_ = window$1.setTimeout(function () { 47684 _this8.trigger('minimumUpdatePeriod'); 47685 }, _this8.master.minimumUpdatePeriod); 47686 }); 47687 } 47688 /** 47689 * Refreshes the media playlist by re-parsing the master xml and updating playlist 47690 * references. If this is an alternate loader, the updated parsed manifest is retrieved 47691 * from the master loader. 47692 */ 47693 47694 }, { 47695 key: 'refreshMedia_', 47696 value: function refreshMedia_(mediaUri) { 47697 var _this9 = this; 47698 47699 if (!mediaUri) { 47700 throw new Error('refreshMedia_ must take a media uri'); 47701 } 47702 47703 var oldMaster = void 0; 47704 var newMaster = void 0; 47705 47706 if (this.masterPlaylistLoader_) { 47707 oldMaster = this.masterPlaylistLoader_.master; 47708 newMaster = this.masterPlaylistLoader_.parseMasterXml(); 47709 } else { 47710 oldMaster = this.master; 47711 newMaster = this.parseMasterXml(); 47712 } 47713 47714 var updatedMaster = updateMaster$1(oldMaster, newMaster); 47715 47716 if (updatedMaster) { 47717 if (this.masterPlaylistLoader_) { 47718 this.masterPlaylistLoader_.master = updatedMaster; 47719 } else { 47720 this.master = updatedMaster; 47721 } 47722 47723 this.media_ = updatedMaster.playlists[mediaUri]; 47724 } else { 47725 this.media_ = newMaster.playlists[mediaUri]; 47726 this.trigger('playlistunchanged'); 47727 } 47728 47729 if (!this.media().endList) { 47730 this.mediaUpdateTimeout = window$1.setTimeout(function () { 47731 _this9.trigger('mediaupdatetimeout'); 47732 }, refreshDelay(this.media(), !!updatedMaster)); 47733 } 47734 47735 this.trigger('loadedplaylist'); 47736 } 47737 }]); 47738 return DashPlaylistLoader; 47739 }(EventTarget$1$1); 47740 47741 var logger = function logger(source) { 47742 if (videojs$1.log.debug) { 47743 return videojs$1.log.debug.bind(videojs$1, 'VHS:', source + ' >'); 47744 } 47745 47746 return function () {}; 47747 }; 47748 47749 function noop$1() {} 47750 /** 47751 * @file source-updater.js 47752 */ 47753 47754 /** 47755 * A queue of callbacks to be serialized and applied when a 47756 * MediaSource and its associated SourceBuffers are not in the 47757 * updating state. It is used by the segment loader to update the 47758 * underlying SourceBuffers when new data is loaded, for instance. 47759 * 47760 * @class SourceUpdater 47761 * @param {MediaSource} mediaSource the MediaSource to create the 47762 * SourceBuffer from 47763 * @param {String} mimeType the desired MIME type of the underlying 47764 * SourceBuffer 47765 * @param {Object} sourceBufferEmitter an event emitter that fires when a source buffer is 47766 * added to the media source 47767 */ 47768 47769 47770 var SourceUpdater = function () { 47771 function SourceUpdater(mediaSource, mimeType, type, sourceBufferEmitter) { 47772 classCallCheck$1(this, SourceUpdater); 47773 this.callbacks_ = []; 47774 this.pendingCallback_ = null; 47775 this.timestampOffset_ = 0; 47776 this.mediaSource = mediaSource; 47777 this.processedAppend_ = false;
vendor: 13,384 bytes, lines 47778-48197
47778 this.type_ = type; 47779 this.mimeType_ = mimeType; 47780 this.logger_ = logger('SourceUpdater[' + type + '][' + mimeType + ']'); 47781 47782 if (mediaSource.readyState === 'closed') { 47783 mediaSource.addEventListener('sourceopen', this.createSourceBuffer_.bind(this, mimeType, sourceBufferEmitter)); 47784 } else { 47785 this.createSourceBuffer_(mimeType, sourceBufferEmitter); 47786 } 47787 } 47788 47789 createClass$1(SourceUpdater, [{ 47790 key: 'createSourceBuffer_', 47791 value: function createSourceBuffer_(mimeType, sourceBufferEmitter) { 47792 var _this = this; 47793 47794 this.sourceBuffer_ = this.mediaSource.addSourceBuffer(mimeType); 47795 this.logger_('created SourceBuffer'); 47796 47797 if (sourceBufferEmitter) { 47798 sourceBufferEmitter.trigger('sourcebufferadded'); 47799 47800 if (this.mediaSource.sourceBuffers.length < 2) { 47801 // There's another source buffer we must wait for before we can start updating 47802 // our own (or else we can get into a bad state, i.e., appending video/audio data 47803 // before the other video/audio source buffer is available and leading to a video 47804 // or audio only buffer). 47805 sourceBufferEmitter.on('sourcebufferadded', function () { 47806 _this.start_(); 47807 }); 47808 return; 47809 } 47810 } 47811 47812 this.start_(); 47813 } 47814 }, { 47815 key: 'start_', 47816 value: function start_() { 47817 var _this2 = this; 47818 47819 this.started_ = true; // run completion handlers and process callbacks as updateend 47820 // events fire 47821 47822 this.onUpdateendCallback_ = function () { 47823 var pendingCallback = _this2.pendingCallback_; 47824 _this2.pendingCallback_ = null; 47825 _this2.sourceBuffer_.removing = false; 47826 47827 _this2.logger_('buffered [' + printableRange(_this2.buffered()) + ']'); 47828 47829 if (pendingCallback) { 47830 pendingCallback(); 47831 } 47832 47833 _this2.runCallback_(); 47834 }; 47835 47836 this.sourceBuffer_.addEventListener('updateend', this.onUpdateendCallback_); 47837 this.runCallback_(); 47838 } 47839 /** 47840 * Aborts the current segment and resets the segment parser. 47841 * 47842 * @param {Function} done function to call when done 47843 * @see http://w3c.github.io/media-source/#widl-SourceBuffer-abort-void 47844 */ 47845 47846 }, { 47847 key: 'abort', 47848 value: function abort(done) { 47849 var _this3 = this; 47850 47851 if (this.processedAppend_) { 47852 this.queueCallback_(function () { 47853 _this3.sourceBuffer_.abort(); 47854 }, done); 47855 } 47856 } 47857 /** 47858 * Queue an update to append an ArrayBuffer. 47859 * 47860 * @param {ArrayBuffer} bytes 47861 * @param {Function} done the function to call when done 47862 * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-appendBuffer-void-ArrayBuffer-data 47863 */ 47864 47865 }, { 47866 key: 'appendBuffer', 47867 value: function appendBuffer(config, done) { 47868 var _this4 = this; 47869 47870 this.processedAppend_ = true; 47871 this.queueCallback_(function () { 47872 if (config.videoSegmentTimingInfoCallback) { 47873 _this4.sourceBuffer_.addEventListener('videoSegmentTimingInfo', config.videoSegmentTimingInfoCallback); 47874 } 47875 47876 _this4.sourceBuffer_.appendBuffer(config.bytes); 47877 }, function () { 47878 if (config.videoSegmentTimingInfoCallback) { 47879 _this4.sourceBuffer_.removeEventListener('videoSegmentTimingInfo', config.videoSegmentTimingInfoCallback); 47880 } 47881 47882 done(); 47883 }); 47884 } 47885 /** 47886 * Indicates what TimeRanges are buffered in the managed SourceBuffer. 47887 * 47888 * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-buffered 47889 */ 47890 47891 }, { 47892 key: 'buffered', 47893 value: function buffered() { 47894 if (!this.sourceBuffer_) { 47895 return videojs$1.createTimeRanges(); 47896 } 47897 47898 return this.sourceBuffer_.buffered; 47899 } 47900 /** 47901 * Queue an update to remove a time range from the buffer. 47902 * 47903 * @param {Number} start where to start the removal 47904 * @param {Number} end where to end the removal 47905 * @param {Function} [done=noop] optional callback to be executed when the remove 47906 * operation is complete 47907 * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-remove-void-double-start-unrestricted-double-end 47908 */ 47909 47910 }, { 47911 key: 'remove', 47912 value: function remove(start, end) { 47913 var _this5 = this; 47914 47915 var done = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : noop$1; 47916 47917 if (this.processedAppend_) { 47918 this.queueCallback_(function () { 47919 _this5.logger_('remove [' + start + ' => ' + end + ']'); 47920 47921 _this5.sourceBuffer_.removing = true; 47922 47923 _this5.sourceBuffer_.remove(start, end); 47924 }, done); 47925 } 47926 } 47927 /** 47928 * Whether the underlying sourceBuffer is updating or not 47929 * 47930 * @return {Boolean} the updating status of the SourceBuffer 47931 */ 47932 47933 }, { 47934 key: 'updating', 47935 value: function updating() { 47936 // we are updating if the sourcebuffer is updating or 47937 return !this.sourceBuffer_ || this.sourceBuffer_.updating || // if we have a pending callback that is not our internal noop 47938 !!this.pendingCallback_ && this.pendingCallback_ !== noop$1; 47939 } 47940 /** 47941 * Set/get the timestampoffset on the SourceBuffer 47942 * 47943 * @return {Number} the timestamp offset 47944 */ 47945 47946 }, { 47947 key: 'timestampOffset', 47948 value: function timestampOffset(offset) { 47949 var _this6 = this; 47950 47951 if (typeof offset !== 'undefined') { 47952 this.queueCallback_(function () { 47953 _this6.sourceBuffer_.timestampOffset = offset; 47954 47955 _this6.runCallback_(); 47956 }); 47957 this.timestampOffset_ = offset; 47958 } 47959 47960 return this.timestampOffset_; 47961 } 47962 /** 47963 * Queue a callback to run 47964 */ 47965 47966 }, { 47967 key: 'queueCallback_', 47968 value: function queueCallback_(callback, done) { 47969 this.callbacks_.push([callback.bind(this), done]); 47970 this.runCallback_(); 47971 } 47972 /** 47973 * Run a queued callback 47974 */ 47975 47976 }, { 47977 key: 'runCallback_', 47978 value: function runCallback_() { 47979 var callbacks = void 0; 47980 47981 if (!this.updating() && this.callbacks_.length && this.started_) { 47982 callbacks = this.callbacks_.shift(); 47983 this.pendingCallback_ = callbacks[1]; 47984 callbacks[0](); 47985 } 47986 } 47987 /** 47988 * dispose of the source updater and the underlying sourceBuffer 47989 */ 47990 47991 }, { 47992 key: 'dispose', 47993 value: function dispose() { 47994 var _this7 = this; 47995 47996 var disposeFn = function disposeFn() { 47997 if (_this7.sourceBuffer_ && _this7.mediaSource.readyState === 'open') { 47998 _this7.sourceBuffer_.abort(); 47999 } 48000 48001 _this7.sourceBuffer_.removeEventListener('updateend', disposeFn); 48002 }; 48003 48004 this.sourceBuffer_.removeEventListener('updateend', this.onUpdateendCallback_); 48005 48006 if (this.sourceBuffer_.removing) { 48007 this.sourceBuffer_.addEventListener('updateend', disposeFn); 48008 } else { 48009 disposeFn(); 48010 } 48011 } 48012 }]); 48013 return SourceUpdater; 48014 }(); 48015 48016 var Config = { 48017 GOAL_BUFFER_LENGTH: 30, 48018 MAX_GOAL_BUFFER_LENGTH: 60, 48019 GOAL_BUFFER_LENGTH_RATE: 1, 48020 // 0.5 MB/s 48021 INITIAL_BANDWIDTH: 4194304, 48022 // A fudge factor to apply to advertised playlist bitrates to account for 48023 // temporary flucations in client bandwidth 48024 BANDWIDTH_VARIANCE: 1.2, 48025 // How much of the buffer must be filled before we consider upswitching 48026 BUFFER_LOW_WATER_LINE: 0, 48027 MAX_BUFFER_LOW_WATER_LINE: 30, 48028 BUFFER_LOW_WATER_LINE_RATE: 1 48029 }; 48030 var REQUEST_ERRORS = { 48031 FAILURE: 2, 48032 TIMEOUT: -101, 48033 ABORTED: -102 48034 }; 48035 /** 48036 * Abort all requests 48037 * 48038 * @param {Object} activeXhrs - an object that tracks all XHR requests 48039 */ 48040 48041 var abortAll = function abortAll(activeXhrs) { 48042 activeXhrs.forEach(function (xhr) { 48043 xhr.abort(); 48044 }); 48045 }; 48046 /** 48047 * Gather important bandwidth stats once a request has completed 48048 * 48049 * @param {Object} request - the XHR request from which to gather stats 48050 */ 48051 48052 48053 var getRequestStats = function getRequestStats(request) { 48054 return { 48055 bandwidth: request.bandwidth, 48056 bytesReceived: request.bytesReceived || 0, 48057 roundTripTime: request.roundTripTime || 0 48058 }; 48059 }; 48060 /** 48061 * If possible gather bandwidth stats as a request is in 48062 * progress 48063 * 48064 * @param {Event} progressEvent - an event object from an XHR's progress event 48065 */ 48066 48067 48068 var getProgressStats = function getProgressStats(progressEvent) { 48069 var request = progressEvent.target; 48070 var roundTripTime = Date.now() - request.requestTime; 48071 var stats = { 48072 bandwidth: Infinity, 48073 bytesReceived: 0, 48074 roundTripTime: roundTripTime || 0 48075 }; 48076 stats.bytesReceived = progressEvent.loaded; // This can result in Infinity if stats.roundTripTime is 0 but that is ok 48077 // because we should only use bandwidth stats on progress to determine when 48078 // abort a request early due to insufficient bandwidth 48079 48080 stats.bandwidth = Math.floor(stats.bytesReceived / stats.roundTripTime * 8 * 1000); 48081 return stats; 48082 }; 48083 /** 48084 * Handle all error conditions in one place and return an object 48085 * with all the information 48086 * 48087 * @param {Error|null} error - if non-null signals an error occured with the XHR 48088 * @param {Object} request - the XHR request that possibly generated the error 48089 */ 48090 48091 48092 var handleErrors = function handleErrors(error, request) { 48093 if (request.timedout) { 48094 return { 48095 status: request.status, 48096 message: 'HLS request timed-out at URL: ' + request.uri, 48097 code: REQUEST_ERRORS.TIMEOUT, 48098 xhr: request 48099 }; 48100 } 48101 48102 if (request.aborted) { 48103 return { 48104 status: request.status, 48105 message: 'HLS request aborted at URL: ' + request.uri, 48106 code: REQUEST_ERRORS.ABORTED, 48107 xhr: request 48108 }; 48109 } 48110 48111 if (error) { 48112 return { 48113 status: request.status, 48114 message: 'HLS request errored at URL: ' + request.uri, 48115 code: REQUEST_ERRORS.FAILURE, 48116 xhr: request 48117 }; 48118 } 48119 48120 return null; 48121 }; 48122 /** 48123 * Handle responses for key data and convert the key data to the correct format 48124 * for the decryption step later 48125 * 48126 * @param {Object} segment - a simplified copy of the segmentInfo object 48127 * from SegmentLoader 48128 * @param {Function} finishProcessingFn - a callback to execute to continue processing 48129 * this request 48130 */ 48131 48132 48133 var handleKeyResponse = function handleKeyResponse(segment, finishProcessingFn) { 48134 return function (error, request) { 48135 var response = request.response; 48136 var errorObj = handleErrors(error, request); 48137 48138 if (errorObj) { 48139 return finishProcessingFn(errorObj, segment); 48140 } 48141 48142 if (response.byteLength !== 16) { 48143 return finishProcessingFn({ 48144 status: request.status, 48145 message: 'Invalid HLS key at URL: ' + request.uri, 48146 code: REQUEST_ERRORS.FAILURE, 48147 xhr: request 48148 }, segment); 48149 } 48150 48151 var view = new DataView(response); 48152 segment.key.bytes = new Uint32Array([view.getUint32(0), view.getUint32(4), view.getUint32(8), view.getUint32(12)]); 48153 return finishProcessingFn(null, segment); 48154 }; 48155 }; 48156 /** 48157 * Handle init-segment responses 48158 * 48159 * @param {Object} segment - a simplified copy of the segmentInfo object 48160 * from SegmentLoader 48161 * @param {Function} finishProcessingFn - a callback to execute to continue processing 48162 * this request 48163 */ 48164 48165 48166 var handleInitSegmentResponse = function handleInitSegmentResponse(segment, captionParser, finishProcessingFn) { 48167 return function (error, request) { 48168 var response = request.response; 48169 var errorObj = handleErrors(error, request); 48170 48171 if (errorObj) { 48172 return finishProcessingFn(errorObj, segment); 48173 } // stop processing if received empty content 48174 48175 48176 if (response.byteLength === 0) { 48177 return finishProcessingFn({ 48178 status: request.status, 48179 message: 'Empty HLS segment content at URL: ' + request.uri, 48180 code: REQUEST_ERRORS.FAILURE, 48181 xhr: request 48182 }, segment); 48183 } 48184 48185 segment.map.bytes = new Uint8Array(request.response); // Initialize CaptionParser if it hasn't been yet 48186 48187 if (captionParser && !captionParser.isInitialized()) { 48188 captionParser.init(); 48189 } 48190 48191 segment.map.timescales = probe.timescale(segment.map.bytes); 48192 segment.map.videoTrackIds = probe.videoTrackIds(segment.map.bytes); 48193 return finishProcessingFn(null, segment); 48194 }; 48195 }; 48196 /** 48197 * Response handler for segment-requests being sure to set the correct
48198 * property depending on whether the segment is encryped or not 48199 * Also records and keeps track of stats that are used for ABR purposes 48200 * 48201 * @param {Object} segment - a simplified copy of the segmentInfo object 48202 * from SegmentLoader 48203 * @param {Function} finishProcessingFn - a callback to execute to continue processing 48204 * this request 48205 */ 48206 48207 48208 var handleSegmentResponse = function handleSegmentResponse(segment, captionParser, finishProcessingFn) { 48209 return function (error, request) { 48210 var response = request.response; 48211 var errorObj = handleErrors(error, request); 48212 var parsed = void 0; 48213 48214 if (errorObj) { 48215 return finishProcessingFn(errorObj, segment); 48216 } // stop processing if received empty content 48217 48218 48219 if (response.byteLength === 0) { 48220 return finishProcessingFn({ 48221 status: request.status, 48222 message: 'Empty HLS segment content at URL: ' + request.uri, 48223 code: REQUEST_ERRORS.FAILURE, 48224 xhr: request 48225 }, segment); 48226 } 48227 48228 segment.stats = getRequestStats(request); 48229 48230 if (segment.key) { 48231 segment.encryptedBytes = new Uint8Array(request.response); 48232 } else { 48233 segment.bytes = new Uint8Array(request.response); 48234 } // This is likely an FMP4 and has the init segment. 48235 // Run through the CaptionParser in case there are captions. 48236 48237 48238 if (captionParser && segment.map && segment.map.bytes) { 48239 // Initialize CaptionParser if it hasn't been yet 48240 if (!captionParser.isInitialized()) { 48241 captionParser.init(); 48242 } 48243 48244 parsed = captionParser.parse(segment.bytes, segment.map.videoTrackIds, segment.map.timescales); 48245 48246 if (parsed && parsed.captions) { 48247 segment.captionStreams = parsed.captionStreams; 48248 segment.fmp4Captions = parsed.captions; 48249 } 48250 } 48251 48252 return finishProcessingFn(null, segment); 48253 }; 48254 }; 48255 /** 48256 * Decrypt the segment via the decryption web worker 48257 * 48258 * @param {WebWorker} decrypter - a WebWorker interface to AES-128 decryption routines 48259 * @param {Object} segment - a simplified copy of the segmentInfo object 48260 * from SegmentLoader 48261 * @param {Function} doneFn - a callback that is executed after decryption has completed 48262 */ 48263 48264 48265 var decryptSegment = function decryptSegment(decrypter, segment, doneFn) { 48266 var decryptionHandler = function decryptionHandler(event) { 48267 if (event.data.source === segment.requestId) { 48268 decrypter.removeEventListener('message', decryptionHandler); 48269 var decrypted = event.data.decrypted; 48270 segment.bytes = new Uint8Array(decrypted.bytes, decrypted.byteOffset, decrypted.byteLength); 48271 return doneFn(null, segment); 48272 } 48273 }; 48274 48275 decrypter.addEventListener('message', decryptionHandler); 48276 var keyBytes = void 0; 48277 48278 if (segment.key.bytes.slice) { 48279 keyBytes = segment.key.bytes.slice(); 48280 } else { 48281 keyBytes = new Uint32Array(Array.prototype.slice.call(segment.key.bytes)); 48282 } // this is an encrypted segment 48283 // incrementally decrypt the segment 48284 48285 48286 decrypter.postMessage(createTransferableMessage({ 48287 source: segment.requestId, 48288 encrypted: segment.encryptedBytes, 48289 key: keyBytes, 48290 iv: segment.key.iv 48291 }), [segment.encryptedBytes.buffer, keyBytes.buffer]); 48292 }; 48293 /** 48294 * This function waits for all XHRs to finish (with either success or failure) 48295 * before continueing processing via it's callback. The function gathers errors 48296 * from each request into a single errors array so that the error status for 48297 * each request can be examined later. 48298 * 48299 * @param {Object} activeXhrs - an object that tracks all XHR requests 48300 * @param {WebWorker} decrypter - a WebWorker interface to AES-128 decryption routines 48301 * @param {Function} doneFn - a callback that is executed after all resources have been 48302 * downloaded and any decryption completed 48303 */ 48304 48305 48306 var waitForCompletion = function waitForCompletion(activeXhrs, decrypter, doneFn) { 48307 var count = 0; 48308 var didError = false;
48309 return function (error, segment) { 48310 if (didError) { 48311 return; 48312 } 48313 48314 if (error) { 48315 didError = true; // If there are errors, we have to abort any outstanding requests 48316 48317 abortAll(activeXhrs); // Even though the requests above are aborted, and in theory we could wait until we 48318 // handle the aborted events from those requests, there are some cases where we may 48319 // never get an aborted event. For instance, if the network connection is lost and 48320 // there were two requests, the first may have triggered an error immediately, while 48321 // the second request remains unsent. In that case, the aborted algorithm will not 48322 // trigger an abort: see https://xhr.spec.whatwg.org/#the-abort()-method 48323 // 48324 // We also can't rely on the ready state of the XHR, since the request that 48325 // triggered the connection error may also show as a ready state of 0 (unsent). 48326 // Therefore, we have to finish this group of requests immediately after the first 48327 // seen error. 48328 48329 return doneFn(error, segment); 48330 } 48331 48332 count += 1; 48333 48334 if (count === activeXhrs.length) { 48335 // Keep track of when *all* of the requests have completed 48336 segment.endOfAllRequests = Date.now(); 48337 48338 if (segment.encryptedBytes) { 48339 return decryptSegment(decrypter, segment, doneFn); 48340 } // Otherwise, everything is ready just continue 48341 48342 48343 return doneFn(null, segment); 48344 } 48345 }; 48346 }; 48347 /** 48348 * Simple progress event callback handler that gathers some stats before 48349 * executing a provided callback with the `segment` object 48350 * 48351 * @param {Object} segment - a simplified copy of the segmentInfo object 48352 * from SegmentLoader 48353 * @param {Function} progressFn - a callback that is executed each time a progress event 48354 * is received 48355 * @param {Event} event - the progress event object from XMLHttpRequest 48356 */ 48357 48358 48359 var handleProgress = function handleProgress(segment, progressFn) { 48360 return function (event) { 48361 segment.stats = videojs$1.mergeOptions(segment.stats, getProgressStats(event)); // record the time that we receive the first byte of data 48362 48363 if (!segment.stats.firstBytesReceivedAt && segment.stats.bytesReceived) { 48364 segment.stats.firstBytesReceivedAt = Date.now(); 48365 } 48366 48367 return progressFn(event, segment); 48368 }; 48369 }; 48370 /** 48371 * Load all resources and does any processing necessary for a media-segment 48372 * 48373 * Features: 48374 * decrypts the media-segment if it has a key uri and an iv 48375 * aborts *all* requests if *any* one request fails 48376 * 48377 * The segment object, at minimum, has the following format: 48378 * { 48379 * resolvedUri: String, 48380 * [byterange]: { 48381 * offset: Number, 48382 * length: Number 48383 * }, 48384 * [key]: { 48385 * resolvedUri: String 48386 * [byterange]: { 48387 * offset: Number, 48388 * length: Number 48389 * }, 48390 * iv: { 48391 * bytes: Uint32Array 48392 * } 48393 * }, 48394 * [map]: { 48395 * resolvedUri: String, 48396 * [byterange]: { 48397 * offset: Number, 48398 * length: Number 48399 * }, 48400 * [bytes]: Uint8Array 48401 * } 48402 * } 48403 * ...where [name] denotes optional properties 48404 * 48405 * @param {Function} xhr - an instance of the xhr wrapper in xhr.js 48406 * @param {Object} xhrOptions - the base options to provide to all xhr requests 48407 * @param {WebWorker} decryptionWorker - a WebWorker interface to AES-128 48408 * decryption routines 48409 * @param {Object} segment - a simplified copy of the segmentInfo object 48410 * from SegmentLoader 48411 * @param {Function} progressFn - a callback that receives progress events from the main 48412 * segment's xhr request 48413 * @param {Function} doneFn - a callback that is executed only once all requests have 48414 * succeeded or failed 48415 * @returns {Function} a function that, when invoked, immediately aborts all 48416 * outstanding requests 48417 */ 48418 48419 48420 var mediaSegmentRequest = function mediaSegmentRequest(xhr, xhrOptions, decryptionWorker, captionParser, segment, progressFn, doneFn) { 48421 var activeXhrs = [];
vendor: 17,376 bytes, lines 48422-48829
48422 var finishProcessingFn = waitForCompletion(activeXhrs, decryptionWorker, doneFn); // optionally, request the decryption key 48423 48424 if (segment.key && !segment.key.bytes) { 48425 var keyRequestOptions = videojs$1.mergeOptions(xhrOptions, { 48426 uri: segment.key.resolvedUri, 48427 responseType: 'arraybuffer' 48428 }); 48429 var keyRequestCallback = handleKeyResponse(segment, finishProcessingFn); 48430 var keyXhr = xhr(keyRequestOptions, keyRequestCallback); 48431 activeXhrs.push(keyXhr); 48432 } // optionally, request the associated media init segment 48433 48434 48435 if (segment.map && !segment.map.bytes) { 48436 var initSegmentOptions = videojs$1.mergeOptions(xhrOptions, { 48437 uri: segment.map.resolvedUri, 48438 responseType: 'arraybuffer', 48439 headers: segmentXhrHeaders(segment.map) 48440 }); 48441 var initSegmentRequestCallback = handleInitSegmentResponse(segment, captionParser, finishProcessingFn); 48442 var initSegmentXhr = xhr(initSegmentOptions, initSegmentRequestCallback); 48443 activeXhrs.push(initSegmentXhr); 48444 } 48445 48446 var segmentRequestOptions = videojs$1.mergeOptions(xhrOptions, { 48447 uri: segment.resolvedUri, 48448 responseType: 'arraybuffer', 48449 headers: segmentXhrHeaders(segment) 48450 }); 48451 var segmentRequestCallback = handleSegmentResponse(segment, captionParser, finishProcessingFn); 48452 var segmentXhr = xhr(segmentRequestOptions, segmentRequestCallback); 48453 segmentXhr.addEventListener('progress', handleProgress(segment, progressFn)); 48454 activeXhrs.push(segmentXhr); 48455 return function () { 48456 return abortAll(activeXhrs); 48457 }; 48458 }; // Utilities 48459 48460 /** 48461 * Returns the CSS value for the specified property on an element 48462 * using `getComputedStyle`. Firefox has a long-standing issue where 48463 * getComputedStyle() may return null when running in an iframe with 48464 * `display: none`. 48465 * 48466 * @see https://bugzilla.mozilla.org/show_bug.cgi?id=548397 48467 * @param {HTMLElement} el the htmlelement to work on 48468 * @param {string} the proprety to get the style for 48469 */ 48470 48471 48472 var safeGetComputedStyle = function safeGetComputedStyle(el, property) { 48473 var result = void 0; 48474 48475 if (!el) { 48476 return ''; 48477 } 48478 48479 result = window$1.getComputedStyle(el); 48480 48481 if (!result) { 48482 return ''; 48483 } 48484 48485 return result[property]; 48486 }; 48487 /** 48488 * Resuable stable sort function 48489 * 48490 * @param {Playlists} array 48491 * @param {Function} sortFn Different comparators 48492 * @function stableSort 48493 */ 48494 48495 48496 var stableSort = function stableSort(array, sortFn) { 48497 var newArray = array.slice(); 48498 array.sort(function (left, right) { 48499 var cmp = sortFn(left, right); 48500 48501 if (cmp === 0) { 48502 return newArray.indexOf(left) - newArray.indexOf(right); 48503 } 48504 48505 return cmp; 48506 }); 48507 }; 48508 /** 48509 * A comparator function to sort two playlist object by bandwidth. 48510 * 48511 * @param {Object} left a media playlist object 48512 * @param {Object} right a media playlist object 48513 * @return {Number} Greater than zero if the bandwidth attribute of 48514 * left is greater than the corresponding attribute of right. Less 48515 * than zero if the bandwidth of right is greater than left and 48516 * exactly zero if the two are equal. 48517 */ 48518 48519 48520 var comparePlaylistBandwidth = function comparePlaylistBandwidth(left, right) { 48521 var leftBandwidth = void 0; 48522 var rightBandwidth = void 0; 48523 48524 if (left.attributes.BANDWIDTH) { 48525 leftBandwidth = left.attributes.BANDWIDTH; 48526 } 48527 48528 leftBandwidth = leftBandwidth || window$1.Number.MAX_VALUE; 48529 48530 if (right.attributes.BANDWIDTH) { 48531 rightBandwidth = right.attributes.BANDWIDTH; 48532 } 48533 48534 rightBandwidth = rightBandwidth || window$1.Number.MAX_VALUE; 48535 return leftBandwidth - rightBandwidth; 48536 }; 48537 /** 48538 * A comparator function to sort two playlist object by resolution (width). 48539 * @param {Object} left a media playlist object 48540 * @param {Object} right a media playlist object 48541 * @return {Number} Greater than zero if the resolution.width attribute of 48542 * left is greater than the corresponding attribute of right. Less 48543 * than zero if the resolution.width of right is greater than left and 48544 * exactly zero if the two are equal. 48545 */ 48546 48547 48548 var comparePlaylistResolution = function comparePlaylistResolution(left, right) { 48549 var leftWidth = void 0; 48550 var rightWidth = void 0; 48551 48552 if (left.attributes.RESOLUTION && left.attributes.RESOLUTION.width) { 48553 leftWidth = left.attributes.RESOLUTION.width; 48554 } 48555 48556 leftWidth = leftWidth || window$1.Number.MAX_VALUE; 48557 48558 if (right.attributes.RESOLUTION && right.attributes.RESOLUTION.width) { 48559 rightWidth = right.attributes.RESOLUTION.width; 48560 } 48561 48562 rightWidth = rightWidth || window$1.Number.MAX_VALUE; // NOTE - Fallback to bandwidth sort as appropriate in cases where multiple renditions 48563 // have the same media dimensions/ resolution 48564 48565 if (leftWidth === rightWidth && left.attributes.BANDWIDTH && right.attributes.BANDWIDTH) { 48566 return left.attributes.BANDWIDTH - right.attributes.BANDWIDTH; 48567 } 48568 48569 return leftWidth - rightWidth; 48570 }; 48571 /** 48572 * Chooses the appropriate media playlist based on bandwidth and player size 48573 * 48574 * @param {Object} master 48575 * Object representation of the master manifest 48576 * @param {Number} playerBandwidth 48577 * Current calculated bandwidth of the player 48578 * @param {Number} playerWidth 48579 * Current width of the player element 48580 * @param {Number} playerHeight 48581 * Current height of the player element 48582 * @param {Boolean} limitRenditionByPlayerDimensions 48583 * True if the player width and height should be used during the selection, false otherwise 48584 * @return {Playlist} the highest bitrate playlist less than the 48585 * currently detected bandwidth, accounting for some amount of 48586 * bandwidth variance 48587 */ 48588 48589 48590 var simpleSelector = function simpleSelector(master, playerBandwidth, playerWidth, playerHeight, limitRenditionByPlayerDimensions) { 48591 // convert the playlists to an intermediary representation to make comparisons easier 48592 var sortedPlaylistReps = master.playlists.map(function (playlist) { 48593 var width = void 0; 48594 var height = void 0; 48595 var bandwidth = void 0; 48596 width = playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.width; 48597 height = playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.height; 48598 bandwidth = playlist.attributes.BANDWIDTH; 48599 bandwidth = bandwidth || window$1.Number.MAX_VALUE; 48600 return { 48601 bandwidth: bandwidth, 48602 width: width, 48603 height: height, 48604 playlist: playlist 48605 }; 48606 }); 48607 stableSort(sortedPlaylistReps, function (left, right) { 48608 return left.bandwidth - right.bandwidth; 48609 }); // filter out any playlists that have been excluded due to 48610 // incompatible configurations 48611 48612 sortedPlaylistReps = sortedPlaylistReps.filter(function (rep) { 48613 return !Playlist.isIncompatible(rep.playlist); 48614 }); // filter out any playlists that have been disabled manually through the representations 48615 // api or blacklisted temporarily due to playback errors. 48616 48617 var enabledPlaylistReps = sortedPlaylistReps.filter(function (rep) { 48618 return Playlist.isEnabled(rep.playlist); 48619 }); 48620 48621 if (!enabledPlaylistReps.length) { 48622 // if there are no enabled playlists, then they have all been blacklisted or disabled 48623 // by the user through the representations api. In this case, ignore blacklisting and 48624 // fallback to what the user wants by using playlists the user has not disabled. 48625 enabledPlaylistReps = sortedPlaylistReps.filter(function (rep) { 48626 return !Playlist.isDisabled(rep.playlist); 48627 }); 48628 } // filter out any variant that has greater effective bitrate 48629 // than the current estimated bandwidth 48630 48631 48632 var bandwidthPlaylistReps = enabledPlaylistReps.filter(function (rep) { 48633 return rep.bandwidth * Config.BANDWIDTH_VARIANCE < playerBandwidth; 48634 }); 48635 var highestRemainingBandwidthRep = bandwidthPlaylistReps[bandwidthPlaylistReps.length - 1]; // get all of the renditions with the same (highest) bandwidth 48636 // and then taking the very first element 48637 48638 var bandwidthBestRep = bandwidthPlaylistReps.filter(function (rep) { 48639 return rep.bandwidth === highestRemainingBandwidthRep.bandwidth; 48640 })[0]; // if we're not going to limit renditions by player size, make an early decision. 48641 48642 if (limitRenditionByPlayerDimensions === false) { 48643 var _chosenRep = bandwidthBestRep || enabledPlaylistReps[0] || sortedPlaylistReps[0]; 48644 48645 return _chosenRep ? _chosenRep.playlist : null; 48646 } // filter out playlists without resolution information 48647 48648 48649 var haveResolution = bandwidthPlaylistReps.filter(function (rep) { 48650 return rep.width && rep.height; 48651 }); // sort variants by resolution 48652 48653 stableSort(haveResolution, function (left, right) { 48654 return left.width - right.width; 48655 }); // if we have the exact resolution as the player use it 48656 48657 var resolutionBestRepList = haveResolution.filter(function (rep) { 48658 return rep.width === playerWidth && rep.height === playerHeight; 48659 }); 48660 highestRemainingBandwidthRep = resolutionBestRepList[resolutionBestRepList.length - 1]; // ensure that we pick the highest bandwidth variant that have exact resolution 48661 48662 var resolutionBestRep = resolutionBestRepList.filter(function (rep) { 48663 return rep.bandwidth === highestRemainingBandwidthRep.bandwidth; 48664 })[0]; 48665 var resolutionPlusOneList = void 0; 48666 var resolutionPlusOneSmallest = void 0; 48667 var resolutionPlusOneRep = void 0; // find the smallest variant that is larger than the player 48668 // if there is no match of exact resolution 48669 48670 if (!resolutionBestRep) { 48671 resolutionPlusOneList = haveResolution.filter(function (rep) { 48672 return rep.width > playerWidth || rep.height > playerHeight; 48673 }); // find all the variants have the same smallest resolution 48674 48675 resolutionPlusOneSmallest = resolutionPlusOneList.filter(function (rep) { 48676 return rep.width === resolutionPlusOneList[0].width && rep.height === resolutionPlusOneList[0].height; 48677 }); // ensure that we also pick the highest bandwidth variant that 48678 // is just-larger-than the video player 48679 48680 highestRemainingBandwidthRep = resolutionPlusOneSmallest[resolutionPlusOneSmallest.length - 1]; 48681 resolutionPlusOneRep = resolutionPlusOneSmallest.filter(function (rep) { 48682 return rep.bandwidth === highestRemainingBandwidthRep.bandwidth; 48683 })[0]; 48684 } // fallback chain of variants 48685 48686 48687 var chosenRep = resolutionPlusOneRep || resolutionBestRep || bandwidthBestRep || enabledPlaylistReps[0] || sortedPlaylistReps[0]; 48688 return chosenRep ? chosenRep.playlist : null; 48689 }; // Playlist Selectors 48690 48691 /** 48692 * Chooses the appropriate media playlist based on the most recent 48693 * bandwidth estimate and the player size. 48694 * 48695 * Expects to be called within the context of an instance of HlsHandler 48696 * 48697 * @return {Playlist} the highest bitrate playlist less than the 48698 * currently detected bandwidth, accounting for some amount of 48699 * bandwidth variance 48700 */ 48701 48702 48703 var lastBandwidthSelector = function lastBandwidthSelector() { 48704 return simpleSelector(this.playlists.master, this.systemBandwidth, parseInt(safeGetComputedStyle(this.tech_.el(), 'width'), 10), parseInt(safeGetComputedStyle(this.tech_.el(), 'height'), 10), this.limitRenditionByPlayerDimensions); 48705 }; 48706 /** 48707 * Chooses the appropriate media playlist based on the potential to rebuffer 48708 * 48709 * @param {Object} settings 48710 * Object of information required to use this selector 48711 * @param {Object} settings.master 48712 * Object representation of the master manifest 48713 * @param {Number} settings.currentTime 48714 * The current time of the player 48715 * @param {Number} settings.bandwidth 48716 * Current measured bandwidth 48717 * @param {Number} settings.duration 48718 * Duration of the media 48719 * @param {Number} settings.segmentDuration 48720 * Segment duration to be used in round trip time calculations 48721 * @param {Number} settings.timeUntilRebuffer 48722 * Time left in seconds until the player has to rebuffer 48723 * @param {Number} settings.currentTimeline 48724 * The current timeline segments are being loaded from 48725 * @param {SyncController} settings.syncController 48726 * SyncController for determining if we have a sync point for a given playlist 48727 * @return {Object|null} 48728 * {Object} return.playlist 48729 * The highest bandwidth playlist with the least amount of rebuffering 48730 * {Number} return.rebufferingImpact 48731 * The amount of time in seconds switching to this playlist will rebuffer. A 48732 * negative value means that switching will cause zero rebuffering. 48733 */ 48734 48735 48736 var minRebufferMaxBandwidthSelector = function minRebufferMaxBandwidthSelector(settings) { 48737 var master = settings.master, 48738 currentTime = settings.currentTime, 48739 bandwidth = settings.bandwidth, 48740 duration$$1 = settings.duration, 48741 segmentDuration = settings.segmentDuration, 48742 timeUntilRebuffer = settings.timeUntilRebuffer, 48743 currentTimeline = settings.currentTimeline, 48744 syncController = settings.syncController; // filter out any playlists that have been excluded due to 48745 // incompatible configurations 48746 48747 var compatiblePlaylists = master.playlists.filter(function (playlist) { 48748 return !Playlist.isIncompatible(playlist); 48749 }); // filter out any playlists that have been disabled manually through the representations 48750 // api or blacklisted temporarily due to playback errors. 48751 48752 var enabledPlaylists = compatiblePlaylists.filter(Playlist.isEnabled); 48753 48754 if (!enabledPlaylists.length) { 48755 // if there are no enabled playlists, then they have all been blacklisted or disabled 48756 // by the user through the representations api. In this case, ignore blacklisting and 48757 // fallback to what the user wants by using playlists the user has not disabled. 48758 enabledPlaylists = compatiblePlaylists.filter(function (playlist) { 48759 return !Playlist.isDisabled(playlist); 48760 }); 48761 } 48762 48763 var bandwidthPlaylists = enabledPlaylists.filter(Playlist.hasAttribute.bind(null, 'BANDWIDTH')); 48764 var rebufferingEstimates = bandwidthPlaylists.map(function (playlist) { 48765 var syncPoint = syncController.getSyncPoint(playlist, duration$$1, currentTimeline, currentTime); // If there is no sync point for this playlist, switching to it will require a 48766 // sync request first. This will double the request time 48767 48768 var numRequests = syncPoint ? 1 : 2; 48769 var requestTimeEstimate = Playlist.estimateSegmentRequestTime(segmentDuration, bandwidth, playlist); 48770 var rebufferingImpact = requestTimeEstimate * numRequests - timeUntilRebuffer; 48771 return { 48772 playlist: playlist, 48773 rebufferingImpact: rebufferingImpact 48774 }; 48775 }); 48776 var noRebufferingPlaylists = rebufferingEstimates.filter(function (estimate) { 48777 return estimate.rebufferingImpact <= 0; 48778 }); // Sort by bandwidth DESC 48779 48780 stableSort(noRebufferingPlaylists, function (a, b) { 48781 return comparePlaylistBandwidth(b.playlist, a.playlist); 48782 }); 48783 48784 if (noRebufferingPlaylists.length) { 48785 return noRebufferingPlaylists[0]; 48786 } 48787 48788 stableSort(rebufferingEstimates, function (a, b) { 48789 return a.rebufferingImpact - b.rebufferingImpact; 48790 }); 48791 return rebufferingEstimates[0] || null; 48792 }; 48793 /** 48794 * Chooses the appropriate media playlist, which in this case is the lowest bitrate 48795 * one with video. If no renditions with video exist, return the lowest audio rendition. 48796 * 48797 * Expects to be called within the context of an instance of HlsHandler 48798 * 48799 * @return {Object|null} 48800 * {Object} return.playlist 48801 * The lowest bitrate playlist that contains a video codec. If no such rendition 48802 * exists pick the lowest audio rendition. 48803 */ 48804 48805 48806 var lowestBitrateCompatibleVariantSelector = function lowestBitrateCompatibleVariantSelector() { 48807 // filter out any playlists that have been excluded due to 48808 // incompatible configurations or playback errors 48809 var playlists = this.playlists.master.playlists.filter(Playlist.isEnabled); // Sort ascending by bitrate 48810 48811 stableSort(playlists, function (a, b) { 48812 return comparePlaylistBandwidth(a, b); 48813 }); // Parse and assume that playlists with no video codec have no video 48814 // (this is not necessarily true, although it is generally true). 48815 // 48816 // If an entire manifest has no valid videos everything will get filtered 48817 // out. 48818 48819 var playlistsWithVideo = playlists.filter(function (playlist) { 48820 return parseCodecs(playlist.attributes.CODECS).videoCodec; 48821 }); 48822 return playlistsWithVideo[0] || null; 48823 }; 48824 /** 48825 * Create captions text tracks on video.js if they do not exist 48826 * 48827 * @param {Object} inbandTextTracks a reference to current inbandTextTracks 48828 * @param {Object} tech the video.js tech 48829 * @param {Object}
48829 captionStreams the caption streams to create 48830 * @private 48831 */ 48832 48833 48834 var createCaptionsTrackIfNotExists = function createCaptionsTrackIfNotExists(inbandTextTracks, tech, captionStreams) { 48835 for (var trackId in captionStreams) { 48836 if (!inbandTextTracks[trackId]) { 48837 tech.trigger({ 48838 type: 'usage', 48839 name: 'hls-608' 48840 }); 48841 var track = tech.textTracks().getTrackById(trackId); 48842 48843 if (track) { 48844 // Resuse an existing track with a CC# id because this was 48845 // very likely created by videojs-contrib-hls from information 48846 // in the m3u8 for us to use 48847 inbandTextTracks[trackId] = track; 48848 } else { 48849 // Otherwise, create a track with the default `CC#` label and 48850 // without a language 48851 inbandTextTracks[trackId] = tech.addRemoteTextTrack({ 48852 kind: 'captions', 48853 id: trackId, 48854 label: trackId 48855 }, false).track; 48856 } 48857 } 48858 } 48859 }; 48860 48861 var addCaptionData = function addCaptionData(_ref) { 48862 var inbandTextTracks = _ref.inbandTextTracks, 48863 captionArray = _ref.captionArray, 48864 timestampOffset = _ref.timestampOffset; 48865 48866 if (!captionArray) { 48867 return; 48868 } 48869 48870 var Cue = window.WebKitDataCue || window.VTTCue;
vendor: 122,673 bytes, lines 48871-52217
48871 captionArray.forEach(function (caption) { 48872 var track = caption.stream; 48873 var startTime = caption.startTime; 48874 var endTime = caption.endTime; 48875 48876 if (!inbandTextTracks[track]) { 48877 return; 48878 } 48879 48880 startTime += timestampOffset; 48881 endTime += timestampOffset; 48882 inbandTextTracks[track].addCue(new Cue(startTime, endTime, caption.text)); 48883 }); 48884 }; 48885 /** 48886 * @file segment-loader.js 48887 */ 48888 // in ms 48889 48890 48891 var CHECK_BUFFER_DELAY = 500; 48892 /** 48893 * Determines if we should call endOfStream on the media source based 48894 * on the state of the buffer or if appened segment was the final 48895 * segment in the playlist. 48896 * 48897 * @param {Object} playlist a media playlist object 48898 * @param {Object} mediaSource the MediaSource object 48899 * @param {Number} segmentIndex the index of segment we last appended 48900 * @returns {Boolean} do we need to call endOfStream on the MediaSource 48901 */ 48902 48903 var detectEndOfStream = function detectEndOfStream(playlist, mediaSource, segmentIndex) { 48904 if (!playlist || !mediaSource) { 48905 return false; 48906 } 48907 48908 var segments = playlist.segments; // determine a few boolean values to help make the branch below easier 48909 // to read 48910 48911 var appendedLastSegment = segmentIndex === segments.length; // if we've buffered to the end of the video, we need to call endOfStream 48912 // so that MediaSources can trigger the `ended` event when it runs out of 48913 // buffered data instead of waiting for me 48914 48915 return playlist.endList && mediaSource.readyState === 'open' && appendedLastSegment; 48916 }; 48917 48918 var finite = function finite(num) { 48919 return typeof num === 'number' && isFinite(num); 48920 }; 48921 48922 var illegalMediaSwitch = function illegalMediaSwitch(loaderType, startingMedia, newSegmentMedia) { 48923 // Although these checks should most likely cover non 'main' types, for now it narrows 48924 // the scope of our checks. 48925 if (loaderType !== 'main' || !startingMedia || !newSegmentMedia) { 48926 return null; 48927 } 48928 48929 if (!newSegmentMedia.containsAudio && !newSegmentMedia.containsVideo) { 48930 return 'Neither audio nor video found in segment.'; 48931 } 48932 48933 if (startingMedia.containsVideo && !newSegmentMedia.containsVideo) { 48934 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.'; 48935 } 48936 48937 if (!startingMedia.containsVideo && newSegmentMedia.containsVideo) { 48938 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.'; 48939 } 48940 48941 return null; 48942 }; 48943 /** 48944 * Calculates a time value that is safe to remove from the back buffer without interupting 48945 * playback. 48946 * 48947 * @param {TimeRange} seekable 48948 * The current seekable range 48949 * @param {Number} currentTime 48950 * The current time of the player 48951 * @param {Number} targetDuration 48952 * The target duration of the current playlist 48953 * @return {Number} 48954 * Time that is safe to remove from the back buffer without interupting playback 48955 */ 48956 48957 48958 var safeBackBufferTrimTime = function safeBackBufferTrimTime(seekable$$1, currentTime, targetDuration) { 48959 var removeToTime = void 0; 48960 48961 if (seekable$$1.length && seekable$$1.start(0) > 0 && seekable$$1.start(0) < currentTime) { 48962 // If we have a seekable range use that as the limit for what can be removed safely 48963 removeToTime = seekable$$1.start(0); 48964 } else { 48965 // otherwise remove anything older than 30 seconds before the current play head 48966 removeToTime = currentTime - 30; 48967 } // Don't allow removing from the buffer within target duration of current time 48968 // to avoid the possibility of removing the GOP currently being played which could 48969 // cause playback stalls. 48970 48971 48972 return Math.min(removeToTime, currentTime - targetDuration); 48973 }; 48974 48975 var segmentInfoString = function segmentInfoString(segmentInfo) { 48976 var _segmentInfo$segment = segmentInfo.segment, 48977 start = _segmentInfo$segment.start, 48978 end = _segmentInfo$segment.end, 48979 _segmentInfo$playlist = segmentInfo.playlist, 48980 seq = _segmentInfo$playlist.mediaSequence, 48981 id = _segmentInfo$playlist.id, 48982 _segmentInfo$playlist2 = _segmentInfo$playlist.segments, 48983 segments = _segmentInfo$playlist2 === undefined ? [] : _segmentInfo$playlist2, 48984 index = segmentInfo.mediaIndex, 48985 timeline = segmentInfo.timeline; 48986 return ['appending [' + index + '] of [' + seq + ', ' + (seq + segments.length) + '] from playlist [' + id + ']', '[' + start + ' => ' + end + '] in timeline [' + timeline + ']'].join(' '); 48987 }; 48988 /** 48989 * An object that manages segment loading and appending. 48990 * 48991 * @class SegmentLoader 48992 * @param {Object} options required and optional options 48993 * @extends videojs.EventTarget 48994 */ 48995 48996 48997 var SegmentLoader = function (_videojs$EventTarget) { 48998 inherits$2(SegmentLoader, _videojs$EventTarget); 48999 49000 function SegmentLoader(settings) { 49001 classCallCheck$1(this, SegmentLoader); // check pre-conditions 49002 49003 var _this = possibleConstructorReturn$1(this, (SegmentLoader.__proto__ || Object.getPrototypeOf(SegmentLoader)).call(this)); 49004 49005 if (!settings) { 49006 throw new TypeError('Initialization settings are required'); 49007 } 49008 49009 if (typeof settings.currentTime !== 'function') { 49010 throw new TypeError('No currentTime getter specified'); 49011 } 49012 49013 if (!settings.mediaSource) { 49014 throw new TypeError('No MediaSource specified'); 49015 } // public properties 49016 49017 49018 _this.bandwidth = settings.bandwidth; 49019 _this.throughput = { 49020 rate: 0, 49021 count: 0 49022 }; 49023 _this.roundTrip = NaN; 49024 49025 _this.resetStats_(); 49026 49027 _this.mediaIndex = null; // private settings 49028 49029 _this.hasPlayed_ = settings.hasPlayed; 49030 _this.currentTime_ = settings.currentTime; 49031 _this.seekable_ = settings.seekable; 49032 _this.seeking_ = settings.seeking; 49033 _this.duration_ = settings.duration; 49034 _this.mediaSource_ = settings.mediaSource; 49035 _this.hls_ = settings.hls; 49036 _this.loaderType_ = settings.loaderType; 49037 _this.startingMedia_ = void 0; 49038 _this.segmentMetadataTrack_ = settings.segmentMetadataTrack; 49039 _this.goalBufferLength_ = settings.goalBufferLength; 49040 _this.sourceType_ = settings.sourceType; 49041 _this.inbandTextTracks_ = settings.inbandTextTracks; 49042 _this.state_ = 'INIT'; // private instance variables 49043 49044 _this.checkBufferTimeout_ = null; 49045 _this.error_ = void 0; 49046 _this.currentTimeline_ = -1; 49047 _this.pendingSegment_ = null; 49048 _this.mimeType_ = null; 49049 _this.sourceUpdater_ = null; 49050 _this.xhrOptions_ = null; // Fragmented mp4 playback 49051 49052 _this.activeInitSegmentId_ = null; 49053 _this.initSegments_ = {}; // HLSe playback 49054 49055 _this.cacheEncryptionKeys_ = settings.cacheEncryptionKeys; 49056 _this.keyCache_ = {}; // Fmp4 CaptionParser 49057 49058 if (_this.loaderType_ === 'main') { 49059 _this.captionParser_ = new captionParser(); 49060 } else { 49061 _this.captionParser_ = null; 49062 } 49063 49064 _this.decrypter_ = settings.decrypter; // Manages the tracking and generation of sync-points, mappings 49065 // between a time in the display time and a segment index within 49066 // a playlist 49067 49068 _this.syncController_ = settings.syncController; 49069 _this.syncPoint_ = { 49070 segmentIndex: 0, 49071 time: 0 49072 }; 49073 49074 _this.syncController_.on('syncinfoupdate', function () { 49075 return _this.trigger('syncinfoupdate'); 49076 }); 49077 49078 _this.mediaSource_.addEventListener('sourceopen', function () { 49079 return _this.ended_ = false; 49080 }); // ...for determining the fetch location 49081 49082 49083 _this.fetchAtBuffer_ = false; 49084 _this.logger_ = logger('SegmentLoader[' + _this.loaderType_ + ']'); 49085 Object.defineProperty(_this, 'state', { 49086 get: function get$$1() { 49087 return this.state_; 49088 }, 49089 set: function set$$1(newState) { 49090 if (newState !== this.state_) { 49091 this.logger_(this.state_ + ' -> ' + newState); 49092 this.state_ = newState; 49093 } 49094 } 49095 }); 49096 return _this; 49097 } 49098 /** 49099 * reset all of our media stats 49100 * 49101 * @private 49102 */ 49103 49104 49105 createClass$1(SegmentLoader, [{ 49106 key: 'resetStats_', 49107 value: function resetStats_() { 49108 this.mediaBytesTransferred = 0; 49109 this.mediaRequests = 0; 49110 this.mediaRequestsAborted = 0; 49111 this.mediaRequestsTimedout = 0; 49112 this.mediaRequestsErrored = 0; 49113 this.mediaTransferDuration = 0; 49114 this.mediaSecondsLoaded = 0; 49115 } 49116 /** 49117 * dispose of the SegmentLoader and reset to the default state 49118 */ 49119 49120 }, { 49121 key: 'dispose', 49122 value: function dispose() { 49123 this.state = 'DISPOSED'; 49124 this.pause(); 49125 this.abort_(); 49126 49127 if (this.sourceUpdater_) { 49128 this.sourceUpdater_.dispose(); 49129 } 49130 49131 this.resetStats_(); 49132 49133 if (this.captionParser_) { 49134 this.captionParser_.reset(); 49135 } 49136 } 49137 /** 49138 * abort anything that is currently doing on with the SegmentLoader 49139 * and reset to a default state 49140 */ 49141 49142 }, { 49143 key: 'abort', 49144 value: function abort() { 49145 if (this.state !== 'WAITING') { 49146 if (this.pendingSegment_) { 49147 this.pendingSegment_ = null; 49148 } 49149 49150 return; 49151 } 49152 49153 this.abort_(); // We aborted the requests we were waiting on, so reset the loader's state to READY 49154 // since we are no longer "waiting" on any requests. XHR callback is not always run 49155 // when the request is aborted. This will prevent the loader from being stuck in the 49156 // WAITING state indefinitely. 49157 49158 this.state = 'READY'; // don't wait for buffer check timeouts to begin fetching the 49159 // next segment 49160 49161 if (!this.paused()) { 49162 this.monitorBuffer_(); 49163 } 49164 } 49165 /** 49166 * abort all pending xhr requests and null any pending segements 49167 * 49168 * @private 49169 */ 49170 49171 }, { 49172 key: 'abort_', 49173 value: function abort_() { 49174 if (this.pendingSegment_) { 49175 this.pendingSegment_.abortRequests(); 49176 } // clear out the segment being processed 49177 49178 49179 this.pendingSegment_ = null; 49180 } 49181 /** 49182 * set an error on the segment loader and null out any pending segements 49183 * 49184 * @param {Error} error the error to set on the SegmentLoader 49185 * @return {Error} the error that was set or that is currently set 49186 */ 49187 49188 }, { 49189 key: 'error', 49190 value: function error(_error) { 49191 if (typeof _error !== 'undefined') { 49192 this.error_ = _error; 49193 } 49194 49195 this.pendingSegment_ = null; 49196 return this.error_; 49197 } 49198 }, { 49199 key: 'endOfStream', 49200 value: function endOfStream() { 49201 this.ended_ = true; 49202 this.pause(); 49203 this.trigger('ended'); 49204 } 49205 /** 49206 * Indicates which time ranges are buffered 49207 * 49208 * @return {TimeRange} 49209 * TimeRange object representing the current buffered ranges 49210 */ 49211 49212 }, { 49213 key: 'buffered_', 49214 value: function buffered_() { 49215 if (!this.sourceUpdater_) { 49216 return videojs$1.createTimeRanges(); 49217 } 49218 49219 return this.sourceUpdater_.buffered(); 49220 } 49221 /** 49222 * Gets and sets init segment for the provided map 49223 * 49224 * @param {Object} map 49225 * The map object representing the init segment to get or set 49226 * @param {Boolean=} set 49227 * If true, the init segment for the provided map should be saved 49228 * @return {Object} 49229 * map object for desired init segment 49230 */ 49231 49232 }, { 49233 key: 'initSegment', 49234 value: function initSegment(map) { 49235 var set$$1 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; 49236 49237 if (!map) { 49238 return null; 49239 } 49240 49241 var id = initSegmentId(map); 49242 var storedMap = this.initSegments_[id]; 49243 49244 if (set$$1 && !storedMap && map.bytes) { 49245 this.initSegments_[id] = storedMap = { 49246 resolvedUri: map.resolvedUri, 49247 byterange: map.byterange, 49248 bytes: map.bytes, 49249 timescales: map.timescales, 49250 videoTrackIds: map.videoTrackIds 49251 }; 49252 } 49253 49254 return storedMap || map; 49255 } 49256 /** 49257 * Gets and sets key for the provided key 49258 * 49259 * @param {Object} key 49260 * The key object representing the key to get or set 49261 * @param {Boolean=} set 49262 * If true, the key for the provided key should be saved 49263 * @return {Object} 49264 * Key object for desired key 49265 */ 49266 49267 }, { 49268 key: 'segmentKey', 49269 value: function segmentKey(key) { 49270 var set$$1 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; 49271 49272 if (!key) { 49273 return null; 49274 } 49275 49276 var id = segmentKeyId(key); 49277 var storedKey = this.keyCache_[id]; // TODO: We should use the HTTP Expires header to invalidate our cache per 49278 // https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-6.2.3 49279 49280 if (this.cacheEncryptionKeys_ && set$$1 && !storedKey && key.bytes) { 49281 this.keyCache_[id] = storedKey = { 49282 resolvedUri: key.resolvedUri, 49283 bytes: key.bytes 49284 }; 49285 } 49286 49287 var result = { 49288 resolvedUri: (storedKey || key).resolvedUri 49289 }; 49290 49291 if (storedKey) { 49292 result.bytes = storedKey.bytes; 49293 } 49294 49295 return result; 49296 } 49297 /** 49298 * Returns true if all configuration required for loading is present, otherwise false. 49299 * 49300 * @return {Boolean} True if the all configuration is ready for loading 49301 * @private 49302 */ 49303 49304 }, { 49305 key: 'couldBeginLoading_', 49306 value: function couldBeginLoading_() { 49307 return this.playlist_ && ( // the source updater is created when init_ is called, so either having a 49308 // source updater or being in the INIT state with a mimeType is enough 49309 // to say we have all the needed configuration to start loading. 49310 this.sourceUpdater_ || this.mimeType_ && this.state === 'INIT') && !this.paused(); 49311 } 49312 /** 49313 * load a playlist and start to fill the buffer 49314 */ 49315 49316 }, { 49317 key: 'load', 49318 value: function load() { 49319 // un-pause 49320 this.monitorBuffer_(); // if we don't have a playlist yet, keep waiting for one to be 49321 // specified 49322 49323 if (!this.playlist_) { 49324 return; 49325 } // not sure if this is the best place for this 49326 49327 49328 this.syncController_.setDateTimeMapping(this.playlist_); // if all the configuration is ready, initialize and begin loading 49329 49330 if (this.state === 'INIT' && this.couldBeginLoading_()) { 49331 return this.init_(); 49332 } // if we're in the middle of processing a segment already, don't 49333 // kick off an additional segment request 49334 49335 49336 if (!this.couldBeginLoading_() || this.state !== 'READY' && this.state !== 'INIT') { 49337 return; 49338 } 49339 49340 this.state = 'READY'; 49341 } 49342 /** 49343 * Once all the starting parameters have been specified, begin 49344 * operation. This method should only be invoked from the INIT 49345 * state. 49346 * 49347 * @private 49348 */ 49349 49350 }, { 49351 key: 'init_', 49352 value: function init_() { 49353 this.state = 'READY'; 49354 this.sourceUpdater_ = new SourceUpdater(this.mediaSource_, this.mimeType_, this.loaderType_, this.sourceBufferEmitter_); 49355 this.resetEverything(); 49356 return this.monitorBuffer_(); 49357 } 49358 /** 49359 * set a playlist on the segment loader 49360 * 49361 * @param {PlaylistLoader} media the playlist to set on the segment loader 49362 */ 49363 49364 }, { 49365 key: 'playlist', 49366 value: function playlist(newPlaylist) { 49367 var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 49368 49369 if (!newPlaylist) { 49370 return; 49371 } 49372 49373 var oldPlaylist = this.playlist_; 49374 var segmentInfo = this.pendingSegment_; 49375 this.playlist_ = newPlaylist; 49376 this.xhrOptions_ = options; // when we haven't started playing yet, the start of a live playlist 49377 // is always our zero-time so force a sync update each time the playlist 49378 // is refreshed from the server 49379 49380 if (!this.hasPlayed_()) { 49381 newPlaylist.syncInfo = { 49382 mediaSequence: newPlaylist.mediaSequence, 49383 time: 0 49384 }; 49385 } 49386 49387 var oldId = null; 49388 49389 if (oldPlaylist) { 49390 if (oldPlaylist.id) { 49391 oldId = oldPlaylist.id; 49392 } else if (oldPlaylist.uri) { 49393 oldId = oldPlaylist.uri; 49394 } 49395 } 49396 49397 this.logger_('playlist update [' + oldId + ' => ' + (newPlaylist.id || newPlaylist.uri) + ']'); // in VOD, this is always a rendition switch (or we updated our syncInfo above) 49398 // in LIVE, we always want to update with new playlists (including refreshes) 49399 49400 this.trigger('syncinfoupdate'); // if we were unpaused but waiting for a playlist, start 49401 // buffering now 49402 49403 if (this.state === 'INIT' && this.couldBeginLoading_()) { 49404 return this.init_(); 49405 } 49406 49407 if (!oldPlaylist || oldPlaylist.uri !== newPlaylist.uri) { 49408 if (this.mediaIndex !== null) { 49409 // we must "resync" the segment loader when we switch renditions and 49410 // the segment loader is already synced to the previous rendition 49411 this.resyncLoader(); 49412 } // the rest of this function depends on `oldPlaylist` being defined 49413 49414 49415 return; 49416 } // we reloaded the same playlist so we are in a live scenario 49417 // and we will likely need to adjust the mediaIndex 49418 49419 49420 var mediaSequenceDiff = newPlaylist.mediaSequence - oldPlaylist.mediaSequence; 49421 this.logger_('live window shift [' + mediaSequenceDiff + ']'); // update the mediaIndex on the SegmentLoader 49422 // this is important because we can abort a request and this value must be 49423 // equal to the last appended mediaIndex 49424 49425 if (this.mediaIndex !== null) { 49426 this.mediaIndex -= mediaSequenceDiff; 49427 } // update the mediaIndex on the SegmentInfo object 49428 // this is important because we will update this.mediaIndex with this value 49429 // in `handleUpdateEnd_` after the segment has been successfully appended 49430 49431 49432 if (segmentInfo) { 49433 segmentInfo.mediaIndex -= mediaSequenceDiff; // we need to update the referenced segment so that timing information is 49434 // saved for the new playlist's segment, however, if the segment fell off the 49435 // playlist, we can leave the old reference and just lose the timing info 49436 49437 if (segmentInfo.mediaIndex >= 0) { 49438 segmentInfo.segment = newPlaylist.segments[segmentInfo.mediaIndex]; 49439 } 49440 } 49441 49442 this.syncController_.saveExpiredSegmentInfo(oldPlaylist, newPlaylist); 49443 } 49444 /** 49445 * Prevent the loader from fetching additional segments. If there 49446 * is a segment request outstanding, it will finish processing 49447 * before the loader halts. A segment loader can be unpaused by 49448 * calling load(). 49449 */ 49450 49451 }, { 49452 key: 'pause', 49453 value: function pause() { 49454 if (this.checkBufferTimeout_) { 49455 window$1.clearTimeout(this.checkBufferTimeout_); 49456 this.checkBufferTimeout_ = null; 49457 } 49458 } 49459 /** 49460 * Returns whether the segment loader is fetching additional 49461 * segments when given the opportunity. This property can be 49462 * modified through calls to pause() and load(). 49463 */ 49464 49465 }, { 49466 key: 'paused', 49467 value: function paused() { 49468 return this.checkBufferTimeout_ === null; 49469 } 49470 /** 49471 * create/set the following mimetype on the SourceBuffer through a 49472 * SourceUpdater 49473 * 49474 * @param {String} mimeType the mime type string to use 49475 * @param {Object} sourceBufferEmitter an event emitter that fires when a source buffer 49476 * is added to the media source 49477 */ 49478 49479 }, { 49480 key: 'mimeType', 49481 value: function mimeType(_mimeType, sourceBufferEmitter) { 49482 if (this.mimeType_) { 49483 return; 49484 } 49485 49486 this.mimeType_ = _mimeType; 49487 this.sourceBufferEmitter_ = sourceBufferEmitter; // if we were unpaused but waiting for a sourceUpdater, start 49488 // buffering now 49489 49490 if (this.state === 'INIT' && this.couldBeginLoading_()) { 49491 this.init_(); 49492 } 49493 } 49494 /** 49495 * Delete all the buffered data and reset the SegmentLoader 49496 * @param {Function} [done] an optional callback to be executed when the remove 49497 * operation is complete 49498 */ 49499 49500 }, { 49501 key: 'resetEverything', 49502 value: function resetEverything(done) { 49503 this.ended_ = false; 49504 this.resetLoader(); 49505 this.remove(0, this.duration_(), done); // clears fmp4 captions 49506 49507 if (this.captionParser_) { 49508 this.captionParser_.clearAllCaptions(); 49509 } 49510 49511 this.trigger('reseteverything'); 49512 } 49513 /** 49514 * Force the SegmentLoader to resync and start loading around the currentTime instead 49515 * of starting at the end of the buffer 49516 * 49517 * Useful for fast quality changes 49518 */ 49519 49520 }, { 49521 key: 'resetLoader', 49522 value: function resetLoader() { 49523 this.fetchAtBuffer_ = false; 49524 this.resyncLoader(); 49525 } 49526 /** 49527 * Force the SegmentLoader to restart synchronization and make a conservative guess 49528 * before returning to the simple walk-forward method 49529 */ 49530 49531 }, { 49532 key: 'resyncLoader', 49533 value: function resyncLoader() { 49534 this.mediaIndex = null; 49535 this.syncPoint_ = null; 49536 this.abort(); 49537 } 49538 /** 49539 * Remove any data in the source buffer between start and end times 49540 * @param {Number} start - the start time of the region to remove from the buffer 49541 * @param {Number} end - the end time of the region to remove from the buffer 49542 * @param {Function} [done] - an optional callback to be executed when the remove 49543 * operation is complete 49544 */ 49545 49546 }, { 49547 key: 'remove', 49548 value: function remove(start, end, done) { 49549 if (this.sourceUpdater_) { 49550 this.sourceUpdater_.remove(start, end, done); 49551 } 49552 49553 removeCuesFromTrack(start, end, this.segmentMetadataTrack_); 49554 49555 if (this.inbandTextTracks_) { 49556 for (var id in this.inbandTextTracks_) { 49557 removeCuesFromTrack(start, end, this.inbandTextTracks_[id]); 49558 } 49559 } 49560 } 49561 /** 49562 * (re-)schedule monitorBufferTick_ to run as soon as possible 49563 * 49564 * @private 49565 */ 49566 49567 }, { 49568 key: 'monitorBuffer_', 49569 value: function monitorBuffer_() { 49570 if (this.checkBufferTimeout_) { 49571 window$1.clearTimeout(this.checkBufferTimeout_); 49572 } 49573 49574 this.checkBufferTimeout_ = window$1.setTimeout(this.monitorBufferTick_.bind(this), 1); 49575 } 49576 /** 49577 * As long as the SegmentLoader is in the READY state, periodically 49578 * invoke fillBuffer_(). 49579 * 49580 * @private 49581 */ 49582 49583 }, { 49584 key: 'monitorBufferTick_', 49585 value: function monitorBufferTick_() { 49586 if (this.state === 'READY') { 49587 this.fillBuffer_(); 49588 } 49589 49590 if (this.checkBufferTimeout_) { 49591 window$1.clearTimeout(this.checkBufferTimeout_); 49592 } 49593 49594 this.checkBufferTimeout_ = window$1.setTimeout(this.monitorBufferTick_.bind(this), CHECK_BUFFER_DELAY); 49595 } 49596 /** 49597 * fill the buffer with segements unless the sourceBuffers are 49598 * currently updating 49599 * 49600 * Note: this function should only ever be called by monitorBuffer_ 49601 * and never directly 49602 * 49603 * @private 49604 */ 49605 49606 }, { 49607 key: 'fillBuffer_', 49608 value: function fillBuffer_() { 49609 if (this.sourceUpdater_.updating()) { 49610 return; 49611 } 49612 49613 if (!this.syncPoint_) { 49614 this.syncPoint_ = this.syncController_.getSyncPoint(this.playlist_, this.duration_(), this.currentTimeline_, this.currentTime_()); 49615 } // see if we need to begin loading immediately 49616 49617 49618 var segmentInfo = this.checkBuffer_(this.buffered_(), this.playlist_, this.mediaIndex, this.hasPlayed_(), this.currentTime_(), this.syncPoint_); 49619 49620 if (!segmentInfo) { 49621 return; 49622 } 49623 49624 if (this.isEndOfStream_(segmentInfo.mediaIndex)) { 49625 this.endOfStream(); 49626 return; 49627 } 49628 49629 if (segmentInfo.mediaIndex === this.playlist_.segments.length - 1 && this.mediaSource_.readyState === 'ended' && !this.seeking_()) { 49630 return; 49631 } // We will need to change timestampOffset of the sourceBuffer if: 49632 // - The segment.timeline !== this.currentTimeline 49633 // (we are crossing a discontinuity somehow) 49634 // - The "timestampOffset" for the start of this segment is less than 49635 // the currently set timestampOffset 49636 // Also, clear captions if we are crossing a discontinuity boundary 49637 // Previously, we changed the timestampOffset if the start of this segment 49638 // is less than the currently set timestampOffset but this isn't wanted 49639 // as it can produce bad behavior, especially around long running 49640 // live streams 49641 49642 49643 if (segmentInfo.timeline !== this.currentTimeline_) { 49644 this.syncController_.reset(); 49645 segmentInfo.timestampOffset = segmentInfo.startOfSegment; 49646 49647 if (this.captionParser_) { 49648 this.captionParser_.clearAllCaptions(); 49649 } 49650 } 49651 49652 this.loadSegment_(segmentInfo); 49653 } 49654 /** 49655 * Determines if this segment loader is at the end of it's stream. 49656 * 49657 * @param {Number} mediaIndex the index of segment we last appended 49658 * @param {Object} [playlist=this.playlist_] a media playlist object 49659 * @returns {Boolean} true if at end of stream, false otherwise. 49660 */ 49661 49662 }, { 49663 key: 'isEndOfStream_', 49664 value: function isEndOfStream_(mediaIndex) { 49665 var playlist = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.playlist_; 49666 return detectEndOfStream(playlist, this.mediaSource_, mediaIndex) && !this.sourceUpdater_.updating(); 49667 } 49668 /** 49669 * Determines what segment request should be made, given current playback 49670 * state. 49671 * 49672 * @param {TimeRanges} buffered - the state of the buffer 49673 * @param {Object} playlist - the playlist object to fetch segments from 49674 * @param {Number} mediaIndex - the previous mediaIndex fetched or null 49675 * @param {Boolean} hasPlayed - a flag indicating whether we have played or not 49676 * @param {Number} currentTime - the playback position in seconds 49677 * @param {Object} syncPoint - a segment info object that describes the 49678 * @returns {Object} a segment request object that describes the segment to load 49679 */ 49680 49681 }, { 49682 key: 'checkBuffer_', 49683 value: function checkBuffer_(buffered, playlist, mediaIndex, hasPlayed, currentTime, syncPoint) { 49684 var lastBufferedEnd = 0; 49685 var startOfSegment = void 0; 49686 49687 if (buffered.length) { 49688 lastBufferedEnd = buffered.end(buffered.length - 1); 49689 } 49690 49691 var bufferedTime = Math.max(0, lastBufferedEnd - currentTime); 49692 49693 if (!playlist.segments.length) { 49694 return null; 49695 } // if there is plenty of content buffered, and the video has 49696 // been played before relax for awhile 49697 49698 49699 if (bufferedTime >= this.goalBufferLength_()) { 49700 return null; 49701 } // if the video has not yet played once, and we already have 49702 // one segment downloaded do nothing 49703 49704 49705 if (!hasPlayed && bufferedTime >= 1) { 49706 return null; 49707 } // When the syncPoint is null, there is no way of determining a good 49708 // conservative segment index to fetch from 49709 // The best thing to do here is to get the kind of sync-point data by 49710 // making a request 49711 49712 49713 if (syncPoint === null) { 49714 mediaIndex = this.getSyncSegmentCandidate_(playlist); 49715 return this.generateSegmentInfo_(playlist, mediaIndex, null, true); 49716 } // Under normal playback conditions fetching is a simple walk forward 49717 49718 49719 if (mediaIndex !== null) { 49720 var segment = playlist.segments[mediaIndex]; 49721 startOfSegment = lastBufferedEnd; 49722 return this.generateSegmentInfo_(playlist, mediaIndex + 1, startOfSegment, false); 49723 } // There is a sync-point but the lack of a mediaIndex indicates that 49724 // we need to make a good conservative guess about which segment to 49725 // fetch 49726 49727 49728 if (this.fetchAtBuffer_) { 49729 // Find the segment containing the end of the buffer 49730 var mediaSourceInfo = Playlist.getMediaInfoForTime(playlist, lastBufferedEnd, syncPoint.segmentIndex, syncPoint.time); 49731 mediaIndex = mediaSourceInfo.mediaIndex; 49732 startOfSegment = mediaSourceInfo.startTime; 49733 } else { 49734 // Find the segment containing currentTime 49735 var _mediaSourceInfo = Playlist.getMediaInfoForTime(playlist, currentTime, syncPoint.segmentIndex, syncPoint.time); 49736 49737 mediaIndex = _mediaSourceInfo.mediaIndex; 49738 startOfSegment = _mediaSourceInfo.startTime; 49739 } 49740 49741 return this.generateSegmentInfo_(playlist, mediaIndex, startOfSegment, false); 49742 } 49743 /** 49744 * The segment loader has no recourse except to fetch a segment in the 49745 * current playlist and use the internal timestamps in that segment to 49746 * generate a syncPoint. This function returns a good candidate index 49747 * for that process. 49748 * 49749 * @param {Object} playlist - the playlist object to look for a 49750 * @returns {Number} An index of a segment from the playlist to load 49751 */ 49752 49753 }, { 49754 key: 'getSyncSegmentCandidate_', 49755 value: function getSyncSegmentCandidate_(playlist) { 49756 var _this2 = this; 49757 49758 if (this.currentTimeline_ === -1) { 49759 return 0; 49760 } 49761 49762 var segmentIndexArray = playlist.segments.map(function (s, i) { 49763 return { 49764 timeline: s.timeline, 49765 segmentIndex: i 49766 }; 49767 }).filter(function (s) { 49768 return s.timeline === _this2.currentTimeline_; 49769 }); 49770 49771 if (segmentIndexArray.length) { 49772 return segmentIndexArray[Math.min(segmentIndexArray.length - 1, 1)].segmentIndex; 49773 } 49774 49775 return Math.max(playlist.segments.length - 1, 0); 49776 } 49777 }, { 49778 key: 'generateSegmentInfo_', 49779 value: function generateSegmentInfo_(playlist, mediaIndex, startOfSegment, isSyncRequest) { 49780 if (mediaIndex < 0 || mediaIndex >= playlist.segments.length) { 49781 return null; 49782 } 49783 49784 var segment = playlist.segments[mediaIndex]; 49785 return { 49786 requestId: 'segment-loader-' + Math.random(), 49787 // resolve the segment URL relative to the playlist 49788 uri: segment.resolvedUri, 49789 // the segment's mediaIndex at the time it was requested 49790 mediaIndex: mediaIndex, 49791 // whether or not to update the SegmentLoader's state with this 49792 // segment's mediaIndex 49793 isSyncRequest: isSyncRequest, 49794 startOfSegment: startOfSegment, 49795 // the segment's playlist 49796 playlist: playlist, 49797 // unencrypted bytes of the segment 49798 bytes: null, 49799 // when a key is defined for this segment, the encrypted bytes 49800 encryptedBytes: null, 49801 // The target timestampOffset for this segment when we append it 49802 // to the source buffer 49803 timestampOffset: null, 49804 // The timeline that the segment is in 49805 timeline: segment.timeline, 49806 // The expected duration of the segment in seconds 49807 duration: segment.duration, 49808 // retain the segment in case the playlist updates while doing an async process 49809 segment: segment 49810 }; 49811 } 49812 /** 49813 * Determines if the network has enough bandwidth to complete the current segment 49814 * request in a timely manner. If not, the request will be aborted early and bandwidth 49815 * updated to trigger a playlist switch. 49816 * 49817 * @param {Object} stats 49818 * Object containing stats about the request timing and size 49819 * @return {Boolean} True if the request was aborted, false otherwise 49820 * @private 49821 */ 49822 49823 }, { 49824 key: 'abortRequestEarly_', 49825 value: function abortRequestEarly_(stats) { 49826 if (this.hls_.tech_.paused() || // Don't abort if the current playlist is on the lowestEnabledRendition 49827 // TODO: Replace using timeout with a boolean indicating whether this playlist is 49828 // the lowestEnabledRendition. 49829 !this.xhrOptions_.timeout || // Don't abort if we have no bandwidth information to estimate segment sizes 49830 !this.playlist_.attributes.BANDWIDTH) { 49831 return false; 49832 } // Wait at least 1 second since the first byte of data has been received before 49833 // using the calculated bandwidth from the progress event to allow the bitrate 49834 // to stabilize 49835 49836 49837 if (Date.now() - (stats.firstBytesReceivedAt || Date.now()) < 1000) { 49838 return false; 49839 } 49840 49841 var currentTime = this.currentTime_(); 49842 var measuredBandwidth = stats.bandwidth; 49843 var segmentDuration = this.pendingSegment_.duration; 49844 var requestTimeRemaining = Playlist.estimateSegmentRequestTime(segmentDuration, measuredBandwidth, this.playlist_, stats.bytesReceived); // Subtract 1 from the timeUntilRebuffer so we still consider an early abort 49845 // if we are only left with less than 1 second when the request completes. 49846 // A negative timeUntilRebuffering indicates we are already rebuffering 49847 49848 var timeUntilRebuffer$$1 = timeUntilRebuffer(this.buffered_(), currentTime, this.hls_.tech_.playbackRate()) - 1; // Only consider aborting early if the estimated time to finish the download 49849 // is larger than the estimated time until the player runs out of forward buffer 49850 49851 if (requestTimeRemaining <= timeUntilRebuffer$$1) { 49852 return false; 49853 } 49854 49855 var switchCandidate = minRebufferMaxBandwidthSelector({ 49856 master: this.hls_.playlists.master, 49857 currentTime: currentTime, 49858 bandwidth: measuredBandwidth, 49859 duration: this.duration_(), 49860 segmentDuration: segmentDuration, 49861 timeUntilRebuffer: timeUntilRebuffer$$1, 49862 currentTimeline: this.currentTimeline_, 49863 syncController: this.syncController_ 49864 }); 49865 49866 if (!switchCandidate) { 49867 return; 49868 } 49869 49870 var rebufferingImpact = requestTimeRemaining - timeUntilRebuffer$$1; 49871 var timeSavedBySwitching = rebufferingImpact - switchCandidate.rebufferingImpact; 49872 var minimumTimeSaving = 0.5; // If we are already rebuffering, increase the amount of variance we add to the 49873 // potential round trip time of the new request so that we are not too aggressive 49874 // with switching to a playlist that might save us a fraction of a second. 49875 49876 if (timeUntilRebuffer$$1 <= TIME_FUDGE_FACTOR) { 49877 minimumTimeSaving = 1; 49878 } 49879 49880 if (!switchCandidate.playlist || switchCandidate.playlist.uri === this.playlist_.uri || timeSavedBySwitching < minimumTimeSaving) { 49881 return false; 49882 } // set the bandwidth to that of the desired playlist being sure to scale by 49883 // BANDWIDTH_VARIANCE and add one so the playlist selector does not exclude it 49884 // don't trigger a bandwidthupdate as the bandwidth is artifial 49885 49886 49887 this.bandwidth = switchCandidate.playlist.attributes.BANDWIDTH * Config.BANDWIDTH_VARIANCE + 1; 49888 this.abort(); 49889 this.trigger('earlyabort'); 49890 return true; 49891 } 49892 /** 49893 * XHR `progress` event handler 49894 * 49895 * @param {Event} 49896 * The XHR `progress` event 49897 * @param {Object} simpleSegment 49898 * A simplified segment object copy 49899 * @private 49900 */ 49901 49902 }, { 49903 key: 'handleProgress_', 49904 value: function handleProgress_(event, simpleSegment) { 49905 if (!this.pendingSegment_ || simpleSegment.requestId !== this.pendingSegment_.requestId || this.abortRequestEarly_(simpleSegment.stats)) { 49906 return; 49907 } 49908 49909 this.trigger('progress'); 49910 } 49911 /** 49912 * load a specific segment from a request into the buffer 49913 * 49914 * @private 49915 */ 49916 49917 }, { 49918 key: 'loadSegment_', 49919 value: function loadSegment_(segmentInfo) { 49920 this.state = 'WAITING'; 49921 this.pendingSegment_ = segmentInfo; 49922 this.trimBackBuffer_(segmentInfo); 49923 segmentInfo.abortRequests = mediaSegmentRequest(this.hls_.xhr, this.xhrOptions_, this.decrypter_, this.captionParser_, this.createSimplifiedSegmentObj_(segmentInfo), // progress callback 49924 this.handleProgress_.bind(this), this.segmentRequestFinished_.bind(this)); 49925 } 49926 /** 49927 * trim the back buffer so that we don't have too much data 49928 * in the source buffer 49929 * 49930 * @private 49931 * 49932 * @param {Object} segmentInfo - the current segment 49933 */ 49934 49935 }, { 49936 key: 'trimBackBuffer_', 49937 value: function trimBackBuffer_(segmentInfo) { 49938 var removeToTime = safeBackBufferTrimTime(this.seekable_(), this.currentTime_(), this.playlist_.targetDuration || 10); // Chrome has a hard limit of 150MB of 49939 // buffer and a very conservative "garbage collector" 49940 // We manually clear out the old buffer to ensure 49941 // we don't trigger the QuotaExceeded error 49942 // on the source buffer during subsequent appends 49943 49944 if (removeToTime > 0) { 49945 this.remove(0, removeToTime); 49946 } 49947 } 49948 /** 49949 * created a simplified copy of the segment object with just the 49950 * information necessary to perform the XHR and decryption 49951 * 49952 * @private 49953 * 49954 * @param {Object} segmentInfo - the current segment 49955 * @returns {Object} a simplified segment object copy 49956 */ 49957 49958 }, { 49959 key: 'createSimplifiedSegmentObj_', 49960 value: function createSimplifiedSegmentObj_(segmentInfo) { 49961 var segment = segmentInfo.segment; 49962 var simpleSegment = { 49963 resolvedUri: segment.resolvedUri, 49964 byterange: segment.byterange, 49965 requestId: segmentInfo.requestId 49966 }; 49967 49968 if (segment.key) { 49969 // if the media sequence is greater than 2^32, the IV will be incorrect 49970 // assuming 10s segments, that would be about 1300 years 49971 var iv = segment.key.iv || new Uint32Array([0, 0, 0, segmentInfo.mediaIndex + segmentInfo.playlist.mediaSequence]); 49972 simpleSegment.key = this.segmentKey(segment.key); 49973 simpleSegment.key.iv = iv; 49974 } 49975 49976 if (segment.map) { 49977 simpleSegment.map = this.initSegment(segment.map); 49978 } 49979 49980 return simpleSegment; 49981 } 49982 /** 49983 * Handle the callback from the segmentRequest function and set the 49984 * associated SegmentLoader state and errors if necessary 49985 * 49986 * @private 49987 */ 49988 49989 }, { 49990 key: 'segmentRequestFinished_', 49991 value: function segmentRequestFinished_(error, simpleSegment) { 49992 // every request counts as a media request even if it has been aborted 49993 // or canceled due to a timeout 49994 this.mediaRequests += 1; 49995 49996 if (simpleSegment.stats) { 49997 this.mediaBytesTransferred += simpleSegment.stats.bytesReceived; 49998 this.mediaTransferDuration += simpleSegment.stats.roundTripTime; 49999 } // The request was aborted and the SegmentLoader has already been reset 50000 50001 50002 if (!this.pendingSegment_) { 50003 this.mediaRequestsAborted += 1; 50004 return; 50005 } // the request was aborted and the SegmentLoader has already started 50006 // another request. this can happen when the timeout for an aborted 50007 // request triggers due to a limitation in the XHR library 50008 // do not count this as any sort of request or we risk double-counting 50009 50010 50011 if (simpleSegment.requestId !== this.pendingSegment_.requestId) { 50012 return; 50013 } // an error occurred from the active pendingSegment_ so reset everything 50014 50015 50016 if (error) { 50017 this.pendingSegment_ = null; 50018 this.state = 'READY'; // the requests were aborted just record the aborted stat and exit 50019 // this is not a true error condition and nothing corrective needs 50020 // to be done 50021 50022 if (error.code === REQUEST_ERRORS.ABORTED) { 50023 this.mediaRequestsAborted += 1; 50024 return; 50025 } 50026 50027 this.pause(); // the error is really just that at least one of the requests timed-out 50028 // set the bandwidth to a very low value and trigger an ABR switch to 50029 // take emergency action 50030 50031 if (error.code === REQUEST_ERRORS.TIMEOUT) { 50032 this.mediaRequestsTimedout += 1; 50033 this.bandwidth = 1; 50034 this.roundTrip = NaN; 50035 this.trigger('bandwidthupdate'); 50036 return; 50037 } // if control-flow has arrived here, then the error is real 50038 // emit an error event to blacklist the current playlist 50039 50040 50041 this.mediaRequestsErrored += 1; 50042 this.error(error); 50043 this.trigger('error'); 50044 return; 50045 } // the response was a success so set any bandwidth stats the request 50046 // generated for ABR purposes 50047 50048 50049 this.bandwidth = simpleSegment.stats.bandwidth; 50050 this.roundTrip = simpleSegment.stats.roundTripTime; // if this request included an initialization segment, save that data 50051 // to the initSegment cache 50052 50053 if (simpleSegment.map) { 50054 simpleSegment.map = this.initSegment(simpleSegment.map, true); 50055 } // if this request included a segment key, save that data in the cache 50056 50057 50058 if (simpleSegment.key) { 50059 this.segmentKey(simpleSegment.key, true); 50060 } 50061 50062 this.processSegmentResponse_(simpleSegment); 50063 } 50064 /** 50065 * Move any important data from the simplified segment object 50066 * back to the real segment object for future phases 50067 * 50068 * @private 50069 */ 50070 50071 }, { 50072 key: 'processSegmentResponse_', 50073 value: function processSegmentResponse_(simpleSegment) { 50074 var segmentInfo = this.pendingSegment_; 50075 segmentInfo.bytes = simpleSegment.bytes; 50076 50077 if (simpleSegment.map) { 50078 segmentInfo.segment.map.bytes = simpleSegment.map.bytes; 50079 } 50080 50081 segmentInfo.endOfAllRequests = simpleSegment.endOfAllRequests; // This has fmp4 captions, add them to text tracks 50082 50083 if (simpleSegment.fmp4Captions) { 50084 createCaptionsTrackIfNotExists(this.inbandTextTracks_, this.hls_.tech_, simpleSegment.captionStreams); 50085 addCaptionData({ 50086 inbandTextTracks: this.inbandTextTracks_, 50087 captionArray: simpleSegment.fmp4Captions, 50088 // fmp4s will not have a timestamp offset 50089 timestampOffset: 0 50090 }); // Reset stored captions since we added parsed 50091 // captions to a text track at this point 50092 50093 if (this.captionParser_) { 50094 this.captionParser_.clearParsedCaptions(); 50095 } 50096 } 50097 50098 this.handleSegment_(); 50099 } 50100 /** 50101 * append a decrypted segement to the SourceBuffer through a SourceUpdater 50102 * 50103 * @private 50104 */ 50105 50106 }, { 50107 key: 'handleSegment_', 50108 value: function handleSegment_() { 50109 var _this3 = this; 50110 50111 if (!this.pendingSegment_) { 50112 this.state = 'READY'; 50113 return; 50114 } 50115 50116 var segmentInfo = this.pendingSegment_; 50117 var segment = segmentInfo.segment; 50118 var timingInfo = this.syncController_.probeSegmentInfo(segmentInfo); // When we have our first timing info, determine what media types this loader is 50119 // dealing with. Although we're maintaining extra state, it helps to preserve the 50120 // separation of segment loader from the actual source buffers. 50121 50122 if (typeof this.startingMedia_ === 'undefined' && timingInfo && ( // Guard against cases where we're not getting timing info at all until we are 50123 // certain that all streams will provide it. 50124 timingInfo.containsAudio || timingInfo.containsVideo)) { 50125 this.startingMedia_ = { 50126 containsAudio: timingInfo.containsAudio, 50127 containsVideo: timingInfo.containsVideo 50128 }; 50129 } 50130 50131 var illegalMediaSwitchError = illegalMediaSwitch(this.loaderType_, this.startingMedia_, timingInfo); 50132 50133 if (illegalMediaSwitchError) { 50134 this.error({ 50135 message: illegalMediaSwitchError, 50136 blacklistDuration: Infinity 50137 }); 50138 this.trigger('error'); 50139 return; 50140 } 50141 50142 if (segmentInfo.isSyncRequest) { 50143 this.trigger('syncinfoupdate'); 50144 this.pendingSegment_ = null; 50145 this.state = 'READY'; 50146 return; 50147 } 50148 50149 if (segmentInfo.timestampOffset !== null && segmentInfo.timestampOffset !== this.sourceUpdater_.timestampOffset()) { 50150 // Subtract any difference between the PTS and DTS times of the first frame 50151 // from the timeStampOffset (which currently equals the buffered.end) to prevent 50152 // creating any gaps in the buffer 50153 if (timingInfo && timingInfo.segmentTimestampInfo) { 50154 var ptsStartTime = timingInfo.segmentTimestampInfo[0].ptsTime; 50155 var dtsStartTime = timingInfo.segmentTimestampInfo[0].dtsTime; 50156 segmentInfo.timestampOffset -= ptsStartTime - dtsStartTime; 50157 } 50158 50159 this.sourceUpdater_.timestampOffset(segmentInfo.timestampOffset); // fired when a timestamp offset is set in HLS (can also identify discontinuities) 50160 50161 this.trigger('timestampoffset'); 50162 } 50163 50164 var timelineMapping = this.syncController_.mappingForTimeline(segmentInfo.timeline); 50165 50166 if (timelineMapping !== null) { 50167 this.trigger({ 50168 type: 'segmenttimemapping', 50169 mapping: timelineMapping 50170 }); 50171 } 50172 50173 this.state = 'APPENDING'; // if the media initialization segment is changing, append it 50174 // before the content segment 50175 50176 if (segment.map) { 50177 var initId = initSegmentId(segment.map); 50178 50179 if (!this.activeInitSegmentId_ || this.activeInitSegmentId_ !== initId) { 50180 var initSegment = this.initSegment(segment.map); 50181 this.sourceUpdater_.appendBuffer({ 50182 bytes: initSegment.bytes 50183 }, function () { 50184 _this3.activeInitSegmentId_ = initId; 50185 }); 50186 } 50187 } 50188 50189 segmentInfo.byteLength = segmentInfo.bytes.byteLength; 50190 50191 if (typeof segment.start === 'number' && typeof segment.end === 'number') { 50192 this.mediaSecondsLoaded += segment.end - segment.start; 50193 } else { 50194 this.mediaSecondsLoaded += segment.duration; 50195 } 50196 50197 this.logger_(segmentInfoString(segmentInfo)); 50198 this.sourceUpdater_.appendBuffer({ 50199 bytes: segmentInfo.bytes, 50200 videoSegmentTimingInfoCallback: this.handleVideoSegmentTimingInfo_.bind(this, segmentInfo.requestId) 50201 }, this.handleUpdateEnd_.bind(this)); 50202 } 50203 }, { 50204 key: 'handleVideoSegmentTimingInfo_', 50205 value: function handleVideoSegmentTimingInfo_(requestId, event) { 50206 if (!this.pendingSegment_ || requestId !== this.pendingSegment_.requestId) { 50207 return; 50208 } 50209 50210 var segment = this.pendingSegment_.segment; 50211 50212 if (!segment.videoTimingInfo) { 50213 segment.videoTimingInfo = {}; 50214 } 50215 50216 segment.videoTimingInfo.transmuxerPrependedSeconds = event.videoSegmentTimingInfo.prependedContentDuration || 0; 50217 segment.videoTimingInfo.transmuxedPresentationStart = event.videoSegmentTimingInfo.start.presentation; 50218 segment.videoTimingInfo.transmuxedPresentationEnd = event.videoSegmentTimingInfo.end.presentation; // mainly used as a reference for debugging 50219 50220 segment.videoTimingInfo.baseMediaDecodeTime = event.videoSegmentTimingInfo.baseMediaDecodeTime; 50221 } 50222 /** 50223 * callback to run when appendBuffer is finished. detects if we are 50224 * in a good state to do things with the data we got, or if we need 50225 * to wait for more 50226 * 50227 * @private 50228 */ 50229 50230 }, { 50231 key: 'handleUpdateEnd_', 50232 value: function handleUpdateEnd_() { 50233 if (!this.pendingSegment_) { 50234 this.state = 'READY'; 50235 50236 if (!this.paused()) { 50237 this.monitorBuffer_(); 50238 } 50239 50240 return; 50241 } 50242 50243 var segmentInfo = this.pendingSegment_; 50244 var segment = segmentInfo.segment; 50245 var isWalkingForward = this.mediaIndex !== null; 50246 this.pendingSegment_ = null; 50247 this.recordThroughput_(segmentInfo); 50248 this.addSegmentMetadataCue_(segmentInfo); 50249 this.state = 'READY'; 50250 this.mediaIndex = segmentInfo.mediaIndex; 50251 this.fetchAtBuffer_ = true; 50252 this.currentTimeline_ = segmentInfo.timeline; // We must update the syncinfo to recalculate the seekable range before 50253 // the following conditional otherwise it may consider this a bad "guess" 50254 // and attempt to resync when the post-update seekable window and live 50255 // point would mean that this was the perfect segment to fetch 50256 50257 this.trigger('syncinfoupdate'); // If we previously appended a segment that ends more than 3 targetDurations before 50258 // the currentTime_ that means that our conservative guess was too conservative. 50259 // In that case, reset the loader state so that we try to use any information gained 50260 // from the previous request to create a new, more accurate, sync-point. 50261 50262 if (segment.end && this.currentTime_() - segment.end > segmentInfo.playlist.targetDuration * 3) { 50263 this.resetEverything(); 50264 return; 50265 } // Don't do a rendition switch unless we have enough time to get a sync segment 50266 // and conservatively guess 50267 50268 50269 if (isWalkingForward) { 50270 this.trigger('bandwidthupdate'); 50271 } 50272 50273 this.trigger('progress'); // any time an update finishes and the last segment is in the 50274 // buffer, end the stream. this ensures the "ended" event will 50275 // fire if playback reaches that point. 50276 50277 if (this.isEndOfStream_(segmentInfo.mediaIndex + 1, segmentInfo.playlist)) { 50278 this.endOfStream(); 50279 } 50280 50281 if (!this.paused()) { 50282 this.monitorBuffer_(); 50283 } 50284 } 50285 /** 50286 * Records the current throughput of the decrypt, transmux, and append 50287 * portion of the semgment pipeline. `throughput.rate` is a the cumulative 50288 * moving average of the throughput. `throughput.count` is the number of 50289 * data points in the average. 50290 * 50291 * @private 50292 * @param {Object} segmentInfo the object returned by loadSegment 50293 */ 50294 50295 }, { 50296 key: 'recordThroughput_', 50297 value: function recordThroughput_(segmentInfo) { 50298 var rate = this.throughput.rate; // Add one to the time to ensure that we don't accidentally attempt to divide 50299 // by zero in the case where the throughput is ridiculously high 50300 50301 var segmentProcessingTime = Date.now() - segmentInfo.endOfAllRequests + 1; // Multiply by 8000 to convert from bytes/millisecond to bits/second 50302 50303 var segmentProcessingThroughput = Math.floor(segmentInfo.byteLength / segmentProcessingTime * 8 * 1000); // This is just a cumulative moving average calculation: 50304 // newAvg = oldAvg + (sample - oldAvg) / (sampleCount + 1) 50305 50306 this.throughput.rate += (segmentProcessingThroughput - rate) / ++this.throughput.count; 50307 } 50308 /** 50309 * Adds a cue to the segment-metadata track with some metadata information about the 50310 * segment 50311 * 50312 * @private 50313 * @param {Object} segmentInfo 50314 * the object returned by loadSegment 50315 * @method addSegmentMetadataCue_ 50316 */ 50317 50318 }, { 50319 key: 'addSegmentMetadataCue_', 50320 value: function addSegmentMetadataCue_(segmentInfo) { 50321 if (!this.segmentMetadataTrack_) { 50322 return; 50323 } 50324 50325 var segment = segmentInfo.segment; 50326 var start = segment.start; 50327 var end = segment.end; // Do not try adding the cue if the start and end times are invalid. 50328 50329 if (!finite(start) || !finite(end)) { 50330 return; 50331 } 50332 50333 removeCuesFromTrack(start, end, this.segmentMetadataTrack_); 50334 var Cue = window$1.WebKitDataCue || window$1.VTTCue; 50335 var value = { 50336 custom: segment.custom, 50337 dateTimeObject: segment.dateTimeObject, 50338 dateTimeString: segment.dateTimeString, 50339 bandwidth: segmentInfo.playlist.attributes.BANDWIDTH, 50340 resolution: segmentInfo.playlist.attributes.RESOLUTION, 50341 codecs: segmentInfo.playlist.attributes.CODECS, 50342 byteLength: segmentInfo.byteLength, 50343 uri: segmentInfo.uri, 50344 timeline: segmentInfo.timeline, 50345 playlist: segmentInfo.playlist.uri, 50346 start: start, 50347 end: end 50348 }; 50349 var data = JSON.stringify(value); 50350 var cue = new Cue(start, end, data); // Attach the metadata to the value property of the cue to keep consistency between 50351 // the differences of WebKitDataCue in safari and VTTCue in other browsers 50352 50353 cue.value = value; 50354 this.segmentMetadataTrack_.addCue(cue); 50355 } 50356 }]); 50357 return SegmentLoader; 50358 }(videojs$1.EventTarget); 50359 50360 var uint8ToUtf8 = function uint8ToUtf8(uintArray) { 50361 return decodeURIComponent(escape(String.fromCharCode.apply(null, uintArray))); 50362 }; 50363 /** 50364 * @file vtt-segment-loader.js 50365 */ 50366 50367 50368 var VTT_LINE_TERMINATORS = new Uint8Array('\n\n'.split('').map(function (_char2) { 50369 return _char2.charCodeAt(0); 50370 })); 50371 /** 50372 * An object that manages segment loading and appending. 50373 * 50374 * @class VTTSegmentLoader 50375 * @param {Object} options required and optional options 50376 * @extends videojs.EventTarget 50377 */ 50378 50379 var VTTSegmentLoader = function (_SegmentLoader) { 50380 inherits$2(VTTSegmentLoader, _SegmentLoader); 50381 50382 function VTTSegmentLoader(settings) { 50383 var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 50384 classCallCheck$1(this, VTTSegmentLoader); // SegmentLoader requires a MediaSource be specified or it will throw an error; 50385 // however, VTTSegmentLoader has no need of a media source, so delete the reference 50386 50387 var _this = possibleConstructorReturn$1(this, (VTTSegmentLoader.__proto__ || Object.getPrototypeOf(VTTSegmentLoader)).call(this, settings, options)); 50388 50389 _this.mediaSource_ = null; 50390 _this.subtitlesTrack_ = null; 50391 return _this; 50392 } 50393 /** 50394 * Indicates which time ranges are buffered 50395 * 50396 * @return {TimeRange} 50397 * TimeRange object representing the current buffered ranges 50398 */ 50399 50400 50401 createClass$1(VTTSegmentLoader, [{ 50402 key: 'buffered_', 50403 value: function buffered_() { 50404 if (!this.subtitlesTrack_ || !this.subtitlesTrack_.cues.length) { 50405 return videojs$1.createTimeRanges(); 50406 } 50407 50408 var cues = this.subtitlesTrack_.cues; 50409 var start = cues[0].startTime; 50410 var end = cues[cues.length - 1].startTime; 50411 return videojs$1.createTimeRanges([[start, end]]); 50412 } 50413 /** 50414 * Gets and sets init segment for the provided map 50415 * 50416 * @param {Object} map 50417 * The map object representing the init segment to get or set 50418 * @param {Boolean=} set 50419 * If true, the init segment for the provided map should be saved 50420 * @return {Object} 50421 * map object for desired init segment 50422 */ 50423 50424 }, { 50425 key: 'initSegment', 50426 value: function initSegment(map) { 50427 var set$$1 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; 50428 50429 if (!map) { 50430 return null; 50431 } 50432 50433 var id = initSegmentId(map); 50434 var storedMap = this.initSegments_[id]; 50435 50436 if (set$$1 && !storedMap && map.bytes) { 50437 // append WebVTT line terminators to the media initialization segment if it exists 50438 // to follow the WebVTT spec (https://w3c.github.io/webvtt/#file-structure) that 50439 // requires two or more WebVTT line terminators between the WebVTT header and the 50440 // rest of the file 50441 var combinedByteLength = VTT_LINE_TERMINATORS.byteLength + map.bytes.byteLength; 50442 var combinedSegment = new Uint8Array(combinedByteLength); 50443 combinedSegment.set(map.bytes); 50444 combinedSegment.set(VTT_LINE_TERMINATORS, map.bytes.byteLength); 50445 this.initSegments_[id] = storedMap = { 50446 resolvedUri: map.resolvedUri, 50447 byterange: map.byterange, 50448 bytes: combinedSegment 50449 }; 50450 } 50451 50452 return storedMap || map; 50453 } 50454 /** 50455 * Returns true if all configuration required for loading is present, otherwise false. 50456 * 50457 * @return {Boolean} True if the all configuration is ready for loading 50458 * @private 50459 */ 50460 50461 }, { 50462 key: 'couldBeginLoading_', 50463 value: function couldBeginLoading_() { 50464 return this.playlist_ && this.subtitlesTrack_ && !this.paused(); 50465 } 50466 /** 50467 * Once all the starting parameters have been specified, begin 50468 * operation. This method should only be invoked from the INIT 50469 * state. 50470 * 50471 * @private 50472 */ 50473 50474 }, { 50475 key: 'init_', 50476 value: function init_() { 50477 this.state = 'READY'; 50478 this.resetEverything(); 50479 return this.monitorBuffer_(); 50480 } 50481 /** 50482 * Set a subtitle track on the segment loader to add subtitles to 50483 * 50484 * @param {TextTrack=} track 50485 * The text track to add loaded subtitles to 50486 * @return {TextTrack} 50487 * Returns the subtitles track 50488 */ 50489 50490 }, { 50491 key: 'track', 50492 value: function track(_track) { 50493 if (typeof _track === 'undefined') { 50494 return this.subtitlesTrack_; 50495 } 50496 50497 this.subtitlesTrack_ = _track; // if we were unpaused but waiting for a sourceUpdater, start 50498 // buffering now 50499 50500 if (this.state === 'INIT' && this.couldBeginLoading_()) { 50501 this.init_(); 50502 } 50503 50504 return this.subtitlesTrack_; 50505 } 50506 /** 50507 * Remove any data in the source buffer between start and end times 50508 * @param {Number} start - the start time of the region to remove from the buffer 50509 * @param {Number} end - the end time of the region to remove from the buffer 50510 */ 50511 50512 }, { 50513 key: 'remove', 50514 value: function remove(start, end) { 50515 removeCuesFromTrack(start, end, this.subtitlesTrack_); 50516 } 50517 /** 50518 * fill the buffer with segements unless the sourceBuffers are 50519 * currently updating 50520 * 50521 * Note: this function should only ever be called by monitorBuffer_ 50522 * and never directly 50523 * 50524 * @private 50525 */ 50526 50527 }, { 50528 key: 'fillBuffer_', 50529 value: function fillBuffer_() { 50530 var _this2 = this; 50531 50532 if (!this.syncPoint_) { 50533 this.syncPoint_ = this.syncController_.getSyncPoint(this.playlist_, this.duration_(), this.currentTimeline_, this.currentTime_()); 50534 } // see if we need to begin loading immediately 50535 50536 50537 var segmentInfo = this.checkBuffer_(this.buffered_(), this.playlist_, this.mediaIndex, this.hasPlayed_(), this.currentTime_(), this.syncPoint_); 50538 segmentInfo = this.skipEmptySegments_(segmentInfo); 50539 50540 if (!segmentInfo) { 50541 return; 50542 } 50543 50544 if (this.syncController_.timestampOffsetForTimeline(segmentInfo.timeline) === null) { 50545 // We don't have the timestamp offset that we need to sync subtitles. 50546 // Rerun on a timestamp offset or user interaction. 50547 var checkTimestampOffset = function checkTimestampOffset() { 50548 _this2.state = 'READY'; 50549 50550 if (!_this2.paused()) { 50551 // if not paused, queue a buffer check as soon as possible 50552 _this2.monitorBuffer_(); 50553 } 50554 }; 50555 50556 this.syncController_.one('timestampoffset', checkTimestampOffset); 50557 this.state = 'WAITING_ON_TIMELINE'; 50558 return; 50559 } 50560 50561 this.loadSegment_(segmentInfo); 50562 } 50563 /** 50564 * Prevents the segment loader from requesting segments we know contain no subtitles 50565 * by walking forward until we find the next segment that we don't know whether it is 50566 * empty or not. 50567 * 50568 * @param {Object} segmentInfo 50569 * a segment info object that describes the current segment 50570 * @return {Object} 50571 * a segment info object that describes the current segment 50572 */ 50573 50574 }, { 50575 key: 'skipEmptySegments_', 50576 value: function skipEmptySegments_(segmentInfo) { 50577 while (segmentInfo && segmentInfo.segment.empty) { 50578 segmentInfo = this.generateSegmentInfo_(segmentInfo.playlist, segmentInfo.mediaIndex + 1, segmentInfo.startOfSegment + segmentInfo.duration, segmentInfo.isSyncRequest); 50579 } 50580 50581 return segmentInfo; 50582 } 50583 /** 50584 * append a decrypted segement to the SourceBuffer through a SourceUpdater 50585 * 50586 * @private 50587 */ 50588 50589 }, { 50590 key: 'handleSegment_', 50591 value: function handleSegment_() { 50592 var _this3 = this; 50593 50594 if (!this.pendingSegment_ || !this.subtitlesTrack_) { 50595 this.state = 'READY'; 50596 return; 50597 } 50598 50599 this.state = 'APPENDING'; 50600 var segmentInfo = this.pendingSegment_; 50601 var segment = segmentInfo.segment; // Make sure that vttjs has loaded, otherwise, wait till it finished loading 50602 50603 if (typeof window$1.WebVTT !== 'function' && this.subtitlesTrack_ && this.subtitlesTrack_.tech_) { 50604 var loadHandler = void 0; 50605 50606 var errorHandler = function errorHandler() { 50607 _this3.subtitlesTrack_.tech_.off('vttjsloaded', loadHandler); 50608 50609 _this3.error({ 50610 message: 'Error loading vtt.js' 50611 }); 50612 50613 _this3.state = 'READY'; 50614 50615 _this3.pause(); 50616 50617 _this3.trigger('error'); 50618 }; 50619 50620 loadHandler = function loadHandler() { 50621 _this3.subtitlesTrack_.tech_.off('vttjserror', errorHandler); 50622 50623 _this3.handleSegment_(); 50624 }; 50625 50626 this.state = 'WAITING_ON_VTTJS'; 50627 this.subtitlesTrack_.tech_.one('vttjsloaded', loadHandler); 50628 this.subtitlesTrack_.tech_.one('vttjserror', errorHandler); 50629 return; 50630 } 50631 50632 segment.requested = true; 50633 50634 try { 50635 this.parseVTTCues_(segmentInfo); 50636 } catch (e) { 50637 this.error({ 50638 message: e.message 50639 }); 50640 this.state = 'READY'; 50641 this.pause(); 50642 return this.trigger('error'); 50643 } 50644 50645 this.updateTimeMapping_(segmentInfo, this.syncController_.timelines[segmentInfo.timeline], this.playlist_); 50646 50647 if (segmentInfo.isSyncRequest) { 50648 this.trigger('syncinfoupdate'); 50649 this.pendingSegment_ = null; 50650 this.state = 'READY'; 50651 return; 50652 } 50653 50654 segmentInfo.byteLength = segmentInfo.bytes.byteLength; 50655 this.mediaSecondsLoaded += segment.duration; 50656 50657 if (segmentInfo.cues.length) { 50658 // remove any overlapping cues to prevent doubling 50659 this.remove(segmentInfo.cues[0].endTime, segmentInfo.cues[segmentInfo.cues.length - 1].endTime); 50660 } 50661 50662 segmentInfo.cues.forEach(function (cue) { 50663 _this3.subtitlesTrack_.addCue(cue); 50664 }); 50665 this.handleUpdateEnd_(); 50666 } 50667 /** 50668 * Uses the WebVTT parser to parse the segment response 50669 * 50670 * @param {Object} segmentInfo 50671 * a segment info object that describes the current segment 50672 * @private 50673 */ 50674 50675 }, { 50676 key: 'parseVTTCues_', 50677 value: function parseVTTCues_(segmentInfo) { 50678 var decoder = void 0; 50679 var decodeBytesToString = false; 50680 50681 if (typeof window$1.TextDecoder === 'function') { 50682 decoder = new window$1.TextDecoder('utf8'); 50683 } else { 50684 decoder = window$1.WebVTT.StringDecoder(); 50685 decodeBytesToString = true; 50686 } 50687 50688 var parser = new window$1.WebVTT.Parser(window$1, window$1.vttjs, decoder); 50689 segmentInfo.cues = []; 50690 segmentInfo.timestampmap = { 50691 MPEGTS: 0, 50692 LOCAL: 0 50693 }; 50694 parser.oncue = segmentInfo.cues.push.bind(segmentInfo.cues); 50695 50696 parser.ontimestampmap = function (map) { 50697 return segmentInfo.timestampmap = map; 50698 }; 50699 50700 parser.onparsingerror = function (error) { 50701 videojs$1.log.warn('Error encountered when parsing cues: ' + error.message); 50702 }; 50703 50704 if (segmentInfo.segment.map) { 50705 var mapData = segmentInfo.segment.map.bytes; 50706 50707 if (decodeBytesToString) { 50708 mapData = uint8ToUtf8(mapData); 50709 } 50710 50711 parser.parse(mapData); 50712 } 50713 50714 var segmentData = segmentInfo.bytes; 50715 50716 if (decodeBytesToString) { 50717 segmentData = uint8ToUtf8(segmentData); 50718 } 50719 50720 parser.parse(segmentData); 50721 parser.flush(); 50722 } 50723 /** 50724 * Updates the start and end times of any cues parsed by the WebVTT parser using 50725 * the information parsed from the X-TIMESTAMP-MAP header and a TS to media time mapping 50726 * from the SyncController 50727 * 50728 * @param {Object} segmentInfo 50729 * a segment info object that describes the current segment 50730 * @param {Object} mappingObj 50731 * object containing a mapping from TS to media time 50732 * @param {Object} playlist 50733 * the playlist object containing the segment 50734 * @private 50735 */ 50736 50737 }, { 50738 key: 'updateTimeMapping_', 50739 value: function updateTimeMapping_(segmentInfo, mappingObj, playlist) { 50740 var segment = segmentInfo.segment; 50741 50742 if (!mappingObj) { 50743 // If the sync controller does not have a mapping of TS to Media Time for the 50744 // timeline, then we don't have enough information to update the cue 50745 // start/end times 50746 return; 50747 } 50748 50749 if (!segmentInfo.cues.length) { 50750 // If there are no cues, we also do not have enough information to figure out 50751 // segment timing. Mark that the segment contains no cues so we don't re-request 50752 // an empty segment. 50753 segment.empty = true; 50754 return; 50755 } 50756 50757 var timestampmap = segmentInfo.timestampmap; 50758 var diff = timestampmap.MPEGTS / 90000 - timestampmap.LOCAL + mappingObj.mapping; 50759 segmentInfo.cues.forEach(function (cue) { 50760 // First convert cue time to TS time using the timestamp-map provided within the vtt 50761 cue.startTime += diff; 50762 cue.endTime += diff; 50763 }); 50764 50765 if (!playlist.syncInfo) { 50766 var firstStart = segmentInfo.cues[0].startTime; 50767 var lastStart = segmentInfo.cues[segmentInfo.cues.length - 1].startTime; 50768 playlist.syncInfo = { 50769 mediaSequence: playlist.mediaSequence + segmentInfo.mediaIndex, 50770 time: Math.min(firstStart, lastStart - segment.duration) 50771 }; 50772 } 50773 } 50774 }]); 50775 return VTTSegmentLoader; 50776 }(SegmentLoader); 50777 /** 50778 * @file ad-cue-tags.js 50779 */ 50780 50781 /** 50782 * Searches for an ad cue that overlaps with the given mediaTime 50783 */ 50784 50785 50786 var findAdCue = function findAdCue(track, mediaTime) { 50787 var cues = track.cues; 50788 50789 for (var i = 0; i < cues.length; i++) { 50790 var cue = cues[i]; 50791 50792 if (mediaTime >= cue.adStartTime && mediaTime <= cue.adEndTime) { 50793 return cue; 50794 } 50795 } 50796 50797 return null; 50798 }; 50799 50800 var updateAdCues = function updateAdCues(media, track) { 50801 var offset = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; 50802 50803 if (!media.segments) { 50804 return; 50805 } 50806 50807 var mediaTime = offset; 50808 var cue = void 0; 50809 50810 for (var i = 0; i < media.segments.length; i++) { 50811 var segment = media.segments[i]; 50812 50813 if (!cue) { 50814 // Since the cues will span for at least the segment duration, adding a fudge 50815 // factor of half segment duration will prevent duplicate cues from being 50816 // created when timing info is not exact (e.g. cue start time initialized 50817 // at 10.006677, but next call mediaTime is 10.003332 ) 50818 cue = findAdCue(track, mediaTime + segment.duration / 2); 50819 } 50820 50821 if (cue) { 50822 if ('cueIn' in segment) { 50823 // Found a CUE-IN so end the cue 50824 cue.endTime = mediaTime; 50825 cue.adEndTime = mediaTime; 50826 mediaTime += segment.duration; 50827 cue = null; 50828 continue; 50829 } 50830 50831 if (mediaTime < cue.endTime) { 50832 // Already processed this mediaTime for this cue 50833 mediaTime += segment.duration; 50834 continue; 50835 } // otherwise extend cue until a CUE-IN is found 50836 50837 50838 cue.endTime += segment.duration; 50839 } else { 50840 if ('cueOut' in segment) { 50841 cue = new window$1.VTTCue(mediaTime, mediaTime + segment.duration, segment.cueOut); 50842 cue.adStartTime = mediaTime; // Assumes tag format to be 50843 // #EXT-X-CUE-OUT:30 50844 50845 cue.adEndTime = mediaTime + parseFloat(segment.cueOut); 50846 track.addCue(cue); 50847 } 50848 50849 if ('cueOutCont' in segment) { 50850 // Entered into the middle of an ad cue 50851 var adOffset = void 0; 50852 var adTotal = void 0; // Assumes tag formate to be 50853 // #EXT-X-CUE-OUT-CONT:10/30 50854 50855 var _segment$cueOutCont$s = segment.cueOutCont.split('/').map(parseFloat); 50856 50857 var _segment$cueOutCont$s2 = slicedToArray(_segment$cueOutCont$s, 2); 50858 50859 adOffset = _segment$cueOutCont$s2[0]; 50860 adTotal = _segment$cueOutCont$s2[1]; 50861 cue = new window$1.VTTCue(mediaTime, mediaTime + segment.duration, ''); 50862 cue.adStartTime = mediaTime - adOffset; 50863 cue.adEndTime = cue.adStartTime + adTotal; 50864 track.addCue(cue); 50865 } 50866 } 50867 50868 mediaTime += segment.duration; 50869 } 50870 }; 50871 /** 50872 * @file sync-controller.js 50873 */ 50874 50875 50876 var tsprobe = tsInspector.inspect; 50877 var syncPointStrategies = [// Stategy "VOD": Handle the VOD-case where the sync-point is *always* 50878 // the equivalence display-time 0 === segment-index 0 50879 { 50880 name: 'VOD', 50881 run: function run(syncController, playlist, duration$$1, currentTimeline, currentTime) { 50882 if (duration$$1 !== Infinity) { 50883 var syncPoint = { 50884 time: 0, 50885 segmentIndex: 0 50886 }; 50887 return syncPoint; 50888 } 50889 50890 return null; 50891 } 50892 }, // Stategy "ProgramDateTime": We have a program-date-time tag in this playlist 50893 { 50894 name: 'ProgramDateTime', 50895 run: function run(syncController, playlist, duration$$1, currentTimeline, currentTime) { 50896 if (!syncController.datetimeToDisplayTime) { 50897 return null; 50898 } 50899 50900 var segments = playlist.segments || []; 50901 var syncPoint = null; 50902 var lastDistance = null; 50903 currentTime = currentTime || 0; 50904 50905 for (var i = 0; i < segments.length; i++) { 50906 var segment = segments[i]; 50907 50908 if (segment.dateTimeObject) { 50909 var segmentTime = segment.dateTimeObject.getTime() / 1000; 50910 var segmentStart = segmentTime + syncController.datetimeToDisplayTime; 50911 var distance = Math.abs(currentTime - segmentStart); // Once the distance begins to increase, or if distance is 0, we have passed 50912 // currentTime and can stop looking for better candidates 50913 50914 if (lastDistance !== null && (distance === 0 || lastDistance < distance)) { 50915 break; 50916 } 50917 50918 lastDistance = distance; 50919 syncPoint = { 50920 time: segmentStart, 50921 segmentIndex: i 50922 }; 50923 } 50924 } 50925 50926 return syncPoint; 50927 } 50928 }, // Stategy "Segment": We have a known time mapping for a timeline and a 50929 // segment in the current timeline with timing data 50930 { 50931 name: 'Segment', 50932 run: function run(syncController, playlist, duration$$1, currentTimeline, currentTime) { 50933 var segments = playlist.segments || []; 50934 var syncPoint = null; 50935 var lastDistance = null; 50936 currentTime = currentTime || 0; 50937 50938 for (var i = 0; i < segments.length; i++) { 50939 var segment = segments[i]; 50940 50941 if (segment.timeline === currentTimeline && typeof segment.start !== 'undefined') { 50942 var distance = Math.abs(currentTime - segment.start); // Once the distance begins to increase, we have passed 50943 // currentTime and can stop looking for better candidates 50944 50945 if (lastDistance !== null && lastDistance < distance) { 50946 break; 50947 } 50948 50949 if (!syncPoint || lastDistance === null || lastDistance >= distance) { 50950 lastDistance = distance; 50951 syncPoint = { 50952 time: segment.start, 50953 segmentIndex: i 50954 }; 50955 } 50956 } 50957 } 50958 50959 return syncPoint; 50960 } 50961 }, // Stategy "Discontinuity": We have a discontinuity with a known 50962 // display-time 50963 { 50964 name: 'Discontinuity', 50965 run: function run(syncController, playlist, duration$$1, currentTimeline, currentTime) { 50966 var syncPoint = null; 50967 currentTime = currentTime || 0; 50968 50969 if (playlist.discontinuityStarts && playlist.discontinuityStarts.length) { 50970 var lastDistance = null; 50971 50972 for (var i = 0; i < playlist.discontinuityStarts.length; i++) { 50973 var segmentIndex = playlist.discontinuityStarts[i]; 50974 var discontinuity = playlist.discontinuitySequence + i + 1; 50975 var discontinuitySync = syncController.discontinuities[discontinuity]; 50976 50977 if (discontinuitySync) { 50978 var distance = Math.abs(currentTime - discontinuitySync.time); // Once the distance begins to increase, we have passed 50979 // currentTime and can stop looking for better candidates 50980 50981 if (lastDistance !== null && lastDistance < distance) { 50982 break; 50983 } 50984 50985 if (!syncPoint || lastDistance === null || lastDistance >= distance) { 50986 lastDistance = distance; 50987 syncPoint = { 50988 time: discontinuitySync.time, 50989 segmentIndex: segmentIndex 50990 }; 50991 } 50992 } 50993 } 50994 } 50995 50996 return syncPoint; 50997 } 50998 }, // Stategy "Playlist": We have a playlist with a known mapping of 50999 // segment index to display time 51000 { 51001 name: 'Playlist', 51002 run: function run(syncController, playlist, duration$$1, currentTimeline, currentTime) { 51003 if (playlist.syncInfo) { 51004 var syncPoint = { 51005 time: playlist.syncInfo.time, 51006 segmentIndex: playlist.syncInfo.mediaSequence - playlist.mediaSequence 51007 }; 51008 return syncPoint; 51009 } 51010 51011 return null; 51012 } 51013 }]; 51014 51015 var SyncController = function (_videojs$EventTarget) { 51016 inherits$2(SyncController, _videojs$EventTarget); 51017 51018 function SyncController() { 51019 classCallCheck$1(this, SyncController); // Segment Loader state variables... 51020 // ...for synching across variants 51021 51022 var _this = possibleConstructorReturn$1(this, (SyncController.__proto__ || Object.getPrototypeOf(SyncController)).call(this)); 51023 51024 _this.inspectCache_ = undefined; // ...for synching across variants 51025 51026 _this.timelines = []; 51027 _this.discontinuities = []; 51028 _this.datetimeToDisplayTime = null; 51029 _this.logger_ = logger('SyncController'); 51030 return _this; 51031 } 51032 /** 51033 * Find a sync-point for the playlist specified 51034 * 51035 * A sync-point is defined as a known mapping from display-time to 51036 * a segment-index in the current playlist. 51037 * 51038 * @param {Playlist} playlist 51039 * The playlist that needs a sync-point 51040 * @param {Number} duration 51041 * Duration of the MediaSource (Infinite if playing a live source) 51042 * @param {Number} currentTimeline 51043 * The last timeline from which a segment was loaded 51044 * @returns {Object} 51045 * A sync-point object 51046 */ 51047 51048 51049 createClass$1(SyncController, [{ 51050 key: 'getSyncPoint', 51051 value: function getSyncPoint(playlist, duration$$1, currentTimeline, currentTime) { 51052 var syncPoints = this.runStrategies_(playlist, duration$$1, currentTimeline, currentTime); 51053 51054 if (!syncPoints.length) { 51055 // Signal that we need to attempt to get a sync-point manually 51056 // by fetching a segment in the playlist and constructing 51057 // a sync-point from that information 51058 return null; 51059 } // Now find the sync-point that is closest to the currentTime because 51060 // that should result in the most accurate guess about which segment 51061 // to fetch 51062 51063 51064 return this.selectSyncPoint_(syncPoints, { 51065 key: 'time', 51066 value: currentTime 51067 }); 51068 } 51069 /** 51070 * Calculate the amount of time that has expired off the playlist during playback 51071 * 51072 * @param {Playlist} playlist 51073 * Playlist object to calculate expired from 51074 * @param {Number} duration 51075 * Duration of the MediaSource (Infinity if playling a live source) 51076 * @returns {Number|null} 51077 * The amount of time that has expired off the playlist during playback. Null 51078 * if no sync-points for the playlist can be found. 51079 */ 51080 51081 }, { 51082 key: 'getExpiredTime', 51083 value: function getExpiredTime(playlist, duration$$1) { 51084 if (!playlist || !playlist.segments) { 51085 return null; 51086 } 51087 51088 var syncPoints = this.runStrategies_(playlist, duration$$1, playlist.discontinuitySequence, 0); // Without sync-points, there is not enough information to determine the expired time 51089 51090 if (!syncPoints.length) { 51091 return null; 51092 } 51093 51094 var syncPoint = this.selectSyncPoint_(syncPoints, { 51095 key: 'segmentIndex', 51096 value: 0 51097 }); // If the sync-point is beyond the start of the playlist, we want to subtract the 51098 // duration from index 0 to syncPoint.segmentIndex instead of adding. 51099 51100 if (syncPoint.segmentIndex > 0) { 51101 syncPoint.time *= -1; 51102 } 51103 51104 return Math.abs(syncPoint.time + sumDurations(playlist, syncPoint.segmentIndex, 0)); 51105 } 51106 /** 51107 * Runs each sync-point strategy and returns a list of sync-points returned by the 51108 * strategies 51109 * 51110 * @private 51111 * @param {Playlist} playlist 51112 * The playlist that needs a sync-point 51113 * @param {Number} duration 51114 * Duration of the MediaSource (Infinity if playing a live source) 51115 * @param {Number} currentTimeline 51116 * The last timeline from which a segment was loaded 51117 * @returns {Array} 51118 * A list of sync-point objects 51119 */ 51120 51121 }, { 51122 key: 'runStrategies_', 51123 value: function runStrategies_(playlist, duration$$1, currentTimeline, currentTime) { 51124 var syncPoints = []; // Try to find a sync-point in by utilizing various strategies... 51125 51126 for (var i = 0; i < syncPointStrategies.length; i++) { 51127 var strategy = syncPointStrategies[i]; 51128 var syncPoint = strategy.run(this, playlist, duration$$1, currentTimeline, currentTime); 51129 51130 if (syncPoint) { 51131 syncPoint.strategy = strategy.name; 51132 syncPoints.push({ 51133 strategy: strategy.name, 51134 syncPoint: syncPoint 51135 }); 51136 } 51137 } 51138 51139 return syncPoints; 51140 } 51141 /** 51142 * Selects the sync-point nearest the specified target 51143 * 51144 * @private 51145 * @param {Array} syncPoints 51146 * List of sync-points to select from 51147 * @param {Object} target 51148 * Object specifying the property and value we are targeting 51149 * @param {String} target.key 51150 * Specifies the property to target. Must be either 'time' or 'segmentIndex' 51151 * @param {Number} target.value 51152 * The value to target for the specified key. 51153 * @returns {Object} 51154 * The sync-point nearest the target 51155 */ 51156 51157 }, { 51158 key: 'selectSyncPoint_', 51159 value: function selectSyncPoint_(syncPoints, target) { 51160 var bestSyncPoint = syncPoints[0].syncPoint; 51161 var bestDistance = Math.abs(syncPoints[0].syncPoint[target.key] - target.value); 51162 var bestStrategy = syncPoints[0].strategy; 51163 51164 for (var i = 1; i < syncPoints.length; i++) { 51165 var newDistance = Math.abs(syncPoints[i].syncPoint[target.key] - target.value); 51166 51167 if (newDistance < bestDistance) { 51168 bestDistance = newDistance; 51169 bestSyncPoint = syncPoints[i].syncPoint; 51170 bestStrategy = syncPoints[i].strategy; 51171 } 51172 } 51173 51174 this.logger_('syncPoint for [' + target.key + ': ' + target.value + '] chosen with strategy' + (' [' + bestStrategy + ']: [time:' + bestSyncPoint.time + ',') + (' segmentIndex:' + bestSyncPoint.segmentIndex + ']')); 51175 return bestSyncPoint; 51176 } 51177 /** 51178 * Save any meta-data present on the segments when segments leave 51179 * the live window to the playlist to allow for synchronization at the 51180 * playlist level later. 51181 * 51182 * @param {Playlist} oldPlaylist - The previous active playlist 51183 * @param {Playlist} newPlaylist - The updated and most current playlist 51184 */ 51185 51186 }, { 51187 key: 'saveExpiredSegmentInfo', 51188 value: function saveExpiredSegmentInfo(oldPlaylist, newPlaylist) { 51189 var mediaSequenceDiff = newPlaylist.mediaSequence - oldPlaylist.mediaSequence; // When a segment expires from the playlist and it has a start time 51190 // save that information as a possible sync-point reference in future 51191 51192 for (var i = mediaSequenceDiff - 1; i >= 0; i--) { 51193 var lastRemovedSegment = oldPlaylist.segments[i]; 51194 51195 if (lastRemovedSegment && typeof lastRemovedSegment.start !== 'undefined') { 51196 newPlaylist.syncInfo = { 51197 mediaSequence: oldPlaylist.mediaSequence + i, 51198 time: lastRemovedSegment.start 51199 }; 51200 this.logger_('playlist refresh sync: [time:' + newPlaylist.syncInfo.time + ',' + (' mediaSequence: ' + newPlaylist.syncInfo.mediaSequence + ']')); 51201 this.trigger('syncinfoupdate'); 51202 break; 51203 } 51204 } 51205 } 51206 /** 51207 * Save the mapping from playlist's ProgramDateTime to display. This should 51208 * only ever happen once at the start of playback. 51209 * 51210 * @param {Playlist} playlist - The currently active playlist 51211 */ 51212 51213 }, { 51214 key: 'setDateTimeMapping', 51215 value: function setDateTimeMapping(playlist) { 51216 if (!this.datetimeToDisplayTime && playlist.segments && playlist.segments.length && playlist.segments[0].dateTimeObject) { 51217 var playlistTimestamp = playlist.segments[0].dateTimeObject.getTime() / 1000; 51218 this.datetimeToDisplayTime = -playlistTimestamp; 51219 } 51220 } 51221 /** 51222 * Reset the state of the inspection cache when we do a rendition 51223 * switch 51224 */ 51225 51226 }, { 51227 key: 'reset', 51228 value: function reset() { 51229 this.inspectCache_ = undefined; 51230 } 51231 /** 51232 * Probe or inspect a fmp4 or an mpeg2-ts segment to determine the start 51233 * and end of the segment in it's internal "media time". Used to generate 51234 * mappings from that internal "media time" to the display time that is 51235 * shown on the player. 51236 * 51237 * @param {SegmentInfo} segmentInfo - The current active request information 51238 */ 51239 51240 }, { 51241 key: 'probeSegmentInfo', 51242 value: function probeSegmentInfo(segmentInfo) { 51243 var segment = segmentInfo.segment; 51244 var playlist = segmentInfo.playlist; 51245 var timingInfo = void 0; 51246 51247 if (segment.map) { 51248 timingInfo = this.probeMp4Segment_(segmentInfo); 51249 } else { 51250 timingInfo = this.probeTsSegment_(segmentInfo); 51251 } 51252 51253 if (timingInfo) { 51254 if (this.calculateSegmentTimeMapping_(segmentInfo, timingInfo)) { 51255 this.saveDiscontinuitySyncInfo_(segmentInfo); // If the playlist does not have sync information yet, record that information 51256 // now with segment timing information 51257 51258 if (!playlist.syncInfo) { 51259 playlist.syncInfo = { 51260 mediaSequence: playlist.mediaSequence + segmentInfo.mediaIndex, 51261 time: segment.start 51262 }; 51263 } 51264 } 51265 } 51266 51267 return timingInfo; 51268 } 51269 /** 51270 * Probe an fmp4 segment to determine the start of the segment 51271 * in it's internal "composition time", which is equal to the base 51272 * media decode time plus the composition time offset value 51273 * 51274 * @private 51275 * @param {SegmentInfo} segmentInfo - The current active request information 51276 * @return {object} The start and end time of the current segment in "composition time" 51277 */ 51278 51279 }, { 51280 key: 'probeMp4Segment_', 51281 value: function probeMp4Segment_(segmentInfo) { 51282 var segment = segmentInfo.segment; // get timescales from init segment 51283 51284 var timescales = probe.timescale(segment.map.bytes); // calculate composition start time using the timescales and information 51285 // contained within the media segment 51286 51287 var compositionStartTime = probe.compositionStartTime(timescales, segmentInfo.bytes); 51288 51289 if (segmentInfo.timestampOffset !== null) { 51290 segmentInfo.timestampOffset -= compositionStartTime; 51291 } 51292 51293 return { 51294 start: compositionStartTime, 51295 end: compositionStartTime + segment.duration 51296 }; 51297 } 51298 /** 51299 * Probe an mpeg2-ts segment to determine the start and end of the segment 51300 * in it's internal "media time". 51301 * 51302 * @private 51303 * @param {SegmentInfo} segmentInfo - The current active request information 51304 * @return {object} The start and end time of the current segment in "media time" 51305 */ 51306 51307 }, { 51308 key: 'probeTsSegment_', 51309 value: function probeTsSegment_(segmentInfo) { 51310 var timeInfo = tsprobe(segmentInfo.bytes, this.inspectCache_); 51311 var segmentStartTime = void 0; 51312 var segmentEndTime = void 0; 51313 var segmentTimestampInfo = void 0; 51314 51315 if (!timeInfo) { 51316 return null; 51317 } 51318 51319 if (timeInfo.video && timeInfo.video.length === 2) { 51320 this.inspectCache_ = timeInfo.video[1].dts; 51321 segmentStartTime = timeInfo.video[0].dtsTime; 51322 segmentEndTime = timeInfo.video[1].dtsTime; 51323 segmentTimestampInfo = timeInfo.video; 51324 } else if (timeInfo.audio && timeInfo.audio.length === 2) { 51325 this.inspectCache_ = timeInfo.audio[1].dts; 51326 segmentStartTime = timeInfo.audio[0].dtsTime; 51327 segmentEndTime = timeInfo.audio[1].dtsTime; 51328 segmentTimestampInfo = timeInfo.audio; 51329 } 51330 51331 var probedInfo = { 51332 segmentTimestampInfo: segmentTimestampInfo, 51333 start: segmentStartTime, 51334 end: segmentEndTime, 51335 containsVideo: timeInfo.video && timeInfo.video.length === 2, 51336 containsAudio: timeInfo.audio && timeInfo.audio.length === 2 51337 }; 51338 return probedInfo; 51339 } 51340 }, { 51341 key: 'timestampOffsetForTimeline', 51342 value: function timestampOffsetForTimeline(timeline) { 51343 if (typeof this.timelines[timeline] === 'undefined') { 51344 return null; 51345 } 51346 51347 return this.timelines[timeline].time; 51348 } 51349 }, { 51350 key: 'mappingForTimeline', 51351 value: function mappingForTimeline(timeline) { 51352 if (typeof this.timelines[timeline] === 'undefined') { 51353 return null; 51354 } 51355 51356 return this.timelines[timeline].mapping; 51357 } 51358 /** 51359 * Use the "media time" for a segment to generate a mapping to "display time" and 51360 * save that display time to the segment. 51361 * 51362 * @private 51363 * @param {SegmentInfo} segmentInfo 51364 * The current active request information 51365 * @param {object} timingInfo 51366 * The start and end time of the current segment in "media time" 51367 * @returns {Boolean} 51368 * Returns false if segment time mapping could not be calculated 51369 */ 51370 51371 }, { 51372 key: 'calculateSegmentTimeMapping_', 51373 value: function calculateSegmentTimeMapping_(segmentInfo, timingInfo) { 51374 var segment = segmentInfo.segment; 51375 var mappingObj = this.timelines[segmentInfo.timeline]; 51376 51377 if (segmentInfo.timestampOffset !== null) { 51378 mappingObj = { 51379 time: segmentInfo.startOfSegment, 51380 mapping: segmentInfo.startOfSegment - timingInfo.start 51381 }; 51382 this.timelines[segmentInfo.timeline] = mappingObj; 51383 this.trigger('timestampoffset'); 51384 this.logger_('time mapping for timeline ' + segmentInfo.timeline + ': ' + ('[time: ' + mappingObj.time + '] [mapping: ' + mappingObj.mapping + ']')); 51385 segment.start = segmentInfo.startOfSegment; 51386 segment.end = timingInfo.end + mappingObj.mapping; 51387 } else if (mappingObj) { 51388 segment.start = timingInfo.start + mappingObj.mapping; 51389 segment.end = timingInfo.end + mappingObj.mapping; 51390 } else { 51391 return false; 51392 } 51393 51394 return true; 51395 } 51396 /** 51397 * Each time we have discontinuity in the playlist, attempt to calculate the location 51398 * in display of the start of the discontinuity and save that. We also save an accuracy 51399 * value so that we save values with the most accuracy (closest to 0.) 51400 * 51401 * @private 51402 * @param {SegmentInfo} segmentInfo - The current active request information 51403 */ 51404 51405 }, { 51406 key: 'saveDiscontinuitySyncInfo_', 51407 value: function saveDiscontinuitySyncInfo_(segmentInfo) { 51408 var playlist = segmentInfo.playlist; 51409 var segment = segmentInfo.segment; // If the current segment is a discontinuity then we know exactly where 51410 // the start of the range and it's accuracy is 0 (greater accuracy values 51411 // mean more approximation) 51412 51413 if (segment.discontinuity) { 51414 this.discontinuities[segment.timeline] = { 51415 time: segment.start, 51416 accuracy: 0 51417 }; 51418 } else if (playlist.discontinuityStarts && playlist.discontinuityStarts.length) { 51419 // Search for future discontinuities that we can provide better timing 51420 // information for and save that information for sync purposes 51421 for (var i = 0; i < playlist.discontinuityStarts.length; i++) { 51422 var segmentIndex = playlist.discontinuityStarts[i]; 51423 var discontinuity = playlist.discontinuitySequence + i + 1; 51424 var mediaIndexDiff = segmentIndex - segmentInfo.mediaIndex; 51425 var accuracy = Math.abs(mediaIndexDiff); 51426 51427 if (!this.discontinuities[discontinuity] || this.discontinuities[discontinuity].accuracy > accuracy) { 51428 var time = void 0; 51429 51430 if (mediaIndexDiff < 0) { 51431 time = segment.start - sumDurations(playlist, segmentInfo.mediaIndex, segmentIndex); 51432 } else { 51433 time = segment.end + sumDurations(playlist, segmentInfo.mediaIndex + 1, segmentIndex); 51434 } 51435 51436 this.discontinuities[discontinuity] = { 51437 time: time, 51438 accuracy: accuracy 51439 }; 51440 } 51441 } 51442 } 51443 } 51444 }]); 51445 return SyncController; 51446 }(videojs$1.EventTarget); 51447 51448 var Decrypter$1 = new shimWorker("./decrypter-worker.worker.js", function (window, document$$1) { 51449 var self = this; 51450 51451 var decrypterWorker = function () { 51452 /* 51453 * pkcs7.pad 51454 * https://github.com/brightcove/pkcs7 51455 * 51456 * Copyright (c) 2014 Brightcove 51457 * Licensed under the apache2 license. 51458 */ 51459 51460 /** 51461 * Returns the subarray of a Uint8Array without PKCS#7 padding. 51462 * @param padded {Uint8Array} unencrypted bytes that have been padded 51463 * @return {Uint8Array} the unpadded bytes 51464 * @see http://tools.ietf.org/html/rfc5652 51465 */ 51466 function unpad(padded) { 51467 return padded.subarray(0, padded.byteLength - padded[padded.byteLength - 1]); 51468 } 51469 51470 var classCallCheck = function classCallCheck(instance, Constructor) { 51471 if (!(instance instanceof Constructor)) { 51472 throw new TypeError("Cannot call a class as a function"); 51473 } 51474 }; 51475 51476 var createClass = function () { 51477 function defineProperties(target, props) { 51478 for (var i = 0; i < props.length; i++) { 51479 var descriptor = props[i]; 51480 descriptor.enumerable = descriptor.enumerable || false; 51481 descriptor.configurable = true; 51482 if ("value" in descriptor) descriptor.writable = true; 51483 Object.defineProperty(target, descriptor.key, descriptor); 51484 } 51485 } 51486 51487 return function (Constructor, protoProps, staticProps) { 51488 if (protoProps) defineProperties(Constructor.prototype, protoProps); 51489 if (staticProps) defineProperties(Constructor, staticProps); 51490 return Constructor; 51491 }; 51492 }(); 51493 51494 var inherits = function inherits(subClass, superClass) { 51495 if (typeof superClass !== "function" && superClass !== null) { 51496 throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); 51497 } 51498 51499 subClass.prototype = Object.create(superClass && superClass.prototype, { 51500 constructor: { 51501 value: subClass, 51502 enumerable: false, 51503 writable: true, 51504 configurable: true 51505 } 51506 }); 51507 if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; 51508 }; 51509 51510 var possibleConstructorReturn = function possibleConstructorReturn(self, call) { 51511 if (!self) { 51512 throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); 51513 } 51514 51515 return call && (typeof call === "object" || typeof call === "function") ? call : self; 51516 }; 51517 /** 51518 * @file aes.js 51519 * 51520 * This file contains an adaptation of the AES decryption algorithm 51521 * from the Standford Javascript Cryptography Library. That work is 51522 * covered by the following copyright and permissions notice: 51523 * 51524 * Copyright 2009-2010 Emily Stark, Mike Hamburg, Dan Boneh. 51525 * All rights reserved. 51526 * 51527 * Redistribution and use in source and binary forms, with or without 51528 * modification, are permitted provided that the following conditions are 51529 * met: 51530 * 51531 * 1. Redistributions of source code must retain the above copyright 51532 * notice, this list of conditions and the following disclaimer. 51533 * 51534 * 2. Redistributions in binary form must reproduce the above 51535 * copyright notice, this list of conditions and the following 51536 * disclaimer in the documentation and/or other materials provided 51537 * with the distribution. 51538 * 51539 * THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR 51540 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 51541 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 51542 * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> OR CONTRIBUTORS BE 51543 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 51544 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 51545 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 51546 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 51547 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 51548 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN 51549 * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 51550 * 51551 * The views and conclusions contained in the software and documentation 51552 * are those of the authors and should not be interpreted as representing 51553 * official policies, either expressed or implied, of the authors. 51554 */ 51555 51556 /** 51557 * Expand the S-box tables. 51558 * 51559 * @private 51560 */ 51561 51562 51563 var precompute = function precompute() { 51564 var tables = [[[], [], [], [], []], [[], [], [], [], []]]; 51565 var encTable = tables[0]; 51566 var decTable = tables[1]; 51567 var sbox = encTable[4]; 51568 var sboxInv = decTable[4]; 51569 var i = void 0; 51570 var x = void 0; 51571 var xInv = void 0; 51572 var d = []; 51573 var th = []; 51574 var x2 = void 0; 51575 var x4 = void 0; 51576 var x8 = void 0; 51577 var s = void 0; 51578 var tEnc = void 0; 51579 var tDec = void 0; // Compute double and third tables 51580 51581 for (i = 0; i < 256; i++) { 51582 th[(d[i] = i << 1 ^ (i >> 7) * 283) ^ i] = i; 51583 } 51584 51585 for (x = xInv = 0; !sbox[x]; x ^= x2 || 1, xInv = th[xInv] || 1) { 51586 // Compute sbox 51587 s = xInv ^ xInv << 1 ^ xInv << 2 ^ xInv << 3 ^ xInv << 4; 51588 s = s >> 8 ^ s & 255 ^ 99; 51589 sbox[x] = s; 51590 sboxInv[s] = x; // Compute MixColumns 51591 51592 x8 = d[x4 = d[x2 = d[x]]]; 51593 tDec = x8 * 0x1010101 ^ x4 * 0x10001 ^ x2 * 0x101 ^ x * 0x1010100; 51594 tEnc = d[s] * 0x101 ^ s * 0x1010100; 51595 51596 for (i = 0; i < 4; i++) { 51597 encTable[i][x] = tEnc = tEnc << 24 ^ tEnc >>> 8; 51598 decTable[i][s] = tDec = tDec << 24 ^ tDec >>> 8; 51599 } 51600 } // Compactify. Considerable speedup on Firefox. 51601 51602 51603 for (i = 0; i < 5; i++) { 51604 encTable[i] = encTable[i].slice(0); 51605 decTable[i] = decTable[i].slice(0); 51606 } 51607 51608 return tables; 51609 }; 51610 51611 var aesTables = null; 51612 /** 51613 * Schedule out an AES key for both encryption and decryption. This 51614 * is a low-level class. Use a cipher mode to do bulk encryption. 51615 * 51616 * @class AES 51617 * @param key {Array} The key as an array of 4, 6 or 8 words. 51618 */ 51619 51620 var AES = function () { 51621 function AES(key) { 51622 classCallCheck(this, AES); 51623 /** 51624 * The expanded S-box and inverse S-box tables. These will be computed 51625 * on the client so that we don't have to send them down the wire. 51626 * 51627 * There are two tables, _tables[0] is for encryption and 51628 * _tables[1] is for decryption. 51629 * 51630 * The first 4 sub-tables are the expanded S-box with MixColumns. The 51631 * last (_tables[01][4]) is the S-box itself. 51632 * 51633 * @private 51634 */ 51635 // if we have yet to precompute the S-box tables 51636 // do so now 51637 51638 if (!aesTables) { 51639 aesTables = precompute(); 51640 } // then make a copy of that object for use 51641 51642 51643 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()]]; 51644 var i = void 0; 51645 var j = void 0; 51646 var tmp = void 0; 51647 var encKey = void 0; 51648 var decKey = void 0; 51649 var sbox = this._tables[0][4]; 51650 var decTable = this._tables[1]; 51651 var keyLen = key.length; 51652 var rcon = 1; 51653 51654 if (keyLen !== 4 && keyLen !== 6 && keyLen !== 8) { 51655 throw new Error('Invalid aes key size'); 51656 } 51657 51658 encKey = key.slice(0); 51659 decKey = []; 51660 this._key = [encKey, decKey]; // schedule encryption keys 51661 51662 for (i = keyLen; i < 4 * keyLen + 28; i++) { 51663 tmp = encKey[i - 1]; // apply sbox 51664 51665 if (i % keyLen === 0 || keyLen === 8 && i % keyLen === 4) { 51666 tmp = sbox[tmp >>> 24] << 24 ^ sbox[tmp >> 16 & 255] << 16 ^ sbox[tmp >> 8 & 255] << 8 ^ sbox[tmp & 255]; // shift rows and add rcon 51667 51668 if (i % keyLen === 0) { 51669 tmp = tmp << 8 ^ tmp >>> 24 ^ rcon << 24; 51670 rcon = rcon << 1 ^ (rcon >> 7) * 283; 51671 } 51672 } 51673 51674 encKey[i] = encKey[i - keyLen] ^ tmp; 51675 } // schedule decryption keys 51676 51677 51678 for (j = 0; i; j++, i--) { 51679 tmp = encKey[j & 3 ? i : i - 4]; 51680 51681 if (i <= 4 || j < 4) { 51682 decKey[j] = tmp; 51683 } else { 51684 decKey[j] = decTable[0][sbox[tmp >>> 24]] ^ decTable[1][sbox[tmp >> 16 & 255]] ^ decTable[2][sbox[tmp >> 8 & 255]] ^ decTable[3][sbox[tmp & 255]]; 51685 } 51686 } 51687 } 51688 /** 51689 * Decrypt 16 bytes, specified as four 32-bit words. 51690 * 51691 * @param {Number} encrypted0 the first word to decrypt 51692 * @param {Number} encrypted1 the second word to decrypt 51693 * @param {Number} encrypted2 the third word to decrypt 51694 * @param {Number} encrypted3 the fourth word to decrypt 51695 * @param {Int32Array} out the array to write the decrypted words 51696 * into 51697 * @param {Number} offset the offset into the output array to start 51698 * writing results 51699 * @return {Array} The plaintext. 51700 */ 51701 51702 51703 AES.prototype.decrypt = function decrypt$$1(encrypted0, encrypted1, encrypted2, encrypted3, out, offset) { 51704 var key = this._key[1]; // state variables a,b,c,d are loaded with pre-whitened data 51705 51706 var a = encrypted0 ^ key[0]; 51707 var b = encrypted3 ^ key[1]; 51708 var c = encrypted2 ^ key[2]; 51709 var d = encrypted1 ^ key[3]; 51710 var a2 = void 0; 51711 var b2 = void 0; 51712 var c2 = void 0; // key.length === 2 ? 51713 51714 var nInnerRounds = key.length / 4 - 2; 51715 var i = void 0; 51716 var kIndex = 4; 51717 var table = this._tables[1]; // load up the tables 51718 51719 var table0 = table[0]; 51720 var table1 = table[1]; 51721 var table2 = table[2]; 51722 var table3 = table[3]; 51723 var sbox = table[4]; // Inner rounds. Cribbed from OpenSSL. 51724 51725 for (i = 0; i < nInnerRounds; i++) { 51726 a2 = table0[a >>> 24] ^ table1[b >> 16 & 255] ^ table2[c >> 8 & 255] ^ table3[d & 255] ^ key[kIndex]; 51727 b2 = table0[b >>> 24] ^ table1[c >> 16 & 255] ^ table2[d >> 8 & 255] ^ table3[a & 255] ^ key[kIndex + 1]; 51728 c2 = table0[c >>> 24] ^ table1[d >> 16 & 255] ^ table2[a >> 8 & 255] ^ table3[b & 255] ^ key[kIndex + 2]; 51729 d = table0[d >>> 24] ^ table1[a >> 16 & 255] ^ table2[b >> 8 & 255] ^ table3[c & 255] ^ key[kIndex + 3]; 51730 kIndex += 4; 51731 a = a2; 51732 b = b2; 51733 c = c2; 51734 } // Last round. 51735 51736 51737 for (i = 0; i < 4; i++) { 51738 out[(3 & -i) + offset] = sbox[a >>> 24] << 24 ^ sbox[b >> 16 & 255] << 16 ^ sbox[c >> 8 & 255] << 8 ^ sbox[d & 255] ^ key[kIndex++]; 51739 a2 = a; 51740 a = b; 51741 b = c; 51742 c = d; 51743 d = a2; 51744 } 51745 }; 51746 51747 return AES; 51748 }(); 51749 /** 51750 * @file stream.js 51751 */ 51752 51753 /** 51754 * A lightweight readable stream implemention that handles event dispatching. 51755 * 51756 * @class Stream 51757 */ 51758 51759 51760 var Stream = function () { 51761 function Stream() { 51762 classCallCheck(this, Stream); 51763 this.listeners = {}; 51764 } 51765 /** 51766 * Add a listener for a specified event type. 51767 * 51768 * @param {String} type the event name 51769 * @param {Function} listener the callback to be invoked when an event of 51770 * the specified type occurs 51771 */ 51772 51773 51774 Stream.prototype.on = function on(type, listener) { 51775 if (!this.listeners[type]) { 51776 this.listeners[type] = []; 51777 } 51778 51779 this.listeners[type].push(listener); 51780 }; 51781 /** 51782 * Remove a listener for a specified event type. 51783 * 51784 * @param {String} type the event name 51785 * @param {Function} listener a function previously registered for this 51786 * type of event through `on` 51787 * @return {Boolean} if we could turn it off or not 51788 */ 51789 51790 51791 Stream.prototype.off = function off(type, listener) { 51792 if (!this.listeners[type]) { 51793 return false; 51794 } 51795 51796 var index = this.listeners[type].indexOf(listener); 51797 this.listeners[type].splice(index, 1); 51798 return index > -1; 51799 }; 51800 /** 51801 * Trigger an event of the specified type on this stream. Any additional 51802 * arguments to this function are passed as parameters to event listeners. 51803 * 51804 * @param {String} type the event name 51805 */ 51806 51807 51808 Stream.prototype.trigger = function trigger(type) { 51809 var callbacks = this.listeners[type]; 51810 51811 if (!callbacks) { 51812 return; 51813 } // Slicing the arguments on every invocation of this method 51814 // can add a significant amount of overhead. Avoid the 51815 // intermediate object creation for the common case of a 51816 // single callback argument 51817 51818 51819 if (arguments.length === 2) { 51820 var length = callbacks.length; 51821 51822 for (var i = 0; i < length; ++i) { 51823 callbacks[i].call(this, arguments[1]); 51824 } 51825 } else { 51826 var args = Array.prototype.slice.call(arguments, 1); 51827 var _length = callbacks.length; 51828 51829 for (var _i = 0; _i < _length; ++_i) { 51830 callbacks[_i].apply(this, args); 51831 } 51832 } 51833 }; 51834 /** 51835 * Destroys the stream and cleans up. 51836 */ 51837 51838 51839 Stream.prototype.dispose = function dispose() { 51840 this.listeners = {}; 51841 }; 51842 /** 51843 * Forwards all `data` events on this stream to the destination stream. The 51844 * destination stream should provide a method `push` to receive the data 51845 * events as they arrive. 51846 * 51847 * @param {Stream} destination the stream that will receive all `data` events 51848 * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options 51849 */ 51850 51851 51852 Stream.prototype.pipe = function pipe(destination) { 51853 this.on('data', function (data) { 51854 destination.push(data); 51855 }); 51856 }; 51857 51858 return Stream; 51859 }(); 51860 /** 51861 * @file async-stream.js 51862 */ 51863 51864 /** 51865 * A wrapper around the Stream class to use setTiemout 51866 * and run stream "jobs" Asynchronously 51867 * 51868 * @class AsyncStream 51869 * @extends Stream 51870 */ 51871 51872 51873 var AsyncStream$$1 = function (_Stream) { 51874 inherits(AsyncStream$$1, _Stream); 51875 51876 function AsyncStream$$1() { 51877 classCallCheck(this, AsyncStream$$1); 51878 51879 var _this = possibleConstructorReturn(this, _Stream.call(this, Stream)); 51880 51881 _this.jobs = []; 51882 _this.delay = 1; 51883 _this.timeout_ = null; 51884 return _this; 51885 } 51886 /** 51887 * process an async job 51888 * 51889 * @private 51890 */ 51891 51892 51893 AsyncStream$$1.prototype.processJob_ = function processJob_() { 51894 this.jobs.shift()(); 51895 51896 if (this.jobs.length) { 51897 this.timeout_ = setTimeout(this.processJob_.bind(this), this.delay); 51898 } else { 51899 this.timeout_ = null; 51900 } 51901 }; 51902 /** 51903 * push a job into the stream 51904 * 51905 * @param {Function} job the job to push into the stream 51906 */ 51907 51908 51909 AsyncStream$$1.prototype.push = function push(job) { 51910 this.jobs.push(job); 51911 51912 if (!this.timeout_) { 51913 this.timeout_ = setTimeout(this.processJob_.bind(this), this.delay); 51914 } 51915 }; 51916 51917 return AsyncStream$$1; 51918 }(Stream); 51919 /** 51920 * @file decrypter.js 51921 * 51922 * An asynchronous implementation of AES-128 CBC decryption with 51923 * PKCS#7 padding. 51924 */ 51925 51926 /** 51927 * Convert network-order (big-endian) bytes into their little-endian 51928 * representation. 51929 */ 51930 51931 51932 var ntoh = function ntoh(word) { 51933 return word << 24 | (word & 0xff00) << 8 | (word & 0xff0000) >> 8 | word >>> 24; 51934 }; 51935 /** 51936 * Decrypt bytes using AES-128 with CBC and PKCS#7 padding. 51937 * 51938 * @param {Uint8Array} encrypted the encrypted bytes 51939 * @param {Uint32Array} key the bytes of the decryption key 51940 * @param {Uint32Array} initVector the initialization vector (IV) to 51941 * use for the first round of CBC. 51942 * @return {Uint8Array} the decrypted bytes 51943 * 51944 * @see http://en.wikipedia.org/wiki/Advanced_Encryption_Standard 51945 * @see http://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_Block_Chaining_.28CBC.29 51946 * @see https://tools.ietf.org/html/rfc2315 51947 */ 51948 51949 51950 var decrypt$$1 = function decrypt$$1(encrypted, key, initVector) { 51951 // word-level access to the encrypted bytes 51952 var encrypted32 = new Int32Array(encrypted.buffer, encrypted.byteOffset, encrypted.byteLength >> 2); 51953 var decipher = new AES(Array.prototype.slice.call(key)); // byte and word-level access for the decrypted output 51954 51955 var decrypted = new Uint8Array(encrypted.byteLength); 51956 var decrypted32 = new Int32Array(decrypted.buffer); // temporary variables for working with the IV, encrypted, and 51957 // decrypted data 51958 51959 var init0 = void 0; 51960 var init1 = void 0; 51961 var init2 = void 0; 51962 var init3 = void 0; 51963 var encrypted0 = void 0; 51964 var encrypted1 = void 0; 51965 var encrypted2 = void 0; 51966 var encrypted3 = void 0; // iteration variable 51967 51968 var wordIx = void 0; // pull out the words of the IV to ensure we don't modify the 51969 // passed-in reference and easier access 51970 51971 init0 = initVector[0]; 51972 init1 = initVector[1]; 51973 init2 = initVector[2]; 51974 init3 = initVector[3]; // decrypt four word sequences, applying cipher-block chaining (CBC) 51975 // to each decrypted block 51976 51977 for (wordIx = 0; wordIx < encrypted32.length; wordIx += 4) { 51978 // convert big-endian (network order) words into little-endian 51979 // (javascript order) 51980 encrypted0 = ntoh(encrypted32[wordIx]); 51981 encrypted1 = ntoh(encrypted32[wordIx + 1]); 51982 encrypted2 = ntoh(encrypted32[wordIx + 2]); 51983 encrypted3 = ntoh(encrypted32[wordIx + 3]); // decrypt the block 51984 51985 decipher.decrypt(encrypted0, encrypted1, encrypted2, encrypted3, decrypted32, wordIx); // XOR with the IV, and restore network byte-order to obtain the 51986 // plaintext 51987 51988 decrypted32[wordIx] = ntoh(decrypted32[wordIx] ^ init0); 51989 decrypted32[wordIx + 1] = ntoh(decrypted32[wordIx + 1] ^ init1); 51990 decrypted32[wordIx + 2] = ntoh(decrypted32[wordIx + 2] ^ init2); 51991 decrypted32[wordIx + 3] = ntoh(decrypted32[wordIx + 3] ^ init3); // setup the IV for the next round 51992 51993 init0 = encrypted0; 51994 init1 = encrypted1; 51995 init2 = encrypted2; 51996 init3 = encrypted3; 51997 } 51998 51999 return decrypted; 52000 }; 52001 /** 52002 * The `Decrypter` class that manages decryption of AES 52003 * data through `AsyncStream` objects and the `decrypt` 52004 * function 52005 * 52006 * @param {Uint8Array} encrypted the encrypted bytes 52007 * @param {Uint32Array} key the bytes of the decryption key 52008 * @param {Uint32Array} initVector the initialization vector (IV) to 52009 * @param {Function} done the function to run when done 52010 * @class Decrypter 52011 */ 52012 52013 52014 var Decrypter$$1 = function () { 52015 function Decrypter$$1(encrypted, key, initVector, done) { 52016 classCallCheck(this, Decrypter$$1); 52017 var step = Decrypter$$1.STEP; 52018 var encrypted32 = new Int32Array(encrypted.buffer); 52019 var decrypted = new Uint8Array(encrypted.byteLength); 52020 var i = 0; 52021 this.asyncStream_ = new AsyncStream$$1(); // split up the encryption job and do the individual chunks asynchronously 52022 52023 this.asyncStream_.push(this.decryptChunk_(encrypted32.subarray(i, i + step), key, initVector, decrypted)); 52024 52025 for (i = step; i < encrypted32.length; i += step) { 52026 initVector = new Uint32Array([ntoh(encrypted32[i - 4]), ntoh(encrypted32[i - 3]), ntoh(encrypted32[i - 2]), ntoh(encrypted32[i - 1])]); 52027 this.asyncStream_.push(this.decryptChunk_(encrypted32.subarray(i, i + step), key, initVector, decrypted)); 52028 } // invoke the done() callback when everything is finished 52029 52030 52031 this.asyncStream_.push(function () { 52032 // remove pkcs#7 padding from the decrypted bytes 52033 done(null, unpad(decrypted)); 52034 }); 52035 } 52036 /** 52037 * a getter for step the maximum number of bytes to process at one time 52038 * 52039 * @return {Number} the value of step 32000 52040 */ 52041 52042 /** 52043 * @private 52044 */ 52045 52046 52047 Decrypter$$1.prototype.decryptChunk_ = function decryptChunk_(encrypted, key, initVector, decrypted) { 52048 return function () { 52049 var bytes = decrypt$$1(encrypted, key, initVector); 52050 decrypted.set(bytes, encrypted.byteOffset); 52051 }; 52052 }; 52053 52054 createClass(Decrypter$$1, null, [{ 52055 key: 'STEP', 52056 get: function get$$1() { 52057 // 4 * 8000; 52058 return 32000; 52059 } 52060 }]); 52061 return Decrypter$$1; 52062 }(); 52063 /** 52064 * @file bin-utils.js 52065 */ 52066 52067 /** 52068 * Creates an object for sending to a web worker modifying properties that are TypedArrays 52069 * into a new object with seperated properties for the buffer, byteOffset, and byteLength. 52070 * 52071 * @param {Object} message 52072 * Object of properties and values to send to the web worker 52073 * @return {Object} 52074 * Modified message with TypedArray values expanded 52075 * @function createTransferableMessage 52076 */ 52077 52078 52079 var createTransferableMessage = function createTransferableMessage(message) { 52080 var transferable = {}; 52081 Object.keys(message).forEach(function (key) { 52082 var value = message[key]; 52083 52084 if (ArrayBuffer.isView(value)) { 52085 transferable[key] = { 52086 bytes: value.buffer, 52087 byteOffset: value.byteOffset, 52088 byteLength: value.byteLength 52089 }; 52090 } else { 52091 transferable[key] = value; 52092 } 52093 }); 52094 return transferable; 52095 }; 52096 /** 52097 * Our web worker interface so that things can talk to aes-decrypter 52098 * that will be running in a web worker. the scope is passed to this by 52099 * webworkify. 52100 * 52101 * @param {Object} self 52102 * the scope for the web worker 52103 */ 52104 52105 52106 var DecrypterWorker = function DecrypterWorker(self) { 52107 self.onmessage = function (event) { 52108 var data = event.data; 52109 var encrypted = new Uint8Array(data.encrypted.bytes, data.encrypted.byteOffset, data.encrypted.byteLength); 52110 var key = new Uint32Array(data.key.bytes, data.key.byteOffset, data.key.byteLength / 4); 52111 var iv = new Uint32Array(data.iv.bytes, data.iv.byteOffset, data.iv.byteLength / 4); 52112 /* eslint-disable no-new, handle-callback-err */ 52113 52114 new Decrypter$$1(encrypted, key, iv, function (err, bytes) { 52115 self.postMessage(createTransferableMessage({ 52116 source: data.source, 52117 decrypted: bytes 52118 }), [bytes.buffer]); 52119 }); 52120 /* eslint-enable */ 52121 }; 52122 }; 52123 52124 var decrypterWorker = new DecrypterWorker(self); 52125 return decrypterWorker; 52126 }(); 52127 }); 52128 /** 52129 * Convert the properties of an HLS track into an audioTrackKind. 52130 * 52131 * @private 52132 */ 52133 52134 var audioTrackKind_ = function audioTrackKind_(properties) { 52135 var kind = properties["default"] ? 'main' : 'alternative'; 52136 52137 if (properties.characteristics && properties.characteristics.indexOf('public.accessibility.describes-video') >= 0) { 52138 kind = 'main-desc'; 52139 } 52140 52141 return kind; 52142 }; 52143 /** 52144 * Pause provided segment loader and playlist loader if active 52145 * 52146 * @param {SegmentLoader} segmentLoader 52147 * SegmentLoader to pause 52148 * @param {Object} mediaType 52149 * Active media type 52150 * @function stopLoaders 52151 */ 52152 52153 52154 var stopLoaders = function stopLoaders(segmentLoader, mediaType) { 52155 segmentLoader.abort(); 52156 segmentLoader.pause(); 52157 52158 if (mediaType && mediaType.activePlaylistLoader) { 52159 mediaType.activePlaylistLoader.pause(); 52160 mediaType.activePlaylistLoader = null; 52161 } 52162 }; 52163 /** 52164 * Start loading provided segment loader and playlist loader 52165 * 52166 * @param {PlaylistLoader} playlistLoader 52167 * PlaylistLoader to start loading 52168 * @param {Object} mediaType 52169 * Active media type 52170 * @function startLoaders 52171 */ 52172 52173 52174 var startLoaders = function startLoaders(playlistLoader, mediaType) { 52175 // Segment loader will be started after `loadedmetadata` or `loadedplaylist` from the 52176 // playlist loader 52177 mediaType.activePlaylistLoader = playlistLoader; 52178 playlistLoader.load(); 52179 }; 52180 /** 52181 * Returns a function to be called when the media group changes. It performs a 52182 * non-destructive (preserve the buffer) resync of the SegmentLoader. This is because a 52183 * change of group is merely a rendition switch of the same content at another encoding, 52184 * rather than a change of content, such as switching audio from English to Spanish. 52185 * 52186 * @param {String} type 52187 * MediaGroup type 52188 * @param {Object} settings 52189 * Object containing required information for media groups 52190 * @return {Function} 52191 * Handler for a non-destructive resync of SegmentLoader when the active media 52192 * group changes. 52193 * @function onGroupChanged 52194 */ 52195 52196 52197 var onGroupChanged = function onGroupChanged(type, settings) { 52198 return function () { 52199 var _settings$segmentLoad = settings.segmentLoaders, 52200 segmentLoader = _settings$segmentLoad[type], 52201 mainSegmentLoader = _settings$segmentLoad.main, 52202 mediaType = settings.mediaTypes[type]; 52203 var activeTrack = mediaType.activeTrack(); 52204 var activeGroup = mediaType.activeGroup(activeTrack); 52205 var previousActiveLoader = mediaType.activePlaylistLoader; 52206 stopLoaders(segmentLoader, mediaType); 52207 52208 if (!activeGroup) { 52209 // there is no group active 52210 return; 52211 } 52212 52213 if (!activeGroup.playlistLoader) { 52214 if (previousActiveLoader) { 52215 // The previous group had a playlist loader but the new active group does not 52216 // this means we are switching from demuxed to muxed audio. In this case we want to 52217 // do a destructive reset of the main segment loader and n
vendor: 16,384 bytes, lines 52217-52644
52217ot restart the audio 52218 // loaders. 52219 mainSegmentLoader.resetEverything(); 52220 } 52221 52222 return; 52223 } // Non-destructive resync 52224 52225 52226 segmentLoader.resyncLoader(); 52227 startLoaders(activeGroup.playlistLoader, mediaType); 52228 }; 52229 }; 52230 /** 52231 * Returns a function to be called when the media track changes. It performs a 52232 * destructive reset of the SegmentLoader to ensure we start loading as close to 52233 * currentTime as possible. 52234 * 52235 * @param {String} type 52236 * MediaGroup type 52237 * @param {Object} settings 52238 * Object containing required information for media groups 52239 * @return {Function} 52240 * Handler for a destructive reset of SegmentLoader when the active media 52241 * track changes. 52242 * @function onTrackChanged 52243 */ 52244 52245 52246 var onTrackChanged = function onTrackChanged(type, settings) { 52247 return function () { 52248 var _settings$segmentLoad2 = settings.segmentLoaders, 52249 segmentLoader = _settings$segmentLoad2[type], 52250 mainSegmentLoader = _settings$segmentLoad2.main, 52251 mediaType = settings.mediaTypes[type]; 52252 var activeTrack = mediaType.activeTrack(); 52253 var activeGroup = mediaType.activeGroup(activeTrack); 52254 var previousActiveLoader = mediaType.activePlaylistLoader; 52255 stopLoaders(segmentLoader, mediaType); 52256 52257 if (!activeGroup) { 52258 // there is no group active so we do not want to restart loaders 52259 return; 52260 } 52261 52262 if (!activeGroup.playlistLoader) { 52263 // when switching from demuxed audio/video to muxed audio/video (noted by no playlist 52264 // loader for the audio group), we want to do a destructive reset of the main segment 52265 // loader and not restart the audio loaders 52266 mainSegmentLoader.resetEverything(); 52267 return; 52268 } 52269 52270 if (previousActiveLoader === activeGroup.playlistLoader) { 52271 // Nothing has actually changed. This can happen because track change events can fire 52272 // multiple times for a "single" change. One for enabling the new active track, and 52273 // one for disabling the track that was active 52274 startLoaders(activeGroup.playlistLoader, mediaType); 52275 return; 52276 } 52277 52278 if (segmentLoader.track) { 52279 // For WebVTT, set the new text track in the segmentloader 52280 segmentLoader.track(activeTrack); 52281 } // destructive reset 52282 52283 52284 segmentLoader.resetEverything(); 52285 startLoaders(activeGroup.playlistLoader, mediaType); 52286 }; 52287 }; 52288 52289 var onError = { 52290 /** 52291 * Returns a function to be called when a SegmentLoader or PlaylistLoader encounters 52292 * an error. 52293 * 52294 * @param {String} type 52295 * MediaGroup type 52296 * @param {Object} settings 52297 * Object containing required information for media groups 52298 * @return {Function} 52299 * Error handler. Logs warning (or error if the playlist is blacklisted) to 52300 * console and switches back to default audio track. 52301 * @function onError.AUDIO 52302 */ 52303 AUDIO: function AUDIO(type, settings) { 52304 return function () { 52305 var segmentLoader = settings.segmentLoaders[type], 52306 mediaType = settings.mediaTypes[type], 52307 blacklistCurrentPlaylist = settings.blacklistCurrentPlaylist; 52308 stopLoaders(segmentLoader, mediaType); // switch back to default audio track 52309 52310 var activeTrack = mediaType.activeTrack(); 52311 var activeGroup = mediaType.activeGroup(); 52312 var id = (activeGroup.filter(function (group) { 52313 return group["default"]; 52314 })[0] || activeGroup[0]).id; 52315 var defaultTrack = mediaType.tracks[id]; 52316 52317 if (activeTrack === defaultTrack) { 52318 // Default track encountered an error. All we can do now is blacklist the current 52319 // rendition and hope another will switch audio groups 52320 blacklistCurrentPlaylist({ 52321 message: 'Problem encountered loading the default audio track.' 52322 }); 52323 return; 52324 } 52325 52326 videojs$1.log.warn('Problem encountered loading the alternate audio track.' + 'Switching back to default.'); 52327 52328 for (var trackId in mediaType.tracks) { 52329 mediaType.tracks[trackId].enabled = mediaType.tracks[trackId] === defaultTrack; 52330 } 52331 52332 mediaType.onTrackChanged(); 52333 }; 52334 }, 52335 52336 /** 52337 * Returns a function to be called when a SegmentLoader or PlaylistLoader encounters 52338 * an error. 52339 * 52340 * @param {String} type 52341 * MediaGroup type 52342 * @param {Object} settings 52343 * Object containing required information for media groups 52344 * @return {Function} 52345 * Error handler. Logs warning to console and disables the active subtitle track 52346 * @function onError.SUBTITLES 52347 */ 52348 SUBTITLES: function SUBTITLES(type, settings) { 52349 return function () { 52350 var segmentLoader = settings.segmentLoaders[type], 52351 mediaType = settings.mediaTypes[type]; 52352 videojs$1.log.warn('Problem encountered loading the subtitle track.' + 'Disabling subtitle track.'); 52353 stopLoaders(segmentLoader, mediaType); 52354 var track = mediaType.activeTrack(); 52355 52356 if (track) { 52357 track.mode = 'disabled'; 52358 } 52359 52360 mediaType.onTrackChanged(); 52361 }; 52362 } 52363 }; 52364 var setupListeners = { 52365 /** 52366 * Setup event listeners for audio playlist loader 52367 * 52368 * @param {String} type 52369 * MediaGroup type 52370 * @param {PlaylistLoader|null} playlistLoader 52371 * PlaylistLoader to register listeners on 52372 * @param {Object} settings 52373 * Object containing required information for media groups 52374 * @function setupListeners.AUDIO 52375 */ 52376 AUDIO: function AUDIO(type, playlistLoader, settings) { 52377 if (!playlistLoader) { 52378 // no playlist loader means audio will be muxed with the video 52379 return; 52380 } 52381 52382 var tech = settings.tech, 52383 requestOptions = settings.requestOptions, 52384 segmentLoader = settings.segmentLoaders[type]; 52385 playlistLoader.on('loadedmetadata', function () { 52386 var media = playlistLoader.media(); 52387 segmentLoader.playlist(media, requestOptions); // if the video is already playing, or if this isn't a live video and preload 52388 // permits, start downloading segments 52389 52390 if (!tech.paused() || media.endList && tech.preload() !== 'none') { 52391 segmentLoader.load(); 52392 } 52393 }); 52394 playlistLoader.on('loadedplaylist', function () { 52395 segmentLoader.playlist(playlistLoader.media(), requestOptions); // If the player isn't paused, ensure that the segment loader is running 52396 52397 if (!tech.paused()) { 52398 segmentLoader.load(); 52399 } 52400 }); 52401 playlistLoader.on('error', onError[type](type, settings)); 52402 }, 52403 52404 /** 52405 * Setup event listeners for subtitle playlist loader 52406 * 52407 * @param {String} type 52408 * MediaGroup type 52409 * @param {PlaylistLoader|null} playlistLoader 52410 * PlaylistLoader to register listeners on 52411 * @param {Object} settings 52412 * Object containing required information for media groups 52413 * @function setupListeners.SUBTITLES 52414 */ 52415 SUBTITLES: function SUBTITLES(type, playlistLoader, settings) { 52416 var tech = settings.tech, 52417 requestOptions = settings.requestOptions, 52418 segmentLoader = settings.segmentLoaders[type], 52419 mediaType = settings.mediaTypes[type]; 52420 playlistLoader.on('loadedmetadata', function () { 52421 var media = playlistLoader.media(); 52422 segmentLoader.playlist(media, requestOptions); 52423 segmentLoader.track(mediaType.activeTrack()); // if the video is already playing, or if this isn't a live video and preload 52424 // permits, start downloading segments 52425 52426 if (!tech.paused() || media.endList && tech.preload() !== 'none') { 52427 segmentLoader.load(); 52428 } 52429 }); 52430 playlistLoader.on('loadedplaylist', function () { 52431 segmentLoader.playlist(playlistLoader.media(), requestOptions); // If the player isn't paused, ensure that the segment loader is running 52432 52433 if (!tech.paused()) { 52434 segmentLoader.load(); 52435 } 52436 }); 52437 playlistLoader.on('error', onError[type](type, settings)); 52438 } 52439 }; 52440 var initialize = { 52441 /** 52442 * Setup PlaylistLoaders and AudioTracks for the audio groups 52443 * 52444 * @param {String} type 52445 * MediaGroup type 52446 * @param {Object} settings 52447 * Object containing required information for media groups 52448 * @function initialize.AUDIO 52449 */ 52450 'AUDIO': function AUDIO(type, settings) { 52451 var hls = settings.hls, 52452 sourceType = settings.sourceType, 52453 segmentLoader = settings.segmentLoaders[type], 52454 requestOptions = settings.requestOptions, 52455 mediaGroups = settings.master.mediaGroups, 52456 _settings$mediaTypes$ = settings.mediaTypes[type], 52457 groups = _settings$mediaTypes$.groups, 52458 tracks = _settings$mediaTypes$.tracks, 52459 masterPlaylistLoader = settings.masterPlaylistLoader; // force a default if we have none 52460 52461 if (!mediaGroups[type] || Object.keys(mediaGroups[type]).length === 0) { 52462 mediaGroups[type] = { 52463 main: { 52464 "default": { 52465 "default": true 52466 } 52467 } 52468 }; 52469 } 52470 52471 for (var groupId in mediaGroups[type]) { 52472 if (!groups[groupId]) { 52473 groups[groupId] = []; 52474 } // List of playlists that have an AUDIO attribute value matching the current 52475 // group ID 52476 52477 52478 for (var variantLabel in mediaGroups[type][groupId]) { 52479 var properties = mediaGroups[type][groupId][variantLabel]; 52480 var playlistLoader = void 0; 52481 52482 if (properties.resolvedUri) { 52483 playlistLoader = new PlaylistLoader(properties.resolvedUri, hls, requestOptions); 52484 } else if (properties.playlists && sourceType === 'dash') { 52485 playlistLoader = new DashPlaylistLoader(properties.playlists[0], hls, requestOptions, masterPlaylistLoader); 52486 } else { 52487 // no resolvedUri means the audio is muxed with the video when using this 52488 // audio track 52489 playlistLoader = null; 52490 } 52491 52492 properties = videojs$1.mergeOptions({ 52493 id: variantLabel, 52494 playlistLoader: playlistLoader 52495 }, properties); 52496 setupListeners[type](type, properties.playlistLoader, settings); 52497 groups[groupId].push(properties); 52498 52499 if (typeof tracks[variantLabel] === 'undefined') { 52500 var track = new videojs$1.AudioTrack({ 52501 id: variantLabel, 52502 kind: audioTrackKind_(properties), 52503 enabled: false, 52504 language: properties.language, 52505 "default": properties["default"], 52506 label: variantLabel 52507 }); 52508 tracks[variantLabel] = track; 52509 } 52510 } 52511 } // setup single error event handler for the segment loader 52512 52513 52514 segmentLoader.on('error', onError[type](type, settings)); 52515 }, 52516 52517 /** 52518 * Setup PlaylistLoaders and TextTracks for the subtitle groups 52519 * 52520 * @param {String} type 52521 * MediaGroup type 52522 * @param {Object} settings 52523 * Object containing required information for media groups 52524 * @function initialize.SUBTITLES 52525 */ 52526 'SUBTITLES': function SUBTITLES(type, settings) { 52527 var tech = settings.tech, 52528 hls = settings.hls, 52529 sourceType = settings.sourceType, 52530 segmentLoader = settings.segmentLoaders[type], 52531 requestOptions = settings.requestOptions, 52532 mediaGroups = settings.master.mediaGroups, 52533 _settings$mediaTypes$2 = settings.mediaTypes[type], 52534 groups = _settings$mediaTypes$2.groups, 52535 tracks = _settings$mediaTypes$2.tracks, 52536 masterPlaylistLoader = settings.masterPlaylistLoader; 52537 52538 for (var groupId in mediaGroups[type]) { 52539 if (!groups[groupId]) { 52540 groups[groupId] = []; 52541 } 52542 52543 for (var variantLabel in mediaGroups[type][groupId]) { 52544 if (mediaGroups[type][groupId][variantLabel].forced) { 52545 // Subtitle playlists with the forced attribute are not selectable in Safari. 52546 // According to Apple's HLS Authoring Specification: 52547 // If content has forced subtitles and regular subtitles in a given language, 52548 // the regular subtitles track in that language MUST contain both the forced 52549 // subtitles and the regular subtitles for that language. 52550 // Because of this requirement and that Safari does not add forced subtitles, 52551 // forced subtitles are skipped here to maintain consistent experience across 52552 // all platforms 52553 continue; 52554 } 52555 52556 var properties = mediaGroups[type][groupId][variantLabel]; 52557 var playlistLoader = void 0; 52558 52559 if (sourceType === 'hls') { 52560 playlistLoader = new PlaylistLoader(properties.resolvedUri, hls, requestOptions); 52561 } else if (sourceType === 'dash') { 52562 playlistLoader = new DashPlaylistLoader(properties.playlists[0], hls, requestOptions, masterPlaylistLoader); 52563 } 52564 52565 properties = videojs$1.mergeOptions({ 52566 id: variantLabel, 52567 playlistLoader: playlistLoader 52568 }, properties); 52569 setupListeners[type](type, properties.playlistLoader, settings); 52570 groups[groupId].push(properties); 52571 52572 if (typeof tracks[variantLabel] === 'undefined') { 52573 var track = tech.addRemoteTextTrack({ 52574 id: variantLabel, 52575 kind: 'subtitles', 52576 "default": properties["default"] && properties.autoselect, 52577 language: properties.language, 52578 label: variantLabel 52579 }, false).track; 52580 tracks[variantLabel] = track; 52581 } 52582 } 52583 } // setup single error event handler for the segment loader 52584 52585 52586 segmentLoader.on('error', onError[type](type, settings)); 52587 }, 52588 52589 /** 52590 * Setup TextTracks for the closed-caption groups 52591 * 52592 * @param {String} type 52593 * MediaGroup type 52594 * @param {Object} settings 52595 * Object containing required information for media groups 52596 * @function initialize['CLOSED-CAPTIONS'] 52597 */ 52598 'CLOSED-CAPTIONS': function CLOSEDCAPTIONS(type, settings) { 52599 var tech = settings.tech, 52600 mediaGroups = settings.master.mediaGroups, 52601 _settings$mediaTypes$3 = settings.mediaTypes[type], 52602 groups = _settings$mediaTypes$3.groups, 52603 tracks = _settings$mediaTypes$3.tracks; 52604 52605 for (var groupId in mediaGroups[type]) { 52606 if (!groups[groupId]) { 52607 groups[groupId] = []; 52608 } 52609 52610 for (var variantLabel in mediaGroups[type][groupId]) { 52611 var properties = mediaGroups[type][groupId][variantLabel]; // We only support CEA608 captions for now, so ignore anything that 52612 // doesn't use a CCx INSTREAM-ID 52613 52614 if (!properties.instreamId.match(/CC\d/)) { 52615 continue; 52616 } // No PlaylistLoader is required for Closed-Captions because the captions are 52617 // embedded within the video stream 52618 52619 52620 groups[groupId].push(videojs$1.mergeOptions({ 52621 id: variantLabel 52622 }, properties)); 52623 52624 if (typeof tracks[variantLabel] === 'undefined') { 52625 var track = tech.addRemoteTextTrack({ 52626 id: properties.instreamId, 52627 kind: 'captions', 52628 "default": properties["default"] && properties.autoselect, 52629 language: properties.language, 52630 label: variantLabel 52631 }, false).track; 52632 tracks[variantLabel] = track; 52633 } 52634 } 52635 } 52636 } 52637 }; 52638 /** 52639 * Returns a function used to get the active group of the provided type 52640 * 52641 * @param {String} type 52642 * MediaGroup type 52643 * @param {Object} settings 52644 * Object containing requi
vendor: 37,520 bytes, lines 52644-53639
52644red information for media groups 52645 * @return {Function} 52646 * Function that returns the active media group for the provided type. Takes an 52647 * optional parameter {TextTrack} track. If no track is provided, a list of all 52648 * variants in the group, otherwise the variant corresponding to the provided 52649 * track is returned. 52650 * @function activeGroup 52651 */ 52652 52653 var activeGroup = function activeGroup(type, settings) { 52654 return function (track) { 52655 var masterPlaylistLoader = settings.masterPlaylistLoader, 52656 groups = settings.mediaTypes[type].groups; 52657 var media = masterPlaylistLoader.media(); 52658 52659 if (!media) { 52660 return null; 52661 } 52662 52663 var variants = null; 52664 52665 if (media.attributes[type]) { 52666 variants = groups[media.attributes[type]]; 52667 } 52668 52669 variants = variants || groups.main; 52670 52671 if (typeof track === 'undefined') { 52672 return variants; 52673 } 52674 52675 if (track === null) { 52676 // An active track was specified so a corresponding group is expected. track === null 52677 // means no track is currently active so there is no corresponding group 52678 return null; 52679 } 52680 52681 return variants.filter(function (props) { 52682 return props.id === track.id; 52683 })[0] || null; 52684 }; 52685 }; 52686 52687 var activeTrack = { 52688 /** 52689 * Returns a function used to get the active track of type provided 52690 * 52691 * @param {String} type 52692 * MediaGroup type 52693 * @param {Object} settings 52694 * Object containing required information for media groups 52695 * @return {Function} 52696 * Function that returns the active media track for the provided type. Returns 52697 * null if no track is active 52698 * @function activeTrack.AUDIO 52699 */ 52700 AUDIO: function AUDIO(type, settings) { 52701 return function () { 52702 var tracks = settings.mediaTypes[type].tracks; 52703 52704 for (var id in tracks) { 52705 if (tracks[id].enabled) { 52706 return tracks[id]; 52707 } 52708 } 52709 52710 return null; 52711 }; 52712 }, 52713 52714 /** 52715 * Returns a function used to get the active track of type provided 52716 * 52717 * @param {String} type 52718 * MediaGroup type 52719 * @param {Object} settings 52720 * Object containing required information for media groups 52721 * @return {Function} 52722 * Function that returns the active media track for the provided type. Returns 52723 * null if no track is active 52724 * @function activeTrack.SUBTITLES 52725 */ 52726 SUBTITLES: function SUBTITLES(type, settings) { 52727 return function () { 52728 var tracks = settings.mediaTypes[type].tracks; 52729 52730 for (var id in tracks) { 52731 if (tracks[id].mode === 'showing') { 52732 return tracks[id]; 52733 } 52734 } 52735 52736 return null; 52737 }; 52738 } 52739 }; 52740 /** 52741 * Setup PlaylistLoaders and Tracks for media groups (Audio, Subtitles, 52742 * Closed-Captions) specified in the master manifest. 52743 * 52744 * @param {Object} settings 52745 * Object containing required information for setting up the media groups 52746 * @param {SegmentLoader} settings.segmentLoaders.AUDIO 52747 * Audio segment loader 52748 * @param {SegmentLoader} settings.segmentLoaders.SUBTITLES 52749 * Subtitle segment loader 52750 * @param {SegmentLoader} settings.segmentLoaders.main 52751 * Main segment loader 52752 * @param {Tech} settings.tech 52753 * The tech of the player 52754 * @param {Object} settings.requestOptions 52755 * XHR request options used by the segment loaders 52756 * @param {PlaylistLoader} settings.masterPlaylistLoader 52757 * PlaylistLoader for the master source 52758 * @param {HlsHandler} settings.hls 52759 * HLS SourceHandler 52760 * @param {Object} settings.master 52761 * The parsed master manifest 52762 * @param {Object} settings.mediaTypes 52763 * Object to store the loaders, tracks, and utility methods for each media type 52764 * @param {Function} settings.blacklistCurrentPlaylist 52765 * Blacklists the current rendition and forces a rendition switch. 52766 * @function setupMediaGroups 52767 */ 52768 52769 var setupMediaGroups = function setupMediaGroups(settings) { 52770 ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach(function (type) { 52771 initialize[type](type, settings); 52772 }); 52773 var mediaTypes = settings.mediaTypes, 52774 masterPlaylistLoader = settings.masterPlaylistLoader, 52775 tech = settings.tech, 52776 hls = settings.hls; // setup active group and track getters and change event handlers 52777 52778 ['AUDIO', 'SUBTITLES'].forEach(function (type) { 52779 mediaTypes[type].activeGroup = activeGroup(type, settings); 52780 mediaTypes[type].activeTrack = activeTrack[type](type, settings); 52781 mediaTypes[type].onGroupChanged = onGroupChanged(type, settings); 52782 mediaTypes[type].onTrackChanged = onTrackChanged(type, settings); 52783 }); // DO NOT enable the default subtitle or caption track. 52784 // DO enable the default audio track 52785 52786 var audioGroup = mediaTypes.AUDIO.activeGroup(); 52787 var groupId = (audioGroup.filter(function (group) { 52788 return group["default"]; 52789 })[0] || audioGroup[0]).id; 52790 mediaTypes.AUDIO.tracks[groupId].enabled = true; 52791 mediaTypes.AUDIO.onTrackChanged(); 52792 masterPlaylistLoader.on('mediachange', function () { 52793 ['AUDIO', 'SUBTITLES'].forEach(function (type) { 52794 return mediaTypes[type].onGroupChanged(); 52795 }); 52796 }); // custom audio track change event handler for usage event 52797 52798 var onAudioTrackChanged = function onAudioTrackChanged() { 52799 mediaTypes.AUDIO.onTrackChanged(); 52800 tech.trigger({ 52801 type: 'usage', 52802 name: 'hls-audio-change' 52803 }); 52804 }; 52805 52806 tech.audioTracks().addEventListener('change', onAudioTrackChanged); 52807 tech.remoteTextTracks().addEventListener('change', mediaTypes.SUBTITLES.onTrackChanged); 52808 hls.on('dispose', function () { 52809 tech.audioTracks().removeEventListener('change', onAudioTrackChanged); 52810 tech.remoteTextTracks().removeEventListener('change', mediaTypes.SUBTITLES.onTrackChanged); 52811 }); // clear existing audio tracks and add the ones we just created 52812 52813 tech.clearTracks('audio'); 52814 52815 for (var id in mediaTypes.AUDIO.tracks) { 52816 tech.audioTracks().addTrack(mediaTypes.AUDIO.tracks[id]); 52817 } 52818 }; 52819 /** 52820 * Creates skeleton object used to store the loaders, tracks, and utility methods for each 52821 * media type 52822 * 52823 * @return {Object} 52824 * Object to store the loaders, tracks, and utility methods for each media type 52825 * @function createMediaTypes 52826 */ 52827 52828 52829 var createMediaTypes = function createMediaTypes() { 52830 var mediaTypes = {}; 52831 ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach(function (type) { 52832 mediaTypes[type] = { 52833 groups: {}, 52834 tracks: {}, 52835 activePlaylistLoader: null, 52836 activeGroup: noop$1, 52837 activeTrack: noop$1, 52838 onGroupChanged: noop$1, 52839 onTrackChanged: noop$1 52840 }; 52841 }); 52842 return mediaTypes; 52843 }; 52844 /** 52845 * @file master-playlist-controller.js 52846 */ 52847 52848 52849 var ABORT_EARLY_BLACKLIST_SECONDS = 60 * 2; 52850 var Hls = void 0; // SegmentLoader stats that need to have each loader's 52851 // values summed to calculate the final value 52852 52853 var loaderStats = ['mediaRequests', 'mediaRequestsAborted', 'mediaRequestsTimedout', 'mediaRequestsErrored', 'mediaTransferDuration', 'mediaBytesTransferred']; 52854 52855 var sumLoaderStat = function sumLoaderStat(stat) { 52856 return this.audioSegmentLoader_[stat] + this.mainSegmentLoader_[stat]; 52857 }; 52858 /** 52859 * the master playlist controller controller all interactons 52860 * between playlists and segmentloaders. At this time this mainly 52861 * involves a master playlist and a series of audio playlists 52862 * if they are available 52863 * 52864 * @class MasterPlaylistController 52865 * @extends videojs.EventTarget 52866 */ 52867 52868 52869 var MasterPlaylistController = function (_videojs$EventTarget) { 52870 inherits$2(MasterPlaylistController, _videojs$EventTarget); 52871 52872 function MasterPlaylistController(options) { 52873 classCallCheck$1(this, MasterPlaylistController); 52874 52875 var _this = possibleConstructorReturn$1(this, (MasterPlaylistController.__proto__ || Object.getPrototypeOf(MasterPlaylistController)).call(this)); 52876 52877 var url = options.url, 52878 handleManifestRedirects = options.handleManifestRedirects, 52879 withCredentials = options.withCredentials, 52880 tech = options.tech, 52881 bandwidth = options.bandwidth, 52882 externHls = options.externHls, 52883 useCueTags = options.useCueTags, 52884 blacklistDuration = options.blacklistDuration, 52885 enableLowInitialPlaylist = options.enableLowInitialPlaylist, 52886 sourceType = options.sourceType, 52887 seekTo = options.seekTo, 52888 cacheEncryptionKeys = options.cacheEncryptionKeys; 52889 52890 if (!url) { 52891 throw new Error('A non-empty playlist URL is required'); 52892 } 52893 52894 Hls = externHls; 52895 _this.withCredentials = withCredentials; 52896 _this.tech_ = tech; 52897 _this.hls_ = tech.hls; 52898 _this.seekTo_ = seekTo; 52899 _this.sourceType_ = sourceType; 52900 _this.useCueTags_ = useCueTags; 52901 _this.blacklistDuration = blacklistDuration; 52902 _this.enableLowInitialPlaylist = enableLowInitialPlaylist; 52903 52904 if (_this.useCueTags_) { 52905 _this.cueTagsTrack_ = _this.tech_.addTextTrack('metadata', 'ad-cues'); 52906 _this.cueTagsTrack_.inBandMetadataTrackDispatchType = ''; 52907 } 52908 52909 _this.requestOptions_ = { 52910 withCredentials: withCredentials, 52911 handleManifestRedirects: handleManifestRedirects, 52912 timeout: null 52913 }; 52914 _this.mediaTypes_ = createMediaTypes(); 52915 _this.mediaSource = new videojs$1.MediaSource(); // load the media source into the player 52916 52917 _this.mediaSource.addEventListener('sourceopen', _this.handleSourceOpen_.bind(_this)); 52918 52919 _this.seekable_ = videojs$1.createTimeRanges(); 52920 52921 _this.hasPlayed_ = function () { 52922 return false; 52923 }; 52924 52925 _this.syncController_ = new SyncController(options); 52926 _this.segmentMetadataTrack_ = tech.addRemoteTextTrack({ 52927 kind: 'metadata', 52928 label: 'segment-metadata' 52929 }, false).track; 52930 _this.decrypter_ = new Decrypter$1(); 52931 _this.inbandTextTracks_ = {}; 52932 var segmentLoaderSettings = { 52933 hls: _this.hls_, 52934 mediaSource: _this.mediaSource, 52935 currentTime: _this.tech_.currentTime.bind(_this.tech_), 52936 seekable: function seekable$$1() { 52937 return _this.seekable(); 52938 }, 52939 seeking: function seeking() { 52940 return _this.tech_.seeking(); 52941 }, 52942 duration: function duration$$1() { 52943 return _this.mediaSource.duration; 52944 }, 52945 hasPlayed: function hasPlayed() { 52946 return _this.hasPlayed_(); 52947 }, 52948 goalBufferLength: function goalBufferLength() { 52949 return _this.goalBufferLength(); 52950 }, 52951 bandwidth: bandwidth, 52952 syncController: _this.syncController_, 52953 decrypter: _this.decrypter_, 52954 sourceType: _this.sourceType_, 52955 inbandTextTracks: _this.inbandTextTracks_, 52956 cacheEncryptionKeys: cacheEncryptionKeys 52957 }; 52958 _this.masterPlaylistLoader_ = _this.sourceType_ === 'dash' ? new DashPlaylistLoader(url, _this.hls_, _this.requestOptions_) : new PlaylistLoader(url, _this.hls_, _this.requestOptions_); 52959 52960 _this.setupMasterPlaylistLoaderListeners_(); // setup segment loaders 52961 // combined audio/video or just video when alternate audio track is selected 52962 52963 52964 _this.mainSegmentLoader_ = new SegmentLoader(videojs$1.mergeOptions(segmentLoaderSettings, { 52965 segmentMetadataTrack: _this.segmentMetadataTrack_, 52966 loaderType: 'main' 52967 }), options); // alternate audio track 52968 52969 _this.audioSegmentLoader_ = new SegmentLoader(videojs$1.mergeOptions(segmentLoaderSettings, { 52970 loaderType: 'audio' 52971 }), options); 52972 _this.subtitleSegmentLoader_ = new VTTSegmentLoader(videojs$1.mergeOptions(segmentLoaderSettings, { 52973 loaderType: 'vtt' 52974 }), options); 52975 52976 _this.setupSegmentLoaderListeners_(); // Create SegmentLoader stat-getters 52977 52978 52979 loaderStats.forEach(function (stat) { 52980 _this[stat + '_'] = sumLoaderStat.bind(_this, stat); 52981 }); 52982 _this.logger_ = logger('MPC'); 52983 52984 _this.masterPlaylistLoader_.load(); 52985 52986 return _this; 52987 } 52988 /** 52989 * Register event handlers on the master playlist loader. A helper 52990 * function for construction time. 52991 * 52992 * @private 52993 */ 52994 52995 52996 createClass$1(MasterPlaylistController, [{ 52997 key: 'setupMasterPlaylistLoaderListeners_', 52998 value: function setupMasterPlaylistLoaderListeners_() { 52999 var _this2 = this; 53000 53001 this.masterPlaylistLoader_.on('loadedmetadata', function () { 53002 var media = _this2.masterPlaylistLoader_.media(); 53003 53004 var requestTimeout = media.targetDuration * 1.5 * 1000; // If we don't have any more available playlists, we don't want to 53005 // timeout the request. 53006 53007 if (isLowestEnabledRendition(_this2.masterPlaylistLoader_.master, _this2.masterPlaylistLoader_.media())) { 53008 _this2.requestOptions_.timeout = 0; 53009 } else { 53010 _this2.requestOptions_.timeout = requestTimeout; 53011 } // if this isn't a live video and preload permits, start 53012 // downloading segments 53013 53014 53015 if (media.endList && _this2.tech_.preload() !== 'none') { 53016 _this2.mainSegmentLoader_.playlist(media, _this2.requestOptions_); 53017 53018 _this2.mainSegmentLoader_.load(); 53019 } 53020 53021 setupMediaGroups({ 53022 sourceType: _this2.sourceType_, 53023 segmentLoaders: { 53024 AUDIO: _this2.audioSegmentLoader_, 53025 SUBTITLES: _this2.subtitleSegmentLoader_, 53026 main: _this2.mainSegmentLoader_ 53027 }, 53028 tech: _this2.tech_, 53029 requestOptions: _this2.requestOptions_, 53030 masterPlaylistLoader: _this2.masterPlaylistLoader_, 53031 hls: _this2.hls_, 53032 master: _this2.master(), 53033 mediaTypes: _this2.mediaTypes_, 53034 blacklistCurrentPlaylist: _this2.blacklistCurrentPlaylist.bind(_this2) 53035 }); 53036 53037 _this2.triggerPresenceUsage_(_this2.master(), media); 53038 53039 try { 53040 _this2.setupSourceBuffers_(); 53041 } catch (e) { 53042 videojs$1.log.warn('Failed to create SourceBuffers', e); 53043 return _this2.mediaSource.endOfStream('decode'); 53044 } 53045 53046 _this2.setupFirstPlay(); 53047 53048 if (!_this2.mediaTypes_.AUDIO.activePlaylistLoader || _this2.mediaTypes_.AUDIO.activePlaylistLoader.media()) { 53049 _this2.trigger('selectedinitialmedia'); 53050 } else { 53051 // We must wait for the active audio playlist loader to 53052 // finish setting up before triggering this event so the 53053 // representations API and EME setup is correct 53054 _this2.mediaTypes_.AUDIO.activePlaylistLoader.one('loadedmetadata', function () { 53055 _this2.trigger('selectedinitialmedia'); 53056 }); 53057 } 53058 }); 53059 this.masterPlaylistLoader_.on('loadedplaylist', function () { 53060 var updatedPlaylist = _this2.masterPlaylistLoader_.media(); 53061 53062 if (!updatedPlaylist) { 53063 // blacklist any variants that are not supported by the browser before selecting 53064 // an initial media as the playlist selectors do not consider browser support 53065 _this2.excludeUnsupportedVariants_(); 53066 53067 var selectedMedia = void 0; 53068 53069 if (_this2.enableLowInitialPlaylist) { 53070 selectedMedia = _this2.selectInitialPlaylist(); 53071 } 53072 53073 if (!selectedMedia) { 53074 selectedMedia = _this2.selectPlaylist(); 53075 } 53076 53077 _this2.initialMedia_ = selectedMedia; 53078 53079 _this2.masterPlaylistLoader_.media(_this2.initialMedia_); 53080 53081 return; 53082 } 53083 53084 if (_this2.useCueTags_) { 53085 _this2.updateAdCues_(updatedPlaylist); 53086 } // TODO: Create a new event on the PlaylistLoader that signals 53087 // that the segments have changed in some way and use that to 53088 // update the SegmentLoader instead of doing it twice here and 53089 // on `mediachange` 53090 53091 53092 _this2.mainSegmentLoader_.playlist(updatedPlaylist, _this2.requestOptions_); 53093 53094 _this2.updateDuration(); // If the player isn't paused, ensure that the segment loader is running, 53095 // as it is possible that it was temporarily stopped while waiting for 53096 // a playlist (e.g., in case the playlist errored and we re-requested it). 53097 53098 53099 if (!_this2.tech_.paused()) { 53100 _this2.mainSegmentLoader_.load(); 53101 53102 if (_this2.audioSegmentLoader_) { 53103 _this2.audioSegmentLoader_.load(); 53104 } 53105 } 53106 53107 if (!updatedPlaylist.endList) { 53108 var addSeekableRange = function addSeekableRange() { 53109 var seekable$$1 = _this2.seekable(); 53110 53111 if (seekable$$1.length !== 0) { 53112 _this2.mediaSource.addSeekableRange_(seekable$$1.start(0), seekable$$1.end(0)); 53113 } 53114 }; 53115 53116 if (_this2.duration() !== Infinity) { 53117 var onDurationchange = function onDurationchange() { 53118 if (_this2.duration() === Infinity) { 53119 addSeekableRange(); 53120 } else { 53121 _this2.tech_.one('durationchange', onDurationchange); 53122 } 53123 }; 53124 53125 _this2.tech_.one('durationchange', onDurationchange); 53126 } else { 53127 addSeekableRange(); 53128 } 53129 } 53130 }); 53131 this.masterPlaylistLoader_.on('error', function () { 53132 _this2.blacklistCurrentPlaylist(_this2.masterPlaylistLoader_.error); 53133 }); 53134 this.masterPlaylistLoader_.on('mediachanging', function () { 53135 _this2.mainSegmentLoader_.abort(); 53136 53137 _this2.mainSegmentLoader_.pause(); 53138 }); 53139 this.masterPlaylistLoader_.on('mediachange', function () { 53140 var media = _this2.masterPlaylistLoader_.media(); 53141 53142 var requestTimeout = media.targetDuration * 1.5 * 1000; // If we don't have any more available playlists, we don't want to 53143 // timeout the request. 53144 53145 if (isLowestEnabledRendition(_this2.masterPlaylistLoader_.master, _this2.masterPlaylistLoader_.media())) { 53146 _this2.requestOptions_.timeout = 0; 53147 } else { 53148 _this2.requestOptions_.timeout = requestTimeout; 53149 } // TODO: Create a new event on the PlaylistLoader that signals 53150 // that the segments have changed in some way and use that to 53151 // update the SegmentLoader instead of doing it twice here and 53152 // on `loadedplaylist` 53153 53154 53155 _this2.mainSegmentLoader_.playlist(media, _this2.requestOptions_); 53156 53157 _this2.mainSegmentLoader_.load(); 53158 53159 _this2.tech_.trigger({ 53160 type: 'mediachange', 53161 bubbles: true 53162 }); 53163 }); 53164 this.masterPlaylistLoader_.on('playlistunchanged', function () { 53165 var updatedPlaylist = _this2.masterPlaylistLoader_.media(); 53166 53167 var playlistOutdated = _this2.stuckAtPlaylistEnd_(updatedPlaylist); 53168 53169 if (playlistOutdated) { 53170 // Playlist has stopped updating and we're stuck at its end. Try to 53171 // blacklist it and switch to another playlist in the hope that that 53172 // one is updating (and give the player a chance to re-adjust to the 53173 // safe live point). 53174 _this2.blacklistCurrentPlaylist({ 53175 message: 'Playlist no longer updating.' 53176 }); // useful for monitoring QoS 53177 53178 53179 _this2.tech_.trigger('playliststuck'); 53180 } 53181 }); 53182 this.masterPlaylistLoader_.on('renditiondisabled', function () { 53183 _this2.tech_.trigger({ 53184 type: 'usage', 53185 name: 'hls-rendition-disabled' 53186 }); 53187 }); 53188 this.masterPlaylistLoader_.on('renditionenabled', function () { 53189 _this2.tech_.trigger({ 53190 type: 'usage', 53191 name: 'hls-rendition-enabled' 53192 }); 53193 }); 53194 } 53195 /** 53196 * A helper function for triggerring presence usage events once per source 53197 * 53198 * @private 53199 */ 53200 53201 }, { 53202 key: 'triggerPresenceUsage_', 53203 value: function triggerPresenceUsage_(master, media) { 53204 var mediaGroups = master.mediaGroups || {}; 53205 var defaultDemuxed = true; 53206 var audioGroupKeys = Object.keys(mediaGroups.AUDIO); 53207 53208 for (var mediaGroup in mediaGroups.AUDIO) { 53209 for (var label in mediaGroups.AUDIO[mediaGroup]) { 53210 var properties = mediaGroups.AUDIO[mediaGroup][label]; 53211 53212 if (!properties.uri) { 53213 defaultDemuxed = false; 53214 } 53215 } 53216 } 53217 53218 if (defaultDemuxed) { 53219 this.tech_.trigger({ 53220 type: 'usage', 53221 name: 'hls-demuxed' 53222 }); 53223 } 53224 53225 if (Object.keys(mediaGroups.SUBTITLES).length) { 53226 this.tech_.trigger({ 53227 type: 'usage', 53228 name: 'hls-webvtt' 53229 }); 53230 } 53231 53232 if (Hls.Playlist.isAes(media)) { 53233 this.tech_.trigger({ 53234 type: 'usage', 53235 name: 'hls-aes' 53236 }); 53237 } 53238 53239 if (Hls.Playlist.isFmp4(media)) { 53240 this.tech_.trigger({ 53241 type: 'usage', 53242 name: 'hls-fmp4' 53243 }); 53244 } 53245 53246 if (audioGroupKeys.length && Object.keys(mediaGroups.AUDIO[audioGroupKeys[0]]).length > 1) { 53247 this.tech_.trigger({ 53248 type: 'usage', 53249 name: 'hls-alternate-audio' 53250 }); 53251 } 53252 53253 if (this.useCueTags_) { 53254 this.tech_.trigger({ 53255 type: 'usage', 53256 name: 'hls-playlist-cue-tags' 53257 }); 53258 } 53259 } 53260 /** 53261 * Register event handlers on the segment loaders. A helper function 53262 * for construction time. 53263 * 53264 * @private 53265 */ 53266 53267 }, { 53268 key: 'setupSegmentLoaderListeners_', 53269 value: function setupSegmentLoaderListeners_() { 53270 var _this3 = this; 53271 53272 this.mainSegmentLoader_.on('bandwidthupdate', function () { 53273 var nextPlaylist = _this3.selectPlaylist(); 53274 53275 var currentPlaylist = _this3.masterPlaylistLoader_.media(); 53276 53277 var buffered = _this3.tech_.buffered(); 53278 53279 var forwardBuffer = buffered.length ? buffered.end(buffered.length - 1) - _this3.tech_.currentTime() : 0; 53280 53281 var bufferLowWaterLine = _this3.bufferLowWaterLine(); // If the playlist is live, then we want to not take low water line into account. 53282 // This is because in LIVE, the player plays 3 segments from the end of the 53283 // playlist, and if `BUFFER_LOW_WATER_LINE` is greater than the duration availble 53284 // in those segments, a viewer will never experience a rendition upswitch. 53285 53286 53287 if (!currentPlaylist.endList || // For the same reason as LIVE, we ignore the low water line when the VOD 53288 // duration is below the max potential low water line 53289 _this3.duration() < Config.MAX_BUFFER_LOW_WATER_LINE || // we want to switch down to lower resolutions quickly to continue playback, but 53290 nextPlaylist.attributes.BANDWIDTH < currentPlaylist.attributes.BANDWIDTH || // ensure we have some buffer before we switch up to prevent us running out of 53291 // buffer while loading a higher rendition. 53292 forwardBuffer >= bufferLowWaterLine) { 53293 _this3.masterPlaylistLoader_.media(nextPlaylist); 53294 } 53295 53296 _this3.tech_.trigger('bandwidthupdate'); 53297 }); 53298 this.mainSegmentLoader_.on('progress', function () { 53299 _this3.trigger('progress'); 53300 }); 53301 this.mainSegmentLoader_.on('error', function () { 53302 _this3.blacklistCurrentPlaylist(_this3.mainSegmentLoader_.error()); 53303 }); 53304 this.mainSegmentLoader_.on('syncinfoupdate', function () { 53305 _this3.onSyncInfoUpdate_(); 53306 }); 53307 this.mainSegmentLoader_.on('timestampoffset', function () { 53308 _this3.tech_.trigger({ 53309 type: 'usage', 53310 name: 'hls-timestamp-offset' 53311 }); 53312 }); 53313 this.audioSegmentLoader_.on('syncinfoupdate', function () { 53314 _this3.onSyncInfoUpdate_(); 53315 }); 53316 this.mainSegmentLoader_.on('ended', function () { 53317 _this3.onEndOfStream(); 53318 }); 53319 this.mainSegmentLoader_.on('earlyabort', function () { 53320 _this3.blacklistCurrentPlaylist({ 53321 message: 'Aborted early because there isn\'t enough bandwidth to complete the ' + 'request without rebuffering.' 53322 }, ABORT_EARLY_BLACKLIST_SECONDS); 53323 }); 53324 this.mainSegmentLoader_.on('reseteverything', function () { 53325 // If playing an MTS stream, a videojs.MediaSource is listening for 53326 // hls-reset to reset caption parsing state in the transmuxer 53327 _this3.tech_.trigger('hls-reset'); 53328 }); 53329 this.mainSegmentLoader_.on('segmenttimemapping', function (event) { 53330 // If playing an MTS stream in html, a videojs.MediaSource is listening for 53331 // hls-segment-time-mapping update its internal mapping of stream to display time 53332 _this3.tech_.trigger({ 53333 type: 'hls-segment-time-mapping', 53334 mapping: event.mapping 53335 }); 53336 }); 53337 this.audioSegmentLoader_.on('ended', function () { 53338 _this3.onEndOfStream(); 53339 }); 53340 } 53341 }, { 53342 key: 'mediaSecondsLoaded_', 53343 value: function mediaSecondsLoaded_() { 53344 return Math.max(this.audioSegmentLoader_.mediaSecondsLoaded + this.mainSegmentLoader_.mediaSecondsLoaded); 53345 } 53346 /** 53347 * Call load on our SegmentLoaders 53348 */ 53349 53350 }, { 53351 key: 'load', 53352 value: function load() { 53353 this.mainSegmentLoader_.load(); 53354 53355 if (this.mediaTypes_.AUDIO.activePlaylistLoader) { 53356 this.audioSegmentLoader_.load(); 53357 } 53358 53359 if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) { 53360 this.subtitleSegmentLoader_.load(); 53361 } 53362 } 53363 /** 53364 * Re-tune playback quality level for the current player 53365 * conditions without performing destructive actions, like 53366 * removing already buffered content 53367 * 53368 * @private 53369 */ 53370 53371 }, { 53372 key: 'smoothQualityChange_', 53373 value: function smoothQualityChange_() { 53374 var media = this.selectPlaylist(); 53375 53376 if (media !== this.masterPlaylistLoader_.media()) { 53377 this.masterPlaylistLoader_.media(media); 53378 this.mainSegmentLoader_.resetLoader(); // don't need to reset audio as it is reset when media changes 53379 } 53380 } 53381 /** 53382 * Re-tune playback quality level for the current player 53383 * conditions. This method will perform destructive actions like removing 53384 * already buffered content in order to readjust the currently active 53385 * playlist quickly. This is good for manual quality changes 53386 * 53387 * @private 53388 */ 53389 53390 }, { 53391 key: 'fastQualityChange_', 53392 value: function fastQualityChange_() { 53393 var _this4 = this; 53394 53395 var media = this.selectPlaylist(); 53396 53397 if (media === this.masterPlaylistLoader_.media()) { 53398 return; 53399 } 53400 53401 this.masterPlaylistLoader_.media(media); // Delete all buffered data to allow an immediate quality switch, then seek to give 53402 // the browser a kick to remove any cached frames from the previous rendtion (.04 seconds 53403 // ahead is roughly the minimum that will accomplish this across a variety of content 53404 // in IE and Edge, but seeking in place is sufficient on all other browsers) 53405 // Edge/IE bug: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/14600375/ 53406 // Chrome bug: https://bugs.chromium.org/p/chromium/issues/detail?id=651904 53407 53408 this.mainSegmentLoader_.resetEverything(function () { 53409 // Since this is not a typical seek, we avoid the seekTo method which can cause segments 53410 // from the previously enabled rendition to load before the new playlist has finished loading 53411 if (videojs$1.browser.IE_VERSION || videojs$1.browser.IS_EDGE) { 53412 _this4.tech_.setCurrentTime(_this4.tech_.currentTime() + 0.04); 53413 } else { 53414 _this4.tech_.setCurrentTime(_this4.tech_.currentTime()); 53415 } 53416 }); // don't need to reset audio as it is reset when media changes 53417 } 53418 /** 53419 * Begin playback. 53420 */ 53421 53422 }, { 53423 key: 'play', 53424 value: function play() { 53425 if (this.setupFirstPlay()) { 53426 return; 53427 } 53428 53429 if (this.tech_.ended()) { 53430 this.seekTo_(0); 53431 } 53432 53433 if (this.hasPlayed_()) { 53434 this.load(); 53435 } 53436 53437 var seekable$$1 = this.tech_.seekable(); // if the viewer has paused and we fell out of the live window, 53438 // seek forward to the live point 53439 53440 if (this.tech_.duration() === Infinity) { 53441 if (this.tech_.currentTime() < seekable$$1.start(0)) { 53442 return this.seekTo_(seekable$$1.end(seekable$$1.length - 1)); 53443 } 53444 } 53445 } 53446 /** 53447 * Seek to the latest media position if this is a live video and the 53448 * player and video are loaded and initialized. 53449 */ 53450 53451 }, { 53452 key: 'setupFirstPlay', 53453 value: function setupFirstPlay() { 53454 var _this5 = this; 53455 53456 var media = this.masterPlaylistLoader_.media(); // Check that everything is ready to begin buffering for the first call to play 53457 // If 1) there is no active media 53458 // 2) the player is paused 53459 // 3) the first play has already been setup 53460 // then exit early 53461 53462 if (!media || this.tech_.paused() || this.hasPlayed_()) { 53463 return false; 53464 } // when the video is a live stream 53465 53466 53467 if (!media.endList) { 53468 var seekable$$1 = this.seekable(); 53469 53470 if (!seekable$$1.length) { 53471 // without a seekable range, the player cannot seek to begin buffering at the live 53472 // point 53473 return false; 53474 } 53475 53476 if (videojs$1.browser.IE_VERSION && this.tech_.readyState() === 0) { 53477 // IE11 throws an InvalidStateError if you try to set currentTime while the 53478 // readyState is 0, so it must be delayed until the tech fires loadedmetadata. 53479 this.tech_.one('loadedmetadata', function () { 53480 _this5.trigger('firstplay'); 53481 53482 _this5.seekTo_(seekable$$1.end(0)); 53483 53484 _this5.hasPlayed_ = function () { 53485 return true; 53486 }; 53487 }); 53488 return false; 53489 } // trigger firstplay to inform the source handler to ignore the next seek event 53490 53491 53492 this.trigger('firstplay'); // seek to the live point 53493 53494 this.seekTo_(seekable$$1.end(0)); 53495 } 53496 53497 this.hasPlayed_ = function () { 53498 return true; 53499 }; // we can begin loading now that everything is ready 53500 53501 53502 this.load(); 53503 return true; 53504 } 53505 /** 53506 * handle the sourceopen event on the MediaSource 53507 * 53508 * @private 53509 */ 53510 53511 }, { 53512 key: 'handleSourceOpen_', 53513 value: function handleSourceOpen_() { 53514 // Only attempt to create the source buffer if none already exist. 53515 // handleSourceOpen is also called when we are "re-opening" a source buffer 53516 // after `endOfStream` has been called (in response to a seek for instance) 53517 try { 53518 this.setupSourceBuffers_(); 53519 } catch (e) { 53520 videojs$1.log.warn('Failed to create Source Buffers', e); 53521 return this.mediaSource.endOfStream('decode'); 53522 } // if autoplay is enabled, begin playback. This is duplicative of 53523 // code in video.js but is required because play() must be invoked 53524 // *after* the media source has opened. 53525 53526 53527 if (this.tech_.autoplay()) { 53528 var playPromise = this.tech_.play(); // Catch/silence error when a pause interrupts a play request 53529 // on browsers which return a promise 53530 53531 if (typeof playPromise !== 'undefined' && typeof playPromise.then === 'function') { 53532 playPromise.then(null, function (e) {}); 53533 } 53534 } 53535 53536 this.trigger('sourceopen'); 53537 } 53538 /** 53539 * Calls endOfStream on the media source when all active stream types have called 53540 * endOfStream 53541 * 53542 * @param {string} streamType 53543 * Stream type of the segment loader that called endOfStream 53544 * @private 53545 */ 53546 53547 }, { 53548 key: 'onEndOfStream', 53549 value: function onEndOfStream() { 53550 var isEndOfStream = this.mainSegmentLoader_.ended_; 53551 53552 if (this.mediaTypes_.AUDIO.activePlaylistLoader) { 53553 // if the audio playlist loader exists, then alternate audio is active 53554 if (!this.mainSegmentLoader_.startingMedia_ || this.mainSegmentLoader_.startingMedia_.containsVideo) { 53555 // if we do not know if the main segment loader contains video yet or if we 53556 // definitively know the main segment loader contains video, then we need to wait 53557 // for both main and audio segment loaders to call endOfStream 53558 isEndOfStream = isEndOfStream && this.audioSegmentLoader_.ended_; 53559 } else { 53560 // otherwise just rely on the audio loader 53561 isEndOfStream = this.audioSegmentLoader_.ended_; 53562 } 53563 } 53564 53565 if (!isEndOfStream) { 53566 return; 53567 } 53568 53569 this.logger_('calling mediaSource.endOfStream()'); // on chrome calling endOfStream can sometimes cause an exception, 53570 // even when the media source is in a valid state. 53571 53572 try { 53573 this.mediaSource.endOfStream(); 53574 } catch (e) { 53575 videojs$1.log.warn('Failed to call media source endOfStream', e); 53576 } 53577 } 53578 /** 53579 * Check if a playlist has stopped being updated 53580 * @param {Object} playlist the media playlist object 53581 * @return {boolean} whether the playlist has stopped being updated or not 53582 */ 53583 53584 }, { 53585 key: 'stuckAtPlaylistEnd_', 53586 value: function stuckAtPlaylistEnd_(playlist) { 53587 var seekable$$1 = this.seekable(); 53588 53589 if (!seekable$$1.length) { 53590 // playlist doesn't have enough information to determine whether we are stuck 53591 return false; 53592 } 53593 53594 var expired = this.syncController_.getExpiredTime(playlist, this.mediaSource.duration); 53595 53596 if (expired === null) { 53597 return false; 53598 } // does not use the safe live end to calculate playlist end, since we 53599 // don't want to say we are stuck while there is still content 53600 53601 53602 var absolutePlaylistEnd = Hls.Playlist.playlistEnd(playlist, expired); 53603 var currentTime = this.tech_.currentTime(); 53604 var buffered = this.tech_.buffered(); 53605 53606 if (!buffered.length) { 53607 // return true if the playhead reached the absolute end of the playlist 53608 return absolutePlaylistEnd - currentTime <= SAFE_TIME_DELTA; 53609 } 53610 53611 var bufferedEnd = buffered.end(buffered.length - 1); // return true if there is too little buffer left and buffer has reached absolute 53612 // end of playlist 53613 53614 return bufferedEnd - currentTime <= SAFE_TIME_DELTA && absolutePlaylistEnd - bufferedEnd <= SAFE_TIME_DELTA; 53615 } 53616 /** 53617 * Blacklists a playlist when an error occurs for a set amount of time 53618 * making it unavailable for selection by the rendition selection algorithm 53619 * and then forces a new playlist (rendition) selection. 53620 * 53621 * @param {Object=} error an optional error that may include the playlist 53622 * to blacklist 53623 * @param {Number=} blacklistDuration an optional number of seconds to blacklist the 53624 * playlist 53625 */ 53626 53627 }, { 53628 key: 'blacklistCurrentPlaylist', 53629 value: function blacklistCurrentPlaylist() { 53630 var error = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 53631 var blacklistDuration = arguments[1]; 53632 var currentPlaylist = void 0; 53633 var nextPlaylist = void 0; // If the `error` was generated by the playlist loader, it will contain 53634 // the playlist we were trying to load (but failed) and that should be 53635 // blacklisted instead of the currently selected playlist which is likely 53636 // out-of-date in this scenario 53637 53638 currentPlaylist = error.playlist || this.masterPlaylistLoader_.media(); 53639 blacklistDuration = blacklistDuration || error.blacklistDuration || this.blackl
53639istDuration; // If there is no current playlist, then an error occurred while we were 53640 // trying to load the master OR while we were disposing of the tech 53641 53642 if (!currentPlaylist) { 53643 this.error = error; 53644 53645 try { 53646 return this.mediaSource.endOfStream('network'); 53647 } catch (e) { 53648 return this.trigger('error'); 53649 } 53650 } 53651 53652 var isFinalRendition = this.masterPlaylistLoader_.master.playlists.filter(isEnabled).length === 1; 53653 var playlists = this.masterPlaylistLoader_.master.playlists; 53654 53655 if (playlists.length === 1) { 53656 // Never blacklisting this playlist because it's the only playlist 53657 videojs$1.log.warn('Problem encountered with the current ' + 'HLS playlist. Trying again since it is the only playlist.'); 53658 this.tech_.trigger('retryplaylist'); 53659 return this.masterPlaylistLoader_.load(isFinalRendition); 53660 } 53661 53662 if (isFinalRendition) { 53663 // Since we're on the final non-blacklisted playlist, and we're about to blacklist 53664 // it, instead of erring the player or retrying this playlist, clear out the current 53665 // blacklist. This allows other playlists to be attempted in case any have been 53666 // fixed. 53667 videojs$1.log.warn('Removing all playlists from the blacklist because the last ' + 'rendition is about to be blacklisted.');
vendor: 11,849 bytes, lines 53668-54010
53668 playlists.forEach(function (playlist) { 53669 if (playlist.excludeUntil !== Infinity) { 53670 delete playlist.excludeUntil; 53671 } 53672 }); // Technically we are retrying a playlist, in that we are simply retrying a previous 53673 // playlist. This is needed for users relying on the retryplaylist event to catch a 53674 // case where the player might be stuck and looping through "dead" playlists. 53675 53676 this.tech_.trigger('retryplaylist'); 53677 } // Blacklist this playlist 53678 53679 53680 currentPlaylist.excludeUntil = Date.now() + blacklistDuration * 1000; 53681 this.tech_.trigger('blacklistplaylist'); 53682 this.tech_.trigger({ 53683 type: 'usage', 53684 name: 'hls-rendition-blacklisted' 53685 }); // Select a new playlist 53686 53687 nextPlaylist = this.selectPlaylist(); 53688 videojs$1.log.warn('Problem encountered with the current HLS playlist.' + (error.message ? ' ' + error.message : '') + ' Switching to another playlist.'); 53689 return this.masterPlaylistLoader_.media(nextPlaylist, isFinalRendition); 53690 } 53691 /** 53692 * Pause all segment loaders 53693 */ 53694 53695 }, { 53696 key: 'pauseLoading', 53697 value: function pauseLoading() { 53698 this.mainSegmentLoader_.pause(); 53699 53700 if (this.mediaTypes_.AUDIO.activePlaylistLoader) { 53701 this.audioSegmentLoader_.pause(); 53702 } 53703 53704 if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) { 53705 this.subtitleSegmentLoader_.pause(); 53706 } 53707 } 53708 /** 53709 * set the current time on all segment loaders 53710 * 53711 * @param {TimeRange} currentTime the current time to set 53712 * @return {TimeRange} the current time 53713 */ 53714 53715 }, { 53716 key: 'setCurrentTime', 53717 value: function setCurrentTime(currentTime) { 53718 var buffered = findRange(this.tech_.buffered(), currentTime); 53719 53720 if (!(this.masterPlaylistLoader_ && this.masterPlaylistLoader_.media())) { 53721 // return immediately if the metadata is not ready yet 53722 return 0; 53723 } // it's clearly an edge-case but don't thrown an error if asked to 53724 // seek within an empty playlist 53725 53726 53727 if (!this.masterPlaylistLoader_.media().segments) { 53728 return 0; 53729 } // In flash playback, the segment loaders should be reset on every seek, even 53730 // in buffer seeks. If the seek location is already buffered, continue buffering as 53731 // usual 53732 // TODO: redo this comment 53733 53734 53735 if (buffered && buffered.length) { 53736 return currentTime; 53737 } // cancel outstanding requests so we begin buffering at the new 53738 // location 53739 53740 53741 this.mainSegmentLoader_.resetEverything(); 53742 this.mainSegmentLoader_.abort(); 53743 53744 if (this.mediaTypes_.AUDIO.activePlaylistLoader) { 53745 this.audioSegmentLoader_.resetEverything(); 53746 this.audioSegmentLoader_.abort(); 53747 } 53748 53749 if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) { 53750 this.subtitleSegmentLoader_.resetEverything(); 53751 this.subtitleSegmentLoader_.abort(); 53752 } // start segment loader loading in case they are paused 53753 53754 53755 this.load(); 53756 } 53757 /** 53758 * get the current duration 53759 * 53760 * @return {TimeRange} the duration 53761 */ 53762 53763 }, { 53764 key: 'duration', 53765 value: function duration$$1() { 53766 if (!this.masterPlaylistLoader_) { 53767 return 0; 53768 } 53769 53770 if (this.mediaSource) { 53771 return this.mediaSource.duration; 53772 } 53773 53774 return Hls.Playlist.duration(this.masterPlaylistLoader_.media()); 53775 } 53776 /** 53777 * check the seekable range 53778 * 53779 * @return {TimeRange} the seekable range 53780 */ 53781 53782 }, { 53783 key: 'seekable', 53784 value: function seekable$$1() { 53785 return this.seekable_; 53786 } 53787 }, { 53788 key: 'onSyncInfoUpdate_', 53789 value: function onSyncInfoUpdate_() { 53790 var mainSeekable = void 0; 53791 var audioSeekable = void 0; 53792 53793 if (!this.masterPlaylistLoader_) { 53794 return; 53795 } 53796 53797 var media = this.masterPlaylistLoader_.media(); 53798 53799 if (!media) { 53800 return; 53801 } 53802 53803 var expired = this.syncController_.getExpiredTime(media, this.mediaSource.duration); 53804 53805 if (expired === null) { 53806 // not enough information to update seekable 53807 return; 53808 } 53809 53810 mainSeekable = Hls.Playlist.seekable(media, expired); 53811 53812 if (mainSeekable.length === 0) { 53813 return; 53814 } 53815 53816 if (this.mediaTypes_.AUDIO.activePlaylistLoader) { 53817 media = this.mediaTypes_.AUDIO.activePlaylistLoader.media(); 53818 expired = this.syncController_.getExpiredTime(media, this.mediaSource.duration); 53819 53820 if (expired === null) { 53821 return; 53822 } 53823 53824 audioSeekable = Hls.Playlist.seekable(media, expired); 53825 53826 if (audioSeekable.length === 0) { 53827 return; 53828 } 53829 } 53830 53831 var oldEnd = void 0; 53832 var oldStart = void 0; 53833 53834 if (this.seekable_ && this.seekable_.length) { 53835 oldEnd = this.seekable_.end(0); 53836 oldStart = this.seekable_.start(0); 53837 } 53838 53839 if (!audioSeekable) { 53840 // seekable has been calculated based on buffering video data so it 53841 // can be returned directly 53842 this.seekable_ = mainSeekable; 53843 } else if (audioSeekable.start(0) > mainSeekable.end(0) || mainSeekable.start(0) > audioSeekable.end(0)) { 53844 // seekables are pretty far off, rely on main 53845 this.seekable_ = mainSeekable; 53846 } else { 53847 this.seekable_ = videojs$1.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)]]); 53848 } // seekable is the same as last time 53849 53850 53851 if (this.seekable_ && this.seekable_.length) { 53852 if (this.seekable_.end(0) === oldEnd && this.seekable_.start(0) === oldStart) { 53853 return; 53854 } 53855 } 53856 53857 this.logger_('seekable updated [' + printableRange(this.seekable_) + ']'); 53858 this.tech_.trigger('seekablechanged'); 53859 } 53860 /** 53861 * Update the player duration 53862 */ 53863 53864 }, { 53865 key: 'updateDuration', 53866 value: function updateDuration() { 53867 var _this6 = this; 53868 53869 var oldDuration = this.mediaSource.duration; 53870 var newDuration = Hls.Playlist.duration(this.masterPlaylistLoader_.media()); 53871 var buffered = this.tech_.buffered(); 53872 53873 var setDuration = function setDuration() { 53874 // on firefox setting the duration may sometimes cause an exception 53875 // even if the media source is open and source buffers are not 53876 // updating, something about the media source being in an invalid state. 53877 _this6.logger_('Setting duration from ' + _this6.mediaSource.duration + ' => ' + newDuration); 53878 53879 try { 53880 _this6.mediaSource.duration = newDuration; 53881 } catch (e) { 53882 videojs$1.log.warn('Failed to set media source duration', e); 53883 } 53884 53885 _this6.tech_.trigger('durationchange'); 53886 53887 _this6.mediaSource.removeEventListener('sourceopen', setDuration); 53888 }; 53889 53890 if (buffered.length > 0) { 53891 newDuration = Math.max(newDuration, buffered.end(buffered.length - 1)); 53892 } // if the duration has changed, invalidate the cached value 53893 53894 53895 if (oldDuration !== newDuration) { 53896 // update the duration 53897 if (this.mediaSource.readyState !== 'open') { 53898 this.mediaSource.addEventListener('sourceopen', setDuration); 53899 } else { 53900 setDuration(); 53901 } 53902 } 53903 } 53904 /** 53905 * dispose of the MasterPlaylistController and everything 53906 * that it controls 53907 */ 53908 53909 }, { 53910 key: 'dispose', 53911 value: function dispose() { 53912 var _this7 = this; 53913 53914 this.decrypter_.terminate(); 53915 this.masterPlaylistLoader_.dispose(); 53916 this.mainSegmentLoader_.dispose(); 53917 ['AUDIO', 'SUBTITLES'].forEach(function (type) { 53918 var groups = _this7.mediaTypes_[type].groups; 53919 53920 for (var id in groups) { 53921 groups[id].forEach(function (group) { 53922 if (group.playlistLoader) { 53923 group.playlistLoader.dispose(); 53924 } 53925 }); 53926 } 53927 }); 53928 this.audioSegmentLoader_.dispose(); 53929 this.subtitleSegmentLoader_.dispose(); 53930 } 53931 /** 53932 * return the master playlist object if we have one 53933 * 53934 * @return {Object} the master playlist object that we parsed 53935 */ 53936 53937 }, { 53938 key: 'master', 53939 value: function master() { 53940 return this.masterPlaylistLoader_.master; 53941 } 53942 /** 53943 * return the currently selected playlist 53944 * 53945 * @return {Object} the currently selected playlist object that we parsed 53946 */ 53947 53948 }, { 53949 key: 'media', 53950 value: function media() { 53951 // playlist loader will not return media if it has not been fully loaded 53952 return this.masterPlaylistLoader_.media() || this.initialMedia_; 53953 } 53954 /** 53955 * setup our internal source buffers on our segment Loaders 53956 * 53957 * @private 53958 */ 53959 53960 }, { 53961 key: 'setupSourceBuffers_', 53962 value: function setupSourceBuffers_() { 53963 var media = this.masterPlaylistLoader_.media(); 53964 var mimeTypes = void 0; // wait until a media playlist is available and the Media Source is 53965 // attached 53966 53967 if (!media || this.mediaSource.readyState !== 'open') { 53968 return; 53969 } 53970 53971 mimeTypes = mimeTypesForPlaylist(this.masterPlaylistLoader_.master, media); 53972 53973 if (mimeTypes.length < 1) { 53974 this.error = 'No compatible SourceBuffer configuration for the variant stream:' + media.resolvedUri; 53975 return this.mediaSource.endOfStream('decode'); 53976 } 53977 53978 this.configureLoaderMimeTypes_(mimeTypes); // exclude any incompatible variant streams from future playlist 53979 // selection 53980 53981 this.excludeIncompatibleVariants_(media); 53982 } 53983 }, { 53984 key: 'configureLoaderMimeTypes_', 53985 value: function configureLoaderMimeTypes_(mimeTypes) { 53986 // If the content is demuxed, we can't start appending segments to a source buffer 53987 // until both source buffers are set up, or else the browser may not let us add the 53988 // second source buffer (it will assume we are playing either audio only or video 53989 // only). 53990 var sourceBufferEmitter = // If there is more than one mime type 53991 mimeTypes.length > 1 && // and the first mime type does not have muxed video and audio 53992 mimeTypes[0].indexOf(',') === -1 && // and the two mime types are different (they can be the same in the case of audio 53993 // only with alternate audio) 53994 mimeTypes[0] !== mimeTypes[1] ? // then we want to wait on the second source buffer 53995 new videojs$1.EventTarget() : // otherwise there is no need to wait as the content is either audio only, 53996 // video only, or muxed content. 53997 null; 53998 this.mainSegmentLoader_.mimeType(mimeTypes[0], sourceBufferEmitter); 53999 54000 if (mimeTypes[1]) { 54001 this.audioSegmentLoader_.mimeType(mimeTypes[1], sourceBufferEmitter); 54002 } 54003 } 54004 /** 54005 * Blacklists playlists with codecs that are unsupported by the browser. 54006 */ 54007 54008 }, { 54009 key: 'excludeUnsupportedVariants_', 54010 value: function excludeUnsupportedVariants_() {
54011 this.master().playlists.forEach(function (variant) { 54012 if (variant.attributes.CODECS && window$1.MediaSource && window$1.MediaSource.isTypeSupported && !window$1.MediaSource.isTypeSupported('video/mp4; codecs="' + mapLegacyAvcCodecs(variant.attributes.CODECS) + '"')) { 54013 variant.excludeUntil = Infinity; 54014 } 54015 }); 54016 } 54017 /** 54018 * Blacklist playlists that are known to be codec or 54019 * stream-incompatible with the SourceBuffer configuration. For 54020 * instance, Media Source Extensions would cause the video element to 54021 * stall waiting for video data if you switched from a variant with 54022 * video and audio to an audio-only one. 54023 * 54024 * @param {Object} media a media playlist compatible with the current 54025 * set of SourceBuffers. Variants in the current master playlist that 54026 * do not appear to have compatible codec or stream configurations 54027 * will be excluded from the default playlist selection algorithm 54028 * indefinitely. 54029 * @private 54030 */ 54031 54032 }, { 54033 key: 'excludeIncompatibleVariants_', 54034 value: function excludeIncompatibleVariants_(media) { 54035 var codecCount = 2; 54036 var videoCodec = null; 54037 var codecs = void 0; 54038 54039 if (media.attributes.CODECS) { 54040 codecs = parseCodecs(media.attributes.CODECS); 54041 videoCodec = codecs.videoCodec; 54042 codecCount = codecs.codecCount; 54043 } 54044
vendor: 28,187 bytes, lines 54045-54819
54045 this.master().playlists.forEach(function (variant) { 54046 var variantCodecs = { 54047 codecCount: 2, 54048 videoCodec: null 54049 }; 54050 54051 if (variant.attributes.CODECS) { 54052 variantCodecs = parseCodecs(variant.attributes.CODECS); 54053 } // if the streams differ in the presence or absence of audio or 54054 // video, they are incompatible 54055 54056 54057 if (variantCodecs.codecCount !== codecCount) { 54058 variant.excludeUntil = Infinity; 54059 } // if h.264 is specified on the current playlist, some flavor of 54060 // it must be specified on all compatible variants 54061 54062 54063 if (variantCodecs.videoCodec !== videoCodec) { 54064 variant.excludeUntil = Infinity; 54065 } 54066 }); 54067 } 54068 }, { 54069 key: 'updateAdCues_', 54070 value: function updateAdCues_(media) { 54071 var offset = 0; 54072 var seekable$$1 = this.seekable(); 54073 54074 if (seekable$$1.length) { 54075 offset = seekable$$1.start(0); 54076 } 54077 54078 updateAdCues(media, this.cueTagsTrack_, offset); 54079 } 54080 /** 54081 * Calculates the desired forward buffer length based on current time 54082 * 54083 * @return {Number} Desired forward buffer length in seconds 54084 */ 54085 54086 }, { 54087 key: 'goalBufferLength', 54088 value: function goalBufferLength() { 54089 var currentTime = this.tech_.currentTime(); 54090 var initial = Config.GOAL_BUFFER_LENGTH; 54091 var rate = Config.GOAL_BUFFER_LENGTH_RATE; 54092 var max = Math.max(initial, Config.MAX_GOAL_BUFFER_LENGTH); 54093 return Math.min(initial + currentTime * rate, max); 54094 } 54095 /** 54096 * Calculates the desired buffer low water line based on current time 54097 * 54098 * @return {Number} Desired buffer low water line in seconds 54099 */ 54100 54101 }, { 54102 key: 'bufferLowWaterLine', 54103 value: function bufferLowWaterLine() { 54104 var currentTime = this.tech_.currentTime(); 54105 var initial = Config.BUFFER_LOW_WATER_LINE; 54106 var rate = Config.BUFFER_LOW_WATER_LINE_RATE; 54107 var max = Math.max(initial, Config.MAX_BUFFER_LOW_WATER_LINE); 54108 return Math.min(initial + currentTime * rate, max); 54109 } 54110 }]); 54111 return MasterPlaylistController; 54112 }(videojs$1.EventTarget); 54113 /** 54114 * Returns a function that acts as the Enable/disable playlist function. 54115 * 54116 * @param {PlaylistLoader} loader - The master playlist loader 54117 * @param {String} playlistUri - uri of the playlist 54118 * @param {Function} changePlaylistFn - A function to be called after a 54119 * playlist's enabled-state has been changed. Will NOT be called if a 54120 * playlist's enabled-state is unchanged 54121 * @param {Boolean=} enable - Value to set the playlist enabled-state to 54122 * or if undefined returns the current enabled-state for the playlist 54123 * @return {Function} Function for setting/getting enabled 54124 */ 54125 54126 54127 var enableFunction = function enableFunction(loader, playlistUri, changePlaylistFn) { 54128 return function (enable) { 54129 var playlist = loader.master.playlists[playlistUri]; 54130 var incompatible = isIncompatible(playlist); 54131 var currentlyEnabled = isEnabled(playlist); 54132 54133 if (typeof enable === 'undefined') { 54134 return currentlyEnabled; 54135 } 54136 54137 if (enable) { 54138 delete playlist.disabled; 54139 } else { 54140 playlist.disabled = true; 54141 } 54142 54143 if (enable !== currentlyEnabled && !incompatible) { 54144 // Ensure the outside world knows about our changes 54145 changePlaylistFn(); 54146 54147 if (enable) { 54148 loader.trigger('renditionenabled'); 54149 } else { 54150 loader.trigger('renditiondisabled'); 54151 } 54152 } 54153 54154 return enable; 54155 }; 54156 }; 54157 /** 54158 * The representation object encapsulates the publicly visible information 54159 * in a media playlist along with a setter/getter-type function (enabled) 54160 * for changing the enabled-state of a particular playlist entry 54161 * 54162 * @class Representation 54163 */ 54164 54165 54166 var Representation = function Representation(hlsHandler, playlist, id) { 54167 classCallCheck$1(this, Representation); 54168 var mpc = hlsHandler.masterPlaylistController_, 54169 smoothQualityChange = hlsHandler.options_.smoothQualityChange; // Get a reference to a bound version of the quality change function 54170 54171 var changeType = smoothQualityChange ? 'smooth' : 'fast'; 54172 var qualityChangeFunction = mpc[changeType + 'QualityChange_'].bind(mpc); // some playlist attributes are optional 54173 54174 if (playlist.attributes.RESOLUTION) { 54175 var resolution = playlist.attributes.RESOLUTION; 54176 this.width = resolution.width; 54177 this.height = resolution.height; 54178 } 54179 54180 this.bandwidth = playlist.attributes.BANDWIDTH; // The id is simply the ordinality of the media playlist 54181 // within the master playlist 54182 54183 this.id = id; // Partially-apply the enableFunction to create a playlist- 54184 // specific variant 54185 54186 this.enabled = enableFunction(hlsHandler.playlists, playlist.uri, qualityChangeFunction); 54187 }; 54188 /** 54189 * A mixin function that adds the `representations` api to an instance 54190 * of the HlsHandler class 54191 * @param {HlsHandler} hlsHandler - An instance of HlsHandler to add the 54192 * representation API into 54193 */ 54194 54195 54196 var renditionSelectionMixin = function renditionSelectionMixin(hlsHandler) { 54197 var playlists = hlsHandler.playlists; // Add a single API-specific function to the HlsHandler instance 54198 54199 hlsHandler.representations = function () { 54200 return playlists.master.playlists.filter(function (media) { 54201 return !isIncompatible(media); 54202 }).map(function (e, i) { 54203 return new Representation(hlsHandler, e, e.uri); 54204 }); 54205 }; 54206 }; 54207 /** 54208 * @file playback-watcher.js 54209 * 54210 * Playback starts, and now my watch begins. It shall not end until my death. I shall 54211 * take no wait, hold no uncleared timeouts, father no bad seeks. I shall wear no crowns 54212 * and win no glory. I shall live and die at my post. I am the corrector of the underflow. 54213 * I am the watcher of gaps. I am the shield that guards the realms of seekable. I pledge 54214 * my life and honor to the Playback Watch, for this Player and all the Players to come. 54215 */ 54216 // Set of events that reset the playback-watcher time check logic and clear the timeout 54217 54218 54219 var timerCancelEvents = ['seeking', 'seeked', 'pause', 'playing', 'error']; 54220 /** 54221 * @class PlaybackWatcher 54222 */ 54223 54224 var PlaybackWatcher = function () { 54225 /** 54226 * Represents an PlaybackWatcher object. 54227 * @constructor 54228 * @param {object} options an object that includes the tech and settings 54229 */ 54230 function PlaybackWatcher(options) { 54231 var _this = this; 54232 54233 classCallCheck$1(this, PlaybackWatcher); 54234 this.tech_ = options.tech; 54235 this.seekable = options.seekable; 54236 this.seekTo = options.seekTo; 54237 this.allowSeeksWithinUnsafeLiveWindow = options.allowSeeksWithinUnsafeLiveWindow; 54238 this.media = options.media; 54239 this.consecutiveUpdates = 0; 54240 this.lastRecordedTime = null; 54241 this.timer_ = null; 54242 this.checkCurrentTimeTimeout_ = null; 54243 this.logger_ = logger('PlaybackWatcher'); 54244 this.logger_('initialize'); 54245 54246 var canPlayHandler = function canPlayHandler() { 54247 return _this.monitorCurrentTime_(); 54248 }; 54249 54250 var waitingHandler = function waitingHandler() { 54251 return _this.techWaiting_(); 54252 }; 54253 54254 var cancelTimerHandler = function cancelTimerHandler() { 54255 return _this.cancelTimer_(); 54256 }; 54257 54258 var fixesBadSeeksHandler = function fixesBadSeeksHandler() { 54259 return _this.fixesBadSeeks_(); 54260 }; 54261 54262 this.tech_.on('seekablechanged', fixesBadSeeksHandler); 54263 this.tech_.on('waiting', waitingHandler); 54264 this.tech_.on(timerCancelEvents, cancelTimerHandler); 54265 this.tech_.on('canplay', canPlayHandler); // Define the dispose function to clean up our events 54266 54267 this.dispose = function () { 54268 _this.logger_('dispose'); 54269 54270 _this.tech_.off('seekablechanged', fixesBadSeeksHandler); 54271 54272 _this.tech_.off('waiting', waitingHandler); 54273 54274 _this.tech_.off(timerCancelEvents, cancelTimerHandler); 54275 54276 _this.tech_.off('canplay', canPlayHandler); 54277 54278 if (_this.checkCurrentTimeTimeout_) { 54279 window$1.clearTimeout(_this.checkCurrentTimeTimeout_); 54280 } 54281 54282 _this.cancelTimer_(); 54283 }; 54284 } 54285 /** 54286 * Periodically check current time to see if playback stopped 54287 * 54288 * @private 54289 */ 54290 54291 54292 createClass$1(PlaybackWatcher, [{ 54293 key: 'monitorCurrentTime_', 54294 value: function monitorCurrentTime_() { 54295 this.checkCurrentTime_(); 54296 54297 if (this.checkCurrentTimeTimeout_) { 54298 window$1.clearTimeout(this.checkCurrentTimeTimeout_); 54299 } // 42 = 24 fps // 250 is what Webkit uses // FF uses 15 54300 54301 54302 this.checkCurrentTimeTimeout_ = window$1.setTimeout(this.monitorCurrentTime_.bind(this), 250); 54303 } 54304 /** 54305 * The purpose of this function is to emulate the "waiting" event on 54306 * browsers that do not emit it when they are waiting for more 54307 * data to continue playback 54308 * 54309 * @private 54310 */ 54311 54312 }, { 54313 key: 'checkCurrentTime_', 54314 value: function checkCurrentTime_() { 54315 if (this.tech_.seeking() && this.fixesBadSeeks_()) { 54316 this.consecutiveUpdates = 0; 54317 this.lastRecordedTime = this.tech_.currentTime(); 54318 return; 54319 } 54320 54321 if (this.tech_.paused() || this.tech_.seeking()) { 54322 return; 54323 } 54324 54325 var currentTime = this.tech_.currentTime(); 54326 var buffered = this.tech_.buffered(); 54327 54328 if (this.lastRecordedTime === currentTime && (!buffered.length || currentTime + SAFE_TIME_DELTA >= buffered.end(buffered.length - 1))) { 54329 // If current time is at the end of the final buffered region, then any playback 54330 // stall is most likely caused by buffering in a low bandwidth environment. The tech 54331 // should fire a `waiting` event in this scenario, but due to browser and tech 54332 // inconsistencies. Calling `techWaiting_` here allows us to simulate 54333 // responding to a native `waiting` event when the tech fails to emit one. 54334 return this.techWaiting_(); 54335 } 54336 54337 if (this.consecutiveUpdates >= 5 && currentTime === this.lastRecordedTime) { 54338 this.consecutiveUpdates++; 54339 this.waiting_(); 54340 } else if (currentTime === this.lastRecordedTime) { 54341 this.consecutiveUpdates++; 54342 } else { 54343 this.consecutiveUpdates = 0; 54344 this.lastRecordedTime = currentTime; 54345 } 54346 } 54347 /** 54348 * Cancels any pending timers and resets the 'timeupdate' mechanism 54349 * designed to detect that we are stalled 54350 * 54351 * @private 54352 */ 54353 54354 }, { 54355 key: 'cancelTimer_', 54356 value: function cancelTimer_() { 54357 this.consecutiveUpdates = 0; 54358 54359 if (this.timer_) { 54360 this.logger_('cancelTimer_'); 54361 clearTimeout(this.timer_); 54362 } 54363 54364 this.timer_ = null; 54365 } 54366 /** 54367 * Fixes situations where there's a bad seek 54368 * 54369 * @return {Boolean} whether an action was taken to fix the seek 54370 * @private 54371 */ 54372 54373 }, { 54374 key: 'fixesBadSeeks_', 54375 value: function fixesBadSeeks_() { 54376 var seeking = this.tech_.seeking(); 54377 54378 if (!seeking) { 54379 return false; 54380 } 54381 54382 var seekable = this.seekable(); 54383 var currentTime = this.tech_.currentTime(); 54384 var isAfterSeekableRange = this.afterSeekableWindow_(seekable, currentTime, this.media(), this.allowSeeksWithinUnsafeLiveWindow); 54385 var seekTo = void 0; 54386 54387 if (isAfterSeekableRange) { 54388 var seekableEnd = seekable.end(seekable.length - 1); // sync to live point (if VOD, our seekable was updated and we're simply adjusting) 54389 54390 seekTo = seekableEnd; 54391 } 54392 54393 if (this.beforeSeekableWindow_(seekable, currentTime)) { 54394 var seekableStart = seekable.start(0); // sync to the beginning of the live window 54395 // provide a buffer of .1 seconds to handle rounding/imprecise numbers 54396 54397 seekTo = seekableStart + SAFE_TIME_DELTA; 54398 } 54399 54400 if (typeof seekTo !== 'undefined') { 54401 this.logger_('Trying to seek outside of seekable at time ' + currentTime + ' with ' + ('seekable range ' + printableRange(seekable) + '. Seeking to ') + (seekTo + '.')); 54402 this.seekTo(seekTo); 54403 return true; 54404 } 54405 54406 return false; 54407 } 54408 /** 54409 * Handler for situations when we determine the player is waiting. 54410 * 54411 * @private 54412 */ 54413 54414 }, { 54415 key: 'waiting_', 54416 value: function waiting_() { 54417 if (this.techWaiting_()) { 54418 return; 54419 } // All tech waiting checks failed. Use last resort correction 54420 54421 54422 var currentTime = this.tech_.currentTime(); 54423 var buffered = this.tech_.buffered(); 54424 var currentRange = findRange(buffered, currentTime); // Sometimes the player can stall for unknown reasons within a contiguous buffered 54425 // region with no indication that anything is amiss (seen in Firefox). Seeking to 54426 // currentTime is usually enough to kickstart the player. This checks that the player 54427 // is currently within a buffered region before attempting a corrective seek. 54428 // Chrome does not appear to continue `timeupdate` events after a `waiting` event 54429 // until there is ~ 3 seconds of forward buffer available. PlaybackWatcher should also 54430 // make sure there is ~3 seconds of forward buffer before taking any corrective action 54431 // to avoid triggering an `unknownwaiting` event when the network is slow. 54432 54433 if (currentRange.length && currentTime + 3 <= currentRange.end(0)) { 54434 this.cancelTimer_(); 54435 this.seekTo(currentTime); 54436 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 54437 54438 this.tech_.trigger({ 54439 type: 'usage', 54440 name: 'hls-unknown-waiting' 54441 }); 54442 return; 54443 } 54444 } 54445 /** 54446 * Handler for situations when the tech fires a `waiting` event 54447 * 54448 * @return {Boolean} 54449 * True if an action (or none) was needed to correct the waiting. False if no 54450 * checks passed 54451 * @private 54452 */ 54453 54454 }, { 54455 key: 'techWaiting_', 54456 value: function techWaiting_() { 54457 var seekable = this.seekable(); 54458 var currentTime = this.tech_.currentTime(); 54459 54460 if (this.tech_.seeking() && this.fixesBadSeeks_()) { 54461 // Tech is seeking or bad seek fixed, no action needed 54462 return true; 54463 } 54464 54465 if (this.tech_.seeking() || this.timer_ !== null) { 54466 // Tech is seeking or already waiting on another action, no action needed 54467 return true; 54468 } 54469 54470 if (this.beforeSeekableWindow_(seekable, currentTime)) { 54471 var livePoint = seekable.end(seekable.length - 1); 54472 this.logger_('Fell out of live window at time ' + currentTime + '. Seeking to ' + ('live point (seekable end) ' + livePoint)); 54473 this.cancelTimer_(); 54474 this.seekTo(livePoint); // live window resyncs may be useful for monitoring QoS 54475 54476 this.tech_.trigger({ 54477 type: 'usage', 54478 name: 'hls-live-resync' 54479 }); 54480 return true; 54481 } 54482 54483 var buffered = this.tech_.buffered(); 54484 var nextRange = findNextRange(buffered, currentTime); 54485 54486 if (this.videoUnderflow_(nextRange, buffered, currentTime)) { 54487 // Even though the video underflowed and was stuck in a gap, the audio overplayed 54488 // the gap, leading currentTime into a buffered range. Seeking to currentTime 54489 // allows the video to catch up to the audio position without losing any audio 54490 // (only suffering ~3 seconds of frozen video and a pause in audio playback). 54491 this.cancelTimer_(); 54492 this.seekTo(currentTime); // video underflow may be useful for monitoring QoS 54493 54494 this.tech_.trigger({ 54495 type: 'usage', 54496 name: 'hls-video-underflow' 54497 }); 54498 return true; 54499 } // check for gap 54500 54501 54502 if (nextRange.length > 0) { 54503 var difference = nextRange.start(0) - currentTime; 54504 this.logger_('Stopped at ' + currentTime + ', setting timer for ' + difference + ', seeking ' + ('to ' + nextRange.start(0))); 54505 this.timer_ = setTimeout(this.skipTheGap_.bind(this), difference * 1000, currentTime); 54506 return true; 54507 } // All checks failed. Returning false to indicate failure to correct waiting 54508 54509 54510 return false; 54511 } 54512 }, { 54513 key: 'afterSeekableWindow_', 54514 value: function afterSeekableWindow_(seekable, currentTime, playlist) { 54515 var allowSeeksWithinUnsafeLiveWindow = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false; 54516 54517 if (!seekable.length) { 54518 // we can't make a solid case if there's no seekable, default to false 54519 return false; 54520 } 54521 54522 var allowedEnd = seekable.end(seekable.length - 1) + SAFE_TIME_DELTA; 54523 var isLive = !playlist.endList; 54524 54525 if (isLive && allowSeeksWithinUnsafeLiveWindow) { 54526 allowedEnd = seekable.end(seekable.length - 1) + playlist.targetDuration * 3; 54527 } 54528 54529 if (currentTime > allowedEnd) { 54530 return true; 54531 } 54532 54533 return false; 54534 } 54535 }, { 54536 key: 'beforeSeekableWindow_', 54537 value: function beforeSeekableWindow_(seekable, currentTime) { 54538 if (seekable.length && // can't fall before 0 and 0 seekable start identifies VOD stream 54539 seekable.start(0) > 0 && currentTime < seekable.start(0) - SAFE_TIME_DELTA) { 54540 return true; 54541 } 54542 54543 return false; 54544 } 54545 }, { 54546 key: 'videoUnderflow_', 54547 value: function videoUnderflow_(nextRange, buffered, currentTime) { 54548 if (nextRange.length === 0) { 54549 // Even if there is no available next range, there is still a possibility we are 54550 // stuck in a gap due to video underflow. 54551 var gap = this.gapFromVideoUnderflow_(buffered, currentTime); 54552 54553 if (gap) { 54554 this.logger_('Encountered a gap in video from ' + gap.start + ' to ' + gap.end + '. ' + ('Seeking to current time ' + currentTime)); 54555 return true; 54556 } 54557 } 54558 54559 return false; 54560 } 54561 /** 54562 * Timer callback. If playback still has not proceeded, then we seek 54563 * to the start of the next buffered region. 54564 * 54565 * @private 54566 */ 54567 54568 }, { 54569 key: 'skipTheGap_', 54570 value: function skipTheGap_(scheduledCurrentTime) { 54571 var buffered = this.tech_.buffered(); 54572 var currentTime = this.tech_.currentTime(); 54573 var nextRange = findNextRange(buffered, currentTime); 54574 this.cancelTimer_(); 54575 54576 if (nextRange.length === 0 || currentTime !== scheduledCurrentTime) { 54577 return; 54578 } 54579 54580 this.logger_('skipTheGap_:', 'currentTime:', currentTime, 'scheduled currentTime:', scheduledCurrentTime, 'nextRange start:', nextRange.start(0)); // only seek if we still have not played 54581 54582 this.seekTo(nextRange.start(0) + TIME_FUDGE_FACTOR); 54583 this.tech_.trigger({ 54584 type: 'usage', 54585 name: 'hls-gap-skip' 54586 }); 54587 } 54588 }, { 54589 key: 'gapFromVideoUnderflow_', 54590 value: function gapFromVideoUnderflow_(buffered, currentTime) { 54591 // At least in Chrome, if there is a gap in the video buffer, the audio will continue 54592 // playing for ~3 seconds after the video gap starts. This is done to account for 54593 // video buffer underflow/underrun (note that this is not done when there is audio 54594 // buffer underflow/underrun -- in that case the video will stop as soon as it 54595 // encounters the gap, as audio stalls are more noticeable/jarring to a user than 54596 // video stalls). The player's time will reflect the playthrough of audio, so the 54597 // time will appear as if we are in a buffered region, even if we are stuck in a 54598 // "gap." 54599 // 54600 // Example: 54601 // video buffer: 0 => 10.1, 10.2 => 20 54602 // audio buffer: 0 => 20 54603 // overall buffer: 0 => 10.1, 10.2 => 20 54604 // current time: 13 54605 // 54606 // Chrome's video froze at 10 seconds, where the video buffer encountered the gap, 54607 // however, the audio continued playing until it reached ~3 seconds past the gap 54608 // (13 seconds), at which point it stops as well. Since current time is past the 54609 // gap, findNextRange will return no ranges. 54610 // 54611 // To check for this issue, we see if there is a gap that starts somewhere within 54612 // a 3 second range (3 seconds +/- 1 second) back from our current time. 54613 var gaps = findGaps(buffered); 54614 54615 for (var i = 0; i < gaps.length; i++) { 54616 var start = gaps.start(i); 54617 var end = gaps.end(i); // gap is starts no more than 4 seconds back 54618 54619 if (currentTime - start < 4 && currentTime - start > 2) { 54620 return { 54621 start: start, 54622 end: end 54623 }; 54624 } 54625 } 54626 54627 return null; 54628 } 54629 }]); 54630 return PlaybackWatcher; 54631 }(); 54632 54633 var defaultOptions = { 54634 errorInterval: 30, 54635 getSource: function getSource(next) { 54636 var tech = this.tech({ 54637 IWillNotUseThisInPlugins: true 54638 }); 54639 var sourceObj = tech.currentSource_; 54640 return next(sourceObj); 54641 } 54642 }; 54643 /** 54644 * Main entry point for the plugin 54645 * 54646 * @param {Player} player a reference to a videojs Player instance 54647 * @param {Object} [options] an object with plugin options 54648 * @private 54649 */ 54650 54651 var initPlugin = function initPlugin(player, options) { 54652 var lastCalled = 0; 54653 var seekTo = 0; 54654 var localOptions = videojs$1.mergeOptions(defaultOptions, options); 54655 player.ready(function () { 54656 player.trigger({ 54657 type: 'usage', 54658 name: 'hls-error-reload-initialized' 54659 }); 54660 }); 54661 /** 54662 * Player modifications to perform that must wait until `loadedmetadata` 54663 * has been triggered 54664 * 54665 * @private 54666 */ 54667 54668 var loadedMetadataHandler = function loadedMetadataHandler() { 54669 if (seekTo) { 54670 player.currentTime(seekTo); 54671 } 54672 }; 54673 /** 54674 * Set the source on the player element, play, and seek if necessary 54675 * 54676 * @param {Object} sourceObj An object specifying the source url and mime-type to play 54677 * @private 54678 */ 54679 54680 54681 var setSource = function setSource(sourceObj) { 54682 if (sourceObj === null || sourceObj === undefined) { 54683 return; 54684 } 54685 54686 seekTo = player.duration() !== Infinity && player.currentTime() || 0; 54687 player.one('loadedmetadata', loadedMetadataHandler); 54688 player.src(sourceObj); 54689 player.trigger({ 54690 type: 'usage', 54691 name: 'hls-error-reload' 54692 }); 54693 player.play(); 54694 }; 54695 /** 54696 * Attempt to get a source from either the built-in getSource function 54697 * or a custom function provided via the options 54698 * 54699 * @private 54700 */ 54701 54702 54703 var errorHandler = function errorHandler() { 54704 // Do not attempt to reload the source if a source-reload occurred before 54705 // 'errorInterval' time has elapsed since the last source-reload 54706 if (Date.now() - lastCalled < localOptions.errorInterval * 1000) { 54707 player.trigger({ 54708 type: 'usage', 54709 name: 'hls-error-reload-canceled' 54710 }); 54711 return; 54712 } 54713 54714 if (!localOptions.getSource || typeof localOptions.getSource !== 'function') { 54715 videojs$1.log.error('ERROR: reloadSourceOnError - The option getSource must be a function!'); 54716 return; 54717 } 54718 54719 lastCalled = Date.now(); 54720 return localOptions.getSource.call(player, setSource); 54721 }; 54722 /** 54723 * Unbind any event handlers that were bound by the plugin 54724 * 54725 * @private 54726 */ 54727 54728 54729 var cleanupEvents = function cleanupEvents() { 54730 player.off('loadedmetadata', loadedMetadataHandler); 54731 player.off('error', errorHandler); 54732 player.off('dispose', cleanupEvents); 54733 }; 54734 /** 54735 * Cleanup before re-initializing the plugin 54736 * 54737 * @param {Object} [newOptions] an object with plugin options 54738 * @private 54739 */ 54740 54741 54742 var reinitPlugin = function reinitPlugin(newOptions) { 54743 cleanupEvents(); 54744 initPlugin(player, newOptions); 54745 }; 54746 54747 player.on('error', errorHandler); 54748 player.on('dispose', cleanupEvents); // Overwrite the plugin function so that we can correctly cleanup before 54749 // initializing the plugin 54750 54751 player.reloadSourceOnError = reinitPlugin; 54752 }; 54753 /** 54754 * Reload the source when an error is detected as long as there 54755 * wasn't an error previously within the last 30 seconds 54756 * 54757 * @param {Object} [options] an object with plugin options 54758 */ 54759 54760 54761 var reloadSourceOnError = function reloadSourceOnError(options) { 54762 initPlugin(this, options); 54763 }; 54764 54765 var version$1 = "1.11.2"; // since VHS handles HLS and DASH (and in the future, more types), use * to capture all 54766 54767 videojs$1.use('*', function (player) { 54768 return { 54769 setSource: function setSource(srcObj, next) { 54770 // pass null as the first argument to indicate that the source is not rejected 54771 next(null, srcObj); 54772 }, 54773 // VHS needs to know when seeks happen. For external seeks (generated at the player 54774 // level), this middleware will capture the action. For internal seeks (generated at 54775 // the tech level), we use a wrapped function so that we can handle it on our own 54776 // (specified elsewhere). 54777 setCurrentTime: function setCurrentTime(time) { 54778 if (player.vhs && player.currentSource().src === player.vhs.source_.src) { 54779 player.vhs.setCurrentTime(time); 54780 } 54781 54782 return time; 54783 }, 54784 // Sync VHS after play requests. 54785 // This specifically handles replay where the order of actions is 54786 // play, video element will seek to 0 (skipping the setCurrentTime middleware) 54787 // then triggers a play event. 54788 play: function play() { 54789 if (player.vhs && player.currentSource().src === player.vhs.source_.src) { 54790 player.vhs.setCurrentTime(player.tech_.currentTime()); 54791 } 54792 } 54793 }; 54794 }); 54795 /** 54796 * @file videojs-http-streaming.js 54797 * 54798 * The main file for the HLS project. 54799 * License: https://github.com/videojs/videojs-http-streaming/blob/master/LICENSE 54800 */ 54801 54802 var Hls$1 = { 54803 PlaylistLoader: PlaylistLoader, 54804 Playlist: Playlist, 54805 Decrypter: Decrypter, 54806 AsyncStream: AsyncStream, 54807 decrypt: decrypt, 54808 utils: utils$1, 54809 STANDARD_PLAYLIST_SELECTOR: lastBandwidthSelector, 54810 INITIAL_PLAYLIST_SELECTOR: lowestBitrateCompatibleVariantSelector, 54811 comparePlaylistBandwidth: comparePlaylistBandwidth, 54812 comparePlaylistResolution: comparePlaylistResolution, 54813 xhr: xhrFactory() 54814 }; // Define getter/setters for config properites 54815 54816 ['GOAL_BUFFER_LENGTH', 'MAX_GOAL_BUFFER_LENGTH', 'GOAL_BUFFER_LENGTH_RATE', 'BUFFER_LOW_WATER_LINE', 'MAX_BUFFER_LOW_WATER_LINE', 'BUFFER_LOW_WATER_LINE_RATE', 'BANDWIDTH_VARIANCE'].forEach(function (prop) { 54817 Object.defineProperty(Hls$1, prop, { 54818 get: function get$$1() { 54819 videojs$1.log.warn('using Hls.' + prop + ' is UNSAFE be sure you know
54819what you are doing'); 54820 return Config[prop]; 54821 }, 54822 set: function set$$1(value) { 54823 videojs$1.log.warn('using Hls.' + prop + ' is UNSAFE be sure you know what you are doing'); 54824 54825 if (typeof value !== 'number' || value < 0) { 54826 videojs$1.log.warn('value of Hls.' + prop + ' must be greater than or equal to 0'); 54827 return; 54828 } 54829 54830 Config[prop] = value; 54831 } 54832 }); 54833 }); 54834 var LOCAL_STORAGE_KEY$1 = 'videojs-vhs'; 54835 54836 var simpleTypeFromSourceType = function simpleTypeFromSourceType(type) { 54837 var mpegurlRE = /^(audio|video|application)\/(x-|vnd\.apple\.)?mpegurl/i; 54838 54839 if (mpegurlRE.test(type)) { 54840 return 'hls'; 54841 } 54842 54843 var dashRE = /^application\/dash\+xml/i; 54844 54845 if (dashRE.test(type)) { 54846 return 'dash'; 54847 } 54848 54849 return null; 54850 }; 54851 /** 54852 * Updates the selectedIndex of the QualityLevelList when a mediachange happens in hls. 54853 * 54854 * @param {QualityLevelList} qualityLevels The QualityLevelList to update. 54855 * @param {PlaylistLoader} playlistLoader PlaylistLoader containing the new media info. 54856 * @function handleHlsMediaChange 54857 */ 54858 54859 54860 var handleHlsMediaChange = function handleHlsMediaChange(qualityLevels, playlistLoader) { 54861 var newPlaylist = playlistLoader.media(); 54862 var selectedIndex = -1; 54863 54864 for (var i = 0; i < qualityLevels.length; i++) { 54865 if (qualityLevels[i].id === newPlaylist.uri) { 54866 selectedIndex = i; 54867 break; 54868 } 54869 } 54870 54871 qualityLevels.selectedIndex_ = selectedIndex; 54872 qualityLevels.trigger({ 54873 selectedIndex: selectedIndex, 54874 type: 'change' 54875 }); 54876 }; 54877 /** 54878 * Adds quality levels to list once playlist metadata is available 54879 * 54880 * @param {QualityLevelList} qualityLevels The QualityLevelList to attach events to. 54881 * @param {Object} hls Hls object to listen to for media events. 54882 * @function handleHlsLoadedMetadata 54883 */ 54884 54885 54886 var handleHlsLoadedMetadata = function handleHlsLoadedMetadata(qualityLevels, hls) { 54887 hls.representations().forEach(function (rep) { 54888 qualityLevels.addQualityLevel(rep); 54889 }); 54890 handleHlsMediaChange(qualityLevels, hls.playlists); 54891 }; // HLS is a source handler, not a tech. Make sure attempts to use it 54892 // as one do not cause exceptions. 54893 54894 54895 Hls$1.canPlaySource = function () { 54896 return videojs$1.log.warn('HLS is no longer a tech. Please remove it from ' + 'your player\'s techOrder.'); 54897 }; 54898 54899 var emeKeySystems = function emeKeySystems(keySystemOptions, mainSegmentLoader, audioSegmentLoader) { 54900 if (!keySystemOptions) { 54901 return keySystemOptions; 54902 } 54903 54904 var videoMimeType = void 0; 54905 var audioMimeType = void 0; // if there is a mimeType associated with the audioSegmentLoader, then the audio 54906 // and video mimeType and codec strings are already in the format we need to 54907 // pass with the other key systems 54908 54909 if (audioSegmentLoader.mimeType_) { 54910 videoMimeType = mainSegmentLoader.mimeType_; 54911 audioMimeType = audioSegmentLoader.mimeType_; // if there is no audioSegmentLoader mimeType, then we have to create the 54912 // the audio and video mimeType/codec strings from information extrapolated 54913 // from the mainSegmentLoader mimeType (ex. 'video/mp4; codecs="mp4, avc1"' --> 54914 // 'video/mp4; codecs="avc1"' and 'audio/mp4; codecs="mp4"') 54915 } else { 54916 var parsedMimeType = parseContentType(mainSegmentLoader.mimeType_); 54917 var codecs = parsedMimeType.parameters.codecs.split(','); 54918 var audioCodec = void 0; 54919 var videoCodec = void 0; 54920 codecs.forEach(function (codec) { 54921 codec = codec.trim(); 54922 54923 if (isAudioCodec(codec)) { 54924 audioCodec = codec; 54925 } else if (isVideoCodec(codec)) { 54926 videoCodec = codec; 54927 } 54928 }); 54929 videoMimeType = parsedMimeType.type + '; codecs="' + videoCodec + '"'; 54930 audioMimeType = parsedMimeType.type.replace('video', 'audio') + '; codecs="' + audioCodec + '"'; 54931 } // upsert the content types based on the selected playlist 54932 54933 54934 var keySystemContentTypes = {}; 54935 var videoPlaylist = mainSegmentLoader.playlist_; 54936 54937 for (var keySystem in keySystemOptions) { 54938 keySystemContentTypes[keySystem] = { 54939 audioContentType: audioMimeType, 54940 videoContentType: videoMimeType 54941 }; 54942 54943 if (videoPlaylist.contentProtection && videoPlaylist.contentProtection[keySystem] && videoPlaylist.contentProtection[keySystem].pssh) { 54944 keySystemContentTypes[keySystem].pssh = videoPlaylist.contentProtection[keySystem].pssh; 54945 } // videojs-contrib-eme accepts the option of specifying: 'com.some.cdm': 'url'
54946 // so we need to prevent overwriting the URL entirely 54947 54948 54949 if (typeof keySystemOptions[keySystem] === 'string') { 54950 keySystemContentTypes[keySystem].url = keySystemOptions[keySystem]; 54951 } 54952 } 54953 54954 return videojs$1.mergeOptions(keySystemOptions, keySystemContentTypes); 54955 }; 54956 54957 var setupEmeOptions = function setupEmeOptions(hlsHandler) { 54958 var mainSegmentLoader = hlsHandler.masterPlaylistController_.mainSegmentLoader_; 54959 var audioSegmentLoader = hlsHandler.masterPlaylistController_.audioSegmentLoader_; 54960 var player = videojs$1.players[hlsHandler.tech_.options_.playerId]; 54961 54962 if (player.eme) { 54963 var sourceOptions = emeKeySystems(hlsHandler.source_.keySystems, mainSegmentLoader, audioSegmentLoader); 54964 54965 if (sourceOptions) { 54966 player.currentSource().keySystems = sourceOptions; // Works around https://bugs.chromium.org/p/chromium/issues/detail?id=895449 54967 // in non-IE11 browsers. In IE11 this is too early to initialize media keys 54968 54969 if (!(videojs$1.browser.IE_VERSION === 11) && player.eme.initializeMediaKeys) { 54970 player.eme.initializeMediaKeys(); 54971 } 54972 } 54973 } 54974 }; 54975 54976 var getVhsLocalStorage = function getVhsLocalStorage() { 54977 if (!window.localStorage) { 54978 return null; 54979 } 54980 54981 var storedObject = window.localStorage.getItem(LOCAL_STORAGE_KEY$1); 54982 54983 if (!storedObject) { 54984 return null; 54985 } 54986 54987 try { 54988 return JSON.parse(storedObject); 54989 } catch (e) { 54990 // someone may have tampered with the value 54991 return null; 54992 } 54993 }; 54994 54995 var updateVhsLocalStorage = function updateVhsLocalStorage(options) { 54996 if (!window.localStorage) { 54997 return false; 54998 } 54999 55000 var objectToStore = getVhsLocalStorage(); 55001 objectToStore = objectToStore ? videojs$1.mergeOptions(objectToStore, options) : options; 55002 55003 try { 55004 window.localStorage.setItem(LOCAL_STORAGE_KEY$1, JSON.stringify(objectToStore)); 55005 } catch (e) { 55006 // Throws if storage is full (e.g., always on iOS 5+ Safari private mode, where 55007 // storage is set to 0). 55008 // https://developer.mozilla.org/en-US/docs/Web/API/Storage/setItem#Exceptions 55009 // No need to perform any operation. 55010 return false; 55011 } 55012 55013 return objectToStore; 55014 }; 55015 /** 55016 * Whether the browser has built-in HLS support. 55017 */ 55018 55019 55020 Hls$1.supportsNativeHls = function () { 55021 var video = document.createElement('video'); // native HLS is definitely not supported if HTML5 video isn't 55022 55023 if (!videojs$1.getTech('Html5').isSupported()) { 55024 return false; 55025 } // HLS manifests can go by many mime-types 55026 55027 55028 var canPlay = [// Apple santioned 55029 'application/vnd.apple.mpegurl', // Apple sanctioned for backwards compatibility 55030 'audio/mpegurl', // Very common 55031 'audio/x-mpegurl', // Very common
vendor: 16,384 bytes, lines 55032-55444
55032 'application/x-mpegurl', // Included for completeness 55033 'video/x-mpegurl', 'video/mpegurl', 'application/mpegurl']; 55034 return canPlay.some(function (canItPlay) { 55035 return /maybe|probably/i.test(video.canPlayType(canItPlay)); 55036 }); 55037 }(); 55038 55039 Hls$1.supportsNativeDash = function () { 55040 if (!videojs$1.getTech('Html5').isSupported()) { 55041 return false; 55042 } 55043 55044 return /maybe|probably/i.test(document.createElement('video').canPlayType('application/dash+xml')); 55045 }(); 55046 55047 Hls$1.supportsTypeNatively = function (type) { 55048 if (type === 'hls') { 55049 return Hls$1.supportsNativeHls; 55050 } 55051 55052 if (type === 'dash') { 55053 return Hls$1.supportsNativeDash; 55054 } 55055 55056 return false; 55057 }; 55058 /** 55059 * HLS is a source handler, not a tech. Make sure attempts to use it 55060 * as one do not cause exceptions. 55061 */ 55062 55063 55064 Hls$1.isSupported = function () { 55065 return videojs$1.log.warn('HLS is no longer a tech. Please remove it from ' + 'your player\'s techOrder.'); 55066 }; 55067 55068 var Component$1 = videojs$1.getComponent('Component'); 55069 /** 55070 * The Hls Handler object, where we orchestrate all of the parts 55071 * of HLS to interact with video.js 55072 * 55073 * @class HlsHandler 55074 * @extends videojs.Component 55075 * @param {Object} source the soruce object 55076 * @param {Tech} tech the parent tech object 55077 * @param {Object} options optional and required options 55078 */ 55079 55080 var HlsHandler = function (_Component) { 55081 inherits$2(HlsHandler, _Component); 55082 55083 function HlsHandler(source, tech, options) { 55084 classCallCheck$1(this, HlsHandler); // tech.player() is deprecated but setup a reference to HLS for 55085 // backwards-compatibility 55086 55087 var _this = possibleConstructorReturn$1(this, (HlsHandler.__proto__ || Object.getPrototypeOf(HlsHandler)).call(this, tech, options.hls)); 55088 55089 if (tech.options_ && tech.options_.playerId) { 55090 var _player = videojs$1(tech.options_.playerId); 55091 55092 if (!_player.hasOwnProperty('hls')) { 55093 Object.defineProperty(_player, 'hls', { 55094 get: function get$$1() { 55095 videojs$1.log.warn('player.hls is deprecated. Use player.tech().hls instead.'); 55096 tech.trigger({ 55097 type: 'usage', 55098 name: 'hls-player-access' 55099 }); 55100 return _this; 55101 }, 55102 configurable: true 55103 }); 55104 } // Set up a reference to the HlsHandler from player.vhs. This allows users to start 55105 // migrating from player.tech_.hls... to player.vhs... for API access. Although this 55106 // isn't the most appropriate form of reference for video.js (since all APIs should 55107 // be provided through core video.js), it is a common pattern for plugins, and vhs 55108 // will act accordingly. 55109 55110 55111 _player.vhs = _this; // deprecated, for backwards compatibility 55112 55113 _player.dash = _this; 55114 _this.player_ = _player; 55115 } 55116 55117 _this.tech_ = tech; 55118 _this.source_ = source; 55119 _this.stats = {}; 55120 55121 _this.setOptions_(); 55122 55123 if (_this.options_.overrideNative && tech.overrideNativeAudioTracks && tech.overrideNativeVideoTracks) { 55124 tech.overrideNativeAudioTracks(true); 55125 tech.overrideNativeVideoTracks(true); 55126 } else if (_this.options_.overrideNative && (tech.featuresNativeVideoTracks || tech.featuresNativeAudioTracks)) { 55127 // overriding native HLS only works if audio tracks have been emulated 55128 // error early if we're misconfigured 55129 throw new Error('Overriding native HLS requires emulated tracks. ' + 'See https://git.io/vMpjB'); 55130 } // listen for fullscreenchange events for this player so that we 55131 // can adjust our quality selection quickly 55132 55133 55134 _this.on(document, ['fullscreenchange', 'webkitfullscreenchange', 'mozfullscreenchange', 'MSFullscreenChange'], function (event) { 55135 var fullscreenElement = document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement; 55136 55137 if (fullscreenElement && fullscreenElement.contains(_this.tech_.el())) { 55138 _this.masterPlaylistController_.smoothQualityChange_(); 55139 } 55140 }); // Handle seeking when looping - middleware doesn't handle this seek event from the tech 55141 55142 55143 _this.on(_this.tech_, 'seeking', function () { 55144 if (this.tech_.currentTime() === 0 && this.tech_.player_.loop()) { 55145 this.setCurrentTime(0); 55146 } 55147 }); 55148 55149 _this.on(_this.tech_, 'error', function () { 55150 if (this.masterPlaylistController_) { 55151 this.masterPlaylistController_.pauseLoading(); 55152 } 55153 }); 55154 55155 _this.on(_this.tech_, 'play', _this.play); 55156 55157 return _this; 55158 } 55159 55160 createClass$1(HlsHandler, [{ 55161 key: 'setOptions_', 55162 value: function setOptions_() { 55163 var _this2 = this; // defaults 55164 55165 55166 this.options_.withCredentials = this.options_.withCredentials || false; 55167 this.options_.handleManifestRedirects = this.options_.handleManifestRedirects || false; 55168 this.options_.limitRenditionByPlayerDimensions = this.options_.limitRenditionByPlayerDimensions === false ? false : true; 55169 this.options_.smoothQualityChange = this.options_.smoothQualityChange || false; 55170 this.options_.useBandwidthFromLocalStorage = typeof this.source_.useBandwidthFromLocalStorage !== 'undefined' ? this.source_.useBandwidthFromLocalStorage : this.options_.useBandwidthFromLocalStorage || false; 55171 this.options_.customTagParsers = this.options_.customTagParsers || []; 55172 this.options_.customTagMappers = this.options_.customTagMappers || []; 55173 this.options_.cacheEncryptionKeys = this.options_.cacheEncryptionKeys || false; 55174 55175 if (typeof this.options_.blacklistDuration !== 'number') { 55176 this.options_.blacklistDuration = 5 * 60; 55177 } 55178 55179 if (typeof this.options_.bandwidth !== 'number') { 55180 if (this.options_.useBandwidthFromLocalStorage) { 55181 var storedObject = getVhsLocalStorage(); 55182 55183 if (storedObject && storedObject.bandwidth) { 55184 this.options_.bandwidth = storedObject.bandwidth; 55185 this.tech_.trigger({ 55186 type: 'usage', 55187 name: 'hls-bandwidth-from-local-storage' 55188 }); 55189 } 55190 55191 if (storedObject && storedObject.throughput) { 55192 this.options_.throughput = storedObject.throughput; 55193 this.tech_.trigger({ 55194 type: 'usage', 55195 name: 'hls-throughput-from-local-storage' 55196 }); 55197 } 55198 } 55199 } // if bandwidth was not set by options or pulled from local storage, start playlist 55200 // selection at a reasonable bandwidth 55201 55202 55203 if (typeof this.options_.bandwidth !== 'number') { 55204 this.options_.bandwidth = Config.INITIAL_BANDWIDTH; 55205 } // If the bandwidth number is unchanged from the initial setting 55206 // then this takes precedence over the enableLowInitialPlaylist option 55207 55208 55209 this.options_.enableLowInitialPlaylist = this.options_.enableLowInitialPlaylist && this.options_.bandwidth === Config.INITIAL_BANDWIDTH; // grab options passed to player.src 55210 55211 ['withCredentials', 'limitRenditionByPlayerDimensions', 'bandwidth', 'smoothQualityChange', 'customTagParsers', 'customTagMappers', 'handleManifestRedirects', 'cacheEncryptionKeys'].forEach(function (option) { 55212 if (typeof _this2.source_[option] !== 'undefined') { 55213 _this2.options_[option] = _this2.source_[option]; 55214 } 55215 }); 55216 this.limitRenditionByPlayerDimensions = this.options_.limitRenditionByPlayerDimensions; 55217 } 55218 /** 55219 * called when player.src gets called, handle a new source 55220 * 55221 * @param {Object} src the source object to handle 55222 */ 55223 55224 }, { 55225 key: 'src', 55226 value: function src(_src, type) { 55227 var _this3 = this; // do nothing if the src is falsey 55228 55229 55230 if (!_src) { 55231 return; 55232 } 55233 55234 this.setOptions_(); // add master playlist controller options 55235 55236 this.options_.url = this.source_.src; 55237 this.options_.tech = this.tech_; 55238 this.options_.externHls = Hls$1; 55239 this.options_.sourceType = simpleTypeFromSourceType(type); // Whenever we seek internally, we should update both the tech and call our own 55240 // setCurrentTime function. This is needed because "seeking" events aren't always 55241 // reliable. External seeks (via the player object) are handled via middleware. 55242 55243 this.options_.seekTo = function (time) { 55244 _this3.tech_.setCurrentTime(time); 55245 55246 _this3.setCurrentTime(time); 55247 }; 55248 55249 this.masterPlaylistController_ = new MasterPlaylistController(this.options_); 55250 this.playbackWatcher_ = new PlaybackWatcher(videojs$1.mergeOptions(this.options_, { 55251 seekable: function seekable$$1() { 55252 return _this3.seekable(); 55253 }, 55254 media: function media() { 55255 return _this3.masterPlaylistController_.media(); 55256 } 55257 })); 55258 this.masterPlaylistController_.on('error', function () { 55259 var player = videojs$1.players[_this3.tech_.options_.playerId]; 55260 player.error(_this3.masterPlaylistController_.error); 55261 }); // `this` in selectPlaylist should be the HlsHandler for backwards 55262 // compatibility with < v2 55263 55264 this.masterPlaylistController_.selectPlaylist = this.selectPlaylist ? this.selectPlaylist.bind(this) : Hls$1.STANDARD_PLAYLIST_SELECTOR.bind(this); 55265 this.masterPlaylistController_.selectInitialPlaylist = Hls$1.INITIAL_PLAYLIST_SELECTOR.bind(this); // re-expose some internal objects for backwards compatibility with < v2 55266 55267 this.playlists = this.masterPlaylistController_.masterPlaylistLoader_; 55268 this.mediaSource = this.masterPlaylistController_.mediaSource; // Proxy assignment of some properties to the master playlist 55269 // controller. Using a custom property for backwards compatibility 55270 // with < v2 55271 55272 Object.defineProperties(this, { 55273 selectPlaylist: { 55274 get: function get$$1() { 55275 return this.masterPlaylistController_.selectPlaylist; 55276 }, 55277 set: function set$$1(selectPlaylist) { 55278 this.masterPlaylistController_.selectPlaylist = selectPlaylist.bind(this); 55279 } 55280 }, 55281 throughput: { 55282 get: function get$$1() { 55283 return this.masterPlaylistController_.mainSegmentLoader_.throughput.rate; 55284 }, 55285 set: function set$$1(throughput) { 55286 this.masterPlaylistController_.mainSegmentLoader_.throughput.rate = throughput; // By setting `count` to 1 the throughput value becomes the starting value 55287 // for the cumulative average 55288 55289 this.masterPlaylistController_.mainSegmentLoader_.throughput.count = 1; 55290 } 55291 }, 55292 bandwidth: { 55293 get: function get$$1() { 55294 return this.masterPlaylistController_.mainSegmentLoader_.bandwidth; 55295 }, 55296 set: function set$$1(bandwidth) { 55297 this.masterPlaylistController_.mainSegmentLoader_.bandwidth = bandwidth; // setting the bandwidth manually resets the throughput counter 55298 // `count` is set to zero that current value of `rate` isn't included 55299 // in the cumulative average 55300 55301 this.masterPlaylistController_.mainSegmentLoader_.throughput = { 55302 rate: 0, 55303 count: 0 55304 }; 55305 } 55306 }, 55307 55308 /** 55309 * `systemBandwidth` is a combination of two serial processes bit-rates. The first 55310 * is the network bitrate provided by `bandwidth` and the second is the bitrate of 55311 * the entire process after that - decryption, transmuxing, and appending - provided 55312 * by `throughput`. 55313 * 55314 * Since the two process are serial, the overall system bandwidth is given by: 55315 * sysBandwidth = 1 / (1 / bandwidth + 1 / throughput) 55316 */ 55317 systemBandwidth: { 55318 get: function get$$1() { 55319 var invBandwidth = 1 / (this.bandwidth || 1); 55320 var invThroughput = void 0; 55321 55322 if (this.throughput > 0) { 55323 invThroughput = 1 / this.throughput; 55324 } else { 55325 invThroughput = 0; 55326 } 55327 55328 var systemBitrate = Math.floor(1 / (invBandwidth + invThroughput)); 55329 return systemBitrate; 55330 }, 55331 set: function set$$1() { 55332 videojs$1.log.error('The "systemBandwidth" property is read-only'); 55333 } 55334 } 55335 }); 55336 55337 if (this.options_.bandwidth) { 55338 this.bandwidth = this.options_.bandwidth; 55339 } 55340 55341 if (this.options_.throughput) { 55342 this.throughput = this.options_.throughput; 55343 } 55344 55345 Object.defineProperties(this.stats, { 55346 bandwidth: { 55347 get: function get$$1() { 55348 return _this3.bandwidth || 0; 55349 }, 55350 enumerable: true 55351 }, 55352 mediaRequests: { 55353 get: function get$$1() { 55354 return _this3.masterPlaylistController_.mediaRequests_() || 0; 55355 }, 55356 enumerable: true 55357 }, 55358 mediaRequestsAborted: { 55359 get: function get$$1() { 55360 return _this3.masterPlaylistController_.mediaRequestsAborted_() || 0; 55361 }, 55362 enumerable: true 55363 }, 55364 mediaRequestsTimedout: { 55365 get: function get$$1() { 55366 return _this3.masterPlaylistController_.mediaRequestsTimedout_() || 0; 55367 }, 55368 enumerable: true 55369 }, 55370 mediaRequestsErrored: { 55371 get: function get$$1() { 55372 return _this3.masterPlaylistController_.mediaRequestsErrored_() || 0; 55373 }, 55374 enumerable: true 55375 }, 55376 mediaTransferDuration: { 55377 get: function get$$1() { 55378 return _this3.masterPlaylistController_.mediaTransferDuration_() || 0; 55379 }, 55380 enumerable: true 55381 }, 55382 mediaBytesTransferred: { 55383 get: function get$$1() { 55384 return _this3.masterPlaylistController_.mediaBytesTransferred_() || 0; 55385 }, 55386 enumerable: true 55387 }, 55388 mediaSecondsLoaded: { 55389 get: function get$$1() { 55390 return _this3.masterPlaylistController_.mediaSecondsLoaded_() || 0; 55391 }, 55392 enumerable: true 55393 }, 55394 buffered: { 55395 get: function get$$1() { 55396 return timeRangesToArray(_this3.tech_.buffered()); 55397 }, 55398 enumerable: true 55399 }, 55400 currentTime: { 55401 get: function get$$1() { 55402 return _this3.tech_.currentTime(); 55403 }, 55404 enumerable: true 55405 }, 55406 currentSource: { 55407 get: function get$$1() { 55408 return _this3.tech_.currentSource_; 55409 }, 55410 enumerable: true 55411 }, 55412 currentTech: { 55413 get: function get$$1() { 55414 return _this3.tech_.name_; 55415 }, 55416 enumerable: true 55417 }, 55418 duration: { 55419 get: function get$$1() { 55420 return _this3.tech_.duration(); 55421 }, 55422 enumerable: true 55423 }, 55424 master: { 55425 get: function get$$1() { 55426 return _this3.playlists.master; 55427 }, 55428 enumerable: true 55429 }, 55430 playerDimensions: { 55431 get: function get$$1() { 55432 return _this3.tech_.currentDimensions(); 55433 }, 55434 enumerable: true 55435 }, 55436 seekable: { 55437 get: function get$$1() { 55438 return timeRangesToArray(_this3.tech_.seekable()); 55439 }, 55440 enumerable: true 55441 }, 55442 timestamp: { 55443 get: function get$$1() { 55444
vendor: 3,650 bytes, lines 55444-55559
55444 return Date.now(); 55445 }, 55446 enumerable: true 55447 }, 55448 videoPlaybackQuality: { 55449 get: function get$$1() { 55450 return _this3.tech_.getVideoPlaybackQuality(); 55451 }, 55452 enumerable: true 55453 } 55454 }); 55455 this.tech_.one('canplay', this.masterPlaylistController_.setupFirstPlay.bind(this.masterPlaylistController_)); 55456 this.tech_.on('bandwidthupdate', function () { 55457 if (_this3.options_.useBandwidthFromLocalStorage) { 55458 updateVhsLocalStorage({ 55459 bandwidth: _this3.bandwidth, 55460 throughput: Math.round(_this3.throughput) 55461 }); 55462 } 55463 }); 55464 this.masterPlaylistController_.on('selectedinitialmedia', function () { 55465 // Add the manual rendition mix-in to HlsHandler 55466 renditionSelectionMixin(_this3); 55467 setupEmeOptions(_this3); 55468 }); // the bandwidth of the primary segment loader is our best 55469 // estimate of overall bandwidth 55470 55471 this.on(this.masterPlaylistController_, 'progress', function () { 55472 this.tech_.trigger('progress'); 55473 }); 55474 this.tech_.ready(function () { 55475 return _this3.setupQualityLevels_(); 55476 }); // do nothing if the tech has been disposed already 55477 // this can occur if someone sets the src in player.ready(), for instance 55478 55479 if (!this.tech_.el()) { 55480 return; 55481 } 55482 55483 this.tech_.src(videojs$1.URL.createObjectURL(this.masterPlaylistController_.mediaSource)); 55484 } 55485 /** 55486 * Initializes the quality levels and sets listeners to update them. 55487 * 55488 * @method setupQualityLevels_ 55489 * @private 55490 */ 55491 55492 }, { 55493 key: 'setupQualityLevels_', 55494 value: function setupQualityLevels_() { 55495 var _this4 = this; 55496 55497 var player = videojs$1.players[this.tech_.options_.playerId]; 55498 55499 if (player && player.qualityLevels) { 55500 this.qualityLevels_ = player.qualityLevels(); 55501 this.masterPlaylistController_.on('selectedinitialmedia', function () { 55502 handleHlsLoadedMetadata(_this4.qualityLevels_, _this4); 55503 }); 55504 this.playlists.on('mediachange', function () { 55505 handleHlsMediaChange(_this4.qualityLevels_, _this4.playlists); 55506 }); 55507 } 55508 } 55509 /** 55510 * Begin playing the video. 55511 */ 55512 55513 }, { 55514 key: 'play', 55515 value: function play() { 55516 this.masterPlaylistController_.play(); 55517 } 55518 /** 55519 * a wrapper around the function in MasterPlaylistController 55520 */ 55521 55522 }, { 55523 key: 'setCurrentTime', 55524 value: function setCurrentTime(currentTime) { 55525 this.masterPlaylistController_.setCurrentTime(currentTime); 55526 } 55527 /** 55528 * a wrapper around the function in MasterPlaylistController 55529 */ 55530 55531 }, { 55532 key: 'duration', 55533 value: function duration$$1() { 55534 return this.masterPlaylistController_.duration(); 55535 } 55536 /** 55537 * a wrapper around the function in MasterPlaylistController 55538 */ 55539 55540 }, { 55541 key: 'seekable', 55542 value: function seekable$$1() { 55543 return this.masterPlaylistController_.seekable(); 55544 } 55545 /** 55546 * Abort all outstanding work and cleanup. 55547 */ 55548 55549 }, { 55550 key: 'dispose', 55551 value: function dispose() { 55552 if (this.playbackWatcher_) { 55553 this.playbackWatcher_.dispose(); 55554 } 55555 55556 if (this.masterPlaylistController_) { 55557 this.masterPlaylistController_.dispose(); 55558 } 55559
55560 if (this.qualityLevels_) { 55561 this.qualityLevels_.dispose(); 55562 } 55563 55564 if (this.player_) { 55565 delete this.player_.vhs; 55566 delete this.player_.dash; 55567 delete this.player_.hls; 55568 } 55569 55570 if (this.tech_ && this.tech_.hls) { 55571 delete this.tech_.hls; 55572 } 55573 55574 get$1(HlsHandler.prototype.__proto__ || Object.getPrototypeOf(HlsHandler.prototype), 'dispose', this).call(this); 55575 } 55576 }, { 55577 key: 'convertToProgramTime', 55578 value: function convertToProgramTime(time, callback) { 55579 return getProgramTime({ 55580 playlist: this.masterPlaylistController_.media(), 55581 time: time, 55582 callback: callback 55583 }); 55584 } // the player must be playing before calling this 55585 55586 }, { 55587 key: 'seekToProgramTime', 55588 value: function seekToProgramTime$$1(programTime, callback) { 55589 var pauseAfterSeek = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true; 55590 var retryCount = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 2; 55591 return seekToProgramTime({ 55592 programTime: programTime, 55593 playlist: this.masterPlaylistController_.media(), 55594 retryCount: retryCount, 55595 pauseAfterSeek: pauseAfterSeek, 55596 seekTo: this.options_.seekTo, 55597 tech: this.options_.tech, 55598 callback: callback 55599 }); 55600 } 55601 }]); 55602 return HlsHandler; 55603 }(Component$1); 55604 /** 55605 * The Source Handler object, which informs video.js what additional 55606 * MIME types are supported and sets up playback. It is registered 55607 * automatically to the appropriate tech based on the capabilities of 55608 * the browser it is running in. It is not necessary to use or modify 55609 * this object in normal usage. 55610 */ 55611 55612 55613 var HlsSourceHandler = { 55614 name: 'videojs-http-streaming', 55615 VERSION: version$1, 55616 canHandleSource: function canHandleSource(srcObj) { 55617 var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 55618 var localOptions = videojs$1.mergeOptions(videojs$1.options, options); 55619 return HlsSourceHandler.canPlayType(srcObj.type, localOptions); 55620 }, 55621 handleSource: function handleSource(source, tech) { 55622 var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 55623 var localOptions = videojs$1.mergeOptions(videojs$1.options, options); 55624 tech.hls = new HlsHandler(source, tech, localOptions); 55625 tech.hls.xhr = xhrFactory(); 55626 tech.hls.src(source.src, source.type); 55627 return tech.hls; 55628 }, 55629 canPlayType: function canPlayType(type) { 55630 var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 55631 55632 var _videojs$mergeOptions = videojs$1.mergeOptions(videojs$1.options, options), 55633 overrideNative = _videojs$mergeOptions.hls.overrideNative; 55634 55635 var supportedType = simpleTypeFromSourceType(type); 55636 var canUseMsePlayback = supportedType && (!Hls$1.supportsTypeNatively(supportedType) || overrideNative); 55637 return canUseMsePlayback ? 'maybe' : ''; 55638 } 55639 }; 55640 55641 if (typeof videojs$1.MediaSource === 'undefined' || typeof videojs$1.URL === 'undefined') { 55642 videojs$1.MediaSource = MediaSource; 55643 videojs$1.URL = URL$1; 55644 } // register source handlers with the appropriate techs 55645 55646 55647 if (MediaSource.supportsNativeMediaSources()) { 55648 videojs$1.getTech('Html5').registerSourceHandler(HlsSourceHandler, 0); 55649 } 55650 55651 videojs$1.HlsHandler = HlsHandler; 55652 videojs$1.HlsSourceHandler = HlsSourceHandler; 55653 videojs$1.Hls = Hls$1; 55654 55655 if (!videojs$1.use) { 55656 videojs$1.registerComponent('Hls', Hls$1); 55657 } 55658 55659 videojs$1.options.hls = videojs$1.options.hls || {}; 55660 55661 if (videojs$1.registerPlugin) { 55662 videojs$1.registerPlugin('reloadSourceOnError', reloadSourceOnError); 55663 } else { 55664 videojs$1.plugin('reloadSourceOnError', reloadSourceOnError); 55665 } 55666 55667 return videojs$1; 55668 55669 }));
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.