vendor: 23,175 bytes, lines 1-831
1/** 2 * @license 3 * Video.js 7.1.0 <http://videojs.com/> 4 * Copyright Brightcove, Inc. <https://www.brightcove.com/> 5 * Available under Apache License Version 2.0 6 * <https://github.com/videojs/video.js/blob/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() : 15 typeof define === 'function' && define.amd ? define(factory) : 16 (global.videojs = factory()); 17}(this, (function () { 18 var version = "7.1.0"; 19 20 var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; 21 22 function createCommonjsModule(fn, module) { 23 return module = { exports: {} }, fn(module, module.exports), module.exports; 24 } 25 26 var win; 27 28 if (typeof window !== "undefined") { 29 win = window; 30 } else if (typeof commonjsGlobal !== "undefined") { 31 win = commonjsGlobal; 32 } else if (typeof self !== "undefined") { 33 win = self; 34 } else { 35 win = {}; 36 } 37 38 var window_1 = win; 39 40 var empty = {}; 41 42 var empty$1 = /*#__PURE__*/Object.freeze({ 43 default: empty 44 }); 45 46 var minDoc = ( empty$1 && empty ) || empty$1; 47 48 var topLevel = typeof commonjsGlobal !== 'undefined' ? commonjsGlobal : typeof window !== 'undefined' ? window : {}; 49 50 var doccy; 51 52 if (typeof document !== 'undefined') { 53 doccy = document; 54 } else { 55 doccy = topLevel['__GLOBAL_DOCUMENT_CACHE@4']; 56 57 if (!doccy) { 58 doccy = topLevel['__GLOBAL_DOCUMENT_CACHE@4'] = minDoc; 59 } 60 } 61 62 var document_1 = doccy; 63 64 /** 65 * @file log.js 66 * @module log 67 */ 68 69 var log = void 0; 70 71 // This is the private tracking variable for logging level. 72 var level = 'info'; 73 74 // This is the private tracking variable for the logging history. 75 var history = []; 76 77 /** 78 * Log messages to the console and history based on the type of message 79 * 80 * @private 81 * @param {string} type 82 * The name of the console method to use. 83 * 84 * @param {Array} args 85 * The arguments to be passed to the matching console method. 86 */ 87 var logByType = function logByType(type, args) { 88 var lvl = log.levels[level]; 89 var lvlRegExp = new RegExp('^(' + lvl + ')$'); 90 91 if (type !== 'log') { 92 93 // Add the type to the front of the message when it's not "log". 94 args.unshift(type.toUpperCase() + ':'); 95 } 96 97 // Add a clone of the args at this point to history. 98 if (history) { 99 history.push([].concat(args)); 100 } 101 102 // Add console prefix after adding to history. 103 args.unshift('VIDEOJS:'); 104 105 // If there's no console then don't try to output messages, but they will 106 // still be stored in history. 107 if (!window_1.console) { 108 return; 109 } 110 111 // Was setting these once outside of this function, but containing them 112 // in the function makes it easier to test cases where console doesn't exist 113 // when the module is executed. 114 var fn = window_1.console[type]; 115 116 if (!fn && type === 'debug') { 117 // Certain browsers don't have support for console.debug. For those, we 118 // should default to the closest comparable log. 119 fn = window_1.console.info || window_1.console.log; 120 } 121 122 // Bail out if there's no console or if this type is not allowed by the 123 // current logging level. 124 if (!fn || !lvl || !lvlRegExp.test(type)) { 125 return; 126 } 127 128 fn[Array.isArray(args) ? 'apply' : 'call'](window_1.console, args); 129 }; 130 131 /** 132 * Logs plain debug messages. Similar to `console.log`. 133 * 134 * @class 135 * @param {Mixed[]} args 136 * One or more messages or objects that should be logged. 137 */ 138 log = function log() { 139 for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { 140 args[_key] = arguments[_key]; 141 } 142 143 logByType('log', args); 144 }; 145 146 /** 147 * Enumeration of available logging levels, where the keys are the level names 148 * and the values are `|`-separated strings containing logging methods allowed 149 * in that logging level. These strings are used to create a regular expression 150 * matching the function name being called. 151 * 152 * Levels provided by video.js are: 153 * 154 * - `off`: Matches no calls. Any value that can be cast to `false` will have 155 * this effect. The most restrictive. 156 * - `all`: Matches only Video.js-provided functions (`debug`, `log`, 157 * `log.warn`, and `log.error`). 158 * - `debug`: Matches `log.debug`, `log`, `log.warn`, and `log.error` calls. 159 * - `info` (default): Matches `log`, `log.warn`, and `log.error` calls. 160 * - `warn`: Matches `log.warn` and `log.error` calls. 161 * - `error`: Matches only `log.error` calls. 162 * 163 * @type {Object} 164 */ 165 log.levels = { 166 all: 'debug|log|warn|error', 167 off: '', 168 debug: 'debug|log|warn|error', 169 info: 'log|warn|error', 170 warn: 'warn|error', 171 error: 'error', 172 DEFAULT: level 173 }; 174 175 /** 176 * Get or set the current logging level. If a string matching a key from 177 * {@link log.levels} is provided, acts as a setter. Regardless of argument, 178 * returns the current logging level. 179 * 180 * @param {string} [lvl] 181 * Pass to set a new logging level. 182 * 183 * @return {string} 184 * The current logging level. 185 */ 186 log.level = function (lvl) { 187 if (typeof lvl === 'string') { 188 if (!log.levels.hasOwnProperty(lvl)) { 189 throw new Error('"' + lvl + '" in not a valid log level'); 190 } 191 level = lvl; 192 } 193 return level; 194 }; 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 log.history = function () { 206 return history ? [].concat(history) : []; 207 }; 208 209 /** 210 * Clears the internal history tracking, but does not prevent further history 211 * tracking. 212 */ 213 log.history.clear = function () { 214 if (history) { 215 history.length = 0; 216 } 217 }; 218 219 /** 220 * Disable history tracking if it is currently enabled. 221 */ 222 log.history.disable = function () { 223 if (history !== null) { 224 history.length = 0; 225 history = null; 226 } 227 }; 228 229 /** 230 * Enable history tracking if it is currently disabled. 231 */ 232 log.history.enable = function () { 233 if (history === null) { 234 history = []; 235 } 236 }; 237 238 /** 239 * Logs error messages. Similar to `console.error`. 240 * 241 * @param {Mixed[]} args 242 * One or more messages or objects that should be logged as an error 243 */ 244 log.error = function () { 245 for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { 246 args[_key2] = arguments[_key2]; 247 } 248 249 return logByType('error', args); 250 }; 251 252 /** 253 * Logs warning messages. Similar to `console.warn`. 254 * 255 * @param {Mixed[]} args 256 * One or more messages or objects that should be logged as a warning. 257 */ 258 log.warn = function () { 259 for (var _len3 = arguments.length, args = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { 260 args[_key3] = arguments[_key3]; 261 } 262 263 return logByType('warn', args); 264 }; 265 266 /** 267 * Logs debug messages. Similar to `console.debug`, but may also act as a comparable 268 * log if `console.debug` is not available 269 * 270 * @param {Mixed[]} args 271 * One or more messages or objects that should be logged as debug. 272 */ 273 log.debug = function () { 274 for (var _len4 = arguments.length, args = Array(_len4), _key4 = 0; _key4 < _len4; _key4++) { 275 args[_key4] = arguments[_key4]; 276 } 277 278 return logByType('debug', args); 279 }; 280 281 var log$1 = log; 282 283 function clean(s) { 284 return s.replace(/\n\r?\s*/g, ''); 285 } 286 287 var tsml = function tsml(sa) { 288 var s = '', 289 i = 0; 290 291 for (; i < arguments.length; i++) { 292 s += clean(sa[i]) + (arguments[i + 1] || ''); 293 }return s; 294 }; 295 296 var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { 297 return typeof obj; 298 } : function (obj) { 299 return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; 300 }; 301 302 var classCallCheck = function (instance, Constructor) { 303 if (!(instance instanceof Constructor)) { 304 throw new TypeError("Cannot call a class as a function"); 305 } 306 }; 307 308 var inherits = function (subClass, superClass) { 309 if (typeof superClass !== "function" && superClass !== null) { 310 throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); 311 } 312 313 subClass.prototype = Object.create(superClass && superClass.prototype, { 314 constructor: { 315 value: subClass, 316 enumerable: false, 317 writable: true, 318 configurable: true 319 } 320 }); 321 if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; 322 }; 323 324 var possibleConstructorReturn = function (self, call) { 325 if (!self) { 326 throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); 327 } 328 329 return call && (typeof call === "object" || typeof call === "function") ? call : self; 330 }; 331 332 var taggedTemplateLiteralLoose = function (strings, raw) { 333 strings.raw = raw; 334 return strings; 335 }; 336 337 /** 338 * @file obj.js 339 * @module obj 340 */ 341 342 /** 343 * @callback obj:EachCallback 344 * 345 * @param {Mixed} value 346 * The current key for the object that is being iterated over. 347 * 348 * @param {string} key 349 * The current key-value for object that is being iterated over 350 */ 351 352 /** 353 * @callback obj:ReduceCallback 354 * 355 * @param {Mixed} accum 356 * The value that is accumulating over the reduce loop. 357 * 358 * @param {Mixed} value 359 * The current key for the object that is being iterated over. 360 * 361 * @param {string} key 362 * The current key-value for object that is being iterated over 363 * 364 * @return {Mixed} 365 * The new accumulated value. 366 */ 367 var toString = Object.prototype.toString; 368 369 /** 370 * Get the keys of an Object 371 * 372 * @param {Object} 373 * The Object to get the keys from 374 * 375 * @return {string[]} 376 * An array of the keys from the object. Returns an empty array if the 377 * object passed in was invalid or had no keys. 378 * 379 * @private 380 */ 381 var keys = function keys(object) { 382 return isObject(object) ? Object.keys(object) : []; 383 }; 384 385 /** 386 * Array-like iteration for objects. 387 * 388 * @param {Object} object 389 * The object to iterate over 390 * 391 * @param {obj:EachCallback} fn 392 * The callback function which is called for each key in the object. 393 */ 394 function each(object, fn) { 395 keys(object).forEach(function (key) { 396 return fn(object[key], key); 397 }); 398 } 399 400 /** 401 * Array-like reduce for objects. 402 * 403 * @param {Object} object 404 * The Object that you want to reduce. 405 * 406 * @param {Function} fn 407 * A callback function which is called for each key in the object. It 408 * receives the accumulated value and the per-iteration value and key 409 * as arguments. 410 * 411 * @param {Mixed} [initial = 0] 412 * Starting value 413 * 414 * @return {Mixed} 415 * The final accumulated value. 416 */ 417 function reduce(object, fn) { 418 var initial = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; 419 420 return keys(object).reduce(function (accum, key) { 421 return fn(accum, object[key], key); 422 }, initial); 423 } 424 425 /** 426 * Object.assign-style object shallow merge/extend. 427 * 428 * @param {Object} target 429 * @param {Object} ...sources 430 * @return {Object} 431 */ 432 function assign(target) { 433 for (var _len = arguments.length, sources = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { 434 sources[_key - 1] = arguments[_key]; 435 } 436 437 if (Object.assign) { 438 return Object.assign.apply(Object, [target].concat(sources)); 439 } 440 441 sources.forEach(function (source) { 442 if (!source) { 443 return; 444 } 445 446 each(source, function (value, key) { 447 target[key] = value; 448 }); 449 }); 450 451 return target; 452 } 453 454 /** 455 * Returns whether a value is an object of any kind - including DOM nodes, 456 * arrays, regular expressions, etc. Not functions, though. 457 * 458 * This avoids the gotcha where using `typeof` on a `null` value 459 * results in `'object'`. 460 * 461 * @param {Object} value 462 * @return {Boolean} 463 */ 464 function isObject(value) { 465 return !!value && (typeof value === 'undefined' ? 'undefined' : _typeof(value)) === 'object'; 466 } 467 468 /** 469 * Returns whether an object appears to be a "plain" object - that is, a 470 * direct instance of `Object`. 471 * 472 * @param {Object} value 473 * @return {Boolean} 474 */ 475 function isPlain(value) { 476 return isObject(value) && toString.call(value) === '[object Object]' && value.constructor === Object; 477 } 478 479 /** 480 * @file computed-style.js 481 * @module computed-style 482 */ 483 484 /** 485 * A safe getComputedStyle. 486 * 487 * This is needed because in Firefox, if the player is loaded in an iframe with 488 * `display:none`, then `getComputedStyle` returns `null`, so, we do a null-check to 489 * make sure that the player doesn't break in these cases. 490 * 491 * @param {Element} el 492 * The element you want the computed style of 493 * 494 * @param {string} prop 495 * The property name you want 496 * 497 * @see https://bugzilla.mozilla.org/show_bug.cgi?id=548397 498 * 499 * @static 500 * @const 501 */ 502 function computedStyle(el, prop) { 503 if (!el || !prop) { 504 return ''; 505 } 506 507 if (typeof window_1.getComputedStyle === 'function') { 508 var cs = window_1.getComputedStyle(el); 509 510 return cs ? cs[prop] : ''; 511 } 512 513 return ''; 514 } 515 516 var _templateObject = taggedTemplateLiteralLoose(['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 ', ' to ', '.'], ['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 ', ' to ', '.']); 517 518 /** 519 * Detect if a value is a string with any non-whitespace characters. 520 * 521 * @param {string} str 522 * The string to check 523 * 524 * @return {boolean} 525 * - True if the string is non-blank 526 * - False otherwise 527 * 528 */ 529 function isNonBlankString(str) { 530 return typeof str === 'string' && /\S/.test(str); 531 } 532 533 /** 534 * Throws an error if the passed string has whitespace. This is used by 535 * class methods to be relatively consistent with the classList API. 536 * 537 * @param {string} str 538 * The string to check for whitespace. 539 * 540 * @throws {Error} 541 * Throws an error if there is whitespace in the string. 542 * 543 */ 544 function throwIfWhitespace(str) { 545 if (/\s/.test(str)) { 546 throw new Error('class has illegal whitespace characters'); 547 } 548 } 549 550 /** 551 * Produce a regular expression for matching a className within an elements className. 552 * 553 * @param {string} className 554 * The className to generate the RegExp for. 555 * 556 * @return {RegExp} 557 * The RegExp that will check for a specific `className` in an elements 558 * className. 559 */ 560 function classRegExp(className) { 561 return new RegExp('(^|\\s)' + className + '($|\\s)'); 562 } 563 564 /** 565 * Whether the current DOM interface appears to be real. 566 * 567 * @return {Boolean} 568 */ 569 function isReal() { 570 // Both document and window will never be undefined thanks to `global`. 571 return document_1 === window_1.document; 572 } 573 574 /** 575 * Determines, via duck typing, whether or not a value is a DOM element. 576 * 577 * @param {Mixed} value 578 * The thing to check 579 * 580 * @return {boolean} 581 * - True if it is a DOM element 582 * - False otherwise 583 */ 584 function isEl(value) { 585 return isObject(value) && value.nodeType === 1; 586 } 587 588 /** 589 * Determines if the current DOM is embedded in an iframe. 590 * 591 * @return {boolean} 592 * 593 */ 594 function isInFrame() { 595 596 // We need a try/catch here because Safari will throw errors when attempting 597 // to get either `parent` or `self` 598 try { 599 return window_1.parent !== window_1.self; 600 } catch (x) { 601 return true; 602 } 603 } 604 605 /** 606 * Creates functions to query the DOM using a given method. 607 * 608 * @param {string} method 609 * The method to create the query with. 610 * 611 * @return {Function} 612 * The query method 613 */ 614 function createQuerier(method) { 615 return function (selector, context) { 616 if (!isNonBlankString(selector)) { 617 return document_1[method](null); 618 } 619 if (isNonBlankString(context)) { 620 context = document_1.querySelector(context); 621 } 622 623 var ctx = isEl(context) ? context : document_1; 624 625 return ctx[method] && ctx[method](selector); 626 }; 627 } 628 629 /** 630 * Creates an element and applies properties. 631 * 632 * @param {string} [tagName='div'] 633 * Name of tag to be created. 634 * 635 * @param {Object} [properties={}] 636 * Element properties to be applied. 637 * 638 * @param {Object} [attributes={}] 639 * Element attributes to be applied. 640 * 641 * @param {String|Element|TextNode|Array|Function} [content] 642 * Contents for the element (see: {@link dom:normalizeContent}) 643 * 644 * @return {Element} 645 * The element that was created. 646 */ 647 function createEl() { 648 var tagName = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'div'; 649 var properties = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 650 var attributes = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 651 var content = arguments[3]; 652 653 var el = document_1.createElement(tagName); 654 655 Object.getOwnPropertyNames(properties).forEach(function (propName) { 656 var val = properties[propName]; 657 658 // See #2176 659 // We originally were accepting both properties and attributes in the 660 // same object, but that doesn't work so well. 661 if (propName.indexOf('aria-') !== -1 || propName === 'role' || propName === 'type') { 662 log$1.warn(tsml(_templateObject, propName, val)); 663 el.setAttribute(propName, val); 664 665 // Handle textContent since it's not supported everywhere and we have a 666 // method for it. 667 } else if (propName === 'textContent') { 668 textContent(el, val); 669 } else { 670 el[propName] = val; 671 } 672 }); 673 674 Object.getOwnPropertyNames(attributes).forEach(function (attrName) { 675 el.setAttribute(attrName, attributes[attrName]); 676 }); 677 678 if (content) { 679 appendContent(el, content); 680 } 681 682 return el; 683 } 684 685 /** 686 * Injects text into an element, replacing any existing contents entirely. 687 * 688 * @param {Element} el 689 * The element to add text content into 690 * 691 * @param {string} text 692 * The text content to add. 693 * 694 * @return {Element} 695 * The element with added text content. 696 */ 697 function textContent(el, text) { 698 if (typeof el.textContent === 'undefined') { 699 el.innerText = text; 700 } else { 701 el.textContent = text; 702 } 703 return el; 704 } 705 706 /** 707 * Insert an element as the first child node of another 708 * 709 * @param {Element} child 710 * Element to insert 711 * 712 * @param {Element} parent 713 * Element to insert child into 714 */ 715 function prependTo(child, parent) { 716 if (parent.firstChild) { 717 parent.insertBefore(child, parent.firstChild); 718 } else { 719 parent.appendChild(child); 720 } 721 } 722 723 /** 724 * Check if an element has a CSS class 725 * 726 * @param {Element} element 727 * Element to check 728 * 729 * @param {string} classToCheck 730 * Class name to check for 731 * 732 * @return {boolean} 733 * - True if the element had the class 734 * - False otherwise. 735 * 736 * @throws {Error} 737 * Throws an error if `classToCheck` has white space. 738 */ 739 function hasClass(element, classToCheck) { 740 throwIfWhitespace(classToCheck); 741 if (element.classList) { 742 return element.classList.contains(classToCheck); 743 } 744 return classRegExp(classToCheck).test(element.className); 745 } 746 747 /** 748 * Add a CSS class name to an element 749 * 750 * @param {Element} element 751 * Element to add class name to. 752 * 753 * @param {string} classToAdd 754 * Class name to add. 755 * 756 * @return {Element} 757 * The dom element with the added class name. 758 */ 759 function addClass(element, classToAdd) { 760 if (element.classList) { 761 element.classList.add(classToAdd); 762 763 // Don't need to `throwIfWhitespace` here because `hasElClass` will do it 764 // in the case of classList not being supported. 765 } else if (!hasClass(element, classToAdd)) { 766 element.className = (element.className + ' ' + classToAdd).trim(); 767 } 768 769 return element; 770 } 771 772 /** 773 * Remove a CSS class name from an element 774 * 775 * @param {Element} element 776 * Element to remove a class name from. 777 * 778 * @param {string} classToRemove 779 * Class name to remove 780 * 781 * @return {Element} 782 * The dom element with class name removed. 783 */ 784 function removeClass(element, classToRemove) { 785 if (element.classList) { 786 element.classList.remove(classToRemove); 787 } else { 788 throwIfWhitespace(classToRemove); 789 element.className = element.className.split(/\s+/).filter(function (c) { 790 return c !== classToRemove; 791 }).join(' '); 792 } 793 794 return element; 795 } 796 797 /** 798 * The callback definition for toggleElClass. 799 * 800 * @callback Dom~PredicateCallback 801 * @param {Element} element 802 * The DOM element of the Component. 803 * 804 * @param {string} classToToggle 805 * The `className` that wants to be toggled 806 * 807 * @return {boolean|undefined} 808 * - If true the `classToToggle` will get added to `element`. 809 * - If false the `classToToggle` will get removed from `element`. 810 * - If undefined this callback will be ignored 811 */ 812 813 /** 814 * Adds or removes a CSS class name on an element depending on an optional 815 * condition or the presence/absence of the class name. 816 * 817 * @param {Element} element 818 * The element to toggle a class name on. 819 * 820 * @param {string} classToToggle 821 * The class that should be toggled 822 * 823 * @param {boolean|PredicateCallback} [predicate] 824 * See the return value for {@link Dom~PredicateCallback} 825 * 826 * @return {Element} 827 * The element with a class that has been toggled. 828 */ 829 function toggleClass(element, classToToggle, predicate) { 830 831 // This CANNOT use `classList` internally because IE11 does not support the
vendor: 4,356 bytes, lines 832-989
832 // second parameter to the `classList.toggle()` method! Which is fine because 833 // `classList` will be used by the add/remove functions. 834 var has = hasClass(element, classToToggle); 835 836 if (typeof predicate === 'function') { 837 predicate = predicate(element, classToToggle); 838 } 839 840 if (typeof predicate !== 'boolean') { 841 predicate = !has; 842 } 843 844 // If the necessary class operation matches the current state of the 845 // element, no action is required. 846 if (predicate === has) { 847 return; 848 } 849 850 if (predicate) { 851 addClass(element, classToToggle); 852 } else { 853 removeClass(element, classToToggle); 854 } 855 856 return element; 857 } 858 859 /** 860 * Apply attributes to an HTML element. 861 * 862 * @param {Element} el 863 * Element to add attributes to. 864 * 865 * @param {Object} [attributes] 866 * Attributes to be applied. 867 */ 868 function setAttributes(el, attributes) { 869 Object.getOwnPropertyNames(attributes).forEach(function (attrName) { 870 var attrValue = attributes[attrName]; 871 872 if (attrValue === null || typeof attrValue === 'undefined' || attrValue === false) { 873 el.removeAttribute(attrName); 874 } else { 875 el.setAttribute(attrName, attrValue === true ? '' : attrValue); 876 } 877 }); 878 } 879 880 /** 881 * Get an element's attribute values, as defined on the HTML tag 882 * Attributes are not the same as properties. They're defined on the tag 883 * or with setAttribute (which shouldn't be used with HTML) 884 * This will return true or false for boolean attributes. 885 * 886 * @param {Element} tag 887 * Element from which to get tag attributes. 888 * 889 * @return {Object} 890 * All attributes of the element. 891 */ 892 function getAttributes(tag) { 893 var obj = {}; 894 895 // known boolean attributes 896 // we can check for matching boolean properties, but not all browsers 897 // and not all tags know about these attributes, so, we still want to check them manually 898 var knownBooleans = ',' + 'autoplay,controls,playsinline,loop,muted,default,defaultMuted' + ','; 899 900 if (tag && tag.attributes && tag.attributes.length > 0) { 901 var attrs = tag.attributes; 902 903 for (var i = attrs.length - 1; i >= 0; i--) { 904 var attrName = attrs[i].name; 905 var attrVal = attrs[i].value; 906 907 // check for known booleans 908 // the matching element property will return a value for typeof 909 if (typeof tag[attrName] === 'boolean' || knownBooleans.indexOf(',' + attrName + ',') !== -1) { 910 // the value of an included boolean attribute is typically an empty 911 // string ('') which would equal false if we just check for a false value. 912 // we also don't want support bad code like autoplay='false' 913 attrVal = attrVal !== null ? true : false; 914 } 915 916 obj[attrName] = attrVal; 917 } 918 } 919 920 return obj; 921 } 922 923 /** 924 * Get the value of an element's attribute 925 * 926 * @param {Element} el 927 * A DOM element 928 * 929 * @param {string} attribute 930 * Attribute to get the value of 931 * 932 * @return {string} 933 * value of the attribute 934 */ 935 function getAttribute(el, attribute) { 936 return el.getAttribute(attribute); 937 } 938 939 /** 940 * Set the value of an element's attribute 941 * 942 * @param {Element} el 943 * A DOM element 944 * 945 * @param {string} attribute 946 * Attribute to set 947 * 948 * @param {string} value 949 * Value to set the attribute to 950 */ 951 function setAttribute(el, attribute, value) { 952 el.setAttribute(attribute, value); 953 } 954 955 /** 956 * Remove an element's attribute 957 * 958 * @param {Element} el 959 * A DOM element 960 * 961 * @param {string} attribute 962 * Attribute to remove 963 */ 964 function removeAttribute(el, attribute) { 965 el.removeAttribute(attribute); 966 } 967 968 /** 969 * Attempt to block the ability to select text while dragging controls 970 */ 971 function blockTextSelection() { 972 document_1.body.focus(); 973 document_1.onselectstart = function () { 974 return false; 975 }; 976 } 977 978 /** 979 * Turn off text selection blocking 980 */ 981 function unblockTextSelection() { 982 document_1.onselectstart = function () { 983 return true; 984 }; 985 } 986 987 /** 988 * Identical to the native `getBoundingClientRect` function, but ensures that 989 * the method is supported at all (it is in all browsers we claim to supp
989ort) 990 * and that the element is in the DOM before continuing. 991 * 992 * This wrapper function also shims properties which are not provided by some 993 * older browsers (namely, IE8). 994 * 995 * Additionally, some browsers do not support adding properties to a 996 * `ClientRect`/`DOMRect` object; so, we shallow-copy it with the standard 997 * properties (except `x` and `y` which are not widely supported). This helps 998 * avoid implementations where keys are non-enumerable. 999 * 1000 * @param {Element} el 1001 * Element whose `ClientRect` we want to calculate. 1002 * 1003 * @return {Object|undefined} 1004 * Always returns a plain 1005 */ 1006 function getBoundingClientRect(el) { 1007 if (el && el.getBoundingClientRect && el.parentNode) { 1008 var rect = el.getBoundingClientRect(); 1009 var result = {}; 1010 1011 ['bottom', 'height', 'left', 'right', 'top', 'width'].forEach(function (k) { 1012 if (rect[k] !== undefined) { 1013 result[k] = rect[k]; 1014 } 1015 }); 1016 1017 if (!result.height) { 1018 result.height = parseFloat(computedStyle(el, 'height')); 1019 } 1020 1021 if (!result.width) { 1022 result.width = parseFloat(computedStyle(el, 'width')); 1023 } 1024 1025 return result; 1026 } 1027 } 1028 1029 /** 1030 * The postion of a DOM element on the page. 1031 * 1032 * @typedef {Object} module:dom~Position 1033 * 1034 * @property {number} left 1035 * Pixels to the left 1036 * 1037 * @property {number} top 1038 * Pixels on top 1039 */ 1040 1041 /** 1042 * Offset Left. 1043 * getBoundingClientRect technique from 1044 * John Resig 1045 * 1046 * @see http://ejohn.org/blog/getboundingclientrect-is-awesome/ 1047 * 1048 * @param {Element} el 1049 * Element from which to get offset 1050 * 1051 * @return {module:dom~Position} 1052 * The position of the element that was passed in. 1053 */ 1054 function findPosition(el) { 1055 var box = void 0; 1056 1057 if (el.getBoundingClientRect && el.parentNode) { 1058 box = el.getBoundingClientRect(); 1059 } 1060 1061 if (!box) { 1062 return { 1063 left: 0, 1064 top: 0 1065 }; 1066 } 1067 1068 var docEl = document_1.documentElement; 1069 var body = document_1.body; 1070 1071 var clientLeft = docEl.clientLeft || body.clientLeft || 0; 1072 var scrollLeft = window_1.pageXOffset || body.scrollLeft; 1073 var left = box.left + scrollLeft - clientLeft; 1074 1075 var clientTop = docEl.clientTop || body.clientTop || 0; 1076 var scrollTop = window_1.pageYOffset || body.scrollTop; 1077 var top = box.top + scrollTop - clientTop; 1078 1079 // Android sometimes returns slightly off decimal values, so need to round 1080 return { 1081 left: Math.round(left), 1082 top: Math.round(top) 1083 }; 1084 } 1085 1086 /** 1087 * x and y coordinates for a dom element or mouse pointer 1088 * 1089 * @typedef {Object} Dom~Coordinates 1090 * 1091 * @property {number} x 1092 * x coordinate in pixels 1093 * 1094 * @property {number} y 1095 * y coordinate in pixels 1096 */ 1097 1098 /** 1099 * Get pointer position in element 1100 * Returns an object with x and y coordinates. 1101 * The base on the coordinates are the bottom left of the element. 1102 * 1103 * @param {Element} el 1104 * Element on which to get the pointer position on 1105 * 1106 * @param {EventTarget~Event} event 1107 * Event object 1108 * 1109 * @return {Dom~Coordinates} 1110 * A Coordinates object corresponding to the mouse position. 1111 * 1112 */ 1113 function getPointerPosition(el, event) { 1114 var position = {}; 1115 var box = findPosition(el); 1116 var boxW = el.offsetWidth; 1117 var boxH = el.offsetHeight; 1118 1119 var boxY = box.top; 1120 var boxX = box.left; 1121 var pageY = event.pageY; 1122 var pageX = event.pageX; 1123 1124 if (event.changedTouches) { 1125 pageX = event.changedTouches[0].pageX; 1126 pageY = event.changedTouches[0].pageY; 1127 } 1128 1129 position.y = Math.max(0, Math.min(1, (boxY - pageY + boxH) / boxH)); 1130 position.x = Math.max(0, Math.min(1, (pageX - boxX) / boxW)); 1131 1132 return position; 1133 } 1134 1135 /** 1136 * Determines, via duck typing, whether or not a value is a text node. 1137 * 1138 * @param {Mixed} value 1139 * Check if this value is a text node. 1140 * 1141 * @return {boolean} 1142 * - True if it is a text node 1143 * - False otherwise 1144 */ 1145 function isTextNode(value) { 1146 return isObject(value) && value.nodeType === 3; 1147 } 1148 1149 /** 1150 * Empties the contents of an element. 1151 * 1152 * @param {Element} el 1153 * The element to empty children from 1154 * 1155 * @return {Element} 1156 * The element with no children 1157 */ 1158 function emptyEl(el) { 1159 while (el.firstChild) { 1160 el.removeChild(el.firstChild); 1161 } 1162 return el; 1163 } 1164 1165 /** 1166 * Normalizes content for eventual insertion into the DOM. 1167 * 1168 * This allows a wide range of content definition methods, but protects 1169 * from falling into the trap of simply writing to `innerHTML`, which is 1170 * an XSS concern. 1171 * 1172 * The content for an element can be passed in multiple types and 1173 * combinations, whose behavior is as follows: 1174 * 1175 * @param {String|Element|TextNode|Array|Function} content 1176 * - String: Normalized into a text node. 1177 * - Element/TextNode: Passed through.
1178 * - Array: A one-dimensional array of strings, elements, nodes, or functions 1179 * (which return single strings, elements, or nodes). 1180 * - Function: If the sole argument, is expected to produce a string, element, 1181 * node, or array as defined above. 1182 * 1183 * @return {Array} 1184 * All of the content that was passed in normalized. 1185 */ 1186 function normalizeContent(content) { 1187 1188 // First, invoke content if it is a function. If it produces an array, 1189 // that needs to happen before normalization. 1190 if (typeof content === 'function') { 1191 content = content(); 1192 } 1193 1194 // Next up, normalize to an array, so one or many items can be normalized, 1195 // filtered, and returned. 1196 return (Array.isArray(content) ? content : [content]).map(function (value) { 1197 1198 // First, invoke value if it is a function to produce a new value, 1199 // which will be subsequently normalized to a Node of some kind. 1200 if (typeof value === 'function') { 1201 value = value(); 1202 } 1203 1204 if (isEl(value) || isTextNode(value)) { 1205 return value; 1206 } 1207 1208 if (typeof value === 'string' && /\S/.test(value)) { 1209 return document_1.createTextNode(value); 1210 } 1211 }).filter(function (value) { 1212 return value; 1213 }); 1214 } 1215 1216 /** 1217 * Normalizes and appends content to an element. 1218 * 1219 * @param {Element} el 1220 * Element to append normalized content to. 1221 * 1222 * 1223 * @param {String|Element|TextNode|Array|Function} content 1224 * See the `content` argument of {@link dom:normalizeContent} 1225 * 1226 * @return {Element} 1227 * The element with appended normalized content. 1228 */ 1229 function appendContent(el, content) { 1230 normalizeContent(content).forEach(function (node) { 1231 return el.appendChild(node); 1232 }); 1233 return el; 1234 } 1235 1236 /** 1237 * Normalizes and inserts content into an element; this is identical to 1238 * `appendContent()`, except it empties the element first. 1239 * 1240 * @param {Element} el 1241 * Element to insert normalized content into. 1242 * 1243 * @param {String|Element|TextNode|Array|Function} content 1244 * See the `content` argument of {@link dom:normalizeContent} 1245 * 1246 * @return {Element} 1247 * The element with inserted normalized content. 1248 * 1249 */ 1250 function insertContent(el, content) { 1251 return appendContent(emptyEl(el), content); 1252 } 1253 1254 /** 1255 * Check if event was a single left click 1256 * 1257 * @param {EventTarget~Event} event 1258 * Event object 1259 * 1260 * @return {boolean} 1261 * - True if a left click 1262 * - False if not a left click 1263 */ 1264 function isSingleLeftClick(event) { 1265 // Note: if you create something draggable, be sure to 1266 // call it on both `mousedown` and `mousemove` event, 1267 // otherwise `mousedown` should be enough for a button 1268 1269 if (event.button === undefined && event.buttons === undefined) { 1270 // Why do we need `buttons` ? 1271 // Because, middle mouse sometimes have this: 1272 // e.button === 0 and e.buttons === 4 1273 // Furthermore, we want to prevent combination click, something like 1274 // HOLD middlemouse then left click, that would be 1275 // e.button === 0, e.buttons === 5 1276 // just `button` is not gonna work 1277 1278 // Alright, then what this block does ? 1279 // this is for chrome `simulate mobile devices` 1280 // I want to support this as well 1281 1282 return true; 1283 } 1284 1285 if (event.button === 0 && event.buttons === undefined) { 1286 // Touch screen, sometimes on some specific device, `buttons` 1287 // doesn't have anything (safari on ios, blackberry...) 1288 1289 return true; 1290 } 1291 1292 if (event.button !== 0 || event.buttons !== 1) { 1293 // This is the reason we have those if else block above 1294 // if any special case we can catch and let it slide 1295 // we do it above, when get to here, this definitely 1296 // is-not-left-click 1297 1298 return false; 1299 } 1300 1301 return true; 1302 } 1303 1304 /** 1305 * Finds a single DOM element matching `selector` within the optional 1306 * `context` of another DOM element (defaulting to `document`). 1307 * 1308 * @param {string} selector 1309 * A valid CSS selector, which will be passed to `querySelector`. 1310 * 1311 * @param {Element|String} [context=document] 1312 * A DOM element within which to query. Can also be a selector
1313 * string in which case the first matching element will be used 1314 * as context. If missing (or no element matches selector), falls 1315 * back to `document`. 1316 * 1317 * @return {Element|null} 1318 * The element that was found or null. 1319 */ 1320 var $ = createQuerier('querySelector'); 1321 1322 /** 1323 * Finds a all DOM elements matching `selector` within the optional 1324 * `context` of another DOM element (defaulting to `document`). 1325 * 1326 * @param {string} selector 1327 * A valid CSS selector, which will be passed to `querySelectorAll`. 1328 * 1329 * @param {Element|String} [context=document] 1330 * A DOM element within which to query. Can also be a selector 1331 * string in which case the first matching element will be used 1332 * as context. If missing (or no element matches selector), falls 1333 * back to `document`. 1334 * 1335 * @return {NodeList} 1336 * A element list of elements that were found. Will be empty if none were found. 1337 * 1338 */ 1339 var $$ = createQuerier('querySelectorAll'); 1340 1341 var Dom = /*#__PURE__*/Object.freeze({ 1342 isReal: isReal, 1343 isEl: isEl, 1344 isInFrame: isInFrame, 1345 createEl: createEl, 1346 textContent: textContent, 1347 prependTo: prependTo, 1348 hasClass: hasClass, 1349 addClass: addClass, 1350 removeClass: removeClass, 1351 toggleClass: toggleClass, 1352 setAttributes: setAttributes, 1353 getAttributes: getAttributes, 1354 getAttribute: getAttribute, 1355 setAttribute: setAttribute, 1356 removeAttribute: removeAttribute, 1357 blockTextSelection: blockTextSelection, 1358 unblockTextSelection: unblockTextSelection, 1359 getBoundingClientRect: getBoundingClientRect, 1360 findPosition: findPosition, 1361 getPointerPosition: getPointerPosition, 1362 isTextNode: isTextNode, 1363 emptyEl: emptyEl, 1364 normalizeContent: normalizeContent, 1365 appendContent: appendContent, 1366 insertContent: insertContent, 1367 isSingleLeftClick: isSingleLeftClick, 1368 $: $, 1369 $$: $$ 1370 }); 1371 1372 /** 1373 * @file guid.js 1374 * @module guid 1375 */ 1376 1377 /** 1378 * Unique ID for an element or function 1379 * @type {Number} 1380 */ 1381 var _guid = 1; 1382 1383 /** 1384 * Get a unique auto-incrementing ID by number that has not been returned before. 1385 * 1386 * @return {number} 1387 * A new unique ID. 1388 */ 1389 function newGUID() { 1390 return _guid++; 1391 } 1392 1393 /** 1394 * @file dom-data.js 1395 * @module dom-data 1396 */ 1397 1398 /** 1399 * Element Data Store. 1400 * 1401 * Allows for binding data to an element without putting it directly on the 1402 * element. Ex. Event listeners are stored here. 1403 * (also from jsninja.com, slightly modified and updated for closure compiler) 1404 * 1405 * @type {Object} 1406 * @private 1407 */ 1408 var elData = {}; 1409 1410 /* 1411 * Unique attribute name to store an element's guid in 1412 * 1413 * @type {String} 1414 * @constant 1415 * @private 1416 */ 1417 var elIdAttr = 'vdata' + new Date().getTime(); 1418 1419 /** 1420 * Returns the cache object where data for an element is stored 1421 * 1422 * @param {Element} el 1423 * Element to store data for. 1424 * 1425 * @return {Object} 1426 * The cache object for that el that was passed in. 1427 */ 1428 function getData(el) { 1429 var id = el[elIdAttr]; 1430 1431 if (!id) { 1432 id = el[elIdAttr] = newGUID(); 1433 } 1434 1435 if (!elData[id]) { 1436 elData[id] = {}; 1437 } 1438 1439 return elData[id]; 1440 } 1441 1442 /** 1443 * Returns whether or not an element has cached data 1444 * 1445 * @param {Element} el 1446 * Check if this element has cached data. 1447 * 1448 * @return {boolean} 1449 * - True if the DOM element has cached data. 1450 * - False otherwise. 1451 */ 1452 function hasData(el) { 1453 var id = el[elIdAttr]; 1454 1455 if (!id) { 1456 return false; 1457 } 1458 1459 return !!Object.getOwnPropertyNames(elData[id]).length; 1460 } 1461 1462 /** 1463 * Delete data for the element from the cache and the guid attr from getElementById 1464 * 1465 * @param {Element} el 1466 * Remove cached data for this element. 1467 */ 1468 function removeData(el) { 1469 var id = el[elIdAttr]; 1470 1471 if (!id) { 1472 return; 1473 } 1474 1475 // Remove all stored data 1476 delete elData[id]; 1477 1478 // Remove the elIdAttr property from the DOM node 1479 try { 1480 delete el[elIdAttr]; 1481 } catch (e) { 1482 if (el.removeAttribute) { 1483 el.removeAttribute(elIdAttr); 1484 } else {
vendor: 4,616 bytes, lines 1485-1622
1485 // IE doesn't appear to support removeAttribute on the document element 1486 el[elIdAttr] = null; 1487 } 1488 } 1489 } 1490 1491 /** 1492 * @file events.js. An Event System (John Resig - Secrets of a JS Ninja http://jsninja.com/) 1493 * (Original book version wasn't completely usable, so fixed some things and made Closure Compiler compatible) 1494 * This should work very similarly to jQuery's events, however it's based off the book version which isn't as 1495 * robust as jquery's, so there's probably some differences. 1496 * 1497 * @module events 1498 */ 1499 1500 /** 1501 * Clean up the listener cache and dispatchers 1502 * 1503 * @param {Element|Object} elem 1504 * Element to clean up 1505 * 1506 * @param {string} type 1507 * Type of event to clean up 1508 */ 1509 function _cleanUpEvents(elem, type) { 1510 var data = getData(elem); 1511 1512 // Remove the events of a particular type if there are none left 1513 if (data.handlers[type].length === 0) { 1514 delete data.handlers[type]; 1515 // data.handlers[type] = null; 1516 // Setting to null was causing an error with data.handlers 1517 1518 // Remove the meta-handler from the element 1519 if (elem.removeEventListener) { 1520 elem.removeEventListener(type, data.dispatcher, false); 1521 } else if (elem.detachEvent) { 1522 elem.detachEvent('on' + type, data.dispatcher); 1523 } 1524 } 1525 1526 // Remove the events object if there are no types left 1527 if (Object.getOwnPropertyNames(data.handlers).length <= 0) { 1528 delete data.handlers; 1529 delete data.dispatcher; 1530 delete data.disabled; 1531 } 1532 1533 // Finally remove the element data if there is no data left 1534 if (Object.getOwnPropertyNames(data).length === 0) { 1535 removeData(elem); 1536 } 1537 } 1538 1539 /** 1540 * Loops through an array of event types and calls the requested method for each type. 1541 * 1542 * @param {Function} fn 1543 * The event method we want to use. 1544 * 1545 * @param {Element|Object} elem 1546 * Element or object to bind listeners to 1547 * 1548 * @param {string} type 1549 * Type of event to bind to. 1550 * 1551 * @param {EventTarget~EventListener} callback 1552 * Event listener. 1553 */ 1554 function _handleMultipleEvents(fn, elem, types, callback) { 1555 types.forEach(function (type) { 1556 // Call the event method for each one of the types 1557 fn(elem, type, callback); 1558 }); 1559 } 1560 1561 /** 1562 * Fix a native event to have standard property values 1563 * 1564 * @param {Object} event 1565 * Event object to fix. 1566 * 1567 * @return {Object} 1568 * Fixed event object. 1569 */ 1570 function fixEvent(event) { 1571 1572 function returnTrue() { 1573 return true; 1574 } 1575 1576 function returnFalse() { 1577 return false; 1578 } 1579 1580 // Test if fixing up is needed 1581 // Used to check if !event.stopPropagation instead of isPropagationStopped 1582 // But native events return true for stopPropagation, but don't have 1583 // other expected methods like isPropagationStopped. Seems to be a problem 1584 // with the Javascript Ninja code. So we're just overriding all events now. 1585 if (!event || !event.isPropagationStopped) { 1586 var old = event || window_1.event; 1587 1588 event = {}; 1589 // Clone the old object so that we can modify the values event = {}; 1590 // IE8 Doesn't like when you mess with native event properties 1591 // Firefox returns false for event.hasOwnProperty('type') and other props 1592 // which makes copying more difficult. 1593 // TODO: Probably best to create a whitelist of event props 1594 for (var key in old) { 1595 // Safari 6.0.3 warns you if you try to copy deprecated layerX/Y 1596 // Chrome warns you if you try to copy deprecated keyboardEvent.keyLocation 1597 // and webkitMovementX/Y 1598 if (key !== 'layerX' && key !== 'layerY' && key !== 'keyLocation' && key !== 'webkitMovementX' && key !== 'webkitMovementY') { 1599 // Chrome 32+ warns if you try to copy deprecated returnValue, but 1600 // we still want to if preventDefault isn't supported (IE8). 1601 if (!(key === 'returnValue' && old.preventDefault)) { 1602 event[key] = old[key]; 1603 } 1604 } 1605 } 1606 1607 // The event occurred on this element 1608 if (!event.target) { 1609 event.target = event.srcElement || document_1; 1610 } 1611 1612 // Handle which other element the event is related to 1613 if (!event.relatedTarget) { 1614 event.relatedTarget = event.fromElement === event.target ? event.toElement : event.fromElement; 1615 } 1616 1617 // Stop the default browser action 1618 event.preventDefault = function () { 1619 if (old.preventDefault) { 1620 old.preventDefault(); 1621 } 1622 event.returnValue = false;
vendor: 12,968 bytes, lines 1623-2093
1623 old.returnValue = false; 1624 event.defaultPrevented = true; 1625 }; 1626 1627 event.defaultPrevented = false; 1628 1629 // Stop the event from bubbling 1630 event.stopPropagation = function () { 1631 if (old.stopPropagation) { 1632 old.stopPropagation(); 1633 } 1634 event.cancelBubble = true; 1635 old.cancelBubble = true; 1636 event.isPropagationStopped = returnTrue; 1637 }; 1638 1639 event.isPropagationStopped = returnFalse; 1640 1641 // Stop the event from bubbling and executing other handlers 1642 event.stopImmediatePropagation = function () { 1643 if (old.stopImmediatePropagation) { 1644 old.stopImmediatePropagation(); 1645 } 1646 event.isImmediatePropagationStopped = returnTrue; 1647 event.stopPropagation(); 1648 }; 1649 1650 event.isImmediatePropagationStopped = returnFalse; 1651 1652 // Handle mouse position 1653 if (event.clientX !== null && event.clientX !== undefined) { 1654 var doc = document_1.documentElement; 1655 var body = document_1.body; 1656 1657 event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0); 1658 event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0); 1659 } 1660 1661 // Handle key presses 1662 event.which = event.charCode || event.keyCode; 1663 1664 // Fix button for mouse clicks: 1665 // 0 == left; 1 == middle; 2 == right 1666 if (event.button !== null && event.button !== undefined) { 1667 1668 // The following is disabled because it does not pass videojs-standard 1669 // and... yikes. 1670 /* eslint-disable */ 1671 event.button = event.button & 1 ? 0 : event.button & 4 ? 1 : event.button & 2 ? 2 : 0; 1672 /* eslint-enable */ 1673 } 1674 } 1675 1676 // Returns fixed-up instance 1677 return event; 1678 } 1679 1680 /** 1681 * Whether passive event listeners are supported 1682 */ 1683 var _supportsPassive = false; 1684 1685 (function () { 1686 try { 1687 var opts = Object.defineProperty({}, 'passive', { 1688 get: function get() { 1689 _supportsPassive = true; 1690 } 1691 }); 1692 1693 window_1.addEventListener('test', null, opts); 1694 window_1.removeEventListener('test', null, opts); 1695 } catch (e) { 1696 // disregard 1697 } 1698 })(); 1699 1700 /** 1701 * Touch events Chrome expects to be passive 1702 */ 1703 var passiveEvents = ['touchstart', 'touchmove']; 1704 1705 /** 1706 * Add an event listener to element 1707 * It stores the handler function in a separate cache object 1708 * and adds a generic handler to the element's event, 1709 * along with a unique id (guid) to the element. 1710 * 1711 * @param {Element|Object} elem 1712 * Element or object to bind listeners to 1713 * 1714 * @param {string|string[]} type 1715 * Type of event to bind to. 1716 * 1717 * @param {EventTarget~EventListener} fn 1718 * Event listener. 1719 */ 1720 function on(elem, type, fn) { 1721 if (Array.isArray(type)) { 1722 return _handleMultipleEvents(on, elem, type, fn); 1723 } 1724 1725 var data = getData(elem); 1726 1727 // We need a place to store all our handler data 1728 if (!data.handlers) { 1729 data.handlers = {}; 1730 } 1731 1732 if (!data.handlers[type]) { 1733 data.handlers[type] = []; 1734 } 1735 1736 if (!fn.guid) { 1737 fn.guid = newGUID(); 1738 } 1739 1740 data.handlers[type].push(fn); 1741 1742 if (!data.dispatcher) { 1743 data.disabled = false; 1744 1745 data.dispatcher = function (event, hash) { 1746 1747 if (data.disabled) { 1748 return; 1749 } 1750 1751 event = fixEvent(event); 1752 1753 var handlers = data.handlers[event.type]; 1754 1755 if (handlers) { 1756 // Copy handlers so if handlers are added/removed during the process it doesn't throw everything off. 1757 var handlersCopy = handlers.slice(0); 1758 1759 for (var m = 0, n = handlersCopy.length; m < n; m++) { 1760 if (event.isImmediatePropagationStopped()) { 1761 break; 1762 } else { 1763 try { 1764 handlersCopy[m].call(elem, event, hash); 1765 } catch (e) { 1766 log$1.error(e); 1767 } 1768 } 1769 } 1770 } 1771 }; 1772 } 1773 1774 if (data.handlers[type].length === 1) { 1775 if (elem.addEventListener) { 1776 var options = false; 1777 1778 if (_supportsPassive && passiveEvents.indexOf(type) > -1) { 1779 options = { passive: true }; 1780 } 1781 elem.addEventListener(type, data.dispatcher, options); 1782 } else if (elem.attachEvent) { 1783 elem.attachEvent('on' + type, data.dispatcher); 1784 } 1785 } 1786 } 1787 1788 /** 1789 * Removes event listeners from an element 1790 * 1791 * @param {Element|Object} elem 1792 * Object to remove listeners from. 1793 * 1794 * @param {string|string[]} [type] 1795 * Type of listener to remove. Don't include to remove all events from element. 1796 * 1797 * @param {EventTarget~EventListener} [fn] 1798 * Specific listener to remove. Don't include to remove listeners for an event 1799 * type. 1800 */ 1801 function off(elem, type, fn) { 1802 // Don't want to add a cache object through getElData if not needed 1803 if (!hasData(elem)) { 1804 return; 1805 } 1806 1807 var data = getData(elem); 1808 1809 // If no events exist, nothing to unbind 1810 if (!data.handlers) { 1811 return; 1812 } 1813 1814 if (Array.isArray(type)) { 1815 return _handleMultipleEvents(off, elem, type, fn); 1816 } 1817 1818 // Utility function 1819 var removeType = function removeType(el, t) { 1820 data.handlers[t] = []; 1821 _cleanUpEvents(el, t); 1822 }; 1823 1824 // Are we removing all bound events? 1825 if (type === undefined) { 1826 for (var t in data.handlers) { 1827 if (Object.prototype.hasOwnProperty.call(data.handlers || {}, t)) { 1828 removeType(elem, t); 1829 } 1830 } 1831 return; 1832 } 1833 1834 var handlers = data.handlers[type]; 1835 1836 // If no handlers exist, nothing to unbind 1837 if (!handlers) { 1838 return; 1839 } 1840 1841 // If no listener was provided, remove all listeners for type 1842 if (!fn) { 1843 removeType(elem, type); 1844 return; 1845 } 1846 1847 // We're only removing a single handler 1848 if (fn.guid) { 1849 for (var n = 0; n < handlers.length; n++) { 1850 if (handlers[n].guid === fn.guid) { 1851 handlers.splice(n--, 1); 1852 } 1853 } 1854 } 1855 1856 _cleanUpEvents(elem, type); 1857 } 1858 1859 /** 1860 * Trigger an event for an element 1861 * 1862 * @param {Element|Object} elem 1863 * Element to trigger an event on 1864 * 1865 * @param {EventTarget~Event|string} event 1866 * A string (the type) or an event object with a type attribute 1867 * 1868 * @param {Object} [hash] 1869 * data hash to pass along with the event 1870 * 1871 * @return {boolean|undefined} 1872 * - Returns the opposite of `defaultPrevented` if default was prevented 1873 * - Otherwise returns undefined 1874 */ 1875 function trigger(elem, event, hash) { 1876 // Fetches element data and a reference to the parent (for bubbling). 1877 // Don't want to add a data object to cache for every parent, 1878 // so checking hasElData first. 1879 var elemData = hasData(elem) ? getData(elem) : {}; 1880 var parent = elem.parentNode || elem.ownerDocument; 1881 // type = event.type || event, 1882 // handler; 1883 1884 // If an event name was passed as a string, creates an event out of it 1885 if (typeof event === 'string') { 1886 event = { type: event, target: elem }; 1887 } else if (!event.target) { 1888 event.target = elem; 1889 } 1890 1891 // Normalizes the event properties. 1892 event = fixEvent(event); 1893 1894 // If the passed element has a dispatcher, executes the established handlers. 1895 if (elemData.dispatcher) { 1896 elemData.dispatcher.call(elem, event, hash); 1897 } 1898 1899 // Unless explicitly stopped or the event does not bubble (e.g. media events) 1900 // recursively calls this function to bubble the event up the DOM. 1901 if (parent && !event.isPropagationStopped() && event.bubbles === true) { 1902 trigger.call(null, parent, event, hash); 1903 1904 // If at the top of the DOM, triggers the default action unless disabled. 1905 } else if (!parent && !event.defaultPrevented) { 1906 var targetData = getData(event.target); 1907 1908 // Checks if the target has a default action for this event. 1909 if (event.target[event.type]) { 1910 // Temporarily disables event dispatching on the target as we have already executed the handler. 1911 targetData.disabled = true; 1912 // Executes the default action. 1913 if (typeof event.target[event.type] === 'function') { 1914 event.target[event.type](); 1915 } 1916 // Re-enables event dispatching. 1917 targetData.disabled = false; 1918 } 1919 } 1920 1921 // Inform the triggerer if the default was prevented by returning false 1922 return !event.defaultPrevented; 1923 } 1924 1925 /** 1926 * Trigger a listener only once for an event 1927 * 1928 * @param {Element|Object} elem 1929 * Element or object to bind to. 1930 * 1931 * @param {string|string[]} type 1932 * Name/type of event 1933 * 1934 * @param {Event~EventListener} fn 1935 * Event Listener function 1936 */ 1937 function one(elem, type, fn) { 1938 if (Array.isArray(type)) { 1939 return _handleMultipleEvents(one, elem, type, fn); 1940 } 1941 var func = function func() { 1942 off(elem, type, func); 1943 fn.apply(this, arguments); 1944 }; 1945 1946 // copy the guid to the new function so it can removed using the original function's ID 1947 func.guid = fn.guid = fn.guid || newGUID(); 1948 on(elem, type, func); 1949 } 1950 1951 var Events = /*#__PURE__*/Object.freeze({ 1952 fixEvent: fixEvent, 1953 on: on, 1954 off: off, 1955 trigger: trigger, 1956 one: one 1957 }); 1958 1959 /** 1960 * @file setup.js - Functions for setting up a player without 1961 * user interaction based on the data-setup `attribute` of the video tag. 1962 * 1963 * @module setup 1964 */ 1965 1966 var _windowLoaded = false; 1967 var videojs = void 0; 1968 1969 /** 1970 * Set up any tags that have a data-setup `attribute` when the player is started. 1971 */ 1972 var autoSetup = function autoSetup() { 1973 1974 // Protect against breakage in non-browser environments and check global autoSetup option. 1975 if (!isReal() || videojs.options.autoSetup === false) { 1976 return; 1977 } 1978 1979 var vids = Array.prototype.slice.call(document_1.getElementsByTagName('video')); 1980 var audios = Array.prototype.slice.call(document_1.getElementsByTagName('audio')); 1981 var divs = Array.prototype.slice.call(document_1.getElementsByTagName('video-js')); 1982 var mediaEls = vids.concat(audios, divs); 1983 1984 // Check if any media elements exist 1985 if (mediaEls && mediaEls.length > 0) { 1986 1987 for (var i = 0, e = mediaEls.length; i < e; i++) { 1988 var mediaEl = mediaEls[i]; 1989 1990 // Check if element exists, has getAttribute func. 1991 if (mediaEl && mediaEl.getAttribute) { 1992 1993 // Make sure this player hasn't already been set up. 1994 if (mediaEl.player === undefined) { 1995 var options = mediaEl.getAttribute('data-setup'); 1996 1997 // Check if data-setup attr exists. 1998 // We only auto-setup if they've added the data-setup attr. 1999 if (options !== null) { 2000 // Create new video.js instance. 2001 videojs(mediaEl); 2002 } 2003 } 2004 2005 // If getAttribute isn't defined, we need to wait for the DOM. 2006 } else { 2007 autoSetupTimeout(1); 2008 break; 2009 } 2010 } 2011 2012 // No videos were found, so keep looping unless page is finished loading. 2013 } else if (!_windowLoaded) { 2014 autoSetupTimeout(1); 2015 } 2016 }; 2017 2018 /** 2019 * Wait until the page is loaded before running autoSetup. This will be called in 2020 * autoSetup if `hasLoaded` returns false. 2021 * 2022 * @param {number} wait 2023 * How long to wait in ms 2024 * 2025 * @param {module:videojs} [vjs] 2026 * The videojs library function 2027 */ 2028 function autoSetupTimeout(wait, vjs) { 2029 if (vjs) { 2030 videojs = vjs; 2031 } 2032 2033 window_1.setTimeout(autoSetup, wait); 2034 } 2035 2036 if (isReal() && document_1.readyState === 'complete') { 2037 _windowLoaded = true; 2038 } else { 2039 /** 2040 * Listen for the load event on window, and set _windowLoaded to true. 2041 * 2042 * @listens load 2043 */ 2044 one(window_1, 'load', function () { 2045 _windowLoaded = true; 2046 }); 2047 } 2048 2049 /** 2050 * @file stylesheet.js 2051 * @module stylesheet 2052 */ 2053 2054 /** 2055 * Create a DOM syle element given a className for it. 2056 * 2057 * @param {string} className 2058 * The className to add to the created style element. 2059 * 2060 * @return {Element} 2061 * The element that was created. 2062 */ 2063 var createStyleElement = function createStyleElement(className) { 2064 var style = document_1.createElement('style'); 2065 2066 style.className = className; 2067 2068 return style; 2069 }; 2070 2071 /** 2072 * Add text to a DOM element. 2073 * 2074 * @param {Element} el 2075 * The Element to add text content to. 2076 * 2077 * @param {string} content 2078 * The text to add to the element. 2079 */ 2080 var setTextContent = function setTextContent(el, content) { 2081 if (el.styleSheet) { 2082 el.styleSheet.cssText = content; 2083 } else { 2084 el.textContent = content; 2085 } 2086 }; 2087 2088 /** 2089 * @file fn.js 2090 * @module fn 2091 */ 2092 2093 /**
2094 * Bind (a.k.a proxy or Context). A simple method for changing the context of a function 2095 * It also stores a unique id on the function so it can be easily removed from events. 2096 * 2097 * @param {Mixed} context 2098 * The object to bind as scope. 2099 * 2100 * @param {Function} fn 2101 * The function to be bound to a scope. 2102 * 2103 * @param {number} [uid] 2104 * An optional unique ID for the function to be set 2105 * 2106 * @return {Function} 2107 * The new function that will be bound into the context given 2108 */ 2109 var bind = function bind(context, fn, uid) { 2110 // Make sure the function has a unique ID 2111 if (!fn.guid) { 2112 fn.guid = newGUID(); 2113 } 2114 2115 // Create the new function that changes the context 2116 var bound = function bound() { 2117 return fn.apply(context, arguments); 2118 }; 2119 2120 // Allow for the ability to individualize this function 2121 // Needed in the case where multiple objects might share the same prototype 2122 // IF both items add an event listener with the same function, then you try to remove just one 2123 // it will remove both because they both have the same guid. 2124 // when using this, you need to use the bind method when you remove the listener as well. 2125 // currently used in text tracks 2126 bound.guid = uid ? uid + '_' + fn.guid : fn.guid; 2127 2128 return bound; 2129 }; 2130 2131 /** 2132 * Wraps the given function, `fn`, with a new function that only invokes `fn` 2133 * at most once per every `wait` milliseconds. 2134 * 2135 * @param {Function} fn 2136 * The function to be throttled. 2137 * 2138 * @param {Number} wait 2139 * The number of milliseconds by which to throttle. 2140 * 2141 * @return {Function} 2142 */ 2143 var throttle = function throttle(fn, wait) { 2144 var last = Date.now(); 2145 2146 var throttled = function throttled() { 2147 var now = Date.now(); 2148 2149 if (now - last >= wait) { 2150 fn.apply(undefined, arguments); 2151 last = now; 2152 } 2153 }; 2154 2155 return throttled; 2156 }; 2157 2158 /** 2159 * Creates a debounced function that delays invoking `func` until after `wait` 2160 * milliseconds have elapsed since the last time the debounced function was 2161 * invoked. 2162 * 2163 * Inspired by lodash and underscore implementations. 2164 * 2165 * @param {Function} func 2166 * The function to wrap with debounce behavior. 2167 * 2168 * @param {number} wait 2169 * The number of milliseconds to wait after the last invocation. 2170 * 2171 * @param {boolean} [immediate] 2172 * Whether or not to invoke the function immediately upon creation. 2173 * 2174 * @param {Object} [context=window] 2175 * The "context" in which the debounced function should debounce. For 2176 * example, if this function should be tied to a Video.js player, 2177 * the player can be passed here. Alternatively, defaults to the 2178 * global `window` object. 2179 * 2180 * @return {Function} 2181 * A debounced function. 2182 */ 2183 var debounce = function debounce(func, wait, immediate) { 2184 var context = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : window_1; 2185 2186 var timeout = void 0; 2187 2188 /* eslint-disable consistent-this */ 2189 return function () { 2190 var self = this; 2191 var args = arguments; 2192 2193 var _later = function later() { 2194 timeout = null; 2195 _later = null; 2196 if (!immediate) { 2197 func.apply(self, args); 2198 } 2199 }; 2200 2201 if (!timeout && immediate) { 2202 func.apply(self, args); 2203 } 2204 2205 context.clearTimeout(timeout); 2206 timeout = context.setTimeout(_later, wait); 2207 }; 2208 /* eslint-enable consistent-this */ 2209 }; 2210 2211 /** 2212 * @file src/js/event-target.js 2213 */ 2214 2215 /** 2216 * `EventTarget` is a class that can have the same API as the DOM `EventTarget`. It 2217 * adds shorthand functions that wrap around lengthy functions. For example: 2218 * the `on` function is a wrapper around `addEventListener`. 2219 * 2220 * @see [EventTarget Spec]{@link https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventTarget} 2221 * @class EventTarget 2222 */ 2223 var EventTarget = function EventTarget() {}; 2224 2225 /** 2226 * A Custom DOM event. 2227 * 2228 * @typedef {Object} EventTarget~Event 2229 * @see [Properties]{@link https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent} 2230 */ 2231 2232 /** 2233 * All event listeners should follow the following format. 2234 * 2235 * @callback EventTarget~EventListener 2236 * @this {EventTarget} 2237 * 2238 * @param {EventTarget~Event} event 2239 * the event that triggered this function 2240 * 2241 * @param {Object} [hash] 2242 * hash of data sent during the event 2243 */ 2244 2245 /**
2246 * An object containing event names as keys and booleans as values. 2247 * 2248 * > NOTE: If an event name is set to a true value here {@link EventTarget#trigger} 2249 * will have extra functionality. See that function for more information. 2250 * 2251 * @property EventTarget.prototype.allowedEvents_ 2252 * @private 2253 */ 2254 EventTarget.prototype.allowedEvents_ = {}; 2255 2256 /** 2257 * Adds an `event listener` to an instance of an `EventTarget`. An `event listener` is a 2258 * function that will get called when an event with a certain name gets triggered. 2259 * 2260 * @param {string|string[]} type 2261 * An event name or an array of event names. 2262 * 2263 * @param {EventTarget~EventListener} fn 2264 * The function to call with `EventTarget`s 2265 */ 2266 EventTarget.prototype.on = function (type, fn) { 2267 // Remove the addEventListener alias before calling Events.on 2268 // so we don't get into an infinite type loop 2269 var ael = this.addEventListener; 2270 2271 this.addEventListener = function () {}; 2272 on(this, type, fn); 2273 this.addEventListener = ael; 2274 }; 2275 2276 /** 2277 * An alias of {@link EventTarget#on}. Allows `EventTarget` to mimic 2278 * the standard DOM API. 2279 * 2280 * @function 2281 * @see {@link EventTarget#on} 2282 */ 2283 EventTarget.prototype.addEventListener = EventTarget.prototype.on; 2284 2285 /** 2286 * Removes an `event listener` for a specific event from an instance of `EventTarget`. 2287 * This makes it so that the `event listener` will no longer get called when the 2288 * named event happens. 2289 * 2290 * @param {string|string[]} type 2291 * An event name or an array of event names. 2292 * 2293 * @param {EventTarget~EventListener} fn 2294 * The function to remove. 2295 */ 2296 EventTarget.prototype.off = function (type, fn) { 2297 off(this, type, fn); 2298 }; 2299 2300 /** 2301 * An alias of {@link EventTarget#off}. Allows `EventTarget` to mimic 2302 * the standard DOM API. 2303 * 2304 * @function 2305 * @see {@link EventTarget#off} 2306 */ 2307 EventTarget.prototype.removeEventListener = EventTarget.prototype.off; 2308 2309 /** 2310 * This function will add an `event listener` that gets triggered only once. After the 2311 * first trigger it will get removed. This is like adding an `event listener` 2312 * with {@link EventTarget#on} that calls {@link EventTarget#off} on itself. 2313 * 2314 * @param {string|string[]} type 2315 * An event name or an array of event names. 2316 * 2317 * @param {EventTarget~EventListener} fn 2318 * The function to be called once for each event name. 2319 */ 2320 EventTarget.prototype.one = function (type, fn) { 2321 // Remove the addEventListener alialing Events.on 2322 // so we don't get into an infinite type loop 2323 var ael = this.addEventListener; 2324 2325 this.addEventListener = function () {}; 2326 one(this, type, fn); 2327 this.addEventListener = ael; 2328 }; 2329 2330 /** 2331 * This function causes an event to happen. This will then cause any `event listeners` 2332 * that are waiting for that event, to get called. If there are no `event listeners` 2333 * for an event then nothing will happen. 2334 * 2335 * If the name of the `Event` that is being triggered is in `EventTarget.allowedEvents_`. 2336 * Trigger will also call the `on` + `uppercaseEventName` function. 2337 * 2338 * Example: 2339 * 'click' is in `EventTarget.allowedEvents_`, so, trigger will attempt to call 2340 * `onClick` if it exists. 2341 * 2342 * @param {string|EventTarget~Event|Object} event 2343 * The name of the event, an `Event`, or an object with a key of type set to 2344 * an event name. 2345 */ 2346 EventTarget.prototype.trigger = function (event) { 2347 var type = event.type || event; 2348 2349 if (typeof event === 'string') { 2350 event = { type: type }; 2351 } 2352 event = fixEvent(event); 2353 2354 if (this.allowedEvents_[type] && this['on' + type]) { 2355 this['on' + type](event); 2356 } 2357 2358 trigger(this, event); 2359 }; 2360 2361 /** 2362 * An alias of {@link EventTarget#trigger}. Allows `EventTarget` to mimic 2363 * the standard DOM API. 2364 * 2365 * @function 2366 * @see {@link EventTarget#trigger} 2367 */ 2368 EventTarget.prototype.dispatchEvent = EventTarget.prototype.trigger; 2369 2370 /** 2371 * @file mixins/evented.js 2372 * @module evented 2373 */ 2374 2375 /** 2376 * Returns whether or not an object has had the evented mixin applied. 2377 * 2378 * @param {Object} object 2379 * An object to test. 2380 * 2381 * @return {boolean} 2382 * Whether or not the object appears to be evented. 2383 */ 2384 var isEvented = function isEvented(object) { 2385 return object instanceof EventTarget || !!object.eventBusEl_ && ['on', 'one', 'off', 'trigger'].every(function (k) { 2386 return typeof object[k] === 'function'; 2387 }); 2388 }; 2389 2390 /** 2391 * Whether a value is a valid event type - non-empty string or array. 2392 * 2393 * @private 2394 * @param {string|Array} type 2395 * The type value to test. 2396 * 2397 * @return {boolean} 2398 * Whether or not the type is a valid event type. 2399 */ 2400 var isValidEventType = function isValidEventType(type) { 2401 return ( 2402 // The regex here verifies that the `type` contains at least one non- 2403 // whitespace character. 2404 typeof type === 'string' && /\S/.test(type) || Array.isArray(type) && !!type.length 2405 ); 2406 }; 2407 2408 /** 2409 * Validates a value to determine if it is a valid event target. Throws if not. 2410 * 2411 * @private 2412 * @throws {Error} 2413 * If the target does not appear to be a valid event target. 2414 * 2415 * @param {Object} target 2416 * The object to test. 2417 */
vendor: 6,324 bytes, lines 2418-2611
2418 var validateTarget = function validateTarget(target) { 2419 if (!target.nodeName && !isEvented(target)) { 2420 throw new Error('Invalid target; must be a DOM node or evented object.'); 2421 } 2422 }; 2423 2424 /** 2425 * Validates a value to determine if it is a valid event target. Throws if not. 2426 * 2427 * @private 2428 * @throws {Error} 2429 * If the type does not appear to be a valid event type. 2430 * 2431 * @param {string|Array} type 2432 * The type to test. 2433 */ 2434 var validateEventType = function validateEventType(type) { 2435 if (!isValidEventType(type)) { 2436 throw new Error('Invalid event type; must be a non-empty string or array.'); 2437 } 2438 }; 2439 2440 /** 2441 * Validates a value to determine if it is a valid listener. Throws if not. 2442 * 2443 * @private 2444 * @throws {Error} 2445 * If the listener is not a function. 2446 * 2447 * @param {Function} listener 2448 * The listener to test. 2449 */ 2450 var validateListener = function validateListener(listener) { 2451 if (typeof listener !== 'function') { 2452 throw new Error('Invalid listener; must be a function.'); 2453 } 2454 }; 2455 2456 /** 2457 * Takes an array of arguments given to `on()` or `one()`, validates them, and 2458 * normalizes them into an object. 2459 * 2460 * @private 2461 * @param {Object} self 2462 * The evented object on which `on()` or `one()` was called. This 2463 * object will be bound as the `this` value for the listener. 2464 * 2465 * @param {Array} args 2466 * An array of arguments passed to `on()` or `one()`. 2467 * 2468 * @return {Object} 2469 * An object containing useful values for `on()` or `one()` calls. 2470 */ 2471 var normalizeListenArgs = function normalizeListenArgs(self, args) { 2472 2473 // If the number of arguments is less than 3, the target is always the 2474 // evented object itself. 2475 var isTargetingSelf = args.length < 3 || args[0] === self || args[0] === self.eventBusEl_; 2476 var target = void 0; 2477 var type = void 0; 2478 var listener = void 0; 2479 2480 if (isTargetingSelf) { 2481 target = self.eventBusEl_; 2482 2483 // Deal with cases where we got 3 arguments, but we are still listening to 2484 // the evented object itself. 2485 if (args.length >= 3) { 2486 args.shift(); 2487 } 2488 2489 type = args[0]; 2490 listener = args[1]; 2491 } else { 2492 target = args[0]; 2493 type = args[1]; 2494 listener = args[2]; 2495 } 2496 2497 validateTarget(target); 2498 validateEventType(type); 2499 validateListener(listener); 2500 2501 listener = bind(self, listener); 2502 2503 return { isTargetingSelf: isTargetingSelf, target: target, type: type, listener: listener }; 2504 }; 2505 2506 /** 2507 * Adds the listener to the event type(s) on the target, normalizing for 2508 * the type of target. 2509 * 2510 * @private 2511 * @param {Element|Object} target 2512 * A DOM node or evented object. 2513 * 2514 * @param {string} method 2515 * The event binding method to use ("on" or "one"). 2516 * 2517 * @param {string|Array} type 2518 * One or more event type(s). 2519 * 2520 * @param {Function} listener 2521 * A listener function. 2522 */ 2523 var listen = function listen(target, method, type, listener) { 2524 validateTarget(target); 2525 2526 if (target.nodeName) { 2527 Events[method](target, type, listener); 2528 } else { 2529 target[method](type, listener); 2530 } 2531 }; 2532 2533 /** 2534 * Contains methods that provide event capabilities to an object which is passed 2535 * to {@link module:evented|evented}. 2536 * 2537 * @mixin EventedMixin 2538 */ 2539 var EventedMixin = { 2540 2541 /** 2542 * Add a listener to an event (or events) on this object or another evented 2543 * object. 2544 * 2545 * @param {string|Array|Element|Object} targetOrType 2546 * If this is a string or array, it represents the event type(s) 2547 * that will trigger the listener. 2548 * 2549 * Another evented object can be passed here instead, which will 2550 * cause the listener to listen for events on _that_ object. 2551 * 2552 * In either case, the listener's `this` value will be bound to 2553 * this object. 2554 * 2555 * @param {string|Array|Function} typeOrListener 2556 * If the first argument was a string or array, this should be the 2557 * listener function. Otherwise, this is a string or array of event 2558 * type(s). 2559 * 2560 * @param {Function} [listener] 2561 * If the first argument was another evented object, this will be 2562 * the listener function. 2563 */ 2564 on: function on$$1() { 2565 var _this = this; 2566 2567 for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { 2568 args[_key] = arguments[_key]; 2569 } 2570 2571 var _normalizeListenArgs = normalizeListenArgs(this, args), 2572 isTargetingSelf = _normalizeListenArgs.isTargetingSelf, 2573 target = _normalizeListenArgs.target, 2574 type = _normalizeListenArgs.type, 2575 listener = _normalizeListenArgs.listener; 2576 2577 listen(target, 'on', type, listener); 2578 2579 // If this object is listening to another evented object. 2580 if (!isTargetingSelf) { 2581 2582 // If this object is disposed, remove the listener. 2583 var removeListenerOnDispose = function removeListenerOnDispose() { 2584 return _this.off(target, type, listener); 2585 }; 2586 2587 // Use the same function ID as the listener so we can remove it later it 2588 // using the ID of the original listener. 2589 removeListenerOnDispose.guid = listener.guid; 2590 2591 // Add a listener to the target's dispose event as well. This ensures 2592 // that if the target is disposed BEFORE this object, we remove the 2593 // removal listener that was just added. Otherwise, we create a memory leak. 2594 var removeRemoverOnTargetDispose = function removeRemoverOnTargetDispose() { 2595 return _this.off('dispose', removeListenerOnDispose); 2596 }; 2597 2598 // Use the same function ID as the listener so we can remove it later 2599 // it using the ID of the original listener. 2600 removeRemoverOnTargetDispose.guid = listener.guid; 2601 2602 listen(this, 'on', 'dispose', removeListenerOnDispose); 2603 listen(target, 'on', 'dispose', removeRemoverOnTargetDispose); 2604 } 2605 }, 2606 2607 2608 /** 2609 * Add a listener to an event (or events) on this object or another evented 2610 * object. The listener will only be called once and then removed. 2611 *
2612 * @param {string|Array|Element|Object} targetOrType 2613 * If this is a string or array, it represents the event type(s) 2614 * that will trigger the listener. 2615 * 2616 * Another evented object can be passed here instead, which will 2617 * cause the listener to listen for events on _that_ object. 2618 * 2619 * In either case, the listener's `this` value will be bound to 2620 * this object. 2621 * 2622 * @param {string|Array|Function} typeOrListener 2623 * If the first argument was a string or array, this should be the 2624 * listener function. Otherwise, this is a string or array of event 2625 * type(s). 2626 * 2627 * @param {Function} [listener] 2628 * If the first argument was another evented object, this will be 2629 * the listener function. 2630 */ 2631 one: function one$$1() { 2632 var _this2 = this; 2633 2634 for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { 2635 args[_key2] = arguments[_key2]; 2636 } 2637 2638 var _normalizeListenArgs2 = normalizeListenArgs(this, args), 2639 isTargetingSelf = _normalizeListenArgs2.isTargetingSelf, 2640 target = _normalizeListenArgs2.target, 2641 type = _normalizeListenArgs2.type, 2642 listener = _normalizeListenArgs2.listener; 2643 2644 // Targeting this evented object. 2645 2646 2647 if (isTargetingSelf) { 2648 listen(target, 'one', type, listener); 2649 2650 // Targeting another evented object. 2651 } else { 2652 var wrapper = function wrapper() { 2653 for (var _len3 = arguments.length, largs = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { 2654 largs[_key3] = arguments[_key3]; 2655 } 2656 2657 _this2.off(target, type, wrapper); 2658 listener.apply(null, largs); 2659 }; 2660 2661 // Use the same function ID as the listener so we can remove it later 2662 // it using the ID of the original listener. 2663 wrapper.guid = listener.guid; 2664 listen(target, 'one', type, wrapper); 2665 } 2666 }, 2667 2668 2669 /** 2670 * Removes listener(s) from event(s) on an evented object. 2671 * 2672 * @param {string|Array|Element|Object} [targetOrType] 2673 * If this is a string or array, it represents the event type(s). 2674 * 2675 * Another evented object can be passed here instead, in which case 2676 * ALL 3 arguments are _required_. 2677 * 2678 * @param {string|Array|Function} [typeOrListener] 2679 * If the first argument was a string or array, this may be the 2680 * listener function. Otherwise, this is a string or array of event 2681 * type(s). 2682 * 2683 * @param {Function} [listener] 2684 * If the first argument was another evented object, this will be 2685 * the listener function; otherwise, _all_ listeners bound to the 2686 * event type(s) will be removed. 2687 */ 2688 off: function off$$1(targetOrType, typeOrListener, listener) { 2689 2690 // Targeting this evented object. 2691 if (!targetOrType || isValidEventType(targetOrType)) { 2692 off(this.eventBusEl_, targetOrType, typeOrListener); 2693 2694 // Targeting another evented object. 2695 } else { 2696 var target = targetOrType; 2697 var type = typeOrListener; 2698 2699 // Fail fast and in a meaningful way! 2700 validateTarget(target); 2701 validateEventType(type); 2702 validateListener(listener); 2703 2704 // Ensure there's at least a guid, even if the function hasn't been used 2705 listener = bind(this, listener); 2706 2707 // Remove the dispose listener on this evented object, which was given 2708 // the same guid as the event listener in on(). 2709 this.off('dispose', listener); 2710 2711 if (target.nodeName) { 2712 off(target, type, listener); 2713 off(target, 'dispose', listener); 2714 } else if (isEvented(target)) { 2715 target.off(type, listener); 2716 target.off('dispose', listener); 2717 } 2718 } 2719 }, 2720 2721 2722 /** 2723 * Fire an event on this evented object, causing its listeners to be called. 2724 * 2725 * @param {string|Object} event 2726 * An event type or an object with a type property. 2727 * 2728 * @param {Object} [hash] 2729 * An additional object to pass along to listeners. 2730 * 2731 * @returns {boolean} 2732 * Whether or not the default behavior was prevented. 2733 */ 2734 trigger: function trigger$$1(event, hash) { 2735 return trigger(this.eventBusEl_, event, hash); 2736 } 2737 }; 2738 2739 /** 2740 * Applies {@link module:evented~EventedMixin|EventedMixin} to a target object. 2741 * 2742 * @param {Object} target 2743 * The object to which to add event methods. 2744 * 2745 * @param {Object} [options={}] 2746 * Options for customizing the mixin behavior. 2747 * 2748 * @param {String} [options.eventBusKey] 2749 * By default, adds a `eventBusEl_` DOM element to the target object, 2750 * which is used as an event bus. If the target object already has a 2751 * DOM element that should be used, pass its key here. 2752 * 2753 * @return {Object} 2754 * The target object. 2755 */ 2756 function evented(target) { 2757 var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 2758 var eventBusKey = options.eventBusKey; 2759 2760 // Set or create the eventBusEl_. 2761
vendor: 28,573 bytes, lines 2762-3727
2762 if (eventBusKey) { 2763 if (!target[eventBusKey].nodeName) { 2764 throw new Error('The eventBusKey "' + eventBusKey + '" does not refer to an element.'); 2765 } 2766 target.eventBusEl_ = target[eventBusKey]; 2767 } else { 2768 target.eventBusEl_ = createEl('span', { className: 'vjs-event-bus' }); 2769 } 2770 2771 assign(target, EventedMixin); 2772 2773 // When any evented object is disposed, it removes all its listeners. 2774 target.on('dispose', function () { 2775 target.off(); 2776 window_1.setTimeout(function () { 2777 target.eventBusEl_ = null; 2778 }, 0); 2779 }); 2780 2781 return target; 2782 } 2783 2784 /** 2785 * @file mixins/stateful.js 2786 * @module stateful 2787 */ 2788 2789 /** 2790 * Contains methods that provide statefulness to an object which is passed 2791 * to {@link module:stateful}. 2792 * 2793 * @mixin StatefulMixin 2794 */ 2795 var StatefulMixin = { 2796 2797 /** 2798 * A hash containing arbitrary keys and values representing the state of 2799 * the object. 2800 * 2801 * @type {Object} 2802 */ 2803 state: {}, 2804 2805 /** 2806 * Set the state of an object by mutating its 2807 * {@link module:stateful~StatefulMixin.state|state} object in place. 2808 * 2809 * @fires module:stateful~StatefulMixin#statechanged 2810 * @param {Object|Function} stateUpdates 2811 * A new set of properties to shallow-merge into the plugin state. 2812 * Can be a plain object or a function returning a plain object. 2813 * 2814 * @returns {Object|undefined} 2815 * An object containing changes that occurred. If no changes 2816 * occurred, returns `undefined`. 2817 */ 2818 setState: function setState(stateUpdates) { 2819 var _this = this; 2820 2821 // Support providing the `stateUpdates` state as a function. 2822 if (typeof stateUpdates === 'function') { 2823 stateUpdates = stateUpdates(); 2824 } 2825 2826 var changes = void 0; 2827 2828 each(stateUpdates, function (value, key) { 2829 2830 // Record the change if the value is different from what's in the 2831 // current state. 2832 if (_this.state[key] !== value) { 2833 changes = changes || {}; 2834 changes[key] = { 2835 from: _this.state[key], 2836 to: value 2837 }; 2838 } 2839 2840 _this.state[key] = value; 2841 }); 2842 2843 // Only trigger "statechange" if there were changes AND we have a trigger 2844 // function. This allows us to not require that the target object be an 2845 // evented object. 2846 if (changes && isEvented(this)) { 2847 2848 /** 2849 * An event triggered on an object that is both 2850 * {@link module:stateful|stateful} and {@link module:evented|evented} 2851 * indicating that its state has changed. 2852 * 2853 * @event module:stateful~StatefulMixin#statechanged 2854 * @type {Object} 2855 * @property {Object} changes 2856 * A hash containing the properties that were changed and 2857 * the values they were changed `from` and `to`. 2858 */ 2859 this.trigger({ 2860 changes: changes, 2861 type: 'statechanged' 2862 }); 2863 } 2864 2865 return changes; 2866 } 2867 }; 2868 2869 /** 2870 * Applies {@link module:stateful~StatefulMixin|StatefulMixin} to a target 2871 * object. 2872 * 2873 * If the target object is {@link module:evented|evented} and has a 2874 * `handleStateChanged` method, that method will be automatically bound to the 2875 * `statechanged` event on itself. 2876 * 2877 * @param {Object} target 2878 * The object to be made stateful. 2879 * 2880 * @param {Object} [defaultState] 2881 * A default set of properties to populate the newly-stateful object's 2882 * `state` property. 2883 * 2884 * @returns {Object} 2885 * Returns the `target`. 2886 */ 2887 function stateful(target, defaultState) { 2888 assign(target, StatefulMixin); 2889 2890 // This happens after the mixing-in because we need to replace the `state` 2891 // added in that step. 2892 target.state = assign({}, target.state, defaultState); 2893 2894 // Auto-bind the `handleStateChanged` method of the target object if it exists. 2895 if (typeof target.handleStateChanged === 'function' && isEvented(target)) { 2896 target.on('statechanged', target.handleStateChanged); 2897 } 2898 2899 return target; 2900 } 2901 2902 /** 2903 * @file to-title-case.js 2904 * @module to-title-case 2905 */ 2906 2907 /** 2908 * Uppercase the first letter of a string. 2909 * 2910 * @param {string} string 2911 * String to be uppercased 2912 * 2913 * @return {string} 2914 * The string with an uppercased first letter 2915 */ 2916 function toTitleCase(string) { 2917 if (typeof string !== 'string') { 2918 return string; 2919 } 2920 2921 return string.charAt(0).toUpperCase() + string.slice(1); 2922 } 2923 2924 /** 2925 * Compares the TitleCase versions of the two strings for equality. 2926 * 2927 * @param {string} str1 2928 * The first string to compare 2929 * 2930 * @param {string} str2 2931 * The second string to compare 2932 * 2933 * @return {boolean} 2934 * Whether the TitleCase versions of the strings are equal 2935 */ 2936 function titleCaseEquals(str1, str2) { 2937 return toTitleCase(str1) === toTitleCase(str2); 2938 } 2939 2940 /** 2941 * @file merge-options.js 2942 * @module merge-options 2943 */ 2944 2945 /** 2946 * Deep-merge one or more options objects, recursively merging **only** plain 2947 * object properties. 2948 * 2949 * @param {Object[]} sources 2950 * One or more objects to merge into a new object. 2951 * 2952 * @returns {Object} 2953 * A new object that is the merged result of all sources. 2954 */ 2955 function mergeOptions() { 2956 var result = {}; 2957 2958 for (var _len = arguments.length, sources = Array(_len), _key = 0; _key < _len; _key++) { 2959 sources[_key] = arguments[_key]; 2960 } 2961 2962 sources.forEach(function (source) { 2963 if (!source) { 2964 return; 2965 } 2966 2967 each(source, function (value, key) { 2968 if (!isPlain(value)) { 2969 result[key] = value; 2970 return; 2971 } 2972 2973 if (!isPlain(result[key])) { 2974 result[key] = {}; 2975 } 2976 2977 result[key] = mergeOptions(result[key], value); 2978 }); 2979 }); 2980 2981 return result; 2982 } 2983 2984 /** 2985 * Player Component - Base class for all UI objects 2986 * 2987 * @file component.js 2988 */ 2989 2990 /** 2991 * Base class for all UI Components. 2992 * Components are UI objects which represent both a javascript object and an element 2993 * in the DOM. They can be children of other components, and can have 2994 * children themselves. 2995 * 2996 * Components can also use methods from {@link EventTarget} 2997 */ 2998 2999 var Component = function () { 3000 3001 /** 3002 * A callback that is called when a component is ready. Does not have any 3003 * paramters and any callback value will be ignored. 3004 * 3005 * @callback Component~ReadyCallback 3006 * @this Component 3007 */ 3008 3009 /** 3010 * Creates an instance of this class. 3011 * 3012 * @param {Player} player 3013 * The `Player` that this class should be attached to. 3014 * 3015 * @param {Object} [options] 3016 * The key/value store of player options. 3017 * 3018 * @param {Object[]} [options.children] 3019 * An array of children objects to intialize this component with. Children objects have 3020 * a name property that will be used if more than one component of the same type needs to be 3021 * added. 3022 * 3023 * @param {Component~ReadyCallback} [ready] 3024 * Function that gets called when the `Component` is ready. 3025 */ 3026 function Component(player, options, ready) { 3027 classCallCheck(this, Component); 3028 3029 3030 // The component might be the player itself and we can't pass `this` to super 3031 if (!player && this.play) { 3032 this.player_ = player = this; // eslint-disable-line 3033 } else { 3034 this.player_ = player; 3035 } 3036 3037 // Make a copy of prototype.options_ to protect against overriding defaults 3038 this.options_ = mergeOptions({}, this.options_); 3039 3040 // Updated options with supplied options 3041 options = this.options_ = mergeOptions(this.options_, options); 3042 3043 // Get ID from options or options element if one is supplied 3044 this.id_ = options.id || options.el && options.el.id; 3045 3046 // If there was no ID from the options, generate one 3047 if (!this.id_) { 3048 // Don't require the player ID function in the case of mock players 3049 var id = player && player.id && player.id() || 'no_player'; 3050 3051 this.id_ = id + '_component_' + newGUID(); 3052 } 3053 3054 this.name_ = options.name || null; 3055 3056 // Create element if one wasn't provided in options 3057 if (options.el) { 3058 this.el_ = options.el; 3059 } else if (options.createEl !== false) { 3060 this.el_ = this.createEl(); 3061 } 3062 3063 // if evented is anything except false, we want to mixin in evented 3064 if (options.evented !== false) { 3065 // Make this an evented object and use `el_`, if available, as its event bus 3066 evented(this, { eventBusKey: this.el_ ? 'el_' : null }); 3067 } 3068 stateful(this, this.constructor.defaultState); 3069 3070 this.children_ = []; 3071 this.childIndex_ = {}; 3072 this.childNameIndex_ = {}; 3073 3074 // Add any child components in options 3075 if (options.initChildren !== false) { 3076 this.initChildren(); 3077 } 3078 3079 this.ready(ready); 3080 // Don't want to trigger ready here or it will before init is actually 3081 // finished for all children that run this constructor 3082 3083 if (options.reportTouchActivity !== false) { 3084 this.enableTouchActivity(); 3085 } 3086 } 3087 3088 /** 3089 * Dispose of the `Component` and all child components. 3090 * 3091 * @fires Component#dispose 3092 */ 3093 3094 3095 Component.prototype.dispose = function dispose() { 3096 3097 /** 3098 * Triggered when a `Component` is disposed. 3099 * 3100 * @event Component#dispose 3101 * @type {EventTarget~Event} 3102 * 3103 * @property {boolean} [bubbles=false] 3104 * set to false so that the close event does not 3105 * bubble up 3106 */ 3107 this.trigger({ type: 'dispose', bubbles: false }); 3108 3109 // Dispose all children. 3110 if (this.children_) { 3111 for (var i = this.children_.length - 1; i >= 0; i--) { 3112 if (this.children_[i].dispose) { 3113 this.children_[i].dispose(); 3114 } 3115 } 3116 } 3117 3118 // Delete child references 3119 this.children_ = null; 3120 this.childIndex_ = null; 3121 this.childNameIndex_ = null; 3122 3123 if (this.el_) { 3124 // Remove element from DOM 3125 if (this.el_.parentNode) { 3126 this.el_.parentNode.removeChild(this.el_); 3127 } 3128 3129 removeData(this.el_); 3130 this.el_ = null; 3131 } 3132 3133 // remove reference to the player after disposing of the element 3134 this.player_ = null; 3135 }; 3136 3137 /** 3138 * Return the {@link Player} that the `Component` has attached to. 3139 * 3140 * @return {Player} 3141 * The player that this `Component` has attached to. 3142 */ 3143 3144 3145 Component.prototype.player = function player() { 3146 return this.player_; 3147 }; 3148 3149 /** 3150 * Deep merge of options objects with new options. 3151 * > Note: When both `obj` and `options` contain properties whose values are objects. 3152 * The two properties get merged using {@link module:mergeOptions} 3153 * 3154 * @param {Object} obj 3155 * The object that contains new options. 3156 * 3157 * @return {Object} 3158 * A new object of `this.options_` and `obj` merged together. 3159 * 3160 * @deprecated since version 5 3161 */ 3162 3163 3164 Component.prototype.options = function options(obj) { 3165 log$1.warn('this.options() has been deprecated and will be moved to the constructor in 6.0'); 3166 3167 if (!obj) { 3168 return this.options_; 3169 } 3170 3171 this.options_ = mergeOptions(this.options_, obj); 3172 return this.options_; 3173 }; 3174 3175 /** 3176 * Get the `Component`s DOM element 3177 * 3178 * @return {Element} 3179 * The DOM element for this `Component`. 3180 */ 3181 3182 3183 Component.prototype.el = function el() { 3184 return this.el_; 3185 }; 3186 3187 /** 3188 * Create the `Component`s DOM element. 3189 * 3190 * @param {string} [tagName] 3191 * Element's DOM node type. e.g. 'div' 3192 * 3193 * @param {Object} [properties] 3194 * An object of properties that should be set. 3195 * 3196 * @param {Object} [attributes] 3197 * An object of attributes that should be set. 3198 * 3199 * @return {Element} 3200 * The element that gets created. 3201 */ 3202 3203 3204 Component.prototype.createEl = function createEl$$1(tagName, properties, attributes) { 3205 return createEl(tagName, properties, attributes); 3206 }; 3207 3208 /** 3209 * Localize a string given the string in english. 3210 * 3211 * If tokens are provided, it'll try and run a simple token replacement on the provided string. 3212 * The tokens it looks for look like `{1}` with the index being 1-indexed into the tokens array. 3213 * 3214 * If a `defaultValue` is provided, it'll use that over `string`, 3215 * if a value isn't found in provided language files. 3216 * This is useful if you want to have a descriptive key for token replacement 3217 * but have a succinct localized string and not require `en.json` to be included. 3218 * 3219 * Currently, it is used for the progress bar timing. 3220 * ```js 3221 * { 3222 * "progress bar timing: currentTime={1} duration={2}": "{1} of {2}" 3223 * } 3224 * ``` 3225 * It is then used like so: 3226 * ```js 3227 * this.localize('progress bar timing: currentTime={1} duration{2}', 3228 * [this.player_.currentTime(), this.player_.duration()], 3229 * '{1} of {2}'); 3230 * ``` 3231 * 3232 * Which outputs something like: `01:23 of 24:56`. 3233 * 3234 * 3235 * @param {string} string 3236 * The string to localize and the key to lookup in the language files. 3237 * @param {string[]} [tokens] 3238 * If the current item has token replacements, provide the tokens here. 3239 * @param {string} [defaultValue] 3240 * Defaults to `string`. Can be a default value to use for token replacement 3241 * if the lookup key is needed to be separate. 3242 * 3243 * @return {string} 3244 * The localized string or if no localization exists the english string. 3245 */ 3246 3247 3248 Component.prototype.localize = function localize(string, tokens) { 3249 var defaultValue = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : string; 3250 3251 var code = this.player_.language && this.player_.language(); 3252 var languages = this.player_.languages && this.player_.languages(); 3253 var language = languages && languages[code]; 3254 var primaryCode = code && code.split('-')[0]; 3255 var primaryLang = languages && languages[primaryCode]; 3256 3257 var localizedString = defaultValue; 3258 3259 if (language && language[string]) { 3260 localizedString = language[string]; 3261 } else if (primaryLang && primaryLang[string]) { 3262 localizedString = primaryLang[string]; 3263 } 3264 3265 if (tokens) { 3266 localizedString = localizedString.replace(/\{(\d+)\}/g, function (match, index) { 3267 var value = tokens[index - 1]; 3268 var ret = value; 3269 3270 if (typeof value === 'undefined') { 3271 ret = match; 3272 } 3273 3274 return ret; 3275 }); 3276 } 3277 3278 return localizedString; 3279 }; 3280 3281 /** 3282 * Return the `Component`s DOM element. This is where children get inserted. 3283 * This will usually be the the same as the element returned in {@link Component#el}. 3284 * 3285 * @return {Element} 3286 * The content element for this `Component`. 3287 */ 3288 3289 3290 Component.prototype.contentEl = function contentEl() { 3291 return this.contentEl_ || this.el_; 3292 }; 3293 3294 /** 3295 * Get this `Component`s ID 3296 * 3297 * @return {string} 3298 * The id of this `Component` 3299 */ 3300 3301 3302 Component.prototype.id = function id() { 3303 return this.id_; 3304 }; 3305 3306 /** 3307 * Get the `Component`s name. The name gets used to reference the `Component` 3308 * and is set during registration. 3309 * 3310 * @return {string} 3311 * The name of this `Component`. 3312 */ 3313 3314 3315 Component.prototype.name = function name() { 3316 return this.name_; 3317 }; 3318 3319 /** 3320 * Get an array of all child components 3321 * 3322 * @return {Array} 3323 * The children 3324 */ 3325 3326 3327 Component.prototype.children = function children() { 3328 return this.children_; 3329 }; 3330 3331 /** 3332 * Returns the child `Component` with the given `id`. 3333 * 3334 * @param {string} id 3335 * The id of the child `Component` to get. 3336 * 3337 * @return {Component|undefined} 3338 * The child `Component` with the given `id` or undefined. 3339 */ 3340 3341 3342 Component.prototype.getChildById = function getChildById(id) { 3343 return this.childIndex_[id]; 3344 }; 3345 3346 /** 3347 * Returns the child `Component` with the given `name`. 3348 * 3349 * @param {string} name 3350 * The name of the child `Component` to get. 3351 * 3352 * @return {Component|undefined} 3353 * The child `Component` with the given `name` or undefined. 3354 */ 3355 3356 3357 Component.prototype.getChild = function getChild(name) { 3358 if (!name) { 3359 return; 3360 } 3361 3362 name = toTitleCase(name); 3363 3364 return this.childNameIndex_[name]; 3365 }; 3366 3367 /** 3368 * Add a child `Component` inside the current `Component`. 3369 * 3370 * 3371 * @param {string|Component} child 3372 * The name or instance of a child to add. 3373 * 3374 * @param {Object} [options={}] 3375 * The key/value store of options that will get passed to children of 3376 * the child. 3377 * 3378 * @param {number} [index=this.children_.length] 3379 * The index to attempt to add a child into. 3380 * 3381 * @return {Component} 3382 * The `Component` that gets added as a child. When using a string the 3383 * `Component` will get created by this process. 3384 */ 3385 3386 3387 Component.prototype.addChild = function addChild(child) { 3388 var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 3389 var index = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : this.children_.length; 3390 3391 var component = void 0; 3392 var componentName = void 0; 3393 3394 // If child is a string, create component with options 3395 if (typeof child === 'string') { 3396 componentName = toTitleCase(child); 3397 3398 var componentClassName = options.componentClass || componentName; 3399 3400 // Set name through options 3401 options.name = componentName; 3402 3403 // Create a new object & element for this controls set 3404 // If there's no .player_, this is a player 3405 var ComponentClass = Component.getComponent(componentClassName); 3406 3407 if (!ComponentClass) { 3408 throw new Error('Component ' + componentClassName + ' does not exist'); 3409 } 3410 3411 // data stored directly on the videojs object may be 3412 // misidentified as a component to retain 3413 // backwards-compatibility with 4.x. check to make sure the 3414 // component class can be instantiated. 3415 if (typeof ComponentClass !== 'function') { 3416 return null; 3417 } 3418 3419 component = new ComponentClass(this.player_ || this, options); 3420 3421 // child is a component instance 3422 } else { 3423 component = child; 3424 } 3425 3426 this.children_.splice(index, 0, component); 3427 3428 if (typeof component.id === 'function') { 3429 this.childIndex_[component.id()] = component; 3430 } 3431 3432 // If a name wasn't used to create the component, check if we can use the 3433 // name function of the component 3434 componentName = componentName || component.name && toTitleCase(component.name()); 3435 3436 if (componentName) { 3437 this.childNameIndex_[componentName] = component; 3438 } 3439 3440 // Add the UI object's element to the container div (box) 3441 // Having an element is not required 3442 if (typeof component.el === 'function' && component.el()) { 3443 var childNodes = this.contentEl().children; 3444 var refNode = childNodes[index] || null; 3445 3446 this.contentEl().insertBefore(component.el(), refNode); 3447 } 3448 3449 // Return so it can stored on parent object if desired. 3450 return component; 3451 }; 3452 3453 /** 3454 * Remove a child `Component` from this `Component`s list of children. Also removes 3455 * the child `Component`s element from this `Component`s element. 3456 * 3457 * @param {Component} component 3458 * The child `Component` to remove. 3459 */ 3460 3461 3462 Component.prototype.removeChild = function removeChild(component) { 3463 if (typeof component === 'string') { 3464 component = this.getChild(component); 3465 } 3466 3467 if (!component || !this.children_) { 3468 return; 3469 } 3470 3471 var childFound = false; 3472 3473 for (var i = this.children_.length - 1; i >= 0; i--) { 3474 if (this.children_[i] === component) { 3475 childFound = true; 3476 this.children_.splice(i, 1); 3477 break; 3478 } 3479 } 3480 3481 if (!childFound) { 3482 return; 3483 } 3484 3485 this.childIndex_[component.id()] = null; 3486 this.childNameIndex_[component.name()] = null; 3487 3488 var compEl = component.el(); 3489 3490 if (compEl && compEl.parentNode === this.contentEl()) { 3491 this.contentEl().removeChild(component.el()); 3492 } 3493 }; 3494 3495 /** 3496 * Add and initialize default child `Component`s based upon options. 3497 */ 3498 3499 3500 Component.prototype.initChildren = function initChildren() { 3501 var _this = this; 3502 3503 var children = this.options_.children; 3504 3505 if (children) { 3506 // `this` is `parent` 3507 var parentOptions = this.options_; 3508 3509 var handleAdd = function handleAdd(child) { 3510 var name = child.name; 3511 var opts = child.opts; 3512 3513 // Allow options for children to be set at the parent options 3514 // e.g. videojs(id, { controlBar: false }); 3515 // instead of videojs(id, { children: { controlBar: false }); 3516 if (parentOptions[name] !== undefined) { 3517 opts = parentOptions[name]; 3518 } 3519 3520 // Allow for disabling default components 3521 // e.g. options['children']['posterImage'] = false 3522 if (opts === false) { 3523 return; 3524 } 3525 3526 // Allow options to be passed as a simple boolean if no configuration 3527 // is necessary. 3528 if (opts === true) { 3529 opts = {}; 3530 } 3531 3532 // We also want to pass the original player options 3533 // to each component as well so they don't need to 3534 // reach back into the player for options later. 3535 opts.playerOptions = _this.options_.playerOptions; 3536 3537 // Create and add the child component. 3538 // Add a direct reference to the child by name on the parent instance. 3539 // If two of the same component are used, different names should be supplied 3540 // for each 3541 var newChild = _this.addChild(name, opts); 3542 3543 if (newChild) { 3544 _this[name] = newChild; 3545 } 3546 }; 3547 3548 // Allow for an array of children details to passed in the options 3549 var workingChildren = void 0; 3550 var Tech = Component.getComponent('Tech'); 3551 3552 if (Array.isArray(children)) { 3553 workingChildren = children; 3554 } else { 3555 workingChildren = Object.keys(children); 3556 } 3557 3558 workingChildren 3559 // children that are in this.options_ but also in workingChildren would 3560 // give us extra children we do not want. So, we want to filter them out. 3561 .concat(Object.keys(this.options_).filter(function (child) { 3562 return !workingChildren.some(function (wchild) { 3563 if (typeof wchild === 'string') { 3564 return child === wchild; 3565 } 3566 return child === wchild.name; 3567 }); 3568 })).map(function (child) { 3569 var name = void 0; 3570 var opts = void 0; 3571 3572 if (typeof child === 'string') { 3573 name = child; 3574 opts = children[name] || _this.options_[name] || {}; 3575 } else { 3576 name = child.name; 3577 opts = child; 3578 } 3579 3580 return { name: name, opts: opts }; 3581 }).filter(function (child) { 3582 // we have to make sure that child.name isn't in the techOrder since 3583 // techs are registerd as Components but can't aren't compatible 3584 // See https://github.com/videojs/video.js/issues/2772 3585 var c = Component.getComponent(child.opts.componentClass || toTitleCase(child.name)); 3586 3587 return c && !Tech.isTech(c); 3588 }).forEach(handleAdd); 3589 } 3590 }; 3591 3592 /** 3593 * Builds the default DOM class name. Should be overriden by sub-components. 3594 * 3595 * @return {string} 3596 * The DOM class name for this object. 3597 * 3598 * @abstract 3599 */ 3600 3601 3602 Component.prototype.buildCSSClass = function buildCSSClass() { 3603 // Child classes can include a function that does: 3604 // return 'CLASS NAME' + this._super(); 3605 return ''; 3606 }; 3607 3608 /** 3609 * Bind a listener to the component's ready state. 3610 * Different from event listeners in that if the ready event has already happened 3611 * it will trigger the function immediately. 3612 * 3613 * @return {Component} 3614 * Returns itself; method can be chained. 3615 */ 3616 3617 3618 Component.prototype.ready = function ready(fn) { 3619 var sync = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; 3620 3621 if (!fn) { 3622 return; 3623 } 3624 3625 if (!this.isReady_) { 3626 this.readyQueue_ = this.readyQueue_ || []; 3627 this.readyQueue_.push(fn); 3628 return; 3629 } 3630 3631 if (sync) { 3632 fn.call(this); 3633 } else { 3634 // Call the function asynchronously by default for consistency 3635 this.setTimeout(fn, 1); 3636 } 3637 }; 3638 3639 /** 3640 * Trigger all the ready listeners for this `Component`. 3641 * 3642 * @fires Component#ready 3643 */ 3644 3645 3646 Component.prototype.triggerReady = function triggerReady() { 3647 this.isReady_ = true; 3648 3649 // Ensure ready is triggered asynchronously 3650 this.setTimeout(function () { 3651 var readyQueue = this.readyQueue_; 3652 3653 // Reset Ready Queue 3654 this.readyQueue_ = []; 3655 3656 if (readyQueue && readyQueue.length > 0) { 3657 readyQueue.forEach(function (fn) { 3658 fn.call(this); 3659 }, this); 3660 } 3661 3662 // Allow for using event listeners also 3663 /** 3664 * Triggered when a `Component` is ready. 3665 * 3666 * @event Component#ready 3667 * @type {EventTarget~Event} 3668 */ 3669 this.trigger('ready'); 3670 }, 1); 3671 }; 3672 3673 /** 3674 * Find a single DOM element matching a `selector`. This can be within the `Component`s 3675 * `contentEl()` or another custom context. 3676 * 3677 * @param {string} selector 3678 * A valid CSS selector, which will be passed to `querySelector`. 3679 * 3680 * @param {Element|string} [context=this.contentEl()] 3681 * A DOM element within which to query. Can also be a selector string in 3682 * which case the first matching element will get used as context. If 3683 * missing `this.contentEl()` gets used. If `this.contentEl()` returns 3684 * nothing it falls back to `document`. 3685 * 3686 * @return {Element|null} 3687 * the dom element that was found, or null 3688 * 3689 * @see [Information on CSS Selectors](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors) 3690 */ 3691 3692 3693 Component.prototype.$ = function $$$1(selector, context) { 3694 return $(selector, context || this.contentEl()); 3695 }; 3696 3697 /** 3698 * Finds all DOM element matching a `selector`. This can be within the `Component`s 3699 * `contentEl()` or another custom context. 3700 * 3701 * @param {string} selector 3702 * A valid CSS selector, which will be passed to `querySelectorAll`. 3703 * 3704 * @param {Element|string} [context=this.contentEl()] 3705 * A DOM element within which to query. Can also be a selector string in 3706 * which case the first matching element will get used as context. If 3707 * missing `this.contentEl()` gets used. If `this.contentEl()` returns 3708 * nothing it falls back to `document`. 3709 * 3710 * @return {NodeList} 3711 * a list of dom elements that were found 3712 * 3713 * @see [Information on CSS Selectors](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors) 3714 */ 3715 3716 3717 Component.prototype.$$ = function $$$$1(selector, context) { 3718 return $$(selector, context || this.contentEl()); 3719 }; 3720 3721 /** 3722 * Check if a component's element has a CSS class name. 3723 * 3724 * @param {string} classToCheck 3725 * CSS class name to check. 3726 * 3727 * @return {boolean}
3728 * - True if the `Component` has the class. 3729 * - False if the `Component` does not have the class` 3730 */ 3731 3732 3733 Component.prototype.hasClass = function hasClass$$1(classToCheck) { 3734 return hasClass(this.el_, classToCheck); 3735 }; 3736 3737 /** 3738 * Add a CSS class name to the `Component`s element. 3739 * 3740 * @param {string} classToAdd 3741 * CSS class name to add 3742 */ 3743 3744 3745 Component.prototype.addClass = function addClass$$1(classToAdd) { 3746 addClass(this.el_, classToAdd); 3747 }; 3748 3749 /** 3750 * Remove a CSS class name from the `Component`s element. 3751 * 3752 * @param {string} classToRemove 3753 * CSS class name to remove 3754 */ 3755 3756 3757 Component.prototype.removeClass = function removeClass$$1(classToRemove) { 3758 removeClass(this.el_, classToRemove); 3759 }; 3760 3761 /** 3762 * Add or remove a CSS class name from the component's element. 3763 * - `classToToggle` gets added when {@link Component#hasClass} would return false. 3764 * - `classToToggle` gets removed when {@link Component#hasClass} would return true. 3765 * 3766 * @param {string} classToToggle 3767 * The class to add or remove based on (@link Component#hasClass} 3768 * 3769 * @param {boolean|Dom~predicate} [predicate] 3770 * An {@link Dom~predicate} function or a boolean 3771 */ 3772 3773 3774 Component.prototype.toggleClass = function toggleClass$$1(classToToggle, predicate) { 3775 toggleClass(this.el_, classToToggle, predicate); 3776 }; 3777 3778 /** 3779 * Show the `Component`s element if it is hidden by removing the 3780 * 'vjs-hidden' class name from it. 3781 */ 3782 3783 3784 Component.prototype.show = function show() { 3785 this.removeClass('vjs-hidden'); 3786 }; 3787 3788 /** 3789 * Hide the `Component`s element if it is currently showing by adding the 3790 * 'vjs-hidden` class name to it. 3791 */ 3792 3793 3794 Component.prototype.hide = function hide() { 3795 this.addClass('vjs-hidden'); 3796 }; 3797 3798 /** 3799 * Lock a `Component`s element in its visible state by adding the 'vjs-lock-showing' 3800 * class name to it. Used during fadeIn/fadeOut. 3801 * 3802 * @private 3803 */ 3804 3805 3806 Component.prototype.lockShowing = function lockShowing() { 3807 this.addClass('vjs-lock-showing'); 3808 }; 3809 3810 /** 3811 * Unlock a `Component`s element from its visible state by removing the 'vjs-lock-showing' 3812 * class name from it. Used during fadeIn/fadeOut. 3813 * 3814 * @private 3815 */ 3816 3817 3818 Component.prototype.unlockShowing = function unlockShowing() { 3819 this.removeClass('vjs-lock-showing'); 3820 }; 3821 3822 /** 3823 * Get the value of an attribute on the `Component`s element. 3824 * 3825 * @param {string} attribute 3826 * Name of the attribute to get the value from. 3827 * 3828 * @return {string|null} 3829 * - The value of the attribute that was asked for. 3830 * - Can be an empty string on some browsers if the attribute does not exist 3831 * or has no value 3832 * - Most browsers will return null if the attibute does not exist or has 3833 * no value. 3834 * 3835 * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute} 3836 */ 3837 3838 3839 Component.prototype.getAttribute = function getAttribute$$1(attribute) { 3840 return getAttribute(this.el_, attribute); 3841 }; 3842 3843 /** 3844 * Set the value of an attribute on the `Component`'s element 3845 * 3846 * @param {string} attribute 3847 * Name of the attribute to set. 3848 * 3849 * @param {string} value 3850 * Value to set the attribute to. 3851 * 3852 * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute} 3853 */ 3854 3855 3856 Component.prototype.setAttribute = function setAttribute$$1(attribute, value) { 3857 setAttribute(this.el_, attribute, value); 3858 }; 3859 3860 /** 3861 * Remove an attribute from the `Component`s element. 3862 * 3863 * @param {string} attribute 3864 * Name of the attribute to remove. 3865 * 3866 * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/removeAttribute} 3867 */ 3868 3869 3870 Component.prototype.removeAttribute = function removeAttribute$$1(attribute) { 3871 removeAttribute(this.el_, attribute); 3872 }; 3873 3874 /** 3875 * Get or set the width of the component based upon the CSS styles. 3876 * See {@link Component#dimension} for more detailed information. 3877 * 3878 * @param {number|string} [num] 3879 * The width that you want to set postfixed with '%', 'px' or nothing. 3880 * 3881 * @param {boolean} [skipListeners] 3882 * Skip the componentresize event trigger 3883 * 3884 * @return {number|string} 3885 * The width when getting, zero if there is no width. Can be a string 3886 * postpixed with '%' or 'px'. 3887 */ 3888 3889 3890 Component.prototype.width = function width(num, skipListeners) { 3891 return this.dimension('width', num, skipListeners); 3892 }; 3893 3894 /** 3895 * Get or set the height of the component based upon the CSS styles. 3896 * See {@link Component#dimension} for more detailed information. 3897 * 3898 * @param {number|string} [num] 3899 * The height that you want to set postfixed with '%', 'px' or nothing. 3900 * 3901 * @param {boolean} [skipListeners] 3902 * Skip the componentresize event trigger 3903 * 3904 * @return {number|string} 3905 * The width when getting, zero if there is no width. Can be a string 3906 * postpixed with '%' or 'px'. 3907 */ 3908 3909 3910 Component.prototype.height = function height(num, skipListeners) { 3911 return this.dimension('height', num, skipListeners); 3912 }; 3913 3914 /** 3915 * Set both the width and height of the `Component` element at the same time. 3916 * 3917 * @param {number|string} width 3918 * Width to set the `Component`s element to. 3919 * 3920 * @param {number|string} height 3921 * Height to set the `Component`s element to. 3922 */ 3923 3924 3925 Component.prototype.dimensions = function dimensions(width, height) { 3926 // Skip componentresize listeners on width for optimization 3927 this.width(width, true); 3928 this.height(height); 3929 }; 3930 3931 /** 3932 * Get or set width or height of the `Component` element. This is the shared code 3933 * for the {@link Component#width} and {@link Component#height}. 3934 * 3935 * Things to know: 3936 * - If the width or height in an number this will return the number postfixed with 'px'. 3937 * - If the width/height is a percent this will return the percent postfixed with '%' 3938 * - Hidden elements have a width of 0 with `window.getComputedStyle`. This function 3939 * defaults to the `Component`s `style.width` and falls back to `window.getComputedStyle`. 3940 * See [this]{@link http://www.foliotek.com/devblog/getting-the-width-of-a-hidden-element-with-jquery-u
3940sing-width/} 3941 * for more information 3942 * - If you want the computed style of the component, use {@link Component#currentWidth} 3943 * and {@link {Component#currentHeight} 3944 * 3945 * @fires Component#componentresize 3946 * 3947 * @param {string} widthOrHeight 3948 8 'width' or 'height' 3949 * 3950 * @param {number|string} [num] 3951 8 New dimension 3952 * 3953 * @param {boolean} [skipListeners] 3954 * Skip componentresize event trigger 3955 * 3956 * @return {number} 3957 * The dimension when getting or 0 if unset 3958 */ 3959 3960 3961 Component.prototype.dimension = function dimension(widthOrHeight, num, skipListeners) { 3962 if (num !== undefined) { 3963 // Set to zero if null or literally NaN (NaN !== NaN) 3964 if (num === null || num !== num) { 3965 num = 0; 3966 } 3967 3968 // Check if using css width/height (% or px) and adjust 3969 if (('' + num).indexOf('%') !== -1 || ('' + num).indexOf('px') !== -1) { 3970 this.el_.style[widthOrHeight] = num; 3971 } else if (num === 'auto') { 3972 this.el_.style[widthOrHeight] = ''; 3973 } else { 3974 this.el_.style[widthOrHeight] = num + 'px'; 3975 } 3976 3977 // skipListeners allows us to avoid triggering the resize event when setting both width and height 3978 if (!skipListeners) { 3979 /** 3980 * Triggered when a component is resized. 3981 * 3982 * @event Component#componentresize 3983 * @type {EventTarget~Event} 3984 */ 3985 this.trigger('componentresize'); 3986 } 3987 3988 return; 3989 } 3990 3991 // Not setting a value, so getting it 3992 // Make sure element exists 3993 if (!this.el_) { 3994 return 0; 3995 } 3996 3997 // Get dimension value from style 3998 var val = this.el_.style[widthOrHeight]; 3999 var pxIndex = val.indexOf('px'); 4000 4001 if (pxIndex !== -1) { 4002 // Return the pixel value with no 'px' 4003 return parseInt(val.slice(0, pxIndex), 10); 4004 } 4005 4006 // No px so using % or no style was set, so falling back to offsetWidth/height 4007 // If component has display:none, offset will return 0 4008 // TODO: handle display:none and no dimension style using px 4009 return parseInt(this.el_['offset' + toTitleCase(widthOrHeight)], 10); 4010 }; 4011 4012 /** 4013 * Get the width or the height of the `Component` elements computed style. Uses 4014 * `window.getComputedStyle`. 4015 * 4016 * @param {string} widthOrHeight 4017 * A string containing 'width' or 'height'. Whichever one you want to get. 4018 * 4019 * @return {number} 4020 * The dimension that gets asked for or 0 if nothing was set 4021 * for that dimension. 4022 */ 4023 4024 4025 Component.prototype.currentDimension = function currentDimension(widthOrHeight) { 4026 var computedWidthOrHeight = 0; 4027 4028 if (widthOrHeight !== 'width' && widthOrHeight !== 'height') { 4029 throw new Error('currentDimension only accepts width or height value'); 4030 } 4031 4032 if (typeof window_1.getComputedStyle === 'function') { 4033 var computedStyle = window_1.getComputedStyle(this.el_); 4034 4035 computedWidthOrHeight = computedStyle.getPropertyValue(widthOrHeight) || computedStyle[widthOrHeight]; 4036 } 4037 4038 // remove 'px' from variable and parse as integer 4039 computedWidthOrHeight = parseFloat(computedWidthOrHeight); 4040 4041 // if the computed value is still 0, it's possible that the browser is lying 4042 // and we want to check the offset values. 4043 // This code also runs wherever getComputedStyle doesn't exist. 4044 if (computedWidthOrHeight === 0) { 4045 var rule = 'offset' + toTitleCase(widthOrHeight); 4046 4047 computedWidthOrHeight = this.el_[rule]; 4048 } 4049 4050 return computedWidthOrHeight; 4051 }; 4052 4053 /** 4054 * An object that contains width and height values of the `Component`s 4055 * computed style. Uses `window.getComputedStyle`. 4056 * 4057 * @typedef {Object} Component~DimensionObject 4058 * 4059 * @property {number} width 4060 * The width of the `Component`s computed style. 4061 * 4062 * @property {number} height 4063 * The height of the `Component`s computed style. 4064 */ 4065 4066 /** 4067 * Get an object that contains width and height values of the `Component`s 4068 * computed style. 4069 * 4070 * @return {Component~DimensionObject} 4071 * The dimensions of the components element 4072 */ 4073 4074 4075 Component.prototype.currentDimensions = function currentDimensions() { 4076 return { 4077 width: this.currentDimension('width'), 4078 height: this.currentDimension('height') 4079 }; 4080 }; 4081 4082 /** 4083 * Get the width of the `Component`s computed style. Uses `window.getComputedStyle`. 4084 * 4085 * @return {number} width 4086 * The width of the `Component`s computed style. 4087 */ 4088 4089 4090 Component.prototype.currentWidth = function currentWidth() { 4091 return this.currentDimension('width'); 4092 }; 4093 4094 /** 4095 * Get the height of the `Component`s computed style. Uses `window.getComputedStyle`. 4096 * 4097 * @return {number} height 4098 * The height of the `Component`s computed style. 4099 */ 4100 4101 4102 Component.prototype.currentHeight = function currentHeight() { 4103 return this.currentDimension('height'); 4104 }; 4105 4106 /** 4107 * Set the focus to this component 4108 */ 4109 4110
vendor: 10,069 bytes, lines 4111-4381
4111 Component.prototype.focus = function focus() { 4112 this.el_.focus(); 4113 }; 4114 4115 /** 4116 * Remove the focus from this component 4117 */ 4118 4119 4120 Component.prototype.blur = function blur() { 4121 this.el_.blur(); 4122 }; 4123 4124 /** 4125 * Emit a 'tap' events when touch event support gets detected. This gets used to 4126 * support toggling the controls through a tap on the video. They get enabled 4127 * because every sub-component would have extra overhead otherwise. 4128 * 4129 * @private 4130 * @fires Component#tap 4131 * @listens Component#touchstart 4132 * @listens Component#touchmove 4133 * @listens Component#touchleave 4134 * @listens Component#touchcancel 4135 * @listens Component#touchend 4136 */ 4137 4138 4139 Component.prototype.emitTapEvents = function emitTapEvents() { 4140 // Track the start time so we can determine how long the touch lasted 4141 var touchStart = 0; 4142 var firstTouch = null; 4143 4144 // Maximum movement allowed during a touch event to still be considered a tap 4145 // Other popular libs use anywhere from 2 (hammer.js) to 15, 4146 // so 10 seems like a nice, round number. 4147 var tapMovementThreshold = 10; 4148 4149 // The maximum length a touch can be while still being considered a tap 4150 var touchTimeThreshold = 200; 4151 4152 var couldBeTap = void 0; 4153 4154 this.on('touchstart', function (event) { 4155 // If more than one finger, don't consider treating this as a click 4156 if (event.touches.length === 1) { 4157 // Copy pageX/pageY from the object 4158 firstTouch = { 4159 pageX: event.touches[0].pageX, 4160 pageY: event.touches[0].pageY 4161 }; 4162 // Record start time so we can detect a tap vs. "touch and hold" 4163 touchStart = new Date().getTime(); 4164 // Reset couldBeTap tracking 4165 couldBeTap = true; 4166 } 4167 }); 4168 4169 this.on('touchmove', function (event) { 4170 // If more than one finger, don't consider treating this as a click 4171 if (event.touches.length > 1) { 4172 couldBeTap = false; 4173 } else if (firstTouch) { 4174 // Some devices will throw touchmoves for all but the slightest of taps. 4175 // So, if we moved only a small distance, this could still be a tap 4176 var xdiff = event.touches[0].pageX - firstTouch.pageX; 4177 var ydiff = event.touches[0].pageY - firstTouch.pageY; 4178 var touchDistance = Math.sqrt(xdiff * xdiff + ydiff * ydiff); 4179 4180 if (touchDistance > tapMovementThreshold) { 4181 couldBeTap = false; 4182 } 4183 } 4184 }); 4185 4186 var noTap = function noTap() { 4187 couldBeTap = false; 4188 }; 4189 4190 // TODO: Listen to the original target. http://youtu.be/DujfpXOKUp8?t=13m8s 4191 this.on('touchleave', noTap); 4192 this.on('touchcancel', noTap); 4193 4194 // When the touch ends, measure how long it took and trigger the appropriate 4195 // event 4196 this.on('touchend', function (event) { 4197 firstTouch = null; 4198 // Proceed only if the touchmove/leave/cancel event didn't happen 4199 if (couldBeTap === true) { 4200 // Measure how long the touch lasted 4201 var touchTime = new Date().getTime() - touchStart; 4202 4203 // Make sure the touch was less than the threshold to be considered a tap 4204 if (touchTime < touchTimeThreshold) { 4205 // Don't let browser turn this into a click 4206 event.preventDefault(); 4207 /** 4208 * Triggered when a `Component` is tapped. 4209 * 4210 * @event Component#tap 4211 * @type {EventTarget~Event} 4212 */ 4213 this.trigger('tap'); 4214 // It may be good to copy the touchend event object and change the 4215 // type to tap, if the other event properties aren't exact after 4216 // Events.fixEvent runs (e.g. event.target) 4217 } 4218 } 4219 }); 4220 }; 4221 4222 /** 4223 * This function reports user activity whenever touch events happen. This can get 4224 * turned off by any sub-components that wants touch events to act another way. 4225 * 4226 * Report user touch activity when touch events occur. User activity gets used to 4227 * determine when controls should show/hide. It is simple when it comes to mouse 4228 * events, because any mouse event should show the controls. So we capture mouse 4229 * events that bubble up to the player and report activity when that happens. 4230 * With touch events it isn't as easy as `touchstart` and `touchend` toggle player 4231 * controls. So touch events can't help us at the player level either. 4232 * 4233 * User activity gets checked asynchronously. So what could happen is a tap event 4234 * on the video turns the controls off. Then the `touchend` event bubbles up to 4235 * the player. Which, if it reported user activity, would turn the controls right 4236 * back on. We also don't want to completely block touch events from bubbling up. 4237 * Furthermore a `touchmove` event and anything other than a tap, should not turn 4238 * controls back on. 4239 * 4240 * @listens Component#touchstart 4241 * @listens Component#touchmove 4242 * @listens Component#touchend 4243 * @listens Component#touchcancel 4244 */ 4245 4246 4247 Component.prototype.enableTouchActivity = function enableTouchActivity() { 4248 // Don't continue if the root player doesn't support reporting user activity 4249 if (!this.player() || !this.player().reportUserActivity) { 4250 return; 4251 } 4252 4253 // listener for reporting that the user is active 4254 var report = bind(this.player(), this.player().reportUserActivity); 4255 4256 var touchHolding = void 0; 4257 4258 this.on('touchstart', function () { 4259 report(); 4260 // For as long as the they are touching the device or have their mouse down, 4261 // we consider them active even if they're not moving their finger or mouse. 4262 // So we want to continue to update that they are active 4263 this.clearInterval(touchHolding); 4264 // report at the same interval as activityCheck 4265 touchHolding = this.setInterval(report, 250); 4266 }); 4267 4268 var touchEnd = function touchEnd(event) { 4269 report(); 4270 // stop the interval that maintains activity if the touch is holding 4271 this.clearInterval(touchHolding); 4272 }; 4273 4274 this.on('touchmove', report); 4275 this.on('touchend', touchEnd); 4276 this.on('touchcancel', touchEnd); 4277 }; 4278 4279 /** 4280 * A callback that has no parameters and is bound into `Component`s context. 4281 * 4282 * @callback Component~GenericCallback 4283 * @this Component 4284 */ 4285 4286 /** 4287 * Creates a function that runs after an `x` millisecond timeout. This function is a 4288 * wrapper around `window.setTimeout`. There are a few reasons to use this one 4289 * instead though: 4290 * 1. It gets cleared via {@link Component#clearTimeout} when 4291 * {@link Component#dispose} gets called. 4292 * 2. The function callback will gets turned into a {@link Component~GenericCallback} 4293 * 4294 * > Note: You can't use `window.clearTimeout` on the id returned by this function. This 4295 * will cause its dispose listener not to get cleaned up! Please use 4296 * {@link Component#clearTimeout} or {@link Component#dispose} instead. 4297 * 4298 * @param {Component~GenericCallback} fn 4299 * The function that will be run after `timeout`. 4300 * 4301 * @param {number} timeout 4302 * Timeout in milliseconds to delay before executing the specified function. 4303 * 4304 * @return {number} 4305 * Returns a timeout ID that gets used to identify the timeout. It can also 4306 * get used in {@link Component#clearTimeout} to clear the timeout that 4307 * was set. 4308 * 4309 * @listens Component#dispose 4310 * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setTimeout} 4311 */ 4312 4313 4314 Component.prototype.setTimeout = function setTimeout(fn, timeout) { 4315 var _this2 = this; 4316 4317 // declare as variables so they are properly available in timeout function 4318 // eslint-disable-next-line 4319 var timeoutId, disposeFn; 4320 4321 fn = bind(this, fn); 4322 4323 timeoutId = window_1.setTimeout(function () { 4324 _this2.off('dispose', disposeFn); 4325 fn(); 4326 }, timeout); 4327 4328 disposeFn = function disposeFn() { 4329 return _this2.clearTimeout(timeoutId); 4330 }; 4331 4332 disposeFn.guid = 'vjs-timeout-' + timeoutId; 4333 4334 this.on('dispose', disposeFn); 4335 4336 return timeoutId; 4337 }; 4338 4339 /** 4340 * Clears a timeout that gets created via `window.setTimeout` or 4341 * {@link Component#setTimeout}. If you set a timeout via {@link Component#setTimeout} 4342 * use this function instead of `window.clearTimout`. If you don't your dispose 4343 * listener will not get cleaned up until {@link Component#dispose}! 4344 * 4345 * @param {number} timeoutId 4346 * The id of the timeout to clear. The return value of 4347 * {@link Component#setTimeout} or `window.setTimeout`. 4348 * 4349 * @return {number} 4350 * Returns the timeout id that was cleared. 4351 * 4352 * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearTimeout} 4353 */ 4354 4355 4356 Component.prototype.clearTimeout = function clearTimeout(timeoutId) { 4357 window_1.clearTimeout(timeoutId); 4358 4359 var disposeFn = function disposeFn() {}; 4360 4361 disposeFn.guid = 'vjs-timeout-' + timeoutId; 4362 4363 this.off('dispose', disposeFn); 4364 4365 return timeoutId; 4366 }; 4367 4368 /** 4369 * Creates a function that gets run every `x` milliseconds. This function is a wrapper 4370 * around `window.setInterval`. There are a few reasons to use this one instead though. 4371 * 1. It gets cleared via {@link Component#clearInterval} when 4372 * {@link Component#dispose} gets called. 4373 * 2. The function callback will be a {@link Component~GenericCallback} 4374 * 4375 * @param {Component~GenericCallback} fn 4376 * The function to run every `x` seconds. 4377 * 4378 * @param {number} interval 4379 * Execute the specified function every `x` milliseconds. 4380 * 4381 * @return {number}
4382 * Returns an id that can be used to identify the interval. It can also be be used in 4383 * {@link Component#clearInterval} to clear the interval. 4384 * 4385 * @listens Component#dispose 4386 * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setInterval} 4387 */ 4388 4389 4390 Component.prototype.setInterval = function setInterval(fn, interval) { 4391 var _this3 = this; 4392 4393 fn = bind(this, fn); 4394 4395 var intervalId = window_1.setInterval(fn, interval); 4396 4397 var disposeFn = function disposeFn() { 4398 return _this3.clearInterval(intervalId); 4399 }; 4400 4401 disposeFn.guid = 'vjs-interval-' + intervalId; 4402 4403 this.on('dispose', disposeFn); 4404 4405 return intervalId; 4406 }; 4407 4408 /** 4409 * Clears an interval that gets created via `window.setInterval` or 4410 * {@link Component#setInterval}. If you set an inteval via {@link Component#setInterval} 4411 * use this function instead of `window.clearInterval`. If you don't your dispose 4412 * listener will not get cleaned up until {@link Component#dispose}! 4413 * 4414 * @param {number} intervalId 4415 * The id of the interval to clear. The return value of 4416 * {@link Component#setInterval} or `window.setInterval`. 4417 * 4418 * @return {number} 4419 * Returns the interval id that was cleared. 4420 * 4421 * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearInterval} 4422 */ 4423 4424 4425 Component.prototype.clearInterval = function clearInterval(intervalId) { 4426 window_1.clearInterval(intervalId); 4427 4428 var disposeFn = function disposeFn() {}; 4429 4430 disposeFn.guid = 'vjs-interval-' + intervalId; 4431 4432 this.off('dispose', disposeFn); 4433 4434 return intervalId; 4435 }; 4436 4437 /** 4438 * Queues up a callback to be passed to requestAnimationFrame (rAF), but 4439 * with a few extra bonuses: 4440 * 4441 * - Supports browsers that do not support rAF by falling back to 4442 * {@link Component#setTimeout}. 4443 * 4444 * - The callback is turned into a {@link Component~GenericCallback} (i.e. 4445 * bound to the component). 4446 * 4447 * - Automatic cancellation of the rAF callback is handled if the component 4448 * is disposed before it is called. 4449 * 4450 * @param {Component~GenericCallback} fn 4451 * A function that will be bound to this component and executed just 4452 * before the browser's next repaint. 4453 * 4454 * @return {number} 4455 * Returns an rAF ID that gets used to identify the timeout. It can 4456 * also be used in {@link Component#cancelAnimationFrame} to cancel 4457 * the animation frame callback. 4458 * 4459 * @listens Component#dispose 4460 * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame} 4461 */ 4462 4463 4464 Component.prototype.requestAnimationFrame = function requestAnimationFrame(fn) { 4465 var _this4 = this; 4466 4467 // declare as variables so they are properly available in rAF function 4468 // eslint-disable-next-line 4469 var id, disposeFn; 4470 4471 if (this.supportsRaf_) { 4472 fn = bind(this, fn); 4473 4474 id = window_1.requestAnimationFrame(function () { 4475 _this4.off('dispose', disposeFn); 4476 fn(); 4477 }); 4478 4479 disposeFn = function disposeFn() { 4480 return _this4.cancelAnimationFrame(id); 4481 }; 4482 4483 disposeFn.guid = 'vjs-raf-' + id; 4484 this.on('dispose', disposeFn); 4485 4486 return id; 4487 } 4488 4489 // Fall back to using a timer. 4490 return this.setTimeout(fn, 1000 / 60); 4491 }; 4492 4493 /** 4494 * Cancels a queued callback passed to {@link Component#requestAnimationFrame} 4495 * (rAF). 4496 * 4497 * If you queue an rAF callback via {@link Component#requestAnimationFrame}, 4498 * use this function instead of `window.cancelAnimationFrame`. If you don't, 4499 * your dispose listener will not get cleaned up until {@link Component#dispose}! 4500 * 4501 * @param {number} id 4502 * The rAF ID to clear. The return value of {@link Component#requestAnimationFrame}. 4503 * 4504 * @return {number} 4505 * Returns the rAF ID that was cleared. 4506 * 4507 * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/cancelAnimationFrame} 4508 */ 4509 4510 4511 Component.prototype.cancelAnimationFrame = function cancelAnimationFrame(id) { 4512 if (this.supportsRaf_) { 4513 window_1.cancelAnimationFrame(id); 4514 4515 var disposeFn = function disposeFn() {}; 4516 4517 disposeFn.guid = 'vjs-raf-' + id; 4518 4519 this.off('dispose', disposeFn); 4520 4521 return id; 4522 } 4523 4524 // Fall back to using a timer. 4525 return this.clearTimeout(id); 4526 }; 4527 4528 /** 4529 * Register a `Component` with `videojs` given the name and the component. 4530 * 4531 * > NOTE: {@link Tech}s should not be registered as a `Component`. {@link Tech}s 4532 * should be registered using {@link Tech.registerTech} or
vendor: 13,371 bytes, lines 4533-4973
4533 * {@link videojs:videojs.registerTech}. 4534 * 4535 * > NOTE: This function can also be seen on videojs as 4536 * {@link videojs:videojs.registerComponent}. 4537 * 4538 * @param {string} name 4539 * The name of the `Component` to register. 4540 * 4541 * @param {Component} ComponentToRegister 4542 * The `Component` class to register. 4543 * 4544 * @return {Component} 4545 * The `Component` that was registered. 4546 */ 4547 4548 4549 Component.registerComponent = function registerComponent(name, ComponentToRegister) { 4550 if (typeof name !== 'string' || !name) { 4551 throw new Error('Illegal component name, "' + name + '"; must be a non-empty string.'); 4552 } 4553 4554 var Tech = Component.getComponent('Tech'); 4555 4556 // We need to make sure this check is only done if Tech has been registered. 4557 var isTech = Tech && Tech.isTech(ComponentToRegister); 4558 var isComp = Component === ComponentToRegister || Component.prototype.isPrototypeOf(ComponentToRegister.prototype); 4559 4560 if (isTech || !isComp) { 4561 var reason = void 0; 4562 4563 if (isTech) { 4564 reason = 'techs must be registered using Tech.registerTech()'; 4565 } else { 4566 reason = 'must be a Component subclass'; 4567 } 4568 4569 throw new Error('Illegal component, "' + name + '"; ' + reason + '.'); 4570 } 4571 4572 name = toTitleCase(name); 4573 4574 if (!Component.components_) { 4575 Component.components_ = {}; 4576 } 4577 4578 var Player = Component.getComponent('Player'); 4579 4580 if (name === 'Player' && Player && Player.players) { 4581 var players = Player.players; 4582 var playerNames = Object.keys(players); 4583 4584 // If we have players that were disposed, then their name will still be 4585 // in Players.players. So, we must loop through and verify that the value 4586 // for each item is not null. This allows registration of the Player component 4587 // after all players have been disposed or before any were created. 4588 if (players && playerNames.length > 0 && playerNames.map(function (pname) { 4589 return players[pname]; 4590 }).every(Boolean)) { 4591 throw new Error('Can not register Player component after player has been created.'); 4592 } 4593 } 4594 4595 Component.components_[name] = ComponentToRegister; 4596 4597 return ComponentToRegister; 4598 }; 4599 4600 /** 4601 * Get a `Component` based on the name it was registered with. 4602 * 4603 * @param {string} name 4604 * The Name of the component to get. 4605 * 4606 * @return {Component} 4607 * The `Component` that got registered under the given name. 4608 * 4609 * @deprecated In `videojs` 6 this will not return `Component`s that were not 4610 * registered using {@link Component.registerComponent}. Currently we 4611 * check the global `videojs` object for a `Component` name and 4612 * return that if it exists. 4613 */ 4614 4615 4616 Component.getComponent = function getComponent(name) { 4617 if (!name) { 4618 return; 4619 } 4620 4621 name = toTitleCase(name); 4622 4623 if (Component.components_ && Component.components_[name]) { 4624 return Component.components_[name]; 4625 } 4626 }; 4627 4628 return Component; 4629 }(); 4630 4631 /** 4632 * Whether or not this component supports `requestAnimationFrame`. 4633 * 4634 * This is exposed primarily for testing purposes. 4635 * 4636 * @private 4637 * @type {Boolean} 4638 */ 4639 4640 4641 Component.prototype.supportsRaf_ = typeof window_1.requestAnimationFrame === 'function' && typeof window_1.cancelAnimationFrame === 'function'; 4642 4643 Component.registerComponent('Component', Component); 4644 4645 /** 4646 * @file browser.js 4647 * @module browser 4648 */ 4649 4650 var USER_AGENT = window_1.navigator && window_1.navigator.userAgent || ''; 4651 var webkitVersionMap = /AppleWebKit\/([\d.]+)/i.exec(USER_AGENT); 4652 var appleWebkitVersion = webkitVersionMap ? parseFloat(webkitVersionMap.pop()) : null; 4653 4654 /* 4655 * Device is an iPhone 4656 * 4657 * @type {Boolean} 4658 * @constant 4659 * @private 4660 */ 4661 var IS_IPAD = /iPad/i.test(USER_AGENT); 4662 4663 // The Facebook app's UIWebView identifies as both an iPhone and iPad, so 4664 // to identify iPhones, we need to exclude iPads. 4665 // http://artsy.github.io/blog/2012/10/18/the-perils-of-ios-user-agent-sniffing/ 4666 var IS_IPHONE = /iPhone/i.test(USER_AGENT) && !IS_IPAD; 4667 var IS_IPOD = /iPod/i.test(USER_AGENT); 4668 var IS_IOS = IS_IPHONE || IS_IPAD || IS_IPOD; 4669 4670 var IOS_VERSION = function () { 4671 var match = USER_AGENT.match(/OS (\d+)_/i); 4672 4673 if (match && match[1]) { 4674 return match[1]; 4675 } 4676 return null; 4677 }(); 4678 4679 var IS_ANDROID = /Android/i.test(USER_AGENT); 4680 var ANDROID_VERSION = function () { 4681 // This matches Android Major.Minor.Patch versions 4682 // ANDROID_VERSION is Major.Minor as a Number, if Minor isn't available, then only Major is returned 4683 var match = USER_AGENT.match(/Android (\d+)(?:\.(\d+))?(?:\.(\d+))*/i); 4684 4685 if (!match) { 4686 return null; 4687 } 4688 4689 var major = match[1] && parseFloat(match[1]); 4690 var minor = match[2] && parseFloat(match[2]); 4691 4692 if (major && minor) { 4693 return parseFloat(match[1] + '.' + match[2]); 4694 } else if (major) { 4695 return major; 4696 } 4697 return null; 4698 }(); 4699 4700 var IS_NATIVE_ANDROID = IS_ANDROID && ANDROID_VERSION < 5 && appleWebkitVersion < 537; 4701 4702 var IS_FIREFOX = /Firefox/i.test(USER_AGENT); 4703 var IS_EDGE = /Edge/i.test(USER_AGENT); 4704 var IS_CHROME = !IS_EDGE && (/Chrome/i.test(USER_AGENT) || /CriOS/i.test(USER_AGENT)); 4705 var CHROME_VERSION = function () { 4706 var match = USER_AGENT.match(/(Chrome|CriOS)\/(\d+)/); 4707 4708 if (match && match[2]) { 4709 return parseFloat(match[2]); 4710 } 4711 return null; 4712 }(); 4713 var IE_VERSION = function () { 4714 var result = /MSIE\s(\d+)\.\d/.exec(USER_AGENT); 4715 var version = result && parseFloat(result[1]); 4716 4717 if (!version && /Trident\/7.0/i.test(USER_AGENT) && /rv:11.0/.test(USER_AGENT)) { 4718 // IE 11 has a different user agent string than other IE versions 4719 version = 11.0; 4720 } 4721 4722 return version; 4723 }(); 4724 4725 var IS_SAFARI = /Safari/i.test(USER_AGENT) && !IS_CHROME && !IS_ANDROID && !IS_EDGE; 4726 var IS_ANY_SAFARI = (IS_SAFARI || IS_IOS) && !IS_CHROME; 4727 4728 var TOUCH_ENABLED = isReal() && ('ontouchstart' in window_1 || window_1.navigator.maxTouchPoints || window_1.DocumentTouch && window_1.document instanceof window_1.DocumentTouch); 4729 4730 var browser = /*#__PURE__*/Object.freeze({ 4731 IS_IPAD: IS_IPAD, 4732 IS_IPHONE: IS_IPHONE, 4733 IS_IPOD: IS_IPOD, 4734 IS_IOS: IS_IOS, 4735 IOS_VERSION: IOS_VERSION, 4736 IS_ANDROID: IS_ANDROID, 4737 ANDROID_VERSION: ANDROID_VERSION, 4738 IS_NATIVE_ANDROID: IS_NATIVE_ANDROID, 4739 IS_FIREFOX: IS_FIREFOX, 4740 IS_EDGE: IS_EDGE, 4741 IS_CHROME: IS_CHROME, 4742 CHROME_VERSION: CHROME_VERSION, 4743 IE_VERSION: IE_VERSION, 4744 IS_SAFARI: IS_SAFARI, 4745 IS_ANY_SAFARI: IS_ANY_SAFARI, 4746 TOUCH_ENABLED: TOUCH_ENABLED 4747 }); 4748 4749 /** 4750 * @file time-ranges.js 4751 * @module time-ranges 4752 */ 4753 4754 /** 4755 * Returns the time for the specified index at the start or end 4756 * of a TimeRange object. 4757 * 4758 * @function time-ranges:indexFunction 4759 * 4760 * @param {number} [index=0] 4761 * The range number to return the time for. 4762 * 4763 * @return {number} 4764 * The time that offset at the specified index. 4765 * 4766 * @depricated index must be set to a value, in the future this will throw an error. 4767 */ 4768 4769 /** 4770 * An object that contains ranges of time for various reasons. 4771 * 4772 * @typedef {Object} TimeRange 4773 * 4774 * @property {number} length 4775 * The number of time ranges represented by this Object 4776 * 4777 * @property {time-ranges:indexFunction} start 4778 * Returns the time offset at which a specified time range begins. 4779 * 4780 * @property {time-ranges:indexFunction} end 4781 * Returns the time offset at which a specified time range ends. 4782 * 4783 * @see https://developer.mozilla.org/en-US/docs/Web/API/TimeRanges 4784 */ 4785 4786 /** 4787 * Check if any of the time ranges are over the maximum index. 4788 * 4789 * @param {string} fnName 4790 * The function name to use for logging 4791 * 4792 * @param {number} index 4793 * The index to check 4794 * 4795 * @param {number} maxIndex 4796 * The maximum possible index 4797 * 4798 * @throws {Error} if the timeRanges provided are over the maxIndex 4799 */ 4800 function rangeCheck(fnName, index, maxIndex) { 4801 if (typeof index !== 'number' || index < 0 || index > maxIndex) { 4802 throw new Error('Failed to execute \'' + fnName + '\' on \'TimeRanges\': The index provided (' + index + ') is non-numeric or out of bounds (0-' + maxIndex + ').'); 4803 } 4804 } 4805 4806 /** 4807 * Get the time for the specified index at the start or end 4808 * of a TimeRange object. 4809 * 4810 * @param {string} fnName 4811 * The function name to use for logging 4812 * 4813 * @param {string} valueIndex 4814 * The property that should be used to get the time. should be 'start' or 'end' 4815 * 4816 * @param {Array} ranges 4817 * An array of time ranges 4818 * 4819 * @param {Array} [rangeIndex=0] 4820 * The index to start the search at 4821 * 4822 * @return {number} 4823 * The time that offset at the specified index. 4824 * 4825 * 4826 * @depricated rangeIndex must be set to a value, in the future this will throw an error. 4827 * @throws {Error} if rangeIndex is more than the length of ranges 4828 */ 4829 function getRange(fnName, valueIndex, ranges, rangeIndex) { 4830 rangeCheck(fnName, rangeIndex, ranges.length - 1); 4831 return ranges[rangeIndex][valueIndex]; 4832 } 4833 4834 /** 4835 * Create a time range object given ranges of time. 4836 * 4837 * @param {Array} [ranges] 4838 * An array of time ranges. 4839 */ 4840 function createTimeRangesObj(ranges) { 4841 if (ranges === undefined || ranges.length === 0) { 4842 return { 4843 length: 0, 4844 start: function start() { 4845 throw new Error('This TimeRanges object is empty'); 4846 }, 4847 end: function end() { 4848 throw new Error('This TimeRanges object is empty'); 4849 } 4850 }; 4851 } 4852 return { 4853 length: ranges.length, 4854 start: getRange.bind(null, 'start', 0, ranges), 4855 end: getRange.bind(null, 'end', 1, ranges) 4856 }; 4857 } 4858 4859 /** 4860 * Should create a fake `TimeRange` object which mimics an HTML5 time range instance. 4861 * 4862 * @param {number|Array} start 4863 * The start of a single range or an array of ranges 4864 * 4865 * @param {number} end 4866 * The end of a single range. 4867 * 4868 * @private 4869 */ 4870 function createTimeRanges(start, end) { 4871 if (Array.isArray(start)) { 4872 return createTimeRangesObj(start); 4873 } else if (start === undefined || end === undefined) { 4874 return createTimeRangesObj(); 4875 } 4876 return createTimeRangesObj([[start, end]]); 4877 } 4878 4879 /** 4880 * @file buffer.js 4881 * @module buffer 4882 */ 4883 4884 /** 4885 * Compute the percentage of the media that has been buffered. 4886 * 4887 * @param {TimeRange} buffered 4888 * The current `TimeRange` object representing buffered time ranges 4889 * 4890 * @param {number} duration 4891 * Total duration of the media 4892 * 4893 * @return {number} 4894 * Percent buffered of the total duration in decimal form. 4895 */ 4896 function bufferedPercent(buffered, duration) { 4897 var bufferedDuration = 0; 4898 var start = void 0; 4899 var end = void 0; 4900 4901 if (!duration) { 4902 return 0; 4903 } 4904 4905 if (!buffered || !buffered.length) { 4906 buffered = createTimeRanges(0, 0); 4907 } 4908 4909 for (var i = 0; i < buffered.length; i++) { 4910 start = buffered.start(i); 4911 end = buffered.end(i); 4912 4913 // buffered end can be bigger than duration by a very small fraction 4914 if (end > duration) { 4915 end = duration; 4916 } 4917 4918 bufferedDuration += end - start; 4919 } 4920 4921 return bufferedDuration / duration; 4922 } 4923 4924 /** 4925 * @file fullscreen-api.js 4926 * @module fullscreen-api 4927 * @private 4928 */ 4929 4930 /** 4931 * Store the browser-specific methods for the fullscreen API. 4932 * 4933 * @type {Object} 4934 * @see [Specification]{@link https://fullscreen.spec.whatwg.org} 4935 * @see [Map Approach From Screenfull.js]{@link https://github.com/sindresorhus/screenfull.js} 4936 */ 4937 var FullscreenApi = {}; 4938 4939 // browser API methods 4940 var apiMap = [['requestFullscreen', 'exitFullscreen', 'fullscreenElement', 'fullscreenEnabled', 'fullscreenchange', 'fullscreenerror'], 4941 // WebKit 4942 ['webkitRequestFullscreen', 'webkitExitFullscreen', 'webkitFullscreenElement', 'webkitFullscreenEnabled', 'webkitfullscreenchange', 'webkitfullscreenerror'], 4943 // Old WebKit (Safari 5.1) 4944 ['webkitRequestFullScreen', 'webkitCancelFullScreen', 'webkitCurrentFullScreenElement', 'webkitCancelFullScreen', 'webkitfullscreenchange', 'webkitfullscreenerror'], 4945 // Mozilla 4946 ['mozRequestFullScreen', 'mozCancelFullScreen', 'mozFullScreenElement', 'mozFullScreenEnabled', 'mozfullscreenchange', 'mozfullscreenerror'], 4947 // Microsoft 4948 ['msRequestFullscreen', 'msExitFullscreen', 'msFullscreenElement', 'msFullscreenEnabled', 'MSFullscreenChange', 'MSFullscreenError']]; 4949 4950 var specApi = apiMap[0]; 4951 var browserApi = void 0; 4952 4953 // determine the supported set of functions 4954 for (var i = 0; i < apiMap.length; i++) { 4955 // check for exitFullscreen function 4956 if (apiMap[i][1] in document_1) { 4957 browserApi = apiMap[i]; 4958 break; 4959 } 4960 } 4961 4962 // map the browser API names to the spec API names 4963 if (browserApi) { 4964 for (var _i = 0; _i < browserApi.length; _i++) { 4965 FullscreenApi[specApi[_i]] = browserApi[_i]; 4966 } 4967 } 4968 4969 /** 4970 * @file media-error.js 4971 */ 4972 4973 /**
4974 * A Custom `MediaError` class which mimics the standard HTML5 `MediaError` class. 4975 * 4976 * @param {number|string|Object|MediaError} value 4977 * This can be of multiple types: 4978 * - number: should be a standard error code 4979 * - string: an error message (the code will be 0) 4980 * - Object: arbitrary properties 4981 * - `MediaError` (native): used to populate a video.js `MediaError` object 4982 * - `MediaError` (video.js): will return itself if it's already a 4983 * video.js `MediaError` object. 4984 * 4985 * @see [MediaError Spec]{@link https://dev.w3.org/html5/spec-author-view/video.html#mediaerror} 4986 * @see [Encrypted MediaError Spec]{@link https://www.w3.org/TR/2013/WD-encrypted-media-20130510/#error-codes} 4987 * 4988 * @class MediaError 4989 */ 4990 function MediaError(value) { 4991 4992 // Allow redundant calls to this constructor to avoid having `instanceof` 4993 // checks peppered around the code. 4994 if (value instanceof MediaError) { 4995 return value; 4996 } 4997 4998 if (typeof value === 'number') { 4999 this.code = value; 5000 } else if (typeof value === 'string') {
vendor: 4,803 bytes, lines 5001-5151
5001 // default code is zero, so this is a custom error 5002 this.message = value; 5003 } else if (isObject(value)) { 5004 5005 // We assign the `code` property manually because native `MediaError` objects 5006 // do not expose it as an own/enumerable property of the object. 5007 if (typeof value.code === 'number') { 5008 this.code = value.code; 5009 } 5010 5011 assign(this, value); 5012 } 5013 5014 if (!this.message) { 5015 this.message = MediaError.defaultMessages[this.code] || ''; 5016 } 5017 } 5018 5019 /** 5020 * The error code that refers two one of the defined `MediaError` types 5021 * 5022 * @type {Number} 5023 */ 5024 MediaError.prototype.code = 0; 5025 5026 /** 5027 * An optional message that to show with the error. Message is not part of the HTML5 5028 * video spec but allows for more informative custom errors. 5029 * 5030 * @type {String} 5031 */ 5032 MediaError.prototype.message = ''; 5033 5034 /** 5035 * An optional status code that can be set by plugins to allow even more detail about 5036 * the error. For example a plugin might provide a specific HTTP status code and an 5037 * error message for that code. Then when the plugin gets that error this class will 5038 * know how to display an error message for it. This allows a custom message to show 5039 * up on the `Player` error overlay. 5040 * 5041 * @type {Array} 5042 */ 5043 MediaError.prototype.status = null; 5044 5045 /** 5046 * Errors indexed by the W3C standard. The order **CANNOT CHANGE**! See the 5047 * specification listed under {@link MediaError} for more information. 5048 * 5049 * @enum {array} 5050 * @readonly 5051 * @property {string} 0 - MEDIA_ERR_CUSTOM 5052 * @property {string} 1 - MEDIA_ERR_CUSTOM 5053 * @property {string} 2 - MEDIA_ERR_ABORTED 5054 * @property {string} 3 - MEDIA_ERR_NETWORK 5055 * @property {string} 4 - MEDIA_ERR_SRC_NOT_SUPPORTED 5056 * @property {string} 5 - MEDIA_ERR_ENCRYPTED 5057 */ 5058 MediaError.errorTypes = ['MEDIA_ERR_CUSTOM', 'MEDIA_ERR_ABORTED', 'MEDIA_ERR_NETWORK', 'MEDIA_ERR_DECODE', 'MEDIA_ERR_SRC_NOT_SUPPORTED', 'MEDIA_ERR_ENCRYPTED']; 5059 5060 /** 5061 * The default `MediaError` messages based on the {@link MediaError.errorTypes}. 5062 * 5063 * @type {Array} 5064 * @constant 5065 */ 5066 MediaError.defaultMessages = { 5067 1: 'You aborted the media playback', 5068 2: 'A network error caused the media download to fail part-way.', 5069 3: 'The media playback was aborted due to a corruption problem or because the media used features your browser did not support.', 5070 4: 'The media could not be loaded, either because the server or network failed or because the format is not supported.', 5071 5: 'The media is encrypted and we do not have the keys to decrypt it.' 5072 }; 5073 5074 // Add types as properties on MediaError 5075 // e.g. MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED = 4; 5076 for (var errNum = 0; errNum < MediaError.errorTypes.length; errNum++) { 5077 MediaError[MediaError.errorTypes[errNum]] = errNum; 5078 // values should be accessible on both the class and instance 5079 MediaError.prototype[MediaError.errorTypes[errNum]] = errNum; 5080 } 5081 5082 var tuple = SafeParseTuple; 5083 5084 function SafeParseTuple(obj, reviver) { 5085 var json; 5086 var error = null; 5087 5088 try { 5089 json = JSON.parse(obj, reviver); 5090 } catch (err) { 5091 error = err; 5092 } 5093 5094 return [error, json]; 5095 } 5096 5097 /** 5098 * Returns whether an object is `Promise`-like (i.e. has a `then` method). 5099 * 5100 * @param {Object} value 5101 * An object that may or may not be `Promise`-like. 5102 * 5103 * @return {Boolean} 5104 * Whether or not the object is `Promise`-like. 5105 */ 5106 function isPromise(value) { 5107 return value !== undefined && value !== null && typeof value.then === 'function'; 5108 } 5109 5110 /** 5111 * Silence a Promise-like object. 5112 * 5113 * This is useful for avoiding non-harmful, but potentially confusing "uncaught 5114 * play promise" rejection error messages. 5115 * 5116 * @param {Object} value 5117 * An object that may or may not be `Promise`-like. 5118 */ 5119 function silencePromise(value) { 5120 if (isPromise(value)) { 5121 value.then(null, function (e) {}); 5122 } 5123 } 5124 5125 /** 5126 * @file text-track-list-converter.js Utilities for capturing text track state and 5127 * re-creating tracks based on a capture. 5128 * 5129 * @module text-track-list-converter 5130 */ 5131 5132 /** 5133 * Examine a single {@link TextTrack} and return a JSON-compatible javascript object that 5134 * represents the {@link TextTrack}'s state. 5135 * 5136 * @param {TextTrack} track 5137 * The text track to query. 5138 * 5139 * @return {Object} 5140 * A serializable javascript representation of the TextTrack. 5141 * @private 5142 */ 5143 var trackToJson_ = function trackToJson_(track) { 5144 var ret = ['kind', 'label', 'language', 'id', 'inBandMetadataTrackDispatchType', 'mode', 'src'].reduce(function (acc, prop, i) { 5145 5146 if (track[prop]) { 5147 acc[prop] = track[prop]; 5148 } 5149 5150 return acc; 5151 }, {
vendor: 45,150 bytes, lines 5152-6836
5152 cues: track.cues && Array.prototype.map.call(track.cues, function (cue) { 5153 return { 5154 startTime: cue.startTime, 5155 endTime: cue.endTime, 5156 text: cue.text, 5157 id: cue.id 5158 }; 5159 }) 5160 }); 5161 5162 return ret; 5163 }; 5164 5165 /** 5166 * Examine a {@link Tech} and return a JSON-compatible javascript array that represents the 5167 * state of all {@link TextTrack}s currently configured. The return array is compatible with 5168 * {@link text-track-list-converter:jsonToTextTracks}. 5169 * 5170 * @param {Tech} tech 5171 * The tech object to query 5172 * 5173 * @return {Array} 5174 * A serializable javascript representation of the {@link Tech}s 5175 * {@link TextTrackList}. 5176 */ 5177 var textTracksToJson = function textTracksToJson(tech) { 5178 5179 var trackEls = tech.$$('track'); 5180 5181 var trackObjs = Array.prototype.map.call(trackEls, function (t) { 5182 return t.track; 5183 }); 5184 var tracks = Array.prototype.map.call(trackEls, function (trackEl) { 5185 var json = trackToJson_(trackEl.track); 5186 5187 if (trackEl.src) { 5188 json.src = trackEl.src; 5189 } 5190 return json; 5191 }); 5192 5193 return tracks.concat(Array.prototype.filter.call(tech.textTracks(), function (track) { 5194 return trackObjs.indexOf(track) === -1; 5195 }).map(trackToJson_)); 5196 }; 5197 5198 /** 5199 * Create a set of remote {@link TextTrack}s on a {@link Tech} based on an array of javascript 5200 * object {@link TextTrack} representations. 5201 * 5202 * @param {Array} json 5203 * An array of `TextTrack` representation objects, like those that would be 5204 * produced by `textTracksToJson`. 5205 * 5206 * @param {Tech} tech 5207 * The `Tech` to create the `TextTrack`s on. 5208 */ 5209 var jsonToTextTracks = function jsonToTextTracks(json, tech) { 5210 json.forEach(function (track) { 5211 var addedTrack = tech.addRemoteTextTrack(track).track; 5212 5213 if (!track.src && track.cues) { 5214 track.cues.forEach(function (cue) { 5215 return addedTrack.addCue(cue); 5216 }); 5217 } 5218 }); 5219 5220 return tech.textTracks(); 5221 }; 5222 5223 var textTrackConverter = { textTracksToJson: textTracksToJson, jsonToTextTracks: jsonToTextTracks, trackToJson_: trackToJson_ }; 5224 5225 /** 5226 * @file modal-dialog.js 5227 */ 5228 5229 var MODAL_CLASS_NAME = 'vjs-modal-dialog'; 5230 var ESC = 27; 5231 5232 /** 5233 * The `ModalDialog` displays over the video and its controls, which blocks 5234 * interaction with the player until it is closed. 5235 * 5236 * Modal dialogs include a "Close" button and will close when that button 5237 * is activated - or when ESC is pressed anywhere. 5238 * 5239 * @extends Component 5240 */ 5241 5242 var ModalDialog = function (_Component) { 5243 inherits(ModalDialog, _Component); 5244 5245 /** 5246 * Create an instance of this class. 5247 * 5248 * @param {Player} player 5249 * The `Player` that this class should be attached to. 5250 * 5251 * @param {Object} [options] 5252 * The key/value store of player options. 5253 * 5254 * @param {Mixed} [options.content=undefined] 5255 * Provide customized content for this modal. 5256 * 5257 * @param {string} [options.description] 5258 * A text description for the modal, primarily for accessibility. 5259 * 5260 * @param {boolean} [options.fillAlways=false] 5261 * Normally, modals are automatically filled only the first time 5262 * they open. This tells the modal to refresh its content 5263 * every time it opens. 5264 * 5265 * @param {string} [options.label] 5266 * A text label for the modal, primarily for accessibility. 5267 * 5268 * @param {boolean} [options.temporary=true] 5269 * If `true`, the modal can only be opened once; it will be 5270 * disposed as soon as it's closed. 5271 * 5272 * @param {boolean} [options.uncloseable=false] 5273 * If `true`, the user will not be able to close the modal 5274 * through the UI in the normal ways. Programmatic closing is 5275 * still possible. 5276 */ 5277 function ModalDialog(player, options) { 5278 classCallCheck(this, ModalDialog); 5279 5280 var _this = possibleConstructorReturn(this, _Component.call(this, player, options)); 5281 5282 _this.opened_ = _this.hasBeenOpened_ = _this.hasBeenFilled_ = false; 5283 5284 _this.closeable(!_this.options_.uncloseable); 5285 _this.content(_this.options_.content); 5286 5287 // Make sure the contentEl is defined AFTER any children are initialized 5288 // because we only want the contents of the modal in the contentEl 5289 // (not the UI elements like the close button). 5290 _this.contentEl_ = createEl('div', { 5291 className: MODAL_CLASS_NAME + '-content' 5292 }, { 5293 role: 'document' 5294 }); 5295 5296 _this.descEl_ = createEl('p', { 5297 className: MODAL_CLASS_NAME + '-description vjs-control-text', 5298 id: _this.el().getAttribute('aria-describedby') 5299 }); 5300 5301 textContent(_this.descEl_, _this.description()); 5302 _this.el_.appendChild(_this.descEl_); 5303 _this.el_.appendChild(_this.contentEl_); 5304 return _this; 5305 } 5306 5307 /** 5308 * Create the `ModalDialog`'s DOM element 5309 * 5310 * @return {Element} 5311 * The DOM element that gets created. 5312 */ 5313 5314 5315 ModalDialog.prototype.createEl = function createEl$$1() { 5316 return _Component.prototype.createEl.call(this, 'div', { 5317 className: this.buildCSSClass(), 5318 tabIndex: -1 5319 }, { 5320 'aria-describedby': this.id() + '_description', 5321 'aria-hidden': 'true', 5322 'aria-label': this.label(), 5323 'role': 'dialog' 5324 }); 5325 }; 5326 5327 ModalDialog.prototype.dispose = function dispose() { 5328 this.contentEl_ = null; 5329 this.descEl_ = null; 5330 this.previouslyActiveEl_ = null; 5331 5332 _Component.prototype.dispose.call(this); 5333 }; 5334 5335 /** 5336 * Builds the default DOM `className`. 5337 * 5338 * @return {string} 5339 * The DOM `className` for this object. 5340 */ 5341 5342 5343 ModalDialog.prototype.buildCSSClass = function buildCSSClass() { 5344 return MODAL_CLASS_NAME + ' vjs-hidden ' + _Component.prototype.buildCSSClass.call(this); 5345 }; 5346 5347 /** 5348 * Handles `keydown` events on the document, looking for ESC, which closes 5349 * the modal. 5350 * 5351 * @param {EventTarget~Event} e 5352 * The keypress that triggered this event. 5353 * 5354 * @listens keydown 5355 */ 5356 5357 5358 ModalDialog.prototype.handleKeyPress = function handleKeyPress(e) { 5359 if (e.which === ESC && this.closeable()) { 5360 this.close(); 5361 } 5362 }; 5363 5364 /** 5365 * Returns the label string for this modal. Primarily used for accessibility. 5366 * 5367 * @return {string} 5368 * the localized or raw label of this modal. 5369 */ 5370 5371 5372 ModalDialog.prototype.label = function label() { 5373 return this.localize(this.options_.label || 'Modal Window'); 5374 }; 5375 5376 /** 5377 * Returns the description string for this modal. Primarily used for 5378 * accessibility. 5379 * 5380 * @return {string} 5381 * The localized or raw description of this modal. 5382 */ 5383 5384 5385 ModalDialog.prototype.description = function description() { 5386 var desc = this.options_.description || this.localize('This is a modal window.'); 5387 5388 // Append a universal closeability message if the modal is closeable. 5389 if (this.closeable()) { 5390 desc += ' ' + this.localize('This modal can be closed by pressing the Escape key or activating the close button.'); 5391 } 5392 5393 return desc; 5394 }; 5395 5396 /** 5397 * Opens the modal. 5398 * 5399 * @fires ModalDialog#beforemodalopen 5400 * @fires ModalDialog#modalopen 5401 */ 5402 5403 5404 ModalDialog.prototype.open = function open() { 5405 if (!this.opened_) { 5406 var player = this.player(); 5407 5408 /** 5409 * Fired just before a `ModalDialog` is opened. 5410 * 5411 * @event ModalDialog#beforemodalopen 5412 * @type {EventTarget~Event} 5413 */ 5414 this.trigger('beforemodalopen'); 5415 this.opened_ = true; 5416 5417 // Fill content if the modal has never opened before and 5418 // never been filled. 5419 if (this.options_.fillAlways || !this.hasBeenOpened_ && !this.hasBeenFilled_) { 5420 this.fill(); 5421 } 5422 5423 // If the player was playing, pause it and take note of its previously 5424 // playing state. 5425 this.wasPlaying_ = !player.paused(); 5426 5427 if (this.options_.pauseOnOpen && this.wasPlaying_) { 5428 player.pause(); 5429 } 5430 5431 if (this.closeable()) { 5432 this.on(this.el_.ownerDocument, 'keydown', bind(this, this.handleKeyPress)); 5433 } 5434 5435 // Hide controls and note if they were enabled. 5436 this.hadControls_ = player.controls(); 5437 player.controls(false); 5438 5439 this.show(); 5440 this.conditionalFocus_(); 5441 this.el().setAttribute('aria-hidden', 'false'); 5442 5443 /** 5444 * Fired just after a `ModalDialog` is opened. 5445 * 5446 * @event ModalDialog#modalopen 5447 * @type {EventTarget~Event} 5448 */ 5449 this.trigger('modalopen'); 5450 this.hasBeenOpened_ = true; 5451 } 5452 }; 5453 5454 /** 5455 * If the `ModalDialog` is currently open or closed. 5456 * 5457 * @param {boolean} [value] 5458 * If given, it will open (`true`) or close (`false`) the modal. 5459 * 5460 * @return {boolean} 5461 * the current open state of the modaldialog 5462 */ 5463 5464 5465 ModalDialog.prototype.opened = function opened(value) { 5466 if (typeof value === 'boolean') { 5467 this[value ? 'open' : 'close'](); 5468 } 5469 return this.opened_; 5470 }; 5471 5472 /** 5473 * Closes the modal, does nothing if the `ModalDialog` is 5474 * not open. 5475 * 5476 * @fires ModalDialog#beforemodalclose 5477 * @fires ModalDialog#modalclose 5478 */ 5479 5480 5481 ModalDialog.prototype.close = function close() { 5482 if (!this.opened_) { 5483 return; 5484 } 5485 var player = this.player(); 5486 5487 /** 5488 * Fired just before a `ModalDialog` is closed. 5489 * 5490 * @event ModalDialog#beforemodalclose 5491 * @type {EventTarget~Event} 5492 */ 5493 this.trigger('beforemodalclose'); 5494 this.opened_ = false; 5495 5496 if (this.wasPlaying_ && this.options_.pauseOnOpen) { 5497 player.play(); 5498 } 5499 5500 if (this.closeable()) { 5501 this.off(this.el_.ownerDocument, 'keydown', bind(this, this.handleKeyPress)); 5502 } 5503 5504 if (this.hadControls_) { 5505 player.controls(true); 5506 } 5507 5508 this.hide(); 5509 this.el().setAttribute('aria-hidden', 'true'); 5510 5511 /** 5512 * Fired just after a `ModalDialog` is closed. 5513 * 5514 * @event ModalDialog#modalclose 5515 * @type {EventTarget~Event} 5516 */ 5517 this.trigger('modalclose'); 5518 this.conditionalBlur_(); 5519 5520 if (this.options_.temporary) { 5521 this.dispose(); 5522 } 5523 }; 5524 5525 /** 5526 * Check to see if the `ModalDialog` is closeable via the UI. 5527 * 5528 * @param {boolean} [value] 5529 * If given as a boolean, it will set the `closeable` option. 5530 * 5531 * @return {boolean} 5532 * Returns the final value of the closable option. 5533 */ 5534 5535 5536 ModalDialog.prototype.closeable = function closeable(value) { 5537 if (typeof value === 'boolean') { 5538 var closeable = this.closeable_ = !!value; 5539 var close = this.getChild('closeButton'); 5540 5541 // If this is being made closeable and has no close button, add one. 5542 if (closeable && !close) { 5543 5544 // The close button should be a child of the modal - not its 5545 // content element, so temporarily change the content element. 5546 var temp = this.contentEl_; 5547 5548 this.contentEl_ = this.el_; 5549 close = this.addChild('closeButton', { controlText: 'Close Modal Dialog' }); 5550 this.contentEl_ = temp; 5551 this.on(close, 'close', this.close); 5552 } 5553 5554 // If this is being made uncloseable and has a close button, remove it. 5555 if (!closeable && close) { 5556 this.off(close, 'close', this.close); 5557 this.removeChild(close); 5558 close.dispose(); 5559 } 5560 } 5561 return this.closeable_; 5562 }; 5563 5564 /** 5565 * Fill the modal's content element with the modal's "content" option. 5566 * The content element will be emptied before this change takes place. 5567 */ 5568 5569 5570 ModalDialog.prototype.fill = function fill() { 5571 this.fillWith(this.content()); 5572 }; 5573 5574 /** 5575 * Fill the modal's content element with arbitrary content. 5576 * The content element will be emptied before this change takes place. 5577 * 5578 * @fires ModalDialog#beforemodalfill 5579 * @fires ModalDialog#modalfill 5580 * 5581 * @param {Mixed} [content] 5582 * The same rules apply to this as apply to the `content` option. 5583 */ 5584 5585 5586 ModalDialog.prototype.fillWith = function fillWith(content) { 5587 var contentEl = this.contentEl(); 5588 var parentEl = contentEl.parentNode; 5589 var nextSiblingEl = contentEl.nextSibling; 5590 5591 /** 5592 * Fired just before a `ModalDialog` is filled with content. 5593 * 5594 * @event ModalDialog#beforemodalfill 5595 * @type {EventTarget~Event} 5596 */ 5597 this.trigger('beforemodalfill'); 5598 this.hasBeenFilled_ = true; 5599 5600 // Detach the content element from the DOM before performing 5601 // manipulation to avoid modifying the live DOM multiple times. 5602 parentEl.removeChild(contentEl); 5603 this.empty(); 5604 insertContent(contentEl, content); 5605 /** 5606 * Fired just after a `ModalDialog` is filled with content. 5607 * 5608 * @event ModalDialog#modalfill 5609 * @type {EventTarget~Event} 5610 */ 5611 this.trigger('modalfill'); 5612 5613 // Re-inject the re-filled content element. 5614 if (nextSiblingEl) { 5615 parentEl.insertBefore(contentEl, nextSiblingEl); 5616 } else { 5617 parentEl.appendChild(contentEl); 5618 } 5619 5620 // make sure that the close button is last in the dialog DOM 5621 var closeButton = this.getChild('closeButton'); 5622 5623 if (closeButton) { 5624 parentEl.appendChild(closeButton.el_); 5625 } 5626 }; 5627 5628 /** 5629 * Empties the content element. This happens anytime the modal is filled. 5630 * 5631 * @fires ModalDialog#beforemodalempty 5632 * @fires ModalDialog#modalempty 5633 */ 5634 5635 5636 ModalDialog.prototype.empty = function empty() { 5637 /** 5638 * Fired just before a `ModalDialog` is emptied. 5639 * 5640 * @event ModalDialog#beforemodalempty 5641 * @type {EventTarget~Event} 5642 */ 5643 this.trigger('beforemodalempty'); 5644 emptyEl(this.contentEl()); 5645 5646 /** 5647 * Fired just after a `ModalDialog` is emptied. 5648 * 5649 * @event ModalDialog#modalempty 5650 * @type {EventTarget~Event} 5651 */ 5652 this.trigger('modalempty'); 5653 }; 5654 5655 /** 5656 * Gets or sets the modal content, which gets normalized before being 5657 * rendered into the DOM. 5658 * 5659 * This does not update the DOM or fill the modal, but it is called during 5660 * that process. 5661 * 5662 * @param {Mixed} [value] 5663 * If defined, sets the internal content value to be used on the 5664 * next call(s) to `fill`. This value is normalized before being 5665 * inserted. To "clear" the internal content value, pass `null`. 5666 * 5667 * @return {Mixed} 5668 * The current content of the modal dialog 5669 */ 5670 5671 5672 ModalDialog.prototype.content = function content(value) { 5673 if (typeof value !== 'undefined') { 5674 this.content_ = value; 5675 } 5676 return this.content_; 5677 }; 5678 5679 /** 5680 * conditionally focus the modal dialog if focus was previously on the player. 5681 * 5682 * @private 5683 */ 5684 5685 5686 ModalDialog.prototype.conditionalFocus_ = function conditionalFocus_() { 5687 var activeEl = document_1.activeElement; 5688 var playerEl = this.player_.el_; 5689 5690 this.previouslyActiveEl_ = null; 5691 5692 if (playerEl.contains(activeEl) || playerEl === activeEl) { 5693 this.previouslyActiveEl_ = activeEl; 5694 5695 this.focus(); 5696 5697 this.on(document_1, 'keydown', this.handleKeyDown); 5698 } 5699 }; 5700 5701 /** 5702 * conditionally blur the element and refocus the last focused element 5703 * 5704 * @private 5705 */ 5706 5707 5708 ModalDialog.prototype.conditionalBlur_ = function conditionalBlur_() { 5709 if (this.previouslyActiveEl_) { 5710 this.previouslyActiveEl_.focus(); 5711 this.previouslyActiveEl_ = null; 5712 } 5713 5714 this.off(document_1, 'keydown', this.handleKeyDown); 5715 }; 5716 5717 /** 5718 * Keydown handler. Attached when modal is focused. 5719 * 5720 * @listens keydown 5721 */ 5722 5723 5724 ModalDialog.prototype.handleKeyDown = function handleKeyDown(event) { 5725 // exit early if it isn't a tab key 5726 if (event.which !== 9) { 5727 return; 5728 } 5729 5730 var focusableEls = this.focusableEls_(); 5731 var activeEl = this.el_.querySelector(':focus'); 5732 var focusIndex = void 0; 5733 5734 for (var i = 0; i < focusableEls.length; i++) { 5735 if (activeEl === focusableEls[i]) { 5736 focusIndex = i; 5737 break; 5738 } 5739 } 5740 5741 if (document_1.activeElement === this.el_) { 5742 focusIndex = 0; 5743 } 5744 5745 if (event.shiftKey && focusIndex === 0) { 5746 focusableEls[focusableEls.length - 1].focus(); 5747 event.preventDefault(); 5748 } else if (!event.shiftKey && focusIndex === focusableEls.length - 1) { 5749 focusableEls[0].focus(); 5750 event.preventDefault(); 5751 } 5752 }; 5753 5754 /** 5755 * get all focusable elements 5756 * 5757 * @private 5758 */ 5759 5760 5761 ModalDialog.prototype.focusableEls_ = function focusableEls_() { 5762 var allChildren = this.el_.querySelectorAll('*'); 5763 5764 return Array.prototype.filter.call(allChildren, function (child) { 5765 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'); 5766 }); 5767 }; 5768 5769 return ModalDialog; 5770 }(Component); 5771 5772 /** 5773 * Default options for `ModalDialog` default options. 5774 * 5775 * @type {Object} 5776 * @private 5777 */ 5778 5779 5780 ModalDialog.prototype.options_ = { 5781 pauseOnOpen: true, 5782 temporary: true 5783 }; 5784 5785 Component.registerComponent('ModalDialog', ModalDialog); 5786 5787 /** 5788 * @file track-list.js 5789 */ 5790 5791 /** 5792 * Common functionaliy between {@link TextTrackList}, {@link AudioTrackList}, and 5793 * {@link VideoTrackList} 5794 * 5795 * @extends EventTarget 5796 */ 5797 5798 var TrackList = function (_EventTarget) { 5799 inherits(TrackList, _EventTarget); 5800 5801 /** 5802 * Create an instance of this class 5803 * 5804 * @param {Track[]} tracks 5805 * A list of tracks to initialize the list with. 5806 * 5807 * @abstract 5808 */ 5809 function TrackList() { 5810 var tracks = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 5811 classCallCheck(this, TrackList); 5812 5813 var _this = possibleConstructorReturn(this, _EventTarget.call(this)); 5814 5815 _this.tracks_ = []; 5816 5817 /** 5818 * @memberof TrackList 5819 * @member {number} length 5820 * The current number of `Track`s in the this Trackist. 5821 * @instance 5822 */ 5823 Object.defineProperty(_this, 'length', { 5824 get: function get$$1() { 5825 return this.tracks_.length; 5826 } 5827 }); 5828 5829 for (var i = 0; i < tracks.length; i++) { 5830 _this.addTrack(tracks[i]); 5831 } 5832 return _this; 5833 } 5834 5835 /** 5836 * Add a {@link Track} to the `TrackList` 5837 * 5838 * @param {Track} track 5839 * The audio, video, or text track to add to the list. 5840 * 5841 * @fires TrackList#addtrack 5842 */ 5843 5844 5845 TrackList.prototype.addTrack = function addTrack(track) { 5846 var index = this.tracks_.length; 5847 5848 if (!('' + index in this)) { 5849 Object.defineProperty(this, index, { 5850 get: function get$$1() { 5851 return this.tracks_[index]; 5852 } 5853 }); 5854 } 5855 5856 // Do not add duplicate tracks 5857 if (this.tracks_.indexOf(track) === -1) { 5858 this.tracks_.push(track); 5859 /** 5860 * Triggered when a track is added to a track list. 5861 * 5862 * @event TrackList#addtrack 5863 * @type {EventTarget~Event} 5864 * @property {Track} track 5865 * A reference to track that was added. 5866 */ 5867 this.trigger({ 5868 track: track, 5869 type: 'addtrack' 5870 }); 5871 } 5872 }; 5873 5874 /** 5875 * Remove a {@link Track} from the `TrackList` 5876 * 5877 * @param {Track} rtrack 5878 * The audio, video, or text track to remove from the list. 5879 * 5880 * @fires TrackList#removetrack 5881 */ 5882 5883 5884 TrackList.prototype.removeTrack = function removeTrack(rtrack) { 5885 var track = void 0; 5886 5887 for (var i = 0, l = this.length; i < l; i++) { 5888 if (this[i] === rtrack) { 5889 track = this[i]; 5890 if (track.off) { 5891 track.off(); 5892 } 5893 5894 this.tracks_.splice(i, 1); 5895 5896 break; 5897 } 5898 } 5899 5900 if (!track) { 5901 return; 5902 } 5903 5904 /** 5905 * Triggered when a track is removed from track list. 5906 * 5907 * @event TrackList#removetrack 5908 * @type {EventTarget~Event} 5909 * @property {Track} track 5910 * A reference to track that was removed. 5911 */ 5912 this.trigger({ 5913 track: track, 5914 type: 'removetrack' 5915 }); 5916 }; 5917 5918 /** 5919 * Get a Track from the TrackList by a tracks id 5920 * 5921 * @param {String} id - the id of the track to get 5922 * @method getTrackById 5923 * @return {Track} 5924 * @private 5925 */ 5926 5927 5928 TrackList.prototype.getTrackById = function getTrackById(id) { 5929 var result = null; 5930 5931 for (var i = 0, l = this.length; i < l; i++) { 5932 var track = this[i]; 5933 5934 if (track.id === id) { 5935 result = track; 5936 break; 5937 } 5938 } 5939 5940 return result; 5941 }; 5942 5943 return TrackList; 5944 }(EventTarget); 5945 5946 /** 5947 * Triggered when a different track is selected/enabled. 5948 * 5949 * @event TrackList#change 5950 * @type {EventTarget~Event} 5951 */ 5952 5953 /** 5954 * Events that can be called with on + eventName. See {@link EventHandler}. 5955 * 5956 * @property {Object} TrackList#allowedEvents_ 5957 * @private 5958 */ 5959 5960 5961 TrackList.prototype.allowedEvents_ = { 5962 change: 'change', 5963 addtrack: 'addtrack', 5964 removetrack: 'removetrack' 5965 }; 5966 5967 // emulate attribute EventHandler support to allow for feature detection 5968 for (var event in TrackList.prototype.allowedEvents_) { 5969 TrackList.prototype['on' + event] = null; 5970 } 5971 5972 /** 5973 * @file audio-track-list.js 5974 */ 5975 5976 /** 5977 * Anywhere we call this function we diverge from the spec 5978 * as we only support one enabled audiotrack at a time 5979 * 5980 * @param {AudioTrackList} list 5981 * list to work on 5982 * 5983 * @param {AudioTrack} track 5984 * The track to skip 5985 * 5986 * @private 5987 */ 5988 var disableOthers = function disableOthers(list, track) { 5989 for (var i = 0; i < list.length; i++) { 5990 if (!Object.keys(list[i]).length || track.id === list[i].id) { 5991 continue; 5992 } 5993 // another audio track is enabled, disable it 5994 list[i].enabled = false; 5995 } 5996 }; 5997 5998 /** 5999 * The current list of {@link AudioTrack} for a media file. 6000 * 6001 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotracklist} 6002 * @extends TrackList 6003 */ 6004 6005 var AudioTrackList = function (_TrackList) { 6006 inherits(AudioTrackList, _TrackList); 6007 6008 /** 6009 * Create an instance of this class. 6010 * 6011 * @param {AudioTrack[]} [tracks=[]] 6012 * A list of `AudioTrack` to instantiate the list with. 6013 */ 6014 function AudioTrackList() { 6015 var tracks = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 6016 classCallCheck(this, AudioTrackList); 6017 6018 // make sure only 1 track is enabled 6019 // sorted from last index to first index 6020 for (var i = tracks.length - 1; i >= 0; i--) { 6021 if (tracks[i].enabled) { 6022 disableOthers(tracks, tracks[i]); 6023 break; 6024 } 6025 } 6026 6027 var _this = possibleConstructorReturn(this, _TrackList.call(this, tracks)); 6028 6029 _this.changing_ = false; 6030 return _this; 6031 } 6032 6033 /** 6034 * Add an {@link AudioTrack} to the `AudioTrackList`. 6035 * 6036 * @param {AudioTrack} track 6037 * The AudioTrack to add to the list 6038 * 6039 * @fires TrackList#addtrack 6040 */ 6041 6042 6043 AudioTrackList.prototype.addTrack = function addTrack(track) { 6044 var _this2 = this; 6045 6046 if (track.enabled) { 6047 disableOthers(this, track); 6048 } 6049 6050 _TrackList.prototype.addTrack.call(this, track); 6051 // native tracks don't have this 6052 if (!track.addEventListener) { 6053 return; 6054 } 6055 6056 /** 6057 * @listens AudioTrack#enabledchange 6058 * @fires TrackList#change 6059 */ 6060 track.addEventListener('enabledchange', function () { 6061 // when we are disabling other tracks (since we don't support 6062 // more than one track at a time) we will set changing_ 6063 // to true so that we don't trigger additional change events 6064 if (_this2.changing_) { 6065 return; 6066 } 6067 _this2.changing_ = true; 6068 disableOthers(_this2, track); 6069 _this2.changing_ = false; 6070 _this2.trigger('change'); 6071 }); 6072 }; 6073 6074 return AudioTrackList; 6075 }(TrackList); 6076 6077 /** 6078 * @file video-track-list.js 6079 */ 6080 6081 /** 6082 * Un-select all other {@link VideoTrack}s that are selected. 6083 * 6084 * @param {VideoTrackList} list 6085 * list to work on 6086 * 6087 * @param {VideoTrack} track 6088 * The track to skip 6089 * 6090 * @private 6091 */ 6092 var disableOthers$1 = function disableOthers(list, track) { 6093 for (var i = 0; i < list.length; i++) { 6094 if (!Object.keys(list[i]).length || track.id === list[i].id) { 6095 continue; 6096 } 6097 // another video track is enabled, disable it 6098 list[i].selected = false; 6099 } 6100 }; 6101 6102 /** 6103 * The current list of {@link VideoTrack} for a video. 6104 * 6105 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#videotracklist} 6106 * @extends TrackList 6107 */ 6108 6109 var VideoTrackList = function (_TrackList) { 6110 inherits(VideoTrackList, _TrackList); 6111 6112 /** 6113 * Create an instance of this class. 6114 * 6115 * @param {VideoTrack[]} [tracks=[]] 6116 * A list of `VideoTrack` to instantiate the list with. 6117 */ 6118 function VideoTrackList() { 6119 var tracks = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 6120 classCallCheck(this, VideoTrackList); 6121 6122 // make sure only 1 track is enabled 6123 // sorted from last index to first index 6124 for (var i = tracks.length - 1; i >= 0; i--) { 6125 if (tracks[i].selected) { 6126 disableOthers$1(tracks, tracks[i]); 6127 break; 6128 } 6129 } 6130 6131 var _this = possibleConstructorReturn(this, _TrackList.call(this, tracks)); 6132 6133 _this.changing_ = false; 6134 6135 /** 6136 * @member {number} VideoTrackList#selectedIndex 6137 * The current index of the selected {@link VideoTrack`}. 6138 */ 6139 Object.defineProperty(_this, 'selectedIndex', { 6140 get: function get$$1() { 6141 for (var _i = 0; _i < this.length; _i++) { 6142 if (this[_i].selected) { 6143 return _i; 6144 } 6145 } 6146 return -1; 6147 }, 6148 set: function set$$1() {} 6149 }); 6150 return _this; 6151 } 6152 6153 /** 6154 * Add a {@link VideoTrack} to the `VideoTrackList`. 6155 * 6156 * @param {VideoTrack} track 6157 * The VideoTrack to add to the list 6158 * 6159 * @fires TrackList#addtrack 6160 */ 6161 6162 6163 VideoTrackList.prototype.addTrack = function addTrack(track) { 6164 var _this2 = this; 6165 6166 if (track.selected) { 6167 disableOthers$1(this, track); 6168 } 6169 6170 _TrackList.prototype.addTrack.call(this, track); 6171 // native tracks don't have this 6172 if (!track.addEventListener) { 6173 return; 6174 } 6175 6176 /** 6177 * @listens VideoTrack#selectedchange 6178 * @fires TrackList#change 6179 */ 6180 track.addEventListener('selectedchange', function () { 6181 if (_this2.changing_) { 6182 return; 6183 } 6184 _this2.changing_ = true; 6185 disableOthers$1(_this2, track); 6186 _this2.changing_ = false; 6187 _this2.trigger('change'); 6188 }); 6189 }; 6190 6191 return VideoTrackList; 6192 }(TrackList); 6193 6194 /** 6195 * @file text-track-list.js 6196 */ 6197 6198 /** 6199 * The current list of {@link TextTrack} for a media file. 6200 * 6201 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttracklist} 6202 * @extends TrackList 6203 */ 6204 6205 var TextTrackList = function (_TrackList) { 6206 inherits(TextTrackList, _TrackList); 6207 6208 function TextTrackList() { 6209 classCallCheck(this, TextTrackList); 6210 return possibleConstructorReturn(this, _TrackList.apply(this, arguments)); 6211 } 6212 6213 /** 6214 * Add a {@link TextTrack} to the `TextTrackList` 6215 * 6216 * @param {TextTrack} track 6217 * The text track to add to the list. 6218 * 6219 * @fires TrackList#addtrack 6220 */ 6221 TextTrackList.prototype.addTrack = function addTrack(track) { 6222 _TrackList.prototype.addTrack.call(this, track); 6223 6224 /** 6225 * @listens TextTrack#modechange 6226 * @fires TrackList#change 6227 */ 6228 track.addEventListener('modechange', bind(this, function () { 6229 this.trigger('change'); 6230 })); 6231 6232 var nonLanguageTextTrackKind = ['metadata', 'chapters']; 6233 6234 if (nonLanguageTextTrackKind.indexOf(track.kind) === -1) { 6235 track.addEventListener('modechange', bind(this, function () { 6236 this.trigger('selectedlanguagechange'); 6237 })); 6238 } 6239 }; 6240 6241 return TextTrackList; 6242 }(TrackList); 6243 6244 /** 6245 * @file html-track-element-list.js 6246 */ 6247 6248 /** 6249 * The current list of {@link HtmlTrackElement}s. 6250 */ 6251 var HtmlTrackElementList = function () { 6252 6253 /** 6254 * Create an instance of this class. 6255 * 6256 * @param {HtmlTrackElement[]} [tracks=[]] 6257 * A list of `HtmlTrackElement` to instantiate the list with. 6258 */ 6259 function HtmlTrackElementList() { 6260 var trackElements = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 6261 classCallCheck(this, HtmlTrackElementList); 6262 6263 this.trackElements_ = []; 6264 6265 /** 6266 * @memberof HtmlTrackElementList 6267 * @member {number} length 6268 * The current number of `Track`s in the this Trackist. 6269 * @instance 6270 */ 6271 Object.defineProperty(this, 'length', { 6272 get: function get$$1() { 6273 return this.trackElements_.length; 6274 } 6275 }); 6276 6277 for (var i = 0, length = trackElements.length; i < length; i++) { 6278 this.addTrackElement_(trackElements[i]); 6279 } 6280 } 6281 6282 /** 6283 * Add an {@link HtmlTrackElement} to the `HtmlTrackElementList` 6284 * 6285 * @param {HtmlTrackElement} trackElement 6286 * The track element to add to the list. 6287 * 6288 * @private 6289 */ 6290 6291 6292 HtmlTrackElementList.prototype.addTrackElement_ = function addTrackElement_(trackElement) { 6293 var index = this.trackElements_.length; 6294 6295 if (!('' + index in this)) { 6296 Object.defineProperty(this, index, { 6297 get: function get$$1() { 6298 return this.trackElements_[index]; 6299 } 6300 }); 6301 } 6302 6303 // Do not add duplicate elements 6304 if (this.trackElements_.indexOf(trackElement) === -1) { 6305 this.trackElements_.push(trackElement); 6306 } 6307 }; 6308 6309 /** 6310 * Get an {@link HtmlTrackElement} from the `HtmlTrackElementList` given an 6311 * {@link TextTrack}. 6312 * 6313 * @param {TextTrack} track 6314 * The track associated with a track element. 6315 * 6316 * @return {HtmlTrackElement|undefined} 6317 * The track element that was found or undefined. 6318 * 6319 * @private 6320 */ 6321 6322 6323 HtmlTrackElementList.prototype.getTrackElementByTrack_ = function getTrackElementByTrack_(track) { 6324 var trackElement_ = void 0; 6325 6326 for (var i = 0, length = this.trackElements_.length; i < length; i++) { 6327 if (track === this.trackElements_[i].track) { 6328 trackElement_ = this.trackElements_[i]; 6329 6330 break; 6331 } 6332 } 6333 6334 return trackElement_; 6335 }; 6336 6337 /** 6338 * Remove a {@link HtmlTrackElement} from the `HtmlTrackElementList` 6339 * 6340 * @param {HtmlTrackElement} trackElement 6341 * The track element to remove from the list. 6342 * 6343 * @private 6344 */ 6345 6346 6347 HtmlTrackElementList.prototype.removeTrackElement_ = function removeTrackElement_(trackElement) { 6348 for (var i = 0, length = this.trackElements_.length; i < length; i++) { 6349 if (trackElement === this.trackElements_[i]) { 6350 this.trackElements_.splice(i, 1); 6351 6352 break; 6353 } 6354 } 6355 }; 6356 6357 return HtmlTrackElementList; 6358 }(); 6359 6360 /** 6361 * @file text-track-cue-list.js 6362 */ 6363 6364 /** 6365 * @typedef {Object} TextTrackCueList~TextTrackCue 6366 * 6367 * @property {string} id 6368 * The unique id for this text track cue 6369 * 6370 * @property {number} startTime 6371 * The start time for this text track cue 6372 * 6373 * @property {number} endTime 6374 * The end time for this text track cue 6375 * 6376 * @property {boolean} pauseOnExit 6377 * Pause when the end time is reached if true. 6378 * 6379 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackcue} 6380 */ 6381 6382 /** 6383 * A List of TextTrackCues. 6384 * 6385 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackcuelist} 6386 */ 6387 var TextTrackCueList = function () { 6388 6389 /** 6390 * Create an instance of this class.. 6391 * 6392 * @param {Array} cues 6393 * A list of cues to be initialized with 6394 */ 6395 function TextTrackCueList(cues) { 6396 classCallCheck(this, TextTrackCueList); 6397 6398 TextTrackCueList.prototype.setCues_.call(this, cues); 6399 6400 /** 6401 * @memberof TextTrackCueList 6402 * @member {number} length 6403 * The current number of `TextTrackCue`s in the TextTrackCueList. 6404 * @instance 6405 */ 6406 Object.defineProperty(this, 'length', { 6407 get: function get$$1() { 6408 return this.length_; 6409 } 6410 }); 6411 } 6412 6413 /** 6414 * A setter for cues in this list. Creates getters 6415 * an an index for the cues. 6416 * 6417 * @param {Array} cues 6418 * An array of cues to set 6419 * 6420 * @private 6421 */ 6422 6423 6424 TextTrackCueList.prototype.setCues_ = function setCues_(cues) { 6425 var oldLength = this.length || 0; 6426 var i = 0; 6427 var l = cues.length; 6428 6429 this.cues_ = cues; 6430 this.length_ = cues.length; 6431 6432 var defineProp = function defineProp(index) { 6433 if (!('' + index in this)) { 6434 Object.defineProperty(this, '' + index, { 6435 get: function get$$1() { 6436 return this.cues_[index]; 6437 } 6438 }); 6439 } 6440 }; 6441 6442 if (oldLength < l) { 6443 i = oldLength; 6444 6445 for (; i < l; i++) { 6446 defineProp.call(this, i); 6447 } 6448 } 6449 }; 6450 6451 /** 6452 * Get a `TextTrackCue` that is currently in the `TextTrackCueList` by id. 6453 * 6454 * @param {string} id 6455 * The id of the cue that should be searched for. 6456 * 6457 * @return {TextTrackCueList~TextTrackCue|null} 6458 * A single cue or null if none was found. 6459 */ 6460 6461 6462 TextTrackCueList.prototype.getCueById = function getCueById(id) { 6463 var result = null; 6464 6465 for (var i = 0, l = this.length; i < l; i++) { 6466 var cue = this[i]; 6467 6468 if (cue.id === id) { 6469 result = cue; 6470 break; 6471 } 6472 } 6473 6474 return result; 6475 }; 6476 6477 return TextTrackCueList; 6478 }(); 6479 6480 /** 6481 * @file track-kinds.js 6482 */ 6483 6484 /** 6485 * All possible `VideoTrackKind`s 6486 * 6487 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-videotrack-kind 6488 * @typedef VideoTrack~Kind 6489 * @enum 6490 */ 6491 var VideoTrackKind = { 6492 alternative: 'alternative', 6493 captions: 'captions', 6494 main: 'main', 6495 sign: 'sign', 6496 subtitles: 'subtitles', 6497 commentary: 'commentary' 6498 }; 6499 6500 /** 6501 * All possible `AudioTrackKind`s 6502 * 6503 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-audiotrack-kind 6504 * @typedef AudioTrack~Kind 6505 * @enum 6506 */ 6507 var AudioTrackKind = { 6508 'alternative': 'alternative', 6509 'descriptions': 'descriptions', 6510 'main': 'main', 6511 'main-desc': 'main-desc', 6512 'translation': 'translation', 6513 'commentary': 'commentary' 6514 }; 6515 6516 /** 6517 * All possible `TextTrackKind`s 6518 * 6519 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-texttrack-kind 6520 * @typedef TextTrack~Kind 6521 * @enum 6522 */ 6523 var TextTrackKind = { 6524 subtitles: 'subtitles', 6525 captions: 'captions', 6526 descriptions: 'descriptions', 6527 chapters: 'chapters', 6528 metadata: 'metadata' 6529 }; 6530 6531 /** 6532 * All possible `TextTrackMode`s 6533 * 6534 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackmode 6535 * @typedef TextTrack~Mode 6536 * @enum 6537 */ 6538 var TextTrackMode = { 6539 disabled: 'disabled', 6540 hidden: 'hidden', 6541 showing: 'showing' 6542 }; 6543 6544 /** 6545 * @file track.js 6546 */ 6547 6548 /** 6549 * A Track class that contains all of the common functionality for {@link AudioTrack}, 6550 * {@link VideoTrack}, and {@link TextTrack}. 6551 * 6552 * > Note: This class should not be used directly 6553 * 6554 * @see {@link https://html.spec.whatwg.org/multipage/embedded-content.html} 6555 * @extends EventTarget 6556 * @abstract 6557 */ 6558 6559 var Track = function (_EventTarget) { 6560 inherits(Track, _EventTarget); 6561 6562 /** 6563 * Create an instance of this class. 6564 * 6565 * @param {Object} [options={}] 6566 * Object of option names and values 6567 * 6568 * @param {string} [options.kind=''] 6569 * A valid kind for the track type you are creating. 6570 * 6571 * @param {string} [options.id='vjs_track_' + Guid.newGUID()] 6572 * A unique id for this AudioTrack. 6573 * 6574 * @param {string} [options.label=''] 6575 * The menu label for this track. 6576 * 6577 * @param {string} [options.language=''] 6578 * A valid two character language code. 6579 * 6580 * @abstract 6581 */ 6582 function Track() { 6583 var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 6584 classCallCheck(this, Track); 6585 6586 var _this = possibleConstructorReturn(this, _EventTarget.call(this)); 6587 6588 var trackProps = { 6589 id: options.id || 'vjs_track_' + newGUID(), 6590 kind: options.kind || '', 6591 label: options.label || '', 6592 language: options.language || '' 6593 }; 6594 6595 /** 6596 * @memberof Track 6597 * @member {string} id 6598 * The id of this track. Cannot be changed after creation. 6599 * @instance 6600 * 6601 * @readonly 6602 */ 6603 6604 /** 6605 * @memberof Track 6606 * @member {string} kind 6607 * The kind of track that this is. Cannot be changed after creation. 6608 * @instance 6609 * 6610 * @readonly 6611 */ 6612 6613 /** 6614 * @memberof Track 6615 * @member {string} label 6616 * The label of this track. Cannot be changed after creation. 6617 * @instance 6618 * 6619 * @readonly 6620 */ 6621 6622 /** 6623 * @memberof Track 6624 * @member {string} language 6625 * The two letter language code for this track. Cannot be changed after 6626 * creation. 6627 * @instance 6628 * 6629 * @readonly 6630 */ 6631 6632 var _loop = function _loop(key) { 6633 Object.defineProperty(_this, key, { 6634 get: function get$$1() { 6635 return trackProps[key]; 6636 }, 6637 set: function set$$1() {} 6638 }); 6639 }; 6640 6641 for (var key in trackProps) { 6642 _loop(key); 6643 } 6644 return _this; 6645 } 6646 6647 return Track; 6648 }(EventTarget); 6649 6650 /** 6651 * @file url.js 6652 * @module url 6653 */ 6654 6655 /** 6656 * @typedef {Object} url:URLObject 6657 * 6658 * @property {string} protocol 6659 * The protocol of the url that was parsed. 6660 * 6661 * @property {string} hostname 6662 * The hostname of the url that was parsed. 6663 * 6664 * @property {string} port 6665 * The port of the url that was parsed. 6666 * 6667 * @property {string} pathname 6668 * The pathname of the url that was parsed. 6669 * 6670 * @property {string} search 6671 * The search query of the url that was parsed. 6672 * 6673 * @property {string} hash 6674 * The hash of the url that was parsed. 6675 * 6676 * @property {string} host 6677 * The host of the url that was parsed. 6678 */ 6679 6680 /** 6681 * Resolve and parse the elements of a URL. 6682 * 6683 * @param {String} url 6684 * The url to parse 6685 * 6686 * @return {url:URLObject} 6687 * An object of url details 6688 */ 6689 var parseUrl = function parseUrl(url) { 6690 var props = ['protocol', 'hostname', 'port', 'pathname', 'search', 'hash', 'host']; 6691 6692 // add the url to an anchor and let the browser parse the URL 6693 var a = document_1.createElement('a'); 6694 6695 a.href = url; 6696 6697 // IE8 (and 9?) Fix 6698 // ie8 doesn't parse the URL correctly until the anchor is actually 6699 // added to the body, and an innerHTML is needed to trigger the parsing 6700 var addToBody = a.host === '' && a.protocol !== 'file:'; 6701 var div = void 0; 6702 6703 if (addToBody) { 6704 div = document_1.createElement('div'); 6705 div.innerHTML = '<a href="' + url + '"></a>'; 6706 a = div.firstChild; 6707 // prevent the div from affecting layout 6708 div.setAttribute('style', 'display:none; position:absolute;'); 6709 document_1.body.appendChild(div); 6710 } 6711 6712 // Copy the specific URL properties to a new object 6713 // This is also needed for IE8 because the anchor loses its 6714 // properties when it's removed from the dom 6715 var details = {}; 6716 6717 for (var i = 0; i < props.length; i++) { 6718 details[props[i]] = a[props[i]]; 6719 } 6720 6721 // IE9 adds the port to the host property unlike everyone else. If 6722 // a port identifier is added for standard ports, strip it. 6723 if (details.protocol === 'http:') { 6724 details.host = details.host.replace(/:80$/, ''); 6725 } 6726 6727 if (details.protocol === 'https:') { 6728 details.host = details.host.replace(/:443$/, ''); 6729 } 6730 6731 if (!details.protocol) { 6732 details.protocol = window_1.location.protocol; 6733 } 6734 6735 if (addToBody) { 6736 document_1.body.removeChild(div); 6737 } 6738 6739 return details; 6740 }; 6741 6742 /** 6743 * Get absolute version of relative URL. Used to tell flash correct URL. 6744 * 6745 * 6746 * @param {string} url 6747 * URL to make absolute 6748 * 6749 * @return {string} 6750 * Absolute URL 6751 * 6752 * @see http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue 6753 */ 6754 var getAbsoluteURL = function getAbsoluteURL(url) { 6755 // Check if absolute URL 6756 if (!url.match(/^https?:\/\//)) { 6757 // Convert to absolute URL. Flash hosted off-site needs an absolute URL. 6758 var div = document_1.createElement('div'); 6759 6760 div.innerHTML = '<a href="' + url + '">x</a>'; 6761 url = div.firstChild.href; 6762 } 6763 6764 return url; 6765 }; 6766 6767 /** 6768 * Returns the extension of the passed file name. It will return an empty string 6769 * if passed an invalid path. 6770 * 6771 * @param {string} path 6772 * The fileName path like '/path/to/file.mp4' 6773 * 6774 * @returns {string} 6775 * The extension in lower case or an empty string if no 6776 * extension could be found. 6777 */ 6778 var getFileExtension = function getFileExtension(path) { 6779 if (typeof path === 'string') { 6780 var splitPathRe = /^(\/?)([\s\S]*?)((?:\.{1,2}|[^\/]+?)(\.([^\.\/\?]+)))(?:[\/]*|[\?].*)$/i; 6781 var pathParts = splitPathRe.exec(path); 6782 6783 if (pathParts) { 6784 return pathParts.pop().toLowerCase(); 6785 } 6786 } 6787 6788 return ''; 6789 }; 6790 6791 /** 6792 * Returns whether the url passed is a cross domain request or not. 6793 * 6794 * @param {string} url 6795 * The url to check. 6796 * 6797 * @return {boolean} 6798 * Whether it is a cross domain request or not. 6799 */ 6800 var isCrossOrigin = function isCrossOrigin(url) { 6801 var winLoc = window_1.location; 6802 var urlInfo = parseUrl(url); 6803 6804 // IE8 protocol relative urls will return ':' for protocol 6805 var srcProtocol = urlInfo.protocol === ':' ? winLoc.protocol : urlInfo.protocol; 6806 6807 // Check if url is for another domain/origin 6808 // IE8 doesn't know location.origin, so we won't rely on it here 6809 var crossOrigin = srcProtocol + urlInfo.host !== winLoc.protocol + winLoc.host; 6810 6811 return crossOrigin; 6812 }; 6813 6814 var Url = /*#__PURE__*/Object.freeze({ 6815 parseUrl: parseUrl, 6816 getAbsoluteURL: getAbsoluteURL, 6817 getFileExtension: getFileExtension, 6818 isCrossOrigin: isCrossOrigin 6819 }); 6820 6821 var isFunction_1 = isFunction; 6822 6823 var toString$1 = Object.prototype.toString; 6824 6825 function isFunction(fn) { 6826 var string = toString$1.call(fn); 6827 return string === '[object Function]' || typeof fn === 'function' && string !== '[object RegExp]' || typeof window !== 'undefined' && ( 6828 // IE8 and below 6829 fn === window.setTimeout || fn === window.alert || fn === window.confirm || fn === window.prompt); 6830 } 6831 6832 var isFunction$1 = /*#__PURE__*/Object.freeze({ 6833 default: isFunction_1, 6834 __moduleExports: isFunction_1 6835 }); 6836
6837 var trim_1 = createCommonjsModule(function (module, exports) { 6838 exports = module.exports = trim; 6839 6840 function trim(str) { 6841 return str.replace(/^\s*|\s*$/g, ''); 6842 } 6843 6844 exports.left = function (str) { 6845 return str.replace(/^\s*/, ''); 6846 }; 6847 6848 exports.right = function (str) { 6849 return str.replace(/\s*$/, ''); 6850 }; 6851 }); 6852 var trim_2 = trim_1.left; 6853 var trim_3 = trim_1.right; 6854 6855 var trim = /*#__PURE__*/Object.freeze({ 6856 default: trim_1, 6857 __moduleExports: trim_1, 6858 left: trim_2, 6859 right: trim_3 6860 }); 6861 6862 var isFunction$2 = ( isFunction$1 && isFunction_1 ) || isFunction$1; 6863 6864 var forEach_1 = forEach; 6865 6866 var toString$2 = Object.prototype.toString; 6867 var hasOwnProperty = Object.prototype.hasOwnProperty; 6868 6869 function forEach(list, iterator, context) { 6870 if (!isFunction$2(iterator)) { 6871 throw new TypeError('iterator must be a function'); 6872 } 6873 6874 if (arguments.length < 3) { 6875 context = this; 6876 } 6877 6878 if (toString$2.call(list) === '[object Array]') forEachArray(list, iterator, context);else if (typeof list === 'string') forEachString(list, iterator, context);else forEachObject(list, iterator, context); 6879 } 6880 6881 function forEachArray(array, iterator, context) { 6882 for (var i = 0, len = array.length; i < len; i++) { 6883 if (hasOwnProperty.call(array, i)) { 6884 iterator.call(context, array[i], i, array); 6885 } 6886 } 6887 } 6888 6889 function forEachString(string, iterator, context) { 6890 for (var i = 0, len = string.length; i < len; i++) { 6891 // no such thing as a sparse string. 6892 iterator.call(context, string.charAt(i), i, string); 6893 } 6894 } 6895 6896 function forEachObject(object, iterator, context) { 6897 for (var k in object) { 6898 if (hasOwnProperty.call(object, k)) { 6899 iterator.call(context, object[k], k, object); 6900 } 6901 } 6902 } 6903 6904 var forEach$1 = /*#__PURE__*/Object.freeze({ 6905 default: forEach_1, 6906 __moduleExports: forEach_1 6907 }); 6908 6909 var trim$1 = ( trim && trim_1 ) || trim; 6910 6911 var forEach$2 = ( forEach$1 && forEach_1 ) || forEach$1; 6912 6913 var isArray = function isArray(arg) { 6914 return Object.prototype.toString.call(arg) === '[object Array]'; 6915 }; 6916 6917 var parseHeaders = function parseHeaders(headers) { 6918 if (!headers) return {}; 6919 6920 var result = {}; 6921 6922 forEach$2(trim$1(headers).split('\n'), function (row) { 6923 var index = row.indexOf(':'), 6924 key = trim$1(row.slice(0, index)).toLowerCase(), 6925 value = trim$1(row.slice(index + 1)); 6926 6927 if (typeof result[key] === 'undefined') { 6928 result[key] = value; 6929 } else if (isArray(result[key])) { 6930 result[key].push(value); 6931 } else { 6932 result[key] = [result[key], value]; 6933 } 6934 }); 6935 6936 return result; 6937 }; 6938 6939 var parseHeaders$1 = /*#__PURE__*/Object.freeze({ 6940 default: parseHeaders, 6941 __moduleExports: parseHeaders 6942 }); 6943 6944 var immutable = extend; 6945 6946 var hasOwnProperty$1 = Object.prototype.hasOwnProperty; 6947 6948 function extend() { 6949 var target = {}; 6950 6951 for (var i = 0; i < arguments.length; i++) { 6952 var source = arguments[i]; 6953 6954 for (var key in source) { 6955 if (hasOwnProperty$1.call(source, key)) { 6956 target[key] = source[key]; 6957 } 6958 } 6959 } 6960 6961 return target; 6962 } 6963 6964 var immutable$1 = /*#__PURE__*/Object.freeze({ 6965 default: immutable, 6966 __moduleExports: immutable 6967 }); 6968 6969 var parseHeaders$2 = ( parseHeaders$1 && parseHeaders ) || parseHeaders$1; 6970 6971 var xtend = ( immutable$1 && immutable ) || immutable$1; 6972 6973 var xhr = createXHR; 6974 createXHR.XMLHttpRequest = window_1.XMLHttpRequest || noop; 6975 createXHR.XDomainRequest = "withCredentials" in new createXHR.XMLHttpRequest() ? createXHR.XMLHttpRequest : window_1.XDomainRequest; 6976 6977 forEachArray$1(["get", "put", "post", "patch", "head", "delete"], function (method) { 6978 createXHR[method === "delete" ? "del" : method] = function (uri, options, callback) { 6979 options = initParams(uri, options, callback); 6980 options.method = method.toUpperCase(); 6981 return _createXHR(options); 6982 }; 6983 }); 6984 6985 function forEachArray$1(array, iterator) { 6986 for (var i = 0; i < array.length; i++) { 6987 iterator(array[i]); 6988 } 6989 } 6990 6991 function isEmpty(obj) { 6992 for (var i in obj) { 6993 if (obj.hasOwnProperty(i)) return false;
vendor: 4,140 bytes, lines 6994-7131
6994 } 6995 return true; 6996 } 6997 6998 function initParams(uri, options, callback) { 6999 var params = uri; 7000 7001 if (isFunction$2(options)) { 7002 callback = options; 7003 if (typeof uri === "string") { 7004 params = { uri: uri }; 7005 } 7006 } else { 7007 params = xtend(options, { uri: uri }); 7008 } 7009 7010 params.callback = callback; 7011 return params; 7012 } 7013 7014 function createXHR(uri, options, callback) { 7015 options = initParams(uri, options, callback); 7016 return _createXHR(options); 7017 } 7018 7019 function _createXHR(options) { 7020 if (typeof options.callback === "undefined") { 7021 throw new Error("callback argument missing"); 7022 } 7023 7024 var called = false; 7025 var callback = function cbOnce(err, response, body) { 7026 if (!called) { 7027 called = true; 7028 options.callback(err, response, body); 7029 } 7030 }; 7031 7032 function readystatechange() { 7033 if (xhr.readyState === 4) { 7034 setTimeout(loadFunc, 0); 7035 } 7036 } 7037 7038 function getBody() { 7039 // Chrome with requestType=blob throws errors arround when even testing access to responseText 7040 var body = undefined; 7041 7042 if (xhr.response) { 7043 body = xhr.response; 7044 } else { 7045 body = xhr.responseText || getXml(xhr); 7046 } 7047 7048 if (isJson) { 7049 try { 7050 body = JSON.parse(body); 7051 } catch (e) {} 7052 } 7053 7054 return body; 7055 } 7056 7057 function errorFunc(evt) { 7058 clearTimeout(timeoutTimer); 7059 if (!(evt instanceof Error)) { 7060 evt = new Error("" + (evt || "Unknown XMLHttpRequest Error")); 7061 } 7062 evt.statusCode = 0; 7063 return callback(evt, failureResponse); 7064 } 7065 7066 // will load the data & process the response in a special response object 7067 function loadFunc() { 7068 if (aborted) return; 7069 var status; 7070 clearTimeout(timeoutTimer); 7071 if (options.useXDR && xhr.status === undefined) { 7072 //IE8 CORS GET successful response doesn't have a status field, but body is fine 7073 status = 200; 7074 } else { 7075 status = xhr.status === 1223 ? 204 : xhr.status; 7076 } 7077 var response = failureResponse; 7078 var err = null; 7079 7080 if (status !== 0) { 7081 response = { 7082 body: getBody(), 7083 statusCode: status, 7084 method: method, 7085 headers: {}, 7086 url: uri, 7087 rawRequest: xhr 7088 }; 7089 if (xhr.getAllResponseHeaders) { 7090 //remember xhr can in fact be XDR for CORS in IE 7091 response.headers = parseHeaders$2(xhr.getAllResponseHeaders()); 7092 } 7093 } else { 7094 err = new Error("Internal XMLHttpRequest Error"); 7095 } 7096 return callback(err, response, response.body); 7097 } 7098 7099 var xhr = options.xhr || null; 7100 7101 if (!xhr) { 7102 if (options.cors || options.useXDR) { 7103 xhr = new createXHR.XDomainRequest(); 7104 } else { 7105 xhr = new createXHR.XMLHttpRequest(); 7106 } 7107 } 7108 7109 var key; 7110 var aborted; 7111 var uri = xhr.url = options.uri || options.url; 7112 var method = xhr.method = options.method || "GET"; 7113 var body = options.body || options.data; 7114 var headers = xhr.headers = options.headers || {}; 7115 var sync = !!options.sync; 7116 var isJson = false; 7117 var timeoutTimer; 7118 var failureResponse = { 7119 body: undefined, 7120 headers: {}, 7121 statusCode: 0, 7122 method: method, 7123 url: uri, 7124 rawRequest: xhr 7125 }; 7126 7127 if ("json" in options && options.json !== false) { 7128 isJson = true; 7129 headers["accept"] || headers["Accept"] || (headers["Accept"] = "application/json"); //Don't override existing accept header declared by user 7130 if (method !== "GET" && method !== "HEAD") { 7131 headers["content-type"] || headers["Content-Type"] || (headers["Content-Type"] = "a
vendor: 6,684 bytes, lines 7131-7352
7131pplication/json"); //Don't override existing accept header declared by user 7132 body = JSON.stringify(options.json === true ? body : options.json); 7133 } 7134 } 7135 7136 xhr.onreadystatechange = readystatechange; 7137 xhr.onload = loadFunc; 7138 xhr.onerror = errorFunc; 7139 // IE9 must have onprogress be set to a unique function. 7140 xhr.onprogress = function () { 7141 // IE must die 7142 }; 7143 xhr.onabort = function () { 7144 aborted = true; 7145 }; 7146 xhr.ontimeout = errorFunc; 7147 xhr.open(method, uri, !sync, options.username, options.password); 7148 //has to be after open 7149 if (!sync) { 7150 xhr.withCredentials = !!options.withCredentials; 7151 } 7152 // Cannot set timeout with sync request 7153 // not setting timeout on the xhr object, because of old webkits etc. not handling that correctly 7154 // both npm's request and jquery 1.x use this kind of timeout, so this is being consistent 7155 if (!sync && options.timeout > 0) { 7156 timeoutTimer = setTimeout(function () { 7157 if (aborted) return; 7158 aborted = true; //IE9 may still call readystatechange 7159 xhr.abort("timeout"); 7160 var e = new Error("XMLHttpRequest timeout"); 7161 e.code = "ETIMEDOUT"; 7162 errorFunc(e); 7163 }, options.timeout); 7164 } 7165 7166 if (xhr.setRequestHeader) { 7167 for (key in headers) { 7168 if (headers.hasOwnProperty(key)) { 7169 xhr.setRequestHeader(key, headers[key]); 7170 } 7171 } 7172 } else if (options.headers && !isEmpty(options.headers)) { 7173 throw new Error("Headers cannot be set on an XDomainRequest object"); 7174 } 7175 7176 if ("responseType" in options) { 7177 xhr.responseType = options.responseType; 7178 } 7179 7180 if ("beforeSend" in options && typeof options.beforeSend === "function") { 7181 options.beforeSend(xhr); 7182 } 7183 7184 // Microsoft Edge browser sends "undefined" when send is called with undefined value. 7185 // XMLHttpRequest spec says to pass null as body to indicate no body 7186 // See https://github.com/naugtur/xhr/issues/100. 7187 xhr.send(body || null); 7188 7189 return xhr; 7190 } 7191 7192 function getXml(xhr) { 7193 if (xhr.responseType === "document") { 7194 return xhr.responseXML; 7195 } 7196 var firefoxBugTakenEffect = xhr.responseXML && xhr.responseXML.documentElement.nodeName === "parsererror"; 7197 if (xhr.responseType === "" && !firefoxBugTakenEffect) { 7198 return xhr.responseXML; 7199 } 7200 7201 return null; 7202 } 7203 7204 function noop() {} 7205 7206 /** 7207 * @file text-track.js 7208 */ 7209 7210 /** 7211 * Takes a webvtt file contents and parses it into cues 7212 * 7213 * @param {string} srcContent 7214 * webVTT file contents 7215 * 7216 * @param {TextTrack} track 7217 * TextTrack to add cues to. Cues come from the srcContent. 7218 * 7219 * @private 7220 */ 7221 var parseCues = function parseCues(srcContent, track) { 7222 var parser = new window_1.WebVTT.Parser(window_1, window_1.vttjs, window_1.WebVTT.StringDecoder()); 7223 var errors = []; 7224 7225 parser.oncue = function (cue) { 7226 track.addCue(cue); 7227 }; 7228 7229 parser.onparsingerror = function (error) { 7230 errors.push(error); 7231 }; 7232 7233 parser.onflush = function () { 7234 track.trigger({ 7235 type: 'loadeddata', 7236 target: track 7237 }); 7238 }; 7239 7240 parser.parse(srcContent); 7241 if (errors.length > 0) { 7242 if (window_1.console && window_1.console.groupCollapsed) { 7243 window_1.console.groupCollapsed('Text Track parsing errors for ' + track.src); 7244 } 7245 errors.forEach(function (error) { 7246 return log$1.error(error); 7247 }); 7248 if (window_1.console && window_1.console.groupEnd) { 7249 window_1.console.groupEnd(); 7250 } 7251 } 7252 7253 parser.flush(); 7254 }; 7255 7256 /** 7257 * Load a `TextTrack` from a specified url. 7258 * 7259 * @param {string} src 7260 * Url to load track from. 7261 * 7262 * @param {TextTrack} track 7263 * Track to add cues to. Comes from the content at the end of `url`. 7264 * 7265 * @private 7266 */ 7267 var loadTrack = function loadTrack(src, track) { 7268 var opts = { 7269 uri: src 7270 }; 7271 var crossOrigin = isCrossOrigin(src); 7272 7273 if (crossOrigin) { 7274 opts.cors = crossOrigin; 7275 } 7276 7277 xhr(opts, bind(this, function (err, response, responseBody) { 7278 if (err) { 7279 return log$1.error(err, response); 7280 } 7281 7282 track.loaded_ = true; 7283 7284 // Make sure that vttjs has loaded, otherwise, wait till it finished loading 7285 // NOTE: this is only used for the alt/video.novtt.js build 7286 if (typeof window_1.WebVTT !== 'function') { 7287 if (track.tech_) { 7288 var loadHandler = function loadHandler() { 7289 return parseCues(responseBody, track); 7290 }; 7291 7292 track.tech_.on('vttjsloaded', loadHandler); 7293 track.tech_.on('vttjserror', function () { 7294 log$1.error('vttjs failed to load, stopping trying to process ' + track.src); 7295 track.tech_.off('vttjsloaded', loadHandler); 7296 }); 7297 } 7298 } else { 7299 parseCues(responseBody, track); 7300 } 7301 })); 7302 }; 7303 7304 /** 7305 * A representation of a single `TextTrack`. 7306 * 7307 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrack} 7308 * @extends Track 7309 */ 7310 7311 var TextTrack = function (_Track) { 7312 inherits(TextTrack, _Track); 7313 7314 /** 7315 * Create an instance of this class. 7316 * 7317 * @param {Object} options={} 7318 * Object of option names and values 7319 * 7320 * @param {Tech} options.tech 7321 * A reference to the tech that owns this TextTrack. 7322 * 7323 * @param {TextTrack~Kind} [options.kind='subtitles'] 7324 * A valid text track kind. 7325 * 7326 * @param {TextTrack~Mode} [options.mode='disabled'] 7327 * A valid text track mode. 7328 * 7329 * @param {string} [options.id='vjs_track_' + Guid.newGUID()] 7330 * A unique id for this TextTrack. 7331 * 7332 * @param {string} [options.label=''] 7333 * The menu label for this track. 7334 * 7335 * @param {string} [options.language=''] 7336 * A valid two character language code. 7337 * 7338 * @param {string} [options.srclang=''] 7339 * A valid two character language code. An alternative, but deprioritized 7340 * version of `options.language` 7341 * 7342 * @param {string} [options.src] 7343 * A url to TextTrack cues. 7344 * 7345 * @param {boolean} [options.default] 7346 * If this track should default to on or off. 7347 */ 7348 function TextTrack() { 7349 var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 7350 classCallCheck(this, TextTrack); 7351 7352 if (!options.tech) {
vendor: 17,280 bytes, lines 7353-7967
7353 throw new Error('A tech was not provided.'); 7354 } 7355 7356 var settings = mergeOptions(options, { 7357 kind: TextTrackKind[options.kind] || 'subtitles', 7358 language: options.language || options.srclang || '' 7359 }); 7360 var mode = TextTrackMode[settings.mode] || 'disabled'; 7361 var default_ = settings.default; 7362 7363 if (settings.kind === 'metadata' || settings.kind === 'chapters') { 7364 mode = 'hidden'; 7365 } 7366 7367 var _this = possibleConstructorReturn(this, _Track.call(this, settings)); 7368 7369 _this.tech_ = settings.tech; 7370 7371 _this.cues_ = []; 7372 _this.activeCues_ = []; 7373 7374 var cues = new TextTrackCueList(_this.cues_); 7375 var activeCues = new TextTrackCueList(_this.activeCues_); 7376 var changed = false; 7377 var timeupdateHandler = bind(_this, function () { 7378 7379 // Accessing this.activeCues for the side-effects of updating itself 7380 // due to it's nature as a getter function. Do not remove or cues will 7381 // stop updating! 7382 /* eslint-disable no-unused-expressions */ 7383 this.activeCues; 7384 /* eslint-enable no-unused-expressions */ 7385 if (changed) { 7386 this.trigger('cuechange'); 7387 changed = false; 7388 } 7389 }); 7390 7391 if (mode !== 'disabled') { 7392 _this.tech_.ready(function () { 7393 _this.tech_.on('timeupdate', timeupdateHandler); 7394 }, true); 7395 } 7396 7397 Object.defineProperties(_this, { 7398 /** 7399 * @memberof TextTrack 7400 * @member {boolean} default 7401 * If this track was set to be on or off by default. Cannot be changed after 7402 * creation. 7403 * @instance 7404 * 7405 * @readonly 7406 */ 7407 default: { 7408 get: function get$$1() { 7409 return default_; 7410 }, 7411 set: function set$$1() {} 7412 }, 7413 7414 /** 7415 * @memberof TextTrack 7416 * @member {string} mode 7417 * Set the mode of this TextTrack to a valid {@link TextTrack~Mode}. Will 7418 * not be set if setting to an invalid mode. 7419 * @instance 7420 * 7421 * @fires TextTrack#modechange 7422 */ 7423 mode: { 7424 get: function get$$1() { 7425 return mode; 7426 }, 7427 set: function set$$1(newMode) { 7428 var _this2 = this; 7429 7430 if (!TextTrackMode[newMode]) { 7431 return; 7432 } 7433 mode = newMode; 7434 if (mode === 'showing') { 7435 7436 this.tech_.ready(function () { 7437 _this2.tech_.on('timeupdate', timeupdateHandler); 7438 }, true); 7439 } 7440 /** 7441 * An event that fires when mode changes on this track. This allows 7442 * the TextTrackList that holds this track to act accordingly. 7443 * 7444 * > Note: This is not part of the spec! 7445 * 7446 * @event TextTrack#modechange 7447 * @type {EventTarget~Event} 7448 */ 7449 this.trigger('modechange'); 7450 } 7451 }, 7452 7453 /** 7454 * @memberof TextTrack 7455 * @member {TextTrackCueList} cues 7456 * The text track cue list for this TextTrack. 7457 * @instance 7458 */ 7459 cues: { 7460 get: function get$$1() { 7461 if (!this.loaded_) { 7462 return null; 7463 } 7464 7465 return cues; 7466 }, 7467 set: function set$$1() {} 7468 }, 7469 7470 /** 7471 * @memberof TextTrack 7472 * @member {TextTrackCueList} activeCues 7473 * The list text track cues that are currently active for this TextTrack. 7474 * @instance 7475 */ 7476 activeCues: { 7477 get: function get$$1() { 7478 if (!this.loaded_) { 7479 return null; 7480 } 7481 7482 // nothing to do 7483 if (this.cues.length === 0) { 7484 return activeCues; 7485 } 7486 7487 var ct = this.tech_.currentTime(); 7488 var active = []; 7489 7490 for (var i = 0, l = this.cues.length; i < l; i++) { 7491 var cue = this.cues[i]; 7492 7493 if (cue.startTime <= ct && cue.endTime >= ct) { 7494 active.push(cue); 7495 } else if (cue.startTime === cue.endTime && cue.startTime <= ct && cue.startTime + 0.5 >= ct) { 7496 active.push(cue); 7497 } 7498 } 7499 7500 changed = false; 7501 7502 if (active.length !== this.activeCues_.length) { 7503 changed = true; 7504 } else { 7505 for (var _i = 0; _i < active.length; _i++) { 7506 if (this.activeCues_.indexOf(active[_i]) === -1) { 7507 changed = true; 7508 } 7509 } 7510 } 7511 7512 this.activeCues_ = active; 7513 activeCues.setCues_(this.activeCues_); 7514 7515 return activeCues; 7516 }, 7517 set: function set$$1() {} 7518 } 7519 }); 7520 7521 if (settings.src) { 7522 _this.src = settings.src; 7523 loadTrack(settings.src, _this); 7524 } else { 7525 _this.loaded_ = true; 7526 } 7527 return _this; 7528 } 7529 7530 /** 7531 * Add a cue to the internal list of cues. 7532 * 7533 * @param {TextTrack~Cue} cue 7534 * The cue to add to our internal list 7535 */ 7536 7537 7538 TextTrack.prototype.addCue = function addCue(originalCue) { 7539 var cue = originalCue; 7540 7541 if (window_1.vttjs && !(originalCue instanceof window_1.vttjs.VTTCue)) { 7542 cue = new window_1.vttjs.VTTCue(originalCue.startTime, originalCue.endTime, originalCue.text); 7543 7544 for (var prop in originalCue) { 7545 if (!(prop in cue)) { 7546 cue[prop] = originalCue[prop]; 7547 } 7548 } 7549 7550 // make sure that `id` is copied over 7551 cue.id = originalCue.id; 7552 cue.originalCue_ = originalCue; 7553 } 7554 7555 var tracks = this.tech_.textTracks(); 7556 7557 for (var i = 0; i < tracks.length; i++) { 7558 if (tracks[i] !== this) { 7559 tracks[i].removeCue(cue); 7560 } 7561 } 7562 7563 this.cues_.push(cue); 7564 this.cues.setCues_(this.cues_); 7565 }; 7566 7567 /** 7568 * Remove a cue from our internal list 7569 * 7570 * @param {TextTrack~Cue} removeCue 7571 * The cue to remove from our internal list 7572 */ 7573 7574 7575 TextTrack.prototype.removeCue = function removeCue(_removeCue) { 7576 var i = this.cues_.length; 7577 7578 while (i--) { 7579 var cue = this.cues_[i]; 7580 7581 if (cue === _removeCue || cue.originalCue_ && cue.originalCue_ === _removeCue) { 7582 this.cues_.splice(i, 1); 7583 this.cues.setCues_(this.cues_); 7584 break; 7585 } 7586 } 7587 }; 7588 7589 return TextTrack; 7590 }(Track); 7591 7592 /** 7593 * cuechange - One or more cues in the track have become active or stopped being active. 7594 */ 7595 7596 7597 TextTrack.prototype.allowedEvents_ = { 7598 cuechange: 'cuechange' 7599 }; 7600 7601 /** 7602 * A representation of a single `AudioTrack`. If it is part of an {@link AudioTrackList} 7603 * only one `AudioTrack` in the list will be enabled at a time. 7604 * 7605 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotrack} 7606 * @extends Track 7607 */ 7608 7609 var AudioTrack = function (_Track) { 7610 inherits(AudioTrack, _Track); 7611 7612 /** 7613 * Create an instance of this class. 7614 * 7615 * @param {Object} [options={}] 7616 * Object of option names and values 7617 * 7618 * @param {AudioTrack~Kind} [options.kind=''] 7619 * A valid audio track kind 7620 * 7621 * @param {string} [options.id='vjs_track_' + Guid.newGUID()] 7622 * A unique id for this AudioTrack. 7623 * 7624 * @param {string} [options.label=''] 7625 * The menu label for this track. 7626 * 7627 * @param {string} [options.language=''] 7628 * A valid two character language code. 7629 * 7630 * @param {boolean} [options.enabled] 7631 * If this track is the one that is currently playing. If this track is part of 7632 * an {@link AudioTrackList}, only one {@link AudioTrack} will be enabled. 7633 */ 7634 function AudioTrack() { 7635 var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 7636 classCallCheck(this, AudioTrack); 7637 7638 var settings = mergeOptions(options, { 7639 kind: AudioTrackKind[options.kind] || '' 7640 }); 7641 7642 var _this = possibleConstructorReturn(this, _Track.call(this, settings)); 7643 7644 var enabled = false; 7645 7646 /** 7647 * @memberof AudioTrack 7648 * @member {boolean} enabled 7649 * If this `AudioTrack` is enabled or not. When setting this will 7650 * fire {@link AudioTrack#enabledchange} if the state of enabled is changed. 7651 * @instance 7652 * 7653 * @fires VideoTrack#selectedchange 7654 */ 7655 Object.defineProperty(_this, 'enabled', { 7656 get: function get$$1() { 7657 return enabled; 7658 }, 7659 set: function set$$1(newEnabled) { 7660 // an invalid or unchanged value 7661 if (typeof newEnabled !== 'boolean' || newEnabled === enabled) { 7662 return; 7663 } 7664 enabled = newEnabled; 7665 7666 /** 7667 * An event that fires when enabled changes on this track. This allows 7668 * the AudioTrackList that holds this track to act accordingly. 7669 * 7670 * > Note: This is not part of the spec! Native tracks will do 7671 * this internally without an event. 7672 * 7673 * @event AudioTrack#enabledchange 7674 * @type {EventTarget~Event} 7675 */ 7676 this.trigger('enabledchange'); 7677 } 7678 }); 7679 7680 // if the user sets this track to selected then 7681 // set selected to that true value otherwise 7682 // we keep it false 7683 if (settings.enabled) { 7684 _this.enabled = settings.enabled; 7685 } 7686 _this.loaded_ = true; 7687 return _this; 7688 } 7689 7690 return AudioTrack; 7691 }(Track); 7692 7693 /** 7694 * A representation of a single `VideoTrack`. 7695 * 7696 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#videotrack} 7697 * @extends Track 7698 */ 7699 7700 var VideoTrack = function (_Track) { 7701 inherits(VideoTrack, _Track); 7702 7703 /** 7704 * Create an instance of this class. 7705 * 7706 * @param {Object} [options={}] 7707 * Object of option names and values 7708 * 7709 * @param {string} [options.kind=''] 7710 * A valid {@link VideoTrack~Kind} 7711 * 7712 * @param {string} [options.id='vjs_track_' + Guid.newGUID()] 7713 * A unique id for this AudioTrack. 7714 * 7715 * @param {string} [options.label=''] 7716 * The menu label for this track. 7717 * 7718 * @param {string} [options.language=''] 7719 * A valid two character language code. 7720 * 7721 * @param {boolean} [options.selected] 7722 * If this track is the one that is currently playing. 7723 */ 7724 function VideoTrack() { 7725 var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 7726 classCallCheck(this, VideoTrack); 7727 7728 var settings = mergeOptions(options, { 7729 kind: VideoTrackKind[options.kind] || '' 7730 }); 7731 7732 var _this = possibleConstructorReturn(this, _Track.call(this, settings)); 7733 7734 var selected = false; 7735 7736 /** 7737 * @memberof VideoTrack 7738 * @member {boolean} selected 7739 * If this `VideoTrack` is selected or not. When setting this will 7740 * fire {@link VideoTrack#selectedchange} if the state of selected changed. 7741 * @instance 7742 * 7743 * @fires VideoTrack#selectedchange 7744 */ 7745 Object.defineProperty(_this, 'selected', { 7746 get: function get$$1() { 7747 return selected; 7748 }, 7749 set: function set$$1(newSelected) { 7750 // an invalid or unchanged value 7751 if (typeof newSelected !== 'boolean' || newSelected === selected) { 7752 return; 7753 } 7754 selected = newSelected; 7755 7756 /** 7757 * An event that fires when selected changes on this track. This allows 7758 * the VideoTrackList that holds this track to act accordingly. 7759 * 7760 * > Note: This is not part of the spec! Native tracks will do 7761 * this internally without an event. 7762 * 7763 * @event VideoTrack#selectedchange 7764 * @type {EventTarget~Event} 7765 */ 7766 this.trigger('selectedchange'); 7767 } 7768 }); 7769 7770 // if the user sets this track to selected then 7771 // set selected to that true value otherwise 7772 // we keep it false 7773 if (settings.selected) { 7774 _this.selected = settings.selected; 7775 } 7776 return _this; 7777 } 7778 7779 return VideoTrack; 7780 }(Track); 7781 7782 /** 7783 * @file html-track-element.js 7784 */ 7785 7786 /** 7787 * @memberof HTMLTrackElement 7788 * @typedef {HTMLTrackElement~ReadyState} 7789 * @enum {number} 7790 */ 7791 var NONE = 0; 7792 var LOADING = 1; 7793 var LOADED = 2; 7794 var ERROR = 3; 7795 7796 /** 7797 * A single track represented in the DOM. 7798 * 7799 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#htmltrackelement} 7800 * @extends EventTarget 7801 */ 7802 7803 var HTMLTrackElement = function (_EventTarget) { 7804 inherits(HTMLTrackElement, _EventTarget); 7805 7806 /** 7807 * Create an instance of this class. 7808 * 7809 * @param {Object} options={} 7810 * Object of option names and values 7811 * 7812 * @param {Tech} options.tech 7813 * A reference to the tech that owns this HTMLTrackElement. 7814 * 7815 * @param {TextTrack~Kind} [options.kind='subtitles'] 7816 * A valid text track kind. 7817 * 7818 * @param {TextTrack~Mode} [options.mode='disabled'] 7819 * A valid text track mode. 7820 * 7821 * @param {string} [options.id='vjs_track_' + Guid.newGUID()] 7822 * A unique id for this TextTrack. 7823 * 7824 * @param {string} [options.label=''] 7825 * The menu label for this track. 7826 * 7827 * @param {string} [options.language=''] 7828 * A valid two character language code. 7829 * 7830 * @param {string} [options.srclang=''] 7831 * A valid two character language code. An alternative, but deprioritized 7832 * vesion of `options.language` 7833 * 7834 * @param {string} [options.src] 7835 * A url to TextTrack cues. 7836 * 7837 * @param {boolean} [options.default] 7838 * If this track should default to on or off. 7839 */ 7840 function HTMLTrackElement() { 7841 var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 7842 classCallCheck(this, HTMLTrackElement); 7843 7844 var _this = possibleConstructorReturn(this, _EventTarget.call(this)); 7845 7846 var readyState = void 0; 7847 7848 var track = new TextTrack(options); 7849 7850 _this.kind = track.kind; 7851 _this.src = track.src; 7852 _this.srclang = track.language; 7853 _this.label = track.label; 7854 _this.default = track.default; 7855 7856 Object.defineProperties(_this, { 7857 7858 /** 7859 * @memberof HTMLTrackElement 7860 * @member {HTMLTrackElement~ReadyState} readyState 7861 * The current ready state of the track element. 7862 * @instance 7863 */ 7864 readyState: { 7865 get: function get$$1() { 7866 return readyState; 7867 } 7868 }, 7869 7870 /** 7871 * @memberof HTMLTrackElement 7872 * @member {TextTrack} track 7873 * The underlying TextTrack object. 7874 * @instance 7875 * 7876 */ 7877 track: { 7878 get: function get$$1() { 7879 return track; 7880 } 7881 } 7882 }); 7883 7884 readyState = NONE; 7885 7886 /** 7887 * @listens TextTrack#loadeddata 7888 * @fires HTMLTrackElement#load 7889 */ 7890 track.addEventListener('loadeddata', function () { 7891 readyState = LOADED; 7892 7893 _this.trigger({ 7894 type: 'load', 7895 target: _this 7896 }); 7897 }); 7898 return _this; 7899 } 7900 7901 return HTMLTrackElement; 7902 }(EventTarget); 7903 7904 HTMLTrackElement.prototype.allowedEvents_ = { 7905 load: 'load' 7906 }; 7907 7908 HTMLTrackElement.NONE = NONE; 7909 HTMLTrackElement.LOADING = LOADING; 7910 HTMLTrackElement.LOADED = LOADED; 7911 HTMLTrackElement.ERROR = ERROR; 7912 7913 /* 7914 * This file contains all track properties that are used in 7915 * player.js, tech.js, html5.js and possibly other techs in the future. 7916 */ 7917 7918 var NORMAL = { 7919 audio: { 7920 ListClass: AudioTrackList, 7921 TrackClass: AudioTrack, 7922 capitalName: 'Audio' 7923 }, 7924 video: { 7925 ListClass: VideoTrackList, 7926 TrackClass: VideoTrack, 7927 capitalName: 'Video' 7928 }, 7929 text: { 7930 ListClass: TextTrackList, 7931 TrackClass: TextTrack, 7932 capitalName: 'Text' 7933 } 7934 }; 7935 7936 Object.keys(NORMAL).forEach(function (type) { 7937 NORMAL[type].getterName = type + 'Tracks'; 7938 NORMAL[type].privateName = type + 'Tracks_'; 7939 }); 7940 7941 var REMOTE = { 7942 remoteText: { 7943 ListClass: TextTrackList, 7944 TrackClass: TextTrack, 7945 capitalName: 'RemoteText', 7946 getterName: 'remoteTextTracks', 7947 privateName: 'remoteTextTracks_' 7948 }, 7949 remoteTextEl: { 7950 ListClass: HtmlTrackElementList, 7951 TrackClass: HTMLTrackElement, 7952 capitalName: 'RemoteTextTrackEls', 7953 getterName: 'remoteTextTrackEls', 7954 privateName: 'remoteTextTrackEls_' 7955 } 7956 }; 7957 7958 var ALL = mergeOptions(NORMAL, REMOTE); 7959 7960 REMOTE.names = Object.keys(REMOTE); 7961 NORMAL.names = Object.keys(NORMAL); 7962 ALL.names = [].concat(REMOTE.names).concat(NORMAL.names); 7963 7964 /** 7965 * Copyright 2013 vtt.js Contributors 7966 * 7967 * Licensed under the Apache License, Version 2.0 (the "License");
7968 * you may not use this file except in compliance with the License. 7969 * You may obtain a copy of the License at 7970 * 7971 * http://www.apache.org/licenses/LICENSE-2.0 7972 * 7973 * Unless required by applicable law or agreed to in writing, software 7974 * distributed under the License is distributed on an "AS IS" BASIS, 7975 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 7976 * See the License for the specific language governing permissions and 7977 * limitations under the License. 7978 */ 7979 7980 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 7981 /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ 7982 var _objCreate = Object.create || function () { 7983 function F() {} 7984 return function (o) { 7985 if (arguments.length !== 1) { 7986 throw new Error('Object.create shim only accepts one parameter.'); 7987 } 7988 F.prototype = o; 7989 return new F(); 7990 }; 7991 }(); 7992 7993 // Creates a new ParserError object from an errorData object. The errorData 7994 // object should have default code and message properties. The default message 7995 // property can be overriden by passing in a message parameter. 7996 // See ParsingError.Errors below for acceptable errors. 7997 function ParsingError(errorData, message) { 7998 this.name = "ParsingError"; 7999 this.code = errorData.code; 8000 this.message = message || errorData.message; 8001 } 8002 ParsingError.prototype = _objCreate(Error.prototype); 8003 ParsingError.prototype.constructor = ParsingError; 8004 8005 // ParsingError metadata for acceptable ParsingErrors. 8006 ParsingError.Errors = { 8007 BadSignature: { 8008 code: 0, 8009 message: "Malformed WebVTT signature." 8010 }, 8011 BadTimeStamp: { 8012 code: 1, 8013 message: "Malformed time stamp." 8014 } 8015 }; 8016 8017 // Try to parse input as a time stamp. 8018 function parseTimeStamp(input) { 8019 8020 function computeSeconds(h, m, s, f) { 8021 return (h | 0) * 3600 + (m | 0) * 60 + (s | 0) + (f | 0) / 1000; 8022 } 8023 8024 var m = input.match(/^(\d+):(\d{2})(:\d{2})?\.(\d{3})/); 8025 if (!m) { 8026 return null; 8027 } 8028 8029 if (m[3]) { 8030 // Timestamp takes the form of [hours]:[minutes]:[seconds].[milliseconds] 8031 return computeSeconds(m[1], m[2], m[3].replace(":", ""), m[4]); 8032 } else if (m[1] > 59) { 8033 // Timestamp takes the form of [hours]:[minutes].[milliseconds] 8034 // First position is hours as it's over 59. 8035 return computeSeconds(m[1], m[2], 0, m[4]); 8036 } else { 8037 // Timestamp takes the form of [minutes]:[seconds].[milliseconds] 8038 return computeSeconds(0, m[1], m[2], m[4]); 8039 } 8040 } 8041 8042 // A settings object holds key/value pairs and will ignore anything but the first 8043 // assignment to a specific key. 8044 function Settings() { 8045 this.values = _objCreate(null); 8046 } 8047 8048 Settings.prototype = { 8049 // Only accept the first assignment to any key. 8050 set: function set(k, v) { 8051 if (!this.get(k) && v !== "") { 8052 this.values[k] = v; 8053 } 8054 }, 8055 // Return the value for a key, or a default value. 8056 // If 'defaultKey' is passed then 'dflt' is assumed to be an object with 8057 // a number of possible default values as properties where 'defaultKey' is 8058 // the key of the property that will be chosen; otherwise it's assumed to be 8059 // a single value. 8060 get: function get(k, dflt, defaultKey) { 8061 if (defaultKey) { 8062 return this.has(k) ? this.values[k] : dflt[defaultKey]; 8063 } 8064 return this.has(k) ? this.values[k] : dflt; 8065 }, 8066 // Check whether we have a value for a key. 8067 has: function has(k) { 8068 return k in this.values; 8069 }, 8070 // Accept a setting if its one of the given alternatives. 8071 alt: function alt(k, v, a) { 8072 for (var n = 0; n < a.length; ++n) { 8073 if (v === a[n]) { 8074 this.set(k, v); 8075 break; 8076 } 8077 } 8078 }, 8079 // Accept a setting if its a valid (signed) integer. 8080 integer: function integer(k, v) { 8081 if (/^-?\d+$/.test(v)) { 8082 // integer 8083 this.set(k, parseInt(v, 10)); 8084 } 8085 }, 8086 // Accept a setting if its a valid percentage. 8087 percent: function percent(k, v) { 8088 var m; 8089 if (m = v.match(/^([\d]{1,3})(\.[\d]*)?%$/)) { 8090 v = parseFloat(v); 8091 if (v >= 0 && v <= 100) { 8092 this.set(k, v); 8093 return true; 8094 } 8095 } 8096 return false;
vendor: 9,025 bytes, lines 8097-8363
8097 } 8098 }; 8099 8100 // Helper function to parse input into groups separated by 'groupDelim', and 8101 // interprete each group as a key/value pair separated by 'keyValueDelim'. 8102 function parseOptions(input, callback, keyValueDelim, groupDelim) { 8103 var groups = groupDelim ? input.split(groupDelim) : [input]; 8104 for (var i in groups) { 8105 if (typeof groups[i] !== "string") { 8106 continue; 8107 } 8108 var kv = groups[i].split(keyValueDelim); 8109 if (kv.length !== 2) { 8110 continue; 8111 } 8112 var k = kv[0]; 8113 var v = kv[1]; 8114 callback(k, v); 8115 } 8116 } 8117 8118 function parseCue(input, cue, regionList) { 8119 // Remember the original input if we need to throw an error. 8120 var oInput = input; 8121 // 4.1 WebVTT timestamp 8122 function consumeTimeStamp() { 8123 var ts = parseTimeStamp(input); 8124 if (ts === null) { 8125 throw new ParsingError(ParsingError.Errors.BadTimeStamp, "Malformed timestamp: " + oInput); 8126 } 8127 // Remove time stamp from input. 8128 input = input.replace(/^[^\sa-zA-Z-]+/, ""); 8129 return ts; 8130 } 8131 8132 // 4.4.2 WebVTT cue settings 8133 function consumeCueSettings(input, cue) { 8134 var settings = new Settings(); 8135 8136 parseOptions(input, function (k, v) { 8137 switch (k) { 8138 case "region": 8139 // Find the last region we parsed with the same region id. 8140 for (var i = regionList.length - 1; i >= 0; i--) { 8141 if (regionList[i].id === v) { 8142 settings.set(k, regionList[i].region); 8143 break; 8144 } 8145 } 8146 break; 8147 case "vertical": 8148 settings.alt(k, v, ["rl", "lr"]); 8149 break; 8150 case "line": 8151 var vals = v.split(","), 8152 vals0 = vals[0]; 8153 settings.integer(k, vals0); 8154 settings.percent(k, vals0) ? settings.set("snapToLines", false) : null; 8155 settings.alt(k, vals0, ["auto"]); 8156 if (vals.length === 2) { 8157 settings.alt("lineAlign", vals[1], ["start", "middle", "end"]); 8158 } 8159 break; 8160 case "position": 8161 vals = v.split(","); 8162 settings.percent(k, vals[0]); 8163 if (vals.length === 2) { 8164 settings.alt("positionAlign", vals[1], ["start", "middle", "end"]); 8165 } 8166 break; 8167 case "size": 8168 settings.percent(k, v); 8169 break; 8170 case "align": 8171 settings.alt(k, v, ["start", "middle", "end", "left", "right"]); 8172 break; 8173 } 8174 }, /:/, /\s/); 8175 8176 // Apply default values for any missing fields. 8177 cue.region = settings.get("region", null); 8178 cue.vertical = settings.get("vertical", ""); 8179 cue.line = settings.get("line", "auto"); 8180 cue.lineAlign = settings.get("lineAlign", "start"); 8181 cue.snapToLines = settings.get("snapToLines", true); 8182 cue.size = settings.get("size", 100); 8183 cue.align = settings.get("align", "middle"); 8184 cue.position = settings.get("position", { 8185 start: 0, 8186 left: 0, 8187 middle: 50, 8188 end: 100, 8189 right: 100 8190 }, cue.align); 8191 cue.positionAlign = settings.get("positionAlign", { 8192 start: "start", 8193 left: "start", 8194 middle: "middle", 8195 end: "end", 8196 right: "end" 8197 }, cue.align); 8198 } 8199 8200 function skipWhitespace() { 8201 input = input.replace(/^\s+/, ""); 8202 } 8203 8204 // 4.1 WebVTT cue timings. 8205 skipWhitespace(); 8206 cue.startTime = consumeTimeStamp(); // (1) collect cue start time 8207 skipWhitespace(); 8208 if (input.substr(0, 3) !== "-->") { 8209 // (3) next characters must match "-->" 8210 throw new ParsingError(ParsingError.Errors.BadTimeStamp, "Malformed time stamp (time stamps must be separated by '-->'): " + oInput); 8211 } 8212 input = input.substr(3); 8213 skipWhitespace(); 8214 cue.endTime = consumeTimeStamp(); // (5) collect cue end time 8215 8216 // 4.1 WebVTT cue settings list. 8217 skipWhitespace(); 8218 consumeCueSettings(input, cue); 8219 } 8220 8221 var ESCAPE = { 8222 "&": "&", 8223 "<": "<", 8224 ">": ">", 8225 "‎": "\u200E", 8226 "‏": "\u200F", 8227 " ": "\xA0" 8228 }; 8229 8230 var TAG_NAME = { 8231 c: "span", 8232 i: "i", 8233 b: "b", 8234 u: "u", 8235 ruby: "ruby", 8236 rt: "rt", 8237 v: "span", 8238 lang: "span" 8239 }; 8240 8241 var TAG_ANNOTATION = { 8242 v: "title", 8243 lang: "lang" 8244 }; 8245 8246 var NEEDS_PARENT = { 8247 rt: "ruby" 8248 }; 8249 8250 // Parse content into a document fragment. 8251 function parseContent(window, input) { 8252 function nextToken() { 8253 // Check for end-of-string. 8254 if (!input) { 8255 return null; 8256 } 8257 8258 // Consume 'n' characters from the input. 8259 function consume(result) { 8260 input = input.substr(result.length); 8261 return result; 8262 } 8263 8264 var m = input.match(/^([^<]*)(<[^>]*>?)?/); 8265 // If there is some text before the next tag, return it, otherwise return 8266 // the tag. 8267 return consume(m[1] ? m[1] : m[2]); 8268 } 8269 8270 // Unescape a string 's'. 8271 function unescape1(e) { 8272 return ESCAPE[e]; 8273 } 8274 function unescape(s) { 8275 while (m = s.match(/&(amp|lt|gt|lrm|rlm|nbsp);/)) { 8276 s = s.replace(m[0], unescape1); 8277 } 8278 return s; 8279 } 8280 8281 function shouldAdd(current, element) { 8282 return !NEEDS_PARENT[element.localName] || NEEDS_PARENT[element.localName] === current.localName; 8283 } 8284 8285 // Create an element for this tag. 8286 function createElement(type, annotation) { 8287 var tagName = TAG_NAME[type]; 8288 if (!tagName) { 8289 return null; 8290 } 8291 var element = window.document.createElement(tagName); 8292 element.localName = tagName; 8293 var name = TAG_ANNOTATION[type]; 8294 if (name && annotation) { 8295 element[name] = annotation.trim(); 8296 } 8297 return element; 8298 } 8299 8300 var rootDiv = window.document.createElement("div"), 8301 current = rootDiv, 8302 t, 8303 tagStack = []; 8304 8305 while ((t = nextToken()) !== null) { 8306 if (t[0] === '<') { 8307 if (t[1] === "/") { 8308 // If the closing tag matches, move back up to the parent node. 8309 if (tagStack.length && tagStack[tagStack.length - 1] === t.substr(2).replace(">", "")) { 8310 tagStack.pop(); 8311 current = current.parentNode; 8312 } 8313 // Otherwise just ignore the end tag. 8314 continue; 8315 } 8316 var ts = parseTimeStamp(t.substr(1, t.length - 2)); 8317 var node; 8318 if (ts) { 8319 // Timestamps are lead nodes as well. 8320 node = window.document.createProcessingInstruction("timestamp", ts); 8321 current.appendChild(node); 8322 continue; 8323 } 8324 var m = t.match(/^<([^.\s/0-9>]+)(\.[^\s\\>]+)?([^>\\]+)?(\\?)>?$/); 8325 // If we can't parse the tag, skip to the next tag. 8326 if (!m) { 8327 continue; 8328 } 8329 // Try to construct an element, and ignore the tag if we couldn't. 8330 node = createElement(m[1], m[3]); 8331 if (!node) { 8332 continue; 8333 } 8334 // Determine if the tag should be added based on the context of where it 8335 // is placed in the cuetext. 8336 if (!shouldAdd(current, node)) { 8337 continue; 8338 } 8339 // Set the class list (as a list of classes, separated by space). 8340 if (m[2]) { 8341 node.className = m[2].substr(1).replace('.', ' '); 8342 } 8343 // Append the node to the current node, and enter the scope of the new 8344 // node. 8345 tagStack.push(m[1]); 8346 current.appendChild(node); 8347 current = node; 8348 continue; 8349 } 8350 8351 // Text nodes are leaf nodes. 8352 current.appendChild(window.document.createTextNode(unescape(t))); 8353 } 8354 8355 return rootDiv; 8356 } 8357 8358 // This is a list of all the Unicode characters that have a strong 8359 // right-to-left category. What this means is that these characters are 8360 // written right-to-left for sure. It was generated by pulling all the strong 8361 // right-to-left characters out of the Unicode data table. That table can 8362 // found at: http://www.unicode.org/Public/UNIDATA/UnicodeData.txt 8363 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,
vendor: 18,697 bytes, lines 8363-8878
83630x1093f], [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]]; 8364 8365 function isStrongRTLChar(charCode) { 8366 for (var i = 0; i < strongRTLRanges.length; i++) { 8367 var currentRange = strongRTLRanges[i]; 8368 if (charCode >= currentRange[0] && charCode <= currentRange[1]) { 8369 return true; 8370 } 8371 } 8372 8373 return false; 8374 } 8375 8376 function determineBidi(cueDiv) { 8377 var nodeStack = [], 8378 text = "", 8379 charCode; 8380 8381 if (!cueDiv || !cueDiv.childNodes) { 8382 return "ltr"; 8383 } 8384 8385 function pushNodes(nodeStack, node) { 8386 for (var i = node.childNodes.length - 1; i >= 0; i--) { 8387 nodeStack.push(node.childNodes[i]); 8388 } 8389 } 8390 8391 function nextTextNode(nodeStack) { 8392 if (!nodeStack || !nodeStack.length) { 8393 return null; 8394 } 8395 8396 var node = nodeStack.pop(), 8397 text = node.textContent || node.innerText; 8398 if (text) { 8399 // TODO: This should match all unicode type B characters (paragraph 8400 // separator characters). See issue #115. 8401 var m = text.match(/^.*(\n|\r)/); 8402 if (m) { 8403 nodeStack.length = 0; 8404 return m[0]; 8405 } 8406 return text; 8407 } 8408 if (node.tagName === "ruby") { 8409 return nextTextNode(nodeStack); 8410 } 8411 if (node.childNodes) { 8412 pushNodes(nodeStack, node); 8413 return nextTextNode(nodeStack); 8414 } 8415 } 8416 8417 pushNodes(nodeStack, cueDiv); 8418 while (text = nextTextNode(nodeStack)) { 8419 for (var i = 0; i < text.length; i++) { 8420 charCode = text.charCodeAt(i); 8421 if (isStrongRTLChar(charCode)) { 8422 return "rtl"; 8423 } 8424 } 8425 } 8426 return "ltr"; 8427 } 8428 8429 function computeLinePos(cue) { 8430 if (typeof cue.line === "number" && (cue.snapToLines || cue.line >= 0 && cue.line <= 100)) { 8431 return cue.line; 8432 } 8433 if (!cue.track || !cue.track.textTrackList || !cue.track.textTrackList.mediaElement) { 8434 return -1; 8435 } 8436 var track = cue.track, 8437 trackList = track.textTrackList, 8438 count = 0; 8439 for (var i = 0; i < trackList.length && trackList[i] !== track; i++) { 8440 if (trackList[i].mode === "showing") { 8441 count++; 8442 } 8443 } 8444 return ++count * -1; 8445 } 8446 8447 function StyleBox() {} 8448 8449 // Apply styles to a div. If there is no div passed then it defaults to the 8450 // div on 'this'. 8451 StyleBox.prototype.applyStyles = function (styles, div) { 8452 div = div || this.div; 8453 for (var prop in styles) { 8454 if (styles.hasOwnProperty(prop)) { 8455 div.style[prop] = styles[prop]; 8456 } 8457 } 8458 }; 8459 8460 StyleBox.prototype.formatStyle = function (val, unit) { 8461 return val === 0 ? 0 : val + unit; 8462 }; 8463 8464 // Constructs the computed display state of the cue (a div). Places the div 8465 // into the overlay which should be a block level element (usually a div). 8466 function CueStyleBox(window, cue, styleOptions) { 8467 StyleBox.call(this); 8468 this.cue = cue; 8469 8470 // Parse our cue's text into a DOM tree rooted at 'cueDiv'. This div will 8471 // have inline positioning and will function as the cue background box. 8472 this.cueDiv = parseContent(window, cue.text); 8473 var styles = { 8474 color: "rgba(255, 255, 255, 1)", 8475 backgroundColor: "rgba(0, 0, 0, 0.8)", 8476 position: "relative", 8477 left: 0, 8478 right: 0, 8479 top: 0, 8480 bottom: 0, 8481 display: "inline", 8482 writingMode: cue.vertical === "" ? "horizontal-tb" : cue.vertical === "lr" ? "vertical-lr" : "vertical-rl", 8483 unicodeBidi: "plaintext" 8484 }; 8485 8486 this.applyStyles(styles, this.cueDiv); 8487 8488 // Create an absolutely positioned div that will be used to position the cue 8489 // div. Note, all WebVTT cue-setting alignments are equivalent to the CSS 8490 // mirrors of them except "middle" which is "center" in CSS. 8491 this.div = window.document.createElement("div"); 8492 styles = { 8493 direction: determineBidi(this.cueDiv), 8494 writingMode: cue.vertical === "" ? "horizontal-tb" : cue.vertical === "lr" ? "vertical-lr" : "vertical-rl", 8495 unicodeBidi: "plaintext", 8496 textAlign: cue.align === "middle" ? "center" : cue.align, 8497 font: styleOptions.font, 8498 whiteSpace: "pre-line", 8499 position: "absolute" 8500 }; 8501 8502 this.applyStyles(styles); 8503 this.div.appendChild(this.cueDiv); 8504 8505 // Calculate the distance from the reference edge of the viewport to the text 8506 // position of the cue box. The reference edge will be resolved later when 8507 // the box orientation styles are applied. 8508 var textPos = 0; 8509 switch (cue.positionAlign) { 8510 case "start": 8511 textPos = cue.position; 8512 break; 8513 case "middle": 8514 textPos = cue.position - cue.size / 2; 8515 break; 8516 case "end": 8517 textPos = cue.position - cue.size; 8518 break; 8519 } 8520 8521 // Horizontal box orientation; textPos is the distance from the left edge of the 8522 // area to the left edge of the box and cue.size is the distance extending to 8523 // the right from there. 8524 if (cue.vertical === "") { 8525 this.applyStyles({ 8526 left: this.formatStyle(textPos, "%"), 8527 width: this.formatStyle(cue.size, "%") 8528 }); 8529 // Vertical box orientation; textPos is the distance from the top edge of the 8530 // area to the top edge of the box and cue.size is the height extending 8531 // downwards from there. 8532 } else { 8533 this.applyStyles({ 8534 top: this.formatStyle(textPos, "%"), 8535 height: this.formatStyle(cue.size, "%") 8536 }); 8537 } 8538 8539 this.move = function (box) { 8540 this.applyStyles({ 8541 top: this.formatStyle(box.top, "px"), 8542 bottom: this.formatStyle(box.bottom, "px"), 8543 left: this.formatStyle(box.left, "px"), 8544 right: this.formatStyle(box.right, "px"), 8545 height: this.formatStyle(box.height, "px"), 8546 width: this.formatStyle(box.width, "px") 8547 }); 8548 }; 8549 } 8550 CueStyleBox.prototype = _objCreate(StyleBox.prototype); 8551 CueStyleBox.prototype.constructor = CueStyleBox; 8552 8553 // Represents the co-ordinates of an Element in a way that we can easily 8554 // compute things with such as if it overlaps or intersects with another Element. 8555 // Can initialize it with either a StyleBox or another BoxPosition. 8556 function BoxPosition(obj) { 8557 // Either a BoxPosition was passed in and we need to copy it, or a StyleBox 8558 // was passed in and we need to copy the results of 'getBoundingClientRect' 8559 // as the object returned is readonly. All co-ordinate values are in reference 8560 // to the viewport origin (top left). 8561 var lh, height, width, top; 8562 if (obj.div) { 8563 height = obj.div.offsetHeight; 8564 width = obj.div.offsetWidth; 8565 top = obj.div.offsetTop; 8566 8567 var rects = (rects = obj.div.childNodes) && (rects = rects[0]) && rects.getClientRects && rects.getClientRects(); 8568 obj = obj.div.getBoundingClientRect(); 8569 // In certain cases the outter div will be slightly larger then the sum of 8570 // the inner div's lines. This could be due to bold text, etc, on some platforms. 8571 // In this case we should get the average line height and use that. This will 8572 // result in the desired behaviour. 8573 lh = rects ? Math.max(rects[0] && rects[0].height || 0, obj.height / rects.length) : 0; 8574 } 8575 this.left = obj.left; 8576 this.right = obj.right; 8577 this.top = obj.top || top; 8578 this.height = obj.height || height; 8579 this.bottom = obj.bottom || top + (obj.height || height); 8580 this.width = obj.width || width; 8581 this.lineHeight = lh !== undefined ? lh : obj.lineHeight; 8582 } 8583 8584 // Move the box along a particular axis. Optionally pass in an amount to move 8585 // the box. If no amount is passed then the default is the line height of the 8586 // box. 8587 BoxPosition.prototype.move = function (axis, toMove) { 8588 toMove = toMove !== undefined ? toMove : this.lineHeight; 8589 switch (axis) { 8590 case "+x": 8591 this.left += toMove; 8592 this.right += toMove; 8593 break; 8594 case "-x": 8595 this.left -= toMove; 8596 this.right -= toMove; 8597 break; 8598 case "+y": 8599 this.top += toMove; 8600 this.bottom += toMove; 8601 break; 8602 case "-y": 8603 this.top -= toMove; 8604 this.bottom -= toMove; 8605 break; 8606 } 8607 }; 8608 8609 // Check if this box overlaps another box, b2. 8610 BoxPosition.prototype.overlaps = function (b2) { 8611 return this.left < b2.right && this.right > b2.left && this.top < b2.bottom && this.bottom > b2.top; 8612 }; 8613 8614 // Check if this box overlaps any other boxes in boxes. 8615 BoxPosition.prototype.overlapsAny = function (boxes) { 8616 for (var i = 0; i < boxes.length; i++) { 8617 if (this.overlaps(boxes[i])) { 8618 return true; 8619 } 8620 } 8621 return false; 8622 }; 8623 8624 // Check if this box is within another box. 8625 BoxPosition.prototype.within = function (container) { 8626 return this.top >= container.top && this.bottom <= container.bottom && this.left >= container.left && this.right <= container.right; 8627 }; 8628 8629 // Check if this box is entirely within the container or it is overlapping 8630 // on the edge opposite of the axis direction passed. For example, if "+x" is 8631 // passed and the box is overlapping on the left edge of the container, then 8632 // return true. 8633 BoxPosition.prototype.overlapsOppositeAxis = function (container, axis) { 8634 switch (axis) { 8635 case "+x": 8636 return this.left < container.left; 8637 case "-x": 8638 return this.right > container.right; 8639 case "+y": 8640 return this.top < container.top; 8641 case "-y": 8642 return this.bottom > container.bottom; 8643 } 8644 }; 8645 8646 // Find the percentage of the area that this box is overlapping with another 8647 // box. 8648 BoxPosition.prototype.intersectPercentage = function (b2) { 8649 var x = Math.max(0, Math.min(this.right, b2.right) - Math.max(this.left, b2.left)), 8650 y = Math.max(0, Math.min(this.bottom, b2.bottom) - Math.max(this.top, b2.top)), 8651 intersectArea = x * y; 8652 return intersectArea / (this.height * this.width); 8653 }; 8654 8655 // Convert the positions from this box to CSS compatible positions using 8656 // the reference container's positions. This has to be done because this 8657 // box's positions are in reference to the viewport origin, whereas, CSS 8658 // values are in referecne to their respective edges. 8659 BoxPosition.prototype.toCSSCompatValues = function (reference) { 8660 return { 8661 top: this.top - reference.top, 8662 bottom: reference.bottom - this.bottom, 8663 left: this.left - reference.left, 8664 right: reference.right - this.right, 8665 height: this.height, 8666 width: this.width 8667 }; 8668 }; 8669 8670 // Get an object that represents the box's position without anything extra. 8671 // Can pass a StyleBox, HTMLElement, or another BoxPositon. 8672 BoxPosition.getSimpleBoxPosition = function (obj) { 8673 var height = obj.div ? obj.div.offsetHeight : obj.tagName ? obj.offsetHeight : 0; 8674 var width = obj.div ? obj.div.offsetWidth : obj.tagName ? obj.offsetWidth : 0; 8675 var top = obj.div ? obj.div.offsetTop : obj.tagName ? obj.offsetTop : 0; 8676 8677 obj = obj.div ? obj.div.getBoundingClientRect() : obj.tagName ? obj.getBoundingClientRect() : obj; 8678 var ret = { 8679 left: obj.left, 8680 right: obj.right, 8681 top: obj.top || top, 8682 height: obj.height || height, 8683 bottom: obj.bottom || top + (obj.height || height), 8684 width: obj.width || width 8685 }; 8686 return ret; 8687 }; 8688 8689 // Move a StyleBox to its specified, or next best, position. The containerBox 8690 // is the box that contains the StyleBox, such as a div. boxPositions are 8691 // a list of other boxes that the styleBox can't overlap with. 8692 function moveBoxToLinePosition(window, styleBox, containerBox, boxPositions) { 8693 8694 // Find the best position for a cue box, b, on the video. The axis parameter 8695 // is a list of axis, the order of which, it will move the box along. For example: 8696 // Passing ["+x", "-x"] will move the box first along the x axis in the positive 8697 // direction. If it doesn't find a good position for it there it will then move 8698 // it along the x axis in the negative direction. 8699 function findBestPosition(b, axis) { 8700 var bestPosition, 8701 specifiedPosition = new BoxPosition(b), 8702 percentage = 1; // Highest possible so the first thing we get is better. 8703 8704 for (var i = 0; i < axis.length; i++) { 8705 while (b.overlapsOppositeAxis(containerBox, axis[i]) || b.within(containerBox) && b.overlapsAny(boxPositions)) { 8706 b.move(axis[i]); 8707 } 8708 // We found a spot where we aren't overlapping anything. This is our 8709 // best position. 8710 if (b.within(containerBox)) { 8711 return b; 8712 } 8713 var p = b.intersectPercentage(containerBox); 8714 // If we're outside the container box less then we were on our last try 8715 // then remember this position as the best position. 8716 if (percentage > p) { 8717 bestPosition = new BoxPosition(b); 8718 percentage = p; 8719 } 8720 // Reset the box position to the specified position. 8721 b = new BoxPosition(specifiedPosition); 8722 } 8723 return bestPosition || specifiedPosition; 8724 } 8725 8726 var boxPosition = new BoxPosition(styleBox), 8727 cue = styleBox.cue, 8728 linePos = computeLinePos(cue), 8729 axis = []; 8730 8731 // If we have a line number to align the cue to. 8732 if (cue.snapToLines) { 8733 var size; 8734 switch (cue.vertical) { 8735 case "": 8736 axis = ["+y", "-y"]; 8737 size = "height"; 8738 break; 8739 case "rl": 8740 axis = ["+x", "-x"]; 8741 size = "width"; 8742 break; 8743 case "lr": 8744 axis = ["-x", "+x"]; 8745 size = "width"; 8746 break; 8747 } 8748 8749 var step = boxPosition.lineHeight, 8750 position = step * Math.round(linePos), 8751 maxPosition = containerBox[size] + step, 8752 initialAxis = axis[0]; 8753 8754 // If the specified intial position is greater then the max position then 8755 // clamp the box to the amount of steps it would take for the box to 8756 // reach the max position. 8757 if (Math.abs(position) > maxPosition) { 8758 position = position < 0 ? -1 : 1; 8759 position *= Math.ceil(maxPosition / step) * step; 8760 } 8761 8762 // If computed line position returns negative then line numbers are 8763 // relative to the bottom of the video instead of the top. Therefore, we 8764 // need to increase our initial position by the length or width of the 8765 // video, depending on the writing direction, and reverse our axis directions. 8766 if (linePos < 0) { 8767 position += cue.vertical === "" ? containerBox.height : containerBox.width; 8768 axis = axis.reverse(); 8769 } 8770 8771 // Move the box to the specified position. This may not be its best 8772 // position. 8773 boxPosition.move(initialAxis, position); 8774 } else { 8775 // If we have a percentage line value for the cue. 8776 var calculatedPercentage = boxPosition.lineHeight / containerBox.height * 100; 8777 8778 switch (cue.lineAlign) { 8779 case "middle": 8780 linePos -= calculatedPercentage / 2; 8781 break; 8782 case "end": 8783 linePos -= calculatedPercentage; 8784 break; 8785 } 8786 8787 // Apply initial line position to the cue box. 8788 switch (cue.vertical) { 8789 case "": 8790 styleBox.applyStyles({ 8791 top: styleBox.formatStyle(linePos, "%") 8792 }); 8793 break; 8794 case "rl": 8795 styleBox.applyStyles({ 8796 left: styleBox.formatStyle(linePos, "%") 8797 }); 8798 break; 8799 case "lr": 8800 styleBox.applyStyles({ 8801 right: styleBox.formatStyle(linePos, "%") 8802 }); 8803 break; 8804 } 8805 8806 axis = ["+y", "-x", "+x", "-y"]; 8807 8808 // Get the box position again after we've applied the specified positioning 8809 // to it. 8810 boxPosition = new BoxPosition(styleBox); 8811 } 8812 8813 var bestPosition = findBestPosition(boxPosition, axis); 8814 styleBox.move(bestPosition.toCSSCompatValues(containerBox)); 8815 } 8816 8817 function WebVTT$1() {} 8818 // Nothing 8819 8820 8821 // Helper to allow strings to be decoded instead of the default binary utf8 data. 8822 WebVTT$1.StringDecoder = function () { 8823 return { 8824 decode: function decode(data) { 8825 if (!data) { 8826 return ""; 8827 } 8828 if (typeof data !== "string") { 8829 throw new Error("Error - expected string data."); 8830 } 8831 return decodeURIComponent(encodeURIComponent(data)); 8832 } 8833 }; 8834 }; 8835 8836 WebVTT$1.convertCueToDOMTree = function (window, cuetext) { 8837 if (!window || !cuetext) { 8838 return null; 8839 } 8840 return parseContent(window, cuetext); 8841 }; 8842 8843 var FONT_SIZE_PERCENT = 0.05; 8844 var FONT_STYLE = "sans-serif"; 8845 var CUE_BACKGROUND_PADDING = "1.5%"; 8846 8847 // Runs the processing model over the cues and regions passed to it. 8848 // @param overlay A block level element (usually a div) that the computed cues 8849 // and regions will be placed into. 8850 WebVTT$1.processCues = function (window, cues, overlay) { 8851 if (!window || !cues || !overlay) { 8852 return null; 8853 } 8854 8855 // Remove all previous children. 8856 while (overlay.firstChild) { 8857 overlay.removeChild(overlay.firstChild); 8858 } 8859 8860 var paddedOverlay = window.document.createElement("div"); 8861 paddedOverlay.style.position = "absolute"; 8862 paddedOverlay.style.left = "0"; 8863 paddedOverlay.style.right = "0"; 8864 paddedOverlay.style.top = "0"; 8865 paddedOverlay.style.bottom = "0"; 8866 paddedOverlay.style.margin = CUE_BACKGROUND_PADDING; 8867 overlay.appendChild(paddedOverlay); 8868 8869 // Determine if we need to compute the display states of the cues. This could 8870 // be the case if a cue's state has been changed since the last computation or 8871 // if it has not been computed yet. 8872 function shouldCompute(cues) { 8873 for (var i = 0; i < cues.length; i++) { 8874 if (cues[i].hasBeenReset || !cues[i].displayState) { 8875 return true; 8876 } 8877 } 8878 return false;
vendor: 16,058 bytes, lines 8879-9412
8879 } 8880 8881 // We don't need to recompute the cues' display states. Just reuse them. 8882 if (!shouldCompute(cues)) { 8883 for (var i = 0; i < cues.length; i++) { 8884 paddedOverlay.appendChild(cues[i].displayState); 8885 } 8886 return; 8887 } 8888 8889 var boxPositions = [], 8890 containerBox = BoxPosition.getSimpleBoxPosition(paddedOverlay), 8891 fontSize = Math.round(containerBox.height * FONT_SIZE_PERCENT * 100) / 100; 8892 var styleOptions = { 8893 font: fontSize + "px " + FONT_STYLE 8894 }; 8895 8896 (function () { 8897 var styleBox, cue; 8898 8899 for (var i = 0; i < cues.length; i++) { 8900 cue = cues[i]; 8901 8902 // Compute the intial position and styles of the cue div. 8903 styleBox = new CueStyleBox(window, cue, styleOptions); 8904 paddedOverlay.appendChild(styleBox.div); 8905 8906 // Move the cue div to it's correct line position. 8907 moveBoxToLinePosition(window, styleBox, containerBox, boxPositions); 8908 8909 // Remember the computed div so that we don't have to recompute it later 8910 // if we don't have too. 8911 cue.displayState = styleBox.div; 8912 8913 boxPositions.push(BoxPosition.getSimpleBoxPosition(styleBox)); 8914 } 8915 })(); 8916 }; 8917 8918 WebVTT$1.Parser = function (window, vttjs, decoder) { 8919 if (!decoder) { 8920 decoder = vttjs; 8921 vttjs = {}; 8922 } 8923 if (!vttjs) { 8924 vttjs = {}; 8925 } 8926 8927 this.window = window; 8928 this.vttjs = vttjs; 8929 this.state = "INITIAL"; 8930 this.buffer = ""; 8931 this.decoder = decoder || new TextDecoder("utf8"); 8932 this.regionList = []; 8933 }; 8934 8935 WebVTT$1.Parser.prototype = { 8936 // If the error is a ParsingError then report it to the consumer if 8937 // possible. If it's not a ParsingError then throw it like normal. 8938 reportOrThrowError: function reportOrThrowError(e) { 8939 if (e instanceof ParsingError) { 8940 this.onparsingerror && this.onparsingerror(e); 8941 } else { 8942 throw e; 8943 } 8944 }, 8945 parse: function parse(data) { 8946 var self = this; 8947 8948 // If there is no data then we won't decode it, but will just try to parse 8949 // whatever is in buffer already. This may occur in circumstances, for 8950 // example when flush() is called. 8951 if (data) { 8952 // Try to decode the data that we received. 8953 self.buffer += self.decoder.decode(data, { stream: true }); 8954 } 8955 8956 function collectNextLine() { 8957 var buffer = self.buffer; 8958 var pos = 0; 8959 while (pos < buffer.length && buffer[pos] !== '\r' && buffer[pos] !== '\n') { 8960 ++pos; 8961 } 8962 var line = buffer.substr(0, pos); 8963 // Advance the buffer early in case we fail below. 8964 if (buffer[pos] === '\r') { 8965 ++pos; 8966 } 8967 if (buffer[pos] === '\n') { 8968 ++pos; 8969 } 8970 self.buffer = buffer.substr(pos); 8971 return line; 8972 } 8973 8974 // 3.4 WebVTT region and WebVTT region settings syntax 8975 function parseRegion(input) { 8976 var settings = new Settings(); 8977 8978 parseOptions(input, function (k, v) { 8979 switch (k) { 8980 case "id": 8981 settings.set(k, v); 8982 break; 8983 case "width": 8984 settings.percent(k, v); 8985 break; 8986 case "lines": 8987 settings.integer(k, v); 8988 break; 8989 case "regionanchor": 8990 case "viewportanchor": 8991 var xy = v.split(','); 8992 if (xy.length !== 2) { 8993 break; 8994 } 8995 // We have to make sure both x and y parse, so use a temporary 8996 // settings object here. 8997 var anchor = new Settings(); 8998 anchor.percent("x", xy[0]); 8999 anchor.percent("y", xy[1]); 9000 if (!anchor.has("x") || !anchor.has("y")) { 9001 break; 9002 } 9003 settings.set(k + "X", anchor.get("x")); 9004 settings.set(k + "Y", anchor.get("y")); 9005 break; 9006 case "scroll": 9007 settings.alt(k, v, ["up"]); 9008 break; 9009 } 9010 }, /=/, /\s/); 9011 9012 // Create the region, using default values for any values that were not 9013 // specified. 9014 if (settings.has("id")) { 9015 var region = new (self.vttjs.VTTRegion || self.window.VTTRegion)(); 9016 region.width = settings.get("width", 100); 9017 region.lines = settings.get("lines", 3); 9018 region.regionAnchorX = settings.get("regionanchorX", 0); 9019 region.regionAnchorY = settings.get("regionanchorY", 100); 9020 region.viewportAnchorX = settings.get("viewportanchorX", 0); 9021 region.viewportAnchorY = settings.get("viewportanchorY", 100); 9022 region.scroll = settings.get("scroll", ""); 9023 // Register the region. 9024 self.onregion && self.onregion(region); 9025 // Remember the VTTRegion for later in case we parse any VTTCues that 9026 // reference it. 9027 self.regionList.push({ 9028 id: settings.get("id"), 9029 region: region 9030 }); 9031 } 9032 } 9033 9034 // draft-pantos-http-live-streaming-20 9035 // https://tools.ietf.org/html/draft-pantos-http-live-streaming-20#section-3.5 9036 // 3.5 WebVTT 9037 function parseTimestampMap(input) { 9038 var settings = new Settings(); 9039 9040 parseOptions(input, function (k, v) { 9041 switch (k) { 9042 case "MPEGT": 9043 settings.integer(k + 'S', v); 9044 break; 9045 case "LOCA": 9046 settings.set(k + 'L', parseTimeStamp(v)); 9047 break; 9048 } 9049 }, /[^\d]:/, /,/); 9050 9051 self.ontimestampmap && self.ontimestampmap({ 9052 "MPEGTS": settings.get("MPEGTS"), 9053 "LOCAL": settings.get("LOCAL") 9054 }); 9055 } 9056 9057 // 3.2 WebVTT metadata header syntax 9058 function parseHeader(input) { 9059 if (input.match(/X-TIMESTAMP-MAP/)) { 9060 // This line contains HLS X-TIMESTAMP-MAP metadata 9061 parseOptions(input, function (k, v) { 9062 switch (k) { 9063 case "X-TIMESTAMP-MAP": 9064 parseTimestampMap(v); 9065 break; 9066 } 9067 }, /=/); 9068 } else { 9069 parseOptions(input, function (k, v) { 9070 switch (k) { 9071 case "Region": 9072 // 3.3 WebVTT region metadata header syntax 9073 parseRegion(v); 9074 break; 9075 } 9076 }, /:/); 9077 } 9078 } 9079 9080 // 5.1 WebVTT file parsing. 9081 try { 9082 var line; 9083 if (self.state === "INITIAL") { 9084 // We can't start parsing until we have the first line. 9085 if (!/\r\n|\n/.test(self.buffer)) { 9086 return this; 9087 } 9088 9089 line = collectNextLine(); 9090 9091 var m = line.match(/^WEBVTT([ \t].*)?$/); 9092 if (!m || !m[0]) { 9093 throw new ParsingError(ParsingError.Errors.BadSignature); 9094 } 9095 9096 self.state = "HEADER"; 9097 } 9098 9099 var alreadyCollectedLine = false; 9100 while (self.buffer) { 9101 // We can't parse a line until we have the full line. 9102 if (!/\r\n|\n/.test(self.buffer)) { 9103 return this; 9104 } 9105 9106 if (!alreadyCollectedLine) { 9107 line = collectNextLine(); 9108 } else { 9109 alreadyCollectedLine = false; 9110 } 9111 9112 switch (self.state) { 9113 case "HEADER": 9114 // 13-18 - Allow a header (metadata) under the WEBVTT line. 9115 if (/:/.test(line)) { 9116 parseHeader(line); 9117 } else if (!line) { 9118 // An empty line terminates the header and starts the body (cues). 9119 self.state = "ID"; 9120 } 9121 continue; 9122 case "NOTE": 9123 // Ignore NOTE blocks. 9124 if (!line) { 9125 self.state = "ID"; 9126 } 9127 continue; 9128 case "ID": 9129 // Check for the start of NOTE blocks. 9130 if (/^NOTE($|[ \t])/.test(line)) { 9131 self.state = "NOTE"; 9132 break; 9133 } 9134 // 19-29 - Allow any number of line terminators, then initialize new cue values. 9135 if (!line) { 9136 continue; 9137 } 9138 self.cue = new (self.vttjs.VTTCue || self.window.VTTCue)(0, 0, ""); 9139 self.state = "CUE"; 9140 // 30-39 - Check if self line contains an optional identifier or timing data. 9141 if (line.indexOf("-->") === -1) { 9142 self.cue.id = line; 9143 continue; 9144 } 9145 // Process line as start of a cue. 9146 /*falls through*/ 9147 case "CUE": 9148 // 40 - Collect cue timings and settings. 9149 try { 9150 parseCue(line, self.cue, self.regionList); 9151 } catch (e) { 9152 self.reportOrThrowError(e); 9153 // In case of an error ignore rest of the cue. 9154 self.cue = null; 9155 self.state = "BADCUE"; 9156 continue; 9157 } 9158 self.state = "CUETEXT"; 9159 continue; 9160 case "CUETEXT": 9161 var hasSubstring = line.indexOf("-->") !== -1; 9162 // 34 - If we have an empty line then report the cue. 9163 // 35 - If we have the special substring '-->' then report the cue, 9164 // but do not collect the line as we need to process the current 9165 // one as a new cue. 9166 if (!line || hasSubstring && (alreadyCollectedLine = true)) { 9167 // We are done parsing self cue. 9168 self.oncue && self.oncue(self.cue); 9169 self.cue = null; 9170 self.state = "ID"; 9171 continue; 9172 } 9173 if (self.cue.text) { 9174 self.cue.text += "\n"; 9175 } 9176 self.cue.text += line; 9177 continue; 9178 case "BADCUE": 9179 // BADCUE 9180 // 54-62 - Collect and discard the remaining cue. 9181 if (!line) { 9182 self.state = "ID"; 9183 } 9184 continue; 9185 } 9186 } 9187 } catch (e) { 9188 self.reportOrThrowError(e); 9189 9190 // If we are currently parsing a cue, report what we have. 9191 if (self.state === "CUETEXT" && self.cue && self.oncue) { 9192 self.oncue(self.cue); 9193 } 9194 self.cue = null; 9195 // Enter BADWEBVTT state if header was not parsed correctly otherwise 9196 // another exception occurred so enter BADCUE state. 9197 self.state = self.state === "INITIAL" ? "BADWEBVTT" : "BADCUE"; 9198 } 9199 return this; 9200 }, 9201 flush: function flush() { 9202 var self = this; 9203 try { 9204 // Finish decoding the stream. 9205 self.buffer += self.decoder.decode(); 9206 // Synthesize the end of the current cue or region. 9207 if (self.cue || self.state === "HEADER") { 9208 self.buffer += "\n\n"; 9209 self.parse(); 9210 } 9211 // If we've flushed, parsed, and we're still on the INITIAL state then 9212 // that means we don't have enough of the stream to parse the first 9213 // line. 9214 if (self.state === "INITIAL") { 9215 throw new ParsingError(ParsingError.Errors.BadSignature); 9216 } 9217 } catch (e) { 9218 self.reportOrThrowError(e); 9219 } 9220 self.onflush && self.onflush(); 9221 return this; 9222 } 9223 }; 9224 9225 var vtt = WebVTT$1; 9226 9227 var vtt$1 = /*#__PURE__*/Object.freeze({ 9228 default: vtt, 9229 __moduleExports: vtt 9230 }); 9231 9232 /** 9233 * Copyright 2013 vtt.js Contributors 9234 * 9235 * Licensed under the Apache License, Version 2.0 (the "License"); 9236 * you may not use this file except in compliance with the License. 9237 * You may obtain a copy of the License at 9238 * 9239 * http://www.apache.org/licenses/LICENSE-2.0 9240 * 9241 * Unless required by applicable law or agreed to in writing, software 9242 * distributed under the License is distributed on an "AS IS" BASIS, 9243 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9244 * See the License for the specific language governing permissions and 9245 * limitations under the License. 9246 */ 9247 9248 var autoKeyword = "auto"; 9249 var directionSetting = { 9250 "": 1, 9251 "lr": 1, 9252 "rl": 1 9253 }; 9254 var alignSetting = { 9255 "start": 1, 9256 "middle": 1, 9257 "end": 1, 9258 "left": 1, 9259 "right": 1 9260 }; 9261 9262 function findDirectionSetting(value) { 9263 if (typeof value !== "string") { 9264 return false; 9265 } 9266 var dir = directionSetting[value.toLowerCase()]; 9267 return dir ? value.toLowerCase() : false; 9268 } 9269 9270 function findAlignSetting(value) { 9271 if (typeof value !== "string") { 9272 return false; 9273 } 9274 var align = alignSetting[value.toLowerCase()]; 9275 return align ? value.toLowerCase() : false; 9276 } 9277 9278 function VTTCue(startTime, endTime, text) { 9279 /** 9280 * Shim implementation specific properties. These properties are not in 9281 * the spec. 9282 */ 9283 9284 // Lets us know when the VTTCue's data has changed in such a way that we need 9285 // to recompute its display state. This lets us compute its display state 9286 // lazily. 9287 this.hasBeenReset = false; 9288 9289 /** 9290 * VTTCue and TextTrackCue properties 9291 * http://dev.w3.org/html5/webvtt/#vttcue-interface 9292 */ 9293 9294 var _id = ""; 9295 var _pauseOnExit = false; 9296 var _startTime = startTime; 9297 var _endTime = endTime; 9298 var _text = text; 9299 var _region = null; 9300 var _vertical = ""; 9301 var _snapToLines = true; 9302 var _line = "auto"; 9303 var _lineAlign = "start"; 9304 var _position = 50; 9305 var _positionAlign = "middle"; 9306 var _size = 50; 9307 var _align = "middle"; 9308 9309 Object.defineProperties(this, { 9310 "id": { 9311 enumerable: true, 9312 get: function get() { 9313 return _id; 9314 }, 9315 set: function set(value) { 9316 _id = "" + value; 9317 } 9318 }, 9319 9320 "pauseOnExit": { 9321 enumerable: true, 9322 get: function get() { 9323 return _pauseOnExit; 9324 }, 9325 set: function set(value) { 9326 _pauseOnExit = !!value; 9327 } 9328 }, 9329 9330 "startTime": { 9331 enumerable: true, 9332 get: function get() { 9333 return _startTime; 9334 }, 9335 set: function set(value) { 9336 if (typeof value !== "number") { 9337 throw new TypeError("Start time must be set to a number."); 9338 } 9339 _startTime = value; 9340 this.hasBeenReset = true; 9341 } 9342 }, 9343 9344 "endTime": { 9345 enumerable: true, 9346 get: function get() { 9347 return _endTime; 9348 }, 9349 set: function set(value) { 9350 if (typeof value !== "number") { 9351 throw new TypeError("End time must be set to a number."); 9352 } 9353 _endTime = value; 9354 this.hasBeenReset = true; 9355 } 9356 }, 9357 9358 "text": { 9359 enumerable: true, 9360 get: function get() { 9361 return _text; 9362 }, 9363 set: function set(value) { 9364 _text = "" + value; 9365 this.hasBeenReset = true; 9366 } 9367 }, 9368 9369 "region": { 9370 enumerable: true, 9371 get: function get() { 9372 return _region; 9373 }, 9374 set: function set(value) { 9375 _region = value; 9376 this.hasBeenReset = true; 9377 } 9378 }, 9379 9380 "vertical": { 9381 enumerable: true, 9382 get: function get() { 9383 return _vertical; 9384 }, 9385 set: function set(value) { 9386 var setting = findDirectionSetting(value); 9387 // Have to check for false because the setting an be an empty string. 9388 if (setting === false) { 9389 throw new SyntaxError("An invalid or illegal string was specified."); 9390 } 9391 _vertical = setting; 9392 this.hasBeenReset = true; 9393 } 9394 }, 9395 9396 "snapToLines": { 9397 enumerable: true, 9398 get: function get() { 9399 return _snapToLines; 9400 }, 9401 set: function set(value) { 9402 _snapToLines = !!value; 9403 this.hasBeenReset = true; 9404 } 9405 }, 9406 9407 "line": { 9408 enumerable: true, 9409 get: function get() { 9410 return _line; 9411 }, 9412 set: function set(value) {
vendor: 4,519 bytes, lines 9413-9580
9413 if (typeof value !== "number" && value !== autoKeyword) { 9414 throw new SyntaxError("An invalid number or illegal string was specified."); 9415 } 9416 _line = value; 9417 this.hasBeenReset = true; 9418 } 9419 }, 9420 9421 "lineAlign": { 9422 enumerable: true, 9423 get: function get() { 9424 return _lineAlign; 9425 }, 9426 set: function set(value) { 9427 var setting = findAlignSetting(value); 9428 if (!setting) { 9429 throw new SyntaxError("An invalid or illegal string was specified."); 9430 } 9431 _lineAlign = setting; 9432 this.hasBeenReset = true; 9433 } 9434 }, 9435 9436 "position": { 9437 enumerable: true, 9438 get: function get() { 9439 return _position; 9440 }, 9441 set: function set(value) { 9442 if (value < 0 || value > 100) { 9443 throw new Error("Position must be between 0 and 100."); 9444 } 9445 _position = value; 9446 this.hasBeenReset = true; 9447 } 9448 }, 9449 9450 "positionAlign": { 9451 enumerable: true, 9452 get: function get() { 9453 return _positionAlign; 9454 }, 9455 set: function set(value) { 9456 var setting = findAlignSetting(value); 9457 if (!setting) { 9458 throw new SyntaxError("An invalid or illegal string was specified."); 9459 } 9460 _positionAlign = setting; 9461 this.hasBeenReset = true; 9462 } 9463 }, 9464 9465 "size": { 9466 enumerable: true, 9467 get: function get() { 9468 return _size; 9469 }, 9470 set: function set(value) { 9471 if (value < 0 || value > 100) { 9472 throw new Error("Size must be between 0 and 100."); 9473 } 9474 _size = value; 9475 this.hasBeenReset = true; 9476 } 9477 }, 9478 9479 "align": { 9480 enumerable: true, 9481 get: function get() { 9482 return _align; 9483 }, 9484 set: function set(value) { 9485 var setting = findAlignSetting(value); 9486 if (!setting) { 9487 throw new SyntaxError("An invalid or illegal string was specified."); 9488 } 9489 _align = setting; 9490 this.hasBeenReset = true; 9491 } 9492 } 9493 }); 9494 9495 /** 9496 * Other <track> spec defined properties 9497 */ 9498 9499 // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-video-element.html#text-track-cue-display-state 9500 this.displayState = undefined; 9501 } 9502 9503 /** 9504 * VTTCue methods 9505 */ 9506 9507 VTTCue.prototype.getCueAsHTML = function () { 9508 // Assume WebVTT.convertCueToDOMTree is on the global. 9509 return WebVTT.convertCueToDOMTree(window, this.text); 9510 }; 9511 9512 var vttcue = VTTCue; 9513 9514 var vttcue$1 = /*#__PURE__*/Object.freeze({ 9515 default: vttcue, 9516 __moduleExports: vttcue 9517 }); 9518 9519 /** 9520 * Copyright 2013 vtt.js Contributors 9521 * 9522 * Licensed under the Apache License, Version 2.0 (the "License"); 9523 * you may not use this file except in compliance with the License. 9524 * You may obtain a copy of the License at 9525 * 9526 * http://www.apache.org/licenses/LICENSE-2.0 9527 * 9528 * Unless required by applicable law or agreed to in writing, software 9529 * distributed under the License is distributed on an "AS IS" BASIS, 9530 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9531 * See the License for the specific language governing permissions and 9532 * limitations under the License. 9533 */ 9534 9535 var scrollSetting = { 9536 "": true, 9537 "up": true 9538 }; 9539 9540 function findScrollSetting(value) { 9541 if (typeof value !== "string") { 9542 return false; 9543 } 9544 var scroll = scrollSetting[value.toLowerCase()]; 9545 return scroll ? value.toLowerCase() : false; 9546 } 9547 9548 function isValidPercentValue(value) { 9549 return typeof value === "number" && value >= 0 && value <= 100; 9550 } 9551 9552 // VTTRegion shim http://dev.w3.org/html5/webvtt/#vttregion-interface 9553 function VTTRegion() { 9554 var _width = 100; 9555 var _lines = 3; 9556 var _regionAnchorX = 0; 9557 var _regionAnchorY = 100; 9558 var _viewportAnchorX = 0; 9559 var _viewportAnchorY = 100; 9560 var _scroll = ""; 9561 9562 Object.defineProperties(this, { 9563 "width": { 9564 enumerable: true, 9565 get: function get() { 9566 return _width; 9567 }, 9568 set: function set(value) { 9569 if (!isValidPercentValue(value)) { 9570 throw new Error("Width must be between 0 and 100."); 9571 } 9572 _width = value; 9573 } 9574 }, 9575 "lines": { 9576 enumerable: true, 9577 get: function get() { 9578 return _lines; 9579 }, 9580 set: function set(value) {
9581 if (typeof value !== "number") { 9582 throw new TypeError("Lines must be set to a number."); 9583 } 9584 _lines = value; 9585 } 9586 }, 9587 "regionAnchorY": { 9588 enumerable: true, 9589 get: function get() { 9590 return _regionAnchorY; 9591 }, 9592 set: function set(value) { 9593 if (!isValidPercentValue(value)) { 9594 throw new Error("RegionAnchorX must be between 0 and 100."); 9595 } 9596 _regionAnchorY = value; 9597 } 9598 }, 9599 "regionAnchorX": { 9600 enumerable: true, 9601 get: function get() { 9602 return _regionAnchorX; 9603 }, 9604 set: function set(value) { 9605 if (!isValidPercentValue(value)) { 9606 throw new Error("RegionAnchorY must be between 0 and 100."); 9607 } 9608 _regionAnchorX = value; 9609 } 9610 }, 9611 "viewportAnchorY": { 9612 enumerable: true, 9613 get: function get() { 9614 return _viewportAnchorY; 9615 }, 9616 set: function set(value) { 9617 if (!isValidPercentValue(value)) { 9618 throw new Error("ViewportAnchorY must be between 0 and 100."); 9619 } 9620 _viewportAnchorY = value; 9621 } 9622 }, 9623 "viewportAnchorX": { 9624 enumerable: true, 9625 get: function get() { 9626 return _viewportAnchorX; 9627 }, 9628 set: function set(value) { 9629 if (!isValidPercentValue(value)) { 9630 throw new Error("ViewportAnchorX must be between 0 and 100."); 9631 } 9632 _viewportAnchorX = value; 9633 } 9634 }, 9635 "scroll": { 9636 enumerable: true, 9637 get: function get() { 9638 return _scroll; 9639 }, 9640 set: function set(value) { 9641 var setting = findScrollSetting(value); 9642 // Have to check for false as an empty string is a legal value. 9643 if (setting === false) { 9644 throw new SyntaxError("An invalid or illegal string was specified."); 9645 } 9646 _scroll = setting; 9647 } 9648 } 9649 }); 9650 } 9651 9652 var vttregion = VTTRegion; 9653 9654 var vttregion$1 = /*#__PURE__*/Object.freeze({ 9655 default: vttregion, 9656 __moduleExports: vttregion 9657 }); 9658 9659 var require$$0 = ( vtt$1 && vtt ) || vtt$1; 9660 9661 var require$$1 = ( vttcue$1 && vttcue ) || vttcue$1; 9662 9663 var require$$2 = ( vttregion$1 && vttregion ) || vttregion$1; 9664 9665 var browserIndex = createCommonjsModule(function (module) { 9666 /** 9667 * Copyright 2013 vtt.js Contributors 9668 * 9669 * Licensed under the Apache License, Version 2.0 (the "License"); 9670 * you may not use this file except in compliance with the License. 9671 * You may obtain a copy of the License at 9672 * 9673 * http://www.apache.org/licenses/LICENSE-2.0 9674 * 9675 * Unless required by applicable law or agreed to in writing, software 9676 * distributed under the License is distributed on an "AS IS" BASIS, 9677 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9678 * See the License for the specific language governing permissions and 9679 * limitations under the License. 9680 */ 9681 9682 // Default exports for Node. Export the extended versions of VTTCue and 9683 // VTTRegion in Node since we likely want the capability to convert back and 9684 // forth between JSON. If we don't then it's not that big of a deal since we're 9685 // off browser. 9686 9687 9688 var vttjs = module.exports = { 9689 WebVTT: require$$0, 9690 VTTCue: require$$1, 9691 VTTRegion: require$$2 9692 }; 9693 9694 window_1.vttjs = vttjs; 9695 window_1.WebVTT = vttjs.WebVTT; 9696 9697 var cueShim = vttjs.VTTCue; 9698 var regionShim = vttjs.VTTRegion; 9699 var nativeVTTCue = window_1.VTTCue; 9700 var nativeVTTRegion = window_1.VTTRegion; 9701 9702 vttjs.shim = function () { 9703 window_1.VTTCue = cueShim; 9704 window_1.VTTRegion = regionShim; 9705 }; 9706 9707 vttjs.restore = function () { 9708 window_1.VTTCue = nativeVTTCue; 9709 window_1.VTTRegion = nativeVTTRegion; 9710 }; 9711 9712 if (!window_1.VTTCue) { 9713 vttjs.shim(); 9714 } 9715 }); 9716 var browserIndex_1 = browserIndex.WebVTT; 9717 var browserIndex_2 = browserIndex.VTTCue; 9718 var browserIndex_3 = browserIndex.VTTRegion; 9719 9720 /** 9721 * @file tech.js 9722 */ 9723 9724 /** 9725 * An Object containing a structure like: `{src: 'url', type: 'mimetype'}` or string 9726 * that just contains the src url alone. 9727 * * `var SourceObject = {src: 'http://ex.com/video.mp4', type: 'video/mp4'};` 9728 * `var SourceString = 'http://example.com/some-video.mp4';` 9729 * 9730 * @typedef {Object|string} Tech~SourceObject 9731 * 9732 * @property {string} src 9733 * The url to the source 9734 * 9735 * @property {string} type 9736 * The mime type of the source 9737 */ 9738 9739 /** 9740 * A function used by {@link Tech} to create a new {@link TextTrack}. 9741 * 9742 * @private 9743 * 9744 * @param {Tech} self 9745 * An instance of the Tech class. 9746 * 9747 * @param {string} kind 9748 * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata) 9749 * 9750 * @param {string} [label] 9751 * Label to identify the text track 9752 * 9753 * @param {string} [language] 9754 * Two letter language abbreviation 9755 * 9756 * @param {Object} [options={}] 9757 * An object with additional text track options 9758 * 9759 * @return {TextTrack} 9760 * The text track that was created. 9761 */ 9762 function createTrackHelper(self, kind, label, language) { 9763 var options = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : {}; 9764 9765 var tracks = self.textTracks(); 9766 9767 options.kind = kind; 9768 9769 if (label) { 9770 options.label = label; 9771 } 9772 if (language) { 9773 options.language = language; 9774 } 9775 options.tech = self; 9776 9777 var track = new ALL.text.TrackClass(options); 9778 9779 tracks.addTrack(track); 9780 9781 return track; 9782 } 9783 9784 /** 9785 * This is the base class for media playback technology controllers, such as 9786 * {@link Flash} and {@link HTML5} 9787 * 9788 * @extends Component 9789 */ 9790 9791 var Tech = function (_Component) { 9792 inherits(Tech, _Component); 9793 9794 /** 9795 * Create an instance of this Tech. 9796 * 9797 * @param {Object} [options] 9798 * The key/value store of player options. 9799 * 9800 * @param {Component~ReadyCallback} ready 9801 * Callback function to call when the `HTML5` Tech is ready. 9802 */ 9803 function Tech() { 9804 var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 9805 var ready = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : function () {}; 9806 classCallCheck(this, Tech); 9807 9808 // we don't want the tech to report user activity automatically.
vendor: 304,903 bytes, lines 9809-20500
9809 // This is done manually in addControlsListeners 9810 options.reportTouchActivity = false; 9811 9812 // keep track of whether the current source has played at all to 9813 // implement a very limited played() 9814 var _this = possibleConstructorReturn(this, _Component.call(this, null, options, ready)); 9815 9816 _this.hasStarted_ = false; 9817 _this.on('playing', function () { 9818 this.hasStarted_ = true; 9819 }); 9820 _this.on('loadstart', function () { 9821 this.hasStarted_ = false; 9822 }); 9823 9824 ALL.names.forEach(function (name) { 9825 var props = ALL[name]; 9826 9827 if (options && options[props.getterName]) { 9828 _this[props.privateName] = options[props.getterName]; 9829 } 9830 }); 9831 9832 // Manually track progress in cases where the browser/flash player doesn't report it. 9833 if (!_this.featuresProgressEvents) { 9834 _this.manualProgressOn(); 9835 } 9836 9837 // Manually track timeupdates in cases where the browser/flash player doesn't report it. 9838 if (!_this.featuresTimeupdateEvents) { 9839 _this.manualTimeUpdatesOn(); 9840 } 9841 9842 ['Text', 'Audio', 'Video'].forEach(function (track) { 9843 if (options['native' + track + 'Tracks'] === false) { 9844 _this['featuresNative' + track + 'Tracks'] = false; 9845 } 9846 }); 9847 9848 if (options.nativeCaptions === false || options.nativeTextTracks === false) { 9849 _this.featuresNativeTextTracks = false; 9850 } else if (options.nativeCaptions === true || options.nativeTextTracks === true) { 9851 _this.featuresNativeTextTracks = true; 9852 } 9853 9854 if (!_this.featuresNativeTextTracks) { 9855 _this.emulateTextTracks(); 9856 } 9857 9858 _this.autoRemoteTextTracks_ = new ALL.text.ListClass(); 9859 9860 _this.initTrackListeners(); 9861 9862 // Turn on component tap events only if not using native controls 9863 if (!options.nativeControlsForTouch) { 9864 _this.emitTapEvents(); 9865 } 9866 9867 if (_this.constructor) { 9868 _this.name_ = _this.constructor.name || 'Unknown Tech'; 9869 } 9870 return _this; 9871 } 9872 9873 /** 9874 * A special function to trigger source set in a way that will allow player 9875 * to re-trigger if the player or tech are not ready yet. 9876 * 9877 * @fires Tech#sourceset 9878 * @param {string} src The source string at the time of the source changing. 9879 */ 9880 9881 9882 Tech.prototype.triggerSourceset = function triggerSourceset(src) { 9883 var _this2 = this; 9884 9885 if (!this.isReady_) { 9886 // on initial ready we have to trigger source set 9887 // 1ms after ready so that player can watch for it. 9888 this.one('ready', function () { 9889 return _this2.setTimeout(function () { 9890 return _this2.triggerSourceset(src); 9891 }, 1); 9892 }); 9893 } 9894 9895 /** 9896 * Fired when the source is set on the tech causing the media element 9897 * to reload. 9898 * 9899 * @see {@link Player#event:sourceset} 9900 * @event Tech#sourceset 9901 * @type {EventTarget~Event} 9902 */ 9903 this.trigger({ 9904 src: src, 9905 type: 'sourceset' 9906 }); 9907 }; 9908 9909 /* Fallbacks for unsupported event types 9910 ================================================================================ */ 9911 9912 /** 9913 * Polyfill the `progress` event for browsers that don't support it natively. 9914 * 9915 * @see {@link Tech#trackProgress} 9916 */ 9917 9918 9919 Tech.prototype.manualProgressOn = function manualProgressOn() { 9920 this.on('durationchange', this.onDurationChange); 9921 9922 this.manualProgress = true; 9923 9924 // Trigger progress watching when a source begins loading 9925 this.one('ready', this.trackProgress); 9926 }; 9927 9928 /** 9929 * Turn off the polyfill for `progress` events that was created in 9930 * {@link Tech#manualProgressOn} 9931 */ 9932 9933 9934 Tech.prototype.manualProgressOff = function manualProgressOff() { 9935 this.manualProgress = false; 9936 this.stopTrackingProgress(); 9937 9938 this.off('durationchange', this.onDurationChange); 9939 }; 9940 9941 /** 9942 * This is used to trigger a `progress` event when the buffered percent changes. It 9943 * sets an interval function that will be called every 500 milliseconds to check if the 9944 * buffer end percent has changed. 9945 * 9946 * > This function is called by {@link Tech#manualProgressOn} 9947 * 9948 * @param {EventTarget~Event} event 9949 * The `ready` event that caused this to run. 9950 * 9951 * @listens Tech#ready 9952 * @fires Tech#progress 9953 */ 9954 9955 9956 Tech.prototype.trackProgress = function trackProgress(event) { 9957 this.stopTrackingProgress(); 9958 this.progressInterval = this.setInterval(bind(this, function () { 9959 // Don't trigger unless buffered amount is greater than last time 9960 9961 var numBufferedPercent = this.bufferedPercent(); 9962 9963 if (this.bufferedPercent_ !== numBufferedPercent) { 9964 /** 9965 * See {@link Player#progress} 9966 * 9967 * @event Tech#progress 9968 * @type {EventTarget~Event} 9969 */ 9970 this.trigger('progress'); 9971 } 9972 9973 this.bufferedPercent_ = numBufferedPercent; 9974 9975 if (numBufferedPercent === 1) { 9976 this.stopTrackingProgress(); 9977 } 9978 }), 500); 9979 }; 9980 9981 /** 9982 * Update our internal duration on a `durationchange` event by calling 9983 * {@link Tech#duration}. 9984 * 9985 * @param {EventTarget~Event} event 9986 * The `durationchange` event that caused this to run. 9987 * 9988 * @listens Tech#durationchange 9989 */ 9990 9991 9992 Tech.prototype.onDurationChange = function onDurationChange(event) { 9993 this.duration_ = this.duration(); 9994 }; 9995 9996 /** 9997 * Get and create a `TimeRange` object for buffering. 9998 * 9999 * @return {TimeRange} 10000 * The time range object that was created. 10001 */ 10002 10003 10004 Tech.prototype.buffered = function buffered() { 10005 return createTimeRanges(0, 0); 10006 }; 10007 10008 /** 10009 * Get the percentage of the current video that is currently buffered. 10010 * 10011 * @return {number} 10012 * A number from 0 to 1 that represents the decimal percentage of the 10013 * video that is buffered. 10014 * 10015 */ 10016 10017 10018 Tech.prototype.bufferedPercent = function bufferedPercent$$1() { 10019 return bufferedPercent(this.buffered(), this.duration_); 10020 }; 10021 10022 /** 10023 * Turn off the polyfill for `progress` events that was created in 10024 * {@link Tech#manualProgressOn} 10025 * Stop manually tracking progress events by clearing the interval that was set in 10026 * {@link Tech#trackProgress}. 10027 */ 10028 10029 10030 Tech.prototype.stopTrackingProgress = function stopTrackingProgress() { 10031 this.clearInterval(this.progressInterval); 10032 }; 10033 10034 /** 10035 * Polyfill the `timeupdate` event for browsers that don't support it. 10036 * 10037 * @see {@link Tech#trackCurrentTime} 10038 */ 10039 10040 10041 Tech.prototype.manualTimeUpdatesOn = function manualTimeUpdatesOn() { 10042 this.manualTimeUpdates = true; 10043 10044 this.on('play', this.trackCurrentTime); 10045 this.on('pause', this.stopTrackingCurrentTime); 10046 }; 10047 10048 /** 10049 * Turn off the polyfill for `timeupdate` events that was created in 10050 * {@link Tech#manualTimeUpdatesOn} 10051 */ 10052 10053 10054 Tech.prototype.manualTimeUpdatesOff = function manualTimeUpdatesOff() { 10055 this.manualTimeUpdates = false; 10056 this.stopTrackingCurrentTime(); 10057 this.off('play', this.trackCurrentTime); 10058 this.off('pause', this.stopTrackingCurrentTime); 10059 }; 10060 10061 /** 10062 * Sets up an interval function to track current time and trigger `timeupdate` every 10063 * 250 milliseconds. 10064 * 10065 * @listens Tech#play 10066 * @triggers Tech#timeupdate 10067 */ 10068 10069 10070 Tech.prototype.trackCurrentTime = function trackCurrentTime() { 10071 if (this.currentTimeInterval) { 10072 this.stopTrackingCurrentTime(); 10073 } 10074 this.currentTimeInterval = this.setInterval(function () { 10075 /** 10076 * Triggered at an interval of 250ms to indicated that time is passing in the video. 10077 * 10078 * @event Tech#timeupdate 10079 * @type {EventTarget~Event} 10080 */ 10081 this.trigger({ type: 'timeupdate', target: this, manuallyTriggered: true }); 10082 10083 // 42 = 24 fps // 250 is what Webkit uses // FF uses 15 10084 }, 250); 10085 }; 10086 10087 /** 10088 * Stop the interval function created in {@link Tech#trackCurrentTime} so that the 10089 * `timeupdate` event is no longer triggered. 10090 * 10091 * @listens {Tech#pause} 10092 */ 10093 10094 10095 Tech.prototype.stopTrackingCurrentTime = function stopTrackingCurrentTime() { 10096 this.clearInterval(this.currentTimeInterval); 10097 10098 // #1002 - if the video ends right before the next timeupdate would happen, 10099 // the progress bar won't make it all the way to the end 10100 this.trigger({ type: 'timeupdate', target: this, manuallyTriggered: true }); 10101 }; 10102 10103 /** 10104 * Turn off all event polyfills, clear the `Tech`s {@link AudioTrackList}, 10105 * {@link VideoTrackList}, and {@link TextTrackList}, and dispose of this Tech. 10106 * 10107 * @fires Component#dispose 10108 */ 10109 10110 10111 Tech.prototype.dispose = function dispose() { 10112 10113 // clear out all tracks because we can't reuse them between techs 10114 this.clearTracks(NORMAL.names); 10115 10116 // Turn off any manual progress or timeupdate tracking 10117 if (this.manualProgress) { 10118 this.manualProgressOff(); 10119 } 10120 10121 if (this.manualTimeUpdates) { 10122 this.manualTimeUpdatesOff(); 10123 } 10124 10125 _Component.prototype.dispose.call(this); 10126 }; 10127 10128 /** 10129 * Clear out a single `TrackList` or an array of `TrackLists` given their names. 10130 * 10131 * > Note: Techs without source handlers should call this between sources for `video` 10132 * & `audio` tracks. You don't want to use them between tracks! 10133 * 10134 * @param {string[]|string} types 10135 * TrackList names to clear, valid names are `video`, `audio`, and 10136 * `text`. 10137 */ 10138 10139 10140 Tech.prototype.clearTracks = function clearTracks(types) { 10141 var _this3 = this; 10142 10143 types = [].concat(types); 10144 // clear out all tracks because we can't reuse them between techs 10145 types.forEach(function (type) { 10146 var list = _this3[type + 'Tracks']() || []; 10147 var i = list.length; 10148 10149 while (i--) { 10150 var track = list[i]; 10151 10152 if (type === 'text') { 10153 _this3.removeRemoteTextTrack(track); 10154 } 10155 list.removeTrack(track); 10156 } 10157 }); 10158 }; 10159 10160 /** 10161 * Remove any TextTracks added via addRemoteTextTrack that are 10162 * flagged for automatic garbage collection 10163 */ 10164 10165 10166 Tech.prototype.cleanupAutoTextTracks = function cleanupAutoTextTracks() { 10167 var list = this.autoRemoteTextTracks_ || []; 10168 var i = list.length; 10169 10170 while (i--) { 10171 var track = list[i]; 10172 10173 this.removeRemoteTextTrack(track); 10174 } 10175 }; 10176 10177 /** 10178 * Reset the tech, which will removes all sources and reset the internal readyState. 10179 * 10180 * @abstract 10181 */ 10182 10183 10184 Tech.prototype.reset = function reset() {}; 10185 10186 /** 10187 * Get or set an error on the Tech. 10188 * 10189 * @param {MediaError} [err] 10190 * Error to set on the Tech 10191 * 10192 * @return {MediaError|null} 10193 * The current error object on the tech, or null if there isn't one. 10194 */ 10195 10196 10197 Tech.prototype.error = function error(err) { 10198 if (err !== undefined) { 10199 this.error_ = new MediaError(err); 10200 this.trigger('error'); 10201 } 10202 return this.error_; 10203 }; 10204 10205 /** 10206 * Returns the `TimeRange`s that have been played through for the current source. 10207 * 10208 * > NOTE: This implementation is incomplete. It does not track the played `TimeRange`. 10209 * It only checks whether the source has played at all or not. 10210 * 10211 * @return {TimeRange} 10212 * - A single time range if this video has played 10213 * - An empty set of ranges if not. 10214 */ 10215 10216 10217 Tech.prototype.played = function played() { 10218 if (this.hasStarted_) { 10219 return createTimeRanges(0, 0); 10220 } 10221 return createTimeRanges(); 10222 }; 10223 10224 /** 10225 * Causes a manual time update to occur if {@link Tech#manualTimeUpdatesOn} was 10226 * previously called. 10227 * 10228 * @fires Tech#timeupdate 10229 */ 10230 10231 10232 Tech.prototype.setCurrentTime = function setCurrentTime() { 10233 // improve the accuracy of manual timeupdates 10234 if (this.manualTimeUpdates) { 10235 /** 10236 * A manual `timeupdate` event. 10237 * 10238 * @event Tech#timeupdate 10239 * @type {EventTarget~Event} 10240 */ 10241 this.trigger({ type: 'timeupdate', target: this, manuallyTriggered: true }); 10242 } 10243 }; 10244 10245 /** 10246 * Turn on listeners for {@link VideoTrackList}, {@link {AudioTrackList}, and 10247 * {@link TextTrackList} events. 10248 * 10249 * This adds {@link EventTarget~EventListeners} for `addtrack`, and `removetrack`. 10250 * 10251 * @fires Tech#audiotrackchange 10252 * @fires Tech#videotrackchange 10253 * @fires Tech#texttrackchange 10254 */ 10255 10256 10257 Tech.prototype.initTrackListeners = function initTrackListeners() { 10258 var _this4 = this; 10259 10260 /** 10261 * Triggered when tracks are added or removed on the Tech {@link AudioTrackList} 10262 * 10263 * @event Tech#audiotrackchange 10264 * @type {EventTarget~Event} 10265 */ 10266 10267 /** 10268 * Triggered when tracks are added or removed on the Tech {@link VideoTrackList} 10269 * 10270 * @event Tech#videotrackchange 10271 * @type {EventTarget~Event} 10272 */ 10273 10274 /** 10275 * Triggered when tracks are added or removed on the Tech {@link TextTrackList} 10276 * 10277 * @event Tech#texttrackchange 10278 * @type {EventTarget~Event} 10279 */ 10280 NORMAL.names.forEach(function (name) { 10281 var props = NORMAL[name]; 10282 var trackListChanges = function trackListChanges() { 10283 _this4.trigger(name + 'trackchange'); 10284 }; 10285 10286 var tracks = _this4[props.getterName](); 10287 10288 tracks.addEventListener('removetrack', trackListChanges); 10289 tracks.addEventListener('addtrack', trackListChanges); 10290 10291 _this4.on('dispose', function () { 10292 tracks.removeEventListener('removetrack', trackListChanges); 10293 tracks.removeEventListener('addtrack', trackListChanges); 10294 }); 10295 }); 10296 }; 10297 10298 /** 10299 * Emulate TextTracks using vtt.js if necessary 10300 * 10301 * @fires Tech#vttjsloaded 10302 * @fires Tech#vttjserror 10303 */ 10304 10305 10306 Tech.prototype.addWebVttScript_ = function addWebVttScript_() { 10307 var _this5 = this; 10308 10309 if (window_1.WebVTT) { 10310 return; 10311 } 10312 10313 // Initially, Tech.el_ is a child of a dummy-div wait until the Component system 10314 // signals that the Tech is ready at which point Tech.el_ is part of the DOM 10315 // before inserting the WebVTT script 10316 if (document_1.body.contains(this.el())) { 10317 10318 // load via require if available and vtt.js script location was not passed in 10319 // as an option. novtt builds will turn the above require call into an empty object 10320 // which will cause this if check to always fail. 10321 if (!this.options_['vtt.js'] && isPlain(browserIndex) && Object.keys(browserIndex).length > 0) { 10322 this.trigger('vttjsloaded'); 10323 return; 10324 } 10325 10326 // load vtt.js via the script location option or the cdn of no location was 10327 // passed in 10328 var script = document_1.createElement('script'); 10329 10330 script.src = this.options_['vtt.js'] || 'https://vjs.zencdn.net/vttjs/0.14.1/vtt.min.js'; 10331 script.onload = function () { 10332 /** 10333 * Fired when vtt.js is loaded. 10334 * 10335 * @event Tech#vttjsloaded 10336 * @type {EventTarget~Event} 10337 */ 10338 _this5.trigger('vttjsloaded'); 10339 }; 10340 script.onerror = function () { 10341 /** 10342 * Fired when vtt.js was not loaded due to an error 10343 * 10344 * @event Tech#vttjsloaded 10345 * @type {EventTarget~Event} 10346 */ 10347 _this5.trigger('vttjserror'); 10348 }; 10349 this.on('dispose', function () { 10350 script.onload = null; 10351 script.onerror = null; 10352 }); 10353 // but have not loaded yet and we set it to true before the inject so that 10354 // we don't overwrite the injected window.WebVTT if it loads right away 10355 window_1.WebVTT = true; 10356 this.el().parentNode.appendChild(script); 10357 } else { 10358 this.ready(this.addWebVttScript_); 10359 } 10360 }; 10361 10362 /** 10363 * Emulate texttracks 10364 * 10365 */ 10366 10367 10368 Tech.prototype.emulateTextTracks = function emulateTextTracks() { 10369 var _this6 = this; 10370 10371 var tracks = this.textTracks(); 10372 var remoteTracks = this.remoteTextTracks(); 10373 var handleAddTrack = function handleAddTrack(e) { 10374 return tracks.addTrack(e.track); 10375 }; 10376 var handleRemoveTrack = function handleRemoveTrack(e) { 10377 return tracks.removeTrack(e.track); 10378 }; 10379 10380 remoteTracks.on('addtrack', handleAddTrack); 10381 remoteTracks.on('removetrack', handleRemoveTrack); 10382 10383 this.addWebVttScript_(); 10384 10385 var updateDisplay = function updateDisplay() { 10386 return _this6.trigger('texttrackchange'); 10387 }; 10388 10389 var textTracksChanges = function textTracksChanges() { 10390 updateDisplay(); 10391 10392 for (var i = 0; i < tracks.length; i++) { 10393 var track = tracks[i]; 10394 10395 track.removeEventListener('cuechange', updateDisplay); 10396 if (track.mode === 'showing') { 10397 track.addEventListener('cuechange', updateDisplay); 10398 } 10399 } 10400 }; 10401 10402 textTracksChanges(); 10403 tracks.addEventListener('change', textTracksChanges); 10404 tracks.addEventListener('addtrack', textTracksChanges); 10405 tracks.addEventListener('removetrack', textTracksChanges); 10406 10407 this.on('dispose', function () { 10408 remoteTracks.off('addtrack', handleAddTrack); 10409 remoteTracks.off('removetrack', handleRemoveTrack); 10410 tracks.removeEventListener('change', textTracksChanges); 10411 tracks.removeEventListener('addtrack', textTracksChanges); 10412 tracks.removeEventListener('removetrack', textTracksChanges); 10413 10414 for (var i = 0; i < tracks.length; i++) { 10415 var track = tracks[i]; 10416 10417 track.removeEventListener('cuechange', updateDisplay); 10418 } 10419 }); 10420 }; 10421 10422 /** 10423 * Create and returns a remote {@link TextTrack} object. 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 * @return {TextTrack} 10435 * The TextTrack that gets created. 10436 */ 10437 10438 10439 Tech.prototype.addTextTrack = function addTextTrack(kind, label, language) { 10440 if (!kind) { 10441 throw new Error('TextTrack kind is required but was not provided'); 10442 } 10443 10444 return createTrackHelper(this, kind, label, language); 10445 }; 10446 10447 /** 10448 * Create an emulated TextTrack for use by addRemoteTextTrack 10449 * 10450 * This is intended to be overridden by classes that inherit from 10451 * Tech in order to create native or custom TextTracks. 10452 * 10453 * @param {Object} options 10454 * The object should contain the options to initialize the TextTrack with. 10455 * 10456 * @param {string} [options.kind] 10457 * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata). 10458 * 10459 * @param {string} [options.label]. 10460 * Label to identify the text track 10461 * 10462 * @param {string} [options.language] 10463 * Two letter language abbreviation. 10464 * 10465 * @return {HTMLTrackElement} 10466 * The track element that gets created. 10467 */ 10468 10469 10470 Tech.prototype.createRemoteTextTrack = function createRemoteTextTrack(options) { 10471 var track = mergeOptions(options, { 10472 tech: this 10473 }); 10474 10475 return new REMOTE.remoteTextEl.TrackClass(track); 10476 }; 10477 10478 /** 10479 * Creates a remote text track object and returns an html track element. 10480 * 10481 * > Note: This can be an emulated {@link HTMLTrackElement} or a native one. 10482 * 10483 * @param {Object} options 10484 * See {@link Tech#createRemoteTextTrack} for more detailed properties. 10485 * 10486 * @param {boolean} [manualCleanup=true] 10487 * - When false: the TextTrack will be automatically removed from the video 10488 * element whenever the source changes 10489 * - When True: The TextTrack will have to be cleaned up manually 10490 * 10491 * @return {HTMLTrackElement} 10492 * An Html Track Element. 10493 * 10494 * @deprecated The default functionality for this function will be equivalent 10495 * to "manualCleanup=false" in the future. The manualCleanup parameter will 10496 * also be removed. 10497 */ 10498 10499 10500 Tech.prototype.addRemoteTextTrack = function addRemoteTextTrack() { 10501 var _this7 = this; 10502 10503 var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 10504 var manualCleanup = arguments[1]; 10505 10506 var htmlTrackElement = this.createRemoteTextTrack(options); 10507 10508 if (manualCleanup !== true && manualCleanup !== false) { 10509 // deprecation warning 10510 log$1.warn('Calling addRemoteTextTrack without explicitly setting the "manualCleanup" parameter to `true` is deprecated and default to `false` in future version of video.js'); 10511 manualCleanup = true; 10512 } 10513 10514 // store HTMLTrackElement and TextTrack to remote list 10515 this.remoteTextTrackEls().addTrackElement_(htmlTrackElement); 10516 this.remoteTextTracks().addTrack(htmlTrackElement.track); 10517 10518 if (manualCleanup !== true) { 10519 // create the TextTrackList if it doesn't exist 10520 this.ready(function () { 10521 return _this7.autoRemoteTextTracks_.addTrack(htmlTrackElement.track); 10522 }); 10523 } 10524 10525 return htmlTrackElement; 10526 }; 10527 10528 /** 10529 * Remove a remote text track from the remote `TextTrackList`. 10530 * 10531 * @param {TextTrack} track 10532 * `TextTrack` to remove from the `TextTrackList` 10533 */ 10534 10535 10536 Tech.prototype.removeRemoteTextTrack = function removeRemoteTextTrack(track) { 10537 var trackElement = this.remoteTextTrackEls().getTrackElementByTrack_(track); 10538 10539 // remove HTMLTrackElement and TextTrack from remote list 10540 this.remoteTextTrackEls().removeTrackElement_(trackElement); 10541 this.remoteTextTracks().removeTrack(track); 10542 this.autoRemoteTextTracks_.removeTrack(track); 10543 }; 10544 10545 /** 10546 * Gets available media playback quality metrics as specified by the W3C's Media 10547 * Playback Quality API. 10548 * 10549 * @see [Spec]{@link https://wicg.github.io/media-playback-quality} 10550 * 10551 * @return {Object} 10552 * An object with supported media playback quality metrics 10553 * 10554 * @abstract 10555 */ 10556 10557 10558 Tech.prototype.getVideoPlaybackQuality = function getVideoPlaybackQuality() { 10559 return {}; 10560 }; 10561 10562 /** 10563 * A method to set a poster from a `Tech`. 10564 * 10565 * @abstract 10566 */ 10567 10568 10569 Tech.prototype.setPoster = function setPoster() {}; 10570 10571 /** 10572 * A method to check for the presence of the 'playsinline' <video> attribute. 10573 * 10574 * @abstract 10575 */ 10576 10577 10578 Tech.prototype.playsinline = function playsinline() {}; 10579 10580 /** 10581 * A method to set or unset the 'playsinline' <video> attribute. 10582 * 10583 * @abstract 10584 */ 10585 10586 10587 Tech.prototype.setPlaysinline = function setPlaysinline() {}; 10588 10589 /** 10590 * Attempt to force override of native audio tracks. 10591 * 10592 * @param {Boolean} override - If set to true native audio will be overridden, 10593 * otherwise native audio will potentially be used. 10594 * 10595 * @abstract 10596 */ 10597 10598 10599 Tech.prototype.overrideNativeAudioTracks = function overrideNativeAudioTracks() {}; 10600 10601 /** 10602 * Attempt to force override of native video tracks. 10603 * 10604 * @param {Boolean} override - If set to true native video will be overridden, 10605 * otherwise native video will potentially be used. 10606 * 10607 * @abstract 10608 */ 10609 10610 10611 Tech.prototype.overrideNativeVideoTracks = function overrideNativeVideoTracks() {}; 10612 10613 /* 10614 * Check if the tech can support the given mime-type. 10615 * 10616 * The base tech does not support any type, but source handlers might 10617 * overwrite this. 10618 * 10619 * @param {string} type 10620 * The mimetype to check for support 10621 * 10622 * @return {string} 10623 * 'probably', 'maybe', or empty string 10624 * 10625 * @see [Spec]{@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/canPlayType} 10626 * 10627 * @abstract 10628 */ 10629 10630 10631 Tech.prototype.canPlayType = function canPlayType() { 10632 return ''; 10633 }; 10634 10635 /** 10636 * Check if the type is supported by this tech. 10637 * 10638 * The base tech does not support any type, but source handlers might 10639 * overwrite this. 10640 * 10641 * @param {string} type 10642 * The media type to check 10643 * @return {string} Returns the native video element's response 10644 */ 10645 10646 10647 Tech.canPlayType = function canPlayType() { 10648 return ''; 10649 }; 10650 10651 /** 10652 * Check if the tech can support the given source 10653 * @param {Object} srcObj 10654 * The source object 10655 * @param {Object} options 10656 * The options passed to the tech 10657 * @return {string} 'probably', 'maybe', or '' (empty string) 10658 */ 10659 10660 10661 Tech.canPlaySource = function canPlaySource(srcObj, options) { 10662 return Tech.canPlayType(srcObj.type); 10663 }; 10664 10665 /* 10666 * Return whether the argument is a Tech or not. 10667 * Can be passed either a Class like `Html5` or a instance like `player.tech_` 10668 * 10669 * @param {Object} component 10670 * The item to check 10671 * 10672 * @return {boolean} 10673 * Whether it is a tech or not 10674 * - True if it is a tech 10675 * - False if it is not 10676 */ 10677 10678 10679 Tech.isTech = function isTech(component) { 10680 return component.prototype instanceof Tech || component instanceof Tech || component === Tech; 10681 }; 10682 10683 /** 10684 * Registers a `Tech` into a shared list for videojs. 10685 * 10686 * @param {string} name 10687 * Name of the `Tech` to register. 10688 * 10689 * @param {Object} tech 10690 * The `Tech` class to register. 10691 */ 10692 10693 10694 Tech.registerTech = function registerTech(name, tech) { 10695 if (!Tech.techs_) { 10696 Tech.techs_ = {}; 10697 } 10698 10699 if (!Tech.isTech(tech)) { 10700 throw new Error('Tech ' + name + ' must be a Tech'); 10701 } 10702 10703 if (!Tech.canPlayType) { 10704 throw new Error('Techs must have a static canPlayType method on them'); 10705 } 10706 if (!Tech.canPlaySource) { 10707 throw new Error('Techs must have a static canPlaySource method on them'); 10708 } 10709 10710 name = toTitleCase(name); 10711 10712 Tech.techs_[name] = tech; 10713 if (name !== 'Tech') { 10714 // camel case the techName for use in techOrder 10715 Tech.defaultTechOrder_.push(name); 10716 } 10717 return tech; 10718 }; 10719 10720 /** 10721 * Get a `Tech` from the shared list by name. 10722 * 10723 * @param {string} name 10724 * `camelCase` or `TitleCase` name of the Tech to get 10725 * 10726 * @return {Tech|undefined} 10727 * The `Tech` or undefined if there was no tech with the name requested. 10728 */ 10729 10730 10731 Tech.getTech = function getTech(name) { 10732 if (!name) { 10733 return; 10734 } 10735 10736 name = toTitleCase(name); 10737 10738 if (Tech.techs_ && Tech.techs_[name]) { 10739 return Tech.techs_[name]; 10740 } 10741 10742 if (window_1 && window_1.videojs && window_1.videojs[name]) { 10743 log$1.warn('The ' + name + ' tech was added to the videojs object when it should be registered using videojs.registerTech(name, tech)'); 10744 return window_1.videojs[name]; 10745 } 10746 }; 10747 10748 return Tech; 10749 }(Component); 10750 10751 /** 10752 * Get the {@link VideoTrackList} 10753 * 10754 * @returns {VideoTrackList} 10755 * @method Tech.prototype.videoTracks 10756 */ 10757 10758 /** 10759 * Get the {@link AudioTrackList} 10760 * 10761 * @returns {AudioTrackList} 10762 * @method Tech.prototype.audioTracks 10763 */ 10764 10765 /** 10766 * Get the {@link TextTrackList} 10767 * 10768 * @returns {TextTrackList} 10769 * @method Tech.prototype.textTracks 10770 */ 10771 10772 /** 10773 * Get the remote element {@link TextTrackList} 10774 * 10775 * @returns {TextTrackList} 10776 * @method Tech.prototype.remoteTextTracks 10777 */ 10778 10779 /** 10780 * Get the remote element {@link HtmlTrackElementList} 10781 * 10782 * @returns {HtmlTrackElementList} 10783 * @method Tech.prototype.remoteTextTrackEls 10784 */ 10785 10786 ALL.names.forEach(function (name) { 10787 var props = ALL[name]; 10788 10789 Tech.prototype[props.getterName] = function () { 10790 this[props.privateName] = this[props.privateName] || new props.ListClass(); 10791 return this[props.privateName]; 10792 }; 10793 }); 10794 10795 /** 10796 * List of associated text tracks 10797 * 10798 * @type {TextTrackList} 10799 * @private 10800 * @property Tech#textTracks_ 10801 */ 10802 10803 /** 10804 * List of associated audio tracks. 10805 * 10806 * @type {AudioTrackList} 10807 * @private 10808 * @property Tech#audioTracks_ 10809 */ 10810 10811 /** 10812 * List of associated video tracks. 10813 * 10814 * @type {VideoTrackList} 10815 * @private 10816 * @property Tech#videoTracks_ 10817 */ 10818 10819 /** 10820 * Boolean indicating whether the `Tech` supports volume control. 10821 * 10822 * @type {boolean} 10823 * @default 10824 */ 10825 Tech.prototype.featuresVolumeControl = true; 10826 10827 /** 10828 * Boolean indicating whether the `Tech` supports muting volume. 10829 * 10830 * @type {bolean} 10831 * @default 10832 */ 10833 Tech.prototype.featuresMuteControl = true; 10834 10835 /** 10836 * Boolean indicating whether the `Tech` supports fullscreen resize control. 10837 * Resizing plugins using request fullscreen reloads the plugin 10838 * 10839 * @type {boolean} 10840 * @default 10841 */ 10842 Tech.prototype.featuresFullscreenResize = false; 10843 10844 /** 10845 * Boolean indicating whether the `Tech` supports changing the speed at which the video 10846 * plays. Examples: 10847 * - Set player to play 2x (twice) as fast 10848 * - Set player to play 0.5x (half) as fast 10849 * 10850 * @type {boolean} 10851 * @default 10852 */ 10853 Tech.prototype.featuresPlaybackRate = false; 10854 10855 /** 10856 * Boolean indicating whether the `Tech` supports the `progress` event. This is currently 10857 * not triggered by video-js-swf. This will be used to determine if 10858 * {@link Tech#manualProgressOn} should be called. 10859 * 10860 * @type {boolean} 10861 * @default 10862 */ 10863 Tech.prototype.featuresProgressEvents = false; 10864 10865 /** 10866 * Boolean indicating whether the `Tech` supports the `sourceset` event. 10867 * 10868 * A tech should set this to `true` and then use {@link Tech#triggerSourceset} 10869 * to trigger a {@link Tech#event:sourceset} at the earliest time after getting 10870 * a new source. 10871 * 10872 * @type {boolean} 10873 * @default 10874 */ 10875 Tech.prototype.featuresSourceset = false; 10876 10877 /** 10878 * Boolean indicating whether the `Tech` supports the `timeupdate` event. This is currently 10879 * not triggered by video-js-swf. This will be used to determine if 10880 * {@link Tech#manualTimeUpdates} should be called. 10881 * 10882 * @type {boolean} 10883 * @default 10884 */ 10885 Tech.prototype.featuresTimeupdateEvents = false; 10886 10887 /** 10888 * Boolean indicating whether the `Tech` supports the native `TextTrack`s. 10889 * This will help us integrate with native `TextTrack`s if the browser supports them. 10890 * 10891 * @type {boolean} 10892 * @default 10893 */ 10894 Tech.prototype.featuresNativeTextTracks = false; 10895 10896 /** 10897 * A functional mixin for techs that want to use the Source Handler pattern. 10898 * Source handlers are scripts for handling specific formats. 10899 * The source handler pattern is used for adaptive formats (HLS, DASH) that 10900 * manually load video data and feed it into a Source Buffer (Media Source Extensions) 10901 * Example: `Tech.withSourceHandlers.call(MyTech);` 10902 * 10903 * @param {Tech} _Tech 10904 * The tech to add source handler functions to. 10905 * 10906 * @mixes Tech~SourceHandlerAdditions 10907 */ 10908 Tech.withSourceHandlers = function (_Tech) { 10909 10910 /** 10911 * Register a source handler 10912 * 10913 * @param {Function} handler 10914 * The source handler class 10915 * 10916 * @param {number} [index] 10917 * Register it at the following index 10918 */ 10919 _Tech.registerSourceHandler = function (handler, index) { 10920 var handlers = _Tech.sourceHandlers; 10921 10922 if (!handlers) { 10923 handlers = _Tech.sourceHandlers = []; 10924 } 10925 10926 if (index === undefined) { 10927 // add to the end of the list 10928 index = handlers.length; 10929 } 10930 10931 handlers.splice(index, 0, handler); 10932 }; 10933 10934 /** 10935 * Check if the tech can support the given type. Also checks the 10936 * Techs sourceHandlers. 10937 * 10938 * @param {string} type 10939 * The mimetype to check. 10940 * 10941 * @return {string} 10942 * 'probably', 'maybe', or '' (empty string) 10943 */ 10944 _Tech.canPlayType = function (type) { 10945 var handlers = _Tech.sourceHandlers || []; 10946 var can = void 0; 10947 10948 for (var i = 0; i < handlers.length; i++) { 10949 can = handlers[i].canPlayType(type); 10950 10951 if (can) { 10952 return can; 10953 } 10954 } 10955 10956 return ''; 10957 }; 10958 10959 /** 10960 * Returns the first source handler that supports the source. 10961 * 10962 * TODO: Answer question: should 'probably' be prioritized over 'maybe' 10963 * 10964 * @param {Tech~SourceObject} source 10965 * The source object 10966 * 10967 * @param {Object} options 10968 * The options passed to the tech 10969 * 10970 * @return {SourceHandler|null} 10971 * The first source handler that supports the source or null if 10972 * no SourceHandler supports the source 10973 */ 10974 _Tech.selectSourceHandler = function (source, options) { 10975 var handlers = _Tech.sourceHandlers || []; 10976 var can = void 0; 10977 10978 for (var i = 0; i < handlers.length; i++) { 10979 can = handlers[i].canHandleSource(source, options); 10980 10981 if (can) { 10982 return handlers[i]; 10983 } 10984 } 10985 10986 return null; 10987 }; 10988 10989 /** 10990 * Check if the tech can support the given source. 10991 * 10992 * @param {Tech~SourceObject} srcObj 10993 * The source object 10994 * 10995 * @param {Object} options 10996 * The options passed to the tech 10997 * 10998 * @return {string} 10999 * 'probably', 'maybe', or '' (empty string) 11000 */ 11001 _Tech.canPlaySource = function (srcObj, options) { 11002 var sh = _Tech.selectSourceHandler(srcObj, options); 11003 11004 if (sh) { 11005 return sh.canHandleSource(srcObj, options); 11006 } 11007 11008 return ''; 11009 }; 11010 11011 /** 11012 * When using a source handler, prefer its implementation of 11013 * any function normally provided by the tech. 11014 */ 11015 var deferrable = ['seekable', 'seeking', 'duration']; 11016 11017 /** 11018 * A wrapper around {@link Tech#seekable} that will call a `SourceHandler`s seekable 11019 * function if it exists, with a fallback to the Techs seekable function. 11020 * 11021 * @method _Tech.seekable 11022 */ 11023 11024 /** 11025 * A wrapper around {@link Tech#duration} that will call a `SourceHandler`s duration 11026 * function if it exists, otherwise it will fallback to the techs duration function. 11027 * 11028 * @method _Tech.duration 11029 */ 11030 11031 deferrable.forEach(function (fnName) { 11032 var originalFn = this[fnName]; 11033 11034 if (typeof originalFn !== 'function') { 11035 return; 11036 } 11037 11038 this[fnName] = function () { 11039 if (this.sourceHandler_ && this.sourceHandler_[fnName]) { 11040 return this.sourceHandler_[fnName].apply(this.sourceHandler_, arguments); 11041 } 11042 return originalFn.apply(this, arguments); 11043 }; 11044 }, _Tech.prototype); 11045 11046 /** 11047 * Create a function for setting the source using a source object 11048 * and source handlers. 11049 * Should never be called unless a source handler was found. 11050 * 11051 * @param {Tech~SourceObject} source 11052 * A source object with src and type keys 11053 */ 11054 _Tech.prototype.setSource = function (source) { 11055 var sh = _Tech.selectSourceHandler(source, this.options_); 11056 11057 if (!sh) { 11058 // Fall back to a native source hander when unsupported sources are 11059 // deliberately set 11060 if (_Tech.nativeSourceHandler) { 11061 sh = _Tech.nativeSourceHandler; 11062 } else { 11063 log$1.error('No source handler found for the current source.'); 11064 } 11065 } 11066 11067 // Dispose any existing source handler 11068 this.disposeSourceHandler(); 11069 this.off('dispose', this.disposeSourceHandler); 11070 11071 if (sh !== _Tech.nativeSourceHandler) { 11072 this.currentSource_ = source; 11073 } 11074 11075 this.sourceHandler_ = sh.handleSource(source, this, this.options_); 11076 this.on('dispose', this.disposeSourceHandler); 11077 }; 11078 11079 /** 11080 * Clean up any existing SourceHandlers and listeners when the Tech is disposed. 11081 * 11082 * @listens Tech#dispose 11083 */ 11084 _Tech.prototype.disposeSourceHandler = function () { 11085 // if we have a source and get another one 11086 // then we are loading something new 11087 // than clear all of our current tracks 11088 if (this.currentSource_) { 11089 this.clearTracks(['audio', 'video']); 11090 this.currentSource_ = null; 11091 } 11092 11093 // always clean up auto-text tracks 11094 this.cleanupAutoTextTracks(); 11095 11096 if (this.sourceHandler_) { 11097 11098 if (this.sourceHandler_.dispose) { 11099 this.sourceHandler_.dispose(); 11100 } 11101 11102 this.sourceHandler_ = null; 11103 } 11104 }; 11105 }; 11106 11107 // The base Tech class needs to be registered as a Component. It is the only 11108 // Tech that can be registered as a Component. 11109 Component.registerComponent('Tech', Tech); 11110 Tech.registerTech('Tech', Tech); 11111 11112 /** 11113 * A list of techs that should be added to techOrder on Players 11114 * 11115 * @private 11116 */ 11117 Tech.defaultTechOrder_ = []; 11118 11119 var middlewares = {}; 11120 var middlewareInstances = {}; 11121 11122 var TERMINATOR = {}; 11123 11124 function use(type, middleware) { 11125 middlewares[type] = middlewares[type] || []; 11126 middlewares[type].push(middleware); 11127 } 11128 11129 function setSource(player, src, next) { 11130 player.setTimeout(function () { 11131 return setSourceHelper(src, middlewares[src.type], next, player); 11132 }, 1); 11133 } 11134 11135 function setTech(middleware, tech) { 11136 middleware.forEach(function (mw) { 11137 return mw.setTech && mw.setTech(tech); 11138 }); 11139 } 11140 11141 /** 11142 * Calls a getter on the tech first, through each middleware 11143 * from right to left to the player. 11144 */ 11145 function get$1(middleware, tech, method) { 11146 return middleware.reduceRight(middlewareIterator(method), tech[method]()); 11147 } 11148 11149 /** 11150 * Takes the argument given to the player and calls the setter method on each 11151 * middleware from left to right to the tech. 11152 */ 11153 function set$1(middleware, tech, method, arg) { 11154 return tech[method](middleware.reduce(middlewareIterator(method), arg)); 11155 } 11156 11157 /** 11158 * Takes the argument given to the player and calls the `call` version of the method 11159 * on each middleware from left to right. 11160 * Then, call the passed in method on the tech and return the result unchanged 11161 * back to the player, through middleware, this time from right to left. 11162 */ 11163 function mediate(middleware, tech, method) { 11164 var arg = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null; 11165 11166 var callMethod = 'call' + toTitleCase(method); 11167 var middlewareValue = middleware.reduce(middlewareIterator(callMethod), arg); 11168 var terminated = middlewareValue === TERMINATOR; 11169 var returnValue = terminated ? null : tech[method](middlewareValue); 11170 11171 executeRight(middleware, method, returnValue, terminated); 11172 11173 return returnValue; 11174 } 11175 11176 var allowedGetters = { 11177 buffered: 1, 11178 currentTime: 1, 11179 duration: 1, 11180 seekable: 1, 11181 played: 1, 11182 paused: 1 11183 }; 11184 11185 var allowedSetters = { 11186 setCurrentTime: 1 11187 }; 11188 11189 var allowedMediators = { 11190 play: 1, 11191 pause: 1 11192 }; 11193 11194 function middlewareIterator(method) { 11195 return function (value, mw) { 11196 // if the previous middleware terminated, pass along the termination 11197 if (value === TERMINATOR) { 11198 return TERMINATOR; 11199 } 11200 11201 if (mw[method]) { 11202 return mw[method](value); 11203 } 11204 11205 return value; 11206 }; 11207 } 11208 11209 function executeRight(mws, method, value, terminated) { 11210 for (var i = mws.length - 1; i >= 0; i--) { 11211 var mw = mws[i]; 11212 11213 if (mw[method]) { 11214 mw[method](terminated, value); 11215 } 11216 } 11217 } 11218 11219 function clearCacheForPlayer(player) { 11220 middlewareInstances[player.id()] = null; 11221 } 11222 11223 /** 11224 * { 11225 * [playerId]: [[mwFactory, mwInstance], ...] 11226 * } 11227 */ 11228 function getOrCreateFactory(player, mwFactory) { 11229 var mws = middlewareInstances[player.id()]; 11230 var mw = null; 11231 11232 if (mws === undefined || mws === null) { 11233 mw = mwFactory(player); 11234 middlewareInstances[player.id()] = [[mwFactory, mw]]; 11235 return mw; 11236 } 11237 11238 for (var i = 0; i < mws.length; i++) { 11239 var _mws$i = mws[i], 11240 mwf = _mws$i[0], 11241 mwi = _mws$i[1]; 11242 11243 11244 if (mwf !== mwFactory) { 11245 continue; 11246 } 11247 11248 mw = mwi; 11249 } 11250 11251 if (mw === null) { 11252 mw = mwFactory(player); 11253 mws.push([mwFactory, mw]); 11254 } 11255 11256 return mw; 11257 } 11258 11259 function setSourceHelper() { 11260 var src = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 11261 var middleware = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; 11262 var next = arguments[2]; 11263 var player = arguments[3]; 11264 var acc = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : []; 11265 var lastRun = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : false; 11266 var mwFactory = middleware[0], 11267 mwrest = middleware.slice(1); 11268 11269 // if mwFactory is a string, then we're at a fork in the road 11270 11271 if (typeof mwFactory === 'string') { 11272 setSourceHelper(src, middlewares[mwFactory], next, player, acc, lastRun); 11273 11274 // if we have an mwFactory, call it with the player to get the mw, 11275 // then call the mw's setSource method 11276 } else if (mwFactory) { 11277 var mw = getOrCreateFactory(player, mwFactory); 11278 11279 // if setSource isn't present, implicitly select this middleware 11280 if (!mw.setSource) { 11281 acc.push(mw); 11282 return setSourceHelper(src, mwrest, next, player, acc, lastRun); 11283 } 11284 11285 mw.setSource(assign({}, src), function (err, _src) { 11286 11287 // something happened, try the next middleware on the current level 11288 // make sure to use the old src 11289 if (err) { 11290 return setSourceHelper(src, mwrest, next, player, acc, lastRun); 11291 } 11292 11293 // we've succeeded, now we need to go deeper 11294 acc.push(mw); 11295 11296 // if it's the same type, continue down the current chain 11297 // otherwise, we want to go down the new chain 11298 setSourceHelper(_src, src.type === _src.type ? mwrest : middlewares[_src.type], next, player, acc, lastRun); 11299 }); 11300 } else if (mwrest.length) { 11301 setSourceHelper(src, mwrest, next, player, acc, lastRun); 11302 } else if (lastRun) { 11303 next(src, acc); 11304 } else { 11305 setSourceHelper(src, middlewares['*'], next, player, acc, true); 11306 } 11307 } 11308 11309 /** 11310 * Mimetypes 11311 * 11312 * @see http://hul.harvard.edu/ois/////systems/wax/wax-public-help/mimetypes.htm 11313 * @typedef Mimetypes~Kind 11314 * @enum 11315 */ 11316 var MimetypesKind = { 11317 opus: 'video/ogg', 11318 ogv: 'video/ogg', 11319 mp4: 'video/mp4', 11320 mov: 'video/mp4', 11321 m4v: 'video/mp4', 11322 mkv: 'video/x-matroska', 11323 mp3: 'audio/mpeg', 11324 aac: 'audio/aac', 11325 oga: 'audio/ogg', 11326 m3u8: 'application/x-mpegURL' 11327 }; 11328 11329 /** 11330 * Get the mimetype of a given src url if possible 11331 * 11332 * @param {string} src 11333 * The url to the src 11334 * 11335 * @return {string} 11336 * return the mimetype if it was known or empty string otherwise 11337 */ 11338 var getMimetype = function getMimetype() { 11339 var src = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; 11340 11341 var ext = getFileExtension(src); 11342 var mimetype = MimetypesKind[ext.toLowerCase()]; 11343 11344 return mimetype || ''; 11345 }; 11346 11347 /** 11348 * Find the mime type of a given source string if possible. Uses the player 11349 * source cache. 11350 * 11351 * @param {Player} player 11352 * The player object 11353 * 11354 * @param {string} src 11355 * The source string 11356 * 11357 * @return {string} 11358 * The type that was found 11359 */ 11360 var findMimetype = function findMimetype(player, src) { 11361 if (!src) { 11362 return ''; 11363 } 11364 11365 // 1. check for the type in the `source` cache 11366 if (player.cache_.source.src === src && player.cache_.source.type) { 11367 return player.cache_.source.type; 11368 } 11369 11370 // 2. see if we have this source in our `currentSources` cache 11371 var matchingSources = player.cache_.sources.filter(function (s) { 11372 return s.src === src; 11373 }); 11374 11375 if (matchingSources.length) { 11376 return matchingSources[0].type; 11377 } 11378 11379 // 3. look for the src url in source elements and use the type there 11380 var sources = player.$$('source'); 11381 11382 for (var i = 0; i < sources.length; i++) { 11383 var s = sources[i]; 11384 11385 if (s.type && s.src && s.src === src) { 11386 return s.type; 11387 } 11388 } 11389 11390 // 4. finally fallback to our list of mime types based on src url extension 11391 return getMimetype(src); 11392 }; 11393 11394 /** 11395 * @module filter-source 11396 */ 11397 11398 /** 11399 * Filter out single bad source objects or multiple source objects in an 11400 * array. Also flattens nested source object arrays into a 1 dimensional 11401 * array of source objects. 11402 * 11403 * @param {Tech~SourceObject|Tech~SourceObject[]} src 11404 * The src object to filter 11405 * 11406 * @return {Tech~SourceObject[]} 11407 * An array of sourceobjects containing only valid sources 11408 * 11409 * @private 11410 */ 11411 var filterSource = function filterSource(src) { 11412 // traverse array 11413 if (Array.isArray(src)) { 11414 var newsrc = []; 11415 11416 src.forEach(function (srcobj) { 11417 srcobj = filterSource(srcobj); 11418 11419 if (Array.isArray(srcobj)) { 11420 newsrc = newsrc.concat(srcobj); 11421 } else if (isObject(srcobj)) { 11422 newsrc.push(srcobj); 11423 } 11424 }); 11425 11426 src = newsrc; 11427 } else if (typeof src === 'string' && src.trim()) { 11428 // convert string into object 11429 src = [fixSource({ src: src })]; 11430 } else if (isObject(src) && typeof src.src === 'string' && src.src && src.src.trim()) { 11431 // src is already valid 11432 src = [fixSource(src)]; 11433 } else { 11434 // invalid source, turn it into an empty array 11435 src = []; 11436 } 11437 11438 return src; 11439 }; 11440 11441 /** 11442 * Checks src mimetype, adding it when possible 11443 * 11444 * @param {Tech~SourceObject} src 11445 * The src object to check 11446 * @return {Tech~SourceObject} 11447 * src Object with known type 11448 */ 11449 function fixSource(src) { 11450 var mimetype = getMimetype(src.src); 11451 11452 if (!src.type && mimetype) { 11453 src.type = mimetype; 11454 } 11455 11456 return src; 11457 } 11458 11459 /** 11460 * @file loader.js 11461 */ 11462 11463 /** 11464 * The `MediaLoader` is the `Component` that decides which playback technology to load 11465 * when a player is initialized. 11466 * 11467 * @extends Component 11468 */ 11469 11470 var MediaLoader = function (_Component) { 11471 inherits(MediaLoader, _Component); 11472 11473 /** 11474 * Create an instance of this class. 11475 * 11476 * @param {Player} player 11477 * The `Player` that this class should attach to. 11478 * 11479 * @param {Object} [options] 11480 * The key/value store of player options. 11481 * 11482 * @param {Component~ReadyCallback} [ready] 11483 * The function that is run when this component is ready. 11484 */ 11485 function MediaLoader(player, options, ready) { 11486 classCallCheck(this, MediaLoader); 11487 11488 // MediaLoader has no element 11489 var options_ = mergeOptions({ createEl: false }, options); 11490 11491 // If there are no sources when the player is initialized, 11492 // load the first supported playback technology. 11493 11494 var _this = possibleConstructorReturn(this, _Component.call(this, player, options_, ready)); 11495 11496 if (!options.playerOptions.sources || options.playerOptions.sources.length === 0) { 11497 for (var i = 0, j = options.playerOptions.techOrder; i < j.length; i++) { 11498 var techName = toTitleCase(j[i]); 11499 var tech = Tech.getTech(techName); 11500 11501 // Support old behavior of techs being registered as components. 11502 // Remove once that deprecated behavior is removed. 11503 if (!techName) { 11504 tech = Component.getComponent(techName); 11505 } 11506 11507 // Check if the browser supports this technology 11508 if (tech && tech.isSupported()) { 11509 player.loadTech_(techName); 11510 break; 11511 } 11512 } 11513 } else { 11514 // Loop through playback technologies (HTML5, Flash) and check for support. 11515 // Then load the best source. 11516 // A few assumptions here: 11517 // All playback technologies respect preload false. 11518 player.src(options.playerOptions.sources); 11519 } 11520 return _this; 11521 } 11522 11523 return MediaLoader; 11524 }(Component); 11525 11526 Component.registerComponent('MediaLoader', MediaLoader); 11527 11528 /** 11529 * @file clickable-component.js 11530 */ 11531 11532 /** 11533 * Clickable Component which is clickable or keyboard actionable, 11534 * but is not a native HTML button. 11535 * 11536 * @extends Component 11537 */ 11538 11539 var ClickableComponent = function (_Component) { 11540 inherits(ClickableComponent, _Component); 11541 11542 /** 11543 * Creates an instance of this class. 11544 * 11545 * @param {Player} player 11546 * The `Player` that this class should be attached to. 11547 * 11548 * @param {Object} [options] 11549 * The key/value store of player options. 11550 */ 11551 function ClickableComponent(player, options) { 11552 classCallCheck(this, ClickableComponent); 11553 11554 var _this = possibleConstructorReturn(this, _Component.call(this, player, options)); 11555 11556 _this.emitTapEvents(); 11557 11558 _this.enable(); 11559 return _this; 11560 } 11561 11562 /** 11563 * Create the `Component`s DOM element. 11564 * 11565 * @param {string} [tag=div] 11566 * The element's node type. 11567 * 11568 * @param {Object} [props={}] 11569 * An object of properties that should be set on the element. 11570 * 11571 * @param {Object} [attributes={}] 11572 * An object of attributes that should be set on the element. 11573 * 11574 * @return {Element} 11575 * The element that gets created. 11576 */ 11577 11578 11579 ClickableComponent.prototype.createEl = function createEl$$1() { 11580 var tag = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'div'; 11581 var props = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 11582 var attributes = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 11583 11584 props = assign({ 11585 innerHTML: '<span aria-hidden="true" class="vjs-icon-placeholder"></span>', 11586 className: this.buildCSSClass(), 11587 tabIndex: 0 11588 }, props); 11589 11590 if (tag === 'button') { 11591 log$1.error('Creating a ClickableComponent with an HTML element of ' + tag + ' is not supported; use a Button instead.'); 11592 } 11593 11594 // Add ARIA attributes for clickable element which is not a native HTML button 11595 attributes = assign({ 11596 role: 'button' 11597 }, attributes); 11598 11599 this.tabIndex_ = props.tabIndex; 11600 11601 var el = _Component.prototype.createEl.call(this, tag, props, attributes); 11602 11603 this.createControlTextEl(el); 11604 11605 return el; 11606 }; 11607 11608 ClickableComponent.prototype.dispose = function dispose() { 11609 // remove controlTextEl_ on dispose 11610 this.controlTextEl_ = null; 11611 11612 _Component.prototype.dispose.call(this); 11613 }; 11614 11615 /** 11616 * Create a control text element on this `Component` 11617 * 11618 * @param {Element} [el] 11619 * Parent element for the control text. 11620 * 11621 * @return {Element} 11622 * The control text element that gets created. 11623 */ 11624 11625 11626 ClickableComponent.prototype.createControlTextEl = function createControlTextEl(el) { 11627 this.controlTextEl_ = createEl('span', { 11628 className: 'vjs-control-text' 11629 }, { 11630 // let the screen reader user know that the text of the element may change 11631 'aria-live': 'polite' 11632 }); 11633 11634 if (el) { 11635 el.appendChild(this.controlTextEl_); 11636 } 11637 11638 this.controlText(this.controlText_, el); 11639 11640 return this.controlTextEl_; 11641 }; 11642 11643 /** 11644 * Get or set the localize text to use for the controls on the `Component`. 11645 * 11646 * @param {string} [text] 11647 * Control text for element. 11648 * 11649 * @param {Element} [el=this.el()] 11650 * Element to set the title on. 11651 * 11652 * @return {string} 11653 * - The control text when getting 11654 */ 11655 11656 11657 ClickableComponent.prototype.controlText = function controlText(text) { 11658 var el = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.el(); 11659 11660 if (text === undefined) { 11661 return this.controlText_ || 'Need Text'; 11662 } 11663 11664 var localizedText = this.localize(text); 11665 11666 this.controlText_ = text; 11667 textContent(this.controlTextEl_, localizedText); 11668 if (!this.nonIconControl) { 11669 // Set title attribute if only an icon is shown 11670 el.setAttribute('title', localizedText); 11671 } 11672 }; 11673 11674 /** 11675 * Builds the default DOM `className`. 11676 * 11677 * @return {string} 11678 * The DOM `className` for this object. 11679 */ 11680 11681 11682 ClickableComponent.prototype.buildCSSClass = function buildCSSClass() { 11683 return 'vjs-control vjs-button ' + _Component.prototype.buildCSSClass.call(this); 11684 }; 11685 11686 /** 11687 * Enable this `Component`s element. 11688 */ 11689 11690 11691 ClickableComponent.prototype.enable = function enable() { 11692 if (!this.enabled_) { 11693 this.enabled_ = true; 11694 this.removeClass('vjs-disabled'); 11695 this.el_.setAttribute('aria-disabled', 'false'); 11696 if (typeof this.tabIndex_ !== 'undefined') { 11697 this.el_.setAttribute('tabIndex', this.tabIndex_); 11698 } 11699 this.on(['tap', 'click'], this.handleClick); 11700 this.on('focus', this.handleFocus); 11701 this.on('blur', this.handleBlur); 11702 } 11703 }; 11704 11705 /** 11706 * Disable this `Component`s element. 11707 */ 11708 11709 11710 ClickableComponent.prototype.disable = function disable() { 11711 this.enabled_ = false; 11712 this.addClass('vjs-disabled'); 11713 this.el_.setAttribute('aria-disabled', 'true'); 11714 if (typeof this.tabIndex_ !== 'undefined') { 11715 this.el_.removeAttribute('tabIndex'); 11716 } 11717 this.off(['tap', 'click'], this.handleClick); 11718 this.off('focus', this.handleFocus); 11719 this.off('blur', this.handleBlur); 11720 }; 11721 11722 /** 11723 * This gets called when a `ClickableComponent` gets: 11724 * - Clicked (via the `click` event, listening starts in the constructor) 11725 * - Tapped (via the `tap` event, listening starts in the constructor) 11726 * - The following things happen in order: 11727 * 1. {@link ClickableComponent#handleFocus} is called via a `focus` event on the 11728 * `ClickableComponent`. 11729 * 2. {@link ClickableComponent#handleFocus} adds a listener for `keydown` on using 11730 * {@link ClickableComponent#handleKeyPress}. 11731 * 3. `ClickableComponent` has not had a `blur` event (`blur` means that focus was lost). The user presses 11732 * the space or enter key. 11733 * 4. {@link ClickableComponent#handleKeyPress} calls this function with the `keydown` 11734 * event as a parameter. 11735 * 11736 * @param {EventTarget~Event} event 11737 * The `keydown`, `tap`, or `click` event that caused this function to be 11738 * called. 11739 * 11740 * @listens tap 11741 * @listens click 11742 * @abstract 11743 */ 11744 11745 11746 ClickableComponent.prototype.handleClick = function handleClick(event) {}; 11747 11748 /** 11749 * This gets called when a `ClickableComponent` gains focus via a `focus` event. 11750 * Turns on listening for `keydown` events. When they happen it 11751 * calls `this.handleKeyPress`. 11752 * 11753 * @param {EventTarget~Event} event 11754 * The `focus` event that caused this function to be called. 11755 * 11756 * @listens focus 11757 */ 11758 11759 11760 ClickableComponent.prototype.handleFocus = function handleFocus(event) { 11761 on(document_1, 'keydown', bind(this, this.handleKeyPress)); 11762 }; 11763 11764 /** 11765 * Called when this ClickableComponent has focus and a key gets pressed down. By 11766 * default it will call `this.handleClick` when the key is space or enter. 11767 * 11768 * @param {EventTarget~Event} event 11769 * The `keydown` event that caused this function to be called. 11770 * 11771 * @listens keydown 11772 */ 11773 11774 11775 ClickableComponent.prototype.handleKeyPress = function handleKeyPress(event) { 11776 11777 // Support Space (32) or Enter (13) key operation to fire a click event 11778 if (event.which === 32 || event.which === 13) { 11779 event.preventDefault(); 11780 this.trigger('click'); 11781 } else if (_Component.prototype.handleKeyPress) { 11782 11783 // Pass keypress handling up for unsupported keys 11784 _Component.prototype.handleKeyPress.call(this, event); 11785 } 11786 }; 11787 11788 /** 11789 * Called when a `ClickableComponent` loses focus. Turns off the listener for 11790 * `keydown` events. Which Stops `this.handleKeyPress` from getting called. 11791 * 11792 * @param {EventTarget~Event} event 11793 * The `blur` event that caused this function to be called. 11794 * 11795 * @listens blur 11796 */ 11797 11798 11799 ClickableComponent.prototype.handleBlur = function handleBlur(event) { 11800 off(document_1, 'keydown', bind(this, this.handleKeyPress)); 11801 }; 11802 11803 return ClickableComponent; 11804 }(Component); 11805 11806 Component.registerComponent('ClickableComponent', ClickableComponent); 11807 11808 /** 11809 * @file poster-image.js 11810 */ 11811 11812 /** 11813 * A `ClickableComponent` that handles showing the poster image for the player. 11814 * 11815 * @extends ClickableComponent 11816 */ 11817 11818 var PosterImage = function (_ClickableComponent) { 11819 inherits(PosterImage, _ClickableComponent); 11820 11821 /** 11822 * Create an instance of this class. 11823 * 11824 * @param {Player} player 11825 * The `Player` that this class should attach to. 11826 * 11827 * @param {Object} [options] 11828 * The key/value store of player options. 11829 */ 11830 function PosterImage(player, options) { 11831 classCallCheck(this, PosterImage); 11832 11833 var _this = possibleConstructorReturn(this, _ClickableComponent.call(this, player, options)); 11834 11835 _this.update(); 11836 player.on('posterchange', bind(_this, _this.update)); 11837 return _this; 11838 } 11839 11840 /** 11841 * Clean up and dispose of the `PosterImage`. 11842 */ 11843 11844 11845 PosterImage.prototype.dispose = function dispose() { 11846 this.player().off('posterchange', this.update); 11847 _ClickableComponent.prototype.dispose.call(this); 11848 }; 11849 11850 /** 11851 * Create the `PosterImage`s DOM element. 11852 * 11853 * @return {Element} 11854 * The element that gets created. 11855 */ 11856 11857 11858 PosterImage.prototype.createEl = function createEl$$1() { 11859 var el = createEl('div', { 11860 className: 'vjs-poster', 11861 11862 // Don't want poster to be tabbable. 11863 tabIndex: -1 11864 }); 11865 11866 return el; 11867 }; 11868 11869 /** 11870 * An {@link EventTarget~EventListener} for {@link Player#posterchange} events. 11871 * 11872 * @listens Player#posterchange 11873 * 11874 * @param {EventTarget~Event} [event] 11875 * The `Player#posterchange` event that triggered this function. 11876 */ 11877 11878 11879 PosterImage.prototype.update = function update(event) { 11880 var url = this.player().poster(); 11881 11882 this.setSrc(url); 11883 11884 // If there's no poster source we should display:none on this component 11885 // so it's not still clickable or right-clickable 11886 if (url) { 11887 this.show(); 11888 } else { 11889 this.hide(); 11890 } 11891 }; 11892 11893 /** 11894 * Set the source of the `PosterImage` depending on the display method. 11895 * 11896 * @param {string} url 11897 * The URL to the source for the `PosterImage`. 11898 */ 11899 11900 11901 PosterImage.prototype.setSrc = function setSrc(url) { 11902 var backgroundImage = ''; 11903 11904 // Any falsy value should stay as an empty string, otherwise 11905 // this will throw an extra error 11906 if (url) { 11907 backgroundImage = 'url("' + url + '")'; 11908 } 11909 11910 this.el_.style.backgroundImage = backgroundImage; 11911 }; 11912 11913 /** 11914 * An {@link EventTarget~EventListener} for clicks on the `PosterImage`. See 11915 * {@link ClickableComponent#handleClick} for instances where this will be triggered. 11916 * 11917 * @listens tap 11918 * @listens click 11919 * @listens keydown 11920 * 11921 * @param {EventTarget~Event} event 11922 + The `click`, `tap` or `keydown` event that caused this function to be called. 11923 */ 11924 11925 11926 PosterImage.prototype.handleClick = function handleClick(event) { 11927 // We don't want a click to trigger playback when controls are disabled 11928 if (!this.player_.controls()) { 11929 return; 11930 } 11931 11932 if (this.player_.paused()) { 11933 silencePromise(this.player_.play()); 11934 } else { 11935 this.player_.pause(); 11936 } 11937 }; 11938 11939 return PosterImage; 11940 }(ClickableComponent); 11941 11942 Component.registerComponent('PosterImage', PosterImage); 11943 11944 /** 11945 * @file text-track-display.js 11946 */ 11947 11948 var darkGray = '#222'; 11949 var lightGray = '#ccc'; 11950 var fontMap = { 11951 monospace: 'monospace', 11952 sansSerif: 'sans-serif', 11953 serif: 'serif', 11954 monospaceSansSerif: '"Andale Mono", "Lucida Console", monospace', 11955 monospaceSerif: '"Courier New", monospace', 11956 proportionalSansSerif: 'sans-serif', 11957 proportionalSerif: 'serif', 11958 casual: '"Comic Sans MS", Impact, fantasy', 11959 script: '"Monotype Corsiva", cursive', 11960 smallcaps: '"Andale Mono", "Lucida Console", monospace, sans-serif' 11961 }; 11962 11963 /** 11964 * Construct an rgba color from a given hex color code. 11965 * 11966 * @param {number} color 11967 * Hex number for color, like #f0e or #f604e2. 11968 * 11969 * @param {number} opacity 11970 * Value for opacity, 0.0 - 1.0. 11971 * 11972 * @return {string} 11973 * The rgba color that was created, like 'rgba(255, 0, 0, 0.3)'. 11974 */ 11975 function constructColor(color, opacity) { 11976 var hex = void 0; 11977 11978 if (color.length === 4) { 11979 // color looks like "#f0e" 11980 hex = color[1] + color[1] + color[2] + color[2] + color[3] + color[3]; 11981 } else if (color.length === 7) { 11982 // color looks like "#f604e2" 11983 hex = color.slice(1); 11984 } else { 11985 throw new Error('Invalid color code provided, ' + color + '; must be formatted as e.g. #f0e or #f604e2.'); 11986 } 11987 return 'rgba(' + parseInt(hex.slice(0, 2), 16) + ',' + parseInt(hex.slice(2, 4), 16) + ',' + parseInt(hex.slice(4, 6), 16) + ',' + opacity + ')'; 11988 } 11989 11990 /** 11991 * Try to update the style of a DOM element. Some style changes will throw an error, 11992 * particularly in IE8. Those should be noops. 11993 * 11994 * @param {Element} el 11995 * The DOM element to be styled. 11996 * 11997 * @param {string} style 11998 * The CSS property on the element that should be styled. 11999 * 12000 * @param {string} rule 12001 * The style rule that should be applied to the property. 12002 * 12003 * @private 12004 */ 12005 function tryUpdateStyle(el, style, rule) { 12006 try { 12007 el.style[style] = rule; 12008 } catch (e) { 12009 12010 // Satisfies linter. 12011 return; 12012 } 12013 } 12014 12015 /** 12016 * The component for displaying text track cues. 12017 * 12018 * @extends Component 12019 */ 12020 12021 var TextTrackDisplay = function (_Component) { 12022 inherits(TextTrackDisplay, _Component); 12023 12024 /** 12025 * Creates an instance of this class. 12026 * 12027 * @param {Player} player 12028 * The `Player` that this class should be attached to. 12029 * 12030 * @param {Object} [options] 12031 * The key/value store of player options. 12032 * 12033 * @param {Component~ReadyCallback} [ready] 12034 * The function to call when `TextTrackDisplay` is ready. 12035 */ 12036 function TextTrackDisplay(player, options, ready) { 12037 classCallCheck(this, TextTrackDisplay); 12038 12039 var _this = possibleConstructorReturn(this, _Component.call(this, player, options, ready)); 12040 12041 player.on('loadstart', bind(_this, _this.toggleDisplay)); 12042 player.on('texttrackchange', bind(_this, _this.updateDisplay)); 12043 player.on('loadstart', bind(_this, _this.preselectTrack)); 12044 12045 // This used to be called during player init, but was causing an error 12046 // if a track should show by default and the display hadn't loaded yet. 12047 // Should probably be moved to an external track loader when we support 12048 // tracks that don't need a display. 12049 player.ready(bind(_this, function () { 12050 if (player.tech_ && player.tech_.featuresNativeTextTracks) { 12051 this.hide(); 12052 return; 12053 } 12054 12055 player.on('fullscreenchange', bind(this, this.updateDisplay)); 12056 12057 var tracks = this.options_.playerOptions.tracks || []; 12058 12059 for (var i = 0; i < tracks.length; i++) { 12060 this.player_.addRemoteTextTrack(tracks[i], true); 12061 } 12062 12063 this.preselectTrack(); 12064 })); 12065 return _this; 12066 } 12067 12068 /** 12069 * Preselect a track following this precedence: 12070 * - matches the previously selected {@link TextTrack}'s language and kind 12071 * - matches the previously selected {@link TextTrack}'s language only 12072 * - is the first default captions track 12073 * - is the first default descriptions track 12074 * 12075 * @listens Player#loadstart 12076 */ 12077 12078 12079 TextTrackDisplay.prototype.preselectTrack = function preselectTrack() { 12080 var modes = { captions: 1, subtitles: 1 }; 12081 var trackList = this.player_.textTracks(); 12082 var userPref = this.player_.cache_.selectedLanguage; 12083 var firstDesc = void 0; 12084 var firstCaptions = void 0; 12085 var preferredTrack = void 0; 12086 12087 for (var i = 0; i < trackList.length; i++) { 12088 var track = trackList[i]; 12089 12090 if (userPref && userPref.enabled && userPref.language === track.language) { 12091 // Always choose the track that matches both language and kind 12092 if (track.kind === userPref.kind) { 12093 preferredTrack = track; 12094 // or choose the first track that matches language 12095 } else if (!preferredTrack) { 12096 preferredTrack = track; 12097 } 12098 12099 // clear everything if offTextTrackMenuItem was clicked 12100 } else if (userPref && !userPref.enabled) { 12101 preferredTrack = null; 12102 firstDesc = null; 12103 firstCaptions = null; 12104 } else if (track.default) { 12105 if (track.kind === 'descriptions' && !firstDesc) { 12106 firstDesc = track; 12107 } else if (track.kind in modes && !firstCaptions) { 12108 firstCaptions = track; 12109 } 12110 } 12111 } 12112 12113 // The preferredTrack matches the user preference and takes 12114 // precedence over all the other tracks. 12115 // So, display the preferredTrack before the first default track 12116 // and the subtitles/captions track before the descriptions track 12117 if (preferredTrack) { 12118 preferredTrack.mode = 'showing'; 12119 } else if (firstCaptions) { 12120 firstCaptions.mode = 'showing'; 12121 } else if (firstDesc) { 12122 firstDesc.mode = 'showing'; 12123 } 12124 }; 12125 12126 /** 12127 * Turn display of {@link TextTrack}'s from the current state into the other state. 12128 * There are only two states: 12129 * - 'shown' 12130 * - 'hidden' 12131 * 12132 * @listens Player#loadstart 12133 */ 12134 12135 12136 TextTrackDisplay.prototype.toggleDisplay = function toggleDisplay() { 12137 if (this.player_.tech_ && this.player_.tech_.featuresNativeTextTracks) { 12138 this.hide(); 12139 } else { 12140 this.show(); 12141 } 12142 }; 12143 12144 /** 12145 * Create the {@link Component}'s DOM element. 12146 * 12147 * @return {Element} 12148 * The element that was created. 12149 */ 12150 12151 12152 TextTrackDisplay.prototype.createEl = function createEl() { 12153 return _Component.prototype.createEl.call(this, 'div', { 12154 className: 'vjs-text-track-display' 12155 }, { 12156 'aria-live': 'off', 12157 'aria-atomic': 'true' 12158 }); 12159 }; 12160 12161 /** 12162 * Clear all displayed {@link TextTrack}s. 12163 */ 12164 12165 12166 TextTrackDisplay.prototype.clearDisplay = function clearDisplay() { 12167 if (typeof window_1.WebVTT === 'function') { 12168 window_1.WebVTT.processCues(window_1, [], this.el_); 12169 } 12170 }; 12171 12172 /** 12173 * Update the displayed TextTrack when a either a {@link Player#texttrackchange} or 12174 * a {@link Player#fullscreenchange} is fired. 12175 * 12176 * @listens Player#texttrackchange 12177 * @listens Player#fullscreenchange 12178 */ 12179 12180 12181 TextTrackDisplay.prototype.updateDisplay = function updateDisplay() { 12182 var tracks = this.player_.textTracks(); 12183 12184 this.clearDisplay(); 12185 12186 // Track display prioritization model: if multiple tracks are 'showing', 12187 // display the first 'subtitles' or 'captions' track which is 'showing', 12188 // otherwise display the first 'descriptions' track which is 'showing' 12189 12190 var descriptionsTrack = null; 12191 var captionsSubtitlesTrack = null; 12192 var i = tracks.length; 12193 12194 while (i--) { 12195 var track = tracks[i]; 12196 12197 if (track.mode === 'showing') { 12198 if (track.kind === 'descriptions') { 12199 descriptionsTrack = track; 12200 } else { 12201 captionsSubtitlesTrack = track; 12202 } 12203 } 12204 } 12205 12206 if (captionsSubtitlesTrack) { 12207 if (this.getAttribute('aria-live') !== 'off') { 12208 this.setAttribute('aria-live', 'off'); 12209 } 12210 this.updateForTrack(captionsSubtitlesTrack); 12211 } else if (descriptionsTrack) { 12212 if (this.getAttribute('aria-live') !== 'assertive') { 12213 this.setAttribute('aria-live', 'assertive'); 12214 } 12215 this.updateForTrack(descriptionsTrack); 12216 } 12217 }; 12218 12219 /** 12220 * Add an {@link TextTrack} to to the {@link Tech}s {@link TextTrackList}. 12221 * 12222 * @param {TextTrack} track 12223 * Text track object to be added to the list. 12224 */ 12225 12226 12227 TextTrackDisplay.prototype.updateForTrack = function updateForTrack(track) { 12228 if (typeof window_1.WebVTT !== 'function' || !track.activeCues) { 12229 return; 12230 } 12231 12232 var cues = []; 12233 12234 for (var _i = 0; _i < track.activeCues.length; _i++) { 12235 cues.push(track.activeCues[_i]); 12236 } 12237 12238 window_1.WebVTT.processCues(window_1, cues, this.el_); 12239 12240 if (!this.player_.textTrackSettings) { 12241 return; 12242 } 12243 12244 var overrides = this.player_.textTrackSettings.getValues(); 12245 12246 var i = cues.length; 12247 12248 while (i--) { 12249 var cue = cues[i]; 12250 12251 if (!cue) { 12252 continue; 12253 } 12254 12255 var cueDiv = cue.displayState; 12256 12257 if (overrides.color) { 12258 cueDiv.firstChild.style.color = overrides.color; 12259 } 12260 if (overrides.textOpacity) { 12261 tryUpdateStyle(cueDiv.firstChild, 'color', constructColor(overrides.color || '#fff', overrides.textOpacity)); 12262 } 12263 if (overrides.backgroundColor) { 12264 cueDiv.firstChild.style.backgroundColor = overrides.backgroundColor; 12265 } 12266 if (overrides.backgroundOpacity) { 12267 tryUpdateStyle(cueDiv.firstChild, 'backgroundColor', constructColor(overrides.backgroundColor || '#000', overrides.backgroundOpacity)); 12268 } 12269 if (overrides.windowColor) { 12270 if (overrides.windowOpacity) { 12271 tryUpdateStyle(cueDiv, 'backgroundColor', constructColor(overrides.windowColor, overrides.windowOpacity)); 12272 } else { 12273 cueDiv.style.backgroundColor = overrides.windowColor; 12274 } 12275 } 12276 if (overrides.edgeStyle) { 12277 if (overrides.edgeStyle === 'dropshadow') { 12278 cueDiv.firstChild.style.textShadow = '2px 2px 3px ' + darkGray + ', 2px 2px 4px ' + darkGray + ', 2px 2px 5px ' + darkGray; 12279 } else if (overrides.edgeStyle === 'raised') { 12280 cueDiv.firstChild.style.textShadow = '1px 1px ' + darkGray + ', 2px 2px ' + darkGray + ', 3px 3px ' + darkGray; 12281 } else if (overrides.edgeStyle === 'depressed') { 12282 cueDiv.firstChild.style.textShadow = '1px 1px ' + lightGray + ', 0 1px ' + lightGray + ', -1px -1px ' + darkGray + ', 0 -1px ' + darkGray; 12283 } else if (overrides.edgeStyle === 'uniform') { 12284 cueDiv.firstChild.style.textShadow = '0 0 4px ' + darkGray + ', 0 0 4px ' + darkGray + ', 0 0 4px ' + darkGray + ', 0 0 4px ' + darkGray; 12285 } 12286 } 12287 if (overrides.fontPercent && overrides.fontPercent !== 1) { 12288 var fontSize = window_1.parseFloat(cueDiv.style.fontSize); 12289 12290 cueDiv.style.fontSize = fontSize * overrides.fontPercent + 'px'; 12291 cueDiv.style.height = 'auto'; 12292 cueDiv.style.top = 'auto'; 12293 cueDiv.style.bottom = '2px'; 12294 } 12295 if (overrides.fontFamily && overrides.fontFamily !== 'default') { 12296 if (overrides.fontFamily === 'small-caps') { 12297 cueDiv.firstChild.style.fontVariant = 'small-caps'; 12298 } else { 12299 cueDiv.firstChild.style.fontFamily = fontMap[overrides.fontFamily]; 12300 } 12301 } 12302 } 12303 }; 12304 12305 return TextTrackDisplay; 12306 }(Component); 12307 12308 Component.registerComponent('TextTrackDisplay', TextTrackDisplay); 12309 12310 /** 12311 * @file loading-spinner.js 12312 */ 12313 12314 /** 12315 * A loading spinner for use during waiting/loading events. 12316 * 12317 * @extends Component 12318 */ 12319 12320 var LoadingSpinner = function (_Component) { 12321 inherits(LoadingSpinner, _Component); 12322 12323 function LoadingSpinner() { 12324 classCallCheck(this, LoadingSpinner); 12325 return possibleConstructorReturn(this, _Component.apply(this, arguments)); 12326 } 12327 12328 /** 12329 * Create the `LoadingSpinner`s DOM element. 12330 * 12331 * @return {Element} 12332 * The dom element that gets created. 12333 */ 12334 LoadingSpinner.prototype.createEl = function createEl$$1() { 12335 var isAudio = this.player_.isAudio(); 12336 var playerType = this.localize(isAudio ? 'Audio Player' : 'Video Player'); 12337 var controlText = createEl('span', { 12338 className: 'vjs-control-text', 12339 innerHTML: this.localize('{1} is loading.', [playerType]) 12340 }); 12341 12342 var el = _Component.prototype.createEl.call(this, 'div', { 12343 className: 'vjs-loading-spinner', 12344 dir: 'ltr' 12345 }); 12346 12347 el.appendChild(controlText); 12348 12349 return el; 12350 }; 12351 12352 return LoadingSpinner; 12353 }(Component); 12354 12355 Component.registerComponent('LoadingSpinner', LoadingSpinner); 12356 12357 /** 12358 * @file button.js 12359 */ 12360 12361 /** 12362 * Base class for all buttons. 12363 * 12364 * @extends ClickableComponent 12365 */ 12366 12367 var Button = function (_ClickableComponent) { 12368 inherits(Button, _ClickableComponent); 12369 12370 function Button() { 12371 classCallCheck(this, Button); 12372 return possibleConstructorReturn(this, _ClickableComponent.apply(this, arguments)); 12373 } 12374 12375 /** 12376 * Create the `Button`s DOM element. 12377 * 12378 * @param {string} [tag="button"] 12379 * The element's node type. This argument is IGNORED: no matter what 12380 * is passed, it will always create a `button` element. 12381 * 12382 * @param {Object} [props={}] 12383 * An object of properties that should be set on the element. 12384 * 12385 * @param {Object} [attributes={}] 12386 * An object of attributes that should be set on the element. 12387 * 12388 * @return {Element} 12389 * The element that gets created. 12390 */ 12391 Button.prototype.createEl = function createEl(tag) { 12392 var props = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 12393 var attributes = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 12394 12395 tag = 'button'; 12396 12397 props = assign({ 12398 innerHTML: '<span aria-hidden="true" class="vjs-icon-placeholder"></span>', 12399 className: this.buildCSSClass() 12400 }, props); 12401 12402 // Add attributes for button element 12403 attributes = assign({ 12404 12405 // Necessary since the default button type is "submit" 12406 type: 'button' 12407 }, attributes); 12408 12409 var el = Component.prototype.createEl.call(this, tag, props, attributes); 12410 12411 this.createControlTextEl(el); 12412 12413 return el; 12414 }; 12415 12416 /** 12417 * Add a child `Component` inside of this `Button`. 12418 * 12419 * @param {string|Component} child 12420 * The name or instance of a child to add. 12421 * 12422 * @param {Object} [options={}] 12423 * The key/value store of options that will get passed to children of 12424 * the child. 12425 * 12426 * @return {Component} 12427 * The `Component` that gets added as a child. When using a string the 12428 * `Component` will get created by this process. 12429 * 12430 * @deprecated since version 5 12431 */ 12432 12433 12434 Button.prototype.addChild = function addChild(child) { 12435 var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 12436 12437 var className = this.constructor.name; 12438 12439 log$1.warn('Adding an actionable (user controllable) child to a Button (' + className + ') is not supported; use a ClickableComponent instead.'); 12440 12441 // Avoid the error message generated by ClickableComponent's addChild method 12442 return Component.prototype.addChild.call(this, child, options); 12443 }; 12444 12445 /** 12446 * Enable the `Button` element so that it can be activated or clicked. Use this with 12447 * {@link Button#disable}. 12448 */ 12449 12450 12451 Button.prototype.enable = function enable() { 12452 _ClickableComponent.prototype.enable.call(this); 12453 this.el_.removeAttribute('disabled'); 12454 }; 12455 12456 /** 12457 * Disable the `Button` element so that it cannot be activated or clicked. Use this with 12458 * {@link Button#enable}. 12459 */ 12460 12461 12462 Button.prototype.disable = function disable() { 12463 _ClickableComponent.prototype.disable.call(this); 12464 this.el_.setAttribute('disabled', 'disabled'); 12465 }; 12466 12467 /** 12468 * This gets called when a `Button` has focus and `keydown` is triggered via a key 12469 * press. 12470 * 12471 * @param {EventTarget~Event} event 12472 * The event that caused this function to get called. 12473 * 12474 * @listens keydown 12475 */ 12476 12477 12478 Button.prototype.handleKeyPress = function handleKeyPress(event) { 12479 12480 // Ignore Space (32) or Enter (13) key operation, which is handled by the browser for a button. 12481 if (event.which === 32 || event.which === 13) { 12482 return; 12483 } 12484 12485 // Pass keypress handling up for unsupported keys 12486 _ClickableComponent.prototype.handleKeyPress.call(this, event); 12487 }; 12488 12489 return Button; 12490 }(ClickableComponent); 12491 12492 Component.registerComponent('Button', Button); 12493 12494 /** 12495 * @file big-play-button.js 12496 */ 12497 12498 /** 12499 * The initial play button that shows before the video has played. The hiding of the 12500 * `BigPlayButton` get done via CSS and `Player` states. 12501 * 12502 * @extends Button 12503 */ 12504 12505 var BigPlayButton = function (_Button) { 12506 inherits(BigPlayButton, _Button); 12507 12508 function BigPlayButton(player, options) { 12509 classCallCheck(this, BigPlayButton); 12510 12511 var _this = possibleConstructorReturn(this, _Button.call(this, player, options)); 12512 12513 _this.mouseused_ = false; 12514 12515 _this.on('mousedown', _this.handleMouseDown); 12516 return _this; 12517 } 12518 12519 /** 12520 * Builds the default DOM `className`. 12521 * 12522 * @return {string} 12523 * The DOM `className` for this object. Always returns 'vjs-big-play-button'. 12524 */ 12525 12526 12527 BigPlayButton.prototype.buildCSSClass = function buildCSSClass() { 12528 return 'vjs-big-play-button'; 12529 }; 12530 12531 /** 12532 * This gets called when a `BigPlayButton` "clicked". See {@link ClickableComponent} 12533 * for more detailed information on what a click can be. 12534 * 12535 * @param {EventTarget~Event} event 12536 * The `keydown`, `tap`, or `click` event that caused this function to be 12537 * called. 12538 * 12539 * @listens tap 12540 * @listens click 12541 */ 12542 12543 12544 BigPlayButton.prototype.handleClick = function handleClick(event) { 12545 var playPromise = this.player_.play(); 12546 12547 // exit early if clicked via the mouse 12548 if (this.mouseused_ && event.clientX && event.clientY) { 12549 silencePromise(playPromise); 12550 return; 12551 } 12552 12553 var cb = this.player_.getChild('controlBar'); 12554 var playToggle = cb && cb.getChild('playToggle'); 12555 12556 if (!playToggle) { 12557 this.player_.focus(); 12558 return; 12559 } 12560 12561 var playFocus = function playFocus() { 12562 return playToggle.focus(); 12563 }; 12564 12565 if (isPromise(playPromise)) { 12566 playPromise.then(playFocus, function () {}); 12567 } else { 12568 this.setTimeout(playFocus, 1); 12569 } 12570 }; 12571 12572 BigPlayButton.prototype.handleKeyPress = function handleKeyPress(event) { 12573 this.mouseused_ = false; 12574 12575 _Button.prototype.handleKeyPress.call(this, event); 12576 }; 12577 12578 BigPlayButton.prototype.handleMouseDown = function handleMouseDown(event) { 12579 this.mouseused_ = true; 12580 }; 12581 12582 return BigPlayButton; 12583 }(Button); 12584 12585 /** 12586 * The text that should display over the `BigPlayButton`s controls. Added to for localization. 12587 * 12588 * @type {string} 12589 * @private 12590 */ 12591 12592 12593 BigPlayButton.prototype.controlText_ = 'Play Video'; 12594 12595 Component.registerComponent('BigPlayButton', BigPlayButton); 12596 12597 /** 12598 * @file close-button.js 12599 */ 12600 12601 /** 12602 * The `CloseButton` is a `{@link Button}` that fires a `close` event when 12603 * it gets clicked. 12604 * 12605 * @extends Button 12606 */ 12607 12608 var CloseButton = function (_Button) { 12609 inherits(CloseButton, _Button); 12610 12611 /** 12612 * Creates an instance of the this class. 12613 * 12614 * @param {Player} player 12615 * The `Player` that this class should be attached to. 12616 * 12617 * @param {Object} [options] 12618 * The key/value store of player options. 12619 */ 12620 function CloseButton(player, options) { 12621 classCallCheck(this, CloseButton); 12622 12623 var _this = possibleConstructorReturn(this, _Button.call(this, player, options)); 12624 12625 _this.controlText(options && options.controlText || _this.localize('Close')); 12626 return _this; 12627 } 12628 12629 /** 12630 * Builds the default DOM `className`. 12631 * 12632 * @return {string} 12633 * The DOM `className` for this object. 12634 */ 12635 12636 12637 CloseButton.prototype.buildCSSClass = function buildCSSClass() { 12638 return 'vjs-close-button ' + _Button.prototype.buildCSSClass.call(this); 12639 }; 12640 12641 /** 12642 * This gets called when a `CloseButton` gets clicked. See 12643 * {@link ClickableComponent#handleClick} for more information on when this will be 12644 * triggered 12645 * 12646 * @param {EventTarget~Event} event 12647 * The `keydown`, `tap`, or `click` event that caused this function to be 12648 * called. 12649 * 12650 * @listens tap 12651 * @listens click 12652 * @fires CloseButton#close 12653 */ 12654 12655 12656 CloseButton.prototype.handleClick = function handleClick(event) { 12657 12658 /** 12659 * Triggered when the a `CloseButton` is clicked. 12660 * 12661 * @event CloseButton#close 12662 * @type {EventTarget~Event} 12663 * 12664 * @property {boolean} [bubbles=false] 12665 * set to false so that the close event does not 12666 * bubble up to parents if there is no listener 12667 */ 12668 this.trigger({ type: 'close', bubbles: false }); 12669 }; 12670 12671 return CloseButton; 12672 }(Button); 12673 12674 Component.registerComponent('CloseButton', CloseButton); 12675 12676 /** 12677 * @file play-toggle.js 12678 */ 12679 12680 /** 12681 * Button to toggle between play and pause. 12682 * 12683 * @extends Button 12684 */ 12685 12686 var PlayToggle = function (_Button) { 12687 inherits(PlayToggle, _Button); 12688 12689 /** 12690 * Creates an instance of this class. 12691 * 12692 * @param {Player} player 12693 * The `Player` that this class should be attached to. 12694 * 12695 * @param {Object} [options] 12696 * The key/value store of player options. 12697 */ 12698 function PlayToggle(player, options) { 12699 classCallCheck(this, PlayToggle); 12700 12701 var _this = possibleConstructorReturn(this, _Button.call(this, player, options)); 12702 12703 _this.on(player, 'play', _this.handlePlay); 12704 _this.on(player, 'pause', _this.handlePause); 12705 _this.on(player, 'ended', _this.handleEnded); 12706 return _this; 12707 } 12708 12709 /** 12710 * Builds the default DOM `className`. 12711 * 12712 * @return {string} 12713 * The DOM `className` for this object. 12714 */ 12715 12716 12717 PlayToggle.prototype.buildCSSClass = function buildCSSClass() { 12718 return 'vjs-play-control ' + _Button.prototype.buildCSSClass.call(this); 12719 }; 12720 12721 /** 12722 * This gets called when an `PlayToggle` is "clicked". See 12723 * {@link ClickableComponent} for more detailed information on what a click can be. 12724 * 12725 * @param {EventTarget~Event} [event] 12726 * The `keydown`, `tap`, or `click` event that caused this function to be 12727 * called. 12728 * 12729 * @listens tap 12730 * @listens click 12731 */ 12732 12733 12734 PlayToggle.prototype.handleClick = function handleClick(event) { 12735 if (this.player_.paused()) { 12736 this.player_.play(); 12737 } else { 12738 this.player_.pause(); 12739 } 12740 }; 12741 12742 /** 12743 * This gets called once after the video has ended and the user seeks so that 12744 * we can change the replay button back to a play button. 12745 * 12746 * @param {EventTarget~Event} [event] 12747 * The event that caused this function to run. 12748 * 12749 * @listens Player#seeked 12750 */ 12751 12752 12753 PlayToggle.prototype.handleSeeked = function handleSeeked(event) { 12754 this.removeClass('vjs-ended'); 12755 12756 if (this.player_.paused()) { 12757 this.handlePause(event); 12758 } else { 12759 this.handlePlay(event); 12760 } 12761 }; 12762 12763 /** 12764 * Add the vjs-playing class to the element so it can change appearance. 12765 * 12766 * @param {EventTarget~Event} [event] 12767 * The event that caused this function to run. 12768 * 12769 * @listens Player#play 12770 */ 12771 12772 12773 PlayToggle.prototype.handlePlay = function handlePlay(event) { 12774 this.removeClass('vjs-ended'); 12775 this.removeClass('vjs-paused'); 12776 this.addClass('vjs-playing'); 12777 // change the button text to "Pause" 12778 this.controlText('Pause'); 12779 }; 12780 12781 /** 12782 * Add the vjs-paused class to the element so it can change appearance. 12783 * 12784 * @param {EventTarget~Event} [event] 12785 * The event that caused this function to run. 12786 * 12787 * @listens Player#pause 12788 */ 12789 12790 12791 PlayToggle.prototype.handlePause = function handlePause(event) { 12792 this.removeClass('vjs-playing'); 12793 this.addClass('vjs-paused'); 12794 // change the button text to "Play" 12795 this.controlText('Play'); 12796 }; 12797 12798 /** 12799 * Add the vjs-ended class to the element so it can change appearance 12800 * 12801 * @param {EventTarget~Event} [event] 12802 * The event that caused this function to run. 12803 * 12804 * @listens Player#ended 12805 */ 12806 12807 12808 PlayToggle.prototype.handleEnded = function handleEnded(event) { 12809 this.removeClass('vjs-playing'); 12810 this.addClass('vjs-ended'); 12811 // change the button text to "Replay" 12812 this.controlText('Replay'); 12813 12814 // on the next seek remove the replay button 12815 this.one(this.player_, 'seeked', this.handleSeeked); 12816 }; 12817 12818 return PlayToggle; 12819 }(Button); 12820 12821 /** 12822 * The text that should display over the `PlayToggle`s controls. Added for localization. 12823 * 12824 * @type {string} 12825 * @private 12826 */ 12827 12828 12829 PlayToggle.prototype.controlText_ = 'Play'; 12830 12831 Component.registerComponent('PlayToggle', PlayToggle); 12832 12833 /** 12834 * @file format-time.js 12835 * @module format-time 12836 */ 12837 12838 /** 12839 * Format seconds as a time string, H:MM:SS or M:SS. Supplying a guide (in seconds) 12840 * will force a number of leading zeros to cover the length of the guide. 12841 * 12842 * @param {number} seconds 12843 * Number of seconds to be turned into a string 12844 * 12845 * @param {number} guide 12846 * Number (in seconds) to model the string after 12847 * 12848 * @return {string} 12849 * Time formatted as H:MM:SS or M:SS 12850 */ 12851 var defaultImplementation = function defaultImplementation(seconds, guide) { 12852 seconds = seconds < 0 ? 0 : seconds; 12853 var s = Math.floor(seconds % 60); 12854 var m = Math.floor(seconds / 60 % 60); 12855 var h = Math.floor(seconds / 3600); 12856 var gm = Math.floor(guide / 60 % 60); 12857 var gh = Math.floor(guide / 3600); 12858 12859 // handle invalid times 12860 if (isNaN(seconds) || seconds === Infinity) { 12861 // '-' is false for all relational operators (e.g. <, >=) so this setting 12862 // will add the minimum number of fields specified by the guide 12863 h = m = s = '-'; 12864 } 12865 12866 // Check if we need to show hours 12867 h = h > 0 || gh > 0 ? h + ':' : ''; 12868 12869 // If hours are showing, we may need to add a leading zero. 12870 // Always show at least one digit of minutes. 12871 m = ((h || gm >= 10) && m < 10 ? '0' + m : m) + ':'; 12872 12873 // Check if leading zero is need for seconds 12874 s = s < 10 ? '0' + s : s; 12875 12876 return h + m + s; 12877 }; 12878 12879 var implementation = defaultImplementation; 12880 12881 /** 12882 * Replaces the default formatTime implementation with a custom implementation. 12883 * 12884 * @param {Function} customImplementation 12885 * A function which will be used in place of the default formatTime implementation. 12886 * Will receive the current time in seconds and the guide (in seconds) as arguments. 12887 */ 12888 function setFormatTime(customImplementation) { 12889 implementation = customImplementation; 12890 } 12891 12892 /** 12893 * Resets formatTime to the default implementation. 12894 */ 12895 function resetFormatTime() { 12896 implementation = defaultImplementation; 12897 } 12898 12899 function formatTime (seconds) { 12900 var guide = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : seconds; 12901 12902 return implementation(seconds, guide); 12903 } 12904 12905 /** 12906 * @file time-display.js 12907 */ 12908 12909 /** 12910 * Displays the time left in the video 12911 * 12912 * @extends Component 12913 */ 12914 12915 var TimeDisplay = function (_Component) { 12916 inherits(TimeDisplay, _Component); 12917 12918 /** 12919 * Creates an instance of this class. 12920 * 12921 * @param {Player} player 12922 * The `Player` that this class should be attached to. 12923 * 12924 * @param {Object} [options] 12925 * The key/value store of player options. 12926 */ 12927 function TimeDisplay(player, options) { 12928 classCallCheck(this, TimeDisplay); 12929 12930 var _this = possibleConstructorReturn(this, _Component.call(this, player, options)); 12931 12932 _this.throttledUpdateContent = throttle(bind(_this, _this.updateContent), 25); 12933 _this.on(player, 'timeupdate', _this.throttledUpdateContent); 12934 return _this; 12935 } 12936 12937 /** 12938 * Create the `Component`'s DOM element 12939 * 12940 * @return {Element} 12941 * The element that was created. 12942 */ 12943 12944 12945 TimeDisplay.prototype.createEl = function createEl$$1(plainName) { 12946 var className = this.buildCSSClass(); 12947 var el = _Component.prototype.createEl.call(this, 'div', { 12948 className: className + ' vjs-time-control vjs-control', 12949 innerHTML: '<span class="vjs-control-text">' + this.localize(this.labelText_) + '\xA0</span>' 12950 }); 12951 12952 this.contentEl_ = createEl('span', { 12953 className: className + '-display' 12954 }, { 12955 // tell screen readers not to automatically read the time as it changes 12956 'aria-live': 'off' 12957 }); 12958 12959 this.updateTextNode_(); 12960 el.appendChild(this.contentEl_); 12961 return el; 12962 }; 12963 12964 TimeDisplay.prototype.dispose = function dispose() { 12965 this.contentEl_ = null; 12966 this.textNode_ = null; 12967 12968 _Component.prototype.dispose.call(this); 12969 }; 12970 12971 /** 12972 * Updates the "remaining time" text node with new content using the 12973 * contents of the `formattedTime_` property. 12974 * 12975 * @private 12976 */ 12977 12978 12979 TimeDisplay.prototype.updateTextNode_ = function updateTextNode_() { 12980 if (!this.contentEl_) { 12981 return; 12982 } 12983 12984 while (this.contentEl_.firstChild) { 12985 this.contentEl_.removeChild(this.contentEl_.firstChild); 12986 } 12987 12988 this.textNode_ = document_1.createTextNode(this.formattedTime_ || this.formatTime_(0)); 12989 this.contentEl_.appendChild(this.textNode_); 12990 }; 12991 12992 /** 12993 * Generates a formatted time for this component to use in display. 12994 * 12995 * @param {number} time 12996 * A numeric time, in seconds. 12997 * 12998 * @return {string} 12999 * A formatted time 13000 * 13001 * @private 13002 */ 13003 13004 13005 TimeDisplay.prototype.formatTime_ = function formatTime_(time) { 13006 return formatTime(time); 13007 }; 13008 13009 /** 13010 * Updates the time display text node if it has what was passed in changed 13011 * the formatted time. 13012 * 13013 * @param {number} time 13014 * The time to update to 13015 * 13016 * @private 13017 */ 13018 13019 13020 TimeDisplay.prototype.updateFormattedTime_ = function updateFormattedTime_(time) { 13021 var formattedTime = this.formatTime_(time); 13022 13023 if (formattedTime === this.formattedTime_) { 13024 return; 13025 } 13026 13027 this.formattedTime_ = formattedTime; 13028 this.requestAnimationFrame(this.updateTextNode_); 13029 }; 13030 13031 /** 13032 * To be filled out in the child class, should update the displayed time 13033 * in accordance with the fact that the current time has changed. 13034 * 13035 * @param {EventTarget~Event} [event] 13036 * The `timeupdate` event that caused this to run. 13037 * 13038 * @listens Player#timeupdate 13039 */ 13040 13041 13042 TimeDisplay.prototype.updateContent = function updateContent(event) {}; 13043 13044 return TimeDisplay; 13045 }(Component); 13046 13047 /** 13048 * The text that is added to the `TimeDisplay` for screen reader users. 13049 * 13050 * @type {string} 13051 * @private 13052 */ 13053 13054 13055 TimeDisplay.prototype.labelText_ = 'Time'; 13056 13057 /** 13058 * The text that should display over the `TimeDisplay`s controls. Added to for localization. 13059 * 13060 * @type {string} 13061 * @private 13062 * 13063 * @deprecated in v7; controlText_ is not used in non-active display Components 13064 */ 13065 TimeDisplay.prototype.controlText_ = 'Time'; 13066 13067 Component.registerComponent('TimeDisplay', TimeDisplay); 13068 13069 /** 13070 * @file current-time-display.js 13071 */ 13072 13073 /** 13074 * Displays the current time 13075 * 13076 * @extends Component 13077 */ 13078 13079 var CurrentTimeDisplay = function (_TimeDisplay) { 13080 inherits(CurrentTimeDisplay, _TimeDisplay); 13081 13082 /** 13083 * Creates an instance of this class. 13084 * 13085 * @param {Player} player 13086 * The `Player` that this class should be attached to. 13087 * 13088 * @param {Object} [options] 13089 * The key/value store of player options. 13090 */ 13091 function CurrentTimeDisplay(player, options) { 13092 classCallCheck(this, CurrentTimeDisplay); 13093 13094 var _this = possibleConstructorReturn(this, _TimeDisplay.call(this, player, options)); 13095 13096 _this.on(player, 'ended', _this.handleEnded); 13097 return _this; 13098 } 13099 13100 /** 13101 * Builds the default DOM `className`. 13102 * 13103 * @return {string} 13104 * The DOM `className` for this object. 13105 */ 13106 13107 13108 CurrentTimeDisplay.prototype.buildCSSClass = function buildCSSClass() { 13109 return 'vjs-current-time'; 13110 }; 13111 13112 /** 13113 * Update current time display 13114 * 13115 * @param {EventTarget~Event} [event] 13116 * The `timeupdate` event that caused this function to run. 13117 * 13118 * @listens Player#timeupdate 13119 */ 13120 13121 13122 CurrentTimeDisplay.prototype.updateContent = function updateContent(event) { 13123 // Allows for smooth scrubbing, when player can't keep up. 13124 var time = this.player_.scrubbing() ? this.player_.getCache().currentTime : this.player_.currentTime(); 13125 13126 this.updateFormattedTime_(time); 13127 }; 13128 13129 /** 13130 * When the player fires ended there should be no time left. Sadly 13131 * this is not always the case, lets make it seem like that is the case 13132 * for users. 13133 * 13134 * @param {EventTarget~Event} [event] 13135 * The `ended` event that caused this to run. 13136 * 13137 * @listens Player#ended 13138 */ 13139 13140 13141 CurrentTimeDisplay.prototype.handleEnded = function handleEnded(event) { 13142 if (!this.player_.duration()) { 13143 return; 13144 } 13145 this.updateFormattedTime_(this.player_.duration()); 13146 }; 13147 13148 return CurrentTimeDisplay; 13149 }(TimeDisplay); 13150 13151 /** 13152 * The text that is added to the `CurrentTimeDisplay` for screen reader users. 13153 * 13154 * @type {string} 13155 * @private 13156 */ 13157 13158 13159 CurrentTimeDisplay.prototype.labelText_ = 'Current Time'; 13160 13161 /** 13162 * The text that should display over the `CurrentTimeDisplay`s controls. Added to for localization. 13163 * 13164 * @type {string} 13165 * @private 13166 * 13167 * @deprecated in v7; controlText_ is not used in non-active display Components 13168 */ 13169 CurrentTimeDisplay.prototype.controlText_ = 'Current Time'; 13170 13171 Component.registerComponent('CurrentTimeDisplay', CurrentTimeDisplay); 13172 13173 /** 13174 * @file duration-display.js 13175 */ 13176 13177 /** 13178 * Displays the duration 13179 * 13180 * @extends Component 13181 */ 13182 13183 var DurationDisplay = function (_TimeDisplay) { 13184 inherits(DurationDisplay, _TimeDisplay); 13185 13186 /** 13187 * Creates an instance of this class. 13188 * 13189 * @param {Player} player 13190 * The `Player` that this class should be attached to. 13191 * 13192 * @param {Object} [options] 13193 * The key/value store of player options. 13194 */ 13195 function DurationDisplay(player, options) { 13196 classCallCheck(this, DurationDisplay); 13197 13198 // we do not want to/need to throttle duration changes, 13199 // as they should always display the changed duration as 13200 // it has changed 13201 var _this = possibleConstructorReturn(this, _TimeDisplay.call(this, player, options)); 13202 13203 _this.on(player, 'durationchange', _this.updateContent); 13204 13205 // Also listen for timeupdate (in the parent) and loadedmetadata because removing those 13206 // listeners could have broken dependent applications/libraries. These 13207 // can likely be removed for 7.0. 13208 _this.on(player, 'loadedmetadata', _this.throttledUpdateContent); 13209 return _this; 13210 } 13211 13212 /** 13213 * Builds the default DOM `className`. 13214 * 13215 * @return {string} 13216 * The DOM `className` for this object. 13217 */ 13218 13219 13220 DurationDisplay.prototype.buildCSSClass = function buildCSSClass() { 13221 return 'vjs-duration'; 13222 }; 13223 13224 /** 13225 * Update duration time display. 13226 * 13227 * @param {EventTarget~Event} [event] 13228 * The `durationchange`, `timeupdate`, or `loadedmetadata` event that caused 13229 * this function to be called. 13230 * 13231 * @listens Player#durationchange 13232 * @listens Player#timeupdate 13233 * @listens Player#loadedmetadata 13234 */ 13235 13236 13237 DurationDisplay.prototype.updateContent = function updateContent(event) { 13238 var duration = this.player_.duration(); 13239 13240 if (duration && this.duration_ !== duration) { 13241 this.duration_ = duration; 13242 this.updateFormattedTime_(duration); 13243 } 13244 }; 13245 13246 return DurationDisplay; 13247 }(TimeDisplay); 13248 13249 /** 13250 * The text that is added to the `DurationDisplay` for screen reader users. 13251 * 13252 * @type {string} 13253 * @private 13254 */ 13255 13256 13257 DurationDisplay.prototype.labelText_ = 'Duration'; 13258 13259 /** 13260 * The text that should display over the `DurationDisplay`s controls. Added to for localization. 13261 * 13262 * @type {string} 13263 * @private 13264 * 13265 * @deprecated in v7; controlText_ is not used in non-active display Components 13266 */ 13267 DurationDisplay.prototype.controlText_ = 'Duration'; 13268 13269 Component.registerComponent('DurationDisplay', DurationDisplay); 13270 13271 /** 13272 * @file time-divider.js 13273 */ 13274 13275 /** 13276 * The separator between the current time and duration. 13277 * Can be hidden if it's not needed in the design. 13278 * 13279 * @extends Component 13280 */ 13281 13282 var TimeDivider = function (_Component) { 13283 inherits(TimeDivider, _Component); 13284 13285 function TimeDivider() { 13286 classCallCheck(this, TimeDivider); 13287 return possibleConstructorReturn(this, _Component.apply(this, arguments)); 13288 } 13289 13290 /** 13291 * Create the component's DOM element 13292 * 13293 * @return {Element} 13294 * The element that was created. 13295 */ 13296 TimeDivider.prototype.createEl = function createEl() { 13297 return _Component.prototype.createEl.call(this, 'div', { 13298 className: 'vjs-time-control vjs-time-divider', 13299 innerHTML: '<div><span>/</span></div>' 13300 }); 13301 }; 13302 13303 return TimeDivider; 13304 }(Component); 13305 13306 Component.registerComponent('TimeDivider', TimeDivider); 13307 13308 /** 13309 * @file remaining-time-display.js 13310 */ 13311 /** 13312 * Displays the time left in the video 13313 * 13314 * @extends Component 13315 */ 13316 13317 var RemainingTimeDisplay = function (_TimeDisplay) { 13318 inherits(RemainingTimeDisplay, _TimeDisplay); 13319 13320 /** 13321 * Creates an instance of this class. 13322 * 13323 * @param {Player} player 13324 * The `Player` that this class should be attached to. 13325 * 13326 * @param {Object} [options] 13327 * The key/value store of player options. 13328 */ 13329 function RemainingTimeDisplay(player, options) { 13330 classCallCheck(this, RemainingTimeDisplay); 13331 13332 var _this = possibleConstructorReturn(this, _TimeDisplay.call(this, player, options)); 13333 13334 _this.on(player, 'durationchange', _this.throttledUpdateContent); 13335 _this.on(player, 'ended', _this.handleEnded); 13336 return _this; 13337 } 13338 13339 /** 13340 * Builds the default DOM `className`. 13341 * 13342 * @return {string} 13343 * The DOM `className` for this object. 13344 */ 13345 13346 13347 RemainingTimeDisplay.prototype.buildCSSClass = function buildCSSClass() { 13348 return 'vjs-remaining-time'; 13349 }; 13350 13351 /** 13352 * The remaining time display prefixes numbers with a "minus" character. 13353 * 13354 * @param {number} time 13355 * A numeric time, in seconds. 13356 * 13357 * @return {string} 13358 * A formatted time 13359 * 13360 * @private 13361 */ 13362 13363 13364 RemainingTimeDisplay.prototype.formatTime_ = function formatTime_(time) { 13365 // TODO: The "-" should be decorative, and not announced by a screen reader 13366 return '-' + _TimeDisplay.prototype.formatTime_.call(this, time); 13367 }; 13368 13369 /** 13370 * Update remaining time display. 13371 * 13372 * @param {EventTarget~Event} [event] 13373 * The `timeupdate` or `durationchange` event that caused this to run. 13374 * 13375 * @listens Player#timeupdate 13376 * @listens Player#durationchange 13377 */ 13378 13379 13380 RemainingTimeDisplay.prototype.updateContent = function updateContent(event) { 13381 if (!this.player_.duration()) { 13382 return; 13383 } 13384 13385 // @deprecated We should only use remainingTimeDisplay 13386 // as of video.js 7 13387 if (this.player_.remainingTimeDisplay) { 13388 this.updateFormattedTime_(this.player_.remainingTimeDisplay()); 13389 } else { 13390 this.updateFormattedTime_(this.player_.remainingTime()); 13391 } 13392 }; 13393 13394 /** 13395 * When the player fires ended there should be no time left. Sadly 13396 * this is not always the case, lets make it seem like that is the case 13397 * for users. 13398 * 13399 * @param {EventTarget~Event} [event] 13400 * The `ended` event that caused this to run. 13401 * 13402 * @listens Player#ended 13403 */ 13404 13405 13406 RemainingTimeDisplay.prototype.handleEnded = function handleEnded(event) { 13407 if (!this.player_.duration()) { 13408 return; 13409 } 13410 this.updateFormattedTime_(0); 13411 }; 13412 13413 return RemainingTimeDisplay; 13414 }(TimeDisplay); 13415 13416 /** 13417 * The text that is added to the `RemainingTimeDisplay` for screen reader users. 13418 * 13419 * @type {string} 13420 * @private 13421 */ 13422 13423 13424 RemainingTimeDisplay.prototype.labelText_ = 'Remaining Time'; 13425 13426 /** 13427 * The text that should display over the `RemainingTimeDisplay`s controls. Added to for localization. 13428 * 13429 * @type {string} 13430 * @private 13431 * 13432 * @deprecated in v7; controlText_ is not used in non-active display Components 13433 */ 13434 RemainingTimeDisplay.prototype.controlText_ = 'Remaining Time'; 13435 13436 Component.registerComponent('RemainingTimeDisplay', RemainingTimeDisplay); 13437 13438 /** 13439 * @file live-display.js 13440 */ 13441 13442 // TODO - Future make it click to snap to live 13443 13444 /** 13445 * Displays the live indicator when duration is Infinity. 13446 * 13447 * @extends Component 13448 */ 13449 13450 var LiveDisplay = function (_Component) { 13451 inherits(LiveDisplay, _Component); 13452 13453 /** 13454 * Creates an instance of this class. 13455 * 13456 * @param {Player} player 13457 * The `Player` that this class should be attached to. 13458 * 13459 * @param {Object} [options] 13460 * The key/value store of player options. 13461 */ 13462 function LiveDisplay(player, options) { 13463 classCallCheck(this, LiveDisplay); 13464 13465 var _this = possibleConstructorReturn(this, _Component.call(this, player, options)); 13466 13467 _this.updateShowing(); 13468 _this.on(_this.player(), 'durationchange', _this.updateShowing); 13469 return _this; 13470 } 13471 13472 /** 13473 * Create the `Component`'s DOM element 13474 * 13475 * @return {Element} 13476 * The element that was created. 13477 */ 13478 13479 13480 LiveDisplay.prototype.createEl = function createEl$$1() { 13481 var el = _Component.prototype.createEl.call(this, 'div', { 13482 className: 'vjs-live-control vjs-control' 13483 }); 13484 13485 this.contentEl_ = createEl('div', { 13486 className: 'vjs-live-display', 13487 innerHTML: '<span class="vjs-control-text">' + this.localize('Stream Type') + '\xA0</span>' + this.localize('LIVE') 13488 }, { 13489 'aria-live': 'off' 13490 }); 13491 13492 el.appendChild(this.contentEl_); 13493 return el; 13494 }; 13495 13496 LiveDisplay.prototype.dispose = function dispose() { 13497 this.contentEl_ = null; 13498 13499 _Component.prototype.dispose.call(this); 13500 }; 13501 13502 /** 13503 * Check the duration to see if the LiveDisplay should be showing or not. Then show/hide 13504 * it accordingly 13505 * 13506 * @param {EventTarget~Event} [event] 13507 * The {@link Player#durationchange} event that caused this function to run. 13508 * 13509 * @listens Player#durationchange 13510 */ 13511 13512 13513 LiveDisplay.prototype.updateShowing = function updateShowing(event) { 13514 if (this.player().duration() === Infinity) { 13515 this.show(); 13516 } else { 13517 this.hide(); 13518 } 13519 }; 13520 13521 return LiveDisplay; 13522 }(Component); 13523 13524 Component.registerComponent('LiveDisplay', LiveDisplay); 13525 13526 /** 13527 * @file slider.js 13528 */ 13529 13530 /** 13531 * The base functionality for a slider. Can be vertical or horizontal. 13532 * For instance the volume bar or the seek bar on a video is a slider. 13533 * 13534 * @extends Component 13535 */ 13536 13537 var Slider = function (_Component) { 13538 inherits(Slider, _Component); 13539 13540 /** 13541 * Create an instance of this class 13542 * 13543 * @param {Player} player 13544 * The `Player` that this class should be attached to. 13545 * 13546 * @param {Object} [options] 13547 * The key/value store of player options. 13548 */ 13549 function Slider(player, options) { 13550 classCallCheck(this, Slider); 13551 13552 // Set property names to bar to match with the child Slider class is looking for 13553 var _this = possibleConstructorReturn(this, _Component.call(this, player, options)); 13554 13555 _this.bar = _this.getChild(_this.options_.barName); 13556 13557 // Set a horizontal or vertical class on the slider depending on the slider type 13558 _this.vertical(!!_this.options_.vertical); 13559 13560 _this.enable(); 13561 return _this; 13562 } 13563 13564 /** 13565 * Are controls are currently enabled for this slider or not. 13566 * 13567 * @return {boolean} 13568 * true if controls are enabled, false otherwise 13569 */ 13570 13571 13572 Slider.prototype.enabled = function enabled() { 13573 return this.enabled_; 13574 }; 13575 13576 /** 13577 * Enable controls for this slider if they are disabled 13578 */ 13579 13580 13581 Slider.prototype.enable = function enable() { 13582 if (this.enabled()) { 13583 return; 13584 } 13585 13586 this.on('mousedown', this.handleMouseDown); 13587 this.on('touchstart', this.handleMouseDown); 13588 this.on('focus', this.handleFocus); 13589 this.on('blur', this.handleBlur); 13590 this.on('click', this.handleClick); 13591 13592 this.on(this.player_, 'controlsvisible', this.update); 13593 13594 if (this.playerEvent) { 13595 this.on(this.player_, this.playerEvent, this.update); 13596 } 13597 13598 this.removeClass('disabled'); 13599 this.setAttribute('tabindex', 0); 13600 13601 this.enabled_ = true; 13602 }; 13603 13604 /** 13605 * Disable controls for this slider if they are enabled 13606 */ 13607 13608 13609 Slider.prototype.disable = function disable() { 13610 if (!this.enabled()) { 13611 return; 13612 } 13613 var doc = this.bar.el_.ownerDocument; 13614 13615 this.off('mousedown', this.handleMouseDown); 13616 this.off('touchstart', this.handleMouseDown); 13617 this.off('focus', this.handleFocus); 13618 this.off('blur', this.handleBlur); 13619 this.off('click', this.handleClick); 13620 this.off(this.player_, 'controlsvisible', this.update); 13621 this.off(doc, 'mousemove', this.handleMouseMove); 13622 this.off(doc, 'mouseup', this.handleMouseUp); 13623 this.off(doc, 'touchmove', this.handleMouseMove); 13624 this.off(doc, 'touchend', this.handleMouseUp); 13625 this.removeAttribute('tabindex'); 13626 13627 this.addClass('disabled'); 13628 13629 if (this.playerEvent) { 13630 this.off(this.player_, this.playerEvent, this.update); 13631 } 13632 this.enabled_ = false; 13633 }; 13634 13635 /** 13636 * Create the `Slider`s DOM element. 13637 * 13638 * @param {string} type 13639 * Type of element to create. 13640 * 13641 * @param {Object} [props={}] 13642 * List of properties in Object form. 13643 * 13644 * @param {Object} [attributes={}] 13645 * list of attributes in Object form. 13646 * 13647 * @return {Element} 13648 * The element that gets created. 13649 */ 13650 13651 13652 Slider.prototype.createEl = function createEl$$1(type) { 13653 var props = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 13654 var attributes = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 13655 13656 // Add the slider element class to all sub classes 13657 props.className = props.className + ' vjs-slider'; 13658 props = assign({ 13659 tabIndex: 0 13660 }, props); 13661 13662 attributes = assign({ 13663 'role': 'slider', 13664 'aria-valuenow': 0, 13665 'aria-valuemin': 0, 13666 'aria-valuemax': 100, 13667 'tabIndex': 0 13668 }, attributes); 13669 13670 return _Component.prototype.createEl.call(this, type, props, attributes); 13671 }; 13672 13673 /** 13674 * Handle `mousedown` or `touchstart` events on the `Slider`. 13675 * 13676 * @param {EventTarget~Event} event 13677 * `mousedown` or `touchstart` event that triggered this function 13678 * 13679 * @listens mousedown 13680 * @listens touchstart 13681 * @fires Slider#slideractive 13682 */ 13683 13684 13685 Slider.prototype.handleMouseDown = function handleMouseDown(event) { 13686 var doc = this.bar.el_.ownerDocument; 13687 13688 if (event.type === 'mousedown') { 13689 event.preventDefault(); 13690 } 13691 // Do not call preventDefault() on touchstart in Chrome 13692 // to avoid console warnings. Use a 'touch-action: none' style 13693 // instead to prevent unintented scrolling. 13694 // https://developers.google.com/web/updates/2017/01/scrolling-intervention 13695 if (event.type === 'touchstart' && !IS_CHROME) { 13696 event.preventDefault(); 13697 } 13698 blockTextSelection(); 13699 13700 this.addClass('vjs-sliding'); 13701 /** 13702 * Triggered when the slider is in an active state 13703 * 13704 * @event Slider#slideractive 13705 * @type {EventTarget~Event} 13706 */ 13707 this.trigger('slideractive'); 13708 13709 this.on(doc, 'mousemove', this.handleMouseMove); 13710 this.on(doc, 'mouseup', this.handleMouseUp); 13711 this.on(doc, 'touchmove', this.handleMouseMove); 13712 this.on(doc, 'touchend', this.handleMouseUp); 13713 13714 this.handleMouseMove(event); 13715 }; 13716 13717 /** 13718 * Handle the `mousemove`, `touchmove`, and `mousedown` events on this `Slider`. 13719 * The `mousemove` and `touchmove` events will only only trigger this function during 13720 * `mousedown` and `touchstart`. This is due to {@link Slider#handleMouseDown} and 13721 * {@link Slider#handleMouseUp}. 13722 * 13723 * @param {EventTarget~Event} event 13724 * `mousedown`, `mousemove`, `touchstart`, or `touchmove` event that triggered 13725 * this function 13726 * 13727 * @listens mousemove 13728 * @listens touchmove 13729 */ 13730 13731 13732 Slider.prototype.handleMouseMove = function handleMouseMove(event) {}; 13733 13734 /** 13735 * Handle `mouseup` or `touchend` events on the `Slider`. 13736 * 13737 * @param {EventTarget~Event} event 13738 * `mouseup` or `touchend` event that triggered this function. 13739 * 13740 * @listens touchend 13741 * @listens mouseup 13742 * @fires Slider#sliderinactive 13743 */ 13744 13745 13746 Slider.prototype.handleMouseUp = function handleMouseUp() { 13747 var doc = this.bar.el_.ownerDocument; 13748 13749 unblockTextSelection(); 13750 13751 this.removeClass('vjs-sliding'); 13752 /** 13753 * Triggered when the slider is no longer in an active state. 13754 * 13755 * @event Slider#sliderinactive 13756 * @type {EventTarget~Event} 13757 */ 13758 this.trigger('sliderinactive'); 13759 13760 this.off(doc, 'mousemove', this.handleMouseMove); 13761 this.off(doc, 'mouseup', this.handleMouseUp); 13762 this.off(doc, 'touchmove', this.handleMouseMove); 13763 this.off(doc, 'touchend', this.handleMouseUp); 13764 13765 this.update(); 13766 }; 13767 13768 /** 13769 * Update the progress bar of the `Slider`. 13770 * 13771 * @returns {number} 13772 * The percentage of progress the progress bar represents as a 13773 * number from 0 to 1. 13774 */ 13775 13776 13777 Slider.prototype.update = function update() { 13778 13779 // In VolumeBar init we have a setTimeout for update that pops and update 13780 // to the end of the execution stack. The player is destroyed before then 13781 // update will cause an error 13782 if (!this.el_) { 13783 return; 13784 } 13785 13786 // If scrubbing, we could use a cached value to make the handle keep up 13787 // with the user's mouse. On HTML5 browsers scrubbing is really smooth, but 13788 // some flash players are slow, so we might want to utilize this later. 13789 // var progress = (this.player_.scrubbing()) ? this.player_.getCache().currentTime / this.player_.duration() : this.player_.currentTime() / this.player_.duration(); 13790 var progress = this.getPercent(); 13791 var bar = this.bar; 13792 13793 // If there's no bar... 13794 if (!bar) { 13795 return; 13796 } 13797 13798 // Protect against no duration and other division issues 13799 if (typeof progress !== 'number' || progress !== progress || progress < 0 || progress === Infinity) { 13800 progress = 0; 13801 } 13802 13803 // Convert to a percentage for setting 13804 var percentage = (progress * 100).toFixed(2) + '%'; 13805 var style = bar.el().style; 13806 13807 // Set the new bar width or height 13808 if (this.vertical()) { 13809 style.height = percentage; 13810 } else { 13811 style.width = percentage; 13812 } 13813 13814 return progress; 13815 }; 13816 13817 /** 13818 * Calculate distance for slider 13819 * 13820 * @param {EventTarget~Event} event 13821 * The event that caused this function to run. 13822 * 13823 * @return {number} 13824 * The current position of the Slider. 13825 * - position.x for vertical `Slider`s 13826 * - position.y for horizontal `Slider`s 13827 */ 13828 13829 13830 Slider.prototype.calculateDistance = function calculateDistance(event) { 13831 var position = getPointerPosition(this.el_, event); 13832 13833 if (this.vertical()) { 13834 return position.y; 13835 } 13836 return position.x; 13837 }; 13838 13839 /** 13840 * Handle a `focus` event on this `Slider`. 13841 * 13842 * @param {EventTarget~Event} event 13843 * The `focus` event that caused this function to run. 13844 * 13845 * @listens focus 13846 */ 13847 13848 13849 Slider.prototype.handleFocus = function handleFocus() { 13850 this.on(this.bar.el_.ownerDocument, 'keydown', this.handleKeyPress); 13851 }; 13852 13853 /** 13854 * Handle a `keydown` event on the `Slider`. Watches for left, rigth, up, and down 13855 * arrow keys. This function will only be called when the slider has focus. See 13856 * {@link Slider#handleFocus} and {@link Slider#handleBlur}. 13857 * 13858 * @param {EventTarget~Event} event 13859 * the `keydown` event that caused this function to run. 13860 * 13861 * @listens keydown 13862 */ 13863 13864 13865 Slider.prototype.handleKeyPress = function handleKeyPress(event) { 13866 // Left and Down Arrows 13867 if (event.which === 37 || event.which === 40) { 13868 event.preventDefault(); 13869 this.stepBack(); 13870 13871 // Up and Right Arrows 13872 } else if (event.which === 38 || event.which === 39) { 13873 event.preventDefault(); 13874 this.stepForward(); 13875 } 13876 }; 13877 13878 /** 13879 * Handle a `blur` event on this `Slider`. 13880 * 13881 * @param {EventTarget~Event} event 13882 * The `blur` event that caused this function to run. 13883 * 13884 * @listens blur 13885 */ 13886 13887 Slider.prototype.handleBlur = function handleBlur() { 13888 this.off(this.bar.el_.ownerDocument, 'keydown', this.handleKeyPress); 13889 }; 13890 13891 /** 13892 * Listener for click events on slider, used to prevent clicks 13893 * from bubbling up to parent elements like button menus. 13894 * 13895 * @param {Object} event 13896 * Event that caused this object to run 13897 */ 13898 13899 13900 Slider.prototype.handleClick = function handleClick(event) { 13901 event.stopImmediatePropagation(); 13902 event.preventDefault(); 13903 }; 13904 13905 /** 13906 * Get/set if slider is horizontal for vertical 13907 * 13908 * @param {boolean} [bool] 13909 * - true if slider is vertical, 13910 * - false is horizontal 13911 * 13912 * @return {boolean} 13913 * - true if slider is vertical, and getting 13914 * - false if the slider is horizontal, and getting 13915 */ 13916 13917 13918 Slider.prototype.vertical = function vertical(bool) { 13919 if (bool === undefined) { 13920 return this.vertical_ || false; 13921 } 13922 13923 this.vertical_ = !!bool; 13924 13925 if (this.vertical_) { 13926 this.addClass('vjs-slider-vertical'); 13927 } else { 13928 this.addClass('vjs-slider-horizontal'); 13929 } 13930 }; 13931 13932 return Slider; 13933 }(Component); 13934 13935 Component.registerComponent('Slider', Slider); 13936 13937 /** 13938 * @file load-progress-bar.js 13939 */ 13940 13941 /** 13942 * Shows loading progress 13943 * 13944 * @extends Component 13945 */ 13946 13947 var LoadProgressBar = function (_Component) { 13948 inherits(LoadProgressBar, _Component); 13949 13950 /** 13951 * Creates an instance of this class. 13952 * 13953 * @param {Player} player 13954 * The `Player` that this class should be attached to. 13955 * 13956 * @param {Object} [options] 13957 * The key/value store of player options. 13958 */ 13959 function LoadProgressBar(player, options) { 13960 classCallCheck(this, LoadProgressBar); 13961 13962 var _this = possibleConstructorReturn(this, _Component.call(this, player, options)); 13963 13964 _this.partEls_ = []; 13965 _this.on(player, 'progress', _this.update); 13966 return _this; 13967 } 13968 13969 /** 13970 * Create the `Component`'s DOM element 13971 * 13972 * @return {Element} 13973 * The element that was created. 13974 */ 13975 13976 13977 LoadProgressBar.prototype.createEl = function createEl$$1() { 13978 return _Component.prototype.createEl.call(this, 'div', { 13979 className: 'vjs-load-progress', 13980 innerHTML: '<span class="vjs-control-text"><span>' + this.localize('Loaded') + '</span>: 0%</span>' 13981 }); 13982 }; 13983 13984 LoadProgressBar.prototype.dispose = function dispose() { 13985 this.partEls_ = null; 13986 13987 _Component.prototype.dispose.call(this); 13988 }; 13989 13990 /** 13991 * Update progress bar 13992 * 13993 * @param {EventTarget~Event} [event] 13994 * The `progress` event that caused this function to run. 13995 * 13996 * @listens Player#progress 13997 */ 13998 13999 14000 LoadProgressBar.prototype.update = function update(event) { 14001 var buffered = this.player_.buffered(); 14002 var duration = this.player_.duration(); 14003 var bufferedEnd = this.player_.bufferedEnd(); 14004 var children = this.partEls_; 14005 14006 // get the percent width of a time compared to the total end 14007 var percentify = function percentify(time, end) { 14008 // no NaN 14009 var percent = time / end || 0; 14010 14011 return (percent >= 1 ? 1 : percent) * 100 + '%'; 14012 }; 14013 14014 // update the width of the progress bar 14015 this.el_.style.width = percentify(bufferedEnd, duration); 14016 14017 // add child elements to represent the individual buffered time ranges 14018 for (var i = 0; i < buffered.length; i++) { 14019 var start = buffered.start(i); 14020 var end = buffered.end(i); 14021 var part = children[i]; 14022 14023 if (!part) { 14024 part = this.el_.appendChild(createEl()); 14025 children[i] = part; 14026 } 14027 14028 // set the percent based on the width of the progress bar (bufferedEnd) 14029 part.style.left = percentify(start, bufferedEnd); 14030 part.style.width = percentify(end - start, bufferedEnd); 14031 } 14032 14033 // remove unused buffered range elements 14034 for (var _i = children.length; _i > buffered.length; _i--) { 14035 this.el_.removeChild(children[_i - 1]); 14036 } 14037 children.length = buffered.length; 14038 }; 14039 14040 return LoadProgressBar; 14041 }(Component); 14042 14043 Component.registerComponent('LoadProgressBar', LoadProgressBar); 14044 14045 /** 14046 * @file time-tooltip.js 14047 */ 14048 14049 /** 14050 * Time tooltips display a time above the progress bar. 14051 * 14052 * @extends Component 14053 */ 14054 14055 var TimeTooltip = function (_Component) { 14056 inherits(TimeTooltip, _Component); 14057 14058 function TimeTooltip() { 14059 classCallCheck(this, TimeTooltip); 14060 return possibleConstructorReturn(this, _Component.apply(this, arguments)); 14061 } 14062 14063 /** 14064 * Create the time tooltip DOM element 14065 * 14066 * @return {Element} 14067 * The element that was created. 14068 */ 14069 TimeTooltip.prototype.createEl = function createEl$$1() { 14070 return _Component.prototype.createEl.call(this, 'div', { 14071 className: 'vjs-time-tooltip' 14072 }); 14073 }; 14074 14075 /** 14076 * Updates the position of the time tooltip relative to the `SeekBar`. 14077 * 14078 * @param {Object} seekBarRect 14079 * The `ClientRect` for the {@link SeekBar} element. 14080 * 14081 * @param {number} seekBarPoint 14082 * A number from 0 to 1, representing a horizontal reference point 14083 * from the left edge of the {@link SeekBar} 14084 */ 14085 14086 14087 TimeTooltip.prototype.update = function update(seekBarRect, seekBarPoint, content) { 14088 var tooltipRect = getBoundingClientRect(this.el_); 14089 var playerRect = getBoundingClientRect(this.player_.el()); 14090 var seekBarPointPx = seekBarRect.width * seekBarPoint; 14091 14092 // do nothing if either rect isn't available 14093 // for example, if the player isn't in the DOM for testing 14094 if (!playerRect || !tooltipRect) { 14095 return; 14096 } 14097 14098 // This is the space left of the `seekBarPoint` available within the bounds 14099 // of the player. We calculate any gap between the left edge of the player 14100 // and the left edge of the `SeekBar` and add the number of pixels in the 14101 // `SeekBar` before hitting the `seekBarPoint` 14102 var spaceLeftOfPoint = seekBarRect.left - playerRect.left + seekBarPointPx; 14103 14104 // This is the space right of the `seekBarPoint` available within the bounds 14105 // of the player. We calculate the number of pixels from the `seekBarPoint` 14106 // to the right edge of the `SeekBar` and add to that any gap between the 14107 // right edge of the `SeekBar` and the player. 14108 var spaceRightOfPoint = seekBarRect.width - seekBarPointPx + (playerRect.right - seekBarRect.right); 14109 14110 // This is the number of pixels by which the tooltip will need to be pulled 14111 // further to the right to center it over the `seekBarPoint`. 14112 var pullTooltipBy = tooltipRect.width / 2; 14113 14114 // Adjust the `pullTooltipBy` distance to the left or right depending on 14115 // the results of the space calculations above. 14116 if (spaceLeftOfPoint < pullTooltipBy) { 14117 pullTooltipBy += pullTooltipBy - spaceLeftOfPoint; 14118 } else if (spaceRightOfPoint < pullTooltipBy) { 14119 pullTooltipBy = spaceRightOfPoint; 14120 } 14121 14122 // Due to the imprecision of decimal/ratio based calculations and varying 14123 // rounding behaviors, there are cases where the spacing adjustment is off 14124 // by a pixel or two. This adds insurance to these calculations. 14125 if (pullTooltipBy < 0) { 14126 pullTooltipBy = 0; 14127 } else if (pullTooltipBy > tooltipRect.width) { 14128 pullTooltipBy = tooltipRect.width; 14129 } 14130 14131 this.el_.style.right = '-' + pullTooltipBy + 'px'; 14132 textContent(this.el_, content); 14133 }; 14134 14135 return TimeTooltip; 14136 }(Component); 14137 14138 Component.registerComponent('TimeTooltip', TimeTooltip); 14139 14140 /** 14141 * @file play-progress-bar.js 14142 */ 14143 14144 /** 14145 * Used by {@link SeekBar} to display media playback progress as part of the 14146 * {@link ProgressControl}. 14147 * 14148 * @extends Component 14149 */ 14150 14151 var PlayProgressBar = function (_Component) { 14152 inherits(PlayProgressBar, _Component); 14153 14154 function PlayProgressBar() { 14155 classCallCheck(this, PlayProgressBar); 14156 return possibleConstructorReturn(this, _Component.apply(this, arguments)); 14157 } 14158 14159 /** 14160 * Create the the DOM element for this class. 14161 * 14162 * @return {Element} 14163 * The element that was created. 14164 */ 14165 PlayProgressBar.prototype.createEl = function createEl() { 14166 return _Component.prototype.createEl.call(this, 'div', { 14167 className: 'vjs-play-progress vjs-slider-bar', 14168 innerHTML: '<span class="vjs-control-text"><span>' + this.localize('Progress') + '</span>: 0%</span>' 14169 }); 14170 }; 14171 14172 /** 14173 * Enqueues updates to its own DOM as well as the DOM of its 14174 * {@link TimeTooltip} child. 14175 * 14176 * @param {Object} seekBarRect 14177 * The `ClientRect` for the {@link SeekBar} element. 14178 * 14179 * @param {number} seekBarPoint 14180 * A number from 0 to 1, representing a horizontal reference point 14181 * from the left edge of the {@link SeekBar} 14182 */ 14183 14184 14185 PlayProgressBar.prototype.update = function update(seekBarRect, seekBarPoint) { 14186 var _this2 = this; 14187 14188 // If there is an existing rAF ID, cancel it so we don't over-queue. 14189 if (this.rafId_) { 14190 this.cancelAnimationFrame(this.rafId_); 14191 } 14192 14193 this.rafId_ = this.requestAnimationFrame(function () { 14194 var time = _this2.player_.scrubbing() ? _this2.player_.getCache().currentTime : _this2.player_.currentTime(); 14195 14196 var content = formatTime(time, _this2.player_.duration()); 14197 var timeTooltip = _this2.getChild('timeTooltip'); 14198 14199 if (timeTooltip) { 14200 timeTooltip.update(seekBarRect, seekBarPoint, content); 14201 } 14202 }); 14203 }; 14204 14205 return PlayProgressBar; 14206 }(Component); 14207 14208 /** 14209 * Default options for {@link PlayProgressBar}. 14210 * 14211 * @type {Object} 14212 * @private 14213 */ 14214 14215 14216 PlayProgressBar.prototype.options_ = { 14217 children: [] 14218 }; 14219 14220 // Time tooltips should not be added to a player on mobile devices 14221 if (!IS_IOS && !IS_ANDROID) { 14222 PlayProgressBar.prototype.options_.children.push('timeTooltip'); 14223 } 14224 14225 Component.registerComponent('PlayProgressBar', PlayProgressBar); 14226 14227 /** 14228 * @file mouse-time-display.js 14229 */ 14230 14231 /** 14232 * The {@link MouseTimeDisplay} component tracks mouse movement over the 14233 * {@link ProgressControl}. It displays an indicator and a {@link TimeTooltip} 14234 * indicating the time which is represented by a given point in the 14235 * {@link ProgressControl}. 14236 * 14237 * @extends Component 14238 */ 14239 14240 var MouseTimeDisplay = function (_Component) { 14241 inherits(MouseTimeDisplay, _Component); 14242 14243 /** 14244 * Creates an instance of this class. 14245 * 14246 * @param {Player} player 14247 * The {@link Player} that this class should be attached to. 14248 * 14249 * @param {Object} [options] 14250 * The key/value store of player options. 14251 */ 14252 function MouseTimeDisplay(player, options) { 14253 classCallCheck(this, MouseTimeDisplay); 14254 14255 var _this = possibleConstructorReturn(this, _Component.call(this, player, options)); 14256 14257 _this.update = throttle(bind(_this, _this.update), 25); 14258 return _this; 14259 } 14260 14261 /** 14262 * Create the DOM element for this class. 14263 * 14264 * @return {Element} 14265 * The element that was created. 14266 */ 14267 14268 14269 MouseTimeDisplay.prototype.createEl = function createEl() { 14270 return _Component.prototype.createEl.call(this, 'div', { 14271 className: 'vjs-mouse-display' 14272 }); 14273 }; 14274 14275 /** 14276 * Enqueues updates to its own DOM as well as the DOM of its 14277 * {@link TimeTooltip} child. 14278 * 14279 * @param {Object} seekBarRect 14280 * The `ClientRect` for the {@link SeekBar} element. 14281 * 14282 * @param {number} seekBarPoint 14283 * A number from 0 to 1, representing a horizontal reference point 14284 * from the left edge of the {@link SeekBar} 14285 */ 14286 14287 14288 MouseTimeDisplay.prototype.update = function update(seekBarRect, seekBarPoint) { 14289 var _this2 = this; 14290 14291 // If there is an existing rAF ID, cancel it so we don't over-queue. 14292 if (this.rafId_) { 14293 this.cancelAnimationFrame(this.rafId_); 14294 } 14295 14296 this.rafId_ = this.requestAnimationFrame(function () { 14297 var duration = _this2.player_.duration(); 14298 var content = formatTime(seekBarPoint * duration, duration); 14299 14300 _this2.el_.style.left = seekBarRect.width * seekBarPoint + 'px'; 14301 _this2.getChild('timeTooltip').update(seekBarRect, seekBarPoint, content); 14302 }); 14303 }; 14304 14305 return MouseTimeDisplay; 14306 }(Component); 14307 14308 /** 14309 * Default options for `MouseTimeDisplay` 14310 * 14311 * @type {Object} 14312 * @private 14313 */ 14314 14315 14316 MouseTimeDisplay.prototype.options_ = { 14317 children: ['timeTooltip'] 14318 }; 14319 14320 Component.registerComponent('MouseTimeDisplay', MouseTimeDisplay); 14321 14322 /** 14323 * @file seek-bar.js 14324 */ 14325 14326 // The number of seconds the `step*` functions move the timeline. 14327 var STEP_SECONDS = 5; 14328 14329 // The interval at which the bar should update as it progresses. 14330 var UPDATE_REFRESH_INTERVAL = 30; 14331 14332 /** 14333 * Seek bar and container for the progress bars. Uses {@link PlayProgressBar} 14334 * as its `bar`. 14335 * 14336 * @extends Slider 14337 */ 14338 14339 var SeekBar = function (_Slider) { 14340 inherits(SeekBar, _Slider); 14341 14342 /** 14343 * Creates an instance of this class. 14344 * 14345 * @param {Player} player 14346 * The `Player` that this class should be attached to. 14347 * 14348 * @param {Object} [options] 14349 * The key/value store of player options. 14350 */ 14351 function SeekBar(player, options) { 14352 classCallCheck(this, SeekBar); 14353 14354 var _this = possibleConstructorReturn(this, _Slider.call(this, player, options)); 14355 14356 _this.setEventHandlers_(); 14357 return _this; 14358 } 14359 14360 /** 14361 * Sets the event handlers 14362 * 14363 * @private 14364 */ 14365 14366 14367 SeekBar.prototype.setEventHandlers_ = function setEventHandlers_() { 14368 var _this2 = this; 14369 14370 this.update = throttle(bind(this, this.update), UPDATE_REFRESH_INTERVAL); 14371 14372 this.on(this.player_, 'timeupdate', this.update); 14373 this.on(this.player_, 'ended', this.handleEnded); 14374 14375 // when playing, let's ensure we smoothly update the play progress bar 14376 // via an interval 14377 this.updateInterval = null; 14378 14379 this.on(this.player_, ['playing'], function () { 14380 _this2.clearInterval(_this2.updateInterval); 14381 14382 _this2.updateInterval = _this2.setInterval(function () { 14383 _this2.requestAnimationFrame(function () { 14384 _this2.update(); 14385 }); 14386 }, UPDATE_REFRESH_INTERVAL); 14387 }); 14388 14389 this.on(this.player_, ['ended', 'pause', 'waiting'], function () { 14390 _this2.clearInterval(_this2.updateInterval); 14391 }); 14392 14393 this.on(this.player_, ['timeupdate', 'ended'], this.update); 14394 }; 14395 14396 /** 14397 * Create the `Component`'s DOM element 14398 * 14399 * @return {Element} 14400 * The element that was created. 14401 */ 14402 14403 14404 SeekBar.prototype.createEl = function createEl$$1() { 14405 return _Slider.prototype.createEl.call(this, 'div', { 14406 className: 'vjs-progress-holder' 14407 }, { 14408 'aria-label': this.localize('Progress Bar') 14409 }); 14410 }; 14411 14412 /** 14413 * This function updates the play progress bar and accessibility 14414 * attributes to whatever is passed in. 14415 * 14416 * @param {number} currentTime 14417 * The currentTime value that should be used for accessibility 14418 * 14419 * @param {number} percent 14420 * The percentage as a decimal that the bar should be filled from 0-1. 14421 * 14422 * @private 14423 */ 14424 14425 14426 SeekBar.prototype.update_ = function update_(currentTime, percent) { 14427 var duration = this.player_.duration(); 14428 14429 // machine readable value of progress bar (percentage complete) 14430 this.el_.setAttribute('aria-valuenow', (percent * 100).toFixed(2)); 14431 14432 // human readable value of progress bar (time complete) 14433 this.el_.setAttribute('aria-valuetext', this.localize('progress bar timing: currentTime={1} duration={2}', [formatTime(currentTime, duration), formatTime(duration, duration)], '{1} of {2}')); 14434 14435 // Update the `PlayProgressBar`. 14436 this.bar.update(getBoundingClientRect(this.el_), percent); 14437 }; 14438 14439 /** 14440 * Update the seek bar's UI. 14441 * 14442 * @param {EventTarget~Event} [event] 14443 * The `timeupdate` or `ended` event that caused this to run. 14444 * 14445 * @listens Player#timeupdate 14446 * 14447 * @returns {number} 14448 * The current percent at a number from 0-1 14449 */ 14450 14451 14452 SeekBar.prototype.update = function update(event) { 14453 var percent = _Slider.prototype.update.call(this); 14454 14455 this.update_(this.getCurrentTime_(), percent); 14456 return percent; 14457 }; 14458 14459 /** 14460 * Get the value of current time but allows for smooth scrubbing, 14461 * when player can't keep up. 14462 * 14463 * @return {number} 14464 * The current time value to display 14465 * 14466 * @private 14467 */ 14468 14469 14470 SeekBar.prototype.getCurrentTime_ = function getCurrentTime_() { 14471 return this.player_.scrubbing() ? this.player_.getCache().currentTime : this.player_.currentTime(); 14472 }; 14473 14474 /** 14475 * We want the seek bar to be full on ended 14476 * no matter what the actual internal values are. so we force it. 14477 * 14478 * @param {EventTarget~Event} [event] 14479 * The `timeupdate` or `ended` event that caused this to run. 14480 * 14481 * @listens Player#ended 14482 */ 14483 14484 14485 SeekBar.prototype.handleEnded = function handleEnded(event) { 14486 this.update_(this.player_.duration(), 1); 14487 }; 14488 14489 /** 14490 * Get the percentage of media played so far. 14491 * 14492 * @return {number} 14493 * The percentage of media played so far (0 to 1). 14494 */ 14495 14496 14497 SeekBar.prototype.getPercent = function getPercent() { 14498 var percent = this.getCurrentTime_() / this.player_.duration(); 14499 14500 return percent >= 1 ? 1 : percent || 0; 14501 }; 14502 14503 /** 14504 * Handle mouse down on seek bar 14505 * 14506 * @param {EventTarget~Event} event 14507 * The `mousedown` event that caused this to run. 14508 * 14509 * @listens mousedown 14510 */ 14511 14512 14513 SeekBar.prototype.handleMouseDown = function handleMouseDown(event) { 14514 if (!isSingleLeftClick(event)) { 14515 return; 14516 } 14517 14518 // Stop event propagation to prevent double fire in progress-control.js 14519 event.stopPropagation(); 14520 this.player_.scrubbing(true); 14521 14522 this.videoWasPlaying = !this.player_.paused(); 14523 this.player_.pause(); 14524 14525 _Slider.prototype.handleMouseDown.call(this, event); 14526 }; 14527 14528 /** 14529 * Handle mouse move on seek bar 14530 * 14531 * @param {EventTarget~Event} event 14532 * The `mousemove` event that caused this to run. 14533 * 14534 * @listens mousemove 14535 */ 14536 14537 14538 SeekBar.prototype.handleMouseMove = function handleMouseMove(event) { 14539 if (!isSingleLeftClick(event)) { 14540 return; 14541 } 14542 14543 var newTime = this.calculateDistance(event) * this.player_.duration(); 14544 14545 // Don't let video end while scrubbing. 14546 if (newTime === this.player_.duration()) { 14547 newTime = newTime - 0.1; 14548 } 14549 14550 // Set new time (tell player to seek to new time) 14551 this.player_.currentTime(newTime); 14552 }; 14553 14554 SeekBar.prototype.enable = function enable() { 14555 _Slider.prototype.enable.call(this); 14556 var mouseTimeDisplay = this.getChild('mouseTimeDisplay'); 14557 14558 if (!mouseTimeDisplay) { 14559 return; 14560 } 14561 14562 mouseTimeDisplay.show(); 14563 }; 14564 14565 SeekBar.prototype.disable = function disable() { 14566 _Slider.prototype.disable.call(this); 14567 var mouseTimeDisplay = this.getChild('mouseTimeDisplay'); 14568 14569 if (!mouseTimeDisplay) { 14570 return; 14571 } 14572 14573 mouseTimeDisplay.hide(); 14574 }; 14575 14576 /** 14577 * Handle mouse up on seek bar 14578 * 14579 * @param {EventTarget~Event} event 14580 * The `mouseup` event that caused this to run. 14581 * 14582 * @listens mouseup 14583 */ 14584 14585 14586 SeekBar.prototype.handleMouseUp = function handleMouseUp(event) { 14587 _Slider.prototype.handleMouseUp.call(this, event); 14588 14589 // Stop event propagation to prevent double fire in progress-control.js 14590 if (event) { 14591 event.stopPropagation(); 14592 } 14593 this.player_.scrubbing(false); 14594 14595 /** 14596 * Trigger timeupdate because we're done seeking and the time has changed. 14597 * This is particularly useful for if the player is paused to time the time displays. 14598 * 14599 * @event Tech#timeupdate 14600 * @type {EventTarget~Event} 14601 */ 14602 this.player_.trigger({ type: 'timeupdate', target: this, manuallyTriggered: true }); 14603 if (this.videoWasPlaying) { 14604 silencePromise(this.player_.play()); 14605 } 14606 }; 14607 14608 /** 14609 * Move more quickly fast forward for keyboard-only users 14610 */ 14611 14612 14613 SeekBar.prototype.stepForward = function stepForward() { 14614 this.player_.currentTime(this.player_.currentTime() + STEP_SECONDS); 14615 }; 14616 14617 /** 14618 * Move more quickly rewind for keyboard-only users 14619 */ 14620 14621 14622 SeekBar.prototype.stepBack = function stepBack() { 14623 this.player_.currentTime(this.player_.currentTime() - STEP_SECONDS); 14624 }; 14625 14626 /** 14627 * Toggles the playback state of the player 14628 * This gets called when enter or space is used on the seekbar 14629 * 14630 * @param {EventTarget~Event} event 14631 * The `keydown` event that caused this function to be called 14632 * 14633 */ 14634 14635 14636 SeekBar.prototype.handleAction = function handleAction(event) { 14637 if (this.player_.paused()) { 14638 this.player_.play(); 14639 } else { 14640 this.player_.pause(); 14641 } 14642 }; 14643 14644 /** 14645 * Called when this SeekBar has focus and a key gets pressed down. By 14646 * default it will call `this.handleAction` when the key is space or enter. 14647 * 14648 * @param {EventTarget~Event} event 14649 * The `keydown` event that caused this function to be called. 14650 * 14651 * @listens keydown 14652 */ 14653 14654 14655 SeekBar.prototype.handleKeyPress = function handleKeyPress(event) { 14656 14657 // Support Space (32) or Enter (13) key operation to fire a click event 14658 if (event.which === 32 || event.which === 13) { 14659 event.preventDefault(); 14660 this.handleAction(event); 14661 } else if (_Slider.prototype.handleKeyPress) { 14662 14663 // Pass keypress handling up for unsupported keys 14664 _Slider.prototype.handleKeyPress.call(this, event); 14665 } 14666 }; 14667 14668 return SeekBar; 14669 }(Slider); 14670 14671 /** 14672 * Default options for the `SeekBar` 14673 * 14674 * @type {Object} 14675 * @private 14676 */ 14677 14678 14679 SeekBar.prototype.options_ = { 14680 children: ['loadProgressBar', 'playProgressBar'], 14681 barName: 'playProgressBar' 14682 }; 14683 14684 // MouseTimeDisplay tooltips should not be added to a player on mobile devices 14685 if (!IS_IOS && !IS_ANDROID) { 14686 SeekBar.prototype.options_.children.splice(1, 0, 'mouseTimeDisplay'); 14687 } 14688 14689 /** 14690 * Call the update event for this Slider when this event happens on the player. 14691 * 14692 * @type {string} 14693 */ 14694 SeekBar.prototype.playerEvent = 'timeupdate'; 14695 14696 Component.registerComponent('SeekBar', SeekBar); 14697 14698 /** 14699 * @file progress-control.js 14700 */ 14701 14702 /** 14703 * The Progress Control component contains the seek bar, load progress, 14704 * and play progress. 14705 * 14706 * @extends Component 14707 */ 14708 14709 var ProgressControl = function (_Component) { 14710 inherits(ProgressControl, _Component); 14711 14712 /** 14713 * Creates an instance of this class. 14714 * 14715 * @param {Player} player 14716 * The `Player` that this class should be attached to. 14717 * 14718 * @param {Object} [options] 14719 * The key/value store of player options. 14720 */ 14721 function ProgressControl(player, options) { 14722 classCallCheck(this, ProgressControl); 14723 14724 var _this = possibleConstructorReturn(this, _Component.call(this, player, options)); 14725 14726 _this.handleMouseMove = throttle(bind(_this, _this.handleMouseMove), 25); 14727 _this.throttledHandleMouseSeek = throttle(bind(_this, _this.handleMouseSeek), 25); 14728 14729 _this.enable(); 14730 return _this; 14731 } 14732 14733 /** 14734 * Create the `Component`'s DOM element 14735 * 14736 * @return {Element} 14737 * The element that was created. 14738 */ 14739 14740 14741 ProgressControl.prototype.createEl = function createEl$$1() { 14742 return _Component.prototype.createEl.call(this, 'div', { 14743 className: 'vjs-progress-control vjs-control' 14744 }); 14745 }; 14746 14747 /** 14748 * When the mouse moves over the `ProgressControl`, the pointer position 14749 * gets passed down to the `MouseTimeDisplay` component. 14750 * 14751 * @param {EventTarget~Event} event 14752 * The `mousemove` event that caused this function to run. 14753 * 14754 * @listen mousemove 14755 */ 14756 14757 14758 ProgressControl.prototype.handleMouseMove = function handleMouseMove(event) { 14759 var seekBar = this.getChild('seekBar'); 14760 14761 if (seekBar) { 14762 var mouseTimeDisplay = seekBar.getChild('mouseTimeDisplay'); 14763 var seekBarEl = seekBar.el(); 14764 var seekBarRect = getBoundingClientRect(seekBarEl); 14765 var seekBarPoint = getPointerPosition(seekBarEl, event).x; 14766 14767 // The default skin has a gap on either side of the `SeekBar`. This means 14768 // that it's possible to trigger this behavior outside the boundaries of 14769 // the `SeekBar`. This ensures we stay within it at all times. 14770 if (seekBarPoint > 1) { 14771 seekBarPoint = 1; 14772 } else if (seekBarPoint < 0) { 14773 seekBarPoint = 0; 14774 } 14775 14776 if (mouseTimeDisplay) { 14777 mouseTimeDisplay.update(seekBarRect, seekBarPoint); 14778 } 14779 } 14780 }; 14781 14782 /** 14783 * A throttled version of the {@link ProgressControl#handleMouseSeek} listener. 14784 * 14785 * @method ProgressControl#throttledHandleMouseSeek 14786 * @param {EventTarget~Event} event 14787 * The `mousemove` event that caused this function to run. 14788 * 14789 * @listen mousemove 14790 * @listen touchmove 14791 */ 14792 14793 /** 14794 * Handle `mousemove` or `touchmove` events on the `ProgressControl`. 14795 * 14796 * @param {EventTarget~Event} event 14797 * `mousedown` or `touchstart` event that triggered this function 14798 * 14799 * @listens mousemove 14800 * @listens touchmove 14801 */ 14802 14803 14804 ProgressControl.prototype.handleMouseSeek = function handleMouseSeek(event) { 14805 var seekBar = this.getChild('seekBar'); 14806 14807 if (seekBar) { 14808 seekBar.handleMouseMove(event); 14809 } 14810 }; 14811 14812 /** 14813 * Are controls are currently enabled for this progress control. 14814 * 14815 * @return {boolean} 14816 * true if controls are enabled, false otherwise 14817 */ 14818 14819 14820 ProgressControl.prototype.enabled = function enabled() { 14821 return this.enabled_; 14822 }; 14823 14824 /** 14825 * Disable all controls on the progress control and its children 14826 */ 14827 14828 14829 ProgressControl.prototype.disable = function disable() { 14830 this.children().forEach(function (child) { 14831 return child.disable && child.disable(); 14832 }); 14833 14834 if (!this.enabled()) { 14835 return; 14836 } 14837 14838 this.off(['mousedown', 'touchstart'], this.handleMouseDown); 14839 this.off(this.el_, 'mousemove', this.handleMouseMove); 14840 this.handleMouseUp(); 14841 14842 this.addClass('disabled'); 14843 14844 this.enabled_ = false; 14845 }; 14846 14847 /** 14848 * Enable all controls on the progress control and its children 14849 */ 14850 14851 14852 ProgressControl.prototype.enable = function enable() { 14853 this.children().forEach(function (child) { 14854 return child.enable && child.enable(); 14855 }); 14856 14857 if (this.enabled()) { 14858 return; 14859 } 14860 14861 this.on(['mousedown', 'touchstart'], this.handleMouseDown); 14862 this.on(this.el_, 'mousemove', this.handleMouseMove); 14863 this.removeClass('disabled'); 14864 14865 this.enabled_ = true; 14866 }; 14867 14868 /** 14869 * Handle `mousedown` or `touchstart` events on the `ProgressControl`. 14870 * 14871 * @param {EventTarget~Event} event 14872 * `mousedown` or `touchstart` event that triggered this function 14873 * 14874 * @listens mousedown 14875 * @listens touchstart 14876 */ 14877 14878 14879 ProgressControl.prototype.handleMouseDown = function handleMouseDown(event) { 14880 var doc = this.el_.ownerDocument; 14881 var seekBar = this.getChild('seekBar'); 14882 14883 if (seekBar) { 14884 seekBar.handleMouseDown(event); 14885 } 14886 14887 this.on(doc, 'mousemove', this.throttledHandleMouseSeek); 14888 this.on(doc, 'touchmove', this.throttledHandleMouseSeek); 14889 this.on(doc, 'mouseup', this.handleMouseUp); 14890 this.on(doc, 'touchend', this.handleMouseUp); 14891 }; 14892 14893 /** 14894 * Handle `mouseup` or `touchend` events on the `ProgressControl`. 14895 * 14896 * @param {EventTarget~Event} event 14897 * `mouseup` or `touchend` event that triggered this function. 14898 * 14899 * @listens touchend 14900 * @listens mouseup 14901 */ 14902 14903 14904 ProgressControl.prototype.handleMouseUp = function handleMouseUp(event) { 14905 var doc = this.el_.ownerDocument; 14906 var seekBar = this.getChild('seekBar'); 14907 14908 if (seekBar) { 14909 seekBar.handleMouseUp(event); 14910 } 14911 14912 this.off(doc, 'mousemove', this.throttledHandleMouseSeek); 14913 this.off(doc, 'touchmove', this.throttledHandleMouseSeek); 14914 this.off(doc, 'mouseup', this.handleMouseUp); 14915 this.off(doc, 'touchend', this.handleMouseUp); 14916 }; 14917 14918 return ProgressControl; 14919 }(Component); 14920 14921 /** 14922 * Default options for `ProgressControl` 14923 * 14924 * @type {Object} 14925 * @private 14926 */ 14927 14928 14929 ProgressControl.prototype.options_ = { 14930 children: ['seekBar'] 14931 }; 14932 14933 Component.registerComponent('ProgressControl', ProgressControl); 14934 14935 /** 14936 * @file fullscreen-toggle.js 14937 */ 14938 14939 /** 14940 * Toggle fullscreen video 14941 * 14942 * @extends Button 14943 */ 14944 14945 var FullscreenToggle = function (_Button) { 14946 inherits(FullscreenToggle, _Button); 14947 14948 /** 14949 * Creates an instance of this class. 14950 * 14951 * @param {Player} player 14952 * The `Player` that this class should be attached to. 14953 * 14954 * @param {Object} [options] 14955 * The key/value store of player options. 14956 */ 14957 function FullscreenToggle(player, options) { 14958 classCallCheck(this, FullscreenToggle); 14959 14960 var _this = possibleConstructorReturn(this, _Button.call(this, player, options)); 14961 14962 _this.on(player, 'fullscreenchange', _this.handleFullscreenChange); 14963 14964 if (document_1[FullscreenApi.fullscreenEnabled] === false) { 14965 _this.disable(); 14966 } 14967 return _this; 14968 } 14969 14970 /** 14971 * Builds the default DOM `className`. 14972 * 14973 * @return {string} 14974 * The DOM `className` for this object. 14975 */ 14976 14977 14978 FullscreenToggle.prototype.buildCSSClass = function buildCSSClass() { 14979 return 'vjs-fullscreen-control ' + _Button.prototype.buildCSSClass.call(this); 14980 }; 14981 14982 /** 14983 * Handles fullscreenchange on the player and change control text accordingly. 14984 * 14985 * @param {EventTarget~Event} [event] 14986 * The {@link Player#fullscreenchange} event that caused this function to be 14987 * called. 14988 * 14989 * @listens Player#fullscreenchange 14990 */ 14991 14992 14993 FullscreenToggle.prototype.handleFullscreenChange = function handleFullscreenChange(event) { 14994 if (this.player_.isFullscreen()) { 14995 this.controlText('Non-Fullscreen'); 14996 } else { 14997 this.controlText('Fullscreen'); 14998 } 14999 }; 15000 15001 /** 15002 * This gets called when an `FullscreenToggle` is "clicked". See 15003 * {@link ClickableComponent} for more detailed information on what a click can be. 15004 * 15005 * @param {EventTarget~Event} [event] 15006 * The `keydown`, `tap`, or `click` event that caused this function to be 15007 * called. 15008 * 15009 * @listens tap 15010 * @listens click 15011 */ 15012 15013 15014 FullscreenToggle.prototype.handleClick = function handleClick(event) { 15015 if (!this.player_.isFullscreen()) { 15016 this.player_.requestFullscreen(); 15017 } else { 15018 this.player_.exitFullscreen(); 15019 } 15020 }; 15021 15022 return FullscreenToggle; 15023 }(Button); 15024 15025 /** 15026 * The text that should display over the `FullscreenToggle`s controls. Added for localization. 15027 * 15028 * @type {string} 15029 * @private 15030 */ 15031 15032 15033 FullscreenToggle.prototype.controlText_ = 'Fullscreen'; 15034 15035 Component.registerComponent('FullscreenToggle', FullscreenToggle); 15036 15037 /** 15038 * Check if volume control is supported and if it isn't hide the 15039 * `Component` that was passed using the `vjs-hidden` class. 15040 * 15041 * @param {Component} self 15042 * The component that should be hidden if volume is unsupported 15043 * 15044 * @param {Player} player 15045 * A reference to the player 15046 * 15047 * @private 15048 */ 15049 var checkVolumeSupport = function checkVolumeSupport(self, player) { 15050 // hide volume controls when they're not supported by the current tech 15051 if (player.tech_ && !player.tech_.featuresVolumeControl) { 15052 self.addClass('vjs-hidden'); 15053 } 15054 15055 self.on(player, 'loadstart', function () { 15056 if (!player.tech_.featuresVolumeControl) { 15057 self.addClass('vjs-hidden'); 15058 } else { 15059 self.removeClass('vjs-hidden'); 15060 } 15061 }); 15062 }; 15063 15064 /** 15065 * @file volume-level.js 15066 */ 15067 15068 /** 15069 * Shows volume level 15070 * 15071 * @extends Component 15072 */ 15073 15074 var VolumeLevel = function (_Component) { 15075 inherits(VolumeLevel, _Component); 15076 15077 function VolumeLevel() { 15078 classCallCheck(this, VolumeLevel); 15079 return possibleConstructorReturn(this, _Component.apply(this, arguments)); 15080 } 15081 15082 /** 15083 * Create the `Component`'s DOM element 15084 * 15085 * @return {Element} 15086 * The element that was created. 15087 */ 15088 VolumeLevel.prototype.createEl = function createEl() { 15089 return _Component.prototype.createEl.call(this, 'div', { 15090 className: 'vjs-volume-level', 15091 innerHTML: '<span class="vjs-control-text"></span>' 15092 }); 15093 }; 15094 15095 return VolumeLevel; 15096 }(Component); 15097 15098 Component.registerComponent('VolumeLevel', VolumeLevel); 15099 15100 /** 15101 * @file volume-bar.js 15102 */ 15103 15104 /** 15105 * The bar that contains the volume level and can be clicked on to adjust the level 15106 * 15107 * @extends Slider 15108 */ 15109 15110 var VolumeBar = function (_Slider) { 15111 inherits(VolumeBar, _Slider); 15112 15113 /** 15114 * Creates an instance of this class. 15115 * 15116 * @param {Player} player 15117 * The `Player` that this class should be attached to. 15118 * 15119 * @param {Object} [options] 15120 * The key/value store of player options. 15121 */ 15122 function VolumeBar(player, options) { 15123 classCallCheck(this, VolumeBar); 15124 15125 var _this = possibleConstructorReturn(this, _Slider.call(this, player, options)); 15126 15127 _this.on('slideractive', _this.updateLastVolume_); 15128 _this.on(player, 'volumechange', _this.updateARIAAttributes); 15129 player.ready(function () { 15130 return _this.updateARIAAttributes(); 15131 }); 15132 return _this; 15133 } 15134 15135 /** 15136 * Create the `Component`'s DOM element 15137 * 15138 * @return {Element} 15139 * The element that was created. 15140 */ 15141 15142 15143 VolumeBar.prototype.createEl = function createEl$$1() { 15144 return _Slider.prototype.createEl.call(this, 'div', { 15145 className: 'vjs-volume-bar vjs-slider-bar' 15146 }, { 15147 'aria-label': this.localize('Volume Level'), 15148 'aria-live': 'polite' 15149 }); 15150 }; 15151 15152 /** 15153 * Handle mouse down on volume bar 15154 * 15155 * @param {EventTarget~Event} event 15156 * The `mousedown` event that caused this to run. 15157 * 15158 * @listens mousedown 15159 */ 15160 15161 15162 VolumeBar.prototype.handleMouseDown = function handleMouseDown(event) { 15163 if (!isSingleLeftClick(event)) { 15164 return; 15165 } 15166 15167 _Slider.prototype.handleMouseDown.call(this, event); 15168 }; 15169 15170 /** 15171 * Handle movement events on the {@link VolumeMenuButton}. 15172 * 15173 * @param {EventTarget~Event} event 15174 * The event that caused this function to run. 15175 * 15176 * @listens mousemove 15177 */ 15178 15179 15180 VolumeBar.prototype.handleMouseMove = function handleMouseMove(event) { 15181 if (!isSingleLeftClick(event)) { 15182 return; 15183 } 15184 15185 this.checkMuted(); 15186 this.player_.volume(this.calculateDistance(event)); 15187 }; 15188 15189 /** 15190 * If the player is muted unmute it. 15191 */ 15192 15193 15194 VolumeBar.prototype.checkMuted = function checkMuted() { 15195 if (this.player_.muted()) { 15196 this.player_.muted(false); 15197 } 15198 }; 15199 15200 /** 15201 * Get percent of volume level 15202 * 15203 * @return {number} 15204 * Volume level percent as a decimal number. 15205 */ 15206 15207 15208 VolumeBar.prototype.getPercent = function getPercent() { 15209 if (this.player_.muted()) { 15210 return 0; 15211 } 15212 return this.player_.volume(); 15213 }; 15214 15215 /** 15216 * Increase volume level for keyboard users 15217 */ 15218 15219 15220 VolumeBar.prototype.stepForward = function stepForward() { 15221 this.checkMuted(); 15222 this.player_.volume(this.player_.volume() + 0.1); 15223 }; 15224 15225 /** 15226 * Decrease volume level for keyboard users 15227 */ 15228 15229 15230 VolumeBar.prototype.stepBack = function stepBack() { 15231 this.checkMuted(); 15232 this.player_.volume(this.player_.volume() - 0.1); 15233 }; 15234 15235 /** 15236 * Update ARIA accessibility attributes 15237 * 15238 * @param {EventTarget~Event} [event] 15239 * The `volumechange` event that caused this function to run. 15240 * 15241 * @listens Player#volumechange 15242 */ 15243 15244 15245 VolumeBar.prototype.updateARIAAttributes = function updateARIAAttributes(event) { 15246 var ariaValue = this.player_.muted() ? 0 : this.volumeAsPercentage_(); 15247 15248 this.el_.setAttribute('aria-valuenow', ariaValue); 15249 this.el_.setAttribute('aria-valuetext', ariaValue + '%'); 15250 }; 15251 15252 /** 15253 * Returns the current value of the player volume as a percentage 15254 * 15255 * @private 15256 */ 15257 15258 15259 VolumeBar.prototype.volumeAsPercentage_ = function volumeAsPercentage_() { 15260 return Math.round(this.player_.volume() * 100); 15261 }; 15262 15263 /** 15264 * When user starts dragging the VolumeBar, store the volume and listen for 15265 * the end of the drag. When the drag ends, if the volume was set to zero, 15266 * set lastVolume to the stored volume. 15267 * 15268 * @listens slideractive 15269 * @private 15270 */ 15271 15272 15273 VolumeBar.prototype.updateLastVolume_ = function updateLastVolume_() { 15274 var _this2 = this; 15275 15276 var volumeBeforeDrag = this.player_.volume(); 15277 15278 this.one('sliderinactive', function () { 15279 if (_this2.player_.volume() === 0) { 15280 _this2.player_.lastVolume_(volumeBeforeDrag); 15281 } 15282 }); 15283 }; 15284 15285 return VolumeBar; 15286 }(Slider); 15287 15288 /** 15289 * Default options for the `VolumeBar` 15290 * 15291 * @type {Object} 15292 * @private 15293 */ 15294 15295 15296 VolumeBar.prototype.options_ = { 15297 children: ['volumeLevel'], 15298 barName: 'volumeLevel' 15299 }; 15300 15301 /** 15302 * Call the update event for this Slider when this event happens on the player. 15303 * 15304 * @type {string} 15305 */ 15306 VolumeBar.prototype.playerEvent = 'volumechange'; 15307 15308 Component.registerComponent('VolumeBar', VolumeBar); 15309 15310 /** 15311 * @file volume-control.js 15312 */ 15313 15314 /** 15315 * The component for controlling the volume level 15316 * 15317 * @extends Component 15318 */ 15319 15320 var VolumeControl = function (_Component) { 15321 inherits(VolumeControl, _Component); 15322 15323 /** 15324 * Creates an instance of this class. 15325 * 15326 * @param {Player} player 15327 * The `Player` that this class should be attached to. 15328 * 15329 * @param {Object} [options={}] 15330 * The key/value store of player options. 15331 */ 15332 function VolumeControl(player) { 15333 var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 15334 classCallCheck(this, VolumeControl); 15335 15336 options.vertical = options.vertical || false; 15337 15338 // Pass the vertical option down to the VolumeBar if 15339 // the VolumeBar is turned on. 15340 if (typeof options.volumeBar === 'undefined' || isPlain(options.volumeBar)) { 15341 options.volumeBar = options.volumeBar || {}; 15342 options.volumeBar.vertical = options.vertical; 15343 } 15344 15345 // hide this control if volume support is missing 15346 var _this = possibleConstructorReturn(this, _Component.call(this, player, options)); 15347 15348 checkVolumeSupport(_this, player); 15349 15350 _this.throttledHandleMouseMove = throttle(bind(_this, _this.handleMouseMove), 25); 15351 15352 _this.on('mousedown', _this.handleMouseDown); 15353 _this.on('touchstart', _this.handleMouseDown); 15354 15355 // while the slider is active (the mouse has been pressed down and 15356 // is dragging) or in focus we do not want to hide the VolumeBar 15357 _this.on(_this.volumeBar, ['focus', 'slideractive'], function () { 15358 _this.volumeBar.addClass('vjs-slider-active'); 15359 _this.addClass('vjs-slider-active'); 15360 _this.trigger('slideractive'); 15361 }); 15362 15363 _this.on(_this.volumeBar, ['blur', 'sliderinactive'], function () { 15364 _this.volumeBar.removeClass('vjs-slider-active'); 15365 _this.removeClass('vjs-slider-active'); 15366 _this.trigger('sliderinactive'); 15367 }); 15368 return _this; 15369 } 15370 15371 /** 15372 * Create the `Component`'s DOM element 15373 * 15374 * @return {Element} 15375 * The element that was created. 15376 */ 15377 15378 15379 VolumeControl.prototype.createEl = function createEl() { 15380 var orientationClass = 'vjs-volume-horizontal'; 15381 15382 if (this.options_.vertical) { 15383 orientationClass = 'vjs-volume-vertical'; 15384 } 15385 15386 return _Component.prototype.createEl.call(this, 'div', { 15387 className: 'vjs-volume-control vjs-control ' + orientationClass 15388 }); 15389 }; 15390 15391 /** 15392 * Handle `mousedown` or `touchstart` events on the `VolumeControl`. 15393 * 15394 * @param {EventTarget~Event} event 15395 * `mousedown` or `touchstart` event that triggered this function 15396 * 15397 * @listens mousedown 15398 * @listens touchstart 15399 */ 15400 15401 15402 VolumeControl.prototype.handleMouseDown = function handleMouseDown(event) { 15403 var doc = this.el_.ownerDocument; 15404 15405 this.on(doc, 'mousemove', this.throttledHandleMouseMove); 15406 this.on(doc, 'touchmove', this.throttledHandleMouseMove); 15407 this.on(doc, 'mouseup', this.handleMouseUp); 15408 this.on(doc, 'touchend', this.handleMouseUp); 15409 }; 15410 15411 /** 15412 * Handle `mouseup` or `touchend` events on the `VolumeControl`. 15413 * 15414 * @param {EventTarget~Event} event 15415 * `mouseup` or `touchend` event that triggered this function. 15416 * 15417 * @listens touchend 15418 * @listens mouseup 15419 */ 15420 15421 15422 VolumeControl.prototype.handleMouseUp = function handleMouseUp(event) { 15423 var doc = this.el_.ownerDocument; 15424 15425 this.off(doc, 'mousemove', this.throttledHandleMouseMove); 15426 this.off(doc, 'touchmove', this.throttledHandleMouseMove); 15427 this.off(doc, 'mouseup', this.handleMouseUp); 15428 this.off(doc, 'touchend', this.handleMouseUp); 15429 }; 15430 15431 /** 15432 * Handle `mousedown` or `touchstart` events on the `VolumeControl`. 15433 * 15434 * @param {EventTarget~Event} event 15435 * `mousedown` or `touchstart` event that triggered this function 15436 * 15437 * @listens mousedown 15438 * @listens touchstart 15439 */ 15440 15441 15442 VolumeControl.prototype.handleMouseMove = function handleMouseMove(event) { 15443 this.volumeBar.handleMouseMove(event); 15444 }; 15445 15446 return VolumeControl; 15447 }(Component); 15448 15449 /** 15450 * Default options for the `VolumeControl` 15451 * 15452 * @type {Object} 15453 * @private 15454 */ 15455 15456 15457 VolumeControl.prototype.options_ = { 15458 children: ['volumeBar'] 15459 }; 15460 15461 Component.registerComponent('VolumeControl', VolumeControl); 15462 15463 /** 15464 * Check if muting volume is supported and if it isn't hide the mute toggle 15465 * button. 15466 * 15467 * @param {Component} self 15468 * A reference to the mute toggle button 15469 * 15470 * @param {Player} player 15471 * A reference to the player 15472 * 15473 * @private 15474 */ 15475 var checkMuteSupport = function checkMuteSupport(self, player) { 15476 // hide mute toggle button if it's not supported by the current tech 15477 if (player.tech_ && !player.tech_.featuresMuteControl) { 15478 self.addClass('vjs-hidden'); 15479 } 15480 15481 self.on(player, 'loadstart', function () { 15482 if (!player.tech_.featuresMuteControl) { 15483 self.addClass('vjs-hidden'); 15484 } else { 15485 self.removeClass('vjs-hidden'); 15486 } 15487 }); 15488 }; 15489 15490 /** 15491 * @file mute-toggle.js 15492 */ 15493 15494 /** 15495 * A button component for muting the audio. 15496 * 15497 * @extends Button 15498 */ 15499 15500 var MuteToggle = function (_Button) { 15501 inherits(MuteToggle, _Button); 15502 15503 /** 15504 * Creates an instance of this class. 15505 * 15506 * @param {Player} player 15507 * The `Player` that this class should be attached to. 15508 * 15509 * @param {Object} [options] 15510 * The key/value store of player options. 15511 */ 15512 function MuteToggle(player, options) { 15513 classCallCheck(this, MuteToggle); 15514 15515 // hide this control if volume support is missing 15516 var _this = possibleConstructorReturn(this, _Button.call(this, player, options)); 15517 15518 checkMuteSupport(_this, player); 15519 15520 _this.on(player, ['loadstart', 'volumechange'], _this.update); 15521 return _this; 15522 } 15523 15524 /** 15525 * Builds the default DOM `className`. 15526 * 15527 * @return {string} 15528 * The DOM `className` for this object. 15529 */ 15530 15531 15532 MuteToggle.prototype.buildCSSClass = function buildCSSClass() { 15533 return 'vjs-mute-control ' + _Button.prototype.buildCSSClass.call(this); 15534 }; 15535 15536 /** 15537 * This gets called when an `MuteToggle` is "clicked". See 15538 * {@link ClickableComponent} for more detailed information on what a click can be. 15539 * 15540 * @param {EventTarget~Event} [event] 15541 * The `keydown`, `tap`, or `click` event that caused this function to be 15542 * called. 15543 * 15544 * @listens tap 15545 * @listens click 15546 */ 15547 15548 15549 MuteToggle.prototype.handleClick = function handleClick(event) { 15550 var vol = this.player_.volume(); 15551 var lastVolume = this.player_.lastVolume_(); 15552 15553 if (vol === 0) { 15554 var volumeToSet = lastVolume < 0.1 ? 0.1 : lastVolume; 15555 15556 this.player_.volume(volumeToSet); 15557 this.player_.muted(false); 15558 } else { 15559 this.player_.muted(this.player_.muted() ? false : true); 15560 } 15561 }; 15562 15563 /** 15564 * Update the `MuteToggle` button based on the state of `volume` and `muted` 15565 * on the player. 15566 * 15567 * @param {EventTarget~Event} [event] 15568 * The {@link Player#loadstart} event if this function was called 15569 * through an event. 15570 * 15571 * @listens Player#loadstart 15572 * @listens Player#volumechange 15573 */ 15574 15575 15576 MuteToggle.prototype.update = function update(event) { 15577 this.updateIcon_(); 15578 this.updateControlText_(); 15579 }; 15580 15581 /** 15582 * Update the appearance of the `MuteToggle` icon. 15583 * 15584 * Possible states (given `level` variable below): 15585 * - 0: crossed out 15586 * - 1: zero bars of volume 15587 * - 2: one bar of volume 15588 * - 3: two bars of volume 15589 * 15590 * @private 15591 */ 15592 15593 15594 MuteToggle.prototype.updateIcon_ = function updateIcon_() { 15595 var vol = this.player_.volume(); 15596 var level = 3; 15597 15598 // in iOS when a player is loaded with muted attribute 15599 // and volume is changed with a native mute button 15600 // we want to make sure muted state is updated 15601 if (IS_IOS) { 15602 this.player_.muted(this.player_.tech_.el_.muted); 15603 } 15604 15605 if (vol === 0 || this.player_.muted()) { 15606 level = 0; 15607 } else if (vol < 0.33) { 15608 level = 1; 15609 } else if (vol < 0.67) { 15610 level = 2; 15611 } 15612 15613 // TODO improve muted icon classes 15614 for (var i = 0; i < 4; i++) { 15615 removeClass(this.el_, 'vjs-vol-' + i); 15616 } 15617 addClass(this.el_, 'vjs-vol-' + level); 15618 }; 15619 15620 /** 15621 * If `muted` has changed on the player, update the control text 15622 * (`title` attribute on `vjs-mute-control` element and content of 15623 * `vjs-control-text` element). 15624 * 15625 * @private 15626 */ 15627 15628 15629 MuteToggle.prototype.updateControlText_ = function updateControlText_() { 15630 var soundOff = this.player_.muted() || this.player_.volume() === 0; 15631 var text = soundOff ? 'Unmute' : 'Mute'; 15632 15633 if (this.controlText() !== text) { 15634 this.controlText(text); 15635 } 15636 }; 15637 15638 return MuteToggle; 15639 }(Button); 15640 15641 /** 15642 * The text that should display over the `MuteToggle`s controls. Added for localization. 15643 * 15644 * @type {string} 15645 * @private 15646 */ 15647 15648 15649 MuteToggle.prototype.controlText_ = 'Mute'; 15650 15651 Component.registerComponent('MuteToggle', MuteToggle); 15652 15653 /** 15654 * @file volume-control.js 15655 */ 15656 15657 /** 15658 * A Component to contain the MuteToggle and VolumeControl so that 15659 * they can work together. 15660 * 15661 * @extends Component 15662 */ 15663 15664 var VolumePanel = function (_Component) { 15665 inherits(VolumePanel, _Component); 15666 15667 /** 15668 * Creates an instance of this class. 15669 * 15670 * @param {Player} player 15671 * The `Player` that this class should be attached to. 15672 * 15673 * @param {Object} [options={}] 15674 * The key/value store of player options. 15675 */ 15676 function VolumePanel(player) { 15677 var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 15678 classCallCheck(this, VolumePanel); 15679 15680 if (typeof options.inline !== 'undefined') { 15681 options.inline = options.inline; 15682 } else { 15683 options.inline = true; 15684 } 15685 15686 // pass the inline option down to the VolumeControl as vertical if 15687 // the VolumeControl is on. 15688 if (typeof options.volumeControl === 'undefined' || isPlain(options.volumeControl)) { 15689 options.volumeControl = options.volumeControl || {}; 15690 options.volumeControl.vertical = !options.inline; 15691 } 15692 15693 var _this = possibleConstructorReturn(this, _Component.call(this, player, options)); 15694 15695 _this.on(player, ['loadstart'], _this.volumePanelState_); 15696 15697 // while the slider is active (the mouse has been pressed down and 15698 // is dragging) we do not want to hide the VolumeBar 15699 _this.on(_this.volumeControl, ['slideractive'], _this.sliderActive_); 15700 15701 _this.on(_this.volumeControl, ['sliderinactive'], _this.sliderInactive_); 15702 return _this; 15703 } 15704 15705 /** 15706 * Add vjs-slider-active class to the VolumePanel 15707 * 15708 * @listens VolumeControl#slideractive 15709 * @private 15710 */ 15711 15712 15713 VolumePanel.prototype.sliderActive_ = function sliderActive_() { 15714 this.addClass('vjs-slider-active'); 15715 }; 15716 15717 /** 15718 * Removes vjs-slider-active class to the VolumePanel 15719 * 15720 * @listens VolumeControl#sliderinactive 15721 * @private 15722 */ 15723 15724 15725 VolumePanel.prototype.sliderInactive_ = function sliderInactive_() { 15726 this.removeClass('vjs-slider-active'); 15727 }; 15728 15729 /** 15730 * Adds vjs-hidden or vjs-mute-toggle-only to the VolumePanel 15731 * depending on MuteToggle and VolumeControl state 15732 * 15733 * @listens Player#loadstart 15734 * @private 15735 */ 15736 15737 15738 VolumePanel.prototype.volumePanelState_ = function volumePanelState_() { 15739 // hide volume panel if neither volume control or mute toggle 15740 // are displayed 15741 if (this.volumeControl.hasClass('vjs-hidden') && this.muteToggle.hasClass('vjs-hidden')) { 15742 this.addClass('vjs-hidden'); 15743 } 15744 15745 // if only mute toggle is visible we don't want 15746 // volume panel expanding when hovered or active 15747 if (this.volumeControl.hasClass('vjs-hidden') && !this.muteToggle.hasClass('vjs-hidden')) { 15748 this.addClass('vjs-mute-toggle-only'); 15749 } 15750 }; 15751 15752 /** 15753 * Create the `Component`'s DOM element 15754 * 15755 * @return {Element} 15756 * The element that was created. 15757 */ 15758 15759 15760 VolumePanel.prototype.createEl = function createEl() { 15761 var orientationClass = 'vjs-volume-panel-horizontal'; 15762 15763 if (!this.options_.inline) { 15764 orientationClass = 'vjs-volume-panel-vertical'; 15765 } 15766 15767 return _Component.prototype.createEl.call(this, 'div', { 15768 className: 'vjs-volume-panel vjs-control ' + orientationClass 15769 }); 15770 }; 15771 15772 return VolumePanel; 15773 }(Component); 15774 15775 /** 15776 * Default options for the `VolumeControl` 15777 * 15778 * @type {Object} 15779 * @private 15780 */ 15781 15782 15783 VolumePanel.prototype.options_ = { 15784 children: ['muteToggle', 'volumeControl'] 15785 }; 15786 15787 Component.registerComponent('VolumePanel', VolumePanel); 15788 15789 /** 15790 * @file menu.js 15791 */ 15792 15793 /** 15794 * The Menu component is used to build popup menus, including subtitle and 15795 * captions selection menus. 15796 * 15797 * @extends Component 15798 */ 15799 15800 var Menu = function (_Component) { 15801 inherits(Menu, _Component); 15802 15803 /** 15804 * Create an instance of this class. 15805 * 15806 * @param {Player} player 15807 * the player that this component should attach to 15808 * 15809 * @param {Object} [options] 15810 * Object of option names and values 15811 * 15812 */ 15813 function Menu(player, options) { 15814 classCallCheck(this, Menu); 15815 15816 var _this = possibleConstructorReturn(this, _Component.call(this, player, options)); 15817 15818 if (options) { 15819 _this.menuButton_ = options.menuButton; 15820 } 15821 15822 _this.focusedChild_ = -1; 15823 15824 _this.on('keydown', _this.handleKeyPress); 15825 return _this; 15826 } 15827 15828 /** 15829 * Add a {@link MenuItem} to the menu. 15830 * 15831 * @param {Object|string} component 15832 * The name or instance of the `MenuItem` to add. 15833 * 15834 */ 15835 15836 15837 Menu.prototype.addItem = function addItem(component) { 15838 this.addChild(component); 15839 component.on('click', bind(this, function (event) { 15840 // Unpress the associated MenuButton, and move focus back to it 15841 if (this.menuButton_) { 15842 this.menuButton_.unpressButton(); 15843 15844 // don't focus menu button if item is a caption settings item 15845 // because focus will move elsewhere 15846 if (component.name() !== 'CaptionSettingsMenuItem') { 15847 this.menuButton_.focus(); 15848 } 15849 } 15850 })); 15851 }; 15852 15853 /** 15854 * Create the `Menu`s DOM element. 15855 * 15856 * @return {Element} 15857 * the element that was created 15858 */ 15859 15860 15861 Menu.prototype.createEl = function createEl$$1() { 15862 var contentElType = this.options_.contentElType || 'ul'; 15863 15864 this.contentEl_ = createEl(contentElType, { 15865 className: 'vjs-menu-content' 15866 }); 15867 15868 this.contentEl_.setAttribute('role', 'menu'); 15869 15870 var el = _Component.prototype.createEl.call(this, 'div', { 15871 append: this.contentEl_, 15872 className: 'vjs-menu' 15873 }); 15874 15875 el.appendChild(this.contentEl_); 15876 15877 // Prevent clicks from bubbling up. Needed for Menu Buttons, 15878 // where a click on the parent is significant 15879 on(el, 'click', function (event) { 15880 event.preventDefault(); 15881 event.stopImmediatePropagation(); 15882 }); 15883 15884 return el; 15885 }; 15886 15887 Menu.prototype.dispose = function dispose() { 15888 this.contentEl_ = null; 15889 15890 _Component.prototype.dispose.call(this); 15891 }; 15892 15893 /** 15894 * Handle a `keydown` event on this menu. This listener is added in the constructor. 15895 * 15896 * @param {EventTarget~Event} event 15897 * A `keydown` event that happened on the menu. 15898 * 15899 * @listens keydown 15900 */ 15901 15902 15903 Menu.prototype.handleKeyPress = function handleKeyPress(event) { 15904 // Left and Down Arrows 15905 if (event.which === 37 || event.which === 40) { 15906 event.preventDefault(); 15907 this.stepForward(); 15908 15909 // Up and Right Arrows 15910 } else if (event.which === 38 || event.which === 39) { 15911 event.preventDefault(); 15912 this.stepBack(); 15913 } 15914 }; 15915 15916 /** 15917 * Move to next (lower) menu item for keyboard users. 15918 */ 15919 15920 15921 Menu.prototype.stepForward = function stepForward() { 15922 var stepChild = 0; 15923 15924 if (this.focusedChild_ !== undefined) { 15925 stepChild = this.focusedChild_ + 1; 15926 } 15927 this.focus(stepChild); 15928 }; 15929 15930 /** 15931 * Move to previous (higher) menu item for keyboard users. 15932 */ 15933 15934 15935 Menu.prototype.stepBack = function stepBack() { 15936 var stepChild = 0; 15937 15938 if (this.focusedChild_ !== undefined) { 15939 stepChild = this.focusedChild_ - 1; 15940 } 15941 this.focus(stepChild); 15942 }; 15943 15944 /** 15945 * Set focus on a {@link MenuItem} in the `Menu`. 15946 * 15947 * @param {Object|string} [item=0] 15948 * Index of child item set focus on. 15949 */ 15950 15951 15952 Menu.prototype.focus = function focus() { 15953 var item = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; 15954 15955 var children = this.children().slice(); 15956 var haveTitle = children.length && children[0].className && /vjs-menu-title/.test(children[0].className); 15957 15958 if (haveTitle) { 15959 children.shift(); 15960 } 15961 15962 if (children.length > 0) { 15963 if (item < 0) { 15964 item = 0; 15965 } else if (item >= children.length) { 15966 item = children.length - 1; 15967 } 15968 15969 this.focusedChild_ = item; 15970 15971 children[item].el_.focus(); 15972 } 15973 }; 15974 15975 return Menu; 15976 }(Component); 15977 15978 Component.registerComponent('Menu', Menu); 15979 15980 /** 15981 * @file menu-button.js 15982 */ 15983 15984 /** 15985 * A `MenuButton` class for any popup {@link Menu}. 15986 * 15987 * @extends Component 15988 */ 15989 15990 var MenuButton = function (_Component) { 15991 inherits(MenuButton, _Component); 15992 15993 /** 15994 * Creates an instance of this class. 15995 * 15996 * @param {Player} player 15997 * The `Player` that this class should be attached to. 15998 * 15999 * @param {Object} [options={}] 16000 * The key/value store of player options. 16001 */ 16002 function MenuButton(player) { 16003 var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 16004 classCallCheck(this, MenuButton); 16005 16006 var _this = possibleConstructorReturn(this, _Component.call(this, player, options)); 16007 16008 _this.menuButton_ = new Button(player, options); 16009 16010 _this.menuButton_.controlText(_this.controlText_); 16011 _this.menuButton_.el_.setAttribute('aria-haspopup', 'true'); 16012 16013 // Add buildCSSClass values to the button, not the wrapper 16014 var buttonClass = Button.prototype.buildCSSClass(); 16015 16016 _this.menuButton_.el_.className = _this.buildCSSClass() + ' ' + buttonClass; 16017 _this.menuButton_.removeClass('vjs-control'); 16018 16019 _this.addChild(_this.menuButton_); 16020 16021 _this.update(); 16022 16023 _this.enabled_ = true; 16024 16025 _this.on(_this.menuButton_, 'tap', _this.handleClick); 16026 _this.on(_this.menuButton_, 'click', _this.handleClick); 16027 _this.on(_this.menuButton_, 'focus', _this.handleFocus); 16028 _this.on(_this.menuButton_, 'blur', _this.handleBlur); 16029 16030 _this.on('keydown', _this.handleSubmenuKeyPress); 16031 return _this; 16032 } 16033 16034 /** 16035 * Update the menu based on the current state of its items. 16036 */ 16037 16038 16039 MenuButton.prototype.update = function update() { 16040 var menu = this.createMenu(); 16041 16042 if (this.menu) { 16043 this.menu.dispose(); 16044 this.removeChild(this.menu); 16045 } 16046 16047 this.menu = menu; 16048 this.addChild(menu); 16049 16050 /** 16051 * Track the state of the menu button 16052 * 16053 * @type {Boolean} 16054 * @private 16055 */ 16056 this.buttonPressed_ = false; 16057 this.menuButton_.el_.setAttribute('aria-expanded', 'false'); 16058 16059 if (this.items && this.items.length <= this.hideThreshold_) { 16060 this.hide(); 16061 } else { 16062 this.show(); 16063 } 16064 }; 16065 16066 /** 16067 * Create the menu and add all items to it. 16068 * 16069 * @return {Menu} 16070 * The constructed menu 16071 */ 16072 16073 16074 MenuButton.prototype.createMenu = function createMenu() { 16075 var menu = new Menu(this.player_, { menuButton: this }); 16076 16077 /** 16078 * Hide the menu if the number of items is less than or equal to this threshold. This defaults 16079 * to 0 and whenever we add items which can be hidden to the menu we'll increment it. We list 16080 * it here because every time we run `createMenu` we need to reset the value. 16081 * 16082 * @protected 16083 * @type {Number} 16084 */ 16085 this.hideThreshold_ = 0; 16086 16087 // Add a title list item to the top 16088 if (this.options_.title) { 16089 var title = createEl('li', { 16090 className: 'vjs-menu-title', 16091 innerHTML: toTitleCase(this.options_.title), 16092 tabIndex: -1 16093 }); 16094 16095 this.hideThreshold_ += 1; 16096 16097 menu.children_.unshift(title); 16098 prependTo(title, menu.contentEl()); 16099 } 16100 16101 this.items = this.createItems(); 16102 16103 if (this.items) { 16104 // Add menu items to the menu 16105 for (var i = 0; i < this.items.length; i++) { 16106 menu.addItem(this.items[i]); 16107 } 16108 } 16109 16110 return menu; 16111 }; 16112 16113 /** 16114 * Create the list of menu items. Specific to each subclass. 16115 * 16116 * @abstract 16117 */ 16118 16119 16120 MenuButton.prototype.createItems = function createItems() {}; 16121 16122 /** 16123 * Create the `MenuButtons`s DOM element. 16124 * 16125 * @return {Element} 16126 * The element that gets created. 16127 */ 16128 16129 16130 MenuButton.prototype.createEl = function createEl$$1() { 16131 return _Component.prototype.createEl.call(this, 'div', { 16132 className: this.buildWrapperCSSClass() 16133 }, {}); 16134 }; 16135 16136 /** 16137 * Allow sub components to stack CSS class names for the wrapper element 16138 * 16139 * @return {string} 16140 * The constructed wrapper DOM `className` 16141 */ 16142 16143 16144 MenuButton.prototype.buildWrapperCSSClass = function buildWrapperCSSClass() { 16145 var menuButtonClass = 'vjs-menu-button'; 16146 16147 // If the inline option is passed, we want to use different styles altogether. 16148 if (this.options_.inline === true) { 16149 menuButtonClass += '-inline'; 16150 } else { 16151 menuButtonClass += '-popup'; 16152 } 16153 16154 // TODO: Fix the CSS so that this isn't necessary 16155 var buttonClass = Button.prototype.buildCSSClass(); 16156 16157 return 'vjs-menu-button ' + menuButtonClass + ' ' + buttonClass + ' ' + _Component.prototype.buildCSSClass.call(this); 16158 }; 16159 16160 /** 16161 * Builds the default DOM `className`. 16162 * 16163 * @return {string} 16164 * The DOM `className` for this object. 16165 */ 16166 16167 16168 MenuButton.prototype.buildCSSClass = function buildCSSClass() { 16169 var menuButtonClass = 'vjs-menu-button'; 16170 16171 // If the inline option is passed, we want to use different styles altogether. 16172 if (this.options_.inline === true) { 16173 menuButtonClass += '-inline'; 16174 } else { 16175 menuButtonClass += '-popup'; 16176 } 16177 16178 return 'vjs-menu-button ' + menuButtonClass + ' ' + _Component.prototype.buildCSSClass.call(this); 16179 }; 16180 16181 /** 16182 * Get or set the localized control text that will be used for accessibility. 16183 * 16184 * > NOTE: This will come from the internal `menuButton_` element. 16185 * 16186 * @param {string} [text] 16187 * Control text for element. 16188 * 16189 * @param {Element} [el=this.menuButton_.el()] 16190 * Element to set the title on. 16191 * 16192 * @return {string} 16193 * - The control text when getting 16194 */ 16195 16196 16197 MenuButton.prototype.controlText = function controlText(text) { 16198 var el = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.menuButton_.el(); 16199 16200 return this.menuButton_.controlText(text, el); 16201 }; 16202 16203 /** 16204 * Handle a click on a `MenuButton`. 16205 * See {@link ClickableComponent#handleClick} for instances where this is called. 16206 * 16207 * @param {EventTarget~Event} event 16208 * The `keydown`, `tap`, or `click` event that caused this function to be 16209 * called. 16210 * 16211 * @listens tap 16212 * @listens click 16213 */ 16214 16215 16216 MenuButton.prototype.handleClick = function handleClick(event) { 16217 // When you click the button it adds focus, which will show the menu. 16218 // So we'll remove focus when the mouse leaves the button. Focus is needed 16219 // for tab navigation. 16220 16221 this.one(this.menu.contentEl(), 'mouseleave', bind(this, function (e) { 16222 this.unpressButton(); 16223 this.el_.blur(); 16224 })); 16225 if (this.buttonPressed_) { 16226 this.unpressButton(); 16227 } else { 16228 this.pressButton(); 16229 } 16230 }; 16231 16232 /** 16233 * Set the focus to the actual button, not to this element 16234 */ 16235 16236 16237 MenuButton.prototype.focus = function focus() { 16238 this.menuButton_.focus(); 16239 }; 16240 16241 /** 16242 * Remove the focus from the actual button, not this element 16243 */ 16244 16245 16246 MenuButton.prototype.blur = function blur() { 16247 this.menuButton_.blur(); 16248 }; 16249 16250 /** 16251 * This gets called when a `MenuButton` gains focus via a `focus` event. 16252 * Turns on listening for `keydown` events. When they happen it 16253 * calls `this.handleKeyPress`. 16254 * 16255 * @param {EventTarget~Event} event 16256 * The `focus` event that caused this function to be called. 16257 * 16258 * @listens focus 16259 */ 16260 16261 16262 MenuButton.prototype.handleFocus = function handleFocus() { 16263 on(document_1, 'keydown', bind(this, this.handleKeyPress)); 16264 }; 16265 16266 /** 16267 * Called when a `MenuButton` loses focus. Turns off the listener for 16268 * `keydown` events. Which Stops `this.handleKeyPress` from getting called. 16269 * 16270 * @param {EventTarget~Event} event 16271 * The `blur` event that caused this function to be called. 16272 * 16273 * @listens blur 16274 */ 16275 16276 16277 MenuButton.prototype.handleBlur = function handleBlur() { 16278 off(document_1, 'keydown', bind(this, this.handleKeyPress)); 16279 }; 16280 16281 /** 16282 * Handle tab, escape, down arrow, and up arrow keys for `MenuButton`. See 16283 * {@link ClickableComponent#handleKeyPress} for instances where this is called. 16284 * 16285 * @param {EventTarget~Event} event 16286 * The `keydown` event that caused this function to be called. 16287 * 16288 * @listens keydown 16289 */ 16290 16291 16292 MenuButton.prototype.handleKeyPress = function handleKeyPress(event) { 16293 16294 // Escape (27) key or Tab (9) key unpress the 'button' 16295 if (event.which === 27 || event.which === 9) { 16296 if (this.buttonPressed_) { 16297 this.unpressButton(); 16298 } 16299 // Don't preventDefault for Tab key - we still want to lose focus 16300 if (event.which !== 9) { 16301 event.preventDefault(); 16302 // Set focus back to the menu button's button 16303 this.menuButton_.el_.focus(); 16304 } 16305 // Up (38) key or Down (40) key press the 'button' 16306 } else if (event.which === 38 || event.which === 40) { 16307 if (!this.buttonPressed_) { 16308 this.pressButton(); 16309 event.preventDefault(); 16310 } 16311 } 16312 }; 16313 16314 /** 16315 * Handle a `keydown` event on a sub-menu. The listener for this is added in 16316 * the constructor. 16317 * 16318 * @param {EventTarget~Event} event 16319 * Key press event 16320 * 16321 * @listens keydown 16322 */ 16323 16324 16325 MenuButton.prototype.handleSubmenuKeyPress = function handleSubmenuKeyPress(event) { 16326 16327 // Escape (27) key or Tab (9) key unpress the 'button' 16328 if (event.which === 27 || event.which === 9) { 16329 if (this.buttonPressed_) { 16330 this.unpressButton(); 16331 } 16332 // Don't preventDefault for Tab key - we still want to lose focus 16333 if (event.which !== 9) { 16334 event.preventDefault(); 16335 // Set focus back to the menu button's button 16336 this.menuButton_.el_.focus(); 16337 } 16338 } 16339 }; 16340 16341 /** 16342 * Put the current `MenuButton` into a pressed state. 16343 */ 16344 16345 16346 MenuButton.prototype.pressButton = function pressButton() { 16347 if (this.enabled_) { 16348 this.buttonPressed_ = true; 16349 this.menu.lockShowing(); 16350 this.menuButton_.el_.setAttribute('aria-expanded', 'true'); 16351 16352 // set the focus into the submenu, except on iOS where it is resulting in 16353 // undesired scrolling behavior when the player is in an iframe 16354 if (IS_IOS && isInFrame()) { 16355 // Return early so that the menu isn't focused 16356 return; 16357 } 16358 16359 this.menu.focus(); 16360 } 16361 }; 16362 16363 /** 16364 * Take the current `MenuButton` out of a pressed state. 16365 */ 16366 16367 16368 MenuButton.prototype.unpressButton = function unpressButton() { 16369 if (this.enabled_) { 16370 this.buttonPressed_ = false; 16371 this.menu.unlockShowing(); 16372 this.menuButton_.el_.setAttribute('aria-expanded', 'false'); 16373 } 16374 }; 16375 16376 /** 16377 * Disable the `MenuButton`. Don't allow it to be clicked. 16378 */ 16379 16380 16381 MenuButton.prototype.disable = function disable() { 16382 this.unpressButton(); 16383 16384 this.enabled_ = false; 16385 this.addClass('vjs-disabled'); 16386 16387 this.menuButton_.disable(); 16388 }; 16389 16390 /** 16391 * Enable the `MenuButton`. Allow it to be clicked. 16392 */ 16393 16394 16395 MenuButton.prototype.enable = function enable() { 16396 this.enabled_ = true; 16397 this.removeClass('vjs-disabled'); 16398 16399 this.menuButton_.enable(); 16400 }; 16401 16402 return MenuButton; 16403 }(Component); 16404 16405 Component.registerComponent('MenuButton', MenuButton); 16406 16407 /** 16408 * @file track-button.js 16409 */ 16410 16411 /** 16412 * The base class for buttons that toggle specific track types (e.g. subtitles). 16413 * 16414 * @extends MenuButton 16415 */ 16416 16417 var TrackButton = function (_MenuButton) { 16418 inherits(TrackButton, _MenuButton); 16419 16420 /** 16421 * Creates an instance of this class. 16422 * 16423 * @param {Player} player 16424 * The `Player` that this class should be attached to. 16425 * 16426 * @param {Object} [options] 16427 * The key/value store of player options. 16428 */ 16429 function TrackButton(player, options) { 16430 classCallCheck(this, TrackButton); 16431 16432 var tracks = options.tracks; 16433 16434 var _this = possibleConstructorReturn(this, _MenuButton.call(this, player, options)); 16435 16436 if (_this.items.length <= 1) { 16437 _this.hide(); 16438 } 16439 16440 if (!tracks) { 16441 return possibleConstructorReturn(_this); 16442 } 16443 16444 var updateHandler = bind(_this, _this.update); 16445 16446 tracks.addEventListener('removetrack', updateHandler); 16447 tracks.addEventListener('addtrack', updateHandler); 16448 _this.player_.on('ready', updateHandler); 16449 16450 _this.player_.on('dispose', function () { 16451 tracks.removeEventListener('removetrack', updateHandler); 16452 tracks.removeEventListener('addtrack', updateHandler); 16453 }); 16454 return _this; 16455 } 16456 16457 return TrackButton; 16458 }(MenuButton); 16459 16460 Component.registerComponent('TrackButton', TrackButton); 16461 16462 /** 16463 * @file menu-item.js 16464 */ 16465 16466 /** 16467 * The component for a menu item. `<li>` 16468 * 16469 * @extends ClickableComponent 16470 */ 16471 16472 var MenuItem = function (_ClickableComponent) { 16473 inherits(MenuItem, _ClickableComponent); 16474 16475 /** 16476 * Creates an instance of the this class. 16477 * 16478 * @param {Player} player 16479 * The `Player` that this class should be attached to. 16480 * 16481 * @param {Object} [options={}] 16482 * The key/value store of player options. 16483 * 16484 */ 16485 function MenuItem(player, options) { 16486 classCallCheck(this, MenuItem); 16487 16488 var _this = possibleConstructorReturn(this, _ClickableComponent.call(this, player, options)); 16489 16490 _this.selectable = options.selectable; 16491 _this.isSelected_ = options.selected || false; 16492 _this.multiSelectable = options.multiSelectable; 16493 16494 _this.selected(_this.isSelected_); 16495 16496 if (_this.selectable) { 16497 if (_this.multiSelectable) { 16498 _this.el_.setAttribute('role', 'menuitemcheckbox'); 16499 } else { 16500 _this.el_.setAttribute('role', 'menuitemradio'); 16501 } 16502 } else { 16503 _this.el_.setAttribute('role', 'menuitem'); 16504 } 16505 return _this; 16506 } 16507 16508 /** 16509 * Create the `MenuItem's DOM element 16510 * 16511 * @param {string} [type=li] 16512 * Element's node type, not actually used, always set to `li`. 16513 * 16514 * @param {Object} [props={}] 16515 * An object of properties that should be set on the element 16516 * 16517 * @param {Object} [attrs={}] 16518 * An object of attributes that should be set on the element 16519 * 16520 * @return {Element} 16521 * The element that gets created. 16522 */ 16523 16524 16525 MenuItem.prototype.createEl = function createEl(type, props, attrs) { 16526 // The control is textual, not just an icon 16527 this.nonIconControl = true; 16528 16529 return _ClickableComponent.prototype.createEl.call(this, 'li', assign({ 16530 className: 'vjs-menu-item', 16531 innerHTML: '<span class="vjs-menu-item-text">' + this.localize(this.options_.label) + '</span>', 16532 tabIndex: -1 16533 }, props), attrs); 16534 }; 16535 16536 /** 16537 * Any click on a `MenuItem` puts it into the selected state. 16538 * See {@link ClickableComponent#handleClick} for instances where this is called. 16539 * 16540 * @param {EventTarget~Event} event 16541 * The `keydown`, `tap`, or `click` event that caused this function to be 16542 * called. 16543 * 16544 * @listens tap 16545 * @listens click 16546 */ 16547 16548 16549 MenuItem.prototype.handleClick = function handleClick(event) { 16550 this.selected(true); 16551 }; 16552 16553 /** 16554 * Set the state for this menu item as selected or not. 16555 * 16556 * @param {boolean} selected 16557 * if the menu item is selected or not 16558 */ 16559 16560 16561 MenuItem.prototype.selected = function selected(_selected) { 16562 if (this.selectable) { 16563 if (_selected) { 16564 this.addClass('vjs-selected'); 16565 this.el_.setAttribute('aria-checked', 'true'); 16566 // aria-checked isn't fully supported by browsers/screen readers, 16567 // so indicate selected state to screen reader in the control text. 16568 this.controlText(', selected'); 16569 this.isSelected_ = true; 16570 } else { 16571 this.removeClass('vjs-selected'); 16572 this.el_.setAttribute('aria-checked', 'false'); 16573 // Indicate un-selected state to screen reader 16574 this.controlText(''); 16575 this.isSelected_ = false; 16576 } 16577 } 16578 }; 16579 16580 return MenuItem; 16581 }(ClickableComponent); 16582 16583 Component.registerComponent('MenuItem', MenuItem); 16584 16585 /** 16586 * @file text-track-menu-item.js 16587 */ 16588 16589 /** 16590 * The specific menu item type for selecting a language within a text track kind 16591 * 16592 * @extends MenuItem 16593 */ 16594 16595 var TextTrackMenuItem = function (_MenuItem) { 16596 inherits(TextTrackMenuItem, _MenuItem); 16597 16598 /** 16599 * Creates an instance of this class. 16600 * 16601 * @param {Player} player 16602 * The `Player` that this class should be attached to. 16603 * 16604 * @param {Object} [options] 16605 * The key/value store of player options. 16606 */ 16607 function TextTrackMenuItem(player, options) { 16608 classCallCheck(this, TextTrackMenuItem); 16609 16610 var track = options.track; 16611 var tracks = player.textTracks(); 16612 16613 // Modify options for parent MenuItem class's init. 16614 options.label = track.label || track.language || 'Unknown'; 16615 options.selected = track.mode === 'showing'; 16616 16617 var _this = possibleConstructorReturn(this, _MenuItem.call(this, player, options)); 16618 16619 _this.track = track; 16620 var changeHandler = function changeHandler() { 16621 for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { 16622 args[_key] = arguments[_key]; 16623 } 16624 16625 _this.handleTracksChange.apply(_this, args); 16626 }; 16627 var selectedLanguageChangeHandler = function selectedLanguageChangeHandler() { 16628 for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { 16629 args[_key2] = arguments[_key2]; 16630 } 16631 16632 _this.handleSelectedLanguageChange.apply(_this, args); 16633 }; 16634 16635 player.on(['loadstart', 'texttrackchange'], changeHandler); 16636 tracks.addEventListener('change', changeHandler); 16637 tracks.addEventListener('selectedlanguagechange', selectedLanguageChangeHandler); 16638 _this.on('dispose', function () { 16639 player.off(['loadstart', 'texttrackchange'], changeHandler); 16640 tracks.removeEventListener('change', changeHandler); 16641 tracks.removeEventListener('selectedlanguagechange', selectedLanguageChangeHandler); 16642 }); 16643 16644 // iOS7 doesn't dispatch change events to TextTrackLists when an 16645 // associated track's mode changes. Without something like 16646 // Object.observe() (also not present on iOS7), it's not 16647 // possible to detect changes to the mode attribute and polyfill 16648 // the change event. As a poor substitute, we manually dispatch 16649 // change events whenever the controls modify the mode. 16650 if (tracks.onchange === undefined) { 16651 var event = void 0; 16652 16653 _this.on(['tap', 'click'], function () { 16654 if (_typeof(window_1.Event) !== 'object') { 16655 // Android 2.3 throws an Illegal Constructor error for window.Event 16656 try { 16657 event = new window_1.Event('change'); 16658 } catch (err) { 16659 // continue regardless of error 16660 } 16661 } 16662 16663 if (!event) { 16664 event = document_1.createEvent('Event'); 16665 event.initEvent('change', true, true); 16666 } 16667 16668 tracks.dispatchEvent(event); 16669 }); 16670 } 16671 16672 // set the default state based on current tracks 16673 _this.handleTracksChange(); 16674 return _this; 16675 } 16676 16677 /** 16678 * This gets called when an `TextTrackMenuItem` is "clicked". See 16679 * {@link ClickableComponent} for more detailed information on what a click can be. 16680 * 16681 * @param {EventTarget~Event} event 16682 * The `keydown`, `tap`, or `click` event that caused this function to be 16683 * called. 16684 * 16685 * @listens tap 16686 * @listens click 16687 */ 16688 16689 16690 TextTrackMenuItem.prototype.handleClick = function handleClick(event) { 16691 var kind = this.track.kind; 16692 var kinds = this.track.kinds; 16693 var tracks = this.player_.textTracks(); 16694 16695 if (!kinds) { 16696 kinds = [kind]; 16697 } 16698 16699 _MenuItem.prototype.handleClick.call(this, event); 16700 16701 if (!tracks) { 16702 return; 16703 } 16704 16705 for (var i = 0; i < tracks.length; i++) { 16706 var track = tracks[i]; 16707 16708 if (track === this.track && kinds.indexOf(track.kind) > -1) { 16709 if (track.mode !== 'showing') { 16710 track.mode = 'showing'; 16711 } 16712 } else if (track.mode !== 'disabled') { 16713 track.mode = 'disabled'; 16714 } 16715 } 16716 }; 16717 16718 /** 16719 * Handle text track list change 16720 * 16721 * @param {EventTarget~Event} event 16722 * The `change` event that caused this function to be called. 16723 * 16724 * @listens TextTrackList#change 16725 */ 16726 16727 16728 TextTrackMenuItem.prototype.handleTracksChange = function handleTracksChange(event) { 16729 var shouldBeSelected = this.track.mode === 'showing'; 16730 16731 // Prevent redundant selected() calls because they may cause 16732 // screen readers to read the appended control text unnecessarily 16733 if (shouldBeSelected !== this.isSelected_) { 16734 this.selected(shouldBeSelected); 16735 } 16736 }; 16737 16738 TextTrackMenuItem.prototype.handleSelectedLanguageChange = function handleSelectedLanguageChange(event) { 16739 if (this.track.mode === 'showing') { 16740 var selectedLanguage = this.player_.cache_.selectedLanguage; 16741 16742 // Don't replace the kind of track across the same language 16743 if (selectedLanguage && selectedLanguage.enabled && selectedLanguage.language === this.track.language && selectedLanguage.kind !== this.track.kind) { 16744 return; 16745 } 16746 16747 this.player_.cache_.selectedLanguage = { 16748 enabled: true, 16749 language: this.track.language, 16750 kind: this.track.kind 16751 }; 16752 } 16753 }; 16754 16755 TextTrackMenuItem.prototype.dispose = function dispose() { 16756 // remove reference to track object on dispose 16757 this.track = null; 16758 16759 _MenuItem.prototype.dispose.call(this); 16760 }; 16761 16762 return TextTrackMenuItem; 16763 }(MenuItem); 16764 16765 Component.registerComponent('TextTrackMenuItem', TextTrackMenuItem); 16766 16767 /** 16768 * @file off-text-track-menu-item.js 16769 */ 16770 16771 /** 16772 * A special menu item for turning of a specific type of text track 16773 * 16774 * @extends TextTrackMenuItem 16775 */ 16776 16777 var OffTextTrackMenuItem = function (_TextTrackMenuItem) { 16778 inherits(OffTextTrackMenuItem, _TextTrackMenuItem); 16779 16780 /** 16781 * Creates an instance of this class. 16782 * 16783 * @param {Player} player 16784 * The `Player` that this class should be attached to. 16785 * 16786 * @param {Object} [options] 16787 * The key/value store of player options. 16788 */ 16789 function OffTextTrackMenuItem(player, options) { 16790 classCallCheck(this, OffTextTrackMenuItem); 16791 16792 // Create pseudo track info 16793 // Requires options['kind'] 16794 options.track = { 16795 player: player, 16796 kind: options.kind, 16797 kinds: options.kinds, 16798 default: false, 16799 mode: 'disabled' 16800 }; 16801 16802 if (!options.kinds) { 16803 options.kinds = [options.kind]; 16804 } 16805 16806 if (options.label) { 16807 options.track.label = options.label; 16808 } else { 16809 options.track.label = options.kinds.join(' and ') + ' off'; 16810 } 16811 16812 // MenuItem is selectable 16813 options.selectable = true; 16814 // MenuItem is NOT multiSelectable (i.e. only one can be marked "selected" at a time) 16815 options.multiSelectable = false; 16816 16817 return possibleConstructorReturn(this, _TextTrackMenuItem.call(this, player, options)); 16818 } 16819 16820 /** 16821 * Handle text track change 16822 * 16823 * @param {EventTarget~Event} event 16824 * The event that caused this function to run 16825 */ 16826 16827 16828 OffTextTrackMenuItem.prototype.handleTracksChange = function handleTracksChange(event) { 16829 var tracks = this.player().textTracks(); 16830 var shouldBeSelected = true; 16831 16832 for (var i = 0, l = tracks.length; i < l; i++) { 16833 var track = tracks[i]; 16834 16835 if (this.options_.kinds.indexOf(track.kind) > -1 && track.mode === 'showing') { 16836 shouldBeSelected = false; 16837 break; 16838 } 16839 } 16840 16841 // Prevent redundant selected() calls because they may cause 16842 // screen readers to read the appended control text unnecessarily 16843 if (shouldBeSelected !== this.isSelected_) { 16844 this.selected(shouldBeSelected); 16845 } 16846 }; 16847 16848 OffTextTrackMenuItem.prototype.handleSelectedLanguageChange = function handleSelectedLanguageChange(event) { 16849 var tracks = this.player().textTracks(); 16850 var allHidden = true; 16851 16852 for (var i = 0, l = tracks.length; i < l; i++) { 16853 var track = tracks[i]; 16854 16855 if (['captions', 'descriptions', 'subtitles'].indexOf(track.kind) > -1 && track.mode === 'showing') { 16856 allHidden = false; 16857 break; 16858 } 16859 } 16860 16861 if (allHidden) { 16862 this.player_.cache_.selectedLanguage = { 16863 enabled: false 16864 }; 16865 } 16866 }; 16867 16868 return OffTextTrackMenuItem; 16869 }(TextTrackMenuItem); 16870 16871 Component.registerComponent('OffTextTrackMenuItem', OffTextTrackMenuItem); 16872 16873 /** 16874 * @file text-track-button.js 16875 */ 16876 16877 /** 16878 * The base class for buttons that toggle specific text track types (e.g. subtitles) 16879 * 16880 * @extends MenuButton 16881 */ 16882 16883 var TextTrackButton = function (_TrackButton) { 16884 inherits(TextTrackButton, _TrackButton); 16885 16886 /** 16887 * Creates an instance of this class. 16888 * 16889 * @param {Player} player 16890 * The `Player` that this class should be attached to. 16891 * 16892 * @param {Object} [options={}] 16893 * The key/value store of player options. 16894 */ 16895 function TextTrackButton(player) { 16896 var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 16897 classCallCheck(this, TextTrackButton); 16898 16899 options.tracks = player.textTracks(); 16900 16901 return possibleConstructorReturn(this, _TrackButton.call(this, player, options)); 16902 } 16903 16904 /** 16905 * Create a menu item for each text track 16906 * 16907 * @param {TextTrackMenuItem[]} [items=[]] 16908 * Existing array of items to use during creation 16909 * 16910 * @return {TextTrackMenuItem[]} 16911 * Array of menu items that were created 16912 */ 16913 16914 16915 TextTrackButton.prototype.createItems = function createItems() { 16916 var items = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 16917 var TrackMenuItem = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : TextTrackMenuItem; 16918 16919 16920 // Label is an override for the [track] off label 16921 // USed to localise captions/subtitles 16922 var label = void 0; 16923 16924 if (this.label_) { 16925 label = this.label_ + ' off'; 16926 } 16927 // Add an OFF menu item to turn all tracks off 16928 items.push(new OffTextTrackMenuItem(this.player_, { 16929 kinds: this.kinds_, 16930 kind: this.kind_, 16931 label: label 16932 })); 16933 16934 this.hideThreshold_ += 1; 16935 16936 var tracks = this.player_.textTracks(); 16937 16938 if (!Array.isArray(this.kinds_)) { 16939 this.kinds_ = [this.kind_]; 16940 } 16941 16942 for (var i = 0; i < tracks.length; i++) { 16943 var track = tracks[i]; 16944 16945 // only add tracks that are of an appropriate kind and have a label 16946 if (this.kinds_.indexOf(track.kind) > -1) { 16947 16948 var item = new TrackMenuItem(this.player_, { 16949 track: track, 16950 // MenuItem is selectable 16951 selectable: true, 16952 // MenuItem is NOT multiSelectable (i.e. only one can be marked "selected" at a time) 16953 multiSelectable: false 16954 }); 16955 16956 item.addClass('vjs-' + track.kind + '-menu-item'); 16957 items.push(item); 16958 } 16959 } 16960 16961 return items; 16962 }; 16963 16964 return TextTrackButton; 16965 }(TrackButton); 16966 16967 Component.registerComponent('TextTrackButton', TextTrackButton); 16968 16969 /** 16970 * @file chapters-track-menu-item.js 16971 */ 16972 16973 /** 16974 * The chapter track menu item 16975 * 16976 * @extends MenuItem 16977 */ 16978 16979 var ChaptersTrackMenuItem = function (_MenuItem) { 16980 inherits(ChaptersTrackMenuItem, _MenuItem); 16981 16982 /** 16983 * Creates an instance of this class. 16984 * 16985 * @param {Player} player 16986 * The `Player` that this class should be attached to. 16987 * 16988 * @param {Object} [options] 16989 * The key/value store of player options. 16990 */ 16991 function ChaptersTrackMenuItem(player, options) { 16992 classCallCheck(this, ChaptersTrackMenuItem); 16993 16994 var track = options.track; 16995 var cue = options.cue; 16996 var currentTime = player.currentTime(); 16997 16998 // Modify options for parent MenuItem class's init. 16999 options.selectable = true; 17000 options.multiSelectable = false; 17001 options.label = cue.text; 17002 options.selected = cue.startTime <= currentTime && currentTime < cue.endTime; 17003 17004 var _this = possibleConstructorReturn(this, _MenuItem.call(this, player, options)); 17005 17006 _this.track = track; 17007 _this.cue = cue; 17008 track.addEventListener('cuechange', bind(_this, _this.update)); 17009 return _this; 17010 } 17011 17012 /** 17013 * This gets called when an `ChaptersTrackMenuItem` is "clicked". See 17014 * {@link ClickableComponent} for more detailed information on what a click can be. 17015 * 17016 * @param {EventTarget~Event} [event] 17017 * The `keydown`, `tap`, or `click` event that caused this function to be 17018 * called. 17019 * 17020 * @listens tap 17021 * @listens click 17022 */ 17023 17024 17025 ChaptersTrackMenuItem.prototype.handleClick = function handleClick(event) { 17026 _MenuItem.prototype.handleClick.call(this); 17027 this.player_.currentTime(this.cue.startTime); 17028 this.update(this.cue.startTime); 17029 }; 17030 17031 /** 17032 * Update chapter menu item 17033 * 17034 * @param {EventTarget~Event} [event] 17035 * The `cuechange` event that caused this function to run. 17036 * 17037 * @listens TextTrack#cuechange 17038 */ 17039 17040 17041 ChaptersTrackMenuItem.prototype.update = function update(event) { 17042 var cue = this.cue; 17043 var currentTime = this.player_.currentTime(); 17044 17045 // vjs.log(currentTime, cue.startTime); 17046 this.selected(cue.startTime <= currentTime && currentTime < cue.endTime); 17047 }; 17048 17049 return ChaptersTrackMenuItem; 17050 }(MenuItem); 17051 17052 Component.registerComponent('ChaptersTrackMenuItem', ChaptersTrackMenuItem); 17053 17054 /** 17055 * @file chapters-button.js 17056 */ 17057 17058 /** 17059 * The button component for toggling and selecting chapters 17060 * Chapters act much differently than other text tracks 17061 * Cues are navigation vs. other tracks of alternative languages 17062 * 17063 * @extends TextTrackButton 17064 */ 17065 17066 var ChaptersButton = function (_TextTrackButton) { 17067 inherits(ChaptersButton, _TextTrackButton); 17068 17069 /** 17070 * Creates an instance of this class. 17071 * 17072 * @param {Player} player 17073 * The `Player` that this class should be attached to. 17074 * 17075 * @param {Object} [options] 17076 * The key/value store of player options. 17077 * 17078 * @param {Component~ReadyCallback} [ready] 17079 * The function to call when this function is ready. 17080 */ 17081 function ChaptersButton(player, options, ready) { 17082 classCallCheck(this, ChaptersButton); 17083 return possibleConstructorReturn(this, _TextTrackButton.call(this, player, options, ready)); 17084 } 17085 17086 /** 17087 * Builds the default DOM `className`. 17088 * 17089 * @return {string} 17090 * The DOM `className` for this object. 17091 */ 17092 17093 17094 ChaptersButton.prototype.buildCSSClass = function buildCSSClass() { 17095 return 'vjs-chapters-button ' + _TextTrackButton.prototype.buildCSSClass.call(this); 17096 }; 17097 17098 ChaptersButton.prototype.buildWrapperCSSClass = function buildWrapperCSSClass() { 17099 return 'vjs-chapters-button ' + _TextTrackButton.prototype.buildWrapperCSSClass.call(this); 17100 }; 17101 17102 /** 17103 * Update the menu based on the current state of its items. 17104 * 17105 * @param {EventTarget~Event} [event] 17106 * An event that triggered this function to run. 17107 * 17108 * @listens TextTrackList#addtrack 17109 * @listens TextTrackList#removetrack 17110 * @listens TextTrackList#change 17111 */ 17112 17113 17114 ChaptersButton.prototype.update = function update(event) { 17115 if (!this.track_ || event && (event.type === 'addtrack' || event.type === 'removetrack')) { 17116 this.setTrack(this.findChaptersTrack()); 17117 } 17118 _TextTrackButton.prototype.update.call(this); 17119 }; 17120 17121 /** 17122 * Set the currently selected track for the chapters button. 17123 * 17124 * @param {TextTrack} track 17125 * The new track to select. Nothing will change if this is the currently selected 17126 * track. 17127 */ 17128 17129 17130 ChaptersButton.prototype.setTrack = function setTrack(track) { 17131 if (this.track_ === track) { 17132 return; 17133 } 17134 17135 if (!this.updateHandler_) { 17136 this.updateHandler_ = this.update.bind(this); 17137 } 17138 17139 // here this.track_ refers to the old track instance 17140 if (this.track_) { 17141 var remoteTextTrackEl = this.player_.remoteTextTrackEls().getTrackElementByTrack_(this.track_); 17142 17143 if (remoteTextTrackEl) { 17144 remoteTextTrackEl.removeEventListener('load', this.updateHandler_); 17145 } 17146 17147 this.track_ = null; 17148 } 17149 17150 this.track_ = track; 17151 17152 // here this.track_ refers to the new track instance 17153 if (this.track_) { 17154 this.track_.mode = 'hidden'; 17155 17156 var _remoteTextTrackEl = this.player_.remoteTextTrackEls().getTrackElementByTrack_(this.track_); 17157 17158 if (_remoteTextTrackEl) { 17159 _remoteTextTrackEl.addEventListener('load', this.updateHandler_); 17160 } 17161 } 17162 }; 17163 17164 /** 17165 * Find the track object that is currently in use by this ChaptersButton 17166 * 17167 * @return {TextTrack|undefined} 17168 * The current track or undefined if none was found. 17169 */ 17170 17171 17172 ChaptersButton.prototype.findChaptersTrack = function findChaptersTrack() { 17173 var tracks = this.player_.textTracks() || []; 17174 17175 for (var i = tracks.length - 1; i >= 0; i--) { 17176 // We will always choose the last track as our chaptersTrack 17177 var track = tracks[i]; 17178 17179 if (track.kind === this.kind_) { 17180 return track; 17181 } 17182 } 17183 }; 17184 17185 /** 17186 * Get the caption for the ChaptersButton based on the track label. This will also 17187 * use the current tracks localized kind as a fallback if a label does not exist. 17188 * 17189 * @return {string} 17190 * The tracks current label or the localized track kind. 17191 */ 17192 17193 17194 ChaptersButton.prototype.getMenuCaption = function getMenuCaption() { 17195 if (this.track_ && this.track_.label) { 17196 return this.track_.label; 17197 } 17198 return this.localize(toTitleCase(this.kind_)); 17199 }; 17200 17201 /** 17202 * Create menu from chapter track 17203 * 17204 * @return {Menu} 17205 * New menu for the chapter buttons 17206 */ 17207 17208 17209 ChaptersButton.prototype.createMenu = function createMenu() { 17210 this.options_.title = this.getMenuCaption(); 17211 return _TextTrackButton.prototype.createMenu.call(this); 17212 }; 17213 17214 /** 17215 * Create a menu item for each text track 17216 * 17217 * @return {TextTrackMenuItem[]} 17218 * Array of menu items 17219 */ 17220 17221 17222 ChaptersButton.prototype.createItems = function createItems() { 17223 var items = []; 17224 17225 if (!this.track_) { 17226 return items; 17227 } 17228 17229 var cues = this.track_.cues; 17230 17231 if (!cues) { 17232 return items; 17233 } 17234 17235 for (var i = 0, l = cues.length; i < l; i++) { 17236 var cue = cues[i]; 17237 var mi = new ChaptersTrackMenuItem(this.player_, { track: this.track_, cue: cue }); 17238 17239 items.push(mi); 17240 } 17241 17242 return items; 17243 }; 17244 17245 return ChaptersButton; 17246 }(TextTrackButton); 17247 17248 /** 17249 * `kind` of TextTrack to look for to associate it with this menu. 17250 * 17251 * @type {string} 17252 * @private 17253 */ 17254 17255 17256 ChaptersButton.prototype.kind_ = 'chapters'; 17257 17258 /** 17259 * The text that should display over the `ChaptersButton`s controls. Added for localization. 17260 * 17261 * @type {string} 17262 * @private 17263 */ 17264 ChaptersButton.prototype.controlText_ = 'Chapters'; 17265 17266 Component.registerComponent('ChaptersButton', ChaptersButton); 17267 17268 /** 17269 * @file descriptions-button.js 17270 */ 17271 17272 /** 17273 * The button component for toggling and selecting descriptions 17274 * 17275 * @extends TextTrackButton 17276 */ 17277 17278 var DescriptionsButton = function (_TextTrackButton) { 17279 inherits(DescriptionsButton, _TextTrackButton); 17280 17281 /** 17282 * Creates an instance of this class. 17283 * 17284 * @param {Player} player 17285 * The `Player` that this class should be attached to. 17286 * 17287 * @param {Object} [options] 17288 * The key/value store of player options. 17289 * 17290 * @param {Component~ReadyCallback} [ready] 17291 * The function to call when this component is ready. 17292 */ 17293 function DescriptionsButton(player, options, ready) { 17294 classCallCheck(this, DescriptionsButton); 17295 17296 var _this = possibleConstructorReturn(this, _TextTrackButton.call(this, player, options, ready)); 17297 17298 var tracks = player.textTracks(); 17299 var changeHandler = bind(_this, _this.handleTracksChange); 17300 17301 tracks.addEventListener('change', changeHandler); 17302 _this.on('dispose', function () { 17303 tracks.removeEventListener('change', changeHandler); 17304 }); 17305 return _this; 17306 } 17307 17308 /** 17309 * Handle text track change 17310 * 17311 * @param {EventTarget~Event} event 17312 * The event that caused this function to run 17313 * 17314 * @listens TextTrackList#change 17315 */ 17316 17317 17318 DescriptionsButton.prototype.handleTracksChange = function handleTracksChange(event) { 17319 var tracks = this.player().textTracks(); 17320 var disabled = false; 17321 17322 // Check whether a track of a different kind is showing 17323 for (var i = 0, l = tracks.length; i < l; i++) { 17324 var track = tracks[i]; 17325 17326 if (track.kind !== this.kind_ && track.mode === 'showing') { 17327 disabled = true; 17328 break; 17329 } 17330 } 17331 17332 // If another track is showing, disable this menu button 17333 if (disabled) { 17334 this.disable(); 17335 } else { 17336 this.enable(); 17337 } 17338 }; 17339 17340 /** 17341 * Builds the default DOM `className`. 17342 * 17343 * @return {string} 17344 * The DOM `className` for this object. 17345 */ 17346 17347 17348 DescriptionsButton.prototype.buildCSSClass = function buildCSSClass() { 17349 return 'vjs-descriptions-button ' + _TextTrackButton.prototype.buildCSSClass.call(this); 17350 }; 17351 17352 DescriptionsButton.prototype.buildWrapperCSSClass = function buildWrapperCSSClass() { 17353 return 'vjs-descriptions-button ' + _TextTrackButton.prototype.buildWrapperCSSClass.call(this); 17354 }; 17355 17356 return DescriptionsButton; 17357 }(TextTrackButton); 17358 17359 /** 17360 * `kind` of TextTrack to look for to associate it with this menu. 17361 * 17362 * @type {string} 17363 * @private 17364 */ 17365 17366 17367 DescriptionsButton.prototype.kind_ = 'descriptions'; 17368 17369 /** 17370 * The text that should display over the `DescriptionsButton`s controls. Added for localization. 17371 * 17372 * @type {string} 17373 * @private 17374 */ 17375 DescriptionsButton.prototype.controlText_ = 'Descriptions'; 17376 17377 Component.registerComponent('DescriptionsButton', DescriptionsButton); 17378 17379 /** 17380 * @file subtitles-button.js 17381 */ 17382 17383 /** 17384 * The button component for toggling and selecting subtitles 17385 * 17386 * @extends TextTrackButton 17387 */ 17388 17389 var SubtitlesButton = function (_TextTrackButton) { 17390 inherits(SubtitlesButton, _TextTrackButton); 17391 17392 /** 17393 * Creates an instance of this class. 17394 * 17395 * @param {Player} player 17396 * The `Player` that this class should be attached to. 17397 * 17398 * @param {Object} [options] 17399 * The key/value store of player options. 17400 * 17401 * @param {Component~ReadyCallback} [ready] 17402 * The function to call when this component is ready. 17403 */ 17404 function SubtitlesButton(player, options, ready) { 17405 classCallCheck(this, SubtitlesButton); 17406 return possibleConstructorReturn(this, _TextTrackButton.call(this, player, options, ready)); 17407 } 17408 17409 /** 17410 * Builds the default DOM `className`. 17411 * 17412 * @return {string} 17413 * The DOM `className` for this object. 17414 */ 17415 17416 17417 SubtitlesButton.prototype.buildCSSClass = function buildCSSClass() { 17418 return 'vjs-subtitles-button ' + _TextTrackButton.prototype.buildCSSClass.call(this); 17419 }; 17420 17421 SubtitlesButton.prototype.buildWrapperCSSClass = function buildWrapperCSSClass() { 17422 return 'vjs-subtitles-button ' + _TextTrackButton.prototype.buildWrapperCSSClass.call(this); 17423 }; 17424 17425 return SubtitlesButton; 17426 }(TextTrackButton); 17427 17428 /** 17429 * `kind` of TextTrack to look for to associate it with this menu. 17430 * 17431 * @type {string} 17432 * @private 17433 */ 17434 17435 17436 SubtitlesButton.prototype.kind_ = 'subtitles'; 17437 17438 /** 17439 * The text that should display over the `SubtitlesButton`s controls. Added for localization. 17440 * 17441 * @type {string} 17442 * @private 17443 */ 17444 SubtitlesButton.prototype.controlText_ = 'Subtitles'; 17445 17446 Component.registerComponent('SubtitlesButton', SubtitlesButton); 17447 17448 /** 17449 * @file caption-settings-menu-item.js 17450 */ 17451 17452 /** 17453 * The menu item for caption track settings menu 17454 * 17455 * @extends TextTrackMenuItem 17456 */ 17457 17458 var CaptionSettingsMenuItem = function (_TextTrackMenuItem) { 17459 inherits(CaptionSettingsMenuItem, _TextTrackMenuItem); 17460 17461 /** 17462 * Creates an instance of this class. 17463 * 17464 * @param {Player} player 17465 * The `Player` that this class should be attached to. 17466 * 17467 * @param {Object} [options] 17468 * The key/value store of player options. 17469 */ 17470 function CaptionSettingsMenuItem(player, options) { 17471 classCallCheck(this, CaptionSettingsMenuItem); 17472 17473 options.track = { 17474 player: player, 17475 kind: options.kind, 17476 label: options.kind + ' settings', 17477 selectable: false, 17478 default: false, 17479 mode: 'disabled' 17480 }; 17481 17482 // CaptionSettingsMenuItem has no concept of 'selected' 17483 options.selectable = false; 17484 17485 options.name = 'CaptionSettingsMenuItem'; 17486 17487 var _this = possibleConstructorReturn(this, _TextTrackMenuItem.call(this, player, options)); 17488 17489 _this.addClass('vjs-texttrack-settings'); 17490 _this.controlText(', opens ' + options.kind + ' settings dialog'); 17491 return _this; 17492 } 17493 17494 /** 17495 * This gets called when an `CaptionSettingsMenuItem` is "clicked". See 17496 * {@link ClickableComponent} for more detailed information on what a click can be. 17497 * 17498 * @param {EventTarget~Event} [event] 17499 * The `keydown`, `tap`, or `click` event that caused this function to be 17500 * called. 17501 * 17502 * @listens tap 17503 * @listens click 17504 */ 17505 17506 17507 CaptionSettingsMenuItem.prototype.handleClick = function handleClick(event) { 17508 this.player().getChild('textTrackSettings').open(); 17509 }; 17510 17511 return CaptionSettingsMenuItem; 17512 }(TextTrackMenuItem); 17513 17514 Component.registerComponent('CaptionSettingsMenuItem', CaptionSettingsMenuItem); 17515 17516 /** 17517 * @file captions-button.js 17518 */ 17519 17520 /** 17521 * The button component for toggling and selecting captions 17522 * 17523 * @extends TextTrackButton 17524 */ 17525 17526 var CaptionsButton = function (_TextTrackButton) { 17527 inherits(CaptionsButton, _TextTrackButton); 17528 17529 /** 17530 * Creates an instance of this class. 17531 * 17532 * @param {Player} player 17533 * The `Player` that this class should be attached to. 17534 * 17535 * @param {Object} [options] 17536 * The key/value store of player options. 17537 * 17538 * @param {Component~ReadyCallback} [ready] 17539 * The function to call when this component is ready. 17540 */ 17541 function CaptionsButton(player, options, ready) { 17542 classCallCheck(this, CaptionsButton); 17543 return possibleConstructorReturn(this, _TextTrackButton.call(this, player, options, ready)); 17544 } 17545 17546 /** 17547 * Builds the default DOM `className`. 17548 * 17549 * @return {string} 17550 * The DOM `className` for this object. 17551 */ 17552 17553 17554 CaptionsButton.prototype.buildCSSClass = function buildCSSClass() { 17555 return 'vjs-captions-button ' + _TextTrackButton.prototype.buildCSSClass.call(this); 17556 }; 17557 17558 CaptionsButton.prototype.buildWrapperCSSClass = function buildWrapperCSSClass() { 17559 return 'vjs-captions-button ' + _TextTrackButton.prototype.buildWrapperCSSClass.call(this); 17560 }; 17561 17562 /** 17563 * Create caption menu items 17564 * 17565 * @return {CaptionSettingsMenuItem[]} 17566 * The array of current menu items. 17567 */ 17568 17569 17570 CaptionsButton.prototype.createItems = function createItems() { 17571 var items = []; 17572 17573 if (!(this.player().tech_ && this.player().tech_.featuresNativeTextTracks) && this.player().getChild('textTrackSettings')) { 17574 items.push(new CaptionSettingsMenuItem(this.player_, { kind: this.kind_ })); 17575 17576 this.hideThreshold_ += 1; 17577 } 17578 17579 return _TextTrackButton.prototype.createItems.call(this, items); 17580 }; 17581 17582 return CaptionsButton; 17583 }(TextTrackButton); 17584 17585 /** 17586 * `kind` of TextTrack to look for to associate it with this menu. 17587 * 17588 * @type {string} 17589 * @private 17590 */ 17591 17592 17593 CaptionsButton.prototype.kind_ = 'captions'; 17594 17595 /** 17596 * The text that should display over the `CaptionsButton`s controls. Added for localization. 17597 * 17598 * @type {string} 17599 * @private 17600 */ 17601 CaptionsButton.prototype.controlText_ = 'Captions'; 17602 17603 Component.registerComponent('CaptionsButton', CaptionsButton); 17604 17605 /** 17606 * @file subs-caps-menu-item.js 17607 */ 17608 17609 /** 17610 * SubsCapsMenuItem has an [cc] icon to distinguish captions from subtitles 17611 * in the SubsCapsMenu. 17612 * 17613 * @extends TextTrackMenuItem 17614 */ 17615 17616 var SubsCapsMenuItem = function (_TextTrackMenuItem) { 17617 inherits(SubsCapsMenuItem, _TextTrackMenuItem); 17618 17619 function SubsCapsMenuItem() { 17620 classCallCheck(this, SubsCapsMenuItem); 17621 return possibleConstructorReturn(this, _TextTrackMenuItem.apply(this, arguments)); 17622 } 17623 17624 SubsCapsMenuItem.prototype.createEl = function createEl(type, props, attrs) { 17625 var innerHTML = '<span class="vjs-menu-item-text">' + this.localize(this.options_.label); 17626 17627 if (this.options_.track.kind === 'captions') { 17628 innerHTML += '\n <span aria-hidden="true" class="vjs-icon-placeholder"></span>\n <span class="vjs-control-text"> ' + this.localize('Captions') + '</span>\n '; 17629 } 17630 17631 innerHTML += '</span>'; 17632 17633 var el = _TextTrackMenuItem.prototype.createEl.call(this, type, assign({ 17634 innerHTML: innerHTML 17635 }, props), attrs); 17636 17637 return el; 17638 }; 17639 17640 return SubsCapsMenuItem; 17641 }(TextTrackMenuItem); 17642 17643 Component.registerComponent('SubsCapsMenuItem', SubsCapsMenuItem); 17644 17645 /** 17646 * @file sub-caps-button.js 17647 */ 17648 /** 17649 * The button component for toggling and selecting captions and/or subtitles 17650 * 17651 * @extends TextTrackButton 17652 */ 17653 17654 var SubsCapsButton = function (_TextTrackButton) { 17655 inherits(SubsCapsButton, _TextTrackButton); 17656 17657 function SubsCapsButton(player) { 17658 var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 17659 classCallCheck(this, SubsCapsButton); 17660 17661 // Although North America uses "captions" in most cases for 17662 // "captions and subtitles" other locales use "subtitles" 17663 var _this = possibleConstructorReturn(this, _TextTrackButton.call(this, player, options)); 17664 17665 _this.label_ = 'subtitles'; 17666 if (['en', 'en-us', 'en-ca', 'fr-ca'].indexOf(_this.player_.language_) > -1) { 17667 _this.label_ = 'captions'; 17668 } 17669 _this.menuButton_.controlText(toTitleCase(_this.label_)); 17670 return _this; 17671 } 17672 17673 /** 17674 * Builds the default DOM `className`. 17675 * 17676 * @return {string} 17677 * The DOM `className` for this object. 17678 */ 17679 17680 17681 SubsCapsButton.prototype.buildCSSClass = function buildCSSClass() { 17682 return 'vjs-subs-caps-button ' + _TextTrackButton.prototype.buildCSSClass.call(this); 17683 }; 17684 17685 SubsCapsButton.prototype.buildWrapperCSSClass = function buildWrapperCSSClass() { 17686 return 'vjs-subs-caps-button ' + _TextTrackButton.prototype.buildWrapperCSSClass.call(this); 17687 }; 17688 17689 /** 17690 * Create caption/subtitles menu items 17691 * 17692 * @return {CaptionSettingsMenuItem[]} 17693 * The array of current menu items. 17694 */ 17695 17696 17697 SubsCapsButton.prototype.createItems = function createItems() { 17698 var items = []; 17699 17700 if (!(this.player().tech_ && this.player().tech_.featuresNativeTextTracks) && this.player().getChild('textTrackSettings')) { 17701 items.push(new CaptionSettingsMenuItem(this.player_, { kind: this.label_ })); 17702 17703 this.hideThreshold_ += 1; 17704 } 17705 17706 items = _TextTrackButton.prototype.createItems.call(this, items, SubsCapsMenuItem); 17707 return items; 17708 }; 17709 17710 return SubsCapsButton; 17711 }(TextTrackButton); 17712 17713 /** 17714 * `kind`s of TextTrack to look for to associate it with this menu. 17715 * 17716 * @type {array} 17717 * @private 17718 */ 17719 17720 17721 SubsCapsButton.prototype.kinds_ = ['captions', 'subtitles']; 17722 17723 /** 17724 * The text that should display over the `SubsCapsButton`s controls. 17725 * 17726 * 17727 * @type {string} 17728 * @private 17729 */ 17730 SubsCapsButton.prototype.controlText_ = 'Subtitles'; 17731 17732 Component.registerComponent('SubsCapsButton', SubsCapsButton); 17733 17734 /** 17735 * @file audio-track-menu-item.js 17736 */ 17737 17738 /** 17739 * An {@link AudioTrack} {@link MenuItem} 17740 * 17741 * @extends MenuItem 17742 */ 17743 17744 var AudioTrackMenuItem = function (_MenuItem) { 17745 inherits(AudioTrackMenuItem, _MenuItem); 17746 17747 /** 17748 * Creates an instance of this class. 17749 * 17750 * @param {Player} player 17751 * The `Player` that this class should be attached to. 17752 * 17753 * @param {Object} [options] 17754 * The key/value store of player options. 17755 */ 17756 function AudioTrackMenuItem(player, options) { 17757 classCallCheck(this, AudioTrackMenuItem); 17758 17759 var track = options.track; 17760 var tracks = player.audioTracks(); 17761 17762 // Modify options for parent MenuItem class's init. 17763 options.label = track.label || track.language || 'Unknown'; 17764 options.selected = track.enabled; 17765 17766 var _this = possibleConstructorReturn(this, _MenuItem.call(this, player, options)); 17767 17768 _this.track = track; 17769 17770 _this.addClass('vjs-' + track.kind + '-menu-item'); 17771 17772 var changeHandler = function changeHandler() { 17773 for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { 17774 args[_key] = arguments[_key]; 17775 } 17776 17777 _this.handleTracksChange.apply(_this, args); 17778 }; 17779 17780 tracks.addEventListener('change', changeHandler); 17781 _this.on('dispose', function () { 17782 tracks.removeEventListener('change', changeHandler); 17783 }); 17784 return _this; 17785 } 17786 17787 AudioTrackMenuItem.prototype.createEl = function createEl(type, props, attrs) { 17788 var innerHTML = '<span class="vjs-menu-item-text">' + this.localize(this.options_.label); 17789 17790 if (this.options_.track.kind === 'main-desc') { 17791 innerHTML += '\n <span aria-hidden="true" class="vjs-icon-placeholder"></span>\n <span class="vjs-control-text"> ' + this.localize('Descriptions') + '</span>\n '; 17792 } 17793 17794 innerHTML += '</span>'; 17795 17796 var el = _MenuItem.prototype.createEl.call(this, type, assign({ 17797 innerHTML: innerHTML 17798 }, props), attrs); 17799 17800 return el; 17801 }; 17802 17803 /** 17804 * This gets called when an `AudioTrackMenuItem is "clicked". See {@link ClickableComponent} 17805 * for more detailed information on what a click can be. 17806 * 17807 * @param {EventTarget~Event} [event] 17808 * The `keydown`, `tap`, or `click` event that caused this function to be 17809 * called. 17810 * 17811 * @listens tap 17812 * @listens click 17813 */ 17814 17815 17816 AudioTrackMenuItem.prototype.handleClick = function handleClick(event) { 17817 var tracks = this.player_.audioTracks(); 17818 17819 _MenuItem.prototype.handleClick.call(this, event); 17820 17821 for (var i = 0; i < tracks.length; i++) { 17822 var track = tracks[i]; 17823 17824 track.enabled = track === this.track; 17825 } 17826 }; 17827 17828 /** 17829 * Handle any {@link AudioTrack} change. 17830 * 17831 * @param {EventTarget~Event} [event] 17832 * The {@link AudioTrackList#change} event that caused this to run. 17833 * 17834 * @listens AudioTrackList#change 17835 */ 17836 17837 17838 AudioTrackMenuItem.prototype.handleTracksChange = function handleTracksChange(event) { 17839 this.selected(this.track.enabled); 17840 }; 17841 17842 return AudioTrackMenuItem; 17843 }(MenuItem); 17844 17845 Component.registerComponent('AudioTrackMenuItem', AudioTrackMenuItem); 17846 17847 /** 17848 * @file audio-track-button.js 17849 */ 17850 17851 /** 17852 * The base class for buttons that toggle specific {@link AudioTrack} types. 17853 * 17854 * @extends TrackButton 17855 */ 17856 17857 var AudioTrackButton = function (_TrackButton) { 17858 inherits(AudioTrackButton, _TrackButton); 17859 17860 /** 17861 * Creates an instance of this class. 17862 * 17863 * @param {Player} player 17864 * The `Player` that this class should be attached to. 17865 * 17866 * @param {Object} [options={}] 17867 * The key/value store of player options. 17868 */ 17869 function AudioTrackButton(player) { 17870 var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 17871 classCallCheck(this, AudioTrackButton); 17872 17873 options.tracks = player.audioTracks(); 17874 17875 return possibleConstructorReturn(this, _TrackButton.call(this, player, options)); 17876 } 17877 17878 /** 17879 * Builds the default DOM `className`. 17880 * 17881 * @return {string} 17882 * The DOM `className` for this object. 17883 */ 17884 17885 17886 AudioTrackButton.prototype.buildCSSClass = function buildCSSClass() { 17887 return 'vjs-audio-button ' + _TrackButton.prototype.buildCSSClass.call(this); 17888 }; 17889 17890 AudioTrackButton.prototype.buildWrapperCSSClass = function buildWrapperCSSClass() { 17891 return 'vjs-audio-button ' + _TrackButton.prototype.buildWrapperCSSClass.call(this); 17892 }; 17893 17894 /** 17895 * Create a menu item for each audio track 17896 * 17897 * @param {AudioTrackMenuItem[]} [items=[]] 17898 * An array of existing menu items to use. 17899 * 17900 * @return {AudioTrackMenuItem[]} 17901 * An array of menu items 17902 */ 17903 17904 17905 AudioTrackButton.prototype.createItems = function createItems() { 17906 var items = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 17907 17908 // if there's only one audio track, there no point in showing it 17909 this.hideThreshold_ = 1; 17910 17911 var tracks = this.player_.audioTracks(); 17912 17913 for (var i = 0; i < tracks.length; i++) { 17914 var track = tracks[i]; 17915 17916 items.push(new AudioTrackMenuItem(this.player_, { 17917 track: track, 17918 // MenuItem is selectable 17919 selectable: true, 17920 // MenuItem is NOT multiSelectable (i.e. only one can be marked "selected" at a time) 17921 multiSelectable: false 17922 })); 17923 } 17924 17925 return items; 17926 }; 17927 17928 return AudioTrackButton; 17929 }(TrackButton); 17930 17931 /** 17932 * The text that should display over the `AudioTrackButton`s controls. Added for localization. 17933 * 17934 * @type {string} 17935 * @private 17936 */ 17937 17938 17939 AudioTrackButton.prototype.controlText_ = 'Audio Track'; 17940 Component.registerComponent('AudioTrackButton', AudioTrackButton); 17941 17942 /** 17943 * @file playback-rate-menu-item.js 17944 */ 17945 17946 /** 17947 * The specific menu item type for selecting a playback rate. 17948 * 17949 * @extends MenuItem 17950 */ 17951 17952 var PlaybackRateMenuItem = function (_MenuItem) { 17953 inherits(PlaybackRateMenuItem, _MenuItem); 17954 17955 /** 17956 * Creates an instance of this class. 17957 * 17958 * @param {Player} player 17959 * The `Player` that this class should be attached to. 17960 * 17961 * @param {Object} [options] 17962 * The key/value store of player options. 17963 */ 17964 function PlaybackRateMenuItem(player, options) { 17965 classCallCheck(this, PlaybackRateMenuItem); 17966 17967 var label = options.rate; 17968 var rate = parseFloat(label, 10); 17969 17970 // Modify options for parent MenuItem class's init. 17971 options.label = label; 17972 options.selected = rate === 1; 17973 options.selectable = true; 17974 options.multiSelectable = false; 17975 17976 var _this = possibleConstructorReturn(this, _MenuItem.call(this, player, options)); 17977 17978 _this.label = label; 17979 _this.rate = rate; 17980 17981 _this.on(player, 'ratechange', _this.update); 17982 return _this; 17983 } 17984 17985 /** 17986 * This gets called when an `PlaybackRateMenuItem` is "clicked". See 17987 * {@link ClickableComponent} for more detailed information on what a click can be. 17988 * 17989 * @param {EventTarget~Event} [event] 17990 * The `keydown`, `tap`, or `click` event that caused this function to be 17991 * called. 17992 * 17993 * @listens tap 17994 * @listens click 17995 */ 17996 17997 17998 PlaybackRateMenuItem.prototype.handleClick = function handleClick(event) { 17999 _MenuItem.prototype.handleClick.call(this); 18000 this.player().playbackRate(this.rate); 18001 }; 18002 18003 /** 18004 * Update the PlaybackRateMenuItem when the playbackrate changes. 18005 * 18006 * @param {EventTarget~Event} [event] 18007 * The `ratechange` event that caused this function to run. 18008 * 18009 * @listens Player#ratechange 18010 */ 18011 18012 18013 PlaybackRateMenuItem.prototype.update = function update(event) { 18014 this.selected(this.player().playbackRate() === this.rate); 18015 }; 18016 18017 return PlaybackRateMenuItem; 18018 }(MenuItem); 18019 18020 /** 18021 * The text that should display over the `PlaybackRateMenuItem`s controls. Added for localization. 18022 * 18023 * @type {string} 18024 * @private 18025 */ 18026 18027 18028 PlaybackRateMenuItem.prototype.contentElType = 'button'; 18029 18030 Component.registerComponent('PlaybackRateMenuItem', PlaybackRateMenuItem); 18031 18032 /** 18033 * @file playback-rate-menu-button.js 18034 */ 18035 18036 /** 18037 * The component for controlling the playback rate. 18038 * 18039 * @extends MenuButton 18040 */ 18041 18042 var PlaybackRateMenuButton = function (_MenuButton) { 18043 inherits(PlaybackRateMenuButton, _MenuButton); 18044 18045 /** 18046 * Creates an instance of this class. 18047 * 18048 * @param {Player} player 18049 * The `Player` that this class should be attached to. 18050 * 18051 * @param {Object} [options] 18052 * The key/value store of player options. 18053 */ 18054 function PlaybackRateMenuButton(player, options) { 18055 classCallCheck(this, PlaybackRateMenuButton); 18056 18057 var _this = possibleConstructorReturn(this, _MenuButton.call(this, player, options)); 18058 18059 _this.updateVisibility(); 18060 _this.updateLabel(); 18061 18062 _this.on(player, 'loadstart', _this.updateVisibility); 18063 _this.on(player, 'ratechange', _this.updateLabel); 18064 return _this; 18065 } 18066 18067 /** 18068 * Create the `Component`'s DOM element 18069 * 18070 * @return {Element} 18071 * The element that was created. 18072 */ 18073 18074 18075 PlaybackRateMenuButton.prototype.createEl = function createEl$$1() { 18076 var el = _MenuButton.prototype.createEl.call(this); 18077 18078 this.labelEl_ = createEl('div', { 18079 className: 'vjs-playback-rate-value', 18080 innerHTML: '1x' 18081 }); 18082 18083 el.appendChild(this.labelEl_); 18084 18085 return el; 18086 }; 18087 18088 PlaybackRateMenuButton.prototype.dispose = function dispose() { 18089 this.labelEl_ = null; 18090 18091 _MenuButton.prototype.dispose.call(this); 18092 }; 18093 18094 /** 18095 * Builds the default DOM `className`. 18096 * 18097 * @return {string} 18098 * The DOM `className` for this object. 18099 */ 18100 18101 18102 PlaybackRateMenuButton.prototype.buildCSSClass = function buildCSSClass() { 18103 return 'vjs-playback-rate ' + _MenuButton.prototype.buildCSSClass.call(this); 18104 }; 18105 18106 PlaybackRateMenuButton.prototype.buildWrapperCSSClass = function buildWrapperCSSClass() { 18107 return 'vjs-playback-rate ' + _MenuButton.prototype.buildWrapperCSSClass.call(this); 18108 }; 18109 18110 /** 18111 * Create the playback rate menu 18112 * 18113 * @return {Menu} 18114 * Menu object populated with {@link PlaybackRateMenuItem}s 18115 */ 18116 18117 18118 PlaybackRateMenuButton.prototype.createMenu = function createMenu() { 18119 var menu = new Menu(this.player()); 18120 var rates = this.playbackRates(); 18121 18122 if (rates) { 18123 for (var i = rates.length - 1; i >= 0; i--) { 18124 menu.addChild(new PlaybackRateMenuItem(this.player(), { rate: rates[i] + 'x' })); 18125 } 18126 } 18127 18128 return menu; 18129 }; 18130 18131 /** 18132 * Updates ARIA accessibility attributes 18133 */ 18134 18135 18136 PlaybackRateMenuButton.prototype.updateARIAAttributes = function updateARIAAttributes() { 18137 // Current playback rate 18138 this.el().setAttribute('aria-valuenow', this.player().playbackRate()); 18139 }; 18140 18141 /** 18142 * This gets called when an `PlaybackRateMenuButton` is "clicked". See 18143 * {@link ClickableComponent} for more detailed information on what a click can be. 18144 * 18145 * @param {EventTarget~Event} [event] 18146 * The `keydown`, `tap`, or `click` event that caused this function to be 18147 * called. 18148 * 18149 * @listens tap 18150 * @listens click 18151 */ 18152 18153 18154 PlaybackRateMenuButton.prototype.handleClick = function handleClick(event) { 18155 // select next rate option 18156 var currentRate = this.player().playbackRate(); 18157 var rates = this.playbackRates(); 18158 18159 // this will select first one if the last one currently selected 18160 var newRate = rates[0]; 18161 18162 for (var i = 0; i < rates.length; i++) { 18163 if (rates[i] > currentRate) { 18164 newRate = rates[i]; 18165 break; 18166 } 18167 } 18168 this.player().playbackRate(newRate); 18169 }; 18170 18171 /** 18172 * Get possible playback rates 18173 * 18174 * @return {Array} 18175 * All possible playback rates 18176 */ 18177 18178 18179 PlaybackRateMenuButton.prototype.playbackRates = function playbackRates() { 18180 return this.options_.playbackRates || this.options_.playerOptions && this.options_.playerOptions.playbackRates; 18181 }; 18182 18183 /** 18184 * Get whether playback rates is supported by the tech 18185 * and an array of playback rates exists 18186 * 18187 * @return {boolean} 18188 * Whether changing playback rate is supported 18189 */ 18190 18191 18192 PlaybackRateMenuButton.prototype.playbackRateSupported = function playbackRateSupported() { 18193 return this.player().tech_ && this.player().tech_.featuresPlaybackRate && this.playbackRates() && this.playbackRates().length > 0; 18194 }; 18195 18196 /** 18197 * Hide playback rate controls when they're no playback rate options to select 18198 * 18199 * @param {EventTarget~Event} [event] 18200 * The event that caused this function to run. 18201 * 18202 * @listens Player#loadstart 18203 */ 18204 18205 18206 PlaybackRateMenuButton.prototype.updateVisibility = function updateVisibility(event) { 18207 if (this.playbackRateSupported()) { 18208 this.removeClass('vjs-hidden'); 18209 } else { 18210 this.addClass('vjs-hidden'); 18211 } 18212 }; 18213 18214 /** 18215 * Update button label when rate changed 18216 * 18217 * @param {EventTarget~Event} [event] 18218 * The event that caused this function to run. 18219 * 18220 * @listens Player#ratechange 18221 */ 18222 18223 18224 PlaybackRateMenuButton.prototype.updateLabel = function updateLabel(event) { 18225 if (this.playbackRateSupported()) { 18226 this.labelEl_.innerHTML = this.player().playbackRate() + 'x'; 18227 } 18228 }; 18229 18230 return PlaybackRateMenuButton; 18231 }(MenuButton); 18232 18233 /** 18234 * The text that should display over the `FullscreenToggle`s controls. Added for localization. 18235 * 18236 * @type {string} 18237 * @private 18238 */ 18239 18240 18241 PlaybackRateMenuButton.prototype.controlText_ = 'Playback Rate'; 18242 18243 Component.registerComponent('PlaybackRateMenuButton', PlaybackRateMenuButton); 18244 18245 /** 18246 * @file spacer.js 18247 */ 18248 18249 /** 18250 * Just an empty spacer element that can be used as an append point for plugins, etc. 18251 * Also can be used to create space between elements when necessary. 18252 * 18253 * @extends Component 18254 */ 18255 18256 var Spacer = function (_Component) { 18257 inherits(Spacer, _Component); 18258 18259 function Spacer() { 18260 classCallCheck(this, Spacer); 18261 return possibleConstructorReturn(this, _Component.apply(this, arguments)); 18262 } 18263 18264 /** 18265 * Builds the default DOM `className`. 18266 * 18267 * @return {string} 18268 * The DOM `className` for this object. 18269 */ 18270 Spacer.prototype.buildCSSClass = function buildCSSClass() { 18271 return 'vjs-spacer ' + _Component.prototype.buildCSSClass.call(this); 18272 }; 18273 18274 /** 18275 * Create the `Component`'s DOM element 18276 * 18277 * @return {Element} 18278 * The element that was created. 18279 */ 18280 18281 18282 Spacer.prototype.createEl = function createEl() { 18283 return _Component.prototype.createEl.call(this, 'div', { 18284 className: this.buildCSSClass() 18285 }); 18286 }; 18287 18288 return Spacer; 18289 }(Component); 18290 18291 Component.registerComponent('Spacer', Spacer); 18292 18293 /** 18294 * @file custom-control-spacer.js 18295 */ 18296 18297 /** 18298 * Spacer specifically meant to be used as an insertion point for new plugins, etc. 18299 * 18300 * @extends Spacer 18301 */ 18302 18303 var CustomControlSpacer = function (_Spacer) { 18304 inherits(CustomControlSpacer, _Spacer); 18305 18306 function CustomControlSpacer() { 18307 classCallCheck(this, CustomControlSpacer); 18308 return possibleConstructorReturn(this, _Spacer.apply(this, arguments)); 18309 } 18310 18311 /** 18312 * Builds the default DOM `className`. 18313 * 18314 * @return {string} 18315 * The DOM `className` for this object. 18316 */ 18317 CustomControlSpacer.prototype.buildCSSClass = function buildCSSClass() { 18318 return 'vjs-custom-control-spacer ' + _Spacer.prototype.buildCSSClass.call(this); 18319 }; 18320 18321 /** 18322 * Create the `Component`'s DOM element 18323 * 18324 * @return {Element} 18325 * The element that was created. 18326 */ 18327 18328 18329 CustomControlSpacer.prototype.createEl = function createEl() { 18330 var el = _Spacer.prototype.createEl.call(this, { 18331 className: this.buildCSSClass() 18332 }); 18333 18334 // No-flex/table-cell mode requires there be some content 18335 // in the cell to fill the remaining space of the table. 18336 el.innerHTML = '\xA0'; 18337 return el; 18338 }; 18339 18340 return CustomControlSpacer; 18341 }(Spacer); 18342 18343 Component.registerComponent('CustomControlSpacer', CustomControlSpacer); 18344 18345 /** 18346 * @file control-bar.js 18347 */ 18348 18349 /** 18350 * Container of main controls. 18351 * 18352 * @extends Component 18353 */ 18354 18355 var ControlBar = function (_Component) { 18356 inherits(ControlBar, _Component); 18357 18358 function ControlBar() { 18359 classCallCheck(this, ControlBar); 18360 return possibleConstructorReturn(this, _Component.apply(this, arguments)); 18361 } 18362 18363 /** 18364 * Create the `Component`'s DOM element 18365 * 18366 * @return {Element} 18367 * The element that was created. 18368 */ 18369 ControlBar.prototype.createEl = function createEl() { 18370 return _Component.prototype.createEl.call(this, 'div', { 18371 className: 'vjs-control-bar', 18372 dir: 'ltr' 18373 }); 18374 }; 18375 18376 return ControlBar; 18377 }(Component); 18378 18379 /** 18380 * Default options for `ControlBar` 18381 * 18382 * @type {Object} 18383 * @private 18384 */ 18385 18386 18387 ControlBar.prototype.options_ = { 18388 children: ['playToggle', 'volumePanel', 'currentTimeDisplay', 'timeDivider', 'durationDisplay', 'progressControl', 'liveDisplay', 'remainingTimeDisplay', 'customControlSpacer', 'playbackRateMenuButton', 'chaptersButton', 'descriptionsButton', 'subsCapsButton', 'audioTrackButton', 'fullscreenToggle'] 18389 }; 18390 18391 Component.registerComponent('ControlBar', ControlBar); 18392 18393 /** 18394 * @file error-display.js 18395 */ 18396 18397 /** 18398 * A display that indicates an error has occurred. This means that the video 18399 * is unplayable. 18400 * 18401 * @extends ModalDialog 18402 */ 18403 18404 var ErrorDisplay = function (_ModalDialog) { 18405 inherits(ErrorDisplay, _ModalDialog); 18406 18407 /** 18408 * Creates an instance of this class. 18409 * 18410 * @param {Player} player 18411 * The `Player` that this class should be attached to. 18412 * 18413 * @param {Object} [options] 18414 * The key/value store of player options. 18415 */ 18416 function ErrorDisplay(player, options) { 18417 classCallCheck(this, ErrorDisplay); 18418 18419 var _this = possibleConstructorReturn(this, _ModalDialog.call(this, player, options)); 18420 18421 _this.on(player, 'error', _this.open); 18422 return _this; 18423 } 18424 18425 /** 18426 * Builds the default DOM `className`. 18427 * 18428 * @return {string} 18429 * The DOM `className` for this object. 18430 * 18431 * @deprecated Since version 5. 18432 */ 18433 18434 18435 ErrorDisplay.prototype.buildCSSClass = function buildCSSClass() { 18436 return 'vjs-error-display ' + _ModalDialog.prototype.buildCSSClass.call(this); 18437 }; 18438 18439 /** 18440 * Gets the localized error message based on the `Player`s error. 18441 * 18442 * @return {string} 18443 * The `Player`s error message localized or an empty string. 18444 */ 18445 18446 18447 ErrorDisplay.prototype.content = function content() { 18448 var error = this.player().error(); 18449 18450 return error ? this.localize(error.message) : ''; 18451 }; 18452 18453 return ErrorDisplay; 18454 }(ModalDialog); 18455 18456 /** 18457 * The default options for an `ErrorDisplay`. 18458 * 18459 * @private 18460 */ 18461 18462 18463 ErrorDisplay.prototype.options_ = mergeOptions(ModalDialog.prototype.options_, { 18464 pauseOnOpen: false, 18465 fillAlways: true, 18466 temporary: false, 18467 uncloseable: true 18468 }); 18469 18470 Component.registerComponent('ErrorDisplay', ErrorDisplay); 18471 18472 /** 18473 * @file text-track-settings.js 18474 */ 18475 18476 var LOCAL_STORAGE_KEY = 'vjs-text-track-settings'; 18477 18478 var COLOR_BLACK = ['#000', 'Black']; 18479 var COLOR_BLUE = ['#00F', 'Blue']; 18480 var COLOR_CYAN = ['#0FF', 'Cyan']; 18481 var COLOR_GREEN = ['#0F0', 'Green']; 18482 var COLOR_MAGENTA = ['#F0F', 'Magenta']; 18483 var COLOR_RED = ['#F00', 'Red']; 18484 var COLOR_WHITE = ['#FFF', 'White']; 18485 var COLOR_YELLOW = ['#FF0', 'Yellow']; 18486 18487 var OPACITY_OPAQUE = ['1', 'Opaque']; 18488 var OPACITY_SEMI = ['0.5', 'Semi-Transparent']; 18489 var OPACITY_TRANS = ['0', 'Transparent']; 18490 18491 // Configuration for the various <select> elements in the DOM of this component. 18492 // 18493 // Possible keys include: 18494 // 18495 // `default`: 18496 // The default option index. Only needs to be provided if not zero. 18497 // `parser`: 18498 // A function which is used to parse the value from the selected option in 18499 // a customized way. 18500 // `selector`: 18501 // The selector used to find the associated <select> element. 18502 var selectConfigs = { 18503 backgroundColor: { 18504 selector: '.vjs-bg-color > select', 18505 id: 'captions-background-color-%s', 18506 label: 'Color', 18507 options: [COLOR_BLACK, COLOR_WHITE, COLOR_RED, COLOR_GREEN, COLOR_BLUE, COLOR_YELLOW, COLOR_MAGENTA, COLOR_CYAN] 18508 }, 18509 18510 backgroundOpacity: { 18511 selector: '.vjs-bg-opacity > select', 18512 id: 'captions-background-opacity-%s', 18513 label: 'Transparency', 18514 options: [OPACITY_OPAQUE, OPACITY_SEMI, OPACITY_TRANS] 18515 }, 18516 18517 color: { 18518 selector: '.vjs-fg-color > select', 18519 id: 'captions-foreground-color-%s', 18520 label: 'Color', 18521 options: [COLOR_WHITE, COLOR_BLACK, COLOR_RED, COLOR_GREEN, COLOR_BLUE, COLOR_YELLOW, COLOR_MAGENTA, COLOR_CYAN] 18522 }, 18523 18524 edgeStyle: { 18525 selector: '.vjs-edge-style > select', 18526 id: '%s', 18527 label: 'Text Edge Style', 18528 options: [['none', 'None'], ['raised', 'Raised'], ['depressed', 'Depressed'], ['uniform', 'Uniform'], ['dropshadow', 'Dropshadow']] 18529 }, 18530 18531 fontFamily: { 18532 selector: '.vjs-font-family > select', 18533 id: 'captions-font-family-%s', 18534 label: 'Font Family', 18535 options: [['proportionalSansSerif', 'Proportional Sans-Serif'], ['monospaceSansSerif', 'Monospace Sans-Serif'], ['proportionalSerif', 'Proportional Serif'], ['monospaceSerif', 'Monospace Serif'], ['casual', 'Casual'], ['script', 'Script'], ['small-caps', 'Small Caps']] 18536 }, 18537 18538 fontPercent: { 18539 selector: '.vjs-font-percent > select', 18540 id: 'captions-font-size-%s', 18541 label: 'Font Size', 18542 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%']], 18543 default: 2, 18544 parser: function parser(v) { 18545 return v === '1.00' ? null : Number(v); 18546 } 18547 }, 18548 18549 textOpacity: { 18550 selector: '.vjs-text-opacity > select', 18551 id: 'captions-foreground-opacity-%s', 18552 label: 'Transparency', 18553 options: [OPACITY_OPAQUE, OPACITY_SEMI] 18554 }, 18555 18556 // Options for this object are defined below. 18557 windowColor: { 18558 selector: '.vjs-window-color > select', 18559 id: 'captions-window-color-%s', 18560 label: 'Color' 18561 }, 18562 18563 // Options for this object are defined below. 18564 windowOpacity: { 18565 selector: '.vjs-window-opacity > select', 18566 id: 'captions-window-opacity-%s', 18567 label: 'Transparency', 18568 options: [OPACITY_TRANS, OPACITY_SEMI, OPACITY_OPAQUE] 18569 } 18570 }; 18571 18572 selectConfigs.windowColor.options = selectConfigs.backgroundColor.options; 18573 18574 /** 18575 * Get the actual value of an option. 18576 * 18577 * @param {string} value 18578 * The value to get 18579 * 18580 * @param {Function} [parser] 18581 * Optional function to adjust the value. 18582 * 18583 * @return {Mixed} 18584 * - Will be `undefined` if no value exists 18585 * - Will be `undefined` if the given value is "none". 18586 * - Will be the actual value otherwise. 18587 * 18588 * @private 18589 */ 18590 function parseOptionValue(value, parser) { 18591 if (parser) { 18592 value = parser(value); 18593 } 18594 18595 if (value && value !== 'none') { 18596 return value; 18597 } 18598 } 18599 18600 /** 18601 * Gets the value of the selected <option> element within a <select> element. 18602 * 18603 * @param {Element} el 18604 * the element to look in 18605 * 18606 * @param {Function} [parser] 18607 * Optional function to adjust the value. 18608 * 18609 * @return {Mixed} 18610 * - Will be `undefined` if no value exists 18611 * - Will be `undefined` if the given value is "none". 18612 * - Will be the actual value otherwise. 18613 * 18614 * @private 18615 */ 18616 function getSelectedOptionValue(el, parser) { 18617 var value = el.options[el.options.selectedIndex].value; 18618 18619 return parseOptionValue(value, parser); 18620 } 18621 18622 /** 18623 * Sets the selected <option> element within a <select> element based on a 18624 * given value. 18625 * 18626 * @param {Element} el 18627 * The element to look in. 18628 * 18629 * @param {string} value 18630 * the property to look on. 18631 * 18632 * @param {Function} [parser] 18633 * Optional function to adjust the value before comparing. 18634 * 18635 * @private 18636 */ 18637 function setSelectedOption(el, value, parser) { 18638 if (!value) { 18639 return; 18640 } 18641 18642 for (var i = 0; i < el.options.length; i++) { 18643 if (parseOptionValue(el.options[i].value, parser) === value) { 18644 el.selectedIndex = i; 18645 break; 18646 } 18647 } 18648 } 18649 18650 /** 18651 * Manipulate Text Tracks settings. 18652 * 18653 * @extends ModalDialog 18654 */ 18655 18656 var TextTrackSettings = function (_ModalDialog) { 18657 inherits(TextTrackSettings, _ModalDialog); 18658 18659 /** 18660 * Creates an instance of this class. 18661 * 18662 * @param {Player} player 18663 * The `Player` that this class should be attached to. 18664 * 18665 * @param {Object} [options] 18666 * The key/value store of player options. 18667 */ 18668 function TextTrackSettings(player, options) { 18669 classCallCheck(this, TextTrackSettings); 18670 18671 options.temporary = false; 18672 18673 var _this = possibleConstructorReturn(this, _ModalDialog.call(this, player, options)); 18674 18675 _this.updateDisplay = bind(_this, _this.updateDisplay); 18676 18677 // fill the modal and pretend we have opened it 18678 _this.fill(); 18679 _this.hasBeenOpened_ = _this.hasBeenFilled_ = true; 18680 18681 _this.endDialog = createEl('p', { 18682 className: 'vjs-control-text', 18683 textContent: _this.localize('End of dialog window.') 18684 }); 18685 _this.el().appendChild(_this.endDialog); 18686 18687 _this.setDefaults(); 18688 18689 // Grab `persistTextTrackSettings` from the player options if not passed in child options 18690 if (options.persistTextTrackSettings === undefined) { 18691 _this.options_.persistTextTrackSettings = _this.options_.playerOptions.persistTextTrackSettings; 18692 } 18693 18694 _this.on(_this.$('.vjs-done-button'), 'click', function () { 18695 _this.saveSettings(); 18696 _this.close(); 18697 }); 18698 18699 _this.on(_this.$('.vjs-default-button'), 'click', function () { 18700 _this.setDefaults(); 18701 _this.updateDisplay(); 18702 }); 18703 18704 each(selectConfigs, function (config) { 18705 _this.on(_this.$(config.selector), 'change', _this.updateDisplay); 18706 }); 18707 18708 if (_this.options_.persistTextTrackSettings) { 18709 _this.restoreSettings(); 18710 } 18711 return _this; 18712 } 18713 18714 TextTrackSettings.prototype.dispose = function dispose() { 18715 this.endDialog = null; 18716 18717 _ModalDialog.prototype.dispose.call(this); 18718 }; 18719 18720 /** 18721 * Create a <select> element with configured options. 18722 * 18723 * @param {string} key 18724 * Configuration key to use during creation. 18725 * 18726 * @return {string} 18727 * An HTML string. 18728 * 18729 * @private 18730 */ 18731 18732 18733 TextTrackSettings.prototype.createElSelect_ = function createElSelect_(key) { 18734 var _this2 = this; 18735 18736 var legendId = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; 18737 var type = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'label'; 18738 18739 var config = selectConfigs[key]; 18740 var id = config.id.replace('%s', this.id_); 18741 var selectLabelledbyIds = [legendId, id].join(' ').trim(); 18742 18743 return ['<' + type + ' id="' + id + '" class="' + (type === 'label' ? 'vjs-label' : '') + '">', this.localize(config.label), '</' + type + '>', '<select aria-labelledby="' + selectLabelledbyIds + '">'].concat(config.options.map(function (o) { 18744 var optionId = id + '-' + o[1].replace(/\W+/g, ''); 18745 18746 return ['<option id="' + optionId + '" value="' + o[0] + '" ', 'aria-labelledby="' + selectLabelledbyIds + ' ' + optionId + '">', _this2.localize(o[1]), '</option>'].join(''); 18747 })).concat('</select>').join(''); 18748 }; 18749 18750 /** 18751 * Create foreground color element for the component 18752 * 18753 * @return {string} 18754 * An HTML string. 18755 * 18756 * @private 18757 */ 18758 18759 18760 TextTrackSettings.prototype.createElFgColor_ = function createElFgColor_() { 18761 var legendId = 'captions-text-legend-' + this.id_; 18762 18763 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(''); 18764 }; 18765 18766 /** 18767 * Create background color element for the component 18768 * 18769 * @return {string} 18770 * An HTML string. 18771 * 18772 * @private 18773 */ 18774 18775 18776 TextTrackSettings.prototype.createElBgColor_ = function createElBgColor_() { 18777 var legendId = 'captions-background-' + this.id_; 18778 18779 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(''); 18780 }; 18781 18782 /** 18783 * Create window color element for the component 18784 * 18785 * @return {string} 18786 * An HTML string. 18787 * 18788 * @private 18789 */ 18790 18791 18792 TextTrackSettings.prototype.createElWinColor_ = function createElWinColor_() { 18793 var legendId = 'captions-window-' + this.id_; 18794 18795 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(''); 18796 }; 18797 18798 /** 18799 * Create color elements for the component 18800 * 18801 * @return {Element} 18802 * The element that was created 18803 * 18804 * @private 18805 */ 18806 18807 18808 TextTrackSettings.prototype.createElColors_ = function createElColors_() { 18809 return createEl('div', { 18810 className: 'vjs-track-settings-colors', 18811 innerHTML: [this.createElFgColor_(), this.createElBgColor_(), this.createElWinColor_()].join('') 18812 }); 18813 }; 18814 18815 /** 18816 * Create font elements for the component 18817 * 18818 * @return {Element} 18819 * The element that was created. 18820 * 18821 * @private 18822 */ 18823 18824 18825 TextTrackSettings.prototype.createElFont_ = function createElFont_() { 18826 return createEl('div', { 18827 className: 'vjs-track-settings-font', 18828 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('') 18829 }); 18830 }; 18831 18832 /** 18833 * Create controls for the component 18834 * 18835 * @return {Element} 18836 * The element that was created. 18837 * 18838 * @private 18839 */ 18840 18841 18842 TextTrackSettings.prototype.createElControls_ = function createElControls_() { 18843 var defaultsDescription = this.localize('restore all settings to the default values'); 18844 18845 return createEl('div', { 18846 className: 'vjs-track-settings-controls', 18847 innerHTML: ['<button class="vjs-default-button" title="' + defaultsDescription + '">', this.localize('Reset'), '<span class="vjs-control-text"> ' + defaultsDescription + '</span>', '</button>', '<button class="vjs-done-button">' + this.localize('Done') + '</button>'].join('') 18848 }); 18849 }; 18850 18851 TextTrackSettings.prototype.content = function content() { 18852 return [this.createElColors_(), this.createElFont_(), this.createElControls_()]; 18853 }; 18854 18855 TextTrackSettings.prototype.label = function label() { 18856 return this.localize('Caption Settings Dialog'); 18857 }; 18858 18859 TextTrackSettings.prototype.description = function description() { 18860 return this.localize('Beginning of dialog window. Escape will cancel and close the window.'); 18861 }; 18862 18863 TextTrackSettings.prototype.buildCSSClass = function buildCSSClass() { 18864 return _ModalDialog.prototype.buildCSSClass.call(this) + ' vjs-text-track-settings'; 18865 }; 18866 18867 /** 18868 * Gets an object of text track settings (or null). 18869 * 18870 * @return {Object} 18871 * An object with config values parsed from the DOM or localStorage. 18872 */ 18873 18874 18875 TextTrackSettings.prototype.getValues = function getValues() { 18876 var _this3 = this; 18877 18878 return reduce(selectConfigs, function (accum, config, key) { 18879 var value = getSelectedOptionValue(_this3.$(config.selector), config.parser); 18880 18881 if (value !== undefined) { 18882 accum[key] = value; 18883 } 18884 18885 return accum; 18886 }, {}); 18887 }; 18888 18889 /** 18890 * Sets text track settings from an object of values. 18891 * 18892 * @param {Object} values 18893 * An object with config values parsed from the DOM or localStorage. 18894 */ 18895 18896 18897 TextTrackSettings.prototype.setValues = function setValues(values) { 18898 var _this4 = this; 18899 18900 each(selectConfigs, function (config, key) { 18901 setSelectedOption(_this4.$(config.selector), values[key], config.parser); 18902 }); 18903 }; 18904 18905 /** 18906 * Sets all `<select>` elements to their default values. 18907 */ 18908 18909 18910 TextTrackSettings.prototype.setDefaults = function setDefaults() { 18911 var _this5 = this; 18912 18913 each(selectConfigs, function (config) { 18914 var index = config.hasOwnProperty('default') ? config.default : 0; 18915 18916 _this5.$(config.selector).selectedIndex = index; 18917 }); 18918 }; 18919 18920 /** 18921 * Restore texttrack settings from localStorage 18922 */ 18923 18924 18925 TextTrackSettings.prototype.restoreSettings = function restoreSettings() { 18926 var values = void 0; 18927 18928 try { 18929 values = JSON.parse(window_1.localStorage.getItem(LOCAL_STORAGE_KEY)); 18930 } catch (err) { 18931 log$1.warn(err); 18932 } 18933 18934 if (values) { 18935 this.setValues(values); 18936 } 18937 }; 18938 18939 /** 18940 * Save text track settings to localStorage 18941 */ 18942 18943 18944 TextTrackSettings.prototype.saveSettings = function saveSettings() { 18945 if (!this.options_.persistTextTrackSettings) { 18946 return; 18947 } 18948 18949 var values = this.getValues(); 18950 18951 try { 18952 if (Object.keys(values).length) { 18953 window_1.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(values)); 18954 } else { 18955 window_1.localStorage.removeItem(LOCAL_STORAGE_KEY); 18956 } 18957 } catch (err) { 18958 log$1.warn(err); 18959 } 18960 }; 18961 18962 /** 18963 * Update display of text track settings 18964 */ 18965 18966 18967 TextTrackSettings.prototype.updateDisplay = function updateDisplay() { 18968 var ttDisplay = this.player_.getChild('textTrackDisplay'); 18969 18970 if (ttDisplay) { 18971 ttDisplay.updateDisplay(); 18972 } 18973 }; 18974 18975 /** 18976 * conditionally blur the element and refocus the captions button 18977 * 18978 * @private 18979 */ 18980 18981 18982 TextTrackSettings.prototype.conditionalBlur_ = function conditionalBlur_() { 18983 this.previouslyActiveEl_ = null; 18984 this.off(document_1, 'keydown', this.handleKeyDown); 18985 18986 var cb = this.player_.controlBar; 18987 var subsCapsBtn = cb && cb.subsCapsButton; 18988 var ccBtn = cb && cb.captionsButton; 18989 18990 if (subsCapsBtn) { 18991 subsCapsBtn.focus(); 18992 } else if (ccBtn) { 18993 ccBtn.focus(); 18994 } 18995 }; 18996 18997 return TextTrackSettings; 18998 }(ModalDialog); 18999 19000 Component.registerComponent('TextTrackSettings', TextTrackSettings); 19001 19002 /** 19003 * @file resize-manager.js 19004 */ 19005 19006 /** 19007 * A Resize Manager. It is in charge of triggering `playerresize` on the player in the right conditions. 19008 * 19009 * 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}. 19010 * 19011 * If the ResizeObserver is available natively, it will be used. A polyfill can be passed in as an option. 19012 * If a `playerresize` event is not needed, the ResizeManager component can be removed from the player, see the example below. 19013 * @example <caption>How to disable the resize manager</caption> 19014 * const player = videojs('#vid', { 19015 * resizeManager: false 19016 * }); 19017 * 19018 * @see {@link https://wicg.github.io/ResizeObserver/|ResizeObserver specification} 19019 * 19020 * @extends Component 19021 */ 19022 19023 var ResizeManager = function (_Component) { 19024 inherits(ResizeManager, _Component); 19025 19026 /** 19027 * Create the ResizeManager. 19028 * 19029 * @param {Object} player 19030 * The `Player` that this class should be attached to. 19031 * 19032 * @param {Object} [options] 19033 * The key/value store of ResizeManager options. 19034 * 19035 * @param {Object} [options.ResizeObserver] 19036 * A polyfill for ResizeObserver can be passed in here. 19037 * If this is set to null it will ignore the native ResizeObserver and fall back to the iframe fallback. 19038 */ 19039 function ResizeManager(player, options) { 19040 classCallCheck(this, ResizeManager); 19041 19042 var RESIZE_OBSERVER_AVAILABLE = options.ResizeObserver || window_1.ResizeObserver; 19043 19044 // if `null` was passed, we want to disable the ResizeObserver 19045 if (options.ResizeObserver === null) { 19046 RESIZE_OBSERVER_AVAILABLE = false; 19047 } 19048 19049 // Only create an element when ResizeObserver isn't available 19050 var options_ = mergeOptions({ createEl: !RESIZE_OBSERVER_AVAILABLE }, options); 19051 19052 var _this = possibleConstructorReturn(this, _Component.call(this, player, options_)); 19053 19054 _this.ResizeObserver = options.ResizeObserver || window_1.ResizeObserver; 19055 _this.loadListener_ = null; 19056 _this.resizeObserver_ = null; 19057 _this.debouncedHandler_ = debounce(function () { 19058 _this.resizeHandler(); 19059 }, 100, false, player); 19060 19061 if (RESIZE_OBSERVER_AVAILABLE) { 19062 _this.resizeObserver_ = new _this.ResizeObserver(_this.debouncedHandler_); 19063 _this.resizeObserver_.observe(player.el()); 19064 } else { 19065 _this.loadListener_ = function () { 19066 if (_this.el_.contentWindow) { 19067 on(_this.el_.contentWindow, 'resize', _this.debouncedHandler_); 19068 } 19069 _this.off('load', _this.loadListener_); 19070 }; 19071 19072 _this.on('load', _this.loadListener_); 19073 } 19074 return _this; 19075 } 19076 19077 ResizeManager.prototype.createEl = function createEl() { 19078 return _Component.prototype.createEl.call(this, 'iframe', { 19079 className: 'vjs-resize-manager' 19080 }); 19081 }; 19082 19083 /** 19084 * Called when a resize is triggered on the iframe or a resize is observed via the ResizeObserver 19085 * 19086 * @fires Player#playerresize 19087 */ 19088 19089 19090 ResizeManager.prototype.resizeHandler = function resizeHandler() { 19091 /** 19092 * Called when the player size has changed 19093 * 19094 * @event Player#playerresize 19095 * @type {EventTarget~Event} 19096 */ 19097 this.player_.trigger('playerresize'); 19098 }; 19099 19100 ResizeManager.prototype.dispose = function dispose() { 19101 if (this.resizeObserver_) { 19102 if (this.player_.el()) { 19103 this.resizeObserver_.unobserve(this.player_.el()); 19104 } 19105 this.resizeObserver_.disconnect(); 19106 } 19107 19108 if (this.el_ && this.el_.contentWindow) { 19109 off(this.el_.contentWindow, 'resize', this.debouncedHandler_); 19110 } 19111 19112 if (this.loadListener_) { 19113 this.off('load', this.loadListener_); 19114 } 19115 19116 this.ResizeObserver = null; 19117 this.resizeObserver = null; 19118 this.debouncedHandler_ = null; 19119 this.loadListener_ = null; 19120 }; 19121 19122 return ResizeManager; 19123 }(Component); 19124 19125 Component.registerComponent('ResizeManager', ResizeManager); 19126 19127 /** 19128 * This function is used to fire a sourceset when there is something 19129 * similar to `mediaEl.load()` being called. It will try to find the source via 19130 * the `src` attribute and then the `<source>` elements. It will then fire `sourceset` 19131 * with the source that was found or empty string if we cannot know. If it cannot 19132 * find a source then `sourceset` will not be fired. 19133 * 19134 * @param {Html5} tech 19135 * The tech object that sourceset was setup on 19136 * 19137 * @return {boolean} 19138 * returns false if the sourceset was not fired and true otherwise. 19139 */ 19140 var sourcesetLoad = function sourcesetLoad(tech) { 19141 var el = tech.el(); 19142 19143 // if `el.src` is set, that source will be loaded. 19144 if (el.hasAttribute('src')) { 19145 tech.triggerSourceset(el.src); 19146 return true; 19147 } 19148 19149 /** 19150 * Since there isn't a src property on the media element, source elements will be used for 19151 * implementing the source selection algorithm. This happens asynchronously and 19152 * for most cases were there is more than one source we cannot tell what source will 19153 * be loaded, without re-implementing the source selection algorithm. At this time we are not 19154 * going to do that. There are three special cases that we do handle here though: 19155 * 19156 * 1. If there are no sources, do not fire `sourceset`. 19157 * 2. If there is only one `<source>` with a `src` property/attribute that is our `src` 19158 * 3. If there is more than one `<source>` but all of them have the same `src` url. 19159 * That will be our src. 19160 */ 19161 var sources = tech.$$('source'); 19162 var srcUrls = []; 19163 var src = ''; 19164 19165 // if there are no sources, do not fire sourceset 19166 if (!sources.length) { 19167 return false; 19168 } 19169 19170 // only count valid/non-duplicate source elements 19171 for (var i = 0; i < sources.length; i++) { 19172 var url = sources[i].src; 19173 19174 if (url && srcUrls.indexOf(url) === -1) { 19175 srcUrls.push(url); 19176 } 19177 } 19178 19179 // there were no valid sources 19180 if (!srcUrls.length) { 19181 return false; 19182 } 19183 19184 // there is only one valid source element url 19185 // use that 19186 if (srcUrls.length === 1) { 19187 src = srcUrls[0]; 19188 } 19189 19190 tech.triggerSourceset(src); 19191 return true; 19192 }; 19193 19194 /** 19195 * our implementation of an `innerHTML` descriptor for browsers 19196 * that do not have one. 19197 */ 19198 var innerHTMLDescriptorPolyfill = Object.defineProperty({}, 'innerHTML', { 19199 get: function get() { 19200 return this.cloneNode(true).innerHTML; 19201 }, 19202 set: function set(v) { 19203 // make a dummy node to use innerHTML on 19204 var dummy = document_1.createElement(this.nodeName.toLowerCase()); 19205 19206 // set innerHTML to the value provided 19207 dummy.innerHTML = v; 19208 19209 // make a document fragment to hold the nodes from dummy 19210 var docFrag = document_1.createDocumentFragment(); 19211 19212 // copy all of the nodes created by the innerHTML on dummy 19213 // to the document fragment 19214 while (dummy.childNodes.length) { 19215 docFrag.appendChild(dummy.childNodes[0]); 19216 } 19217 19218 // remove content 19219 this.innerText = ''; 19220 19221 // now we add all of that html in one by appending the 19222 // document fragment. This is how innerHTML does it. 19223 window_1.Element.prototype.appendChild.call(this, docFrag); 19224 19225 // then return the result that innerHTML's setter would 19226 return this.innerHTML; 19227 } 19228 }); 19229 19230 /** 19231 * Get a property descriptor given a list of priorities and the 19232 * property to get. 19233 */ 19234 var getDescriptor = function getDescriptor(priority, prop) { 19235 var descriptor = {}; 19236 19237 for (var i = 0; i < priority.length; i++) { 19238 descriptor = Object.getOwnPropertyDescriptor(priority[i], prop); 19239 19240 if (descriptor && descriptor.set && descriptor.get) { 19241 break; 19242 } 19243 } 19244 19245 descriptor.enumerable = true; 19246 descriptor.configurable = true; 19247 19248 return descriptor; 19249 }; 19250 19251 var getInnerHTMLDescriptor = function getInnerHTMLDescriptor(tech) { 19252 return getDescriptor([tech.el(), window_1.HTMLMediaElement.prototype, window_1.Element.prototype, innerHTMLDescriptorPolyfill], 'innerHTML'); 19253 }; 19254 19255 /** 19256 * Patches browser internal functions so that we can tell synchronously 19257 * if a `<source>` was appended to the media element. For some reason this 19258 * causes a `sourceset` if the the media element is ready and has no source. 19259 * This happens when: 19260 * - The page has just loaded and the media element does not have a source. 19261 * - The media element was emptied of all sources, then `load()` was called. 19262 * 19263 * It does this by patching the following functions/properties when they are supported: 19264 * 19265 * - `append()` - can be used to add a `<source>` element to the media element 19266 * - `appendChild()` - can be used to add a `<source>` element to the media element 19267 * - `insertAdjacentHTML()` - can be used to add a `<source>` element to the media element 19268 * - `innerHTML` - can be used to add a `<source>` element to the media element 19269 * 19270 * @param {Html5} tech 19271 * The tech object that sourceset is being setup on. 19272 */ 19273 var firstSourceWatch = function firstSourceWatch(tech) { 19274 var el = tech.el(); 19275 19276 // make sure firstSourceWatch isn't setup twice. 19277 if (el.resetSourceWatch_) { 19278 return; 19279 } 19280 19281 var old = {}; 19282 var innerDescriptor = getInnerHTMLDescriptor(tech); 19283 var appendWrapper = function appendWrapper(appendFn) { 19284 return function () { 19285 for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { 19286 args[_key] = arguments[_key]; 19287 } 19288 19289 var retval = appendFn.apply(el, args); 19290 19291 sourcesetLoad(tech); 19292 19293 return retval; 19294 }; 19295 }; 19296 19297 ['append', 'appendChild', 'insertAdjacentHTML'].forEach(function (k) { 19298 if (!el[k]) { 19299 return; 19300 } 19301 19302 // store the old function 19303 old[k] = el[k]; 19304 19305 // call the old function with a sourceset if a source 19306 // was loaded 19307 el[k] = appendWrapper(old[k]); 19308 }); 19309 19310 Object.defineProperty(el, 'innerHTML', mergeOptions(innerDescriptor, { 19311 set: appendWrapper(innerDescriptor.set) 19312 })); 19313 19314 el.resetSourceWatch_ = function () { 19315 el.resetSourceWatch_ = null; 19316 Object.keys(old).forEach(function (k) { 19317 el[k] = old[k]; 19318 }); 19319 19320 Object.defineProperty(el, 'innerHTML', innerDescriptor); 19321 }; 19322 19323 // on the first sourceset, we need to revert our changes 19324 tech.one('sourceset', el.resetSourceWatch_); 19325 }; 19326 19327 /** 19328 * our implementation of a `src` descriptor for browsers 19329 * that do not have one. 19330 */ 19331 var srcDescriptorPolyfill = Object.defineProperty({}, 'src', { 19332 get: function get() { 19333 if (this.hasAttribute('src')) { 19334 return getAbsoluteURL(window_1.Element.prototype.getAttribute.call(this, 'src')); 19335 } 19336 19337 return ''; 19338 }, 19339 set: function set(v) { 19340 window_1.Element.prototype.setAttribute.call(this, 'src', v); 19341 19342 return v; 19343 } 19344 }); 19345 19346 var getSrcDescriptor = function getSrcDescriptor(tech) { 19347 return getDescriptor([tech.el(), window_1.HTMLMediaElement.prototype, srcDescriptorPolyfill], 'src'); 19348 }; 19349 19350 /** 19351 * setup `sourceset` handling on the `Html5` tech. This function 19352 * patches the following element properties/functions: 19353 * 19354 * - `src` - to determine when `src` is set 19355 * - `setAttribute()` - to determine when `src` is set 19356 * - `load()` - this re-triggers the source selection algorithm, and can 19357 * cause a sourceset. 19358 * 19359 * If there is no source when we are adding `sourceset` support or during a `load()` 19360 * we also patch the functions listed in `firstSourceWatch`. 19361 * 19362 * @param {Html5} tech 19363 * The tech to patch 19364 */ 19365 var setupSourceset = function setupSourceset(tech) { 19366 if (!tech.featuresSourceset) { 19367 return; 19368 } 19369 19370 var el = tech.el(); 19371 19372 // make sure sourceset isn't setup twice. 19373 if (el.resetSourceset_) { 19374 return; 19375 } 19376 19377 var srcDescriptor = getSrcDescriptor(tech); 19378 var oldSetAttribute = el.setAttribute; 19379 var oldLoad = el.load; 19380 19381 Object.defineProperty(el, 'src', mergeOptions(srcDescriptor, { 19382 set: function set(v) { 19383 var retval = srcDescriptor.set.call(el, v); 19384 19385 // we use the getter here to get the actual value set on src 19386 tech.triggerSourceset(el.src); 19387 19388 return retval; 19389 } 19390 })); 19391 19392 el.setAttribute = function (n, v) { 19393 var retval = oldSetAttribute.call(el, n, v); 19394 19395 if (/src/i.test(n)) { 19396 tech.triggerSourceset(el.src); 19397 } 19398 19399 return retval; 19400 }; 19401 19402 el.load = function () { 19403 var retval = oldLoad.call(el); 19404 19405 // if load was called, but there was no source to fire 19406 // sourceset on. We have to watch for a source append 19407 // as that can trigger a `sourceset` when the media element 19408 // has no source 19409 if (!sourcesetLoad(tech)) { 19410 tech.triggerSourceset(''); 19411 firstSourceWatch(tech); 19412 } 19413 19414 return retval; 19415 }; 19416 19417 if (el.currentSrc) { 19418 tech.triggerSourceset(el.currentSrc); 19419 } else if (!sourcesetLoad(tech)) { 19420 firstSourceWatch(tech); 19421 } 19422 19423 el.resetSourceset_ = function () { 19424 el.resetSourceset_ = null; 19425 el.load = oldLoad; 19426 el.setAttribute = oldSetAttribute; 19427 Object.defineProperty(el, 'src', srcDescriptor); 19428 if (el.resetSourceWatch_) { 19429 el.resetSourceWatch_(); 19430 } 19431 }; 19432 }; 19433 19434 var _templateObject$1 = taggedTemplateLiteralLoose(['Text Tracks are being loaded from another origin but the crossorigin attribute isn\'t used.\n This may prevent text tracks from loading.'], ['Text Tracks are being loaded from another origin but the crossorigin attribute isn\'t used.\n This may prevent text tracks from loading.']); 19435 19436 /** 19437 * HTML5 Media Controller - Wrapper for HTML5 Media API 19438 * 19439 * @mixes Tech~SourceHandlerAdditions 19440 * @extends Tech 19441 */ 19442 19443 var Html5 = function (_Tech) { 19444 inherits(Html5, _Tech); 19445 19446 /** 19447 * Create an instance of this Tech. 19448 * 19449 * @param {Object} [options] 19450 * The key/value store of player options. 19451 * 19452 * @param {Component~ReadyCallback} ready 19453 * Callback function to call when the `HTML5` Tech is ready. 19454 */ 19455 function Html5(options, ready) { 19456 classCallCheck(this, Html5); 19457 19458 var _this = possibleConstructorReturn(this, _Tech.call(this, options, ready)); 19459 19460 var source = options.source; 19461 var crossoriginTracks = false; 19462 19463 // Set the source if one is provided 19464 // 1) Check if the source is new (if not, we want to keep the original so playback isn't interrupted) 19465 // 2) Check to see if the network state of the tag was failed at init, and if so, reset the source 19466 // anyway so the error gets fired. 19467 if (source && (_this.el_.currentSrc !== source.src || options.tag && options.tag.initNetworkState_ === 3)) { 19468 _this.setSource(source); 19469 } else { 19470 _this.handleLateInit_(_this.el_); 19471 } 19472 19473 // setup sourceset after late sourceset/init 19474 if (options.enableSourceset) { 19475 _this.setupSourcesetHandling_(); 19476 } 19477 19478 if (_this.el_.hasChildNodes()) { 19479 19480 var nodes = _this.el_.childNodes; 19481 var nodesLength = nodes.length; 19482 var removeNodes = []; 19483 19484 while (nodesLength--) { 19485 var node = nodes[nodesLength]; 19486 var nodeName = node.nodeName.toLowerCase(); 19487 19488 if (nodeName === 'track') { 19489 if (!_this.featuresNativeTextTracks) { 19490 // Empty video tag tracks so the built-in player doesn't use them also. 19491 // This may not be fast enough to stop HTML5 browsers from reading the tags 19492 // so we'll need to turn off any default tracks if we're manually doing 19493 // captions and subtitles. videoElement.textTracks 19494 removeNodes.push(node); 19495 } else { 19496 // store HTMLTrackElement and TextTrack to remote list 19497 _this.remoteTextTrackEls().addTrackElement_(node); 19498 _this.remoteTextTracks().addTrack(node.track); 19499 _this.textTracks().addTrack(node.track); 19500 if (!crossoriginTracks && !_this.el_.hasAttribute('crossorigin') && isCrossOrigin(node.src)) { 19501 crossoriginTracks = true; 19502 } 19503 } 19504 } 19505 } 19506 19507 for (var i = 0; i < removeNodes.length; i++) { 19508 _this.el_.removeChild(removeNodes[i]); 19509 } 19510 } 19511 19512 _this.proxyNativeTracks_(); 19513 if (_this.featuresNativeTextTracks && crossoriginTracks) { 19514 log$1.warn(tsml(_templateObject$1)); 19515 } 19516 19517 // prevent iOS Safari from disabling metadata text tracks during native playback 19518 _this.restoreMetadataTracksInIOSNativePlayer_(); 19519 19520 // Determine if native controls should be used 19521 // Our goal should be to get the custom controls on mobile solid everywhere 19522 // so we can remove this all together. Right now this will block custom 19523 // controls on touch enabled laptops like the Chrome Pixel 19524 if ((TOUCH_ENABLED || IS_IPHONE || IS_NATIVE_ANDROID) && options.nativeControlsForTouch === true) { 19525 _this.setControls(true); 19526 } 19527 19528 // on iOS, we want to proxy `webkitbeginfullscreen` and `webkitendfullscreen` 19529 // into a `fullscreenchange` event 19530 _this.proxyWebkitFullscreen_(); 19531 19532 _this.triggerReady(); 19533 return _this; 19534 } 19535 19536 /** 19537 * Dispose of `HTML5` media element and remove all tracks. 19538 */ 19539 19540 19541 Html5.prototype.dispose = function dispose() { 19542 if (this.el_ && this.el_.resetSourceset_) { 19543 this.el_.resetSourceset_(); 19544 } 19545 Html5.disposeMediaElement(this.el_); 19546 this.options_ = null; 19547 19548 // tech will handle clearing of the emulated track list 19549 _Tech.prototype.dispose.call(this); 19550 }; 19551 19552 /** 19553 * Modify the media element so that we can detect when 19554 * the source is changed. Fires `sourceset` just after the source has changed 19555 */ 19556 19557 19558 Html5.prototype.setupSourcesetHandling_ = function setupSourcesetHandling_() { 19559 setupSourceset(this); 19560 }; 19561 19562 /** 19563 * When a captions track is enabled in the iOS Safari native player, all other 19564 * tracks are disabled (including metadata tracks), which nulls all of their 19565 * associated cue points. This will restore metadata tracks to their pre-fullscreen 19566 * state in those cases so that cue points are not needlessly lost. 19567 * 19568 * @private 19569 */ 19570 19571 19572 Html5.prototype.restoreMetadataTracksInIOSNativePlayer_ = function restoreMetadataTracksInIOSNativePlayer_() { 19573 var textTracks = this.textTracks(); 19574 var metadataTracksPreFullscreenState = void 0; 19575 19576 // captures a snapshot of every metadata track's current state 19577 var takeMetadataTrackSnapshot = function takeMetadataTrackSnapshot() { 19578 metadataTracksPreFullscreenState = []; 19579 19580 for (var i = 0; i < textTracks.length; i++) { 19581 var track = textTracks[i]; 19582 19583 if (track.kind === 'metadata') { 19584 metadataTracksPreFullscreenState.push({ 19585 track: track, 19586 storedMode: track.mode 19587 }); 19588 } 19589 } 19590 }; 19591 19592 // snapshot each metadata track's initial state, and update the snapshot 19593 // each time there is a track 'change' event 19594 takeMetadataTrackSnapshot(); 19595 textTracks.addEventListener('change', takeMetadataTrackSnapshot); 19596 19597 this.on('dispose', function () { 19598 return textTracks.removeEventListener('change', takeMetadataTrackSnapshot); 19599 }); 19600 19601 var restoreTrackMode = function restoreTrackMode() { 19602 for (var i = 0; i < metadataTracksPreFullscreenState.length; i++) { 19603 var storedTrack = metadataTracksPreFullscreenState[i]; 19604 19605 if (storedTrack.track.mode === 'disabled' && storedTrack.track.mode !== storedTrack.storedMode) { 19606 storedTrack.track.mode = storedTrack.storedMode; 19607 } 19608 } 19609 // we only want this handler to be executed on the first 'change' event 19610 textTracks.removeEventListener('change', restoreTrackMode); 19611 }; 19612 19613 // when we enter fullscreen playback, stop updating the snapshot and 19614 // restore all track modes to their pre-fullscreen state 19615 this.on('webkitbeginfullscreen', function () { 19616 textTracks.removeEventListener('change', takeMetadataTrackSnapshot); 19617 19618 // remove the listener before adding it just in case it wasn't previously removed 19619 textTracks.removeEventListener('change', restoreTrackMode); 19620 textTracks.addEventListener('change', restoreTrackMode); 19621 }); 19622 19623 // start updating the snapshot again after leaving fullscreen 19624 this.on('webkitendfullscreen', function () { 19625 // remove the listener before adding it just in case it wasn't previously removed 19626 textTracks.removeEventListener('change', takeMetadataTrackSnapshot); 19627 textTracks.addEventListener('change', takeMetadataTrackSnapshot); 19628 19629 // remove the restoreTrackMode handler in case it wasn't triggered during fullscreen playback 19630 textTracks.removeEventListener('change', restoreTrackMode); 19631 }); 19632 }; 19633 19634 /** 19635 * Attempt to force override of tracks for the given type 19636 * 19637 * @param {String} type - Track type to override, possible values include 'Audio', 19638 * 'Video', and 'Text'. 19639 * @param {Boolean} override - If set to true native audio/video will be overridden, 19640 * otherwise native audio/video will potentially be used. 19641 * @private 19642 */ 19643 19644 19645 Html5.prototype.overrideNative_ = function overrideNative_(type, override) { 19646 var _this2 = this; 19647 19648 // If there is no behavioral change don't add/remove listeners 19649 if (override !== this['featuresNative' + type + 'Tracks']) { 19650 return; 19651 } 19652 19653 var lowerCaseType = type.toLowerCase(); 19654 19655 if (this[lowerCaseType + 'TracksListeners_']) { 19656 Object.keys(this[lowerCaseType + 'TracksListeners_']).forEach(function (eventName) { 19657 var elTracks = _this2.el()[lowerCaseType + 'Tracks']; 19658 19659 elTracks.removeEventListener(eventName, _this2[lowerCaseType + 'TracksListeners_'][eventName]); 19660 }); 19661 } 19662 19663 this['featuresNative' + type + 'Tracks'] = !override; 19664 this[lowerCaseType + 'TracksListeners_'] = null; 19665 19666 this.proxyNativeTracksForType_(lowerCaseType); 19667 }; 19668 19669 /** 19670 * Attempt to force override of native audio tracks. 19671 * 19672 * @param {Boolean} override - If set to true native audio will be overridden, 19673 * otherwise native audio will potentially be used. 19674 */ 19675 19676 19677 Html5.prototype.overrideNativeAudioTracks = function overrideNativeAudioTracks(override) { 19678 this.overrideNative_('Audio', override); 19679 }; 19680 19681 /** 19682 * Attempt to force override of native video tracks. 19683 * 19684 * @param {Boolean} override - If set to true native video will be overridden, 19685 * otherwise native video will potentially be used. 19686 */ 19687 19688 19689 Html5.prototype.overrideNativeVideoTracks = function overrideNativeVideoTracks(override) { 19690 this.overrideNative_('Video', override); 19691 }; 19692 19693 /** 19694 * Proxy native track list events for the given type to our track 19695 * lists if the browser we are playing in supports that type of track list. 19696 * 19697 * @param {string} name - Track type; values include 'audio', 'video', and 'text' 19698 * @private 19699 */ 19700 19701 19702 Html5.prototype.proxyNativeTracksForType_ = function proxyNativeTracksForType_(name) { 19703 var _this3 = this; 19704 19705 var props = NORMAL[name]; 19706 var elTracks = this.el()[props.getterName]; 19707 var techTracks = this[props.getterName](); 19708 19709 if (!this['featuresNative' + props.capitalName + 'Tracks'] || !elTracks || !elTracks.addEventListener) { 19710 return; 19711 } 19712 var listeners = { 19713 change: function change(e) { 19714 techTracks.trigger({ 19715 type: 'change', 19716 target: techTracks, 19717 currentTarget: techTracks, 19718 srcElement: techTracks 19719 }); 19720 }, 19721 addtrack: function addtrack(e) { 19722 techTracks.addTrack(e.track); 19723 }, 19724 removetrack: function removetrack(e) { 19725 techTracks.removeTrack(e.track); 19726 } 19727 }; 19728 var removeOldTracks = function removeOldTracks() { 19729 var removeTracks = []; 19730 19731 for (var i = 0; i < techTracks.length; i++) { 19732 var found = false; 19733 19734 for (var j = 0; j < elTracks.length; j++) { 19735 if (elTracks[j] === techTracks[i]) { 19736 found = true; 19737 break; 19738 } 19739 } 19740 19741 if (!found) { 19742 removeTracks.push(techTracks[i]); 19743 } 19744 } 19745 19746 while (removeTracks.length) { 19747 techTracks.removeTrack(removeTracks.shift()); 19748 } 19749 }; 19750 19751 this[props.getterName + 'Listeners_'] = listeners; 19752 19753 Object.keys(listeners).forEach(function (eventName) { 19754 var listener = listeners[eventName]; 19755 19756 elTracks.addEventListener(eventName, listener); 19757 _this3.on('dispose', function (e) { 19758 return elTracks.removeEventListener(eventName, listener); 19759 }); 19760 }); 19761 19762 // Remove (native) tracks that are not used anymore 19763 this.on('loadstart', removeOldTracks); 19764 this.on('dispose', function (e) { 19765 return _this3.off('loadstart', removeOldTracks); 19766 }); 19767 }; 19768 19769 /** 19770 * Proxy all native track list events to our track lists if the browser we are playing 19771 * in supports that type of track list. 19772 * 19773 * @private 19774 */ 19775 19776 19777 Html5.prototype.proxyNativeTracks_ = function proxyNativeTracks_() { 19778 var _this4 = this; 19779 19780 NORMAL.names.forEach(function (name) { 19781 _this4.proxyNativeTracksForType_(name); 19782 }); 19783 }; 19784 19785 /** 19786 * Create the `Html5` Tech's DOM element. 19787 * 19788 * @return {Element} 19789 * The element that gets created. 19790 */ 19791 19792 19793 Html5.prototype.createEl = function createEl$$1() { 19794 var el = this.options_.tag; 19795 19796 // Check if this browser supports moving the element into the box. 19797 // On the iPhone video will break if you move the element, 19798 // So we have to create a brand new element. 19799 // If we ingested the player div, we do not need to move the media element. 19800 if (!el || !(this.options_.playerElIngest || this.movingMediaElementInDOM)) { 19801 19802 // If the original tag is still there, clone and remove it. 19803 if (el) { 19804 var clone = el.cloneNode(true); 19805 19806 if (el.parentNode) { 19807 el.parentNode.insertBefore(clone, el); 19808 } 19809 Html5.disposeMediaElement(el); 19810 el = clone; 19811 } else { 19812 el = document_1.createElement('video'); 19813 19814 // determine if native controls should be used 19815 var tagAttributes = this.options_.tag && getAttributes(this.options_.tag); 19816 var attributes = mergeOptions({}, tagAttributes); 19817 19818 if (!TOUCH_ENABLED || this.options_.nativeControlsForTouch !== true) { 19819 delete attributes.controls; 19820 } 19821 19822 setAttributes(el, assign(attributes, { 19823 id: this.options_.techId, 19824 class: 'vjs-tech' 19825 })); 19826 } 19827 19828 el.playerId = this.options_.playerId; 19829 } 19830 19831 if (typeof this.options_.preload !== 'undefined') { 19832 setAttribute(el, 'preload', this.options_.preload); 19833 } 19834 19835 // Update specific tag settings, in case they were overridden 19836 // `autoplay` has to be *last* so that `muted` and `playsinline` are present 19837 // when iOS/Safari or other browsers attempt to autoplay. 19838 var settingsAttrs = ['loop', 'muted', 'playsinline', 'autoplay']; 19839 19840 for (var i = 0; i < settingsAttrs.length; i++) { 19841 var attr = settingsAttrs[i]; 19842 var value = this.options_[attr]; 19843 19844 if (typeof value !== 'undefined') { 19845 if (value) { 19846 setAttribute(el, attr, attr); 19847 } else { 19848 removeAttribute(el, attr); 19849 } 19850 el[attr] = value; 19851 } 19852 } 19853 19854 return el; 19855 }; 19856 19857 /** 19858 * This will be triggered if the loadstart event has already fired, before videojs was 19859 * ready. Two known examples of when this can happen are: 19860 * 1. If we're loading the playback object after it has started loading 19861 * 2. The media is already playing the (often with autoplay on) then 19862 * 19863 * This function will fire another loadstart so that videojs can catchup. 19864 * 19865 * @fires Tech#loadstart 19866 * 19867 * @return {undefined} 19868 * returns nothing. 19869 */ 19870 19871 19872 Html5.prototype.handleLateInit_ = function handleLateInit_(el) { 19873 if (el.networkState === 0 || el.networkState === 3) { 19874 // The video element hasn't started loading the source yet 19875 // or didn't find a source 19876 return; 19877 } 19878 19879 if (el.readyState === 0) { 19880 // NetworkState is set synchronously BUT loadstart is fired at the 19881 // end of the current stack, usually before setInterval(fn, 0). 19882 // So at this point we know loadstart may have already fired or is 19883 // about to fire, and either way the player hasn't seen it yet. 19884 // We don't want to fire loadstart prematurely here and cause a 19885 // double loadstart so we'll wait and see if it happens between now 19886 // and the next loop, and fire it if not. 19887 // HOWEVER, we also want to make sure it fires before loadedmetadata 19888 // which could also happen between now and the next loop, so we'll 19889 // watch for that also. 19890 var loadstartFired = false; 19891 var setLoadstartFired = function setLoadstartFired() { 19892 loadstartFired = true; 19893 }; 19894 19895 this.on('loadstart', setLoadstartFired); 19896 19897 var triggerLoadstart = function triggerLoadstart() { 19898 // We did miss the original loadstart. Make sure the player 19899 // sees loadstart before loadedmetadata 19900 if (!loadstartFired) { 19901 this.trigger('loadstart'); 19902 } 19903 }; 19904 19905 this.on('loadedmetadata', triggerLoadstart); 19906 19907 this.ready(function () { 19908 this.off('loadstart', setLoadstartFired); 19909 this.off('loadedmetadata', triggerLoadstart); 19910 19911 if (!loadstartFired) { 19912 // We did miss the original native loadstart. Fire it now. 19913 this.trigger('loadstart'); 19914 } 19915 }); 19916 19917 return; 19918 } 19919 19920 // From here on we know that loadstart already fired and we missed it. 19921 // The other readyState events aren't as much of a problem if we double 19922 // them, so not going to go to as much trouble as loadstart to prevent 19923 // that unless we find reason to. 19924 var eventsToTrigger = ['loadstart']; 19925 19926 // loadedmetadata: newly equal to HAVE_METADATA (1) or greater 19927 eventsToTrigger.push('loadedmetadata'); 19928 19929 // loadeddata: newly increased to HAVE_CURRENT_DATA (2) or greater 19930 if (el.readyState >= 2) { 19931 eventsToTrigger.push('loadeddata'); 19932 } 19933 19934 // canplay: newly increased to HAVE_FUTURE_DATA (3) or greater 19935 if (el.readyState >= 3) { 19936 eventsToTrigger.push('canplay'); 19937 } 19938 19939 // canplaythrough: newly equal to HAVE_ENOUGH_DATA (4) 19940 if (el.readyState >= 4) { 19941 eventsToTrigger.push('canplaythrough'); 19942 } 19943 19944 // We still need to give the player time to add event listeners 19945 this.ready(function () { 19946 eventsToTrigger.forEach(function (type) { 19947 this.trigger(type); 19948 }, this); 19949 }); 19950 }; 19951 19952 /** 19953 * Set current time for the `HTML5` tech. 19954 * 19955 * @param {number} seconds 19956 * Set the current time of the media to this. 19957 */ 19958 19959 19960 Html5.prototype.setCurrentTime = function setCurrentTime(seconds) { 19961 try { 19962 this.el_.currentTime = seconds; 19963 } catch (e) { 19964 log$1(e, 'Video is not ready. (Video.js)'); 19965 // this.warning(VideoJS.warnings.videoNotReady); 19966 } 19967 }; 19968 19969 /** 19970 * Get the current duration of the HTML5 media element. 19971 * 19972 * @return {number} 19973 * The duration of the media or 0 if there is no duration. 19974 */ 19975 19976 19977 Html5.prototype.duration = function duration() { 19978 var _this5 = this; 19979 19980 // Android Chrome will report duration as Infinity for VOD HLS until after 19981 // playback has started, which triggers the live display erroneously. 19982 // Return NaN if playback has not started and trigger a durationupdate once 19983 // the duration can be reliably known. 19984 if (this.el_.duration === Infinity && IS_ANDROID && IS_CHROME && this.el_.currentTime === 0) { 19985 // Wait for the first `timeupdate` with currentTime > 0 - there may be 19986 // several with 0 19987 var checkProgress = function checkProgress() { 19988 if (_this5.el_.currentTime > 0) { 19989 // Trigger durationchange for genuinely live video 19990 if (_this5.el_.duration === Infinity) { 19991 _this5.trigger('durationchange'); 19992 } 19993 _this5.off('timeupdate', checkProgress); 19994 } 19995 }; 19996 19997 this.on('timeupdate', checkProgress); 19998 return NaN; 19999 } 20000 return this.el_.duration || NaN; 20001 }; 20002 20003 /** 20004 * Get the current width of the HTML5 media element. 20005 * 20006 * @return {number} 20007 * The width of the HTML5 media element. 20008 */ 20009 20010 20011 Html5.prototype.width = function width() { 20012 return this.el_.offsetWidth; 20013 }; 20014 20015 /** 20016 * Get the current height of the HTML5 media element. 20017 * 20018 * @return {number} 20019 * The height of the HTML5 media element. 20020 */ 20021 20022 20023 Html5.prototype.height = function height() { 20024 return this.el_.offsetHeight; 20025 }; 20026 20027 /** 20028 * Proxy iOS `webkitbeginfullscreen` and `webkitendfullscreen` into 20029 * `fullscreenchange` event. 20030 * 20031 * @private 20032 * @fires fullscreenchange 20033 * @listens webkitendfullscreen 20034 * @listens webkitbeginfullscreen 20035 * @listens webkitbeginfullscreen 20036 */ 20037 20038 20039 Html5.prototype.proxyWebkitFullscreen_ = function proxyWebkitFullscreen_() { 20040 var _this6 = this; 20041 20042 if (!('webkitDisplayingFullscreen' in this.el_)) { 20043 return; 20044 } 20045 20046 var endFn = function endFn() { 20047 this.trigger('fullscreenchange', { isFullscreen: false }); 20048 }; 20049 20050 var beginFn = function beginFn() { 20051 if ('webkitPresentationMode' in this.el_ && this.el_.webkitPresentationMode !== 'picture-in-picture') { 20052 this.one('webkitendfullscreen', endFn); 20053 20054 this.trigger('fullscreenchange', { isFullscreen: true }); 20055 } 20056 }; 20057 20058 this.on('webkitbeginfullscreen', beginFn); 20059 this.on('dispose', function () { 20060 _this6.off('webkitbeginfullscreen', beginFn); 20061 _this6.off('webkitendfullscreen', endFn); 20062 }); 20063 }; 20064 20065 /** 20066 * Check if fullscreen is supported on the current playback device. 20067 * 20068 * @return {boolean} 20069 * - True if fullscreen is supported. 20070 * - False if fullscreen is not supported. 20071 */ 20072 20073 20074 Html5.prototype.supportsFullScreen = function supportsFullScreen() { 20075 if (typeof this.el_.webkitEnterFullScreen === 'function') { 20076 var userAgent = window_1.navigator && window_1.navigator.userAgent || ''; 20077 20078 // Seems to be broken in Chromium/Chrome && Safari in Leopard 20079 if (/Android/.test(userAgent) || !/Chrome|Mac OS X 10.5/.test(userAgent)) { 20080 return true; 20081 } 20082 } 20083 return false; 20084 }; 20085 20086 /** 20087 * Request that the `HTML5` Tech enter fullscreen. 20088 */ 20089 20090 20091 Html5.prototype.enterFullScreen = function enterFullScreen() { 20092 var video = this.el_; 20093 20094 if (video.paused && video.networkState <= video.HAVE_METADATA) { 20095 // attempt to prime the video element for programmatic access 20096 // this isn't necessary on the desktop but shouldn't hurt 20097 this.el_.play(); 20098 20099 // playing and pausing synchronously during the transition to fullscreen 20100 // can get iOS ~6.1 devices into a play/pause loop 20101 this.setTimeout(function () { 20102 video.pause(); 20103 video.webkitEnterFullScreen(); 20104 }, 0); 20105 } else { 20106 video.webkitEnterFullScreen(); 20107 } 20108 }; 20109 20110 /** 20111 * Request that the `HTML5` Tech exit fullscreen. 20112 */ 20113 20114 20115 Html5.prototype.exitFullScreen = function exitFullScreen() { 20116 this.el_.webkitExitFullScreen(); 20117 }; 20118 20119 /** 20120 * A getter/setter for the `Html5` Tech's source object. 20121 * > Note: Please use {@link Html5#setSource} 20122 * 20123 * @param {Tech~SourceObject} [src] 20124 * The source object you want to set on the `HTML5` techs element. 20125 * 20126 * @return {Tech~SourceObject|undefined} 20127 * - The current source object when a source is not passed in. 20128 * - undefined when setting 20129 * 20130 * @deprecated Since version 5. 20131 */ 20132 20133 20134 Html5.prototype.src = function src(_src) { 20135 if (_src === undefined) { 20136 return this.el_.src; 20137 } 20138 20139 // Setting src through `src` instead of `setSrc` will be deprecated 20140 this.setSrc(_src); 20141 }; 20142 20143 /** 20144 * Reset the tech by removing all sources and then calling 20145 * {@link Html5.resetMediaElement}. 20146 */ 20147 20148 20149 Html5.prototype.reset = function reset() { 20150 Html5.resetMediaElement(this.el_); 20151 }; 20152 20153 /** 20154 * Get the current source on the `HTML5` Tech. Falls back to returning the source from 20155 * the HTML5 media element. 20156 * 20157 * @return {Tech~SourceObject} 20158 * The current source object from the HTML5 tech. With a fallback to the 20159 * elements source. 20160 */ 20161 20162 20163 Html5.prototype.currentSrc = function currentSrc() { 20164 if (this.currentSource_) { 20165 return this.currentSource_.src; 20166 } 20167 return this.el_.currentSrc; 20168 }; 20169 20170 /** 20171 * Set controls attribute for the HTML5 media Element. 20172 * 20173 * @param {string} val 20174 * Value to set the controls attribute to 20175 */ 20176 20177 20178 Html5.prototype.setControls = function setControls(val) { 20179 this.el_.controls = !!val; 20180 }; 20181 20182 /** 20183 * Create and returns a remote {@link TextTrack} object. 20184 * 20185 * @param {string} kind 20186 * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata) 20187 * 20188 * @param {string} [label] 20189 * Label to identify the text track 20190 * 20191 * @param {string} [language] 20192 * Two letter language abbreviation 20193 * 20194 * @return {TextTrack} 20195 * The TextTrack that gets created. 20196 */ 20197 20198 20199 Html5.prototype.addTextTrack = function addTextTrack(kind, label, language) { 20200 if (!this.featuresNativeTextTracks) { 20201 return _Tech.prototype.addTextTrack.call(this, kind, label, language); 20202 } 20203 20204 return this.el_.addTextTrack(kind, label, language); 20205 }; 20206 20207 /** 20208 * Creates either native TextTrack or an emulated TextTrack depending 20209 * on the value of `featuresNativeTextTracks` 20210 * 20211 * @param {Object} options 20212 * The object should contain the options to initialize the TextTrack with. 20213 * 20214 * @param {string} [options.kind] 20215 * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata). 20216 * 20217 * @param {string} [options.label] 20218 * Label to identify the text track 20219 * 20220 * @param {string} [options.language] 20221 * Two letter language abbreviation. 20222 * 20223 * @param {boolean} [options.default] 20224 * Default this track to on. 20225 * 20226 * @param {string} [options.id] 20227 * The internal id to assign this track. 20228 * 20229 * @param {string} [options.src] 20230 * A source url for the track. 20231 * 20232 * @return {HTMLTrackElement} 20233 * The track element that gets created. 20234 */ 20235 20236 20237 Html5.prototype.createRemoteTextTrack = function createRemoteTextTrack(options) { 20238 if (!this.featuresNativeTextTracks) { 20239 return _Tech.prototype.createRemoteTextTrack.call(this, options); 20240 } 20241 var htmlTrackElement = document_1.createElement('track'); 20242 20243 if (options.kind) { 20244 htmlTrackElement.kind = options.kind; 20245 } 20246 if (options.label) { 20247 htmlTrackElement.label = options.label; 20248 } 20249 if (options.language || options.srclang) { 20250 htmlTrackElement.srclang = options.language || options.srclang; 20251 } 20252 if (options.default) { 20253 htmlTrackElement.default = options.default; 20254 } 20255 if (options.id) { 20256 htmlTrackElement.id = options.id; 20257 } 20258 if (options.src) { 20259 htmlTrackElement.src = options.src; 20260 } 20261 20262 return htmlTrackElement; 20263 }; 20264 20265 /** 20266 * Creates a remote text track object and returns an html track element. 20267 * 20268 * @param {Object} options The object should contain values for 20269 * kind, language, label, and src (location of the WebVTT file) 20270 * @param {Boolean} [manualCleanup=true] if set to false, the TextTrack will be 20271 * automatically removed from the video element whenever the source changes 20272 * @return {HTMLTrackElement} An Html Track Element. 20273 * This can be an emulated {@link HTMLTrackElement} or a native one. 20274 * @deprecated The default value of the "manualCleanup" parameter will default 20275 * to "false" in upcoming versions of Video.js 20276 */ 20277 20278 20279 Html5.prototype.addRemoteTextTrack = function addRemoteTextTrack(options, manualCleanup) { 20280 var htmlTrackElement = _Tech.prototype.addRemoteTextTrack.call(this, options, manualCleanup); 20281 20282 if (this.featuresNativeTextTracks) { 20283 this.el().appendChild(htmlTrackElement); 20284 } 20285 20286 return htmlTrackElement; 20287 }; 20288 20289 /** 20290 * Remove remote `TextTrack` from `TextTrackList` object 20291 * 20292 * @param {TextTrack} track 20293 * `TextTrack` object to remove 20294 */ 20295 20296 20297 Html5.prototype.removeRemoteTextTrack = function removeRemoteTextTrack(track) { 20298 _Tech.prototype.removeRemoteTextTrack.call(this, track); 20299 20300 if (this.featuresNativeTextTracks) { 20301 var tracks = this.$$('track'); 20302 20303 var i = tracks.length; 20304 20305 while (i--) { 20306 if (track === tracks[i] || track === tracks[i].track) { 20307 this.el().removeChild(tracks[i]); 20308 } 20309 } 20310 } 20311 }; 20312 20313 /** 20314 * Gets available media playback quality metrics as specified by the W3C's Media 20315 * Playback Quality API. 20316 * 20317 * @see [Spec]{@link https://wicg.github.io/media-playback-quality} 20318 * 20319 * @return {Object} 20320 * An object with supported media playback quality metrics 20321 */ 20322 20323 20324 Html5.prototype.getVideoPlaybackQuality = function getVideoPlaybackQuality() { 20325 if (typeof this.el().getVideoPlaybackQuality === 'function') { 20326 return this.el().getVideoPlaybackQuality(); 20327 } 20328 20329 var videoPlaybackQuality = {}; 20330 20331 if (typeof this.el().webkitDroppedFrameCount !== 'undefined' && typeof this.el().webkitDecodedFrameCount !== 'undefined') { 20332 videoPlaybackQuality.droppedVideoFrames = this.el().webkitDroppedFrameCount; 20333 videoPlaybackQuality.totalVideoFrames = this.el().webkitDecodedFrameCount; 20334 } 20335 20336 if (window_1.performance && typeof window_1.performance.now === 'function') { 20337 videoPlaybackQuality.creationTime = window_1.performance.now(); 20338 } else if (window_1.performance && window_1.performance.timing && typeof window_1.performance.timing.navigationStart === 'number') { 20339 videoPlaybackQuality.creationTime = window_1.Date.now() - window_1.performance.timing.navigationStart; 20340 } 20341 20342 return videoPlaybackQuality; 20343 }; 20344 20345 return Html5; 20346 }(Tech); 20347 20348 /* HTML5 Support Testing ---------------------------------------------------- */ 20349 20350 if (isReal()) { 20351 20352 /** 20353 * Element for testing browser HTML5 media capabilities 20354 * 20355 * @type {Element} 20356 * @constant 20357 * @private 20358 */ 20359 Html5.TEST_VID = document_1.createElement('video'); 20360 var track = document_1.createElement('track'); 20361 20362 track.kind = 'captions'; 20363 track.srclang = 'en'; 20364 track.label = 'English'; 20365 Html5.TEST_VID.appendChild(track); 20366 } 20367 20368 /** 20369 * Check if HTML5 media is supported by this browser/device. 20370 * 20371 * @return {boolean} 20372 * - True if HTML5 media is supported. 20373 * - False if HTML5 media is not supported. 20374 */ 20375 Html5.isSupported = function () { 20376 // IE with no Media Player is a LIAR! (#984) 20377 try { 20378 Html5.TEST_VID.volume = 0.5; 20379 } catch (e) { 20380 return false; 20381 } 20382 20383 return !!(Html5.TEST_VID && Html5.TEST_VID.canPlayType); 20384 }; 20385 20386 /** 20387 * Check if the tech can support the given type 20388 * 20389 * @param {string} type 20390 * The mimetype to check 20391 * @return {string} 'probably', 'maybe', or '' (empty string) 20392 */ 20393 Html5.canPlayType = function (type) { 20394 return Html5.TEST_VID.canPlayType(type); 20395 }; 20396 20397 /** 20398 * Check if the tech can support the given source 20399 * @param {Object} srcObj 20400 * The source object 20401 * @param {Object} options 20402 * The options passed to the tech 20403 * @return {string} 'probably', 'maybe', or '' (empty string) 20404 */ 20405 Html5.canPlaySource = function (srcObj, options) { 20406 return Html5.canPlayType(srcObj.type); 20407 }; 20408 20409 /** 20410 * Check if the volume can be changed in this browser/device. 20411 * Volume cannot be changed in a lot of mobile devices. 20412 * Specifically, it can't be changed from 1 on iOS. 20413 * 20414 * @return {boolean} 20415 * - True if volume can be controlled 20416 * - False otherwise 20417 */ 20418 Html5.canControlVolume = function () { 20419 // IE will error if Windows Media Player not installed #3315 20420 try { 20421 var volume = Html5.TEST_VID.volume; 20422 20423 Html5.TEST_VID.volume = volume / 2 + 0.1; 20424 return volume !== Html5.TEST_VID.volume; 20425 } catch (e) { 20426 return false; 20427 } 20428 }; 20429 20430 /** 20431 * Check if the volume can be muted in this browser/device. 20432 * Some devices, e.g. iOS, don't allow changing volume 20433 * but permits muting/unmuting. 20434 * 20435 * @return {bolean} 20436 * - True if volume can be muted 20437 * - False otherwise 20438 */ 20439 Html5.canMuteVolume = function () { 20440 try { 20441 var muted = Html5.TEST_VID.muted; 20442 20443 // in some versions of iOS muted property doesn't always 20444 // work, so we want to set both property and attribute 20445 Html5.TEST_VID.muted = !muted; 20446 if (Html5.TEST_VID.muted) { 20447 setAttribute(Html5.TEST_VID, 'muted', 'muted'); 20448 } else { 20449 removeAttribute(Html5.TEST_VID, 'muted', 'muted'); 20450 } 20451 return muted !== Html5.TEST_VID.muted; 20452 } catch (e) { 20453 return false; 20454 } 20455 }; 20456 20457 /** 20458 * Check if the playback rate can be changed in this browser/device. 20459 * 20460 * @return {boolean} 20461 * - True if playback rate can be controlled 20462 * - False otherwise 20463 */ 20464 Html5.canControlPlaybackRate = function () { 20465 // Playback rate API is implemented in Android Chrome, but doesn't do anything 20466 // https://github.com/videojs/video.js/issues/3180 20467 if (IS_ANDROID && IS_CHROME && CHROME_VERSION < 58) { 20468 return false; 20469 } 20470 // IE will error if Windows Media Player not installed #3315 20471 try { 20472 var playbackRate = Html5.TEST_VID.playbackRate; 20473 20474 Html5.TEST_VID.playbackRate = playbackRate / 2 + 0.1; 20475 return playbackRate !== Html5.TEST_VID.playbackRate; 20476 } catch (e) { 20477 return false; 20478 } 20479 }; 20480 20481 /** 20482 * Check if we can override a video/audio elements attributes, with 20483 * Object.defineProperty. 20484 * 20485 * @return {boolean} 20486 * - True if builtin attributes can be overridden 20487 * - False otherwise 20488 */ 20489 Html5.canOverrideAttributes = function () { 20490 // if we cannot overwrite the src/innerHTML property, there is no support 20491 // iOS 7 safari for instance cannot do this. 20492 try { 20493 var noop = function noop() {}; 20494 20495 Object.defineProperty(document_1.createElement('video'), 'src', { get: noop, set: noop }); 20496 Object.defineProperty(document_1.createElement('audio'), 'src', { get: noop, set: noop }); 20497 Object.defineProperty(document_1.createElement('video'), 'innerHTML', { get: noop, set: noop }); 20498 Object.defineProperty(document_1.createElement('audio'), 'innerHTML', { get: noop, set: noop }); 20499 } catch (e) { 20500 return false;
20501 } 20502 20503 return true; 20504 }; 20505 20506 /** 20507 * Check to see if native `TextTrack`s are supported by this browser/device. 20508 * 20509 * @return {boolean} 20510 * - True if native `TextTrack`s are supported. 20511 * - False otherwise 20512 */ 20513 Html5.supportsNativeTextTracks = function () { 20514 return IS_ANY_SAFARI || IS_IOS && IS_CHROME; 20515 }; 20516 20517 /** 20518 * Check to see if native `VideoTrack`s are supported by this browser/device 20519 * 20520 * @return {boolean} 20521 * - True if native `VideoTrack`s are supported. 20522 * - False otherwise 20523 */ 20524 Html5.supportsNativeVideoTracks = function () { 20525 return !!(Html5.TEST_VID && Html5.TEST_VID.videoTracks); 20526 }; 20527 20528 /** 20529 * Check to see if native `AudioTrack`s are supported by this browser/device 20530 * 20531 * @return {boolean} 20532 * - True if native `AudioTrack`s are supported. 20533 * - False otherwise 20534 */ 20535 Html5.supportsNativeAudioTracks = function () { 20536 return !!(Html5.TEST_VID && Html5.TEST_VID.audioTracks); 20537 }; 20538 20539 /** 20540 * An array of events available on the Html5 tech. 20541 * 20542 * @private 20543 * @type {Array} 20544 */ 20545 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']; 20546 20547 /** 20548 * Boolean indicating whether the `Tech` supports volume control. 20549 * 20550 * @type {boolean} 20551 * @default {@link Html5.canControlVolume} 20552 */ 20553 Html5.prototype.featuresVolumeControl = Html5.canControlVolume(); 20554 20555 /** 20556 * Boolean indicating whether the `Tech` supports muting volume. 20557 * 20558 * @type {bolean} 20559 * @default {@link Html5.canMuteVolume} 20560 */ 20561 Html5.prototype.featuresMuteControl = Html5.canMuteVolume(); 20562 20563 /** 20564 * Boolean indicating whether the `Tech` supports changing the speed at which the media 20565 * plays. Examples: 20566 * - Set player to play 2x (twice) as fast 20567 * - Set player to play 0.5x (half) as fast 20568 * 20569 * @type {boolean} 20570 * @default {@link Html5.canControlPlaybackRate} 20571 */ 20572 Html5.prototype.featuresPlaybackRate = Html5.canControlPlaybackRate(); 20573 20574 /** 20575 * Boolean indicating whether the `Tech` supports the `sourceset` event. 20576 * 20577 * @type {boolean} 20578 * @default 20579 */ 20580 Html5.prototype.featuresSourceset = Html5.canOverrideAttributes(); 20581 20582 /** 20583 * Boolean indicating whether the `HTML5` tech currently supports the media element 20584 * moving in the DOM. iOS breaks if you move the media element, so this is set this to 20585 * false there. Everywhere else this should be true. 20586 * 20587 * @type {boolean} 20588 * @default 20589 */ 20590 Html5.prototype.movingMediaElementInDOM = !IS_IOS; 20591 20592 // TODO: Previous comment: No longer appears to be used. Can probably be removed. 20593 // Is this true? 20594 /** 20595 * Boolean indicating whether the `HTML5` tech currently supports automatic media resize 20596 * when going into fullscreen. 20597 * 20598 * @type {boolean} 20599 * @default 20600 */ 20601 Html5.prototype.featuresFullscreenResize = true; 20602 20603 /** 20604 * Boolean indicating whether the `HTML5` tech currently supports the progress event. 20605 * If this is false, manual `progress` events will be triggered instead. 20606 * 20607 * @type {boolean} 20608 * @default 20609 */ 20610 Html5.prototype.featuresProgressEvents = true; 20611 20612 /** 20613 * Boolean indicating whether the `HTML5` tech currently supports the timeupdate event. 20614 * If this is false, manual `timeupdate` events will be triggered instead. 20615 * 20616 * @default 20617 */ 20618 Html5.prototype.featuresTimeupdateEvents = true; 20619 20620 /** 20621 * Boolean indicating whether the `HTML5` tech currently supports native `TextTrack`s. 20622 * 20623 * @type {boolean} 20624 * @default {@link Html5.supportsNativeTextTracks} 20625 */ 20626 Html5.prototype.featuresNativeTextTracks = Html5.supportsNativeTextTracks(); 20627 20628 /** 20629 * Boolean indicating whether the `HTML5` tech currently supports native `VideoTrack`s. 20630 * 20631 * @type {boolean} 20632 * @default {@link Html5.supportsNativeVideoTracks} 20633 */ 20634 Html5.prototype.featuresNativeVideoTracks = Html5.supportsNativeVideoTracks(); 20635 20636 /** 20637 * Boolean indicating whether the `HTML5` tech currently supports native `AudioTrack`s. 20638 * 20639 * @type {boolean} 20640 * @default {@link Html5.supportsNativeAudioTracks} 20641 */ 20642 Html5.prototype.featuresNativeAudioTracks = Html5.supportsNativeAudioTracks(); 20643 20644 // HTML5 Feature detection and Device Fixes --------------------------------- //
vendor: 14,640 bytes, lines 20645-21059
20645 var canPlayType = Html5.TEST_VID && Html5.TEST_VID.constructor.prototype.canPlayType; 20646 var mpegurlRE = /^application\/(?:x-|vnd\.apple\.)mpegurl/i; 20647 20648 Html5.patchCanPlayType = function () { 20649 20650 // Android 4.0 and above can play HLS to some extent but it reports being unable to do so 20651 // Firefox and Chrome report correctly 20652 if (ANDROID_VERSION >= 4.0 && !IS_FIREFOX && !IS_CHROME) { 20653 Html5.TEST_VID.constructor.prototype.canPlayType = function (type) { 20654 if (type && mpegurlRE.test(type)) { 20655 return 'maybe'; 20656 } 20657 return canPlayType.call(this, type); 20658 }; 20659 } 20660 }; 20661 20662 Html5.unpatchCanPlayType = function () { 20663 var r = Html5.TEST_VID.constructor.prototype.canPlayType; 20664 20665 Html5.TEST_VID.constructor.prototype.canPlayType = canPlayType; 20666 return r; 20667 }; 20668 20669 // by default, patch the media element 20670 Html5.patchCanPlayType(); 20671 20672 Html5.disposeMediaElement = function (el) { 20673 if (!el) { 20674 return; 20675 } 20676 20677 if (el.parentNode) { 20678 el.parentNode.removeChild(el); 20679 } 20680 20681 // remove any child track or source nodes to prevent their loading 20682 while (el.hasChildNodes()) { 20683 el.removeChild(el.firstChild); 20684 } 20685 20686 // remove any src reference. not setting `src=''` because that causes a warning 20687 // in firefox 20688 el.removeAttribute('src'); 20689 20690 // force the media element to update its loading state by calling load() 20691 // however IE on Windows 7N has a bug that throws an error so need a try/catch (#793) 20692 if (typeof el.load === 'function') { 20693 // wrapping in an iife so it's not deoptimized (#1060#discussion_r10324473) 20694 (function () { 20695 try { 20696 el.load(); 20697 } catch (e) { 20698 // not supported 20699 } 20700 })(); 20701 } 20702 }; 20703 20704 Html5.resetMediaElement = function (el) { 20705 if (!el) { 20706 return; 20707 } 20708 20709 var sources = el.querySelectorAll('source'); 20710 var i = sources.length; 20711 20712 while (i--) { 20713 el.removeChild(sources[i]); 20714 } 20715 20716 // remove any src reference. 20717 // not setting `src=''` because that throws an error 20718 el.removeAttribute('src'); 20719 20720 if (typeof el.load === 'function') { 20721 // wrapping in an iife so it's not deoptimized (#1060#discussion_r10324473) 20722 (function () { 20723 try { 20724 el.load(); 20725 } catch (e) { 20726 // satisfy linter 20727 } 20728 })(); 20729 } 20730 }; 20731 20732 /* Native HTML5 element property wrapping ----------------------------------- */ 20733 // Wrap native boolean attributes with getters that check both property and attribute 20734 // The list is as followed: 20735 // muted, defaultMuted, autoplay, controls, loop, playsinline 20736 [ 20737 /** 20738 * Get the value of `muted` from the media element. `muted` indicates 20739 * that the volume for the media should be set to silent. This does not actually change 20740 * the `volume` attribute. 20741 * 20742 * @method Html5#muted 20743 * @return {boolean} 20744 * - True if the value of `volume` should be ignored and the audio set to silent. 20745 * - False if the value of `volume` should be used. 20746 * 20747 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-muted} 20748 */ 20749 'muted', 20750 20751 /** 20752 * Get the value of `defaultMuted` from the media element. `defaultMuted` indicates 20753 * whether the media should start muted or not. Only changes the default state of the 20754 * media. `muted` and `defaultMuted` can have different values. {@link Html5#muted} indicates the 20755 * current state. 20756 * 20757 * @method Html5#defaultMuted 20758 * @return {boolean} 20759 * - The value of `defaultMuted` from the media element. 20760 * - True indicates that the media should start muted. 20761 * - False indicates that the media should not start muted 20762 * 20763 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultmuted} 20764 */ 20765 'defaultMuted', 20766 20767 /** 20768 * Get the value of `autoplay` from the media element. `autoplay` indicates 20769 * that the media should start to play as soon as the page is ready. 20770 * 20771 * @method Html5#autoplay 20772 * @return {boolean} 20773 * - The value of `autoplay` from the media element. 20774 * - True indicates that the media should start as soon as the page loads. 20775 * - False indicates that the media should not start as soon as the page loads. 20776 * 20777 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-autoplay} 20778 */ 20779 'autoplay', 20780 20781 /** 20782 * Get the value of `controls` from the media element. `controls` indicates 20783 * whether the native media controls should be shown or hidden. 20784 * 20785 * @method Html5#controls 20786 * @return {boolean} 20787 * - The value of `controls` from the media element. 20788 * - True indicates that native controls should be showing. 20789 * - False indicates that native controls should be hidden. 20790 * 20791 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-controls} 20792 */ 20793 'controls', 20794 20795 /** 20796 * Get the value of `loop` from the media element. `loop` indicates 20797 * that the media should return to the start of the media and continue playing once 20798 * it reaches the end. 20799 * 20800 * @method Html5#loop 20801 * @return {boolean} 20802 * - The value of `loop` from the media element. 20803 * - True indicates that playback should seek back to start once 20804 * the end of a media is reached. 20805 * - False indicates that playback should not loop back to the start when the 20806 * end of the media is reached. 20807 * 20808 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-loop} 20809 */ 20810 'loop', 20811 20812 /** 20813 * Get the value of `playsinline` from the media element. `playsinline` indicates 20814 * to the browser that non-fullscreen playback is preferred when fullscreen 20815 * playback is the native default, such as in iOS Safari. 20816 * 20817 * @method Html5#playsinline 20818 * @return {boolean} 20819 * - The value of `playsinline` from the media element. 20820 * - True indicates that the media should play inline. 20821 * - False indicates that the media should not play inline. 20822 * 20823 * @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline} 20824 */ 20825 'playsinline'].forEach(function (prop) { 20826 Html5.prototype[prop] = function () { 20827 return this.el_[prop] || this.el_.hasAttribute(prop); 20828 }; 20829 }); 20830 20831 // Wrap native boolean attributes with setters that set both property and attribute 20832 // The list is as followed: 20833 // setMuted, setDefaultMuted, setAutoplay, setLoop, setPlaysinline 20834 // setControls is special-cased above 20835 [ 20836 /** 20837 * Set the value of `muted` on the media element. `muted` indicates that the current 20838 * audio level should be silent. 20839 * 20840 * @method Html5#setMuted 20841 * @param {boolean} muted 20842 * - True if the audio should be set to silent 20843 * - False otherwise 20844 * 20845 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-muted} 20846 */ 20847 'muted', 20848 20849 /** 20850 * Set the value of `defaultMuted` on the media element. `defaultMuted` indicates that the current 20851 * audio level should be silent, but will only effect the muted level on intial playback.. 20852 * 20853 * @method Html5.prototype.setDefaultMuted 20854 * @param {boolean} defaultMuted 20855 * - True if the audio should be set to silent 20856 * - False otherwise 20857 * 20858 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultmuted} 20859 */ 20860 'defaultMuted', 20861 20862 /** 20863 * Set the value of `autoplay` on the media element. `autoplay` indicates 20864 * that the media should start to play as soon as the page is ready. 20865 * 20866 * @method Html5#setAutoplay 20867 * @param {boolean} autoplay 20868 * - True indicates that the media should start as soon as the page loads. 20869 * - False indicates that the media should not start as soon as the page loads. 20870 * 20871 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-autoplay} 20872 */ 20873 'autoplay', 20874 20875 /** 20876 * Set the value of `loop` on the media element. `loop` indicates 20877 * that the media should return to the start of the media and continue playing once 20878 * it reaches the end. 20879 * 20880 * @method Html5#setLoop 20881 * @param {boolean} loop 20882 * - True indicates that playback should seek back to start once 20883 * the end of a media is reached. 20884 * - False indicates that playback should not loop back to the start when the 20885 * end of the media is reached. 20886 * 20887 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-loop} 20888 */ 20889 'loop', 20890 20891 /** 20892 * Set the value of `playsinline` from the media element. `playsinline` indicates 20893 * to the browser that non-fullscreen playback is preferred when fullscreen 20894 * playback is the native default, such as in iOS Safari. 20895 * 20896 * @method Html5#setPlaysinline 20897 * @param {boolean} playsinline 20898 * - True indicates that the media should play inline. 20899 * - False indicates that the media should not play inline. 20900 * 20901 * @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline} 20902 */ 20903 'playsinline'].forEach(function (prop) { 20904 Html5.prototype['set' + toTitleCase(prop)] = function (v) { 20905 this.el_[prop] = v; 20906 20907 if (v) { 20908 this.el_.setAttribute(prop, prop); 20909 } else { 20910 this.el_.removeAttribute(prop); 20911 } 20912 }; 20913 }); 20914 20915 // Wrap native properties with a getter 20916 // The list is as followed 20917 // paused, currentTime, buffered, volume, poster, preload, error, seeking 20918 // seekable, ended, playbackRate, defaultPlaybackRate, played, networkState 20919 // readyState, videoWidth, videoHeight 20920 [ 20921 /** 20922 * Get the value of `paused` from the media element. `paused` indicates whether the media element 20923 * is currently paused or not. 20924 * 20925 * @method Html5#paused 20926 * @return {boolean} 20927 * The value of `paused` from the media element. 20928 * 20929 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-paused} 20930 */ 20931 'paused', 20932 20933 /** 20934 * Get the value of `currentTime` from the media element. `currentTime` indicates 20935 * the current second that the media is at in playback. 20936 * 20937 * @method Html5#currentTime 20938 * @return {number} 20939 * The value of `currentTime` from the media element. 20940 * 20941 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-currenttime} 20942 */ 20943 'currentTime', 20944 20945 /** 20946 * Get the value of `buffered` from the media element. `buffered` is a `TimeRange` 20947 * object that represents the parts of the media that are already downloaded and 20948 * available for playback. 20949 * 20950 * @method Html5#buffered 20951 * @return {TimeRange} 20952 * The value of `buffered` from the media element. 20953 * 20954 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-buffered} 20955 */ 20956 'buffered', 20957 20958 /** 20959 * Get the value of `volume` from the media element. `volume` indicates 20960 * the current playback volume of audio for a media. `volume` will be a value from 0 20961 * (silent) to 1 (loudest and default). 20962 * 20963 * @method Html5#volume 20964 * @return {number} 20965 * The value of `volume` from the media element. Value will be between 0-1. 20966 * 20967 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-a-volume} 20968 */ 20969 'volume', 20970 20971 /** 20972 * Get the value of `poster` from the media element. `poster` indicates 20973 * that the url of an image file that can/will be shown when no media data is available. 20974 * 20975 * @method Html5#poster 20976 * @return {string} 20977 * The value of `poster` from the media element. Value will be a url to an 20978 * image. 20979 * 20980 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-video-poster} 20981 */ 20982 'poster', 20983 20984 /** 20985 * Get the value of `preload` from the media element. `preload` indicates 20986 * what should download before the media is interacted with. It can have the following 20987 * values: 20988 * - none: nothing should be downloaded 20989 * - metadata: poster and the first few frames of the media may be downloaded to get 20990 * media dimensions and other metadata 20991 * - auto: allow the media and metadata for the media to be downloaded before 20992 * interaction 20993 * 20994 * @method Html5#preload 20995 * @return {string} 20996 * The value of `preload` from the media element. Will be 'none', 'metadata', 20997 * or 'auto'. 20998 * 20999 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-preload} 21000 */ 21001 'preload', 21002 21003 /** 21004 * Get the value of the `error` from the media element. `error` indicates any 21005 * MediaError that may have occurred during playback. If error returns null there is no 21006 * current error. 21007 * 21008 * @method Html5#error 21009 * @return {MediaError|null} 21010 * The value of `error` from the media element. Will be `MediaError` if there 21011 * is a current error and null otherwise. 21012 * 21013 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-error} 21014 */ 21015 'error', 21016 21017 /** 21018 * Get the value of `seeking` from the media element. `seeking` indicates whether the 21019 * media is currently seeking to a new position or not. 21020 * 21021 * @method Html5#seeking 21022 * @return {boolean} 21023 * - The value of `seeking` from the media element. 21024 * - True indicates that the media is currently seeking to a new position. 21025 * - False indicates that the media is not seeking to a new position at this time. 21026 * 21027 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-seeking} 21028 */ 21029 'seeking', 21030 21031 /** 21032 * Get the value of `seekable` from the media element. `seekable` returns a 21033 * `TimeRange` object indicating ranges of time that can currently be `seeked` to. 21034 * 21035 * @method Html5#seekable 21036 * @return {TimeRange} 21037 * The value of `seekable` from the media element. A `TimeRange` object 21038 * indicating the current ranges of time that can be seeked to. 21039 * 21040 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-seekable} 21041 */ 21042 'seekable', 21043 21044 /** 21045 * Get the value of `ended` from the media element. `ended` indicates whether 21046 * the media has reached the end or not. 21047 * 21048 * @method Html5#ended 21049 * @return {boolean} 21050 * - The value of `ended` from the media element. 21051 * - True indicates that the media has ended. 21052 * - False indicates that the media has not ended. 21053 * 21054 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-ended} 21055 */ 21056 'ended', 21057 21058 /** 21059 * Get the value of `playbackRate` from the media element. `playbackRate` indicates
21060 * the rate at which the media is currently playing back. Examples: 21061 * - if playbackRate is set to 2, media will play twice as fast. 21062 * - if playbackRate is set to 0.5, media will play half as fast. 21063 * 21064 * @method Html5#playbackRate 21065 * @return {number} 21066 * The value of `playbackRate` from the media element. A number indicating 21067 * the current playback speed of the media, where 1 is normal speed. 21068 * 21069 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate} 21070 */ 21071 'playbackRate', 21072 21073 /** 21074 * Get the value of `defaultPlaybackRate` from the media element. `defaultPlaybackRate` indicates 21075 * the rate at which the media is currently playing back. This value will not indicate the current 21076 * `playbackRate` after playback has started, use {@link Html5#playbackRate} for that. 21077 * 21078 * Examples: 21079 * - if defaultPlaybackRate is set to 2, media will play twice as fast. 21080 * - if defaultPlaybackRate is set to 0.5, media will play half as fast. 21081 * 21082 * @method Html5.prototype.defaultPlaybackRate 21083 * @return {number} 21084 * The value of `defaultPlaybackRate` from the media element. A number indicating 21085 * the current playback speed of the media, where 1 is normal speed. 21086 * 21087 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate} 21088 */ 21089 'defaultPlaybackRate', 21090 21091 /** 21092 * Get the value of `played` from the media element. `played` returns a `TimeRange` 21093 * object representing points in the media timeline that have been played. 21094 * 21095 * @method Html5#played 21096 * @return {TimeRange} 21097 * The value of `played` from the media element. A `TimeRange` object indicating 21098 * the ranges of time that have been played. 21099 * 21100 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-played} 21101 */ 21102 'played', 21103 21104 /** 21105 * Get the value of `networkState` from the media element. `networkState` indicates 21106 * the current network state. It returns an enumeration from the following list: 21107 * - 0: NETWORK_EMPTY 21108 * - 1: NETWORK_IDLE 21109 * - 2: NETWORK_LOADING 21110 * - 3: NETWORK_NO_SOURCE 21111 * 21112 * @method Html5#networkState 21113 * @return {number} 21114 * The value of `networkState` from the media element. This will be a number 21115 * from the list in the description. 21116 * 21117 * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-networkstate} 21118 */ 21119 'networkState', 21120 21121 /** 21122 * Get the value of `readyState` from the media element. `readyState` indicates 21123 * the current state of the media element. It returns an enumeration from the 21124 * following list: 21125 * - 0: HAVE_NOTHING 21126 * - 1: HAVE_METADATA 21127 * - 2: HAVE_CURRENT_DATA 21128 * - 3: HAVE_FUTURE_DATA 21129 * - 4: HAVE_ENOUGH_DATA 21130 * 21131 * @method Html5#readyState 21132 * @return {number} 21133 * The value of `readyState` from the media element. This will be a number 21134 * from the list in the description. 21135 * 21136 * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#ready-states} 21137 */ 21138 'readyState', 21139 21140 /** 21141 * Get the value of `videoWidth` from the video element. `videoWidth` indicates 21142 * the current width of the video in css pixels. 21143 * 21144 * @method Html5#videoWidth 21145 * @return {number} 21146 * The value of `videoWidth` from the video element. This will be a number 21147 * in css pixels. 21148 * 21149 * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-video-videowidth} 21150 */ 21151 'videoWidth', 21152 21153 /** 21154 * Get the value of `videoHeight` from the video element. `videoHeight` indicates 21155 * the current height of the video in css pixels. 21156 * 21157 * @method Html5#videoHeight 21158 * @return {number} 21159 * The value of `videoHeight` from the video element. This will be a number 21160 * in css pixels. 21161 * 21162 * @see [Spec] {@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-video-videowidth} 21163 */ 21164 'videoHeight'].forEach(function (prop) { 21165 Html5.prototype[prop] = function () { 21166 return this.el_[prop]; 21167 }; 21168 }); 21169 21170 // Wrap native properties with a setter in this format: 21171 // set + toTitleCase(name) 21172 // The list is as follows: 21173 // setVolume, setSrc, setPoster, setPreload, setPlaybackRate, setDefaultPlaybackRate 21174 [ 21175 /** 21176 * Set the value of `volume` on the media element. `volume` indicates the current 21177 * audio level as a percentage in decimal form. This means that 1 is 100%, 0.5 is 50%, and 21178 * so on. 21179 * 21180 * @method Html5#setVolume 21181 * @param {number} percentAsDecimal 21182 * The volume percent as a decimal. Valid range is from 0-1. 21183 * 21184 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-a-volume} 21185 */ 21186 'volume', 21187 21188 /** 21189 * Set the value of `src` on the media element. `src` indicates the current 21190 * {@link Tech~SourceObject} for the media. 21191 * 21192 * @method Html5#setSrc 21193 * @param {Tech~SourceObject} src 21194 * The source object to set as the current source. 21195 * 21196 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-src} 21197 */ 21198 'src', 21199 21200 /** 21201 * Set the value of `poster` on the media element. `poster` is the url to 21202 * an image file that can/will be shown when no media data is available. 21203 * 21204 * @method Html5#setPoster 21205 * @param {string} poster 21206 * The url to an image that should be used as the `poster` for the media 21207 * element. 21208 * 21209 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-poster} 21210 */ 21211 'poster', 21212 21213 /** 21214 * Set the value of `preload` on the media element. `preload` indicates 21215 * what should download before the media is interacted with. It can have the following 21216 * values: 21217 * - none: nothing should be downloaded 21218 * - metadata: poster and the first few frames of the media may be downloaded to get 21219 * media dimensions and other metadata 21220 * - auto: allow the media and metadata for the media to be downloaded before 21221 * interaction 21222 * 21223 * @method Html5#setPreload 21224 * @param {string} preload 21225 * The value of `preload` to set on the media element. Must be 'none', 'metadata', 21226 * or 'auto'. 21227 * 21228 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#attr-media-preload} 21229 */ 21230 'preload', 21231 21232 /** 21233 * Set the value of `playbackRate` on the media element. `playbackRate` indicates 21234 * the rate at which the media should play back. Examples: 21235 * - if playbackRate is set to 2, media will play twice as fast. 21236 * - if playbackRate is set to 0.5, media will play half as fast. 21237 * 21238 * @method Html5#setPlaybackRate 21239 * @return {number} 21240 * The value of `playbackRate` from the media element. A number indicating 21241 * the current playback speed of the media, where 1 is normal speed. 21242 * 21243 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-playbackrate} 21244 */ 21245 'playbackRate', 21246 21247 /** 21248 * Set the value of `defaultPlaybackRate` on the media element. `defaultPlaybackRate` indicates 21249 * the rate at which the media should play back upon initial startup. Changing this value 21250 * after a video has started will do nothing. Instead you should used {@link Html5#setPlaybackRate}. 21251 * 21252 * Example Values: 21253 * - if playbackRate is set to 2, media will play twice as fast. 21254 * - if playbackRate is set to 0.5, media will play half as fast. 21255 * 21256 * @method Html5.prototype.setDefaultPlaybackRate 21257 * @return {number} 21258 * The value of `defaultPlaybackRate` from the media element. A number indicating 21259 * the current playback speed of the media, where 1 is normal speed. 21260 * 21261 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-defaultplaybackrate} 21262 */ 21263 'defaultPlaybackRate'].forEach(function (prop) { 21264 Html5.prototype['set' + toTitleCase(prop)] = function (v) { 21265 this.el_[prop] = v; 21266 }; 21267 }); 21268 21269 // wrap native functions with a function 21270 // The list is as follows: 21271 // pause, load, play 21272 [ 21273 /** 21274 * A wrapper around the media elements `pause` function. This will call the `HTML5` 21275 * media elements `pause` function. 21276 * 21277 * @method Html5#pause 21278 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-pause} 21279 */ 21280 'pause', 21281 21282 /** 21283 * A wrapper around the media elements `load` function. This will call the `HTML5`s 21284 * media element `load` function. 21285 * 21286 * @method Html5#load 21287 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-load} 21288 */ 21289 'load', 21290 21291 /** 21292 * A wrapper around the media elements `play` function. This will call the `HTML5`s 21293 * media element `play` function. 21294 * 21295 * @method Html5#play 21296 * @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-play} 21297 */ 21298 'play'].forEach(function (prop) { 21299 Html5.prototype[prop] = function () { 21300 return this.el_[prop](); 21301 }; 21302 }); 21303 21304 Tech.withSourceHandlers(Html5); 21305 21306 /** 21307 * Native source handler for Html5, simply passes the source to the media element. 21308 * 21309 * @property {Tech~SourceObject} source 21310 * The source object 21311 * 21312 * @property {Html5} tech 21313 * The instance of the HTML5 tech. 21314 */ 21315 Html5.nativeSourceHandler = {}; 21316 21317 /** 21318 * Check if the media element can play the given mime type. 21319 * 21320 * @param {string} type 21321 * The mimetype to check 21322 * 21323 * @return {string} 21324 * 'probably', 'maybe', or '' (empty string) 21325 */ 21326 Html5.nativeSourceHandler.canPlayType = function (type) { 21327 // IE without MediaPlayer throws an error (#519) 21328 try {
21329 return Html5.TEST_VID.canPlayType(type); 21330 } catch (e) { 21331 return ''; 21332 } 21333 }; 21334 21335 /** 21336 * Check if the media element can handle a source natively. 21337 * 21338 * @param {Tech~SourceObject} source 21339 * The source object 21340 * 21341 * @param {Object} [options] 21342 * Options to be passed to the tech. 21343 * 21344 * @return {string} 21345 * 'probably', 'maybe', or '' (empty string). 21346 */ 21347 Html5.nativeSourceHandler.canHandleSource = function (source, options) { 21348 21349 // If a type was provided we should rely on that 21350 if (source.type) { 21351 return Html5.nativeSourceHandler.canPlayType(source.type); 21352 21353 // If no type, fall back to checking 'video/[EXTENSION]' 21354 } else if (source.src) { 21355 var ext = getFileExtension(source.src); 21356 21357 return Html5.nativeSourceHandler.canPlayType('video/' + ext); 21358 } 21359 21360 return ''; 21361 }; 21362 21363 /** 21364 * Pass the source to the native media element. 21365 * 21366 * @param {Tech~SourceObject} source 21367 * The source object 21368 * 21369 * @param {Html5} tech 21370 * The instance of the Html5 tech 21371 * 21372 * @param {Object} [options] 21373 * The options to pass to the source 21374 */ 21375 Html5.nativeSourceHandler.handleSource = function (source, tech, options) { 21376 tech.setSrc(source.src); 21377 }; 21378 21379 /** 21380 * A noop for the native dispose function, as cleanup is not needed. 21381 */ 21382 Html5.nativeSourceHandler.dispose = function () {}; 21383 21384 // Register the native source handler 21385 Html5.registerSourceHandler(Html5.nativeSourceHandler); 21386 21387 Tech.registerTech('Html5', Html5); 21388 21389 var _templateObject$2 = taggedTemplateLiteralLoose(['\n Using the tech directly can be dangerous. I hope you know
21389what you\'re doing.\n See https://github.com/videojs/video.js/issues/2617 for more info.\n '], ['\n 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 ']); 21390 21391 // The following tech events are simply re-triggered 21392 // on the player when they happen 21393 var TECH_EVENTS_RETRIGGER = [ 21394 /** 21395 * Fired while the user agent is downloading media data. 21396 * 21397 * @event Player#progress 21398 * @type {EventTarget~Event} 21399 */ 21400 /** 21401 * Retrigger the `progress` event that was triggered by the {@link Tech}. 21402 * 21403 * @private 21404 * @method Player#handleTechProgress_ 21405 * @fires Player#progress 21406 * @listens Tech#progress 21407 */ 21408 'progress', 21409 21410 /** 21411 * Fires when the loading of an audio/video is aborted. 21412 * 21413 * @event Player#abort 21414 * @type {EventTarget~Event} 21415 */ 21416 /** 21417 * Retrigger the `abort` event that was triggered by the {@link Tech}. 21418 * 21419 * @private 21420 * @method Player#handleTechAbort_ 21421 * @fires Player#abort 21422 * @listens Tech#abort 21423 */ 21424 'abort', 21425 21426 /** 21427 * Fires when the browser is intentionally not getting media data. 21428 * 21429 * @event Player#suspend 21430 * @type {EventTarget~Event} 21431 */ 21432 /** 21433 * Retrigger the `suspend` event that was triggered by the {@link Tech}. 21434 * 21435 * @private 21436 * @method Player#handleTechSuspend_ 21437 * @fires Player#suspend 21438 * @listens Tech#suspend 21439 */ 21440 'suspend', 21441 21442 /** 21443 * Fires when the current playlist is empty. 21444 * 21445 * @event Player#emptied 21446 * @type {EventTarget~Event} 21447 */ 21448 /** 21449 * Retrigger the `emptied` event that was triggered by the {@link Tech}. 21450 * 21451 * @private 21452 * @method Player#handleTechEmptied_ 21453 * @fires Player#emptied 21454 * @listens Tech#emptied 21455 */ 21456 'emptied', 21457 /** 21458 * Fires when the browser is trying to get media data, but data is not available. 21459 * 21460 * @event Player#stalled 21461 * @type {EventTarget~Event} 21462 */ 21463 /** 21464 * Retrigger the `stalled` event that was triggered by the {@link Tech}. 21465 * 21466 * @private 21467 * @method Player#handleTechStalled_ 21468 * @fires Player#stalled 21469 * @listens Tech#stalled 21470 */ 21471 'stalled', 21472 21473 /** 21474 * Fires when the browser has loaded meta data for the audio/video. 21475 * 21476 * @event Player#loadedmetadata 21477 * @type {EventTarget~Event} 21478 */ 21479 /** 21480 * Retrigger the `stalled` event that was triggered by the {@link Tech}. 21481 * 21482 * @private 21483 * @method Player#handleTechLoadedmetadata_ 21484 * @fires Player#loadedmetadata 21485 * @listens Tech#loadedmetadata 21486 */ 21487 'loadedmetadata', 21488 21489 /** 21490 * Fires when the browser has loaded the current frame of the audio/video. 21491 * 21492 * @event Player#loadeddata 21493 * @type {event} 21494 */ 21495 /** 21496 * Retrigger the `loadeddata` event that was triggered by the {@link Tech}. 21497 * 21498 * @private 21499 * @method Player#handleTechLoaddeddata_ 21500 * @fires Player#loadeddata 21501 * @listens Tech#loadeddata 21502 */ 21503 'loadeddata', 21504 21505 /** 21506 * Fires when the current playback position has changed. 21507 * 21508 * @event Player#timeupdate 21509 * @type {event} 21510 */ 21511 /** 21512 * Retrigger the `timeupdate` event that was triggered by the {@link Tech}. 21513 * 21514 * @private 21515 * @method Player#handleTechTimeUpdate_ 21516 * @fires Player#timeupdate 21517 * @listens Tech#timeupdate 21518 */ 21519 'timeupdate', 21520 21521 /** 21522 * Fires when the video's intrinsic dimensions change 21523 * 21524 * @event Player#resize 21525 * @type {event} 21526 */ 21527 /** 21528 * Retrigger the `resize` event that was triggered by the {@link Tech}. 21529 * 21530 * @private 21531 * @method Player#handleTechResize_ 21532 * @fires Player#resize 21533 * @listens Tech#resize 21534 */ 21535 'resize', 21536 21537 /** 21538 * Fires when the volume has been changed 21539 * 21540 * @event Player#volumechange 21541 * @type {event} 21542 */ 21543 /** 21544 * Retrigger the `volumechange` event that was triggered by the {@link Tech}. 21545 * 21546 * @private 21547 * @method Player#handleTechVolumechange_ 21548 * @fires Player#volumechange 21549 * @listens Tech#volumechange 21550 */ 21551 'volumechange', 21552 21553 /** 21554 * Fires when the text track has been changed 21555 * 21556 * @event Player#texttrackchange 21557 * @type {event} 21558 */ 21559 /** 21560 * Retrigger the `texttrackchange` event that was triggered by the {@link Tech}. 21561 * 21562 * @private 21563 * @method Player#handleTechTexttrackchange_ 21564 * @fires Player#texttrackchange 21565 * @listens Tech#texttrackchange 21566 */ 21567 'texttrackchange']; 21568 21569 // events to queue when playback rate is zero 21570 // this is a hash for the sole purpose of mapping non-camel-cased event names 21571 // to camel-cased function names 21572 var TECH_EVENTS_QUEUE = { 21573 canplay: 'CanPlay', 21574 canplaythrough: 'CanPlayThrough', 21575 playing: 'Playing', 21576 seeked: 'Seeked' 21577 }; 21578 21579 /** 21580 * An instance of the `Player` class is created when any of the Video.js setup methods 21581 * are used to initialize a video. 21582 * 21583 * After an instance has been created it can be accessed globally in two ways: 21584 * 1. By calling `videojs('example_video_1');` 21585 * 2. By using it directly via `videojs.players.example_video_1;` 21586 * 21587 * @extends Component 21588 */ 21589 21590 var Player = function (_Component) { 21591 inherits(Player, _Component); 21592 21593 /** 21594 * Create an instance of this class. 21595 * 21596 * @param {Element} tag
21597 * The original video DOM element used for configuring options. 21598 * 21599 * @param {Object} [options] 21600 * Object of option names and values. 21601 * 21602 * @param {Component~ReadyCallback} [ready] 21603 * Ready callback function. 21604 */ 21605 function Player(tag, options, ready) { 21606 classCallCheck(this, Player); 21607 21608 // Make sure tag ID exists 21609 tag.id = tag.id || options.id || 'vjs_video_' + newGUID(); 21610 21611 // Set Options 21612 // The options argument overrides options set in the video tag 21613 // which overrides globally set options. 21614 // This latter part coincides with the load order 21615 // (tag must exist before Player) 21616 options = assign(Player.getTagSettings(tag), options); 21617 21618 // Delay the initialization of children because we need to set up 21619 // player properties first, and can't use `this` before `super()` 21620 options.initChildren = false; 21621 21622 // Same with creating the element 21623 options.createEl = false; 21624 21625 // don't auto mixin the evented mixin 21626 options.evented = false; 21627 21628 // we don't want the player to report touch activity on itself 21629 // see enableTouchActivity in Component 21630 options.reportTouchActivity = false; 21631 21632 // If language is not set, get the closest lang attribute 21633 if (!options.language) { 21634 if (typeof tag.closest === 'function') { 21635 var closest = tag.closest('[lang]'); 21636 21637 if (closest && closest.getAttribute) { 21638 options.language = closest.getAttribute('lang'); 21639 } 21640 } else { 21641 var element = tag; 21642 21643 while (element && element.nodeType === 1) { 21644 if (getAttributes(element).hasOwnProperty('lang')) { 21645 options.language = element.getAttribute('lang'); 21646 break; 21647 } 21648 element = element.parentNode; 21649 } 21650 } 21651 } 21652 21653 // Run base component initializing with new options 21654 21655 // Tracks when a tech changes the poster 21656 var _this = possibleConstructorReturn(this, _Component.call(this, null, options, ready)); 21657 21658 _this.isPosterFromTech_ = false;
vendor: 5,558 bytes, lines 21659-21824
21659 21660 // Holds callback info that gets queued when playback rate is zero 21661 // and a seek is happening 21662 _this.queuedCallbacks_ = []; 21663 21664 // Turn off API access because we're loading a new tech that might load asynchronously 21665 _this.isReady_ = false; 21666 21667 // Init state hasStarted_ 21668 _this.hasStarted_ = false; 21669 21670 // Init state userActive_ 21671 _this.userActive_ = false; 21672 21673 // if the global option object was accidentally blown away by 21674 // someone, bail early with an informative error 21675 if (!_this.options_ || !_this.options_.techOrder || !_this.options_.techOrder.length) { 21676 throw new Error('No techOrder specified. Did you overwrite ' + 'videojs.options instead of just changing the ' + 'properties you want to override?'); 21677 } 21678 21679 // Store the original tag used to set options 21680 _this.tag = tag; 21681 21682 // Store the tag attributes used to restore html5 element 21683 _this.tagAttributes = tag && getAttributes(tag); 21684 21685 // Update current language 21686 _this.language(_this.options_.language); 21687 21688 // Update Supported Languages 21689 if (options.languages) { 21690 // Normalise player option languages to lowercase 21691 var languagesToLower = {}; 21692 21693 Object.getOwnPropertyNames(options.languages).forEach(function (name$$1) { 21694 languagesToLower[name$$1.toLowerCase()] = options.languages[name$$1]; 21695 }); 21696 _this.languages_ = languagesToLower; 21697 } else { 21698 _this.languages_ = Player.prototype.options_.languages; 21699 } 21700 21701 // Cache for video property values. 21702 _this.cache_ = {}; 21703 21704 // Set poster 21705 _this.poster_ = options.poster || ''; 21706 21707 // Set controls 21708 _this.controls_ = !!options.controls; 21709 21710 // Set default values for lastVolume 21711 _this.cache_.lastVolume = 1; 21712 21713 // Original tag settings stored in options 21714 // now remove immediately so native controls don't flash. 21715 // May be turned back on by HTML5 tech if nativeControlsForTouch is true 21716 tag.controls = false; 21717 tag.removeAttribute('controls'); 21718 21719 // the attribute overrides the option 21720 if (tag.hasAttribute('autoplay')) { 21721 _this.options_.autoplay = true; 21722 } else { 21723 // otherwise use the setter to validate and 21724 // set the correct value. 21725 _this.autoplay(_this.options_.autoplay); 21726 } 21727 21728 /* 21729 * Store the internal state of scrubbing 21730 * 21731 * @private 21732 * @return {Boolean} True if the user is scrubbing 21733 */ 21734 _this.scrubbing_ = false; 21735 21736 _this.el_ = _this.createEl(); 21737 21738 // Set default value for lastPlaybackRate 21739 _this.cache_.lastPlaybackRate = _this.defaultPlaybackRate(); 21740 21741 // Make this an evented object and use `el_` as its event bus. 21742 evented(_this, { eventBusKey: 'el_' }); 21743 21744 // We also want to pass the original player options to each component and plugin 21745 // as well so they don't need to reach back into the player for options later. 21746 // We also need to do another copy of this.options_ so we don't end up with 21747 // an infinite loop. 21748 var playerOptionsCopy = mergeOptions(_this.options_); 21749 21750 // Load plugins 21751 if (options.plugins) { 21752 var plugins = options.plugins; 21753 21754 Object.keys(plugins).forEach(function (name$$1) { 21755 if (typeof this[name$$1] === 'function') { 21756 this[name$$1](plugins[name$$1]); 21757 } else { 21758 throw new Error('plugin "' + name$$1 + '" does not exist'); 21759 } 21760 }, _this); 21761 } 21762 21763 _this.options_.playerOptions = playerOptionsCopy; 21764 21765 _this.middleware_ = []; 21766 21767 _this.initChildren(); 21768 21769 // Set isAudio based on whether or not an audio tag was used 21770 _this.isAudio(tag.nodeName.toLowerCase() === 'audio'); 21771 21772 // Update controls className. Can't do this when the controls are initially 21773 // set because the element doesn't exist yet. 21774 if (_this.controls()) { 21775 _this.addClass('vjs-controls-enabled'); 21776 } else { 21777 _this.addClass('vjs-controls-disabled'); 21778 } 21779 21780 // Set ARIA label and region role depending on player type 21781 _this.el_.setAttribute('role', 'region'); 21782 if (_this.isAudio()) { 21783 _this.el_.setAttribute('aria-label', _this.localize('Audio Player')); 21784 } else { 21785 _this.el_.setAttribute('aria-label', _this.localize('Video Player')); 21786 } 21787 21788 if (_this.isAudio()) { 21789 _this.addClass('vjs-audio'); 21790 } 21791 21792 if (_this.flexNotSupported_()) { 21793 _this.addClass('vjs-no-flex'); 21794 } 21795 21796 // TODO: Make this smarter. Toggle user state between touching/mousing 21797 // using events, since devices can have both touch and mouse events. 21798 // if (browser.TOUCH_ENABLED) { 21799 // this.addClass('vjs-touch-enabled'); 21800 // } 21801 21802 // iOS Safari has broken hover handling 21803 if (!IS_IOS) { 21804 _this.addClass('vjs-workinghover'); 21805 } 21806 21807 // Make player easily findable by ID 21808 Player.players[_this.id_] = _this; 21809 21810 // Add a major version class to aid css in plugins 21811 var majorVersion = version.split('.')[0]; 21812 21813 _this.addClass('vjs-v' + majorVersion); 21814 21815 // When the player is first initialized, trigger activity so components 21816 // like the control bar show themselves if needed 21817 _this.userActive(true); 21818 _this.reportUserActivity(); 21819 21820 _this.one('play', _this.listenForUserActivity_); 21821 _this.on('fullscreenchange', _this.handleFullscreenChange_); 21822 _this.on('stageclick', _this.handleStageClick_); 21823 21824 _this.changingSrc_ = false;
21825 _this.playWaitingForReady_ = false; 21826 _this.playOnLoadstart_ = null; 21827 return _this; 21828 } 21829 21830 /** 21831 * Destroys the video player and does any necessary cleanup. 21832 * 21833 * This is especially helpful if you are dynamically adding and removing videos 21834 * to/from the DOM. 21835 * 21836 * @fires Player#dispose 21837 */ 21838 21839 21840 Player.prototype.dispose = function dispose() { 21841 /** 21842 * Called when the player is being disposed of. 21843 * 21844 * @event Player#dispose 21845 * @type {EventTarget~Event} 21846 */ 21847 this.trigger('dispose'); 21848 // prevent dispose from being called twice 21849 this.off('dispose'); 21850 21851 if (this.styleEl_ && this.styleEl_.parentNode) { 21852 this.styleEl_.parentNode.removeChild(this.styleEl_); 21853 this.styleEl_ = null; 21854 } 21855 21856 // Kill reference to this player 21857 Player.players[this.id_] = null; 21858 21859 if (this.tag && this.tag.player) { 21860 this.tag.player = null; 21861 } 21862 21863 if (this.el_ && this.el_.player) { 21864 this.el_.player = null; 21865 } 21866 21867 if (this.tech_) { 21868 this.tech_.dispose(); 21869 this.isPosterFromTech_ = false;
vendor: 57,008 bytes, lines 21870-23739
21870 this.poster_ = ''; 21871 } 21872 21873 if (this.playerElIngest_) { 21874 this.playerElIngest_ = null; 21875 } 21876 21877 if (this.tag) { 21878 this.tag = null; 21879 } 21880 21881 clearCacheForPlayer(this); 21882 21883 // the actual .el_ is removed here 21884 _Component.prototype.dispose.call(this); 21885 }; 21886 21887 /** 21888 * Create the `Player`'s DOM element. 21889 * 21890 * @return {Element} 21891 * The DOM element that gets created. 21892 */ 21893 21894 21895 Player.prototype.createEl = function createEl$$1() { 21896 var tag = this.tag; 21897 var el = void 0; 21898 var playerElIngest = this.playerElIngest_ = tag.parentNode && tag.parentNode.hasAttribute && tag.parentNode.hasAttribute('data-vjs-player'); 21899 var divEmbed = this.tag.tagName.toLowerCase() === 'video-js'; 21900 21901 if (playerElIngest) { 21902 el = this.el_ = tag.parentNode; 21903 } else if (!divEmbed) { 21904 el = this.el_ = _Component.prototype.createEl.call(this, 'div'); 21905 } 21906 21907 // Copy over all the attributes from the tag, including ID and class 21908 // ID will now reference player box, not the video tag 21909 var attrs = getAttributes(tag); 21910 21911 if (divEmbed) { 21912 el = this.el_ = tag; 21913 tag = this.tag = document_1.createElement('video'); 21914 while (el.children.length) { 21915 tag.appendChild(el.firstChild); 21916 } 21917 21918 if (!hasClass(el, 'video-js')) { 21919 addClass(el, 'video-js'); 21920 } 21921 21922 el.appendChild(tag); 21923 21924 playerElIngest = this.playerElIngest_ = el; 21925 // move properties over from our custom `video-js` element 21926 // to our new `video` element. This will move things like 21927 // `src` or `controls` that were set via js before the player 21928 // was initialized. 21929 Object.keys(el).forEach(function (k) { 21930 tag[k] = el[k]; 21931 }); 21932 } 21933 21934 // set tabindex to -1 to remove the video element from the focus order 21935 tag.setAttribute('tabindex', '-1'); 21936 // Workaround for #4583 (JAWS+IE doesn't announce BPB or play button) 21937 // See https://github.com/FreedomScientific/VFO-standards-support/issues/78 21938 // Note that we can't detect if JAWS is being used, but this ARIA attribute 21939 // doesn't change behavior of IE11 if JAWS is not being used 21940 if (IE_VERSION) { 21941 tag.setAttribute('role', 'application'); 21942 } 21943 21944 // Remove width/height attrs from tag so CSS can make it 100% width/height 21945 tag.removeAttribute('width'); 21946 tag.removeAttribute('height'); 21947 21948 Object.getOwnPropertyNames(attrs).forEach(function (attr) { 21949 // don't copy over the class attribute to the player element when we're in a div embed 21950 // the class is already set up properly in the divEmbed case 21951 // and we want to make sure that the `video-js` class doesn't get lost 21952 if (!(divEmbed && attr === 'class')) { 21953 el.setAttribute(attr, attrs[attr]); 21954 } 21955 21956 if (divEmbed) { 21957 tag.setAttribute(attr, attrs[attr]); 21958 } 21959 }); 21960 21961 // Update tag id/class for use as HTML5 playback tech 21962 // Might think we should do this after embedding in container so .vjs-tech class 21963 // doesn't flash 100% width/height, but class only applies with .video-js parent 21964 tag.playerId = tag.id; 21965 tag.id += '_html5_api'; 21966 tag.className = 'vjs-tech'; 21967 21968 // Make player findable on elements 21969 tag.player = el.player = this; 21970 // Default state of video is paused 21971 this.addClass('vjs-paused'); 21972 21973 // Add a style element in the player that we'll use to set the width/height 21974 // of the player in a way that's still overrideable by CSS, just like the 21975 // video element 21976 if (window_1.VIDEOJS_NO_DYNAMIC_STYLE !== true) { 21977 this.styleEl_ = createStyleElement('vjs-styles-dimensions'); 21978 var defaultsStyleEl = $('.vjs-styles-defaults'); 21979 var head = $('head'); 21980 21981 head.insertBefore(this.styleEl_, defaultsStyleEl ? defaultsStyleEl.nextSibling : head.firstChild); 21982 } 21983 21984 // Pass in the width/height/aspectRatio options which will update the style el 21985 this.width(this.options_.width); 21986 this.height(this.options_.height); 21987 this.fluid(this.options_.fluid); 21988 this.aspectRatio(this.options_.aspectRatio); 21989 21990 // Hide any links within the video/audio tag, 21991 // because IE doesn't hide them completely from screen readers. 21992 var links = tag.getElementsByTagName('a'); 21993 21994 for (var i = 0; i < links.length; i++) { 21995 var linkEl = links.item(i); 21996 21997 addClass(linkEl, 'vjs-hidden'); 21998 linkEl.setAttribute('hidden', 'hidden'); 21999 } 22000 22001 // insertElFirst seems to cause the networkState to flicker from 3 to 2, so 22002 // keep track of the original for later so we can know if the source originally failed 22003 tag.initNetworkState_ = tag.networkState; 22004 22005 // Wrap video tag in div (el/box) container 22006 if (tag.parentNode && !playerElIngest) { 22007 tag.parentNode.insertBefore(el, tag); 22008 } 22009 22010 // insert the tag as the first child of the player element 22011 // then manually add it to the children array so that this.addChild 22012 // will work properly for other components 22013 // 22014 // Breaks iPhone, fixed in HTML5 setup. 22015 prependTo(tag, el); 22016 this.children_.unshift(tag); 22017 22018 // Set lang attr on player to ensure CSS :lang() in consistent with player 22019 // if it's been set to something different to the doc 22020 this.el_.setAttribute('lang', this.language_); 22021 22022 this.el_ = el; 22023 22024 return el; 22025 }; 22026 22027 /** 22028 * A getter/setter for the `Player`'s width. Returns the player's configured value. 22029 * To get the current width use `currentWidth()`. 22030 * 22031 * @param {number} [value] 22032 * The value to set the `Player`'s width to. 22033 * 22034 * @return {number} 22035 * The current width of the `Player` when getting. 22036 */ 22037 22038 22039 Player.prototype.width = function width(value) { 22040 return this.dimension('width', value); 22041 }; 22042 22043 /** 22044 * A getter/setter for the `Player`'s height. Returns the player's configured value. 22045 * To get the current height use `currentheight()`. 22046 * 22047 * @param {number} [value] 22048 * The value to set the `Player`'s heigth to. 22049 * 22050 * @return {number} 22051 * The current height of the `Player` when getting. 22052 */ 22053 22054 22055 Player.prototype.height = function height(value) { 22056 return this.dimension('height', value); 22057 }; 22058 22059 /** 22060 * A getter/setter for the `Player`'s width & height. 22061 * 22062 * @param {string} dimension 22063 * This string can be: 22064 * - 'width' 22065 * - 'height' 22066 * 22067 * @param {number} [value] 22068 * Value for dimension specified in the first argument. 22069 * 22070 * @return {number} 22071 * The dimension arguments value when getting (width/height). 22072 */ 22073 22074 22075 Player.prototype.dimension = function dimension(_dimension, value) { 22076 var privDimension = _dimension + '_'; 22077 22078 if (value === undefined) { 22079 return this[privDimension] || 0; 22080 } 22081 22082 if (value === '') { 22083 // If an empty string is given, reset the dimension to be automatic 22084 this[privDimension] = undefined; 22085 this.updateStyleEl_(); 22086 return; 22087 } 22088 22089 var parsedVal = parseFloat(value); 22090 22091 if (isNaN(parsedVal)) { 22092 log$1.error('Improper value "' + value + '" supplied for for ' + _dimension); 22093 return; 22094 } 22095 22096 this[privDimension] = parsedVal; 22097 this.updateStyleEl_(); 22098 }; 22099 22100 /** 22101 * A getter/setter/toggler for the vjs-fluid `className` on the `Player`. 22102 * 22103 * @param {boolean} [bool] 22104 * - A value of true adds the class. 22105 * - A value of false removes the class. 22106 * - No value will toggle the fluid class. 22107 * 22108 * @return {boolean|undefined} 22109 * - The value of fluid when getting. 22110 * - `undefined` when setting. 22111 */ 22112 22113 22114 Player.prototype.fluid = function fluid(bool) { 22115 if (bool === undefined) { 22116 return !!this.fluid_; 22117 } 22118 22119 this.fluid_ = !!bool; 22120 22121 if (bool) { 22122 this.addClass('vjs-fluid'); 22123 } else { 22124 this.removeClass('vjs-fluid'); 22125 } 22126 22127 this.updateStyleEl_(); 22128 }; 22129 22130 /** 22131 * Get/Set the aspect ratio 22132 * 22133 * @param {string} [ratio] 22134 * Aspect ratio for player 22135 * 22136 * @return {string|undefined} 22137 * returns the current aspect ratio when getting 22138 */ 22139 22140 /** 22141 * A getter/setter for the `Player`'s aspect ratio. 22142 * 22143 * @param {string} [ratio] 22144 * The value to set the `Player's aspect ratio to. 22145 * 22146 * @return {string|undefined} 22147 * - The current aspect ratio of the `Player` when getting. 22148 * - undefined when setting 22149 */ 22150 22151 22152 Player.prototype.aspectRatio = function aspectRatio(ratio) { 22153 if (ratio === undefined) { 22154 return this.aspectRatio_; 22155 } 22156 22157 // Check for width:height format 22158 if (!/^\d+\:\d+$/.test(ratio)) { 22159 throw new Error('Improper value supplied for aspect ratio. The format should be width:height, for example 16:9.'); 22160 } 22161 this.aspectRatio_ = ratio; 22162 22163 // We're assuming if you set an aspect ratio you want fluid mode, 22164 // because in fixed mode you could calculate width and height yourself. 22165 this.fluid(true); 22166 22167 this.updateStyleEl_(); 22168 }; 22169 22170 /** 22171 * Update styles of the `Player` element (height, width and aspect ratio). 22172 * 22173 * @private 22174 * @listens Tech#loadedmetadata 22175 */ 22176 22177 22178 Player.prototype.updateStyleEl_ = function updateStyleEl_() { 22179 if (window_1.VIDEOJS_NO_DYNAMIC_STYLE === true) { 22180 var _width = typeof this.width_ === 'number' ? this.width_ : this.options_.width; 22181 var _height = typeof this.height_ === 'number' ? this.height_ : this.options_.height; 22182 var techEl = this.tech_ && this.tech_.el(); 22183 22184 if (techEl) { 22185 if (_width >= 0) { 22186 techEl.width = _width; 22187 } 22188 if (_height >= 0) { 22189 techEl.height = _height; 22190 } 22191 } 22192 22193 return; 22194 } 22195 22196 var width = void 0; 22197 var height = void 0; 22198 var aspectRatio = void 0; 22199 var idClass = void 0; 22200 22201 // The aspect ratio is either used directly or to calculate width and height. 22202 if (this.aspectRatio_ !== undefined && this.aspectRatio_ !== 'auto') { 22203 // Use any aspectRatio that's been specifically set 22204 aspectRatio = this.aspectRatio_; 22205 } else if (this.videoWidth() > 0) { 22206 // Otherwise try to get the aspect ratio from the video metadata 22207 aspectRatio = this.videoWidth() + ':' + this.videoHeight(); 22208 } else { 22209 // Or use a default. The video element's is 2:1, but 16:9 is more common. 22210 aspectRatio = '16:9'; 22211 } 22212 22213 // Get the ratio as a decimal we can use to calculate dimensions 22214 var ratioParts = aspectRatio.split(':'); 22215 var ratioMultiplier = ratioParts[1] / ratioParts[0]; 22216 22217 if (this.width_ !== undefined) { 22218 // Use any width that's been specifically set 22219 width = this.width_; 22220 } else if (this.height_ !== undefined) { 22221 // Or calulate the width from the aspect ratio if a height has been set 22222 width = this.height_ / ratioMultiplier; 22223 } else { 22224 // Or use the video's metadata, or use the video el's default of 300 22225 width = this.videoWidth() || 300; 22226 } 22227 22228 if (this.height_ !== undefined) { 22229 // Use any height that's been specifically set 22230 height = this.height_; 22231 } else { 22232 // Otherwise calculate the height from the ratio and the width 22233 height = width * ratioMultiplier; 22234 } 22235 22236 // Ensure the CSS class is valid by starting with an alpha character 22237 if (/^[^a-zA-Z]/.test(this.id())) { 22238 idClass = 'dimensions-' + this.id(); 22239 } else { 22240 idClass = this.id() + '-dimensions'; 22241 } 22242 22243 // Ensure the right class is still on the player for the style element 22244 this.addClass(idClass); 22245 22246 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 '); 22247 }; 22248 22249 /** 22250 * Load/Create an instance of playback {@link Tech} including element 22251 * and API methods. Then append the `Tech` element in `Player` as a child. 22252 * 22253 * @param {string} techName 22254 * name of the playback technology 22255 * 22256 * @param {string} source 22257 * video source 22258 * 22259 * @private 22260 */ 22261 22262 22263 Player.prototype.loadTech_ = function loadTech_(techName, source) { 22264 var _this2 = this; 22265 22266 // Pause and remove current playback technology 22267 if (this.tech_) { 22268 this.unloadTech_(); 22269 } 22270 22271 var titleTechName = toTitleCase(techName); 22272 var camelTechName = techName.charAt(0).toLowerCase() + techName.slice(1); 22273 22274 // get rid of the HTML5 video tag as soon as we are using another tech 22275 if (titleTechName !== 'Html5' && this.tag) { 22276 Tech.getTech('Html5').disposeMediaElement(this.tag); 22277 this.tag.player = null; 22278 this.tag = null; 22279 } 22280 22281 this.techName_ = titleTechName; 22282 22283 // Turn off API access because we're loading a new tech that might load asynchronously 22284 this.isReady_ = false; 22285 22286 // if autoplay is a string we pass false to the tech 22287 // because the player is going to handle autoplay on `loadstart` 22288 var autoplay = typeof this.autoplay() === 'string' ? false : this.autoplay(); 22289 22290 // Grab tech-specific options from player options and add source and parent element to use. 22291 var techOptions = { 22292 source: source, 22293 autoplay: autoplay, 22294 'nativeControlsForTouch': this.options_.nativeControlsForTouch, 22295 'playerId': this.id(), 22296 'techId': this.id() + '_' + titleTechName + '_api', 22297 'playsinline': this.options_.playsinline, 22298 'preload': this.options_.preload, 22299 'loop': this.options_.loop, 22300 'muted': this.options_.muted, 22301 'poster': this.poster(), 22302 'language': this.language(), 22303 'playerElIngest': this.playerElIngest_ || false, 22304 'vtt.js': this.options_['vtt.js'], 22305 'canOverridePoster': !!this.options_.techCanOverridePoster, 22306 'enableSourceset': this.options_.enableSourceset 22307 }; 22308 22309 ALL.names.forEach(function (name$$1) { 22310 var props = ALL[name$$1]; 22311 22312 techOptions[props.getterName] = _this2[props.privateName]; 22313 }); 22314 22315 assign(techOptions, this.options_[titleTechName]); 22316 assign(techOptions, this.options_[camelTechName]); 22317 assign(techOptions, this.options_[techName.toLowerCase()]); 22318 22319 if (this.tag) { 22320 techOptions.tag = this.tag; 22321 } 22322 22323 if (source && source.src === this.cache_.src && this.cache_.currentTime > 0) { 22324 techOptions.startTime = this.cache_.currentTime; 22325 } 22326 22327 // Initialize tech instance 22328 var TechClass = Tech.getTech(techName); 22329 22330 if (!TechClass) { 22331 throw new Error('No Tech named \'' + titleTechName + '\' exists! \'' + titleTechName + '\' should be registered using videojs.registerTech()\''); 22332 } 22333 22334 this.tech_ = new TechClass(techOptions); 22335 22336 // player.triggerReady is always async, so don't need this to be async 22337 this.tech_.ready(bind(this, this.handleTechReady_), true); 22338 22339 textTrackConverter.jsonToTextTracks(this.textTracksJson_ || [], this.tech_); 22340 22341 // Listen to all HTML5-defined events and trigger them on the player 22342 TECH_EVENTS_RETRIGGER.forEach(function (event) { 22343 _this2.on(_this2.tech_, event, _this2['handleTech' + toTitleCase(event) + '_']); 22344 }); 22345 22346 Object.keys(TECH_EVENTS_QUEUE).forEach(function (event) { 22347 _this2.on(_this2.tech_, event, function (eventObj) { 22348 if (_this2.tech_.playbackRate() === 0 && _this2.tech_.seeking()) { 22349 _this2.queuedCallbacks_.push({ 22350 callback: _this2['handleTech' + TECH_EVENTS_QUEUE[event] + '_'].bind(_this2), 22351 event: eventObj 22352 }); 22353 return; 22354 } 22355 _this2['handleTech' + TECH_EVENTS_QUEUE[event] + '_'](eventObj); 22356 }); 22357 }); 22358 22359 this.on(this.tech_, 'loadstart', this.handleTechLoadStart_); 22360 this.on(this.tech_, 'sourceset', this.handleTechSourceset_); 22361 this.on(this.tech_, 'waiting', this.handleTechWaiting_); 22362 this.on(this.tech_, 'ended', this.handleTechEnded_); 22363 this.on(this.tech_, 'seeking', this.handleTechSeeking_); 22364 this.on(this.tech_, 'play', this.handleTechPlay_); 22365 this.on(this.tech_, 'firstplay', this.handleTechFirstPlay_); 22366 this.on(this.tech_, 'pause', this.handleTechPause_); 22367 this.on(this.tech_, 'durationchange', this.handleTechDurationChange_); 22368 this.on(this.tech_, 'fullscreenchange', this.handleTechFullscreenChange_); 22369 this.on(this.tech_, 'error', this.handleTechError_); 22370 this.on(this.tech_, 'loadedmetadata', this.updateStyleEl_); 22371 this.on(this.tech_, 'posterchange', this.handleTechPosterChange_); 22372 this.on(this.tech_, 'textdata', this.handleTechTextData_); 22373 this.on(this.tech_, 'ratechange', this.handleTechRateChange_); 22374 22375 this.usingNativeControls(this.techGet_('controls')); 22376 22377 if (this.controls() && !this.usingNativeControls()) { 22378 this.addTechControlsListeners_(); 22379 } 22380 22381 // Add the tech element in the DOM if it was not already there 22382 // Make sure to not insert the original video element if using Html5 22383 if (this.tech_.el().parentNode !== this.el() && (titleTechName !== 'Html5' || !this.tag)) { 22384 prependTo(this.tech_.el(), this.el()); 22385 } 22386 22387 // Get rid of the original video tag reference after the first tech is loaded 22388 if (this.tag) { 22389 this.tag.player = null; 22390 this.tag = null; 22391 } 22392 }; 22393 22394 /** 22395 * Unload and dispose of the current playback {@link Tech}. 22396 * 22397 * @private 22398 */ 22399 22400 22401 Player.prototype.unloadTech_ = function unloadTech_() { 22402 var _this3 = this; 22403 22404 // Save the current text tracks so that we can reuse the same text tracks with the next tech 22405 ALL.names.forEach(function (name$$1) { 22406 var props = ALL[name$$1]; 22407 22408 _this3[props.privateName] = _this3[props.getterName](); 22409 }); 22410 this.textTracksJson_ = textTrackConverter.textTracksToJson(this.tech_); 22411 22412 this.isReady_ = false; 22413 22414 this.tech_.dispose(); 22415 22416 this.tech_ = false; 22417 22418 if (this.isPosterFromTech_) { 22419 this.poster_ = ''; 22420 this.trigger('posterchange'); 22421 } 22422 22423 this.isPosterFromTech_ = false; 22424 }; 22425 22426 /** 22427 * Return a reference to the current {@link Tech}. 22428 * It will print a warning by default about the danger of using the tech directly 22429 * but any argument that is passed in will silence the warning. 22430 * 22431 * @param {*} [safety] 22432 * Anything passed in to silence the warning 22433 * 22434 * @return {Tech} 22435 * The Tech 22436 */ 22437 22438 22439 Player.prototype.tech = function tech(safety) { 22440 if (safety === undefined) { 22441 log$1.warn(tsml(_templateObject$2)); 22442 } 22443 22444 return this.tech_; 22445 }; 22446 22447 /** 22448 * Set up click and touch listeners for the playback element 22449 * 22450 * - On desktops: a click on the video itself will toggle playback 22451 * - On mobile devices: a click on the video toggles controls 22452 * which is done by toggling the user state between active and 22453 * inactive 22454 * - A tap can signal that a user has become active or has become inactive 22455 * e.g. a quick tap on an iPhone movie should reveal the controls. Another 22456 * quick tap should hide them again (signaling the user is in an inactive 22457 * viewing state) 22458 * - In addition to this, we still want the user to be considered inactive after 22459 * a few seconds of inactivity. 22460 * 22461 * > Note: the only part of iOS interaction we can't mimic with this setup 22462 * is a touch and hold on the video element counting as activity in order to 22463 * keep the controls showing, but that shouldn't be an issue. A touch and hold 22464 * on any controls will still keep the user active 22465 * 22466 * @private 22467 */ 22468 22469 22470 Player.prototype.addTechControlsListeners_ = function addTechControlsListeners_() { 22471 // Make sure to remove all the previous listeners in case we are called multiple times. 22472 this.removeTechControlsListeners_(); 22473 22474 // Some browsers (Chrome & IE) don't trigger a click on a flash swf, but do 22475 // trigger mousedown/up. 22476 // http://stackoverflow.com/questions/1444562/javascript-onclick-event-over-flash-object 22477 // Any touch events are set to block the mousedown event from happening 22478 this.on(this.tech_, 'mousedown', this.handleTechClick_); 22479 this.on(this.tech_, 'dblclick', this.handleTechDoubleClick_); 22480 22481 // If the controls were hidden we don't want that to change without a tap event 22482 // so we'll check if the controls were already showing before reporting user 22483 // activity 22484 this.on(this.tech_, 'touchstart', this.handleTechTouchStart_); 22485 this.on(this.tech_, 'touchmove', this.handleTechTouchMove_); 22486 this.on(this.tech_, 'touchend', this.handleTechTouchEnd_); 22487 22488 // The tap listener needs to come after the touchend listener because the tap 22489 // listener cancels out any reportedUserActivity when setting userActive(false) 22490 this.on(this.tech_, 'tap', this.handleTechTap_); 22491 }; 22492 22493 /** 22494 * Remove the listeners used for click and tap controls. This is needed for 22495 * toggling to controls disabled, where a tap/touch should do nothing. 22496 * 22497 * @private 22498 */ 22499 22500 22501 Player.prototype.removeTechControlsListeners_ = function removeTechControlsListeners_() { 22502 // We don't want to just use `this.off()` because there might be other needed 22503 // listeners added by techs that extend this. 22504 this.off(this.tech_, 'tap', this.handleTechTap_); 22505 this.off(this.tech_, 'touchstart', this.handleTechTouchStart_); 22506 this.off(this.tech_, 'touchmove', this.handleTechTouchMove_); 22507 this.off(this.tech_, 'touchend', this.handleTechTouchEnd_); 22508 this.off(this.tech_, 'mousedown', this.handleTechClick_); 22509 this.off(this.tech_, 'dblclick', this.handleTechDoubleClick_); 22510 }; 22511 22512 /** 22513 * Player waits for the tech to be ready 22514 * 22515 * @private 22516 */ 22517 22518 22519 Player.prototype.handleTechReady_ = function handleTechReady_() { 22520 this.triggerReady(); 22521 22522 // Keep the same volume as before 22523 if (this.cache_.volume) { 22524 this.techCall_('setVolume', this.cache_.volume); 22525 } 22526 22527 // Look if the tech found a higher resolution poster while loading 22528 this.handleTechPosterChange_(); 22529 22530 // Update the duration if available 22531 this.handleTechDurationChange_(); 22532 }; 22533 22534 /** 22535 * Retrigger the `loadstart` event that was triggered by the {@link Tech}. This 22536 * function will also trigger {@link Player#firstplay} if it is the first loadstart 22537 * for a video. 22538 * 22539 * @fires Player#loadstart 22540 * @fires Player#firstplay 22541 * @listens Tech#loadstart 22542 * @private 22543 */ 22544 22545 22546 Player.prototype.handleTechLoadStart_ = function handleTechLoadStart_() { 22547 // TODO: Update to use `emptied` event instead. See #1277. 22548 22549 this.removeClass('vjs-ended'); 22550 this.removeClass('vjs-seeking'); 22551 22552 // reset the error state 22553 this.error(null); 22554 22555 // If it's already playing we want to trigger a firstplay event now. 22556 // The firstplay event relies on both the play and loadstart events 22557 // which can happen in any order for a new source 22558 if (!this.paused()) { 22559 /** 22560 * Fired when the user agent begins looking for media data 22561 * 22562 * @event Player#loadstart 22563 * @type {EventTarget~Event} 22564 */ 22565 this.trigger('loadstart'); 22566 this.trigger('firstplay'); 22567 } else { 22568 // reset the hasStarted state 22569 this.hasStarted(false); 22570 this.trigger('loadstart'); 22571 } 22572 22573 // autoplay happens after loadstart for the browser, 22574 // so we mimic that behavior 22575 this.manualAutoplay_(this.autoplay()); 22576 }; 22577 22578 /** 22579 * Handle autoplay string values, rather than the typical boolean 22580 * values that should be handled by the tech. Note that this is not 22581 * part of any specification. Valid values and what they do can be 22582 * found on the autoplay getter at Player#autoplay() 22583 */ 22584 22585 22586 Player.prototype.manualAutoplay_ = function manualAutoplay_(type) { 22587 var _this4 = this; 22588 22589 if (!this.tech_ || typeof type !== 'string') { 22590 return; 22591 } 22592 22593 var muted = function muted() { 22594 var previouslyMuted = _this4.muted(); 22595 22596 _this4.muted(true); 22597 22598 var playPromise = _this4.play(); 22599 22600 if (!playPromise || !playPromise.then || !playPromise.catch) { 22601 return; 22602 } 22603 22604 return playPromise.catch(function (e) { 22605 // restore old value of muted on failure 22606 _this4.muted(previouslyMuted); 22607 }); 22608 }; 22609 22610 var promise = void 0; 22611 22612 if (type === 'any') { 22613 promise = this.play(); 22614 22615 if (promise && promise.then && promise.catch) { 22616 promise.catch(function () { 22617 return muted(); 22618 }); 22619 } 22620 } else if (type === 'muted') { 22621 promise = muted(); 22622 } else { 22623 promise = this.play(); 22624 } 22625 22626 if (!promise || !promise.then || !promise.catch) { 22627 return; 22628 } 22629 22630 return promise.then(function () { 22631 _this4.trigger({ type: 'autoplay-success', autoplay: type }); 22632 }).catch(function (e) { 22633 _this4.trigger({ type: 'autoplay-failure', autoplay: type }); 22634 }); 22635 }; 22636 22637 /** 22638 * Update the internal source caches so that we return the correct source from 22639 * `src()`, `currentSource()`, and `currentSources()`. 22640 * 22641 * > Note: `currentSources` will not be updated if the source that is passed in exists 22642 * in the current `currentSources` cache. 22643 * 22644 * 22645 * @param {Tech~SourceObject} srcObj 22646 * A string or object source to update our caches to. 22647 */ 22648 22649 22650 Player.prototype.updateSourceCaches_ = function updateSourceCaches_() { 22651 var srcObj = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; 22652 22653 22654 var src = srcObj; 22655 var type = ''; 22656 22657 if (typeof src !== 'string') { 22658 src = srcObj.src; 22659 type = srcObj.type; 22660 } 22661 // make sure all the caches are set to default values 22662 // to prevent null checking 22663 this.cache_.source = this.cache_.source || {}; 22664 this.cache_.sources = this.cache_.sources || []; 22665 22666 // try to get the type of the src that was passed in 22667 if (src && !type) { 22668 type = findMimetype(this, src); 22669 } 22670 22671 // update `currentSource` cache always 22672 this.cache_.source = mergeOptions({}, srcObj, { src: src, type: type }); 22673 22674 var matchingSources = this.cache_.sources.filter(function (s) { 22675 return s.src && s.src === src; 22676 }); 22677 var sourceElSources = []; 22678 var sourceEls = this.$$('source'); 22679 var matchingSourceEls = []; 22680 22681 for (var i = 0; i < sourceEls.length; i++) { 22682 var sourceObj = getAttributes(sourceEls[i]); 22683 22684 sourceElSources.push(sourceObj); 22685 22686 if (sourceObj.src && sourceObj.src === src) { 22687 matchingSourceEls.push(sourceObj.src); 22688 } 22689 } 22690 22691 // if we have matching source els but not matching sources 22692 // the current source cache is not up to date 22693 if (matchingSourceEls.length && !matchingSources.length) { 22694 this.cache_.sources = sourceElSources; 22695 // if we don't have matching source or source els set the 22696 // sources cache to the `currentSource` cache 22697 } else if (!matchingSources.length) { 22698 this.cache_.sources = [this.cache_.source]; 22699 } 22700 22701 // update the tech `src` cache 22702 this.cache_.src = src; 22703 }; 22704 22705 /** 22706 * *EXPERIMENTAL* Fired when the source is set or changed on the {@link Tech} 22707 * causing the media element to reload. 22708 * 22709 * It will fire for the initial source and each subsequent source. 22710 * This event is a custom event from Video.js and is triggered by the {@link Tech}. 22711 * 22712 * The event object for this event contains a `src` property that will contain the source 22713 * that was available when the event was triggered. This is generally only necessary if Video.js 22714 * is switching techs while the source was being changed. 22715 * 22716 * It is also fired when `load` is called on the player (or media element) 22717 * because the {@link https://html.spec.whatwg.org/multipage/media.html#dom-media-load|specification for `load`} 22718 * says that the resource selection algorithm needs to be aborted and restarted. 22719 * In this case, it is very likely that the `src` property will be set to the 22720 * empty string `""` to indicate we do not know what the source will be but 22721 * that it is changing. 22722 * 22723 * *This event is currently still experimental and may change in minor releases.* 22724 * __To use this, pass `enableSourceset` option to the player.__ 22725 * 22726 * @event Player#sourceset 22727 * @type {EventTarget~Event} 22728 * @prop {string} src 22729 * The source url available when the `sourceset` was triggered. 22730 * It will be an empty string if we cannot know what the source is 22731 * but know that the source will change. 22732 */ 22733 /** 22734 * Retrigger the `sourceset` event that was triggered by the {@link Tech}. 22735 * 22736 * @fires Player#sourceset 22737 * @listens Tech#sourceset 22738 * @private 22739 */ 22740 22741 22742 Player.prototype.handleTechSourceset_ = function handleTechSourceset_(event) { 22743 var _this5 = this; 22744 22745 // only update the source cache when the source 22746 // was not updated using the player api 22747 if (!this.changingSrc_) { 22748 // update the source to the intial source right away 22749 // in some cases this will be empty string 22750 this.updateSourceCaches_(event.src); 22751 22752 // if the `sourceset` `src` was an empty string 22753 // wait for a `loadstart` to update the cache to `currentSrc`. 22754 // If a sourceset happens before a `loadstart`, we reset the state 22755 // as this function will be called again. 22756 if (!event.src) { 22757 var updateCache = function updateCache(e) { 22758 if (e.type !== 'sourceset') { 22759 _this5.updateSourceCaches_(_this5.techGet_('currentSrc')); 22760 } 22761 22762 _this5.tech_.off(['sourceset', 'loadstart'], updateCache); 22763 }; 22764 22765 this.tech_.one(['sourceset', 'loadstart'], updateCache); 22766 } 22767 } 22768 22769 this.trigger({ 22770 src: event.src, 22771 type: 'sourceset' 22772 }); 22773 }; 22774 22775 /** 22776 * Add/remove the vjs-has-started class 22777 * 22778 * @fires Player#firstplay 22779 * 22780 * @param {boolean} request 22781 * - true: adds the class 22782 * - false: remove the class 22783 * 22784 * @return {boolean} 22785 * the boolean value of hasStarted_ 22786 */ 22787 22788 22789 Player.prototype.hasStarted = function hasStarted(request) { 22790 if (request === undefined) { 22791 // act as getter, if we have no request to change 22792 return this.hasStarted_; 22793 } 22794 22795 if (request === this.hasStarted_) { 22796 return; 22797 } 22798 22799 this.hasStarted_ = request; 22800 22801 if (this.hasStarted_) { 22802 this.addClass('vjs-has-started'); 22803 this.trigger('firstplay'); 22804 } else { 22805 this.removeClass('vjs-has-started'); 22806 } 22807 }; 22808 22809 /** 22810 * Fired whenever the media begins or resumes playback 22811 * 22812 * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-play} 22813 * @fires Player#play 22814 * @listens Tech#play 22815 * @private 22816 */ 22817 22818 22819 Player.prototype.handleTechPlay_ = function handleTechPlay_() { 22820 this.removeClass('vjs-ended'); 22821 this.removeClass('vjs-paused'); 22822 this.addClass('vjs-playing'); 22823 22824 // hide the poster when the user hits play 22825 this.hasStarted(true); 22826 /** 22827 * Triggered whenever an {@link Tech#play} event happens. Indicates that 22828 * playback has started or resumed. 22829 * 22830 * @event Player#play 22831 * @type {EventTarget~Event} 22832 */ 22833 this.trigger('play'); 22834 }; 22835 22836 /** 22837 * Retrigger the `ratechange` event that was triggered by the {@link Tech}. 22838 * 22839 * If there were any events queued while the playback rate was zero, fire 22840 * those events now. 22841 * 22842 * @private 22843 * @method Player#handleTechRateChange_ 22844 * @fires Player#ratechange 22845 * @listens Tech#ratechange 22846 */ 22847 22848 22849 Player.prototype.handleTechRateChange_ = function handleTechRateChange_() { 22850 if (this.tech_.playbackRate() > 0 && this.cache_.lastPlaybackRate === 0) { 22851 this.queuedCallbacks_.forEach(function (queued) { 22852 return queued.callback(queued.event); 22853 }); 22854 this.queuedCallbacks_ = []; 22855 } 22856 this.cache_.lastPlaybackRate = this.tech_.playbackRate(); 22857 /** 22858 * Fires when the playing speed of the audio/video is changed 22859 * 22860 * @event Player#ratechange 22861 * @type {event} 22862 */ 22863 this.trigger('ratechange'); 22864 }; 22865 22866 /** 22867 * Retrigger the `waiting` event that was triggered by the {@link Tech}. 22868 * 22869 * @fires Player#waiting 22870 * @listens Tech#waiting 22871 * @private 22872 */ 22873 22874 22875 Player.prototype.handleTechWaiting_ = function handleTechWaiting_() { 22876 var _this6 = this; 22877 22878 this.addClass('vjs-waiting'); 22879 /** 22880 * A readyState change on the DOM element has caused playback to stop. 22881 * 22882 * @event Player#waiting 22883 * @type {EventTarget~Event} 22884 */ 22885 this.trigger('waiting'); 22886 this.one('timeupdate', function () { 22887 return _this6.removeClass('vjs-waiting'); 22888 }); 22889 }; 22890 22891 /** 22892 * Retrigger the `canplay` event that was triggered by the {@link Tech}. 22893 * > Note: This is not consistent between browsers. See #1351 22894 * 22895 * @fires Player#canplay 22896 * @listens Tech#canplay 22897 * @private 22898 */ 22899 22900 22901 Player.prototype.handleTechCanPlay_ = function handleTechCanPlay_() { 22902 this.removeClass('vjs-waiting'); 22903 /** 22904 * The media has a readyState of HAVE_FUTURE_DATA or greater. 22905 * 22906 * @event Player#canplay 22907 * @type {EventTarget~Event} 22908 */ 22909 this.trigger('canplay'); 22910 }; 22911 22912 /** 22913 * Retrigger the `canplaythrough` event that was triggered by the {@link Tech}. 22914 * 22915 * @fires Player#canplaythrough 22916 * @listens Tech#canplaythrough 22917 * @private 22918 */ 22919 22920 22921 Player.prototype.handleTechCanPlayThrough_ = function handleTechCanPlayThrough_() { 22922 this.removeClass('vjs-waiting'); 22923 /** 22924 * The media has a readyState of HAVE_ENOUGH_DATA or greater. This means that the 22925 * entire media file can be played without buffering. 22926 * 22927 * @event Player#canplaythrough 22928 * @type {EventTarget~Event} 22929 */ 22930 this.trigger('canplaythrough'); 22931 }; 22932 22933 /** 22934 * Retrigger the `playing` event that was triggered by the {@link Tech}. 22935 * 22936 * @fires Player#playing 22937 * @listens Tech#playing 22938 * @private 22939 */ 22940 22941 22942 Player.prototype.handleTechPlaying_ = function handleTechPlaying_() { 22943 this.removeClass('vjs-waiting'); 22944 /** 22945 * The media is no longer blocked from playback, and has started playing. 22946 * 22947 * @event Player#playing 22948 * @type {EventTarget~Event} 22949 */ 22950 this.trigger('playing'); 22951 }; 22952 22953 /** 22954 * Retrigger the `seeking` event that was triggered by the {@link Tech}. 22955 * 22956 * @fires Player#seeking 22957 * @listens Tech#seeking 22958 * @private 22959 */ 22960 22961 22962 Player.prototype.handleTechSeeking_ = function handleTechSeeking_() { 22963 this.addClass('vjs-seeking'); 22964 /** 22965 * Fired whenever the player is jumping to a new time 22966 * 22967 * @event Player#seeking 22968 * @type {EventTarget~Event} 22969 */ 22970 this.trigger('seeking'); 22971 }; 22972 22973 /** 22974 * Retrigger the `seeked` event that was triggered by the {@link Tech}. 22975 * 22976 * @fires Player#seeked 22977 * @listens Tech#seeked 22978 * @private 22979 */ 22980 22981 22982 Player.prototype.handleTechSeeked_ = function handleTechSeeked_() { 22983 this.removeClass('vjs-seeking'); 22984 /** 22985 * Fired when the player has finished jumping to a new time 22986 * 22987 * @event Player#seeked 22988 * @type {EventTarget~Event} 22989 */ 22990 this.trigger('seeked'); 22991 }; 22992 22993 /** 22994 * Retrigger the `firstplay` event that was triggered by the {@link Tech}. 22995 * 22996 * @fires Player#firstplay 22997 * @listens Tech#firstplay 22998 * @deprecated As of 6.0 firstplay event is deprecated. 22999 * @deprecated As of 6.0 passing the `starttime` option to the player and the firstplay event are deprecated. 23000 * @private 23001 */ 23002 23003 23004 Player.prototype.handleTechFirstPlay_ = function handleTechFirstPlay_() { 23005 // If the first starttime attribute is specified 23006 // then we will start at the given offset in seconds 23007 if (this.options_.starttime) { 23008 log$1.warn('Passing the `starttime` option to the player will be deprecated in 6.0'); 23009 this.currentTime(this.options_.starttime); 23010 } 23011 23012 this.addClass('vjs-has-started'); 23013 /** 23014 * Fired the first time a video is played. Not part of the HLS spec, and this is 23015 * probably not the best implementation yet, so use sparingly. If you don't have a 23016 * reason to prevent playback, use `myPlayer.one('play');` instead. 23017 * 23018 * @event Player#firstplay 23019 * @deprecated As of 6.0 firstplay event is deprecated. 23020 * @type {EventTarget~Event} 23021 */ 23022 this.trigger('firstplay'); 23023 }; 23024 23025 /** 23026 * Retrigger the `pause` event that was triggered by the {@link Tech}. 23027 * 23028 * @fires Player#pause 23029 * @listens Tech#pause 23030 * @private 23031 */ 23032 23033 23034 Player.prototype.handleTechPause_ = function handleTechPause_() { 23035 this.removeClass('vjs-playing'); 23036 this.addClass('vjs-paused'); 23037 /** 23038 * Fired whenever the media has been paused 23039 * 23040 * @event Player#pause 23041 * @type {EventTarget~Event} 23042 */ 23043 this.trigger('pause'); 23044 }; 23045 23046 /** 23047 * Retrigger the `ended` event that was triggered by the {@link Tech}. 23048 * 23049 * @fires Player#ended 23050 * @listens Tech#ended 23051 * @private 23052 */ 23053 23054 23055 Player.prototype.handleTechEnded_ = function handleTechEnded_() { 23056 this.addClass('vjs-ended'); 23057 if (this.options_.loop) { 23058 this.currentTime(0); 23059 this.play(); 23060 } else if (!this.paused()) { 23061 this.pause(); 23062 } 23063 23064 /** 23065 * Fired when the end of the media resource is reached (currentTime == duration) 23066 * 23067 * @event Player#ended 23068 * @type {EventTarget~Event} 23069 */ 23070 this.trigger('ended'); 23071 }; 23072 23073 /** 23074 * Fired when the duration of the media resource is first known or changed 23075 * 23076 * @listens Tech#durationchange 23077 * @private 23078 */ 23079 23080 23081 Player.prototype.handleTechDurationChange_ = function handleTechDurationChange_() { 23082 this.duration(this.techGet_('duration')); 23083 }; 23084 23085 /** 23086 * Handle a click on the media element to play/pause 23087 * 23088 * @param {EventTarget~Event} event 23089 * the event that caused this function to trigger 23090 * 23091 * @listens Tech#mousedown 23092 * @private 23093 */ 23094 23095 23096 Player.prototype.handleTechClick_ = function handleTechClick_(event) { 23097 if (!isSingleLeftClick(event)) { 23098 return; 23099 } 23100 23101 // When controls are disabled a click should not toggle playback because 23102 // the click is considered a control 23103 if (!this.controls_) { 23104 return; 23105 } 23106 23107 if (this.paused()) { 23108 silencePromise(this.play()); 23109 } else { 23110 this.pause(); 23111 } 23112 }; 23113 23114 /** 23115 * Handle a double-click on the media element to enter/exit fullscreen 23116 * 23117 * @param {EventTarget~Event} event 23118 * the event that caused this function to trigger 23119 * 23120 * @listens Tech#dblclick 23121 * @private 23122 */ 23123 23124 23125 Player.prototype.handleTechDoubleClick_ = function handleTechDoubleClick_(event) { 23126 if (!this.controls_) { 23127 return; 23128 } 23129 23130 // we do not want to toggle fullscreen state 23131 // when double-clicking inside a control bar or a modal 23132 var inAllowedEls = Array.prototype.some.call(this.$$('.vjs-control-bar, .vjs-modal-dialog'), function (el) { 23133 return el.contains(event.target); 23134 }); 23135 23136 if (!inAllowedEls) { 23137 if (this.isFullscreen()) { 23138 this.exitFullscreen(); 23139 } else { 23140 this.requestFullscreen(); 23141 } 23142 } 23143 }; 23144 23145 /** 23146 * Handle a tap on the media element. It will toggle the user 23147 * activity state, which hides and shows the controls. 23148 * 23149 * @listens Tech#tap 23150 * @private 23151 */ 23152 23153 23154 Player.prototype.handleTechTap_ = function handleTechTap_() { 23155 this.userActive(!this.userActive()); 23156 }; 23157 23158 /** 23159 * Handle touch to start 23160 * 23161 * @listens Tech#touchstart 23162 * @private 23163 */ 23164 23165 23166 Player.prototype.handleTechTouchStart_ = function handleTechTouchStart_() { 23167 this.userWasActive = this.userActive(); 23168 }; 23169 23170 /** 23171 * Handle touch to move 23172 * 23173 * @listens Tech#touchmove 23174 * @private 23175 */ 23176 23177 23178 Player.prototype.handleTechTouchMove_ = function handleTechTouchMove_() { 23179 if (this.userWasActive) { 23180 this.reportUserActivity(); 23181 } 23182 }; 23183 23184 /** 23185 * Handle touch to end 23186 * 23187 * @param {EventTarget~Event} event 23188 * the touchend event that triggered 23189 * this function 23190 * 23191 * @listens Tech#touchend 23192 * @private 23193 */ 23194 23195 23196 Player.prototype.handleTechTouchEnd_ = function handleTechTouchEnd_(event) { 23197 // Stop the mouse events from also happening 23198 event.preventDefault(); 23199 }; 23200 23201 /** 23202 * Fired when the player switches in or out of fullscreen mode 23203 * 23204 * @private 23205 * @listens Player#fullscreenchange 23206 */ 23207 23208 23209 Player.prototype.handleFullscreenChange_ = function handleFullscreenChange_() { 23210 if (this.isFullscreen()) { 23211 this.addClass('vjs-fullscreen'); 23212 } else { 23213 this.removeClass('vjs-fullscreen'); 23214 } 23215 }; 23216 23217 /** 23218 * native click events on the SWF aren't triggered on IE11, Win8.1RT 23219 * use stageclick events triggered from inside the SWF instead 23220 * 23221 * @private 23222 * @listens stageclick 23223 */ 23224 23225 23226 Player.prototype.handleStageClick_ = function handleStageClick_() { 23227 this.reportUserActivity(); 23228 }; 23229 23230 /** 23231 * Handle Tech Fullscreen Change 23232 * 23233 * @param {EventTarget~Event} event 23234 * the fullscreenchange event that triggered this function 23235 * 23236 * @param {Object} data 23237 * the data that was sent with the event 23238 * 23239 * @private 23240 * @listens Tech#fullscreenchange 23241 * @fires Player#fullscreenchange 23242 */ 23243 23244 23245 Player.prototype.handleTechFullscreenChange_ = function handleTechFullscreenChange_(event, data) { 23246 if (data) { 23247 this.isFullscreen(data.isFullscreen); 23248 } 23249 /** 23250 * Fired when going in and out of fullscreen. 23251 * 23252 * @event Player#fullscreenchange 23253 * @type {EventTarget~Event} 23254 */ 23255 this.trigger('fullscreenchange'); 23256 }; 23257 23258 /** 23259 * Fires when an error occurred during the loading of an audio/video. 23260 * 23261 * @private 23262 * @listens Tech#error 23263 */ 23264 23265 23266 Player.prototype.handleTechError_ = function handleTechError_() { 23267 var error = this.tech_.error(); 23268 23269 this.error(error); 23270 }; 23271 23272 /** 23273 * Retrigger the `textdata` event that was triggered by the {@link Tech}. 23274 * 23275 * @fires Player#textdata 23276 * @listens Tech#textdata 23277 * @private 23278 */ 23279 23280 23281 Player.prototype.handleTechTextData_ = function handleTechTextData_() { 23282 var data = null; 23283 23284 if (arguments.length > 1) { 23285 data = arguments[1]; 23286 } 23287 23288 /** 23289 * Fires when we get a textdata event from tech 23290 * 23291 * @event Player#textdata 23292 * @type {EventTarget~Event} 23293 */ 23294 this.trigger('textdata', data); 23295 }; 23296 23297 /** 23298 * Get object for cached values. 23299 * 23300 * @return {Object} 23301 * get the current object cache 23302 */ 23303 23304 23305 Player.prototype.getCache = function getCache() { 23306 return this.cache_; 23307 }; 23308 23309 /** 23310 * Pass values to the playback tech 23311 * 23312 * @param {string} [method] 23313 * the method to call 23314 * 23315 * @param {Object} arg 23316 * the argument to pass 23317 * 23318 * @private 23319 */ 23320 23321 23322 Player.prototype.techCall_ = function techCall_(method, arg) { 23323 // If it's not ready yet, call method when it is 23324 23325 this.ready(function () { 23326 if (method in allowedSetters) { 23327 return set$1(this.middleware_, this.tech_, method, arg); 23328 } else if (method in allowedMediators) { 23329 return mediate(this.middleware_, this.tech_, method, arg); 23330 } 23331 23332 try { 23333 if (this.tech_) { 23334 this.tech_[method](arg); 23335 } 23336 } catch (e) { 23337 log$1(e); 23338 throw e; 23339 } 23340 }, true); 23341 }; 23342 23343 /** 23344 * Get calls can't wait for the tech, and sometimes don't need to. 23345 * 23346 * @param {string} method 23347 * Tech method 23348 * 23349 * @return {Function|undefined} 23350 * the method or undefined 23351 * 23352 * @private 23353 */ 23354 23355 23356 Player.prototype.techGet_ = function techGet_(method) { 23357 if (!this.tech_ || !this.tech_.isReady_) { 23358 return; 23359 } 23360 23361 if (method in allowedGetters) { 23362 return get$1(this.middleware_, this.tech_, method); 23363 } else if (method in allowedMediators) { 23364 return mediate(this.middleware_, this.tech_, method); 23365 } 23366 23367 // Flash likes to die and reload when you hide or reposition it. 23368 // In these cases the object methods go away and we get errors. 23369 // When that happens we'll catch the errors and inform tech that it's not ready any more. 23370 try { 23371 return this.tech_[method](); 23372 } catch (e) { 23373 23374 // When building additional tech libs, an expected method may not be defined yet 23375 if (this.tech_[method] === undefined) { 23376 log$1('Video.js: ' + method + ' method not defined for ' + this.techName_ + ' playback technology.', e); 23377 throw e; 23378 } 23379 23380 // When a method isn't available on the object it throws a TypeError 23381 if (e.name === 'TypeError') { 23382 log$1('Video.js: ' + method + ' unavailable on ' + this.techName_ + ' playback technology element.', e); 23383 this.tech_.isReady_ = false; 23384 throw e; 23385 } 23386 23387 // If error unknown, just log and throw 23388 log$1(e); 23389 throw e; 23390 } 23391 }; 23392 23393 /** 23394 * Attempt to begin playback at the first opportunity. 23395 * 23396 * @return {Promise|undefined} 23397 * Returns a `Promise` only if the browser returns one and the player 23398 * is ready to begin playback. For some browsers and all non-ready 23399 * situations, this will return `undefined`. 23400 */ 23401 23402 23403 Player.prototype.play = function play() { 23404 var _this7 = this; 23405 23406 // If this is called while we have a play queued up on a loadstart, remove 23407 // that listener to avoid getting in a potentially bad state. 23408 if (this.playOnLoadstart_) { 23409 this.off('loadstart', this.playOnLoadstart_); 23410 } 23411 23412 // If the player/tech is not ready, queue up another call to `play()` for 23413 // when it is. This will loop back into this method for another attempt at 23414 // playback when the tech is ready. 23415 if (!this.isReady_) { 23416 23417 // Bail out if we're already waiting for `ready`! 23418 if (this.playWaitingForReady_) { 23419 return; 23420 } 23421 23422 this.playWaitingForReady_ = true; 23423 this.ready(function () { 23424 _this7.playWaitingForReady_ = false; 23425 silencePromise(_this7.play()); 23426 }); 23427 23428 // If the player/tech is ready and we have a source, we can attempt playback. 23429 } else if (!this.changingSrc_ && (this.src() || this.currentSrc())) { 23430 return this.techGet_('play'); 23431 23432 // If the tech is ready, but we do not have a source, we'll need to wait 23433 // for both the `ready` and a `loadstart` when the source is finally 23434 // resolved by middleware and set on the player. 23435 // 23436 // This can happen if `play()` is called while changing sources or before 23437 // one has been set on the player. 23438 } else { 23439 23440 this.playOnLoadstart_ = function () { 23441 _this7.playOnLoadstart_ = null; 23442 silencePromise(_this7.play()); 23443 }; 23444 23445 this.one('loadstart', this.playOnLoadstart_); 23446 } 23447 }; 23448 23449 /** 23450 * Pause the video playback 23451 * 23452 * @return {Player} 23453 * A reference to the player object this function was called on 23454 */ 23455 23456 23457 Player.prototype.pause = function pause() { 23458 this.techCall_('pause'); 23459 }; 23460 23461 /** 23462 * Check if the player is paused or has yet to play 23463 * 23464 * @return {boolean} 23465 * - false: if the media is currently playing 23466 * - true: if media is not currently playing 23467 */ 23468 23469 23470 Player.prototype.paused = function paused() { 23471 // The initial state of paused should be true (in Safari it's actually false) 23472 return this.techGet_('paused') === false ? false : true; 23473 }; 23474 23475 /** 23476 * Get a TimeRange object representing the current ranges of time that the user 23477 * has played. 23478 * 23479 * @return {TimeRange} 23480 * A time range object that represents all the increments of time that have 23481 * been played. 23482 */ 23483 23484 23485 Player.prototype.played = function played() { 23486 return this.techGet_('played') || createTimeRanges(0, 0); 23487 }; 23488 23489 /** 23490 * Returns whether or not the user is "scrubbing". Scrubbing is 23491 * when the user has clicked the progress bar handle and is 23492 * dragging it along the progress bar. 23493 * 23494 * @param {boolean} [isScrubbing] 23495 * whether the user is or is not scrubbing 23496 * 23497 * @return {boolean} 23498 * The value of scrubbing when getting 23499 */ 23500 23501 23502 Player.prototype.scrubbing = function scrubbing(isScrubbing) { 23503 if (typeof isScrubbing === 'undefined') { 23504 return this.scrubbing_; 23505 } 23506 this.scrubbing_ = !!isScrubbing; 23507 23508 if (isScrubbing) { 23509 this.addClass('vjs-scrubbing'); 23510 } else { 23511 this.removeClass('vjs-scrubbing'); 23512 } 23513 }; 23514 23515 /** 23516 * Get or set the current time (in seconds) 23517 * 23518 * @param {number|string} [seconds] 23519 * The time to seek to in seconds 23520 * 23521 * @return {number} 23522 * - the current time in seconds when getting 23523 */ 23524 23525 23526 Player.prototype.currentTime = function currentTime(seconds) { 23527 if (typeof seconds !== 'undefined') { 23528 if (seconds < 0) { 23529 seconds = 0; 23530 } 23531 this.techCall_('setCurrentTime', seconds); 23532 return; 23533 } 23534 23535 // cache last currentTime and return. default to 0 seconds 23536 // 23537 // Caching the currentTime is meant to prevent a massive amount of reads on the tech's 23538 // currentTime when scrubbing, but may not provide much performance benefit afterall. 23539 // Should be tested. Also something has to read the actual current time or the cache will 23540 // never get updated. 23541 this.cache_.currentTime = this.techGet_('currentTime') || 0; 23542 return this.cache_.currentTime; 23543 }; 23544 23545 /** 23546 * Normally gets the length in time of the video in seconds; 23547 * in all but the rarest use cases an argument will NOT be passed to the method 23548 * 23549 * > **NOTE**: The video must have started loading before the duration can be 23550 * known, and in the case of Flash, may not be known until the video starts 23551 * playing. 23552 * 23553 * @fires Player#durationchange 23554 * 23555 * @param {number} [seconds] 23556 * The duration of the video to set in seconds 23557 * 23558 * @return {number} 23559 * - The duration of the video in seconds when getting 23560 */ 23561 23562 23563 Player.prototype.duration = function duration(seconds) { 23564 if (seconds === undefined) { 23565 // return NaN if the duration is not known 23566 return this.cache_.duration !== undefined ? this.cache_.duration : NaN; 23567 } 23568 23569 seconds = parseFloat(seconds); 23570 23571 // Standardize on Infinity for signaling video is live 23572 if (seconds < 0) { 23573 seconds = Infinity; 23574 } 23575 23576 if (seconds !== this.cache_.duration) { 23577 // Cache the last set value for optimized scrubbing (esp. Flash) 23578 this.cache_.duration = seconds; 23579 23580 if (seconds === Infinity) { 23581 this.addClass('vjs-live'); 23582 } else { 23583 this.removeClass('vjs-live'); 23584 } 23585 /** 23586 * @event Player#durationchange 23587 * @type {EventTarget~Event} 23588 */ 23589 this.trigger('durationchange'); 23590 } 23591 }; 23592 23593 /** 23594 * Calculates how much time is left in the video. Not part 23595 * of the native video API. 23596 * 23597 * @return {number} 23598 * The time remaining in seconds 23599 */ 23600 23601 23602 Player.prototype.remainingTime = function remainingTime() { 23603 return this.duration() - this.currentTime(); 23604 }; 23605 23606 /** 23607 * A remaining time function that is intented to be used when 23608 * the time is to be displayed directly to the user. 23609 * 23610 * @return {number} 23611 * The rounded time remaining in seconds 23612 */ 23613 23614 23615 Player.prototype.remainingTimeDisplay = function remainingTimeDisplay() { 23616 return Math.floor(this.duration()) - Math.floor(this.currentTime()); 23617 }; 23618 23619 // 23620 // Kind of like an array of portions of the video that have been downloaded. 23621 23622 /** 23623 * Get a TimeRange object with an array of the times of the video 23624 * that have been downloaded. If you just want the percent of the 23625 * video that's been downloaded, use bufferedPercent. 23626 * 23627 * @see [Buffered Spec]{@link http://dev.w3.org/html5/spec/video.html#dom-media-buffered} 23628 * 23629 * @return {TimeRange} 23630 * A mock TimeRange object (following HTML spec) 23631 */ 23632 23633 23634 Player.prototype.buffered = function buffered() { 23635 var buffered = this.techGet_('buffered'); 23636 23637 if (!buffered || !buffered.length) { 23638 buffered = createTimeRanges(0, 0); 23639 } 23640 23641 return buffered; 23642 }; 23643 23644 /** 23645 * Get the percent (as a decimal) of the video that's been downloaded. 23646 * This method is not a part of the native HTML video API. 23647 * 23648 * @return {number} 23649 * A decimal between 0 and 1 representing the percent 23650 * that is buffered 0 being 0% and 1 being 100% 23651 */ 23652 23653 23654 Player.prototype.bufferedPercent = function bufferedPercent$$1() { 23655 return bufferedPercent(this.buffered(), this.duration()); 23656 }; 23657 23658 /** 23659 * Get the ending time of the last buffered time range 23660 * This is used in the progress bar to encapsulate all time ranges. 23661 * 23662 * @return {number} 23663 * The end of the last buffered time range 23664 */ 23665 23666 23667 Player.prototype.bufferedEnd = function bufferedEnd() { 23668 var buffered = this.buffered(); 23669 var duration = this.duration(); 23670 var end = buffered.end(buffered.length - 1); 23671 23672 if (end > duration) { 23673 end = duration; 23674 } 23675 23676 return end; 23677 }; 23678 23679 /** 23680 * Get or set the current volume of the media 23681 * 23682 * @param {number} [percentAsDecimal] 23683 * The new volume as a decimal percent: 23684 * - 0 is muted/0%/off 23685 * - 1.0 is 100%/full 23686 * - 0.5 is half volume or 50% 23687 * 23688 * @return {number} 23689 * The current volume as a percent when getting 23690 */ 23691 23692 23693 Player.prototype.volume = function volume(percentAsDecimal) { 23694 var vol = void 0; 23695 23696 if (percentAsDecimal !== undefined) { 23697 // Force value to between 0 and 1 23698 vol = Math.max(0, Math.min(1, parseFloat(percentAsDecimal))); 23699 this.cache_.volume = vol; 23700 this.techCall_('setVolume', vol); 23701 23702 if (vol > 0) { 23703 this.lastVolume_(vol); 23704 } 23705 23706 return; 23707 } 23708 23709 // Default to 1 when returning current volume. 23710 vol = parseFloat(this.techGet_('volume')); 23711 return isNaN(vol) ? 1 : vol; 23712 }; 23713 23714 /** 23715 * Get the current muted state, or turn mute on or off 23716 * 23717 * @param {boolean} [muted] 23718 * - true to mute 23719 * - false to unmute 23720 * 23721 * @return {boolean} 23722 * - true if mute is on and getting 23723 * - false if mute is off and getting 23724 */ 23725 23726 23727 Player.prototype.muted = function muted(_muted) { 23728 if (_muted !== undefined) { 23729 this.techCall_('setMuted', _muted); 23730 return; 23731 } 23732 return this.techGet_('muted') || false; 23733 }; 23734 23735 /** 23736 * Get the current defaultMuted state, or turn defaultMuted on or off. defaultMuted 23737 * indicates the state of muted on initial playback. 23738 * 23739 * ```js
23740 * var myPlayer = videojs('some-player-id'); 23741 * 23742 * myPlayer.src("http://www.example.com/path/to/video.mp4"); 23743 * 23744 * // get, should be false 23745 * console.log(myPlayer.defaultMuted()); 23746 * // set to true 23747 * myPlayer.defaultMuted(true); 23748 * // get should be true 23749 * console.log(myPlayer.defaultMuted()); 23750 * ``` 23751 * 23752 * @param {boolean} [defaultMuted] 23753 * - true to mute 23754 * - false to unmute 23755 * 23756 * @return {boolean|Player} 23757 * - true if defaultMuted is on and getting 23758 * - false if defaultMuted is off and getting 23759 * - A reference to the current player when setting 23760 */ 23761 23762 23763 Player.prototype.defaultMuted = function defaultMuted(_defaultMuted) { 23764 if (_defaultMuted !== undefined) { 23765 return this.techCall_('setDefaultMuted', _defaultMuted); 23766 } 23767 return this.techGet_('defaultMuted') || false; 23768 }; 23769 23770 /** 23771 * Get the last volume, or set it 23772 * 23773 * @param {number} [percentAsDecimal] 23774 * The new last volume as a decimal percent: 23775 * - 0 is muted/0%/off 23776 * - 1.0 is 100%/full 23777 * - 0.5 is half volume or 50% 23778 * 23779 * @return {number} 23780 * the current value of lastVolume as a percent when getting 23781 * 23782 * @private 23783 */ 23784 23785 23786 Player.prototype.lastVolume_ = function lastVolume_(percentAsDecimal) { 23787 if (percentAsDecimal !== undefined && percentAsDecimal !== 0) { 23788 this.cache_.lastVolume = percentAsDecimal; 23789 return; 23790 } 23791 return this.cache_.lastVolume; 23792 }; 23793 23794 /** 23795 * Check if current tech can support native fullscreen 23796 * (e.g. with built in controls like iOS, so not our flash swf) 23797 * 23798 * @return {boolean} 23799 * if native fullscreen is supported 23800 */ 23801 23802 23803 Player.prototype.supportsFullScreen = function supportsFullScreen() { 23804 return this.techGet_('supportsFullScreen') || false; 23805 }; 23806 23807 /** 23808 * Check if the player is in fullscreen mode or tell the player that it 23809 * is or is not in fullscreen mode. 23810 * 23811 * > NOTE: As of the latest HTML5 spec, isFullscreen is no longer an official 23812 * property and instead document.fullscreenElement is used. But isFullscreen is 23813 * still a valuable property for internal player workings. 23814 * 23815 * @param {boolean} [isFS] 23816 * Set the players current fullscreen state 23817 * 23818 * @return {boolean} 23819 * - true if fullscreen is on and getting 23820 * - false if fullscreen is off and getting 23821 */ 23822 23823 23824 Player.prototype.isFullscreen = function isFullscreen(isFS) { 23825 if (isFS !== undefined) { 23826 this.isFullscreen_ = !!isFS; 23827 return; 23828 } 23829 return !!this.isFullscreen_; 23830 }; 23831 23832 /** 23833 * Increase the size of the video to full screen 23834 * In some browsers, full screen is not supported natively, so it enters 23835 * "full window mode", where the video fills the browser window. 23836 * In browsers and devices that support native full screen, sometimes the 23837 * browser's default controls will be shown, and not the Video.js custom skin. 23838 * This includes most mobile devices (iOS, Android) and older versions of 23839 * Safari. 23840 * 23841 * @fires Player#fullscreenchange 23842 */ 23843 23844 23845 Player.prototype.requestFullscreen = function requestFullscreen() { 23846 var fsApi = FullscreenApi; 23847 23848 this.isFullscreen(true); 23849 23850 if (fsApi.requestFullscreen) { 23851 // the browser supports going fullscreen at the element level so we can 23852 // take the controls fullscreen as well as the video 23853 23854 // Trigger fullscreenchange event after change 23855 // We have to specifically add this each time, and remove 23856 // when canceling fullscreen. Otherwise if there's multiple 23857 // players on a page, they would all be reacting to the same fullscreen 23858 // events 23859 on(document_1, fsApi.fullscreenchange, bind(this, function documentFullscreenChange(e) { 23860 this.isFullscreen(document_1[fsApi.fullscreenElement]); 23861 23862 // If cancelling fullscreen, remove event listener. 23863 if (this.isFullscreen() === false) { 23864 off(document_1, fsApi.fullscreenchange, documentFullscreenChange); 23865 } 23866 /** 23867 * @event Player#fullscreenchange 23868 * @type {EventTarget~Event} 23869 */ 23870 this.trigger('fullscreenchange'); 23871 })); 23872 23873 this.el_[fsApi.requestFullscreen](); 23874 } else if (this.tech_.supportsFullScreen()) { 23875 // we can't take the video.js controls fullscreen but we can go fullscreen 23876 // with native controls 23877 this.techCall_('enterFullScreen'); 23878 } else { 23879 // fullscreen isn't supported so we'll just stretch the video element to 23880 // fill the viewport 23881 this.enterFullWindow(); 23882 /** 23883 * @event Player#fullscreenchange 23884 * @type {EventTarget~Event} 23885 */ 23886 this.trigger('fullscreenchange'); 23887 } 23888 }; 23889 23890 /** 23891 * Return the video to its normal size after having been in full screen mode 23892 * 23893 * @fires Player#fullscreenchange 23894 */ 23895 23896 23897 Player.prototype.exitFullscreen = function exitFullscreen() { 23898 var fsApi = FullscreenApi; 23899 23900 this.isFullscreen(false); 23901 23902 // Check for browser element fullscreen support 23903 if (fsApi.requestFullscreen) { 23904 document_1[fsApi.exitFullscreen](); 23905 } else if (this.tech_.supportsFullScreen()) { 23906 this.techCall_('exitFullScreen'); 23907 } else { 23908 this.exitFullWindow(); 23909 /** 23910 * @event Player#fullscreenchange 23911 * @type {EventTarget~Event} 23912 */ 23913 this.trigger('fullscreenchange'); 23914 } 23915 }; 23916 23917 /** 23918 * When fullscreen isn't supported we can stretch the 23919 * video container to as wide as the browser will let us. 23920 * 23921 * @fires Player#enterFullWindow 23922 */ 23923 23924 23925 Player.prototype.enterFullWindow = function enterFullWindow() { 23926 this.isFullWindow = true; 23927 23928 // Storing original doc overflow value to return to when fullscreen is off 23929 this.docOrigOverflow = document_1.documentElement.style.overflow; 23930 23931 // Add listener for esc key to exit fullscreen 23932 on(document_1, 'keydown', bind(this, this.fullWindowOnEscKey)); 23933 23934 // Hide any scroll bars 23935 document_1.documentElement.style.overflow = 'hidden'; 23936 23937 // Apply fullscreen styles 23938 addClass(document_1.body, 'vjs-full-window'); 23939 23940 /** 23941 * @event Player#enterFullWindow 23942 * @type {EventTarget~Event} 23943 */ 23944 this.trigger('enterFullWindow'); 23945 }; 23946 23947 /** 23948 * Check for call to either exit full window or 23949 * full screen on ESC key 23950 * 23951 * @param {string} event 23952 * Event to check for key press 23953 */ 23954 23955 23956 Player.prototype.fullWindowOnEscKey = function fullWindowOnEscKey(event) { 23957 if (event.keyCode === 27) { 23958 if (this.isFullscreen() === true) { 23959 this.exitFullscreen(); 23960 } else { 23961 this.exitFullWindow(); 23962 } 23963 } 23964 }; 23965 23966 /** 23967 * Exit full window 23968 * 23969 * @fires Player#exitFullWindow 23970 */ 23971 23972 23973 Player.prototype.exitFullWindow = function exitFullWindow() { 23974 this.isFullWindow = false;
vendor: 35,746 bytes, lines 23975-25158
23975 off(document_1, 'keydown', this.fullWindowOnEscKey); 23976 23977 // Unhide scroll bars. 23978 document_1.documentElement.style.overflow = this.docOrigOverflow; 23979 23980 // Remove fullscreen styles 23981 removeClass(document_1.body, 'vjs-full-window'); 23982 23983 // Resize the box, controller, and poster to original sizes 23984 // this.positionAll(); 23985 /** 23986 * @event Player#exitFullWindow 23987 * @type {EventTarget~Event} 23988 */ 23989 this.trigger('exitFullWindow'); 23990 }; 23991 23992 /** 23993 * Check whether the player can play a given mimetype 23994 * 23995 * @see https://www.w3.org/TR/2011/WD-html5-20110113/video.html#dom-navigator-canplaytype 23996 * 23997 * @param {string} type 23998 * The mimetype to check 23999 * 24000 * @return {string} 24001 * 'probably', 'maybe', or '' (empty string) 24002 */ 24003 24004 24005 Player.prototype.canPlayType = function canPlayType(type) { 24006 var can = void 0; 24007 24008 // Loop through each playback technology in the options order 24009 for (var i = 0, j = this.options_.techOrder; i < j.length; i++) { 24010 var techName = j[i]; 24011 var tech = Tech.getTech(techName); 24012 24013 // Support old behavior of techs being registered as components. 24014 // Remove once that deprecated behavior is removed. 24015 if (!tech) { 24016 tech = Component.getComponent(techName); 24017 } 24018 24019 // Check if the current tech is defined before continuing 24020 if (!tech) { 24021 log$1.error('The "' + techName + '" tech is undefined. Skipped browser support check for that tech.'); 24022 continue; 24023 } 24024 24025 // Check if the browser supports this technology 24026 if (tech.isSupported()) { 24027 can = tech.canPlayType(type); 24028 24029 if (can) { 24030 return can; 24031 } 24032 } 24033 } 24034 24035 return ''; 24036 }; 24037 24038 /** 24039 * Select source based on tech-order or source-order 24040 * Uses source-order selection if `options.sourceOrder` is truthy. Otherwise, 24041 * defaults to tech-order selection 24042 * 24043 * @param {Array} sources 24044 * The sources for a media asset 24045 * 24046 * @return {Object|boolean} 24047 * Object of source and tech order or false 24048 */ 24049 24050 24051 Player.prototype.selectSource = function selectSource(sources) { 24052 var _this8 = this; 24053 24054 // Get only the techs specified in `techOrder` that exist and are supported by the 24055 // current platform 24056 var techs = this.options_.techOrder.map(function (techName) { 24057 return [techName, Tech.getTech(techName)]; 24058 }).filter(function (_ref) { 24059 var techName = _ref[0], 24060 tech = _ref[1]; 24061 24062 // Check if the current tech is defined before continuing 24063 if (tech) { 24064 // Check if the browser supports this technology 24065 return tech.isSupported(); 24066 } 24067 24068 log$1.error('The "' + techName + '" tech is undefined. Skipped browser support check for that tech.'); 24069 return false; 24070 }); 24071 24072 // Iterate over each `innerArray` element once per `outerArray` element and execute 24073 // `tester` with both. If `tester` returns a non-falsy value, exit early and return 24074 // that value. 24075 var findFirstPassingTechSourcePair = function findFirstPassingTechSourcePair(outerArray, innerArray, tester) { 24076 var found = void 0; 24077 24078 outerArray.some(function (outerChoice) { 24079 return innerArray.some(function (innerChoice) { 24080 found = tester(outerChoice, innerChoice); 24081 24082 if (found) { 24083 return true; 24084 } 24085 }); 24086 }); 24087 24088 return found; 24089 }; 24090 24091 var foundSourceAndTech = void 0; 24092 var flip = function flip(fn) { 24093 return function (a, b) { 24094 return fn(b, a); 24095 }; 24096 }; 24097 var finder = function finder(_ref2, source) { 24098 var techName = _ref2[0], 24099 tech = _ref2[1]; 24100 24101 if (tech.canPlaySource(source, _this8.options_[techName.toLowerCase()])) { 24102 return { source: source, tech: techName }; 24103 } 24104 }; 24105 24106 // Depending on the truthiness of `options.sourceOrder`, we swap the order of techs and sources 24107 // to select from them based on their priority. 24108 if (this.options_.sourceOrder) { 24109 // Source-first ordering 24110 foundSourceAndTech = findFirstPassingTechSourcePair(sources, techs, flip(finder)); 24111 } else { 24112 // Tech-first ordering 24113 foundSourceAndTech = findFirstPassingTechSourcePair(techs, sources, finder); 24114 } 24115 24116 return foundSourceAndTech || false; 24117 }; 24118 24119 /** 24120 * Get or set the video source. 24121 * 24122 * @param {Tech~SourceObject|Tech~SourceObject[]|string} [source] 24123 * A SourceObject, an array of SourceObjects, or a string referencing 24124 * a URL to a media source. It is _highly recommended_ that an object 24125 * or array of objects is used here, so that source selection 24126 * algorithms can take the `type` into account. 24127 * 24128 * If not provided, this method acts as a getter. 24129 * 24130 * @return {string|undefined} 24131 * If the `source` argument is missing, returns the current source 24132 * URL. Otherwise, returns nothing/undefined. 24133 */ 24134 24135 24136 Player.prototype.src = function src(source) { 24137 var _this9 = this; 24138 24139 // getter usage 24140 if (typeof source === 'undefined') { 24141 return this.cache_.src || ''; 24142 } 24143 // filter out invalid sources and turn our source into 24144 // an array of source objects 24145 var sources = filterSource(source); 24146 24147 // if a source was passed in then it is invalid because 24148 // it was filtered to a zero length Array. So we have to 24149 // show an error 24150 if (!sources.length) { 24151 this.setTimeout(function () { 24152 this.error({ code: 4, message: this.localize(this.options_.notSupportedMessage) }); 24153 }, 0); 24154 return; 24155 } 24156 24157 // intial sources 24158 this.changingSrc_ = true; 24159 24160 this.cache_.sources = sources; 24161 this.updateSourceCaches_(sources[0]); 24162 24163 // middlewareSource is the source after it has been changed by middleware 24164 setSource(this, sources[0], function (middlewareSource, mws) { 24165 _this9.middleware_ = mws; 24166 24167 // since sourceSet is async we have to update the cache again after we select a source since 24168 // the source that is selected could be out of order from the cache update above this callback. 24169 _this9.cache_.sources = sources; 24170 _this9.updateSourceCaches_(middlewareSource); 24171 24172 var err = _this9.src_(middlewareSource); 24173 24174 if (err) { 24175 if (sources.length > 1) { 24176 return _this9.src(sources.slice(1)); 24177 } 24178 24179 _this9.changingSrc_ = false; 24180 24181 // We need to wrap this in a timeout to give folks a chance to add error event handlers 24182 _this9.setTimeout(function () { 24183 this.error({ code: 4, message: this.localize(this.options_.notSupportedMessage) }); 24184 }, 0); 24185 24186 // we could not find an appropriate tech, but let's still notify the delegate that this is it 24187 // this needs a better comment about why this is needed 24188 _this9.triggerReady(); 24189 24190 return; 24191 } 24192 24193 setTech(mws, _this9.tech_); 24194 }); 24195 }; 24196 24197 /** 24198 * Set the source object on the tech, returns a boolean that indicates whether 24199 * there is a tech that can play the source or not 24200 * 24201 * @param {Tech~SourceObject} source 24202 * The source object to set on the Tech 24203 * 24204 * @return {Boolean} 24205 * - True if there is no Tech to playback this source 24206 * - False otherwise 24207 * 24208 * @private 24209 */ 24210 24211 24212 Player.prototype.src_ = function src_(source) { 24213 var _this10 = this; 24214 24215 var sourceTech = this.selectSource([source]); 24216 24217 if (!sourceTech) { 24218 return true; 24219 } 24220 24221 if (!titleCaseEquals(sourceTech.tech, this.techName_)) { 24222 this.changingSrc_ = true; 24223 // load this technology with the chosen source 24224 this.loadTech_(sourceTech.tech, sourceTech.source); 24225 this.tech_.ready(function () { 24226 _this10.changingSrc_ = false; 24227 }); 24228 return false; 24229 } 24230 24231 // wait until the tech is ready to set the source 24232 // and set it synchronously if possible (#2326) 24233 this.ready(function () { 24234 24235 // The setSource tech method was added with source handlers 24236 // so older techs won't support it 24237 // We need to check the direct prototype for the case where subclasses 24238 // of the tech do not support source handlers 24239 if (this.tech_.constructor.prototype.hasOwnProperty('setSource')) { 24240 this.techCall_('setSource', source); 24241 } else { 24242 this.techCall_('src', source.src); 24243 } 24244 24245 this.changingSrc_ = false; 24246 }, true); 24247 24248 return false; 24249 }; 24250 24251 /** 24252 * Begin loading the src data. 24253 */ 24254 24255 24256 Player.prototype.load = function load() { 24257 this.techCall_('load'); 24258 }; 24259 24260 /** 24261 * Reset the player. Loads the first tech in the techOrder, 24262 * and calls `reset` on the tech`. 24263 */ 24264 24265 24266 Player.prototype.reset = function reset() { 24267 this.loadTech_(this.options_.techOrder[0], null); 24268 this.techCall_('reset'); 24269 }; 24270 24271 /** 24272 * Returns all of the current source objects. 24273 * 24274 * @return {Tech~SourceObject[]} 24275 * The current source objects 24276 */ 24277 24278 24279 Player.prototype.currentSources = function currentSources() { 24280 var source = this.currentSource(); 24281 var sources = []; 24282 24283 // assume `{}` or `{ src }` 24284 if (Object.keys(source).length !== 0) { 24285 sources.push(source); 24286 } 24287 24288 return this.cache_.sources || sources; 24289 }; 24290 24291 /** 24292 * Returns the current source object. 24293 * 24294 * @return {Tech~SourceObject} 24295 * The current source object 24296 */ 24297 24298 24299 Player.prototype.currentSource = function currentSource() { 24300 return this.cache_.source || {}; 24301 }; 24302 24303 /** 24304 * Returns the fully qualified URL of the current source value e.g. http://mysite.com/video.mp4 24305 * Can be used in conjunction with `currentType` to assist in rebuilding the current source object. 24306 * 24307 * @return {string} 24308 * The current source 24309 */ 24310 24311 24312 Player.prototype.currentSrc = function currentSrc() { 24313 return this.currentSource() && this.currentSource().src || ''; 24314 }; 24315 24316 /** 24317 * Get the current source type e.g. video/mp4 24318 * This can allow you rebuild the current source object so that you could load the same 24319 * source and tech later 24320 * 24321 * @return {string} 24322 * The source MIME type 24323 */ 24324 24325 24326 Player.prototype.currentType = function currentType() { 24327 return this.currentSource() && this.currentSource().type || ''; 24328 }; 24329 24330 /** 24331 * Get or set the preload attribute 24332 * 24333 * @param {boolean} [value] 24334 * - true means that we should preload 24335 * - false means that we should not preload 24336 * 24337 * @return {string} 24338 * The preload attribute value when getting 24339 */ 24340 24341 24342 Player.prototype.preload = function preload(value) { 24343 if (value !== undefined) { 24344 this.techCall_('setPreload', value); 24345 this.options_.preload = value; 24346 return; 24347 } 24348 return this.techGet_('preload'); 24349 }; 24350 24351 /** 24352 * Get or set the autoplay option. When this is a boolean it will 24353 * modify the attribute on the tech. When this is a string the attribute on 24354 * the tech will be removed and `Player` will handle autoplay on loadstarts. 24355 * 24356 * @param {boolean|string} [value] 24357 * - true: autoplay using the browser behavior 24358 * - false: do not autoplay 24359 * - 'play': call play() on every loadstart 24360 * - 'muted': call muted() then play() on every loadstart 24361 * - 'any': call play() on every loadstart. if that fails call muted() then play(). 24362 * - *: values other than those listed here will be set `autoplay` to true 24363 * 24364 * @return {boolean|string} 24365 * The current value of autoplay when getting 24366 */ 24367 24368 24369 Player.prototype.autoplay = function autoplay(value) { 24370 // getter usage 24371 if (value === undefined) { 24372 return this.options_.autoplay || false; 24373 } 24374 24375 var techAutoplay = void 0; 24376 24377 // if the value is a valid string set it to that 24378 if (typeof value === 'string' && /(any|play|muted)/.test(value)) { 24379 this.options_.autoplay = value; 24380 this.manualAutoplay_(value); 24381 techAutoplay = false; 24382 24383 // any falsy value sets autoplay to false in the browser, 24384 // lets do the same 24385 } else if (!value) { 24386 this.options_.autoplay = false; 24387 24388 // any other value (ie truthy) sets autoplay to true 24389 } else { 24390 this.options_.autoplay = true; 24391 } 24392 24393 techAutoplay = techAutoplay || this.options_.autoplay; 24394 24395 // if we don't have a tech then we do not queue up 24396 // a setAutoplay call on tech ready. We do this because the 24397 // autoplay option will be passed in the constructor and we 24398 // do not need to set it twice 24399 if (this.tech_) { 24400 this.techCall_('setAutoplay', techAutoplay); 24401 } 24402 }; 24403 24404 /** 24405 * Set or unset the playsinline attribute. 24406 * Playsinline tells the browser that non-fullscreen playback is preferred. 24407 * 24408 * @param {boolean} [value] 24409 * - true means that we should try to play inline by default 24410 * - false means that we should use the browser's default playback mode, 24411 * which in most cases is inline. iOS Safari is a notable exception 24412 * and plays fullscreen by default. 24413 * 24414 * @return {string|Player} 24415 * - the current value of playsinline 24416 * - the player when setting 24417 * 24418 * @see [Spec]{@link https://html.spec.whatwg.org/#attr-video-playsinline} 24419 */ 24420 24421 24422 Player.prototype.playsinline = function playsinline(value) { 24423 if (value !== undefined) { 24424 this.techCall_('setPlaysinline', value); 24425 this.options_.playsinline = value; 24426 return this; 24427 } 24428 return this.techGet_('playsinline'); 24429 }; 24430 24431 /** 24432 * Get or set the loop attribute on the video element. 24433 * 24434 * @param {boolean} [value] 24435 * - true means that we should loop the video 24436 * - false means that we should not loop the video 24437 * 24438 * @return {string} 24439 * The current value of loop when getting 24440 */ 24441 24442 24443 Player.prototype.loop = function loop(value) { 24444 if (value !== undefined) { 24445 this.techCall_('setLoop', value); 24446 this.options_.loop = value; 24447 return; 24448 } 24449 return this.techGet_('loop'); 24450 }; 24451 24452 /** 24453 * Get or set the poster image source url 24454 * 24455 * @fires Player#posterchange 24456 * 24457 * @param {string} [src] 24458 * Poster image source URL 24459 * 24460 * @return {string} 24461 * The current value of poster when getting 24462 */ 24463 24464 24465 Player.prototype.poster = function poster(src) { 24466 if (src === undefined) { 24467 return this.poster_; 24468 } 24469 24470 // The correct way to remove a poster is to set as an empty string 24471 // other falsey values will throw errors 24472 if (!src) { 24473 src = ''; 24474 } 24475 24476 if (src === this.poster_) { 24477 return; 24478 } 24479 24480 // update the internal poster variable 24481 this.poster_ = src; 24482 24483 // update the tech's poster 24484 this.techCall_('setPoster', src); 24485 24486 this.isPosterFromTech_ = false; 24487 24488 // alert components that the poster has been set 24489 /** 24490 * This event fires when the poster image is changed on the player. 24491 * 24492 * @event Player#posterchange 24493 * @type {EventTarget~Event} 24494 */ 24495 this.trigger('posterchange'); 24496 }; 24497 24498 /** 24499 * Some techs (e.g. YouTube) can provide a poster source in an 24500 * asynchronous way. We want the poster component to use this 24501 * poster source so that it covers up the tech's controls. 24502 * (YouTube's play button). However we only want to use this 24503 * source if the player user hasn't set a poster through 24504 * the normal APIs. 24505 * 24506 * @fires Player#posterchange 24507 * @listens Tech#posterchange 24508 * @private 24509 */ 24510 24511 24512 Player.prototype.handleTechPosterChange_ = function handleTechPosterChange_() { 24513 if ((!this.poster_ || this.options_.techCanOverridePoster) && this.tech_ && this.tech_.poster) { 24514 var newPoster = this.tech_.poster() || ''; 24515 24516 if (newPoster !== this.poster_) { 24517 this.poster_ = newPoster; 24518 this.isPosterFromTech_ = true; 24519 24520 // Let components know the poster has changed 24521 this.trigger('posterchange'); 24522 } 24523 } 24524 }; 24525 24526 /** 24527 * Get or set whether or not the controls are showing. 24528 * 24529 * @fires Player#controlsenabled 24530 * 24531 * @param {boolean} [bool] 24532 * - true to turn controls on 24533 * - false to turn controls off 24534 * 24535 * @return {boolean} 24536 * The current value of controls when getting 24537 */ 24538 24539 24540 Player.prototype.controls = function controls(bool) { 24541 if (bool === undefined) { 24542 return !!this.controls_; 24543 } 24544 24545 bool = !!bool; 24546 24547 // Don't trigger a change event unless it actually changed 24548 if (this.controls_ === bool) { 24549 return; 24550 } 24551 24552 this.controls_ = bool; 24553 24554 if (this.usingNativeControls()) { 24555 this.techCall_('setControls', bool); 24556 } 24557 24558 if (this.controls_) { 24559 this.removeClass('vjs-controls-disabled'); 24560 this.addClass('vjs-controls-enabled'); 24561 /** 24562 * @event Player#controlsenabled 24563 * @type {EventTarget~Event} 24564 */ 24565 this.trigger('controlsenabled'); 24566 if (!this.usingNativeControls()) { 24567 this.addTechControlsListeners_(); 24568 } 24569 } else { 24570 this.removeClass('vjs-controls-enabled'); 24571 this.addClass('vjs-controls-disabled'); 24572 /** 24573 * @event Player#controlsdisabled 24574 * @type {EventTarget~Event} 24575 */ 24576 this.trigger('controlsdisabled'); 24577 if (!this.usingNativeControls()) { 24578 this.removeTechControlsListeners_(); 24579 } 24580 } 24581 }; 24582 24583 /** 24584 * Toggle native controls on/off. Native controls are the controls built into 24585 * devices (e.g. default iPhone controls), Flash, or other techs 24586 * (e.g. Vimeo Controls) 24587 * **This should only be set by the current tech, because only the tech knows 24588 * if it can support native controls** 24589 * 24590 * @fires Player#usingnativecontrols 24591 * @fires Player#usingcustomcontrols 24592 * 24593 * @param {boolean} [bool] 24594 * - true to turn native controls on 24595 * - false to turn native controls off 24596 * 24597 * @return {boolean} 24598 * The current value of native controls when getting 24599 */ 24600 24601 24602 Player.prototype.usingNativeControls = function usingNativeControls(bool) { 24603 if (bool === undefined) { 24604 return !!this.usingNativeControls_; 24605 } 24606 24607 bool = !!bool; 24608 24609 // Don't trigger a change event unless it actually changed 24610 if (this.usingNativeControls_ === bool) { 24611 return; 24612 } 24613 24614 this.usingNativeControls_ = bool; 24615 24616 if (this.usingNativeControls_) { 24617 this.addClass('vjs-using-native-controls'); 24618 24619 /** 24620 * player is using the native device controls 24621 * 24622 * @event Player#usingnativecontrols 24623 * @type {EventTarget~Event} 24624 */ 24625 this.trigger('usingnativecontrols'); 24626 } else { 24627 this.removeClass('vjs-using-native-controls'); 24628 24629 /** 24630 * player is using the custom HTML controls 24631 * 24632 * @event Player#usingcustomcontrols 24633 * @type {EventTarget~Event} 24634 */ 24635 this.trigger('usingcustomcontrols'); 24636 } 24637 }; 24638 24639 /** 24640 * Set or get the current MediaError 24641 * 24642 * @fires Player#error 24643 * 24644 * @param {MediaError|string|number} [err] 24645 * A MediaError or a string/number to be turned 24646 * into a MediaError 24647 * 24648 * @return {MediaError|null} 24649 * The current MediaError when getting (or null) 24650 */ 24651 24652 24653 Player.prototype.error = function error(err) { 24654 if (err === undefined) { 24655 return this.error_ || null; 24656 } 24657 24658 // restoring to default 24659 if (err === null) { 24660 this.error_ = err; 24661 this.removeClass('vjs-error'); 24662 if (this.errorDisplay) { 24663 this.errorDisplay.close(); 24664 } 24665 return; 24666 } 24667 24668 this.error_ = new MediaError(err); 24669 24670 // add the vjs-error classname to the player 24671 this.addClass('vjs-error'); 24672 24673 // log the name of the error type and any message 24674 // IE11 logs "[object object]" and required you to expand message to see error object 24675 log$1.error('(CODE:' + this.error_.code + ' ' + MediaError.errorTypes[this.error_.code] + ')', this.error_.message, this.error_); 24676 24677 /** 24678 * @event Player#error 24679 * @type {EventTarget~Event} 24680 */ 24681 this.trigger('error'); 24682 24683 return; 24684 }; 24685 24686 /** 24687 * Report user activity 24688 * 24689 * @param {Object} event 24690 * Event object 24691 */ 24692 24693 24694 Player.prototype.reportUserActivity = function reportUserActivity(event) { 24695 this.userActivity_ = true; 24696 }; 24697 24698 /** 24699 * Get/set if user is active 24700 * 24701 * @fires Player#useractive 24702 * @fires Player#userinactive 24703 * 24704 * @param {boolean} [bool] 24705 * - true if the user is active 24706 * - false if the user is inactive 24707 * 24708 * @return {boolean} 24709 * The current value of userActive when getting 24710 */ 24711 24712 24713 Player.prototype.userActive = function userActive(bool) { 24714 if (bool === undefined) { 24715 return this.userActive_; 24716 } 24717 24718 bool = !!bool; 24719 24720 if (bool === this.userActive_) { 24721 return; 24722 } 24723 24724 this.userActive_ = bool; 24725 24726 if (this.userActive_) { 24727 this.userActivity_ = true; 24728 this.removeClass('vjs-user-inactive'); 24729 this.addClass('vjs-user-active'); 24730 /** 24731 * @event Player#useractive 24732 * @type {EventTarget~Event} 24733 */ 24734 this.trigger('useractive'); 24735 return; 24736 } 24737 24738 // Chrome/Safari/IE have bugs where when you change the cursor it can 24739 // trigger a mousemove event. This causes an issue when you're hiding 24740 // the cursor when the user is inactive, and a mousemove signals user 24741 // activity. Making it impossible to go into inactive mode. Specifically 24742 // this happens in fullscreen when we really need to hide the cursor. 24743 // 24744 // When this gets resolved in ALL browsers it can be removed 24745 // https://code.google.com/p/chromium/issues/detail?id=103041 24746 if (this.tech_) { 24747 this.tech_.one('mousemove', function (e) { 24748 e.stopPropagation(); 24749 e.preventDefault(); 24750 }); 24751 } 24752 24753 this.userActivity_ = false; 24754 this.removeClass('vjs-user-active'); 24755 this.addClass('vjs-user-inactive'); 24756 /** 24757 * @event Player#userinactive 24758 * @type {EventTarget~Event} 24759 */ 24760 this.trigger('userinactive'); 24761 }; 24762 24763 /** 24764 * Listen for user activity based on timeout value 24765 * 24766 * @private 24767 */ 24768 24769 24770 Player.prototype.listenForUserActivity_ = function listenForUserActivity_() { 24771 var mouseInProgress = void 0; 24772 var lastMoveX = void 0; 24773 var lastMoveY = void 0; 24774 var handleActivity = bind(this, this.reportUserActivity); 24775 24776 var handleMouseMove = function handleMouseMove(e) { 24777 // #1068 - Prevent mousemove spamming 24778 // Chrome Bug: https://code.google.com/p/chromium/issues/detail?id=366970 24779 if (e.screenX !== lastMoveX || e.screenY !== lastMoveY) { 24780 lastMoveX = e.screenX; 24781 lastMoveY = e.screenY; 24782 handleActivity(); 24783 } 24784 }; 24785 24786 var handleMouseDown = function handleMouseDown() { 24787 handleActivity(); 24788 // For as long as the they are touching the device or have their mouse down, 24789 // we consider them active even if they're not moving their finger or mouse. 24790 // So we want to continue to update that they are active 24791 this.clearInterval(mouseInProgress); 24792 // Setting userActivity=true now and setting the interval to the same time 24793 // as the activityCheck interval (250) should ensure we never miss the 24794 // next activityCheck 24795 mouseInProgress = this.setInterval(handleActivity, 250); 24796 }; 24797 24798 var handleMouseUp = function handleMouseUp(event) { 24799 handleActivity(); 24800 // Stop the interval that maintains activity if the mouse/touch is down 24801 this.clearInterval(mouseInProgress); 24802 }; 24803 24804 // Any mouse movement will be considered user activity 24805 this.on('mousedown', handleMouseDown); 24806 this.on('mousemove', handleMouseMove); 24807 this.on('mouseup', handleMouseUp); 24808 24809 // Listen for keyboard navigation 24810 // Shouldn't need to use inProgress interval because of key repeat 24811 this.on('keydown', handleActivity); 24812 this.on('keyup', handleActivity); 24813 24814 // Run an interval every 250 milliseconds instead of stuffing everything into 24815 // the mousemove/touchmove function itself, to prevent performance degradation. 24816 // `this.reportUserActivity` simply sets this.userActivity_ to true, which 24817 // then gets picked up by this loop 24818 // http://ejohn.org/blog/learning-from-twitter/ 24819 var inactivityTimeout = void 0; 24820 24821 this.setInterval(function () { 24822 // Check to see if mouse/touch activity has happened 24823 if (!this.userActivity_) { 24824 return; 24825 } 24826 24827 // Reset the activity tracker 24828 this.userActivity_ = false; 24829 24830 // If the user state was inactive, set the state to active 24831 this.userActive(true); 24832 24833 // Clear any existing inactivity timeout to start the timer over 24834 this.clearTimeout(inactivityTimeout); 24835 24836 var timeout = this.options_.inactivityTimeout; 24837 24838 if (timeout <= 0) { 24839 return; 24840 } 24841 24842 // In <timeout> milliseconds, if no more activity has occurred the 24843 // user will be considered inactive 24844 inactivityTimeout = this.setTimeout(function () { 24845 // Protect against the case where the inactivityTimeout can trigger just 24846 // before the next user activity is picked up by the activity check loop 24847 // causing a flicker 24848 if (!this.userActivity_) { 24849 this.userActive(false); 24850 } 24851 }, timeout); 24852 }, 250); 24853 }; 24854 24855 /** 24856 * Gets or sets the current playback rate. A playback rate of 24857 * 1.0 represents normal speed and 0.5 would indicate half-speed 24858 * playback, for instance. 24859 * 24860 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-playbackrate 24861 * 24862 * @param {number} [rate] 24863 * New playback rate to set. 24864 * 24865 * @return {number} 24866 * The current playback rate when getting or 1.0 24867 */ 24868 24869 24870 Player.prototype.playbackRate = function playbackRate(rate) { 24871 if (rate !== undefined) { 24872 // NOTE: this.cache_.lastPlaybackRate is set from the tech handler 24873 // that is registered above 24874 this.techCall_('setPlaybackRate', rate); 24875 return; 24876 } 24877 24878 if (this.tech_ && this.tech_.featuresPlaybackRate) { 24879 return this.cache_.lastPlaybackRate || this.techGet_('playbackRate'); 24880 } 24881 return 1.0; 24882 }; 24883 24884 /** 24885 * Gets or sets the current default playback rate. A default playback rate of 24886 * 1.0 represents normal speed and 0.5 would indicate half-speed playback, for instance. 24887 * defaultPlaybackRate will only represent what the initial playbackRate of a video was, not 24888 * not the current playbackRate. 24889 * 24890 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-defaultplaybackrate 24891 * 24892 * @param {number} [rate] 24893 * New default playback rate to set. 24894 * 24895 * @return {number|Player} 24896 * - The default playback rate when getting or 1.0 24897 * - the player when setting 24898 */ 24899 24900 24901 Player.prototype.defaultPlaybackRate = function defaultPlaybackRate(rate) { 24902 if (rate !== undefined) { 24903 return this.techCall_('setDefaultPlaybackRate', rate); 24904 } 24905 24906 if (this.tech_ && this.tech_.featuresPlaybackRate) { 24907 return this.techGet_('defaultPlaybackRate'); 24908 } 24909 return 1.0; 24910 }; 24911 24912 /** 24913 * Gets or sets the audio flag 24914 * 24915 * @param {boolean} bool 24916 * - true signals that this is an audio player 24917 * - false signals that this is not an audio player 24918 * 24919 * @return {boolean} 24920 * The current value of isAudio when getting 24921 */ 24922 24923 24924 Player.prototype.isAudio = function isAudio(bool) { 24925 if (bool !== undefined) { 24926 this.isAudio_ = !!bool; 24927 return; 24928 } 24929 24930 return !!this.isAudio_; 24931 }; 24932 24933 /** 24934 * A helper method for adding a {@link TextTrack} to our 24935 * {@link TextTrackList}. 24936 * 24937 * In addition to the W3C settings we allow adding additional info through options. 24938 * 24939 * @see http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-addtexttrack 24940 * 24941 * @param {string} [kind] 24942 * the kind of TextTrack you are adding 24943 * 24944 * @param {string} [label] 24945 * the label to give the TextTrack label 24946 * 24947 * @param {string} [language] 24948 * the language to set on the TextTrack 24949 * 24950 * @return {TextTrack|undefined} 24951 * the TextTrack that was added or undefined 24952 * if there is no tech 24953 */ 24954 24955 24956 Player.prototype.addTextTrack = function addTextTrack(kind, label, language) { 24957 if (this.tech_) { 24958 return this.tech_.addTextTrack(kind, label, language); 24959 } 24960 }; 24961 24962 /** 24963 * Create a remote {@link TextTrack} and an {@link HTMLTrackElement}. It will 24964 * automatically removed from the video element whenever the source changes, unless 24965 * manualCleanup is set to false. 24966 * 24967 * @param {Object} options 24968 * Options to pass to {@link HTMLTrackElement} during creation. See 24969 * {@link HTMLTrackElement} for object properties that you should use. 24970 * 24971 * @param {boolean} [manualCleanup=true] if set to false, the TextTrack will be 24972 * 24973 * @return {HtmlTrackElement} 24974 * the HTMLTrackElement that was created and added 24975 * to the HtmlTrackElementList and the remote 24976 * TextTrackList 24977 * 24978 * @deprecated The default value of the "manualCleanup" parameter will default 24979 * to "false" in upcoming versions of Video.js 24980 */ 24981 24982 24983 Player.prototype.addRemoteTextTrack = function addRemoteTextTrack(options, manualCleanup) { 24984 if (this.tech_) { 24985 return this.tech_.addRemoteTextTrack(options, manualCleanup); 24986 } 24987 }; 24988 24989 /** 24990 * Remove a remote {@link TextTrack} from the respective 24991 * {@link TextTrackList} and {@link HtmlTrackElementList}. 24992 * 24993 * @param {Object} track 24994 * Remote {@link TextTrack} to remove 24995 * 24996 * @return {undefined} 24997 * does not return anything 24998 */ 24999 25000 25001 Player.prototype.removeRemoteTextTrack = function removeRemoteTextTrack() { 25002 var _ref3 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, 25003 _ref3$track = _ref3.track, 25004 track = _ref3$track === undefined ? arguments[0] : _ref3$track; 25005 25006 // destructure the input into an object with a track argument, defaulting to arguments[0] 25007 // default the whole argument to an empty object if nothing was passed in 25008 25009 if (this.tech_) { 25010 return this.tech_.removeRemoteTextTrack(track); 25011 } 25012 }; 25013 25014 /** 25015 * Gets available media playback quality metrics as specified by the W3C's Media 25016 * Playback Quality API. 25017 * 25018 * @see [Spec]{@link https://wicg.github.io/media-playback-quality} 25019 * 25020 * @return {Object|undefined} 25021 * An object with supported media playback quality metrics or undefined if there 25022 * is no tech or the tech does not support it. 25023 */ 25024 25025 25026 Player.prototype.getVideoPlaybackQuality = function getVideoPlaybackQuality() { 25027 return this.techGet_('getVideoPlaybackQuality'); 25028 }; 25029 25030 /** 25031 * Get video width 25032 * 25033 * @return {number} 25034 * current video width 25035 */ 25036 25037 25038 Player.prototype.videoWidth = function videoWidth() { 25039 return this.tech_ && this.tech_.videoWidth && this.tech_.videoWidth() || 0; 25040 }; 25041 25042 /** 25043 * Get video height 25044 * 25045 * @return {number} 25046 * current video height 25047 */ 25048 25049 25050 Player.prototype.videoHeight = function videoHeight() { 25051 return this.tech_ && this.tech_.videoHeight && this.tech_.videoHeight() || 0; 25052 }; 25053 25054 /** 25055 * The player's language code 25056 * NOTE: The language should be set in the player options if you want the 25057 * the controls to be built with a specific language. Changing the language 25058 * later will not update controls text. 25059 * 25060 * @param {string} [code] 25061 * the language code to set the player to 25062 * 25063 * @return {string} 25064 * The current language code when getting 25065 */ 25066 25067 25068 Player.prototype.language = function language(code) { 25069 if (code === undefined) { 25070 return this.language_; 25071 } 25072 25073 this.language_ = String(code).toLowerCase(); 25074 }; 25075 25076 /** 25077 * Get the player's language dictionary 25078 * Merge every time, because a newly added plugin might call videojs.addLanguage() at any time 25079 * Languages specified directly in the player options have precedence 25080 * 25081 * @return {Array} 25082 * An array of of supported languages 25083 */ 25084 25085 25086 Player.prototype.languages = function languages() { 25087 return mergeOptions(Player.prototype.options_.languages, this.languages_); 25088 }; 25089 25090 /** 25091 * returns a JavaScript object reperesenting the current track 25092 * information. **DOES not return it as JSON** 25093 * 25094 * @return {Object} 25095 * Object representing the current of track info 25096 */ 25097 25098 25099 Player.prototype.toJSON = function toJSON() { 25100 var options = mergeOptions(this.options_); 25101 var tracks = options.tracks; 25102 25103 options.tracks = []; 25104 25105 for (var i = 0; i < tracks.length; i++) { 25106 var track = tracks[i]; 25107 25108 // deep merge tracks and null out player so no circular references 25109 track = mergeOptions(track); 25110 track.player = undefined; 25111 options.tracks[i] = track; 25112 } 25113 25114 return options; 25115 }; 25116 25117 /** 25118 * Creates a simple modal dialog (an instance of the {@link ModalDialog} 25119 * component) that immediately overlays the player with arbitrary 25120 * content and removes itself when closed. 25121 * 25122 * @param {string|Function|Element|Array|null} content 25123 * Same as {@link ModalDialog#content}'s param of the same name. 25124 * The most straight-forward usage is to provide a string or DOM 25125 * element. 25126 * 25127 * @param {Object} [options] 25128 * Extra options which will be passed on to the {@link ModalDialog}. 25129 * 25130 * @return {ModalDialog} 25131 * the {@link ModalDialog} that was created 25132 */ 25133 25134 25135 Player.prototype.createModal = function createModal(content, options) { 25136 var _this11 = this; 25137 25138 options = options || {}; 25139 options.content = content || ''; 25140 25141 var modal = new ModalDialog(this, options); 25142 25143 this.addChild(modal); 25144 modal.on('dispose', function () { 25145 _this11.removeChild(modal); 25146 }); 25147 25148 modal.open(); 25149 return modal; 25150 }; 25151 25152 /** 25153 * Gets tag settings 25154 * 25155 * @param {Element} tag 25156 * The player tag 25157 * 25158 * @return {Object}
vendor: 4,580 bytes, lines 25159-25329
25159 * An object containing all of the settings 25160 * for a player tag 25161 */ 25162 25163 25164 Player.getTagSettings = function getTagSettings(tag) { 25165 var baseOptions = { 25166 sources: [], 25167 tracks: [] 25168 }; 25169 25170 var tagOptions = getAttributes(tag); 25171 var dataSetup = tagOptions['data-setup']; 25172 25173 if (hasClass(tag, 'vjs-fluid')) { 25174 tagOptions.fluid = true; 25175 } 25176 25177 // Check if data-setup attr exists. 25178 if (dataSetup !== null) { 25179 // Parse options JSON 25180 // If empty string, make it a parsable json object. 25181 var _safeParseTuple = tuple(dataSetup || '{}'), 25182 err = _safeParseTuple[0], 25183 data = _safeParseTuple[1]; 25184 25185 if (err) { 25186 log$1.error(err); 25187 } 25188 assign(tagOptions, data); 25189 } 25190 25191 assign(baseOptions, tagOptions); 25192 25193 // Get tag children settings 25194 if (tag.hasChildNodes()) { 25195 var children = tag.childNodes; 25196 25197 for (var i = 0, j = children.length; i < j; i++) { 25198 var child = children[i]; 25199 // Change case needed: http://ejohn.org/blog/nodename-case-sensitivity/ 25200 var childName = child.nodeName.toLowerCase(); 25201 25202 if (childName === 'source') { 25203 baseOptions.sources.push(getAttributes(child)); 25204 } else if (childName === 'track') { 25205 baseOptions.tracks.push(getAttributes(child)); 25206 } 25207 } 25208 } 25209 25210 return baseOptions; 25211 }; 25212 25213 /** 25214 * Determine whether or not flexbox is supported 25215 * 25216 * @return {boolean} 25217 * - true if flexbox is supported 25218 * - false if flexbox is not supported 25219 */ 25220 25221 25222 Player.prototype.flexNotSupported_ = function flexNotSupported_() { 25223 var elem = document_1.createElement('i'); 25224 25225 // Note: We don't actually use flexBasis (or flexOrder), but it's one of the more 25226 // common flex features that we can rely on when checking for flex support. 25227 return !('flexBasis' in elem.style || 'webkitFlexBasis' in elem.style || 'mozFlexBasis' in elem.style || 'msFlexBasis' in elem.style || 25228 // IE10-specific (2012 flex spec), available for completeness 25229 'msFlexOrder' in elem.style); 25230 }; 25231 25232 return Player; 25233 }(Component); 25234 25235 /** 25236 * Get the {@link VideoTrackList} 25237 * @link https://html.spec.whatwg.org/multipage/embedded-content.html#videotracklist 25238 * 25239 * @return {VideoTrackList} 25240 * the current video track list 25241 * 25242 * @method Player.prototype.videoTracks 25243 */ 25244 25245 /** 25246 * Get the {@link AudioTrackList} 25247 * @link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotracklist 25248 * 25249 * @return {AudioTrackList} 25250 * the current audio track list 25251 * 25252 * @method Player.prototype.audioTracks 25253 */ 25254 25255 /** 25256 * Get the {@link TextTrackList} 25257 * 25258 * @link http://www.w3.org/html/wg/drafts/html/master/embedded-content-0.html#dom-media-texttracks 25259 * 25260 * @return {TextTrackList} 25261 * the current text track list 25262 * 25263 * @method Player.prototype.textTracks 25264 */ 25265 25266 /** 25267 * Get the remote {@link TextTrackList} 25268 * 25269 * @return {TextTrackList} 25270 * The current remote text track list 25271 * 25272 * @method Player.prototype.remoteTextTracks 25273 */ 25274 25275 /** 25276 * Get the remote {@link HtmlTrackElementList} tracks. 25277 * 25278 * @return {HtmlTrackElementList} 25279 * The current remote text track element list 25280 * 25281 * @method Player.prototype.remoteTextTrackEls 25282 */ 25283 25284 ALL.names.forEach(function (name$$1) { 25285 var props = ALL[name$$1]; 25286 25287 Player.prototype[props.getterName] = function () { 25288 if (this.tech_) { 25289 return this.tech_[props.getterName](); 25290 } 25291 25292 // if we have not yet loadTech_, we create {video,audio,text}Tracks_ 25293 // these will be passed to the tech during loading 25294 this[props.privateName] = this[props.privateName] || new props.ListClass(); 25295 return this[props.privateName]; 25296 }; 25297 }); 25298 25299 /** 25300 * Global player list 25301 * 25302 * @type {Object} 25303 */ 25304 Player.players = {}; 25305 25306 var navigator = window_1.navigator; 25307 25308 /* 25309 * Player instance options, surfaced using options 25310 * options = Player.prototype.options_ 25311 * Make changes in options, not here. 25312 * 25313 * @type {Object} 25314 * @private 25315 */ 25316 Player.prototype.options_ = { 25317 // Default order of fallback technology 25318 techOrder: Tech.defaultTechOrder_, 25319 25320 html5: {}, 25321 flash: {}, 25322 25323 // default inactivity timeout 25324 inactivityTimeout: 2000, 25325 25326 // default playback rates 25327 playbackRates: [], 25328 // Add playback rate selection by adding rates 25329 // 'playbackRates': [0.5, 1, 1.5, 2],
vendor: 34,162 bytes, lines 25330-26521
25330 25331 // Included control sets 25332 children: ['mediaLoader', 'posterImage', 'textTrackDisplay', 'loadingSpinner', 'bigPlayButton', 'controlBar', 'errorDisplay', 'textTrackSettings', 'resizeManager'], 25333 25334 language: navigator && (navigator.languages && navigator.languages[0] || navigator.userLanguage || navigator.language) || 'en', 25335 25336 // locales and their language translations 25337 languages: {}, 25338 25339 // Default message to show when a video cannot be played. 25340 notSupportedMessage: 'No compatible source was found for this media.' 25341 }; 25342 25343 [ 25344 /** 25345 * Returns whether or not the player is in the "ended" state. 25346 * 25347 * @return {Boolean} True if the player is in the ended state, false if not. 25348 * @method Player#ended 25349 */ 25350 'ended', 25351 /** 25352 * Returns whether or not the player is in the "seeking" state. 25353 * 25354 * @return {Boolean} True if the player is in the seeking state, false if not. 25355 * @method Player#seeking 25356 */ 25357 'seeking', 25358 /** 25359 * Returns the TimeRanges of the media that are currently available 25360 * for seeking to. 25361 * 25362 * @return {TimeRanges} the seekable intervals of the media timeline 25363 * @method Player#seekable 25364 */ 25365 'seekable', 25366 /** 25367 * Returns the current state of network activity for the element, from 25368 * the codes in the list below. 25369 * - NETWORK_EMPTY (numeric value 0) 25370 * The element has not yet been initialised. All attributes are in 25371 * their initial states. 25372 * - NETWORK_IDLE (numeric value 1) 25373 * The element's resource selection algorithm is active and has 25374 * selected a resource, but it is not actually using the network at 25375 * this time. 25376 * - NETWORK_LOADING (numeric value 2) 25377 * The user agent is actively trying to download data. 25378 * - NETWORK_NO_SOURCE (numeric value 3) 25379 * The element's resource selection algorithm is active, but it has 25380 * not yet found a resource to use. 25381 * 25382 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#network-states 25383 * @return {number} the current network activity state 25384 * @method Player#networkState 25385 */ 25386 'networkState', 25387 /** 25388 * Returns a value that expresses the current state of the element 25389 * with respect to rendering the current playback position, from the 25390 * codes in the list below. 25391 * - HAVE_NOTHING (numeric value 0) 25392 * No information regarding the media resource is available. 25393 * - HAVE_METADATA (numeric value 1) 25394 * Enough of the resource has been obtained that the duration of the 25395 * resource is available. 25396 * - HAVE_CURRENT_DATA (numeric value 2) 25397 * Data for the immediate current playback position is available. 25398 * - HAVE_FUTURE_DATA (numeric value 3) 25399 * Data for the immediate current playback position is available, as 25400 * well as enough data for the user agent to advance the current 25401 * playback position in the direction of playback. 25402 * - HAVE_ENOUGH_DATA (numeric value 4) 25403 * The user agent estimates that enough data is available for 25404 * playback to proceed uninterrupted. 25405 * 25406 * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-media-readystate 25407 * @return {number} the current playback rendering state 25408 * @method Player#readyState 25409 */ 25410 'readyState'].forEach(function (fn) { 25411 Player.prototype[fn] = function () { 25412 return this.techGet_(fn); 25413 }; 25414 }); 25415 25416 TECH_EVENTS_RETRIGGER.forEach(function (event) { 25417 Player.prototype['handleTech' + toTitleCase(event) + '_'] = function () { 25418 return this.trigger(event); 25419 }; 25420 }); 25421 25422 /** 25423 * Fired when the player has initial duration and dimension information 25424 * 25425 * @event Player#loadedmetadata 25426 * @type {EventTarget~Event} 25427 */ 25428 25429 /** 25430 * Fired when the player has downloaded data at the current playback position 25431 * 25432 * @event Player#loadeddata 25433 * @type {EventTarget~Event} 25434 */ 25435 25436 /** 25437 * Fired when the current playback position has changed * 25438 * During playback this is fired every 15-250 milliseconds, depending on the 25439 * playback technology in use. 25440 * 25441 * @event Player#timeupdate 25442 * @type {EventTarget~Event} 25443 */ 25444 25445 /** 25446 * Fired when the volume changes 25447 * 25448 * @event Player#volumechange 25449 * @type {EventTarget~Event} 25450 */ 25451 25452 /** 25453 * Reports whether or not a player has a plugin available. 25454 * 25455 * This does not report whether or not the plugin has ever been initialized 25456 * on this player. For that, [usingPlugin]{@link Player#usingPlugin}. 25457 * 25458 * @method Player#hasPlugin 25459 * @param {string} name 25460 * The name of a plugin. 25461 * 25462 * @return {boolean} 25463 * Whether or not this player has the requested plugin available. 25464 */ 25465 25466 /** 25467 * Reports whether or not a player is using a plugin by name. 25468 * 25469 * For basic plugins, this only reports whether the plugin has _ever_ been 25470 * initialized on this player. 25471 * 25472 * @method Player#usingPlugin 25473 * @param {string} name 25474 * The name of a plugin. 25475 * 25476 * @return {boolean} 25477 * Whether or not this player is using the requested plugin. 25478 */ 25479 25480 Component.registerComponent('Player', Player); 25481 25482 /** 25483 * @file plugin.js 25484 */ 25485 25486 /** 25487 * The base plugin name. 25488 * 25489 * @private 25490 * @constant 25491 * @type {string} 25492 */ 25493 var BASE_PLUGIN_NAME = 'plugin'; 25494 25495 /** 25496 * The key on which a player's active plugins cache is stored. 25497 * 25498 * @private 25499 * @constant 25500 * @type {string} 25501 */ 25502 var PLUGIN_CACHE_KEY = 'activePlugins_'; 25503 25504 /** 25505 * Stores registered plugins in a private space. 25506 * 25507 * @private 25508 * @type {Object} 25509 */ 25510 var pluginStorage = {}; 25511 25512 /** 25513 * Reports whether or not a plugin has been registered. 25514 * 25515 * @private 25516 * @param {string} name 25517 * The name of a plugin. 25518 * 25519 * @returns {boolean} 25520 * Whether or not the plugin has been registered. 25521 */ 25522 var pluginExists = function pluginExists(name) { 25523 return pluginStorage.hasOwnProperty(name); 25524 }; 25525 25526 /** 25527 * Get a single registered plugin by name. 25528 * 25529 * @private 25530 * @param {string} name 25531 * The name of a plugin. 25532 * 25533 * @returns {Function|undefined} 25534 * The plugin (or undefined). 25535 */ 25536 var getPlugin = function getPlugin(name) { 25537 return pluginExists(name) ? pluginStorage[name] : undefined; 25538 }; 25539 25540 /** 25541 * Marks a plugin as "active" on a player. 25542 * 25543 * Also, ensures that the player has an object for tracking active plugins. 25544 * 25545 * @private 25546 * @param {Player} player 25547 * A Video.js player instance. 25548 * 25549 * @param {string} name 25550 * The name of a plugin. 25551 */ 25552 var markPluginAsActive = function markPluginAsActive(player, name) { 25553 player[PLUGIN_CACHE_KEY] = player[PLUGIN_CACHE_KEY] || {}; 25554 player[PLUGIN_CACHE_KEY][name] = true; 25555 }; 25556 25557 /** 25558 * Triggers a pair of plugin setup events. 25559 * 25560 * @private 25561 * @param {Player} player 25562 * A Video.js player instance. 25563 * 25564 * @param {Plugin~PluginEventHash} hash 25565 * A plugin event hash. 25566 * 25567 * @param {Boolean} [before] 25568 * If true, prefixes the event name with "before". In other words, 25569 * use this to trigger "beforepluginsetup" instead of "pluginsetup". 25570 */ 25571 var triggerSetupEvent = function triggerSetupEvent(player, hash, before) { 25572 var eventName = (before ? 'before' : '') + 'pluginsetup'; 25573 25574 player.trigger(eventName, hash); 25575 player.trigger(eventName + ':' + hash.name, hash); 25576 }; 25577 25578 /** 25579 * Takes a basic plugin function and returns a wrapper function which marks 25580 * on the player that the plugin has been activated. 25581 * 25582 * @private 25583 * @param {string} name 25584 * The name of the plugin. 25585 * 25586 * @param {Function} plugin 25587 * The basic plugin. 25588 * 25589 * @returns {Function} 25590 * A wrapper function for the given plugin. 25591 */ 25592 var createBasicPlugin = function createBasicPlugin(name, plugin) { 25593 var basicPluginWrapper = function basicPluginWrapper() { 25594 25595 // We trigger the "beforepluginsetup" and "pluginsetup" events on the player 25596 // regardless, but we want the hash to be consistent with the hash provided 25597 // for advanced plugins. 25598 // 25599 // The only potentially counter-intuitive thing here is the `instance` in 25600 // the "pluginsetup" event is the value returned by the `plugin` function. 25601 triggerSetupEvent(this, { name: name, plugin: plugin, instance: null }, true); 25602 25603 var instance = plugin.apply(this, arguments); 25604 25605 markPluginAsActive(this, name); 25606 triggerSetupEvent(this, { name: name, plugin: plugin, instance: instance }); 25607 25608 return instance; 25609 }; 25610 25611 Object.keys(plugin).forEach(function (prop) { 25612 basicPluginWrapper[prop] = plugin[prop]; 25613 }); 25614 25615 return basicPluginWrapper; 25616 }; 25617 25618 /** 25619 * Takes a plugin sub-class and returns a factory function for generating 25620 * instances of it. 25621 * 25622 * This factory function will replace itself with an instance of the requested 25623 * sub-class of Plugin. 25624 * 25625 * @private 25626 * @param {string} name 25627 * The name of the plugin. 25628 * 25629 * @param {Plugin} PluginSubClass 25630 * The advanced plugin. 25631 * 25632 * @returns {Function} 25633 */ 25634 var createPluginFactory = function createPluginFactory(name, PluginSubClass) { 25635 25636 // Add a `name` property to the plugin prototype so that each plugin can 25637 // refer to itself by name. 25638 PluginSubClass.prototype.name = name; 25639 25640 return function () { 25641 triggerSetupEvent(this, { name: name, plugin: PluginSubClass, instance: null }, true); 25642 25643 for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { 25644 args[_key] = arguments[_key]; 25645 } 25646 25647 var instance = new (Function.prototype.bind.apply(PluginSubClass, [null].concat([this].concat(args))))(); 25648 25649 // The plugin is replaced by a function that returns the current instance. 25650 this[name] = function () { 25651 return instance; 25652 }; 25653 25654 triggerSetupEvent(this, instance.getEventHash()); 25655 25656 return instance; 25657 }; 25658 }; 25659 25660 /** 25661 * Parent class for all advanced plugins. 25662 * 25663 * @mixes module:evented~EventedMixin 25664 * @mixes module:stateful~StatefulMixin 25665 * @fires Player#beforepluginsetup 25666 * @fires Player#beforepluginsetup:$name 25667 * @fires Player#pluginsetup 25668 * @fires Player#pluginsetup:$name 25669 * @listens Player#dispose 25670 * @throws {Error} 25671 * If attempting to instantiate the base {@link Plugin} class 25672 * directly instead of via a sub-class. 25673 */ 25674 25675 var Plugin = function () { 25676 25677 /** 25678 * Creates an instance of this class. 25679 * 25680 * Sub-classes should call `super` to ensure plugins are properly initialized. 25681 * 25682 * @param {Player} player 25683 * A Video.js player instance. 25684 */ 25685 function Plugin(player) { 25686 classCallCheck(this, Plugin); 25687 25688 if (this.constructor === Plugin) { 25689 throw new Error('Plugin must be sub-classed; not directly instantiated.'); 25690 } 25691 25692 this.player = player; 25693 25694 // Make this object evented, but remove the added `trigger` method so we 25695 // use the prototype version instead. 25696 evented(this); 25697 delete this.trigger; 25698 25699 stateful(this, this.constructor.defaultState); 25700 markPluginAsActive(player, this.name); 25701 25702 // Auto-bind the dispose method so we can use it as a listener and unbind 25703 // it later easily. 25704 this.dispose = bind(this, this.dispose); 25705 25706 // If the player is disposed, dispose the plugin. 25707 player.on('dispose', this.dispose); 25708 } 25709 25710 /** 25711 * Get the version of the plugin that was set on <pluginName>.VERSION 25712 */ 25713 25714 25715 Plugin.prototype.version = function version() { 25716 return this.constructor.VERSION; 25717 }; 25718 25719 /** 25720 * Each event triggered by plugins includes a hash of additional data with 25721 * conventional properties. 25722 * 25723 * This returns that object or mutates an existing hash. 25724 * 25725 * @param {Object} [hash={}] 25726 * An object to be used as event an event hash. 25727 * 25728 * @returns {Plugin~PluginEventHash} 25729 * An event hash object with provided properties mixed-in. 25730 */ 25731 25732 25733 Plugin.prototype.getEventHash = function getEventHash() { 25734 var hash = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 25735 25736 hash.name = this.name; 25737 hash.plugin = this.constructor; 25738 hash.instance = this; 25739 return hash; 25740 }; 25741 25742 /** 25743 * Triggers an event on the plugin object and overrides 25744 * {@link module:evented~EventedMixin.trigger|EventedMixin.trigger}. 25745 * 25746 * @param {string|Object} event 25747 * An event type or an object with a type property. 25748 * 25749 * @param {Object} [hash={}] 25750 * Additional data hash to merge with a 25751 * {@link Plugin~PluginEventHash|PluginEventHash}. 25752 * 25753 * @returns {boolean} 25754 * Whether or not default was prevented. 25755 */ 25756 25757 25758 Plugin.prototype.trigger = function trigger$$1(event) { 25759 var hash = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 25760 25761 return trigger(this.eventBusEl_, event, this.getEventHash(hash)); 25762 }; 25763 25764 /** 25765 * Handles "statechanged" events on the plugin. No-op by default, override by 25766 * subclassing. 25767 * 25768 * @abstract 25769 * @param {Event} e 25770 * An event object provided by a "statechanged" event. 25771 * 25772 * @param {Object} e.changes 25773 * An object describing changes that occurred with the "statechanged" 25774 * event. 25775 */ 25776 25777 25778 Plugin.prototype.handleStateChanged = function handleStateChanged(e) {}; 25779 25780 /** 25781 * Disposes a plugin. 25782 * 25783 * Subclasses can override this if they want, but for the sake of safety, 25784 * it's probably best to subscribe the "dispose" event. 25785 * 25786 * @fires Plugin#dispose 25787 */ 25788 25789 25790 Plugin.prototype.dispose = function dispose() { 25791 var name = this.name, 25792 player = this.player; 25793 25794 /** 25795 * Signals that a advanced plugin is about to be disposed. 25796 * 25797 * @event Plugin#dispose 25798 * @type {EventTarget~Event} 25799 */ 25800 25801 this.trigger('dispose'); 25802 this.off(); 25803 player.off('dispose', this.dispose); 25804 25805 // Eliminate any possible sources of leaking memory by clearing up 25806 // references between the player and the plugin instance and nulling out 25807 // the plugin's state and replacing methods with a function that throws. 25808 player[PLUGIN_CACHE_KEY][name] = false; 25809 this.player = this.state = null; 25810 25811 // Finally, replace the plugin name on the player with a new factory 25812 // function, so that the plugin is ready to be set up again. 25813 player[name] = createPluginFactory(name, pluginStorage[name]); 25814 }; 25815 25816 /** 25817 * Determines if a plugin is a basic plugin (i.e. not a sub-class of `Plugin`). 25818 * 25819 * @param {string|Function} plugin 25820 * If a string, matches the name of a plugin. If a function, will be 25821 * tested directly. 25822 * 25823 * @returns {boolean} 25824 * Whether or not a plugin is a basic plugin. 25825 */ 25826 25827 25828 Plugin.isBasic = function isBasic(plugin) { 25829 var p = typeof plugin === 'string' ? getPlugin(plugin) : plugin; 25830 25831 return typeof p === 'function' && !Plugin.prototype.isPrototypeOf(p.prototype); 25832 }; 25833 25834 /** 25835 * Register a Video.js plugin. 25836 * 25837 * @param {string} name 25838 * The name of the plugin to be registered. Must be a string and 25839 * must not match an existing plugin or a method on the `Player` 25840 * prototype. 25841 * 25842 * @param {Function} plugin 25843 * A sub-class of `Plugin` or a function for basic plugins. 25844 * 25845 * @returns {Function} 25846 * For advanced plugins, a factory function for that plugin. For 25847 * basic plugins, a wrapper function that initializes the plugin. 25848 */ 25849 25850 25851 Plugin.registerPlugin = function registerPlugin(name, plugin) { 25852 if (typeof name !== 'string') { 25853 throw new Error('Illegal plugin name, "' + name + '", must be a string, was ' + (typeof name === 'undefined' ? 'undefined' : _typeof(name)) + '.'); 25854 } 25855 25856 if (pluginExists(name)) { 25857 log$1.warn('A plugin named "' + name + '" already exists. You may want to avoid re-registering plugins!'); 25858 } else if (Player.prototype.hasOwnProperty(name)) { 25859 throw new Error('Illegal plugin name, "' + name + '", cannot share a name with an existing player method!'); 25860 } 25861 25862 if (typeof plugin !== 'function') { 25863 throw new Error('Illegal plugin for "' + name + '", must be a function, was ' + (typeof plugin === 'undefined' ? 'undefined' : _typeof(plugin)) + '.'); 25864 } 25865 25866 pluginStorage[name] = plugin; 25867 25868 // Add a player prototype method for all sub-classed plugins (but not for 25869 // the base Plugin class). 25870 if (name !== BASE_PLUGIN_NAME) { 25871 if (Plugin.isBasic(plugin)) { 25872 Player.prototype[name] = createBasicPlugin(name, plugin); 25873 } else { 25874 Player.prototype[name] = createPluginFactory(name, plugin); 25875 } 25876 } 25877 25878 return plugin; 25879 }; 25880 25881 /** 25882 * De-register a Video.js plugin. 25883 * 25884 * @param {string} name 25885 * The name of the plugin to be deregistered. 25886 */ 25887 25888 25889 Plugin.deregisterPlugin = function deregisterPlugin(name) { 25890 if (name === BASE_PLUGIN_NAME) { 25891 throw new Error('Cannot de-register base plugin.'); 25892 } 25893 if (pluginExists(name)) { 25894 delete pluginStorage[name]; 25895 delete Player.prototype[name]; 25896 } 25897 }; 25898 25899 /** 25900 * Gets an object containing multiple Video.js plugins. 25901 * 25902 * @param {Array} [names] 25903 * If provided, should be an array of plugin names. Defaults to _all_ 25904 * plugin names. 25905 * 25906 * @returns {Object|undefined} 25907 * An object containing plugin(s) associated with their name(s) or 25908 * `undefined` if no matching plugins exist). 25909 */ 25910 25911 25912 Plugin.getPlugins = function getPlugins() { 25913 var names = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : Object.keys(pluginStorage); 25914 25915 var result = void 0; 25916 25917 names.forEach(function (name) { 25918 var plugin = getPlugin(name); 25919 25920 if (plugin) { 25921 result = result || {}; 25922 result[name] = plugin; 25923 } 25924 }); 25925 25926 return result; 25927 }; 25928 25929 /** 25930 * Gets a plugin's version, if available 25931 * 25932 * @param {string} name 25933 * The name of a plugin. 25934 * 25935 * @returns {string} 25936 * The plugin's version or an empty string. 25937 */ 25938 25939 25940 Plugin.getPluginVersion = function getPluginVersion(name) { 25941 var plugin = getPlugin(name); 25942 25943 return plugin && plugin.VERSION || ''; 25944 }; 25945 25946 return Plugin; 25947 }(); 25948 25949 /** 25950 * Gets a plugin by name if it exists. 25951 * 25952 * @static 25953 * @method getPlugin 25954 * @memberOf Plugin 25955 * @param {string} name 25956 * The name of a plugin. 25957 * 25958 * @returns {Function|undefined} 25959 * The plugin (or `undefined`). 25960 */ 25961 25962 25963 Plugin.getPlugin = getPlugin; 25964 25965 /** 25966 * The name of the base plugin class as it is registered. 25967 * 25968 * @type {string} 25969 */ 25970 Plugin.BASE_PLUGIN_NAME = BASE_PLUGIN_NAME; 25971 25972 Plugin.registerPlugin(BASE_PLUGIN_NAME, Plugin); 25973 25974 /** 25975 * Documented in player.js 25976 * 25977 * @ignore 25978 */ 25979 Player.prototype.usingPlugin = function (name) { 25980 return !!this[PLUGIN_CACHE_KEY] && this[PLUGIN_CACHE_KEY][name] === true; 25981 }; 25982 25983 /** 25984 * Documented in player.js 25985 * 25986 * @ignore 25987 */ 25988 Player.prototype.hasPlugin = function (name) { 25989 return !!pluginExists(name); 25990 }; 25991 25992 /** 25993 * @file extend.js 25994 * @module extend 25995 */ 25996 25997 /** 25998 * A combination of node inherits and babel's inherits (after transpile). 25999 * Both work the same but node adds `super_` to the subClass 26000 * and Bable adds the superClass as __proto__. Both seem useful. 26001 * 26002 * @param {Object} subClass 26003 * The class to inherit to 26004 * 26005 * @param {Object} superClass 26006 * The class to inherit from 26007 * 26008 * @private 26009 */ 26010 var _inherits = function _inherits(subClass, superClass) { 26011 if (typeof superClass !== 'function' && superClass !== null) { 26012 throw new TypeError('Super expression must either be null or a function, not ' + (typeof superClass === 'undefined' ? 'undefined' : _typeof(superClass))); 26013 } 26014 26015 subClass.prototype = Object.create(superClass && superClass.prototype, { 26016 constructor: { 26017 value: subClass, 26018 enumerable: false, 26019 writable: true, 26020 configurable: true 26021 } 26022 }); 26023 26024 if (superClass) { 26025 // node 26026 subClass.super_ = superClass; 26027 } 26028 }; 26029 26030 /** 26031 * Function for subclassing using the same inheritance that 26032 * videojs uses internally 26033 * 26034 * @static 26035 * @const 26036 * 26037 * @param {Object} superClass 26038 * The class to inherit from 26039 * 26040 * @param {Object} [subClassMethods={}] 26041 * The class to inherit to 26042 * 26043 * @return {Object} 26044 * The new object with subClassMethods that inherited superClass. 26045 */ 26046 var extendFn = function extendFn(superClass) { 26047 var subClassMethods = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 26048 26049 var subClass = function subClass() { 26050 superClass.apply(this, arguments); 26051 }; 26052 26053 var methods = {}; 26054 26055 if ((typeof subClassMethods === 'undefined' ? 'undefined' : _typeof(subClassMethods)) === 'object') { 26056 if (subClassMethods.constructor !== Object.prototype.constructor) { 26057 subClass = subClassMethods.constructor; 26058 } 26059 methods = subClassMethods; 26060 } else if (typeof subClassMethods === 'function') { 26061 subClass = subClassMethods; 26062 } 26063 26064 _inherits(subClass, superClass); 26065 26066 // Extend subObj's prototype with functions and other properties from props 26067 for (var name in methods) { 26068 if (methods.hasOwnProperty(name)) { 26069 subClass.prototype[name] = methods[name]; 26070 } 26071 } 26072 26073 return subClass; 26074 }; 26075 26076 /** 26077 * @file video.js 26078 * @module videojs 26079 */ 26080 26081 /** 26082 * Normalize an `id` value by trimming off a leading `#` 26083 * 26084 * @param {string} id 26085 * A string, maybe with a leading `#`. 26086 * 26087 * @returns {string} 26088 * The string, without any leading `#`. 26089 */ 26090 var normalizeId = function normalizeId(id) { 26091 return id.indexOf('#') === 0 ? id.slice(1) : id; 26092 }; 26093 26094 /** 26095 * Doubles as the main function for users to create a player instance and also 26096 * the main library object. 26097 * The `videojs` function can be used to initialize or retrieve a player. 26098 * 26099 * @param {string|Element} id 26100 * Video element or video element ID 26101 * 26102 * @param {Object} [options] 26103 * Optional options object for config/settings 26104 * 26105 * @param {Component~ReadyCallback} [ready] 26106 * Optional ready callback 26107 * 26108 * @return {Player} 26109 * A player instance 26110 */ 26111 function videojs$1(id, options, ready) { 26112 var player = videojs$1.getPlayer(id); 26113 26114 if (player) { 26115 if (options) { 26116 log$1.warn('Player "' + id + '" is already initialised. Options will not be applied.'); 26117 } 26118 if (ready) { 26119 player.ready(ready); 26120 } 26121 return player; 26122 } 26123 26124 var el = typeof id === 'string' ? $('#' + normalizeId(id)) : id; 26125 26126 if (!isEl(el)) { 26127 throw new TypeError('The element or ID supplied is not valid. (videojs)'); 26128 } 26129 26130 if (!document_1.body.contains(el)) { 26131 log$1.warn('The element supplied is not included in the DOM'); 26132 } 26133 26134 options = options || {}; 26135 26136 videojs$1.hooks('beforesetup').forEach(function (hookFunction) { 26137 var opts = hookFunction(el, mergeOptions(options)); 26138 26139 if (!isObject(opts) || Array.isArray(opts)) { 26140 log$1.error('please return an object in beforesetup hooks'); 26141 return; 26142 } 26143 26144 options = mergeOptions(options, opts); 26145 }); 26146 26147 // We get the current "Player" component here in case an integration has 26148 // replaced it with a custom player. 26149 var PlayerComponent = Component.getComponent('Player'); 26150 26151 player = new PlayerComponent(el, options, ready); 26152 26153 videojs$1.hooks('setup').forEach(function (hookFunction) { 26154 return hookFunction(player); 26155 }); 26156 26157 return player; 26158 } 26159 26160 /** 26161 * An Object that contains lifecycle hooks as keys which point to an array 26162 * of functions that are run when a lifecycle is triggered 26163 */ 26164 videojs$1.hooks_ = {}; 26165 26166 /** 26167 * Get a list of hooks for a specific lifecycle 26168 * @function videojs.hooks 26169 * 26170 * @param {string} type 26171 * the lifecyle to get hooks from 26172 * 26173 * @param {Function|Function[]} [fn] 26174 * Optionally add a hook (or hooks) to the lifecycle that your are getting. 26175 * 26176 * @return {Array} 26177 * an array of hooks, or an empty array if there are none. 26178 */ 26179 videojs$1.hooks = function (type, fn) { 26180 videojs$1.hooks_[type] = videojs$1.hooks_[type] || []; 26181 if (fn) { 26182 videojs$1.hooks_[type] = videojs$1.hooks_[type].concat(fn); 26183 } 26184 return videojs$1.hooks_[type]; 26185 }; 26186 26187 /** 26188 * Add a function hook to a specific videojs lifecycle. 26189 * 26190 * @param {string} type 26191 * the lifecycle to hook the function to. 26192 * 26193 * @param {Function|Function[]} 26194 * The function or array of functions to attach. 26195 */ 26196 videojs$1.hook = function (type, fn) { 26197 videojs$1.hooks(type, fn); 26198 }; 26199 26200 /** 26201 * Add a function hook that will only run once to a specific videojs lifecycle. 26202 * 26203 * @param {string} type 26204 * the lifecycle to hook the function to. 26205 * 26206 * @param {Function|Function[]} 26207 * The function or array of functions to attach. 26208 */ 26209 videojs$1.hookOnce = function (type, fn) { 26210 videojs$1.hooks(type, [].concat(fn).map(function (original) { 26211 var wrapper = function wrapper() { 26212 videojs$1.removeHook(type, wrapper); 26213 return original.apply(undefined, arguments); 26214 }; 26215 26216 return wrapper; 26217 })); 26218 }; 26219 26220 /** 26221 * Remove a hook from a specific videojs lifecycle. 26222 * 26223 * @param {string} type 26224 * the lifecycle that the function hooked to 26225 * 26226 * @param {Function} fn 26227 * The hooked function to remove 26228 * 26229 * @return {boolean} 26230 * The function that was removed or undef 26231 */ 26232 videojs$1.removeHook = function (type, fn) { 26233 var index = videojs$1.hooks(type).indexOf(fn); 26234 26235 if (index <= -1) { 26236 return false; 26237 } 26238 26239 videojs$1.hooks_[type] = videojs$1.hooks_[type].slice(); 26240 videojs$1.hooks_[type].splice(index, 1); 26241 26242 return true; 26243 }; 26244 26245 // Add default styles 26246 if (window_1.VIDEOJS_NO_DYNAMIC_STYLE !== true && isReal()) { 26247 var style$1 = $('.vjs-styles-defaults'); 26248 26249 if (!style$1) { 26250 style$1 = createStyleElement('vjs-styles-defaults'); 26251 var head = $('head'); 26252 26253 if (head) { 26254 head.insertBefore(style$1, head.firstChild); 26255 } 26256 setTextContent(style$1, '\n .video-js {\n width: 300px;\n height: 150px;\n }\n\n .vjs-fluid {\n padding-top: 56.25%\n }\n '); 26257 } 26258 } 26259 26260 // Run Auto-load players 26261 // You have to wait at least once in case this script is loaded after your 26262 // video in the DOM (weird behavior only with minified version) 26263 autoSetupTimeout(1, videojs$1); 26264 26265 /** 26266 * Current software version. Follows semver. 26267 * 26268 * @type {string} 26269 */ 26270 videojs$1.VERSION = version; 26271 26272 /** 26273 * The global options object. These are the settings that take effect 26274 * if no overrides are specified when the player is created. 26275 * 26276 * @type {Object} 26277 */ 26278 videojs$1.options = Player.prototype.options_; 26279 26280 /** 26281 * Get an object with the currently created players, keyed by player ID 26282 * 26283 * @return {Object} 26284 * The created players 26285 */ 26286 videojs$1.getPlayers = function () { 26287 return Player.players; 26288 }; 26289 26290 /** 26291 * Get a single player based on an ID or DOM element. 26292 * 26293 * This is useful if you want to check if an element or ID has an associated 26294 * Video.js player, but not create one if it doesn't. 26295 * 26296 * @param {string|Element} id 26297 * An HTML element - `<video>`, `<audio>`, or `<video-js>` - 26298 * or a string matching the `id` of such an element. 26299 * 26300 * @returns {Player|undefined} 26301 * A player instance or `undefined` if there is no player instance 26302 * matching the argument. 26303 */ 26304 videojs$1.getPlayer = function (id) { 26305 var players = Player.players; 26306 var tag = void 0; 26307 26308 if (typeof id === 'string') { 26309 var nId = normalizeId(id); 26310 var player = players[nId]; 26311 26312 if (player) { 26313 return player; 26314 } 26315 26316 tag = $('#' + nId); 26317 } else { 26318 tag = id; 26319 } 26320 26321 if (isEl(tag)) { 26322 var _tag = tag, 26323 _player = _tag.player, 26324 playerId = _tag.playerId; 26325 26326 // Element may have a `player` property referring to an already created 26327 // player instance. If so, return that. 26328 26329 if (_player || players[playerId]) { 26330 return _player || players[playerId]; 26331 } 26332 } 26333 }; 26334 26335 /** 26336 * Returns an array of all current players. 26337 * 26338 * @return {Array} 26339 * An array of all players. The array will be in the order that 26340 * `Object.keys` provides, which could potentially vary between 26341 * JavaScript engines. 26342 * 26343 */ 26344 videojs$1.getAllPlayers = function () { 26345 return ( 26346 26347 // Disposed players leave a key with a `null` value, so we need to make sure 26348 // we filter those out. 26349 Object.keys(Player.players).map(function (k) { 26350 return Player.players[k]; 26351 }).filter(Boolean) 26352 ); 26353 }; 26354 26355 /** 26356 * Expose players object. 26357 * 26358 * @memberOf videojs 26359 * @property {Object} players 26360 */ 26361 videojs$1.players = Player.players; 26362 26363 /** 26364 * Get a component class object by name 26365 * 26366 * @borrows Component.getComponent as videojs.getComponent 26367 */ 26368 videojs$1.getComponent = Component.getComponent; 26369 26370 /** 26371 * Register a component so it can referred to by name. Used when adding to other 26372 * components, either through addChild `component.addChild('myComponent')` or through 26373 * default children options `{ children: ['myComponent'] }`. 26374 * 26375 * > NOTE: You could also just initialize the component before adding. 26376 * `component.addChild(new MyComponent());` 26377 * 26378 * @param {string} name 26379 * The class name of the component 26380 * 26381 * @param {Component} comp 26382 * The component class 26383 * 26384 * @return {Component} 26385 * The newly registered component 26386 */ 26387 videojs$1.registerComponent = function (name$$1, comp) { 26388 if (Tech.isTech(comp)) { 26389 log$1.warn('The ' + name$$1 + ' tech was registered as a component. It should instead be registered using videojs.registerTech(name, tech)'); 26390 } 26391 26392 Component.registerComponent.call(Component, name$$1, comp); 26393 }; 26394 26395 /** 26396 * Get a Tech class object by name 26397 * 26398 * @borrows Tech.getTech as videojs.getTech 26399 */ 26400 videojs$1.getTech = Tech.getTech; 26401 26402 /** 26403 * Register a Tech so it can referred to by name. 26404 * This is used in the tech order for the player. 26405 * 26406 * @borrows Tech.registerTech as videojs.registerTech 26407 */ 26408 videojs$1.registerTech = Tech.registerTech; 26409 26410 /** 26411 * Register a middleware to a source type. 26412 * 26413 * @param {String} type A string representing a MIME type. 26414 * @param {function(player):object} middleware A middleware factory that takes a player. 26415 */ 26416 videojs$1.use = use; 26417 26418 /** 26419 * An object that can be returned by a middleware to signify 26420 * that the middleware is being terminated. 26421 * 26422 * @type {object} 26423 * @memberOf {videojs} 26424 * @property {object} middleware.TERMINATOR 26425 */ 26426 Object.defineProperty(videojs$1, 'middleware', { 26427 value: {}, 26428 writeable: false, 26429 enumerable: true 26430 }); 26431 26432 Object.defineProperty(videojs$1.middleware, 'TERMINATOR', { 26433 value: TERMINATOR, 26434 writeable: false, 26435 enumerable: true 26436 }); 26437 26438 /** 26439 * A suite of browser and device tests from {@link browser}. 26440 * 26441 * @type {Object} 26442 * @private 26443 */ 26444 videojs$1.browser = browser; 26445 26446 /** 26447 * Whether or not the browser supports touch events. Included for backward 26448 * compatibility with 4.x, but deprecated. Use `videojs.browser.TOUCH_ENABLED` 26449 * instead going forward. 26450 * 26451 * @deprecated since version 5.0 26452 * @type {boolean} 26453 */ 26454 videojs$1.TOUCH_ENABLED = TOUCH_ENABLED; 26455 26456 /** 26457 * Subclass an existing class 26458 * Mimics ES6 subclassing with the `extend` keyword 26459 * 26460 * @borrows extend:extendFn as videojs.extend 26461 */ 26462 videojs$1.extend = extendFn; 26463 26464 /** 26465 * Merge two options objects recursively 26466 * Performs a deep merge like lodash.merge but **only merges plain objects** 26467 * (not arrays, elements, anything else) 26468 * Other values will be copied directly from the second object. 26469 * 26470 * @borrows merge-options:mergeOptions as videojs.mergeOptions 26471 */ 26472 videojs$1.mergeOptions = mergeOptions; 26473 26474 /** 26475 * Change the context (this) of a function 26476 * 26477 * > NOTE: as of v5.0 we require an ES5 shim, so you should use the native 26478 * `function() {}.bind(newContext);` instead of this. 26479 * 26480 * @borrows fn:bind as videojs.bind 26481 */ 26482 videojs$1.bind = bind; 26483 26484 /** 26485 * Register a Video.js plugin. 26486 * 26487 * @borrows plugin:registerPlugin as videojs.registerPlugin 26488 * @method registerPlugin 26489 * 26490 * @param {string} name 26491 * The name of the plugin to be registered. Must be a string and 26492 * must not match an existing plugin or a method on the `Player` 26493 * prototype. 26494 * 26495 * @param {Function} plugin 26496 * A sub-class of `Plugin` or a function for basic plugins. 26497 * 26498 * @return {Function} 26499 * For advanced plugins, a factory function for that plugin. For 26500 * basic plugins, a wrapper function that initializes the plugin. 26501 */ 26502 videojs$1.registerPlugin = Plugin.registerPlugin; 26503 26504 /** 26505 * Deprecated method to register a plugin with Video.js 26506 * 26507 * @deprecated 26508 * videojs.plugin() is deprecated; use videojs.registerPlugin() instead 26509 * 26510 * @param {string} name 26511 * The plugin name 26512 * 26513 * @param {Plugin|Function} plugin 26514 * The plugin sub-class or function 26515 */ 26516 videojs$1.plugin = function (name$$1, plugin) { 26517 log$1.warn('videojs.plugin() is deprecated; use videojs.registerPlugin() instead'); 26518 return Plugin.registerPlugin(name$$1, plugin); 26519 }; 26520 26521 /**
26522 * Gets an object containing multiple Video.js plugins. 26523 * 26524 * @param {Array} [names] 26525 * If provided, should be an array of plugin names. Defaults to _all_ 26526 * plugin names. 26527 * 26528 * @return {Object|undefined} 26529 * An object containing plugin(s) associated with their name(s) or 26530 * `undefined` if no matching plugins exist). 26531 */ 26532 videojs$1.getPlugins = Plugin.getPlugins; 26533 26534 /** 26535 * Gets a plugin by name if it exists. 26536 * 26537 * @param {string} name 26538 * The name of a plugin. 26539 * 26540 * @return {Function|undefined} 26541 * The plugin (or `undefined`). 26542 */ 26543 videojs$1.getPlugin = Plugin.getPlugin; 26544 26545 /** 26546 * Gets a plugin's version, if available 26547 * 26548 * @param {string} name 26549 * The name of a plugin. 26550 * 26551 * @return {string} 26552 * The plugin's version or an empty string. 26553 */ 26554 videojs$1.getPluginVersion = Plugin.getPluginVersion; 26555 26556 /** 26557 * Adding languages so that they're available to all players. 26558 * Example: `videojs.addLanguage('es', { 'Hello': 'Hola' });` 26559 * 26560 * @param {string} code 26561 * The language code or dictionary property 26562 * 26563 * @param {Object} data 26564 * The data values to be translated 26565 * 26566 * @return {Object} 26567 * The resulting language dictionary object 26568 */ 26569 videojs$1.addLanguage = function (code, data) { 26570 var _mergeOptions; 26571 26572 code = ('' + code).toLowerCase(); 26573 26574 videojs$1.options.languages = mergeOptions(videojs$1.options.languages, (_mergeOptions = {}, _mergeOptions[code] = data, _mergeOptions)); 26575 26576 return videojs$1.options.languages[code]; 26577 }; 26578 26579 /** 26580 * Log messages 26581 * 26582 * @borrows log:log as videojs.log 26583 */ 26584 videojs$1.log = log$1; 26585 26586 /** 26587 * Creates an emulated TimeRange object. 26588 * 26589 * @borrows time-ranges:createTimeRanges as videojs.createTimeRange 26590 */ 26591 /** 26592 * @borrows time-ranges:createTimeRanges as videojs.createTimeRanges 26593 */ 26594 videojs$1.createTimeRange = videojs$1.createTimeRanges = createTimeRanges; 26595 26596 /** 26597 * Format seconds as a time string, H:MM:SS or M:SS 26598 * Supplying a guide (in seconds) will force a number of leading zeros 26599 * to cover the length of the guide 26600 * 26601 * @borrows format-time:formatTime as videojs.formatTime 26602 */ 26603 videojs$1.formatTime = formatTime; 26604 26605 /** 26606 * Replaces format-time with a custom implementation, to be used in place of the default. 26607 * 26608 * @borrows format-time:setFormatTime as videojs.setFormatTime 26609 * 26610 * @method setFormatTime 26611 * 26612 * @param {Function} customFn 26613 * A custom format-time function which will be called with the current time and guide (in seconds) as arguments. 26614 * Passed fn should return a string. 26615 */ 26616 videojs$1.setFormatTime = setFormatTime; 26617 26618 /** 26619 * Resets format-time to the default implementation. 26620 * 26621 * @borrows format-time:resetFormatTime as videojs.resetFormatTime 26622 * 26623 * @method resetFormatTime 26624 */ 26625 videojs$1.resetFormatTime = resetFormatTime; 26626 26627 /** 26628 * Resolve and parse the elements of a URL 26629 * 26630 * @borrows url:parseUrl as videojs.parseUrl 26631 * 26632 */ 26633 videojs$1.parseUrl = parseUrl; 26634 26635 /** 26636 * Returns whether the url passed is a cross domain request or not. 26637 * 26638 * @borrows url:isCrossOrigin as videojs.isCrossOrigin 26639 */ 26640 videojs$1.isCrossOrigin = isCrossOrigin; 26641 26642 /** 26643 * Event target class. 26644 * 26645 * @borrows EventTarget as videojs.EventTarget 26646 */ 26647 videojs$1.EventTarget = EventTarget; 26648 26649 /** 26650 * Add an event listener to element 26651 * It stores the handler function in a separate cache object 26652 * and adds a generic handler to the element's event, 26653 * along with a unique id (guid) to the element. 26654 * 26655 * @borrows events:on as videojs.on 26656 */ 26657 videojs$1.on = on; 26658 26659 /** 26660 * Trigger a listener only once for an event 26661 * 26662 * @borrows events:one as videojs.one 26663 */ 26664 videojs$1.one = one; 26665 26666 /** 26667 * Removes event listeners from an element 26668 * 26669 * @borrows events:off as videojs.off 26670 */ 26671 videojs$1.off = off; 26672 26673 /** 26674 * Trigger an event for an element 26675 * 26676 * @borrows events:trigger as videojs.trigger 26677 */ 26678 videojs$1.trigger = trigger; 26679 26680 /** 26681 * A cross-browser XMLHttpRequest wrapper. Here's a simple example: 26682 * 26683 * @param {Object} options 26684 * settings for the request. 26685 * 26686 * @return {XMLHttpRequest|XDomainRequest} 26687 * The request object. 26688 * 26689 * @see https://github.com/Raynos/xhr 26690 */ 26691 videojs$1.xhr = xhr; 26692 26693 /** 26694 * TextTrack class 26695 * 26696 * @borrows TextTrack as videojs.TextTrack 26697 */ 26698 videojs$1.TextTrack = TextTrack; 26699 26700 /** 26701 * export the AudioTrack class so that source handlers can create 26702 * AudioTracks and then add them to the players AudioTrackList 26703 * 26704 * @borrows AudioTrack as videojs.AudioTrack 26705 */ 26706 videojs$1.AudioTrack = AudioTrack; 26707 26708 /** 26709 * export the VideoTrack class so that source handlers can create 26710 * VideoTracks and then add them to the players VideoTrackList 26711 * 26712 * @borrows VideoTrack as videojs.VideoTrack 26713 */ 26714 videojs$1.VideoTrack = VideoTrack; 26715 26716 /** 26717 * Determines, via duck typing, whether or not a value is a DOM element. 26718 * 26719 * @borrows dom:isEl as videojs.isEl 26720 * @deprecated Use videojs.dom.isEl() instead 26721 */ 26722 26723 /** 26724 * Determines, via duck typing, whether or not a value is a text node. 26725 * 26726 * @borrows dom:isTextNode as videojs.isTextNode 26727 * @deprecated Use videojs.dom.isTextNode() instead 26728 */ 26729 26730 /** 26731 * Creates an element and applies properties. 26732 * 26733 * @borrows dom:createEl as videojs.createEl 26734 * @deprecated Use videojs.dom.createEl() instead 26735 */ 26736 26737 /** 26738 * Check if an element has a CSS class 26739 * 26740 * @borrows dom:hasElClass as videojs.hasClass 26741 * @deprecated Use videojs.dom.hasClass() instead 26742 */ 26743 26744 /** 26745 * Add a CSS class name to an element 26746 * 26747 * @borrows dom:addElClass as videojs.addClass 26748 * @deprecated Use videojs.dom.addClass() instead 26749 */ 26750 26751 /** 26752 * Remove a CSS class name from an element 26753 * 26754 * @borrows dom:removeElClass as videojs.removeClass 26755 * @deprecated Use videojs.dom.removeClass() instead 26756 */ 26757 26758 /** 26759 * Adds or removes a CSS class name on an element depending on an optional 26760 * condition or the presence/absence of the class name. 26761 * 26762 * @borrows dom:toggleElClass as videojs.toggleClass 26763 * @deprecated Use videojs.dom.toggleClass() instead 26764 */ 26765 26766 /** 26767 * Apply attributes to an HTML element. 26768 * 26769 * @borrows dom:setElAttributes as videojs.setAttribute 26770 * @deprecated Use videojs.dom.setAttributes() instead 26771 */ 26772 26773 /** 26774 * Get an element's attribute values, as defined on the HTML tag 26775 * Attributes are not the same as properties. They're defined on the tag 26776 * or with setAttribute (which shouldn't be used with HTML) 26777 * This will return true or false for boolean attributes. 26778 * 26779 * @borrows dom:getElAttributes as videojs.getAttributes 26780 * @deprecated Use videojs.dom.getAttributes() instead 26781 */ 26782 26783 /** 26784 * Empties the contents of an element. 26785 * 26786 * @borrows dom:emptyEl as videojs.emptyEl 26787 * @deprecated Use videojs.dom.emptyEl() instead 26788 */ 26789 26790 /** 26791 * Normalizes and appends content to an element. 26792 * 26793 * The content for an element can be passed in multiple types and 26794 * combinations, whose behavior is as follows: 26795 * 26796 * - String 26797 * Normalized into a text node. 26798 * 26799 * - Element, TextNode 26800 * Passed through. 26801 * 26802 * - Array
26803 * A one-dimensional array of strings, elements, nodes, or functions (which 26804 * return single strings, elements, or nodes). 26805 * 26806 * - Function 26807 * If the sole argument, is expected to produce a string, element, 26808 * node, or array. 26809 * 26810 * @borrows dom:appendContents as videojs.appendContet 26811 * @deprecated Use videojs.dom.appendContent() instead 26812 */ 26813 26814 /** 26815 * Normalizes and inserts content into an element; this is identical to 26816 * `appendContent()`, except it empties the element first. 26817 * 26818 * The content for an element can be passed in multiple types and 26819 * combinations, whose behavior is as follows: 26820 * 26821 * - String 26822 * Normalized into a text node. 26823 * 26824 * - Element, TextNode 26825 * Passed through. 26826 * 26827 * - Array 26828 * A one-dimensional array of strings, elements, nodes, or functions (which 26829 * return single strings, elements, or nodes). 26830 * 26831 * - Function 26832 * If the sole argument, is expected to produce a string, element, 26833 * node, or array. 26834 * 26835 * @borrows dom:insertContent as videojs.insertContent 26836 * @deprecated Use videojs.dom.insertContent() instead 26837 */ 26838 ['isEl', 'isTextNode', 'createEl', 'hasClass', 'addClass', 'removeClass', 'toggleClass', 'setAttributes', 'getAttributes', 'emptyEl', 'appendContent', 'insertContent'].forEach(function (k) { 26839 videojs$1[k] = function () { 26840 log$1.warn('videojs.' + k + '() is deprecated; use videojs.dom.' + k + '() instead'); 26841 return Dom[k].apply(null, arguments); 26842 }; 26843 }); 26844 26845 /** 26846 * A safe getComputedStyle. 26847 * 26848 * This is because in Firefox, if the player is loaded in an iframe with `display:none`, 26849 * then `getComputedStyle` returns `null`, so, we do a null-check to make sure 26850 * that the player doesn't break in these cases. 26851 * See https://bugzilla.mozilla.org/show_bug.cgi?id=548397 for more details. 26852 * 26853 * @borrows computed-style:computedStyle as videojs.computedStyle 26854 */ 26855 videojs$1.computedStyle = computedStyle; 26856 26857 /** 26858 * Export the Dom utilities for use in external plugins 26859 * and Tech's 26860 */ 26861 videojs$1.dom = Dom; 26862 26863 /** 26864 * Export the Url utilities for use in external plugins 26865 * and Tech's 26866 */ 26867 videojs$1.url = Url; 26868 26869 var urlToolkit = createCommonjsModule(function (module, exports) { 26870 // see https://tools.ietf.org/html/rfc1808 26871 26872 /* jshint ignore:start */ 26873 (function (root) { 26874 /* jshint ignore:end */ 26875 26876 var URL_REGEX = /^((?:[a-zA-Z0-9+\-.]+:)?)(\/\/[^\/\;?#]*)?(.*?)??(;.*?)?(\?.*?)?(#.*?)?$/; 26877 var FIRST_SEGMENT_REGEX = /^([^\/;?#]*)(.*)$/; 26878 var SLASH_DOT_REGEX = /(?:\/|^)\.(?=\/)/g; 26879 var SLASH_DOT_DOT_REGEX = /(?:\/|^)\.\.\/(?!\.\.\/).*?(?=\/)/g; 26880 26881 var URLToolkit = { // jshint ignore:line 26882 // If opts.alwaysNormalize is true then the path will always be normalized even when it starts with / or // 26883 // E.g 26884 // With opts.alwaysNormalize = false (default, spec compliant) 26885 // http://a.com/b/cd + /e/f/../g => http://a.com/e/f/../g 26886 // With opts.alwaysNormalize = true (not spec compliant) 26887 // http://a.com/b/cd + /e/f/../g => http://a.com/e/g 26888 buildAbsoluteURL: function buildAbsoluteURL(baseURL, relativeURL, opts) { 26889 opts = opts || {}; 26890 // remove any remaining space and CRLF 26891 baseURL = baseURL.trim(); 26892 relativeURL = relativeURL.trim(); 26893 if (!relativeURL) { 26894 // 2a) If the embedded URL is entirely empty, it inherits the 26895 // entire base URL (i.e., is set equal to the base URL) 26896 // and we are done. 26897 if (!opts.alwaysNormalize) { 26898 return baseURL; 26899 } 26900 var basePartsForNormalise = URLToolkit.parseURL(baseURL); 26901 if (!basePartsForNormalise) { 26902 throw new Error('Error trying to parse base URL.'); 26903 } 26904 basePartsForNormalise.path = URLToolkit.normalizePath(basePartsForNormalise.path); 26905 return URLToolkit.buildURLFromParts(basePartsForNormalise); 26906 } 26907 var relativeParts = URLToolkit.parseURL(relativeURL); 26908 if (!relativeParts) { 26909 throw new Error('Error trying to parse relative URL.'); 26910 } 26911 if (relativeParts.scheme) { 26912 // 2b) If the embedded URL starts with a scheme name, it is 26913 // interpreted as an absolute URL and we are done.
vendor: 2,564 bytes, lines 26914-26963
26914 if (!opts.alwaysNormalize) { 26915 return relativeURL; 26916 } 26917 relativeParts.path = URLToolkit.normalizePath(relativeParts.path); 26918 return URLToolkit.buildURLFromParts(relativeParts); 26919 } 26920 var baseParts = URLToolkit.parseURL(baseURL); 26921 if (!baseParts) { 26922 throw new Error('Error trying to parse base URL.'); 26923 } 26924 if (!baseParts.netLoc && baseParts.path && baseParts.path[0] !== '/') { 26925 // If netLoc missing and path doesn't start with '/', assume everthing before the first '/' is the netLoc 26926 // This causes 'example.com/a' to be handled as '//example.com/a' instead of '/example.com/a' 26927 var pathParts = FIRST_SEGMENT_REGEX.exec(baseParts.path); 26928 baseParts.netLoc = pathParts[1]; 26929 baseParts.path = pathParts[2]; 26930 } 26931 if (baseParts.netLoc && !baseParts.path) { 26932 baseParts.path = '/'; 26933 } 26934 var builtParts = { 26935 // 2c) Otherwise, the embedded URL inherits the scheme of 26936 // the base URL. 26937 scheme: baseParts.scheme, 26938 netLoc: relativeParts.netLoc, 26939 path: null, 26940 params: relativeParts.params, 26941 query: relativeParts.query, 26942 fragment: relativeParts.fragment 26943 }; 26944 if (!relativeParts.netLoc) { 26945 // 3) If the embedded URL's <net_loc> is non-empty, we skip to 26946 // Step 7. Otherwise, the embedded URL inherits the <net_loc> 26947 // (if any) of the base URL. 26948 builtParts.netLoc = baseParts.netLoc; 26949 // 4) If the embedded URL path is preceded by a slash "/", the 26950 // path is not relative and we skip to Step 7. 26951 if (relativeParts.path[0] !== '/') { 26952 if (!relativeParts.path) { 26953 // 5) If the embedded URL path is empty (and not preceded by a 26954 // slash), then the embedded URL inherits the base URL path 26955 builtParts.path = baseParts.path; 26956 // 5a) if the embedded URL's <params> is non-empty, we skip to 26957 // step 7; otherwise, it inherits the <params> of the base 26958 // URL (if any) and 26959 if (!relativeParts.params) { 26960 builtParts.params = baseParts.params; 26961 // 5b) if the embedded URL's <query> is non-empty, we skip to 26962 // step 7; otherwise, it inherits the <query> of the base 26963 // URL (if any) and we skip to step 7.
vendor: 63,872 bytes, lines 26964-28884
26964 if (!relativeParts.query) { 26965 builtParts.query = baseParts.query; 26966 } 26967 } 26968 } else { 26969 // 6) The last segment of the base URL's path (anything 26970 // following the rightmost slash "/", or the entire path if no 26971 // slash is present) is removed and the embedded URL's path is 26972 // appended in its place. 26973 var baseURLPath = baseParts.path; 26974 var newPath = baseURLPath.substring(0, baseURLPath.lastIndexOf('/') + 1) + relativeParts.path; 26975 builtParts.path = URLToolkit.normalizePath(newPath); 26976 } 26977 } 26978 } 26979 if (builtParts.path === null) { 26980 builtParts.path = opts.alwaysNormalize ? URLToolkit.normalizePath(relativeParts.path) : relativeParts.path; 26981 } 26982 return URLToolkit.buildURLFromParts(builtParts); 26983 }, 26984 parseURL: function parseURL(url) { 26985 var parts = URL_REGEX.exec(url); 26986 if (!parts) { 26987 return null; 26988 } 26989 return { 26990 scheme: parts[1] || '', 26991 netLoc: parts[2] || '', 26992 path: parts[3] || '', 26993 params: parts[4] || '', 26994 query: parts[5] || '', 26995 fragment: parts[6] || '' 26996 }; 26997 }, 26998 normalizePath: function normalizePath(path) { 26999 // The following operations are 27000 // then applied, in order, to the new path: 27001 // 6a) All occurrences of "./", where "." is a complete path 27002 // segment, are removed. 27003 // 6b) If the path ends with "." as a complete path segment, 27004 // that "." is removed. 27005 path = path.split('').reverse().join('').replace(SLASH_DOT_REGEX, ''); 27006 // 6c) All occurrences of "<segment>/../", where <segment> is a 27007 // complete path segment not equal to "..", are removed. 27008 // Removal of these path segments is performed iteratively, 27009 // removing the leftmost matching pattern on each iteration, 27010 // until no matching pattern remains. 27011 // 6d) If the path ends with "<segment>/..", where <segment> is a 27012 // complete path segment not equal to "..", that 27013 // "<segment>/.." is removed. 27014 while (path.length !== (path = path.replace(SLASH_DOT_DOT_REGEX, '')).length) {} // jshint ignore:line 27015 return path.split('').reverse().join(''); 27016 }, 27017 buildURLFromParts: function buildURLFromParts(parts) { 27018 return parts.scheme + parts.netLoc + parts.path + parts.params + parts.query + parts.fragment; 27019 } 27020 }; 27021 27022 /* jshint ignore:start */ 27023 module.exports = URLToolkit; 27024 })(commonjsGlobal); 27025 /* jshint ignore:end */ 27026 }); 27027 27028 var classCallCheck$1 = function classCallCheck$$1(instance, Constructor) { 27029 if (!(instance instanceof Constructor)) { 27030 throw new TypeError("Cannot call a class as a function"); 27031 } 27032 }; 27033 27034 var _extends$1 = Object.assign || function (target) { 27035 for (var i = 1; i < arguments.length; i++) { 27036 var source = arguments[i]; 27037 27038 for (var key in source) { 27039 if (Object.prototype.hasOwnProperty.call(source, key)) { 27040 target[key] = source[key]; 27041 } 27042 } 27043 } 27044 27045 return target; 27046 }; 27047 27048 var inherits$1 = function inherits$$1(subClass, superClass) { 27049 if (typeof superClass !== "function" && superClass !== null) { 27050 throw new TypeError("Super expression must either be null or a function, not " + (typeof superClass === "undefined" ? "undefined" : _typeof(superClass))); 27051 } 27052 27053 subClass.prototype = Object.create(superClass && superClass.prototype, { 27054 constructor: { 27055 value: subClass, 27056 enumerable: false, 27057 writable: true, 27058 configurable: true 27059 } 27060 }); 27061 if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; 27062 }; 27063 27064 var possibleConstructorReturn$1 = function possibleConstructorReturn$$1(self, call) { 27065 if (!self) { 27066 throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); 27067 } 27068 27069 return call && ((typeof call === "undefined" ? "undefined" : _typeof(call)) === "object" || typeof call === "function") ? call : self; 27070 }; 27071 27072 /** 27073 * @file stream.js 27074 */ 27075 /** 27076 * A lightweight readable stream implemention that handles event dispatching. 27077 * 27078 * @class Stream 27079 */ 27080 var Stream = function () { 27081 function Stream() { 27082 classCallCheck$1(this, Stream); 27083 27084 this.listeners = {}; 27085 } 27086 27087 /** 27088 * Add a listener for a specified event type. 27089 * 27090 * @param {String} type the event name 27091 * @param {Function} listener the callback to be invoked when an event of 27092 * the specified type occurs 27093 */ 27094 27095 Stream.prototype.on = function on(type, listener) { 27096 if (!this.listeners[type]) { 27097 this.listeners[type] = []; 27098 } 27099 this.listeners[type].push(listener); 27100 }; 27101 27102 /** 27103 * Remove a listener for a specified event type. 27104 * 27105 * @param {String} type the event name 27106 * @param {Function} listener a function previously registered for this 27107 * type of event through `on` 27108 * @return {Boolean} if we could turn it off or not 27109 */ 27110 27111 Stream.prototype.off = function off(type, listener) { 27112 if (!this.listeners[type]) { 27113 return false; 27114 } 27115 27116 var index = this.listeners[type].indexOf(listener); 27117 27118 this.listeners[type].splice(index, 1); 27119 return index > -1; 27120 }; 27121 27122 /** 27123 * Trigger an event of the specified type on this stream. Any additional 27124 * arguments to this function are passed as parameters to event listeners. 27125 * 27126 * @param {String} type the event name 27127 */ 27128 27129 Stream.prototype.trigger = function trigger(type) { 27130 var callbacks = this.listeners[type]; 27131 var i = void 0; 27132 var length = void 0; 27133 var args = void 0; 27134 27135 if (!callbacks) { 27136 return; 27137 } 27138 // Slicing the arguments on every invocation of this method 27139 // can add a significant amount of overhead. Avoid the 27140 // intermediate object creation for the common case of a 27141 // single callback argument 27142 if (arguments.length === 2) { 27143 length = callbacks.length; 27144 for (i = 0; i < length; ++i) { 27145 callbacks[i].call(this, arguments[1]); 27146 } 27147 } else { 27148 args = Array.prototype.slice.call(arguments, 1); 27149 length = callbacks.length; 27150 for (i = 0; i < length; ++i) { 27151 callbacks[i].apply(this, args); 27152 } 27153 } 27154 }; 27155 27156 /** 27157 * Destroys the stream and cleans up. 27158 */ 27159 27160 Stream.prototype.dispose = function dispose() { 27161 this.listeners = {}; 27162 }; 27163 /** 27164 * Forwards all `data` events on this stream to the destination stream. The 27165 * destination stream should provide a method `push` to receive the data 27166 * events as they arrive. 27167 * 27168 * @param {Stream} destination the stream that will receive all `data` events 27169 * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options 27170 */ 27171 27172 Stream.prototype.pipe = function pipe(destination) { 27173 this.on('data', function (data) { 27174 destination.push(data); 27175 }); 27176 }; 27177 27178 return Stream; 27179 }(); 27180 27181 /** 27182 * @file m3u8/line-stream.js 27183 */ 27184 /** 27185 * A stream that buffers string input and generates a `data` event for each 27186 * line. 27187 * 27188 * @class LineStream 27189 * @extends Stream 27190 */ 27191 27192 var LineStream = function (_Stream) { 27193 inherits$1(LineStream, _Stream); 27194 27195 function LineStream() { 27196 classCallCheck$1(this, LineStream); 27197 27198 var _this = possibleConstructorReturn$1(this, _Stream.call(this)); 27199 27200 _this.buffer = ''; 27201 return _this; 27202 } 27203 27204 /** 27205 * Add new data to be parsed. 27206 * 27207 * @param {String} data the text to process 27208 */ 27209 27210 LineStream.prototype.push = function push(data) { 27211 var nextNewline = void 0; 27212 27213 this.buffer += data; 27214 nextNewline = this.buffer.indexOf('\n'); 27215 27216 for (; nextNewline > -1; nextNewline = this.buffer.indexOf('\n')) { 27217 this.trigger('data', this.buffer.substring(0, nextNewline)); 27218 this.buffer = this.buffer.substring(nextNewline + 1); 27219 } 27220 }; 27221 27222 return LineStream; 27223 }(Stream); 27224 27225 /** 27226 * @file m3u8/parse-stream.js 27227 */ 27228 /** 27229 * "forgiving" attribute list psuedo-grammar: 27230 * attributes -> keyvalue (',' keyvalue)* 27231 * keyvalue -> key '=' value 27232 * key -> [^=]* 27233 * value -> '"' [^"]* '"' | [^,]* 27234 */ 27235 var attributeSeparator = function attributeSeparator() { 27236 var key = '[^=]*'; 27237 var value = '"[^"]*"|[^,]*'; 27238 var keyvalue = '(?:' + key + ')=(?:' + value + ')'; 27239 27240 return new RegExp('(?:^|,)(' + keyvalue + ')'); 27241 }; 27242 27243 /** 27244 * Parse attributes from a line given the seperator 27245 * 27246 * @param {String} attributes the attibute line to parse 27247 */ 27248 var parseAttributes = function parseAttributes(attributes) { 27249 // split the string using attributes as the separator 27250 var attrs = attributes.split(attributeSeparator()); 27251 var result = {}; 27252 var i = attrs.length; 27253 var attr = void 0; 27254 27255 while (i--) { 27256 // filter out unmatched portions of the string 27257 if (attrs[i] === '') { 27258 continue; 27259 } 27260 27261 // split the key and value 27262 attr = /([^=]*)=(.*)/.exec(attrs[i]).slice(1); 27263 // trim whitespace and remove optional quotes around the value 27264 attr[0] = attr[0].replace(/^\s+|\s+$/g, ''); 27265 attr[1] = attr[1].replace(/^\s+|\s+$/g, ''); 27266 attr[1] = attr[1].replace(/^['"](.*)['"]$/g, '$1'); 27267 result[attr[0]] = attr[1]; 27268 } 27269 return result; 27270 }; 27271 27272 /** 27273 * A line-level M3U8 parser event stream. It expects to receive input one 27274 * line at a time and performs a context-free parse of its contents. A stream 27275 * interpretation of a manifest can be useful if the manifest is expected to 27276 * be too large to fit comfortably into memory or the entirety of the input 27277 * is not immediately available. Otherwise, it's probably much easier to work 27278 * with a regular `Parser` object. 27279 * 27280 * Produces `data` events with an object that captures the parser's 27281 * interpretation of the input. That object has a property `tag` that is one 27282 * of `uri`, `comment`, or `tag`. URIs only have a single additional 27283 * property, `line`, which captures the entirety of the input without 27284 * interpretation. Comments similarly have a single additional property 27285 * `text` which is the input without the leading `#`. 27286 * 27287 * Tags always have a property `tagType` which is the lower-cased version of 27288 * the M3U8 directive without the `#EXT` or `#EXT-X-` prefix. For instance, 27289 * `#EXT-X-MEDIA-SEQUENCE` becomes `media-sequence` when parsed. Unrecognized 27290 * tags are given the tag type `unknown` and a single additional property 27291 * `data` with the remainder of the input. 27292 * 27293 * @class ParseStream 27294 * @extends Stream 27295 */ 27296 27297 var ParseStream = function (_Stream) { 27298 inherits$1(ParseStream, _Stream); 27299 27300 function ParseStream() { 27301 classCallCheck$1(this, ParseStream); 27302 27303 var _this = possibleConstructorReturn$1(this, _Stream.call(this)); 27304 27305 _this.customParsers = []; 27306 return _this; 27307 } 27308 27309 /** 27310 * Parses an additional line of input. 27311 * 27312 * @param {String} line a single line of an M3U8 file to parse 27313 */ 27314 27315 ParseStream.prototype.push = function push(line) { 27316 var match = void 0; 27317 var event = void 0; 27318 27319 // strip whitespace 27320 line = line.replace(/^[\u0000\s]+|[\u0000\s]+$/g, ''); 27321 if (line.length === 0) { 27322 // ignore empty lines 27323 return; 27324 } 27325 27326 // URIs 27327 if (line[0] !== '#') { 27328 this.trigger('data', { 27329 type: 'uri', 27330 uri: line 27331 }); 27332 return; 27333 } 27334 27335 for (var i = 0; i < this.customParsers.length; i++) { 27336 if (this.customParsers[i].call(this, line)) { 27337 return; 27338 } 27339 } 27340 27341 // Comments 27342 if (line.indexOf('#EXT') !== 0) { 27343 this.trigger('data', { 27344 type: 'comment', 27345 text: line.slice(1) 27346 }); 27347 return; 27348 } 27349 27350 // strip off any carriage returns here so the regex matching 27351 // doesn't have to account for them. 27352 line = line.replace('\r', ''); 27353 27354 // Tags 27355 match = /^#EXTM3U/.exec(line); 27356 if (match) { 27357 this.trigger('data', { 27358 type: 'tag', 27359 tagType: 'm3u' 27360 }); 27361 return; 27362 } 27363 match = /^#EXTINF:?([0-9\.]*)?,?(.*)?$/.exec(line); 27364 if (match) { 27365 event = { 27366 type: 'tag', 27367 tagType: 'inf' 27368 }; 27369 if (match[1]) { 27370 event.duration = parseFloat(match[1]); 27371 } 27372 if (match[2]) { 27373 event.title = match[2]; 27374 } 27375 this.trigger('data', event); 27376 return; 27377 } 27378 match = /^#EXT-X-TARGETDURATION:?([0-9.]*)?/.exec(line); 27379 if (match) { 27380 event = { 27381 type: 'tag', 27382 tagType: 'targetduration' 27383 }; 27384 if (match[1]) { 27385 event.duration = parseInt(match[1], 10); 27386 } 27387 this.trigger('data', event); 27388 return; 27389 } 27390 match = /^#ZEN-TOTAL-DURATION:?([0-9.]*)?/.exec(line); 27391 if (match) { 27392 event = { 27393 type: 'tag', 27394 tagType: 'totalduration' 27395 }; 27396 if (match[1]) { 27397 event.duration = parseInt(match[1], 10); 27398 } 27399 this.trigger('data', event); 27400 return; 27401 } 27402 match = /^#EXT-X-VERSION:?([0-9.]*)?/.exec(line); 27403 if (match) { 27404 event = { 27405 type: 'tag', 27406 tagType: 'version' 27407 }; 27408 if (match[1]) { 27409 event.version = parseInt(match[1], 10); 27410 } 27411 this.trigger('data', event); 27412 return; 27413 } 27414 match = /^#EXT-X-MEDIA-SEQUENCE:?(\-?[0-9.]*)?/.exec(line); 27415 if (match) { 27416 event = { 27417 type: 'tag', 27418 tagType: 'media-sequence' 27419 }; 27420 if (match[1]) { 27421 event.number = parseInt(match[1], 10); 27422 } 27423 this.trigger('data', event); 27424 return; 27425 } 27426 match = /^#EXT-X-DISCONTINUITY-SEQUENCE:?(\-?[0-9.]*)?/.exec(line); 27427 if (match) { 27428 event = { 27429 type: 'tag', 27430 tagType: 'discontinuity-sequence' 27431 }; 27432 if (match[1]) { 27433 event.number = parseInt(match[1], 10); 27434 } 27435 this.trigger('data', event); 27436 return; 27437 } 27438 match = /^#EXT-X-PLAYLIST-TYPE:?(.*)?$/.exec(line); 27439 if (match) { 27440 event = { 27441 type: 'tag', 27442 tagType: 'playlist-type' 27443 }; 27444 if (match[1]) { 27445 event.playlistType = match[1]; 27446 } 27447 this.trigger('data', event); 27448 return; 27449 } 27450 match = /^#EXT-X-BYTERANGE:?([0-9.]*)?@?([0-9.]*)?/.exec(line); 27451 if (match) { 27452 event = { 27453 type: 'tag', 27454 tagType: 'byterange' 27455 }; 27456 if (match[1]) { 27457 event.length = parseInt(match[1], 10); 27458 } 27459 if (match[2]) { 27460 event.offset = parseInt(match[2], 10); 27461 } 27462 this.trigger('data', event); 27463 return; 27464 } 27465 match = /^#EXT-X-ALLOW-CACHE:?(YES|NO)?/.exec(line); 27466 if (match) { 27467 event = { 27468 type: 'tag', 27469 tagType: 'allow-cache' 27470 }; 27471 if (match[1]) { 27472 event.allowed = !/NO/.test(match[1]); 27473 } 27474 this.trigger('data', event); 27475 return; 27476 } 27477 match = /^#EXT-X-MAP:?(.*)$/.exec(line); 27478 if (match) { 27479 event = { 27480 type: 'tag', 27481 tagType: 'map' 27482 }; 27483 27484 if (match[1]) { 27485 var attributes = parseAttributes(match[1]); 27486 27487 if (attributes.URI) { 27488 event.uri = attributes.URI; 27489 } 27490 if (attributes.BYTERANGE) { 27491 var _attributes$BYTERANGE = attributes.BYTERANGE.split('@'), 27492 length = _attributes$BYTERANGE[0], 27493 offset = _attributes$BYTERANGE[1]; 27494 27495 event.byterange = {}; 27496 if (length) { 27497 event.byterange.length = parseInt(length, 10); 27498 } 27499 if (offset) { 27500 event.byterange.offset = parseInt(offset, 10); 27501 } 27502 } 27503 } 27504 27505 this.trigger('data', event); 27506 return; 27507 } 27508 match = /^#EXT-X-STREAM-INF:?(.*)$/.exec(line); 27509 if (match) { 27510 event = { 27511 type: 'tag', 27512 tagType: 'stream-inf' 27513 }; 27514 if (match[1]) { 27515 event.attributes = parseAttributes(match[1]); 27516 27517 if (event.attributes.RESOLUTION) { 27518 var split = event.attributes.RESOLUTION.split('x'); 27519 var resolution = {}; 27520 27521 if (split[0]) { 27522 resolution.width = parseInt(split[0], 10); 27523 } 27524 if (split[1]) { 27525 resolution.height = parseInt(split[1], 10); 27526 } 27527 event.attributes.RESOLUTION = resolution; 27528 } 27529 if (event.attributes.BANDWIDTH) { 27530 event.attributes.BANDWIDTH = parseInt(event.attributes.BANDWIDTH, 10); 27531 } 27532 if (event.attributes['PROGRAM-ID']) { 27533 event.attributes['PROGRAM-ID'] = parseInt(event.attributes['PROGRAM-ID'], 10); 27534 } 27535 } 27536 this.trigger('data', event); 27537 return; 27538 } 27539 match = /^#EXT-X-MEDIA:?(.*)$/.exec(line); 27540 if (match) { 27541 event = { 27542 type: 'tag', 27543 tagType: 'media' 27544 }; 27545 if (match[1]) { 27546 event.attributes = parseAttributes(match[1]); 27547 } 27548 this.trigger('data', event); 27549 return; 27550 } 27551 match = /^#EXT-X-ENDLIST/.exec(line); 27552 if (match) { 27553 this.trigger('data', { 27554 type: 'tag', 27555 tagType: 'endlist' 27556 }); 27557 return; 27558 } 27559 match = /^#EXT-X-DISCONTINUITY/.exec(line); 27560 if (match) { 27561 this.trigger('data', { 27562 type: 'tag', 27563 tagType: 'discontinuity' 27564 }); 27565 return; 27566 } 27567 match = /^#EXT-X-PROGRAM-DATE-TIME:?(.*)$/.exec(line); 27568 if (match) { 27569 event = { 27570 type: 'tag', 27571 tagType: 'program-date-time' 27572 }; 27573 if (match[1]) { 27574 event.dateTimeString = match[1]; 27575 event.dateTimeObject = new Date(match[1]); 27576 } 27577 this.trigger('data', event); 27578 return; 27579 } 27580 match = /^#EXT-X-KEY:?(.*)$/.exec(line); 27581 if (match) { 27582 event = { 27583 type: 'tag', 27584 tagType: 'key' 27585 }; 27586 if (match[1]) { 27587 event.attributes = parseAttributes(match[1]); 27588 // parse the IV string into a Uint32Array 27589 if (event.attributes.IV) { 27590 if (event.attributes.IV.substring(0, 2).toLowerCase() === '0x') { 27591 event.attributes.IV = event.attributes.IV.substring(2); 27592 } 27593 27594 event.attributes.IV = event.attributes.IV.match(/.{8}/g); 27595 event.attributes.IV[0] = parseInt(event.attributes.IV[0], 16); 27596 event.attributes.IV[1] = parseInt(event.attributes.IV[1], 16); 27597 event.attributes.IV[2] = parseInt(event.attributes.IV[2], 16); 27598 event.attributes.IV[3] = parseInt(event.attributes.IV[3], 16); 27599 event.attributes.IV = new Uint32Array(event.attributes.IV); 27600 } 27601 } 27602 this.trigger('data', event); 27603 return; 27604 } 27605 match = /^#EXT-X-START:?(.*)$/.exec(line); 27606 if (match) { 27607 event = { 27608 type: 'tag', 27609 tagType: 'start' 27610 }; 27611 if (match[1]) { 27612 event.attributes = parseAttributes(match[1]); 27613 27614 event.attributes['TIME-OFFSET'] = parseFloat(event.attributes['TIME-OFFSET']); 27615 event.attributes.PRECISE = /YES/.test(event.attributes.PRECISE); 27616 } 27617 this.trigger('data', event); 27618 return; 27619 } 27620 match = /^#EXT-X-CUE-OUT-CONT:?(.*)?$/.exec(line); 27621 if (match) { 27622 event = { 27623 type: 'tag', 27624 tagType: 'cue-out-cont' 27625 }; 27626 if (match[1]) { 27627 event.data = match[1]; 27628 } else { 27629 event.data = ''; 27630 } 27631 this.trigger('data', event); 27632 return; 27633 } 27634 match = /^#EXT-X-CUE-OUT:?(.*)?$/.exec(line); 27635 if (match) { 27636 event = { 27637 type: 'tag', 27638 tagType: 'cue-out' 27639 }; 27640 if (match[1]) { 27641 event.data = match[1]; 27642 } else { 27643 event.data = ''; 27644 } 27645 this.trigger('data', event); 27646 return; 27647 } 27648 match = /^#EXT-X-CUE-IN:?(.*)?$/.exec(line); 27649 if (match) { 27650 event = { 27651 type: 'tag', 27652 tagType: 'cue-in' 27653 }; 27654 if (match[1]) { 27655 event.data = match[1]; 27656 } else { 27657 event.data = ''; 27658 } 27659 this.trigger('data', event); 27660 return; 27661 } 27662 27663 // unknown tag type 27664 this.trigger('data', { 27665 type: 'tag', 27666 data: line.slice(4) 27667 }); 27668 }; 27669 27670 /** 27671 * Add a parser for custom headers 27672 * 27673 * @param {Object} options a map of options for the added parser 27674 * @param {RegExp} options.expression a regular expression to match the custom header 27675 * @param {string} options.customType the custom type to register to the output 27676 * @param {Function} [options.dataParser] function to parse the line into an object 27677 * @param {boolean} [options.segment] should tag data be attached to the segment object 27678 */ 27679 27680 ParseStream.prototype.addParser = function addParser(_ref) { 27681 var _this2 = this; 27682 27683 var expression = _ref.expression, 27684 customType = _ref.customType, 27685 dataParser = _ref.dataParser, 27686 segment = _ref.segment; 27687 27688 if (typeof dataParser !== 'function') { 27689 dataParser = function dataParser(line) { 27690 return line; 27691 }; 27692 } 27693 this.customParsers.push(function (line) { 27694 var match = expression.exec(line); 27695 27696 if (match) { 27697 _this2.trigger('data', { 27698 type: 'custom', 27699 data: dataParser(line), 27700 customType: customType, 27701 segment: segment 27702 }); 27703 return true; 27704 } 27705 }); 27706 }; 27707 27708 return ParseStream; 27709 }(Stream); 27710 27711 /** 27712 * @file m3u8/parser.js 27713 */ 27714 /** 27715 * A parser for M3U8 files. The current interpretation of the input is 27716 * exposed as a property `manifest` on parser objects. It's just two lines to 27717 * create and parse a manifest once you have the contents available as a string: 27718 * 27719 * ```js 27720 * var parser = new m3u8.Parser(); 27721 * parser.push(xhr.responseText); 27722 * ``` 27723 * 27724 * New input can later be applied to update the manifest object by calling 27725 * `push` again. 27726 * 27727 * The parser attempts to create a usable manifest object even if the 27728 * underlying input is somewhat nonsensical. It emits `info` and `warning` 27729 * events during the parse if it encounters input that seems invalid or 27730 * requires some property of the manifest object to be defaulted. 27731 * 27732 * @class Parser 27733 * @extends Stream 27734 */ 27735 27736 var Parser = function (_Stream) { 27737 inherits$1(Parser, _Stream); 27738 27739 function Parser() { 27740 classCallCheck$1(this, Parser); 27741 27742 var _this = possibleConstructorReturn$1(this, _Stream.call(this)); 27743 27744 _this.lineStream = new LineStream(); 27745 _this.parseStream = new ParseStream(); 27746 _this.lineStream.pipe(_this.parseStream); 27747 27748 /* eslint-disable consistent-this */ 27749 var self = _this; 27750 /* eslint-enable consistent-this */ 27751 var uris = []; 27752 var currentUri = {}; 27753 // if specified, the active EXT-X-MAP definition 27754 var currentMap = void 0; 27755 // if specified, the active decryption key 27756 var _key = void 0; 27757 var noop = function noop() {}; 27758 var defaultMediaGroups = { 27759 'AUDIO': {}, 27760 'VIDEO': {}, 27761 'CLOSED-CAPTIONS': {}, 27762 'SUBTITLES': {} 27763 }; 27764 // group segments into numbered timelines delineated by discontinuities 27765 var currentTimeline = 0; 27766 27767 // the manifest is empty until the parse stream begins delivering data 27768 _this.manifest = { 27769 allowCache: true, 27770 discontinuityStarts: [], 27771 segments: [] 27772 }; 27773 27774 // update the manifest with the m3u8 entry from the parse stream 27775 _this.parseStream.on('data', function (entry) { 27776 var mediaGroup = void 0; 27777 var rendition = void 0; 27778 27779 ({ 27780 tag: function tag() { 27781 // switch based on the tag type 27782 (({ 27783 'allow-cache': function allowCache() { 27784 this.manifest.allowCache = entry.allowed; 27785 if (!('allowed' in entry)) { 27786 this.trigger('info', { 27787 message: 'defaulting allowCache to YES' 27788 }); 27789 this.manifest.allowCache = true; 27790 } 27791 }, 27792 byterange: function byterange() { 27793 var byterange = {}; 27794 27795 if ('length' in entry) { 27796 currentUri.byterange = byterange; 27797 byterange.length = entry.length; 27798 27799 if (!('offset' in entry)) { 27800 this.trigger('info', { 27801 message: 'defaulting offset to zero' 27802 }); 27803 entry.offset = 0; 27804 } 27805 } 27806 if ('offset' in entry) { 27807 currentUri.byterange = byterange; 27808 byterange.offset = entry.offset; 27809 } 27810 }, 27811 endlist: function endlist() { 27812 this.manifest.endList = true; 27813 }, 27814 inf: function inf() { 27815 if (!('mediaSequence' in this.manifest)) { 27816 this.manifest.mediaSequence = 0; 27817 this.trigger('info', { 27818 message: 'defaulting media sequence to zero' 27819 }); 27820 } 27821 if (!('discontinuitySequence' in this.manifest)) { 27822 this.manifest.discontinuitySequence = 0; 27823 this.trigger('info', { 27824 message: 'defaulting discontinuity sequence to zero' 27825 }); 27826 } 27827 if (entry.duration > 0) { 27828 currentUri.duration = entry.duration; 27829 } 27830 27831 if (entry.duration === 0) { 27832 currentUri.duration = 0.01; 27833 this.trigger('info', { 27834 message: 'updating zero segment duration to a small value' 27835 }); 27836 } 27837 27838 this.manifest.segments = uris; 27839 }, 27840 key: function key() { 27841 if (!entry.attributes) { 27842 this.trigger('warn', { 27843 message: 'ignoring key declaration without attribute list' 27844 }); 27845 return; 27846 } 27847 // clear the active encryption key 27848 if (entry.attributes.METHOD === 'NONE') { 27849 _key = null; 27850 return; 27851 } 27852 if (!entry.attributes.URI) { 27853 this.trigger('warn', { 27854 message: 'ignoring key declaration without URI' 27855 }); 27856 return; 27857 } 27858 if (!entry.attributes.METHOD) { 27859 this.trigger('warn', { 27860 message: 'defaulting key method to AES-128' 27861 }); 27862 } 27863 27864 // setup an encryption key for upcoming segments 27865 _key = { 27866 method: entry.attributes.METHOD || 'AES-128', 27867 uri: entry.attributes.URI 27868 }; 27869 27870 if (typeof entry.attributes.IV !== 'undefined') { 27871 _key.iv = entry.attributes.IV; 27872 } 27873 }, 27874 'media-sequence': function mediaSequence() { 27875 if (!isFinite(entry.number)) { 27876 this.trigger('warn', { 27877 message: 'ignoring invalid media sequence: ' + entry.number 27878 }); 27879 return; 27880 } 27881 this.manifest.mediaSequence = entry.number; 27882 }, 27883 'discontinuity-sequence': function discontinuitySequence() { 27884 if (!isFinite(entry.number)) { 27885 this.trigger('warn', { 27886 message: 'ignoring invalid discontinuity sequence: ' + entry.number 27887 }); 27888 return; 27889 } 27890 this.manifest.discontinuitySequence = entry.number; 27891 currentTimeline = entry.number; 27892 }, 27893 'playlist-type': function playlistType() { 27894 if (!/VOD|EVENT/.test(entry.playlistType)) { 27895 this.trigger('warn', { 27896 message: 'ignoring unknown playlist type: ' + entry.playlist 27897 }); 27898 return; 27899 } 27900 this.manifest.playlistType = entry.playlistType; 27901 }, 27902 map: function map() { 27903 currentMap = {}; 27904 if (entry.uri) { 27905 currentMap.uri = entry.uri; 27906 } 27907 if (entry.byterange) { 27908 currentMap.byterange = entry.byterange; 27909 } 27910 }, 27911 'stream-inf': function streamInf() { 27912 this.manifest.playlists = uris; 27913 this.manifest.mediaGroups = this.manifest.mediaGroups || defaultMediaGroups; 27914 27915 if (!entry.attributes) { 27916 this.trigger('warn', { 27917 message: 'ignoring empty stream-inf attributes' 27918 }); 27919 return; 27920 } 27921 27922 if (!currentUri.attributes) { 27923 currentUri.attributes = {}; 27924 } 27925 _extends$1(currentUri.attributes, entry.attributes); 27926 }, 27927 media: function media() { 27928 this.manifest.mediaGroups = this.manifest.mediaGroups || defaultMediaGroups; 27929 27930 if (!(entry.attributes && entry.attributes.TYPE && entry.attributes['GROUP-ID'] && entry.attributes.NAME)) { 27931 this.trigger('warn', { 27932 message: 'ignoring incomplete or missing media group' 27933 }); 27934 return; 27935 } 27936 27937 // find the media group, creating defaults as necessary 27938 var mediaGroupType = this.manifest.mediaGroups[entry.attributes.TYPE]; 27939 27940 mediaGroupType[entry.attributes['GROUP-ID']] = mediaGroupType[entry.attributes['GROUP-ID']] || {}; 27941 mediaGroup = mediaGroupType[entry.attributes['GROUP-ID']]; 27942 27943 // collect the rendition metadata 27944 rendition = { 27945 'default': /yes/i.test(entry.attributes.DEFAULT) 27946 }; 27947 if (rendition['default']) { 27948 rendition.autoselect = true; 27949 } else { 27950 rendition.autoselect = /yes/i.test(entry.attributes.AUTOSELECT); 27951 } 27952 if (entry.attributes.LANGUAGE) { 27953 rendition.language = entry.attributes.LANGUAGE; 27954 } 27955 if (entry.attributes.URI) { 27956 rendition.uri = entry.attributes.URI; 27957 } 27958 if (entry.attributes['INSTREAM-ID']) { 27959 rendition.instreamId = entry.attributes['INSTREAM-ID']; 27960 } 27961 if (entry.attributes.CHARACTERISTICS) { 27962 rendition.characteristics = entry.attributes.CHARACTERISTICS; 27963 } 27964 if (entry.attributes.FORCED) { 27965 rendition.forced = /yes/i.test(entry.attributes.FORCED); 27966 } 27967 27968 // insert the new rendition 27969 mediaGroup[entry.attributes.NAME] = rendition; 27970 }, 27971 discontinuity: function discontinuity() { 27972 currentTimeline += 1; 27973 currentUri.discontinuity = true; 27974 this.manifest.discontinuityStarts.push(uris.length); 27975 }, 27976 'program-date-time': function programDateTime() { 27977 if (typeof this.manifest.dateTimeString === 'undefined') { 27978 // PROGRAM-DATE-TIME is a media-segment tag, but for backwards 27979 // compatibility, we add the first occurence of the PROGRAM-DATE-TIME tag 27980 // to the manifest object 27981 // TODO: Consider removing this in future major version 27982 this.manifest.dateTimeString = entry.dateTimeString; 27983 this.manifest.dateTimeObject = entry.dateTimeObject; 27984 } 27985 27986 currentUri.dateTimeString = entry.dateTimeString; 27987 currentUri.dateTimeObject = entry.dateTimeObject; 27988 }, 27989 targetduration: function targetduration() { 27990 if (!isFinite(entry.duration) || entry.duration < 0) { 27991 this.trigger('warn', { 27992 message: 'ignoring invalid target duration: ' + entry.duration 27993 }); 27994 return; 27995 } 27996 this.manifest.targetDuration = entry.duration; 27997 }, 27998 totalduration: function totalduration() { 27999 if (!isFinite(entry.duration) || entry.duration < 0) { 28000 this.trigger('warn', { 28001 message: 'ignoring invalid total duration: ' + entry.duration 28002 }); 28003 return; 28004 } 28005 this.manifest.totalDuration = entry.duration; 28006 }, 28007 start: function start() { 28008 if (!entry.attributes || isNaN(entry.attributes['TIME-OFFSET'])) { 28009 this.trigger('warn', { 28010 message: 'ignoring start declaration without appropriate attribute list' 28011 }); 28012 return; 28013 } 28014 this.manifest.start = { 28015 timeOffset: entry.attributes['TIME-OFFSET'], 28016 precise: entry.attributes.PRECISE 28017 }; 28018 }, 28019 'cue-out': function cueOut() { 28020 currentUri.cueOut = entry.data; 28021 }, 28022 'cue-out-cont': function cueOutCont() { 28023 currentUri.cueOutCont = entry.data; 28024 }, 28025 'cue-in': function cueIn() { 28026 currentUri.cueIn = entry.data; 28027 } 28028 })[entry.tagType] || noop).call(self); 28029 }, 28030 uri: function uri() { 28031 currentUri.uri = entry.uri; 28032 uris.push(currentUri); 28033 28034 // if no explicit duration was declared, use the target duration 28035 if (this.manifest.targetDuration && !('duration' in currentUri)) { 28036 this.trigger('warn', { 28037 message: 'defaulting segment duration to the target duration' 28038 }); 28039 currentUri.duration = this.manifest.targetDuration; 28040 } 28041 // annotate with encryption information, if necessary 28042 if (_key) { 28043 currentUri.key = _key; 28044 } 28045 currentUri.timeline = currentTimeline; 28046 // annotate with initialization segment information, if necessary 28047 if (currentMap) { 28048 currentUri.map = currentMap; 28049 } 28050 28051 // prepare for the next URI 28052 currentUri = {}; 28053 }, 28054 comment: function comment() { 28055 // comments are not important for playback 28056 }, 28057 custom: function custom() { 28058 // if this is segment-level data attach the output to the segment 28059 if (entry.segment) { 28060 currentUri.custom = currentUri.custom || {}; 28061 currentUri.custom[entry.customType] = entry.data; 28062 // if this is manifest-level data attach to the top level manifest object 28063 } else { 28064 this.manifest.custom = this.manifest.custom || {}; 28065 this.manifest.custom[entry.customType] = entry.data; 28066 } 28067 } 28068 })[entry.type].call(self); 28069 }); 28070 return _this; 28071 } 28072 28073 /** 28074 * Parse the input string and update the manifest object. 28075 * 28076 * @param {String} chunk a potentially incomplete portion of the manifest 28077 */ 28078 28079 Parser.prototype.push = function push(chunk) { 28080 this.lineStream.push(chunk); 28081 }; 28082 28083 /** 28084 * Flush any remaining input. This can be handy if the last line of an M3U8 28085 * manifest did not contain a trailing newline but the file has been 28086 * completely received. 28087 */ 28088 28089 Parser.prototype.end = function end() { 28090 // flush any buffered input 28091 this.lineStream.push('\n'); 28092 }; 28093 /** 28094 * Add an additional parser for non-standard tags 28095 * 28096 * @param {Object} options a map of options for the added parser 28097 * @param {RegExp} options.expression a regular expression to match the custom header 28098 * @param {string} options.type the type to register to the output 28099 * @param {Function} [options.dataParser] function to parse the line into an object 28100 * @param {boolean} [options.segment] should tag data be attached to the segment object 28101 */ 28102 28103 Parser.prototype.addParser = function addParser(options) { 28104 this.parseStream.addParser(options); 28105 }; 28106 28107 return Parser; 28108 }(Stream); 28109 28110 /** 28111 * mpd-parser 28112 * @version 0.6.1 28113 * @copyright 2018 Brightcove, Inc 28114 * @license Apache-2.0 28115 */ 28116 28117 var formatAudioPlaylist = function formatAudioPlaylist(_ref) { 28118 var _attributes; 28119 28120 var attributes = _ref.attributes, 28121 segments = _ref.segments; 28122 28123 var playlist = { 28124 attributes: (_attributes = { 28125 NAME: attributes.id, 28126 BANDWIDTH: attributes.bandwidth, 28127 CODECS: attributes.codecs 28128 }, _attributes['PROGRAM-ID'] = 1, _attributes), 28129 uri: '', 28130 endList: (attributes.type || 'static') === 'static', 28131 timeline: attributes.periodIndex, 28132 resolvedUri: '', 28133 targetDuration: attributes.duration, 28134 segments: segments, 28135 mediaSequence: segments.length ? segments[0].number : 1 28136 }; 28137 28138 if (attributes.contentProtection) { 28139 playlist.contentProtection = attributes.contentProtection; 28140 } 28141 28142 return playlist; 28143 }; 28144 28145 var formatVttPlaylist = function formatVttPlaylist(_ref2) { 28146 var _attributes2; 28147 28148 var attributes = _ref2.attributes, 28149 segments = _ref2.segments; 28150 28151 if (typeof segments === 'undefined') { 28152 // vtt tracks may use single file in BaseURL 28153 segments = [{ 28154 uri: attributes.baseUrl, 28155 timeline: attributes.periodIndex, 28156 resolvedUri: attributes.baseUrl || '', 28157 duration: attributes.sourceDuration, 28158 number: 0 28159 }]; 28160 // targetDuration should be the same duration as the only segment 28161 attributes.duration = attributes.sourceDuration; 28162 } 28163 return { 28164 attributes: (_attributes2 = { 28165 NAME: attributes.id, 28166 BANDWIDTH: attributes.bandwidth 28167 }, _attributes2['PROGRAM-ID'] = 1, _attributes2), 28168 uri: '', 28169 endList: (attributes.type || 'static') === 'static', 28170 timeline: attributes.periodIndex, 28171 resolvedUri: attributes.baseUrl || '', 28172 targetDuration: attributes.duration, 28173 segments: segments, 28174 mediaSequence: segments.length ? segments[0].number : 1 28175 }; 28176 }; 28177 28178 var organizeAudioPlaylists = function organizeAudioPlaylists(playlists) { 28179 return playlists.reduce(function (a, playlist) { 28180 var role = playlist.attributes.role && playlist.attributes.role.value || 'main'; 28181 var language = playlist.attributes.lang || ''; 28182 28183 var label = 'main'; 28184 28185 if (language) { 28186 label = playlist.attributes.lang + ' (' + role + ')'; 28187 } 28188 28189 // skip if we already have the highest quality audio for a language 28190 if (a[label] && a[label].playlists[0].attributes.BANDWIDTH > playlist.attributes.bandwidth) { 28191 return a; 28192 } 28193 28194 a[label] = { 28195 language: language, 28196 autoselect: true, 28197 'default': role === 'main', 28198 playlists: [formatAudioPlaylist(playlist)], 28199 uri: '' 28200 }; 28201 28202 return a; 28203 }, {}); 28204 }; 28205 28206 var organizeVttPlaylists = function organizeVttPlaylists(playlists) { 28207 return playlists.reduce(function (a, playlist) { 28208 var label = playlist.attributes.lang || 'text'; 28209 28210 // skip if we already have subtitles 28211 if (a[label]) { 28212 return a; 28213 } 28214 28215 a[label] = { 28216 language: label, 28217 'default': false, 28218 autoselect: false, 28219 playlists: [formatVttPlaylist(playlist)], 28220 uri: '' 28221 }; 28222 28223 return a; 28224 }, {}); 28225 }; 28226 28227 var formatVideoPlaylist = function formatVideoPlaylist(_ref3) { 28228 var _attributes3; 28229 28230 var attributes = _ref3.attributes, 28231 segments = _ref3.segments; 28232 28233 var playlist = { 28234 attributes: (_attributes3 = { 28235 NAME: attributes.id, 28236 AUDIO: 'audio', 28237 SUBTITLES: 'subs', 28238 RESOLUTION: { 28239 width: attributes.width, 28240 height: attributes.height 28241 }, 28242 CODECS: attributes.codecs, 28243 BANDWIDTH: attributes.bandwidth 28244 }, _attributes3['PROGRAM-ID'] = 1, _attributes3), 28245 uri: '', 28246 endList: (attributes.type || 'static') === 'static', 28247 timeline: attributes.periodIndex, 28248 resolvedUri: '', 28249 targetDuration: attributes.duration, 28250 segments: segments, 28251 mediaSequence: segments.length ? segments[0].number : 1 28252 }; 28253 28254 if (attributes.contentProtection) { 28255 playlist.contentProtection = attributes.contentProtection; 28256 } 28257 28258 return playlist; 28259 }; 28260 28261 var toM3u8 = function toM3u8(dashPlaylists) { 28262 var _mediaGroups; 28263 28264 if (!dashPlaylists.length) { 28265 return {}; 28266 } 28267 28268 // grab all master attributes 28269 var _dashPlaylists$0$attr = dashPlaylists[0].attributes, 28270 duration = _dashPlaylists$0$attr.sourceDuration, 28271 _dashPlaylists$0$attr2 = _dashPlaylists$0$attr.minimumUpdatePeriod, 28272 minimumUpdatePeriod = _dashPlaylists$0$attr2 === undefined ? 0 : _dashPlaylists$0$attr2; 28273 28274 var videoOnly = function videoOnly(_ref4) { 28275 var attributes = _ref4.attributes; 28276 return attributes.mimeType === 'video/mp4' || attributes.contentType === 'video'; 28277 }; 28278 var audioOnly = function audioOnly(_ref5) { 28279 var attributes = _ref5.attributes; 28280 return attributes.mimeType === 'audio/mp4' || attributes.contentType === 'audio'; 28281 }; 28282 var vttOnly = function vttOnly(_ref6) { 28283 var attributes = _ref6.attributes; 28284 return attributes.mimeType === 'text/vtt' || attributes.contentType === 'text'; 28285 }; 28286 28287 var videoPlaylists = dashPlaylists.filter(videoOnly).map(formatVideoPlaylist); 28288 var audioPlaylists = dashPlaylists.filter(audioOnly); 28289 var vttPlaylists = dashPlaylists.filter(vttOnly); 28290 28291 var master = { 28292 allowCache: true, 28293 discontinuityStarts: [], 28294 segments: [], 28295 endList: true, 28296 mediaGroups: (_mediaGroups = { 28297 AUDIO: {}, 28298 VIDEO: {} 28299 }, _mediaGroups['CLOSED-CAPTIONS'] = {}, _mediaGroups.SUBTITLES = {}, _mediaGroups), 28300 uri: '', 28301 duration: duration, 28302 playlists: videoPlaylists, 28303 minimumUpdatePeriod: minimumUpdatePeriod * 1000 28304 }; 28305 28306 if (audioPlaylists.length) { 28307 master.mediaGroups.AUDIO.audio = organizeAudioPlaylists(audioPlaylists); 28308 } 28309 28310 if (vttPlaylists.length) { 28311 master.mediaGroups.SUBTITLES.subs = organizeVttPlaylists(vttPlaylists); 28312 } 28313 28314 return master; 28315 }; 28316 28317 var _typeof$1 = typeof Symbol === "function" && _typeof(Symbol.iterator) === "symbol" ? function (obj) { 28318 return typeof obj === 'undefined' ? 'undefined' : _typeof(obj); 28319 } : function (obj) { 28320 return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj === 'undefined' ? 'undefined' : _typeof(obj); 28321 }; 28322 28323 var isObject$1 = function isObject(obj) { 28324 return !!obj && (typeof obj === 'undefined' ? 'undefined' : _typeof$1(obj)) === 'object'; 28325 }; 28326 28327 var merge = function merge() { 28328 for (var _len = arguments.length, objects = Array(_len), _key = 0; _key < _len; _key++) { 28329 objects[_key] = arguments[_key]; 28330 } 28331 28332 return objects.reduce(function (result, source) { 28333 28334 Object.keys(source).forEach(function (key) { 28335 28336 if (Array.isArray(result[key]) && Array.isArray(source[key])) { 28337 result[key] = result[key].concat(source[key]); 28338 } else if (isObject$1(result[key]) && isObject$1(source[key])) { 28339 result[key] = merge(result[key], source[key]); 28340 } else { 28341 result[key] = source[key]; 28342 } 28343 }); 28344 return result; 28345 }, {}); 28346 }; 28347 28348 var resolveUrl = function resolveUrl(baseUrl, relativeUrl) { 28349 // return early if we don't need to resolve 28350 if (/^[a-z]+:/i.test(relativeUrl)) { 28351 return relativeUrl; 28352 } 28353 28354 // if the base URL is relative then combine with the current location 28355 if (!/\/\//i.test(baseUrl)) { 28356 baseUrl = urlToolkit.buildAbsoluteURL(window_1.location.href, baseUrl); 28357 } 28358 28359 return urlToolkit.buildAbsoluteURL(baseUrl, relativeUrl); 28360 }; 28361 28362 /** 28363 * @typedef {Object} SingleUri 28364 * @property {string} uri - relative location of segment 28365 * @property {string} resolvedUri - resolved location of segment 28366 * @property {Object} byterange - Object containing information on how to make byte range 28367 * requests following byte-range-spec per RFC2616. 28368 * @property {String} byterange.length - length of range request 28369 * @property {String} byterange.offset - byte offset of range request 28370 * 28371 * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.1 28372 */ 28373 28374 /** 28375 * Converts a URLType node (5.3.9.2.3 Table 13) to a segment object 28376 * that conforms to how m3u8-parser is structured 28377 * 28378 * @see https://github.com/videojs/m3u8-parser 28379 * 28380 * @param {string} baseUrl - baseUrl provided by <BaseUrl> nodes 28381 * @param {string} source - source url for segment 28382 * @param {string} range - optional range used for range calls, follows 28383 * @return {SingleUri} full segment information transformed into a format similar 28384 * to m3u8-parser 28385 */ 28386 var urlTypeToSegment = function urlTypeToSegment(_ref) { 28387 var _ref$baseUrl = _ref.baseUrl, 28388 baseUrl = _ref$baseUrl === undefined ? '' : _ref$baseUrl, 28389 _ref$source = _ref.source, 28390 source = _ref$source === undefined ? '' : _ref$source, 28391 _ref$range = _ref.range, 28392 range = _ref$range === undefined ? '' : _ref$range; 28393 28394 var init = { 28395 uri: source, 28396 resolvedUri: resolveUrl(baseUrl || '', source) 28397 }; 28398 28399 if (range) { 28400 var ranges = range.split('-'); 28401 var startRange = parseInt(ranges[0], 10); 28402 var endRange = parseInt(ranges[1], 10); 28403 28404 init.byterange = { 28405 length: endRange - startRange, 28406 offset: startRange 28407 }; 28408 } 28409 28410 return init; 28411 }; 28412 28413 /** 28414 * Calculates the R (repetition) value for a live stream (for the final segment 28415 * in a manifest where the r value is negative 1) 28416 * 28417 * @param {Object} attributes 28418 * Object containing all inherited attributes from parent elements with attribute 28419 * names as keys 28420 * @param {number} time 28421 * current time (typically the total time up until the final segment) 28422 * @param {number} duration 28423 * duration property for the given <S /> 28424 * 28425 * @return {number} 28426 * R value to reach the end of the given period 28427 */ 28428 var getLiveRValue = function getLiveRValue(attributes, time, duration) { 28429 var NOW = attributes.NOW, 28430 clientOffset = attributes.clientOffset, 28431 availabilityStartTime = attributes.availabilityStartTime, 28432 _attributes$timescale = attributes.timescale, 28433 timescale = _attributes$timescale === undefined ? 1 : _attributes$timescale, 28434 _attributes$start = attributes.start, 28435 start = _attributes$start === undefined ? 0 : _attributes$start, 28436 _attributes$minimumUp = attributes.minimumUpdatePeriod, 28437 minimumUpdatePeriod = _attributes$minimumUp === undefined ? 0 : _attributes$minimumUp; 28438 28439 var now = (NOW + clientOffset) / 1000; 28440 var periodStartWC = availabilityStartTime + start; 28441 var periodEndWC = now + minimumUpdatePeriod; 28442 var periodDuration = periodEndWC - periodStartWC; 28443 28444 return Math.ceil((periodDuration * timescale - time) / duration); 28445 }; 28446 28447 /** 28448 * Uses information provided by SegmentTemplate.SegmentTimeline to determine segment 28449 * timing and duration 28450 * 28451 * @param {Object} attributes 28452 * Object containing all inherited attributes from parent elements with attribute 28453 * names as keys 28454 * @param {Object[]} segmentTimeline 28455 * List of objects representing the attributes of each S element contained within 28456 * 28457 * @return {{number: number, duration: number, time: number, timeline: number}[]} 28458 * List of Objects with segment timing and duration info 28459 */ 28460 var parseByTimeline = function parseByTimeline(attributes, segmentTimeline) { 28461 var _attributes$type = attributes.type, 28462 type = _attributes$type === undefined ? 'static' : _attributes$type, 28463 _attributes$minimumUp2 = attributes.minimumUpdatePeriod, 28464 minimumUpdatePeriod = _attributes$minimumUp2 === undefined ? 0 : _attributes$minimumUp2, 28465 _attributes$media = attributes.media, 28466 media = _attributes$media === undefined ? '' : _attributes$media, 28467 sourceDuration = attributes.sourceDuration, 28468 _attributes$timescale2 = attributes.timescale, 28469 timescale = _attributes$timescale2 === undefined ? 1 : _attributes$timescale2, 28470 _attributes$startNumb = attributes.startNumber, 28471 startNumber = _attributes$startNumb === undefined ? 1 : _attributes$startNumb, 28472 timeline = attributes.periodIndex; 28473 28474 var segments = []; 28475 var time = -1; 28476 28477 for (var sIndex = 0; sIndex < segmentTimeline.length; sIndex++) { 28478 var S = segmentTimeline[sIndex]; 28479 var duration = S.d; 28480 var repeat = S.r || 0; 28481 var segmentTime = S.t || 0; 28482 28483 if (time < 0) { 28484 // first segment 28485 time = segmentTime; 28486 } 28487 28488 if (segmentTime && segmentTime > time) { 28489 // discontinuity 28490 28491 // TODO: How to handle this type of discontinuity 28492 // timeline++ here would treat it like HLS discontuity and content would 28493 // get appended without gap 28494 // E.G. 28495 // <S t="0" d="1" /> 28496 // <S d="1" /> 28497 // <S d="1" /> 28498 // <S t="5" d="1" /> 28499 // would have $Time$ values of [0, 1, 2, 5] 28500 // should this be appened at time positions [0, 1, 2, 3],(#EXT-X-DISCONTINUITY) 28501 // or [0, 1, 2, gap, gap, 5]? (#EXT-X-GAP) 28502 // does the value of sourceDuration consider this when calculating arbitrary 28503 // negative @r repeat value? 28504 // E.G. Same elements as above with this added at the end 28505 // <S d="1" r="-1" /> 28506 // with a sourceDuration of 10 28507 // Would the 2 gaps be included in the time duration calculations resulting in 28508 // 8 segments with $Time$ values of [0, 1, 2, 5, 6, 7, 8, 9] or 10 segments 28509 // with $Time$ values of [0, 1, 2, 5, 6, 7, 8, 9, 10, 11] ? 28510 28511 time = segmentTime; 28512 } 28513 28514 var count = void 0; 28515 28516 if (repeat < 0) { 28517 var nextS = sIndex + 1; 28518 28519 if (nextS === segmentTimeline.length) { 28520 // last segment 28521 if (type === 'dynamic' && minimumUpdatePeriod > 0 && media.indexOf('$Number$') > 0) { 28522 count = getLiveRValue(attributes, time, duration); 28523 } else { 28524 // TODO: This may be incorrect depending on conclusion of TODO above 28525 count = (sourceDuration * timescale - time) / duration; 28526 } 28527 } else { 28528 count = (segmentTimeline[nextS].t - time) / duration; 28529 } 28530 } else { 28531 count = repeat + 1; 28532 } 28533 28534 var end = startNumber + segments.length + count; 28535 var number = startNumber + segments.length; 28536 28537 while (number < end) { 28538 segments.push({ number: number, duration: duration / timescale, time: time, timeline: timeline }); 28539 time += duration; 28540 number++; 28541 } 28542 } 28543 28544 return segments; 28545 }; 28546 28547 var range = function range(start, end) { 28548 var result = []; 28549 28550 for (var i = start; i < end; i++) { 28551 result.push(i); 28552 } 28553 28554 return result; 28555 }; 28556 28557 var flatten = function flatten(lists) { 28558 return lists.reduce(function (x, y) { 28559 return x.concat(y); 28560 }, []); 28561 }; 28562 28563 var from = function from(list) { 28564 if (!list.length) { 28565 return []; 28566 } 28567 28568 var result = []; 28569 28570 for (var i = 0; i < list.length; i++) { 28571 result.push(list[i]); 28572 } 28573 28574 return result; 28575 }; 28576 28577 /** 28578 * Functions for calculating the range of available segments in static and dynamic 28579 * manifests. 28580 */ 28581 var segmentRange = { 28582 /** 28583 * Returns the entire range of available segments for a static MPD 28584 * 28585 * @param {Object} attributes 28586 * Inheritied MPD attributes 28587 * @return {{ start: number, end: number }} 28588 * The start and end numbers for available segments 28589 */ 28590 'static': function _static(attributes) { 28591 var duration = attributes.duration, 28592 _attributes$timescale = attributes.timescale, 28593 timescale = _attributes$timescale === undefined ? 1 : _attributes$timescale, 28594 sourceDuration = attributes.sourceDuration; 28595 28596 return { 28597 start: 0, 28598 end: Math.ceil(sourceDuration / (duration / timescale)) 28599 }; 28600 }, 28601 28602 /** 28603 * Returns the current live window range of available segments for a dynamic MPD 28604 * 28605 * @param {Object} attributes 28606 * Inheritied MPD attributes 28607 * @return {{ start: number, end: number }} 28608 * The start and end numbers for available segments 28609 */ 28610 dynamic: function dynamic(attributes) { 28611 var NOW = attributes.NOW, 28612 clientOffset = attributes.clientOffset, 28613 availabilityStartTime = attributes.availabilityStartTime, 28614 _attributes$timescale2 = attributes.timescale, 28615 timescale = _attributes$timescale2 === undefined ? 1 : _attributes$timescale2, 28616 duration = attributes.duration, 28617 _attributes$start = attributes.start, 28618 start = _attributes$start === undefined ? 0 : _attributes$start, 28619 _attributes$minimumUp = attributes.minimumUpdatePeriod, 28620 minimumUpdatePeriod = _attributes$minimumUp === undefined ? 0 : _attributes$minimumUp, 28621 _attributes$timeShift = attributes.timeShiftBufferDepth, 28622 timeShiftBufferDepth = _attributes$timeShift === undefined ? Infinity : _attributes$timeShift; 28623 28624 var now = (NOW + clientOffset) / 1000; 28625 var periodStartWC = availabilityStartTime + start; 28626 var periodEndWC = now + minimumUpdatePeriod; 28627 var periodDuration = periodEndWC - periodStartWC; 28628 var segmentCount = Math.ceil(periodDuration * timescale / duration); 28629 var availableStart = Math.floor((now - periodStartWC - timeShiftBufferDepth) * timescale / duration); 28630 var availableEnd = Math.floor((now - periodStartWC) * timescale / duration); 28631 28632 return { 28633 start: Math.max(0, availableStart), 28634 end: Math.min(segmentCount, availableEnd) 28635 }; 28636 } 28637 }; 28638 28639 /** 28640 * Maps a range of numbers to objects with information needed to build the corresponding 28641 * segment list 28642 * 28643 * @name toSegmentsCallback 28644 * @function 28645 * @param {number} number 28646 * Number of the segment 28647 * @param {number} index 28648 * Index of the number in the range list 28649 * @return {{ number: Number, duration: Number, timeline: Number, time: Number }} 28650 * Object with segment timing and duration info 28651 */ 28652 28653 /** 28654 * Returns a callback for Array.prototype.map for mapping a range of numbers to 28655 * information needed to build the segment list. 28656 * 28657 * @param {Object} attributes 28658 * Inherited MPD attributes 28659 * @return {toSegmentsCallback} 28660 * Callback map function 28661 */ 28662 var toSegments = function toSegments(attributes) { 28663 return function (number, index) { 28664 var duration = attributes.duration, 28665 _attributes$timescale3 = attributes.timescale, 28666 timescale = _attributes$timescale3 === undefined ? 1 : _attributes$timescale3, 28667 periodIndex = attributes.periodIndex, 28668 _attributes$startNumb = attributes.startNumber, 28669 startNumber = _attributes$startNumb === undefined ? 1 : _attributes$startNumb; 28670 28671 return { 28672 number: startNumber + number, 28673 duration: duration / timescale, 28674 timeline: periodIndex, 28675 time: index * duration 28676 }; 28677 }; 28678 }; 28679 28680 /** 28681 * Returns a list of objects containing segment timing and duration info used for 28682 * building the list of segments. This uses the @duration attribute specified 28683 * in the MPD manifest to derive the range of segments. 28684 * 28685 * @param {Object} attributes 28686 * Inherited MPD attributes 28687 * @return {{number: number, duration: number, time: number, timeline: number}[]} 28688 * List of Objects with segment timing and duration info 28689 */ 28690 var parseByDuration = function parseByDuration(attributes) { 28691 var _attributes$type = attributes.type, 28692 type = _attributes$type === undefined ? 'static' : _attributes$type, 28693 duration = attributes.duration, 28694 _attributes$timescale4 = attributes.timescale, 28695 timescale = _attributes$timescale4 === undefined ? 1 : _attributes$timescale4, 28696 sourceDuration = attributes.sourceDuration; 28697 28698 var _segmentRange$type = segmentRange[type](attributes), 28699 start = _segmentRange$type.start, 28700 end = _segmentRange$type.end; 28701 28702 var segments = range(start, end).map(toSegments(attributes)); 28703 28704 if (type === 'static') { 28705 var index = segments.length - 1; 28706 28707 // final segment may be less than full segment duration 28708 segments[index].duration = sourceDuration - duration / timescale * index; 28709 } 28710 28711 return segments; 28712 }; 28713 28714 var identifierPattern = /\$([A-z]*)(?:(%0)([0-9]+)d)?\$/g; 28715 28716 /** 28717 * Replaces template identifiers with corresponding values. To be used as the callback 28718 * for String.prototype.replace 28719 * 28720 * @name replaceCallback 28721 * @function 28722 * @param {string} match 28723 * Entire match of identifier 28724 * @param {string} identifier 28725 * Name of matched identifier 28726 * @param {string} format 28727 * Format tag string. Its presence indicates that padding is expected 28728 * @param {string} width 28729 * Desired length of the replaced value. Values less than this width shall be left 28730 * zero padded 28731 * @return {string} 28732 * Replacement for the matched identifier 28733 */ 28734 28735 /** 28736 * Returns a function to be used as a callback for String.prototype.replace to replace 28737 * template identifiers 28738 * 28739 * @param {Obect} values 28740 * Object containing values that shall be used to replace known identifiers 28741 * @param {number} values.RepresentationID 28742 * Value of the Representation@id attribute 28743 * @param {number} values.Number 28744 * Number of the corresponding segment 28745 * @param {number} values.Bandwidth 28746 * Value of the Representation@bandwidth attribute. 28747 * @param {number} values.Time 28748 * Timestamp value of the corresponding segment 28749 * @return {replaceCallback} 28750 * Callback to be used with String.prototype.replace to replace identifiers 28751 */ 28752 var identifierReplacement = function identifierReplacement(values) { 28753 return function (match, identifier, format, width) { 28754 if (match === '$$') { 28755 // escape sequence 28756 return '$'; 28757 } 28758 28759 if (typeof values[identifier] === 'undefined') { 28760 return match; 28761 } 28762 28763 var value = '' + values[identifier]; 28764 28765 if (identifier === 'RepresentationID') { 28766 // Format tag shall not be present with RepresentationID 28767 return value; 28768 } 28769 28770 if (!format) { 28771 width = 1; 28772 } else { 28773 width = parseInt(width, 10); 28774 } 28775 28776 if (value.length >= width) { 28777 return value; 28778 } 28779 28780 return '' + new Array(width - value.length + 1).join('0') + value; 28781 }; 28782 }; 28783 28784 /** 28785 * Constructs a segment url from a template string 28786 * 28787 * @param {string} url 28788 * Template string to construct url from 28789 * @param {Obect} values 28790 * Object containing values that shall be used to replace known identifiers 28791 * @param {number} values.RepresentationID 28792 * Value of the Representation@id attribute 28793 * @param {number} values.Number 28794 * Number of the corresponding segment 28795 * @param {number} values.Bandwidth 28796 * Value of the Representation@bandwidth attribute. 28797 * @param {number} values.Time 28798 * Timestamp value of the corresponding segment 28799 * @return {string} 28800 * Segment url with identifiers replaced 28801 */ 28802 var constructTemplateUrl = function constructTemplateUrl(url, values) { 28803 return url.replace(identifierPattern, identifierReplacement(values)); 28804 }; 28805 28806 /** 28807 * Generates a list of objects containing timing and duration information about each 28808 * segment needed to generate segment uris and the complete segment object 28809 * 28810 * @param {Object} attributes 28811 * Object containing all inherited attributes from parent elements with attribute 28812 * names as keys 28813 * @param {Object[]|undefined} segmentTimeline 28814 * List of objects representing the attributes of each S element contained within 28815 * the SegmentTimeline element 28816 * @return {{number: number, duration: number, time: number, timeline: number}[]} 28817 * List of Objects with segment timing and duration info 28818 */ 28819 var parseTemplateInfo = function parseTemplateInfo(attributes, segmentTimeline) { 28820 if (!attributes.duration && !segmentTimeline) { 28821 // if neither @duration or SegmentTimeline are present, then there shall be exactly 28822 // one media segment 28823 return [{ 28824 number: attributes.startNumber || 1, 28825 duration: attributes.sourceDuration, 28826 time: 0, 28827 timeline: attributes.periodIndex 28828 }]; 28829 } 28830 28831 if (attributes.duration) { 28832 return parseByDuration(attributes); 28833 } 28834 28835 return parseByTimeline(attributes, segmentTimeline); 28836 }; 28837 28838 /** 28839 * Generates a list of segments using information provided by the SegmentTemplate element 28840 * 28841 * @param {Object} attributes 28842 * Object containing all inherited attributes from parent elements with attribute 28843 * names as keys 28844 * @param {Object[]|undefined} segmentTimeline 28845 * List of objects representing the attributes of each S element contained within 28846 * the SegmentTimeline element 28847 * @return {Object[]} 28848 * List of segment objects 28849 */ 28850 var segmentsFromTemplate = function segmentsFromTemplate(attributes, segmentTimeline) { 28851 var templateValues = { 28852 RepresentationID: attributes.id, 28853 Bandwidth: attributes.bandwidth || 0 28854 }; 28855 28856 var _attributes$initializ = attributes.initialization, 28857 initialization = _attributes$initializ === undefined ? { sourceURL: '', range: '' } : _attributes$initializ; 28858 28859 var mapSegment = urlTypeToSegment({ 28860 baseUrl: attributes.baseUrl, 28861 source: constructTemplateUrl(initialization.sourceURL, templateValues), 28862 range: initialization.range 28863 }); 28864 28865 var segments = parseTemplateInfo(attributes, segmentTimeline); 28866 28867 return segments.map(function (segment) { 28868 templateValues.Number = segment.number; 28869 templateValues.Time = segment.time; 28870 28871 var uri = constructTemplateUrl(attributes.media || '', templateValues); 28872 28873 return { 28874 uri: uri, 28875 timeline: segment.timeline, 28876 duration: segment.duration, 28877 resolvedUri: resolveUrl(attributes.baseUrl || '', uri), 28878 map: mapSegment, 28879 number: segment.number 28880 }; 28881 }); 28882 }; 28883 28884 var errors = {
28885 INVALID_NUMBER_OF_PERIOD: 'INVALID_NUMBER_OF_PERIOD', 28886 DASH_EMPTY_MANIFEST: 'DASH_EMPTY_MANIFEST', 28887 DASH_INVALID_XML: 'DASH_INVALID_XML', 28888 NO_BASE_URL: 'NO_BASE_URL', 28889 MISSING_SEGMENT_INFORMATION: 'MISSING_SEGMENT_INFORMATION', 28890 SEGMENT_TIME_UNSPECIFIED: 'SEGMENT_TIME_UNSPECIFIED', 28891 UNSUPPORTED_UTC_TIMING_SCHEME: 'UNSUPPORTED_UTC_TIMING_SCHEME' 28892 }; 28893 28894 /** 28895 * Converts a <SegmentUrl> (of type URLType from the DASH spec 5.3.9.2 Table 14) 28896 * to an object that matches the output of a segment in videojs/mpd-parser 28897 * 28898 * @param {Object} attributes 28899 * Object containing all inherited attributes from parent elements with attribute 28900 * names as keys 28901 * @param {Object} segmentUrl 28902 * <SegmentURL> node to translate into a segment object 28903 * @return {Object} translated segment object 28904 */ 28905 var SegmentURLToSegmentObject = function SegmentURLToSegmentObject(attributes, segmentUrl) { 28906 var baseUrl = attributes.baseUrl, 28907 _attributes$initializ = attributes.initialization, 28908 initialization = _attributes$initializ === undefined ? {} : _attributes$initializ; 28909 28910 var initSegment = urlTypeToSegment({ 28911 baseUrl: baseUrl, 28912 source: initialization.sourceURL, 28913 range: initialization.range 28914 }); 28915 28916 var segment = urlTypeToSegment({ 28917 baseUrl: baseUrl, 28918 source: segmentUrl.media, 28919 range: segmentUrl.mediaRange 28920 }); 28921 28922 segment.map = initSegment; 28923 28924 return segment; 28925 }; 28926 28927 /** 28928 * Generates a list of segments using information provided by the SegmentList element 28929 * SegmentList (DASH SPEC Section 5.3.9.3.2) contains a set of <SegmentURL> nodes. Each 28930 * node should be translated into segment. 28931 * 28932 * @param {Object} attributes 28933 * Object containing all inherited attributes from parent elements with attribute 28934 * names as keys 28935 * @param {Object[]|undefined} segmentTimeline 28936 * List of objects representing the attributes of each S element contained within 28937 * the SegmentTimeline element 28938 * @return {Object.<Array>} list of segments 28939 */
vendor: 18,658 bytes, lines 28940-29493
28940 var segmentsFromList = function segmentsFromList(attributes, segmentTimeline) { 28941 var duration = attributes.duration, 28942 _attributes$segmentUr = attributes.segmentUrls, 28943 segmentUrls = _attributes$segmentUr === undefined ? [] : _attributes$segmentUr; 28944 28945 // Per spec (5.3.9.2.1) no way to determine segment duration OR 28946 // if both SegmentTimeline and @duration are defined, it is outside of spec. 28947 28948 if (!duration && !segmentTimeline || duration && segmentTimeline) { 28949 throw new Error(errors.SEGMENT_TIME_UNSPECIFIED); 28950 } 28951 28952 var segmentUrlMap = segmentUrls.map(function (segmentUrlObject) { 28953 return SegmentURLToSegmentObject(attributes, segmentUrlObject); 28954 }); 28955 var segmentTimeInfo = void 0; 28956 28957 if (duration) { 28958 segmentTimeInfo = parseByDuration(attributes); 28959 } 28960 28961 if (segmentTimeline) { 28962 segmentTimeInfo = parseByTimeline(attributes, segmentTimeline); 28963 } 28964 28965 var segments = segmentTimeInfo.map(function (segmentTime, index) { 28966 if (segmentUrlMap[index]) { 28967 var segment = segmentUrlMap[index]; 28968 28969 segment.timeline = segmentTime.timeline; 28970 segment.duration = segmentTime.duration; 28971 segment.number = segmentTime.number; 28972 return segment; 28973 } 28974 // Since we're mapping we should get rid of any blank segments (in case 28975 // the given SegmentTimeline is handling for more elements than we have 28976 // SegmentURLs for). 28977 }).filter(function (segment) { 28978 return segment; 28979 }); 28980 28981 return segments; 28982 }; 28983 28984 /** 28985 * Translates SegmentBase into a set of segments. 28986 * (DASH SPEC Section 5.3.9.3.2) contains a set of <SegmentURL> nodes. Each 28987 * node should be translated into segment. 28988 * 28989 * @param {Object} attributes 28990 * Object containing all inherited attributes from parent elements with attribute 28991 * names as keys 28992 * @return {Object.<Array>} list of segments 28993 */ 28994 var segmentsFromBase = function segmentsFromBase(attributes) { 28995 var baseUrl = attributes.baseUrl, 28996 _attributes$initializ = attributes.initialization, 28997 initialization = _attributes$initializ === undefined ? {} : _attributes$initializ, 28998 sourceDuration = attributes.sourceDuration, 28999 _attributes$timescale = attributes.timescale, 29000 timescale = _attributes$timescale === undefined ? 1 : _attributes$timescale, 29001 _attributes$indexRang = attributes.indexRange, 29002 indexRange = _attributes$indexRang === undefined ? '' : _attributes$indexRang, 29003 duration = attributes.duration; 29004 29005 // base url is required for SegmentBase to work, per spec (Section 5.3.9.2.1) 29006 29007 if (!baseUrl) { 29008 throw new Error(errors.NO_BASE_URL); 29009 } 29010 29011 var initSegment = urlTypeToSegment({ 29012 baseUrl: baseUrl, 29013 source: initialization.sourceURL, 29014 range: initialization.range 29015 }); 29016 var segment = urlTypeToSegment({ baseUrl: baseUrl, source: baseUrl, range: indexRange }); 29017 29018 segment.map = initSegment; 29019 29020 // If there is a duration, use it, otherwise use the given duration of the source 29021 // (since SegmentBase is only for one total segment) 29022 if (duration) { 29023 var segmentTimeInfo = parseByDuration(attributes); 29024 29025 if (segmentTimeInfo.length) { 29026 segment.duration = segmentTimeInfo[0].duration; 29027 segment.timeline = segmentTimeInfo[0].timeline; 29028 } 29029 } else if (sourceDuration) { 29030 segment.duration = sourceDuration / timescale; 29031 segment.timeline = 0; 29032 } 29033 29034 // This is used for mediaSequence 29035 segment.number = 0; 29036 29037 return [segment]; 29038 }; 29039 29040 var generateSegments = function generateSegments(_ref) { 29041 var attributes = _ref.attributes, 29042 segmentInfo = _ref.segmentInfo; 29043 29044 var segmentAttributes = void 0; 29045 var segmentsFn = void 0; 29046 29047 if (segmentInfo.template) { 29048 segmentsFn = segmentsFromTemplate; 29049 segmentAttributes = merge(attributes, segmentInfo.template); 29050 } else if (segmentInfo.base) { 29051 segmentsFn = segmentsFromBase; 29052 segmentAttributes = merge(attributes, segmentInfo.base); 29053 } else if (segmentInfo.list) { 29054 segmentsFn = segmentsFromList; 29055 segmentAttributes = merge(attributes, segmentInfo.list); 29056 } 29057 29058 if (!segmentsFn) { 29059 return { attributes: attributes }; 29060 } 29061 29062 var segments = segmentsFn(segmentAttributes, segmentInfo.timeline); 29063 29064 // The @duration attribute will be used to determin the playlist's targetDuration which 29065 // must be in seconds. Since we've generated the segment list, we no longer need 29066 // @duration to be in @timescale units, so we can convert it here. 29067 if (segmentAttributes.duration) { 29068 var _segmentAttributes = segmentAttributes, 29069 duration = _segmentAttributes.duration, 29070 _segmentAttributes$ti = _segmentAttributes.timescale, 29071 timescale = _segmentAttributes$ti === undefined ? 1 : _segmentAttributes$ti; 29072 29073 segmentAttributes.duration = duration / timescale; 29074 } else if (segments.length) { 29075 // if there is no @duration attribute, use the largest segment duration as 29076 // as target duration 29077 segmentAttributes.duration = segments.reduce(function (max, segment) { 29078 return Math.max(max, Math.ceil(segment.duration)); 29079 }, 0); 29080 } else { 29081 segmentAttributes.duration = 0; 29082 } 29083 29084 return { 29085 attributes: segmentAttributes, 29086 segments: segments 29087 }; 29088 }; 29089 29090 var toPlaylists = function toPlaylists(representations) { 29091 return representations.map(generateSegments); 29092 }; 29093 29094 var findChildren = function findChildren(element, name) { 29095 return from(element.childNodes).filter(function (_ref) { 29096 var tagName = _ref.tagName; 29097 return tagName === name; 29098 }); 29099 }; 29100 29101 var getContent = function getContent(element) { 29102 return element.textContent.trim(); 29103 }; 29104 29105 var parseDuration = function parseDuration(str) { 29106 var SECONDS_IN_YEAR = 365 * 24 * 60 * 60; 29107 var SECONDS_IN_MONTH = 30 * 24 * 60 * 60; 29108 var SECONDS_IN_DAY = 24 * 60 * 60; 29109 var SECONDS_IN_HOUR = 60 * 60; 29110 var SECONDS_IN_MIN = 60; 29111 29112 // P10Y10M10DT10H10M10.1S 29113 var durationRegex = /P(?:(\d*)Y)?(?:(\d*)M)?(?:(\d*)D)?(?:T(?:(\d*)H)?(?:(\d*)M)?(?:([\d.]*)S)?)?/; 29114 var match = durationRegex.exec(str); 29115 29116 if (!match) { 29117 return 0; 29118 } 29119 29120 var _match$slice = match.slice(1), 29121 year = _match$slice[0], 29122 month = _match$slice[1], 29123 day = _match$slice[2], 29124 hour = _match$slice[3], 29125 minute = _match$slice[4], 29126 second = _match$slice[5]; 29127 29128 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); 29129 }; 29130 29131 var parseDate = function parseDate(str) { 29132 // Date format without timezone according to ISO 8601 29133 // YYY-MM-DDThh:mm:ss.ssssss 29134 var dateRegex = /^\d+-\d+-\d+T\d+:\d+:\d+(\.\d+)?$/; 29135 29136 // If the date string does not specifiy a timezone, we must specifiy UTC. This is 29137 // expressed by ending with 'Z' 29138 if (dateRegex.test(str)) { 29139 str += 'Z'; 29140 } 29141 29142 return Date.parse(str); 29143 }; 29144 29145 // TODO: maybe order these in some way that makes it easy to find specific attributes 29146 var parsers = { 29147 /** 29148 * Specifies the duration of the entire Media Presentation. Format is a duration string 29149 * as specified in ISO 8601 29150 * 29151 * @param {string} value 29152 * value of attribute as a string 29153 * @return {number} 29154 * The duration in seconds 29155 */ 29156 mediaPresentationDuration: function mediaPresentationDuration(value) { 29157 return parseDuration(value); 29158 }, 29159 29160 /** 29161 * Specifies the Segment availability start time for all Segments referred to in this 29162 * MPD. For a dynamic manifest, it specifies the anchor for the earliest availability 29163 * time. Format is a date string as specified in ISO 8601 29164 * 29165 * @param {string} value 29166 * value of attribute as a string 29167 * @return {number} 29168 * The date as seconds from unix epoch 29169 */ 29170 availabilityStartTime: function availabilityStartTime(value) { 29171 return parseDate(value) / 1000; 29172 }, 29173 29174 /** 29175 * Specifies the smallest period between potential changes to the MPD. Format is a 29176 * duration string as specified in ISO 8601 29177 * 29178 * @param {string} value 29179 * value of attribute as a string 29180 * @return {number} 29181 * The duration in seconds 29182 */ 29183 minimumUpdatePeriod: function minimumUpdatePeriod(value) { 29184 return parseDuration(value); 29185 }, 29186 29187 /** 29188 * Specifies the duration of the smallest time shifting buffer for any Representation 29189 * in the MPD. Format is a duration string as specified in ISO 8601 29190 * 29191 * @param {string} value 29192 * value of attribute as a string 29193 * @return {number} 29194 * The duration in seconds 29195 */ 29196 timeShiftBufferDepth: function timeShiftBufferDepth(value) { 29197 return parseDuration(value); 29198 }, 29199 29200 /** 29201 * Specifies the PeriodStart time of the Period relative to the availabilityStarttime. 29202 * Format is a duration string as specified in ISO 8601 29203 * 29204 * @param {string} value 29205 * value of attribute as a string 29206 * @return {number} 29207 * The duration in seconds 29208 */ 29209 start: function start(value) { 29210 return parseDuration(value); 29211 }, 29212 29213 /** 29214 * Specifies the width of the visual presentation 29215 * 29216 * @param {string} value 29217 * value of attribute as a string 29218 * @return {number} 29219 * The parsed width 29220 */ 29221 width: function width(value) { 29222 return parseInt(value, 10); 29223 }, 29224 29225 /** 29226 * Specifies the height of the visual presentation 29227 * 29228 * @param {string} value 29229 * value of attribute as a string 29230 * @return {number} 29231 * The parsed height 29232 */ 29233 height: function height(value) { 29234 return parseInt(value, 10); 29235 }, 29236 29237 /** 29238 * Specifies the bitrate of the representation 29239 * 29240 * @param {string} value 29241 * value of attribute as a string 29242 * @return {number} 29243 * The parsed bandwidth 29244 */ 29245 bandwidth: function bandwidth(value) { 29246 return parseInt(value, 10); 29247 }, 29248 29249 /** 29250 * Specifies the number of the first Media Segment in this Representation in the Period 29251 * 29252 * @param {string} value 29253 * value of attribute as a string 29254 * @return {number} 29255 * The parsed number 29256 */ 29257 startNumber: function startNumber(value) { 29258 return parseInt(value, 10); 29259 }, 29260 29261 /** 29262 * Specifies the timescale in units per seconds 29263 * 29264 * @param {string} value 29265 * value of attribute as a string 29266 * @return {number} 29267 * The aprsed timescale 29268 */ 29269 timescale: function timescale(value) { 29270 return parseInt(value, 10); 29271 }, 29272 29273 /** 29274 * Specifies the constant approximate Segment duration 29275 * NOTE: The <Period> element also contains an @duration attribute. This duration 29276 * specifies the duration of the Period. This attribute is currently not 29277 * supported by the rest of the parser, however we still check for it to prevent 29278 * errors. 29279 * 29280 * @param {string} value 29281 * value of attribute as a string 29282 * @return {number} 29283 * The parsed duration 29284 */ 29285 duration: function duration(value) { 29286 var parsedValue = parseInt(value, 10); 29287 29288 if (isNaN(parsedValue)) { 29289 return parseDuration(value); 29290 } 29291 29292 return parsedValue; 29293 }, 29294 29295 /** 29296 * Specifies the Segment duration, in units of the value of the @timescale. 29297 * 29298 * @param {string} value 29299 * value of attribute as a string 29300 * @return {number} 29301 * The parsed duration 29302 */ 29303 d: function d(value) { 29304 return parseInt(value, 10); 29305 }, 29306 29307 /** 29308 * Specifies the MPD start time, in @timescale units, the first Segment in the series 29309 * starts relative to the beginning of the Period 29310 * 29311 * @param {string} value 29312 * value of attribute as a string 29313 * @return {number} 29314 * The parsed time 29315 */ 29316 t: function t(value) { 29317 return parseInt(value, 10); 29318 }, 29319 29320 /** 29321 * Specifies the repeat count of the number of following contiguous Segments with the 29322 * same duration expressed by the value of @d 29323 * 29324 * @param {string} value 29325 * value of attribute as a string 29326 * @return {number} 29327 * The parsed number 29328 */ 29329 r: function r(value) { 29330 return parseInt(value, 10); 29331 }, 29332 29333 /** 29334 * Default parser for all other attributes. Acts as a no-op and just returns the value 29335 * as a string 29336 * 29337 * @param {string} value 29338 * value of attribute as a string 29339 * @return {string} 29340 * Unparsed value 29341 */ 29342 DEFAULT: function DEFAULT(value) { 29343 return value; 29344 } 29345 }; 29346 29347 /** 29348 * Gets all the attributes and values of the provided node, parses attributes with known 29349 * types, and returns an object with attribute names mapped to values. 29350 * 29351 * @param {Node} el 29352 * The node to parse attributes from 29353 * @return {Object} 29354 * Object with all attributes of el parsed 29355 */ 29356 var parseAttributes$1 = function parseAttributes(el) { 29357 if (!(el && el.attributes)) { 29358 return {}; 29359 } 29360 29361 return from(el.attributes).reduce(function (a, e) { 29362 var parseFn = parsers[e.name] || parsers.DEFAULT; 29363 29364 a[e.name] = parseFn(e.value); 29365 29366 return a; 29367 }, {}); 29368 }; 29369 29370 function decodeB64ToUint8Array(b64Text) { 29371 var decodedString = window_1.atob(b64Text); 29372 var array = new Uint8Array(decodedString.length); 29373 29374 for (var i = 0; i < decodedString.length; i++) { 29375 array[i] = decodedString.charCodeAt(i); 29376 } 29377 return array; 29378 } 29379 29380 var keySystemsMap = { 29381 'urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b': 'org.w3.clearkey', 29382 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed': 'com.widevine.alpha', 29383 'urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95': 'com.microsoft.playready', 29384 'urn:uuid:f239e769-efa3-4850-9c16-a903c6932efb': 'com.adobe.primetime' 29385 }; 29386 29387 /** 29388 * Builds a list of urls that is the product of the reference urls and BaseURL values 29389 * 29390 * @param {string[]} referenceUrls 29391 * List of reference urls to resolve to 29392 * @param {Node[]} baseUrlElements 29393 * List of BaseURL nodes from the mpd 29394 * @return {string[]} 29395 * List of resolved urls 29396 */ 29397 var buildBaseUrls = function buildBaseUrls(referenceUrls, baseUrlElements) { 29398 if (!baseUrlElements.length) { 29399 return referenceUrls; 29400 } 29401 29402 return flatten(referenceUrls.map(function (reference) { 29403 return baseUrlElements.map(function (baseUrlElement) { 29404 return resolveUrl(reference, getContent(baseUrlElement)); 29405 }); 29406 })); 29407 }; 29408 29409 /** 29410 * Contains all Segment information for its containing AdaptationSet 29411 * 29412 * @typedef {Object} SegmentInformation 29413 * @property {Object|undefined} template 29414 * Contains the attributes for the SegmentTemplate node 29415 * @property {Object[]|undefined} timeline 29416 * Contains a list of atrributes for each S node within the SegmentTimeline node 29417 * @property {Object|undefined} list 29418 * Contains the attributes for the SegmentList node 29419 * @property {Object|undefined} base 29420 * Contains the attributes for the SegmentBase node 29421 */ 29422 29423 /** 29424 * Returns all available Segment information contained within the AdaptationSet node 29425 * 29426 * @param {Node} adaptationSet 29427 * The AdaptationSet node to get Segment information from 29428 * @return {SegmentInformation} 29429 * The Segment information contained within the provided AdaptationSet 29430 */ 29431 var getSegmentInformation = function getSegmentInformation(adaptationSet) { 29432 var segmentTemplate = findChildren(adaptationSet, 'SegmentTemplate')[0]; 29433 var segmentList = findChildren(adaptationSet, 'SegmentList')[0]; 29434 var segmentUrls = segmentList && findChildren(segmentList, 'SegmentURL').map(function (s) { 29435 return merge({ tag: 'SegmentURL' }, parseAttributes$1(s)); 29436 }); 29437 var segmentBase = findChildren(adaptationSet, 'SegmentBase')[0]; 29438 var segmentTimelineParentNode = segmentList || segmentTemplate; 29439 var segmentTimeline = segmentTimelineParentNode && findChildren(segmentTimelineParentNode, 'SegmentTimeline')[0]; 29440 var segmentInitializationParentNode = segmentList || segmentBase || segmentTemplate; 29441 var segmentInitialization = segmentInitializationParentNode && findChildren(segmentInitializationParentNode, 'Initialization')[0]; 29442 29443 // SegmentTemplate is handled slightly differently, since it can have both 29444 // @initialization and an <Initialization> node. @initialization can be templated, 29445 // while the node can have a url and range specified. If the <SegmentTemplate> has 29446 // both @initialization and an <Initialization> subelement we opt to override with 29447 // the node, as this interaction is not defined in the spec. 29448 var template = segmentTemplate && parseAttributes$1(segmentTemplate); 29449 29450 if (template && segmentInitialization) { 29451 template.initialization = segmentInitialization && parseAttributes$1(segmentInitialization); 29452 } else if (template && template.initialization) { 29453 // If it is @initialization we convert it to an object since this is the format that 29454 // later functions will rely on for the initialization segment. This is only valid 29455 // for <SegmentTemplate> 29456 template.initialization = { sourceURL: template.initialization }; 29457 } 29458 29459 var segmentInfo = { 29460 template: template, 29461 timeline: segmentTimeline && findChildren(segmentTimeline, 'S').map(function (s) { 29462 return parseAttributes$1(s); 29463 }), 29464 list: segmentList && merge(parseAttributes$1(segmentList), { 29465 segmentUrls: segmentUrls, 29466 initialization: parseAttributes$1(segmentInitialization) 29467 }), 29468 base: segmentBase && merge(parseAttributes$1(segmentBase), { 29469 initialization: parseAttributes$1(segmentInitialization) 29470 }) 29471 }; 29472 29473 Object.keys(segmentInfo).forEach(function (key) { 29474 if (!segmentInfo[key]) { 29475 delete segmentInfo[key]; 29476 } 29477 }); 29478 29479 return segmentInfo; 29480 }; 29481 29482 /** 29483 * Contains Segment information and attributes needed to construct a Playlist object 29484 * from a Representation 29485 * 29486 * @typedef {Object} RepresentationInformation 29487 * @property {SegmentInformation} segmentInfo 29488 * Segment information for this Representation 29489 * @property {Object} attributes 29490 * Inherited attributes for this Representation 29491 */ 29492 29493 /**
29494 * Maps a Representation node to an object containing Segment information and attributes 29495 * 29496 * @name inheritBaseUrlsCallback 29497 * @function 29498 * @param {Node} representation 29499 * Representation node from the mpd 29500 * @return {RepresentationInformation} 29501 * Representation information needed to construct a Playlist object 29502 */ 29503 29504 /** 29505 * Returns a callback for Array.prototype.map for mapping Representation nodes to 29506 * Segment information and attributes using inherited BaseURL nodes. 29507 * 29508 * @param {Object} adaptationSetAttributes 29509 * Contains attributes inherited by the AdaptationSet 29510 * @param {string[]} adaptationSetBaseUrls 29511 * Contains list of resolved base urls inherited by the AdaptationSet 29512 * @param {SegmentInformation} adaptationSetSegmentInfo 29513 * Contains Segment information for the AdaptationSet 29514 * @return {inheritBaseUrlsCallback} 29515 * Callback map function 29516 */ 29517 var inheritBaseUrls = function inheritBaseUrls(adaptationSetAttributes, adaptationSetBaseUrls, adaptationSetSegmentInfo) { 29518 return function (representation) { 29519 var repBaseUrlElements = findChildren(representation, 'BaseURL'); 29520 var repBaseUrls = buildBaseUrls(adaptationSetBaseUrls, repBaseUrlElements); 29521 var attributes = merge(adaptationSetAttributes, parseAttributes$1(representation)); 29522 var representationSegmentInfo = getSegmentInformation(representation); 29523 29524 return repBaseUrls.map(function (baseUrl) { 29525 return { 29526 segmentInfo: merge(adaptationSetSegmentInfo, representationSegmentInfo), 29527 attributes: merge(attributes, { baseUrl: baseUrl }) 29528 }; 29529 }); 29530 }; 29531 }; 29532 29533 /** 29534 * Tranforms a series of content protection nodes to
vendor: 10,747 bytes, lines 29535-29843
29535 * an object containing pssh data by key system 29536 * 29537 * @param {Node[]} contentProtectionNodes 29538 * Content protection nodes 29539 * @return {Object} 29540 * Object containing pssh data by key system 29541 */ 29542 var generateKeySystemInformation = function generateKeySystemInformation(contentProtectionNodes) { 29543 return contentProtectionNodes.reduce(function (acc, node) { 29544 var attributes = parseAttributes$1(node); 29545 var keySystem = keySystemsMap[attributes.schemeIdUri]; 29546 29547 if (keySystem) { 29548 acc[keySystem] = { attributes: attributes }; 29549 29550 var psshNode = findChildren(node, 'cenc:pssh')[0]; 29551 29552 if (psshNode) { 29553 var pssh = getContent(psshNode); 29554 var psshBuffer = pssh && decodeB64ToUint8Array(pssh); 29555 29556 acc[keySystem].pssh = psshBuffer; 29557 } 29558 } 29559 29560 return acc; 29561 }, {}); 29562 }; 29563 29564 /** 29565 * Maps an AdaptationSet node to a list of Representation information objects 29566 * 29567 * @name toRepresentationsCallback 29568 * @function 29569 * @param {Node} adaptationSet 29570 * AdaptationSet node from the mpd 29571 * @return {RepresentationInformation[]} 29572 * List of objects containing Representaion information 29573 */ 29574 29575 /** 29576 * Returns a callback for Array.prototype.map for mapping AdaptationSet nodes to a list of 29577 * Representation information objects 29578 * 29579 * @param {Object} periodAttributes 29580 * Contains attributes inherited by the Period 29581 * @param {string[]} periodBaseUrls 29582 * Contains list of resolved base urls inherited by the Period 29583 * @param {string[]} periodSegmentInfo 29584 * Contains Segment Information at the period level 29585 * @return {toRepresentationsCallback} 29586 * Callback map function 29587 */ 29588 var toRepresentations = function toRepresentations(periodAttributes, periodBaseUrls, periodSegmentInfo) { 29589 return function (adaptationSet) { 29590 var adaptationSetAttributes = parseAttributes$1(adaptationSet); 29591 var adaptationSetBaseUrls = buildBaseUrls(periodBaseUrls, findChildren(adaptationSet, 'BaseURL')); 29592 var role = findChildren(adaptationSet, 'Role')[0]; 29593 var roleAttributes = { role: parseAttributes$1(role) }; 29594 29595 var attrs = merge(periodAttributes, adaptationSetAttributes, roleAttributes); 29596 29597 var contentProtection = generateKeySystemInformation(findChildren(adaptationSet, 'ContentProtection')); 29598 29599 if (Object.keys(contentProtection).length) { 29600 attrs = merge(attrs, { contentProtection: contentProtection }); 29601 } 29602 29603 var segmentInfo = getSegmentInformation(adaptationSet); 29604 var representations = findChildren(adaptationSet, 'Representation'); 29605 var adaptationSetSegmentInfo = merge(periodSegmentInfo, segmentInfo); 29606 29607 return flatten(representations.map(inheritBaseUrls(attrs, adaptationSetBaseUrls, adaptationSetSegmentInfo))); 29608 }; 29609 }; 29610 29611 /** 29612 * Maps an Period node to a list of Representation inforamtion objects for all 29613 * AdaptationSet nodes contained within the Period 29614 * 29615 * @name toAdaptationSetsCallback 29616 * @function 29617 * @param {Node} period 29618 * Period node from the mpd 29619 * @param {number} periodIndex 29620 * Index of the Period within the mpd 29621 * @return {RepresentationInformation[]} 29622 * List of objects containing Representaion information 29623 */ 29624 29625 /** 29626 * Returns a callback for Array.prototype.map for mapping Period nodes to a list of 29627 * Representation information objects 29628 * 29629 * @param {Object} mpdAttributes 29630 * Contains attributes inherited by the mpd 29631 * @param {string[]} mpdBaseUrls 29632 * Contains list of resolved base urls inherited by the mpd 29633 * @return {toAdaptationSetsCallback} 29634 * Callback map function 29635 */ 29636 var toAdaptationSets = function toAdaptationSets(mpdAttributes, mpdBaseUrls) { 29637 return function (period, periodIndex) { 29638 var periodBaseUrls = buildBaseUrls(mpdBaseUrls, findChildren(period, 'BaseURL')); 29639 var periodAtt = parseAttributes$1(period); 29640 var periodAttributes = merge(mpdAttributes, periodAtt, { periodIndex: periodIndex }); 29641 var adaptationSets = findChildren(period, 'AdaptationSet'); 29642 var periodSegmentInfo = getSegmentInformation(period); 29643 29644 return flatten(adaptationSets.map(toRepresentations(periodAttributes, periodBaseUrls, periodSegmentInfo))); 29645 }; 29646 }; 29647 29648 /** 29649 * Traverses the mpd xml tree to generate a list of Representation information objects 29650 * that have inherited attributes from parent nodes 29651 * 29652 * @param {Node} mpd 29653 * The root node of the mpd 29654 * @param {Object} options 29655 * Available options for inheritAttributes 29656 * @param {string} options.manifestUri 29657 * The uri source of the mpd 29658 * @param {number} options.NOW 29659 * Current time per DASH IOP. Default is current time in ms since epoch 29660 * @param {number} options.clientOffset 29661 * Client time difference from NOW (in milliseconds) 29662 * @return {RepresentationInformation[]} 29663 * List of objects containing Representation information 29664 */ 29665 var inheritAttributes = function inheritAttributes(mpd) { 29666 var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 29667 var _options$manifestUri = options.manifestUri, 29668 manifestUri = _options$manifestUri === undefined ? '' : _options$manifestUri, 29669 _options$NOW = options.NOW, 29670 NOW = _options$NOW === undefined ? Date.now() : _options$NOW, 29671 _options$clientOffset = options.clientOffset, 29672 clientOffset = _options$clientOffset === undefined ? 0 : _options$clientOffset; 29673 29674 var periods = findChildren(mpd, 'Period'); 29675 29676 if (periods.length !== 1) { 29677 // TODO add support for multiperiod 29678 throw new Error(errors.INVALID_NUMBER_OF_PERIOD); 29679 } 29680 29681 var mpdAttributes = parseAttributes$1(mpd); 29682 var mpdBaseUrls = buildBaseUrls([manifestUri], findChildren(mpd, 'BaseURL')); 29683 29684 mpdAttributes.sourceDuration = mpdAttributes.mediaPresentationDuration || 0; 29685 mpdAttributes.NOW = NOW; 29686 mpdAttributes.clientOffset = clientOffset; 29687 29688 return flatten(periods.map(toAdaptationSets(mpdAttributes, mpdBaseUrls))); 29689 }; 29690 29691 var stringToMpdXml = function stringToMpdXml(manifestString) { 29692 if (manifestString === '') { 29693 throw new Error(errors.DASH_EMPTY_MANIFEST); 29694 } 29695 29696 var parser = new window_1.DOMParser(); 29697 var xml = parser.parseFromString(manifestString, 'application/xml'); 29698 var mpd = xml && xml.documentElement.tagName === 'MPD' ? xml.documentElement : null; 29699 29700 if (!mpd || mpd && mpd.getElementsByTagName('parsererror').length > 0) { 29701 throw new Error(errors.DASH_INVALID_XML); 29702 } 29703 29704 return mpd; 29705 }; 29706 29707 /** 29708 * Parses the manifest for a UTCTiming node, returning the nodes attributes if found 29709 * 29710 * @param {string} mpd 29711 * XML string of the MPD manifest 29712 * @return {Object|null} 29713 * Attributes of UTCTiming node specified in the manifest. Null if none found 29714 */ 29715 var parseUTCTimingScheme = function parseUTCTimingScheme(mpd) { 29716 var UTCTimingNode = findChildren(mpd, 'UTCTiming')[0]; 29717 29718 if (!UTCTimingNode) { 29719 return null; 29720 } 29721 29722 var attributes = parseAttributes$1(UTCTimingNode); 29723 29724 switch (attributes.schemeIdUri) { 29725 case 'urn:mpeg:dash:utc:http-head:2014': 29726 case 'urn:mpeg:dash:utc:http-head:2012': 29727 attributes.method = 'HEAD'; 29728 break; 29729 case 'urn:mpeg:dash:utc:http-xsdate:2014': 29730 case 'urn:mpeg:dash:utc:http-iso:2014': 29731 case 'urn:mpeg:dash:utc:http-xsdate:2012': 29732 case 'urn:mpeg:dash:utc:http-iso:2012': 29733 attributes.method = 'GET'; 29734 break; 29735 case 'urn:mpeg:dash:utc:direct:2014': 29736 case 'urn:mpeg:dash:utc:direct:2012': 29737 attributes.method = 'DIRECT'; 29738 attributes.value = Date.parse(attributes.value); 29739 break; 29740 case 'urn:mpeg:dash:utc:http-ntp:2014': 29741 case 'urn:mpeg:dash:utc:ntp:2014': 29742 case 'urn:mpeg:dash:utc:sntp:2014': 29743 default: 29744 throw new Error(errors.UNSUPPORTED_UTC_TIMING_SCHEME); 29745 } 29746 29747 return attributes; 29748 }; 29749 29750 var parse = function parse(manifestString, options) { 29751 return toM3u8(toPlaylists(inheritAttributes(stringToMpdXml(manifestString), options))); 29752 }; 29753 29754 /** 29755 * Parses the manifest for a UTCTiming node, returning the nodes attributes if found 29756 * 29757 * @param {string} manifestString 29758 * XML string of the MPD manifest 29759 * @return {Object|null} 29760 * Attributes of UTCTiming node specified in the manifest. Null if none found 29761 */ 29762 var parseUTCTiming = function parseUTCTiming(manifestString) { 29763 return parseUTCTimingScheme(stringToMpdXml(manifestString)); 29764 }; 29765 29766 var toUnsigned = function toUnsigned(value) { 29767 return value >>> 0; 29768 }; 29769 29770 var bin = { 29771 toUnsigned: toUnsigned 29772 }; 29773 var bin_1 = bin.toUnsigned; 29774 29775 var bin$1 = /*#__PURE__*/Object.freeze({ 29776 default: bin, 29777 __moduleExports: bin, 29778 toUnsigned: bin_1 29779 }); 29780 29781 var require$$0$1 = ( bin$1 && bin ) || bin$1; 29782 29783 var toUnsigned$1 = require$$0$1.toUnsigned; 29784 var _findBox, parseType, timescale, startTime; 29785 29786 // Find the data for a box specified by its path 29787 _findBox = function findBox(data, path) { 29788 var results = [], 29789 i, 29790 size, 29791 type, 29792 end, 29793 subresults; 29794 29795 if (!path.length) { 29796 // short-circuit the search for empty paths 29797 return null; 29798 } 29799 29800 for (i = 0; i < data.byteLength;) { 29801 size = toUnsigned$1(data[i] << 24 | data[i + 1] << 16 | data[i + 2] << 8 | data[i + 3]); 29802 29803 type = parseType(data.subarray(i + 4, i + 8)); 29804 29805 end = size > 1 ? i + size : data.byteLength; 29806 29807 if (type === path[0]) { 29808 if (path.length === 1) { 29809 // this is the end of the path and we've found the box we were 29810 // looking for 29811 results.push(data.subarray(i + 8, end)); 29812 } else { 29813 // recursively search for the next box along the path 29814 subresults = _findBox(data.subarray(i + 8, end), path.slice(1)); 29815 if (subresults.length) { 29816 results = results.concat(subresults); 29817 } 29818 } 29819 } 29820 i = end; 29821 } 29822 29823 // we've finished searching all of data 29824 return results; 29825 }; 29826 29827 /** 29828 * Returns the string representation of an ASCII encoded four byte buffer. 29829 * @param buffer {Uint8Array} a four-byte buffer to translate 29830 * @return {string} the corresponding string 29831 */ 29832 parseType = function parseType(buffer) { 29833 var result = ''; 29834 result += String.fromCharCode(buffer[0]); 29835 result += String.fromCharCode(buffer[1]); 29836 result += String.fromCharCode(buffer[2]); 29837 result += String.fromCharCode(buffer[3]); 29838 return result; 29839 }; 29840 29841 /** 29842 * Parses an MP4 initialization segment and extracts the timescale 29843 * values for any declared tracks. Timescale values indicate the
29844 * number of clock ticks per second to assume for time-based values 29845 * elsewhere in the MP4. 29846 * 29847 * To determine the start time of an MP4, you need two pieces of 29848 * information: the timescale unit and the earliest base media decode 29849 * time. Multiple timescales can be specified within an MP4 but the 29850 * base media decode time is always expressed in the timescale from 29851 * the media header box for the track: 29852 * ``` 29853 * moov > trak > mdia > mdhd.timescale 29854 * ``` 29855 * @param init {Uint8Array} the bytes of the init segment 29856 * @return {object} a hash of track ids to timescale values or null if 29857 * the init segment is malformed. 29858 */ 29859 timescale = function timescale(init) { 29860 var result = {}, 29861 traks = _findBox(init, ['moov', 'trak']); 29862 29863 // mdhd timescale 29864 return traks.reduce(function (result, trak) { 29865 var tkhd, version, index, id, mdhd; 29866 29867 tkhd = _findBox(trak, ['tkhd'])[0]; 29868 if (!tkhd) { 29869 return null; 29870 } 29871 version = tkhd[0]; 29872 index = version === 0 ? 12 : 20; 29873 id = toUnsigned$1(tkhd[index] << 24 | tkhd[index + 1] << 16 | tkhd[index + 2] << 8 | tkhd[index + 3]); 29874 29875 mdhd = _findBox(trak, ['mdia', 'mdhd'])[0]; 29876 if (!mdhd) { 29877 return null; 29878 } 29879 version = mdhd[0]; 29880 index = version === 0 ? 12 : 20; 29881 result[id] = toUnsigned$1(mdhd[index] << 24 | mdhd[index + 1] << 16 | mdhd[index + 2] << 8 | mdhd[index + 3]); 29882 return result; 29883 }, result); 29884 }; 29885 29886 /** 29887 * Determine the base media decode start time, in seconds, for an MP4 29888 * fragment. If multiple fragments are specified, the earliest time is 29889 * returned. 29890 * 29891 * The base media decode time can be parsed from track fragment 29892 * metadata: 29893 * ``` 29894 * moof > traf > tfdt.baseMediaDecodeTime 29895 * ``` 29896 * It requires the timescale value from the mdhd to interpret. 29897 * 29898 * @param timescale {object} a hash of track ids to timescale values. 29899 * @return {number} the earliest base media decode start time for the 29900 * fragment, in seconds 29901 */ 29902 startTime = function startTime(timescale, fragment) { 29903 var trafs, baseTimes, result; 29904 29905 // we need info from two childrend of each track fragment box 29906 trafs = _findBox(fragment, ['moof', 'traf']); 29907 29908 // determine the start times for each track 29909 baseTimes = [].concat.apply([], trafs.map(function (traf) { 29910 return _findBox(traf, ['tfhd']).map(function (tfhd) { 29911 var id, scale, baseTime; 29912 29913 // get the track id from the tfhd 29914 id = toUnsigned$1(tfhd[4] << 24 | tfhd[5] << 16 | tfhd[6] << 8 | tfhd[7]); 29915 // assume a 90kHz clock if no timescale was specified 29916 scale = timescale[id] || 90e3; 29917 29918 // get the base media decode time from the tfdt 29919 baseTime = _findBox(traf, ['tfdt']).map(function (tfdt) { 29920 var version, result; 29921 29922 version = tfdt[0]; 29923 result = toUnsigned$1(tfdt[4] << 24 | tfdt[5] << 16 | tfdt[6] << 8 | tfdt[7]); 29924 if (version === 1) { 29925 result *= Math.pow(2, 32); 29926 result += toUnsigned$1(tfdt[8] << 24 | tfdt[9] << 16 | tfdt[10] << 8 | tfdt[11]); 29927 } 29928 return result; 29929 })[0]; 29930 baseTime = baseTime || Infinity; 29931 29932 // convert base time to seconds 29933 return baseTime / scale; 29934 }); 29935 })); 29936 29937 // return the minimum 29938 result = Math.min.apply(null, baseTimes); 29939 return isFinite(result) ? result : 0; 29940 }; 29941 29942 var probe = { 29943 parseType: parseType, 29944 timescale: timescale, 29945 startTime: startTime 29946 }; 29947 29948 var streamTypes = { 29949 H264_STREAM_TYPE: 0x1B, 29950 ADTS_STREAM_TYPE: 0x0F, 29951 METADATA_STREAM_TYPE: 0x15 29952 }; 29953 var streamTypes_1 = streamTypes.H264_STREAM_TYPE; 29954 var streamTypes_2 = streamTypes.ADTS_STREAM_TYPE; 29955 var streamTypes_3 = streamTypes.METADATA_STREAM_TYPE; 29956 29957 var streamTypes$1 = /*#__PURE__*/Object.freeze({ 29958 default: streamTypes, 29959 __moduleExports: streamTypes, 29960 H264_STREAM_TYPE: streamTypes_1, 29961 ADTS_STREAM_TYPE: streamTypes_2, 29962 METADATA_STREAM_TYPE: streamTypes_3 29963 }); 29964 29965 /** 29966 * mux.js 29967 * 29968 * Copyright (c) 2014 Brightcove 29969 * All rights reserved. 29970 * 29971 * A lightweight readable stream implemention that handles event dispatching. 29972 * Objects that inherit from streams should call init in their constructors. 29973 */ 29974 29975 var Stream$1 = function Stream() { 29976 this.init = function () { 29977 var listeners = {}; 29978 /** 29979 * Add a listener for a specified event type. 29980 * @param type {string} the event name 29981 * @param listener {function} the callback to be invoked when an event of 29982 * the specified type occurs 29983 */ 29984 this.on = function (type, listener) { 29985 if (!listeners[type]) { 29986 listeners[type] = []; 29987 } 29988 listeners[type] = listeners[type].concat(listener); 29989 }; 29990 /** 29991 * Remove a listener for a specified event type. 29992 * @param type {string} the event name 29993 * @param listener {function} a function previously registered for this 29994 * type of event through `on` 29995 */ 29996 this.off = function (type, listener) { 29997 var index; 29998 if (!listeners[type]) { 29999 return false;
vendor: 20,700 bytes, lines 30000-30619
30000 } 30001 index = listeners[type].indexOf(listener); 30002 listeners[type] = listeners[type].slice(); 30003 listeners[type].splice(index, 1); 30004 return index > -1; 30005 }; 30006 /** 30007 * Trigger an event of the specified type on this stream. Any additional 30008 * arguments to this function are passed as parameters to event listeners. 30009 * @param type {string} the event name 30010 */ 30011 this.trigger = function (type) { 30012 var callbacks, i, length, args; 30013 callbacks = listeners[type]; 30014 if (!callbacks) { 30015 return; 30016 } 30017 // Slicing the arguments on every invocation of this method 30018 // can add a significant amount of overhead. Avoid the 30019 // intermediate object creation for the common case of a 30020 // single callback argument 30021 if (arguments.length === 2) { 30022 length = callbacks.length; 30023 for (i = 0; i < length; ++i) { 30024 callbacks[i].call(this, arguments[1]); 30025 } 30026 } else { 30027 args = []; 30028 i = arguments.length; 30029 for (i = 1; i < arguments.length; ++i) { 30030 args.push(arguments[i]); 30031 } 30032 length = callbacks.length; 30033 for (i = 0; i < length; ++i) { 30034 callbacks[i].apply(this, args); 30035 } 30036 } 30037 }; 30038 /** 30039 * Destroys the stream and cleans up. 30040 */ 30041 this.dispose = function () { 30042 listeners = {}; 30043 }; 30044 }; 30045 }; 30046 30047 /** 30048 * Forwards all `data` events on this stream to the destination stream. The 30049 * destination stream should provide a method `push` to receive the data 30050 * events as they arrive. 30051 * @param destination {stream} the stream that will receive all `data` events 30052 * @param autoFlush {boolean} if false, we will not call `flush` on the destination 30053 * when the current stream emits a 'done' event 30054 * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options 30055 */ 30056 Stream$1.prototype.pipe = function (destination) { 30057 this.on('data', function (data) { 30058 destination.push(data); 30059 }); 30060 30061 this.on('done', function (flushSource) { 30062 destination.flush(flushSource); 30063 }); 30064 30065 return destination; 30066 }; 30067 30068 // Default stream functions that are expected to be overridden to perform 30069 // actual work. These are provided by the prototype as a sort of no-op 30070 // implementation so that we don't have to check for their existence in the 30071 // `pipe` function above. 30072 Stream$1.prototype.push = function (data) { 30073 this.trigger('data', data); 30074 }; 30075 30076 Stream$1.prototype.flush = function (flushSource) { 30077 this.trigger('done', flushSource); 30078 }; 30079 30080 var stream = Stream$1; 30081 30082 var stream$1 = /*#__PURE__*/Object.freeze({ 30083 default: stream, 30084 __moduleExports: stream 30085 }); 30086 30087 var Stream$2 = ( stream$1 && stream ) || stream$1; 30088 30089 var MAX_TS = 8589934592; 30090 30091 var RO_THRESH = 4294967296; 30092 30093 var handleRollover = function handleRollover(value, reference) { 30094 var direction = 1; 30095 30096 if (value > reference) { 30097 // If the current timestamp value is greater than our reference timestamp and we detect a 30098 // timestamp rollover, this means the roll over is happening in the opposite direction. 30099 // Example scenario: Enter a long stream/video just after a rollover occurred. The reference 30100 // point will be set to a small number, e.g. 1. The user then seeks backwards over the 30101 // rollover point. In loading this segment, the timestamp values will be very large, 30102 // e.g. 2^33 - 1. Since this comes before the data we loaded previously, we want to adjust 30103 // the time stamp to be `value - 2^33`. 30104 direction = -1; 30105 } 30106 30107 // Note: A seek forwards or back that is greater than the RO_THRESH (2^32, ~13 hours) will 30108 // cause an incorrect adjustment. 30109 while (Math.abs(reference - value) > RO_THRESH) { 30110 value += direction * MAX_TS; 30111 } 30112 30113 return value; 30114 }; 30115 30116 var TimestampRolloverStream = function TimestampRolloverStream(type) { 30117 var lastDTS, referenceDTS; 30118 30119 TimestampRolloverStream.prototype.init.call(this); 30120 30121 this.type_ = type; 30122 30123 this.push = function (data) { 30124 if (data.type !== this.type_) { 30125 return; 30126 } 30127 30128 if (referenceDTS === undefined) { 30129 referenceDTS = data.dts; 30130 } 30131 30132 data.dts = handleRollover(data.dts, referenceDTS); 30133 data.pts = handleRollover(data.pts, referenceDTS); 30134 30135 lastDTS = data.dts; 30136 30137 this.trigger('data', data); 30138 }; 30139 30140 this.flush = function () { 30141 referenceDTS = lastDTS; 30142 this.trigger('done'); 30143 }; 30144 30145 this.discontinuity = function () { 30146 referenceDTS = void 0; 30147 lastDTS = void 0; 30148 }; 30149 }; 30150 30151 TimestampRolloverStream.prototype = new Stream$2(); 30152 30153 var timestampRolloverStream = { 30154 TimestampRolloverStream: TimestampRolloverStream, 30155 handleRollover: handleRollover 30156 }; 30157 var timestampRolloverStream_1 = timestampRolloverStream.TimestampRolloverStream; 30158 var timestampRolloverStream_2 = timestampRolloverStream.handleRollover; 30159 30160 var timestampRolloverStream$1 = /*#__PURE__*/Object.freeze({ 30161 default: timestampRolloverStream, 30162 __moduleExports: timestampRolloverStream, 30163 TimestampRolloverStream: timestampRolloverStream_1, 30164 handleRollover: timestampRolloverStream_2 30165 }); 30166 30167 var StreamTypes = ( streamTypes$1 && streamTypes ) || streamTypes$1; 30168 30169 var parsePid = function parsePid(packet) { 30170 var pid = packet[1] & 0x1f; 30171 pid <<= 8; 30172 pid |= packet[2]; 30173 return pid; 30174 }; 30175 30176 var parsePayloadUnitStartIndicator = function parsePayloadUnitStartIndicator(packet) { 30177 return !!(packet[1] & 0x40); 30178 }; 30179 30180 var parseAdaptionField = function parseAdaptionField(packet) { 30181 var offset = 0; 30182 // if an adaption field is present, its length is specified by the 30183 // fifth byte of the TS packet header. The adaptation field is 30184 // used to add stuffing to PES packets that don't fill a complete 30185 // TS packet, and to specify some forms of timing and control data 30186 // that we do not currently use. 30187 if ((packet[3] & 0x30) >>> 4 > 0x01) { 30188 offset += packet[4] + 1; 30189 } 30190 return offset; 30191 }; 30192 30193 var parseType$1 = function parseType(packet, pmtPid) { 30194 var pid = parsePid(packet); 30195 if (pid === 0) { 30196 return 'pat'; 30197 } else if (pid === pmtPid) { 30198 return 'pmt'; 30199 } else if (pmtPid) { 30200 return 'pes'; 30201 } 30202 return null; 30203 }; 30204 30205 var parsePat = function parsePat(packet) { 30206 var pusi = parsePayloadUnitStartIndicator(packet); 30207 var offset = 4 + parseAdaptionField(packet); 30208 30209 if (pusi) { 30210 offset += packet[offset] + 1; 30211 } 30212 30213 return (packet[offset + 10] & 0x1f) << 8 | packet[offset + 11]; 30214 }; 30215 30216 var parsePmt = function parsePmt(packet) { 30217 var programMapTable = {}; 30218 var pusi = parsePayloadUnitStartIndicator(packet); 30219 var payloadOffset = 4 + parseAdaptionField(packet); 30220 30221 if (pusi) { 30222 payloadOffset += packet[payloadOffset] + 1; 30223 } 30224 30225 // PMTs can be sent ahead of the time when they should actually 30226 // take effect. We don't believe this should ever be the case 30227 // for HLS but we'll ignore "forward" PMT declarations if we see 30228 // them. Future PMT declarations have the current_next_indicator 30229 // set to zero. 30230 if (!(packet[payloadOffset + 5] & 0x01)) { 30231 return; 30232 } 30233 30234 var sectionLength, tableEnd, programInfoLength; 30235 // the mapping table ends at the end of the current section 30236 sectionLength = (packet[payloadOffset + 1] & 0x0f) << 8 | packet[payloadOffset + 2]; 30237 tableEnd = 3 + sectionLength - 4; 30238 30239 // to determine where the table is, we have to figure out how 30240 // long the program info descriptors are 30241 programInfoLength = (packet[payloadOffset + 10] & 0x0f) << 8 | packet[payloadOffset + 11]; 30242 30243 // advance the offset to the first entry in the mapping table 30244 var offset = 12 + programInfoLength; 30245 while (offset < tableEnd) { 30246 var i = payloadOffset + offset; 30247 // add an entry that maps the elementary_pid to the stream_type 30248 programMapTable[(packet[i + 1] & 0x1F) << 8 | packet[i + 2]] = packet[i]; 30249 30250 // move to the next table entry 30251 // skip past the elementary stream descriptors, if present 30252 offset += ((packet[i + 3] & 0x0F) << 8 | packet[i + 4]) + 5; 30253 } 30254 return programMapTable; 30255 }; 30256 30257 var parsePesType = function parsePesType(packet, programMapTable) { 30258 var pid = parsePid(packet); 30259 var type = programMapTable[pid]; 30260 switch (type) { 30261 case StreamTypes.H264_STREAM_TYPE: 30262 return 'video'; 30263 case StreamTypes.ADTS_STREAM_TYPE: 30264 return 'audio'; 30265 case StreamTypes.METADATA_STREAM_TYPE: 30266 return 'timed-metadata'; 30267 default: 30268 return null; 30269 } 30270 }; 30271 30272 var parsePesTime = function parsePesTime(packet) { 30273 var pusi = parsePayloadUnitStartIndicator(packet); 30274 if (!pusi) { 30275 return null; 30276 } 30277 30278 var offset = 4 + parseAdaptionField(packet); 30279 30280 if (offset >= packet.byteLength) { 30281 // From the H 222.0 MPEG-TS spec 30282 // "For transport stream packets carrying PES packets, stuffing is needed when there 30283 // is insufficient PES packet data to completely fill the transport stream packet 30284 // payload bytes. Stuffing is accomplished by defining an adaptation field longer than 30285 // the sum of the lengths of the data elements in it, so that the payload bytes 30286 // remaining after the adaptation field exactly accommodates the available PES packet 30287 // data." 30288 // 30289 // If the offset is >= the length of the packet, then the packet contains no data 30290 // and instead is just adaption field stuffing bytes 30291 return null; 30292 } 30293 30294 var pes = null; 30295 var ptsDtsFlags; 30296 30297 // PES packets may be annotated with a PTS value, or a PTS value 30298 // and a DTS value. Determine what combination of values is 30299 // available to work with. 30300 ptsDtsFlags = packet[offset + 7]; 30301 30302 // PTS and DTS are normally stored as a 33-bit number. Javascript 30303 // performs all bitwise operations on 32-bit integers but javascript 30304 // supports a much greater range (52-bits) of integer using standard 30305 // mathematical operations. 30306 // We construct a 31-bit value using bitwise operators over the 31 30307 // most significant bits and then multiply by 4 (equal to a left-shift 30308 // of 2) before we add the final 2 least significant bits of the 30309 // timestamp (equal to an OR.) 30310 if (ptsDtsFlags & 0xC0) { 30311 pes = {}; 30312 // the PTS and DTS are not written out directly. For information 30313 // on how they are encoded, see 30314 // http://dvd.sourceforge.net/dvdinfo/pes-hdr.html 30315 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; 30316 pes.pts *= 4; // Left shift by 2 30317 pes.pts += (packet[offset + 13] & 0x06) >>> 1; // OR by the two LSBs 30318 pes.dts = pes.pts; 30319 if (ptsDtsFlags & 0x40) { 30320 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; 30321 pes.dts *= 4; // Left shift by 2 30322 pes.dts += (packet[offset + 18] & 0x06) >>> 1; // OR by the two LSBs 30323 } 30324 } 30325 return pes; 30326 }; 30327 30328 var parseNalUnitType = function parseNalUnitType(type) { 30329 switch (type) { 30330 case 0x05: 30331 return 'slice_layer_without_partitioning_rbsp_idr'; 30332 case 0x06: 30333 return 'sei_rbsp'; 30334 case 0x07: 30335 return 'seq_parameter_set_rbsp'; 30336 case 0x08: 30337 return 'pic_parameter_set_rbsp'; 30338 case 0x09: 30339 return 'access_unit_delimiter_rbsp'; 30340 default: 30341 return null; 30342 } 30343 }; 30344 30345 var videoPacketContainsKeyFrame = function videoPacketContainsKeyFrame(packet) { 30346 var offset = 4 + parseAdaptionField(packet); 30347 var frameBuffer = packet.subarray(offset); 30348 var frameI = 0; 30349 var frameSyncPoint = 0; 30350 var foundKeyFrame = false; 30351 var nalType; 30352 30353 // advance the sync point to a NAL start, if necessary 30354 for (; frameSyncPoint < frameBuffer.byteLength - 3; frameSyncPoint++) { 30355 if (frameBuffer[frameSyncPoint + 2] === 1) { 30356 // the sync point is properly aligned 30357 frameI = frameSyncPoint + 5; 30358 break; 30359 } 30360 } 30361 30362 while (frameI < frameBuffer.byteLength) { 30363 // look at the current byte to determine if we've hit the end of 30364 // a NAL unit boundary 30365 switch (frameBuffer[frameI]) { 30366 case 0: 30367 // skip past non-sync sequences 30368 if (frameBuffer[frameI - 1] !== 0) { 30369 frameI += 2; 30370 break; 30371 } else if (frameBuffer[frameI - 2] !== 0) { 30372 frameI++; 30373 break; 30374 } 30375 30376 if (frameSyncPoint + 3 !== frameI - 2) { 30377 nalType = parseNalUnitType(frameBuffer[frameSyncPoint + 3] & 0x1f); 30378 if (nalType === 'slice_layer_without_partitioning_rbsp_idr') { 30379 foundKeyFrame = true; 30380 } 30381 } 30382 30383 // drop trailing zeroes 30384 do { 30385 frameI++; 30386 } while (frameBuffer[frameI] !== 1 && frameI < frameBuffer.length); 30387 frameSyncPoint = frameI - 2; 30388 frameI += 3; 30389 break; 30390 case 1: 30391 // skip past non-sync sequences 30392 if (frameBuffer[frameI - 1] !== 0 || frameBuffer[frameI - 2] !== 0) { 30393 frameI += 3; 30394 break; 30395 } 30396 30397 nalType = parseNalUnitType(frameBuffer[frameSyncPoint + 3] & 0x1f); 30398 if (nalType === 'slice_layer_without_partitioning_rbsp_idr') { 30399 foundKeyFrame = true; 30400 } 30401 frameSyncPoint = frameI - 2; 30402 frameI += 3; 30403 break; 30404 default: 30405 // the current byte isn't a one or zero, so it cannot be part 30406 // of a sync sequence 30407 frameI += 3; 30408 break; 30409 } 30410 } 30411 frameBuffer = frameBuffer.subarray(frameSyncPoint); 30412 frameI -= frameSyncPoint; 30413 frameSyncPoint = 0; 30414 // parse the final nal 30415 if (frameBuffer && frameBuffer.byteLength > 3) { 30416 nalType = parseNalUnitType(frameBuffer[frameSyncPoint + 3] & 0x1f); 30417 if (nalType === 'slice_layer_without_partitioning_rbsp_idr') { 30418 foundKeyFrame = true; 30419 } 30420 } 30421 30422 return foundKeyFrame; 30423 }; 30424 30425 var probe$1 = { 30426 parseType: parseType$1, 30427 parsePat: parsePat, 30428 parsePmt: parsePmt, 30429 parsePayloadUnitStartIndicator: parsePayloadUnitStartIndicator, 30430 parsePesType: parsePesType, 30431 parsePesTime: parsePesTime, 30432 videoPacketContainsKeyFrame: videoPacketContainsKeyFrame 30433 }; 30434 var probe_1$1 = probe$1.parseType; 30435 var probe_2$1 = probe$1.parsePat; 30436 var probe_3$1 = probe$1.parsePmt; 30437 var probe_4 = probe$1.parsePayloadUnitStartIndicator; 30438 var probe_5 = probe$1.parsePesType; 30439 var probe_6 = probe$1.parsePesTime; 30440 var probe_7 = probe$1.videoPacketContainsKeyFrame; 30441 30442 var probe$2 = /*#__PURE__*/Object.freeze({ 30443 default: probe$1, 30444 __moduleExports: probe$1, 30445 parseType: probe_1$1, 30446 parsePat: probe_2$1, 30447 parsePmt: probe_3$1, 30448 parsePayloadUnitStartIndicator: probe_4, 30449 parsePesType: probe_5, 30450 parsePesTime: probe_6, 30451 videoPacketContainsKeyFrame: probe_7 30452 }); 30453 30454 /** 30455 * mux.js 30456 * 30457 * Copyright (c) 2016 Brightcove 30458 * All rights reserved. 30459 * 30460 * Utilities to detect basic properties and metadata about Aac data. 30461 */ 30462 30463 var ADTS_SAMPLING_FREQUENCIES = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350]; 30464 30465 var parseSyncSafeInteger = function parseSyncSafeInteger(data) { 30466 return data[0] << 21 | data[1] << 14 | data[2] << 7 | data[3]; 30467 }; 30468 30469 // return a percent-encoded representation of the specified byte range 30470 // @see http://en.wikipedia.org/wiki/Percent-encoding 30471 var percentEncode = function percentEncode(bytes, start, end) { 30472 var i, 30473 result = ''; 30474 for (i = start; i < end; i++) { 30475 result += '%' + ('00' + bytes[i].toString(16)).slice(-2); 30476 } 30477 return result; 30478 }; 30479 30480 // return the string representation of the specified byte range, 30481 // interpreted as ISO-8859-1. 30482 var parseIso88591 = function parseIso88591(bytes, start, end) { 30483 return unescape(percentEncode(bytes, start, end)); // jshint ignore:line 30484 }; 30485 30486 var parseId3TagSize = function parseId3TagSize(header, byteIndex) { 30487 var returnSize = header[byteIndex + 6] << 21 | header[byteIndex + 7] << 14 | header[byteIndex + 8] << 7 | header[byteIndex + 9], 30488 flags = header[byteIndex + 5], 30489 footerPresent = (flags & 16) >> 4; 30490 30491 if (footerPresent) { 30492 return returnSize + 20; 30493 } 30494 return returnSize + 10; 30495 }; 30496 30497 var parseAdtsSize = function parseAdtsSize(header, byteIndex) { 30498 var lowThree = (header[byteIndex + 5] & 0xE0) >> 5, 30499 middle = header[byteIndex + 4] << 3, 30500 highTwo = header[byteIndex + 3] & 0x3 << 11; 30501 30502 return highTwo | middle | lowThree; 30503 }; 30504 30505 var parseType$2 = function parseType(header, byteIndex) { 30506 if (header[byteIndex] === 'I'.charCodeAt(0) && header[byteIndex + 1] === 'D'.charCodeAt(0) && header[byteIndex + 2] === '3'.charCodeAt(0)) { 30507 return 'timed-metadata'; 30508 } else if (header[byteIndex] & 0xff === 0xff && (header[byteIndex + 1] & 0xf0) === 0xf0) { 30509 return 'audio'; 30510 } 30511 return null; 30512 }; 30513 30514 var parseSampleRate = function parseSampleRate(packet) { 30515 var i = 0; 30516 30517 while (i + 5 < packet.length) { 30518 if (packet[i] !== 0xFF || (packet[i + 1] & 0xF6) !== 0xF0) { 30519 // If a valid header was not found, jump one forward and attempt to 30520 // find a valid ADTS header starting at the next byte 30521 i++; 30522 continue; 30523 } 30524 return ADTS_SAMPLING_FREQUENCIES[(packet[i + 2] & 0x3c) >>> 2]; 30525 } 30526 30527 return null; 30528 }; 30529 30530 var parseAacTimestamp = function parseAacTimestamp(packet) { 30531 var frameStart, frameSize, frame, frameHeader; 30532 30533 // find the start of the first frame and the end of the tag 30534 frameStart = 10; 30535 if (packet[5] & 0x40) { 30536 // advance the frame start past the extended header 30537 frameStart += 4; // header size field 30538 frameStart += parseSyncSafeInteger(packet.subarray(10, 14)); 30539 } 30540 30541 // parse one or more ID3 frames 30542 // http://id3.org/id3v2.3.0#ID3v2_frame_overview 30543 do { 30544 // determine the number of bytes in this frame 30545 frameSize = parseSyncSafeInteger(packet.subarray(frameStart + 4, frameStart + 8)); 30546 if (frameSize < 1) { 30547 return null; 30548 } 30549 frameHeader = String.fromCharCode(packet[frameStart], packet[frameStart + 1], packet[frameStart + 2], packet[frameStart + 3]); 30550 30551 if (frameHeader === 'PRIV') { 30552 frame = packet.subarray(frameStart + 10, frameStart + frameSize + 10); 30553 30554 for (var i = 0; i < frame.byteLength; i++) { 30555 if (frame[i] === 0) { 30556 var owner = parseIso88591(frame, 0, i); 30557 if (owner === 'com.apple.streaming.transportStreamTimestamp') { 30558 var d = frame.subarray(i + 1); 30559 var size = (d[3] & 0x01) << 30 | d[4] << 22 | d[5] << 14 | d[6] << 6 | d[7] >>> 2; 30560 size *= 4; 30561 size += d[7] & 0x03; 30562 30563 return size; 30564 } 30565 break; 30566 } 30567 } 30568 } 30569 30570 frameStart += 10; // advance past the frame header 30571 frameStart += frameSize; // advance past the frame body 30572 } while (frameStart < packet.byteLength); 30573 return null; 30574 }; 30575 30576 var probe$3 = { 30577 parseId3TagSize: parseId3TagSize, 30578 parseAdtsSize: parseAdtsSize, 30579 parseType: parseType$2, 30580 parseSampleRate: parseSampleRate, 30581 parseAacTimestamp: parseAacTimestamp 30582 }; 30583 var probe_1$2 = probe$3.parseId3TagSize; 30584 var probe_2$2 = probe$3.parseAdtsSize; 30585 var probe_3$2 = probe$3.parseType; 30586 var probe_4$1 = probe$3.parseSampleRate; 30587 var probe_5$1 = probe$3.parseAacTimestamp; 30588 30589 var probe$4 = /*#__PURE__*/Object.freeze({ 30590 default: probe$3, 30591 __moduleExports: probe$3, 30592 parseId3TagSize: probe_1$2, 30593 parseAdtsSize: probe_2$2, 30594 parseType: probe_3$2, 30595 parseSampleRate: probe_4$1, 30596 parseAacTimestamp: probe_5$1 30597 }); 30598 30599 var require$$0$2 = ( timestampRolloverStream$1 && timestampRolloverStream ) || timestampRolloverStream$1; 30600 30601 var require$$1$1 = ( probe$2 && probe$1 ) || probe$2; 30602 30603 var require$$2$1 = ( probe$4 && probe$3 ) || probe$4; 30604 30605 var handleRollover$1 = require$$0$2.handleRollover; 30606 var probe$5 = {}; 30607 probe$5.ts = require$$1$1; 30608 probe$5.aac = require$$2$1; 30609 30610 var PES_TIMESCALE = 90000, 30611 MP2T_PACKET_LENGTH = 188, 30612 // bytes 30613 SYNC_BYTE = 0x47; 30614 30615 var isLikelyAacData = function isLikelyAacData(data) { 30616 if (data[0] === 'I'.charCodeAt(0) && data[1] === 'D'.charCodeAt(0) && data[2] === '3'.charCodeAt(0)) { 30617 return true; 30618 } 30619 return false;
vendor: 9,174 bytes, lines 30620-30914
30620 }; 30621 30622 /** 30623 * walks through segment data looking for pat and pmt packets to parse out 30624 * program map table information 30625 */ 30626 var parsePsi_ = function parsePsi_(bytes, pmt) { 30627 var startIndex = 0, 30628 endIndex = MP2T_PACKET_LENGTH, 30629 packet, 30630 type; 30631 30632 while (endIndex < bytes.byteLength) { 30633 // Look for a pair of start and end sync bytes in the data.. 30634 if (bytes[startIndex] === SYNC_BYTE && bytes[endIndex] === SYNC_BYTE) { 30635 // We found a packet 30636 packet = bytes.subarray(startIndex, endIndex); 30637 type = probe$5.ts.parseType(packet, pmt.pid); 30638 30639 switch (type) { 30640 case 'pat': 30641 if (!pmt.pid) { 30642 pmt.pid = probe$5.ts.parsePat(packet); 30643 } 30644 break; 30645 case 'pmt': 30646 if (!pmt.table) { 30647 pmt.table = probe$5.ts.parsePmt(packet); 30648 } 30649 break; 30650 default: 30651 break; 30652 } 30653 30654 // Found the pat and pmt, we can stop walking the segment 30655 if (pmt.pid && pmt.table) { 30656 return; 30657 } 30658 30659 startIndex += MP2T_PACKET_LENGTH; 30660 endIndex += MP2T_PACKET_LENGTH; 30661 continue; 30662 } 30663 30664 // If we get here, we have somehow become de-synchronized and we need to step 30665 // forward one byte at a time until we find a pair of sync bytes that denote 30666 // a packet 30667 startIndex++; 30668 endIndex++; 30669 } 30670 }; 30671 30672 /** 30673 * walks through the segment data from the start and end to get timing information 30674 * for the first and last audio pes packets 30675 */ 30676 var parseAudioPes_ = function parseAudioPes_(bytes, pmt, result) { 30677 var startIndex = 0, 30678 endIndex = MP2T_PACKET_LENGTH, 30679 packet, 30680 type, 30681 pesType, 30682 pusi, 30683 parsed; 30684 30685 var endLoop = false; 30686 30687 // Start walking from start of segment to get first audio packet 30688 while (endIndex < bytes.byteLength) { 30689 // Look for a pair of start and end sync bytes in the data.. 30690 if (bytes[startIndex] === SYNC_BYTE && bytes[endIndex] === SYNC_BYTE) { 30691 // We found a packet 30692 packet = bytes.subarray(startIndex, endIndex); 30693 type = probe$5.ts.parseType(packet, pmt.pid); 30694 30695 switch (type) { 30696 case 'pes': 30697 pesType = probe$5.ts.parsePesType(packet, pmt.table); 30698 pusi = probe$5.ts.parsePayloadUnitStartIndicator(packet); 30699 if (pesType === 'audio' && pusi) { 30700 parsed = probe$5.ts.parsePesTime(packet); 30701 if (parsed) { 30702 parsed.type = 'audio'; 30703 result.audio.push(parsed); 30704 endLoop = true; 30705 } 30706 } 30707 break; 30708 default: 30709 break; 30710 } 30711 30712 if (endLoop) { 30713 break; 30714 } 30715 30716 startIndex += MP2T_PACKET_LENGTH; 30717 endIndex += MP2T_PACKET_LENGTH; 30718 continue; 30719 } 30720 30721 // If we get here, we have somehow become de-synchronized and we need to step 30722 // forward one byte at a time until we find a pair of sync bytes that denote 30723 // a packet 30724 startIndex++; 30725 endIndex++; 30726 } 30727 30728 // Start walking from end of segment to get last audio packet 30729 endIndex = bytes.byteLength; 30730 startIndex = endIndex - MP2T_PACKET_LENGTH; 30731 endLoop = false; 30732 while (startIndex >= 0) { 30733 // Look for a pair of start and end sync bytes in the data.. 30734 if (bytes[startIndex] === SYNC_BYTE && bytes[endIndex] === SYNC_BYTE) { 30735 // We found a packet 30736 packet = bytes.subarray(startIndex, endIndex); 30737 type = probe$5.ts.parseType(packet, pmt.pid); 30738 30739 switch (type) { 30740 case 'pes': 30741 pesType = probe$5.ts.parsePesType(packet, pmt.table); 30742 pusi = probe$5.ts.parsePayloadUnitStartIndicator(packet); 30743 if (pesType === 'audio' && pusi) { 30744 parsed = probe$5.ts.parsePesTime(packet); 30745 if (parsed) { 30746 parsed.type = 'audio'; 30747 result.audio.push(parsed); 30748 endLoop = true; 30749 } 30750 } 30751 break; 30752 default: 30753 break; 30754 } 30755 30756 if (endLoop) { 30757 break; 30758 } 30759 30760 startIndex -= MP2T_PACKET_LENGTH; 30761 endIndex -= MP2T_PACKET_LENGTH; 30762 continue; 30763 } 30764 30765 // If we get here, we have somehow become de-synchronized and we need to step 30766 // forward one byte at a time until we find a pair of sync bytes that denote 30767 // a packet 30768 startIndex--; 30769 endIndex--; 30770 } 30771 }; 30772 30773 /** 30774 * walks through the segment data from the start and end to get timing information 30775 * for the first and last video pes packets as well as timing information for the first 30776 * key frame. 30777 */ 30778 var parseVideoPes_ = function parseVideoPes_(bytes, pmt, result) { 30779 var startIndex = 0, 30780 endIndex = MP2T_PACKET_LENGTH, 30781 packet, 30782 type, 30783 pesType, 30784 pusi, 30785 parsed, 30786 frame, 30787 i, 30788 pes; 30789 30790 var endLoop = false; 30791 30792 var currentFrame = { 30793 data: [], 30794 size: 0 30795 }; 30796 30797 // Start walking from start of segment to get first video packet 30798 while (endIndex < bytes.byteLength) { 30799 // Look for a pair of start and end sync bytes in the data.. 30800 if (bytes[startIndex] === SYNC_BYTE && bytes[endIndex] === SYNC_BYTE) { 30801 // We found a packet 30802 packet = bytes.subarray(startIndex, endIndex); 30803 type = probe$5.ts.parseType(packet, pmt.pid); 30804 30805 switch (type) { 30806 case 'pes': 30807 pesType = probe$5.ts.parsePesType(packet, pmt.table); 30808 pusi = probe$5.ts.parsePayloadUnitStartIndicator(packet); 30809 if (pesType === 'video') { 30810 if (pusi && !endLoop) { 30811 parsed = probe$5.ts.parsePesTime(packet); 30812 if (parsed) { 30813 parsed.type = 'video'; 30814 result.video.push(parsed); 30815 endLoop = true; 30816 } 30817 } 30818 if (!result.firstKeyFrame) { 30819 if (pusi) { 30820 if (currentFrame.size !== 0) { 30821 frame = new Uint8Array(currentFrame.size); 30822 i = 0; 30823 while (currentFrame.data.length) { 30824 pes = currentFrame.data.shift(); 30825 frame.set(pes, i); 30826 i += pes.byteLength; 30827 } 30828 if (probe$5.ts.videoPacketContainsKeyFrame(frame)) { 30829 result.firstKeyFrame = probe$5.ts.parsePesTime(frame); 30830 result.firstKeyFrame.type = 'video'; 30831 } 30832 currentFrame.size = 0; 30833 } 30834 } 30835 currentFrame.data.push(packet); 30836 currentFrame.size += packet.byteLength; 30837 } 30838 } 30839 break; 30840 default: 30841 break; 30842 } 30843 30844 if (endLoop && result.firstKeyFrame) { 30845 break; 30846 } 30847 30848 startIndex += MP2T_PACKET_LENGTH; 30849 endIndex += MP2T_PACKET_LENGTH; 30850 continue; 30851 } 30852 30853 // If we get here, we have somehow become de-synchronized and we need to step 30854 // forward one byte at a time until we find a pair of sync bytes that denote 30855 // a packet 30856 startIndex++; 30857 endIndex++; 30858 } 30859 30860 // Start walking from end of segment to get last video packet 30861 endIndex = bytes.byteLength; 30862 startIndex = endIndex - MP2T_PACKET_LENGTH; 30863 endLoop = false; 30864 while (startIndex >= 0) { 30865 // Look for a pair of start and end sync bytes in the data.. 30866 if (bytes[startIndex] === SYNC_BYTE && bytes[endIndex] === SYNC_BYTE) { 30867 // We found a packet 30868 packet = bytes.subarray(startIndex, endIndex); 30869 type = probe$5.ts.parseType(packet, pmt.pid); 30870 30871 switch (type) { 30872 case 'pes': 30873 pesType = probe$5.ts.parsePesType(packet, pmt.table); 30874 pusi = probe$5.ts.parsePayloadUnitStartIndicator(packet); 30875 if (pesType === 'video' && pusi) { 30876 parsed = probe$5.ts.parsePesTime(packet); 30877 if (parsed) { 30878 parsed.type = 'video'; 30879 result.video.push(parsed); 30880 endLoop = true; 30881 } 30882 } 30883 break; 30884 default: 30885 break; 30886 } 30887 30888 if (endLoop) { 30889 break; 30890 } 30891 30892 startIndex -= MP2T_PACKET_LENGTH; 30893 endIndex -= MP2T_PACKET_LENGTH; 30894 continue; 30895 } 30896 30897 // If we get here, we have somehow become de-synchronized and we need to step 30898 // forward one byte at a time until we find a pair of sync bytes that denote 30899 // a packet 30900 startIndex--; 30901 endIndex--; 30902 } 30903 }; 30904 30905 /** 30906 * Adjusts the timestamp information for the segment to account for 30907 * rollover and convert to seconds based on pes packet timescale (90khz clock) 30908 */ 30909 var adjustTimestamp_ = function adjustTimestamp_(segmentInfo, baseTimestamp) { 30910 if (segmentInfo.audio && segmentInfo.audio.length) { 30911 var audioBaseTimestamp = baseTimestamp; 30912 if (typeof audioBaseTimestamp === 'undefined') { 30913 audioBaseTimestamp = segmentInfo.audio[0].dts; 30914 }
vendor: 5,675 bytes, lines 30915-31113
30915 segmentInfo.audio.forEach(function (info) { 30916 info.dts = handleRollover$1(info.dts, audioBaseTimestamp); 30917 info.pts = handleRollover$1(info.pts, audioBaseTimestamp); 30918 // time in seconds 30919 info.dtsTime = info.dts / PES_TIMESCALE; 30920 info.ptsTime = info.pts / PES_TIMESCALE; 30921 }); 30922 } 30923 30924 if (segmentInfo.video && segmentInfo.video.length) { 30925 var videoBaseTimestamp = baseTimestamp; 30926 if (typeof videoBaseTimestamp === 'undefined') { 30927 videoBaseTimestamp = segmentInfo.video[0].dts; 30928 } 30929 segmentInfo.video.forEach(function (info) { 30930 info.dts = handleRollover$1(info.dts, videoBaseTimestamp); 30931 info.pts = handleRollover$1(info.pts, videoBaseTimestamp); 30932 // time in seconds 30933 info.dtsTime = info.dts / PES_TIMESCALE; 30934 info.ptsTime = info.pts / PES_TIMESCALE; 30935 }); 30936 if (segmentInfo.firstKeyFrame) { 30937 var frame = segmentInfo.firstKeyFrame; 30938 frame.dts = handleRollover$1(frame.dts, videoBaseTimestamp); 30939 frame.pts = handleRollover$1(frame.pts, videoBaseTimestamp); 30940 // time in seconds 30941 frame.dtsTime = frame.dts / PES_TIMESCALE; 30942 frame.ptsTime = frame.dts / PES_TIMESCALE; 30943 } 30944 } 30945 }; 30946 30947 /** 30948 * inspects the aac data stream for start and end time information 30949 */ 30950 var inspectAac_ = function inspectAac_(bytes) { 30951 var endLoop = false, 30952 audioCount = 0, 30953 sampleRate = null, 30954 timestamp = null, 30955 frameSize = 0, 30956 byteIndex = 0, 30957 packet; 30958 30959 while (bytes.length - byteIndex >= 3) { 30960 var type = probe$5.aac.parseType(bytes, byteIndex); 30961 switch (type) { 30962 case 'timed-metadata': 30963 // Exit early because we don't have enough to parse 30964 // the ID3 tag header 30965 if (bytes.length - byteIndex < 10) { 30966 endLoop = true; 30967 break; 30968 } 30969 30970 frameSize = probe$5.aac.parseId3TagSize(bytes, byteIndex); 30971 30972 // Exit early if we don't have enough in the buffer 30973 // to emit a full packet 30974 if (frameSize > bytes.length) { 30975 endLoop = true; 30976 break; 30977 } 30978 if (timestamp === null) { 30979 packet = bytes.subarray(byteIndex, byteIndex + frameSize); 30980 timestamp = probe$5.aac.parseAacTimestamp(packet); 30981 } 30982 byteIndex += frameSize; 30983 break; 30984 case 'audio': 30985 // Exit early because we don't have enough to parse 30986 // the ADTS frame header 30987 if (bytes.length - byteIndex < 7) { 30988 endLoop = true; 30989 break; 30990 } 30991 30992 frameSize = probe$5.aac.parseAdtsSize(bytes, byteIndex); 30993 30994 // Exit early if we don't have enough in the buffer 30995 // to emit a full packet 30996 if (frameSize > bytes.length) { 30997 endLoop = true; 30998 break; 30999 } 31000 if (sampleRate === null) { 31001 packet = bytes.subarray(byteIndex, byteIndex + frameSize); 31002 sampleRate = probe$5.aac.parseSampleRate(packet); 31003 } 31004 audioCount++; 31005 byteIndex += frameSize; 31006 break; 31007 default: 31008 byteIndex++; 31009 break; 31010 } 31011 if (endLoop) { 31012 return null; 31013 } 31014 } 31015 if (sampleRate === null || timestamp === null) { 31016 return null; 31017 } 31018 31019 var audioTimescale = PES_TIMESCALE / sampleRate; 31020 31021 var result = { 31022 audio: [{ 31023 type: 'audio', 31024 dts: timestamp, 31025 pts: timestamp 31026 }, { 31027 type: 'audio', 31028 dts: timestamp + audioCount * 1024 * audioTimescale, 31029 pts: timestamp + audioCount * 1024 * audioTimescale 31030 }] 31031 }; 31032 31033 return result; 31034 }; 31035 31036 /** 31037 * inspects the transport stream segment data for start and end time information 31038 * of the audio and video tracks (when present) as well as the first key frame's 31039 * start time. 31040 */ 31041 var inspectTs_ = function inspectTs_(bytes) { 31042 var pmt = { 31043 pid: null, 31044 table: null 31045 }; 31046 31047 var result = {}; 31048 31049 parsePsi_(bytes, pmt); 31050 31051 for (var pid in pmt.table) { 31052 if (pmt.table.hasOwnProperty(pid)) { 31053 var type = pmt.table[pid]; 31054 switch (type) { 31055 case StreamTypes.H264_STREAM_TYPE: 31056 result.video = []; 31057 parseVideoPes_(bytes, pmt, result); 31058 if (result.video.length === 0) { 31059 delete result.video; 31060 } 31061 break; 31062 case StreamTypes.ADTS_STREAM_TYPE: 31063 result.audio = []; 31064 parseAudioPes_(bytes, pmt, result); 31065 if (result.audio.length === 0) { 31066 delete result.audio; 31067 } 31068 break; 31069 default: 31070 break; 31071 } 31072 } 31073 } 31074 return result; 31075 }; 31076 31077 /** 31078 * Inspects segment byte data and returns an object with start and end timing information 31079 * 31080 * @param {Uint8Array} bytes The segment byte data 31081 * @param {Number} baseTimestamp Relative reference timestamp used when adjusting frame 31082 * timestamps for rollover. This value must be in 90khz clock. 31083 * @return {Object} Object containing start and end frame timing info of segment. 31084 */ 31085 var inspect = function inspect(bytes, baseTimestamp) { 31086 var isAacData = isLikelyAacData(bytes); 31087 31088 var result; 31089 31090 if (isAacData) { 31091 result = inspectAac_(bytes); 31092 } else { 31093 result = inspectTs_(bytes); 31094 } 31095 31096 if (!result || !result.audio && !result.video) { 31097 return null; 31098 } 31099 31100 adjustTimestamp_(result, baseTimestamp); 31101 31102 return result; 31103 }; 31104 31105 var tsInspector = { 31106 inspect: inspect 31107 }; 31108 31109 /* 31110 * pkcs7.pad 31111 * https://github.com/brightcove/pkcs7 31112 * 31113 * Copyright (c) 2014 Brightcove
vendor: 121,872 bytes, lines 31114-34780
31114 * Licensed under the apache2 license. 31115 */ 31116 31117 /** 31118 * Returns the subarray of a Uint8Array without PKCS#7 padding. 31119 * @param padded {Uint8Array} unencrypted bytes that have been padded 31120 * @return {Uint8Array} the unpadded bytes 31121 * @see http://tools.ietf.org/html/rfc5652 31122 */ 31123 function unpad(padded) { 31124 return padded.subarray(0, padded.byteLength - padded[padded.byteLength - 1]); 31125 } 31126 31127 var classCallCheck$2 = function classCallCheck$$1(instance, Constructor) { 31128 if (!(instance instanceof Constructor)) { 31129 throw new TypeError("Cannot call a class as a function"); 31130 } 31131 }; 31132 31133 var createClass$1 = function () { 31134 function defineProperties(target, props) { 31135 for (var i = 0; i < props.length; i++) { 31136 var descriptor = props[i]; 31137 descriptor.enumerable = descriptor.enumerable || false; 31138 descriptor.configurable = true; 31139 if ("value" in descriptor) descriptor.writable = true; 31140 Object.defineProperty(target, descriptor.key, descriptor); 31141 } 31142 } 31143 31144 return function (Constructor, protoProps, staticProps) { 31145 if (protoProps) defineProperties(Constructor.prototype, protoProps); 31146 if (staticProps) defineProperties(Constructor, staticProps); 31147 return Constructor; 31148 }; 31149 }(); 31150 31151 var inherits$2 = function inherits$$1(subClass, superClass) { 31152 if (typeof superClass !== "function" && superClass !== null) { 31153 throw new TypeError("Super expression must either be null or a function, not " + (typeof superClass === "undefined" ? "undefined" : _typeof(superClass))); 31154 } 31155 31156 subClass.prototype = Object.create(superClass && superClass.prototype, { 31157 constructor: { 31158 value: subClass, 31159 enumerable: false, 31160 writable: true, 31161 configurable: true 31162 } 31163 }); 31164 if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; 31165 }; 31166 31167 var possibleConstructorReturn$2 = function possibleConstructorReturn$$1(self, call) { 31168 if (!self) { 31169 throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); 31170 } 31171 31172 return call && ((typeof call === "undefined" ? "undefined" : _typeof(call)) === "object" || typeof call === "function") ? call : self; 31173 }; 31174 31175 /** 31176 * @file aes.js 31177 * 31178 * This file contains an adaptation of the AES decryption algorithm 31179 * from the Standford Javascript Cryptography Library. That work is 31180 * covered by the following copyright and permissions notice: 31181 * 31182 * Copyright 2009-2010 Emily Stark, Mike Hamburg, Dan Boneh. 31183 * All rights reserved. 31184 * 31185 * Redistribution and use in source and binary forms, with or without 31186 * modification, are permitted provided that the following conditions are 31187 * met: 31188 * 31189 * 1. Redistributions of source code must retain the above copyright 31190 * notice, this list of conditions and the following disclaimer. 31191 * 31192 * 2. Redistributions in binary form must reproduce the above 31193 * copyright notice, this list of conditions and the following 31194 * disclaimer in the documentation and/or other materials provided 31195 * with the distribution. 31196 * 31197 * THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR 31198 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 31199 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 31200 * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> OR CONTRIBUTORS BE 31201 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 31202 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 31203 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 31204 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 31205 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 31206 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN 31207 * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31208 * 31209 * The views and conclusions contained in the software and documentation 31210 * are those of the authors and should not be interpreted as representing 31211 * official policies, either expressed or implied, of the authors. 31212 */ 31213 31214 /** 31215 * Expand the S-box tables. 31216 * 31217 * @private 31218 */ 31219 var precompute = function precompute() { 31220 var tables = [[[], [], [], [], []], [[], [], [], [], []]]; 31221 var encTable = tables[0]; 31222 var decTable = tables[1]; 31223 var sbox = encTable[4]; 31224 var sboxInv = decTable[4]; 31225 var i = void 0; 31226 var x = void 0; 31227 var xInv = void 0; 31228 var d = []; 31229 var th = []; 31230 var x2 = void 0; 31231 var x4 = void 0; 31232 var x8 = void 0; 31233 var s = void 0; 31234 var tEnc = void 0; 31235 var tDec = void 0; 31236 31237 // Compute double and third tables 31238 for (i = 0; i < 256; i++) { 31239 th[(d[i] = i << 1 ^ (i >> 7) * 283) ^ i] = i; 31240 } 31241 31242 for (x = xInv = 0; !sbox[x]; x ^= x2 || 1, xInv = th[xInv] || 1) { 31243 // Compute sbox 31244 s = xInv ^ xInv << 1 ^ xInv << 2 ^ xInv << 3 ^ xInv << 4; 31245 s = s >> 8 ^ s & 255 ^ 99; 31246 sbox[x] = s; 31247 sboxInv[s] = x; 31248 31249 // Compute MixColumns 31250 x8 = d[x4 = d[x2 = d[x]]]; 31251 tDec = x8 * 0x1010101 ^ x4 * 0x10001 ^ x2 * 0x101 ^ x * 0x1010100; 31252 tEnc = d[s] * 0x101 ^ s * 0x1010100; 31253 31254 for (i = 0; i < 4; i++) { 31255 encTable[i][x] = tEnc = tEnc << 24 ^ tEnc >>> 8; 31256 decTable[i][s] = tDec = tDec << 24 ^ tDec >>> 8; 31257 } 31258 } 31259 31260 // Compactify. Considerable speedup on Firefox. 31261 for (i = 0; i < 5; i++) { 31262 encTable[i] = encTable[i].slice(0); 31263 decTable[i] = decTable[i].slice(0); 31264 } 31265 return tables; 31266 }; 31267 var aesTables = null; 31268 31269 /** 31270 * Schedule out an AES key for both encryption and decryption. This 31271 * is a low-level class. Use a cipher mode to do bulk encryption. 31272 * 31273 * @class AES 31274 * @param key {Array} The key as an array of 4, 6 or 8 words. 31275 */ 31276 31277 var AES = function () { 31278 function AES(key) { 31279 classCallCheck$2(this, AES); 31280 31281 /** 31282 * The expanded S-box and inverse S-box tables. These will be computed 31283 * on the client so that we don't have to send them down the wire. 31284 * 31285 * There are two tables, _tables[0] is for encryption and 31286 * _tables[1] is for decryption. 31287 * 31288 * The first 4 sub-tables are the expanded S-box with MixColumns. The 31289 * last (_tables[01][4]) is the S-box itself. 31290 * 31291 * @private 31292 */ 31293 // if we have yet to precompute the S-box tables 31294 // do so now 31295 if (!aesTables) { 31296 aesTables = precompute(); 31297 } 31298 // then make a copy of that object for use 31299 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()]]; 31300 var i = void 0; 31301 var j = void 0; 31302 var tmp = void 0; 31303 var encKey = void 0; 31304 var decKey = void 0; 31305 var sbox = this._tables[0][4]; 31306 var decTable = this._tables[1]; 31307 var keyLen = key.length; 31308 var rcon = 1; 31309 31310 if (keyLen !== 4 && keyLen !== 6 && keyLen !== 8) { 31311 throw new Error('Invalid aes key size'); 31312 } 31313 31314 encKey = key.slice(0); 31315 decKey = []; 31316 this._key = [encKey, decKey]; 31317 31318 // schedule encryption keys 31319 for (i = keyLen; i < 4 * keyLen + 28; i++) { 31320 tmp = encKey[i - 1]; 31321 31322 // apply sbox 31323 if (i % keyLen === 0 || keyLen === 8 && i % keyLen === 4) { 31324 tmp = sbox[tmp >>> 24] << 24 ^ sbox[tmp >> 16 & 255] << 16 ^ sbox[tmp >> 8 & 255] << 8 ^ sbox[tmp & 255]; 31325 31326 // shift rows and add rcon 31327 if (i % keyLen === 0) { 31328 tmp = tmp << 8 ^ tmp >>> 24 ^ rcon << 24; 31329 rcon = rcon << 1 ^ (rcon >> 7) * 283; 31330 } 31331 } 31332 31333 encKey[i] = encKey[i - keyLen] ^ tmp; 31334 } 31335 31336 // schedule decryption keys 31337 for (j = 0; i; j++, i--) { 31338 tmp = encKey[j & 3 ? i : i - 4]; 31339 if (i <= 4 || j < 4) { 31340 decKey[j] = tmp; 31341 } else { 31342 decKey[j] = decTable[0][sbox[tmp >>> 24]] ^ decTable[1][sbox[tmp >> 16 & 255]] ^ decTable[2][sbox[tmp >> 8 & 255]] ^ decTable[3][sbox[tmp & 255]]; 31343 } 31344 } 31345 } 31346 31347 /** 31348 * Decrypt 16 bytes, specified as four 32-bit words. 31349 * 31350 * @param {Number} encrypted0 the first word to decrypt 31351 * @param {Number} encrypted1 the second word to decrypt 31352 * @param {Number} encrypted2 the third word to decrypt 31353 * @param {Number} encrypted3 the fourth word to decrypt 31354 * @param {Int32Array} out the array to write the decrypted words 31355 * into 31356 * @param {Number} offset the offset into the output array to start 31357 * writing results 31358 * @return {Array} The plaintext. 31359 */ 31360 31361 AES.prototype.decrypt = function decrypt(encrypted0, encrypted1, encrypted2, encrypted3, out, offset) { 31362 var key = this._key[1]; 31363 // state variables a,b,c,d are loaded with pre-whitened data 31364 var a = encrypted0 ^ key[0]; 31365 var b = encrypted3 ^ key[1]; 31366 var c = encrypted2 ^ key[2]; 31367 var d = encrypted1 ^ key[3]; 31368 var a2 = void 0; 31369 var b2 = void 0; 31370 var c2 = void 0; 31371 31372 // key.length === 2 ? 31373 var nInnerRounds = key.length / 4 - 2; 31374 var i = void 0; 31375 var kIndex = 4; 31376 var table = this._tables[1]; 31377 31378 // load up the tables 31379 var table0 = table[0]; 31380 var table1 = table[1]; 31381 var table2 = table[2]; 31382 var table3 = table[3]; 31383 var sbox = table[4]; 31384 31385 // Inner rounds. Cribbed from OpenSSL. 31386 for (i = 0; i < nInnerRounds; i++) { 31387 a2 = table0[a >>> 24] ^ table1[b >> 16 & 255] ^ table2[c >> 8 & 255] ^ table3[d & 255] ^ key[kIndex]; 31388 b2 = table0[b >>> 24] ^ table1[c >> 16 & 255] ^ table2[d >> 8 & 255] ^ table3[a & 255] ^ key[kIndex + 1]; 31389 c2 = table0[c >>> 24] ^ table1[d >> 16 & 255] ^ table2[a >> 8 & 255] ^ table3[b & 255] ^ key[kIndex + 2]; 31390 d = table0[d >>> 24] ^ table1[a >> 16 & 255] ^ table2[b >> 8 & 255] ^ table3[c & 255] ^ key[kIndex + 3]; 31391 kIndex += 4; 31392 a = a2;b = b2;c = c2; 31393 } 31394 31395 // Last round. 31396 for (i = 0; i < 4; i++) { 31397 out[(3 & -i) + offset] = sbox[a >>> 24] << 24 ^ sbox[b >> 16 & 255] << 16 ^ sbox[c >> 8 & 255] << 8 ^ sbox[d & 255] ^ key[kIndex++]; 31398 a2 = a;a = b;b = c;c = d;d = a2; 31399 } 31400 }; 31401 31402 return AES; 31403 }(); 31404 31405 /** 31406 * @file stream.js 31407 */ 31408 /** 31409 * A lightweight readable stream implemention that handles event dispatching. 31410 * 31411 * @class Stream 31412 */ 31413 var Stream$3 = function () { 31414 function Stream() { 31415 classCallCheck$2(this, Stream); 31416 31417 this.listeners = {}; 31418 } 31419 31420 /** 31421 * Add a listener for a specified event type. 31422 * 31423 * @param {String} type the event name 31424 * @param {Function} listener the callback to be invoked when an event of 31425 * the specified type occurs 31426 */ 31427 31428 Stream.prototype.on = function on(type, listener) { 31429 if (!this.listeners[type]) { 31430 this.listeners[type] = []; 31431 } 31432 this.listeners[type].push(listener); 31433 }; 31434 31435 /** 31436 * Remove a listener for a specified event type. 31437 * 31438 * @param {String} type the event name 31439 * @param {Function} listener a function previously registered for this 31440 * type of event through `on` 31441 * @return {Boolean} if we could turn it off or not 31442 */ 31443 31444 Stream.prototype.off = function off(type, listener) { 31445 if (!this.listeners[type]) { 31446 return false; 31447 } 31448 31449 var index = this.listeners[type].indexOf(listener); 31450 31451 this.listeners[type].splice(index, 1); 31452 return index > -1; 31453 }; 31454 31455 /** 31456 * Trigger an event of the specified type on this stream. Any additional 31457 * arguments to this function are passed as parameters to event listeners. 31458 * 31459 * @param {String} type the event name 31460 */ 31461 31462 Stream.prototype.trigger = function trigger(type) { 31463 var callbacks = this.listeners[type]; 31464 31465 if (!callbacks) { 31466 return; 31467 } 31468 31469 // Slicing the arguments on every invocation of this method 31470 // can add a significant amount of overhead. Avoid the 31471 // intermediate object creation for the common case of a 31472 // single callback argument 31473 if (arguments.length === 2) { 31474 var length = callbacks.length; 31475 31476 for (var i = 0; i < length; ++i) { 31477 callbacks[i].call(this, arguments[1]); 31478 } 31479 } else { 31480 var args = Array.prototype.slice.call(arguments, 1); 31481 var _length = callbacks.length; 31482 31483 for (var _i = 0; _i < _length; ++_i) { 31484 callbacks[_i].apply(this, args); 31485 } 31486 } 31487 }; 31488 31489 /** 31490 * Destroys the stream and cleans up. 31491 */ 31492 31493 Stream.prototype.dispose = function dispose() { 31494 this.listeners = {}; 31495 }; 31496 /** 31497 * Forwards all `data` events on this stream to the destination stream. The 31498 * destination stream should provide a method `push` to receive the data 31499 * events as they arrive. 31500 * 31501 * @param {Stream} destination the stream that will receive all `data` events 31502 * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options 31503 */ 31504 31505 Stream.prototype.pipe = function pipe(destination) { 31506 this.on('data', function (data) { 31507 destination.push(data); 31508 }); 31509 }; 31510 31511 return Stream; 31512 }(); 31513 31514 /** 31515 * @file async-stream.js 31516 */ 31517 /** 31518 * A wrapper around the Stream class to use setTiemout 31519 * and run stream "jobs" Asynchronously 31520 * 31521 * @class AsyncStream 31522 * @extends Stream 31523 */ 31524 31525 var AsyncStream = function (_Stream) { 31526 inherits$2(AsyncStream, _Stream); 31527 31528 function AsyncStream() { 31529 classCallCheck$2(this, AsyncStream); 31530 31531 var _this = possibleConstructorReturn$2(this, _Stream.call(this, Stream$3)); 31532 31533 _this.jobs = []; 31534 _this.delay = 1; 31535 _this.timeout_ = null; 31536 return _this; 31537 } 31538 31539 /** 31540 * process an async job 31541 * 31542 * @private 31543 */ 31544 31545 AsyncStream.prototype.processJob_ = function processJob_() { 31546 this.jobs.shift()(); 31547 if (this.jobs.length) { 31548 this.timeout_ = setTimeout(this.processJob_.bind(this), this.delay); 31549 } else { 31550 this.timeout_ = null; 31551 } 31552 }; 31553 31554 /** 31555 * push a job into the stream 31556 * 31557 * @param {Function} job the job to push into the stream 31558 */ 31559 31560 AsyncStream.prototype.push = function push(job) { 31561 this.jobs.push(job); 31562 if (!this.timeout_) { 31563 this.timeout_ = setTimeout(this.processJob_.bind(this), this.delay); 31564 } 31565 }; 31566 31567 return AsyncStream; 31568 }(Stream$3); 31569 31570 /** 31571 * @file decrypter.js 31572 * 31573 * An asynchronous implementation of AES-128 CBC decryption with 31574 * PKCS#7 padding. 31575 */ 31576 31577 /** 31578 * Convert network-order (big-endian) bytes into their little-endian 31579 * representation. 31580 */ 31581 var ntoh = function ntoh(word) { 31582 return word << 24 | (word & 0xff00) << 8 | (word & 0xff0000) >> 8 | word >>> 24; 31583 }; 31584 31585 /** 31586 * Decrypt bytes using AES-128 with CBC and PKCS#7 padding. 31587 * 31588 * @param {Uint8Array} encrypted the encrypted bytes 31589 * @param {Uint32Array} key the bytes of the decryption key 31590 * @param {Uint32Array} initVector the initialization vector (IV) to 31591 * use for the first round of CBC. 31592 * @return {Uint8Array} the decrypted bytes 31593 * 31594 * @see http://en.wikipedia.org/wiki/Advanced_Encryption_Standard 31595 * @see http://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_Block_Chaining_.28CBC.29 31596 * @see https://tools.ietf.org/html/rfc2315 31597 */ 31598 var decrypt = function decrypt(encrypted, key, initVector) { 31599 // word-level access to the encrypted bytes 31600 var encrypted32 = new Int32Array(encrypted.buffer, encrypted.byteOffset, encrypted.byteLength >> 2); 31601 31602 var decipher = new AES(Array.prototype.slice.call(key)); 31603 31604 // byte and word-level access for the decrypted output 31605 var decrypted = new Uint8Array(encrypted.byteLength); 31606 var decrypted32 = new Int32Array(decrypted.buffer); 31607 31608 // temporary variables for working with the IV, encrypted, and 31609 // decrypted data 31610 var init0 = void 0; 31611 var init1 = void 0; 31612 var init2 = void 0; 31613 var init3 = void 0; 31614 var encrypted0 = void 0; 31615 var encrypted1 = void 0; 31616 var encrypted2 = void 0; 31617 var encrypted3 = void 0; 31618 31619 // iteration variable 31620 var wordIx = void 0; 31621 31622 // pull out the words of the IV to ensure we don't modify the 31623 // passed-in reference and easier access 31624 init0 = initVector[0]; 31625 init1 = initVector[1]; 31626 init2 = initVector[2]; 31627 init3 = initVector[3]; 31628 31629 // decrypt four word sequences, applying cipher-block chaining (CBC) 31630 // to each decrypted block 31631 for (wordIx = 0; wordIx < encrypted32.length; wordIx += 4) { 31632 // convert big-endian (network order) words into little-endian 31633 // (javascript order) 31634 encrypted0 = ntoh(encrypted32[wordIx]); 31635 encrypted1 = ntoh(encrypted32[wordIx + 1]); 31636 encrypted2 = ntoh(encrypted32[wordIx + 2]); 31637 encrypted3 = ntoh(encrypted32[wordIx + 3]); 31638 31639 // decrypt the block 31640 decipher.decrypt(encrypted0, encrypted1, encrypted2, encrypted3, decrypted32, wordIx); 31641 31642 // XOR with the IV, and restore network byte-order to obtain the 31643 // plaintext 31644 decrypted32[wordIx] = ntoh(decrypted32[wordIx] ^ init0); 31645 decrypted32[wordIx + 1] = ntoh(decrypted32[wordIx + 1] ^ init1); 31646 decrypted32[wordIx + 2] = ntoh(decrypted32[wordIx + 2] ^ init2); 31647 decrypted32[wordIx + 3] = ntoh(decrypted32[wordIx + 3] ^ init3); 31648 31649 // setup the IV for the next round 31650 init0 = encrypted0; 31651 init1 = encrypted1; 31652 init2 = encrypted2; 31653 init3 = encrypted3; 31654 } 31655 31656 return decrypted; 31657 }; 31658 31659 /** 31660 * The `Decrypter` class that manages decryption of AES 31661 * data through `AsyncStream` objects and the `decrypt` 31662 * function 31663 * 31664 * @param {Uint8Array} encrypted the encrypted bytes 31665 * @param {Uint32Array} key the bytes of the decryption key 31666 * @param {Uint32Array} initVector the initialization vector (IV) to 31667 * @param {Function} done the function to run when done 31668 * @class Decrypter 31669 */ 31670 31671 var Decrypter = function () { 31672 function Decrypter(encrypted, key, initVector, done) { 31673 classCallCheck$2(this, Decrypter); 31674 31675 var step = Decrypter.STEP; 31676 var encrypted32 = new Int32Array(encrypted.buffer); 31677 var decrypted = new Uint8Array(encrypted.byteLength); 31678 var i = 0; 31679 31680 this.asyncStream_ = new AsyncStream(); 31681 31682 // split up the encryption job and do the individual chunks asynchronously 31683 this.asyncStream_.push(this.decryptChunk_(encrypted32.subarray(i, i + step), key, initVector, decrypted)); 31684 for (i = step; i < encrypted32.length; i += step) { 31685 initVector = new Uint32Array([ntoh(encrypted32[i - 4]), ntoh(encrypted32[i - 3]), ntoh(encrypted32[i - 2]), ntoh(encrypted32[i - 1])]); 31686 this.asyncStream_.push(this.decryptChunk_(encrypted32.subarray(i, i + step), key, initVector, decrypted)); 31687 } 31688 // invoke the done() callback when everything is finished 31689 this.asyncStream_.push(function () { 31690 // remove pkcs#7 padding from the decrypted bytes 31691 done(null, unpad(decrypted)); 31692 }); 31693 } 31694 31695 /** 31696 * a getter for step the maximum number of bytes to process at one time 31697 * 31698 * @return {Number} the value of step 32000 31699 */ 31700 31701 /** 31702 * @private 31703 */ 31704 Decrypter.prototype.decryptChunk_ = function decryptChunk_(encrypted, key, initVector, decrypted) { 31705 return function () { 31706 var bytes = decrypt(encrypted, key, initVector); 31707 31708 decrypted.set(bytes, encrypted.byteOffset); 31709 }; 31710 }; 31711 31712 createClass$1(Decrypter, null, [{ 31713 key: 'STEP', 31714 get: function get$$1() { 31715 // 4 * 8000; 31716 return 32000; 31717 } 31718 }]); 31719 return Decrypter; 31720 }(); 31721 31722 /** 31723 * @videojs/http-streaming 31724 * @version 1.1.0 31725 * @copyright 2018 Brightcove, Inc 31726 * @license Apache-2.0 31727 */ 31728 31729 /** 31730 * @file resolve-url.js 31731 */ 31732 31733 var resolveUrl$1 = function resolveUrl(baseURL, relativeURL) { 31734 // return early if we don't need to resolve 31735 if (/^[a-z]+:/i.test(relativeURL)) { 31736 return relativeURL; 31737 } 31738 31739 // if the base URL is relative then combine with the current location 31740 if (!/\/\//i.test(baseURL)) { 31741 baseURL = urlToolkit.buildAbsoluteURL(window_1.location.href, baseURL); 31742 } 31743 31744 return urlToolkit.buildAbsoluteURL(baseURL, relativeURL); 31745 }; 31746 31747 var classCallCheck$3 = function classCallCheck$$1(instance, Constructor) { 31748 if (!(instance instanceof Constructor)) { 31749 throw new TypeError("Cannot call a class as a function"); 31750 } 31751 }; 31752 31753 var createClass$2 = function () { 31754 function defineProperties(target, props) { 31755 for (var i = 0; i < props.length; i++) { 31756 var descriptor = props[i]; 31757 descriptor.enumerable = descriptor.enumerable || false; 31758 descriptor.configurable = true; 31759 if ("value" in descriptor) descriptor.writable = true; 31760 Object.defineProperty(target, descriptor.key, descriptor); 31761 } 31762 } 31763 31764 return function (Constructor, protoProps, staticProps) { 31765 if (protoProps) defineProperties(Constructor.prototype, protoProps); 31766 if (staticProps) defineProperties(Constructor, staticProps); 31767 return Constructor; 31768 }; 31769 }(); 31770 31771 var get$2 = function get$$1(object, property, receiver) { 31772 if (object === null) object = Function.prototype; 31773 var desc = Object.getOwnPropertyDescriptor(object, property); 31774 31775 if (desc === undefined) { 31776 var parent = Object.getPrototypeOf(object); 31777 31778 if (parent === null) { 31779 return undefined; 31780 } else { 31781 return get$$1(parent, property, receiver); 31782 } 31783 } else if ("value" in desc) { 31784 return desc.value; 31785 } else { 31786 var getter = desc.get; 31787 31788 if (getter === undefined) { 31789 return undefined; 31790 } 31791 31792 return getter.call(receiver); 31793 } 31794 }; 31795 31796 var inherits$3 = function inherits$$1(subClass, superClass) { 31797 if (typeof superClass !== "function" && superClass !== null) { 31798 throw new TypeError("Super expression must either be null or a function, not " + (typeof superClass === 'undefined' ? 'undefined' : _typeof(superClass))); 31799 } 31800 31801 subClass.prototype = Object.create(superClass && superClass.prototype, { 31802 constructor: { 31803 value: subClass, 31804 enumerable: false, 31805 writable: true, 31806 configurable: true 31807 } 31808 }); 31809 if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; 31810 }; 31811 31812 var possibleConstructorReturn$3 = function possibleConstructorReturn$$1(self, call) { 31813 if (!self) { 31814 throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); 31815 } 31816 31817 return call && ((typeof call === 'undefined' ? 'undefined' : _typeof(call)) === "object" || typeof call === "function") ? call : self; 31818 }; 31819 31820 var slicedToArray$1 = function () { 31821 function sliceIterator(arr, i) { 31822 var _arr = []; 31823 var _n = true; 31824 var _d = false; 31825 var _e = undefined; 31826 31827 try { 31828 for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { 31829 _arr.push(_s.value); 31830 31831 if (i && _arr.length === i) break; 31832 } 31833 } catch (err) { 31834 _d = true; 31835 _e = err; 31836 } finally { 31837 try { 31838 if (!_n && _i["return"]) _i["return"](); 31839 } finally { 31840 if (_d) throw _e; 31841 } 31842 } 31843 31844 return _arr; 31845 } 31846 31847 return function (arr, i) { 31848 if (Array.isArray(arr)) { 31849 return arr; 31850 } else if (Symbol.iterator in Object(arr)) { 31851 return sliceIterator(arr, i); 31852 } else { 31853 throw new TypeError("Invalid attempt to destructure non-iterable instance"); 31854 } 31855 }; 31856 }(); 31857 31858 /** 31859 * @file playlist-loader.js 31860 * 31861 * A state machine that manages the loading, caching, and updating of 31862 * M3U8 playlists. 31863 * 31864 */ 31865 31866 var mergeOptions$1 = videojs$1.mergeOptions, 31867 EventTarget$1 = videojs$1.EventTarget, 31868 log$2 = videojs$1.log; 31869 31870 /** 31871 * Loops through all supported media groups in master and calls the provided 31872 * callback for each group 31873 * 31874 * @param {Object} master 31875 * The parsed master manifest object 31876 * @param {Function} callback 31877 * Callback to call for each media group 31878 */ 31879 31880 var forEachMediaGroup = function forEachMediaGroup(master, callback) { 31881 ['AUDIO', 'SUBTITLES'].forEach(function (mediaType) { 31882 for (var groupKey in master.mediaGroups[mediaType]) { 31883 for (var labelKey in master.mediaGroups[mediaType][groupKey]) { 31884 var mediaProperties = master.mediaGroups[mediaType][groupKey][labelKey]; 31885 31886 callback(mediaProperties, mediaType, groupKey, labelKey); 31887 } 31888 } 31889 }); 31890 }; 31891 31892 /** 31893 * Returns a new array of segments that is the result of merging 31894 * properties from an older list of segments onto an updated 31895 * list. No properties on the updated playlist will be overridden. 31896 * 31897 * @param {Array} original the outdated list of segments 31898 * @param {Array} update the updated list of segments 31899 * @param {Number=} offset the index of the first update 31900 * segment in the original segment list. For non-live playlists, 31901 * this should always be zero and does not need to be 31902 * specified. For live playlists, it should be the difference 31903 * between the media sequence numbers in the original and updated 31904 * playlists. 31905 * @return a list of merged segment objects 31906 */ 31907 var updateSegments = function updateSegments(original, update, offset) { 31908 var result = update.slice(); 31909 31910 offset = offset || 0; 31911 var length = Math.min(original.length, update.length + offset); 31912 31913 for (var i = offset; i < length; i++) { 31914 result[i - offset] = mergeOptions$1(original[i], result[i - offset]); 31915 } 31916 return result; 31917 }; 31918 31919 var resolveSegmentUris = function resolveSegmentUris(segment, baseUri) { 31920 if (!segment.resolvedUri) { 31921 segment.resolvedUri = resolveUrl$1(baseUri, segment.uri); 31922 } 31923 if (segment.key && !segment.key.resolvedUri) { 31924 segment.key.resolvedUri = resolveUrl$1(baseUri, segment.key.uri); 31925 } 31926 if (segment.map && !segment.map.resolvedUri) { 31927 segment.map.resolvedUri = resolveUrl$1(baseUri, segment.map.uri); 31928 } 31929 }; 31930 31931 /** 31932 * Returns a new master playlist that is the result of merging an 31933 * updated media playlist into the original version. If the 31934 * updated media playlist does not match any of the playlist 31935 * entries in the original master playlist, null is returned. 31936 * 31937 * @param {Object} master a parsed master M3U8 object 31938 * @param {Object} media a parsed media M3U8 object 31939 * @return {Object} a new object that represents the original 31940 * master playlist with the updated media playlist merged in, or 31941 * null if the merge produced no change. 31942 */ 31943 var updateMaster = function updateMaster(master, media) { 31944 var result = mergeOptions$1(master, {}); 31945 var playlist = result.playlists[media.uri]; 31946 31947 if (!playlist) { 31948 return null; 31949 } 31950 31951 // consider the playlist unchanged if the number of segments is equal and the media 31952 // sequence number is unchanged 31953 if (playlist.segments && media.segments && playlist.segments.length === media.segments.length && playlist.mediaSequence === media.mediaSequence) { 31954 return null; 31955 } 31956 31957 var mergedPlaylist = mergeOptions$1(playlist, media); 31958 31959 // if the update could overlap existing segment information, merge the two segment lists 31960 if (playlist.segments) { 31961 mergedPlaylist.segments = updateSegments(playlist.segments, media.segments, media.mediaSequence - playlist.mediaSequence); 31962 } 31963 31964 // resolve any segment URIs to prevent us from having to do it later 31965 mergedPlaylist.segments.forEach(function (segment) { 31966 resolveSegmentUris(segment, mergedPlaylist.resolvedUri); 31967 }); 31968 31969 // TODO Right now in the playlists array there are two references to each playlist, one 31970 // that is referenced by index, and one by URI. The index reference may no longer be 31971 // necessary. 31972 for (var i = 0; i < result.playlists.length; i++) { 31973 if (result.playlists[i].uri === media.uri) { 31974 result.playlists[i] = mergedPlaylist; 31975 } 31976 } 31977 result.playlists[media.uri] = mergedPlaylist; 31978 31979 return result; 31980 }; 31981 31982 var setupMediaPlaylists = function setupMediaPlaylists(master) { 31983 // setup by-URI lookups and resolve media playlist URIs 31984 var i = master.playlists.length; 31985 31986 while (i--) { 31987 var playlist = master.playlists[i]; 31988 31989 master.playlists[playlist.uri] = playlist; 31990 playlist.resolvedUri = resolveUrl$1(master.uri, playlist.uri); 31991 playlist.id = i; 31992 31993 if (!playlist.attributes) { 31994 // Although the spec states an #EXT-X-STREAM-INF tag MUST have a 31995 // BANDWIDTH attribute, we can play the stream without it. This means a poorly 31996 // formatted master playlist may not have an attribute list. An attributes 31997 // property is added here to prevent undefined references when we encounter 31998 // this scenario. 31999 playlist.attributes = {}; 32000 32001 log$2.warn('Invalid playlist STREAM-INF detected. Missing BANDWIDTH attribute.'); 32002 } 32003 } 32004 }; 32005 32006 var resolveMediaGroupUris = function resolveMediaGroupUris(master) { 32007 forEachMediaGroup(master, function (properties) { 32008 if (properties.uri) { 32009 properties.resolvedUri = resolveUrl$1(master.uri, properties.uri); 32010 } 32011 }); 32012 }; 32013 32014 /** 32015 * Calculates the time to wait before refreshing a live playlist 32016 * 32017 * @param {Object} media 32018 * The current media 32019 * @param {Boolean} update 32020 * True if there were any updates from the last refresh, false otherwise 32021 * @return {Number} 32022 * The time in ms to wait before refreshing the live playlist 32023 */ 32024 var refreshDelay = function refreshDelay(media, update) { 32025 var lastSegment = media.segments[media.segments.length - 1]; 32026 var delay = void 0; 32027 32028 if (update && lastSegment && lastSegment.duration) { 32029 delay = lastSegment.duration * 1000; 32030 } else { 32031 // if the playlist is unchanged since the last reload or last segment duration 32032 // cannot be determined, try again after half the target duration 32033 delay = (media.targetDuration || 10) * 500; 32034 } 32035 return delay; 32036 }; 32037 32038 /** 32039 * Load a playlist from a remote location 32040 * 32041 * @class PlaylistLoader 32042 * @extends Stream 32043 * @param {String} srcUrl the url to start with 32044 * @param {Boolean} withCredentials the withCredentials xhr option 32045 * @constructor 32046 */ 32047 32048 var PlaylistLoader = function (_EventTarget) { 32049 inherits$3(PlaylistLoader, _EventTarget); 32050 32051 function PlaylistLoader(srcUrl, hls, withCredentials) { 32052 classCallCheck$3(this, PlaylistLoader); 32053 32054 var _this = possibleConstructorReturn$3(this, (PlaylistLoader.__proto__ || Object.getPrototypeOf(PlaylistLoader)).call(this)); 32055 32056 _this.srcUrl = srcUrl; 32057 _this.hls_ = hls; 32058 _this.withCredentials = withCredentials; 32059 32060 if (!_this.srcUrl) { 32061 throw new Error('A non-empty playlist URL is required'); 32062 } 32063 32064 // initialize the loader state 32065 _this.state = 'HAVE_NOTHING'; 32066 32067 // live playlist staleness timeout 32068 _this.on('mediaupdatetimeout', function () { 32069 if (_this.state !== 'HAVE_METADATA') { 32070 // only refresh the media playlist if no other activity is going on 32071 return; 32072 } 32073 32074 _this.state = 'HAVE_CURRENT_METADATA'; 32075 32076 _this.request = _this.hls_.xhr({ 32077 uri: resolveUrl$1(_this.master.uri, _this.media().uri), 32078 withCredentials: _this.withCredentials 32079 }, function (error, req) { 32080 // disposed 32081 if (!_this.request) { 32082 return; 32083 } 32084 32085 if (error) { 32086 return _this.playlistRequestError(_this.request, _this.media().uri, 'HAVE_METADATA'); 32087 } 32088 32089 _this.haveMetadata(_this.request, _this.media().uri); 32090 }); 32091 }); 32092 return _this; 32093 } 32094 32095 createClass$2(PlaylistLoader, [{ 32096 key: 'playlistRequestError', 32097 value: function playlistRequestError(xhr, url, startingState) { 32098 // any in-flight request is now finished 32099 this.request = null; 32100 32101 if (startingState) { 32102 this.state = startingState; 32103 } 32104 32105 this.error = { 32106 playlist: this.master.playlists[url], 32107 status: xhr.status, 32108 message: 'HLS playlist request error at URL: ' + url, 32109 responseText: xhr.responseText, 32110 code: xhr.status >= 500 ? 4 : 2 32111 }; 32112 32113 this.trigger('error'); 32114 } 32115 32116 // update the playlist loader's state in response to a new or 32117 // updated playlist. 32118 32119 }, { 32120 key: 'haveMetadata', 32121 value: function haveMetadata(xhr, url) { 32122 var _this2 = this; 32123 32124 // any in-flight request is now finished 32125 this.request = null; 32126 this.state = 'HAVE_METADATA'; 32127 32128 var parser = new Parser(); 32129 32130 parser.push(xhr.responseText); 32131 parser.end(); 32132 parser.manifest.uri = url; 32133 // m3u8-parser does not attach an attributes property to media playlists so make 32134 // sure that the property is attached to avoid undefined reference errors 32135 parser.manifest.attributes = parser.manifest.attributes || {}; 32136 32137 // merge this playlist into the master 32138 var update = updateMaster(this.master, parser.manifest); 32139 32140 this.targetDuration = parser.manifest.targetDuration; 32141 32142 if (update) { 32143 this.master = update; 32144 this.media_ = this.master.playlists[parser.manifest.uri]; 32145 } else { 32146 this.trigger('playlistunchanged'); 32147 } 32148 32149 // refresh live playlists after a target duration passes 32150 if (!this.media().endList) { 32151 window_1.clearTimeout(this.mediaUpdateTimeout); 32152 this.mediaUpdateTimeout = window_1.setTimeout(function () { 32153 _this2.trigger('mediaupdatetimeout'); 32154 }, refreshDelay(this.media(), !!update)); 32155 } 32156 32157 this.trigger('loadedplaylist'); 32158 } 32159 32160 /** 32161 * Abort any outstanding work and clean up. 32162 */ 32163 32164 }, { 32165 key: 'dispose', 32166 value: function dispose() { 32167 this.stopRequest(); 32168 window_1.clearTimeout(this.mediaUpdateTimeout); 32169 } 32170 }, { 32171 key: 'stopRequest', 32172 value: function stopRequest() { 32173 if (this.request) { 32174 var oldRequest = this.request; 32175 32176 this.request = null; 32177 oldRequest.onreadystatechange = null; 32178 oldRequest.abort(); 32179 } 32180 } 32181 32182 /** 32183 * When called without any arguments, returns the currently 32184 * active media playlist. When called with a single argument, 32185 * triggers the playlist loader to asynchronously switch to the 32186 * specified media playlist. Calling this method while the 32187 * loader is in the HAVE_NOTHING causes an error to be emitted 32188 * but otherwise has no effect. 32189 * 32190 * @param {Object=} playlist the parsed media playlist 32191 * object to switch to 32192 * @return {Playlist} the current loaded media 32193 */ 32194 32195 }, { 32196 key: 'media', 32197 value: function media(playlist) { 32198 var _this3 = this; 32199 32200 // getter 32201 if (!playlist) { 32202 return this.media_; 32203 } 32204 32205 // setter 32206 if (this.state === 'HAVE_NOTHING') { 32207 throw new Error('Cannot switch media playlist from ' + this.state); 32208 } 32209 32210 var startingState = this.state; 32211 32212 // find the playlist object if the target playlist has been 32213 // specified by URI 32214 if (typeof playlist === 'string') { 32215 if (!this.master.playlists[playlist]) { 32216 throw new Error('Unknown playlist URI: ' + playlist); 32217 } 32218 playlist = this.master.playlists[playlist]; 32219 } 32220 32221 var mediaChange = !this.media_ || playlist.uri !== this.media_.uri; 32222 32223 // switch to fully loaded playlists immediately 32224 if (this.master.playlists[playlist.uri].endList) { 32225 // abort outstanding playlist requests 32226 if (this.request) { 32227 this.request.onreadystatechange = null; 32228 this.request.abort(); 32229 this.request = null; 32230 } 32231 this.state = 'HAVE_METADATA'; 32232 this.media_ = playlist; 32233 32234 // trigger media change if the active media has been updated 32235 if (mediaChange) { 32236 this.trigger('mediachanging'); 32237 this.trigger('mediachange'); 32238 } 32239 return; 32240 } 32241 32242 // switching to the active playlist is a no-op 32243 if (!mediaChange) { 32244 return; 32245 } 32246 32247 this.state = 'SWITCHING_MEDIA'; 32248 32249 // there is already an outstanding playlist request 32250 if (this.request) { 32251 if (resolveUrl$1(this.master.uri, playlist.uri) === this.request.url) { 32252 // requesting to switch to the same playlist multiple times 32253 // has no effect after the first 32254 return; 32255 } 32256 this.request.onreadystatechange = null; 32257 this.request.abort(); 32258 this.request = null; 32259 } 32260 32261 // request the new playlist 32262 if (this.media_) { 32263 this.trigger('mediachanging'); 32264 } 32265 32266 this.request = this.hls_.xhr({ 32267 uri: resolveUrl$1(this.master.uri, playlist.uri), 32268 withCredentials: this.withCredentials 32269 }, function (error, req) { 32270 // disposed 32271 if (!_this3.request) { 32272 return; 32273 } 32274 32275 if (error) { 32276 return _this3.playlistRequestError(_this3.request, playlist.uri, startingState); 32277 } 32278 32279 _this3.haveMetadata(req, playlist.uri); 32280 32281 // fire loadedmetadata the first time a media playlist is loaded 32282 if (startingState === 'HAVE_MASTER') { 32283 _this3.trigger('loadedmetadata'); 32284 } else { 32285 _this3.trigger('mediachange'); 32286 } 32287 }); 32288 } 32289 32290 /** 32291 * pause loading of the playlist 32292 */ 32293 32294 }, { 32295 key: 'pause', 32296 value: function pause() { 32297 this.stopRequest(); 32298 window_1.clearTimeout(this.mediaUpdateTimeout); 32299 if (this.state === 'HAVE_NOTHING') { 32300 // If we pause the loader before any data has been retrieved, its as if we never 32301 // started, so reset to an unstarted state. 32302 this.started = false; 32303 } 32304 // Need to restore state now that no activity is happening 32305 if (this.state === 'SWITCHING_MEDIA') { 32306 // if the loader was in the process of switching media, it should either return to 32307 // HAVE_MASTER or HAVE_METADATA depending on if the loader has loaded a media 32308 // playlist yet. This is determined by the existence of loader.media_ 32309 if (this.media_) { 32310 this.state = 'HAVE_METADATA'; 32311 } else { 32312 this.state = 'HAVE_MASTER'; 32313 } 32314 } else if (this.state === 'HAVE_CURRENT_METADATA') { 32315 this.state = 'HAVE_METADATA'; 32316 } 32317 } 32318 32319 /** 32320 * start loading of the playlist 32321 */ 32322 32323 }, { 32324 key: 'load', 32325 value: function load(isFinalRendition) { 32326 var _this4 = this; 32327 32328 window_1.clearTimeout(this.mediaUpdateTimeout); 32329 32330 var media = this.media(); 32331 32332 if (isFinalRendition) { 32333 var delay = media ? media.targetDuration / 2 * 1000 : 5 * 1000; 32334 32335 this.mediaUpdateTimeout = window_1.setTimeout(function () { 32336 return _this4.load(); 32337 }, delay); 32338 return; 32339 } 32340 32341 if (!this.started) { 32342 this.start(); 32343 return; 32344 } 32345 32346 if (media && !media.endList) { 32347 this.trigger('mediaupdatetimeout'); 32348 } else { 32349 this.trigger('loadedplaylist'); 32350 } 32351 } 32352 32353 /** 32354 * start loading of the playlist 32355 */ 32356 32357 }, { 32358 key: 'start', 32359 value: function start() { 32360 var _this5 = this; 32361 32362 this.started = true; 32363 32364 // request the specified URL 32365 this.request = this.hls_.xhr({ 32366 uri: this.srcUrl, 32367 withCredentials: this.withCredentials 32368 }, function (error, req) { 32369 // disposed 32370 if (!_this5.request) { 32371 return; 32372 } 32373 32374 // clear the loader's request reference 32375 _this5.request = null; 32376 32377 if (error) { 32378 _this5.error = { 32379 status: req.status, 32380 message: 'HLS playlist request error at URL: ' + _this5.srcUrl, 32381 responseText: req.responseText, 32382 // MEDIA_ERR_NETWORK 32383 code: 2 32384 }; 32385 if (_this5.state === 'HAVE_NOTHING') { 32386 _this5.started = false; 32387 } 32388 return _this5.trigger('error'); 32389 } 32390 32391 var parser = new Parser(); 32392 32393 parser.push(req.responseText); 32394 parser.end(); 32395 32396 _this5.state = 'HAVE_MASTER'; 32397 32398 parser.manifest.uri = _this5.srcUrl; 32399 32400 // loaded a master playlist 32401 if (parser.manifest.playlists) { 32402 _this5.master = parser.manifest; 32403 32404 setupMediaPlaylists(_this5.master); 32405 resolveMediaGroupUris(_this5.master); 32406 32407 _this5.trigger('loadedplaylist'); 32408 if (!_this5.request) { 32409 // no media playlist was specifically selected so start 32410 // from the first listed one 32411 _this5.media(parser.manifest.playlists[0]); 32412 } 32413 return; 32414 } 32415 32416 // loaded a media playlist 32417 // infer a master playlist if none was previously requested 32418 _this5.master = { 32419 mediaGroups: { 32420 'AUDIO': {}, 32421 'VIDEO': {}, 32422 'CLOSED-CAPTIONS': {}, 32423 'SUBTITLES': {} 32424 }, 32425 uri: window_1.location.href, 32426 playlists: [{ 32427 uri: _this5.srcUrl, 32428 id: 0 32429 }] 32430 }; 32431 _this5.master.playlists[_this5.srcUrl] = _this5.master.playlists[0]; 32432 _this5.master.playlists[0].resolvedUri = _this5.srcUrl; 32433 // m3u8-parser does not attach an attributes property to media playlists so make 32434 // sure that the property is attached to avoid undefined reference errors 32435 _this5.master.playlists[0].attributes = _this5.master.playlists[0].attributes || {}; 32436 _this5.haveMetadata(req, _this5.srcUrl); 32437 return _this5.trigger('loadedmetadata'); 32438 }); 32439 } 32440 }]); 32441 return PlaylistLoader; 32442 }(EventTarget$1); 32443 32444 /** 32445 * @file playlist.js 32446 * 32447 * Playlist related utilities. 32448 */ 32449 32450 var createTimeRange = videojs$1.createTimeRange; 32451 32452 /** 32453 * walk backward until we find a duration we can use 32454 * or return a failure 32455 * 32456 * @param {Playlist} playlist the playlist to walk through 32457 * @param {Number} endSequence the mediaSequence to stop walking on 32458 */ 32459 32460 var backwardDuration = function backwardDuration(playlist, endSequence) { 32461 var result = 0; 32462 var i = endSequence - playlist.mediaSequence; 32463 // if a start time is available for segment immediately following 32464 // the interval, use it 32465 var segment = playlist.segments[i]; 32466 32467 // Walk backward until we find the latest segment with timeline 32468 // information that is earlier than endSequence 32469 if (segment) { 32470 if (typeof segment.start !== 'undefined') { 32471 return { result: segment.start, precise: true }; 32472 } 32473 if (typeof segment.end !== 'undefined') { 32474 return { 32475 result: segment.end - segment.duration, 32476 precise: true 32477 }; 32478 } 32479 } 32480 while (i--) { 32481 segment = playlist.segments[i]; 32482 if (typeof segment.end !== 'undefined') { 32483 return { result: result + segment.end, precise: true }; 32484 } 32485 32486 result += segment.duration; 32487 32488 if (typeof segment.start !== 'undefined') { 32489 return { result: result + segment.start, precise: true }; 32490 } 32491 } 32492 return { result: result, precise: false }; 32493 }; 32494 32495 /** 32496 * walk forward until we find a duration we can use 32497 * or return a failure 32498 * 32499 * @param {Playlist} playlist the playlist to walk through 32500 * @param {Number} endSequence the mediaSequence to stop walking on 32501 */ 32502 var forwardDuration = function forwardDuration(playlist, endSequence) { 32503 var result = 0; 32504 var segment = void 0; 32505 var i = endSequence - playlist.mediaSequence; 32506 // Walk forward until we find the earliest segment with timeline 32507 // information 32508 32509 for (; i < playlist.segments.length; i++) { 32510 segment = playlist.segments[i]; 32511 if (typeof segment.start !== 'undefined') { 32512 return { 32513 result: segment.start - result, 32514 precise: true 32515 }; 32516 } 32517 32518 result += segment.duration; 32519 32520 if (typeof segment.end !== 'undefined') { 32521 return { 32522 result: segment.end - result, 32523 precise: true 32524 }; 32525 } 32526 } 32527 // indicate we didn't find a useful duration estimate 32528 return { result: -1, precise: false }; 32529 }; 32530 32531 /** 32532 * Calculate the media duration from the segments associated with a 32533 * playlist. The duration of a subinterval of the available segments 32534 * may be calculated by specifying an end index. 32535 * 32536 * @param {Object} playlist a media playlist object 32537 * @param {Number=} endSequence an exclusive upper boundary 32538 * for the playlist. Defaults to playlist length. 32539 * @param {Number} expired the amount of time that has dropped 32540 * off the front of the playlist in a live scenario 32541 * @return {Number} the duration between the first available segment 32542 * and end index. 32543 */ 32544 var intervalDuration = function intervalDuration(playlist, endSequence, expired) { 32545 var backward = void 0; 32546 var forward = void 0; 32547 32548 if (typeof endSequence === 'undefined') { 32549 endSequence = playlist.mediaSequence + playlist.segments.length; 32550 } 32551 32552 if (endSequence < playlist.mediaSequence) { 32553 return 0; 32554 } 32555 32556 // do a backward walk to estimate the duration 32557 backward = backwardDuration(playlist, endSequence); 32558 if (backward.precise) { 32559 // if we were able to base our duration estimate on timing 32560 // information provided directly from the Media Source, return 32561 // it 32562 return backward.result; 32563 } 32564 32565 // walk forward to see if a precise duration estimate can be made 32566 // that way 32567 forward = forwardDuration(playlist, endSequence); 32568 if (forward.precise) { 32569 // we found a segment that has been buffered and so it's 32570 // position is known precisely 32571 return forward.result; 32572 } 32573 32574 // return the less-precise, playlist-based duration estimate 32575 return backward.result + expired; 32576 }; 32577 32578 /** 32579 * Calculates the duration of a playlist. If a start and end index 32580 * are specified, the duration will be for the subset of the media 32581 * timeline between those two indices. The total duration for live 32582 * playlists is always Infinity. 32583 * 32584 * @param {Object} playlist a media playlist object 32585 * @param {Number=} endSequence an exclusive upper 32586 * boundary for the playlist. Defaults to the playlist media 32587 * sequence number plus its length. 32588 * @param {Number=} expired the amount of time that has 32589 * dropped off the front of the playlist in a live scenario 32590 * @return {Number} the duration between the start index and end 32591 * index. 32592 */ 32593 var duration = function duration(playlist, endSequence, expired) { 32594 if (!playlist) { 32595 return 0; 32596 } 32597 32598 if (typeof expired !== 'number') { 32599 expired = 0; 32600 } 32601 32602 // if a slice of the total duration is not requested, use 32603 // playlist-level duration indicators when they're present 32604 if (typeof endSequence === 'undefined') { 32605 // if present, use the duration specified in the playlist 32606 if (playlist.totalDuration) { 32607 return playlist.totalDuration; 32608 } 32609 32610 // duration should be Infinity for live playlists 32611 if (!playlist.endList) { 32612 return window_1.Infinity; 32613 } 32614 } 32615 32616 // calculate the total duration based on the segment durations 32617 return intervalDuration(playlist, endSequence, expired); 32618 }; 32619 32620 /** 32621 * Calculate the time between two indexes in the current playlist 32622 * neight the start- nor the end-index need to be within the current 32623 * playlist in which case, the targetDuration of the playlist is used 32624 * to approximate the durations of the segments 32625 * 32626 * @param {Object} playlist a media playlist object 32627 * @param {Number} startIndex 32628 * @param {Number} endIndex 32629 * @return {Number} the number of seconds between startIndex and endIndex 32630 */ 32631 var sumDurations = function sumDurations(playlist, startIndex, endIndex) { 32632 var durations = 0; 32633 32634 if (startIndex > endIndex) { 32635 var _ref = [endIndex, startIndex]; 32636 startIndex = _ref[0]; 32637 endIndex = _ref[1]; 32638 } 32639 32640 if (startIndex < 0) { 32641 for (var i = startIndex; i < Math.min(0, endIndex); i++) { 32642 durations += playlist.targetDuration; 32643 } 32644 startIndex = 0; 32645 } 32646 32647 for (var _i = startIndex; _i < endIndex; _i++) { 32648 durations += playlist.segments[_i].duration; 32649 } 32650 32651 return durations; 32652 }; 32653 32654 /** 32655 * Determines the media index of the segment corresponding to the safe edge of the live 32656 * window which is the duration of the last segment plus 2 target durations from the end 32657 * of the playlist. 32658 * 32659 * @param {Object} playlist 32660 * a media playlist object 32661 * @return {Number} 32662 * The media index of the segment at the safe live point. 0 if there is no "safe" 32663 * point. 32664 * @function safeLiveIndex 32665 */ 32666 var safeLiveIndex = function safeLiveIndex(playlist) { 32667 if (!playlist.segments.length) { 32668 return 0; 32669 } 32670 32671 var i = playlist.segments.length - 1; 32672 var distanceFromEnd = playlist.segments[i].duration || playlist.targetDuration; 32673 var safeDistance = distanceFromEnd + playlist.targetDuration * 2; 32674 32675 while (i--) { 32676 distanceFromEnd += playlist.segments[i].duration; 32677 32678 if (distanceFromEnd >= safeDistance) { 32679 break; 32680 } 32681 } 32682 32683 return Math.max(0, i); 32684 }; 32685 32686 /** 32687 * Calculates the playlist end time 32688 * 32689 * @param {Object} playlist a media playlist object 32690 * @param {Number=} expired the amount of time that has 32691 * dropped off the front of the playlist in a live scenario 32692 * @param {Boolean|false} useSafeLiveEnd a boolean value indicating whether or not the 32693 * playlist end calculation should consider the safe live end 32694 * (truncate the playlist end by three segments). This is normally 32695 * used for calculating the end of the playlist's seekable range. 32696 * @returns {Number} the end time of playlist 32697 * @function playlistEnd 32698 */ 32699 var playlistEnd = function playlistEnd(playlist, expired, useSafeLiveEnd) { 32700 if (!playlist || !playlist.segments) { 32701 return null; 32702 } 32703 if (playlist.endList) { 32704 return duration(playlist); 32705 } 32706 32707 if (expired === null) { 32708 return null; 32709 } 32710 32711 expired = expired || 0; 32712 32713 var endSequence = useSafeLiveEnd ? safeLiveIndex(playlist) : playlist.segments.length; 32714 32715 return intervalDuration(playlist, playlist.mediaSequence + endSequence, expired); 32716 }; 32717 32718 /** 32719 * Calculates the interval of time that is currently seekable in a 32720 * playlist. The returned time ranges are relative to the earliest 32721 * moment in the specified playlist that is still available. A full 32722 * seekable implementation for live streams would need to offset 32723 * these values by the duration of content that has expired from the 32724 * stream. 32725 * 32726 * @param {Object} playlist a media playlist object 32727 * dropped off the front of the playlist in a live scenario 32728 * @param {Number=} expired the amount of time that has 32729 * dropped off the front of the playlist in a live scenario 32730 * @return {TimeRanges} the periods of time that are valid targets 32731 * for seeking 32732 */ 32733 var seekable = function seekable(playlist, expired) { 32734 var useSafeLiveEnd = true; 32735 var seekableStart = expired || 0; 32736 var seekableEnd = playlistEnd(playlist, expired, useSafeLiveEnd); 32737 32738 if (seekableEnd === null) { 32739 return createTimeRange(); 32740 } 32741 return createTimeRange(seekableStart, seekableEnd); 32742 }; 32743 32744 var isWholeNumber = function isWholeNumber(num) { 32745 return num - Math.floor(num) === 0; 32746 }; 32747 32748 var roundSignificantDigit = function roundSignificantDigit(increment, num) { 32749 // If we have a whole number, just add 1 to it 32750 if (isWholeNumber(num)) { 32751 return num + increment * 0.1; 32752 } 32753 32754 var numDecimalDigits = num.toString().split('.')[1].length; 32755 32756 for (var i = 1; i <= numDecimalDigits; i++) { 32757 var scale = Math.pow(10, i); 32758 var temp = num * scale; 32759 32760 if (isWholeNumber(temp) || i === numDecimalDigits) { 32761 return (temp + increment) / scale; 32762 } 32763 } 32764 }; 32765 32766 var ceilLeastSignificantDigit = roundSignificantDigit.bind(null, 1); 32767 var floorLeastSignificantDigit = roundSignificantDigit.bind(null, -1); 32768 32769 /** 32770 * Determine the index and estimated starting time of the segment that 32771 * contains a specified playback position in a media playlist. 32772 * 32773 * @param {Object} playlist the media playlist to query 32774 * @param {Number} currentTime The number of seconds since the earliest 32775 * possible position to determine the containing segment for 32776 * @param {Number} startIndex 32777 * @param {Number} startTime 32778 * @return {Object} 32779 */ 32780 var getMediaInfoForTime = function getMediaInfoForTime(playlist, currentTime, startIndex, startTime) { 32781 var i = void 0; 32782 var segment = void 0; 32783 var numSegments = playlist.segments.length; 32784 32785 var time = currentTime - startTime; 32786 32787 if (time < 0) { 32788 // Walk backward from startIndex in the playlist, adding durations 32789 // until we find a segment that contains `time` and return it 32790 if (startIndex > 0) { 32791 for (i = startIndex - 1; i >= 0; i--) { 32792 segment = playlist.segments[i]; 32793 time += floorLeastSignificantDigit(segment.duration); 32794 if (time > 0) { 32795 return { 32796 mediaIndex: i, 32797 startTime: startTime - sumDurations(playlist, startIndex, i) 32798 }; 32799 } 32800 } 32801 } 32802 // We were unable to find a good segment within the playlist 32803 // so select the first segment 32804 return { 32805 mediaIndex: 0, 32806 startTime: currentTime 32807 }; 32808 } 32809 32810 // When startIndex is negative, we first walk forward to first segment 32811 // adding target durations. If we "run out of time" before getting to 32812 // the first segment, return the first segment 32813 if (startIndex < 0) { 32814 for (i = startIndex; i < 0; i++) { 32815 time -= playlist.targetDuration; 32816 if (time < 0) { 32817 return { 32818 mediaIndex: 0, 32819 startTime: currentTime 32820 }; 32821 } 32822 } 32823 startIndex = 0; 32824 } 32825 32826 // Walk forward from startIndex in the playlist, subtracting durations 32827 // until we find a segment that contains `time` and return it 32828 for (i = startIndex; i < numSegments; i++) { 32829 segment = playlist.segments[i]; 32830 time -= ceilLeastSignificantDigit(segment.duration); 32831 if (time < 0) { 32832 return { 32833 mediaIndex: i, 32834 startTime: startTime + sumDurations(playlist, startIndex, i) 32835 }; 32836 } 32837 } 32838 32839 // We are out of possible candidates so load the last one... 32840 return { 32841 mediaIndex: numSegments - 1, 32842 startTime: currentTime 32843 }; 32844 }; 32845 32846 /** 32847 * Check whether the playlist is blacklisted or not. 32848 * 32849 * @param {Object} playlist the media playlist object 32850 * @return {boolean} whether the playlist is blacklisted or not 32851 * @function isBlacklisted 32852 */ 32853 var isBlacklisted = function isBlacklisted(playlist) { 32854 return playlist.excludeUntil && playlist.excludeUntil > Date.now(); 32855 }; 32856 32857 /** 32858 * Check whether the playlist is compatible with current playback configuration or has 32859 * been blacklisted permanently for being incompatible. 32860 * 32861 * @param {Object} playlist the media playlist object 32862 * @return {boolean} whether the playlist is incompatible or not 32863 * @function isIncompatible 32864 */ 32865 var isIncompatible = function isIncompatible(playlist) { 32866 return playlist.excludeUntil && playlist.excludeUntil === Infinity; 32867 }; 32868 32869 /** 32870 * Check whether the playlist is enabled or not. 32871 * 32872 * @param {Object} playlist the media playlist object 32873 * @return {boolean} whether the playlist is enabled or not 32874 * @function isEnabled 32875 */ 32876 var isEnabled = function isEnabled(playlist) { 32877 var blacklisted = isBlacklisted(playlist); 32878 32879 return !playlist.disabled && !blacklisted; 32880 }; 32881 32882 /** 32883 * Check whether the playlist has been manually disabled through the representations api. 32884 * 32885 * @param {Object} playlist the media playlist object 32886 * @return {boolean} whether the playlist is disabled manually or not 32887 * @function isDisabled 32888 */ 32889 var isDisabled = function isDisabled(playlist) { 32890 return playlist.disabled; 32891 }; 32892 32893 /** 32894 * Returns whether the current playlist is an AES encrypted HLS stream 32895 * 32896 * @return {Boolean} true if it's an AES encrypted HLS stream 32897 */ 32898 var isAes = function isAes(media) { 32899 for (var i = 0; i < media.segments.length; i++) { 32900 if (media.segments[i].key) { 32901 return true; 32902 } 32903 } 32904 return false; 32905 }; 32906 32907 /** 32908 * Returns whether the current playlist contains fMP4 32909 * 32910 * @return {Boolean} true if the playlist contains fMP4 32911 */ 32912 var isFmp4 = function isFmp4(media) { 32913 for (var i = 0; i < media.segments.length; i++) { 32914 if (media.segments[i].map) { 32915 return true; 32916 } 32917 } 32918 return false; 32919 }; 32920 32921 /** 32922 * Checks if the playlist has a value for the specified attribute 32923 * 32924 * @param {String} attr 32925 * Attribute to check for 32926 * @param {Object} playlist 32927 * The media playlist object 32928 * @return {Boolean} 32929 * Whether the playlist contains a value for the attribute or not 32930 * @function hasAttribute 32931 */ 32932 var hasAttribute = function hasAttribute(attr, playlist) { 32933 return playlist.attributes && playlist.attributes[attr]; 32934 }; 32935 32936 /** 32937 * Estimates the time required to complete a segment download from the specified playlist 32938 * 32939 * @param {Number} segmentDuration 32940 * Duration of requested segment 32941 * @param {Number} bandwidth 32942 * Current measured bandwidth of the player 32943 * @param {Object} playlist 32944 * The media playlist object 32945 * @param {Number=} bytesReceived 32946 * Number of bytes already received for the request. Defaults to 0 32947 * @return {Number|NaN} 32948 * The estimated time to request the segment. NaN if bandwidth information for 32949 * the given playlist is unavailable 32950 * @function estimateSegmentRequestTime 32951 */ 32952 var estimateSegmentRequestTime = function estimateSegmentRequestTime(segmentDuration, bandwidth, playlist) { 32953 var bytesReceived = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0; 32954 32955 if (!hasAttribute('BANDWIDTH', playlist)) { 32956 return NaN; 32957 } 32958 32959 var size = segmentDuration * playlist.attributes.BANDWIDTH; 32960 32961 return (size - bytesReceived * 8) / bandwidth; 32962 }; 32963 32964 /* 32965 * Returns whether the current playlist is the lowest rendition 32966 * 32967 * @return {Boolean} true if on lowest rendition 32968 */ 32969 var isLowestEnabledRendition = function isLowestEnabledRendition(master, media) { 32970 if (master.playlists.length === 1) { 32971 return true; 32972 } 32973 32974 var currentBandwidth = media.attributes.BANDWIDTH || Number.MAX_VALUE; 32975 32976 return master.playlists.filter(function (playlist) { 32977 if (!isEnabled(playlist)) { 32978 return false; 32979 } 32980 32981 return (playlist.attributes.BANDWIDTH || 0) < currentBandwidth; 32982 }).length === 0; 32983 }; 32984 32985 // exports 32986 var Playlist = { 32987 duration: duration, 32988 seekable: seekable, 32989 safeLiveIndex: safeLiveIndex, 32990 getMediaInfoForTime: getMediaInfoForTime, 32991 isEnabled: isEnabled, 32992 isDisabled: isDisabled, 32993 isBlacklisted: isBlacklisted, 32994 isIncompatible: isIncompatible, 32995 playlistEnd: playlistEnd, 32996 isAes: isAes, 32997 isFmp4: isFmp4, 32998 hasAttribute: hasAttribute, 32999 estimateSegmentRequestTime: estimateSegmentRequestTime, 33000 isLowestEnabledRendition: isLowestEnabledRendition 33001 }; 33002 33003 /** 33004 * @file xhr.js 33005 */ 33006 33007 var videojsXHR = videojs$1.xhr, 33008 mergeOptions$1$1 = videojs$1.mergeOptions; 33009 33010 var xhrFactory = function xhrFactory() { 33011 var xhr = function XhrFunction(options, callback) { 33012 // Add a default timeout for all hls requests 33013 options = mergeOptions$1$1({ 33014 timeout: 45e3 33015 }, options); 33016 33017 // Allow an optional user-specified function to modify the option 33018 // object before we construct the xhr request 33019 var beforeRequest = XhrFunction.beforeRequest || videojs$1.Hls.xhr.beforeRequest; 33020 33021 if (beforeRequest && typeof beforeRequest === 'function') { 33022 var newOptions = beforeRequest(options); 33023 33024 if (newOptions) { 33025 options = newOptions; 33026 } 33027 } 33028 33029 var request = videojsXHR(options, function (error, response) { 33030 var reqResponse = request.response; 33031 33032 if (!error && reqResponse) { 33033 request.responseTime = Date.now(); 33034 request.roundTripTime = request.responseTime - request.requestTime; 33035 request.bytesReceived = reqResponse.byteLength || reqResponse.length; 33036 if (!request.bandwidth) { 33037 request.bandwidth = Math.floor(request.bytesReceived / request.roundTripTime * 8 * 1000); 33038 } 33039 } 33040 33041 if (response.headers) { 33042 request.responseHeaders = response.headers; 33043 } 33044 33045 // videojs.xhr now uses a specific code on the error 33046 // object to signal that a request has timed out instead 33047 // of setting a boolean on the request object 33048 if (error && error.code === 'ETIMEDOUT') { 33049 request.timedout = true; 33050 } 33051 33052 // videojs.xhr no longer considers status codes outside of 200 and 0 33053 // (for file uris) to be errors, but the old XHR did, so emulate that 33054 // behavior. Status 206 may be used in response to byterange requests. 33055 if (!error && !request.aborted && response.statusCode !== 200 && response.statusCode !== 206 && response.statusCode !== 0) { 33056 error = new Error('XHR Failed with a response of: ' + (request && (reqResponse || request.responseText))); 33057 } 33058 33059 callback(error, request); 33060 }); 33061 var originalAbort = request.abort; 33062 33063 request.abort = function () { 33064 request.aborted = true; 33065 return originalAbort.apply(request, arguments); 33066 }; 33067 request.uri = options.uri; 33068 request.requestTime = Date.now(); 33069 return request; 33070 }; 33071 33072 return xhr; 33073 }; 33074 33075 /** 33076 * @file bin-utils.js 33077 */ 33078 33079 /** 33080 * convert a TimeRange to text 33081 * 33082 * @param {TimeRange} range the timerange to use for conversion 33083 * @param {Number} i the iterator on the range to convert 33084 */ 33085 var textRange = function textRange(range, i) { 33086 return range.start(i) + '-' + range.end(i); 33087 }; 33088 33089 /** 33090 * format a number as hex string 33091 * 33092 * @param {Number} e The number 33093 * @param {Number} i the iterator 33094 */ 33095 var formatHexString = function formatHexString(e, i) { 33096 var value = e.toString(16); 33097 33098 return '00'.substring(0, 2 - value.length) + value + (i % 2 ? ' ' : ''); 33099 }; 33100 var formatAsciiString = function formatAsciiString(e) { 33101 if (e >= 0x20 && e < 0x7e) { 33102 return String.fromCharCode(e); 33103 } 33104 return '.'; 33105 }; 33106 33107 /** 33108 * Creates an object for sending to a web worker modifying properties that are TypedArrays 33109 * into a new object with seperated properties for the buffer, byteOffset, and byteLength. 33110 * 33111 * @param {Object} message 33112 * Object of properties and values to send to the web worker 33113 * @return {Object} 33114 * Modified message with TypedArray values expanded 33115 * @function createTransferableMessage 33116 */ 33117 var createTransferableMessage = function createTransferableMessage(message) { 33118 var transferable = {}; 33119 33120 Object.keys(message).forEach(function (key) { 33121 var value = message[key]; 33122 33123 if (ArrayBuffer.isView(value)) { 33124 transferable[key] = { 33125 bytes: value.buffer, 33126 byteOffset: value.byteOffset, 33127 byteLength: value.byteLength 33128 }; 33129 } else { 33130 transferable[key] = value; 33131 } 33132 }); 33133 33134 return transferable; 33135 }; 33136 33137 /** 33138 * Returns a unique string identifier for a media initialization 33139 * segment. 33140 */ 33141 var initSegmentId = function initSegmentId(initSegment) { 33142 var byterange = initSegment.byterange || { 33143 length: Infinity, 33144 offset: 0 33145 }; 33146 33147 return [byterange.length, byterange.offset, initSegment.resolvedUri].join(','); 33148 }; 33149 33150 /** 33151 * utils to help dump binary data to the console 33152 */ 33153 var hexDump = function hexDump(data) { 33154 var bytes = Array.prototype.slice.call(data); 33155 var step = 16; 33156 var result = ''; 33157 var hex = void 0; 33158 var ascii = void 0; 33159 33160 for (var j = 0; j < bytes.length / step; j++) { 33161 hex = bytes.slice(j * step, j * step + step).map(formatHexString).join(''); 33162 ascii = bytes.slice(j * step, j * step + step).map(formatAsciiString).join(''); 33163 result += hex + ' ' + ascii + '\n'; 33164 } 33165 33166 return result; 33167 }; 33168 33169 var tagDump = function tagDump(_ref) { 33170 var bytes = _ref.bytes; 33171 return hexDump(bytes); 33172 }; 33173 33174 var textRanges = function textRanges(ranges) { 33175 var result = ''; 33176 var i = void 0; 33177 33178 for (i = 0; i < ranges.length; i++) { 33179 result += textRange(ranges, i) + ' '; 33180 } 33181 return result; 33182 }; 33183 33184 var utils = /*#__PURE__*/Object.freeze({ 33185 createTransferableMessage: createTransferableMessage, 33186 initSegmentId: initSegmentId, 33187 hexDump: hexDump, 33188 tagDump: tagDump, 33189 textRanges: textRanges 33190 }); 33191 33192 /** 33193 * ranges 33194 * 33195 * Utilities for working with TimeRanges. 33196 * 33197 */ 33198 33199 // Fudge factor to account for TimeRanges rounding 33200 var TIME_FUDGE_FACTOR = 1 / 30; 33201 // Comparisons between time values such as current time and the end of the buffered range 33202 // can be misleading because of precision differences or when the current media has poorly 33203 // aligned audio and video, which can cause values to be slightly off from what you would 33204 // expect. This value is what we consider to be safe to use in such comparisons to account 33205 // for these scenarios. 33206 var SAFE_TIME_DELTA = TIME_FUDGE_FACTOR * 3; 33207 var filterRanges = function filterRanges(timeRanges, predicate) { 33208 var results = []; 33209 var i = void 0; 33210 33211 if (timeRanges && timeRanges.length) { 33212 // Search for ranges that match the predicate 33213 for (i = 0; i < timeRanges.length; i++) { 33214 if (predicate(timeRanges.start(i), timeRanges.end(i))) { 33215 results.push([timeRanges.start(i), timeRanges.end(i)]); 33216 } 33217 } 33218 } 33219 33220 return videojs$1.createTimeRanges(results); 33221 }; 33222 33223 /** 33224 * Attempts to find the buffered TimeRange that contains the specified 33225 * time. 33226 * @param {TimeRanges} buffered - the TimeRanges object to query 33227 * @param {number} time - the time to filter on. 33228 * @returns {TimeRanges} a new TimeRanges object 33229 */ 33230 var findRange = function findRange(buffered, time) { 33231 return filterRanges(buffered, function (start, end) { 33232 return start - TIME_FUDGE_FACTOR <= time && end + TIME_FUDGE_FACTOR >= time; 33233 }); 33234 }; 33235 33236 /** 33237 * Returns the TimeRanges that begin later than the specified time. 33238 * @param {TimeRanges} timeRanges - the TimeRanges object to query 33239 * @param {number} time - the time to filter on. 33240 * @returns {TimeRanges} a new TimeRanges object. 33241 */ 33242 var findNextRange = function findNextRange(timeRanges, time) { 33243 return filterRanges(timeRanges, function (start) { 33244 return start - TIME_FUDGE_FACTOR >= time; 33245 }); 33246 }; 33247 33248 /** 33249 * Returns gaps within a list of TimeRanges 33250 * @param {TimeRanges} buffered - the TimeRanges object 33251 * @return {TimeRanges} a TimeRanges object of gaps 33252 */ 33253 var findGaps = function findGaps(buffered) { 33254 if (buffered.length < 2) { 33255 return videojs$1.createTimeRanges(); 33256 } 33257 33258 var ranges = []; 33259 33260 for (var i = 1; i < buffered.length; i++) { 33261 var start = buffered.end(i - 1); 33262 var end = buffered.start(i); 33263 33264 ranges.push([start, end]); 33265 } 33266 33267 return videojs$1.createTimeRanges(ranges); 33268 }; 33269 33270 /** 33271 * Gets a human readable string for a TimeRange 33272 * 33273 * @param {TimeRange} range 33274 * @returns {String} a human readable string 33275 */ 33276 var printableRange = function printableRange(range) { 33277 var strArr = []; 33278 33279 if (!range || !range.length) { 33280 return ''; 33281 } 33282 33283 for (var i = 0; i < range.length; i++) { 33284 strArr.push(range.start(i) + ' => ' + range.end(i)); 33285 } 33286 33287 return strArr.join(', '); 33288 }; 33289 33290 /** 33291 * Calculates the amount of time left in seconds until the player hits the end of the 33292 * buffer and causes a rebuffer 33293 * 33294 * @param {TimeRange} buffered 33295 * The state of the buffer 33296 * @param {Numnber} currentTime 33297 * The current time of the player 33298 * @param {Number} playbackRate 33299 * The current playback rate of the player. Defaults to 1. 33300 * @return {Number} 33301 * Time until the player has to start rebuffering in seconds. 33302 * @function timeUntilRebuffer 33303 */ 33304 var timeUntilRebuffer = function timeUntilRebuffer(buffered, currentTime) { 33305 var playbackRate = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 1; 33306 33307 var bufferedEnd = buffered.length ? buffered.end(buffered.length - 1) : 0; 33308 33309 return (bufferedEnd - currentTime) / playbackRate; 33310 }; 33311 33312 /** 33313 * Converts a TimeRanges object into an array representation 33314 * @param {TimeRanges} timeRanges 33315 * @returns {Array} 33316 */ 33317 var timeRangesToArray = function timeRangesToArray(timeRanges) { 33318 var timeRangesList = []; 33319 33320 for (var i = 0; i < timeRanges.length; i++) { 33321 timeRangesList.push({ 33322 start: timeRanges.start(i), 33323 end: timeRanges.end(i) 33324 }); 33325 } 33326 33327 return timeRangesList; 33328 }; 33329 33330 /** 33331 * @file create-text-tracks-if-necessary.js 33332 */ 33333 33334 /** 33335 * Create text tracks on video.js if they exist on a segment. 33336 * 33337 * @param {Object} sourceBuffer the VSB or FSB 33338 * @param {Object} mediaSource the HTML media source 33339 * @param {Object} segment the segment that may contain the text track 33340 * @private 33341 */ 33342 var createTextTracksIfNecessary = function createTextTracksIfNecessary(sourceBuffer, mediaSource, segment) { 33343 var player = mediaSource.player_; 33344 33345 // create an in-band caption track if one is present in the segment 33346 if (segment.captions && segment.captions.length) { 33347 if (!sourceBuffer.inbandTextTracks_) { 33348 sourceBuffer.inbandTextTracks_ = {}; 33349 } 33350 33351 for (var trackId in segment.captionStreams) { 33352 if (!sourceBuffer.inbandTextTracks_[trackId]) { 33353 player.tech_.trigger({ type: 'usage', name: 'hls-608' }); 33354 var track = player.textTracks().getTrackById(trackId); 33355 33356 if (track) { 33357 // Resuse an existing track with a CC# id because this was 33358 // very likely created by videojs-contrib-hls from information 33359 // in the m3u8 for us to use 33360 sourceBuffer.inbandTextTracks_[trackId] = track; 33361 } else { 33362 // Otherwise, create a track with the default `CC#` label and 33363 // without a language 33364 sourceBuffer.inbandTextTracks_[trackId] = player.addRemoteTextTrack({ 33365 kind: 'captions', 33366 id: trackId, 33367 label: trackId 33368 }, false).track; 33369 } 33370 } 33371 } 33372 } 33373 33374 if (segment.metadata && segment.metadata.length && !sourceBuffer.metadataTrack_) { 33375 sourceBuffer.metadataTrack_ = player.addRemoteTextTrack({ 33376 kind: 'metadata', 33377 label: 'Timed Metadata' 33378 }, false).track; 33379 sourceBuffer.metadataTrack_.inBandMetadataTrackDispatchType = segment.metadata.dispatchType; 33380 } 33381 }; 33382 33383 /** 33384 * @file remove-cues-from-track.js 33385 */ 33386 33387 /** 33388 * Remove cues from a track on video.js. 33389 * 33390 * @param {Double} start start of where we should remove the cue 33391 * @param {Double} end end of where the we should remove the cue 33392 * @param {Object} track the text track to remove the cues from 33393 * @private 33394 */ 33395 var removeCuesFromTrack = function removeCuesFromTrack(start, end, track) { 33396 var i = void 0; 33397 var cue = void 0; 33398 33399 if (!track) { 33400 return; 33401 } 33402 33403 if (!track.cues) { 33404 return; 33405 } 33406 33407 i = track.cues.length; 33408 33409 while (i--) { 33410 cue = track.cues[i]; 33411 33412 // Remove any overlapping cue 33413 if (cue.startTime <= end && cue.endTime >= start) { 33414 track.removeCue(cue); 33415 } 33416 } 33417 }; 33418 33419 /** 33420 * @file add-text-track-data.js 33421 */ 33422 /** 33423 * Define properties on a cue for backwards compatability, 33424 * but warn the user that the way that they are using it 33425 * is depricated and will be removed at a later date. 33426 * 33427 * @param {Cue} cue the cue to add the properties on 33428 * @private 33429 */ 33430 var deprecateOldCue = function deprecateOldCue(cue) { 33431 Object.defineProperties(cue.frame, { 33432 id: { 33433 get: function get$$1() { 33434 videojs$1.log.warn('cue.frame.id is deprecated. Use cue.value.key instead.'); 33435 return cue.value.key; 33436 } 33437 }, 33438 value: { 33439 get: function get$$1() { 33440 videojs$1.log.warn('cue.frame.value is deprecated. Use cue.value.data instead.'); 33441 return cue.value.data; 33442 } 33443 }, 33444 privateData: { 33445 get: function get$$1() { 33446 videojs$1.log.warn('cue.frame.privateData is deprecated. Use cue.value.data instead.'); 33447 return cue.value.data; 33448 } 33449 } 33450 }); 33451 }; 33452 33453 var durationOfVideo = function durationOfVideo(duration) { 33454 var dur = void 0; 33455 33456 if (isNaN(duration) || Math.abs(duration) === Infinity) { 33457 dur = Number.MAX_VALUE; 33458 } else { 33459 dur = duration; 33460 } 33461 return dur; 33462 }; 33463 /** 33464 * Add text track data to a source handler given the captions and 33465 * metadata from the buffer. 33466 * 33467 * @param {Object} sourceHandler the virtual source buffer 33468 * @param {Array} captionArray an array of caption data 33469 * @param {Array} metadataArray an array of meta data 33470 * @private 33471 */ 33472 var addTextTrackData = function addTextTrackData(sourceHandler, captionArray, metadataArray) { 33473 var Cue = window_1.WebKitDataCue || window_1.VTTCue; 33474 33475 if (captionArray) { 33476 captionArray.forEach(function (caption) { 33477 var track = caption.stream; 33478 33479 this.inbandTextTracks_[track].addCue(new Cue(caption.startTime + this.timestampOffset, caption.endTime + this.timestampOffset, caption.text)); 33480 }, sourceHandler); 33481 } 33482 33483 if (metadataArray) { 33484 var videoDuration = durationOfVideo(sourceHandler.mediaSource_.duration); 33485 33486 metadataArray.forEach(function (metadata) { 33487 var time = metadata.cueTime + this.timestampOffset; 33488 33489 metadata.frames.forEach(function (frame) { 33490 var cue = new Cue(time, time, frame.value || frame.url || frame.data || ''); 33491 33492 cue.frame = frame; 33493 cue.value = frame; 33494 deprecateOldCue(cue); 33495 33496 this.metadataTrack_.addCue(cue); 33497 }, this); 33498 }, sourceHandler); 33499 33500 // Updating the metadeta cues so that 33501 // the endTime of each cue is the startTime of the next cue 33502 // the endTime of last cue is the duration of the video 33503 if (sourceHandler.metadataTrack_ && sourceHandler.metadataTrack_.cues && sourceHandler.metadataTrack_.cues.length) { 33504 var cues = sourceHandler.metadataTrack_.cues; 33505 var cuesArray = []; 33506 33507 // Create a copy of the TextTrackCueList... 33508 // ...disregarding cues with a falsey value 33509 for (var i = 0; i < cues.length; i++) { 33510 if (cues[i]) { 33511 cuesArray.push(cues[i]); 33512 } 33513 } 33514 33515 // Group cues by their startTime value 33516 var cuesGroupedByStartTime = cuesArray.reduce(function (obj, cue) { 33517 var timeSlot = obj[cue.startTime] || []; 33518 33519 timeSlot.push(cue); 33520 obj[cue.startTime] = timeSlot; 33521 33522 return obj; 33523 }, {}); 33524 33525 // Sort startTimes by ascending order 33526 var sortedStartTimes = Object.keys(cuesGroupedByStartTime).sort(function (a, b) { 33527 return Number(a) - Number(b); 33528 }); 33529 33530 // Map each cue group's endTime to the next group's startTime 33531 sortedStartTimes.forEach(function (startTime, idx) { 33532 var cueGroup = cuesGroupedByStartTime[startTime]; 33533 var nextTime = Number(sortedStartTimes[idx + 1]) || videoDuration; 33534 33535 // Map each cue's endTime the next group's startTime 33536 cueGroup.forEach(function (cue) { 33537 cue.endTime = nextTime; 33538 }); 33539 }); 33540 } 33541 } 33542 }; 33543 33544 var win$1 = typeof window !== 'undefined' ? window : {}, 33545 TARGET = typeof Symbol === 'undefined' ? '__target' : Symbol(), 33546 SCRIPT_TYPE = 'application/javascript', 33547 BlobBuilder = win$1.BlobBuilder || win$1.WebKitBlobBuilder || win$1.MozBlobBuilder || win$1.MSBlobBuilder, 33548 URL = win$1.URL || win$1.webkitURL || URL && URL.msURL, 33549 Worker = win$1.Worker; 33550 33551 /** 33552 * Returns a wrapper around Web Worker code that is constructible. 33553 * 33554 * @function shimWorker 33555 * 33556 * @param { String } filename The name of the file 33557 * @param { Function } fn Function wrapping the code of the worker 33558 */ 33559 function shimWorker(filename, fn) { 33560 return function ShimWorker(forceFallback) { 33561 var o = this; 33562 33563 if (!fn) { 33564 return new Worker(filename); 33565 } else if (Worker && !forceFallback) { 33566 // Convert the function's inner code to a string to construct the worker 33567 var source = fn.toString().replace(/^function.+?{/, '').slice(0, -1), 33568 objURL = createSourceObject(source); 33569 33570 this[TARGET] = new Worker(objURL); 33571 wrapTerminate(this[TARGET], objURL); 33572 return this[TARGET]; 33573 } else { 33574 var selfShim = { 33575 postMessage: function postMessage(m) { 33576 if (o.onmessage) { 33577 setTimeout(function () { 33578 o.onmessage({ data: m, target: selfShim }); 33579 }); 33580 } 33581 } 33582 }; 33583 33584 fn.call(selfShim); 33585 this.postMessage = function (m) { 33586 setTimeout(function () { 33587 selfShim.onmessage({ data: m, target: o }); 33588 }); 33589 }; 33590 this.isThisThread = true; 33591 } 33592 }; 33593 } 33594 // Test Worker capabilities 33595 if (Worker) { 33596 var testWorker, 33597 objURL = createSourceObject('self.onmessage = function () {}'), 33598 testArray = new Uint8Array(1); 33599 33600 try { 33601 testWorker = new Worker(objURL); 33602 33603 // Native browser on some Samsung devices throws for transferables, let's detect it 33604 testWorker.postMessage(testArray, [testArray.buffer]); 33605 } catch (e) { 33606 Worker = null; 33607 } finally { 33608 URL.revokeObjectURL(objURL); 33609 if (testWorker) { 33610 testWorker.terminate(); 33611 } 33612 } 33613 } 33614 33615 function createSourceObject(str) { 33616 try { 33617 return URL.createObjectURL(new Blob([str], { type: SCRIPT_TYPE })); 33618 } catch (e) { 33619 var blob = new BlobBuilder(); 33620 blob.append(str); 33621 return URL.createObjectURL(blob.getBlob(type)); 33622 } 33623 } 33624 33625 function wrapTerminate(worker, objURL) { 33626 if (!worker || !objURL) return; 33627 var term = worker.terminate; 33628 worker.objURL = objURL; 33629 worker.terminate = function () { 33630 if (worker.objURL) URL.revokeObjectURL(worker.objURL); 33631 term.call(worker); 33632 }; 33633 } 33634 33635 var TransmuxWorker = new shimWorker("./transmuxer-worker.worker.js", function (window, document$$1) { 33636 var self = this; 33637 var transmuxerWorker = function () { 33638 33639 var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; 33640 33641 var win; 33642 33643 if (typeof window !== "undefined") { 33644 win = window; 33645 } else if (typeof commonjsGlobal !== "undefined") { 33646 win = commonjsGlobal; 33647 } else if (typeof self !== "undefined") { 33648 win = self; 33649 } else { 33650 win = {}; 33651 } 33652 33653 var window_1$$1 = win; 33654 33655 /** 33656 * mux.js 33657 * 33658 * Copyright (c) 2015 Brightcove 33659 * All rights reserved. 33660 * 33661 * Functions that generate fragmented MP4s suitable for use with Media 33662 * Source Extensions. 33663 */ 33664 33665 var UINT32_MAX = Math.pow(2, 32) - 1; 33666 33667 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; 33668 33669 // pre-calculate constants 33670 (function () { 33671 var i; 33672 types = { 33673 avc1: [], // codingname 33674 avcC: [], 33675 btrt: [], 33676 dinf: [], 33677 dref: [], 33678 esds: [], 33679 ftyp: [], 33680 hdlr: [], 33681 mdat: [], 33682 mdhd: [], 33683 mdia: [], 33684 mfhd: [], 33685 minf: [], 33686 moof: [], 33687 moov: [], 33688 mp4a: [], // codingname 33689 mvex: [], 33690 mvhd: [], 33691 sdtp: [], 33692 smhd: [], 33693 stbl: [], 33694 stco: [], 33695 stsc: [], 33696 stsd: [], 33697 stsz: [], 33698 stts: [], 33699 styp: [], 33700 tfdt: [], 33701 tfhd: [], 33702 traf: [], 33703 trak: [], 33704 trun: [], 33705 trex: [], 33706 tkhd: [], 33707 vmhd: [] 33708 }; 33709 33710 // In environments where Uint8Array is undefined (e.g., IE8), skip set up so that we 33711 // don't throw an error 33712 if (typeof Uint8Array === 'undefined') { 33713 return; 33714 } 33715 33716 for (i in types) { 33717 if (types.hasOwnProperty(i)) { 33718 types[i] = [i.charCodeAt(0), i.charCodeAt(1), i.charCodeAt(2), i.charCodeAt(3)]; 33719 } 33720 } 33721 33722 MAJOR_BRAND = new Uint8Array(['i'.charCodeAt(0), 's'.charCodeAt(0), 'o'.charCodeAt(0), 'm'.charCodeAt(0)]); 33723 AVC1_BRAND = new Uint8Array(['a'.charCodeAt(0), 'v'.charCodeAt(0), 'c'.charCodeAt(0), '1'.charCodeAt(0)]); 33724 MINOR_VERSION = new Uint8Array([0, 0, 0, 1]); 33725 VIDEO_HDLR = new Uint8Array([0x00, // version 0 33726 0x00, 0x00, 0x00, // flags 33727 0x00, 0x00, 0x00, 0x00, // pre_defined 33728 0x76, 0x69, 0x64, 0x65, // handler_type: 'vide' 33729 0x00, 0x00, 0x00, 0x00, // reserved 33730 0x00, 0x00, 0x00, 0x00, // reserved 33731 0x00, 0x00, 0x00, 0x00, // reserved 33732 0x56, 0x69, 0x64, 0x65, 0x6f, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'VideoHandler' 33733 ]); 33734 AUDIO_HDLR = new Uint8Array([0x00, // version 0 33735 0x00, 0x00, 0x00, // flags 33736 0x00, 0x00, 0x00, 0x00, // pre_defined 33737 0x73, 0x6f, 0x75, 0x6e, // handler_type: 'soun' 33738 0x00, 0x00, 0x00, 0x00, // reserved 33739 0x00, 0x00, 0x00, 0x00, // reserved 33740 0x00, 0x00, 0x00, 0x00, // reserved 33741 0x53, 0x6f, 0x75, 0x6e, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'SoundHandler' 33742 ]); 33743 HDLR_TYPES = { 33744 video: VIDEO_HDLR, 33745 audio: AUDIO_HDLR 33746 }; 33747 DREF = new Uint8Array([0x00, // version 0 33748 0x00, 0x00, 0x00, // flags 33749 0x00, 0x00, 0x00, 0x01, // entry_count 33750 0x00, 0x00, 0x00, 0x0c, // entry_size 33751 0x75, 0x72, 0x6c, 0x20, // 'url' type 33752 0x00, // version 0 33753 0x00, 0x00, 0x01 // entry_flags 33754 ]); 33755 SMHD = new Uint8Array([0x00, // version 33756 0x00, 0x00, 0x00, // flags 33757 0x00, 0x00, // balance, 0 means centered 33758 0x00, 0x00 // reserved 33759 ]); 33760 STCO = new Uint8Array([0x00, // version 33761 0x00, 0x00, 0x00, // flags 33762 0x00, 0x00, 0x00, 0x00 // entry_count 33763 ]); 33764 STSC = STCO; 33765 STSZ = new Uint8Array([0x00, // version 33766 0x00, 0x00, 0x00, // flags 33767 0x00, 0x00, 0x00, 0x00, // sample_size 33768 0x00, 0x00, 0x00, 0x00 // sample_count 33769 ]); 33770 STTS = STCO; 33771 VMHD = new Uint8Array([0x00, // version 33772 0x00, 0x00, 0x01, // flags 33773 0x00, 0x00, // graphicsmode 33774 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // opcolor 33775 ]); 33776 })(); 33777 33778 box = function box(type) { 33779 var payload = [], 33780 size = 0, 33781 i, 33782 result, 33783 view; 33784 33785 for (i = 1; i < arguments.length; i++) { 33786 payload.push(arguments[i]); 33787 } 33788 33789 i = payload.length; 33790 33791 // calculate the total size we need to allocate 33792 while (i--) { 33793 size += payload[i].byteLength; 33794 } 33795 result = new Uint8Array(size + 8); 33796 view = new DataView(result.buffer, result.byteOffset, result.byteLength); 33797 view.setUint32(0, result.byteLength); 33798 result.set(type, 4); 33799 33800 // copy the payload into the result 33801 for (i = 0, size = 8; i < payload.length; i++) { 33802 result.set(payload[i], size); 33803 size += payload[i].byteLength; 33804 } 33805 return result; 33806 }; 33807 33808 dinf = function dinf() { 33809 return box(types.dinf, box(types.dref, DREF)); 33810 }; 33811 33812 esds = function esds(track) { 33813 return box(types.esds, new Uint8Array([0x00, // version 33814 0x00, 0x00, 0x00, // flags 33815 33816 // ES_Descriptor 33817 0x03, // tag, ES_DescrTag 33818 0x19, // length 33819 0x00, 0x00, // ES_ID 33820 0x00, // streamDependenceFlag, URL_flag, reserved, streamPriority 33821 33822 // DecoderConfigDescriptor 33823 0x04, // tag, DecoderConfigDescrTag 33824 0x11, // length 33825 0x40, // object type 33826 0x15, // streamType 33827 0x00, 0x06, 0x00, // bufferSizeDB 33828 0x00, 0x00, 0xda, 0xc0, // maxBitrate 33829 0x00, 0x00, 0xda, 0xc0, // avgBitrate 33830 33831 // DecoderSpecificInfo 33832 0x05, // tag, DecoderSpecificInfoTag 33833 0x02, // length 33834 // ISO/IEC 14496-3, AudioSpecificConfig 33835 // for samplingFrequencyIndex see ISO/IEC 13818-7:2006, 8.1.3.2.2, Table 35 33836 track.audioobjecttype << 3 | track.samplingfrequencyindex >>> 1, track.samplingfrequencyindex << 7 | track.channelcount << 3, 0x06, 0x01, 0x02 // GASpecificConfig 33837 ])); 33838 }; 33839 33840 ftyp = function ftyp() { 33841 return box(types.ftyp, MAJOR_BRAND, MINOR_VERSION, MAJOR_BRAND, AVC1_BRAND); 33842 }; 33843 33844 hdlr = function hdlr(type) { 33845 return box(types.hdlr, HDLR_TYPES[type]); 33846 }; 33847 mdat = function mdat(data) { 33848 return box(types.mdat, data); 33849 }; 33850 mdhd = function mdhd(track) { 33851 var result = new Uint8Array([0x00, // version 0 33852 0x00, 0x00, 0x00, // flags 33853 0x00, 0x00, 0x00, 0x02, // creation_time 33854 0x00, 0x00, 0x00, 0x03, // modification_time 33855 0x00, 0x01, 0x5f, 0x90, // timescale, 90,000 "ticks" per second 33856 33857 track.duration >>> 24 & 0xFF, track.duration >>> 16 & 0xFF, track.duration >>> 8 & 0xFF, track.duration & 0xFF, // duration 33858 0x55, 0xc4, // 'und' language (undetermined) 33859 0x00, 0x00]); 33860 33861 // Use the sample rate from the track metadata, when it is 33862 // defined. The sample rate can be parsed out of an ADTS header, for 33863 // instance. 33864 if (track.samplerate) { 33865 result[12] = track.samplerate >>> 24 & 0xFF; 33866 result[13] = track.samplerate >>> 16 & 0xFF; 33867 result[14] = track.samplerate >>> 8 & 0xFF; 33868 result[15] = track.samplerate & 0xFF; 33869 } 33870 33871 return box(types.mdhd, result); 33872 }; 33873 mdia = function mdia(track) { 33874 return box(types.mdia, mdhd(track), hdlr(track.type), minf(track)); 33875 }; 33876 mfhd = function mfhd(sequenceNumber) { 33877 return box(types.mfhd, new Uint8Array([0x00, 0x00, 0x00, 0x00, // flags 33878 (sequenceNumber & 0xFF000000) >> 24, (sequenceNumber & 0xFF0000) >> 16, (sequenceNumber & 0xFF00) >> 8, sequenceNumber & 0xFF // sequence_number 33879 ])); 33880 }; 33881 minf = function minf(track) { 33882 return box(types.minf, track.type === 'video' ? box(types.vmhd, VMHD) : box(types.smhd, SMHD), dinf(), stbl(track)); 33883 }; 33884 moof = function moof(sequenceNumber, tracks) { 33885 var trackFragments = [], 33886 i = tracks.length; 33887 // build traf boxes for each track fragment 33888 while (i--) { 33889 trackFragments[i] = traf(tracks[i]); 33890 } 33891 return box.apply(null, [types.moof, mfhd(sequenceNumber)].concat(trackFragments)); 33892 }; 33893 /** 33894 * Returns a movie box. 33895 * @param tracks {array} the tracks associated with this movie 33896 * @see ISO/IEC 14496-12:2012(E), section 8.2.1 33897 */ 33898 moov = function moov(tracks) { 33899 var i = tracks.length, 33900 boxes = []; 33901 33902 while (i--) { 33903 boxes[i] = trak(tracks[i]); 33904 } 33905 33906 return box.apply(null, [types.moov, mvhd(0xffffffff)].concat(boxes).concat(mvex(tracks))); 33907 }; 33908 mvex = function mvex(tracks) { 33909 var i = tracks.length, 33910 boxes = []; 33911 33912 while (i--) { 33913 boxes[i] = trex(tracks[i]); 33914 } 33915 return box.apply(null, [types.mvex].concat(boxes)); 33916 }; 33917 mvhd = function mvhd(duration) { 33918 var bytes = new Uint8Array([0x00, // version 0 33919 0x00, 0x00, 0x00, // flags 33920 0x00, 0x00, 0x00, 0x01, // creation_time 33921 0x00, 0x00, 0x00, 0x02, // modification_time 33922 0x00, 0x01, 0x5f, 0x90, // timescale, 90,000 "ticks" per second 33923 (duration & 0xFF000000) >> 24, (duration & 0xFF0000) >> 16, (duration & 0xFF00) >> 8, duration & 0xFF, // duration 33924 0x00, 0x01, 0x00, 0x00, // 1.0 rate 33925 0x01, 0x00, // 1.0 volume 33926 0x00, 0x00, // reserved 33927 0x00, 0x00, 0x00, 0x00, // reserved 33928 0x00, 0x00, 0x00, 0x00, // reserved 33929 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 33930 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 33931 0xff, 0xff, 0xff, 0xff // next_track_ID 33932 ]); 33933 return box(types.mvhd, bytes); 33934 }; 33935 33936 sdtp = function sdtp(track) { 33937 var samples = track.samples || [], 33938 bytes = new Uint8Array(4 + samples.length), 33939 flags, 33940 i; 33941 33942 // leave the full box header (4 bytes) all zero 33943 33944 // write the sample table 33945 for (i = 0; i < samples.length; i++) { 33946 flags = samples[i].flags; 33947 33948 bytes[i + 4] = flags.dependsOn << 4 | flags.isDependedOn << 2 | flags.hasRedundancy; 33949 } 33950 33951 return box(types.sdtp, bytes); 33952 }; 33953 33954 stbl = function stbl(track) { 33955 return box(types.stbl, stsd(track), box(types.stts, STTS), box(types.stsc, STSC), box(types.stsz, STSZ), box(types.stco, STCO)); 33956 }; 33957 33958 (function () { 33959 var videoSample, audioSample; 33960 33961 stsd = function stsd(track) { 33962 33963 return box(types.stsd, new Uint8Array([0x00, // version 0 33964 0x00, 0x00, 0x00, // flags 33965 0x00, 0x00, 0x00, 0x01]), track.type === 'video' ? videoSample(track) : audioSample(track)); 33966 }; 33967 33968 videoSample = function videoSample(track) { 33969 var sps = track.sps || [], 33970 pps = track.pps || [], 33971 sequenceParameterSets = [], 33972 pictureParameterSets = [], 33973 i; 33974 33975 // assemble the SPSs 33976 for (i = 0; i < sps.length; i++) { 33977 sequenceParameterSets.push((sps[i].byteLength & 0xFF00) >>> 8); 33978 sequenceParameterSets.push(sps[i].byteLength & 0xFF); // sequenceParameterSetLength 33979 sequenceParameterSets = sequenceParameterSets.concat(Array.prototype.slice.call(sps[i])); // SPS 33980 } 33981 33982 // assemble the PPSs 33983 for (i = 0; i < pps.length; i++) { 33984 pictureParameterSets.push((pps[i].byteLength & 0xFF00) >>> 8); 33985 pictureParameterSets.push(pps[i].byteLength & 0xFF); 33986 pictureParameterSets = pictureParameterSets.concat(Array.prototype.slice.call(pps[i])); 33987 } 33988 33989 return box(types.avc1, new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // reserved 33990 0x00, 0x01, // data_reference_index 33991 0x00, 0x00, // pre_defined 33992 0x00, 0x00, // reserved 33993 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // pre_defined 33994 (track.width & 0xff00) >> 8, track.width & 0xff, // width 33995 (track.height & 0xff00) >> 8, track.height & 0xff, // height 33996 0x00, 0x48, 0x00, 0x00, // horizresolution 33997 0x00, 0x48, 0x00, 0x00, // vertresolution 33998 0x00, 0x00, 0x00, 0x00, // reserved 33999 0x00, 0x01, // frame_count 34000 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 34001 0x00, 0x18, // depth = 24 34002 0x11, 0x11 // pre_defined = -1 34003 ]), box(types.avcC, new Uint8Array([0x01, // configurationVersion 34004 track.profileIdc, // AVCProfileIndication 34005 track.profileCompatibility, // profile_compatibility 34006 track.levelIdc, // AVCLevelIndication 34007 0xff // lengthSizeMinusOne, hard-coded to 4 bytes 34008 ].concat([sps.length // numOfSequenceParameterSets 34009 ]).concat(sequenceParameterSets).concat([pps.length // numOfPictureParameterSets 34010 ]).concat(pictureParameterSets))), // "PPS" 34011 box(types.btrt, new Uint8Array([0x00, 0x1c, 0x9c, 0x80, // bufferSizeDB 34012 0x00, 0x2d, 0xc6, 0xc0, // maxBitrate 34013 0x00, 0x2d, 0xc6, 0xc0])) // avgBitrate 34014 ); 34015 }; 34016 34017 audioSample = function audioSample(track) { 34018 return box(types.mp4a, new Uint8Array([ 34019 34020 // SampleEntry, ISO/IEC 14496-12 34021 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // reserved 34022 0x00, 0x01, // data_reference_index 34023 34024 // AudioSampleEntry, ISO/IEC 14496-12 34025 0x00, 0x00, 0x00, 0x00, // reserved 34026 0x00, 0x00, 0x00, 0x00, // reserved 34027 (track.channelcount & 0xff00) >> 8, track.channelcount & 0xff, // channelcount 34028 34029 (track.samplesize & 0xff00) >> 8, track.samplesize & 0xff, // samplesize 34030 0x00, 0x00, // pre_defined 34031 0x00, 0x00, // reserved 34032 34033 (track.samplerate & 0xff00) >> 8, track.samplerate & 0xff, 0x00, 0x00 // samplerate, 16.16 34034 34035 // MP4AudioSampleEntry, ISO/IEC 14496-14 34036 ]), esds(track)); 34037 }; 34038 })(); 34039 34040 tkhd = function tkhd(track) { 34041 var result = new Uint8Array([0x00, // version 0 34042 0x00, 0x00, 0x07, // flags 34043 0x00, 0x00, 0x00, 0x00, // creation_time 34044 0x00, 0x00, 0x00, 0x00, // modification_time 34045 (track.id & 0xFF000000) >> 24, (track.id & 0xFF0000) >> 16, (track.id & 0xFF00) >> 8, track.id & 0xFF, // track_ID 34046 0x00, 0x00, 0x00, 0x00, // reserved 34047 (track.duration & 0xFF000000) >> 24, (track.duration & 0xFF0000) >> 16, (track.duration & 0xFF00) >> 8, track.duration & 0xFF, // duration 34048 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // reserved 34049 0x00, 0x00, // layer 34050 0x00, 0x00, // alternate_group 34051 0x01, 0x00, // non-audio track volume 34052 0x00, 0x00, // reserved 34053 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 34054 (track.width & 0xFF00) >> 8, track.width & 0xFF, 0x00, 0x00, // width 34055 (track.height & 0xFF00) >> 8, track.height & 0xFF, 0x00, 0x00 // height 34056 ]); 34057 34058 return box(types.tkhd, result); 34059 }; 34060 34061 /** 34062 * Generate a track fragment (traf) box. A traf box collects metadata 34063 * about tracks in a movie fragment (moof) box. 34064 */ 34065 traf = function traf(track) { 34066 var trackFragmentHeader, trackFragmentDecodeTime, trackFragmentRun, sampleDependencyTable, dataOffset, upperWordBaseMediaDecodeTime, lowerWordBaseMediaDecodeTime; 34067 34068 trackFragmentHeader = box(types.tfhd, new Uint8Array([0x00, // version 0 34069 0x00, 0x00, 0x3a, // flags 34070 (track.id & 0xFF000000) >> 24, (track.id & 0xFF0000) >> 16, (track.id & 0xFF00) >> 8, track.id & 0xFF, // track_ID 34071 0x00, 0x00, 0x00, 0x01, // sample_description_index 34072 0x00, 0x00, 0x00, 0x00, // default_sample_duration 34073 0x00, 0x00, 0x00, 0x00, // default_sample_size 34074 0x00, 0x00, 0x00, 0x00 // default_sample_flags 34075 ])); 34076 34077 upperWordBaseMediaDecodeTime = Math.floor(track.baseMediaDecodeTime / (UINT32_MAX + 1)); 34078 lowerWordBaseMediaDecodeTime = Math.floor(track.baseMediaDecodeTime % (UINT32_MAX + 1)); 34079 34080 trackFragmentDecodeTime = box(types.tfdt, new Uint8Array([0x01, // version 1 34081 0x00, 0x00, 0x00, // flags 34082 // baseMediaDecodeTime 34083 upperWordBaseMediaDecodeTime >>> 24 & 0xFF, upperWordBaseMediaDecodeTime >>> 16 & 0xFF, upperWordBaseMediaDecodeTime >>> 8 & 0xFF, upperWordBaseMediaDecodeTime & 0xFF, lowerWordBaseMediaDecodeTime >>> 24 & 0xFF, lowerWordBaseMediaDecodeTime >>> 16 & 0xFF, lowerWordBaseMediaDecodeTime >>> 8 & 0xFF, lowerWordBaseMediaDecodeTime & 0xFF])); 34084 34085 // the data offset specifies the number of bytes from the start of 34086 // the containing moof to the first payload byte of the associated 34087 // mdat 34088 dataOffset = 32 + // tfhd 34089 20 + // tfdt 34090 8 + // traf header 34091 16 + // mfhd 34092 8 + // moof header 34093 8; // mdat header 34094 34095 // audio tracks require less metadata 34096 if (track.type === 'audio') { 34097 trackFragmentRun = trun(track, dataOffset); 34098 return box(types.traf, trackFragmentHeader, trackFragmentDecodeTime, trackFragmentRun); 34099 } 34100 34101 // video tracks should contain an independent and disposable samples 34102 // box (sdtp) 34103 // generate one and adjust offsets to match 34104 sampleDependencyTable = sdtp(track); 34105 trackFragmentRun = trun(track, sampleDependencyTable.length + dataOffset); 34106 return box(types.traf, trackFragmentHeader, trackFragmentDecodeTime, trackFragmentRun, sampleDependencyTable); 34107 }; 34108 34109 /** 34110 * Generate a track box. 34111 * @param track {object} a track definition 34112 * @return {Uint8Array} the track box 34113 */ 34114 trak = function trak(track) { 34115 track.duration = track.duration || 0xffffffff; 34116 return box(types.trak, tkhd(track), mdia(track)); 34117 }; 34118 34119 trex = function trex(track) { 34120 var result = new Uint8Array([0x00, // version 0 34121 0x00, 0x00, 0x00, // flags 34122 (track.id & 0xFF000000) >> 24, (track.id & 0xFF0000) >> 16, (track.id & 0xFF00) >> 8, track.id & 0xFF, // track_ID 34123 0x00, 0x00, 0x00, 0x01, // default_sample_description_index 34124 0x00, 0x00, 0x00, 0x00, // default_sample_duration 34125 0x00, 0x00, 0x00, 0x00, // default_sample_size 34126 0x00, 0x01, 0x00, 0x01 // default_sample_flags 34127 ]); 34128 // the last two bytes of default_sample_flags is the sample 34129 // degradation priority, a hint about the importance of this sample 34130 // relative to others. Lower the degradation priority for all sample 34131 // types other than video. 34132 if (track.type !== 'video') { 34133 result[result.length - 1] = 0x00; 34134 } 34135 34136 return box(types.trex, result); 34137 }; 34138 34139 (function () { 34140 var audioTrun, videoTrun, trunHeader; 34141 34142 // This method assumes all samples are uniform. That is, if a 34143 // duration is present for the first sample, it will be present for 34144 // all subsequent samples. 34145 // see ISO/IEC 14496-12:2012, Section 8.8.8.1 34146 trunHeader = function trunHeader(samples, offset) { 34147 var durationPresent = 0, 34148 sizePresent = 0, 34149 flagsPresent = 0, 34150 compositionTimeOffset = 0; 34151 34152 // trun flag constants 34153 if (samples.length) { 34154 if (samples[0].duration !== undefined) { 34155 durationPresent = 0x1; 34156 } 34157 if (samples[0].size !== undefined) { 34158 sizePresent = 0x2; 34159 } 34160 if (samples[0].flags !== undefined) { 34161 flagsPresent = 0x4; 34162 } 34163 if (samples[0].compositionTimeOffset !== undefined) { 34164 compositionTimeOffset = 0x8; 34165 } 34166 } 34167 34168 return [0x00, // version 0 34169 0x00, durationPresent | sizePresent | flagsPresent | compositionTimeOffset, 0x01, // flags 34170 (samples.length & 0xFF000000) >>> 24, (samples.length & 0xFF0000) >>> 16, (samples.length & 0xFF00) >>> 8, samples.length & 0xFF, // sample_count 34171 (offset & 0xFF000000) >>> 24, (offset & 0xFF0000) >>> 16, (offset & 0xFF00) >>> 8, offset & 0xFF // data_offset 34172 ]; 34173 }; 34174 34175 videoTrun = function videoTrun(track, offset) { 34176 var bytes, samples, sample, i; 34177 34178 samples = track.samples || []; 34179 offset += 8 + 12 + 16 * samples.length; 34180 34181 bytes = trunHeader(samples, offset); 34182 34183 for (i = 0; i < samples.length; i++) { 34184 sample = samples[i]; 34185 bytes = bytes.concat([(sample.duration & 0xFF000000) >>> 24, (sample.duration & 0xFF0000) >>> 16, (sample.duration & 0xFF00) >>> 8, sample.duration & 0xFF, // sample_duration 34186 (sample.size & 0xFF000000) >>> 24, (sample.size & 0xFF0000) >>> 16, (sample.size & 0xFF00) >>> 8, sample.size & 0xFF, // sample_size 34187 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 34188 (sample.compositionTimeOffset & 0xFF000000) >>> 24, (sample.compositionTimeOffset & 0xFF0000) >>> 16, (sample.compositionTimeOffset & 0xFF00) >>> 8, sample.compositionTimeOffset & 0xFF // sample_composition_time_offset 34189 ]); 34190 } 34191 return box(types.trun, new Uint8Array(bytes)); 34192 }; 34193 34194 audioTrun = function audioTrun(track, offset) { 34195 var bytes, samples, sample, i; 34196 34197 samples = track.samples || []; 34198 offset += 8 + 12 + 8 * samples.length; 34199 34200 bytes = trunHeader(samples, offset); 34201 34202 for (i = 0; i < samples.length; i++) { 34203 sample = samples[i]; 34204 bytes = bytes.concat([(sample.duration & 0xFF000000) >>> 24, (sample.duration & 0xFF0000) >>> 16, (sample.duration & 0xFF00) >>> 8, sample.duration & 0xFF, // sample_duration 34205 (sample.size & 0xFF000000) >>> 24, (sample.size & 0xFF0000) >>> 16, (sample.size & 0xFF00) >>> 8, sample.size & 0xFF]); // sample_size 34206 } 34207 34208 return box(types.trun, new Uint8Array(bytes)); 34209 }; 34210 34211 trun = function trun(track, offset) { 34212 if (track.type === 'audio') { 34213 return audioTrun(track, offset); 34214 } 34215 34216 return videoTrun(track, offset); 34217 }; 34218 })(); 34219 34220 var mp4Generator = { 34221 ftyp: ftyp, 34222 mdat: mdat, 34223 moof: moof, 34224 moov: moov, 34225 initSegment: function initSegment(tracks) { 34226 var fileType = ftyp(), 34227 movie = moov(tracks), 34228 result; 34229 34230 result = new Uint8Array(fileType.byteLength + movie.byteLength); 34231 result.set(fileType); 34232 result.set(movie, fileType.byteLength); 34233 return result; 34234 } 34235 }; 34236 34237 /** 34238 * mux.js 34239 * 34240 * Copyright (c) 2014 Brightcove 34241 * All rights reserved. 34242 * 34243 * A lightweight readable stream implemention that handles event dispatching. 34244 * Objects that inherit from streams should call init in their constructors. 34245 */ 34246 34247 var Stream = function Stream() { 34248 this.init = function () { 34249 var listeners = {}; 34250 /** 34251 * Add a listener for a specified event type. 34252 * @param type {string} the event name 34253 * @param listener {function} the callback to be invoked when an event of 34254 * the specified type occurs 34255 */ 34256 this.on = function (type, listener) { 34257 if (!listeners[type]) { 34258 listeners[type] = []; 34259 } 34260 listeners[type] = listeners[type].concat(listener); 34261 }; 34262 /** 34263 * Remove a listener for a specified event type. 34264 * @param type {string} the event name 34265 * @param listener {function} a function previously registered for this 34266 * type of event through `on` 34267 */ 34268 this.off = function (type, listener) { 34269 var index; 34270 if (!listeners[type]) { 34271 return false; 34272 } 34273 index = listeners[type].indexOf(listener); 34274 listeners[type] = listeners[type].slice(); 34275 listeners[type].splice(index, 1); 34276 return index > -1; 34277 }; 34278 /** 34279 * Trigger an event of the specified type on this stream. Any additional 34280 * arguments to this function are passed as parameters to event listeners. 34281 * @param type {string} the event name 34282 */ 34283 this.trigger = function (type) { 34284 var callbacks, i, length, args; 34285 callbacks = listeners[type]; 34286 if (!callbacks) { 34287 return; 34288 } 34289 // Slicing the arguments on every invocation of this method 34290 // can add a significant amount of overhead. Avoid the 34291 // intermediate object creation for the common case of a 34292 // single callback argument 34293 if (arguments.length === 2) { 34294 length = callbacks.length; 34295 for (i = 0; i < length; ++i) { 34296 callbacks[i].call(this, arguments[1]); 34297 } 34298 } else { 34299 args = []; 34300 i = arguments.length; 34301 for (i = 1; i < arguments.length; ++i) { 34302 args.push(arguments[i]); 34303 } 34304 length = callbacks.length; 34305 for (i = 0; i < length; ++i) { 34306 callbacks[i].apply(this, args); 34307 } 34308 } 34309 }; 34310 /** 34311 * Destroys the stream and cleans up. 34312 */ 34313 this.dispose = function () { 34314 listeners = {}; 34315 }; 34316 }; 34317 }; 34318 34319 /** 34320 * Forwards all `data` events on this stream to the destination stream. The 34321 * destination stream should provide a method `push` to receive the data 34322 * events as they arrive. 34323 * @param destination {stream} the stream that will receive all `data` events 34324 * @param autoFlush {boolean} if false, we will not call `flush` on the destination 34325 * when the current stream emits a 'done' event 34326 * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options 34327 */ 34328 Stream.prototype.pipe = function (destination) { 34329 this.on('data', function (data) { 34330 destination.push(data); 34331 }); 34332 34333 this.on('done', function (flushSource) { 34334 destination.flush(flushSource); 34335 }); 34336 34337 return destination; 34338 }; 34339 34340 // Default stream functions that are expected to be overridden to perform 34341 // actual work. These are provided by the prototype as a sort of no-op 34342 // implementation so that we don't have to check for their existence in the 34343 // `pipe` function above. 34344 Stream.prototype.push = function (data) { 34345 this.trigger('data', data); 34346 }; 34347 34348 Stream.prototype.flush = function (flushSource) { 34349 this.trigger('done', flushSource); 34350 }; 34351 34352 var stream = Stream; 34353 34354 // ----------------- 34355 // Link To Transport 34356 // ----------------- 34357 34358 // Supplemental enhancement information (SEI) NAL units have a 34359 // payload type field to indicate how they are to be 34360 // interpreted. CEAS-708 caption content is always transmitted with 34361 // payload type 0x04. 34362 var USER_DATA_REGISTERED_ITU_T_T35 = 4, 34363 RBSP_TRAILING_BITS = 128; 34364 34365 /** 34366 * Parse a supplemental enhancement information (SEI) NAL unit. 34367 * Stops parsing once a message of type ITU T T35 has been found. 34368 * 34369 * @param bytes {Uint8Array} the bytes of a SEI NAL unit 34370 * @return {object} the parsed SEI payload 34371 * @see Rec. ITU-T H.264, 7.3.2.3.1 34372 */ 34373 var parseSei = function parseSei(bytes) { 34374 var i = 0, 34375 result = { 34376 payloadType: -1, 34377 payloadSize: 0 34378 }, 34379 payloadType = 0, 34380 payloadSize = 0; 34381 34382 // go through the sei_rbsp parsing each each individual sei_message 34383 while (i < bytes.byteLength) { 34384 // stop once we have hit the end of the sei_rbsp 34385 if (bytes[i] === RBSP_TRAILING_BITS) { 34386 break; 34387 } 34388 34389 // Parse payload type 34390 while (bytes[i] === 0xFF) { 34391 payloadType += 255; 34392 i++; 34393 } 34394 payloadType += bytes[i++]; 34395 34396 // Parse payload size 34397 while (bytes[i] === 0xFF) { 34398 payloadSize += 255; 34399 i++; 34400 } 34401 payloadSize += bytes[i++]; 34402 34403 // this sei_message is a 608/708 caption so save it and break 34404 // there can only ever be one caption message in a frame's sei 34405 if (!result.payload && payloadType === USER_DATA_REGISTERED_ITU_T_T35) { 34406 result.payloadType = payloadType; 34407 result.payloadSize = payloadSize; 34408 result.payload = bytes.subarray(i, i + payloadSize); 34409 break; 34410 } 34411 34412 // skip the payload and parse the next message 34413 i += payloadSize; 34414 payloadType = 0; 34415 payloadSize = 0; 34416 } 34417 34418 return result; 34419 }; 34420 34421 // see ANSI/SCTE 128-1 (2013), section 8.1 34422 var parseUserData = function parseUserData(sei) { 34423 // itu_t_t35_contry_code must be 181 (United States) for 34424 // captions 34425 if (sei.payload[0] !== 181) { 34426 return null; 34427 } 34428 34429 // itu_t_t35_provider_code should be 49 (ATSC) for captions 34430 if ((sei.payload[1] << 8 | sei.payload[2]) !== 49) { 34431 return null; 34432 } 34433 34434 // the user_identifier should be "GA94" to indicate ATSC1 data 34435 if (String.fromCharCode(sei.payload[3], sei.payload[4], sei.payload[5], sei.payload[6]) !== 'GA94') { 34436 return null; 34437 } 34438 34439 // finally, user_data_type_code should be 0x03 for caption data 34440 if (sei.payload[7] !== 0x03) { 34441 return null; 34442 } 34443 34444 // return the user_data_type_structure and strip the trailing 34445 // marker bits 34446 return sei.payload.subarray(8, sei.payload.length - 1); 34447 }; 34448 34449 // see CEA-708-D, section 4.4 34450 var parseCaptionPackets = function parseCaptionPackets(pts, userData) { 34451 var results = [], 34452 i, 34453 count, 34454 offset, 34455 data; 34456 34457 // if this is just filler, return immediately 34458 if (!(userData[0] & 0x40)) { 34459 return results; 34460 } 34461 34462 // parse out the cc_data_1 and cc_data_2 fields 34463 count = userData[0] & 0x1f; 34464 for (i = 0; i < count; i++) { 34465 offset = i * 3; 34466 data = { 34467 type: userData[offset + 2] & 0x03, 34468 pts: pts 34469 }; 34470 34471 // capture cc data when cc_valid is 1 34472 if (userData[offset + 2] & 0x04) { 34473 data.ccData = userData[offset + 3] << 8 | userData[offset + 4]; 34474 results.push(data); 34475 } 34476 } 34477 return results; 34478 }; 34479 34480 var CaptionStream = function CaptionStream() { 34481 34482 CaptionStream.prototype.init.call(this); 34483 34484 this.captionPackets_ = []; 34485 34486 this.ccStreams_ = [new Cea608Stream(0, 0), // eslint-disable-line no-use-before-define 34487 new Cea608Stream(0, 1), // eslint-disable-line no-use-before-define 34488 new Cea608Stream(1, 0), // eslint-disable-line no-use-before-define 34489 new Cea608Stream(1, 1) // eslint-disable-line no-use-before-define 34490 ]; 34491 34492 this.reset(); 34493 34494 // forward data and done events from CCs to this CaptionStream 34495 this.ccStreams_.forEach(function (cc) { 34496 cc.on('data', this.trigger.bind(this, 'data')); 34497 cc.on('done', this.trigger.bind(this, 'done')); 34498 }, this); 34499 }; 34500 34501 CaptionStream.prototype = new stream(); 34502 CaptionStream.prototype.push = function (event) { 34503 var sei, userData; 34504 34505 // only examine SEI NALs 34506 if (event.nalUnitType !== 'sei_rbsp') { 34507 return; 34508 } 34509 34510 // parse the sei 34511 sei = parseSei(event.escapedRBSP); 34512 34513 // ignore everything but user_data_registered_itu_t_t35 34514 if (sei.payloadType !== USER_DATA_REGISTERED_ITU_T_T35) { 34515 return; 34516 } 34517 34518 // parse out the user data payload 34519 userData = parseUserData(sei); 34520 34521 // ignore unrecognized userData 34522 if (!userData) { 34523 return; 34524 } 34525 34526 // Sometimes, the same segment # will be downloaded twice. To stop the 34527 // caption data from being processed twice, we track the latest dts we've 34528 // received and ignore everything with a dts before that. However, since 34529 // data for a specific dts can be split across packets on either side of 34530 // a segment boundary, we need to make sure we *don't* ignore the packets 34531 // from the *next* segment that have dts === this.latestDts_. By constantly 34532 // tracking the number of packets received with dts === this.latestDts_, we 34533 // know how many should be ignored once we start receiving duplicates. 34534 if (event.dts < this.latestDts_) { 34535 // We've started getting older data, so set the flag. 34536 this.ignoreNextEqualDts_ = true; 34537 return; 34538 } else if (event.dts === this.latestDts_ && this.ignoreNextEqualDts_) { 34539 this.numSameDts_--; 34540 if (!this.numSameDts_) { 34541 // We've received the last duplicate packet, time to start processing again 34542 this.ignoreNextEqualDts_ = false; 34543 } 34544 return; 34545 } 34546 34547 // parse out CC data packets and save them for later 34548 this.captionPackets_ = this.captionPackets_.concat(parseCaptionPackets(event.pts, userData)); 34549 if (this.latestDts_ !== event.dts) { 34550 this.numSameDts_ = 0; 34551 } 34552 this.numSameDts_++; 34553 this.latestDts_ = event.dts; 34554 }; 34555 34556 CaptionStream.prototype.flush = function () { 34557 // make sure we actually parsed captions before proceeding 34558 if (!this.captionPackets_.length) { 34559 this.ccStreams_.forEach(function (cc) { 34560 cc.flush(); 34561 }, this); 34562 return; 34563 } 34564 34565 // In Chrome, the Array#sort function is not stable so add a 34566 // presortIndex that we can use to ensure we get a stable-sort 34567 this.captionPackets_.forEach(function (elem, idx) { 34568 elem.presortIndex = idx; 34569 }); 34570 34571 // sort caption byte-pairs based on their PTS values 34572 this.captionPackets_.sort(function (a, b) { 34573 if (a.pts === b.pts) { 34574 return a.presortIndex - b.presortIndex; 34575 } 34576 return a.pts - b.pts; 34577 }); 34578 34579 this.captionPackets_.forEach(function (packet) { 34580 if (packet.type < 2) { 34581 // Dispatch packet to the right Cea608Stream 34582 this.dispatchCea608Packet(packet); 34583 } 34584 // this is where an 'else' would go for a dispatching packets 34585 // to a theoretical Cea708Stream that handles SERVICEn data 34586 }, this); 34587 34588 this.captionPackets_.length = 0; 34589 this.ccStreams_.forEach(function (cc) { 34590 cc.flush(); 34591 }, this); 34592 return; 34593 }; 34594 34595 CaptionStream.prototype.reset = function () { 34596 this.latestDts_ = null; 34597 this.ignoreNextEqualDts_ = false; 34598 this.numSameDts_ = 0; 34599 this.activeCea608Channel_ = [null, null]; 34600 this.ccStreams_.forEach(function (ccStream) { 34601 ccStream.reset(); 34602 }); 34603 }; 34604 34605 CaptionStream.prototype.dispatchCea608Packet = function (packet) { 34606 // NOTE: packet.type is the CEA608 field 34607 if (this.setsChannel1Active(packet)) { 34608 this.activeCea608Channel_[packet.type] = 0; 34609 } else if (this.setsChannel2Active(packet)) { 34610 this.activeCea608Channel_[packet.type] = 1; 34611 } 34612 if (this.activeCea608Channel_[packet.type] === null) { 34613 // If we haven't received anything to set the active channel, discard the 34614 // data; we don't want jumbled captions 34615 return; 34616 } 34617 this.ccStreams_[(packet.type << 1) + this.activeCea608Channel_[packet.type]].push(packet); 34618 }; 34619 34620 CaptionStream.prototype.setsChannel1Active = function (packet) { 34621 return (packet.ccData & 0x7800) === 0x1000; 34622 }; 34623 CaptionStream.prototype.setsChannel2Active = function (packet) { 34624 return (packet.ccData & 0x7800) === 0x1800; 34625 }; 34626 34627 // ---------------------- 34628 // Session to Application 34629 // ---------------------- 34630 34631 var CHARACTER_TRANSLATION = { 34632 0x2a: 0xe1, // á 34633 0x5c: 0xe9, // é 34634 0x5e: 0xed, // à 34635 0x5f: 0xf3, // ó 34636 0x60: 0xfa, // ú 34637 0x7b: 0xe7, // ç 34638 0x7c: 0xf7, // ÷ 34639 0x7d: 0xd1, // à 34640 0x7e: 0xf1, // ñ 34641 0x7f: 0x2588, // â 34642 0x0130: 0xae, // ® 34643 0x0131: 0xb0, // ° 34644 0x0132: 0xbd, // ½ 34645 0x0133: 0xbf, // ¿ 34646 0x0134: 0x2122, // ⢠34647 0x0135: 0xa2, // ¢ 34648 0x0136: 0xa3, // £ 34649 0x0137: 0x266a, // ⪠34650 0x0138: 0xe0, // à 34651 0x0139: 0xa0, // 34652 0x013a: 0xe8, // è 34653 0x013b: 0xe2, // â 34654 0x013c: 0xea, // ê 34655 0x013d: 0xee, // î 34656 0x013e: 0xf4, // ô 34657 0x013f: 0xfb, // û 34658 0x0220: 0xc1, // à 34659 0x0221: 0xc9, // à 34660 0x0222: 0xd3, // à 34661 0x0223: 0xda, // à 34662 0x0224: 0xdc, // à 34663 0x0225: 0xfc, // ü 34664 0x0226: 0x2018, // â 34665 0x0227: 0xa1, // ¡ 34666 0x0228: 0x2a, // * 34667 0x0229: 0x27, // ' 34668 0x022a: 0x2014, // â 34669 0x022b: 0xa9, // © 34670 0x022c: 0x2120, // â 34671 0x022d: 0x2022, // ⢠34672 0x022e: 0x201c, // â 34673 0x022f: 0x201d, // â 34674 0x0230: 0xc0, // à 34675 0x0231: 0xc2, // à 34676 0x0232: 0xc7, // à 34677 0x0233: 0xc8, // à 34678 0x0234: 0xca, // à 34679 0x0235: 0xcb, // à 34680 0x0236: 0xeb, // ë 34681 0x0237: 0xce, // à 34682 0x0238: 0xcf, // à 34683 0x0239: 0xef, // ï 34684 0x023a: 0xd4, // à 34685 0x023b: 0xd9, // à 34686 0x023c: 0xf9, // ù 34687 0x023d: 0xdb, // à 34688 0x023e: 0xab, // « 34689 0x023f: 0xbb, // » 34690 0x0320: 0xc3, // à 34691 0x0321: 0xe3, // ã 34692 0x0322: 0xcd, // à 34693 0x0323: 0xcc, // à 34694 0x0324: 0xec, // ì 34695 0x0325: 0xd2, // à 34696 0x0326: 0xf2, // ò 34697 0x0327: 0xd5, // à 34698 0x0328: 0xf5, // õ 34699 0x0329: 0x7b, // { 34700 0x032a: 0x7d, // } 34701 0x032b: 0x5c, // \ 34702 0x032c: 0x5e, // ^ 34703 0x032d: 0x5f, // _ 34704 0x032e: 0x7c, // | 34705 0x032f: 0x7e, // ~ 34706 0x0330: 0xc4, // à 34707 0x0331: 0xe4, // ä 34708 0x0332: 0xd6, // à 34709 0x0333: 0xf6, // ö 34710 0x0334: 0xdf, // à 34711 0x0335: 0xa5, // Â¥ 34712 0x0336: 0xa4, // ¤ 34713 0x0337: 0x2502, // â 34714 0x0338: 0xc5, // à 34715 0x0339: 0xe5, // Ã¥ 34716 0x033a: 0xd8, // à 34717 0x033b: 0xf8, // ø 34718 0x033c: 0x250c, // â 34719 0x033d: 0x2510, // â 34720 0x033e: 0x2514, // â 34721 0x033f: 0x2518 // â 34722 }; 34723 34724 var getCharFromCode = function getCharFromCode(code) { 34725 if (code === null) { 34726 return ''; 34727 } 34728 code = CHARACTER_TRANSLATION[code] || code; 34729 return String.fromCharCode(code); 34730 }; 34731 34732 // the index of the last row in a CEA-608 display buffer 34733 var BOTTOM_ROW = 14; 34734 34735 // This array is used for mapping PACs -> row #, since there's no way of 34736 // getting it through bit logic. 34737 var ROWS = [0x1100, 0x1120, 0x1200, 0x1220, 0x1500, 0x1520, 0x1600, 0x1620, 0x1700, 0x1720, 0x1000, 0x1300, 0x1320, 0x1400, 0x1420]; 34738 34739 // CEA-608 captions are rendered onto a 34x15 matrix of character 34740 // cells. The "bottom" row is the last element in the outer array. 34741 var createDisplayBuffer = function createDisplayBuffer() { 34742 var result = [], 34743 i = BOTTOM_ROW + 1; 34744 while (i--) { 34745 result.push(''); 34746 } 34747 return result; 34748 }; 34749 34750 var Cea608Stream = function Cea608Stream(field, dataChannel) { 34751 Cea608Stream.prototype.init.call(this); 34752 34753 this.field_ = field || 0; 34754 this.dataChannel_ = dataChannel || 0; 34755 34756 this.name_ = 'CC' + ((this.field_ << 1 | this.dataChannel_) + 1); 34757 34758 this.setConstants(); 34759 this.reset(); 34760 34761 this.push = function (packet) { 34762 var data, swap, char0, char1, text; 34763 // remove the parity bits 34764 data = packet.ccData & 0x7f7f; 34765 34766 // ignore duplicate control codes; the spec demands they're sent twice 34767 if (data === this.lastControlCode_) { 34768 this.lastControlCode_ = null; 34769 return; 34770 } 34771 34772 // Store control codes 34773 if ((data & 0xf000) === 0x1000) { 34774 this.lastControlCode_ = data; 34775 } else if (data !== this.PADDING_) { 34776 this.lastControlCode_ = null; 34777 } 34778 34779 char0 = data >>> 8; 34780 char1 = data & 0xff;
vendor: 4,605 bytes, lines 34781-34882
34781 34782 if (data === this.PADDING_) { 34783 return; 34784 } else if (data === this.RESUME_CAPTION_LOADING_) { 34785 this.mode_ = 'popOn'; 34786 } else if (data === this.END_OF_CAPTION_) { 34787 this.clearFormatting(packet.pts); 34788 // if a caption was being displayed, it's gone now 34789 this.flushDisplayed(packet.pts); 34790 34791 // flip memory 34792 swap = this.displayed_; 34793 this.displayed_ = this.nonDisplayed_; 34794 this.nonDisplayed_ = swap; 34795 34796 // start measuring the time to display the caption 34797 this.startPts_ = packet.pts; 34798 } else if (data === this.ROLL_UP_2_ROWS_) { 34799 this.topRow_ = BOTTOM_ROW - 1; 34800 this.mode_ = 'rollUp'; 34801 } else if (data === this.ROLL_UP_3_ROWS_) { 34802 this.topRow_ = BOTTOM_ROW - 2; 34803 this.mode_ = 'rollUp'; 34804 } else if (data === this.ROLL_UP_4_ROWS_) { 34805 this.topRow_ = BOTTOM_ROW - 3; 34806 this.mode_ = 'rollUp'; 34807 } else if (data === this.CARRIAGE_RETURN_) { 34808 this.clearFormatting(packet.pts); 34809 this.flushDisplayed(packet.pts); 34810 this.shiftRowsUp_(); 34811 this.startPts_ = packet.pts; 34812 } else if (data === this.BACKSPACE_) { 34813 if (this.mode_ === 'popOn') { 34814 this.nonDisplayed_[BOTTOM_ROW] = this.nonDisplayed_[BOTTOM_ROW].slice(0, -1); 34815 } else { 34816 this.displayed_[BOTTOM_ROW] = this.displayed_[BOTTOM_ROW].slice(0, -1); 34817 } 34818 } else if (data === this.ERASE_DISPLAYED_MEMORY_) { 34819 this.flushDisplayed(packet.pts); 34820 this.displayed_ = createDisplayBuffer(); 34821 } else if (data === this.ERASE_NON_DISPLAYED_MEMORY_) { 34822 this.nonDisplayed_ = createDisplayBuffer(); 34823 } else if (data === this.RESUME_DIRECT_CAPTIONING_) { 34824 this.mode_ = 'paintOn'; 34825 34826 // Append special characters to caption text 34827 } else if (this.isSpecialCharacter(char0, char1)) { 34828 // Bitmask char0 so that we can apply character transformations 34829 // regardless of field and data channel. 34830 // Then byte-shift to the left and OR with char1 so we can pass the 34831 // entire character code to `getCharFromCode`. 34832 char0 = (char0 & 0x03) << 8; 34833 text = getCharFromCode(char0 | char1); 34834 this[this.mode_](packet.pts, text); 34835 this.column_++; 34836 34837 // Append extended characters to caption text 34838 } else if (this.isExtCharacter(char0, char1)) { 34839 // Extended characters always follow their "non-extended" equivalents. 34840 // IE if a "è" is desired, you'll always receive "eè"; non-compliant 34841 // decoders are supposed to drop the "è", while compliant decoders 34842 // backspace the "e" and insert "è". 34843 34844 // Delete the previous character 34845 if (this.mode_ === 'popOn') { 34846 this.nonDisplayed_[this.row_] = this.nonDisplayed_[this.row_].slice(0, -1); 34847 } else { 34848 this.displayed_[BOTTOM_ROW] = this.displayed_[BOTTOM_ROW].slice(0, -1); 34849 } 34850 34851 // Bitmask char0 so that we can apply character transformations 34852 // regardless of field and data channel. 34853 // Then byte-shift to the left and OR with char1 so we can pass the 34854 // entire character code to `getCharFromCode`. 34855 char0 = (char0 & 0x03) << 8; 34856 text = getCharFromCode(char0 | char1); 34857 this[this.mode_](packet.pts, text); 34858 this.column_++; 34859 34860 // Process mid-row codes 34861 } else if (this.isMidRowCode(char0, char1)) { 34862 // Attributes are not additive, so clear all formatting 34863 this.clearFormatting(packet.pts); 34864 34865 // According to the standard, mid-row codes 34866 // should be replaced with spaces, so add one now 34867 this[this.mode_](packet.pts, ' '); 34868 this.column_++; 34869 34870 if ((char1 & 0xe) === 0xe) { 34871 this.addFormatting(packet.pts, ['i']); 34872 } 34873 34874 if ((char1 & 0x1) === 0x1) { 34875 this.addFormatting(packet.pts, ['u']); 34876 } 34877 34878 // Detect offset control codes and adjust cursor 34879 } else if (this.isOffsetControlCode(char0, char1)) { 34880 // Cursor position is set by indent PAC (see below) in 4-column 34881 // increments, with an additional offset code of 1-3 to reach any 34882 // of the 32 columns specified by CEA-608. So all we need to do
vendor: 80,906 bytes, lines 34883-37094
34883 // here is increment the column cursor by the given offset. 34884 this.column_ += char1 & 0x03; 34885 34886 // Detect PACs (Preamble Address Codes) 34887 } else if (this.isPAC(char0, char1)) { 34888 34889 // There's no logic for PAC -> row mapping, so we have to just 34890 // find the row code in an array and use its index :( 34891 var row = ROWS.indexOf(data & 0x1f20); 34892 34893 if (row !== this.row_) { 34894 // formatting is only persistent for current row 34895 this.clearFormatting(packet.pts); 34896 this.row_ = row; 34897 } 34898 // All PACs can apply underline, so detect and apply 34899 // (All odd-numbered second bytes set underline) 34900 if (char1 & 0x1 && this.formatting_.indexOf('u') === -1) { 34901 this.addFormatting(packet.pts, ['u']); 34902 } 34903 34904 if ((data & 0x10) === 0x10) { 34905 // We've got an indent level code. Each successive even number 34906 // increments the column cursor by 4, so we can get the desired 34907 // column position by bit-shifting to the right (to get n/2) 34908 // and multiplying by 4. 34909 this.column_ = ((data & 0xe) >> 1) * 4; 34910 } 34911 34912 if (this.isColorPAC(char1)) { 34913 // it's a color code, though we only support white, which 34914 // can be either normal or italicized. white italics can be 34915 // either 0x4e or 0x6e depending on the row, so we just 34916 // bitwise-and with 0xe to see if italics should be turned on 34917 if ((char1 & 0xe) === 0xe) { 34918 this.addFormatting(packet.pts, ['i']); 34919 } 34920 } 34921 34922 // We have a normal character in char0, and possibly one in char1 34923 } else if (this.isNormalChar(char0)) { 34924 if (char1 === 0x00) { 34925 char1 = null; 34926 } 34927 text = getCharFromCode(char0); 34928 text += getCharFromCode(char1); 34929 this[this.mode_](packet.pts, text); 34930 this.column_ += text.length; 34931 } // finish data processing 34932 }; 34933 }; 34934 Cea608Stream.prototype = new stream(); 34935 // Trigger a cue point that captures the current state of the 34936 // display buffer 34937 Cea608Stream.prototype.flushDisplayed = function (pts) { 34938 var content = this.displayed_ 34939 // remove spaces from the start and end of the string 34940 .map(function (row) { 34941 return row.trim(); 34942 }) 34943 // combine all text rows to display in one cue 34944 .join('\n') 34945 // and remove blank rows from the start and end, but not the middle 34946 .replace(/^\n+|\n+$/g, ''); 34947 34948 if (content.length) { 34949 this.trigger('data', { 34950 startPts: this.startPts_, 34951 endPts: pts, 34952 text: content, 34953 stream: this.name_ 34954 }); 34955 } 34956 }; 34957 34958 /** 34959 * Zero out the data, used for startup and on seek 34960 */ 34961 Cea608Stream.prototype.reset = function () { 34962 this.mode_ = 'popOn'; 34963 // When in roll-up mode, the index of the last row that will 34964 // actually display captions. If a caption is shifted to a row 34965 // with a lower index than this, it is cleared from the display 34966 // buffer 34967 this.topRow_ = 0; 34968 this.startPts_ = 0; 34969 this.displayed_ = createDisplayBuffer(); 34970 this.nonDisplayed_ = createDisplayBuffer(); 34971 this.lastControlCode_ = null; 34972 34973 // Track row and column for proper line-breaking and spacing 34974 this.column_ = 0; 34975 this.row_ = BOTTOM_ROW; 34976 34977 // This variable holds currently-applied formatting 34978 this.formatting_ = []; 34979 }; 34980 34981 /** 34982 * Sets up control code and related constants for this instance 34983 */ 34984 Cea608Stream.prototype.setConstants = function () { 34985 // The following attributes have these uses: 34986 // ext_ : char0 for mid-row codes, and the base for extended 34987 // chars (ext_+0, ext_+1, and ext_+2 are char0s for 34988 // extended codes) 34989 // control_: char0 for control codes, except byte-shifted to the 34990 // left so that we can do this.control_ | CONTROL_CODE 34991 // offset_: char0 for tab offset codes 34992 // 34993 // It's also worth noting that control codes, and _only_ control codes, 34994 // differ between field 1 and field2. Field 2 control codes are always 34995 // their field 1 value plus 1. That's why there's the "| field" on the 34996 // control value. 34997 if (this.dataChannel_ === 0) { 34998 this.BASE_ = 0x10; 34999 this.EXT_ = 0x11; 35000 this.CONTROL_ = (0x14 | this.field_) << 8; 35001 this.OFFSET_ = 0x17; 35002 } else if (this.dataChannel_ === 1) { 35003 this.BASE_ = 0x18; 35004 this.EXT_ = 0x19; 35005 this.CONTROL_ = (0x1c | this.field_) << 8; 35006 this.OFFSET_ = 0x1f; 35007 } 35008 35009 // Constants for the LSByte command codes recognized by Cea608Stream. This 35010 // list is not exhaustive. For a more comprehensive listing and semantics see 35011 // http://www.gpo.gov/fdsys/pkg/CFR-2010-title47-vol1/pdf/CFR-2010-title47-vol1-sec15-119.pdf 35012 // Padding 35013 this.PADDING_ = 0x0000; 35014 // Pop-on Mode 35015 this.RESUME_CAPTION_LOADING_ = this.CONTROL_ | 0x20; 35016 this.END_OF_CAPTION_ = this.CONTROL_ | 0x2f; 35017 // Roll-up Mode 35018 this.ROLL_UP_2_ROWS_ = this.CONTROL_ | 0x25; 35019 this.ROLL_UP_3_ROWS_ = this.CONTROL_ | 0x26; 35020 this.ROLL_UP_4_ROWS_ = this.CONTROL_ | 0x27; 35021 this.CARRIAGE_RETURN_ = this.CONTROL_ | 0x2d; 35022 // paint-on mode (not supported) 35023 this.RESUME_DIRECT_CAPTIONING_ = this.CONTROL_ | 0x29; 35024 // Erasure 35025 this.BACKSPACE_ = this.CONTROL_ | 0x21; 35026 this.ERASE_DISPLAYED_MEMORY_ = this.CONTROL_ | 0x2c; 35027 this.ERASE_NON_DISPLAYED_MEMORY_ = this.CONTROL_ | 0x2e; 35028 }; 35029 35030 /** 35031 * Detects if the 2-byte packet data is a special character 35032 * 35033 * Special characters have a second byte in the range 0x30 to 0x3f, 35034 * with the first byte being 0x11 (for data channel 1) or 0x19 (for 35035 * data channel 2). 35036 * 35037 * @param {Integer} char0 The first byte 35038 * @param {Integer} char1 The second byte 35039 * @return {Boolean} Whether the 2 bytes are an special character 35040 */ 35041 Cea608Stream.prototype.isSpecialCharacter = function (char0, char1) { 35042 return char0 === this.EXT_ && char1 >= 0x30 && char1 <= 0x3f; 35043 }; 35044 35045 /** 35046 * Detects if the 2-byte packet data is an extended character 35047 * 35048 * Extended characters have a second byte in the range 0x20 to 0x3f, 35049 * with the first byte being 0x12 or 0x13 (for data channel 1) or 35050 * 0x1a or 0x1b (for data channel 2). 35051 * 35052 * @param {Integer} char0 The first byte 35053 * @param {Integer} char1 The second byte 35054 * @return {Boolean} Whether the 2 bytes are an extended character 35055 */ 35056 Cea608Stream.prototype.isExtCharacter = function (char0, char1) { 35057 return (char0 === this.EXT_ + 1 || char0 === this.EXT_ + 2) && char1 >= 0x20 && char1 <= 0x3f; 35058 }; 35059 35060 /** 35061 * Detects if the 2-byte packet is a mid-row code 35062 * 35063 * Mid-row codes have a second byte in the range 0x20 to 0x2f, with 35064 * the first byte being 0x11 (for data channel 1) or 0x19 (for data 35065 * channel 2). 35066 * 35067 * @param {Integer} char0 The first byte 35068 * @param {Integer} char1 The second byte 35069 * @return {Boolean} Whether the 2 bytes are a mid-row code 35070 */ 35071 Cea608Stream.prototype.isMidRowCode = function (char0, char1) { 35072 return char0 === this.EXT_ && char1 >= 0x20 && char1 <= 0x2f; 35073 }; 35074 35075 /** 35076 * Detects if the 2-byte packet is an offset control code 35077 * 35078 * Offset control codes have a second byte in the range 0x21 to 0x23, 35079 * with the first byte being 0x17 (for data channel 1) or 0x1f (for 35080 * data channel 2). 35081 * 35082 * @param {Integer} char0 The first byte 35083 * @param {Integer} char1 The second byte 35084 * @return {Boolean} Whether the 2 bytes are an offset control code 35085 */ 35086 Cea608Stream.prototype.isOffsetControlCode = function (char0, char1) { 35087 return char0 === this.OFFSET_ && char1 >= 0x21 && char1 <= 0x23; 35088 }; 35089 35090 /** 35091 * Detects if the 2-byte packet is a Preamble Address Code 35092 * 35093 * PACs have a first byte in the range 0x10 to 0x17 (for data channel 1) 35094 * or 0x18 to 0x1f (for data channel 2), with the second byte in the 35095 * range 0x40 to 0x7f. 35096 * 35097 * @param {Integer} char0 The first byte 35098 * @param {Integer} char1 The second byte 35099 * @return {Boolean} Whether the 2 bytes are a PAC 35100 */ 35101 Cea608Stream.prototype.isPAC = function (char0, char1) { 35102 return char0 >= this.BASE_ && char0 < this.BASE_ + 8 && char1 >= 0x40 && char1 <= 0x7f; 35103 }; 35104 35105 /** 35106 * Detects if a packet's second byte is in the range of a PAC color code 35107 * 35108 * PAC color codes have the second byte be in the range 0x40 to 0x4f, or 35109 * 0x60 to 0x6f. 35110 * 35111 * @param {Integer} char1 The second byte 35112 * @return {Boolean} Whether the byte is a color PAC 35113 */ 35114 Cea608Stream.prototype.isColorPAC = function (char1) { 35115 return char1 >= 0x40 && char1 <= 0x4f || char1 >= 0x60 && char1 <= 0x7f; 35116 }; 35117 35118 /** 35119 * Detects if a single byte is in the range of a normal character 35120 * 35121 * Normal text bytes are in the range 0x20 to 0x7f. 35122 * 35123 * @param {Integer} char The byte 35124 * @return {Boolean} Whether the byte is a normal character 35125 */ 35126 Cea608Stream.prototype.isNormalChar = function (char) { 35127 return char >= 0x20 && char <= 0x7f; 35128 }; 35129 35130 // Adds the opening HTML tag for the passed character to the caption text, 35131 // and keeps track of it for later closing 35132 Cea608Stream.prototype.addFormatting = function (pts, format) { 35133 this.formatting_ = this.formatting_.concat(format); 35134 var text = format.reduce(function (text, format) { 35135 return text + '<' + format + '>'; 35136 }, ''); 35137 this[this.mode_](pts, text); 35138 }; 35139 35140 // Adds HTML closing tags for current formatting to caption text and 35141 // clears remembered formatting 35142 Cea608Stream.prototype.clearFormatting = function (pts) { 35143 if (!this.formatting_.length) { 35144 return; 35145 } 35146 var text = this.formatting_.reverse().reduce(function (text, format) { 35147 return text + '</' + format + '>'; 35148 }, ''); 35149 this.formatting_ = []; 35150 this[this.mode_](pts, text); 35151 }; 35152 35153 // Mode Implementations 35154 Cea608Stream.prototype.popOn = function (pts, text) { 35155 var baseRow = this.nonDisplayed_[this.row_]; 35156 35157 // buffer characters 35158 baseRow += text; 35159 this.nonDisplayed_[this.row_] = baseRow; 35160 }; 35161 35162 Cea608Stream.prototype.rollUp = function (pts, text) { 35163 var baseRow = this.displayed_[BOTTOM_ROW]; 35164 35165 baseRow += text; 35166 this.displayed_[BOTTOM_ROW] = baseRow; 35167 }; 35168 35169 Cea608Stream.prototype.shiftRowsUp_ = function () { 35170 var i; 35171 // clear out inactive rows 35172 for (i = 0; i < this.topRow_; i++) { 35173 this.displayed_[i] = ''; 35174 } 35175 // shift displayed rows up 35176 for (i = this.topRow_; i < BOTTOM_ROW; i++) { 35177 this.displayed_[i] = this.displayed_[i + 1]; 35178 } 35179 // clear out the bottom row 35180 this.displayed_[BOTTOM_ROW] = ''; 35181 }; 35182 35183 // paintOn mode is not implemented 35184 Cea608Stream.prototype.paintOn = function () {}; 35185 35186 // exports 35187 var captionStream = { 35188 CaptionStream: CaptionStream, 35189 Cea608Stream: Cea608Stream 35190 }; 35191 35192 var streamTypes = { 35193 H264_STREAM_TYPE: 0x1B, 35194 ADTS_STREAM_TYPE: 0x0F, 35195 METADATA_STREAM_TYPE: 0x15 35196 }; 35197 35198 var MAX_TS = 8589934592; 35199 35200 var RO_THRESH = 4294967296; 35201 35202 var handleRollover = function handleRollover(value, reference) { 35203 var direction = 1; 35204 35205 if (value > reference) { 35206 // If the current timestamp value is greater than our reference timestamp and we detect a 35207 // timestamp rollover, this means the roll over is happening in the opposite direction. 35208 // Example scenario: Enter a long stream/video just after a rollover occurred. The reference 35209 // point will be set to a small number, e.g. 1. The user then seeks backwards over the 35210 // rollover point. In loading this segment, the timestamp values will be very large, 35211 // e.g. 2^33 - 1. Since this comes before the data we loaded previously, we want to adjust 35212 // the time stamp to be `value - 2^33`. 35213 direction = -1; 35214 } 35215 35216 // Note: A seek forwards or back that is greater than the RO_THRESH (2^32, ~13 hours) will 35217 // cause an incorrect adjustment. 35218 while (Math.abs(reference - value) > RO_THRESH) { 35219 value += direction * MAX_TS; 35220 } 35221 35222 return value; 35223 }; 35224 35225 var TimestampRolloverStream = function TimestampRolloverStream(type) { 35226 var lastDTS, referenceDTS; 35227 35228 TimestampRolloverStream.prototype.init.call(this); 35229 35230 this.type_ = type; 35231 35232 this.push = function (data) { 35233 if (data.type !== this.type_) { 35234 return; 35235 } 35236 35237 if (referenceDTS === undefined) { 35238 referenceDTS = data.dts; 35239 } 35240 35241 data.dts = handleRollover(data.dts, referenceDTS); 35242 data.pts = handleRollover(data.pts, referenceDTS); 35243 35244 lastDTS = data.dts; 35245 35246 this.trigger('data', data); 35247 }; 35248 35249 this.flush = function () { 35250 referenceDTS = lastDTS; 35251 this.trigger('done'); 35252 }; 35253 35254 this.discontinuity = function () { 35255 referenceDTS = void 0; 35256 lastDTS = void 0; 35257 }; 35258 }; 35259 35260 TimestampRolloverStream.prototype = new stream(); 35261 35262 var timestampRolloverStream = { 35263 TimestampRolloverStream: TimestampRolloverStream, 35264 handleRollover: handleRollover 35265 }; 35266 35267 var percentEncode = function percentEncode(bytes, start, end) { 35268 var i, 35269 result = ''; 35270 for (i = start; i < end; i++) { 35271 result += '%' + ('00' + bytes[i].toString(16)).slice(-2); 35272 } 35273 return result; 35274 }, 35275 35276 35277 // return the string representation of the specified byte range, 35278 // interpreted as UTf-8. 35279 parseUtf8 = function parseUtf8(bytes, start, end) { 35280 return decodeURIComponent(percentEncode(bytes, start, end)); 35281 }, 35282 35283 35284 // return the string representation of the specified byte range, 35285 // interpreted as ISO-8859-1. 35286 parseIso88591 = function parseIso88591(bytes, start, end) { 35287 return unescape(percentEncode(bytes, start, end)); // jshint ignore:line 35288 }, 35289 parseSyncSafeInteger = function parseSyncSafeInteger(data) { 35290 return data[0] << 21 | data[1] << 14 | data[2] << 7 | data[3]; 35291 }, 35292 tagParsers = { 35293 TXXX: function TXXX(tag) { 35294 var i; 35295 if (tag.data[0] !== 3) { 35296 // ignore frames with unrecognized character encodings 35297 return; 35298 } 35299 35300 for (i = 1; i < tag.data.length; i++) { 35301 if (tag.data[i] === 0) { 35302 // parse the text fields 35303 tag.description = parseUtf8(tag.data, 1, i); 35304 // do not include the null terminator in the tag value 35305 tag.value = parseUtf8(tag.data, i + 1, tag.data.length).replace(/\0*$/, ''); 35306 break; 35307 } 35308 } 35309 tag.data = tag.value; 35310 }, 35311 WXXX: function WXXX(tag) { 35312 var i; 35313 if (tag.data[0] !== 3) { 35314 // ignore frames with unrecognized character encodings 35315 return; 35316 } 35317 35318 for (i = 1; i < tag.data.length; i++) { 35319 if (tag.data[i] === 0) { 35320 // parse the description and URL fields 35321 tag.description = parseUtf8(tag.data, 1, i); 35322 tag.url = parseUtf8(tag.data, i + 1, tag.data.length); 35323 break; 35324 } 35325 } 35326 }, 35327 PRIV: function PRIV(tag) { 35328 var i; 35329 35330 for (i = 0; i < tag.data.length; i++) { 35331 if (tag.data[i] === 0) { 35332 // parse the description and URL fields 35333 tag.owner = parseIso88591(tag.data, 0, i); 35334 break; 35335 } 35336 } 35337 tag.privateData = tag.data.subarray(i + 1); 35338 tag.data = tag.privateData; 35339 } 35340 }, 35341 _MetadataStream; 35342 35343 _MetadataStream = function MetadataStream(options) { 35344 var settings = { 35345 debug: !!(options && options.debug), 35346 35347 // the bytes of the program-level descriptor field in MP2T 35348 // see ISO/IEC 13818-1:2013 (E), section 2.6 "Program and 35349 // program element descriptors" 35350 descriptor: options && options.descriptor 35351 }, 35352 35353 35354 // the total size in bytes of the ID3 tag being parsed 35355 tagSize = 0, 35356 35357 35358 // tag data that is not complete enough to be parsed 35359 buffer = [], 35360 35361 35362 // the total number of bytes currently in the buffer 35363 bufferSize = 0, 35364 i; 35365 35366 _MetadataStream.prototype.init.call(this); 35367 35368 // calculate the text track in-band metadata track dispatch type 35369 // https://html.spec.whatwg.org/multipage/embedded-content.html#steps-to-expose-a-media-resource-specific-text-track 35370 this.dispatchType = streamTypes.METADATA_STREAM_TYPE.toString(16); 35371 if (settings.descriptor) { 35372 for (i = 0; i < settings.descriptor.length; i++) { 35373 this.dispatchType += ('00' + settings.descriptor[i].toString(16)).slice(-2); 35374 } 35375 } 35376 35377 this.push = function (chunk) { 35378 var tag, frameStart, frameSize, frame, i, frameHeader; 35379 if (chunk.type !== 'timed-metadata') { 35380 return; 35381 } 35382 35383 // if data_alignment_indicator is set in the PES header, 35384 // we must have the start of a new ID3 tag. Assume anything 35385 // remaining in the buffer was malformed and throw it out 35386 if (chunk.dataAlignmentIndicator) { 35387 bufferSize = 0; 35388 buffer.length = 0; 35389 } 35390 35391 // ignore events that don't look like ID3 data 35392 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))) { 35393 if (settings.debug) { 35394 // eslint-disable-next-line no-console 35395 console.log('Skipping unrecognized metadata packet'); 35396 } 35397 return; 35398 } 35399 35400 // add this chunk to the data we've collected so far 35401 35402 buffer.push(chunk); 35403 bufferSize += chunk.data.byteLength; 35404 35405 // grab the size of the entire frame from the ID3 header 35406 if (buffer.length === 1) { 35407 // the frame size is transmitted as a 28-bit integer in the 35408 // last four bytes of the ID3 header. 35409 // The most significant bit of each byte is dropped and the 35410 // results concatenated to recover the actual value. 35411 tagSize = parseSyncSafeInteger(chunk.data.subarray(6, 10)); 35412 35413 // ID3 reports the tag size excluding the header but it's more 35414 // convenient for our comparisons to include it 35415 tagSize += 10; 35416 } 35417 35418 // if the entire frame has not arrived, wait for more data 35419 if (bufferSize < tagSize) { 35420 return; 35421 } 35422 35423 // collect the entire frame so it can be parsed 35424 tag = { 35425 data: new Uint8Array(tagSize), 35426 frames: [], 35427 pts: buffer[0].pts, 35428 dts: buffer[0].dts 35429 }; 35430 for (i = 0; i < tagSize;) { 35431 tag.data.set(buffer[0].data.subarray(0, tagSize - i), i); 35432 i += buffer[0].data.byteLength; 35433 bufferSize -= buffer[0].data.byteLength; 35434 buffer.shift(); 35435 } 35436 35437 // find the start of the first frame and the end of the tag 35438 frameStart = 10; 35439 if (tag.data[5] & 0x40) { 35440 // advance the frame start past the extended header 35441 frameStart += 4; // header size field 35442 frameStart += parseSyncSafeInteger(tag.data.subarray(10, 14)); 35443 35444 // clip any padding off the end 35445 tagSize -= parseSyncSafeInteger(tag.data.subarray(16, 20)); 35446 } 35447 35448 // parse one or more ID3 frames 35449 // http://id3.org/id3v2.3.0#ID3v2_frame_overview 35450 do { 35451 // determine the number of bytes in this frame 35452 frameSize = parseSyncSafeInteger(tag.data.subarray(frameStart + 4, frameStart + 8)); 35453 if (frameSize < 1) { 35454 // eslint-disable-next-line no-console 35455 return console.log('Malformed ID3 frame encountered. Skipping metadata parsing.'); 35456 } 35457 frameHeader = String.fromCharCode(tag.data[frameStart], tag.data[frameStart + 1], tag.data[frameStart + 2], tag.data[frameStart + 3]); 35458 35459 frame = { 35460 id: frameHeader, 35461 data: tag.data.subarray(frameStart + 10, frameStart + frameSize + 10) 35462 }; 35463 frame.key = frame.id; 35464 if (tagParsers[frame.id]) { 35465 tagParsers[frame.id](frame); 35466 35467 // handle the special PRIV frame used to indicate the start 35468 // time for raw AAC data 35469 if (frame.owner === 'com.apple.streaming.transportStreamTimestamp') { 35470 var d = frame.data, 35471 size = (d[3] & 0x01) << 30 | d[4] << 22 | d[5] << 14 | d[6] << 6 | d[7] >>> 2; 35472 35473 size *= 4; 35474 size += d[7] & 0x03; 35475 frame.timeStamp = size; 35476 // in raw AAC, all subsequent data will be timestamped based 35477 // on the value of this frame 35478 // we couldn't have known the appropriate pts and dts before 35479 // parsing this ID3 tag so set those values now 35480 if (tag.pts === undefined && tag.dts === undefined) { 35481 tag.pts = frame.timeStamp; 35482 tag.dts = frame.timeStamp; 35483 } 35484 this.trigger('timestamp', frame); 35485 } 35486 } 35487 tag.frames.push(frame); 35488 35489 frameStart += 10; // advance past the frame header 35490 frameStart += frameSize; // advance past the frame body 35491 } while (frameStart < tagSize); 35492 this.trigger('data', tag); 35493 }; 35494 }; 35495 _MetadataStream.prototype = new stream(); 35496 35497 var metadataStream = _MetadataStream; 35498 35499 var TimestampRolloverStream$1 = timestampRolloverStream.TimestampRolloverStream; 35500 35501 // object types 35502 var _TransportPacketStream, _TransportParseStream, _ElementaryStream; 35503 35504 // constants 35505 var MP2T_PACKET_LENGTH = 188, 35506 35507 35508 // bytes 35509 SYNC_BYTE = 0x47; 35510 35511 /** 35512 * Splits an incoming stream of binary data into MPEG-2 Transport 35513 * Stream packets. 35514 */ 35515 _TransportPacketStream = function TransportPacketStream() { 35516 var buffer = new Uint8Array(MP2T_PACKET_LENGTH), 35517 bytesInBuffer = 0; 35518 35519 _TransportPacketStream.prototype.init.call(this); 35520 35521 // Deliver new bytes to the stream. 35522 35523 this.push = function (bytes) { 35524 var startIndex = 0, 35525 endIndex = MP2T_PACKET_LENGTH, 35526 everything; 35527 35528 // If there are bytes remaining from the last segment, prepend them to the 35529 // bytes that were pushed in 35530 if (bytesInBuffer) { 35531 everything = new Uint8Array(bytes.byteLength + bytesInBuffer); 35532 everything.set(buffer.subarray(0, bytesInBuffer)); 35533 everything.set(bytes, bytesInBuffer); 35534 bytesInBuffer = 0; 35535 } else { 35536 everything = bytes; 35537 } 35538 35539 // While we have enough data for a packet 35540 while (endIndex < everything.byteLength) { 35541 // Look for a pair of start and end sync bytes in the data.. 35542 if (everything[startIndex] === SYNC_BYTE && everything[endIndex] === SYNC_BYTE) { 35543 // We found a packet so emit it and jump one whole packet forward in 35544 // the stream 35545 this.trigger('data', everything.subarray(startIndex, endIndex)); 35546 startIndex += MP2T_PACKET_LENGTH; 35547 endIndex += MP2T_PACKET_LENGTH; 35548 continue; 35549 } 35550 // If we get here, we have somehow become de-synchronized and we need to step 35551 // forward one byte at a time until we find a pair of sync bytes that denote 35552 // a packet 35553 startIndex++; 35554 endIndex++; 35555 } 35556 35557 // If there was some data left over at the end of the segment that couldn't 35558 // possibly be a whole packet, keep it because it might be the start of a packet 35559 // that continues in the next segment 35560 if (startIndex < everything.byteLength) { 35561 buffer.set(everything.subarray(startIndex), 0); 35562 bytesInBuffer = everything.byteLength - startIndex; 35563 } 35564 }; 35565 35566 this.flush = function () { 35567 // If the buffer contains a whole packet when we are being flushed, emit it 35568 // and empty the buffer. Otherwise hold onto the data because it may be 35569 // important for decoding the next segment 35570 if (bytesInBuffer === MP2T_PACKET_LENGTH && buffer[0] === SYNC_BYTE) { 35571 this.trigger('data', buffer); 35572 bytesInBuffer = 0; 35573 } 35574 this.trigger('done'); 35575 }; 35576 }; 35577 _TransportPacketStream.prototype = new stream(); 35578 35579 /** 35580 * Accepts an MP2T TransportPacketStream and emits data events with parsed 35581 * forms of the individual transport stream packets. 35582 */ 35583 _TransportParseStream = function TransportParseStream() { 35584 var parsePsi, parsePat, parsePmt, self; 35585 _TransportParseStream.prototype.init.call(this); 35586 self = this; 35587 35588 this.packetsWaitingForPmt = []; 35589 this.programMapTable = undefined; 35590 35591 parsePsi = function parsePsi(payload, psi) { 35592 var offset = 0; 35593 35594 // PSI packets may be split into multiple sections and those 35595 // sections may be split into multiple packets. If a PSI 35596 // section starts in this packet, the payload_unit_start_indicator 35597 // will be true and the first byte of the payload will indicate 35598 // the offset from the current position to the start of the 35599 // section. 35600 if (psi.payloadUnitStartIndicator) { 35601 offset += payload[offset] + 1; 35602 } 35603 35604 if (psi.type === 'pat') { 35605 parsePat(payload.subarray(offset), psi); 35606 } else { 35607 parsePmt(payload.subarray(offset), psi); 35608 } 35609 }; 35610 35611 parsePat = function parsePat(payload, pat) { 35612 pat.section_number = payload[7]; // eslint-disable-line camelcase 35613 pat.last_section_number = payload[8]; // eslint-disable-line camelcase 35614 35615 // skip the PSI header and parse the first PMT entry 35616 self.pmtPid = (payload[10] & 0x1F) << 8 | payload[11]; 35617 pat.pmtPid = self.pmtPid; 35618 }; 35619 35620 /** 35621 * Parse out the relevant fields of a Program Map Table (PMT). 35622 * @param payload {Uint8Array} the PMT-specific portion of an MP2T 35623 * packet. The first byte in this array should be the table_id 35624 * field. 35625 * @param pmt {object} the object that should be decorated with 35626 * fields parsed from the PMT. 35627 */ 35628 parsePmt = function parsePmt(payload, pmt) { 35629 var sectionLength, tableEnd, programInfoLength, offset; 35630 35631 // PMTs can be sent ahead of the time when they should actually 35632 // take effect. We don't believe this should ever be the case 35633 // for HLS but we'll ignore "forward" PMT declarations if we see 35634 // them. Future PMT declarations have the current_next_indicator 35635 // set to zero. 35636 if (!(payload[5] & 0x01)) { 35637 return; 35638 } 35639 35640 // overwrite any existing program map table 35641 self.programMapTable = { 35642 video: null, 35643 audio: null, 35644 'timed-metadata': {} 35645 }; 35646 35647 // the mapping table ends at the end of the current section 35648 sectionLength = (payload[1] & 0x0f) << 8 | payload[2]; 35649 tableEnd = 3 + sectionLength - 4; 35650 35651 // to determine where the table is, we have to figure out how 35652 // long the program info descriptors are 35653 programInfoLength = (payload[10] & 0x0f) << 8 | payload[11]; 35654 35655 // advance the offset to the first entry in the mapping table 35656 offset = 12 + programInfoLength; 35657 while (offset < tableEnd) { 35658 var streamType = payload[offset]; 35659 var pid = (payload[offset + 1] & 0x1F) << 8 | payload[offset + 2]; 35660 35661 // only map a single elementary_pid for audio and video stream types 35662 // TODO: should this be done for metadata too? for now maintain behavior of 35663 // multiple metadata streams 35664 if (streamType === streamTypes.H264_STREAM_TYPE && self.programMapTable.video === null) { 35665 self.programMapTable.video = pid; 35666 } else if (streamType === streamTypes.ADTS_STREAM_TYPE && self.programMapTable.audio === null) { 35667 self.programMapTable.audio = pid; 35668 } else if (streamType === streamTypes.METADATA_STREAM_TYPE) { 35669 // map pid to stream type for metadata streams 35670 self.programMapTable['timed-metadata'][pid] = streamType; 35671 } 35672 35673 // move to the next table entry 35674 // skip past the elementary stream descriptors, if present 35675 offset += ((payload[offset + 3] & 0x0F) << 8 | payload[offset + 4]) + 5; 35676 } 35677 35678 // record the map on the packet as well 35679 pmt.programMapTable = self.programMapTable; 35680 }; 35681 35682 /** 35683 * Deliver a new MP2T packet to the stream. 35684 */ 35685 this.push = function (packet) { 35686 var result = {}, 35687 offset = 4; 35688 35689 result.payloadUnitStartIndicator = !!(packet[1] & 0x40); 35690 35691 // pid is a 13-bit field starting at the last bit of packet[1] 35692 result.pid = packet[1] & 0x1f; 35693 result.pid <<= 8; 35694 result.pid |= packet[2]; 35695 35696 // if an adaption field is present, its length is specified by the 35697 // fifth byte of the TS packet header. The adaptation field is 35698 // used to add stuffing to PES packets that don't fill a complete 35699 // TS packet, and to specify some forms of timing and control data 35700 // that we do not currently use. 35701 if ((packet[3] & 0x30) >>> 4 > 0x01) { 35702 offset += packet[offset] + 1; 35703 } 35704 35705 // parse the rest of the packet based on the type 35706 if (result.pid === 0) { 35707 result.type = 'pat'; 35708 parsePsi(packet.subarray(offset), result); 35709 this.trigger('data', result); 35710 } else if (result.pid === this.pmtPid) { 35711 result.type = 'pmt'; 35712 parsePsi(packet.subarray(offset), result); 35713 this.trigger('data', result); 35714 35715 // if there are any packets waiting for a PMT to be found, process them now 35716 while (this.packetsWaitingForPmt.length) { 35717 this.processPes_.apply(this, this.packetsWaitingForPmt.shift()); 35718 } 35719 } else if (this.programMapTable === undefined) { 35720 // When we have not seen a PMT yet, defer further processing of 35721 // PES packets until one has been parsed 35722 this.packetsWaitingForPmt.push([packet, offset, result]); 35723 } else { 35724 this.processPes_(packet, offset, result); 35725 } 35726 }; 35727 35728 this.processPes_ = function (packet, offset, result) { 35729 // set the appropriate stream type 35730 if (result.pid === this.programMapTable.video) { 35731 result.streamType = streamTypes.H264_STREAM_TYPE; 35732 } else if (result.pid === this.programMapTable.audio) { 35733 result.streamType = streamTypes.ADTS_STREAM_TYPE; 35734 } else { 35735 // if not video or audio, it is timed-metadata or unknown 35736 // if unknown, streamType will be undefined 35737 result.streamType = this.programMapTable['timed-metadata'][result.pid]; 35738 } 35739 35740 result.type = 'pes'; 35741 result.data = packet.subarray(offset); 35742 35743 this.trigger('data', result); 35744 }; 35745 }; 35746 _TransportParseStream.prototype = new stream(); 35747 _TransportParseStream.STREAM_TYPES = { 35748 h264: 0x1b, 35749 adts: 0x0f 35750 }; 35751 35752 /** 35753 * Reconsistutes program elementary stream (PES) packets from parsed 35754 * transport stream packets. That is, if you pipe an 35755 * mp2t.TransportParseStream into a mp2t.ElementaryStream, the output 35756 * events will be events which capture the bytes for individual PES 35757 * packets plus relevant metadata that has been extracted from the 35758 * container. 35759 */ 35760 _ElementaryStream = function ElementaryStream() { 35761 var self = this, 35762 35763 35764 // PES packet fragments 35765 video = { 35766 data: [], 35767 size: 0 35768 }, 35769 audio = { 35770 data: [], 35771 size: 0 35772 }, 35773 timedMetadata = { 35774 data: [], 35775 size: 0 35776 }, 35777 parsePes = function parsePes(payload, pes) { 35778 var ptsDtsFlags; 35779 35780 // get the packet length, this will be 0 for video 35781 pes.packetLength = 6 + (payload[4] << 8 | payload[5]); 35782 35783 // find out if this packets starts a new keyframe 35784 pes.dataAlignmentIndicator = (payload[6] & 0x04) !== 0; 35785 // PES packets may be annotated with a PTS value, or a PTS value 35786 // and a DTS value. Determine what combination of values is 35787 // available to work with. 35788 ptsDtsFlags = payload[7]; 35789 35790 // PTS and DTS are normally stored as a 33-bit number. Javascript 35791 // performs all bitwise operations on 32-bit integers but javascript 35792 // supports a much greater range (52-bits) of integer using standard 35793 // mathematical operations. 35794 // We construct a 31-bit value using bitwise operators over the 31 35795 // most significant bits and then multiply by 4 (equal to a left-shift 35796 // of 2) before we add the final 2 least significant bits of the 35797 // timestamp (equal to an OR.) 35798 if (ptsDtsFlags & 0xC0) { 35799 // the PTS and DTS are not written out directly. For information 35800 // on how they are encoded, see 35801 // http://dvd.sourceforge.net/dvdinfo/pes-hdr.html 35802 pes.pts = (payload[9] & 0x0E) << 27 | (payload[10] & 0xFF) << 20 | (payload[11] & 0xFE) << 12 | (payload[12] & 0xFF) << 5 | (payload[13] & 0xFE) >>> 3; 35803 pes.pts *= 4; // Left shift by 2 35804 pes.pts += (payload[13] & 0x06) >>> 1; // OR by the two LSBs 35805 pes.dts = pes.pts; 35806 if (ptsDtsFlags & 0x40) { 35807 pes.dts = (payload[14] & 0x0E) << 27 | (payload[15] & 0xFF) << 20 | (payload[16] & 0xFE) << 12 | (payload[17] & 0xFF) << 5 | (payload[18] & 0xFE) >>> 3; 35808 pes.dts *= 4; // Left shift by 2 35809 pes.dts += (payload[18] & 0x06) >>> 1; // OR by the two LSBs 35810 } 35811 } 35812 // the data section starts immediately after the PES header. 35813 // pes_header_data_length specifies the number of header bytes 35814 // that follow the last byte of the field. 35815 pes.data = payload.subarray(9 + payload[8]); 35816 }, 35817 flushStream = function flushStream(stream$$1, type, forceFlush) { 35818 var packetData = new Uint8Array(stream$$1.size), 35819 event = { 35820 type: type 35821 }, 35822 i = 0, 35823 offset = 0, 35824 packetFlushable = false, 35825 fragment; 35826 35827 // do nothing if there is not enough buffered data for a complete 35828 // PES header 35829 if (!stream$$1.data.length || stream$$1.size < 9) { 35830 return; 35831 } 35832 event.trackId = stream$$1.data[0].pid; 35833 35834 // reassemble the packet 35835 for (i = 0; i < stream$$1.data.length; i++) { 35836 fragment = stream$$1.data[i]; 35837 35838 packetData.set(fragment.data, offset); 35839 offset += fragment.data.byteLength; 35840 } 35841 35842 // parse assembled packet's PES header 35843 parsePes(packetData, event); 35844 35845 // non-video PES packets MUST have a non-zero PES_packet_length 35846 // check that there is enough stream data to fill the packet 35847 packetFlushable = type === 'video' || event.packetLength <= stream$$1.size; 35848 35849 // flush pending packets if the conditions are right 35850 if (forceFlush || packetFlushable) { 35851 stream$$1.size = 0; 35852 stream$$1.data.length = 0; 35853 } 35854 35855 // only emit packets that are complete. this is to avoid assembling 35856 // incomplete PES packets due to poor segmentation 35857 if (packetFlushable) { 35858 self.trigger('data', event); 35859 } 35860 }; 35861 35862 _ElementaryStream.prototype.init.call(this); 35863 35864 this.push = function (data) { 35865 ({ 35866 pat: function pat() { 35867 // we have to wait for the PMT to arrive as well before we 35868 // have any meaningful metadata 35869 }, 35870 pes: function pes() { 35871 var stream$$1, streamType; 35872 35873 switch (data.streamType) { 35874 case streamTypes.H264_STREAM_TYPE: 35875 case streamTypes.H264_STREAM_TYPE: 35876 stream$$1 = video; 35877 streamType = 'video'; 35878 break; 35879 case streamTypes.ADTS_STREAM_TYPE: 35880 stream$$1 = audio; 35881 streamType = 'audio'; 35882 break; 35883 case streamTypes.METADATA_STREAM_TYPE: 35884 stream$$1 = timedMetadata; 35885 streamType = 'timed-metadata'; 35886 break; 35887 default: 35888 // ignore unknown stream types 35889 return; 35890 } 35891 35892 // if a new packet is starting, we can flush the completed 35893 // packet 35894 if (data.payloadUnitStartIndicator) { 35895 flushStream(stream$$1, streamType, true); 35896 } 35897 35898 // buffer this fragment until we are sure we've received the 35899 // complete payload 35900 stream$$1.data.push(data); 35901 stream$$1.size += data.data.byteLength; 35902 }, 35903 pmt: function pmt() { 35904 var event = { 35905 type: 'metadata', 35906 tracks: [] 35907 }, 35908 programMapTable = data.programMapTable; 35909 35910 // translate audio and video streams to tracks 35911 if (programMapTable.video !== null) { 35912 event.tracks.push({ 35913 timelineStartInfo: { 35914 baseMediaDecodeTime: 0 35915 }, 35916 id: +programMapTable.video, 35917 codec: 'avc', 35918 type: 'video' 35919 }); 35920 } 35921 if (programMapTable.audio !== null) { 35922 event.tracks.push({ 35923 timelineStartInfo: { 35924 baseMediaDecodeTime: 0 35925 }, 35926 id: +programMapTable.audio, 35927 codec: 'adts', 35928 type: 'audio' 35929 }); 35930 } 35931 35932 self.trigger('data', event); 35933 } 35934 })[data.type](); 35935 }; 35936 35937 /** 35938 * Flush any remaining input. Video PES packets may be of variable 35939 * length. Normally, the start of a new video packet can trigger the 35940 * finalization of the previous packet. That is not possible if no 35941 * more video is forthcoming, however. In that case, some other 35942 * mechanism (like the end of the file) has to be employed. When it is 35943 * clear that no additional data is forthcoming, calling this method 35944 * will flush the buffered packets. 35945 */ 35946 this.flush = function () { 35947 // !!THIS ORDER IS IMPORTANT!! 35948 // video first then audio 35949 flushStream(video, 'video'); 35950 flushStream(audio, 'audio'); 35951 flushStream(timedMetadata, 'timed-metadata'); 35952 this.trigger('done'); 35953 }; 35954 }; 35955 _ElementaryStream.prototype = new stream(); 35956 35957 var m2ts = { 35958 PAT_PID: 0x0000, 35959 MP2T_PACKET_LENGTH: MP2T_PACKET_LENGTH, 35960 TransportPacketStream: _TransportPacketStream, 35961 TransportParseStream: _TransportParseStream, 35962 ElementaryStream: _ElementaryStream, 35963 TimestampRolloverStream: TimestampRolloverStream$1, 35964 CaptionStream: captionStream.CaptionStream, 35965 Cea608Stream: captionStream.Cea608Stream, 35966 MetadataStream: metadataStream 35967 }; 35968 35969 for (var type in streamTypes) { 35970 if (streamTypes.hasOwnProperty(type)) { 35971 m2ts[type] = streamTypes[type]; 35972 } 35973 } 35974 35975 var m2ts_1 = m2ts; 35976 35977 var _AdtsStream; 35978 35979 var ADTS_SAMPLING_FREQUENCIES = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350]; 35980 35981 /* 35982 * Accepts a ElementaryStream and emits data events with parsed 35983 * AAC Audio Frames of the individual packets. Input audio in ADTS 35984 * format is unpacked and re-emitted as AAC frames. 35985 * 35986 * @see http://wiki.multimedia.cx/index.php?title=ADTS 35987 * @see http://wiki.multimedia.cx/?title=Understanding_AAC 35988 */ 35989 _AdtsStream = function AdtsStream() { 35990 var buffer; 35991 35992 _AdtsStream.prototype.init.call(this); 35993 35994 this.push = function (packet) { 35995 var i = 0, 35996 frameNum = 0, 35997 frameLength, 35998 protectionSkipBytes, 35999 frameEnd, 36000 oldBuffer, 36001 sampleCount, 36002 adtsFrameDuration; 36003 36004 if (packet.type !== 'audio') { 36005 // ignore non-audio data 36006 return; 36007 } 36008 36009 // Prepend any data in the buffer to the input data so that we can parse 36010 // aac frames the cross a PES packet boundary 36011 if (buffer) { 36012 oldBuffer = buffer; 36013 buffer = new Uint8Array(oldBuffer.byteLength + packet.data.byteLength); 36014 buffer.set(oldBuffer); 36015 buffer.set(packet.data, oldBuffer.byteLength); 36016 } else { 36017 buffer = packet.data; 36018 } 36019 36020 // unpack any ADTS frames which have been fully received 36021 // for details on the ADTS header, see http://wiki.multimedia.cx/index.php?title=ADTS 36022 while (i + 5 < buffer.length) { 36023 36024 // Loook for the start of an ADTS header.. 36025 if (buffer[i] !== 0xFF || (buffer[i + 1] & 0xF6) !== 0xF0) { 36026 // If a valid header was not found, jump one forward and attempt to 36027 // find a valid ADTS header starting at the next byte 36028 i++; 36029 continue; 36030 } 36031 36032 // The protection skip bit tells us if we have 2 bytes of CRC data at the 36033 // end of the ADTS header 36034 protectionSkipBytes = (~buffer[i + 1] & 0x01) * 2; 36035 36036 // Frame length is a 13 bit integer starting 16 bits from the 36037 // end of the sync sequence 36038 frameLength = (buffer[i + 3] & 0x03) << 11 | buffer[i + 4] << 3 | (buffer[i + 5] & 0xe0) >> 5; 36039 36040 sampleCount = ((buffer[i + 6] & 0x03) + 1) * 1024; 36041 adtsFrameDuration = sampleCount * 90000 / ADTS_SAMPLING_FREQUENCIES[(buffer[i + 2] & 0x3c) >>> 2]; 36042 36043 frameEnd = i + frameLength; 36044 36045 // If we don't have enough data to actually finish this ADTS frame, return 36046 // and wait for more data 36047 if (buffer.byteLength < frameEnd) { 36048 return; 36049 } 36050 36051 // Otherwise, deliver the complete AAC frame 36052 this.trigger('data', { 36053 pts: packet.pts + frameNum * adtsFrameDuration, 36054 dts: packet.dts + frameNum * adtsFrameDuration, 36055 sampleCount: sampleCount, 36056 audioobjecttype: (buffer[i + 2] >>> 6 & 0x03) + 1, 36057 channelcount: (buffer[i + 2] & 1) << 2 | (buffer[i + 3] & 0xc0) >>> 6, 36058 samplerate: ADTS_SAMPLING_FREQUENCIES[(buffer[i + 2] & 0x3c) >>> 2], 36059 samplingfrequencyindex: (buffer[i + 2] & 0x3c) >>> 2, 36060 // assume ISO/IEC 14496-12 AudioSampleEntry default of 16 36061 samplesize: 16, 36062 data: buffer.subarray(i + 7 + protectionSkipBytes, frameEnd) 36063 }); 36064 36065 // If the buffer is empty, clear it and return 36066 if (buffer.byteLength === frameEnd) { 36067 buffer = undefined; 36068 return; 36069 } 36070 36071 frameNum++; 36072 36073 // Remove the finished frame from the buffer and start the process again 36074 buffer = buffer.subarray(frameEnd); 36075 } 36076 }; 36077 this.flush = function () { 36078 this.trigger('done'); 36079 }; 36080 }; 36081 36082 _AdtsStream.prototype = new stream(); 36083 36084 var adts = _AdtsStream; 36085 36086 var ExpGolomb; 36087 36088 /** 36089 * Parser for exponential Golomb codes, a variable-bitwidth number encoding 36090 * scheme used by h264. 36091 */ 36092 ExpGolomb = function ExpGolomb(workingData) { 36093 var 36094 // the number of bytes left to examine in workingData 36095 workingBytesAvailable = workingData.byteLength, 36096 36097 36098 // the current word being examined 36099 workingWord = 0, 36100 36101 36102 // :uint 36103 36104 // the number of bits left to examine in the current word 36105 workingBitsAvailable = 0; // :uint; 36106 36107 // ():uint 36108 this.length = function () { 36109 return 8 * workingBytesAvailable; 36110 }; 36111 36112 // ():uint 36113 this.bitsAvailable = function () { 36114 return 8 * workingBytesAvailable + workingBitsAvailable; 36115 }; 36116 36117 // ():void 36118 this.loadWord = function () { 36119 var position = workingData.byteLength - workingBytesAvailable, 36120 workingBytes = new Uint8Array(4), 36121 availableBytes = Math.min(4, workingBytesAvailable); 36122 36123 if (availableBytes === 0) { 36124 throw new Error('no bytes available'); 36125 } 36126 36127 workingBytes.set(workingData.subarray(position, position + availableBytes)); 36128 workingWord = new DataView(workingBytes.buffer).getUint32(0); 36129 36130 // track the amount of workingData that has been processed 36131 workingBitsAvailable = availableBytes * 8; 36132 workingBytesAvailable -= availableBytes; 36133 }; 36134 36135 // (count:int):void 36136 this.skipBits = function (count) { 36137 var skipBytes; // :int 36138 if (workingBitsAvailable > count) { 36139 workingWord <<= count; 36140 workingBitsAvailable -= count; 36141 } else { 36142 count -= workingBitsAvailable; 36143 skipBytes = Math.floor(count / 8); 36144 36145 count -= skipBytes * 8; 36146 workingBytesAvailable -= skipBytes; 36147 36148 this.loadWord(); 36149 36150 workingWord <<= count; 36151 workingBitsAvailable -= count; 36152 } 36153 }; 36154 36155 // (size:int):uint 36156 this.readBits = function (size) { 36157 var bits = Math.min(workingBitsAvailable, size), 36158 36159 36160 // :uint 36161 valu = workingWord >>> 32 - bits; // :uint 36162 // if size > 31, handle error 36163 workingBitsAvailable -= bits; 36164 if (workingBitsAvailable > 0) { 36165 workingWord <<= bits; 36166 } else if (workingBytesAvailable > 0) { 36167 this.loadWord(); 36168 } 36169 36170 bits = size - bits; 36171 if (bits > 0) { 36172 return valu << bits | this.readBits(bits); 36173 } 36174 return valu; 36175 }; 36176 36177 // ():uint 36178 this.skipLeadingZeros = function () { 36179 var leadingZeroCount; // :uint 36180 for (leadingZeroCount = 0; leadingZeroCount < workingBitsAvailable; ++leadingZeroCount) { 36181 if ((workingWord & 0x80000000 >>> leadingZeroCount) !== 0) { 36182 // the first bit of working word is 1 36183 workingWord <<= leadingZeroCount; 36184 workingBitsAvailable -= leadingZeroCount; 36185 return leadingZeroCount; 36186 } 36187 } 36188 36189 // we exhausted workingWord and still have not found a 1 36190 this.loadWord(); 36191 return leadingZeroCount + this.skipLeadingZeros(); 36192 }; 36193 36194 // ():void 36195 this.skipUnsignedExpGolomb = function () { 36196 this.skipBits(1 + this.skipLeadingZeros()); 36197 }; 36198 36199 // ():void 36200 this.skipExpGolomb = function () { 36201 this.skipBits(1 + this.skipLeadingZeros()); 36202 }; 36203 36204 // ():uint 36205 this.readUnsignedExpGolomb = function () { 36206 var clz = this.skipLeadingZeros(); // :uint 36207 return this.readBits(clz + 1) - 1; 36208 }; 36209 36210 // ():int 36211 this.readExpGolomb = function () { 36212 var valu = this.readUnsignedExpGolomb(); // :int 36213 if (0x01 & valu) { 36214 // the number is odd if the low order bit is set 36215 return 1 + valu >>> 1; // add 1 to make it even, and divide by 2 36216 } 36217 return -1 * (valu >>> 1); // divide by two then make it negative 36218 }; 36219 36220 // Some convenience functions 36221 // :Boolean 36222 this.readBoolean = function () { 36223 return this.readBits(1) === 1; 36224 }; 36225 36226 // ():int 36227 this.readUnsignedByte = function () { 36228 return this.readBits(8); 36229 }; 36230 36231 this.loadWord(); 36232 }; 36233 36234 var expGolomb = ExpGolomb; 36235 36236 var _H264Stream, _NalByteStream; 36237 var PROFILES_WITH_OPTIONAL_SPS_DATA; 36238 36239 /** 36240 * Accepts a NAL unit byte stream and unpacks the embedded NAL units. 36241 */ 36242 _NalByteStream = function NalByteStream() { 36243 var syncPoint = 0, 36244 i, 36245 buffer; 36246 _NalByteStream.prototype.init.call(this); 36247 36248 this.push = function (data) { 36249 var swapBuffer; 36250 36251 if (!buffer) { 36252 buffer = data.data; 36253 } else { 36254 swapBuffer = new Uint8Array(buffer.byteLength + data.data.byteLength); 36255 swapBuffer.set(buffer); 36256 swapBuffer.set(data.data, buffer.byteLength); 36257 buffer = swapBuffer; 36258 } 36259 36260 // Rec. ITU-T H.264, Annex B 36261 // scan for NAL unit boundaries 36262 36263 // a match looks like this: 36264 // 0 0 1 .. NAL .. 0 0 1 36265 // ^ sync point ^ i 36266 // or this: 36267 // 0 0 1 .. NAL .. 0 0 0 36268 // ^ sync point ^ i 36269 36270 // advance the sync point to a NAL start, if necessary 36271 for (; syncPoint < buffer.byteLength - 3; syncPoint++) { 36272 if (buffer[syncPoint + 2] === 1) { 36273 // the sync point is properly aligned 36274 i = syncPoint + 5; 36275 break; 36276 } 36277 } 36278 36279 while (i < buffer.byteLength) { 36280 // look at the current byte to determine if we've hit the end of 36281 // a NAL unit boundary 36282 switch (buffer[i]) { 36283 case 0: 36284 // skip past non-sync sequences 36285 if (buffer[i - 1] !== 0) { 36286 i += 2; 36287 break; 36288 } else if (buffer[i - 2] !== 0) { 36289 i++; 36290 break; 36291 } 36292 36293 // deliver the NAL unit if it isn't empty 36294 if (syncPoint + 3 !== i - 2) { 36295 this.trigger('data', buffer.subarray(syncPoint + 3, i - 2)); 36296 } 36297 36298 // drop trailing zeroes 36299 do { 36300 i++; 36301 } while (buffer[i] !== 1 && i < buffer.length); 36302 syncPoint = i - 2; 36303 i += 3; 36304 break; 36305 case 1: 36306 // skip past non-sync sequences 36307 if (buffer[i - 1] !== 0 || buffer[i - 2] !== 0) { 36308 i += 3; 36309 break; 36310 } 36311 36312 // deliver the NAL unit 36313 this.trigger('data', buffer.subarray(syncPoint + 3, i - 2)); 36314 syncPoint = i - 2; 36315 i += 3; 36316 break; 36317 default: 36318 // the current byte isn't a one or zero, so it cannot be part 36319 // of a sync sequence 36320 i += 3; 36321 break; 36322 } 36323 } 36324 // filter out the NAL units that were delivered 36325 buffer = buffer.subarray(syncPoint); 36326 i -= syncPoint; 36327 syncPoint = 0; 36328 }; 36329 36330 this.flush = function () { 36331 // deliver the last buffered NAL unit 36332 if (buffer && buffer.byteLength > 3) { 36333 this.trigger('data', buffer.subarray(syncPoint + 3)); 36334 } 36335 // reset the stream state 36336 buffer = null; 36337 syncPoint = 0; 36338 this.trigger('done'); 36339 }; 36340 }; 36341 _NalByteStream.prototype = new stream(); 36342 36343 // values of profile_idc that indicate additional fields are included in the SPS 36344 // see Recommendation ITU-T H.264 (4/2013), 36345 // 7.3.2.1.1 Sequence parameter set data syntax 36346 PROFILES_WITH_OPTIONAL_SPS_DATA = { 36347 100: true, 36348 110: true, 36349 122: true, 36350 244: true, 36351 44: true, 36352 83: true, 36353 86: true, 36354 118: true, 36355 128: true, 36356 138: true, 36357 139: true, 36358 134: true 36359 }; 36360 36361 /** 36362 * Accepts input from a ElementaryStream and produces H.264 NAL unit data 36363 * events. 36364 */ 36365 _H264Stream = function H264Stream() { 36366 var nalByteStream = new _NalByteStream(), 36367 self, 36368 trackId, 36369 currentPts, 36370 currentDts, 36371 discardEmulationPreventionBytes, 36372 readSequenceParameterSet, 36373 skipScalingList; 36374 36375 _H264Stream.prototype.init.call(this); 36376 self = this; 36377 36378 this.push = function (packet) { 36379 if (packet.type !== 'video') { 36380 return; 36381 } 36382 trackId = packet.trackId; 36383 currentPts = packet.pts; 36384 currentDts = packet.dts; 36385 36386 nalByteStream.push(packet); 36387 }; 36388 36389 nalByteStream.on('data', function (data) { 36390 var event = { 36391 trackId: trackId, 36392 pts: currentPts, 36393 dts: currentDts, 36394 data: data 36395 }; 36396 36397 switch (data[0] & 0x1f) { 36398 case 0x05: 36399 event.nalUnitType = 'slice_layer_without_partitioning_rbsp_idr'; 36400 break; 36401 case 0x06: 36402 event.nalUnitType = 'sei_rbsp'; 36403 event.escapedRBSP = discardEmulationPreventionBytes(data.subarray(1)); 36404 break; 36405 case 0x07: 36406 event.nalUnitType = 'seq_parameter_set_rbsp'; 36407 event.escapedRBSP = discardEmulationPreventionBytes(data.subarray(1)); 36408 event.config = readSequenceParameterSet(event.escapedRBSP); 36409 break; 36410 case 0x08: 36411 event.nalUnitType = 'pic_parameter_set_rbsp'; 36412 break; 36413 case 0x09: 36414 event.nalUnitType = 'access_unit_delimiter_rbsp'; 36415 break; 36416 36417 default: 36418 break; 36419 } 36420 self.trigger('data', event); 36421 }); 36422 nalByteStream.on('done', function () { 36423 self.trigger('done'); 36424 }); 36425 36426 this.flush = function () { 36427 nalByteStream.flush(); 36428 }; 36429 36430 /** 36431 * Advance the ExpGolomb decoder past a scaling list. The scaling 36432 * list is optionally transmitted as part of a sequence parameter 36433 * set and is not relevant to transmuxing. 36434 * @param count {number} the number of entries in this scaling list 36435 * @param expGolombDecoder {object} an ExpGolomb pointed to the 36436 * start of a scaling list 36437 * @see Recommendation ITU-T H.264, Section 7.3.2.1.1.1 36438 */ 36439 skipScalingList = function skipScalingList(count, expGolombDecoder) { 36440 var lastScale = 8, 36441 nextScale = 8, 36442 j, 36443 deltaScale; 36444 36445 for (j = 0; j < count; j++) { 36446 if (nextScale !== 0) { 36447 deltaScale = expGolombDecoder.readExpGolomb(); 36448 nextScale = (lastScale + deltaScale + 256) % 256; 36449 } 36450 36451 lastScale = nextScale === 0 ? lastScale : nextScale; 36452 } 36453 }; 36454 36455 /** 36456 * Expunge any "Emulation Prevention" bytes from a "Raw Byte 36457 * Sequence Payload" 36458 * @param data {Uint8Array} the bytes of a RBSP from a NAL 36459 * unit 36460 * @return {Uint8Array} the RBSP without any Emulation 36461 * Prevention Bytes 36462 */ 36463 discardEmulationPreventionBytes = function discardEmulationPreventionBytes(data) { 36464 var length = data.byteLength, 36465 emulationPreventionBytesPositions = [], 36466 i = 1, 36467 newLength, 36468 newData; 36469 36470 // Find all `Emulation Prevention Bytes` 36471 while (i < length - 2) { 36472 if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0x03) { 36473 emulationPreventionBytesPositions.push(i + 2); 36474 i += 2; 36475 } else { 36476 i++; 36477 } 36478 } 36479 36480 // If no Emulation Prevention Bytes were found just return the original 36481 // array 36482 if (emulationPreventionBytesPositions.length === 0) { 36483 return data; 36484 } 36485 36486 // Create a new array to hold the NAL unit data 36487 newLength = length - emulationPreventionBytesPositions.length; 36488 newData = new Uint8Array(newLength); 36489 var sourceIndex = 0; 36490 36491 for (i = 0; i < newLength; sourceIndex++, i++) { 36492 if (sourceIndex === emulationPreventionBytesPositions[0]) { 36493 // Skip this byte 36494 sourceIndex++; 36495 // Remove this position index 36496 emulationPreventionBytesPositions.shift(); 36497 } 36498 newData[i] = data[sourceIndex]; 36499 } 36500 36501 return newData; 36502 }; 36503 36504 /** 36505 * Read a sequence parameter set and return some interesting video 36506 * properties. A sequence parameter set is the H264 metadata that 36507 * describes the properties of upcoming video frames. 36508 * @param data {Uint8Array} the bytes of a sequence parameter set 36509 * @return {object} an object with configuration parsed from the 36510 * sequence parameter set, including the dimensions of the 36511 * associated video frames. 36512 */ 36513 readSequenceParameterSet = function readSequenceParameterSet(data) { 36514 var frameCropLeftOffset = 0, 36515 frameCropRightOffset = 0, 36516 frameCropTopOffset = 0, 36517 frameCropBottomOffset = 0, 36518 sarScale = 1, 36519 expGolombDecoder, 36520 profileIdc, 36521 levelIdc, 36522 profileCompatibility, 36523 chromaFormatIdc, 36524 picOrderCntType, 36525 numRefFramesInPicOrderCntCycle, 36526 picWidthInMbsMinus1, 36527 picHeightInMapUnitsMinus1, 36528 frameMbsOnlyFlag, 36529 scalingListCount, 36530 sarRatio, 36531 aspectRatioIdc, 36532 i; 36533 36534 expGolombDecoder = new expGolomb(data); 36535 profileIdc = expGolombDecoder.readUnsignedByte(); // profile_idc 36536 profileCompatibility = expGolombDecoder.readUnsignedByte(); // constraint_set[0-5]_flag 36537 levelIdc = expGolombDecoder.readUnsignedByte(); // level_idc u(8) 36538 expGolombDecoder.skipUnsignedExpGolomb(); // seq_parameter_set_id 36539 36540 // some profiles have more optional data we don't need 36541 if (PROFILES_WITH_OPTIONAL_SPS_DATA[profileIdc]) { 36542 chromaFormatIdc = expGolombDecoder.readUnsignedExpGolomb(); 36543 if (chromaFormatIdc === 3) { 36544 expGolombDecoder.skipBits(1); // separate_colour_plane_flag 36545 } 36546 expGolombDecoder.skipUnsignedExpGolomb(); // bit_depth_luma_minus8 36547 expGolombDecoder.skipUnsignedExpGolomb(); // bit_depth_chroma_minus8 36548 expGolombDecoder.skipBits(1); // qpprime_y_zero_transform_bypass_flag 36549 if (expGolombDecoder.readBoolean()) { 36550 // seq_scaling_matrix_present_flag 36551 scalingListCount = chromaFormatIdc !== 3 ? 8 : 12; 36552 for (i = 0; i < scalingListCount; i++) { 36553 if (expGolombDecoder.readBoolean()) { 36554 // seq_scaling_list_present_flag[ i ] 36555 if (i < 6) { 36556 skipScalingList(16, expGolombDecoder); 36557 } else { 36558 skipScalingList(64, expGolombDecoder); 36559 } 36560 } 36561 } 36562 } 36563 } 36564 36565 expGolombDecoder.skipUnsignedExpGolomb(); // log2_max_frame_num_minus4 36566 picOrderCntType = expGolombDecoder.readUnsignedExpGolomb(); 36567 36568 if (picOrderCntType === 0) { 36569 expGolombDecoder.readUnsignedExpGolomb(); // log2_max_pic_order_cnt_lsb_minus4 36570 } else if (picOrderCntType === 1) { 36571 expGolombDecoder.skipBits(1); // delta_pic_order_always_zero_flag 36572 expGolombDecoder.skipExpGolomb(); // offset_for_non_ref_pic 36573 expGolombDecoder.skipExpGolomb(); // offset_for_top_to_bottom_field 36574 numRefFramesInPicOrderCntCycle = expGolombDecoder.readUnsignedExpGolomb(); 36575 for (i = 0; i < numRefFramesInPicOrderCntCycle; i++) { 36576 expGolombDecoder.skipExpGolomb(); // offset_for_ref_frame[ i ] 36577 } 36578 } 36579 36580 expGolombDecoder.skipUnsignedExpGolomb(); // max_num_ref_frames 36581 expGolombDecoder.skipBits(1); // gaps_in_frame_num_value_allowed_flag 36582 36583 picWidthInMbsMinus1 = expGolombDecoder.readUnsignedExpGolomb(); 36584 picHeightInMapUnitsMinus1 = expGolombDecoder.readUnsignedExpGolomb(); 36585 36586 frameMbsOnlyFlag = expGolombDecoder.readBits(1); 36587 if (frameMbsOnlyFlag === 0) { 36588 expGolombDecoder.skipBits(1); // mb_adaptive_frame_field_flag 36589 } 36590 36591 expGolombDecoder.skipBits(1); // direct_8x8_inference_flag 36592 if (expGolombDecoder.readBoolean()) { 36593 // frame_cropping_flag 36594 frameCropLeftOffset = expGolombDecoder.readUnsignedExpGolomb(); 36595 frameCropRightOffset = expGolombDecoder.readUnsignedExpGolomb(); 36596 frameCropTopOffset = expGolombDecoder.readUnsignedExpGolomb(); 36597 frameCropBottomOffset = expGolombDecoder.readUnsignedExpGolomb(); 36598 } 36599 if (expGolombDecoder.readBoolean()) { 36600 // vui_parameters_present_flag 36601 if (expGolombDecoder.readBoolean()) { 36602 // aspect_ratio_info_present_flag 36603 aspectRatioIdc = expGolombDecoder.readUnsignedByte(); 36604 switch (aspectRatioIdc) { 36605 case 1: 36606 sarRatio = [1, 1];break; 36607 case 2: 36608 sarRatio = [12, 11];break; 36609 case 3: 36610 sarRatio = [10, 11];break; 36611 case 4: 36612 sarRatio = [16, 11];break; 36613 case 5: 36614 sarRatio = [40, 33];break; 36615 case 6: 36616 sarRatio = [24, 11];break; 36617 case 7: 36618 sarRatio = [20, 11];break; 36619 case 8: 36620 sarRatio = [32, 11];break; 36621 case 9: 36622 sarRatio = [80, 33];break; 36623 case 10: 36624 sarRatio = [18, 11];break; 36625 case 11: 36626 sarRatio = [15, 11];break; 36627 case 12: 36628 sarRatio = [64, 33];break; 36629 case 13: 36630 sarRatio = [160, 99];break; 36631 case 14: 36632 sarRatio = [4, 3];break; 36633 case 15: 36634 sarRatio = [3, 2];break; 36635 case 16: 36636 sarRatio = [2, 1];break; 36637 case 255: 36638 { 36639 sarRatio = [expGolombDecoder.readUnsignedByte() << 8 | expGolombDecoder.readUnsignedByte(), expGolombDecoder.readUnsignedByte() << 8 | expGolombDecoder.readUnsignedByte()]; 36640 break; 36641 } 36642 } 36643 if (sarRatio) { 36644 sarScale = sarRatio[0] / sarRatio[1]; 36645 } 36646 } 36647 } 36648 return { 36649 profileIdc: profileIdc, 36650 levelIdc: levelIdc, 36651 profileCompatibility: profileCompatibility, 36652 width: Math.ceil(((picWidthInMbsMinus1 + 1) * 16 - frameCropLeftOffset * 2 - frameCropRightOffset * 2) * sarScale), 36653 height: (2 - frameMbsOnlyFlag) * (picHeightInMapUnitsMinus1 + 1) * 16 - frameCropTopOffset * 2 - frameCropBottomOffset * 2 36654 }; 36655 }; 36656 }; 36657 _H264Stream.prototype = new stream(); 36658 36659 var h264 = { 36660 H264Stream: _H264Stream, 36661 NalByteStream: _NalByteStream 36662 }; 36663 36664 // Constants 36665 var _AacStream; 36666 36667 /** 36668 * Splits an incoming stream of binary data into ADTS and ID3 Frames. 36669 */ 36670 36671 _AacStream = function AacStream() { 36672 var everything = new Uint8Array(), 36673 timeStamp = 0; 36674 36675 _AacStream.prototype.init.call(this); 36676 36677 this.setTimestamp = function (timestamp) { 36678 timeStamp = timestamp; 36679 }; 36680 36681 this.parseId3TagSize = function (header, byteIndex) { 36682 var returnSize = header[byteIndex + 6] << 21 | header[byteIndex + 7] << 14 | header[byteIndex + 8] << 7 | header[byteIndex + 9], 36683 flags = header[byteIndex + 5], 36684 footerPresent = (flags & 16) >> 4; 36685 36686 if (footerPresent) { 36687 return returnSize + 20; 36688 } 36689 return returnSize + 10; 36690 }; 36691 36692 this.parseAdtsSize = function (header, byteIndex) { 36693 var lowThree = (header[byteIndex + 5] & 0xE0) >> 5, 36694 middle = header[byteIndex + 4] << 3, 36695 highTwo = header[byteIndex + 3] & 0x3 << 11; 36696 36697 return highTwo | middle | lowThree; 36698 }; 36699 36700 this.push = function (bytes) { 36701 var frameSize = 0, 36702 byteIndex = 0, 36703 bytesLeft, 36704 chunk, 36705 packet, 36706 tempLength; 36707 36708 // If there are bytes remaining from the last segment, prepend them to the 36709 // bytes that were pushed in 36710 if (everything.length) { 36711 tempLength = everything.length; 36712 everything = new Uint8Array(bytes.byteLength + tempLength); 36713 everything.set(everything.subarray(0, tempLength)); 36714 everything.set(bytes, tempLength); 36715 } else { 36716 everything = bytes; 36717 } 36718 36719 while (everything.length - byteIndex >= 3) { 36720 if (everything[byteIndex] === 'I'.charCodeAt(0) && everything[byteIndex + 1] === 'D'.charCodeAt(0) && everything[byteIndex + 2] === '3'.charCodeAt(0)) { 36721 36722 // Exit early because we don't have enough to parse 36723 // the ID3 tag header 36724 if (everything.length - byteIndex < 10) { 36725 break; 36726 } 36727 36728 // check framesize 36729 frameSize = this.parseId3TagSize(everything, byteIndex); 36730 36731 // Exit early if we don't have enough in the buffer 36732 // to emit a full packet 36733 if (frameSize > everything.length) { 36734 break; 36735 } 36736 chunk = { 36737 type: 'timed-metadata', 36738 data: everything.subarray(byteIndex, byteIndex + frameSize) 36739 }; 36740 this.trigger('data', chunk); 36741 byteIndex += frameSize; 36742 continue; 36743 } else if (everything[byteIndex] & 0xff === 0xff && (everything[byteIndex + 1] & 0xf0) === 0xf0) { 36744 36745 // Exit early because we don't have enough to parse 36746 // the ADTS frame header 36747 if (everything.length - byteIndex < 7) { 36748 break; 36749 } 36750 36751 frameSize = this.parseAdtsSize(everything, byteIndex); 36752 36753 // Exit early if we don't have enough in the buffer 36754 // to emit a full packet 36755 if (frameSize > everything.length) { 36756 break; 36757 } 36758 36759 packet = { 36760 type: 'audio', 36761 data: everything.subarray(byteIndex, byteIndex + frameSize), 36762 pts: timeStamp, 36763 dts: timeStamp 36764 }; 36765 this.trigger('data', packet); 36766 byteIndex += frameSize; 36767 continue; 36768 } 36769 byteIndex++; 36770 } 36771 bytesLeft = everything.length - byteIndex; 36772 36773 if (bytesLeft > 0) { 36774 everything = everything.subarray(byteIndex); 36775 } else { 36776 everything = new Uint8Array(); 36777 } 36778 }; 36779 }; 36780 36781 _AacStream.prototype = new stream(); 36782 36783 var aac = _AacStream; 36784 36785 var highPrefix = [33, 16, 5, 32, 164, 27]; 36786 var lowPrefix = [33, 65, 108, 84, 1, 2, 4, 8, 168, 2, 4, 8, 17, 191, 252]; 36787 var zeroFill = function zeroFill(count) { 36788 var a = []; 36789 while (count--) { 36790 a.push(0); 36791 } 36792 return a; 36793 }; 36794 36795 var makeTable = function makeTable(metaTable) { 36796 return Object.keys(metaTable).reduce(function (obj, key) { 36797 obj[key] = new Uint8Array(metaTable[key].reduce(function (arr, part) { 36798 return arr.concat(part); 36799 }, [])); 36800 return obj; 36801 }, {}); 36802 }; 36803 36804 // Frames-of-silence to use for filling in missing AAC frames 36805 var coneOfSilence = { 36806 96000: [highPrefix, [227, 64], zeroFill(154), [56]], 36807 88200: [highPrefix, [231], zeroFill(170), [56]], 36808 64000: [highPrefix, [248, 192], zeroFill(240), [56]], 36809 48000: [highPrefix, [255, 192], zeroFill(268), [55, 148, 128], zeroFill(54), [112]], 36810 44100: [highPrefix, [255, 192], zeroFill(268), [55, 163, 128], zeroFill(84), [112]], 36811 32000: [highPrefix, [255, 192], zeroFill(268), [55, 234], zeroFill(226), [112]], 36812 24000: [highPrefix, [255, 192], zeroFill(268), [55, 255, 128], zeroFill(268), [111, 112], zeroFill(126), [224]], 36813 16000: [highPrefix, [255, 192], zeroFill(268), [55, 255, 128], zeroFill(268), [111, 255], zeroFill(269), [223, 108], zeroFill(195), [1, 192]], 36814 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]], 36815 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]], 36816 8000: [lowPrefix, zeroFill(268), [3, 121, 16], zeroFill(47), [7]] 36817 }; 36818 36819 var silence = makeTable(coneOfSilence); 36820 36821 var ONE_SECOND_IN_TS = 90000, 36822 36823 36824 // 90kHz clock 36825 secondsToVideoTs, 36826 secondsToAudioTs, 36827 videoTsToSeconds, 36828 audioTsToSeconds, 36829 audioTsToVideoTs, 36830 videoTsToAudioTs; 36831 36832 secondsToVideoTs = function secondsToVideoTs(seconds) { 36833 return seconds * ONE_SECOND_IN_TS; 36834 }; 36835 36836 secondsToAudioTs = function secondsToAudioTs(seconds, sampleRate) { 36837 return seconds * sampleRate; 36838 }; 36839 36840 videoTsToSeconds = function videoTsToSeconds(timestamp) { 36841 return timestamp / ONE_SECOND_IN_TS; 36842 }; 36843 36844 audioTsToSeconds = function audioTsToSeconds(timestamp, sampleRate) { 36845 return timestamp / sampleRate; 36846 }; 36847 36848 audioTsToVideoTs = function audioTsToVideoTs(timestamp, sampleRate) { 36849 return secondsToVideoTs(audioTsToSeconds(timestamp, sampleRate)); 36850 }; 36851 36852 videoTsToAudioTs = function videoTsToAudioTs(timestamp, sampleRate) { 36853 return secondsToAudioTs(videoTsToSeconds(timestamp), sampleRate); 36854 }; 36855 36856 var clock = { 36857 secondsToVideoTs: secondsToVideoTs, 36858 secondsToAudioTs: secondsToAudioTs, 36859 videoTsToSeconds: videoTsToSeconds, 36860 audioTsToSeconds: audioTsToSeconds, 36861 audioTsToVideoTs: audioTsToVideoTs, 36862 videoTsToAudioTs: videoTsToAudioTs 36863 }; 36864 36865 var H264Stream = h264.H264Stream; 36866 36867 // constants 36868 var AUDIO_PROPERTIES = ['audioobjecttype', 'channelcount', 'samplerate', 'samplingfrequencyindex', 'samplesize']; 36869 36870 var VIDEO_PROPERTIES = ['width', 'height', 'profileIdc', 'levelIdc', 'profileCompatibility']; 36871 36872 var ONE_SECOND_IN_TS$1 = 90000; // 90kHz clock 36873 36874 // object types 36875 var _VideoSegmentStream, _AudioSegmentStream, _Transmuxer, _CoalesceStream; 36876 36877 // Helper functions 36878 var createDefaultSample, isLikelyAacData, collectDtsInfo, clearDtsInfo, calculateTrackBaseMediaDecodeTime, arrayEquals, sumFrameByteLengths; 36879 36880 /** 36881 * Default sample object 36882 * see ISO/IEC 14496-12:2012, section 8.6.4.3 36883 */ 36884 createDefaultSample = function createDefaultSample() { 36885 return { 36886 size: 0, 36887 flags: { 36888 isLeading: 0, 36889 dependsOn: 1, 36890 isDependedOn: 0, 36891 hasRedundancy: 0, 36892 degradationPriority: 0 36893 } 36894 }; 36895 }; 36896 36897 isLikelyAacData = function isLikelyAacData(data) { 36898 if (data[0] === 'I'.charCodeAt(0) && data[1] === 'D'.charCodeAt(0) && data[2] === '3'.charCodeAt(0)) { 36899 return true; 36900 } 36901 return false; 36902 }; 36903 36904 /** 36905 * Compare two arrays (even typed) for same-ness 36906 */ 36907 arrayEquals = function arrayEquals(a, b) { 36908 var i; 36909 36910 if (a.length !== b.length) { 36911 return false; 36912 } 36913 36914 // compare the value of each element in the array 36915 for (i = 0; i < a.length; i++) { 36916 if (a[i] !== b[i]) { 36917 return false; 36918 } 36919 } 36920 36921 return true; 36922 }; 36923 36924 /** 36925 * Sum the `byteLength` properties of the data in each AAC frame 36926 */ 36927 sumFrameByteLengths = function sumFrameByteLengths(array) { 36928 var i, 36929 currentObj, 36930 sum = 0; 36931 36932 // sum the byteLength's all each nal unit in the frame 36933 for (i = 0; i < array.length; i++) { 36934 currentObj = array[i]; 36935 sum += currentObj.data.byteLength; 36936 } 36937 36938 return sum; 36939 }; 36940 36941 /** 36942 * Constructs a single-track, ISO BMFF media segment from AAC data 36943 * events. The output of this stream can be fed to a SourceBuffer 36944 * configured with a suitable initialization segment. 36945 * @param track {object} track metadata configuration 36946 * @param options {object} transmuxer options object 36947 * @param options.keepOriginalTimestamps {boolean} If true, keep the timestamps 36948 * in the source; false to adjust the first segment to start at 0. 36949 */ 36950 _AudioSegmentStream = function AudioSegmentStream(track, options) { 36951 var adtsFrames = [], 36952 sequenceNumber = 0, 36953 earliestAllowedDts = 0, 36954 audioAppendStartTs = 0, 36955 videoBaseMediaDecodeTime = Infinity; 36956 36957 options = options || {}; 36958 36959 _AudioSegmentStream.prototype.init.call(this); 36960 36961 this.push = function (data) { 36962 collectDtsInfo(track, data); 36963 36964 if (track) { 36965 AUDIO_PROPERTIES.forEach(function (prop) { 36966 track[prop] = data[prop]; 36967 }); 36968 } 36969 36970 // buffer audio data until end() is called 36971 adtsFrames.push(data); 36972 }; 36973 36974 this.setEarliestDts = function (earliestDts) { 36975 earliestAllowedDts = earliestDts - track.timelineStartInfo.baseMediaDecodeTime; 36976 }; 36977 36978 this.setVideoBaseMediaDecodeTime = function (baseMediaDecodeTime) { 36979 videoBaseMediaDecodeTime = baseMediaDecodeTime; 36980 }; 36981 36982 this.setAudioAppendStart = function (timestamp) { 36983 audioAppendStartTs = timestamp; 36984 }; 36985 36986 this.flush = function () { 36987 var frames, moof, mdat, boxes; 36988 36989 // return early if no audio data has been observed 36990 if (adtsFrames.length === 0) { 36991 this.trigger('done', 'AudioSegmentStream'); 36992 return; 36993 } 36994 36995 frames = this.trimAdtsFramesByEarliestDts_(adtsFrames); 36996 track.baseMediaDecodeTime = calculateTrackBaseMediaDecodeTime(track, options.keepOriginalTimestamps); 36997 36998 this.prefixWithSilence_(track, frames); 36999 37000 // we have to build the index from byte locations to 37001 // samples (that is, adts frames) in the audio data 37002 track.samples = this.generateSampleTable_(frames); 37003 37004 // concatenate the audio data to constuct the mdat 37005 mdat = mp4Generator.mdat(this.concatenateFrameData_(frames)); 37006 37007 adtsFrames = []; 37008 37009 moof = mp4Generator.moof(sequenceNumber, [track]); 37010 boxes = new Uint8Array(moof.byteLength + mdat.byteLength); 37011 37012 // bump the sequence number for next time 37013 sequenceNumber++; 37014 37015 boxes.set(moof); 37016 boxes.set(mdat, moof.byteLength); 37017 37018 clearDtsInfo(track); 37019 37020 this.trigger('data', { track: track, boxes: boxes }); 37021 this.trigger('done', 'AudioSegmentStream'); 37022 }; 37023 37024 // Possibly pad (prefix) the audio track with silence if appending this track 37025 // would lead to the introduction of a gap in the audio buffer 37026 this.prefixWithSilence_ = function (track, frames) { 37027 var baseMediaDecodeTimeTs, 37028 frameDuration = 0, 37029 audioGapDuration = 0, 37030 audioFillFrameCount = 0, 37031 audioFillDuration = 0, 37032 silentFrame, 37033 i; 37034 37035 if (!frames.length) { 37036 return; 37037 } 37038 37039 baseMediaDecodeTimeTs = clock.audioTsToVideoTs(track.baseMediaDecodeTime, track.samplerate); 37040 // determine frame clock duration based on sample rate, round up to avoid overfills 37041 frameDuration = Math.ceil(ONE_SECOND_IN_TS$1 / (track.samplerate / 1024)); 37042 37043 if (audioAppendStartTs && videoBaseMediaDecodeTime) { 37044 // insert the shortest possible amount (audio gap or audio to video gap) 37045 audioGapDuration = baseMediaDecodeTimeTs - Math.max(audioAppendStartTs, videoBaseMediaDecodeTime); 37046 // number of full frames in the audio gap 37047 audioFillFrameCount = Math.floor(audioGapDuration / frameDuration); 37048 audioFillDuration = audioFillFrameCount * frameDuration; 37049 } 37050 37051 // don't attempt to fill gaps smaller than a single frame or larger 37052 // than a half second 37053 if (audioFillFrameCount < 1 || audioFillDuration > ONE_SECOND_IN_TS$1 / 2) { 37054 return; 37055 } 37056 37057 silentFrame = silence[track.samplerate]; 37058 37059 if (!silentFrame) { 37060 // we don't have a silent frame pregenerated for the sample rate, so use a frame 37061 // from the content instead 37062 silentFrame = frames[0].data; 37063 } 37064 37065 for (i = 0; i < audioFillFrameCount; i++) { 37066 frames.splice(i, 0, { 37067 data: silentFrame 37068 }); 37069 } 37070 37071 track.baseMediaDecodeTime -= Math.floor(clock.videoTsToAudioTs(audioFillDuration, track.samplerate)); 37072 }; 37073 37074 // If the audio segment extends before the earliest allowed dts 37075 // value, remove AAC frames until starts at or after the earliest 37076 // allowed DTS so that we don't end up with a negative baseMedia- 37077 // DecodeTime for the audio track 37078 this.trimAdtsFramesByEarliestDts_ = function (adtsFrames) { 37079 if (track.minSegmentDts >= earliestAllowedDts) { 37080 return adtsFrames; 37081 } 37082 37083 // We will need to recalculate the earliest segment Dts 37084 track.minSegmentDts = Infinity; 37085 37086 return adtsFrames.filter(function (currentFrame) { 37087 // If this is an allowed frame, keep it and record it's Dts 37088 if (currentFrame.dts >= earliestAllowedDts) { 37089 track.minSegmentDts = Math.min(track.minSegmentDts, currentFrame.dts); 37090 track.minSegmentPts = track.minSegmentDts; 37091 return true; 37092 } 37093 // Otherwise, discard it 37094 return false;
vendor: 20,968 bytes, lines 37095-37683
37095 }); 37096 }; 37097 37098 // generate the track's raw mdat data from an array of frames 37099 this.generateSampleTable_ = function (frames) { 37100 var i, 37101 currentFrame, 37102 samples = []; 37103 37104 for (i = 0; i < frames.length; i++) { 37105 currentFrame = frames[i]; 37106 samples.push({ 37107 size: currentFrame.data.byteLength, 37108 duration: 1024 // For AAC audio, all samples contain 1024 samples 37109 }); 37110 } 37111 return samples; 37112 }; 37113 37114 // generate the track's sample table from an array of frames 37115 this.concatenateFrameData_ = function (frames) { 37116 var i, 37117 currentFrame, 37118 dataOffset = 0, 37119 data = new Uint8Array(sumFrameByteLengths(frames)); 37120 37121 for (i = 0; i < frames.length; i++) { 37122 currentFrame = frames[i]; 37123 37124 data.set(currentFrame.data, dataOffset); 37125 dataOffset += currentFrame.data.byteLength; 37126 } 37127 return data; 37128 }; 37129 }; 37130 37131 _AudioSegmentStream.prototype = new stream(); 37132 37133 /** 37134 * Constructs a single-track, ISO BMFF media segment from H264 data 37135 * events. The output of this stream can be fed to a SourceBuffer 37136 * configured with a suitable initialization segment. 37137 * @param track {object} track metadata configuration 37138 * @param options {object} transmuxer options object 37139 * @param options.alignGopsAtEnd {boolean} If true, start from the end of the 37140 * gopsToAlignWith list when attempting to align gop pts 37141 * @param options.keepOriginalTimestamps {boolean} If true, keep the timestamps 37142 * in the source; false to adjust the first segment to start at 0. 37143 */ 37144 _VideoSegmentStream = function VideoSegmentStream(track, options) { 37145 var sequenceNumber = 0, 37146 nalUnits = [], 37147 gopsToAlignWith = [], 37148 config, 37149 pps; 37150 37151 options = options || {}; 37152 37153 _VideoSegmentStream.prototype.init.call(this); 37154 37155 delete track.minPTS; 37156 37157 this.gopCache_ = []; 37158 37159 this.push = function (nalUnit) { 37160 collectDtsInfo(track, nalUnit); 37161 37162 // record the track config 37163 if (nalUnit.nalUnitType === 'seq_parameter_set_rbsp' && !config) { 37164 config = nalUnit.config; 37165 track.sps = [nalUnit.data]; 37166 37167 VIDEO_PROPERTIES.forEach(function (prop) { 37168 track[prop] = config[prop]; 37169 }, this); 37170 } 37171 37172 if (nalUnit.nalUnitType === 'pic_parameter_set_rbsp' && !pps) { 37173 pps = nalUnit.data; 37174 track.pps = [nalUnit.data]; 37175 } 37176 37177 // buffer video until flush() is called 37178 nalUnits.push(nalUnit); 37179 }; 37180 37181 this.flush = function () { 37182 var frames, gopForFusion, gops, moof, mdat, boxes; 37183 37184 // Throw away nalUnits at the start of the byte stream until 37185 // we find the first AUD 37186 while (nalUnits.length) { 37187 if (nalUnits[0].nalUnitType === 'access_unit_delimiter_rbsp') { 37188 break; 37189 } 37190 nalUnits.shift(); 37191 } 37192 37193 // Return early if no video data has been observed 37194 if (nalUnits.length === 0) { 37195 this.resetStream_(); 37196 this.trigger('done', 'VideoSegmentStream'); 37197 return; 37198 } 37199 37200 // Organize the raw nal-units into arrays that represent 37201 // higher-level constructs such as frames and gops 37202 // (group-of-pictures) 37203 frames = this.groupNalsIntoFrames_(nalUnits); 37204 gops = this.groupFramesIntoGops_(frames); 37205 37206 // If the first frame of this fragment is not a keyframe we have 37207 // a problem since MSE (on Chrome) requires a leading keyframe. 37208 // 37209 // We have two approaches to repairing this situation: 37210 // 1) GOP-FUSION: 37211 // This is where we keep track of the GOPS (group-of-pictures) 37212 // from previous fragments and attempt to find one that we can 37213 // prepend to the current fragment in order to create a valid 37214 // fragment. 37215 // 2) KEYFRAME-PULLING: 37216 // Here we search for the first keyframe in the fragment and 37217 // throw away all the frames between the start of the fragment 37218 // and that keyframe. We then extend the duration and pull the 37219 // PTS of the keyframe forward so that it covers the time range 37220 // of the frames that were disposed of. 37221 // 37222 // #1 is far prefereable over #2 which can cause "stuttering" but 37223 // requires more things to be just right. 37224 if (!gops[0][0].keyFrame) { 37225 // Search for a gop for fusion from our gopCache 37226 gopForFusion = this.getGopForFusion_(nalUnits[0], track); 37227 37228 if (gopForFusion) { 37229 gops.unshift(gopForFusion); 37230 // Adjust Gops' metadata to account for the inclusion of the 37231 // new gop at the beginning 37232 gops.byteLength += gopForFusion.byteLength; 37233 gops.nalCount += gopForFusion.nalCount; 37234 gops.pts = gopForFusion.pts; 37235 gops.dts = gopForFusion.dts; 37236 gops.duration += gopForFusion.duration; 37237 } else { 37238 // If we didn't find a candidate gop fall back to keyframe-pulling 37239 gops = this.extendFirstKeyFrame_(gops); 37240 } 37241 } 37242 37243 // Trim gops to align with gopsToAlignWith 37244 if (gopsToAlignWith.length) { 37245 var alignedGops; 37246 37247 if (options.alignGopsAtEnd) { 37248 alignedGops = this.alignGopsAtEnd_(gops); 37249 } else { 37250 alignedGops = this.alignGopsAtStart_(gops); 37251 } 37252 37253 if (!alignedGops) { 37254 // save all the nals in the last GOP into the gop cache 37255 this.gopCache_.unshift({ 37256 gop: gops.pop(), 37257 pps: track.pps, 37258 sps: track.sps 37259 }); 37260 37261 // Keep a maximum of 6 GOPs in the cache 37262 this.gopCache_.length = Math.min(6, this.gopCache_.length); 37263 37264 // Clear nalUnits 37265 nalUnits = []; 37266 37267 // return early no gops can be aligned with desired gopsToAlignWith 37268 this.resetStream_(); 37269 this.trigger('done', 'VideoSegmentStream'); 37270 return; 37271 } 37272 37273 // Some gops were trimmed. clear dts info so minSegmentDts and pts are correct 37274 // when recalculated before sending off to CoalesceStream 37275 clearDtsInfo(track); 37276 37277 gops = alignedGops; 37278 } 37279 37280 collectDtsInfo(track, gops); 37281 37282 // First, we have to build the index from byte locations to 37283 // samples (that is, frames) in the video data 37284 track.samples = this.generateSampleTable_(gops); 37285 37286 // Concatenate the video data and construct the mdat 37287 mdat = mp4Generator.mdat(this.concatenateNalData_(gops)); 37288 37289 track.baseMediaDecodeTime = calculateTrackBaseMediaDecodeTime(track, options.keepOriginalTimestamps); 37290 37291 this.trigger('processedGopsInfo', gops.map(function (gop) { 37292 return { 37293 pts: gop.pts, 37294 dts: gop.dts, 37295 byteLength: gop.byteLength 37296 }; 37297 })); 37298 37299 // save all the nals in the last GOP into the gop cache 37300 this.gopCache_.unshift({ 37301 gop: gops.pop(), 37302 pps: track.pps, 37303 sps: track.sps 37304 }); 37305 37306 // Keep a maximum of 6 GOPs in the cache 37307 this.gopCache_.length = Math.min(6, this.gopCache_.length); 37308 37309 // Clear nalUnits 37310 nalUnits = []; 37311 37312 this.trigger('baseMediaDecodeTime', track.baseMediaDecodeTime); 37313 this.trigger('timelineStartInfo', track.timelineStartInfo); 37314 37315 moof = mp4Generator.moof(sequenceNumber, [track]); 37316 37317 // it would be great to allocate this array up front instead of 37318 // throwing away hundreds of media segment fragments 37319 boxes = new Uint8Array(moof.byteLength + mdat.byteLength); 37320 37321 // Bump the sequence number for next time 37322 sequenceNumber++; 37323 37324 boxes.set(moof); 37325 boxes.set(mdat, moof.byteLength); 37326 37327 this.trigger('data', { track: track, boxes: boxes }); 37328 37329 this.resetStream_(); 37330 37331 // Continue with the flush process now 37332 this.trigger('done', 'VideoSegmentStream'); 37333 }; 37334 37335 this.resetStream_ = function () { 37336 clearDtsInfo(track); 37337 37338 // reset config and pps because they may differ across segments 37339 // for instance, when we are rendition switching 37340 config = undefined; 37341 pps = undefined; 37342 }; 37343 37344 // Search for a candidate Gop for gop-fusion from the gop cache and 37345 // return it or return null if no good candidate was found 37346 this.getGopForFusion_ = function (nalUnit) { 37347 var halfSecond = 45000, 37348 37349 37350 // Half-a-second in a 90khz clock 37351 allowableOverlap = 10000, 37352 37353 37354 // About 3 frames @ 30fps 37355 nearestDistance = Infinity, 37356 dtsDistance, 37357 nearestGopObj, 37358 currentGop, 37359 currentGopObj, 37360 i; 37361 37362 // Search for the GOP nearest to the beginning of this nal unit 37363 for (i = 0; i < this.gopCache_.length; i++) { 37364 currentGopObj = this.gopCache_[i]; 37365 currentGop = currentGopObj.gop; 37366 37367 // Reject Gops with different SPS or PPS 37368 if (!(track.pps && arrayEquals(track.pps[0], currentGopObj.pps[0])) || !(track.sps && arrayEquals(track.sps[0], currentGopObj.sps[0]))) { 37369 continue; 37370 } 37371 37372 // Reject Gops that would require a negative baseMediaDecodeTime 37373 if (currentGop.dts < track.timelineStartInfo.dts) { 37374 continue; 37375 } 37376 37377 // The distance between the end of the gop and the start of the nalUnit 37378 dtsDistance = nalUnit.dts - currentGop.dts - currentGop.duration; 37379 37380 // Only consider GOPS that start before the nal unit and end within 37381 // a half-second of the nal unit 37382 if (dtsDistance >= -allowableOverlap && dtsDistance <= halfSecond) { 37383 37384 // Always use the closest GOP we found if there is more than 37385 // one candidate 37386 if (!nearestGopObj || nearestDistance > dtsDistance) { 37387 nearestGopObj = currentGopObj; 37388 nearestDistance = dtsDistance; 37389 } 37390 } 37391 } 37392 37393 if (nearestGopObj) { 37394 return nearestGopObj.gop; 37395 } 37396 return null; 37397 }; 37398 37399 this.extendFirstKeyFrame_ = function (gops) { 37400 var currentGop; 37401 37402 if (!gops[0][0].keyFrame && gops.length > 1) { 37403 // Remove the first GOP 37404 currentGop = gops.shift(); 37405 37406 gops.byteLength -= currentGop.byteLength; 37407 gops.nalCount -= currentGop.nalCount; 37408 37409 // Extend the first frame of what is now the 37410 // first gop to cover the time period of the 37411 // frames we just removed 37412 gops[0][0].dts = currentGop.dts; 37413 gops[0][0].pts = currentGop.pts; 37414 gops[0][0].duration += currentGop.duration; 37415 } 37416 37417 return gops; 37418 }; 37419 37420 // Convert an array of nal units into an array of frames with each frame being 37421 // composed of the nal units that make up that frame 37422 // Also keep track of cummulative data about the frame from the nal units such 37423 // as the frame duration, starting pts, etc. 37424 this.groupNalsIntoFrames_ = function (nalUnits) { 37425 var i, 37426 currentNal, 37427 currentFrame = [], 37428 frames = []; 37429 37430 currentFrame.byteLength = 0; 37431 37432 for (i = 0; i < nalUnits.length; i++) { 37433 currentNal = nalUnits[i]; 37434 37435 // Split on 'aud'-type nal units 37436 if (currentNal.nalUnitType === 'access_unit_delimiter_rbsp') { 37437 // Since the very first nal unit is expected to be an AUD 37438 // only push to the frames array when currentFrame is not empty 37439 if (currentFrame.length) { 37440 currentFrame.duration = currentNal.dts - currentFrame.dts; 37441 frames.push(currentFrame); 37442 } 37443 currentFrame = [currentNal]; 37444 currentFrame.byteLength = currentNal.data.byteLength; 37445 currentFrame.pts = currentNal.pts; 37446 currentFrame.dts = currentNal.dts; 37447 } else { 37448 // Specifically flag key frames for ease of use later 37449 if (currentNal.nalUnitType === 'slice_layer_without_partitioning_rbsp_idr') { 37450 currentFrame.keyFrame = true; 37451 } 37452 currentFrame.duration = currentNal.dts - currentFrame.dts; 37453 currentFrame.byteLength += currentNal.data.byteLength; 37454 currentFrame.push(currentNal); 37455 } 37456 } 37457 37458 // For the last frame, use the duration of the previous frame if we 37459 // have nothing better to go on 37460 if (frames.length && (!currentFrame.duration || currentFrame.duration <= 0)) { 37461 currentFrame.duration = frames[frames.length - 1].duration; 37462 } 37463 37464 // Push the final frame 37465 frames.push(currentFrame); 37466 return frames; 37467 }; 37468 37469 // Convert an array of frames into an array of Gop with each Gop being composed 37470 // of the frames that make up that Gop 37471 // Also keep track of cummulative data about the Gop from the frames such as the 37472 // Gop duration, starting pts, etc. 37473 this.groupFramesIntoGops_ = function (frames) { 37474 var i, 37475 currentFrame, 37476 currentGop = [], 37477 gops = []; 37478 37479 // We must pre-set some of the values on the Gop since we 37480 // keep running totals of these values 37481 currentGop.byteLength = 0; 37482 currentGop.nalCount = 0; 37483 currentGop.duration = 0; 37484 currentGop.pts = frames[0].pts; 37485 currentGop.dts = frames[0].dts; 37486 37487 // store some metadata about all the Gops 37488 gops.byteLength = 0; 37489 gops.nalCount = 0; 37490 gops.duration = 0; 37491 gops.pts = frames[0].pts; 37492 gops.dts = frames[0].dts; 37493 37494 for (i = 0; i < frames.length; i++) { 37495 currentFrame = frames[i]; 37496 37497 if (currentFrame.keyFrame) { 37498 // Since the very first frame is expected to be an keyframe 37499 // only push to the gops array when currentGop is not empty 37500 if (currentGop.length) { 37501 gops.push(currentGop); 37502 gops.byteLength += currentGop.byteLength; 37503 gops.nalCount += currentGop.nalCount; 37504 gops.duration += currentGop.duration; 37505 } 37506 37507 currentGop = [currentFrame]; 37508 currentGop.nalCount = currentFrame.length; 37509 currentGop.byteLength = currentFrame.byteLength; 37510 currentGop.pts = currentFrame.pts; 37511 currentGop.dts = currentFrame.dts; 37512 currentGop.duration = currentFrame.duration; 37513 } else { 37514 currentGop.duration += currentFrame.duration; 37515 currentGop.nalCount += currentFrame.length; 37516 currentGop.byteLength += currentFrame.byteLength; 37517 currentGop.push(currentFrame); 37518 } 37519 } 37520 37521 if (gops.length && currentGop.duration <= 0) { 37522 currentGop.duration = gops[gops.length - 1].duration; 37523 } 37524 gops.byteLength += currentGop.byteLength; 37525 gops.nalCount += currentGop.nalCount; 37526 gops.duration += currentGop.duration; 37527 37528 // push the final Gop 37529 gops.push(currentGop); 37530 return gops; 37531 }; 37532 37533 // generate the track's sample table from an array of gops 37534 this.generateSampleTable_ = function (gops, baseDataOffset) { 37535 var h, 37536 i, 37537 sample, 37538 currentGop, 37539 currentFrame, 37540 dataOffset = baseDataOffset || 0, 37541 samples = []; 37542 37543 for (h = 0; h < gops.length; h++) { 37544 currentGop = gops[h]; 37545 37546 for (i = 0; i < currentGop.length; i++) { 37547 currentFrame = currentGop[i]; 37548 37549 sample = createDefaultSample(); 37550 37551 sample.dataOffset = dataOffset; 37552 sample.compositionTimeOffset = currentFrame.pts - currentFrame.dts; 37553 sample.duration = currentFrame.duration; 37554 sample.size = 4 * currentFrame.length; // Space for nal unit size 37555 sample.size += currentFrame.byteLength; 37556 37557 if (currentFrame.keyFrame) { 37558 sample.flags.dependsOn = 2; 37559 } 37560 37561 dataOffset += sample.size; 37562 37563 samples.push(sample); 37564 } 37565 } 37566 return samples; 37567 }; 37568 37569 // generate the track's raw mdat data from an array of gops 37570 this.concatenateNalData_ = function (gops) { 37571 var h, 37572 i, 37573 j, 37574 currentGop, 37575 currentFrame, 37576 currentNal, 37577 dataOffset = 0, 37578 nalsByteLength = gops.byteLength, 37579 numberOfNals = gops.nalCount, 37580 totalByteLength = nalsByteLength + 4 * numberOfNals, 37581 data = new Uint8Array(totalByteLength), 37582 view = new DataView(data.buffer); 37583 37584 // For each Gop.. 37585 for (h = 0; h < gops.length; h++) { 37586 currentGop = gops[h]; 37587 37588 // For each Frame.. 37589 for (i = 0; i < currentGop.length; i++) { 37590 currentFrame = currentGop[i]; 37591 37592 // For each NAL.. 37593 for (j = 0; j < currentFrame.length; j++) { 37594 currentNal = currentFrame[j]; 37595 37596 view.setUint32(dataOffset, currentNal.data.byteLength); 37597 dataOffset += 4; 37598 data.set(currentNal.data, dataOffset); 37599 dataOffset += currentNal.data.byteLength; 37600 } 37601 } 37602 } 37603 return data; 37604 }; 37605 37606 // trim gop list to the first gop found that has a matching pts with a gop in the list 37607 // of gopsToAlignWith starting from the START of the list 37608 this.alignGopsAtStart_ = function (gops) { 37609 var alignIndex, gopIndex, align, gop, byteLength, nalCount, duration, alignedGops; 37610 37611 byteLength = gops.byteLength; 37612 nalCount = gops.nalCount; 37613 duration = gops.duration; 37614 alignIndex = gopIndex = 0; 37615 37616 while (alignIndex < gopsToAlignWith.length && gopIndex < gops.length) { 37617 align = gopsToAlignWith[alignIndex]; 37618 gop = gops[gopIndex]; 37619 37620 if (align.pts === gop.pts) { 37621 break; 37622 } 37623 37624 if (gop.pts > align.pts) { 37625 // this current gop starts after the current gop we want to align on, so increment 37626 // align index 37627 alignIndex++; 37628 continue; 37629 } 37630 37631 // current gop starts before the current gop we want to align on. so increment gop 37632 // index 37633 gopIndex++; 37634 byteLength -= gop.byteLength; 37635 nalCount -= gop.nalCount; 37636 duration -= gop.duration; 37637 } 37638 37639 if (gopIndex === 0) { 37640 // no gops to trim 37641 return gops; 37642 } 37643 37644 if (gopIndex === gops.length) { 37645 // all gops trimmed, skip appending all gops 37646 return null; 37647 } 37648 37649 alignedGops = gops.slice(gopIndex); 37650 alignedGops.byteLength = byteLength; 37651 alignedGops.duration = duration; 37652 alignedGops.nalCount = nalCount; 37653 alignedGops.pts = alignedGops[0].pts; 37654 alignedGops.dts = alignedGops[0].dts; 37655 37656 return alignedGops; 37657 }; 37658 37659 // trim gop list to the first gop found that has a matching pts with a gop in the list 37660 // of gopsToAlignWith starting from the END of the list 37661 this.alignGopsAtEnd_ = function (gops) { 37662 var alignIndex, gopIndex, align, gop, alignEndIndex, matchFound; 37663 37664 alignIndex = gopsToAlignWith.length - 1; 37665 gopIndex = gops.length - 1; 37666 alignEndIndex = null; 37667 matchFound = false; 37668 37669 while (alignIndex >= 0 && gopIndex >= 0) { 37670 align = gopsToAlignWith[alignIndex]; 37671 gop = gops[gopIndex]; 37672 37673 if (align.pts === gop.pts) { 37674 matchFound = true; 37675 break; 37676 } 37677 37678 if (align.pts > gop.pts) { 37679 alignIndex--; 37680 continue; 37681 } 37682 37683 if (alignIndex === gopsToAlignWith.length - 1) {
vendor: 6,958 bytes, lines 37684-37886
37684 // gop.pts is greater than the last alignment candidate. If no match is found 37685 // by the end of this loop, we still want to append gops that come after this 37686 // point 37687 alignEndIndex = gopIndex; 37688 } 37689 37690 gopIndex--; 37691 } 37692 37693 if (!matchFound && alignEndIndex === null) { 37694 return null; 37695 } 37696 37697 var trimIndex; 37698 37699 if (matchFound) { 37700 trimIndex = gopIndex; 37701 } else { 37702 trimIndex = alignEndIndex; 37703 } 37704 37705 if (trimIndex === 0) { 37706 return gops; 37707 } 37708 37709 var alignedGops = gops.slice(trimIndex); 37710 var metadata = alignedGops.reduce(function (total, gop) { 37711 total.byteLength += gop.byteLength; 37712 total.duration += gop.duration; 37713 total.nalCount += gop.nalCount; 37714 return total; 37715 }, { byteLength: 0, duration: 0, nalCount: 0 }); 37716 37717 alignedGops.byteLength = metadata.byteLength; 37718 alignedGops.duration = metadata.duration; 37719 alignedGops.nalCount = metadata.nalCount; 37720 alignedGops.pts = alignedGops[0].pts; 37721 alignedGops.dts = alignedGops[0].dts; 37722 37723 return alignedGops; 37724 }; 37725 37726 this.alignGopsWith = function (newGopsToAlignWith) { 37727 gopsToAlignWith = newGopsToAlignWith; 37728 }; 37729 }; 37730 37731 _VideoSegmentStream.prototype = new stream(); 37732 37733 /** 37734 * Store information about the start and end of the track and the 37735 * duration for each frame/sample we process in order to calculate 37736 * the baseMediaDecodeTime 37737 */ 37738 collectDtsInfo = function collectDtsInfo(track, data) { 37739 if (typeof data.pts === 'number') { 37740 if (track.timelineStartInfo.pts === undefined) { 37741 track.timelineStartInfo.pts = data.pts; 37742 } 37743 37744 if (track.minSegmentPts === undefined) { 37745 track.minSegmentPts = data.pts; 37746 } else { 37747 track.minSegmentPts = Math.min(track.minSegmentPts, data.pts); 37748 } 37749 37750 if (track.maxSegmentPts === undefined) { 37751 track.maxSegmentPts = data.pts; 37752 } else { 37753 track.maxSegmentPts = Math.max(track.maxSegmentPts, data.pts); 37754 } 37755 } 37756 37757 if (typeof data.dts === 'number') { 37758 if (track.timelineStartInfo.dts === undefined) { 37759 track.timelineStartInfo.dts = data.dts; 37760 } 37761 37762 if (track.minSegmentDts === undefined) { 37763 track.minSegmentDts = data.dts; 37764 } else { 37765 track.minSegmentDts = Math.min(track.minSegmentDts, data.dts); 37766 } 37767 37768 if (track.maxSegmentDts === undefined) { 37769 track.maxSegmentDts = data.dts; 37770 } else { 37771 track.maxSegmentDts = Math.max(track.maxSegmentDts, data.dts); 37772 } 37773 } 37774 }; 37775 37776 /** 37777 * Clear values used to calculate the baseMediaDecodeTime between 37778 * tracks 37779 */ 37780 clearDtsInfo = function clearDtsInfo(track) { 37781 delete track.minSegmentDts; 37782 delete track.maxSegmentDts; 37783 delete track.minSegmentPts; 37784 delete track.maxSegmentPts; 37785 }; 37786 37787 /** 37788 * Calculate the track's baseMediaDecodeTime based on the earliest 37789 * DTS the transmuxer has ever seen and the minimum DTS for the 37790 * current track 37791 * @param track {object} track metadata configuration 37792 * @param keepOriginalTimestamps {boolean} If true, keep the timestamps 37793 * in the source; false to adjust the first segment to start at 0. 37794 */ 37795 calculateTrackBaseMediaDecodeTime = function calculateTrackBaseMediaDecodeTime(track, keepOriginalTimestamps) { 37796 var baseMediaDecodeTime, 37797 scale, 37798 minSegmentDts = track.minSegmentDts; 37799 37800 // Optionally adjust the time so the first segment starts at zero. 37801 if (!keepOriginalTimestamps) { 37802 minSegmentDts -= track.timelineStartInfo.dts; 37803 } 37804 37805 // track.timelineStartInfo.baseMediaDecodeTime is the location, in time, where 37806 // we want the start of the first segment to be placed 37807 baseMediaDecodeTime = track.timelineStartInfo.baseMediaDecodeTime; 37808 37809 // Add to that the distance this segment is from the very first 37810 baseMediaDecodeTime += minSegmentDts; 37811 37812 // baseMediaDecodeTime must not become negative 37813 baseMediaDecodeTime = Math.max(0, baseMediaDecodeTime); 37814 37815 if (track.type === 'audio') { 37816 // Audio has a different clock equal to the sampling_rate so we need to 37817 // scale the PTS values into the clock rate of the track 37818 scale = track.samplerate / ONE_SECOND_IN_TS$1; 37819 baseMediaDecodeTime *= scale; 37820 baseMediaDecodeTime = Math.floor(baseMediaDecodeTime); 37821 } 37822 37823 return baseMediaDecodeTime; 37824 }; 37825 37826 /** 37827 * A Stream that can combine multiple streams (ie. audio & video) 37828 * into a single output segment for MSE. Also supports audio-only 37829 * and video-only streams. 37830 */ 37831 _CoalesceStream = function CoalesceStream(options, metadataStream) { 37832 // Number of Tracks per output segment 37833 // If greater than 1, we combine multiple 37834 // tracks into a single segment 37835 this.numberOfTracks = 0; 37836 this.metadataStream = metadataStream; 37837 37838 if (typeof options.remux !== 'undefined') { 37839 this.remuxTracks = !!options.remux; 37840 } else { 37841 this.remuxTracks = true; 37842 } 37843 37844 this.pendingTracks = []; 37845 this.videoTrack = null; 37846 this.pendingBoxes = []; 37847 this.pendingCaptions = []; 37848 this.pendingMetadata = []; 37849 this.pendingBytes = 0; 37850 this.emittedTracks = 0; 37851 37852 _CoalesceStream.prototype.init.call(this); 37853 37854 // Take output from multiple 37855 this.push = function (output) { 37856 // buffer incoming captions until the associated video segment 37857 // finishes 37858 if (output.text) { 37859 return this.pendingCaptions.push(output); 37860 } 37861 // buffer incoming id3 tags until the final flush 37862 if (output.frames) { 37863 return this.pendingMetadata.push(output); 37864 } 37865 37866 // Add this track to the list of pending tracks and store 37867 // important information required for the construction of 37868 // the final segment 37869 this.pendingTracks.push(output.track); 37870 this.pendingBoxes.push(output.boxes); 37871 this.pendingBytes += output.boxes.byteLength; 37872 37873 if (output.track.type === 'video') { 37874 this.videoTrack = output.track; 37875 } 37876 if (output.track.type === 'audio') { 37877 this.audioTrack = output.track; 37878 } 37879 }; 37880 }; 37881 37882 _CoalesceStream.prototype = new stream(); 37883 _CoalesceStream.prototype.flush = function (flushSource) { 37884 var offset = 0, 37885 event = { 37886 captions: [],
vendor: 14,628 bytes, lines 37887-38217
37887 captionStreams: {}, 37888 metadata: [], 37889 info: {} 37890 }, 37891 caption, 37892 id3, 37893 initSegment, 37894 timelineStartPts = 0, 37895 i; 37896 37897 if (this.pendingTracks.length < this.numberOfTracks) { 37898 if (flushSource !== 'VideoSegmentStream' && flushSource !== 'AudioSegmentStream') { 37899 // Return because we haven't received a flush from a data-generating 37900 // portion of the segment (meaning that we have only recieved meta-data 37901 // or captions.) 37902 return; 37903 } else if (this.remuxTracks) { 37904 // Return until we have enough tracks from the pipeline to remux (if we 37905 // are remuxing audio and video into a single MP4) 37906 return; 37907 } else if (this.pendingTracks.length === 0) { 37908 // In the case where we receive a flush without any data having been 37909 // received we consider it an emitted track for the purposes of coalescing 37910 // `done` events. 37911 // We do this for the case where there is an audio and video track in the 37912 // segment but no audio data. (seen in several playlists with alternate 37913 // audio tracks and no audio present in the main TS segments.) 37914 this.emittedTracks++; 37915 37916 if (this.emittedTracks >= this.numberOfTracks) { 37917 this.trigger('done'); 37918 this.emittedTracks = 0; 37919 } 37920 return; 37921 } 37922 } 37923 37924 if (this.videoTrack) { 37925 timelineStartPts = this.videoTrack.timelineStartInfo.pts; 37926 VIDEO_PROPERTIES.forEach(function (prop) { 37927 event.info[prop] = this.videoTrack[prop]; 37928 }, this); 37929 } else if (this.audioTrack) { 37930 timelineStartPts = this.audioTrack.timelineStartInfo.pts; 37931 AUDIO_PROPERTIES.forEach(function (prop) { 37932 event.info[prop] = this.audioTrack[prop]; 37933 }, this); 37934 } 37935 37936 if (this.pendingTracks.length === 1) { 37937 event.type = this.pendingTracks[0].type; 37938 } else { 37939 event.type = 'combined'; 37940 } 37941 37942 this.emittedTracks += this.pendingTracks.length; 37943 37944 initSegment = mp4Generator.initSegment(this.pendingTracks); 37945 37946 // Create a new typed array to hold the init segment 37947 event.initSegment = new Uint8Array(initSegment.byteLength); 37948 37949 // Create an init segment containing a moov 37950 // and track definitions 37951 event.initSegment.set(initSegment); 37952 37953 // Create a new typed array to hold the moof+mdats 37954 event.data = new Uint8Array(this.pendingBytes); 37955 37956 // Append each moof+mdat (one per track) together 37957 for (i = 0; i < this.pendingBoxes.length; i++) { 37958 event.data.set(this.pendingBoxes[i], offset); 37959 offset += this.pendingBoxes[i].byteLength; 37960 } 37961 37962 // Translate caption PTS times into second offsets into the 37963 // video timeline for the segment, and add track info 37964 for (i = 0; i < this.pendingCaptions.length; i++) { 37965 caption = this.pendingCaptions[i]; 37966 caption.startTime = caption.startPts - timelineStartPts; 37967 caption.startTime /= 90e3; 37968 caption.endTime = caption.endPts - timelineStartPts; 37969 caption.endTime /= 90e3; 37970 event.captionStreams[caption.stream] = true; 37971 event.captions.push(caption); 37972 } 37973 37974 // Translate ID3 frame PTS times into second offsets into the 37975 // video timeline for the segment 37976 for (i = 0; i < this.pendingMetadata.length; i++) { 37977 id3 = this.pendingMetadata[i]; 37978 id3.cueTime = id3.pts - timelineStartPts; 37979 id3.cueTime /= 90e3; 37980 event.metadata.push(id3); 37981 } 37982 // We add this to every single emitted segment even though we only need 37983 // it for the first 37984 event.metadata.dispatchType = this.metadataStream.dispatchType; 37985 37986 // Reset stream state 37987 this.pendingTracks.length = 0; 37988 this.videoTrack = null; 37989 this.pendingBoxes.length = 0; 37990 this.pendingCaptions.length = 0; 37991 this.pendingBytes = 0; 37992 this.pendingMetadata.length = 0; 37993 37994 // Emit the built segment 37995 this.trigger('data', event); 37996 37997 // Only emit `done` if all tracks have been flushed and emitted 37998 if (this.emittedTracks >= this.numberOfTracks) { 37999 this.trigger('done'); 38000 this.emittedTracks = 0; 38001 } 38002 }; 38003 /** 38004 * A Stream that expects MP2T binary data as input and produces 38005 * corresponding media segments, suitable for use with Media Source 38006 * Extension (MSE) implementations that support the ISO BMFF byte 38007 * stream format, like Chrome. 38008 */ 38009 _Transmuxer = function Transmuxer(options) { 38010 var self = this, 38011 hasFlushed = true, 38012 videoTrack, 38013 audioTrack; 38014 38015 _Transmuxer.prototype.init.call(this); 38016 38017 options = options || {}; 38018 this.baseMediaDecodeTime = options.baseMediaDecodeTime || 0; 38019 this.transmuxPipeline_ = {}; 38020 38021 this.setupAacPipeline = function () { 38022 var pipeline = {}; 38023 this.transmuxPipeline_ = pipeline; 38024 38025 pipeline.type = 'aac'; 38026 pipeline.metadataStream = new m2ts_1.MetadataStream(); 38027 38028 // set up the parsing pipeline 38029 pipeline.aacStream = new aac(); 38030 pipeline.audioTimestampRolloverStream = new m2ts_1.TimestampRolloverStream('audio'); 38031 pipeline.timedMetadataTimestampRolloverStream = new m2ts_1.TimestampRolloverStream('timed-metadata'); 38032 pipeline.adtsStream = new adts(); 38033 pipeline.coalesceStream = new _CoalesceStream(options, pipeline.metadataStream); 38034 pipeline.headOfPipeline = pipeline.aacStream; 38035 38036 pipeline.aacStream.pipe(pipeline.audioTimestampRolloverStream).pipe(pipeline.adtsStream); 38037 pipeline.aacStream.pipe(pipeline.timedMetadataTimestampRolloverStream).pipe(pipeline.metadataStream).pipe(pipeline.coalesceStream); 38038 38039 pipeline.metadataStream.on('timestamp', function (frame) { 38040 pipeline.aacStream.setTimestamp(frame.timeStamp); 38041 }); 38042 38043 pipeline.aacStream.on('data', function (data) { 38044 if (data.type === 'timed-metadata' && !pipeline.audioSegmentStream) { 38045 audioTrack = audioTrack || { 38046 timelineStartInfo: { 38047 baseMediaDecodeTime: self.baseMediaDecodeTime 38048 }, 38049 codec: 'adts', 38050 type: 'audio' 38051 }; 38052 // hook up the audio segment stream to the first track with aac data 38053 pipeline.coalesceStream.numberOfTracks++; 38054 pipeline.audioSegmentStream = new _AudioSegmentStream(audioTrack, options); 38055 // Set up the final part of the audio pipeline 38056 pipeline.adtsStream.pipe(pipeline.audioSegmentStream).pipe(pipeline.coalesceStream); 38057 } 38058 }); 38059 38060 // Re-emit any data coming from the coalesce stream to the outside world 38061 pipeline.coalesceStream.on('data', this.trigger.bind(this, 'data')); 38062 // Let the consumer know we have finished flushing the entire pipeline 38063 pipeline.coalesceStream.on('done', this.trigger.bind(this, 'done')); 38064 }; 38065 38066 this.setupTsPipeline = function () { 38067 var pipeline = {}; 38068 this.transmuxPipeline_ = pipeline; 38069 38070 pipeline.type = 'ts'; 38071 pipeline.metadataStream = new m2ts_1.MetadataStream(); 38072 38073 // set up the parsing pipeline 38074 pipeline.packetStream = new m2ts_1.TransportPacketStream(); 38075 pipeline.parseStream = new m2ts_1.TransportParseStream(); 38076 pipeline.elementaryStream = new m2ts_1.ElementaryStream(); 38077 pipeline.videoTimestampRolloverStream = new m2ts_1.TimestampRolloverStream('video'); 38078 pipeline.audioTimestampRolloverStream = new m2ts_1.TimestampRolloverStream('audio'); 38079 pipeline.timedMetadataTimestampRolloverStream = new m2ts_1.TimestampRolloverStream('timed-metadata'); 38080 pipeline.adtsStream = new adts(); 38081 pipeline.h264Stream = new H264Stream(); 38082 pipeline.captionStream = new m2ts_1.CaptionStream(); 38083 pipeline.coalesceStream = new _CoalesceStream(options, pipeline.metadataStream); 38084 pipeline.headOfPipeline = pipeline.packetStream; 38085 38086 // disassemble MPEG2-TS packets into elementary streams 38087 pipeline.packetStream.pipe(pipeline.parseStream).pipe(pipeline.elementaryStream); 38088 38089 // !!THIS ORDER IS IMPORTANT!! 38090 // demux the streams 38091 pipeline.elementaryStream.pipe(pipeline.videoTimestampRolloverStream).pipe(pipeline.h264Stream); 38092 pipeline.elementaryStream.pipe(pipeline.audioTimestampRolloverStream).pipe(pipeline.adtsStream); 38093 38094 pipeline.elementaryStream.pipe(pipeline.timedMetadataTimestampRolloverStream).pipe(pipeline.metadataStream).pipe(pipeline.coalesceStream); 38095 38096 // Hook up CEA-608/708 caption stream 38097 pipeline.h264Stream.pipe(pipeline.captionStream).pipe(pipeline.coalesceStream); 38098 38099 pipeline.elementaryStream.on('data', function (data) { 38100 var i; 38101 38102 if (data.type === 'metadata') { 38103 i = data.tracks.length; 38104 38105 // scan the tracks listed in the metadata 38106 while (i--) { 38107 if (!videoTrack && data.tracks[i].type === 'video') { 38108 videoTrack = data.tracks[i]; 38109 videoTrack.timelineStartInfo.baseMediaDecodeTime = self.baseMediaDecodeTime; 38110 } else if (!audioTrack && data.tracks[i].type === 'audio') { 38111 audioTrack = data.tracks[i]; 38112 audioTrack.timelineStartInfo.baseMediaDecodeTime = self.baseMediaDecodeTime; 38113 } 38114 } 38115 38116 // hook up the video segment stream to the first track with h264 data 38117 if (videoTrack && !pipeline.videoSegmentStream) { 38118 pipeline.coalesceStream.numberOfTracks++; 38119 pipeline.videoSegmentStream = new _VideoSegmentStream(videoTrack, options); 38120 38121 pipeline.videoSegmentStream.on('timelineStartInfo', function (timelineStartInfo) { 38122 // When video emits timelineStartInfo data after a flush, we forward that 38123 // info to the AudioSegmentStream, if it exists, because video timeline 38124 // data takes precedence. 38125 if (audioTrack) { 38126 audioTrack.timelineStartInfo = timelineStartInfo; 38127 // On the first segment we trim AAC frames that exist before the 38128 // very earliest DTS we have seen in video because Chrome will 38129 // interpret any video track with a baseMediaDecodeTime that is 38130 // non-zero as a gap. 38131 pipeline.audioSegmentStream.setEarliestDts(timelineStartInfo.dts); 38132 } 38133 }); 38134 38135 pipeline.videoSegmentStream.on('processedGopsInfo', self.trigger.bind(self, 'gopInfo')); 38136 38137 pipeline.videoSegmentStream.on('baseMediaDecodeTime', function (baseMediaDecodeTime) { 38138 if (audioTrack) { 38139 pipeline.audioSegmentStream.setVideoBaseMediaDecodeTime(baseMediaDecodeTime); 38140 } 38141 }); 38142 38143 // Set up the final part of the video pipeline 38144 pipeline.h264Stream.pipe(pipeline.videoSegmentStream).pipe(pipeline.coalesceStream); 38145 } 38146 38147 if (audioTrack && !pipeline.audioSegmentStream) { 38148 // hook up the audio segment stream to the first track with aac data 38149 pipeline.coalesceStream.numberOfTracks++; 38150 pipeline.audioSegmentStream = new _AudioSegmentStream(audioTrack, options); 38151 38152 // Set up the final part of the audio pipeline 38153 pipeline.adtsStream.pipe(pipeline.audioSegmentStream).pipe(pipeline.coalesceStream); 38154 } 38155 } 38156 }); 38157 38158 // Re-emit any data coming from the coalesce stream to the outside world 38159 pipeline.coalesceStream.on('data', this.trigger.bind(this, 'data')); 38160 // Let the consumer know we have finished flushing the entire pipeline 38161 pipeline.coalesceStream.on('done', this.trigger.bind(this, 'done')); 38162 }; 38163 38164 // hook up the segment streams once track metadata is delivered 38165 this.setBaseMediaDecodeTime = function (baseMediaDecodeTime) { 38166 var pipeline = this.transmuxPipeline_; 38167 38168 this.baseMediaDecodeTime = baseMediaDecodeTime; 38169 if (audioTrack) { 38170 audioTrack.timelineStartInfo.dts = undefined; 38171 audioTrack.timelineStartInfo.pts = undefined; 38172 clearDtsInfo(audioTrack); 38173 audioTrack.timelineStartInfo.baseMediaDecodeTime = baseMediaDecodeTime; 38174 if (pipeline.audioTimestampRolloverStream) { 38175 pipeline.audioTimestampRolloverStream.discontinuity(); 38176 } 38177 } 38178 if (videoTrack) { 38179 if (pipeline.videoSegmentStream) { 38180 pipeline.videoSegmentStream.gopCache_ = []; 38181 pipeline.videoTimestampRolloverStream.discontinuity(); 38182 } 38183 videoTrack.timelineStartInfo.dts = undefined; 38184 videoTrack.timelineStartInfo.pts = undefined; 38185 clearDtsInfo(videoTrack); 38186 pipeline.captionStream.reset(); 38187 videoTrack.timelineStartInfo.baseMediaDecodeTime = baseMediaDecodeTime; 38188 } 38189 38190 if (pipeline.timedMetadataTimestampRolloverStream) { 38191 pipeline.timedMetadataTimestampRolloverStream.discontinuity(); 38192 } 38193 }; 38194 38195 this.setAudioAppendStart = function (timestamp) { 38196 if (audioTrack) { 38197 this.transmuxPipeline_.audioSegmentStream.setAudioAppendStart(timestamp); 38198 } 38199 }; 38200 38201 this.alignGopsWith = function (gopsToAlignWith) { 38202 if (videoTrack && this.transmuxPipeline_.videoSegmentStream) { 38203 this.transmuxPipeline_.videoSegmentStream.alignGopsWith(gopsToAlignWith); 38204 } 38205 }; 38206 38207 // feed incoming data to the front of the parsing pipeline 38208 this.push = function (data) { 38209 if (hasFlushed) { 38210 var isAac = isLikelyAacData(data); 38211 38212 if (isAac && this.transmuxPipeline_.type !== 'aac') { 38213 this.setupAacPipeline(); 38214 } else if (!isAac && this.transmuxPipeline_.type !== 'ts') { 38215 this.setupTsPipeline(); 38216 } 38217 hasFlushed = false;
vendor: 76,965 bytes, lines 38218-40556
38218 } 38219 this.transmuxPipeline_.headOfPipeline.push(data); 38220 }; 38221 38222 // flush any buffered data 38223 this.flush = function () { 38224 hasFlushed = true; 38225 // Start at the top of the pipeline and flush all pending work 38226 this.transmuxPipeline_.headOfPipeline.flush(); 38227 }; 38228 38229 // Caption data has to be reset when seeking outside buffered range 38230 this.resetCaptions = function () { 38231 if (this.transmuxPipeline_.captionStream) { 38232 this.transmuxPipeline_.captionStream.reset(); 38233 } 38234 }; 38235 }; 38236 _Transmuxer.prototype = new stream(); 38237 38238 var transmuxer = { 38239 Transmuxer: _Transmuxer, 38240 VideoSegmentStream: _VideoSegmentStream, 38241 AudioSegmentStream: _AudioSegmentStream, 38242 AUDIO_PROPERTIES: AUDIO_PROPERTIES, 38243 VIDEO_PROPERTIES: VIDEO_PROPERTIES 38244 }; 38245 38246 var mp4 = { 38247 generator: mp4Generator, 38248 Transmuxer: transmuxer.Transmuxer, 38249 AudioSegmentStream: transmuxer.AudioSegmentStream, 38250 VideoSegmentStream: transmuxer.VideoSegmentStream 38251 }; 38252 38253 var classCallCheck$$1 = function classCallCheck$$1(instance, Constructor) { 38254 if (!(instance instanceof Constructor)) { 38255 throw new TypeError("Cannot call a class as a function"); 38256 } 38257 }; 38258 38259 var createClass$$1 = function () { 38260 function defineProperties(target, props) { 38261 for (var i = 0; i < props.length; i++) { 38262 var descriptor = props[i]; 38263 descriptor.enumerable = descriptor.enumerable || false; 38264 descriptor.configurable = true; 38265 if ("value" in descriptor) descriptor.writable = true; 38266 Object.defineProperty(target, descriptor.key, descriptor); 38267 } 38268 } 38269 38270 return function (Constructor, protoProps, staticProps) { 38271 if (protoProps) defineProperties(Constructor.prototype, protoProps); 38272 if (staticProps) defineProperties(Constructor, staticProps); 38273 return Constructor; 38274 }; 38275 }(); 38276 38277 /** 38278 * @file transmuxer-worker.js 38279 */ 38280 38281 /** 38282 * Re-emits transmuxer events by converting them into messages to the 38283 * world outside the worker. 38284 * 38285 * @param {Object} transmuxer the transmuxer to wire events on 38286 * @private 38287 */ 38288 var wireTransmuxerEvents = function wireTransmuxerEvents(transmuxer) { 38289 transmuxer.on('data', function (segment) { 38290 // transfer ownership of the underlying ArrayBuffer 38291 // instead of doing a copy to save memory 38292 // ArrayBuffers are transferable but generic TypedArrays are not 38293 // @link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers#Passing_data_by_transferring_ownership_(transferable_objects) 38294 var initArray = segment.initSegment; 38295 38296 segment.initSegment = { 38297 data: initArray.buffer, 38298 byteOffset: initArray.byteOffset, 38299 byteLength: initArray.byteLength 38300 }; 38301 38302 var typedArray = segment.data; 38303 38304 segment.data = typedArray.buffer; 38305 window_1$$1.postMessage({ 38306 action: 'data', 38307 segment: segment, 38308 byteOffset: typedArray.byteOffset, 38309 byteLength: typedArray.byteLength 38310 }, [segment.data]); 38311 }); 38312 38313 if (transmuxer.captionStream) { 38314 transmuxer.captionStream.on('data', function (caption) { 38315 window_1$$1.postMessage({ 38316 action: 'caption', 38317 data: caption 38318 }); 38319 }); 38320 } 38321 38322 transmuxer.on('done', function (data) { 38323 window_1$$1.postMessage({ action: 'done' }); 38324 }); 38325 38326 transmuxer.on('gopInfo', function (gopInfo) { 38327 window_1$$1.postMessage({ 38328 action: 'gopInfo', 38329 gopInfo: gopInfo 38330 }); 38331 }); 38332 }; 38333 38334 /** 38335 * All incoming messages route through this hash. If no function exists 38336 * to handle an incoming message, then we ignore the message. 38337 * 38338 * @class MessageHandlers 38339 * @param {Object} options the options to initialize with 38340 */ 38341 38342 var MessageHandlers = function () { 38343 function MessageHandlers(options) { 38344 classCallCheck$$1(this, MessageHandlers); 38345 38346 this.options = options || {}; 38347 this.init(); 38348 } 38349 38350 /** 38351 * initialize our web worker and wire all the events. 38352 */ 38353 38354 createClass$$1(MessageHandlers, [{ 38355 key: 'init', 38356 value: function init() { 38357 if (this.transmuxer) { 38358 this.transmuxer.dispose(); 38359 } 38360 this.transmuxer = new mp4.Transmuxer(this.options); 38361 wireTransmuxerEvents(this.transmuxer); 38362 } 38363 38364 /** 38365 * Adds data (a ts segment) to the start of the transmuxer pipeline for 38366 * processing. 38367 * 38368 * @param {ArrayBuffer} data data to push into the muxer 38369 */ 38370 38371 }, { 38372 key: 'push', 38373 value: function push(data) { 38374 // Cast array buffer to correct type for transmuxer 38375 var segment = new Uint8Array(data.data, data.byteOffset, data.byteLength); 38376 38377 this.transmuxer.push(segment); 38378 } 38379 38380 /** 38381 * Recreate the transmuxer so that the next segment added via `push` 38382 * start with a fresh transmuxer. 38383 */ 38384 38385 }, { 38386 key: 'reset', 38387 value: function reset() { 38388 this.init(); 38389 } 38390 38391 /** 38392 * Set the value that will be used as the `baseMediaDecodeTime` time for the 38393 * next segment pushed in. Subsequent segments will have their `baseMediaDecodeTime` 38394 * set relative to the first based on the PTS values. 38395 * 38396 * @param {Object} data used to set the timestamp offset in the muxer 38397 */ 38398 38399 }, { 38400 key: 'setTimestampOffset', 38401 value: function setTimestampOffset(data) { 38402 var timestampOffset = data.timestampOffset || 0; 38403 38404 this.transmuxer.setBaseMediaDecodeTime(Math.round(timestampOffset * 90000)); 38405 } 38406 }, { 38407 key: 'setAudioAppendStart', 38408 value: function setAudioAppendStart(data) { 38409 this.transmuxer.setAudioAppendStart(Math.ceil(data.appendStart * 90000)); 38410 } 38411 38412 /** 38413 * Forces the pipeline to finish processing the last segment and emit it's 38414 * results. 38415 * 38416 * @param {Object} data event data, not really used 38417 */ 38418 38419 }, { 38420 key: 'flush', 38421 value: function flush(data) { 38422 this.transmuxer.flush(); 38423 } 38424 }, { 38425 key: 'resetCaptions', 38426 value: function resetCaptions() { 38427 this.transmuxer.resetCaptions(); 38428 } 38429 }, { 38430 key: 'alignGopsWith', 38431 value: function alignGopsWith(data) { 38432 this.transmuxer.alignGopsWith(data.gopsToAlignWith.slice()); 38433 } 38434 }]); 38435 return MessageHandlers; 38436 }(); 38437 38438 /** 38439 * Our web wroker interface so that things can talk to mux.js 38440 * that will be running in a web worker. the scope is passed to this by 38441 * webworkify. 38442 * 38443 * @param {Object} self the scope for the web worker 38444 */ 38445 38446 var TransmuxerWorker = function TransmuxerWorker(self) { 38447 self.onmessage = function (event) { 38448 if (event.data.action === 'init' && event.data.options) { 38449 this.messageHandlers = new MessageHandlers(event.data.options); 38450 return; 38451 } 38452 38453 if (!this.messageHandlers) { 38454 this.messageHandlers = new MessageHandlers(); 38455 } 38456 38457 if (event.data && event.data.action && event.data.action !== 'init') { 38458 if (this.messageHandlers[event.data.action]) { 38459 this.messageHandlers[event.data.action](event.data); 38460 } 38461 } 38462 }; 38463 }; 38464 38465 var transmuxerWorker = new TransmuxerWorker(self); 38466 38467 return transmuxerWorker; 38468 }(); 38469 }); 38470 38471 /** 38472 * @file codec-utils.js 38473 */ 38474 38475 /** 38476 * Check if a codec string refers to an audio codec. 38477 * 38478 * @param {String} codec codec string to check 38479 * @return {Boolean} if this is an audio codec 38480 * @private 38481 */ 38482 var isAudioCodec = function isAudioCodec(codec) { 38483 return (/mp4a\.\d+.\d+/i.test(codec) 38484 ); 38485 }; 38486 38487 /** 38488 * Check if a codec string refers to a video codec. 38489 * 38490 * @param {String} codec codec string to check 38491 * @return {Boolean} if this is a video codec 38492 * @private 38493 */ 38494 var isVideoCodec = function isVideoCodec(codec) { 38495 return (/avc1\.[\da-f]+/i.test(codec) 38496 ); 38497 }; 38498 38499 /** 38500 * Parse a content type header into a type and parameters 38501 * object 38502 * 38503 * @param {String} type the content type header 38504 * @return {Object} the parsed content-type 38505 * @private 38506 */ 38507 var parseContentType = function parseContentType(type) { 38508 var object = { type: '', parameters: {} }; 38509 var parameters = type.trim().split(';'); 38510 38511 // first parameter should always be content-type 38512 object.type = parameters.shift().trim(); 38513 parameters.forEach(function (parameter) { 38514 var pair = parameter.trim().split('='); 38515 38516 if (pair.length > 1) { 38517 var name = pair[0].replace(/"/g, '').trim(); 38518 var value = pair[1].replace(/"/g, '').trim(); 38519 38520 object.parameters[name] = value; 38521 } 38522 }); 38523 38524 return object; 38525 }; 38526 38527 /** 38528 * Replace the old apple-style `avc1.<dd>.<dd>` codec string with the standard 38529 * `avc1.<hhhhhh>` 38530 * 38531 * @param {Array} codecs an array of codec strings to fix 38532 * @return {Array} the translated codec array 38533 * @private 38534 */ 38535 var translateLegacyCodecs = function translateLegacyCodecs(codecs) { 38536 return codecs.map(function (codec) { 38537 return codec.replace(/avc1\.(\d+)\.(\d+)/i, function (orig, profile, avcLevel) { 38538 var profileHex = ('00' + Number(profile).toString(16)).slice(-2); 38539 var avcLevelHex = ('00' + Number(avcLevel).toString(16)).slice(-2); 38540 38541 return 'avc1.' + profileHex + '00' + avcLevelHex; 38542 }); 38543 }); 38544 }; 38545 38546 /** 38547 * @file virtual-source-buffer.js 38548 */ 38549 38550 // We create a wrapper around the SourceBuffer so that we can manage the 38551 // state of the `updating` property manually. We have to do this because 38552 // Firefox changes `updating` to false long before triggering `updateend` 38553 // events and that was causing strange problems in videojs-contrib-hls 38554 var makeWrappedSourceBuffer = function makeWrappedSourceBuffer(mediaSource, mimeType) { 38555 var sourceBuffer = mediaSource.addSourceBuffer(mimeType); 38556 var wrapper = Object.create(null); 38557 38558 wrapper.updating = false; 38559 wrapper.realBuffer_ = sourceBuffer; 38560 38561 var _loop = function _loop(key) { 38562 if (typeof sourceBuffer[key] === 'function') { 38563 wrapper[key] = function () { 38564 return sourceBuffer[key].apply(sourceBuffer, arguments); 38565 }; 38566 } else if (typeof wrapper[key] === 'undefined') { 38567 Object.defineProperty(wrapper, key, { 38568 get: function get$$1() { 38569 return sourceBuffer[key]; 38570 }, 38571 set: function set$$1(v) { 38572 return sourceBuffer[key] = v; 38573 } 38574 }); 38575 } 38576 }; 38577 38578 for (var key in sourceBuffer) { 38579 _loop(key); 38580 } 38581 38582 return wrapper; 38583 }; 38584 38585 /** 38586 * Returns a list of gops in the buffer that have a pts value of 3 seconds or more in 38587 * front of current time. 38588 * 38589 * @param {Array} buffer 38590 * The current buffer of gop information 38591 * @param {Player} player 38592 * The player instance 38593 * @param {Double} mapping 38594 * Offset to map display time to stream presentation time 38595 * @return {Array} 38596 * List of gops considered safe to append over 38597 */ 38598 var gopsSafeToAlignWith = function gopsSafeToAlignWith(buffer, player, mapping) { 38599 if (!player || !buffer.length) { 38600 return []; 38601 } 38602 38603 // pts value for current time + 3 seconds to give a bit more wiggle room 38604 var currentTimePts = Math.ceil((player.currentTime() - mapping + 3) * 90000); 38605 38606 var i = void 0; 38607 38608 for (i = 0; i < buffer.length; i++) { 38609 if (buffer[i].pts > currentTimePts) { 38610 break; 38611 } 38612 } 38613 38614 return buffer.slice(i); 38615 }; 38616 38617 /** 38618 * Appends gop information (timing and byteLength) received by the transmuxer for the 38619 * gops appended in the last call to appendBuffer 38620 * 38621 * @param {Array} buffer 38622 * The current buffer of gop information 38623 * @param {Array} gops 38624 * List of new gop information 38625 * @param {boolean} replace 38626 * If true, replace the buffer with the new gop information. If false, append the 38627 * new gop information to the buffer in the right location of time. 38628 * @return {Array} 38629 * Updated list of gop information 38630 */ 38631 var updateGopBuffer = function updateGopBuffer(buffer, gops, replace) { 38632 if (!gops.length) { 38633 return buffer; 38634 } 38635 38636 if (replace) { 38637 // If we are in safe append mode, then completely overwrite the gop buffer 38638 // with the most recent appeneded data. This will make sure that when appending 38639 // future segments, we only try to align with gops that are both ahead of current 38640 // time and in the last segment appended. 38641 return gops.slice(); 38642 } 38643 38644 var start = gops[0].pts; 38645 38646 var i = 0; 38647 38648 for (i; i < buffer.length; i++) { 38649 if (buffer[i].pts >= start) { 38650 break; 38651 } 38652 } 38653 38654 return buffer.slice(0, i).concat(gops); 38655 }; 38656 38657 /** 38658 * Removes gop information in buffer that overlaps with provided start and end 38659 * 38660 * @param {Array} buffer 38661 * The current buffer of gop information 38662 * @param {Double} start 38663 * position to start the remove at 38664 * @param {Double} end 38665 * position to end the remove at 38666 * @param {Double} mapping 38667 * Offset to map display time to stream presentation time 38668 */ 38669 var removeGopBuffer = function removeGopBuffer(buffer, start, end, mapping) { 38670 var startPts = Math.ceil((start - mapping) * 90000); 38671 var endPts = Math.ceil((end - mapping) * 90000); 38672 var updatedBuffer = buffer.slice(); 38673 38674 var i = buffer.length; 38675 38676 while (i--) { 38677 if (buffer[i].pts <= endPts) { 38678 break; 38679 } 38680 } 38681 38682 if (i === -1) { 38683 // no removal because end of remove range is before start of buffer 38684 return updatedBuffer; 38685 } 38686 38687 var j = i + 1; 38688 38689 while (j--) { 38690 if (buffer[j].pts <= startPts) { 38691 break; 38692 } 38693 } 38694 38695 // clamp remove range start to 0 index 38696 j = Math.max(j, 0); 38697 38698 updatedBuffer.splice(j, i - j + 1); 38699 38700 return updatedBuffer; 38701 }; 38702 38703 /** 38704 * VirtualSourceBuffers exist so that we can transmux non native formats 38705 * into a native format, but keep the same api as a native source buffer. 38706 * It creates a transmuxer, that works in its own thread (a web worker) and 38707 * that transmuxer muxes the data into a native format. VirtualSourceBuffer will 38708 * then send all of that data to the naive sourcebuffer so that it is 38709 * indestinguishable from a natively supported format. 38710 * 38711 * @param {HtmlMediaSource} mediaSource the parent mediaSource 38712 * @param {Array} codecs array of codecs that we will be dealing with 38713 * @class VirtualSourceBuffer 38714 * @extends video.js.EventTarget 38715 */ 38716 38717 var VirtualSourceBuffer = function (_videojs$EventTarget) { 38718 inherits$3(VirtualSourceBuffer, _videojs$EventTarget); 38719 38720 function VirtualSourceBuffer(mediaSource, codecs) { 38721 classCallCheck$3(this, VirtualSourceBuffer); 38722 38723 var _this = possibleConstructorReturn$3(this, (VirtualSourceBuffer.__proto__ || Object.getPrototypeOf(VirtualSourceBuffer)).call(this, videojs$1.EventTarget)); 38724 38725 _this.timestampOffset_ = 0; 38726 _this.pendingBuffers_ = []; 38727 _this.bufferUpdating_ = false; 38728 38729 _this.mediaSource_ = mediaSource; 38730 _this.codecs_ = codecs; 38731 _this.audioCodec_ = null; 38732 _this.videoCodec_ = null; 38733 _this.audioDisabled_ = false; 38734 _this.appendAudioInitSegment_ = true; 38735 _this.gopBuffer_ = []; 38736 _this.timeMapping_ = 0; 38737 _this.safeAppend_ = videojs$1.browser.IE_VERSION >= 11; 38738 38739 var options = { 38740 remux: false, 38741 alignGopsAtEnd: _this.safeAppend_ 38742 }; 38743 38744 _this.codecs_.forEach(function (codec) { 38745 if (isAudioCodec(codec)) { 38746 _this.audioCodec_ = codec; 38747 } else if (isVideoCodec(codec)) { 38748 _this.videoCodec_ = codec; 38749 } 38750 }); 38751 38752 // append muxed segments to their respective native buffers as 38753 // soon as they are available 38754 _this.transmuxer_ = new TransmuxWorker(); 38755 _this.transmuxer_.postMessage({ action: 'init', options: options }); 38756 38757 _this.transmuxer_.onmessage = function (event) { 38758 if (event.data.action === 'data') { 38759 return _this.data_(event); 38760 } 38761 38762 if (event.data.action === 'done') { 38763 return _this.done_(event); 38764 } 38765 38766 if (event.data.action === 'gopInfo') { 38767 return _this.appendGopInfo_(event); 38768 } 38769 }; 38770 38771 // this timestampOffset is a property with the side-effect of resetting 38772 // baseMediaDecodeTime in the transmuxer on the setter 38773 Object.defineProperty(_this, 'timestampOffset', { 38774 get: function get$$1() { 38775 return this.timestampOffset_; 38776 }, 38777 set: function set$$1(val) { 38778 if (typeof val === 'number' && val >= 0) { 38779 this.timestampOffset_ = val; 38780 this.appendAudioInitSegment_ = true; 38781 38782 // reset gop buffer on timestampoffset as this signals a change in timeline 38783 this.gopBuffer_.length = 0; 38784 this.timeMapping_ = 0; 38785 38786 // We have to tell the transmuxer to set the baseMediaDecodeTime to 38787 // the desired timestampOffset for the next segment 38788 this.transmuxer_.postMessage({ 38789 action: 'setTimestampOffset', 38790 timestampOffset: val 38791 }); 38792 } 38793 } 38794 }); 38795 38796 // setting the append window affects both source buffers 38797 Object.defineProperty(_this, 'appendWindowStart', { 38798 get: function get$$1() { 38799 return (this.videoBuffer_ || this.audioBuffer_).appendWindowStart; 38800 }, 38801 set: function set$$1(start) { 38802 if (this.videoBuffer_) { 38803 this.videoBuffer_.appendWindowStart = start; 38804 } 38805 if (this.audioBuffer_) { 38806 this.audioBuffer_.appendWindowStart = start; 38807 } 38808 } 38809 }); 38810 38811 // this buffer is "updating" if either of its native buffers are 38812 Object.defineProperty(_this, 'updating', { 38813 get: function get$$1() { 38814 return !!(this.bufferUpdating_ || !this.audioDisabled_ && this.audioBuffer_ && this.audioBuffer_.updating || this.videoBuffer_ && this.videoBuffer_.updating); 38815 } 38816 }); 38817 38818 // the buffered property is the intersection of the buffered 38819 // ranges of the native source buffers 38820 Object.defineProperty(_this, 'buffered', { 38821 get: function get$$1() { 38822 var start = null; 38823 var end = null; 38824 var arity = 0; 38825 var extents = []; 38826 var ranges = []; 38827 38828 // neither buffer has been created yet 38829 if (!this.videoBuffer_ && !this.audioBuffer_) { 38830 return videojs$1.createTimeRange(); 38831 } 38832 38833 // only one buffer is configured 38834 if (!this.videoBuffer_) { 38835 return this.audioBuffer_.buffered; 38836 } 38837 if (!this.audioBuffer_) { 38838 return this.videoBuffer_.buffered; 38839 } 38840 38841 // both buffers are configured 38842 if (this.audioDisabled_) { 38843 return this.videoBuffer_.buffered; 38844 } 38845 38846 // both buffers are empty 38847 if (this.videoBuffer_.buffered.length === 0 && this.audioBuffer_.buffered.length === 0) { 38848 return videojs$1.createTimeRange(); 38849 } 38850 38851 // Handle the case where we have both buffers and create an 38852 // intersection of the two 38853 var videoBuffered = this.videoBuffer_.buffered; 38854 var audioBuffered = this.audioBuffer_.buffered; 38855 var count = videoBuffered.length; 38856 38857 // A) Gather up all start and end times 38858 while (count--) { 38859 extents.push({ time: videoBuffered.start(count), type: 'start' }); 38860 extents.push({ time: videoBuffered.end(count), type: 'end' }); 38861 } 38862 count = audioBuffered.length; 38863 while (count--) { 38864 extents.push({ time: audioBuffered.start(count), type: 'start' }); 38865 extents.push({ time: audioBuffered.end(count), type: 'end' }); 38866 } 38867 // B) Sort them by time 38868 extents.sort(function (a, b) { 38869 return a.time - b.time; 38870 }); 38871 38872 // C) Go along one by one incrementing arity for start and decrementing 38873 // arity for ends 38874 for (count = 0; count < extents.length; count++) { 38875 if (extents[count].type === 'start') { 38876 arity++; 38877 38878 // D) If arity is ever incremented to 2 we are entering an 38879 // overlapping range 38880 if (arity === 2) { 38881 start = extents[count].time; 38882 } 38883 } else if (extents[count].type === 'end') { 38884 arity--; 38885 38886 // E) If arity is ever decremented to 1 we leaving an 38887 // overlapping range 38888 if (arity === 1) { 38889 end = extents[count].time; 38890 } 38891 } 38892 38893 // F) Record overlapping ranges 38894 if (start !== null && end !== null) { 38895 ranges.push([start, end]); 38896 start = null; 38897 end = null; 38898 } 38899 } 38900 38901 return videojs$1.createTimeRanges(ranges); 38902 } 38903 }); 38904 return _this; 38905 } 38906 38907 /** 38908 * When we get a data event from the transmuxer 38909 * we call this function and handle the data that 38910 * was sent to us 38911 * 38912 * @private 38913 * @param {Event} event the data event from the transmuxer 38914 */ 38915 38916 createClass$2(VirtualSourceBuffer, [{ 38917 key: 'data_', 38918 value: function data_(event) { 38919 var segment = event.data.segment; 38920 38921 // Cast ArrayBuffer to TypedArray 38922 segment.data = new Uint8Array(segment.data, event.data.byteOffset, event.data.byteLength); 38923 38924 segment.initSegment = new Uint8Array(segment.initSegment.data, segment.initSegment.byteOffset, segment.initSegment.byteLength); 38925 38926 createTextTracksIfNecessary(this, this.mediaSource_, segment); 38927 38928 // Add the segments to the pendingBuffers array 38929 this.pendingBuffers_.push(segment); 38930 return; 38931 } 38932 38933 /** 38934 * When we get a done event from the transmuxer 38935 * we call this function and we process all 38936 * of the pending data that we have been saving in the 38937 * data_ function 38938 * 38939 * @private 38940 * @param {Event} event the done event from the transmuxer 38941 */ 38942 38943 }, { 38944 key: 'done_', 38945 value: function done_(event) { 38946 // Don't process and append data if the mediaSource is closed 38947 if (this.mediaSource_.readyState === 'closed') { 38948 this.pendingBuffers_.length = 0; 38949 return; 38950 } 38951 38952 // All buffers should have been flushed from the muxer 38953 // start processing anything we have received 38954 this.processPendingSegments_(); 38955 return; 38956 } 38957 38958 /** 38959 * Create our internal native audio/video source buffers and add 38960 * event handlers to them with the following conditions: 38961 * 1. they do not already exist on the mediaSource 38962 * 2. this VSB has a codec for them 38963 * 38964 * @private 38965 */ 38966 38967 }, { 38968 key: 'createRealSourceBuffers_', 38969 value: function createRealSourceBuffers_() { 38970 var _this2 = this; 38971 38972 var types = ['audio', 'video']; 38973 38974 types.forEach(function (type) { 38975 // Don't create a SourceBuffer of this type if we don't have a 38976 // codec for it 38977 if (!_this2[type + 'Codec_']) { 38978 return; 38979 } 38980 38981 // Do nothing if a SourceBuffer of this type already exists 38982 if (_this2[type + 'Buffer_']) { 38983 return; 38984 } 38985 38986 var buffer = null; 38987 38988 // If the mediasource already has a SourceBuffer for the codec 38989 // use that 38990 if (_this2.mediaSource_[type + 'Buffer_']) { 38991 buffer = _this2.mediaSource_[type + 'Buffer_']; 38992 // In multiple audio track cases, the audio source buffer is disabled 38993 // on the main VirtualSourceBuffer by the HTMLMediaSource much earlier 38994 // than createRealSourceBuffers_ is called to create the second 38995 // VirtualSourceBuffer because that happens as a side-effect of 38996 // videojs-contrib-hls starting the audioSegmentLoader. As a result, 38997 // the audioBuffer is essentially "ownerless" and no one will toggle 38998 // the `updating` state back to false once the `updateend` event is received 38999 // 39000 // Setting `updating` to false manually will work around this 39001 // situation and allow work to continue 39002 buffer.updating = false; 39003 } else { 39004 var codecProperty = type + 'Codec_'; 39005 var mimeType = type + '/mp4;codecs="' + _this2[codecProperty] + '"'; 39006 39007 buffer = makeWrappedSourceBuffer(_this2.mediaSource_.nativeMediaSource_, mimeType); 39008 39009 _this2.mediaSource_[type + 'Buffer_'] = buffer; 39010 } 39011 39012 _this2[type + 'Buffer_'] = buffer; 39013 39014 // Wire up the events to the SourceBuffer 39015 ['update', 'updatestart', 'updateend'].forEach(function (event) { 39016 buffer.addEventListener(event, function () { 39017 // if audio is disabled 39018 if (type === 'audio' && _this2.audioDisabled_) { 39019 return; 39020 } 39021 39022 if (event === 'updateend') { 39023 _this2[type + 'Buffer_'].updating = false; 39024 } 39025 39026 var shouldTrigger = types.every(function (t) { 39027 // skip checking audio's updating status if audio 39028 // is not enabled 39029 if (t === 'audio' && _this2.audioDisabled_) { 39030 return true; 39031 } 39032 // if the other type if updating we don't trigger 39033 if (type !== t && _this2[t + 'Buffer_'] && _this2[t + 'Buffer_'].updating) { 39034 return false; 39035 } 39036 return true; 39037 }); 39038 39039 if (shouldTrigger) { 39040 return _this2.trigger(event); 39041 } 39042 }); 39043 }); 39044 }); 39045 } 39046 39047 /** 39048 * Emulate the native mediasource function, but our function will 39049 * send all of the proposed segments to the transmuxer so that we 39050 * can transmux them before we append them to our internal 39051 * native source buffers in the correct format. 39052 * 39053 * @link https://developer.mozilla.org/en-US/docs/Web/API/SourceBuffer/appendBuffer 39054 * @param {Uint8Array} segment the segment to append to the buffer 39055 */ 39056 39057 }, { 39058 key: 'appendBuffer', 39059 value: function appendBuffer(segment) { 39060 // Start the internal "updating" state 39061 this.bufferUpdating_ = true; 39062 39063 if (this.audioBuffer_ && this.audioBuffer_.buffered.length) { 39064 var audioBuffered = this.audioBuffer_.buffered; 39065 39066 this.transmuxer_.postMessage({ 39067 action: 'setAudioAppendStart', 39068 appendStart: audioBuffered.end(audioBuffered.length - 1) 39069 }); 39070 } 39071 39072 if (this.videoBuffer_) { 39073 this.transmuxer_.postMessage({ 39074 action: 'alignGopsWith', 39075 gopsToAlignWith: gopsSafeToAlignWith(this.gopBuffer_, this.mediaSource_.player_, this.timeMapping_) 39076 }); 39077 } 39078 39079 this.transmuxer_.postMessage({ 39080 action: 'push', 39081 // Send the typed-array of data as an ArrayBuffer so that 39082 // it can be sent as a "Transferable" and avoid the costly 39083 // memory copy 39084 data: segment.buffer, 39085 39086 // To recreate the original typed-array, we need information 39087 // about what portion of the ArrayBuffer it was a view into 39088 byteOffset: segment.byteOffset, 39089 byteLength: segment.byteLength 39090 }, [segment.buffer]); 39091 this.transmuxer_.postMessage({ action: 'flush' }); 39092 } 39093 39094 /** 39095 * Appends gop information (timing and byteLength) received by the transmuxer for the 39096 * gops appended in the last call to appendBuffer 39097 * 39098 * @param {Event} event 39099 * The gopInfo event from the transmuxer 39100 * @param {Array} event.data.gopInfo 39101 * List of gop info to append 39102 */ 39103 39104 }, { 39105 key: 'appendGopInfo_', 39106 value: function appendGopInfo_(event) { 39107 this.gopBuffer_ = updateGopBuffer(this.gopBuffer_, event.data.gopInfo, this.safeAppend_); 39108 } 39109 39110 /** 39111 * Emulate the native mediasource function and remove parts 39112 * of the buffer from any of our internal buffers that exist 39113 * 39114 * @link https://developer.mozilla.org/en-US/docs/Web/API/SourceBuffer/remove 39115 * @param {Double} start position to start the remove at 39116 * @param {Double} end position to end the remove at 39117 */ 39118 39119 }, { 39120 key: 'remove', 39121 value: function remove(start, end) { 39122 if (this.videoBuffer_) { 39123 this.videoBuffer_.updating = true; 39124 this.videoBuffer_.remove(start, end); 39125 this.gopBuffer_ = removeGopBuffer(this.gopBuffer_, start, end, this.timeMapping_); 39126 } 39127 if (!this.audioDisabled_ && this.audioBuffer_) { 39128 this.audioBuffer_.updating = true; 39129 this.audioBuffer_.remove(start, end); 39130 } 39131 39132 // Remove Metadata Cues (id3) 39133 removeCuesFromTrack(start, end, this.metadataTrack_); 39134 39135 // Remove Any Captions 39136 if (this.inbandTextTracks_) { 39137 for (var track in this.inbandTextTracks_) { 39138 removeCuesFromTrack(start, end, this.inbandTextTracks_[track]); 39139 } 39140 } 39141 } 39142 39143 /** 39144 * Process any segments that the muxer has output 39145 * Concatenate segments together based on type and append them into 39146 * their respective sourceBuffers 39147 * 39148 * @private 39149 */ 39150 39151 }, { 39152 key: 'processPendingSegments_', 39153 value: function processPendingSegments_() { 39154 var sortedSegments = { 39155 video: { 39156 segments: [], 39157 bytes: 0 39158 }, 39159 audio: { 39160 segments: [], 39161 bytes: 0 39162 }, 39163 captions: [], 39164 metadata: [] 39165 }; 39166 39167 // Sort segments into separate video/audio arrays and 39168 // keep track of their total byte lengths 39169 sortedSegments = this.pendingBuffers_.reduce(function (segmentObj, segment) { 39170 var type = segment.type; 39171 var data = segment.data; 39172 var initSegment = segment.initSegment; 39173 39174 segmentObj[type].segments.push(data); 39175 segmentObj[type].bytes += data.byteLength; 39176 39177 segmentObj[type].initSegment = initSegment; 39178 39179 // Gather any captions into a single array 39180 if (segment.captions) { 39181 segmentObj.captions = segmentObj.captions.concat(segment.captions); 39182 } 39183 39184 if (segment.info) { 39185 segmentObj[type].info = segment.info; 39186 } 39187 39188 // Gather any metadata into a single array 39189 if (segment.metadata) { 39190 segmentObj.metadata = segmentObj.metadata.concat(segment.metadata); 39191 } 39192 39193 return segmentObj; 39194 }, sortedSegments); 39195 39196 // Create the real source buffers if they don't exist by now since we 39197 // finally are sure what tracks are contained in the source 39198 if (!this.videoBuffer_ && !this.audioBuffer_) { 39199 // Remove any codecs that may have been specified by default but 39200 // are no longer applicable now 39201 if (sortedSegments.video.bytes === 0) { 39202 this.videoCodec_ = null; 39203 } 39204 if (sortedSegments.audio.bytes === 0) { 39205 this.audioCodec_ = null; 39206 } 39207 39208 this.createRealSourceBuffers_(); 39209 } 39210 39211 if (sortedSegments.audio.info) { 39212 this.mediaSource_.trigger({ type: 'audioinfo', info: sortedSegments.audio.info }); 39213 } 39214 if (sortedSegments.video.info) { 39215 this.mediaSource_.trigger({ type: 'videoinfo', info: sortedSegments.video.info }); 39216 } 39217 39218 if (this.appendAudioInitSegment_) { 39219 if (!this.audioDisabled_ && this.audioBuffer_) { 39220 sortedSegments.audio.segments.unshift(sortedSegments.audio.initSegment); 39221 sortedSegments.audio.bytes += sortedSegments.audio.initSegment.byteLength; 39222 } 39223 this.appendAudioInitSegment_ = false; 39224 } 39225 39226 var triggerUpdateend = false; 39227 39228 // Merge multiple video and audio segments into one and append 39229 if (this.videoBuffer_ && sortedSegments.video.bytes) { 39230 sortedSegments.video.segments.unshift(sortedSegments.video.initSegment); 39231 sortedSegments.video.bytes += sortedSegments.video.initSegment.byteLength; 39232 this.concatAndAppendSegments_(sortedSegments.video, this.videoBuffer_); 39233 // TODO: are video tracks the only ones with text tracks? 39234 addTextTrackData(this, sortedSegments.captions, sortedSegments.metadata); 39235 } else if (this.videoBuffer_ && (this.audioDisabled_ || !this.audioBuffer_)) { 39236 // The transmuxer did not return any bytes of video, meaning it was all trimmed 39237 // for gop alignment. Since we have a video buffer and audio is disabled, updateend 39238 // will never be triggered by this source buffer, which will cause contrib-hls 39239 // to be stuck forever waiting for updateend. If audio is not disabled, updateend 39240 // will be triggered by the audio buffer, which will be sent upwards since the video 39241 // buffer will not be in an updating state. 39242 triggerUpdateend = true; 39243 } 39244 39245 if (!this.audioDisabled_ && this.audioBuffer_) { 39246 this.concatAndAppendSegments_(sortedSegments.audio, this.audioBuffer_); 39247 } 39248 39249 this.pendingBuffers_.length = 0; 39250 39251 if (triggerUpdateend) { 39252 this.trigger('updateend'); 39253 } 39254 39255 // We are no longer in the internal "updating" state 39256 this.bufferUpdating_ = false; 39257 } 39258 39259 /** 39260 * Combine all segments into a single Uint8Array and then append them 39261 * to the destination buffer 39262 * 39263 * @param {Object} segmentObj 39264 * @param {SourceBuffer} destinationBuffer native source buffer to append data to 39265 * @private 39266 */ 39267 39268 }, { 39269 key: 'concatAndAppendSegments_', 39270 value: function concatAndAppendSegments_(segmentObj, destinationBuffer) { 39271 var offset = 0; 39272 var tempBuffer = void 0; 39273 39274 if (segmentObj.bytes) { 39275 tempBuffer = new Uint8Array(segmentObj.bytes); 39276 39277 // Combine the individual segments into one large typed-array 39278 segmentObj.segments.forEach(function (segment) { 39279 tempBuffer.set(segment, offset); 39280 offset += segment.byteLength; 39281 }); 39282 39283 try { 39284 destinationBuffer.updating = true; 39285 destinationBuffer.appendBuffer(tempBuffer); 39286 } catch (error) { 39287 if (this.mediaSource_.player_) { 39288 this.mediaSource_.player_.error({ 39289 code: -3, 39290 type: 'APPEND_BUFFER_ERR', 39291 message: error.message, 39292 originalError: error 39293 }); 39294 } 39295 } 39296 } 39297 } 39298 39299 /** 39300 * Emulate the native mediasource function. abort any soureBuffer 39301 * actions and throw out any un-appended data. 39302 * 39303 * @link https://developer.mozilla.org/en-US/docs/Web/API/SourceBuffer/abort 39304 */ 39305 39306 }, { 39307 key: 'abort', 39308 value: function abort() { 39309 if (this.videoBuffer_) { 39310 this.videoBuffer_.abort(); 39311 } 39312 if (!this.audioDisabled_ && this.audioBuffer_) { 39313 this.audioBuffer_.abort(); 39314 } 39315 if (this.transmuxer_) { 39316 this.transmuxer_.postMessage({ action: 'reset' }); 39317 } 39318 this.pendingBuffers_.length = 0; 39319 this.bufferUpdating_ = false; 39320 } 39321 }]); 39322 return VirtualSourceBuffer; 39323 }(videojs$1.EventTarget); 39324 39325 /** 39326 * @file html-media-source.js 39327 */ 39328 39329 /** 39330 * Our MediaSource implementation in HTML, mimics native 39331 * MediaSource where/if possible. 39332 * 39333 * @link https://developer.mozilla.org/en-US/docs/Web/API/MediaSource 39334 * @class HtmlMediaSource 39335 * @extends videojs.EventTarget 39336 */ 39337 39338 var HtmlMediaSource = function (_videojs$EventTarget) { 39339 inherits$3(HtmlMediaSource, _videojs$EventTarget); 39340 39341 function HtmlMediaSource() { 39342 classCallCheck$3(this, HtmlMediaSource); 39343 39344 var _this = possibleConstructorReturn$3(this, (HtmlMediaSource.__proto__ || Object.getPrototypeOf(HtmlMediaSource)).call(this)); 39345 39346 var property = void 0; 39347 39348 _this.nativeMediaSource_ = new window_1.MediaSource(); 39349 // delegate to the native MediaSource's methods by default 39350 for (property in _this.nativeMediaSource_) { 39351 if (!(property in HtmlMediaSource.prototype) && typeof _this.nativeMediaSource_[property] === 'function') { 39352 _this[property] = _this.nativeMediaSource_[property].bind(_this.nativeMediaSource_); 39353 } 39354 } 39355 39356 // emulate `duration` and `seekable` until seeking can be 39357 // handled uniformly for live streams 39358 // see https://github.com/w3c/media-source/issues/5 39359 _this.duration_ = NaN; 39360 Object.defineProperty(_this, 'duration', { 39361 get: function get$$1() { 39362 if (this.duration_ === Infinity) { 39363 return this.duration_; 39364 } 39365 return this.nativeMediaSource_.duration; 39366 }, 39367 set: function set$$1(duration) { 39368 this.duration_ = duration; 39369 if (duration !== Infinity) { 39370 this.nativeMediaSource_.duration = duration; 39371 return; 39372 } 39373 } 39374 }); 39375 Object.defineProperty(_this, 'seekable', { 39376 get: function get$$1() { 39377 if (this.duration_ === Infinity) { 39378 return videojs$1.createTimeRanges([[0, this.nativeMediaSource_.duration]]); 39379 } 39380 return this.nativeMediaSource_.seekable; 39381 } 39382 }); 39383 39384 Object.defineProperty(_this, 'readyState', { 39385 get: function get$$1() { 39386 return this.nativeMediaSource_.readyState; 39387 } 39388 }); 39389 39390 Object.defineProperty(_this, 'activeSourceBuffers', { 39391 get: function get$$1() { 39392 return this.activeSourceBuffers_; 39393 } 39394 }); 39395 39396 // the list of virtual and native SourceBuffers created by this 39397 // MediaSource 39398 _this.sourceBuffers = []; 39399 39400 _this.activeSourceBuffers_ = []; 39401 39402 /** 39403 * update the list of active source buffers based upon various 39404 * imformation from HLS and video.js 39405 * 39406 * @private 39407 */ 39408 _this.updateActiveSourceBuffers_ = function () { 39409 // Retain the reference but empty the array 39410 _this.activeSourceBuffers_.length = 0; 39411 39412 // If there is only one source buffer, then it will always be active and audio will 39413 // be disabled based on the codec of the source buffer 39414 if (_this.sourceBuffers.length === 1) { 39415 var sourceBuffer = _this.sourceBuffers[0]; 39416 39417 sourceBuffer.appendAudioInitSegment_ = true; 39418 sourceBuffer.audioDisabled_ = !sourceBuffer.audioCodec_; 39419 _this.activeSourceBuffers_.push(sourceBuffer); 39420 return; 39421 } 39422 39423 // There are 2 source buffers, a combined (possibly video only) source buffer and 39424 // and an audio only source buffer. 39425 // By default, the audio in the combined virtual source buffer is enabled 39426 // and the audio-only source buffer (if it exists) is disabled. 39427 var disableCombined = false; 39428 var disableAudioOnly = true; 39429 39430 // TODO: maybe we can store the sourcebuffers on the track objects? 39431 // safari may do something like this 39432 for (var i = 0; i < _this.player_.audioTracks().length; i++) { 39433 var track = _this.player_.audioTracks()[i]; 39434 39435 if (track.enabled && track.kind !== 'main') { 39436 // The enabled track is an alternate audio track so disable the audio in 39437 // the combined source buffer and enable the audio-only source buffer. 39438 disableCombined = true; 39439 disableAudioOnly = false; 39440 break; 39441 } 39442 } 39443 39444 _this.sourceBuffers.forEach(function (sourceBuffer, index) { 39445 /* eslinst-disable */ 39446 // TODO once codecs are required, we can switch to using the codecs to determine 39447 // what stream is the video stream, rather than relying on videoTracks 39448 /* eslinst-enable */ 39449 39450 sourceBuffer.appendAudioInitSegment_ = true; 39451 39452 if (sourceBuffer.videoCodec_ && sourceBuffer.audioCodec_) { 39453 // combined 39454 sourceBuffer.audioDisabled_ = disableCombined; 39455 } else if (sourceBuffer.videoCodec_ && !sourceBuffer.audioCodec_) { 39456 // If the "combined" source buffer is video only, then we do not want 39457 // disable the audio-only source buffer (this is mostly for demuxed 39458 // audio and video hls) 39459 sourceBuffer.audioDisabled_ = true; 39460 disableAudioOnly = false; 39461 } else if (!sourceBuffer.videoCodec_ && sourceBuffer.audioCodec_) { 39462 // audio only 39463 // In the case of audio only with alternate audio and disableAudioOnly is true 39464 // this means we want to disable the audio on the alternate audio sourcebuffer 39465 // but not the main "combined" source buffer. The "combined" source buffer is 39466 // always at index 0, so this ensures audio won't be disabled in both source 39467 // buffers. 39468 sourceBuffer.audioDisabled_ = index ? disableAudioOnly : !disableAudioOnly; 39469 if (sourceBuffer.audioDisabled_) { 39470 return; 39471 } 39472 } 39473 39474 _this.activeSourceBuffers_.push(sourceBuffer); 39475 }); 39476 }; 39477 39478 _this.onPlayerMediachange_ = function () { 39479 _this.sourceBuffers.forEach(function (sourceBuffer) { 39480 sourceBuffer.appendAudioInitSegment_ = true; 39481 }); 39482 }; 39483 39484 _this.onHlsReset_ = function () { 39485 _this.sourceBuffers.forEach(function (sourceBuffer) { 39486 if (sourceBuffer.transmuxer_) { 39487 sourceBuffer.transmuxer_.postMessage({ action: 'resetCaptions' }); 39488 } 39489 }); 39490 }; 39491 39492 _this.onHlsSegmentTimeMapping_ = function (event) { 39493 _this.sourceBuffers.forEach(function (buffer) { 39494 return buffer.timeMapping_ = event.mapping; 39495 }); 39496 }; 39497 39498 // Re-emit MediaSource events on the polyfill 39499 ['sourceopen', 'sourceclose', 'sourceended'].forEach(function (eventName) { 39500 this.nativeMediaSource_.addEventListener(eventName, this.trigger.bind(this)); 39501 }, _this); 39502 39503 // capture the associated player when the MediaSource is 39504 // successfully attached 39505 _this.on('sourceopen', function (event) { 39506 // Get the player this MediaSource is attached to 39507 var video = document_1.querySelector('[src="' + _this.url_ + '"]'); 39508 39509 if (!video) { 39510 return; 39511 } 39512 39513 _this.player_ = videojs$1(video.parentNode); 39514 39515 // hls-reset is fired by videojs.Hls on to the tech after the main SegmentLoader 39516 // resets its state and flushes the buffer 39517 _this.player_.tech_.on('hls-reset', _this.onHlsReset_); 39518 // hls-segment-time-mapping is fired by videojs.Hls on to the tech after the main 39519 // SegmentLoader inspects an MTS segment and has an accurate stream to display 39520 // time mapping 39521 _this.player_.tech_.on('hls-segment-time-mapping', _this.onHlsSegmentTimeMapping_); 39522 39523 if (_this.player_.audioTracks && _this.player_.audioTracks()) { 39524 _this.player_.audioTracks().on('change', _this.updateActiveSourceBuffers_); 39525 _this.player_.audioTracks().on('addtrack', _this.updateActiveSourceBuffers_); 39526 _this.player_.audioTracks().on('removetrack', _this.updateActiveSourceBuffers_); 39527 } 39528 39529 _this.player_.on('mediachange', _this.onPlayerMediachange_); 39530 }); 39531 39532 _this.on('sourceended', function (event) { 39533 var duration = durationOfVideo(_this.duration); 39534 39535 for (var i = 0; i < _this.sourceBuffers.length; i++) { 39536 var sourcebuffer = _this.sourceBuffers[i]; 39537 var cues = sourcebuffer.metadataTrack_ && sourcebuffer.metadataTrack_.cues; 39538 39539 if (cues && cues.length) { 39540 cues[cues.length - 1].endTime = duration; 39541 } 39542 } 39543 }); 39544 39545 // explicitly terminate any WebWorkers that were created 39546 // by SourceHandlers 39547 _this.on('sourceclose', function (event) { 39548 this.sourceBuffers.forEach(function (sourceBuffer) { 39549 if (sourceBuffer.transmuxer_) { 39550 sourceBuffer.transmuxer_.terminate(); 39551 } 39552 }); 39553 39554 this.sourceBuffers.length = 0; 39555 if (!this.player_) { 39556 return; 39557 } 39558 39559 if (this.player_.audioTracks && this.player_.audioTracks()) { 39560 this.player_.audioTracks().off('change', this.updateActiveSourceBuffers_); 39561 this.player_.audioTracks().off('addtrack', this.updateActiveSourceBuffers_); 39562 this.player_.audioTracks().off('removetrack', this.updateActiveSourceBuffers_); 39563 } 39564 39565 // We can only change this if the player hasn't been disposed of yet 39566 // because `off` eventually tries to use the el_ property. If it has 39567 // been disposed of, then don't worry about it because there are no 39568 // event handlers left to unbind anyway 39569 if (this.player_.el_) { 39570 this.player_.off('mediachange', this.onPlayerMediachange_); 39571 this.player_.tech_.off('hls-reset', this.onHlsReset_); 39572 this.player_.tech_.off('hls-segment-time-mapping', this.onHlsSegmentTimeMapping_); 39573 } 39574 }); 39575 return _this; 39576 } 39577 39578 /** 39579 * Add a range that that can now be seeked to. 39580 * 39581 * @param {Double} start where to start the addition 39582 * @param {Double} end where to end the addition 39583 * @private 39584 */ 39585 39586 createClass$2(HtmlMediaSource, [{ 39587 key: 'addSeekableRange_', 39588 value: function addSeekableRange_(start, end) { 39589 var error = void 0; 39590 39591 if (this.duration !== Infinity) { 39592 error = new Error('MediaSource.addSeekableRange() can only be invoked ' + 'when the duration is Infinity'); 39593 error.name = 'InvalidStateError'; 39594 error.code = 11; 39595 throw error; 39596 } 39597 39598 if (end > this.nativeMediaSource_.duration || isNaN(this.nativeMediaSource_.duration)) { 39599 this.nativeMediaSource_.duration = end; 39600 } 39601 } 39602 39603 /** 39604 * Add a source buffer to the media source. 39605 * 39606 * @link https://developer.mozilla.org/en-US/docs/Web/API/MediaSource/addSourceBuffer 39607 * @param {String} type the content-type of the content 39608 * @return {Object} the created source buffer 39609 */ 39610 39611 }, { 39612 key: 'addSourceBuffer', 39613 value: function addSourceBuffer(type) { 39614 var buffer = void 0; 39615 var parsedType = parseContentType(type); 39616 39617 // Create a VirtualSourceBuffer to transmux MPEG-2 transport 39618 // stream segments into fragmented MP4s 39619 if (/^(video|audio)\/mp2t$/i.test(parsedType.type)) { 39620 var codecs = []; 39621 39622 if (parsedType.parameters && parsedType.parameters.codecs) { 39623 codecs = parsedType.parameters.codecs.split(','); 39624 codecs = translateLegacyCodecs(codecs); 39625 codecs = codecs.filter(function (codec) { 39626 return isAudioCodec(codec) || isVideoCodec(codec); 39627 }); 39628 } 39629 39630 if (codecs.length === 0) { 39631 codecs = ['avc1.4d400d', 'mp4a.40.2']; 39632 } 39633 39634 buffer = new VirtualSourceBuffer(this, codecs); 39635 39636 if (this.sourceBuffers.length !== 0) { 39637 // If another VirtualSourceBuffer already exists, then we are creating a 39638 // SourceBuffer for an alternate audio track and therefore we know that 39639 // the source has both an audio and video track. 39640 // That means we should trigger the manual creation of the real 39641 // SourceBuffers instead of waiting for the transmuxer to return data 39642 this.sourceBuffers[0].createRealSourceBuffers_(); 39643 buffer.createRealSourceBuffers_(); 39644 39645 // Automatically disable the audio on the first source buffer if 39646 // a second source buffer is ever created 39647 this.sourceBuffers[0].audioDisabled_ = true; 39648 } 39649 } else { 39650 // delegate to the native implementation 39651 buffer = this.nativeMediaSource_.addSourceBuffer(type); 39652 } 39653 39654 this.sourceBuffers.push(buffer); 39655 return buffer; 39656 } 39657 }]); 39658 return HtmlMediaSource; 39659 }(videojs$1.EventTarget); 39660 39661 /** 39662 * @file videojs-contrib-media-sources.js 39663 */ 39664 var urlCount = 0; 39665 39666 // ------------ 39667 // Media Source 39668 // ------------ 39669 39670 // store references to the media sources so they can be connected 39671 // to a video element (a swf object) 39672 // TODO: can we store this somewhere local to this module? 39673 videojs$1.mediaSources = {}; 39674 39675 /** 39676 * Provide a method for a swf object to notify JS that a 39677 * media source is now open. 39678 * 39679 * @param {String} msObjectURL string referencing the MSE Object URL 39680 * @param {String} swfId the swf id 39681 */ 39682 var open = function open(msObjectURL, swfId) { 39683 var mediaSource = videojs$1.mediaSources[msObjectURL]; 39684 39685 if (mediaSource) { 39686 mediaSource.trigger({ type: 'sourceopen', swfId: swfId }); 39687 } else { 39688 throw new Error('Media Source not found (Video.js)'); 39689 } 39690 }; 39691 39692 /** 39693 * Check to see if the native MediaSource object exists and supports 39694 * an MP4 container with both H.264 video and AAC-LC audio. 39695 * 39696 * @return {Boolean} if native media sources are supported 39697 */ 39698 var supportsNativeMediaSources = function supportsNativeMediaSources() { 39699 return !!window_1.MediaSource && !!window_1.MediaSource.isTypeSupported && window_1.MediaSource.isTypeSupported('video/mp4;codecs="avc1.4d400d,mp4a.40.2"'); 39700 }; 39701 39702 /** 39703 * An emulation of the MediaSource API so that we can support 39704 * native and non-native functionality. returns an instance of 39705 * HtmlMediaSource. 39706 * 39707 * @link https://developer.mozilla.org/en-US/docs/Web/API/MediaSource/MediaSource 39708 */ 39709 var MediaSource = function MediaSource() { 39710 this.MediaSource = { 39711 open: open, 39712 supportsNativeMediaSources: supportsNativeMediaSources 39713 }; 39714 39715 if (supportsNativeMediaSources()) { 39716 return new HtmlMediaSource(); 39717 } 39718 39719 throw new Error('Cannot use create a virtual MediaSource for this video'); 39720 }; 39721 39722 MediaSource.open = open; 39723 MediaSource.supportsNativeMediaSources = supportsNativeMediaSources; 39724 39725 /** 39726 * A wrapper around the native URL for our MSE object 39727 * implementation, this object is exposed under videojs.URL 39728 * 39729 * @link https://developer.mozilla.org/en-US/docs/Web/API/URL/URL 39730 */ 39731 var URL$1 = { 39732 /** 39733 * A wrapper around the native createObjectURL for our objects. 39734 * This function maps a native or emulated mediaSource to a blob 39735 * url so that it can be loaded into video.js 39736 * 39737 * @link https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL 39738 * @param {MediaSource} object the object to create a blob url to 39739 */ 39740 createObjectURL: function createObjectURL(object) { 39741 var objectUrlPrefix = 'blob:vjs-media-source/'; 39742 var url = void 0; 39743 39744 // use the native MediaSource to generate an object URL 39745 if (object instanceof HtmlMediaSource) { 39746 url = window_1.URL.createObjectURL(object.nativeMediaSource_); 39747 object.url_ = url; 39748 return url; 39749 } 39750 // if the object isn't an emulated MediaSource, delegate to the 39751 // native implementation 39752 if (!(object instanceof HtmlMediaSource)) { 39753 url = window_1.URL.createObjectURL(object); 39754 object.url_ = url; 39755 return url; 39756 } 39757 39758 // build a URL that can be used to map back to the emulated 39759 // MediaSource 39760 url = objectUrlPrefix + urlCount; 39761 39762 urlCount++; 39763 39764 // setup the mapping back to object 39765 videojs$1.mediaSources[url] = object; 39766 39767 return url; 39768 } 39769 }; 39770 39771 videojs$1.MediaSource = MediaSource; 39772 videojs$1.URL = URL$1; 39773 39774 var EventTarget$1$1 = videojs$1.EventTarget, 39775 mergeOptions$2 = videojs$1.mergeOptions; 39776 39777 /** 39778 * Returns a new master manifest that is the result of merging an updated master manifest 39779 * into the original version. 39780 * 39781 * @param {Object} oldMaster 39782 * The old parsed mpd object 39783 * @param {Object} newMaster 39784 * The updated parsed mpd object 39785 * @return {Object} 39786 * A new object representing the original master manifest with the updated media 39787 * playlists merged in 39788 */ 39789 39790 var updateMaster$1 = function updateMaster$$1(oldMaster, newMaster) { 39791 var update = mergeOptions$2(oldMaster, { 39792 // These are top level properties that can be updated 39793 duration: newMaster.duration, 39794 minimumUpdatePeriod: newMaster.minimumUpdatePeriod 39795 }); 39796 39797 // First update the playlists in playlist list 39798 for (var i = 0; i < newMaster.playlists.length; i++) { 39799 var playlistUpdate = updateMaster(update, newMaster.playlists[i]); 39800 39801 if (playlistUpdate) { 39802 update = playlistUpdate; 39803 } 39804 } 39805 39806 // Then update media group playlists 39807 forEachMediaGroup(newMaster, function (properties, type, group, label) { 39808 if (properties.playlists && properties.playlists.length) { 39809 var uri = properties.playlists[0].uri; 39810 var _playlistUpdate = updateMaster(update, properties.playlists[0]); 39811 39812 if (_playlistUpdate) { 39813 update = _playlistUpdate; 39814 // update the playlist reference within media groups 39815 update.mediaGroups[type][group][label].playlists[0] = update.playlists[uri]; 39816 } 39817 } 39818 }); 39819 39820 return update; 39821 }; 39822 39823 var DashPlaylistLoader = function (_EventTarget) { 39824 inherits$3(DashPlaylistLoader, _EventTarget); 39825 39826 // DashPlaylistLoader must accept either a src url or a playlist because subsequent 39827 // playlist loader setups from media groups will expect to be able to pass a playlist 39828 // (since there aren't external URLs to media playlists with DASH) 39829 function DashPlaylistLoader(srcUrlOrPlaylist, hls, withCredentials, masterPlaylistLoader) { 39830 classCallCheck$3(this, DashPlaylistLoader); 39831 39832 var _this = possibleConstructorReturn$3(this, (DashPlaylistLoader.__proto__ || Object.getPrototypeOf(DashPlaylistLoader)).call(this)); 39833 39834 _this.hls_ = hls; 39835 _this.withCredentials = withCredentials; 39836 39837 if (!srcUrlOrPlaylist) { 39838 throw new Error('A non-empty playlist URL or playlist is required'); 39839 } 39840 39841 // event naming? 39842 _this.on('minimumUpdatePeriod', function () { 39843 _this.refreshXml_(); 39844 }); 39845 39846 // live playlist staleness timeout 39847 _this.on('mediaupdatetimeout', function () { 39848 _this.refreshMedia_(); 39849 }); 39850 39851 // initialize the loader state 39852 if (typeof srcUrlOrPlaylist === 'string') { 39853 _this.srcUrl = srcUrlOrPlaylist; 39854 _this.state = 'HAVE_NOTHING'; 39855 return possibleConstructorReturn$3(_this); 39856 } 39857 39858 _this.masterPlaylistLoader_ = masterPlaylistLoader; 39859 39860 _this.state = 'HAVE_METADATA'; 39861 _this.started = true; 39862 // we only should have one playlist so select it 39863 _this.media(srcUrlOrPlaylist); 39864 // trigger async to mimic behavior of HLS, where it must request a playlist 39865 window_1.setTimeout(function () { 39866 _this.trigger('loadedmetadata'); 39867 }, 0); 39868 return _this; 39869 } 39870 39871 createClass$2(DashPlaylistLoader, [{ 39872 key: 'dispose', 39873 value: function dispose() { 39874 this.stopRequest(); 39875 window_1.clearTimeout(this.mediaUpdateTimeout); 39876 } 39877 }, { 39878 key: 'stopRequest', 39879 value: function stopRequest() { 39880 if (this.request) { 39881 var oldRequest = this.request; 39882 39883 this.request = null; 39884 oldRequest.onreadystatechange = null; 39885 oldRequest.abort(); 39886 } 39887 } 39888 }, { 39889 key: 'media', 39890 value: function media(playlist) { 39891 // getter 39892 if (!playlist) { 39893 return this.media_; 39894 } 39895 39896 // setter 39897 if (this.state === 'HAVE_NOTHING') { 39898 throw new Error('Cannot switch media playlist from ' + this.state); 39899 } 39900 39901 var startingState = this.state; 39902 39903 // find the playlist object if the target playlist has been specified by URI 39904 if (typeof playlist === 'string') { 39905 if (!this.master.playlists[playlist]) { 39906 throw new Error('Unknown playlist URI: ' + playlist); 39907 } 39908 playlist = this.master.playlists[playlist]; 39909 } 39910 39911 var mediaChange = !this.media_ || playlist.uri !== this.media_.uri; 39912 39913 this.state = 'HAVE_METADATA'; 39914 39915 // switching to the active playlist is a no-op 39916 if (!mediaChange) { 39917 return; 39918 } 39919 39920 // switching from an already loaded playlist 39921 if (this.media_) { 39922 this.trigger('mediachanging'); 39923 } 39924 39925 this.media_ = playlist; 39926 39927 this.refreshMedia_(); 39928 39929 // trigger media change if the active media has been updated 39930 if (startingState !== 'HAVE_MASTER') { 39931 this.trigger('mediachange'); 39932 } 39933 } 39934 }, { 39935 key: 'pause', 39936 value: function pause() { 39937 this.stopRequest(); 39938 if (this.state === 'HAVE_NOTHING') { 39939 // If we pause the loader before any data has been retrieved, its as if we never 39940 // started, so reset to an unstarted state. 39941 this.started = false; 39942 } 39943 } 39944 }, { 39945 key: 'load', 39946 value: function load() { 39947 // because the playlists are internal to the manifest, load should either load the 39948 // main manifest, or do nothing but trigger an event 39949 if (!this.started) { 39950 this.start(); 39951 return; 39952 } 39953 39954 this.trigger('loadedplaylist'); 39955 } 39956 39957 /** 39958 * Parses the master xml string and updates playlist uri references 39959 * 39960 * @return {Object} 39961 * The parsed mpd manifest object 39962 */ 39963 39964 }, { 39965 key: 'parseMasterXml', 39966 value: function parseMasterXml() { 39967 var master = parse(this.masterXml_, { 39968 manifestUri: this.srcUrl, 39969 clientOffset: this.clientOffset_ 39970 }); 39971 39972 master.uri = this.srcUrl; 39973 39974 // Set up phony URIs for the playlists since we won't have external URIs for DASH 39975 // but reference playlists by their URI throughout the project 39976 // TODO: Should we create the dummy uris in mpd-parser as well (leaning towards yes). 39977 for (var i = 0; i < master.playlists.length; i++) { 39978 var phonyUri = 'placeholder-uri-' + i; 39979 39980 master.playlists[i].uri = phonyUri; 39981 // set up by URI references 39982 master.playlists[phonyUri] = master.playlists[i]; 39983 } 39984 39985 // set up phony URIs for the media group playlists since we won't have external 39986 // URIs for DASH but reference playlists by their URI throughout the project 39987 forEachMediaGroup(master, function (properties, mediaType, groupKey, labelKey) { 39988 if (properties.playlists && properties.playlists.length) { 39989 var _phonyUri = 'placeholder-uri-' + mediaType + '-' + groupKey + '-' + labelKey; 39990 39991 properties.playlists[0].uri = _phonyUri; 39992 // setup URI references 39993 master.playlists[_phonyUri] = properties.playlists[0]; 39994 } 39995 }); 39996 39997 setupMediaPlaylists(master); 39998 resolveMediaGroupUris(master); 39999 40000 return master; 40001 } 40002 }, { 40003 key: 'start', 40004 value: function start() { 40005 var _this2 = this; 40006 40007 this.started = true; 40008 40009 // request the specified URL 40010 this.request = this.hls_.xhr({ 40011 uri: this.srcUrl, 40012 withCredentials: this.withCredentials 40013 }, function (error, req) { 40014 // disposed 40015 if (!_this2.request) { 40016 return; 40017 } 40018 40019 // clear the loader's request reference 40020 _this2.request = null; 40021 40022 if (error) { 40023 _this2.error = { 40024 status: req.status, 40025 message: 'DASH playlist request error at URL: ' + _this2.srcUrl, 40026 responseText: req.responseText, 40027 // MEDIA_ERR_NETWORK 40028 code: 2 40029 }; 40030 if (_this2.state === 'HAVE_NOTHING') { 40031 _this2.started = false; 40032 } 40033 return _this2.trigger('error'); 40034 } 40035 40036 _this2.masterXml_ = req.responseText; 40037 40038 if (req.responseHeaders && req.responseHeaders.date) { 40039 _this2.masterLoaded_ = Date.parse(req.responseHeaders.date); 40040 } else { 40041 _this2.masterLoaded_ = Date.now(); 40042 } 40043 40044 _this2.syncClientServerClock_(_this2.onClientServerClockSync_.bind(_this2)); 40045 }); 40046 } 40047 40048 /** 40049 * Parses the master xml for UTCTiming node to sync the client clock to the server 40050 * clock. If the UTCTiming node requires a HEAD or GET request, that request is made. 40051 * 40052 * @param {Function} done 40053 * Function to call when clock sync has completed 40054 */ 40055 40056 }, { 40057 key: 'syncClientServerClock_', 40058 value: function syncClientServerClock_(done) { 40059 var _this3 = this; 40060 40061 var utcTiming = parseUTCTiming(this.masterXml_); 40062 40063 // No UTCTiming element found in the mpd. Use Date header from mpd request as the 40064 // server clock 40065 if (utcTiming === null) { 40066 this.clientOffset_ = this.masterLoaded_ - Date.now(); 40067 return done(); 40068 } 40069 40070 if (utcTiming.method === 'DIRECT') { 40071 this.clientOffset_ = utcTiming.value - Date.now(); 40072 return done(); 40073 } 40074 40075 this.request = this.hls_.xhr({ 40076 uri: resolveUrl$1(this.srcUrl, utcTiming.value), 40077 method: utcTiming.method, 40078 withCredentials: this.withCredentials 40079 }, function (error, req) { 40080 // disposed 40081 if (!_this3.request) { 40082 return; 40083 } 40084 40085 if (error) { 40086 // sync request failed, fall back to using date header from mpd 40087 // TODO: log warning 40088 _this3.clientOffset_ = _this3.masterLoaded_ - Date.now(); 40089 return done(); 40090 } 40091 40092 var serverTime = void 0; 40093 40094 if (utcTiming.method === 'HEAD') { 40095 if (!req.responseHeaders || !req.responseHeaders.date) { 40096 // expected date header not preset, fall back to using date header from mpd 40097 // TODO: log warning 40098 serverTime = _this3.masterLoaded_; 40099 } else { 40100 serverTime = Date.parse(req.responseHeaders.date); 40101 } 40102 } else { 40103 serverTime = Date.parse(req.responseText); 40104 } 40105 40106 _this3.clientOffset_ = serverTime - Date.now(); 40107 40108 done(); 40109 }); 40110 } 40111 40112 /** 40113 * Handler for after client/server clock synchronization has happened. Sets up 40114 * xml refresh timer if specificed by the manifest. 40115 */ 40116 40117 }, { 40118 key: 'onClientServerClockSync_', 40119 value: function onClientServerClockSync_() { 40120 var _this4 = this; 40121 40122 this.master = this.parseMasterXml(); 40123 40124 this.state = 'HAVE_MASTER'; 40125 40126 this.trigger('loadedplaylist'); 40127 40128 if (!this.media_) { 40129 // no media playlist was specifically selected so start 40130 // from the first listed one 40131 this.media(this.master.playlists[0]); 40132 } 40133 // trigger loadedmetadata to resolve setup of media groups 40134 // trigger async to mimic behavior of HLS, where it must request a playlist 40135 window_1.setTimeout(function () { 40136 _this4.trigger('loadedmetadata'); 40137 }, 0); 40138 40139 // TODO: minimumUpdatePeriod can have a value of 0. Currently the manifest will not 40140 // be refreshed when this is the case. The inter-op guide says that when the 40141 // minimumUpdatePeriod is 0, the manifest should outline all currently available 40142 // segments, but future segments may require an update. I think a good solution 40143 // would be to update the manifest at the same rate that the media playlists 40144 // are "refreshed", i.e. every targetDuration. 40145 if (this.master.minimumUpdatePeriod) { 40146 window_1.setTimeout(function () { 40147 _this4.trigger('minimumUpdatePeriod'); 40148 }, this.master.minimumUpdatePeriod); 40149 } 40150 } 40151 40152 /** 40153 * Sends request to refresh the master xml and updates the parsed master manifest 40154 * TODO: Does the client offset need to be recalculated when the xml is refreshed? 40155 */ 40156 40157 }, { 40158 key: 'refreshXml_', 40159 value: function refreshXml_() { 40160 var _this5 = this; 40161 40162 this.request = this.hls_.xhr({ 40163 uri: this.srcUrl, 40164 withCredentials: this.withCredentials 40165 }, function (error, req) { 40166 // disposed 40167 if (!_this5.request) { 40168 return; 40169 } 40170 40171 // clear the loader's request reference 40172 _this5.request = null; 40173 40174 if (error) { 40175 _this5.error = { 40176 status: req.status, 40177 message: 'DASH playlist request error at URL: ' + _this5.srcUrl, 40178 responseText: req.responseText, 40179 // MEDIA_ERR_NETWORK 40180 code: 2 40181 }; 40182 if (_this5.state === 'HAVE_NOTHING') { 40183 _this5.started = false; 40184 } 40185 return _this5.trigger('error'); 40186 } 40187 40188 _this5.masterXml_ = req.responseText; 40189 40190 var newMaster = _this5.parseMasterXml(); 40191 40192 _this5.master = updateMaster$1(_this5.master, newMaster); 40193 40194 window_1.setTimeout(function () { 40195 _this5.trigger('minimumUpdatePeriod'); 40196 }, _this5.master.minimumUpdatePeriod); 40197 }); 40198 } 40199 40200 /** 40201 * Refreshes the media playlist by re-parsing the master xml and updating playlist 40202 * references. If this is an alternate loader, the updated parsed manifest is retrieved 40203 * from the master loader. 40204 */ 40205 40206 }, { 40207 key: 'refreshMedia_', 40208 value: function refreshMedia_() { 40209 var _this6 = this; 40210 40211 var oldMaster = void 0; 40212 var newMaster = void 0; 40213 40214 if (this.masterPlaylistLoader_) { 40215 oldMaster = this.masterPlaylistLoader_.master; 40216 newMaster = this.masterPlaylistLoader_.parseMasterXml(); 40217 } else { 40218 oldMaster = this.master; 40219 newMaster = this.parseMasterXml(); 40220 } 40221 40222 var updatedMaster = updateMaster$1(oldMaster, newMaster); 40223 40224 if (updatedMaster) { 40225 if (this.masterPlaylistLoader_) { 40226 this.masterPlaylistLoader_.master = updatedMaster; 40227 } else { 40228 this.master = updatedMaster; 40229 } 40230 this.media_ = updatedMaster.playlists[this.media_.uri]; 40231 } else { 40232 this.trigger('playlistunchanged'); 40233 } 40234 40235 if (!this.media().endList) { 40236 this.mediaUpdateTimeout = window_1.setTimeout(function () { 40237 _this6.trigger('mediaupdatetimeout'); 40238 }, refreshDelay(this.media(), !!updatedMaster)); 40239 } 40240 40241 this.trigger('loadedplaylist'); 40242 } 40243 }]); 40244 return DashPlaylistLoader; 40245 }(EventTarget$1$1); 40246 40247 var logger = function logger(source) { 40248 if (videojs$1.log.debug) { 40249 return videojs$1.log.debug.bind(videojs$1, 'VHS:', source + ' >'); 40250 } 40251 40252 return function () {}; 40253 }; 40254 40255 function noop$1() {} 40256 40257 /** 40258 * @file source-updater.js 40259 */ 40260 40261 /** 40262 * A queue of callbacks to be serialized and applied when a 40263 * MediaSource and its associated SourceBuffers are not in the 40264 * updating state. It is used by the segment loader to update the 40265 * underlying SourceBuffers when new data is loaded, for instance. 40266 * 40267 * @class SourceUpdater 40268 * @param {MediaSource} mediaSource the MediaSource to create the 40269 * SourceBuffer from 40270 * @param {String} mimeType the desired MIME type of the underlying 40271 * SourceBuffer 40272 * @param {Object} sourceBufferEmitter an event emitter that fires when a source buffer is 40273 * added to the media source 40274 */ 40275 40276 var SourceUpdater = function () { 40277 function SourceUpdater(mediaSource, mimeType, type, sourceBufferEmitter) { 40278 classCallCheck$3(this, SourceUpdater); 40279 40280 this.callbacks_ = []; 40281 this.pendingCallback_ = null; 40282 this.timestampOffset_ = 0; 40283 this.mediaSource = mediaSource; 40284 this.processedAppend_ = false; 40285 this.type_ = type; 40286 this.mimeType_ = mimeType; 40287 this.logger_ = logger('SourceUpdater[' + type + '][' + mimeType + ']'); 40288 40289 if (mediaSource.readyState === 'closed') { 40290 mediaSource.addEventListener('sourceopen', this.createSourceBuffer_.bind(this, mimeType, sourceBufferEmitter)); 40291 } else { 40292 this.createSourceBuffer_(mimeType, sourceBufferEmitter); 40293 } 40294 } 40295 40296 createClass$2(SourceUpdater, [{ 40297 key: 'createSourceBuffer_', 40298 value: function createSourceBuffer_(mimeType, sourceBufferEmitter) { 40299 var _this = this; 40300 40301 this.sourceBuffer_ = this.mediaSource.addSourceBuffer(mimeType); 40302 40303 this.logger_('created SourceBuffer'); 40304 40305 if (sourceBufferEmitter) { 40306 sourceBufferEmitter.trigger('sourcebufferadded'); 40307 40308 if (this.mediaSource.sourceBuffers.length < 2) { 40309 // There's another source buffer we must wait for before we can start updating 40310 // our own (or else we can get into a bad state, i.e., appending video/audio data 40311 // before the other video/audio source buffer is available and leading to a video 40312 // or audio only buffer). 40313 sourceBufferEmitter.on('sourcebufferadded', function () { 40314 _this.start_(); 40315 }); 40316 return; 40317 } 40318 } 40319 40320 this.start_(); 40321 } 40322 }, { 40323 key: 'start_', 40324 value: function start_() { 40325 var _this2 = this; 40326 40327 this.started_ = true; 40328 40329 // run completion handlers and process callbacks as updateend 40330 // events fire 40331 this.onUpdateendCallback_ = function () { 40332 var pendingCallback = _this2.pendingCallback_; 40333 40334 _this2.pendingCallback_ = null; 40335 40336 _this2.logger_('buffered [' + printableRange(_this2.buffered()) + ']'); 40337 40338 if (pendingCallback) { 40339 pendingCallback(); 40340 } 40341 40342 _this2.runCallback_(); 40343 }; 40344 40345 this.sourceBuffer_.addEventListener('updateend', this.onUpdateendCallback_); 40346 40347 this.runCallback_(); 40348 } 40349 40350 /** 40351 * Aborts the current segment and resets the segment parser. 40352 * 40353 * @param {Function} done function to call when done 40354 * @see http://w3c.github.io/media-source/#widl-SourceBuffer-abort-void 40355 */ 40356 40357 }, { 40358 key: 'abort', 40359 value: function abort(done) { 40360 var _this3 = this; 40361 40362 if (this.processedAppend_) { 40363 this.queueCallback_(function () { 40364 _this3.sourceBuffer_.abort(); 40365 }, done); 40366 } 40367 } 40368 40369 /** 40370 * Queue an update to append an ArrayBuffer. 40371 * 40372 * @param {ArrayBuffer} bytes 40373 * @param {Function} done the function to call when done 40374 * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-appendBuffer-void-ArrayBuffer-data 40375 */ 40376 40377 }, { 40378 key: 'appendBuffer', 40379 value: function appendBuffer(bytes, done) { 40380 var _this4 = this; 40381 40382 this.processedAppend_ = true; 40383 this.queueCallback_(function () { 40384 _this4.sourceBuffer_.appendBuffer(bytes); 40385 }, done); 40386 } 40387 40388 /** 40389 * Indicates what TimeRanges are buffered in the managed SourceBuffer. 40390 * 40391 * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-buffered 40392 */ 40393 40394 }, { 40395 key: 'buffered', 40396 value: function buffered() { 40397 if (!this.sourceBuffer_) { 40398 return videojs$1.createTimeRanges(); 40399 } 40400 return this.sourceBuffer_.buffered; 40401 } 40402 40403 /** 40404 * Queue an update to remove a time range from the buffer. 40405 * 40406 * @param {Number} start where to start the removal 40407 * @param {Number} end where to end the removal 40408 * @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-remove-void-double-start-unrestricted-double-end 40409 */ 40410 40411 }, { 40412 key: 'remove', 40413 value: function remove(start, end) { 40414 var _this5 = this; 40415 40416 if (this.processedAppend_) { 40417 this.queueCallback_(function () { 40418 _this5.logger_('remove [' + start + ' => ' + end + ']'); 40419 _this5.sourceBuffer_.remove(start, end); 40420 }, noop$1); 40421 } 40422 } 40423 40424 /** 40425 * Whether the underlying sourceBuffer is updating or not 40426 * 40427 * @return {Boolean} the updating status of the SourceBuffer 40428 */ 40429 40430 }, { 40431 key: 'updating', 40432 value: function updating() { 40433 return !this.sourceBuffer_ || this.sourceBuffer_.updating || this.pendingCallback_; 40434 } 40435 40436 /** 40437 * Set/get the timestampoffset on the SourceBuffer 40438 * 40439 * @return {Number} the timestamp offset 40440 */ 40441 40442 }, { 40443 key: 'timestampOffset', 40444 value: function timestampOffset(offset) { 40445 var _this6 = this; 40446 40447 if (typeof offset !== 'undefined') { 40448 this.queueCallback_(function () { 40449 _this6.sourceBuffer_.timestampOffset = offset; 40450 }); 40451 this.timestampOffset_ = offset; 40452 } 40453 return this.timestampOffset_; 40454 } 40455 40456 /** 40457 * Queue a callback to run 40458 */ 40459 40460 }, { 40461 key: 'queueCallback_', 40462 value: function queueCallback_(callback, done) { 40463 this.callbacks_.push([callback.bind(this), done]); 40464 this.runCallback_(); 40465 } 40466 40467 /** 40468 * Run a queued callback 40469 */ 40470 40471 }, { 40472 key: 'runCallback_', 40473 value: function runCallback_() { 40474 var callbacks = void 0; 40475 40476 if (!this.updating() && this.callbacks_.length && this.started_) { 40477 callbacks = this.callbacks_.shift(); 40478 this.pendingCallback_ = callbacks[1]; 40479 callbacks[0](); 40480 } 40481 } 40482 40483 /** 40484 * dispose of the source updater and the underlying sourceBuffer 40485 */ 40486 40487 }, { 40488 key: 'dispose', 40489 value: function dispose() { 40490 this.sourceBuffer_.removeEventListener('updateend', this.onUpdateendCallback_); 40491 if (this.sourceBuffer_ && this.mediaSource.readyState === 'open') { 40492 this.sourceBuffer_.abort(); 40493 } 40494 } 40495 }]); 40496 return SourceUpdater; 40497 }(); 40498 40499 var Config = { 40500 GOAL_BUFFER_LENGTH: 30, 40501 MAX_GOAL_BUFFER_LENGTH: 60, 40502 GOAL_BUFFER_LENGTH_RATE: 1, 40503 // A fudge factor to apply to advertised playlist bitrates to account for 40504 // temporary flucations in client bandwidth 40505 BANDWIDTH_VARIANCE: 1.2, 40506 // How much of the buffer must be filled before we consider upswitching 40507 BUFFER_LOW_WATER_LINE: 0, 40508 MAX_BUFFER_LOW_WATER_LINE: 30, 40509 BUFFER_LOW_WATER_LINE_RATE: 1 40510 }; 40511 40512 var REQUEST_ERRORS = { 40513 FAILURE: 2, 40514 TIMEOUT: -101, 40515 ABORTED: -102 40516 }; 40517 40518 /** 40519 * Turns segment byterange into a string suitable for use in 40520 * HTTP Range requests 40521 * 40522 * @param {Object} byterange - an object with two values defining the start and end 40523 * of a byte-range 40524 */ 40525 var byterangeStr = function byterangeStr(byterange) { 40526 var byterangeStart = void 0; 40527 var byterangeEnd = void 0; 40528 40529 // `byterangeEnd` is one less than `offset + length` because the HTTP range 40530 // header uses inclusive ranges 40531 byterangeEnd = byterange.offset + byterange.length - 1; 40532 byterangeStart = byterange.offset; 40533 return 'bytes=' + byterangeStart + '-' + byterangeEnd; 40534 }; 40535 40536 /** 40537 * Defines headers for use in the xhr request for a particular segment. 40538 * 40539 * @param {Object} segment - a simplified copy of the segmentInfo object 40540 * from SegmentLoader 40541 */ 40542 var segmentXhrHeaders = function segmentXhrHeaders(segment) { 40543 var headers = {}; 40544 40545 if (segment.byterange) { 40546 headers.Range = byterangeStr(segment.byterange); 40547 } 40548 return headers; 40549 }; 40550 40551 /** 40552 * Abort all requests 40553 * 40554 * @param {Object} activeXhrs - an object that tracks all XHR requests 40555 */ 40556 var abortAll = function abortAll(activeXhrs) {
vendor: 4,629 bytes, lines 40557-40704
40557 activeXhrs.forEach(function (xhr) { 40558 xhr.abort(); 40559 }); 40560 }; 40561 40562 /** 40563 * Gather important bandwidth stats once a request has completed 40564 * 40565 * @param {Object} request - the XHR request from which to gather stats 40566 */ 40567 var getRequestStats = function getRequestStats(request) { 40568 return { 40569 bandwidth: request.bandwidth, 40570 bytesReceived: request.bytesReceived || 0, 40571 roundTripTime: request.roundTripTime || 0 40572 }; 40573 }; 40574 40575 /** 40576 * If possible gather bandwidth stats as a request is in 40577 * progress 40578 * 40579 * @param {Event} progressEvent - an event object from an XHR's progress event 40580 */ 40581 var getProgressStats = function getProgressStats(progressEvent) { 40582 var request = progressEvent.target; 40583 var roundTripTime = Date.now() - request.requestTime; 40584 var stats = { 40585 bandwidth: Infinity, 40586 bytesReceived: 0, 40587 roundTripTime: roundTripTime || 0 40588 }; 40589 40590 stats.bytesReceived = progressEvent.loaded; 40591 // This can result in Infinity if stats.roundTripTime is 0 but that is ok 40592 // because we should only use bandwidth stats on progress to determine when 40593 // abort a request early due to insufficient bandwidth 40594 stats.bandwidth = Math.floor(stats.bytesReceived / stats.roundTripTime * 8 * 1000); 40595 40596 return stats; 40597 }; 40598 40599 /** 40600 * Handle all error conditions in one place and return an object 40601 * with all the information 40602 * 40603 * @param {Error|null} error - if non-null signals an error occured with the XHR 40604 * @param {Object} request - the XHR request that possibly generated the error 40605 */ 40606 var handleErrors = function handleErrors(error, request) { 40607 if (request.timedout) { 40608 return { 40609 status: request.status, 40610 message: 'HLS request timed-out at URL: ' + request.uri, 40611 code: REQUEST_ERRORS.TIMEOUT, 40612 xhr: request 40613 }; 40614 } 40615 40616 if (request.aborted) { 40617 return { 40618 status: request.status, 40619 message: 'HLS request aborted at URL: ' + request.uri, 40620 code: REQUEST_ERRORS.ABORTED, 40621 xhr: request 40622 }; 40623 } 40624 40625 if (error) { 40626 return { 40627 status: request.status, 40628 message: 'HLS request errored at URL: ' + request.uri, 40629 code: REQUEST_ERRORS.FAILURE, 40630 xhr: request 40631 }; 40632 } 40633 40634 return null; 40635 }; 40636 40637 /** 40638 * Handle responses for key data and convert the key data to the correct format 40639 * for the decryption step later 40640 * 40641 * @param {Object} segment - a simplified copy of the segmentInfo object 40642 * from SegmentLoader 40643 * @param {Function} finishProcessingFn - a callback to execute to continue processing 40644 * this request 40645 */ 40646 var handleKeyResponse = function handleKeyResponse(segment, finishProcessingFn) { 40647 return function (error, request) { 40648 var response = request.response; 40649 var errorObj = handleErrors(error, request); 40650 40651 if (errorObj) { 40652 return finishProcessingFn(errorObj, segment); 40653 } 40654 40655 if (response.byteLength !== 16) { 40656 return finishProcessingFn({ 40657 status: request.status, 40658 message: 'Invalid HLS key at URL: ' + request.uri, 40659 code: REQUEST_ERRORS.FAILURE, 40660 xhr: request 40661 }, segment); 40662 } 40663 40664 var view = new DataView(response); 40665 40666 segment.key.bytes = new Uint32Array([view.getUint32(0), view.getUint32(4), view.getUint32(8), view.getUint32(12)]); 40667 return finishProcessingFn(null, segment); 40668 }; 40669 }; 40670 40671 /** 40672 * Handle init-segment responses 40673 * 40674 * @param {Object} segment - a simplified copy of the segmentInfo object 40675 * from SegmentLoader 40676 * @param {Function} finishProcessingFn - a callback to execute to continue processing 40677 * this request 40678 */ 40679 var handleInitSegmentResponse = function handleInitSegmentResponse(segment, finishProcessingFn) { 40680 return function (error, request) { 40681 var response = request.response; 40682 var errorObj = handleErrors(error, request); 40683 40684 if (errorObj) { 40685 return finishProcessingFn(errorObj, segment); 40686 } 40687 40688 // stop processing if received empty content 40689 if (response.byteLength === 0) { 40690 return finishProcessingFn({ 40691 status: request.status, 40692 message: 'Empty HLS segment content at URL: ' + request.uri, 40693 code: REQUEST_ERRORS.FAILURE, 40694 xhr: request 40695 }, segment); 40696 } 40697 40698 segment.map.bytes = new Uint8Array(request.response); 40699 return finishProcessingFn(null, segment); 40700 }; 40701 }; 40702 40703 /** 40704 * Response handler for segment-requests being sure to set the correct
vendor: 207,496 bytes, lines 40705-46609
40705 * property depending on whether the segment is encryped or not 40706 * Also records and keeps track of stats that are used for ABR purposes 40707 * 40708 * @param {Object} segment - a simplified copy of the segmentInfo object 40709 * from SegmentLoader 40710 * @param {Function} finishProcessingFn - a callback to execute to continue processing 40711 * this request 40712 */ 40713 var handleSegmentResponse = function handleSegmentResponse(segment, finishProcessingFn) { 40714 return function (error, request) { 40715 var response = request.response; 40716 var errorObj = handleErrors(error, request); 40717 40718 if (errorObj) { 40719 return finishProcessingFn(errorObj, segment); 40720 } 40721 40722 // stop processing if received empty content 40723 if (response.byteLength === 0) { 40724 return finishProcessingFn({ 40725 status: request.status, 40726 message: 'Empty HLS segment content at URL: ' + request.uri, 40727 code: REQUEST_ERRORS.FAILURE, 40728 xhr: request 40729 }, segment); 40730 } 40731 40732 segment.stats = getRequestStats(request); 40733 40734 if (segment.key) { 40735 segment.encryptedBytes = new Uint8Array(request.response); 40736 } else { 40737 segment.bytes = new Uint8Array(request.response); 40738 } 40739 40740 return finishProcessingFn(null, segment); 40741 }; 40742 }; 40743 40744 /** 40745 * Decrypt the segment via the decryption web worker 40746 * 40747 * @param {WebWorker} decrypter - a WebWorker interface to AES-128 decryption routines 40748 * @param {Object} segment - a simplified copy of the segmentInfo object 40749 * from SegmentLoader 40750 * @param {Function} doneFn - a callback that is executed after decryption has completed 40751 */ 40752 var decryptSegment = function decryptSegment(decrypter, segment, doneFn) { 40753 var decryptionHandler = function decryptionHandler(event) { 40754 if (event.data.source === segment.requestId) { 40755 decrypter.removeEventListener('message', decryptionHandler); 40756 var decrypted = event.data.decrypted; 40757 40758 segment.bytes = new Uint8Array(decrypted.bytes, decrypted.byteOffset, decrypted.byteLength); 40759 return doneFn(null, segment); 40760 } 40761 }; 40762 40763 decrypter.addEventListener('message', decryptionHandler); 40764 40765 // this is an encrypted segment 40766 // incrementally decrypt the segment 40767 decrypter.postMessage(createTransferableMessage({ 40768 source: segment.requestId, 40769 encrypted: segment.encryptedBytes, 40770 key: segment.key.bytes, 40771 iv: segment.key.iv 40772 }), [segment.encryptedBytes.buffer, segment.key.bytes.buffer]); 40773 }; 40774 40775 /** 40776 * The purpose of this function is to get the most pertinent error from the 40777 * array of errors. 40778 * For instance if a timeout and two aborts occur, then the aborts were 40779 * likely triggered by the timeout so return that error object. 40780 */ 40781 var getMostImportantError = function getMostImportantError(errors) { 40782 return errors.reduce(function (prev, err) { 40783 return err.code > prev.code ? err : prev; 40784 }); 40785 }; 40786 40787 /** 40788 * This function waits for all XHRs to finish (with either success or failure) 40789 * before continueing processing via it's callback. The function gathers errors 40790 * from each request into a single errors array so that the error status for 40791 * each request can be examined later. 40792 * 40793 * @param {Object} activeXhrs - an object that tracks all XHR requests 40794 * @param {WebWorker} decrypter - a WebWorker interface to AES-128 decryption routines 40795 * @param {Function} doneFn - a callback that is executed after all resources have been 40796 * downloaded and any decryption completed 40797 */ 40798 var waitForCompletion = function waitForCompletion(activeXhrs, decrypter, doneFn) { 40799 var errors = []; 40800 var count = 0; 40801 40802 return function (error, segment) { 40803 if (error) { 40804 // If there are errors, we have to abort any outstanding requests 40805 abortAll(activeXhrs); 40806 errors.push(error); 40807 } 40808 count += 1; 40809 40810 if (count === activeXhrs.length) { 40811 // Keep track of when *all* of the requests have completed 40812 segment.endOfAllRequests = Date.now(); 40813 40814 if (errors.length > 0) { 40815 var worstError = getMostImportantError(errors); 40816 40817 return doneFn(worstError, segment); 40818 } 40819 if (segment.encryptedBytes) { 40820 return decryptSegment(decrypter, segment, doneFn); 40821 } 40822 // Otherwise, everything is ready just continue 40823 return doneFn(null, segment); 40824 } 40825 }; 40826 }; 40827 40828 /** 40829 * Simple progress event callback handler that gathers some stats before 40830 * executing a provided callback with the `segment` object 40831 * 40832 * @param {Object} segment - a simplified copy of the segmentInfo object 40833 * from SegmentLoader 40834 * @param {Function} progressFn - a callback that is executed each time a progress event 40835 * is received 40836 * @param {Event} event - the progress event object from XMLHttpRequest 40837 */ 40838 var handleProgress = function handleProgress(segment, progressFn) { 40839 return function (event) { 40840 segment.stats = videojs$1.mergeOptions(segment.stats, getProgressStats(event)); 40841 40842 // record the time that we receive the first byte of data 40843 if (!segment.stats.firstBytesReceivedAt && segment.stats.bytesReceived) { 40844 segment.stats.firstBytesReceivedAt = Date.now(); 40845 } 40846 40847 return progressFn(event, segment); 40848 }; 40849 }; 40850 40851 /** 40852 * Load all resources and does any processing necessary for a media-segment 40853 * 40854 * Features: 40855 * decrypts the media-segment if it has a key uri and an iv 40856 * aborts *all* requests if *any* one request fails 40857 * 40858 * The segment object, at minimum, has the following format: 40859 * { 40860 * resolvedUri: String, 40861 * [byterange]: { 40862 * offset: Number, 40863 * length: Number 40864 * }, 40865 * [key]: { 40866 * resolvedUri: String 40867 * [byterange]: { 40868 * offset: Number, 40869 * length: Number 40870 * }, 40871 * iv: { 40872 * bytes: Uint32Array 40873 * } 40874 * }, 40875 * [map]: { 40876 * resolvedUri: String, 40877 * [byterange]: { 40878 * offset: Number, 40879 * length: Number 40880 * }, 40881 * [bytes]: Uint8Array 40882 * } 40883 * } 40884 * ...where [name] denotes optional properties 40885 * 40886 * @param {Function} xhr - an instance of the xhr wrapper in xhr.js 40887 * @param {Object} xhrOptions - the base options to provide to all xhr requests 40888 * @param {WebWorker} decryptionWorker - a WebWorker interface to AES-128 40889 * decryption routines 40890 * @param {Object} segment - a simplified copy of the segmentInfo object 40891 * from SegmentLoader 40892 * @param {Function} progressFn - a callback that receives progress events from the main 40893 * segment's xhr request 40894 * @param {Function} doneFn - a callback that is executed only once all requests have 40895 * succeeded or failed 40896 * @returns {Function} a function that, when invoked, immediately aborts all 40897 * outstanding requests 40898 */ 40899 var mediaSegmentRequest = function mediaSegmentRequest(xhr, xhrOptions, decryptionWorker, segment, progressFn, doneFn) { 40900 var activeXhrs = []; 40901 var finishProcessingFn = waitForCompletion(activeXhrs, decryptionWorker, doneFn); 40902 40903 // optionally, request the decryption key 40904 if (segment.key) { 40905 var keyRequestOptions = videojs$1.mergeOptions(xhrOptions, { 40906 uri: segment.key.resolvedUri, 40907 responseType: 'arraybuffer' 40908 }); 40909 var keyRequestCallback = handleKeyResponse(segment, finishProcessingFn); 40910 var keyXhr = xhr(keyRequestOptions, keyRequestCallback); 40911 40912 activeXhrs.push(keyXhr); 40913 } 40914 40915 // optionally, request the associated media init segment 40916 if (segment.map && !segment.map.bytes) { 40917 var initSegmentOptions = videojs$1.mergeOptions(xhrOptions, { 40918 uri: segment.map.resolvedUri, 40919 responseType: 'arraybuffer', 40920 headers: segmentXhrHeaders(segment.map) 40921 }); 40922 var initSegmentRequestCallback = handleInitSegmentResponse(segment, finishProcessingFn); 40923 var initSegmentXhr = xhr(initSegmentOptions, initSegmentRequestCallback); 40924 40925 activeXhrs.push(initSegmentXhr); 40926 } 40927 40928 var segmentRequestOptions = videojs$1.mergeOptions(xhrOptions, { 40929 uri: segment.resolvedUri, 40930 responseType: 'arraybuffer', 40931 headers: segmentXhrHeaders(segment) 40932 }); 40933 var segmentRequestCallback = handleSegmentResponse(segment, finishProcessingFn); 40934 var segmentXhr = xhr(segmentRequestOptions, segmentRequestCallback); 40935 40936 segmentXhr.addEventListener('progress', handleProgress(segment, progressFn)); 40937 activeXhrs.push(segmentXhr); 40938 40939 return function () { 40940 return abortAll(activeXhrs); 40941 }; 40942 }; 40943 40944 /** 40945 * @file - codecs.js - Handles tasks regarding codec strings such as translating them to 40946 * codec strings, or translating codec strings into objects that can be examined. 40947 */ 40948 40949 // Default codec parameters if none were provided for video and/or audio 40950 var defaultCodecs = { 40951 videoCodec: 'avc1', 40952 videoObjectTypeIndicator: '.4d400d', 40953 // AAC-LC 40954 audioProfile: '2' 40955 }; 40956 40957 /** 40958 * Parses a codec string to retrieve the number of codecs specified, 40959 * the video codec and object type indicator, and the audio profile. 40960 */ 40961 40962 var parseCodecs = function parseCodecs() { 40963 var codecs = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; 40964 40965 var result = { 40966 codecCount: 0 40967 }; 40968 var parsed = void 0; 40969 40970 result.codecCount = codecs.split(',').length; 40971 result.codecCount = result.codecCount || 2; 40972 40973 // parse the video codec 40974 parsed = /(^|\s|,)+(avc[13])([^ ,]*)/i.exec(codecs); 40975 if (parsed) { 40976 result.videoCodec = parsed[2]; 40977 result.videoObjectTypeIndicator = parsed[3]; 40978 } 40979 40980 // parse the last field of the audio codec 40981 result.audioProfile = /(^|\s|,)+mp4a.[0-9A-Fa-f]+\.([0-9A-Fa-f]+)/i.exec(codecs); 40982 result.audioProfile = result.audioProfile && result.audioProfile[2]; 40983 40984 return result; 40985 }; 40986 40987 /** 40988 * Replace codecs in the codec string with the old apple-style `avc1.<dd>.<dd>` to the 40989 * standard `avc1.<hhhhhh>`. 40990 * 40991 * @param codecString {String} the codec string 40992 * @return {String} the codec string with old apple-style codecs replaced 40993 * 40994 * @private 40995 */ 40996 var mapLegacyAvcCodecs = function mapLegacyAvcCodecs(codecString) { 40997 return codecString.replace(/avc1\.(\d+)\.(\d+)/i, function (match) { 40998 return translateLegacyCodecs([match])[0]; 40999 }); 41000 }; 41001 41002 /** 41003 * Build a media mime-type string from a set of parameters 41004 * @param {String} type either 'audio' or 'video' 41005 * @param {String} container either 'mp2t' or 'mp4' 41006 * @param {Array} codecs an array of codec strings to add 41007 * @return {String} a valid media mime-type 41008 */ 41009 var makeMimeTypeString = function makeMimeTypeString(type, container, codecs) { 41010 // The codecs array is filtered so that falsey values are 41011 // dropped and don't cause Array#join to create spurious 41012 // commas 41013 return type + '/' + container + '; codecs="' + codecs.filter(function (c) { 41014 return !!c; 41015 }).join(', ') + '"'; 41016 }; 41017 41018 /** 41019 * Returns the type container based on information in the playlist 41020 * @param {Playlist} media the current media playlist 41021 * @return {String} a valid media container type 41022 */ 41023 var getContainerType = function getContainerType(media) { 41024 // An initialization segment means the media playlist is an iframe 41025 // playlist or is using the mp4 container. We don't currently 41026 // support iframe playlists, so assume this is signalling mp4 41027 // fragments. 41028 if (media.segments && media.segments.length && media.segments[0].map) { 41029 return 'mp4'; 41030 } 41031 return 'mp2t'; 41032 }; 41033 41034 /** 41035 * Returns a set of codec strings parsed from the playlist or the default 41036 * codec strings if no codecs were specified in the playlist 41037 * @param {Playlist} media the current media playlist 41038 * @return {Object} an object with the video and audio codecs 41039 */ 41040 var getCodecs = function getCodecs(media) { 41041 // if the codecs were explicitly specified, use them instead of the 41042 // defaults 41043 var mediaAttributes = media.attributes || {}; 41044 41045 if (mediaAttributes.CODECS) { 41046 return parseCodecs(mediaAttributes.CODECS); 41047 } 41048 return defaultCodecs; 41049 }; 41050 41051 var audioProfileFromDefault = function audioProfileFromDefault(master, audioGroupId) { 41052 if (!master.mediaGroups.AUDIO || !audioGroupId) { 41053 return null; 41054 } 41055 41056 var audioGroup = master.mediaGroups.AUDIO[audioGroupId]; 41057 41058 if (!audioGroup) { 41059 return null; 41060 } 41061 41062 for (var name in audioGroup) { 41063 var audioType = audioGroup[name]; 41064 41065 if (audioType.default && audioType.playlists) { 41066 // codec should be the same for all playlists within the audio type 41067 return parseCodecs(audioType.playlists[0].attributes.CODECS).audioProfile; 41068 } 41069 } 41070 41071 return null; 41072 }; 41073 41074 /** 41075 * Calculates the MIME type strings for a working configuration of 41076 * SourceBuffers to play variant streams in a master playlist. If 41077 * there is no possible working configuration, an empty array will be 41078 * returned. 41079 * 41080 * @param master {Object} the m3u8 object for the master playlist 41081 * @param media {Object} the m3u8 object for the variant playlist 41082 * @return {Array} the MIME type strings. If the array has more than 41083 * one entry, the first element should be applied to the video 41084 * SourceBuffer and the second to the audio SourceBuffer. 41085 * 41086 * @private 41087 */ 41088 var mimeTypesForPlaylist = function mimeTypesForPlaylist(master, media) { 41089 var containerType = getContainerType(media); 41090 var codecInfo = getCodecs(media); 41091 var mediaAttributes = media.attributes || {}; 41092 // Default condition for a traditional HLS (no demuxed audio/video) 41093 var isMuxed = true; 41094 var isMaat = false; 41095 41096 if (!media) { 41097 // Not enough information 41098 return []; 41099 } 41100 41101 if (master.mediaGroups.AUDIO && mediaAttributes.AUDIO) { 41102 var audioGroup = master.mediaGroups.AUDIO[mediaAttributes.AUDIO]; 41103 41104 // Handle the case where we are in a multiple-audio track scenario 41105 if (audioGroup) { 41106 isMaat = true; 41107 // Start with the everything demuxed then... 41108 isMuxed = false; 41109 // ...check to see if any audio group tracks are muxed (ie. lacking a uri) 41110 for (var groupId in audioGroup) { 41111 // either a uri is present (if the case of HLS and an external playlist), or 41112 // playlists is present (in the case of DASH where we don't have external audio 41113 // playlists) 41114 if (!audioGroup[groupId].uri && !audioGroup[groupId].playlists) { 41115 isMuxed = true; 41116 break; 41117 } 41118 } 41119 } 41120 } 41121 41122 // HLS with multiple-audio tracks must always get an audio codec. 41123 // Put another way, there is no way to have a video-only multiple-audio HLS! 41124 if (isMaat && !codecInfo.audioProfile) { 41125 if (!isMuxed) { 41126 // It is possible for codecs to be specified on the audio media group playlist but 41127 // not on the rendition playlist. This is mostly the case for DASH, where audio and 41128 // video are always separate (and separately specified). 41129 codecInfo.audioProfile = audioProfileFromDefault(master, mediaAttributes.AUDIO); 41130 } 41131 41132 if (!codecInfo.audioProfile) { 41133 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)'); 41134 codecInfo.audioProfile = defaultCodecs.audioProfile; 41135 } 41136 } 41137 41138 // Generate the final codec strings from the codec object generated above 41139 var codecStrings = {}; 41140 41141 if (codecInfo.videoCodec) { 41142 codecStrings.video = '' + codecInfo.videoCodec + codecInfo.videoObjectTypeIndicator; 41143 } 41144 41145 if (codecInfo.audioProfile) { 41146 codecStrings.audio = 'mp4a.40.' + codecInfo.audioProfile; 41147 } 41148 41149 // Finally, make and return an array with proper mime-types depending on 41150 // the configuration 41151 var justAudio = makeMimeTypeString('audio', containerType, [codecStrings.audio]); 41152 var justVideo = makeMimeTypeString('video', containerType, [codecStrings.video]); 41153 var bothVideoAudio = makeMimeTypeString('video', containerType, [codecStrings.video, codecStrings.audio]); 41154 41155 if (isMaat) { 41156 if (!isMuxed && codecStrings.video) { 41157 return [justVideo, justAudio]; 41158 } 41159 41160 if (!isMuxed && !codecStrings.video) { 41161 // There is no muxed content and no video codec string, so this is an audio only 41162 // stream with alternate audio. 41163 return [justAudio, justAudio]; 41164 } 41165 41166 // There exists the possiblity that this will return a `video/container` 41167 // mime-type for the first entry in the array even when there is only audio. 41168 // This doesn't appear to be a problem and simplifies the code. 41169 return [bothVideoAudio, justAudio]; 41170 } 41171 41172 // If there is no video codec at all, always just return a single 41173 // audio/<container> mime-type 41174 if (!codecStrings.video) { 41175 return [justAudio]; 41176 } 41177 41178 // When not using separate audio media groups, audio and video is 41179 // *always* muxed 41180 return [bothVideoAudio]; 41181 }; 41182 41183 // Utilities 41184 41185 /** 41186 * Returns the CSS value for the specified property on an element 41187 * using `getComputedStyle`. Firefox has a long-standing issue where 41188 * getComputedStyle() may return null when running in an iframe with 41189 * `display: none`. 41190 * 41191 * @see https://bugzilla.mozilla.org/show_bug.cgi?id=548397 41192 * @param {HTMLElement} el the htmlelement to work on 41193 * @param {string} the proprety to get the style for 41194 */ 41195 var safeGetComputedStyle = function safeGetComputedStyle(el, property) { 41196 var result = void 0; 41197 41198 if (!el) { 41199 return ''; 41200 } 41201 41202 result = window_1.getComputedStyle(el); 41203 if (!result) { 41204 return ''; 41205 } 41206 41207 return result[property]; 41208 }; 41209 41210 /** 41211 * Resuable stable sort function 41212 * 41213 * @param {Playlists} array 41214 * @param {Function} sortFn Different comparators 41215 * @function stableSort 41216 */ 41217 var stableSort = function stableSort(array, sortFn) { 41218 var newArray = array.slice(); 41219 41220 array.sort(function (left, right) { 41221 var cmp = sortFn(left, right); 41222 41223 if (cmp === 0) { 41224 return newArray.indexOf(left) - newArray.indexOf(right); 41225 } 41226 return cmp; 41227 }); 41228 }; 41229 41230 /** 41231 * A comparator function to sort two playlist object by bandwidth. 41232 * 41233 * @param {Object} left a media playlist object 41234 * @param {Object} right a media playlist object 41235 * @return {Number} Greater than zero if the bandwidth attribute of 41236 * left is greater than the corresponding attribute of right. Less 41237 * than zero if the bandwidth of right is greater than left and 41238 * exactly zero if the two are equal. 41239 */ 41240 var comparePlaylistBandwidth = function comparePlaylistBandwidth(left, right) { 41241 var leftBandwidth = void 0; 41242 var rightBandwidth = void 0; 41243 41244 if (left.attributes.BANDWIDTH) { 41245 leftBandwidth = left.attributes.BANDWIDTH; 41246 } 41247 leftBandwidth = leftBandwidth || window_1.Number.MAX_VALUE; 41248 if (right.attributes.BANDWIDTH) { 41249 rightBandwidth = right.attributes.BANDWIDTH; 41250 } 41251 rightBandwidth = rightBandwidth || window_1.Number.MAX_VALUE; 41252 41253 return leftBandwidth - rightBandwidth; 41254 }; 41255 41256 /** 41257 * A comparator function to sort two playlist object by resolution (width). 41258 * @param {Object} left a media playlist object 41259 * @param {Object} right a media playlist object 41260 * @return {Number} Greater than zero if the resolution.width attribute of 41261 * left is greater than the corresponding attribute of right. Less 41262 * than zero if the resolution.width of right is greater than left and 41263 * exactly zero if the two are equal. 41264 */ 41265 var comparePlaylistResolution = function comparePlaylistResolution(left, right) { 41266 var leftWidth = void 0; 41267 var rightWidth = void 0; 41268 41269 if (left.attributes.RESOLUTION && left.attributes.RESOLUTION.width) { 41270 leftWidth = left.attributes.RESOLUTION.width; 41271 } 41272 41273 leftWidth = leftWidth || window_1.Number.MAX_VALUE; 41274 41275 if (right.attributes.RESOLUTION && right.attributes.RESOLUTION.width) { 41276 rightWidth = right.attributes.RESOLUTION.width; 41277 } 41278 41279 rightWidth = rightWidth || window_1.Number.MAX_VALUE; 41280 41281 // NOTE - Fallback to bandwidth sort as appropriate in cases where multiple renditions 41282 // have the same media dimensions/ resolution 41283 if (leftWidth === rightWidth && left.attributes.BANDWIDTH && right.attributes.BANDWIDTH) { 41284 return left.attributes.BANDWIDTH - right.attributes.BANDWIDTH; 41285 } 41286 return leftWidth - rightWidth; 41287 }; 41288 41289 /** 41290 * Chooses the appropriate media playlist based on bandwidth and player size 41291 * 41292 * @param {Object} master 41293 * Object representation of the master manifest 41294 * @param {Number} playerBandwidth 41295 * Current calculated bandwidth of the player 41296 * @param {Number} playerWidth 41297 * Current width of the player element 41298 * @param {Number} playerHeight 41299 * Current height of the player element 41300 * @return {Playlist} the highest bitrate playlist less than the 41301 * currently detected bandwidth, accounting for some amount of 41302 * bandwidth variance 41303 */ 41304 var simpleSelector = function simpleSelector(master, playerBandwidth, playerWidth, playerHeight) { 41305 // convert the playlists to an intermediary representation to make comparisons easier 41306 var sortedPlaylistReps = master.playlists.map(function (playlist) { 41307 var width = void 0; 41308 var height = void 0; 41309 var bandwidth = void 0; 41310 41311 width = playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.width; 41312 height = playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.height; 41313 bandwidth = playlist.attributes.BANDWIDTH; 41314 41315 bandwidth = bandwidth || window_1.Number.MAX_VALUE; 41316 41317 return { 41318 bandwidth: bandwidth, 41319 width: width, 41320 height: height, 41321 playlist: playlist 41322 }; 41323 }); 41324 41325 stableSort(sortedPlaylistReps, function (left, right) { 41326 return left.bandwidth - right.bandwidth; 41327 }); 41328 41329 // filter out any playlists that have been excluded due to 41330 // incompatible configurations 41331 sortedPlaylistReps = sortedPlaylistReps.filter(function (rep) { 41332 return !Playlist.isIncompatible(rep.playlist); 41333 }); 41334 41335 // filter out any playlists that have been disabled manually through the representations 41336 // api or blacklisted temporarily due to playback errors. 41337 var enabledPlaylistReps = sortedPlaylistReps.filter(function (rep) { 41338 return Playlist.isEnabled(rep.playlist); 41339 }); 41340 41341 if (!enabledPlaylistReps.length) { 41342 // if there are no enabled playlists, then they have all been blacklisted or disabled 41343 // by the user through the representations api. In this case, ignore blacklisting and 41344 // fallback to what the user wants by using playlists the user has not disabled. 41345 enabledPlaylistReps = sortedPlaylistReps.filter(function (rep) { 41346 return !Playlist.isDisabled(rep.playlist); 41347 }); 41348 } 41349 41350 // filter out any variant that has greater effective bitrate 41351 // than the current estimated bandwidth 41352 var bandwidthPlaylistReps = enabledPlaylistReps.filter(function (rep) { 41353 return rep.bandwidth * Config.BANDWIDTH_VARIANCE < playerBandwidth; 41354 }); 41355 41356 var highestRemainingBandwidthRep = bandwidthPlaylistReps[bandwidthPlaylistReps.length - 1]; 41357 41358 // get all of the renditions with the same (highest) bandwidth 41359 // and then taking the very first element 41360 var bandwidthBestRep = bandwidthPlaylistReps.filter(function (rep) { 41361 return rep.bandwidth === highestRemainingBandwidthRep.bandwidth; 41362 })[0]; 41363 41364 // filter out playlists without resolution information 41365 var haveResolution = bandwidthPlaylistReps.filter(function (rep) { 41366 return rep.width && rep.height; 41367 }); 41368 41369 // sort variants by resolution 41370 stableSort(haveResolution, function (left, right) { 41371 return left.width - right.width; 41372 }); 41373 41374 // if we have the exact resolution as the player use it 41375 var resolutionBestRepList = haveResolution.filter(function (rep) { 41376 return rep.width === playerWidth && rep.height === playerHeight; 41377 }); 41378 41379 highestRemainingBandwidthRep = resolutionBestRepList[resolutionBestRepList.length - 1]; 41380 // ensure that we pick the highest bandwidth variant that have exact resolution 41381 var resolutionBestRep = resolutionBestRepList.filter(function (rep) { 41382 return rep.bandwidth === highestRemainingBandwidthRep.bandwidth; 41383 })[0]; 41384 41385 var resolutionPlusOneList = void 0; 41386 var resolutionPlusOneSmallest = void 0; 41387 var resolutionPlusOneRep = void 0; 41388 41389 // find the smallest variant that is larger than the player 41390 // if there is no match of exact resolution 41391 if (!resolutionBestRep) { 41392 resolutionPlusOneList = haveResolution.filter(function (rep) { 41393 return rep.width > playerWidth || rep.height > playerHeight; 41394 }); 41395 41396 // find all the variants have the same smallest resolution 41397 resolutionPlusOneSmallest = resolutionPlusOneList.filter(function (rep) { 41398 return rep.width === resolutionPlusOneList[0].width && rep.height === resolutionPlusOneList[0].height; 41399 }); 41400 41401 // ensure that we also pick the highest bandwidth variant that 41402 // is just-larger-than the video player 41403 highestRemainingBandwidthRep = resolutionPlusOneSmallest[resolutionPlusOneSmallest.length - 1]; 41404 resolutionPlusOneRep = resolutionPlusOneSmallest.filter(function (rep) { 41405 return rep.bandwidth === highestRemainingBandwidthRep.bandwidth; 41406 })[0]; 41407 } 41408 41409 // fallback chain of variants 41410 var chosenRep = resolutionPlusOneRep || resolutionBestRep || bandwidthBestRep || enabledPlaylistReps[0] || sortedPlaylistReps[0]; 41411 41412 return chosenRep ? chosenRep.playlist : null; 41413 }; 41414 41415 // Playlist Selectors 41416 41417 /** 41418 * Chooses the appropriate media playlist based on the most recent 41419 * bandwidth estimate and the player size. 41420 * 41421 * Expects to be called within the context of an instance of HlsHandler 41422 * 41423 * @return {Playlist} the highest bitrate playlist less than the 41424 * currently detected bandwidth, accounting for some amount of 41425 * bandwidth variance 41426 */ 41427 var lastBandwidthSelector = function lastBandwidthSelector() { 41428 return simpleSelector(this.playlists.master, this.systemBandwidth, parseInt(safeGetComputedStyle(this.tech_.el(), 'width'), 10), parseInt(safeGetComputedStyle(this.tech_.el(), 'height'), 10)); 41429 }; 41430 41431 /** 41432 * Chooses the appropriate media playlist based on the potential to rebuffer 41433 * 41434 * @param {Object} settings 41435 * Object of information required to use this selector 41436 * @param {Object} settings.master 41437 * Object representation of the master manifest 41438 * @param {Number} settings.currentTime 41439 * The current time of the player 41440 * @param {Number} settings.bandwidth 41441 * Current measured bandwidth 41442 * @param {Number} settings.duration 41443 * Duration of the media 41444 * @param {Number} settings.segmentDuration 41445 * Segment duration to be used in round trip time calculations 41446 * @param {Number} settings.timeUntilRebuffer 41447 * Time left in seconds until the player has to rebuffer 41448 * @param {Number} settings.currentTimeline 41449 * The current timeline segments are being loaded from 41450 * @param {SyncController} settings.syncController 41451 * SyncController for determining if we have a sync point for a given playlist 41452 * @return {Object|null} 41453 * {Object} return.playlist 41454 * The highest bandwidth playlist with the least amount of rebuffering 41455 * {Number} return.rebufferingImpact 41456 * The amount of time in seconds switching to this playlist will rebuffer. A 41457 * negative value means that switching will cause zero rebuffering. 41458 */ 41459 var minRebufferMaxBandwidthSelector = function minRebufferMaxBandwidthSelector(settings) { 41460 var master = settings.master, 41461 currentTime = settings.currentTime, 41462 bandwidth = settings.bandwidth, 41463 duration$$1 = settings.duration, 41464 segmentDuration = settings.segmentDuration, 41465 timeUntilRebuffer = settings.timeUntilRebuffer, 41466 currentTimeline = settings.currentTimeline, 41467 syncController = settings.syncController; 41468 41469 // filter out any playlists that have been excluded due to 41470 // incompatible configurations 41471 41472 var compatiblePlaylists = master.playlists.filter(function (playlist) { 41473 return !Playlist.isIncompatible(playlist); 41474 }); 41475 41476 // filter out any playlists that have been disabled manually through the representations 41477 // api or blacklisted temporarily due to playback errors. 41478 var enabledPlaylists = compatiblePlaylists.filter(Playlist.isEnabled); 41479 41480 if (!enabledPlaylists.length) { 41481 // if there are no enabled playlists, then they have all been blacklisted or disabled 41482 // by the user through the representations api. In this case, ignore blacklisting and 41483 // fallback to what the user wants by using playlists the user has not disabled. 41484 enabledPlaylists = compatiblePlaylists.filter(function (playlist) { 41485 return !Playlist.isDisabled(playlist); 41486 }); 41487 } 41488 41489 var bandwidthPlaylists = enabledPlaylists.filter(Playlist.hasAttribute.bind(null, 'BANDWIDTH')); 41490 41491 var rebufferingEstimates = bandwidthPlaylists.map(function (playlist) { 41492 var syncPoint = syncController.getSyncPoint(playlist, duration$$1, currentTimeline, currentTime); 41493 // If there is no sync point for this playlist, switching to it will require a 41494 // sync request first. This will double the request time 41495 var numRequests = syncPoint ? 1 : 2; 41496 var requestTimeEstimate = Playlist.estimateSegmentRequestTime(segmentDuration, bandwidth, playlist); 41497 var rebufferingImpact = requestTimeEstimate * numRequests - timeUntilRebuffer; 41498 41499 return { 41500 playlist: playlist, 41501 rebufferingImpact: rebufferingImpact 41502 }; 41503 }); 41504 41505 var noRebufferingPlaylists = rebufferingEstimates.filter(function (estimate) { 41506 return estimate.rebufferingImpact <= 0; 41507 }); 41508 41509 // Sort by bandwidth DESC 41510 stableSort(noRebufferingPlaylists, function (a, b) { 41511 return comparePlaylistBandwidth(b.playlist, a.playlist); 41512 }); 41513 41514 if (noRebufferingPlaylists.length) { 41515 return noRebufferingPlaylists[0]; 41516 } 41517 41518 stableSort(rebufferingEstimates, function (a, b) { 41519 return a.rebufferingImpact - b.rebufferingImpact; 41520 }); 41521 41522 return rebufferingEstimates[0] || null; 41523 }; 41524 41525 /** 41526 * Chooses the appropriate media playlist, which in this case is the lowest bitrate 41527 * one with video. If no renditions with video exist, return the lowest audio rendition. 41528 * 41529 * Expects to be called within the context of an instance of HlsHandler 41530 * 41531 * @return {Object|null} 41532 * {Object} return.playlist 41533 * The lowest bitrate playlist that contains a video codec. If no such rendition 41534 * exists pick the lowest audio rendition. 41535 */ 41536 var lowestBitrateCompatibleVariantSelector = function lowestBitrateCompatibleVariantSelector() { 41537 // filter out any playlists that have been excluded due to 41538 // incompatible configurations or playback errors 41539 var playlists = this.playlists.master.playlists.filter(Playlist.isEnabled); 41540 41541 // Sort ascending by bitrate 41542 stableSort(playlists, function (a, b) { 41543 return comparePlaylistBandwidth(a, b); 41544 }); 41545 41546 // Parse and assume that playlists with no video codec have no video 41547 // (this is not necessarily true, although it is generally true). 41548 // 41549 // If an entire manifest has no valid videos everything will get filtered 41550 // out. 41551 var playlistsWithVideo = playlists.filter(function (playlist) { 41552 return parseCodecs(playlist.attributes.CODECS).videoCodec; 41553 }); 41554 41555 return playlistsWithVideo[0] || null; 41556 }; 41557 41558 /** 41559 * @file segment-loader.js 41560 */ 41561 41562 // in ms 41563 var CHECK_BUFFER_DELAY = 500; 41564 41565 /** 41566 * Determines if we should call endOfStream on the media source based 41567 * on the state of the buffer or if appened segment was the final 41568 * segment in the playlist. 41569 * 41570 * @param {Object} playlist a media playlist object 41571 * @param {Object} mediaSource the MediaSource object 41572 * @param {Number} segmentIndex the index of segment we last appended 41573 * @returns {Boolean} do we need to call endOfStream on the MediaSource 41574 */ 41575 var detectEndOfStream = function detectEndOfStream(playlist, mediaSource, segmentIndex) { 41576 if (!playlist || !mediaSource) { 41577 return false; 41578 } 41579 41580 var segments = playlist.segments; 41581 41582 // determine a few boolean values to help make the branch below easier 41583 // to read 41584 var appendedLastSegment = segmentIndex === segments.length; 41585 41586 // if we've buffered to the end of the video, we need to call endOfStream 41587 // so that MediaSources can trigger the `ended` event when it runs out of 41588 // buffered data instead of waiting for me 41589 return playlist.endList && mediaSource.readyState === 'open' && appendedLastSegment; 41590 }; 41591 41592 var finite = function finite(num) { 41593 return typeof num === 'number' && isFinite(num); 41594 }; 41595 41596 var illegalMediaSwitch = function illegalMediaSwitch(loaderType, startingMedia, newSegmentMedia) { 41597 // Although these checks should most likely cover non 'main' types, for now it narrows 41598 // the scope of our checks. 41599 if (loaderType !== 'main' || !startingMedia || !newSegmentMedia) { 41600 return null; 41601 } 41602 41603 if (!newSegmentMedia.containsAudio && !newSegmentMedia.containsVideo) { 41604 return 'Neither audio nor video found in segment.'; 41605 } 41606 41607 if (startingMedia.containsVideo && !newSegmentMedia.containsVideo) { 41608 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.'; 41609 } 41610 41611 if (!startingMedia.containsVideo && newSegmentMedia.containsVideo) { 41612 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.'; 41613 } 41614 41615 return null; 41616 }; 41617 41618 /** 41619 * Calculates a time value that is safe to remove from the back buffer without interupting 41620 * playback. 41621 * 41622 * @param {TimeRange} seekable 41623 * The current seekable range 41624 * @param {Number} currentTime 41625 * The current time of the player 41626 * @param {Number} targetDuration 41627 * The target duration of the current playlist 41628 * @return {Number} 41629 * Time that is safe to remove from the back buffer without interupting playback 41630 */ 41631 var safeBackBufferTrimTime = function safeBackBufferTrimTime(seekable$$1, currentTime, targetDuration) { 41632 var removeToTime = void 0; 41633 41634 if (seekable$$1.length && seekable$$1.start(0) > 0 && seekable$$1.start(0) < currentTime) { 41635 // If we have a seekable range use that as the limit for what can be removed safely 41636 removeToTime = seekable$$1.start(0); 41637 } else { 41638 // otherwise remove anything older than 30 seconds before the current play head 41639 removeToTime = currentTime - 30; 41640 } 41641 41642 // Don't allow removing from the buffer within target duration of current time 41643 // to avoid the possibility of removing the GOP currently being played which could 41644 // cause playback stalls. 41645 return Math.min(removeToTime, currentTime - targetDuration); 41646 }; 41647 41648 var segmentInfoString = function segmentInfoString(segmentInfo) { 41649 var _segmentInfo$segment = segmentInfo.segment, 41650 start = _segmentInfo$segment.start, 41651 end = _segmentInfo$segment.end, 41652 _segmentInfo$playlist = segmentInfo.playlist, 41653 seq = _segmentInfo$playlist.mediaSequence, 41654 id = _segmentInfo$playlist.id, 41655 _segmentInfo$playlist2 = _segmentInfo$playlist.segments, 41656 segments = _segmentInfo$playlist2 === undefined ? [] : _segmentInfo$playlist2, 41657 index = segmentInfo.mediaIndex, 41658 timeline = segmentInfo.timeline; 41659 41660 return ['appending [' + index + '] of [' + seq + ', ' + (seq + segments.length) + '] from playlist [' + id + ']', '[' + start + ' => ' + end + '] in timeline [' + timeline + ']'].join(' '); 41661 }; 41662 41663 /** 41664 * An object that manages segment loading and appending. 41665 * 41666 * @class SegmentLoader 41667 * @param {Object} options required and optional options 41668 * @extends videojs.EventTarget 41669 */ 41670 41671 var SegmentLoader = function (_videojs$EventTarget) { 41672 inherits$3(SegmentLoader, _videojs$EventTarget); 41673 41674 function SegmentLoader(settings) { 41675 classCallCheck$3(this, SegmentLoader); 41676 41677 // check pre-conditions 41678 var _this = possibleConstructorReturn$3(this, (SegmentLoader.__proto__ || Object.getPrototypeOf(SegmentLoader)).call(this)); 41679 41680 if (!settings) { 41681 throw new TypeError('Initialization settings are required'); 41682 } 41683 if (typeof settings.currentTime !== 'function') { 41684 throw new TypeError('No currentTime getter specified'); 41685 } 41686 if (!settings.mediaSource) { 41687 throw new TypeError('No MediaSource specified'); 41688 } 41689 // public properties 41690 _this.bandwidth = settings.bandwidth; 41691 _this.throughput = { rate: 0, count: 0 }; 41692 _this.roundTrip = NaN; 41693 _this.resetStats_(); 41694 _this.mediaIndex = null; 41695 41696 // private settings 41697 _this.hasPlayed_ = settings.hasPlayed; 41698 _this.currentTime_ = settings.currentTime; 41699 _this.seekable_ = settings.seekable; 41700 _this.seeking_ = settings.seeking; 41701 _this.duration_ = settings.duration; 41702 _this.mediaSource_ = settings.mediaSource; 41703 _this.hls_ = settings.hls; 41704 _this.loaderType_ = settings.loaderType; 41705 _this.startingMedia_ = void 0; 41706 _this.segmentMetadataTrack_ = settings.segmentMetadataTrack; 41707 _this.goalBufferLength_ = settings.goalBufferLength; 41708 _this.sourceType_ = settings.sourceType; 41709 _this.state_ = 'INIT'; 41710 41711 // private instance variables 41712 _this.checkBufferTimeout_ = null; 41713 _this.error_ = void 0; 41714 _this.currentTimeline_ = -1; 41715 _this.pendingSegment_ = null; 41716 _this.mimeType_ = null; 41717 _this.sourceUpdater_ = null; 41718 _this.xhrOptions_ = null; 41719 41720 // Fragmented mp4 playback 41721 _this.activeInitSegmentId_ = null; 41722 _this.initSegments_ = {}; 41723 41724 _this.decrypter_ = settings.decrypter; 41725 41726 // Manages the tracking and generation of sync-points, mappings 41727 // between a time in the display time and a segment index within 41728 // a playlist 41729 _this.syncController_ = settings.syncController; 41730 _this.syncPoint_ = { 41731 segmentIndex: 0, 41732 time: 0 41733 }; 41734 41735 _this.syncController_.on('syncinfoupdate', function () { 41736 return _this.trigger('syncinfoupdate'); 41737 }); 41738 41739 _this.mediaSource_.addEventListener('sourceopen', function () { 41740 return _this.ended_ = false; 41741 }); 41742 41743 // ...for determining the fetch location 41744 _this.fetchAtBuffer_ = false; 41745 41746 _this.logger_ = logger('SegmentLoader[' + _this.loaderType_ + ']'); 41747 41748 Object.defineProperty(_this, 'state', { 41749 get: function get$$1() { 41750 return this.state_; 41751 }, 41752 set: function set$$1(newState) { 41753 if (newState !== this.state_) { 41754 this.logger_(this.state_ + ' -> ' + newState); 41755 this.state_ = newState; 41756 } 41757 } 41758 }); 41759 return _this; 41760 } 41761 41762 /** 41763 * reset all of our media stats 41764 * 41765 * @private 41766 */ 41767 41768 createClass$2(SegmentLoader, [{ 41769 key: 'resetStats_', 41770 value: function resetStats_() { 41771 this.mediaBytesTransferred = 0; 41772 this.mediaRequests = 0; 41773 this.mediaRequestsAborted = 0; 41774 this.mediaRequestsTimedout = 0; 41775 this.mediaRequestsErrored = 0; 41776 this.mediaTransferDuration = 0; 41777 this.mediaSecondsLoaded = 0; 41778 } 41779 41780 /** 41781 * dispose of the SegmentLoader and reset to the default state 41782 */ 41783 41784 }, { 41785 key: 'dispose', 41786 value: function dispose() { 41787 this.state = 'DISPOSED'; 41788 this.pause(); 41789 this.abort_(); 41790 if (this.sourceUpdater_) { 41791 this.sourceUpdater_.dispose(); 41792 } 41793 this.resetStats_(); 41794 } 41795 41796 /** 41797 * abort anything that is currently doing on with the SegmentLoader 41798 * and reset to a default state 41799 */ 41800 41801 }, { 41802 key: 'abort', 41803 value: function abort() { 41804 if (this.state !== 'WAITING') { 41805 if (this.pendingSegment_) { 41806 this.pendingSegment_ = null; 41807 } 41808 return; 41809 } 41810 41811 this.abort_(); 41812 41813 // We aborted the requests we were waiting on, so reset the loader's state to READY 41814 // since we are no longer "waiting" on any requests. XHR callback is not always run 41815 // when the request is aborted. This will prevent the loader from being stuck in the 41816 // WAITING state indefinitely. 41817 this.state = 'READY'; 41818 41819 // don't wait for buffer check timeouts to begin fetching the 41820 // next segment 41821 if (!this.paused()) { 41822 this.monitorBuffer_(); 41823 } 41824 } 41825 41826 /** 41827 * abort all pending xhr requests and null any pending segements 41828 * 41829 * @private 41830 */ 41831 41832 }, { 41833 key: 'abort_', 41834 value: function abort_() { 41835 if (this.pendingSegment_) { 41836 this.pendingSegment_.abortRequests(); 41837 } 41838 41839 // clear out the segment being processed 41840 this.pendingSegment_ = null; 41841 } 41842 41843 /** 41844 * set an error on the segment loader and null out any pending segements 41845 * 41846 * @param {Error} error the error to set on the SegmentLoader 41847 * @return {Error} the error that was set or that is currently set 41848 */ 41849 41850 }, { 41851 key: 'error', 41852 value: function error(_error) { 41853 if (typeof _error !== 'undefined') { 41854 this.error_ = _error; 41855 } 41856 41857 this.pendingSegment_ = null; 41858 return this.error_; 41859 } 41860 }, { 41861 key: 'endOfStream', 41862 value: function endOfStream() { 41863 this.ended_ = true; 41864 this.pause(); 41865 this.trigger('ended'); 41866 } 41867 41868 /** 41869 * Indicates which time ranges are buffered 41870 * 41871 * @return {TimeRange} 41872 * TimeRange object representing the current buffered ranges 41873 */ 41874 41875 }, { 41876 key: 'buffered_', 41877 value: function buffered_() { 41878 if (!this.sourceUpdater_) { 41879 return videojs$1.createTimeRanges(); 41880 } 41881 41882 return this.sourceUpdater_.buffered(); 41883 } 41884 41885 /** 41886 * Gets and sets init segment for the provided map 41887 * 41888 * @param {Object} map 41889 * The map object representing the init segment to get or set 41890 * @param {Boolean=} set 41891 * If true, the init segment for the provided map should be saved 41892 * @return {Object} 41893 * map object for desired init segment 41894 */ 41895 41896 }, { 41897 key: 'initSegment', 41898 value: function initSegment(map) { 41899 var set$$1 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; 41900 41901 if (!map) { 41902 return null; 41903 } 41904 41905 var id = initSegmentId(map); 41906 var storedMap = this.initSegments_[id]; 41907 41908 if (set$$1 && !storedMap && map.bytes) { 41909 this.initSegments_[id] = storedMap = { 41910 resolvedUri: map.resolvedUri, 41911 byterange: map.byterange, 41912 bytes: map.bytes 41913 }; 41914 } 41915 41916 return storedMap || map; 41917 } 41918 41919 /** 41920 * Returns true if all configuration required for loading is present, otherwise false. 41921 * 41922 * @return {Boolean} True if the all configuration is ready for loading 41923 * @private 41924 */ 41925 41926 }, { 41927 key: 'couldBeginLoading_', 41928 value: function couldBeginLoading_() { 41929 return this.playlist_ && ( 41930 // the source updater is created when init_ is called, so either having a 41931 // source updater or being in the INIT state with a mimeType is enough 41932 // to say we have all the needed configuration to start loading. 41933 this.sourceUpdater_ || this.mimeType_ && this.state === 'INIT') && !this.paused(); 41934 } 41935 41936 /** 41937 * load a playlist and start to fill the buffer 41938 */ 41939 41940 }, { 41941 key: 'load', 41942 value: function load() { 41943 // un-pause 41944 this.monitorBuffer_(); 41945 41946 // if we don't have a playlist yet, keep waiting for one to be 41947 // specified 41948 if (!this.playlist_) { 41949 return; 41950 } 41951 41952 // not sure if this is the best place for this 41953 this.syncController_.setDateTimeMapping(this.playlist_); 41954 41955 // if all the configuration is ready, initialize and begin loading 41956 if (this.state === 'INIT' && this.couldBeginLoading_()) { 41957 return this.init_(); 41958 } 41959 41960 // if we're in the middle of processing a segment already, don't 41961 // kick off an additional segment request 41962 if (!this.couldBeginLoading_() || this.state !== 'READY' && this.state !== 'INIT') { 41963 return; 41964 } 41965 41966 this.state = 'READY'; 41967 } 41968 41969 /** 41970 * Once all the starting parameters have been specified, begin 41971 * operation. This method should only be invoked from the INIT 41972 * state. 41973 * 41974 * @private 41975 */ 41976 41977 }, { 41978 key: 'init_', 41979 value: function init_() { 41980 this.state = 'READY'; 41981 this.sourceUpdater_ = new SourceUpdater(this.mediaSource_, this.mimeType_, this.loaderType_, this.sourceBufferEmitter_); 41982 this.resetEverything(); 41983 return this.monitorBuffer_(); 41984 } 41985 41986 /** 41987 * set a playlist on the segment loader 41988 * 41989 * @param {PlaylistLoader} media the playlist to set on the segment loader 41990 */ 41991 41992 }, { 41993 key: 'playlist', 41994 value: function playlist(newPlaylist) { 41995 var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 41996 41997 if (!newPlaylist) { 41998 return; 41999 } 42000 42001 var oldPlaylist = this.playlist_; 42002 var segmentInfo = this.pendingSegment_; 42003 42004 this.playlist_ = newPlaylist; 42005 this.xhrOptions_ = options; 42006 42007 // when we haven't started playing yet, the start of a live playlist 42008 // is always our zero-time so force a sync update each time the playlist 42009 // is refreshed from the server 42010 if (!this.hasPlayed_()) { 42011 newPlaylist.syncInfo = { 42012 mediaSequence: newPlaylist.mediaSequence, 42013 time: 0 42014 }; 42015 } 42016 42017 var oldId = oldPlaylist ? oldPlaylist.id : null; 42018 42019 this.logger_('playlist update [' + oldId + ' => ' + newPlaylist.id + ']'); 42020 42021 // in VOD, this is always a rendition switch (or we updated our syncInfo above) 42022 // in LIVE, we always want to update with new playlists (including refreshes) 42023 this.trigger('syncinfoupdate'); 42024 42025 // if we were unpaused but waiting for a playlist, start 42026 // buffering now 42027 if (this.state === 'INIT' && this.couldBeginLoading_()) { 42028 return this.init_(); 42029 } 42030 42031 if (!oldPlaylist || oldPlaylist.uri !== newPlaylist.uri) { 42032 if (this.mediaIndex !== null) { 42033 // we must "resync" the segment loader when we switch renditions and 42034 // the segment loader is already synced to the previous rendition 42035 this.resyncLoader(); 42036 } 42037 42038 // the rest of this function depends on `oldPlaylist` being defined 42039 return; 42040 } 42041 42042 // we reloaded the same playlist so we are in a live scenario 42043 // and we will likely need to adjust the mediaIndex 42044 var mediaSequenceDiff = newPlaylist.mediaSequence - oldPlaylist.mediaSequence; 42045 42046 this.logger_('live window shift [' + mediaSequenceDiff + ']'); 42047 42048 // update the mediaIndex on the SegmentLoader 42049 // this is important because we can abort a request and this value must be 42050 // equal to the last appended mediaIndex 42051 if (this.mediaIndex !== null) { 42052 this.mediaIndex -= mediaSequenceDiff; 42053 } 42054 42055 // update the mediaIndex on the SegmentInfo object 42056 // this is important because we will update this.mediaIndex with this value 42057 // in `handleUpdateEnd_` after the segment has been successfully appended 42058 if (segmentInfo) { 42059 segmentInfo.mediaIndex -= mediaSequenceDiff; 42060 42061 // we need to update the referenced segment so that timing information is 42062 // saved for the new playlist's segment, however, if the segment fell off the 42063 // playlist, we can leave the old reference and just lose the timing info 42064 if (segmentInfo.mediaIndex >= 0) { 42065 segmentInfo.segment = newPlaylist.segments[segmentInfo.mediaIndex]; 42066 } 42067 } 42068 42069 this.syncController_.saveExpiredSegmentInfo(oldPlaylist, newPlaylist); 42070 } 42071 42072 /** 42073 * Prevent the loader from fetching additional segments. If there 42074 * is a segment request outstanding, it will finish processing 42075 * before the loader halts. A segment loader can be unpaused by 42076 * calling load(). 42077 */ 42078 42079 }, { 42080 key: 'pause', 42081 value: function pause() { 42082 if (this.checkBufferTimeout_) { 42083 window_1.clearTimeout(this.checkBufferTimeout_); 42084 42085 this.checkBufferTimeout_ = null; 42086 } 42087 } 42088 42089 /** 42090 * Returns whether the segment loader is fetching additional 42091 * segments when given the opportunity. This property can be 42092 * modified through calls to pause() and load(). 42093 */ 42094 42095 }, { 42096 key: 'paused', 42097 value: function paused() { 42098 return this.checkBufferTimeout_ === null; 42099 } 42100 42101 /** 42102 * create/set the following mimetype on the SourceBuffer through a 42103 * SourceUpdater 42104 * 42105 * @param {String} mimeType the mime type string to use 42106 * @param {Object} sourceBufferEmitter an event emitter that fires when a source buffer 42107 * is added to the media source 42108 */ 42109 42110 }, { 42111 key: 'mimeType', 42112 value: function mimeType(_mimeType, sourceBufferEmitter) { 42113 if (this.mimeType_) { 42114 return; 42115 } 42116 42117 this.mimeType_ = _mimeType; 42118 this.sourceBufferEmitter_ = sourceBufferEmitter; 42119 // if we were unpaused but waiting for a sourceUpdater, start 42120 // buffering now 42121 if (this.state === 'INIT' && this.couldBeginLoading_()) { 42122 this.init_(); 42123 } 42124 } 42125 42126 /** 42127 * Delete all the buffered data and reset the SegmentLoader 42128 */ 42129 42130 }, { 42131 key: 'resetEverything', 42132 value: function resetEverything() { 42133 this.ended_ = false; 42134 this.resetLoader(); 42135 this.remove(0, this.duration_()); 42136 this.trigger('reseteverything'); 42137 } 42138 42139 /** 42140 * Force the SegmentLoader to resync and start loading around the currentTime instead 42141 * of starting at the end of the buffer 42142 * 42143 * Useful for fast quality changes 42144 */ 42145 42146 }, { 42147 key: 'resetLoader', 42148 value: function resetLoader() { 42149 this.fetchAtBuffer_ = false; 42150 this.resyncLoader(); 42151 } 42152 42153 /** 42154 * Force the SegmentLoader to restart synchronization and make a conservative guess 42155 * before returning to the simple walk-forward method 42156 */ 42157 42158 }, { 42159 key: 'resyncLoader', 42160 value: function resyncLoader() { 42161 this.mediaIndex = null; 42162 this.syncPoint_ = null; 42163 this.abort(); 42164 } 42165 42166 /** 42167 * Remove any data in the source buffer between start and end times 42168 * @param {Number} start - the start time of the region to remove from the buffer 42169 * @param {Number} end - the end time of the region to remove from the buffer 42170 */ 42171 42172 }, { 42173 key: 'remove', 42174 value: function remove(start, end) { 42175 if (this.sourceUpdater_) { 42176 this.sourceUpdater_.remove(start, end); 42177 } 42178 removeCuesFromTrack(start, end, this.segmentMetadataTrack_); 42179 } 42180 42181 /** 42182 * (re-)schedule monitorBufferTick_ to run as soon as possible 42183 * 42184 * @private 42185 */ 42186 42187 }, { 42188 key: 'monitorBuffer_', 42189 value: function monitorBuffer_() { 42190 if (this.checkBufferTimeout_) { 42191 window_1.clearTimeout(this.checkBufferTimeout_); 42192 } 42193 42194 this.checkBufferTimeout_ = window_1.setTimeout(this.monitorBufferTick_.bind(this), 1); 42195 } 42196 42197 /** 42198 * As long as the SegmentLoader is in the READY state, periodically 42199 * invoke fillBuffer_(). 42200 * 42201 * @private 42202 */ 42203 42204 }, { 42205 key: 'monitorBufferTick_', 42206 value: function monitorBufferTick_() { 42207 if (this.state === 'READY') { 42208 this.fillBuffer_(); 42209 } 42210 42211 if (this.checkBufferTimeout_) { 42212 window_1.clearTimeout(this.checkBufferTimeout_); 42213 } 42214 42215 this.checkBufferTimeout_ = window_1.setTimeout(this.monitorBufferTick_.bind(this), CHECK_BUFFER_DELAY); 42216 } 42217 42218 /** 42219 * fill the buffer with segements unless the sourceBuffers are 42220 * currently updating 42221 * 42222 * Note: this function should only ever be called by monitorBuffer_ 42223 * and never directly 42224 * 42225 * @private 42226 */ 42227 42228 }, { 42229 key: 'fillBuffer_', 42230 value: function fillBuffer_() { 42231 if (this.sourceUpdater_.updating()) { 42232 return; 42233 } 42234 42235 if (!this.syncPoint_) { 42236 this.syncPoint_ = this.syncController_.getSyncPoint(this.playlist_, this.duration_(), this.currentTimeline_, this.currentTime_()); 42237 } 42238 42239 // see if we need to begin loading immediately 42240 var segmentInfo = this.checkBuffer_(this.buffered_(), this.playlist_, this.mediaIndex, this.hasPlayed_(), this.currentTime_(), this.syncPoint_); 42241 42242 if (!segmentInfo) { 42243 return; 42244 } 42245 42246 var isEndOfStream = detectEndOfStream(this.playlist_, this.mediaSource_, segmentInfo.mediaIndex); 42247 42248 if (isEndOfStream) { 42249 this.endOfStream(); 42250 return; 42251 } 42252 42253 if (segmentInfo.mediaIndex === this.playlist_.segments.length - 1 && this.mediaSource_.readyState === 'ended' && !this.seeking_()) { 42254 return; 42255 } 42256 42257 // We will need to change timestampOffset of the sourceBuffer if either of 42258 // the following conditions are true: 42259 // - The segment.timeline !== this.currentTimeline 42260 // (we are crossing a discontinuity somehow) 42261 // - The "timestampOffset" for the start of this segment is less than 42262 // the currently set timestampOffset 42263 if (segmentInfo.timeline !== this.currentTimeline_ || segmentInfo.startOfSegment !== null && segmentInfo.startOfSegment < this.sourceUpdater_.timestampOffset()) { 42264 this.syncController_.reset(); 42265 segmentInfo.timestampOffset = segmentInfo.startOfSegment; 42266 } 42267 42268 this.loadSegment_(segmentInfo); 42269 } 42270 42271 /** 42272 * Determines what segment request should be made, given current playback 42273 * state. 42274 * 42275 * @param {TimeRanges} buffered - the state of the buffer 42276 * @param {Object} playlist - the playlist object to fetch segments from 42277 * @param {Number} mediaIndex - the previous mediaIndex fetched or null 42278 * @param {Boolean} hasPlayed - a flag indicating whether we have played or not 42279 * @param {Number} currentTime - the playback position in seconds 42280 * @param {Object} syncPoint - a segment info object that describes the 42281 * @returns {Object} a segment request object that describes the segment to load 42282 */ 42283 42284 }, { 42285 key: 'checkBuffer_', 42286 value: function checkBuffer_(buffered, playlist, mediaIndex, hasPlayed, currentTime, syncPoint) { 42287 var lastBufferedEnd = 0; 42288 var startOfSegment = void 0; 42289 42290 if (buffered.length) { 42291 lastBufferedEnd = buffered.end(buffered.length - 1); 42292 } 42293 42294 var bufferedTime = Math.max(0, lastBufferedEnd - currentTime); 42295 42296 if (!playlist.segments.length) { 42297 return null; 42298 } 42299 42300 // if there is plenty of content buffered, and the video has 42301 // been played before relax for awhile 42302 if (bufferedTime >= this.goalBufferLength_()) { 42303 return null; 42304 } 42305 42306 // if the video has not yet played once, and we already have 42307 // one segment downloaded do nothing 42308 if (!hasPlayed && bufferedTime >= 1) { 42309 return null; 42310 } 42311 42312 // When the syncPoint is null, there is no way of determining a good 42313 // conservative segment index to fetch from 42314 // The best thing to do here is to get the kind of sync-point data by 42315 // making a request 42316 if (syncPoint === null) { 42317 mediaIndex = this.getSyncSegmentCandidate_(playlist); 42318 return this.generateSegmentInfo_(playlist, mediaIndex, null, true); 42319 } 42320 42321 // Under normal playback conditions fetching is a simple walk forward 42322 if (mediaIndex !== null) { 42323 var segment = playlist.segments[mediaIndex]; 42324 42325 if (segment && segment.end) { 42326 startOfSegment = segment.end; 42327 } else { 42328 startOfSegment = lastBufferedEnd; 42329 } 42330 return this.generateSegmentInfo_(playlist, mediaIndex + 1, startOfSegment, false); 42331 } 42332 42333 // There is a sync-point but the lack of a mediaIndex indicates that 42334 // we need to make a good conservative guess about which segment to 42335 // fetch 42336 if (this.fetchAtBuffer_) { 42337 // Find the segment containing the end of the buffer 42338 var mediaSourceInfo = Playlist.getMediaInfoForTime(playlist, lastBufferedEnd, syncPoint.segmentIndex, syncPoint.time); 42339 42340 mediaIndex = mediaSourceInfo.mediaIndex; 42341 startOfSegment = mediaSourceInfo.startTime; 42342 } else { 42343 // Find the segment containing currentTime 42344 var _mediaSourceInfo = Playlist.getMediaInfoForTime(playlist, currentTime, syncPoint.segmentIndex, syncPoint.time); 42345 42346 mediaIndex = _mediaSourceInfo.mediaIndex; 42347 startOfSegment = _mediaSourceInfo.startTime; 42348 } 42349 42350 return this.generateSegmentInfo_(playlist, mediaIndex, startOfSegment, false); 42351 } 42352 42353 /** 42354 * The segment loader has no recourse except to fetch a segment in the 42355 * current playlist and use the internal timestamps in that segment to 42356 * generate a syncPoint. This function returns a good candidate index 42357 * for that process. 42358 * 42359 * @param {Object} playlist - the playlist object to look for a 42360 * @returns {Number} An index of a segment from the playlist to load 42361 */ 42362 42363 }, { 42364 key: 'getSyncSegmentCandidate_', 42365 value: function getSyncSegmentCandidate_(playlist) { 42366 var _this2 = this; 42367 42368 if (this.currentTimeline_ === -1) { 42369 return 0; 42370 } 42371 42372 var segmentIndexArray = playlist.segments.map(function (s, i) { 42373 return { 42374 timeline: s.timeline, 42375 segmentIndex: i 42376 }; 42377 }).filter(function (s) { 42378 return s.timeline === _this2.currentTimeline_; 42379 }); 42380 42381 if (segmentIndexArray.length) { 42382 return segmentIndexArray[Math.min(segmentIndexArray.length - 1, 1)].segmentIndex; 42383 } 42384 42385 return Math.max(playlist.segments.length - 1, 0); 42386 } 42387 }, { 42388 key: 'generateSegmentInfo_', 42389 value: function generateSegmentInfo_(playlist, mediaIndex, startOfSegment, isSyncRequest) { 42390 if (mediaIndex < 0 || mediaIndex >= playlist.segments.length) { 42391 return null; 42392 } 42393 42394 var segment = playlist.segments[mediaIndex]; 42395 42396 return { 42397 requestId: 'segment-loader-' + Math.random(), 42398 // resolve the segment URL relative to the playlist 42399 uri: segment.resolvedUri, 42400 // the segment's mediaIndex at the time it was requested 42401 mediaIndex: mediaIndex, 42402 // whether or not to update the SegmentLoader's state with this 42403 // segment's mediaIndex 42404 isSyncRequest: isSyncRequest, 42405 startOfSegment: startOfSegment, 42406 // the segment's playlist 42407 playlist: playlist, 42408 // unencrypted bytes of the segment 42409 bytes: null, 42410 // when a key is defined for this segment, the encrypted bytes 42411 encryptedBytes: null, 42412 // The target timestampOffset for this segment when we append it 42413 // to the source buffer 42414 timestampOffset: null, 42415 // The timeline that the segment is in 42416 timeline: segment.timeline, 42417 // The expected duration of the segment in seconds 42418 duration: segment.duration, 42419 // retain the segment in case the playlist updates while doing an async process 42420 segment: segment 42421 }; 42422 } 42423 42424 /** 42425 * Determines if the network has enough bandwidth to complete the current segment 42426 * request in a timely manner. If not, the request will be aborted early and bandwidth 42427 * updated to trigger a playlist switch. 42428 * 42429 * @param {Object} stats 42430 * Object containing stats about the request timing and size 42431 * @return {Boolean} True if the request was aborted, false otherwise 42432 * @private 42433 */ 42434 42435 }, { 42436 key: 'abortRequestEarly_', 42437 value: function abortRequestEarly_(stats) { 42438 if (this.hls_.tech_.paused() || 42439 // Don't abort if the current playlist is on the lowestEnabledRendition 42440 // TODO: Replace using timeout with a boolean indicating whether this playlist is 42441 // the lowestEnabledRendition. 42442 !this.xhrOptions_.timeout || 42443 // Don't abort if we have no bandwidth information to estimate segment sizes 42444 !this.playlist_.attributes.BANDWIDTH) { 42445 return false; 42446 } 42447 42448 // Wait at least 1 second since the first byte of data has been received before 42449 // using the calculated bandwidth from the progress event to allow the bitrate 42450 // to stabilize 42451 if (Date.now() - (stats.firstBytesReceivedAt || Date.now()) < 1000) { 42452 return false; 42453 } 42454 42455 var currentTime = this.currentTime_(); 42456 var measuredBandwidth = stats.bandwidth; 42457 var segmentDuration = this.pendingSegment_.duration; 42458 42459 var requestTimeRemaining = Playlist.estimateSegmentRequestTime(segmentDuration, measuredBandwidth, this.playlist_, stats.bytesReceived); 42460 42461 // Subtract 1 from the timeUntilRebuffer so we still consider an early abort 42462 // if we are only left with less than 1 second when the request completes. 42463 // A negative timeUntilRebuffering indicates we are already rebuffering 42464 var timeUntilRebuffer$$1 = timeUntilRebuffer(this.buffered_(), currentTime, this.hls_.tech_.playbackRate()) - 1; 42465 42466 // Only consider aborting early if the estimated time to finish the download 42467 // is larger than the estimated time until the player runs out of forward buffer 42468 if (requestTimeRemaining <= timeUntilRebuffer$$1) { 42469 return false; 42470 } 42471 42472 var switchCandidate = minRebufferMaxBandwidthSelector({ 42473 master: this.hls_.playlists.master, 42474 currentTime: currentTime, 42475 bandwidth: measuredBandwidth, 42476 duration: this.duration_(), 42477 segmentDuration: segmentDuration, 42478 timeUntilRebuffer: timeUntilRebuffer$$1, 42479 currentTimeline: this.currentTimeline_, 42480 syncController: this.syncController_ 42481 }); 42482 42483 if (!switchCandidate) { 42484 return; 42485 } 42486 42487 var rebufferingImpact = requestTimeRemaining - timeUntilRebuffer$$1; 42488 42489 var timeSavedBySwitching = rebufferingImpact - switchCandidate.rebufferingImpact; 42490 42491 var minimumTimeSaving = 0.5; 42492 42493 // If we are already rebuffering, increase the amount of variance we add to the 42494 // potential round trip time of the new request so that we are not too aggressive 42495 // with switching to a playlist that might save us a fraction of a second. 42496 if (timeUntilRebuffer$$1 <= TIME_FUDGE_FACTOR) { 42497 minimumTimeSaving = 1; 42498 } 42499 42500 if (!switchCandidate.playlist || switchCandidate.playlist.uri === this.playlist_.uri || timeSavedBySwitching < minimumTimeSaving) { 42501 return false; 42502 } 42503 42504 // set the bandwidth to that of the desired playlist being sure to scale by 42505 // BANDWIDTH_VARIANCE and add one so the playlist selector does not exclude it 42506 // don't trigger a bandwidthupdate as the bandwidth is artifial 42507 this.bandwidth = switchCandidate.playlist.attributes.BANDWIDTH * Config.BANDWIDTH_VARIANCE + 1; 42508 this.abort(); 42509 this.trigger('earlyabort'); 42510 return true; 42511 } 42512 42513 /** 42514 * XHR `progress` event handler 42515 * 42516 * @param {Event} 42517 * The XHR `progress` event 42518 * @param {Object} simpleSegment 42519 * A simplified segment object copy 42520 * @private 42521 */ 42522 42523 }, { 42524 key: 'handleProgress_', 42525 value: function handleProgress_(event, simpleSegment) { 42526 if (!this.pendingSegment_ || simpleSegment.requestId !== this.pendingSegment_.requestId || this.abortRequestEarly_(simpleSegment.stats)) { 42527 return; 42528 } 42529 42530 this.trigger('progress'); 42531 } 42532 42533 /** 42534 * load a specific segment from a request into the buffer 42535 * 42536 * @private 42537 */ 42538 42539 }, { 42540 key: 'loadSegment_', 42541 value: function loadSegment_(segmentInfo) { 42542 this.state = 'WAITING'; 42543 this.pendingSegment_ = segmentInfo; 42544 this.trimBackBuffer_(segmentInfo); 42545 42546 segmentInfo.abortRequests = mediaSegmentRequest(this.hls_.xhr, this.xhrOptions_, this.decrypter_, this.createSimplifiedSegmentObj_(segmentInfo), 42547 // progress callback 42548 this.handleProgress_.bind(this), this.segmentRequestFinished_.bind(this)); 42549 } 42550 42551 /** 42552 * trim the back buffer so that we don't have too much data 42553 * in the source buffer 42554 * 42555 * @private 42556 * 42557 * @param {Object} segmentInfo - the current segment 42558 */ 42559 42560 }, { 42561 key: 'trimBackBuffer_', 42562 value: function trimBackBuffer_(segmentInfo) { 42563 var removeToTime = safeBackBufferTrimTime(this.seekable_(), this.currentTime_(), this.playlist_.targetDuration || 10); 42564 42565 // Chrome has a hard limit of 150MB of 42566 // buffer and a very conservative "garbage collector" 42567 // We manually clear out the old buffer to ensure 42568 // we don't trigger the QuotaExceeded error 42569 // on the source buffer during subsequent appends 42570 42571 if (removeToTime > 0) { 42572 this.remove(0, removeToTime); 42573 } 42574 } 42575 42576 /** 42577 * created a simplified copy of the segment object with just the 42578 * information necessary to perform the XHR and decryption 42579 * 42580 * @private 42581 * 42582 * @param {Object} segmentInfo - the current segment 42583 * @returns {Object} a simplified segment object copy 42584 */ 42585 42586 }, { 42587 key: 'createSimplifiedSegmentObj_', 42588 value: function createSimplifiedSegmentObj_(segmentInfo) { 42589 var segment = segmentInfo.segment; 42590 var simpleSegment = { 42591 resolvedUri: segment.resolvedUri, 42592 byterange: segment.byterange, 42593 requestId: segmentInfo.requestId 42594 }; 42595 42596 if (segment.key) { 42597 // if the media sequence is greater than 2^32, the IV will be incorrect 42598 // assuming 10s segments, that would be about 1300 years 42599 var iv = segment.key.iv || new Uint32Array([0, 0, 0, segmentInfo.mediaIndex + segmentInfo.playlist.mediaSequence]); 42600 42601 simpleSegment.key = { 42602 resolvedUri: segment.key.resolvedUri, 42603 iv: iv 42604 }; 42605 } 42606 42607 if (segment.map) { 42608 simpleSegment.map = this.initSegment(segment.map); 42609 } 42610 42611 return simpleSegment; 42612 } 42613 42614 /** 42615 * Handle the callback from the segmentRequest function and set the 42616 * associated SegmentLoader state and errors if necessary 42617 * 42618 * @private 42619 */ 42620 42621 }, { 42622 key: 'segmentRequestFinished_', 42623 value: function segmentRequestFinished_(error, simpleSegment) { 42624 // every request counts as a media request even if it has been aborted 42625 // or canceled due to a timeout 42626 this.mediaRequests += 1; 42627 42628 if (simpleSegment.stats) { 42629 this.mediaBytesTransferred += simpleSegment.stats.bytesReceived; 42630 this.mediaTransferDuration += simpleSegment.stats.roundTripTime; 42631 } 42632 42633 // The request was aborted and the SegmentLoader has already been reset 42634 if (!this.pendingSegment_) { 42635 this.mediaRequestsAborted += 1; 42636 return; 42637 } 42638 42639 // the request was aborted and the SegmentLoader has already started 42640 // another request. this can happen when the timeout for an aborted 42641 // request triggers due to a limitation in the XHR library 42642 // do not count this as any sort of request or we risk double-counting 42643 if (simpleSegment.requestId !== this.pendingSegment_.requestId) { 42644 return; 42645 } 42646 42647 // an error occurred from the active pendingSegment_ so reset everything 42648 if (error) { 42649 this.pendingSegment_ = null; 42650 this.state = 'READY'; 42651 42652 // the requests were aborted just record the aborted stat and exit 42653 // this is not a true error condition and nothing corrective needs 42654 // to be done 42655 if (error.code === REQUEST_ERRORS.ABORTED) { 42656 this.mediaRequestsAborted += 1; 42657 return; 42658 } 42659 42660 this.pause(); 42661 42662 // the error is really just that at least one of the requests timed-out 42663 // set the bandwidth to a very low value and trigger an ABR switch to 42664 // take emergency action 42665 if (error.code === REQUEST_ERRORS.TIMEOUT) { 42666 this.mediaRequestsTimedout += 1; 42667 this.bandwidth = 1; 42668 this.roundTrip = NaN; 42669 this.trigger('bandwidthupdate'); 42670 return; 42671 } 42672 42673 // if control-flow has arrived here, then the error is real 42674 // emit an error event to blacklist the current playlist 42675 this.mediaRequestsErrored += 1; 42676 this.error(error); 42677 this.trigger('error'); 42678 return; 42679 } 42680 42681 // the response was a success so set any bandwidth stats the request 42682 // generated for ABR purposes 42683 this.bandwidth = simpleSegment.stats.bandwidth; 42684 this.roundTrip = simpleSegment.stats.roundTripTime; 42685 42686 // if this request included an initialization segment, save that data 42687 // to the initSegment cache 42688 if (simpleSegment.map) { 42689 simpleSegment.map = this.initSegment(simpleSegment.map, true); 42690 } 42691 42692 this.processSegmentResponse_(simpleSegment); 42693 } 42694 42695 /** 42696 * Move any important data from the simplified segment object 42697 * back to the real segment object for future phases 42698 * 42699 * @private 42700 */ 42701 42702 }, { 42703 key: 'processSegmentResponse_', 42704 value: function processSegmentResponse_(simpleSegment) { 42705 var segmentInfo = this.pendingSegment_; 42706 42707 segmentInfo.bytes = simpleSegment.bytes; 42708 if (simpleSegment.map) { 42709 segmentInfo.segment.map.bytes = simpleSegment.map.bytes; 42710 } 42711 42712 segmentInfo.endOfAllRequests = simpleSegment.endOfAllRequests; 42713 this.handleSegment_(); 42714 } 42715 42716 /** 42717 * append a decrypted segement to the SourceBuffer through a SourceUpdater 42718 * 42719 * @private 42720 */ 42721 42722 }, { 42723 key: 'handleSegment_', 42724 value: function handleSegment_() { 42725 var _this3 = this; 42726 42727 if (!this.pendingSegment_) { 42728 this.state = 'READY'; 42729 return; 42730 } 42731 42732 var segmentInfo = this.pendingSegment_; 42733 var segment = segmentInfo.segment; 42734 var timingInfo = this.syncController_.probeSegmentInfo(segmentInfo); 42735 42736 // When we have our first timing info, determine what media types this loader is 42737 // dealing with. Although we're maintaining extra state, it helps to preserve the 42738 // separation of segment loader from the actual source buffers. 42739 if (typeof this.startingMedia_ === 'undefined' && timingInfo && ( 42740 // Guard against cases where we're not getting timing info at all until we are 42741 // certain that all streams will provide it. 42742 timingInfo.containsAudio || timingInfo.containsVideo)) { 42743 this.startingMedia_ = { 42744 containsAudio: timingInfo.containsAudio, 42745 containsVideo: timingInfo.containsVideo 42746 }; 42747 } 42748 42749 var illegalMediaSwitchError = illegalMediaSwitch(this.loaderType_, this.startingMedia_, timingInfo); 42750 42751 if (illegalMediaSwitchError) { 42752 this.error({ 42753 message: illegalMediaSwitchError, 42754 blacklistDuration: Infinity 42755 }); 42756 this.trigger('error'); 42757 return; 42758 } 42759 42760 if (segmentInfo.isSyncRequest) { 42761 this.trigger('syncinfoupdate'); 42762 this.pendingSegment_ = null; 42763 this.state = 'READY'; 42764 return; 42765 } 42766 42767 if (segmentInfo.timestampOffset !== null && segmentInfo.timestampOffset !== this.sourceUpdater_.timestampOffset()) { 42768 this.sourceUpdater_.timestampOffset(segmentInfo.timestampOffset); 42769 // fired when a timestamp offset is set in HLS (can also identify discontinuities) 42770 this.trigger('timestampoffset'); 42771 } 42772 42773 var timelineMapping = this.syncController_.mappingForTimeline(segmentInfo.timeline); 42774 42775 if (timelineMapping !== null) { 42776 this.trigger({ 42777 type: 'segmenttimemapping', 42778 mapping: timelineMapping 42779 }); 42780 } 42781 42782 this.state = 'APPENDING'; 42783 42784 // if the media initialization segment is changing, append it 42785 // before the content segment 42786 if (segment.map) { 42787 var initId = initSegmentId(segment.map); 42788 42789 if (!this.activeInitSegmentId_ || this.activeInitSegmentId_ !== initId) { 42790 var initSegment = this.initSegment(segment.map); 42791 42792 this.sourceUpdater_.appendBuffer(initSegment.bytes, function () { 42793 _this3.activeInitSegmentId_ = initId; 42794 }); 42795 } 42796 } 42797 42798 segmentInfo.byteLength = segmentInfo.bytes.byteLength; 42799 if (typeof segment.start === 'number' && typeof segment.end === 'number') { 42800 this.mediaSecondsLoaded += segment.end - segment.start; 42801 } else { 42802 this.mediaSecondsLoaded += segment.duration; 42803 } 42804 42805 this.logger_(segmentInfoString(segmentInfo)); 42806 42807 this.sourceUpdater_.appendBuffer(segmentInfo.bytes, this.handleUpdateEnd_.bind(this)); 42808 } 42809 42810 /** 42811 * callback to run when appendBuffer is finished. detects if we are 42812 * in a good state to do things with the data we got, or if we need 42813 * to wait for more 42814 * 42815 * @private 42816 */ 42817 42818 }, { 42819 key: 'handleUpdateEnd_', 42820 value: function handleUpdateEnd_() { 42821 if (!this.pendingSegment_) { 42822 this.state = 'READY'; 42823 if (!this.paused()) { 42824 this.monitorBuffer_(); 42825 } 42826 return; 42827 } 42828 42829 var segmentInfo = this.pendingSegment_; 42830 var segment = segmentInfo.segment; 42831 var isWalkingForward = this.mediaIndex !== null; 42832 42833 this.pendingSegment_ = null; 42834 this.recordThroughput_(segmentInfo); 42835 this.addSegmentMetadataCue_(segmentInfo); 42836 42837 this.state = 'READY'; 42838 42839 this.mediaIndex = segmentInfo.mediaIndex; 42840 this.fetchAtBuffer_ = true; 42841 this.currentTimeline_ = segmentInfo.timeline; 42842 42843 // We must update the syncinfo to recalculate the seekable range before 42844 // the following conditional otherwise it may consider this a bad "guess" 42845 // and attempt to resync when the post-update seekable window and live 42846 // point would mean that this was the perfect segment to fetch 42847 this.trigger('syncinfoupdate'); 42848 42849 // If we previously appended a segment that ends more than 3 targetDurations before 42850 // the currentTime_ that means that our conservative guess was too conservative. 42851 // In that case, reset the loader state so that we try to use any information gained 42852 // from the previous request to create a new, more accurate, sync-point. 42853 if (segment.end && this.currentTime_() - segment.end > segmentInfo.playlist.targetDuration * 3) { 42854 this.resetEverything(); 42855 return; 42856 } 42857 42858 // Don't do a rendition switch unless we have enough time to get a sync segment 42859 // and conservatively guess 42860 if (isWalkingForward) { 42861 this.trigger('bandwidthupdate'); 42862 } 42863 this.trigger('progress'); 42864 42865 // any time an update finishes and the last segment is in the 42866 // buffer, end the stream. this ensures the "ended" event will 42867 // fire if playback reaches that point. 42868 var isEndOfStream = detectEndOfStream(segmentInfo.playlist, this.mediaSource_, segmentInfo.mediaIndex + 1); 42869 42870 if (isEndOfStream) { 42871 this.endOfStream(); 42872 } 42873 42874 if (!this.paused()) { 42875 this.monitorBuffer_(); 42876 } 42877 } 42878 42879 /** 42880 * Records the current throughput of the decrypt, transmux, and append 42881 * portion of the semgment pipeline. `throughput.rate` is a the cumulative 42882 * moving average of the throughput. `throughput.count` is the number of 42883 * data points in the average. 42884 * 42885 * @private 42886 * @param {Object} segmentInfo the object returned by loadSegment 42887 */ 42888 42889 }, { 42890 key: 'recordThroughput_', 42891 value: function recordThroughput_(segmentInfo) { 42892 var rate = this.throughput.rate; 42893 // Add one to the time to ensure that we don't accidentally attempt to divide 42894 // by zero in the case where the throughput is ridiculously high 42895 var segmentProcessingTime = Date.now() - segmentInfo.endOfAllRequests + 1; 42896 // Multiply by 8000 to convert from bytes/millisecond to bits/second 42897 var segmentProcessingThroughput = Math.floor(segmentInfo.byteLength / segmentProcessingTime * 8 * 1000); 42898 42899 // This is just a cumulative moving average calculation: 42900 // newAvg = oldAvg + (sample - oldAvg) / (sampleCount + 1) 42901 this.throughput.rate += (segmentProcessingThroughput - rate) / ++this.throughput.count; 42902 } 42903 42904 /** 42905 * Adds a cue to the segment-metadata track with some metadata information about the 42906 * segment 42907 * 42908 * @private 42909 * @param {Object} segmentInfo 42910 * the object returned by loadSegment 42911 * @method addSegmentMetadataCue_ 42912 */ 42913 42914 }, { 42915 key: 'addSegmentMetadataCue_', 42916 value: function addSegmentMetadataCue_(segmentInfo) { 42917 if (!this.segmentMetadataTrack_) { 42918 return; 42919 } 42920 42921 var segment = segmentInfo.segment; 42922 var start = segment.start; 42923 var end = segment.end; 42924 42925 // Do not try adding the cue if the start and end times are invalid. 42926 if (!finite(start) || !finite(end)) { 42927 return; 42928 } 42929 42930 removeCuesFromTrack(start, end, this.segmentMetadataTrack_); 42931 42932 var Cue = window_1.WebKitDataCue || window_1.VTTCue; 42933 var value = { 42934 bandwidth: segmentInfo.playlist.attributes.BANDWIDTH, 42935 resolution: segmentInfo.playlist.attributes.RESOLUTION, 42936 codecs: segmentInfo.playlist.attributes.CODECS, 42937 byteLength: segmentInfo.byteLength, 42938 uri: segmentInfo.uri, 42939 timeline: segmentInfo.timeline, 42940 playlist: segmentInfo.playlist.uri, 42941 start: start, 42942 end: end 42943 }; 42944 var data = JSON.stringify(value); 42945 var cue = new Cue(start, end, data); 42946 42947 // Attach the metadata to the value property of the cue to keep consistency between 42948 // the differences of WebKitDataCue in safari and VTTCue in other browsers 42949 cue.value = value; 42950 42951 this.segmentMetadataTrack_.addCue(cue); 42952 } 42953 }]); 42954 return SegmentLoader; 42955 }(videojs$1.EventTarget); 42956 42957 /** 42958 * @file vtt-segment-loader.js 42959 */ 42960 42961 var VTT_LINE_TERMINATORS = new Uint8Array('\n\n'.split('').map(function (char) { 42962 return char.charCodeAt(0); 42963 })); 42964 42965 var uintToString = function uintToString(uintArray) { 42966 return String.fromCharCode.apply(null, uintArray); 42967 }; 42968 42969 /** 42970 * An object that manages segment loading and appending. 42971 * 42972 * @class VTTSegmentLoader 42973 * @param {Object} options required and optional options 42974 * @extends videojs.EventTarget 42975 */ 42976 42977 var VTTSegmentLoader = function (_SegmentLoader) { 42978 inherits$3(VTTSegmentLoader, _SegmentLoader); 42979 42980 function VTTSegmentLoader(settings) { 42981 var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 42982 classCallCheck$3(this, VTTSegmentLoader); 42983 42984 // SegmentLoader requires a MediaSource be specified or it will throw an error; 42985 // however, VTTSegmentLoader has no need of a media source, so delete the reference 42986 var _this = possibleConstructorReturn$3(this, (VTTSegmentLoader.__proto__ || Object.getPrototypeOf(VTTSegmentLoader)).call(this, settings, options)); 42987 42988 _this.mediaSource_ = null; 42989 42990 _this.subtitlesTrack_ = null; 42991 return _this; 42992 } 42993 42994 /** 42995 * Indicates which time ranges are buffered 42996 * 42997 * @return {TimeRange} 42998 * TimeRange object representing the current buffered ranges 42999 */ 43000 43001 createClass$2(VTTSegmentLoader, [{ 43002 key: 'buffered_', 43003 value: function buffered_() { 43004 if (!this.subtitlesTrack_ || !this.subtitlesTrack_.cues.length) { 43005 return videojs$1.createTimeRanges(); 43006 } 43007 43008 var cues = this.subtitlesTrack_.cues; 43009 var start = cues[0].startTime; 43010 var end = cues[cues.length - 1].startTime; 43011 43012 return videojs$1.createTimeRanges([[start, end]]); 43013 } 43014 43015 /** 43016 * Gets and sets init segment for the provided map 43017 * 43018 * @param {Object} map 43019 * The map object representing the init segment to get or set 43020 * @param {Boolean=} set 43021 * If true, the init segment for the provided map should be saved 43022 * @return {Object} 43023 * map object for desired init segment 43024 */ 43025 43026 }, { 43027 key: 'initSegment', 43028 value: function initSegment(map) { 43029 var set$$1 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; 43030 43031 if (!map) { 43032 return null; 43033 } 43034 43035 var id = initSegmentId(map); 43036 var storedMap = this.initSegments_[id]; 43037 43038 if (set$$1 && !storedMap && map.bytes) { 43039 // append WebVTT line terminators to the media initialization segment if it exists 43040 // to follow the WebVTT spec (https://w3c.github.io/webvtt/#file-structure) that 43041 // requires two or more WebVTT line terminators between the WebVTT header and the 43042 // rest of the file 43043 var combinedByteLength = VTT_LINE_TERMINATORS.byteLength + map.bytes.byteLength; 43044 var combinedSegment = new Uint8Array(combinedByteLength); 43045 43046 combinedSegment.set(map.bytes); 43047 combinedSegment.set(VTT_LINE_TERMINATORS, map.bytes.byteLength); 43048 43049 this.initSegments_[id] = storedMap = { 43050 resolvedUri: map.resolvedUri, 43051 byterange: map.byterange, 43052 bytes: combinedSegment 43053 }; 43054 } 43055 43056 return storedMap || map; 43057 } 43058 43059 /** 43060 * Returns true if all configuration required for loading is present, otherwise false. 43061 * 43062 * @return {Boolean} True if the all configuration is ready for loading 43063 * @private 43064 */ 43065 43066 }, { 43067 key: 'couldBeginLoading_', 43068 value: function couldBeginLoading_() { 43069 return this.playlist_ && this.subtitlesTrack_ && !this.paused(); 43070 } 43071 43072 /** 43073 * Once all the starting parameters have been specified, begin 43074 * operation. This method should only be invoked from the INIT 43075 * state. 43076 * 43077 * @private 43078 */ 43079 43080 }, { 43081 key: 'init_', 43082 value: function init_() { 43083 this.state = 'READY'; 43084 this.resetEverything(); 43085 return this.monitorBuffer_(); 43086 } 43087 43088 /** 43089 * Set a subtitle track on the segment loader to add subtitles to 43090 * 43091 * @param {TextTrack=} track 43092 * The text track to add loaded subtitles to 43093 * @return {TextTrack} 43094 * Returns the subtitles track 43095 */ 43096 43097 }, { 43098 key: 'track', 43099 value: function track(_track) { 43100 if (typeof _track === 'undefined') { 43101 return this.subtitlesTrack_; 43102 } 43103 43104 this.subtitlesTrack_ = _track; 43105 43106 // if we were unpaused but waiting for a sourceUpdater, start 43107 // buffering now 43108 if (this.state === 'INIT' && this.couldBeginLoading_()) { 43109 this.init_(); 43110 } 43111 43112 return this.subtitlesTrack_; 43113 } 43114 43115 /** 43116 * Remove any data in the source buffer between start and end times 43117 * @param {Number} start - the start time of the region to remove from the buffer 43118 * @param {Number} end - the end time of the region to remove from the buffer 43119 */ 43120 43121 }, { 43122 key: 'remove', 43123 value: function remove(start, end) { 43124 removeCuesFromTrack(start, end, this.subtitlesTrack_); 43125 } 43126 43127 /** 43128 * fill the buffer with segements unless the sourceBuffers are 43129 * currently updating 43130 * 43131 * Note: this function should only ever be called by monitorBuffer_ 43132 * and never directly 43133 * 43134 * @private 43135 */ 43136 43137 }, { 43138 key: 'fillBuffer_', 43139 value: function fillBuffer_() { 43140 var _this2 = this; 43141 43142 if (!this.syncPoint_) { 43143 this.syncPoint_ = this.syncController_.getSyncPoint(this.playlist_, this.duration_(), this.currentTimeline_, this.currentTime_()); 43144 } 43145 43146 // see if we need to begin loading immediately 43147 var segmentInfo = this.checkBuffer_(this.buffered_(), this.playlist_, this.mediaIndex, this.hasPlayed_(), this.currentTime_(), this.syncPoint_); 43148 43149 segmentInfo = this.skipEmptySegments_(segmentInfo); 43150 43151 if (!segmentInfo) { 43152 return; 43153 } 43154 43155 if (this.syncController_.timestampOffsetForTimeline(segmentInfo.timeline) === null) { 43156 // We don't have the timestamp offset that we need to sync subtitles. 43157 // Rerun on a timestamp offset or user interaction. 43158 var checkTimestampOffset = function checkTimestampOffset() { 43159 _this2.state = 'READY'; 43160 if (!_this2.paused()) { 43161 // if not paused, queue a buffer check as soon as possible 43162 _this2.monitorBuffer_(); 43163 } 43164 }; 43165 43166 this.syncController_.one('timestampoffset', checkTimestampOffset); 43167 this.state = 'WAITING_ON_TIMELINE'; 43168 return; 43169 } 43170 43171 this.loadSegment_(segmentInfo); 43172 } 43173 43174 /** 43175 * Prevents the segment loader from requesting segments we know contain no subtitles 43176 * by walking forward until we find the next segment that we don't know whether it is 43177 * empty or not. 43178 * 43179 * @param {Object} segmentInfo 43180 * a segment info object that describes the current segment 43181 * @return {Object} 43182 * a segment info object that describes the current segment 43183 */ 43184 43185 }, { 43186 key: 'skipEmptySegments_', 43187 value: function skipEmptySegments_(segmentInfo) { 43188 while (segmentInfo && segmentInfo.segment.empty) { 43189 segmentInfo = this.generateSegmentInfo_(segmentInfo.playlist, segmentInfo.mediaIndex + 1, segmentInfo.startOfSegment + segmentInfo.duration, segmentInfo.isSyncRequest); 43190 } 43191 return segmentInfo; 43192 } 43193 43194 /** 43195 * append a decrypted segement to the SourceBuffer through a SourceUpdater 43196 * 43197 * @private 43198 */ 43199 43200 }, { 43201 key: 'handleSegment_', 43202 value: function handleSegment_() { 43203 var _this3 = this; 43204 43205 if (!this.pendingSegment_ || !this.subtitlesTrack_) { 43206 this.state = 'READY'; 43207 return; 43208 } 43209 43210 this.state = 'APPENDING'; 43211 43212 var segmentInfo = this.pendingSegment_; 43213 var segment = segmentInfo.segment; 43214 43215 // Make sure that vttjs has loaded, otherwise, wait till it finished loading 43216 if (typeof window_1.WebVTT !== 'function' && this.subtitlesTrack_ && this.subtitlesTrack_.tech_) { 43217 43218 var loadHandler = function loadHandler() { 43219 _this3.handleSegment_(); 43220 }; 43221 43222 this.state = 'WAITING_ON_VTTJS'; 43223 this.subtitlesTrack_.tech_.one('vttjsloaded', loadHandler); 43224 this.subtitlesTrack_.tech_.one('vttjserror', function () { 43225 _this3.subtitlesTrack_.tech_.off('vttjsloaded', loadHandler); 43226 _this3.error({ 43227 message: 'Error loading vtt.js' 43228 }); 43229 _this3.state = 'READY'; 43230 _this3.pause(); 43231 _this3.trigger('error'); 43232 }); 43233 43234 return; 43235 } 43236 43237 segment.requested = true; 43238 43239 try { 43240 this.parseVTTCues_(segmentInfo); 43241 } catch (e) { 43242 this.error({ 43243 message: e.message 43244 }); 43245 this.state = 'READY'; 43246 this.pause(); 43247 return this.trigger('error'); 43248 } 43249 43250 this.updateTimeMapping_(segmentInfo, this.syncController_.timelines[segmentInfo.timeline], this.playlist_); 43251 43252 if (segmentInfo.isSyncRequest) { 43253 this.trigger('syncinfoupdate'); 43254 this.pendingSegment_ = null; 43255 this.state = 'READY'; 43256 return; 43257 } 43258 43259 segmentInfo.byteLength = segmentInfo.bytes.byteLength; 43260 43261 this.mediaSecondsLoaded += segment.duration; 43262 43263 if (segmentInfo.cues.length) { 43264 // remove any overlapping cues to prevent doubling 43265 this.remove(segmentInfo.cues[0].endTime, segmentInfo.cues[segmentInfo.cues.length - 1].endTime); 43266 } 43267 43268 segmentInfo.cues.forEach(function (cue) { 43269 _this3.subtitlesTrack_.addCue(cue); 43270 }); 43271 43272 this.handleUpdateEnd_(); 43273 } 43274 43275 /** 43276 * Uses the WebVTT parser to parse the segment response 43277 * 43278 * @param {Object} segmentInfo 43279 * a segment info object that describes the current segment 43280 * @private 43281 */ 43282 43283 }, { 43284 key: 'parseVTTCues_', 43285 value: function parseVTTCues_(segmentInfo) { 43286 var decoder = void 0; 43287 var decodeBytesToString = false; 43288 43289 if (typeof window_1.TextDecoder === 'function') { 43290 decoder = new window_1.TextDecoder('utf8'); 43291 } else { 43292 decoder = window_1.WebVTT.StringDecoder(); 43293 decodeBytesToString = true; 43294 } 43295 43296 var parser = new window_1.WebVTT.Parser(window_1, window_1.vttjs, decoder); 43297 43298 segmentInfo.cues = []; 43299 segmentInfo.timestampmap = { MPEGTS: 0, LOCAL: 0 }; 43300 43301 parser.oncue = segmentInfo.cues.push.bind(segmentInfo.cues); 43302 parser.ontimestampmap = function (map) { 43303 return segmentInfo.timestampmap = map; 43304 }; 43305 parser.onparsingerror = function (error) { 43306 videojs$1.log.warn('Error encountered when parsing cues: ' + error.message); 43307 }; 43308 43309 if (segmentInfo.segment.map) { 43310 var mapData = segmentInfo.segment.map.bytes; 43311 43312 if (decodeBytesToString) { 43313 mapData = uintToString(mapData); 43314 } 43315 43316 parser.parse(mapData); 43317 } 43318 43319 var segmentData = segmentInfo.bytes; 43320 43321 if (decodeBytesToString) { 43322 segmentData = uintToString(segmentData); 43323 } 43324 43325 parser.parse(segmentData); 43326 parser.flush(); 43327 } 43328 43329 /** 43330 * Updates the start and end times of any cues parsed by the WebVTT parser using 43331 * the information parsed from the X-TIMESTAMP-MAP header and a TS to media time mapping 43332 * from the SyncController 43333 * 43334 * @param {Object} segmentInfo 43335 * a segment info object that describes the current segment 43336 * @param {Object} mappingObj 43337 * object containing a mapping from TS to media time 43338 * @param {Object} playlist 43339 * the playlist object containing the segment 43340 * @private 43341 */ 43342 43343 }, { 43344 key: 'updateTimeMapping_', 43345 value: function updateTimeMapping_(segmentInfo, mappingObj, playlist) { 43346 var segment = segmentInfo.segment; 43347 43348 if (!mappingObj) { 43349 // If the sync controller does not have a mapping of TS to Media Time for the 43350 // timeline, then we don't have enough information to update the cue 43351 // start/end times 43352 return; 43353 } 43354 43355 if (!segmentInfo.cues.length) { 43356 // If there are no cues, we also do not have enough information to figure out 43357 // segment timing. Mark that the segment contains no cues so we don't re-request 43358 // an empty segment. 43359 segment.empty = true; 43360 return; 43361 } 43362 43363 var timestampmap = segmentInfo.timestampmap; 43364 var diff = timestampmap.MPEGTS / 90000 - timestampmap.LOCAL + mappingObj.mapping; 43365 43366 segmentInfo.cues.forEach(function (cue) { 43367 // First convert cue time to TS time using the timestamp-map provided within the vtt 43368 cue.startTime += diff; 43369 cue.endTime += diff; 43370 }); 43371 43372 if (!playlist.syncInfo) { 43373 var firstStart = segmentInfo.cues[0].startTime; 43374 var lastStart = segmentInfo.cues[segmentInfo.cues.length - 1].startTime; 43375 43376 playlist.syncInfo = { 43377 mediaSequence: playlist.mediaSequence + segmentInfo.mediaIndex, 43378 time: Math.min(firstStart, lastStart - segment.duration) 43379 }; 43380 } 43381 } 43382 }]); 43383 return VTTSegmentLoader; 43384 }(SegmentLoader); 43385 43386 /** 43387 * @file ad-cue-tags.js 43388 */ 43389 43390 /** 43391 * Searches for an ad cue that overlaps with the given mediaTime 43392 */ 43393 var findAdCue = function findAdCue(track, mediaTime) { 43394 var cues = track.cues; 43395 43396 for (var i = 0; i < cues.length; i++) { 43397 var cue = cues[i]; 43398 43399 if (mediaTime >= cue.adStartTime && mediaTime <= cue.adEndTime) { 43400 return cue; 43401 } 43402 } 43403 return null; 43404 }; 43405 43406 var updateAdCues = function updateAdCues(media, track) { 43407 var offset = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; 43408 43409 if (!media.segments) { 43410 return; 43411 } 43412 43413 var mediaTime = offset; 43414 var cue = void 0; 43415 43416 for (var i = 0; i < media.segments.length; i++) { 43417 var segment = media.segments[i]; 43418 43419 if (!cue) { 43420 // Since the cues will span for at least the segment duration, adding a fudge 43421 // factor of half segment duration will prevent duplicate cues from being 43422 // created when timing info is not exact (e.g. cue start time initialized 43423 // at 10.006677, but next call mediaTime is 10.003332 ) 43424 cue = findAdCue(track, mediaTime + segment.duration / 2); 43425 } 43426 43427 if (cue) { 43428 if ('cueIn' in segment) { 43429 // Found a CUE-IN so end the cue 43430 cue.endTime = mediaTime; 43431 cue.adEndTime = mediaTime; 43432 mediaTime += segment.duration; 43433 cue = null; 43434 continue; 43435 } 43436 43437 if (mediaTime < cue.endTime) { 43438 // Already processed this mediaTime for this cue 43439 mediaTime += segment.duration; 43440 continue; 43441 } 43442 43443 // otherwise extend cue until a CUE-IN is found 43444 cue.endTime += segment.duration; 43445 } else { 43446 if ('cueOut' in segment) { 43447 cue = new window_1.VTTCue(mediaTime, mediaTime + segment.duration, segment.cueOut); 43448 cue.adStartTime = mediaTime; 43449 // Assumes tag format to be 43450 // #EXT-X-CUE-OUT:30 43451 cue.adEndTime = mediaTime + parseFloat(segment.cueOut); 43452 track.addCue(cue); 43453 } 43454 43455 if ('cueOutCont' in segment) { 43456 // Entered into the middle of an ad cue 43457 var adOffset = void 0; 43458 var adTotal = void 0; 43459 43460 // Assumes tag formate to be 43461 // #EXT-X-CUE-OUT-CONT:10/30 43462 43463 var _segment$cueOutCont$s = segment.cueOutCont.split('/').map(parseFloat); 43464 43465 var _segment$cueOutCont$s2 = slicedToArray$1(_segment$cueOutCont$s, 2); 43466 43467 adOffset = _segment$cueOutCont$s2[0]; 43468 adTotal = _segment$cueOutCont$s2[1]; 43469 43470 cue = new window_1.VTTCue(mediaTime, mediaTime + segment.duration, ''); 43471 cue.adStartTime = mediaTime - adOffset; 43472 cue.adEndTime = cue.adStartTime + adTotal; 43473 track.addCue(cue); 43474 } 43475 } 43476 mediaTime += segment.duration; 43477 } 43478 }; 43479 43480 /** 43481 * @file sync-controller.js 43482 */ 43483 43484 var tsprobe = tsInspector.inspect; 43485 43486 var syncPointStrategies = [ 43487 // Stategy "VOD": Handle the VOD-case where the sync-point is *always* 43488 // the equivalence display-time 0 === segment-index 0 43489 { 43490 name: 'VOD', 43491 run: function run(syncController, playlist, duration$$1, currentTimeline, currentTime) { 43492 if (duration$$1 !== Infinity) { 43493 var syncPoint = { 43494 time: 0, 43495 segmentIndex: 0 43496 }; 43497 43498 return syncPoint; 43499 } 43500 return null; 43501 } 43502 }, 43503 // Stategy "ProgramDateTime": We have a program-date-time tag in this playlist 43504 { 43505 name: 'ProgramDateTime', 43506 run: function run(syncController, playlist, duration$$1, currentTimeline, currentTime) { 43507 if (!syncController.datetimeToDisplayTime) { 43508 return null; 43509 } 43510 43511 var segments = playlist.segments || []; 43512 var syncPoint = null; 43513 var lastDistance = null; 43514 43515 currentTime = currentTime || 0; 43516 43517 for (var i = 0; i < segments.length; i++) { 43518 var segment = segments[i]; 43519 43520 if (segment.dateTimeObject) { 43521 var segmentTime = segment.dateTimeObject.getTime() / 1000; 43522 var segmentStart = segmentTime + syncController.datetimeToDisplayTime; 43523 var distance = Math.abs(currentTime - segmentStart); 43524 43525 // Once the distance begins to increase, we have passed 43526 // currentTime and can stop looking for better candidates 43527 if (lastDistance !== null && lastDistance < distance) { 43528 break; 43529 } 43530 43531 lastDistance = distance; 43532 syncPoint = { 43533 time: segmentStart, 43534 segmentIndex: i 43535 }; 43536 } 43537 } 43538 return syncPoint; 43539 } 43540 }, 43541 // Stategy "Segment": We have a known time mapping for a timeline and a 43542 // segment in the current timeline with timing data 43543 { 43544 name: 'Segment', 43545 run: function run(syncController, playlist, duration$$1, currentTimeline, currentTime) { 43546 var segments = playlist.segments || []; 43547 var syncPoint = null; 43548 var lastDistance = null; 43549 43550 currentTime = currentTime || 0; 43551 43552 for (var i = 0; i < segments.length; i++) { 43553 var segment = segments[i]; 43554 43555 if (segment.timeline === currentTimeline && typeof segment.start !== 'undefined') { 43556 var distance = Math.abs(currentTime - segment.start); 43557 43558 // Once the distance begins to increase, we have passed 43559 // currentTime and can stop looking for better candidates 43560 if (lastDistance !== null && lastDistance < distance) { 43561 break; 43562 } 43563 43564 if (!syncPoint || lastDistance === null || lastDistance >= distance) { 43565 lastDistance = distance; 43566 syncPoint = { 43567 time: segment.start, 43568 segmentIndex: i 43569 }; 43570 } 43571 } 43572 } 43573 return syncPoint; 43574 } 43575 }, 43576 // Stategy "Discontinuity": We have a discontinuity with a known 43577 // display-time 43578 { 43579 name: 'Discontinuity', 43580 run: function run(syncController, playlist, duration$$1, currentTimeline, currentTime) { 43581 var syncPoint = null; 43582 43583 currentTime = currentTime || 0; 43584 43585 if (playlist.discontinuityStarts && playlist.discontinuityStarts.length) { 43586 var lastDistance = null; 43587 43588 for (var i = 0; i < playlist.discontinuityStarts.length; i++) { 43589 var segmentIndex = playlist.discontinuityStarts[i]; 43590 var discontinuity = playlist.discontinuitySequence + i + 1; 43591 var discontinuitySync = syncController.discontinuities[discontinuity]; 43592 43593 if (discontinuitySync) { 43594 var distance = Math.abs(currentTime - discontinuitySync.time); 43595 43596 // Once the distance begins to increase, we have passed 43597 // currentTime and can stop looking for better candidates 43598 if (lastDistance !== null && lastDistance < distance) { 43599 break; 43600 } 43601 43602 if (!syncPoint || lastDistance === null || lastDistance >= distance) { 43603 lastDistance = distance; 43604 syncPoint = { 43605 time: discontinuitySync.time, 43606 segmentIndex: segmentIndex 43607 }; 43608 } 43609 } 43610 } 43611 } 43612 return syncPoint; 43613 } 43614 }, 43615 // Stategy "Playlist": We have a playlist with a known mapping of 43616 // segment index to display time 43617 { 43618 name: 'Playlist', 43619 run: function run(syncController, playlist, duration$$1, currentTimeline, currentTime) { 43620 if (playlist.syncInfo) { 43621 var syncPoint = { 43622 time: playlist.syncInfo.time, 43623 segmentIndex: playlist.syncInfo.mediaSequence - playlist.mediaSequence 43624 }; 43625 43626 return syncPoint; 43627 } 43628 return null; 43629 } 43630 }]; 43631 43632 var SyncController = function (_videojs$EventTarget) { 43633 inherits$3(SyncController, _videojs$EventTarget); 43634 43635 function SyncController() { 43636 classCallCheck$3(this, SyncController); 43637 43638 // Segment Loader state variables... 43639 // ...for synching across variants 43640 var _this = possibleConstructorReturn$3(this, (SyncController.__proto__ || Object.getPrototypeOf(SyncController)).call(this)); 43641 43642 _this.inspectCache_ = undefined; 43643 43644 // ...for synching across variants 43645 _this.timelines = []; 43646 _this.discontinuities = []; 43647 _this.datetimeToDisplayTime = null; 43648 43649 _this.logger_ = logger('SyncController'); 43650 return _this; 43651 } 43652 43653 /** 43654 * Find a sync-point for the playlist specified 43655 * 43656 * A sync-point is defined as a known mapping from display-time to 43657 * a segment-index in the current playlist. 43658 * 43659 * @param {Playlist} playlist 43660 * The playlist that needs a sync-point 43661 * @param {Number} duration 43662 * Duration of the MediaSource (Infinite if playing a live source) 43663 * @param {Number} currentTimeline 43664 * The last timeline from which a segment was loaded 43665 * @returns {Object} 43666 * A sync-point object 43667 */ 43668 43669 createClass$2(SyncController, [{ 43670 key: 'getSyncPoint', 43671 value: function getSyncPoint(playlist, duration$$1, currentTimeline, currentTime) { 43672 var syncPoints = this.runStrategies_(playlist, duration$$1, currentTimeline, currentTime); 43673 43674 if (!syncPoints.length) { 43675 // Signal that we need to attempt to get a sync-point manually 43676 // by fetching a segment in the playlist and constructing 43677 // a sync-point from that information 43678 return null; 43679 } 43680 43681 // Now find the sync-point that is closest to the currentTime because 43682 // that should result in the most accurate guess about which segment 43683 // to fetch 43684 return this.selectSyncPoint_(syncPoints, { key: 'time', value: currentTime }); 43685 } 43686 43687 /** 43688 * Calculate the amount of time that has expired off the playlist during playback 43689 * 43690 * @param {Playlist} playlist 43691 * Playlist object to calculate expired from 43692 * @param {Number} duration 43693 * Duration of the MediaSource (Infinity if playling a live source) 43694 * @returns {Number|null} 43695 * The amount of time that has expired off the playlist during playback. Null 43696 * if no sync-points for the playlist can be found. 43697 */ 43698 43699 }, { 43700 key: 'getExpiredTime', 43701 value: function getExpiredTime(playlist, duration$$1) { 43702 if (!playlist || !playlist.segments) { 43703 return null; 43704 } 43705 43706 var syncPoints = this.runStrategies_(playlist, duration$$1, playlist.discontinuitySequence, 0); 43707 43708 // Without sync-points, there is not enough information to determine the expired time 43709 if (!syncPoints.length) { 43710 return null; 43711 } 43712 43713 var syncPoint = this.selectSyncPoint_(syncPoints, { 43714 key: 'segmentIndex', 43715 value: 0 43716 }); 43717 43718 // If the sync-point is beyond the start of the playlist, we want to subtract the 43719 // duration from index 0 to syncPoint.segmentIndex instead of adding. 43720 if (syncPoint.segmentIndex > 0) { 43721 syncPoint.time *= -1; 43722 } 43723 43724 return Math.abs(syncPoint.time + sumDurations(playlist, syncPoint.segmentIndex, 0)); 43725 } 43726 43727 /** 43728 * Runs each sync-point strategy and returns a list of sync-points returned by the 43729 * strategies 43730 * 43731 * @private 43732 * @param {Playlist} playlist 43733 * The playlist that needs a sync-point 43734 * @param {Number} duration 43735 * Duration of the MediaSource (Infinity if playing a live source) 43736 * @param {Number} currentTimeline 43737 * The last timeline from which a segment was loaded 43738 * @returns {Array} 43739 * A list of sync-point objects 43740 */ 43741 43742 }, { 43743 key: 'runStrategies_', 43744 value: function runStrategies_(playlist, duration$$1, currentTimeline, currentTime) { 43745 var syncPoints = []; 43746 43747 // Try to find a sync-point in by utilizing various strategies... 43748 for (var i = 0; i < syncPointStrategies.length; i++) { 43749 var strategy = syncPointStrategies[i]; 43750 var syncPoint = strategy.run(this, playlist, duration$$1, currentTimeline, currentTime); 43751 43752 if (syncPoint) { 43753 syncPoint.strategy = strategy.name; 43754 syncPoints.push({ 43755 strategy: strategy.name, 43756 syncPoint: syncPoint 43757 }); 43758 } 43759 } 43760 43761 return syncPoints; 43762 } 43763 43764 /** 43765 * Selects the sync-point nearest the specified target 43766 * 43767 * @private 43768 * @param {Array} syncPoints 43769 * List of sync-points to select from 43770 * @param {Object} target 43771 * Object specifying the property and value we are targeting 43772 * @param {String} target.key 43773 * Specifies the property to target. Must be either 'time' or 'segmentIndex' 43774 * @param {Number} target.value 43775 * The value to target for the specified key. 43776 * @returns {Object} 43777 * The sync-point nearest the target 43778 */ 43779 43780 }, { 43781 key: 'selectSyncPoint_', 43782 value: function selectSyncPoint_(syncPoints, target) { 43783 var bestSyncPoint = syncPoints[0].syncPoint; 43784 var bestDistance = Math.abs(syncPoints[0].syncPoint[target.key] - target.value); 43785 var bestStrategy = syncPoints[0].strategy; 43786 43787 for (var i = 1; i < syncPoints.length; i++) { 43788 var newDistance = Math.abs(syncPoints[i].syncPoint[target.key] - target.value); 43789 43790 if (newDistance < bestDistance) { 43791 bestDistance = newDistance; 43792 bestSyncPoint = syncPoints[i].syncPoint; 43793 bestStrategy = syncPoints[i].strategy; 43794 } 43795 } 43796 43797 this.logger_('syncPoint for [' + target.key + ': ' + target.value + '] chosen with strategy' + (' [' + bestStrategy + ']: [time:' + bestSyncPoint.time + ',') + (' segmentIndex:' + bestSyncPoint.segmentIndex + ']')); 43798 43799 return bestSyncPoint; 43800 } 43801 43802 /** 43803 * Save any meta-data present on the segments when segments leave 43804 * the live window to the playlist to allow for synchronization at the 43805 * playlist level later. 43806 * 43807 * @param {Playlist} oldPlaylist - The previous active playlist 43808 * @param {Playlist} newPlaylist - The updated and most current playlist 43809 */ 43810 43811 }, { 43812 key: 'saveExpiredSegmentInfo', 43813 value: function saveExpiredSegmentInfo(oldPlaylist, newPlaylist) { 43814 var mediaSequenceDiff = newPlaylist.mediaSequence - oldPlaylist.mediaSequence; 43815 43816 // When a segment expires from the playlist and it has a start time 43817 // save that information as a possible sync-point reference in future 43818 for (var i = mediaSequenceDiff - 1; i >= 0; i--) { 43819 var lastRemovedSegment = oldPlaylist.segments[i]; 43820 43821 if (lastRemovedSegment && typeof lastRemovedSegment.start !== 'undefined') { 43822 newPlaylist.syncInfo = { 43823 mediaSequence: oldPlaylist.mediaSequence + i, 43824 time: lastRemovedSegment.start 43825 }; 43826 this.logger_('playlist refresh sync: [time:' + newPlaylist.syncInfo.time + ',' + (' mediaSequence: ' + newPlaylist.syncInfo.mediaSequence + ']')); 43827 this.trigger('syncinfoupdate'); 43828 break; 43829 } 43830 } 43831 } 43832 43833 /** 43834 * Save the mapping from playlist's ProgramDateTime to display. This should 43835 * only ever happen once at the start of playback. 43836 * 43837 * @param {Playlist} playlist - The currently active playlist 43838 */ 43839 43840 }, { 43841 key: 'setDateTimeMapping', 43842 value: function setDateTimeMapping(playlist) { 43843 if (!this.datetimeToDisplayTime && playlist.segments && playlist.segments.length && playlist.segments[0].dateTimeObject) { 43844 var playlistTimestamp = playlist.segments[0].dateTimeObject.getTime() / 1000; 43845 43846 this.datetimeToDisplayTime = -playlistTimestamp; 43847 } 43848 } 43849 43850 /** 43851 * Reset the state of the inspection cache when we do a rendition 43852 * switch 43853 */ 43854 43855 }, { 43856 key: 'reset', 43857 value: function reset() { 43858 this.inspectCache_ = undefined; 43859 } 43860 43861 /** 43862 * Probe or inspect a fmp4 or an mpeg2-ts segment to determine the start 43863 * and end of the segment in it's internal "media time". Used to generate 43864 * mappings from that internal "media time" to the display time that is 43865 * shown on the player. 43866 * 43867 * @param {SegmentInfo} segmentInfo - The current active request information 43868 */ 43869 43870 }, { 43871 key: 'probeSegmentInfo', 43872 value: function probeSegmentInfo(segmentInfo) { 43873 var segment = segmentInfo.segment; 43874 var playlist = segmentInfo.playlist; 43875 var timingInfo = void 0; 43876 43877 if (segment.map) { 43878 timingInfo = this.probeMp4Segment_(segmentInfo); 43879 } else { 43880 timingInfo = this.probeTsSegment_(segmentInfo); 43881 } 43882 43883 if (timingInfo) { 43884 if (this.calculateSegmentTimeMapping_(segmentInfo, timingInfo)) { 43885 this.saveDiscontinuitySyncInfo_(segmentInfo); 43886 43887 // If the playlist does not have sync information yet, record that information 43888 // now with segment timing information 43889 if (!playlist.syncInfo) { 43890 playlist.syncInfo = { 43891 mediaSequence: playlist.mediaSequence + segmentInfo.mediaIndex, 43892 time: segment.start 43893 }; 43894 } 43895 } 43896 } 43897 43898 return timingInfo; 43899 } 43900 43901 /** 43902 * Probe an fmp4 or an mpeg2-ts segment to determine the start of the segment 43903 * in it's internal "media time". 43904 * 43905 * @private 43906 * @param {SegmentInfo} segmentInfo - The current active request information 43907 * @return {object} The start and end time of the current segment in "media time" 43908 */ 43909 43910 }, { 43911 key: 'probeMp4Segment_', 43912 value: function probeMp4Segment_(segmentInfo) { 43913 var segment = segmentInfo.segment; 43914 var timescales = probe.timescale(segment.map.bytes); 43915 var startTime = probe.startTime(timescales, segmentInfo.bytes); 43916 43917 if (segmentInfo.timestampOffset !== null) { 43918 segmentInfo.timestampOffset -= startTime; 43919 } 43920 43921 return { 43922 start: startTime, 43923 end: startTime + segment.duration 43924 }; 43925 } 43926 43927 /** 43928 * Probe an mpeg2-ts segment to determine the start and end of the segment 43929 * in it's internal "media time". 43930 * 43931 * @private 43932 * @param {SegmentInfo} segmentInfo - The current active request information 43933 * @return {object} The start and end time of the current segment in "media time" 43934 */ 43935 43936 }, { 43937 key: 'probeTsSegment_', 43938 value: function probeTsSegment_(segmentInfo) { 43939 var timeInfo = tsprobe(segmentInfo.bytes, this.inspectCache_); 43940 var segmentStartTime = void 0; 43941 var segmentEndTime = void 0; 43942 43943 if (!timeInfo) { 43944 return null; 43945 } 43946 43947 if (timeInfo.video && timeInfo.video.length === 2) { 43948 this.inspectCache_ = timeInfo.video[1].dts; 43949 segmentStartTime = timeInfo.video[0].dtsTime; 43950 segmentEndTime = timeInfo.video[1].dtsTime; 43951 } else if (timeInfo.audio && timeInfo.audio.length === 2) { 43952 this.inspectCache_ = timeInfo.audio[1].dts; 43953 segmentStartTime = timeInfo.audio[0].dtsTime; 43954 segmentEndTime = timeInfo.audio[1].dtsTime; 43955 } 43956 43957 return { 43958 start: segmentStartTime, 43959 end: segmentEndTime, 43960 containsVideo: timeInfo.video && timeInfo.video.length === 2, 43961 containsAudio: timeInfo.audio && timeInfo.audio.length === 2 43962 }; 43963 } 43964 }, { 43965 key: 'timestampOffsetForTimeline', 43966 value: function timestampOffsetForTimeline(timeline) { 43967 if (typeof this.timelines[timeline] === 'undefined') { 43968 return null; 43969 } 43970 return this.timelines[timeline].time; 43971 } 43972 }, { 43973 key: 'mappingForTimeline', 43974 value: function mappingForTimeline(timeline) { 43975 if (typeof this.timelines[timeline] === 'undefined') { 43976 return null; 43977 } 43978 return this.timelines[timeline].mapping; 43979 } 43980 43981 /** 43982 * Use the "media time" for a segment to generate a mapping to "display time" and 43983 * save that display time to the segment. 43984 * 43985 * @private 43986 * @param {SegmentInfo} segmentInfo 43987 * The current active request information 43988 * @param {object} timingInfo 43989 * The start and end time of the current segment in "media time" 43990 * @returns {Boolean} 43991 * Returns false if segment time mapping could not be calculated 43992 */ 43993 43994 }, { 43995 key: 'calculateSegmentTimeMapping_', 43996 value: function calculateSegmentTimeMapping_(segmentInfo, timingInfo) { 43997 var segment = segmentInfo.segment; 43998 var mappingObj = this.timelines[segmentInfo.timeline]; 43999 44000 if (segmentInfo.timestampOffset !== null) { 44001 mappingObj = { 44002 time: segmentInfo.startOfSegment, 44003 mapping: segmentInfo.startOfSegment - timingInfo.start 44004 }; 44005 this.timelines[segmentInfo.timeline] = mappingObj; 44006 this.trigger('timestampoffset'); 44007 44008 this.logger_('time mapping for timeline ' + segmentInfo.timeline + ': ' + ('[time: ' + mappingObj.time + '] [mapping: ' + mappingObj.mapping + ']')); 44009 44010 segment.start = segmentInfo.startOfSegment; 44011 segment.end = timingInfo.end + mappingObj.mapping; 44012 } else if (mappingObj) { 44013 segment.start = timingInfo.start + mappingObj.mapping; 44014 segment.end = timingInfo.end + mappingObj.mapping; 44015 } else { 44016 return false; 44017 } 44018 44019 return true; 44020 } 44021 44022 /** 44023 * Each time we have discontinuity in the playlist, attempt to calculate the location 44024 * in display of the start of the discontinuity and save that. We also save an accuracy 44025 * value so that we save values with the most accuracy (closest to 0.) 44026 * 44027 * @private 44028 * @param {SegmentInfo} segmentInfo - The current active request information 44029 */ 44030 44031 }, { 44032 key: 'saveDiscontinuitySyncInfo_', 44033 value: function saveDiscontinuitySyncInfo_(segmentInfo) { 44034 var playlist = segmentInfo.playlist; 44035 var segment = segmentInfo.segment; 44036 44037 // If the current segment is a discontinuity then we know exactly where 44038 // the start of the range and it's accuracy is 0 (greater accuracy values 44039 // mean more approximation) 44040 if (segment.discontinuity) { 44041 this.discontinuities[segment.timeline] = { 44042 time: segment.start, 44043 accuracy: 0 44044 }; 44045 } else if (playlist.discontinuityStarts && playlist.discontinuityStarts.length) { 44046 // Search for future discontinuities that we can provide better timing 44047 // information for and save that information for sync purposes 44048 for (var i = 0; i < playlist.discontinuityStarts.length; i++) { 44049 var segmentIndex = playlist.discontinuityStarts[i]; 44050 var discontinuity = playlist.discontinuitySequence + i + 1; 44051 var mediaIndexDiff = segmentIndex - segmentInfo.mediaIndex; 44052 var accuracy = Math.abs(mediaIndexDiff); 44053 44054 if (!this.discontinuities[discontinuity] || this.discontinuities[discontinuity].accuracy > accuracy) { 44055 var time = void 0; 44056 44057 if (mediaIndexDiff < 0) { 44058 time = segment.start - sumDurations(playlist, segmentInfo.mediaIndex, segmentIndex); 44059 } else { 44060 time = segment.end + sumDurations(playlist, segmentInfo.mediaIndex + 1, segmentIndex); 44061 } 44062 44063 this.discontinuities[discontinuity] = { 44064 time: time, 44065 accuracy: accuracy 44066 }; 44067 } 44068 } 44069 } 44070 } 44071 }]); 44072 return SyncController; 44073 }(videojs$1.EventTarget); 44074 44075 var Decrypter$1 = new shimWorker("./decrypter-worker.worker.js", function (window, document$$1) { 44076 var self = this; 44077 var decrypterWorker = function () { 44078 44079 var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; 44080 44081 var win; 44082 44083 if (typeof window !== "undefined") { 44084 win = window; 44085 } else if (typeof commonjsGlobal !== "undefined") { 44086 win = commonjsGlobal; 44087 } else if (typeof self !== "undefined") { 44088 win = self; 44089 } else { 44090 win = {}; 44091 } 44092 44093 var window_1$$1 = win; 44094 44095 /* 44096 * pkcs7.pad 44097 * https://github.com/brightcove/pkcs7 44098 * 44099 * Copyright (c) 2014 Brightcove 44100 * Licensed under the apache2 license. 44101 */ 44102 44103 /** 44104 * Returns the subarray of a Uint8Array without PKCS#7 padding. 44105 * @param padded {Uint8Array} unencrypted bytes that have been padded 44106 * @return {Uint8Array} the unpadded bytes 44107 * @see http://tools.ietf.org/html/rfc5652 44108 */ 44109 function unpad(padded) { 44110 return padded.subarray(0, padded.byteLength - padded[padded.byteLength - 1]); 44111 } 44112 44113 var classCallCheck$$1 = function classCallCheck$$1(instance, Constructor) { 44114 if (!(instance instanceof Constructor)) { 44115 throw new TypeError("Cannot call a class as a function"); 44116 } 44117 }; 44118 44119 var createClass$$1 = function () { 44120 function defineProperties(target, props) { 44121 for (var i = 0; i < props.length; i++) { 44122 var descriptor = props[i]; 44123 descriptor.enumerable = descriptor.enumerable || false; 44124 descriptor.configurable = true; 44125 if ("value" in descriptor) descriptor.writable = true; 44126 Object.defineProperty(target, descriptor.key, descriptor); 44127 } 44128 } 44129 44130 return function (Constructor, protoProps, staticProps) { 44131 if (protoProps) defineProperties(Constructor.prototype, protoProps); 44132 if (staticProps) defineProperties(Constructor, staticProps); 44133 return Constructor; 44134 }; 44135 }(); 44136 44137 var inherits$$1 = function inherits$$1(subClass, superClass) { 44138 if (typeof superClass !== "function" && superClass !== null) { 44139 throw new TypeError("Super expression must either be null or a function, not " + (typeof superClass === 'undefined' ? 'undefined' : _typeof(superClass))); 44140 } 44141 44142 subClass.prototype = Object.create(superClass && superClass.prototype, { 44143 constructor: { 44144 value: subClass, 44145 enumerable: false, 44146 writable: true, 44147 configurable: true 44148 } 44149 }); 44150 if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; 44151 }; 44152 44153 var possibleConstructorReturn$$1 = function possibleConstructorReturn$$1(self, call) { 44154 if (!self) { 44155 throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); 44156 } 44157 44158 return call && ((typeof call === 'undefined' ? 'undefined' : _typeof(call)) === "object" || typeof call === "function") ? call : self; 44159 }; 44160 44161 /** 44162 * @file aes.js 44163 * 44164 * This file contains an adaptation of the AES decryption algorithm 44165 * from the Standford Javascript Cryptography Library. That work is 44166 * covered by the following copyright and permissions notice: 44167 * 44168 * Copyright 2009-2010 Emily Stark, Mike Hamburg, Dan Boneh. 44169 * All rights reserved. 44170 * 44171 * Redistribution and use in source and binary forms, with or without 44172 * modification, are permitted provided that the following conditions are 44173 * met: 44174 * 44175 * 1. Redistributions of source code must retain the above copyright 44176 * notice, this list of conditions and the following disclaimer. 44177 * 44178 * 2. Redistributions in binary form must reproduce the above 44179 * copyright notice, this list of conditions and the following 44180 * disclaimer in the documentation and/or other materials provided 44181 * with the distribution. 44182 * 44183 * THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR 44184 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 44185 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 44186 * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> OR CONTRIBUTORS BE 44187 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 44188 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 44189 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 44190 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 44191 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 44192 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN 44193 * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 44194 * 44195 * The views and conclusions contained in the software and documentation 44196 * are those of the authors and should not be interpreted as representing 44197 * official policies, either expressed or implied, of the authors. 44198 */ 44199 44200 /** 44201 * Expand the S-box tables. 44202 * 44203 * @private 44204 */ 44205 var precompute = function precompute() { 44206 var tables = [[[], [], [], [], []], [[], [], [], [], []]]; 44207 var encTable = tables[0]; 44208 var decTable = tables[1]; 44209 var sbox = encTable[4]; 44210 var sboxInv = decTable[4]; 44211 var i = void 0; 44212 var x = void 0; 44213 var xInv = void 0; 44214 var d = []; 44215 var th = []; 44216 var x2 = void 0; 44217 var x4 = void 0; 44218 var x8 = void 0; 44219 var s = void 0; 44220 var tEnc = void 0; 44221 var tDec = void 0; 44222 44223 // Compute double and third tables 44224 for (i = 0; i < 256; i++) { 44225 th[(d[i] = i << 1 ^ (i >> 7) * 283) ^ i] = i; 44226 } 44227 44228 for (x = xInv = 0; !sbox[x]; x ^= x2 || 1, xInv = th[xInv] || 1) { 44229 // Compute sbox 44230 s = xInv ^ xInv << 1 ^ xInv << 2 ^ xInv << 3 ^ xInv << 4; 44231 s = s >> 8 ^ s & 255 ^ 99; 44232 sbox[x] = s; 44233 sboxInv[s] = x; 44234 44235 // Compute MixColumns 44236 x8 = d[x4 = d[x2 = d[x]]]; 44237 tDec = x8 * 0x1010101 ^ x4 * 0x10001 ^ x2 * 0x101 ^ x * 0x1010100; 44238 tEnc = d[s] * 0x101 ^ s * 0x1010100; 44239 44240 for (i = 0; i < 4; i++) { 44241 encTable[i][x] = tEnc = tEnc << 24 ^ tEnc >>> 8; 44242 decTable[i][s] = tDec = tDec << 24 ^ tDec >>> 8; 44243 } 44244 } 44245 44246 // Compactify. Considerable speedup on Firefox. 44247 for (i = 0; i < 5; i++) { 44248 encTable[i] = encTable[i].slice(0); 44249 decTable[i] = decTable[i].slice(0); 44250 } 44251 return tables; 44252 }; 44253 var aesTables = null; 44254 44255 /** 44256 * Schedule out an AES key for both encryption and decryption. This 44257 * is a low-level class. Use a cipher mode to do bulk encryption. 44258 * 44259 * @class AES 44260 * @param key {Array} The key as an array of 4, 6 or 8 words. 44261 */ 44262 44263 var AES = function () { 44264 function AES(key) { 44265 classCallCheck$$1(this, AES); 44266 44267 /** 44268 * The expanded S-box and inverse S-box tables. These will be computed 44269 * on the client so that we don't have to send them down the wire. 44270 * 44271 * There are two tables, _tables[0] is for encryption and 44272 * _tables[1] is for decryption. 44273 * 44274 * The first 4 sub-tables are the expanded S-box with MixColumns. The 44275 * last (_tables[01][4]) is the S-box itself. 44276 * 44277 * @private 44278 */ 44279 // if we have yet to precompute the S-box tables 44280 // do so now 44281 if (!aesTables) { 44282 aesTables = precompute(); 44283 } 44284 // then make a copy of that object for use 44285 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()]]; 44286 var i = void 0; 44287 var j = void 0; 44288 var tmp = void 0; 44289 var encKey = void 0; 44290 var decKey = void 0; 44291 var sbox = this._tables[0][4]; 44292 var decTable = this._tables[1]; 44293 var keyLen = key.length; 44294 var rcon = 1; 44295 44296 if (keyLen !== 4 && keyLen !== 6 && keyLen !== 8) { 44297 throw new Error('Invalid aes key size'); 44298 } 44299 44300 encKey = key.slice(0); 44301 decKey = []; 44302 this._key = [encKey, decKey]; 44303 44304 // schedule encryption keys 44305 for (i = keyLen; i < 4 * keyLen + 28; i++) { 44306 tmp = encKey[i - 1]; 44307 44308 // apply sbox 44309 if (i % keyLen === 0 || keyLen === 8 && i % keyLen === 4) { 44310 tmp = sbox[tmp >>> 24] << 24 ^ sbox[tmp >> 16 & 255] << 16 ^ sbox[tmp >> 8 & 255] << 8 ^ sbox[tmp & 255]; 44311 44312 // shift rows and add rcon 44313 if (i % keyLen === 0) { 44314 tmp = tmp << 8 ^ tmp >>> 24 ^ rcon << 24; 44315 rcon = rcon << 1 ^ (rcon >> 7) * 283; 44316 } 44317 } 44318 44319 encKey[i] = encKey[i - keyLen] ^ tmp; 44320 } 44321 44322 // schedule decryption keys 44323 for (j = 0; i; j++, i--) { 44324 tmp = encKey[j & 3 ? i : i - 4]; 44325 if (i <= 4 || j < 4) { 44326 decKey[j] = tmp; 44327 } else { 44328 decKey[j] = decTable[0][sbox[tmp >>> 24]] ^ decTable[1][sbox[tmp >> 16 & 255]] ^ decTable[2][sbox[tmp >> 8 & 255]] ^ decTable[3][sbox[tmp & 255]]; 44329 } 44330 } 44331 } 44332 44333 /** 44334 * Decrypt 16 bytes, specified as four 32-bit words. 44335 * 44336 * @param {Number} encrypted0 the first word to decrypt 44337 * @param {Number} encrypted1 the second word to decrypt 44338 * @param {Number} encrypted2 the third word to decrypt 44339 * @param {Number} encrypted3 the fourth word to decrypt 44340 * @param {Int32Array} out the array to write the decrypted words 44341 * into 44342 * @param {Number} offset the offset into the output array to start 44343 * writing results 44344 * @return {Array} The plaintext. 44345 */ 44346 44347 AES.prototype.decrypt = function decrypt$$1(encrypted0, encrypted1, encrypted2, encrypted3, out, offset) { 44348 var key = this._key[1]; 44349 // state variables a,b,c,d are loaded with pre-whitened data 44350 var a = encrypted0 ^ key[0]; 44351 var b = encrypted3 ^ key[1]; 44352 var c = encrypted2 ^ key[2]; 44353 var d = encrypted1 ^ key[3]; 44354 var a2 = void 0; 44355 var b2 = void 0; 44356 var c2 = void 0; 44357 44358 // key.length === 2 ? 44359 var nInnerRounds = key.length / 4 - 2; 44360 var i = void 0; 44361 var kIndex = 4; 44362 var table = this._tables[1]; 44363 44364 // load up the tables 44365 var table0 = table[0]; 44366 var table1 = table[1]; 44367 var table2 = table[2]; 44368 var table3 = table[3]; 44369 var sbox = table[4]; 44370 44371 // Inner rounds. Cribbed from OpenSSL. 44372 for (i = 0; i < nInnerRounds; i++) { 44373 a2 = table0[a >>> 24] ^ table1[b >> 16 & 255] ^ table2[c >> 8 & 255] ^ table3[d & 255] ^ key[kIndex]; 44374 b2 = table0[b >>> 24] ^ table1[c >> 16 & 255] ^ table2[d >> 8 & 255] ^ table3[a & 255] ^ key[kIndex + 1]; 44375 c2 = table0[c >>> 24] ^ table1[d >> 16 & 255] ^ table2[a >> 8 & 255] ^ table3[b & 255] ^ key[kIndex + 2]; 44376 d = table0[d >>> 24] ^ table1[a >> 16 & 255] ^ table2[b >> 8 & 255] ^ table3[c & 255] ^ key[kIndex + 3]; 44377 kIndex += 4; 44378 a = a2;b = b2;c = c2; 44379 } 44380 44381 // Last round. 44382 for (i = 0; i < 4; i++) { 44383 out[(3 & -i) + offset] = sbox[a >>> 24] << 24 ^ sbox[b >> 16 & 255] << 16 ^ sbox[c >> 8 & 255] << 8 ^ sbox[d & 255] ^ key[kIndex++]; 44384 a2 = a;a = b;b = c;c = d;d = a2; 44385 } 44386 }; 44387 44388 return AES; 44389 }(); 44390 44391 /** 44392 * @file stream.js 44393 */ 44394 /** 44395 * A lightweight readable stream implemention that handles event dispatching. 44396 * 44397 * @class Stream 44398 */ 44399 var Stream = function () { 44400 function Stream() { 44401 classCallCheck$$1(this, Stream); 44402 44403 this.listeners = {}; 44404 } 44405 44406 /** 44407 * Add a listener for a specified event type. 44408 * 44409 * @param {String} type the event name 44410 * @param {Function} listener the callback to be invoked when an event of 44411 * the specified type occurs 44412 */ 44413 44414 Stream.prototype.on = function on(type, listener) { 44415 if (!this.listeners[type]) { 44416 this.listeners[type] = []; 44417 } 44418 this.listeners[type].push(listener); 44419 }; 44420 44421 /** 44422 * Remove a listener for a specified event type. 44423 * 44424 * @param {String} type the event name 44425 * @param {Function} listener a function previously registered for this 44426 * type of event through `on` 44427 * @return {Boolean} if we could turn it off or not 44428 */ 44429 44430 Stream.prototype.off = function off(type, listener) { 44431 if (!this.listeners[type]) { 44432 return false; 44433 } 44434 44435 var index = this.listeners[type].indexOf(listener); 44436 44437 this.listeners[type].splice(index, 1); 44438 return index > -1; 44439 }; 44440 44441 /** 44442 * Trigger an event of the specified type on this stream. Any additional 44443 * arguments to this function are passed as parameters to event listeners. 44444 * 44445 * @param {String} type the event name 44446 */ 44447 44448 Stream.prototype.trigger = function trigger(type) { 44449 var callbacks = this.listeners[type]; 44450 44451 if (!callbacks) { 44452 return; 44453 } 44454 44455 // Slicing the arguments on every invocation of this method 44456 // can add a significant amount of overhead. Avoid the 44457 // intermediate object creation for the common case of a 44458 // single callback argument 44459 if (arguments.length === 2) { 44460 var length = callbacks.length; 44461 44462 for (var i = 0; i < length; ++i) { 44463 callbacks[i].call(this, arguments[1]); 44464 } 44465 } else { 44466 var args = Array.prototype.slice.call(arguments, 1); 44467 var _length = callbacks.length; 44468 44469 for (var _i = 0; _i < _length; ++_i) { 44470 callbacks[_i].apply(this, args); 44471 } 44472 } 44473 }; 44474 44475 /** 44476 * Destroys the stream and cleans up. 44477 */ 44478 44479 Stream.prototype.dispose = function dispose() { 44480 this.listeners = {}; 44481 }; 44482 /** 44483 * Forwards all `data` events on this stream to the destination stream. The 44484 * destination stream should provide a method `push` to receive the data 44485 * events as they arrive. 44486 * 44487 * @param {Stream} destination the stream that will receive all `data` events 44488 * @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options 44489 */ 44490 44491 Stream.prototype.pipe = function pipe(destination) { 44492 this.on('data', function (data) { 44493 destination.push(data); 44494 }); 44495 }; 44496 44497 return Stream; 44498 }(); 44499 44500 /** 44501 * @file async-stream.js 44502 */ 44503 /** 44504 * A wrapper around the Stream class to use setTiemout 44505 * and run stream "jobs" Asynchronously 44506 * 44507 * @class AsyncStream 44508 * @extends Stream 44509 */ 44510 44511 var AsyncStream$$1 = function (_Stream) { 44512 inherits$$1(AsyncStream$$1, _Stream); 44513 44514 function AsyncStream$$1() { 44515 classCallCheck$$1(this, AsyncStream$$1); 44516 44517 var _this = possibleConstructorReturn$$1(this, _Stream.call(this, Stream)); 44518 44519 _this.jobs = []; 44520 _this.delay = 1; 44521 _this.timeout_ = null; 44522 return _this; 44523 } 44524 44525 /** 44526 * process an async job 44527 * 44528 * @private 44529 */ 44530 44531 AsyncStream$$1.prototype.processJob_ = function processJob_() { 44532 this.jobs.shift()(); 44533 if (this.jobs.length) { 44534 this.timeout_ = setTimeout(this.processJob_.bind(this), this.delay); 44535 } else { 44536 this.timeout_ = null; 44537 } 44538 }; 44539 44540 /** 44541 * push a job into the stream 44542 * 44543 * @param {Function} job the job to push into the stream 44544 */ 44545 44546 AsyncStream$$1.prototype.push = function push(job) { 44547 this.jobs.push(job); 44548 if (!this.timeout_) { 44549 this.timeout_ = setTimeout(this.processJob_.bind(this), this.delay); 44550 } 44551 }; 44552 44553 return AsyncStream$$1; 44554 }(Stream); 44555 44556 /** 44557 * @file decrypter.js 44558 * 44559 * An asynchronous implementation of AES-128 CBC decryption with 44560 * PKCS#7 padding. 44561 */ 44562 44563 /** 44564 * Convert network-order (big-endian) bytes into their little-endian 44565 * representation. 44566 */ 44567 var ntoh = function ntoh(word) { 44568 return word << 24 | (word & 0xff00) << 8 | (word & 0xff0000) >> 8 | word >>> 24; 44569 }; 44570 44571 /** 44572 * Decrypt bytes using AES-128 with CBC and PKCS#7 padding. 44573 * 44574 * @param {Uint8Array} encrypted the encrypted bytes 44575 * @param {Uint32Array} key the bytes of the decryption key 44576 * @param {Uint32Array} initVector the initialization vector (IV) to 44577 * use for the first round of CBC. 44578 * @return {Uint8Array} the decrypted bytes 44579 * 44580 * @see http://en.wikipedia.org/wiki/Advanced_Encryption_Standard 44581 * @see http://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_Block_Chaining_.28CBC.29 44582 * @see https://tools.ietf.org/html/rfc2315 44583 */ 44584 var decrypt$$1 = function decrypt$$1(encrypted, key, initVector) { 44585 // word-level access to the encrypted bytes 44586 var encrypted32 = new Int32Array(encrypted.buffer, encrypted.byteOffset, encrypted.byteLength >> 2); 44587 44588 var decipher = new AES(Array.prototype.slice.call(key)); 44589 44590 // byte and word-level access for the decrypted output 44591 var decrypted = new Uint8Array(encrypted.byteLength); 44592 var decrypted32 = new Int32Array(decrypted.buffer); 44593 44594 // temporary variables for working with the IV, encrypted, and 44595 // decrypted data 44596 var init0 = void 0; 44597 var init1 = void 0; 44598 var init2 = void 0; 44599 var init3 = void 0; 44600 var encrypted0 = void 0; 44601 var encrypted1 = void 0; 44602 var encrypted2 = void 0; 44603 var encrypted3 = void 0; 44604 44605 // iteration variable 44606 var wordIx = void 0; 44607 44608 // pull out the words of the IV to ensure we don't modify the 44609 // passed-in reference and easier access 44610 init0 = initVector[0]; 44611 init1 = initVector[1]; 44612 init2 = initVector[2]; 44613 init3 = initVector[3]; 44614 44615 // decrypt four word sequences, applying cipher-block chaining (CBC) 44616 // to each decrypted block 44617 for (wordIx = 0; wordIx < encrypted32.length; wordIx += 4) { 44618 // convert big-endian (network order) words into little-endian 44619 // (javascript order) 44620 encrypted0 = ntoh(encrypted32[wordIx]); 44621 encrypted1 = ntoh(encrypted32[wordIx + 1]); 44622 encrypted2 = ntoh(encrypted32[wordIx + 2]); 44623 encrypted3 = ntoh(encrypted32[wordIx + 3]); 44624 44625 // decrypt the block 44626 decipher.decrypt(encrypted0, encrypted1, encrypted2, encrypted3, decrypted32, wordIx); 44627 44628 // XOR with the IV, and restore network byte-order to obtain the 44629 // plaintext 44630 decrypted32[wordIx] = ntoh(decrypted32[wordIx] ^ init0); 44631 decrypted32[wordIx + 1] = ntoh(decrypted32[wordIx + 1] ^ init1); 44632 decrypted32[wordIx + 2] = ntoh(decrypted32[wordIx + 2] ^ init2); 44633 decrypted32[wordIx + 3] = ntoh(decrypted32[wordIx + 3] ^ init3); 44634 44635 // setup the IV for the next round 44636 init0 = encrypted0; 44637 init1 = encrypted1; 44638 init2 = encrypted2; 44639 init3 = encrypted3; 44640 } 44641 44642 return decrypted; 44643 }; 44644 44645 /** 44646 * The `Decrypter` class that manages decryption of AES 44647 * data through `AsyncStream` objects and the `decrypt` 44648 * function 44649 * 44650 * @param {Uint8Array} encrypted the encrypted bytes 44651 * @param {Uint32Array} key the bytes of the decryption key 44652 * @param {Uint32Array} initVector the initialization vector (IV) to 44653 * @param {Function} done the function to run when done 44654 * @class Decrypter 44655 */ 44656 44657 var Decrypter$$1 = function () { 44658 function Decrypter$$1(encrypted, key, initVector, done) { 44659 classCallCheck$$1(this, Decrypter$$1); 44660 44661 var step = Decrypter$$1.STEP; 44662 var encrypted32 = new Int32Array(encrypted.buffer); 44663 var decrypted = new Uint8Array(encrypted.byteLength); 44664 var i = 0; 44665 44666 this.asyncStream_ = new AsyncStream$$1(); 44667 44668 // split up the encryption job and do the individual chunks asynchronously 44669 this.asyncStream_.push(this.decryptChunk_(encrypted32.subarray(i, i + step), key, initVector, decrypted)); 44670 for (i = step; i < encrypted32.length; i += step) { 44671 initVector = new Uint32Array([ntoh(encrypted32[i - 4]), ntoh(encrypted32[i - 3]), ntoh(encrypted32[i - 2]), ntoh(encrypted32[i - 1])]); 44672 this.asyncStream_.push(this.decryptChunk_(encrypted32.subarray(i, i + step), key, initVector, decrypted)); 44673 } 44674 // invoke the done() callback when everything is finished 44675 this.asyncStream_.push(function () { 44676 // remove pkcs#7 padding from the decrypted bytes 44677 done(null, unpad(decrypted)); 44678 }); 44679 } 44680 44681 /** 44682 * a getter for step the maximum number of bytes to process at one time 44683 * 44684 * @return {Number} the value of step 32000 44685 */ 44686 44687 /** 44688 * @private 44689 */ 44690 Decrypter$$1.prototype.decryptChunk_ = function decryptChunk_(encrypted, key, initVector, decrypted) { 44691 return function () { 44692 var bytes = decrypt$$1(encrypted, key, initVector); 44693 44694 decrypted.set(bytes, encrypted.byteOffset); 44695 }; 44696 }; 44697 44698 createClass$$1(Decrypter$$1, null, [{ 44699 key: 'STEP', 44700 get: function get$$1() { 44701 // 4 * 8000; 44702 return 32000; 44703 } 44704 }]); 44705 return Decrypter$$1; 44706 }(); 44707 44708 /** 44709 * @file bin-utils.js 44710 */ 44711 44712 /** 44713 * Creates an object for sending to a web worker modifying properties that are TypedArrays 44714 * into a new object with seperated properties for the buffer, byteOffset, and byteLength. 44715 * 44716 * @param {Object} message 44717 * Object of properties and values to send to the web worker 44718 * @return {Object} 44719 * Modified message with TypedArray values expanded 44720 * @function createTransferableMessage 44721 */ 44722 var createTransferableMessage = function createTransferableMessage(message) { 44723 var transferable = {}; 44724 44725 Object.keys(message).forEach(function (key) { 44726 var value = message[key]; 44727 44728 if (ArrayBuffer.isView(value)) { 44729 transferable[key] = { 44730 bytes: value.buffer, 44731 byteOffset: value.byteOffset, 44732 byteLength: value.byteLength 44733 }; 44734 } else { 44735 transferable[key] = value; 44736 } 44737 }); 44738 44739 return transferable; 44740 }; 44741 44742 /** 44743 * Our web worker interface so that things can talk to aes-decrypter 44744 * that will be running in a web worker. the scope is passed to this by 44745 * webworkify. 44746 * 44747 * @param {Object} self 44748 * the scope for the web worker 44749 */ 44750 var DecrypterWorker = function DecrypterWorker(self) { 44751 self.onmessage = function (event) { 44752 var data = event.data; 44753 var encrypted = new Uint8Array(data.encrypted.bytes, data.encrypted.byteOffset, data.encrypted.byteLength); 44754 var key = new Uint32Array(data.key.bytes, data.key.byteOffset, data.key.byteLength / 4); 44755 var iv = new Uint32Array(data.iv.bytes, data.iv.byteOffset, data.iv.byteLength / 4); 44756 44757 /* eslint-disable no-new, handle-callback-err */ 44758 new Decrypter$$1(encrypted, key, iv, function (err, bytes) { 44759 window_1$$1.postMessage(createTransferableMessage({ 44760 source: data.source, 44761 decrypted: bytes 44762 }), [bytes.buffer]); 44763 }); 44764 /* eslint-enable */ 44765 }; 44766 }; 44767 44768 var decrypterWorker = new DecrypterWorker(self); 44769 44770 return decrypterWorker; 44771 }(); 44772 }); 44773 44774 /** 44775 * Convert the properties of an HLS track into an audioTrackKind. 44776 * 44777 * @private 44778 */ 44779 var audioTrackKind_ = function audioTrackKind_(properties) { 44780 var kind = properties.default ? 'main' : 'alternative'; 44781 44782 if (properties.characteristics && properties.characteristics.indexOf('public.accessibility.describes-video') >= 0) { 44783 kind = 'main-desc'; 44784 } 44785 44786 return kind; 44787 }; 44788 44789 /** 44790 * Pause provided segment loader and playlist loader if active 44791 * 44792 * @param {SegmentLoader} segmentLoader 44793 * SegmentLoader to pause 44794 * @param {Object} mediaType 44795 * Active media type 44796 * @function stopLoaders 44797 */ 44798 var stopLoaders = function stopLoaders(segmentLoader, mediaType) { 44799 segmentLoader.abort(); 44800 segmentLoader.pause(); 44801 44802 if (mediaType && mediaType.activePlaylistLoader) { 44803 mediaType.activePlaylistLoader.pause(); 44804 mediaType.activePlaylistLoader = null; 44805 } 44806 }; 44807 44808 /** 44809 * Start loading provided segment loader and playlist loader 44810 * 44811 * @param {PlaylistLoader} playlistLoader 44812 * PlaylistLoader to start loading 44813 * @param {Object} mediaType 44814 * Active media type 44815 * @function startLoaders 44816 */ 44817 var startLoaders = function startLoaders(playlistLoader, mediaType) { 44818 // Segment loader will be started after `loadedmetadata` or `loadedplaylist` from the 44819 // playlist loader 44820 mediaType.activePlaylistLoader = playlistLoader; 44821 playlistLoader.load(); 44822 }; 44823 44824 /** 44825 * Returns a function to be called when the media group changes. It performs a 44826 * non-destructive (preserve the buffer) resync of the SegmentLoader. This is because a 44827 * change of group is merely a rendition switch of the same content at another encoding, 44828 * rather than a change of content, such as switching audio from English to Spanish. 44829 * 44830 * @param {String} type 44831 * MediaGroup type 44832 * @param {Object} settings 44833 * Object containing required information for media groups 44834 * @return {Function} 44835 * Handler for a non-destructive resync of SegmentLoader when the active media 44836 * group changes. 44837 * @function onGroupChanged 44838 */ 44839 var onGroupChanged = function onGroupChanged(type, settings) { 44840 return function () { 44841 var _settings$segmentLoad = settings.segmentLoaders, 44842 segmentLoader = _settings$segmentLoad[type], 44843 mainSegmentLoader = _settings$segmentLoad.main, 44844 mediaType = settings.mediaTypes[type]; 44845 44846 var activeTrack = mediaType.activeTrack(); 44847 var activeGroup = mediaType.activeGroup(activeTrack); 44848 var previousActiveLoader = mediaType.activePlaylistLoader; 44849 44850 stopLoaders(segmentLoader, mediaType); 44851 44852 if (!activeGroup) { 44853 // there is no group active 44854 return; 44855 } 44856 44857 if (!activeGroup.playlistLoader) { 44858 if (previousActiveLoader) { 44859 // The previous group had a playlist loader but the new active group does not 44860 // this means we are switching from demuxed to muxed audio. In this case we want to 44861 // do a destructive reset of the main segment loader and not restart the audio 44862 // loaders. 44863 mainSegmentLoader.resetEverything(); 44864 } 44865 return; 44866 } 44867 44868 // Non-destructive resync 44869 segmentLoader.resyncLoader(); 44870 44871 startLoaders(activeGroup.playlistLoader, mediaType); 44872 }; 44873 }; 44874 44875 /** 44876 * Returns a function to be called when the media track changes. It performs a 44877 * destructive reset of the SegmentLoader to ensure we start loading as close to 44878 * currentTime as possible. 44879 * 44880 * @param {String} type 44881 * MediaGroup type 44882 * @param {Object} settings 44883 * Object containing required information for media groups 44884 * @return {Function} 44885 * Handler for a destructive reset of SegmentLoader when the active media 44886 * track changes. 44887 * @function onTrackChanged 44888 */ 44889 var onTrackChanged = function onTrackChanged(type, settings) { 44890 return function () { 44891 var _settings$segmentLoad2 = settings.segmentLoaders, 44892 segmentLoader = _settings$segmentLoad2[type], 44893 mainSegmentLoader = _settings$segmentLoad2.main, 44894 mediaType = settings.mediaTypes[type]; 44895 44896 var activeTrack = mediaType.activeTrack(); 44897 var activeGroup = mediaType.activeGroup(activeTrack); 44898 var previousActiveLoader = mediaType.activePlaylistLoader; 44899 44900 stopLoaders(segmentLoader, mediaType); 44901 44902 if (!activeGroup) { 44903 // there is no group active so we do not want to restart loaders 44904 return; 44905 } 44906 44907 if (!activeGroup.playlistLoader) { 44908 // when switching from demuxed audio/video to muxed audio/video (noted by no playlist 44909 // loader for the audio group), we want to do a destructive reset of the main segment 44910 // loader and not restart the audio loaders 44911 mainSegmentLoader.resetEverything(); 44912 return; 44913 } 44914 44915 if (previousActiveLoader === activeGroup.playlistLoader) { 44916 // Nothing has actually changed. This can happen because track change events can fire 44917 // multiple times for a "single" change. One for enabling the new active track, and 44918 // one for disabling the track that was active 44919 startLoaders(activeGroup.playlistLoader, mediaType); 44920 return; 44921 } 44922 44923 if (segmentLoader.track) { 44924 // For WebVTT, set the new text track in the segmentloader 44925 segmentLoader.track(activeTrack); 44926 } 44927 44928 // destructive reset 44929 segmentLoader.resetEverything(); 44930 44931 startLoaders(activeGroup.playlistLoader, mediaType); 44932 }; 44933 }; 44934 44935 var onError = { 44936 /** 44937 * Returns a function to be called when a SegmentLoader or PlaylistLoader encounters 44938 * an error. 44939 * 44940 * @param {String} type 44941 * MediaGroup type 44942 * @param {Object} settings 44943 * Object containing required information for media groups 44944 * @return {Function} 44945 * Error handler. Logs warning (or error if the playlist is blacklisted) to 44946 * console and switches back to default audio track. 44947 * @function onError.AUDIO 44948 */ 44949 AUDIO: function AUDIO(type, settings) { 44950 return function () { 44951 var segmentLoader = settings.segmentLoaders[type], 44952 mediaType = settings.mediaTypes[type], 44953 blacklistCurrentPlaylist = settings.blacklistCurrentPlaylist; 44954 44955 stopLoaders(segmentLoader, mediaType); 44956 44957 // switch back to default audio track 44958 var activeTrack = mediaType.activeTrack(); 44959 var activeGroup = mediaType.activeGroup(); 44960 var id = (activeGroup.filter(function (group) { 44961 return group.default; 44962 })[0] || activeGroup[0]).id; 44963 var defaultTrack = mediaType.tracks[id]; 44964 44965 if (activeTrack === defaultTrack) { 44966 // Default track encountered an error. All we can do now is blacklist the current 44967 // rendition and hope another will switch audio groups 44968 blacklistCurrentPlaylist({ 44969 message: 'Problem encountered loading the default audio track.' 44970 }); 44971 return; 44972 } 44973 44974 videojs$1.log.warn('Problem encountered loading the alternate audio track.' + 'Switching back to default.'); 44975 44976 for (var trackId in mediaType.tracks) { 44977 mediaType.tracks[trackId].enabled = mediaType.tracks[trackId] === defaultTrack; 44978 } 44979 44980 mediaType.onTrackChanged(); 44981 }; 44982 }, 44983 /** 44984 * Returns a function to be called when a SegmentLoader or PlaylistLoader encounters 44985 * an error. 44986 * 44987 * @param {String} type 44988 * MediaGroup type 44989 * @param {Object} settings 44990 * Object containing required information for media groups 44991 * @return {Function} 44992 * Error handler. Logs warning to console and disables the active subtitle track 44993 * @function onError.SUBTITLES 44994 */ 44995 SUBTITLES: function SUBTITLES(type, settings) { 44996 return function () { 44997 var segmentLoader = settings.segmentLoaders[type], 44998 mediaType = settings.mediaTypes[type]; 44999 45000 videojs$1.log.warn('Problem encountered loading the subtitle track.' + 'Disabling subtitle track.'); 45001 45002 stopLoaders(segmentLoader, mediaType); 45003 45004 var track = mediaType.activeTrack(); 45005 45006 if (track) { 45007 track.mode = 'disabled'; 45008 } 45009 45010 mediaType.onTrackChanged(); 45011 }; 45012 } 45013 }; 45014 45015 var setupListeners = { 45016 /** 45017 * Setup event listeners for audio playlist loader 45018 * 45019 * @param {String} type 45020 * MediaGroup type 45021 * @param {PlaylistLoader|null} playlistLoader 45022 * PlaylistLoader to register listeners on 45023 * @param {Object} settings 45024 * Object containing required information for media groups 45025 * @function setupListeners.AUDIO 45026 */ 45027 AUDIO: function AUDIO(type, playlistLoader, settings) { 45028 if (!playlistLoader) { 45029 // no playlist loader means audio will be muxed with the video 45030 return; 45031 } 45032 45033 var tech = settings.tech, 45034 requestOptions = settings.requestOptions, 45035 segmentLoader = settings.segmentLoaders[type]; 45036 45037 playlistLoader.on('loadedmetadata', function () { 45038 var media = playlistLoader.media(); 45039 45040 segmentLoader.playlist(media, requestOptions); 45041 45042 // if the video is already playing, or if this isn't a live video and preload 45043 // permits, start downloading segments 45044 if (!tech.paused() || media.endList && tech.preload() !== 'none') { 45045 segmentLoader.load(); 45046 } 45047 }); 45048 45049 playlistLoader.on('loadedplaylist', function () { 45050 segmentLoader.playlist(playlistLoader.media(), requestOptions); 45051 45052 // If the player isn't paused, ensure that the segment loader is running 45053 if (!tech.paused()) { 45054 segmentLoader.load(); 45055 } 45056 }); 45057 45058 playlistLoader.on('error', onError[type](type, settings)); 45059 }, 45060 /** 45061 * Setup event listeners for subtitle playlist loader 45062 * 45063 * @param {String} type 45064 * MediaGroup type 45065 * @param {PlaylistLoader|null} playlistLoader 45066 * PlaylistLoader to register listeners on 45067 * @param {Object} settings 45068 * Object containing required information for media groups 45069 * @function setupListeners.SUBTITLES 45070 */ 45071 SUBTITLES: function SUBTITLES(type, playlistLoader, settings) { 45072 var tech = settings.tech, 45073 requestOptions = settings.requestOptions, 45074 segmentLoader = settings.segmentLoaders[type], 45075 mediaType = settings.mediaTypes[type]; 45076 45077 playlistLoader.on('loadedmetadata', function () { 45078 var media = playlistLoader.media(); 45079 45080 segmentLoader.playlist(media, requestOptions); 45081 segmentLoader.track(mediaType.activeTrack()); 45082 45083 // if the video is already playing, or if this isn't a live video and preload 45084 // permits, start downloading segments 45085 if (!tech.paused() || media.endList && tech.preload() !== 'none') { 45086 segmentLoader.load(); 45087 } 45088 }); 45089 45090 playlistLoader.on('loadedplaylist', function () { 45091 segmentLoader.playlist(playlistLoader.media(), requestOptions); 45092 45093 // If the player isn't paused, ensure that the segment loader is running 45094 if (!tech.paused()) { 45095 segmentLoader.load(); 45096 } 45097 }); 45098 45099 playlistLoader.on('error', onError[type](type, settings)); 45100 } 45101 }; 45102 45103 var byGroupId = function byGroupId(type, groupId) { 45104 return function (playlist) { 45105 return playlist.attributes[type] === groupId; 45106 }; 45107 }; 45108 45109 var byResolvedUri = function byResolvedUri(resolvedUri) { 45110 return function (playlist) { 45111 return playlist.resolvedUri === resolvedUri; 45112 }; 45113 }; 45114 45115 var initialize = { 45116 /** 45117 * Setup PlaylistLoaders and AudioTracks for the audio groups 45118 * 45119 * @param {String} type 45120 * MediaGroup type 45121 * @param {Object} settings 45122 * Object containing required information for media groups 45123 * @function initialize.AUDIO 45124 */ 45125 'AUDIO': function AUDIO(type, settings) { 45126 var hls = settings.hls, 45127 sourceType = settings.sourceType, 45128 segmentLoader = settings.segmentLoaders[type], 45129 withCredentials = settings.requestOptions.withCredentials, 45130 _settings$master = settings.master, 45131 mediaGroups = _settings$master.mediaGroups, 45132 playlists = _settings$master.playlists, 45133 _settings$mediaTypes$ = settings.mediaTypes[type], 45134 groups = _settings$mediaTypes$.groups, 45135 tracks = _settings$mediaTypes$.tracks, 45136 masterPlaylistLoader = settings.masterPlaylistLoader; 45137 45138 // force a default if we have none 45139 45140 if (!mediaGroups[type] || Object.keys(mediaGroups[type]).length === 0) { 45141 mediaGroups[type] = { main: { default: { default: true } } }; 45142 } 45143 45144 for (var groupId in mediaGroups[type]) { 45145 if (!groups[groupId]) { 45146 groups[groupId] = []; 45147 } 45148 45149 // List of playlists that have an AUDIO attribute value matching the current 45150 // group ID 45151 var groupPlaylists = playlists.filter(byGroupId(type, groupId)); 45152 45153 for (var variantLabel in mediaGroups[type][groupId]) { 45154 var properties = mediaGroups[type][groupId][variantLabel]; 45155 45156 // List of playlists for the current group ID that have a matching uri with 45157 // this alternate audio variant 45158 var matchingPlaylists = groupPlaylists.filter(byResolvedUri(properties.resolvedUri)); 45159 45160 if (matchingPlaylists.length) { 45161 // If there is a playlist that has the same uri as this audio variant, assume 45162 // that the playlist is audio only. We delete the resolvedUri property here 45163 // to prevent a playlist loader from being created so that we don't have 45164 // both the main and audio segment loaders loading the same audio segments 45165 // from the same playlist. 45166 delete properties.resolvedUri; 45167 } 45168 45169 var playlistLoader = void 0; 45170 45171 if (properties.resolvedUri) { 45172 playlistLoader = new PlaylistLoader(properties.resolvedUri, hls, withCredentials); 45173 } else if (properties.playlists && sourceType === 'dash') { 45174 playlistLoader = new DashPlaylistLoader(properties.playlists[0], hls, withCredentials, masterPlaylistLoader); 45175 } else { 45176 // no resolvedUri means the audio is muxed with the video when using this 45177 // audio track 45178 playlistLoader = null; 45179 } 45180 45181 properties = videojs$1.mergeOptions({ id: variantLabel, playlistLoader: playlistLoader }, properties); 45182 45183 setupListeners[type](type, properties.playlistLoader, settings); 45184 45185 groups[groupId].push(properties); 45186 45187 if (typeof tracks[variantLabel] === 'undefined') { 45188 var track = new videojs$1.AudioTrack({ 45189 id: variantLabel, 45190 kind: audioTrackKind_(properties), 45191 enabled: false, 45192 language: properties.language, 45193 default: properties.default, 45194 label: variantLabel 45195 }); 45196 45197 tracks[variantLabel] = track; 45198 } 45199 } 45200 } 45201 45202 // setup single error event handler for the segment loader 45203 segmentLoader.on('error', onError[type](type, settings)); 45204 }, 45205 /** 45206 * Setup PlaylistLoaders and TextTracks for the subtitle groups 45207 * 45208 * @param {String} type 45209 * MediaGroup type 45210 * @param {Object} settings 45211 * Object containing required information for media groups 45212 * @function initialize.SUBTITLES 45213 */ 45214 'SUBTITLES': function SUBTITLES(type, settings) { 45215 var tech = settings.tech, 45216 hls = settings.hls, 45217 sourceType = settings.sourceType, 45218 segmentLoader = settings.segmentLoaders[type], 45219 withCredentials = settings.requestOptions.withCredentials, 45220 mediaGroups = settings.master.mediaGroups, 45221 _settings$mediaTypes$2 = settings.mediaTypes[type], 45222 groups = _settings$mediaTypes$2.groups, 45223 tracks = _settings$mediaTypes$2.tracks, 45224 masterPlaylistLoader = settings.masterPlaylistLoader; 45225 45226 for (var groupId in mediaGroups[type]) { 45227 if (!groups[groupId]) { 45228 groups[groupId] = []; 45229 } 45230 45231 for (var variantLabel in mediaGroups[type][groupId]) { 45232 if (mediaGroups[type][groupId][variantLabel].forced) { 45233 // Subtitle playlists with the forced attribute are not selectable in Safari. 45234 // According to Apple's HLS Authoring Specification: 45235 // If content has forced subtitles and regular subtitles in a given language, 45236 // the regular subtitles track in that language MUST contain both the forced 45237 // subtitles and the regular subtitles for that language. 45238 // Because of this requirement and that Safari does not add forced subtitles, 45239 // forced subtitles are skipped here to maintain consistent experience across 45240 // all platforms 45241 continue; 45242 } 45243 45244 var properties = mediaGroups[type][groupId][variantLabel]; 45245 45246 var playlistLoader = void 0; 45247 45248 if (sourceType === 'hls') { 45249 playlistLoader = new PlaylistLoader(properties.resolvedUri, hls, withCredentials); 45250 } else if (sourceType === 'dash') { 45251 playlistLoader = new DashPlaylistLoader(properties.playlists[0], hls, withCredentials, masterPlaylistLoader); 45252 } 45253 45254 properties = videojs$1.mergeOptions({ 45255 id: variantLabel, 45256 playlistLoader: playlistLoader 45257 }, properties); 45258 45259 setupListeners[type](type, properties.playlistLoader, settings); 45260 45261 groups[groupId].push(properties); 45262 45263 if (typeof tracks[variantLabel] === 'undefined') { 45264 var track = tech.addRemoteTextTrack({ 45265 id: variantLabel, 45266 kind: 'subtitles', 45267 enabled: false, 45268 language: properties.language, 45269 label: variantLabel 45270 }, false).track; 45271 45272 tracks[variantLabel] = track; 45273 } 45274 } 45275 } 45276 45277 // setup single error event handler for the segment loader 45278 segmentLoader.on('error', onError[type](type, settings)); 45279 }, 45280 /** 45281 * Setup TextTracks for the closed-caption groups 45282 * 45283 * @param {String} type 45284 * MediaGroup type 45285 * @param {Object} settings 45286 * Object containing required information for media groups 45287 * @function initialize['CLOSED-CAPTIONS'] 45288 */ 45289 'CLOSED-CAPTIONS': function CLOSEDCAPTIONS(type, settings) { 45290 var tech = settings.tech, 45291 mediaGroups = settings.master.mediaGroups, 45292 _settings$mediaTypes$3 = settings.mediaTypes[type], 45293 groups = _settings$mediaTypes$3.groups, 45294 tracks = _settings$mediaTypes$3.tracks; 45295 45296 for (var groupId in mediaGroups[type]) { 45297 if (!groups[groupId]) { 45298 groups[groupId] = []; 45299 } 45300 45301 for (var variantLabel in mediaGroups[type][groupId]) { 45302 var properties = mediaGroups[type][groupId][variantLabel]; 45303 45304 // We only support CEA608 captions for now, so ignore anything that 45305 // doesn't use a CCx INSTREAM-ID 45306 if (!properties.instreamId.match(/CC\d/)) { 45307 continue; 45308 } 45309 45310 // No PlaylistLoader is required for Closed-Captions because the captions are 45311 // embedded within the video stream 45312 groups[groupId].push(videojs$1.mergeOptions({ id: variantLabel }, properties)); 45313 45314 if (typeof tracks[variantLabel] === 'undefined') { 45315 var track = tech.addRemoteTextTrack({ 45316 id: properties.instreamId, 45317 kind: 'captions', 45318 enabled: false, 45319 language: properties.language, 45320 label: variantLabel 45321 }, false).track; 45322 45323 tracks[variantLabel] = track; 45324 } 45325 } 45326 } 45327 } 45328 }; 45329 45330 /** 45331 * Returns a function used to get the active group of the provided type 45332 * 45333 * @param {String} type 45334 * MediaGroup type 45335 * @param {Object} settings 45336 * Object containing required information for media groups 45337 * @return {Function} 45338 * Function that returns the active media group for the provided type. Takes an 45339 * optional parameter {TextTrack} track. If no track is provided, a list of all 45340 * variants in the group, otherwise the variant corresponding to the provided 45341 * track is returned. 45342 * @function activeGroup 45343 */ 45344 var activeGroup = function activeGroup(type, settings) { 45345 return function (track) { 45346 var masterPlaylistLoader = settings.masterPlaylistLoader, 45347 groups = settings.mediaTypes[type].groups; 45348 45349 var media = masterPlaylistLoader.media(); 45350 45351 if (!media) { 45352 return null; 45353 } 45354 45355 var variants = null; 45356 45357 if (media.attributes[type]) { 45358 variants = groups[media.attributes[type]]; 45359 } 45360 45361 variants = variants || groups.main; 45362 45363 if (typeof track === 'undefined') { 45364 return variants; 45365 } 45366 45367 if (track === null) { 45368 // An active track was specified so a corresponding group is expected. track === null 45369 // means no track is currently active so there is no corresponding group 45370 return null; 45371 } 45372 45373 return variants.filter(function (props) { 45374 return props.id === track.id; 45375 })[0] || null; 45376 }; 45377 }; 45378 45379 var activeTrack = { 45380 /** 45381 * Returns a function used to get the active track of type provided 45382 * 45383 * @param {String} type 45384 * MediaGroup type 45385 * @param {Object} settings 45386 * Object containing required information for media groups 45387 * @return {Function} 45388 * Function that returns the active media track for the provided type. Returns 45389 * null if no track is active 45390 * @function activeTrack.AUDIO 45391 */ 45392 AUDIO: function AUDIO(type, settings) { 45393 return function () { 45394 var tracks = settings.mediaTypes[type].tracks; 45395 45396 for (var id in tracks) { 45397 if (tracks[id].enabled) { 45398 return tracks[id]; 45399 } 45400 } 45401 45402 return null; 45403 }; 45404 }, 45405 /** 45406 * Returns a function used to get the active track of type provided 45407 * 45408 * @param {String} type 45409 * MediaGroup type 45410 * @param {Object} settings 45411 * Object containing required information for media groups 45412 * @return {Function} 45413 * Function that returns the active media track for the provided type. Returns 45414 * null if no track is active 45415 * @function activeTrack.SUBTITLES 45416 */ 45417 SUBTITLES: function SUBTITLES(type, settings) { 45418 return function () { 45419 var tracks = settings.mediaTypes[type].tracks; 45420 45421 for (var id in tracks) { 45422 if (tracks[id].mode === 'showing') { 45423 return tracks[id]; 45424 } 45425 } 45426 45427 return null; 45428 }; 45429 } 45430 }; 45431 45432 /** 45433 * Setup PlaylistLoaders and Tracks for media groups (Audio, Subtitles, 45434 * Closed-Captions) specified in the master manifest. 45435 * 45436 * @param {Object} settings 45437 * Object containing required information for setting up the media groups 45438 * @param {SegmentLoader} settings.segmentLoaders.AUDIO 45439 * Audio segment loader 45440 * @param {SegmentLoader} settings.segmentLoaders.SUBTITLES 45441 * Subtitle segment loader 45442 * @param {SegmentLoader} settings.segmentLoaders.main 45443 * Main segment loader 45444 * @param {Tech} settings.tech 45445 * The tech of the player 45446 * @param {Object} settings.requestOptions 45447 * XHR request options used by the segment loaders 45448 * @param {PlaylistLoader} settings.masterPlaylistLoader 45449 * PlaylistLoader for the master source 45450 * @param {HlsHandler} settings.hls 45451 * HLS SourceHandler 45452 * @param {Object} settings.master 45453 * The parsed master manifest 45454 * @param {Object} settings.mediaTypes 45455 * Object to store the loaders, tracks, and utility methods for each media type 45456 * @param {Function} settings.blacklistCurrentPlaylist 45457 * Blacklists the current rendition and forces a rendition switch. 45458 * @function setupMediaGroups 45459 */ 45460 var setupMediaGroups = function setupMediaGroups(settings) { 45461 ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach(function (type) { 45462 initialize[type](type, settings); 45463 }); 45464 45465 var mediaTypes = settings.mediaTypes, 45466 masterPlaylistLoader = settings.masterPlaylistLoader, 45467 tech = settings.tech, 45468 hls = settings.hls; 45469 45470 // setup active group and track getters and change event handlers 45471 45472 ['AUDIO', 'SUBTITLES'].forEach(function (type) { 45473 mediaTypes[type].activeGroup = activeGroup(type, settings); 45474 mediaTypes[type].activeTrack = activeTrack[type](type, settings); 45475 mediaTypes[type].onGroupChanged = onGroupChanged(type, settings); 45476 mediaTypes[type].onTrackChanged = onTrackChanged(type, settings); 45477 }); 45478 45479 // DO NOT enable the default subtitle or caption track. 45480 // DO enable the default audio track 45481 var audioGroup = mediaTypes.AUDIO.activeGroup(); 45482 var groupId = (audioGroup.filter(function (group) { 45483 return group.default; 45484 })[0] || audioGroup[0]).id; 45485 45486 mediaTypes.AUDIO.tracks[groupId].enabled = true; 45487 mediaTypes.AUDIO.onTrackChanged(); 45488 45489 masterPlaylistLoader.on('mediachange', function () { 45490 ['AUDIO', 'SUBTITLES'].forEach(function (type) { 45491 return mediaTypes[type].onGroupChanged(); 45492 }); 45493 }); 45494 45495 // custom audio track change event handler for usage event 45496 var onAudioTrackChanged = function onAudioTrackChanged() { 45497 mediaTypes.AUDIO.onTrackChanged(); 45498 tech.trigger({ type: 'usage', name: 'hls-audio-change' }); 45499 }; 45500 45501 tech.audioTracks().addEventListener('change', onAudioTrackChanged); 45502 tech.remoteTextTracks().addEventListener('change', mediaTypes.SUBTITLES.onTrackChanged); 45503 45504 hls.on('dispose', function () { 45505 tech.audioTracks().removeEventListener('change', onAudioTrackChanged); 45506 tech.remoteTextTracks().removeEventListener('change', mediaTypes.SUBTITLES.onTrackChanged); 45507 }); 45508 45509 // clear existing audio tracks and add the ones we just created 45510 tech.clearTracks('audio'); 45511 45512 for (var id in mediaTypes.AUDIO.tracks) { 45513 tech.audioTracks().addTrack(mediaTypes.AUDIO.tracks[id]); 45514 } 45515 }; 45516 45517 /** 45518 * Creates skeleton object used to store the loaders, tracks, and utility methods for each 45519 * media type 45520 * 45521 * @return {Object} 45522 * Object to store the loaders, tracks, and utility methods for each media type 45523 * @function createMediaTypes 45524 */ 45525 var createMediaTypes = function createMediaTypes() { 45526 var mediaTypes = {}; 45527 45528 ['AUDIO', 'SUBTITLES', 'CLOSED-CAPTIONS'].forEach(function (type) { 45529 mediaTypes[type] = { 45530 groups: {}, 45531 tracks: {}, 45532 activePlaylistLoader: null, 45533 activeGroup: noop$1, 45534 activeTrack: noop$1, 45535 onGroupChanged: noop$1, 45536 onTrackChanged: noop$1 45537 }; 45538 }); 45539 45540 return mediaTypes; 45541 }; 45542 45543 /** 45544 * @file master-playlist-controller.js 45545 */ 45546 45547 var ABORT_EARLY_BLACKLIST_SECONDS = 60 * 2; 45548 45549 var Hls = void 0; 45550 45551 // SegmentLoader stats that need to have each loader's 45552 // values summed to calculate the final value 45553 var loaderStats = ['mediaRequests', 'mediaRequestsAborted', 'mediaRequestsTimedout', 'mediaRequestsErrored', 'mediaTransferDuration', 'mediaBytesTransferred']; 45554 var sumLoaderStat = function sumLoaderStat(stat) { 45555 return this.audioSegmentLoader_[stat] + this.mainSegmentLoader_[stat]; 45556 }; 45557 45558 /** 45559 * the master playlist controller controller all interactons 45560 * between playlists and segmentloaders. At this time this mainly 45561 * involves a master playlist and a series of audio playlists 45562 * if they are available 45563 * 45564 * @class MasterPlaylistController 45565 * @extends videojs.EventTarget 45566 */ 45567 var MasterPlaylistController = function (_videojs$EventTarget) { 45568 inherits$3(MasterPlaylistController, _videojs$EventTarget); 45569 45570 function MasterPlaylistController(options) { 45571 classCallCheck$3(this, MasterPlaylistController); 45572 45573 var _this = possibleConstructorReturn$3(this, (MasterPlaylistController.__proto__ || Object.getPrototypeOf(MasterPlaylistController)).call(this)); 45574 45575 var url = options.url, 45576 withCredentials = options.withCredentials, 45577 tech = options.tech, 45578 bandwidth = options.bandwidth, 45579 externHls = options.externHls, 45580 useCueTags = options.useCueTags, 45581 blacklistDuration = options.blacklistDuration, 45582 enableLowInitialPlaylist = options.enableLowInitialPlaylist, 45583 sourceType = options.sourceType; 45584 45585 if (!url) { 45586 throw new Error('A non-empty playlist URL is required'); 45587 } 45588 45589 Hls = externHls; 45590 45591 _this.withCredentials = withCredentials; 45592 _this.tech_ = tech; 45593 _this.hls_ = tech.hls; 45594 _this.sourceType_ = sourceType; 45595 _this.useCueTags_ = useCueTags; 45596 _this.blacklistDuration = blacklistDuration; 45597 _this.enableLowInitialPlaylist = enableLowInitialPlaylist; 45598 if (_this.useCueTags_) { 45599 _this.cueTagsTrack_ = _this.tech_.addTextTrack('metadata', 'ad-cues'); 45600 _this.cueTagsTrack_.inBandMetadataTrackDispatchType = ''; 45601 } 45602 45603 _this.requestOptions_ = { 45604 withCredentials: _this.withCredentials, 45605 timeout: null 45606 }; 45607 45608 _this.mediaTypes_ = createMediaTypes(); 45609 45610 _this.mediaSource = new videojs$1.MediaSource(); 45611 45612 // load the media source into the player 45613 _this.mediaSource.addEventListener('sourceopen', _this.handleSourceOpen_.bind(_this)); 45614 45615 _this.seekable_ = videojs$1.createTimeRanges(); 45616 _this.hasPlayed_ = function () { 45617 return false; 45618 }; 45619 45620 _this.syncController_ = new SyncController(options); 45621 _this.segmentMetadataTrack_ = tech.addRemoteTextTrack({ 45622 kind: 'metadata', 45623 label: 'segment-metadata' 45624 }, false).track; 45625 45626 _this.decrypter_ = new Decrypter$1(); 45627 45628 var segmentLoaderSettings = { 45629 hls: _this.hls_, 45630 mediaSource: _this.mediaSource, 45631 currentTime: _this.tech_.currentTime.bind(_this.tech_), 45632 seekable: function seekable$$1() { 45633 return _this.seekable(); 45634 }, 45635 seeking: function seeking() { 45636 return _this.tech_.seeking(); 45637 }, 45638 duration: function duration$$1() { 45639 return _this.mediaSource.duration; 45640 }, 45641 hasPlayed: function hasPlayed() { 45642 return _this.hasPlayed_(); 45643 }, 45644 goalBufferLength: function goalBufferLength() { 45645 return _this.goalBufferLength(); 45646 }, 45647 bandwidth: bandwidth, 45648 syncController: _this.syncController_, 45649 decrypter: _this.decrypter_, 45650 sourceType: _this.sourceType_ 45651 }; 45652 45653 _this.masterPlaylistLoader_ = _this.sourceType_ === 'dash' ? new DashPlaylistLoader(url, _this.hls_, _this.withCredentials) : new PlaylistLoader(url, _this.hls_, _this.withCredentials); 45654 _this.setupMasterPlaylistLoaderListeners_(); 45655 45656 // setup segment loaders 45657 // combined audio/video or just video when alternate audio track is selected 45658 _this.mainSegmentLoader_ = new SegmentLoader(videojs$1.mergeOptions(segmentLoaderSettings, { 45659 segmentMetadataTrack: _this.segmentMetadataTrack_, 45660 loaderType: 'main' 45661 }), options); 45662 45663 // alternate audio track 45664 _this.audioSegmentLoader_ = new SegmentLoader(videojs$1.mergeOptions(segmentLoaderSettings, { 45665 loaderType: 'audio' 45666 }), options); 45667 45668 _this.subtitleSegmentLoader_ = new VTTSegmentLoader(videojs$1.mergeOptions(segmentLoaderSettings, { 45669 loaderType: 'vtt' 45670 }), options); 45671 45672 _this.setupSegmentLoaderListeners_(); 45673 45674 // Create SegmentLoader stat-getters 45675 loaderStats.forEach(function (stat) { 45676 _this[stat + '_'] = sumLoaderStat.bind(_this, stat); 45677 }); 45678 45679 _this.logger_ = logger('MPC'); 45680 45681 _this.masterPlaylistLoader_.load(); 45682 return _this; 45683 } 45684 45685 /** 45686 * Register event handlers on the master playlist loader. A helper 45687 * function for construction time. 45688 * 45689 * @private 45690 */ 45691 45692 createClass$2(MasterPlaylistController, [{ 45693 key: 'setupMasterPlaylistLoaderListeners_', 45694 value: function setupMasterPlaylistLoaderListeners_() { 45695 var _this2 = this; 45696 45697 this.masterPlaylistLoader_.on('loadedmetadata', function () { 45698 var media = _this2.masterPlaylistLoader_.media(); 45699 var requestTimeout = _this2.masterPlaylistLoader_.targetDuration * 1.5 * 1000; 45700 45701 // If we don't have any more available playlists, we don't want to 45702 // timeout the request. 45703 if (isLowestEnabledRendition(_this2.masterPlaylistLoader_.master, _this2.masterPlaylistLoader_.media())) { 45704 _this2.requestOptions_.timeout = 0; 45705 } else { 45706 _this2.requestOptions_.timeout = requestTimeout; 45707 } 45708 45709 // if this isn't a live video and preload permits, start 45710 // downloading segments 45711 if (media.endList && _this2.tech_.preload() !== 'none') { 45712 _this2.mainSegmentLoader_.playlist(media, _this2.requestOptions_); 45713 _this2.mainSegmentLoader_.load(); 45714 } 45715 45716 setupMediaGroups({ 45717 sourceType: _this2.sourceType_, 45718 segmentLoaders: { 45719 AUDIO: _this2.audioSegmentLoader_, 45720 SUBTITLES: _this2.subtitleSegmentLoader_, 45721 main: _this2.mainSegmentLoader_ 45722 }, 45723 tech: _this2.tech_, 45724 requestOptions: _this2.requestOptions_, 45725 masterPlaylistLoader: _this2.masterPlaylistLoader_, 45726 hls: _this2.hls_, 45727 master: _this2.master(), 45728 mediaTypes: _this2.mediaTypes_, 45729 blacklistCurrentPlaylist: _this2.blacklistCurrentPlaylist.bind(_this2) 45730 }); 45731 45732 _this2.triggerPresenceUsage_(_this2.master(), media); 45733 45734 try { 45735 _this2.setupSourceBuffers_(); 45736 } catch (e) { 45737 videojs$1.log.warn('Failed to create SourceBuffers', e); 45738 return _this2.mediaSource.endOfStream('decode'); 45739 } 45740 _this2.setupFirstPlay(); 45741 45742 _this2.trigger('selectedinitialmedia'); 45743 }); 45744 45745 this.masterPlaylistLoader_.on('loadedplaylist', function () { 45746 var updatedPlaylist = _this2.masterPlaylistLoader_.media(); 45747 45748 if (!updatedPlaylist) { 45749 // blacklist any variants that are not supported by the browser before selecting 45750 // an initial media as the playlist selectors do not consider browser support 45751 _this2.excludeUnsupportedVariants_(); 45752 45753 var selectedMedia = void 0; 45754 45755 if (_this2.enableLowInitialPlaylist) { 45756 selectedMedia = _this2.selectInitialPlaylist(); 45757 } 45758 45759 if (!selectedMedia) { 45760 selectedMedia = _this2.selectPlaylist(); 45761 } 45762 45763 _this2.initialMedia_ = selectedMedia; 45764 _this2.masterPlaylistLoader_.media(_this2.initialMedia_); 45765 return; 45766 } 45767 45768 if (_this2.useCueTags_) { 45769 _this2.updateAdCues_(updatedPlaylist); 45770 } 45771 45772 // TODO: Create a new event on the PlaylistLoader that signals 45773 // that the segments have changed in some way and use that to 45774 // update the SegmentLoader instead of doing it twice here and 45775 // on `mediachange` 45776 _this2.mainSegmentLoader_.playlist(updatedPlaylist, _this2.requestOptions_); 45777 _this2.updateDuration(); 45778 45779 // If the player isn't paused, ensure that the segment loader is running, 45780 // as it is possible that it was temporarily stopped while waiting for 45781 // a playlist (e.g., in case the playlist errored and we re-requested it). 45782 if (!_this2.tech_.paused()) { 45783 _this2.mainSegmentLoader_.load(); 45784 } 45785 45786 if (!updatedPlaylist.endList) { 45787 var addSeekableRange = function addSeekableRange() { 45788 var seekable$$1 = _this2.seekable(); 45789 45790 if (seekable$$1.length !== 0) { 45791 _this2.mediaSource.addSeekableRange_(seekable$$1.start(0), seekable$$1.end(0)); 45792 } 45793 }; 45794 45795 if (_this2.duration() !== Infinity) { 45796 var onDurationchange = function onDurationchange() { 45797 if (_this2.duration() === Infinity) { 45798 addSeekableRange(); 45799 } else { 45800 _this2.tech_.one('durationchange', onDurationchange); 45801 } 45802 }; 45803 45804 _this2.tech_.one('durationchange', onDurationchange); 45805 } else { 45806 addSeekableRange(); 45807 } 45808 } 45809 }); 45810 45811 this.masterPlaylistLoader_.on('error', function () { 45812 _this2.blacklistCurrentPlaylist(_this2.masterPlaylistLoader_.error); 45813 }); 45814 45815 this.masterPlaylistLoader_.on('mediachanging', function () { 45816 _this2.mainSegmentLoader_.abort(); 45817 _this2.mainSegmentLoader_.pause(); 45818 }); 45819 45820 this.masterPlaylistLoader_.on('mediachange', function () { 45821 var media = _this2.masterPlaylistLoader_.media(); 45822 var requestTimeout = _this2.masterPlaylistLoader_.targetDuration * 1.5 * 1000; 45823 45824 // If we don't have any more available playlists, we don't want to 45825 // timeout the request. 45826 if (isLowestEnabledRendition(_this2.masterPlaylistLoader_.master, _this2.masterPlaylistLoader_.media())) { 45827 _this2.requestOptions_.timeout = 0; 45828 } else { 45829 _this2.requestOptions_.timeout = requestTimeout; 45830 } 45831 45832 // TODO: Create a new event on the PlaylistLoader that signals 45833 // that the segments have changed in some way and use that to 45834 // update the SegmentLoader instead of doing it twice here and 45835 // on `loadedplaylist` 45836 _this2.mainSegmentLoader_.playlist(media, _this2.requestOptions_); 45837 _this2.mainSegmentLoader_.load(); 45838 45839 _this2.tech_.trigger({ 45840 type: 'mediachange', 45841 bubbles: true 45842 }); 45843 }); 45844 45845 this.masterPlaylistLoader_.on('playlistunchanged', function () { 45846 var updatedPlaylist = _this2.masterPlaylistLoader_.media(); 45847 var playlistOutdated = _this2.stuckAtPlaylistEnd_(updatedPlaylist); 45848 45849 if (playlistOutdated) { 45850 // Playlist has stopped updating and we're stuck at its end. Try to 45851 // blacklist it and switch to another playlist in the hope that that 45852 // one is updating (and give the player a chance to re-adjust to the 45853 // safe live point). 45854 _this2.blacklistCurrentPlaylist({ 45855 message: 'Playlist no longer updating.' 45856 }); 45857 // useful for monitoring QoS 45858 _this2.tech_.trigger('playliststuck'); 45859 } 45860 }); 45861 45862 this.masterPlaylistLoader_.on('renditiondisabled', function () { 45863 _this2.tech_.trigger({ type: 'usage', name: 'hls-rendition-disabled' }); 45864 }); 45865 this.masterPlaylistLoader_.on('renditionenabled', function () { 45866 _this2.tech_.trigger({ type: 'usage', name: 'hls-rendition-enabled' }); 45867 }); 45868 } 45869 45870 /** 45871 * A helper function for triggerring presence usage events once per source 45872 * 45873 * @private 45874 */ 45875 45876 }, { 45877 key: 'triggerPresenceUsage_', 45878 value: function triggerPresenceUsage_(master, media) { 45879 var mediaGroups = master.mediaGroups || {}; 45880 var defaultDemuxed = true; 45881 var audioGroupKeys = Object.keys(mediaGroups.AUDIO); 45882 45883 for (var mediaGroup in mediaGroups.AUDIO) { 45884 for (var label in mediaGroups.AUDIO[mediaGroup]) { 45885 var properties = mediaGroups.AUDIO[mediaGroup][label]; 45886 45887 if (!properties.uri) { 45888 defaultDemuxed = false; 45889 } 45890 } 45891 } 45892 45893 if (defaultDemuxed) { 45894 this.tech_.trigger({ type: 'usage', name: 'hls-demuxed' }); 45895 } 45896 45897 if (Object.keys(mediaGroups.SUBTITLES).length) { 45898 this.tech_.trigger({ type: 'usage', name: 'hls-webvtt' }); 45899 } 45900 45901 if (Hls.Playlist.isAes(media)) { 45902 this.tech_.trigger({ type: 'usage', name: 'hls-aes' }); 45903 } 45904 45905 if (Hls.Playlist.isFmp4(media)) { 45906 this.tech_.trigger({ type: 'usage', name: 'hls-fmp4' }); 45907 } 45908 45909 if (audioGroupKeys.length && Object.keys(mediaGroups.AUDIO[audioGroupKeys[0]]).length > 1) { 45910 this.tech_.trigger({ type: 'usage', name: 'hls-alternate-audio' }); 45911 } 45912 45913 if (this.useCueTags_) { 45914 this.tech_.trigger({ type: 'usage', name: 'hls-playlist-cue-tags' }); 45915 } 45916 } 45917 /** 45918 * Register event handlers on the segment loaders. A helper function 45919 * for construction time. 45920 * 45921 * @private 45922 */ 45923 45924 }, { 45925 key: 'setupSegmentLoaderListeners_', 45926 value: function setupSegmentLoaderListeners_() { 45927 var _this3 = this; 45928 45929 this.mainSegmentLoader_.on('bandwidthupdate', function () { 45930 var nextPlaylist = _this3.selectPlaylist(); 45931 var currentPlaylist = _this3.masterPlaylistLoader_.media(); 45932 var buffered = _this3.tech_.buffered(); 45933 var forwardBuffer = buffered.length ? buffered.end(buffered.length - 1) - _this3.tech_.currentTime() : 0; 45934 45935 var bufferLowWaterLine = _this3.bufferLowWaterLine(); 45936 45937 // If the playlist is live, then we want to not take low water line into account. 45938 // This is because in LIVE, the player plays 3 segments from the end of the 45939 // playlist, and if `BUFFER_LOW_WATER_LINE` is greater than the duration availble 45940 // in those segments, a viewer will never experience a rendition upswitch. 45941 if (!currentPlaylist.endList || 45942 // For the same reason as LIVE, we ignore the low water line when the VOD 45943 // duration is below the max potential low water line 45944 _this3.duration() < Config.MAX_BUFFER_LOW_WATER_LINE || 45945 // we want to switch down to lower resolutions quickly to continue playback, but 45946 nextPlaylist.attributes.BANDWIDTH < currentPlaylist.attributes.BANDWIDTH || 45947 // ensure we have some buffer before we switch up to prevent us running out of 45948 // buffer while loading a higher rendition. 45949 forwardBuffer >= bufferLowWaterLine) { 45950 _this3.masterPlaylistLoader_.media(nextPlaylist); 45951 } 45952 45953 _this3.tech_.trigger('bandwidthupdate'); 45954 }); 45955 this.mainSegmentLoader_.on('progress', function () { 45956 _this3.trigger('progress'); 45957 }); 45958 45959 this.mainSegmentLoader_.on('error', function () { 45960 _this3.blacklistCurrentPlaylist(_this3.mainSegmentLoader_.error()); 45961 }); 45962 45963 this.mainSegmentLoader_.on('syncinfoupdate', function () { 45964 _this3.onSyncInfoUpdate_(); 45965 }); 45966 45967 this.mainSegmentLoader_.on('timestampoffset', function () { 45968 _this3.tech_.trigger({ type: 'usage', name: 'hls-timestamp-offset' }); 45969 }); 45970 this.audioSegmentLoader_.on('syncinfoupdate', function () { 45971 _this3.onSyncInfoUpdate_(); 45972 }); 45973 45974 this.mainSegmentLoader_.on('ended', function () { 45975 _this3.onEndOfStream(); 45976 }); 45977 45978 this.mainSegmentLoader_.on('earlyabort', function () { 45979 _this3.blacklistCurrentPlaylist({ 45980 message: 'Aborted early because there isn\'t enough bandwidth to complete the ' + 'request without rebuffering.' 45981 }, ABORT_EARLY_BLACKLIST_SECONDS); 45982 }); 45983 45984 this.mainSegmentLoader_.on('reseteverything', function () { 45985 // If playing an MTS stream, a videojs.MediaSource is listening for 45986 // hls-reset to reset caption parsing state in the transmuxer 45987 _this3.tech_.trigger('hls-reset'); 45988 }); 45989 45990 this.mainSegmentLoader_.on('segmenttimemapping', function (event) { 45991 // If playing an MTS stream in html, a videojs.MediaSource is listening for 45992 // hls-segment-time-mapping update its internal mapping of stream to display time 45993 _this3.tech_.trigger({ 45994 type: 'hls-segment-time-mapping', 45995 mapping: event.mapping 45996 }); 45997 }); 45998 45999 this.audioSegmentLoader_.on('ended', function () { 46000 _this3.onEndOfStream(); 46001 }); 46002 } 46003 }, { 46004 key: 'mediaSecondsLoaded_', 46005 value: function mediaSecondsLoaded_() { 46006 return Math.max(this.audioSegmentLoader_.mediaSecondsLoaded + this.mainSegmentLoader_.mediaSecondsLoaded); 46007 } 46008 46009 /** 46010 * Call load on our SegmentLoaders 46011 */ 46012 46013 }, { 46014 key: 'load', 46015 value: function load() { 46016 this.mainSegmentLoader_.load(); 46017 if (this.mediaTypes_.AUDIO.activePlaylistLoader) { 46018 this.audioSegmentLoader_.load(); 46019 } 46020 if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) { 46021 this.subtitleSegmentLoader_.load(); 46022 } 46023 } 46024 46025 /** 46026 * Re-tune playback quality level for the current player 46027 * conditions. This method may perform destructive actions, like 46028 * removing already buffered content, to readjust the currently 46029 * active playlist quickly. 46030 * 46031 * @private 46032 */ 46033 46034 }, { 46035 key: 'fastQualityChange_', 46036 value: function fastQualityChange_() { 46037 var media = this.selectPlaylist(); 46038 46039 if (media !== this.masterPlaylistLoader_.media()) { 46040 this.masterPlaylistLoader_.media(media); 46041 46042 this.mainSegmentLoader_.resetLoader(); 46043 // don't need to reset audio as it is reset when media changes 46044 } 46045 } 46046 46047 /** 46048 * Begin playback. 46049 */ 46050 46051 }, { 46052 key: 'play', 46053 value: function play() { 46054 if (this.setupFirstPlay()) { 46055 return; 46056 } 46057 46058 if (this.tech_.ended()) { 46059 this.tech_.setCurrentTime(0); 46060 } 46061 46062 if (this.hasPlayed_()) { 46063 this.load(); 46064 } 46065 46066 var seekable$$1 = this.tech_.seekable(); 46067 46068 // if the viewer has paused and we fell out of the live window, 46069 // seek forward to the live point 46070 if (this.tech_.duration() === Infinity) { 46071 if (this.tech_.currentTime() < seekable$$1.start(0)) { 46072 return this.tech_.setCurrentTime(seekable$$1.end(seekable$$1.length - 1)); 46073 } 46074 } 46075 } 46076 46077 /** 46078 * Seek to the latest media position if this is a live video and the 46079 * player and video are loaded and initialized. 46080 */ 46081 46082 }, { 46083 key: 'setupFirstPlay', 46084 value: function setupFirstPlay() { 46085 var _this4 = this; 46086 46087 var media = this.masterPlaylistLoader_.media(); 46088 46089 // Check that everything is ready to begin buffering for the first call to play 46090 // If 1) there is no active media 46091 // 2) the player is paused 46092 // 3) the first play has already been setup 46093 // then exit early 46094 if (!media || this.tech_.paused() || this.hasPlayed_()) { 46095 return false; 46096 } 46097 46098 // when the video is a live stream 46099 if (!media.endList) { 46100 var seekable$$1 = this.seekable(); 46101 46102 if (!seekable$$1.length) { 46103 // without a seekable range, the player cannot seek to begin buffering at the live 46104 // point 46105 return false; 46106 } 46107 46108 if (videojs$1.browser.IE_VERSION && this.tech_.readyState() === 0) { 46109 // IE11 throws an InvalidStateError if you try to set currentTime while the 46110 // readyState is 0, so it must be delayed until the tech fires loadedmetadata. 46111 this.tech_.one('loadedmetadata', function () { 46112 _this4.trigger('firstplay'); 46113 _this4.tech_.setCurrentTime(seekable$$1.end(0)); 46114 _this4.hasPlayed_ = function () { 46115 return true; 46116 }; 46117 }); 46118 46119 return false; 46120 } 46121 46122 // trigger firstplay to inform the source handler to ignore the next seek event 46123 this.trigger('firstplay'); 46124 // seek to the live point 46125 this.tech_.setCurrentTime(seekable$$1.end(0)); 46126 } 46127 46128 this.hasPlayed_ = function () { 46129 return true; 46130 }; 46131 // we can begin loading now that everything is ready 46132 this.load(); 46133 return true; 46134 } 46135 46136 /** 46137 * handle the sourceopen event on the MediaSource 46138 * 46139 * @private 46140 */ 46141 46142 }, { 46143 key: 'handleSourceOpen_', 46144 value: function handleSourceOpen_() { 46145 // Only attempt to create the source buffer if none already exist. 46146 // handleSourceOpen is also called when we are "re-opening" a source buffer 46147 // after `endOfStream` has been called (in response to a seek for instance) 46148 try { 46149 this.setupSourceBuffers_(); 46150 } catch (e) { 46151 videojs$1.log.warn('Failed to create Source Buffers', e); 46152 return this.mediaSource.endOfStream('decode'); 46153 } 46154 46155 // if autoplay is enabled, begin playback. This is duplicative of 46156 // code in video.js but is required because play() must be invoked 46157 // *after* the media source has opened. 46158 if (this.tech_.autoplay()) { 46159 var playPromise = this.tech_.play(); 46160 46161 // Catch/silence error when a pause interrupts a play request 46162 // on browsers which return a promise 46163 if (typeof playPromise !== 'undefined' && typeof playPromise.then === 'function') { 46164 playPromise.then(null, function (e) {}); 46165 } 46166 } 46167 46168 this.trigger('sourceopen'); 46169 } 46170 46171 /** 46172 * Calls endOfStream on the media source when all active stream types have called 46173 * endOfStream 46174 * 46175 * @param {string} streamType 46176 * Stream type of the segment loader that called endOfStream 46177 * @private 46178 */ 46179 46180 }, { 46181 key: 'onEndOfStream', 46182 value: function onEndOfStream() { 46183 var isEndOfStream = this.mainSegmentLoader_.ended_; 46184 46185 if (this.mediaTypes_.AUDIO.activePlaylistLoader) { 46186 // if the audio playlist loader exists, then alternate audio is active 46187 if (!this.mainSegmentLoader_.startingMedia_ || this.mainSegmentLoader_.startingMedia_.containsVideo) { 46188 // if we do not know if the main segment loader contains video yet or if we 46189 // definitively know the main segment loader contains video, then we need to wait 46190 // for both main and audio segment loaders to call endOfStream 46191 isEndOfStream = isEndOfStream && this.audioSegmentLoader_.ended_; 46192 } else { 46193 // otherwise just rely on the audio loader 46194 isEndOfStream = this.audioSegmentLoader_.ended_; 46195 } 46196 } 46197 46198 if (isEndOfStream) { 46199 this.mediaSource.endOfStream(); 46200 } 46201 } 46202 46203 /** 46204 * Check if a playlist has stopped being updated 46205 * @param {Object} playlist the media playlist object 46206 * @return {boolean} whether the playlist has stopped being updated or not 46207 */ 46208 46209 }, { 46210 key: 'stuckAtPlaylistEnd_', 46211 value: function stuckAtPlaylistEnd_(playlist) { 46212 var seekable$$1 = this.seekable(); 46213 46214 if (!seekable$$1.length) { 46215 // playlist doesn't have enough information to determine whether we are stuck 46216 return false; 46217 } 46218 46219 var expired = this.syncController_.getExpiredTime(playlist, this.mediaSource.duration); 46220 46221 if (expired === null) { 46222 return false; 46223 } 46224 46225 // does not use the safe live end to calculate playlist end, since we 46226 // don't want to say we are stuck while there is still content 46227 var absolutePlaylistEnd = Hls.Playlist.playlistEnd(playlist, expired); 46228 var currentTime = this.tech_.currentTime(); 46229 var buffered = this.tech_.buffered(); 46230 46231 if (!buffered.length) { 46232 // return true if the playhead reached the absolute end of the playlist 46233 return absolutePlaylistEnd - currentTime <= SAFE_TIME_DELTA; 46234 } 46235 var bufferedEnd = buffered.end(buffered.length - 1); 46236 46237 // return true if there is too little buffer left and buffer has reached absolute 46238 // end of playlist 46239 return bufferedEnd - currentTime <= SAFE_TIME_DELTA && absolutePlaylistEnd - bufferedEnd <= SAFE_TIME_DELTA; 46240 } 46241 46242 /** 46243 * Blacklists a playlist when an error occurs for a set amount of time 46244 * making it unavailable for selection by the rendition selection algorithm 46245 * and then forces a new playlist (rendition) selection. 46246 * 46247 * @param {Object=} error an optional error that may include the playlist 46248 * to blacklist 46249 * @param {Number=} blacklistDuration an optional number of seconds to blacklist the 46250 * playlist 46251 */ 46252 46253 }, { 46254 key: 'blacklistCurrentPlaylist', 46255 value: function blacklistCurrentPlaylist() { 46256 var error = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 46257 var blacklistDuration = arguments[1]; 46258 46259 var currentPlaylist = void 0; 46260 var nextPlaylist = void 0; 46261 46262 // If the `error` was generated by the playlist loader, it will contain 46263 // the playlist we were trying to load (but failed) and that should be 46264 // blacklisted instead of the currently selected playlist which is likely 46265 // out-of-date in this scenario 46266 currentPlaylist = error.playlist || this.masterPlaylistLoader_.media(); 46267 46268 blacklistDuration = blacklistDuration || error.blacklistDuration || this.blacklistDuration; 46269 46270 // If there is no current playlist, then an error occurred while we were 46271 // trying to load the master OR while we were disposing of the tech 46272 if (!currentPlaylist) { 46273 this.error = error; 46274 46275 try { 46276 return this.mediaSource.endOfStream('network'); 46277 } catch (e) { 46278 return this.trigger('error'); 46279 } 46280 } 46281 46282 var isFinalRendition = this.masterPlaylistLoader_.master.playlists.filter(isEnabled).length === 1; 46283 46284 if (isFinalRendition) { 46285 // Never blacklisting this playlist because it's final rendition 46286 videojs$1.log.warn('Problem encountered with the current ' + 'HLS playlist. Trying again since it is the final playlist.'); 46287 46288 this.tech_.trigger('retryplaylist'); 46289 return this.masterPlaylistLoader_.load(isFinalRendition); 46290 } 46291 // Blacklist this playlist 46292 currentPlaylist.excludeUntil = Date.now() + blacklistDuration * 1000; 46293 this.tech_.trigger('blacklistplaylist'); 46294 this.tech_.trigger({ type: 'usage', name: 'hls-rendition-blacklisted' }); 46295 46296 // Select a new playlist 46297 nextPlaylist = this.selectPlaylist(); 46298 videojs$1.log.warn('Problem encountered with the current HLS playlist.' + (error.message ? ' ' + error.message : '') + ' Switching to another playlist.'); 46299 46300 return this.masterPlaylistLoader_.media(nextPlaylist); 46301 } 46302 46303 /** 46304 * Pause all segment loaders 46305 */ 46306 46307 }, { 46308 key: 'pauseLoading', 46309 value: function pauseLoading() { 46310 this.mainSegmentLoader_.pause(); 46311 if (this.mediaTypes_.AUDIO.activePlaylistLoader) { 46312 this.audioSegmentLoader_.pause(); 46313 } 46314 if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) { 46315 this.subtitleSegmentLoader_.pause(); 46316 } 46317 } 46318 46319 /** 46320 * set the current time on all segment loaders 46321 * 46322 * @param {TimeRange} currentTime the current time to set 46323 * @return {TimeRange} the current time 46324 */ 46325 46326 }, { 46327 key: 'setCurrentTime', 46328 value: function setCurrentTime(currentTime) { 46329 var buffered = findRange(this.tech_.buffered(), currentTime); 46330 46331 if (!(this.masterPlaylistLoader_ && this.masterPlaylistLoader_.media())) { 46332 // return immediately if the metadata is not ready yet 46333 return 0; 46334 } 46335 46336 // it's clearly an edge-case but don't thrown an error if asked to 46337 // seek within an empty playlist 46338 if (!this.masterPlaylistLoader_.media().segments) { 46339 return 0; 46340 } 46341 46342 // In flash playback, the segment loaders should be reset on every seek, even 46343 // in buffer seeks. If the seek location is already buffered, continue buffering as 46344 // usual 46345 // TODO: redo this comment 46346 if (buffered && buffered.length) { 46347 return currentTime; 46348 } 46349 46350 // cancel outstanding requests so we begin buffering at the new 46351 // location 46352 this.mainSegmentLoader_.resetEverything(); 46353 this.mainSegmentLoader_.abort(); 46354 if (this.mediaTypes_.AUDIO.activePlaylistLoader) { 46355 this.audioSegmentLoader_.resetEverything(); 46356 this.audioSegmentLoader_.abort(); 46357 } 46358 if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) { 46359 this.subtitleSegmentLoader_.resetEverything(); 46360 this.subtitleSegmentLoader_.abort(); 46361 } 46362 46363 // start segment loader loading in case they are paused 46364 this.load(); 46365 } 46366 46367 /** 46368 * get the current duration 46369 * 46370 * @return {TimeRange} the duration 46371 */ 46372 46373 }, { 46374 key: 'duration', 46375 value: function duration$$1() { 46376 if (!this.masterPlaylistLoader_) { 46377 return 0; 46378 } 46379 46380 if (this.mediaSource) { 46381 return this.mediaSource.duration; 46382 } 46383 46384 return Hls.Playlist.duration(this.masterPlaylistLoader_.media()); 46385 } 46386 46387 /** 46388 * check the seekable range 46389 * 46390 * @return {TimeRange} the seekable range 46391 */ 46392 46393 }, { 46394 key: 'seekable', 46395 value: function seekable$$1() { 46396 return this.seekable_; 46397 } 46398 }, { 46399 key: 'onSyncInfoUpdate_', 46400 value: function onSyncInfoUpdate_() { 46401 var mainSeekable = void 0; 46402 var audioSeekable = void 0; 46403 46404 if (!this.masterPlaylistLoader_) { 46405 return; 46406 } 46407 46408 var media = this.masterPlaylistLoader_.media(); 46409 46410 if (!media) { 46411 return; 46412 } 46413 46414 var expired = this.syncController_.getExpiredTime(media, this.mediaSource.duration); 46415 46416 if (expired === null) { 46417 // not enough information to update seekable 46418 return; 46419 } 46420 46421 mainSeekable = Hls.Playlist.seekable(media, expired); 46422 46423 if (mainSeekable.length === 0) { 46424 return; 46425 } 46426 46427 if (this.mediaTypes_.AUDIO.activePlaylistLoader) { 46428 media = this.mediaTypes_.AUDIO.activePlaylistLoader.media(); 46429 expired = this.syncController_.getExpiredTime(media, this.mediaSource.duration); 46430 46431 if (expired === null) { 46432 return; 46433 } 46434 46435 audioSeekable = Hls.Playlist.seekable(media, expired); 46436 46437 if (audioSeekable.length === 0) { 46438 return; 46439 } 46440 } 46441 46442 if (!audioSeekable) { 46443 // seekable has been calculated based on buffering video data so it 46444 // can be returned directly 46445 this.seekable_ = mainSeekable; 46446 } else if (audioSeekable.start(0) > mainSeekable.end(0) || mainSeekable.start(0) > audioSeekable.end(0)) { 46447 // seekables are pretty far off, rely on main 46448 this.seekable_ = mainSeekable; 46449 } else { 46450 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)]]); 46451 } 46452 46453 this.logger_('seekable updated [' + printableRange(this.seekable_) + ']'); 46454 46455 this.tech_.trigger('seekablechanged'); 46456 } 46457 46458 /** 46459 * Update the player duration 46460 */ 46461 46462 }, { 46463 key: 'updateDuration', 46464 value: function updateDuration() { 46465 var _this5 = this; 46466 46467 var oldDuration = this.mediaSource.duration; 46468 var newDuration = Hls.Playlist.duration(this.masterPlaylistLoader_.media()); 46469 var buffered = this.tech_.buffered(); 46470 var setDuration = function setDuration() { 46471 _this5.mediaSource.duration = newDuration; 46472 _this5.tech_.trigger('durationchange'); 46473 46474 _this5.mediaSource.removeEventListener('sourceopen', setDuration); 46475 }; 46476 46477 if (buffered.length > 0) { 46478 newDuration = Math.max(newDuration, buffered.end(buffered.length - 1)); 46479 } 46480 46481 // if the duration has changed, invalidate the cached value 46482 if (oldDuration !== newDuration) { 46483 // update the duration 46484 if (this.mediaSource.readyState !== 'open') { 46485 this.mediaSource.addEventListener('sourceopen', setDuration); 46486 } else { 46487 setDuration(); 46488 } 46489 } 46490 } 46491 46492 /** 46493 * dispose of the MasterPlaylistController and everything 46494 * that it controls 46495 */ 46496 46497 }, { 46498 key: 'dispose', 46499 value: function dispose() { 46500 var _this6 = this; 46501 46502 this.decrypter_.terminate(); 46503 this.masterPlaylistLoader_.dispose(); 46504 this.mainSegmentLoader_.dispose(); 46505 46506 ['AUDIO', 'SUBTITLES'].forEach(function (type) { 46507 var groups = _this6.mediaTypes_[type].groups; 46508 46509 for (var id in groups) { 46510 groups[id].forEach(function (group) { 46511 if (group.playlistLoader) { 46512 group.playlistLoader.dispose(); 46513 } 46514 }); 46515 } 46516 }); 46517 46518 this.audioSegmentLoader_.dispose(); 46519 this.subtitleSegmentLoader_.dispose(); 46520 } 46521 46522 /** 46523 * return the master playlist object if we have one 46524 * 46525 * @return {Object} the master playlist object that we parsed 46526 */ 46527 46528 }, { 46529 key: 'master', 46530 value: function master() { 46531 return this.masterPlaylistLoader_.master; 46532 } 46533 46534 /** 46535 * return the currently selected playlist 46536 * 46537 * @return {Object} the currently selected playlist object that we parsed 46538 */ 46539 46540 }, { 46541 key: 'media', 46542 value: function media() { 46543 // playlist loader will not return media if it has not been fully loaded 46544 return this.masterPlaylistLoader_.media() || this.initialMedia_; 46545 } 46546 46547 /** 46548 * setup our internal source buffers on our segment Loaders 46549 * 46550 * @private 46551 */ 46552 46553 }, { 46554 key: 'setupSourceBuffers_', 46555 value: function setupSourceBuffers_() { 46556 var media = this.masterPlaylistLoader_.media(); 46557 var mimeTypes = void 0; 46558 46559 // wait until a media playlist is available and the Media Source is 46560 // attached 46561 if (!media || this.mediaSource.readyState !== 'open') { 46562 return; 46563 } 46564 46565 mimeTypes = mimeTypesForPlaylist(this.masterPlaylistLoader_.master, media); 46566 if (mimeTypes.length < 1) { 46567 this.error = 'No compatible SourceBuffer configuration for the variant stream:' + media.resolvedUri; 46568 return this.mediaSource.endOfStream('decode'); 46569 } 46570 46571 this.configureLoaderMimeTypes_(mimeTypes); 46572 // exclude any incompatible variant streams from future playlist 46573 // selection 46574 this.excludeIncompatibleVariants_(media); 46575 } 46576 }, { 46577 key: 'configureLoaderMimeTypes_', 46578 value: function configureLoaderMimeTypes_(mimeTypes) { 46579 // If the content is demuxed, we can't start appending segments to a source buffer 46580 // until both source buffers are set up, or else the browser may not let us add the 46581 // second source buffer (it will assume we are playing either audio only or video 46582 // only). 46583 var sourceBufferEmitter = 46584 // If there is more than one mime type 46585 mimeTypes.length > 1 && 46586 // and the first mime type does not have muxed video and audio 46587 mimeTypes[0].indexOf(',') === -1 && 46588 // and the two mime types are different (they can be the same in the case of audio 46589 // only with alternate audio) 46590 mimeTypes[0] !== mimeTypes[1] ? 46591 // then we want to wait on the second source buffer 46592 new videojs$1.EventTarget() : 46593 // otherwise there is no need to wait as the content is either audio only, 46594 // video only, or muxed content. 46595 null; 46596 46597 this.mainSegmentLoader_.mimeType(mimeTypes[0], sourceBufferEmitter); 46598 if (mimeTypes[1]) { 46599 this.audioSegmentLoader_.mimeType(mimeTypes[1], sourceBufferEmitter); 46600 } 46601 } 46602 46603 /** 46604 * Blacklists playlists with codecs that are unsupported by the browser. 46605 */ 46606 46607 }, { 46608 key: 'excludeUnsupportedVariants_', 46609 value: function excludeUnsupportedVariants_() {
46610 this.master().playlists.forEach(function (variant) { 46611 if (variant.attributes.CODECS && window_1.MediaSource && window_1.MediaSource.isTypeSupported && !window_1.MediaSource.isTypeSupported('video/mp4; codecs="' + mapLegacyAvcCodecs(variant.attributes.CODECS) + '"')) { 46612 variant.excludeUntil = Infinity; 46613 } 46614 }); 46615 } 46616 46617 /** 46618 * Blacklist playlists that are known to be codec or 46619 * stream-incompatible with the SourceBuffer configuration. For 46620 * instance, Media Source Extensions would cause the video element to 46621 * stall waiting for video data if you switched from a variant with 46622 * video and audio to an audio-only one. 46623 * 46624 * @param {Object} media a media playlist compatible with the current 46625 * set of SourceBuffers. Variants in the current master playlist that 46626 * do not appear to have compatible codec or stream configurations 46627 * will be excluded from the default playlist selection algorithm 46628 * indefinitely. 46629 * @private 46630 */ 46631 46632 }, { 46633 key: 'excludeIncompatibleVariants_', 46634 value: function excludeIncompatibleVariants_(media) { 46635 var codecCount = 2; 46636 var videoCodec = null; 46637 var codecs = void 0; 46638 46639 if (media.attributes.CODECS) { 46640 codecs = parseCodecs(media.attributes.CODECS); 46641 videoCodec = codecs.videoCodec; 46642 codecCount = codecs.codecCount; 46643 } 46644
vendor: 45,268 bytes, lines 46645-47941
46645 this.master().playlists.forEach(function (variant) { 46646 var variantCodecs = { 46647 codecCount: 2, 46648 videoCodec: null 46649 }; 46650 46651 if (variant.attributes.CODECS) { 46652 variantCodecs = parseCodecs(variant.attributes.CODECS); 46653 } 46654 46655 // if the streams differ in the presence or absence of audio or 46656 // video, they are incompatible 46657 if (variantCodecs.codecCount !== codecCount) { 46658 variant.excludeUntil = Infinity; 46659 } 46660 46661 // if h.264 is specified on the current playlist, some flavor of 46662 // it must be specified on all compatible variants 46663 if (variantCodecs.videoCodec !== videoCodec) { 46664 variant.excludeUntil = Infinity; 46665 } 46666 }); 46667 } 46668 }, { 46669 key: 'updateAdCues_', 46670 value: function updateAdCues_(media) { 46671 var offset = 0; 46672 var seekable$$1 = this.seekable(); 46673 46674 if (seekable$$1.length) { 46675 offset = seekable$$1.start(0); 46676 } 46677 46678 updateAdCues(media, this.cueTagsTrack_, offset); 46679 } 46680 46681 /** 46682 * Calculates the desired forward buffer length based on current time 46683 * 46684 * @return {Number} Desired forward buffer length in seconds 46685 */ 46686 46687 }, { 46688 key: 'goalBufferLength', 46689 value: function goalBufferLength() { 46690 var currentTime = this.tech_.currentTime(); 46691 var initial = Config.GOAL_BUFFER_LENGTH; 46692 var rate = Config.GOAL_BUFFER_LENGTH_RATE; 46693 var max = Math.max(initial, Config.MAX_GOAL_BUFFER_LENGTH); 46694 46695 return Math.min(initial + currentTime * rate, max); 46696 } 46697 46698 /** 46699 * Calculates the desired buffer low water line based on current time 46700 * 46701 * @return {Number} Desired buffer low water line in seconds 46702 */ 46703 46704 }, { 46705 key: 'bufferLowWaterLine', 46706 value: function bufferLowWaterLine() { 46707 var currentTime = this.tech_.currentTime(); 46708 var initial = Config.BUFFER_LOW_WATER_LINE; 46709 var rate = Config.BUFFER_LOW_WATER_LINE_RATE; 46710 var max = Math.max(initial, Config.MAX_BUFFER_LOW_WATER_LINE); 46711 46712 return Math.min(initial + currentTime * rate, max); 46713 } 46714 }]); 46715 return MasterPlaylistController; 46716 }(videojs$1.EventTarget); 46717 46718 /** 46719 * Returns a function that acts as the Enable/disable playlist function. 46720 * 46721 * @param {PlaylistLoader} loader - The master playlist loader 46722 * @param {String} playlistUri - uri of the playlist 46723 * @param {Function} changePlaylistFn - A function to be called after a 46724 * playlist's enabled-state has been changed. Will NOT be called if a 46725 * playlist's enabled-state is unchanged 46726 * @param {Boolean=} enable - Value to set the playlist enabled-state to 46727 * or if undefined returns the current enabled-state for the playlist 46728 * @return {Function} Function for setting/getting enabled 46729 */ 46730 var enableFunction = function enableFunction(loader, playlistUri, changePlaylistFn) { 46731 return function (enable) { 46732 var playlist = loader.master.playlists[playlistUri]; 46733 var incompatible = isIncompatible(playlist); 46734 var currentlyEnabled = isEnabled(playlist); 46735 46736 if (typeof enable === 'undefined') { 46737 return currentlyEnabled; 46738 } 46739 46740 if (enable) { 46741 delete playlist.disabled; 46742 } else { 46743 playlist.disabled = true; 46744 } 46745 46746 if (enable !== currentlyEnabled && !incompatible) { 46747 // Ensure the outside world knows about our changes 46748 changePlaylistFn(); 46749 if (enable) { 46750 loader.trigger('renditionenabled'); 46751 } else { 46752 loader.trigger('renditiondisabled'); 46753 } 46754 } 46755 return enable; 46756 }; 46757 }; 46758 46759 /** 46760 * The representation object encapsulates the publicly visible information 46761 * in a media playlist along with a setter/getter-type function (enabled) 46762 * for changing the enabled-state of a particular playlist entry 46763 * 46764 * @class Representation 46765 */ 46766 46767 var Representation = function Representation(hlsHandler, playlist, id) { 46768 classCallCheck$3(this, Representation); 46769 46770 // Get a reference to a bound version of fastQualityChange_ 46771 var fastChangeFunction = hlsHandler.masterPlaylistController_.fastQualityChange_.bind(hlsHandler.masterPlaylistController_); 46772 46773 // some playlist attributes are optional 46774 if (playlist.attributes.RESOLUTION) { 46775 var resolution = playlist.attributes.RESOLUTION; 46776 46777 this.width = resolution.width; 46778 this.height = resolution.height; 46779 } 46780 46781 this.bandwidth = playlist.attributes.BANDWIDTH; 46782 46783 // The id is simply the ordinality of the media playlist 46784 // within the master playlist 46785 this.id = id; 46786 46787 // Partially-apply the enableFunction to create a playlist- 46788 // specific variant 46789 this.enabled = enableFunction(hlsHandler.playlists, playlist.uri, fastChangeFunction); 46790 }; 46791 46792 /** 46793 * A mixin function that adds the `representations` api to an instance 46794 * of the HlsHandler class 46795 * @param {HlsHandler} hlsHandler - An instance of HlsHandler to add the 46796 * representation API into 46797 */ 46798 46799 var renditionSelectionMixin = function renditionSelectionMixin(hlsHandler) { 46800 var playlists = hlsHandler.playlists; 46801 46802 // Add a single API-specific function to the HlsHandler instance 46803 hlsHandler.representations = function () { 46804 return playlists.master.playlists.filter(function (media) { 46805 return !isIncompatible(media); 46806 }).map(function (e, i) { 46807 return new Representation(hlsHandler, e, e.uri); 46808 }); 46809 }; 46810 }; 46811 46812 /** 46813 * @file playback-watcher.js 46814 * 46815 * Playback starts, and now my watch begins. It shall not end until my death. I shall 46816 * take no wait, hold no uncleared timeouts, father no bad seeks. I shall wear no crowns 46817 * and win no glory. I shall live and die at my post. I am the corrector of the underflow. 46818 * I am the watcher of gaps. I am the shield that guards the realms of seekable. I pledge 46819 * my life and honor to the Playback Watch, for this Player and all the Players to come. 46820 */ 46821 46822 // Set of events that reset the playback-watcher time check logic and clear the timeout 46823 var timerCancelEvents = ['seeking', 'seeked', 'pause', 'playing', 'error']; 46824 46825 /** 46826 * @class PlaybackWatcher 46827 */ 46828 46829 var PlaybackWatcher = function () { 46830 /** 46831 * Represents an PlaybackWatcher object. 46832 * @constructor 46833 * @param {object} options an object that includes the tech and settings 46834 */ 46835 function PlaybackWatcher(options) { 46836 var _this = this; 46837 46838 classCallCheck$3(this, PlaybackWatcher); 46839 46840 this.tech_ = options.tech; 46841 this.seekable = options.seekable; 46842 46843 this.consecutiveUpdates = 0; 46844 this.lastRecordedTime = null; 46845 this.timer_ = null; 46846 this.checkCurrentTimeTimeout_ = null; 46847 this.logger_ = logger('PlaybackWatcher'); 46848 46849 this.logger_('initialize'); 46850 46851 var canPlayHandler = function canPlayHandler() { 46852 return _this.monitorCurrentTime_(); 46853 }; 46854 var waitingHandler = function waitingHandler() { 46855 return _this.techWaiting_(); 46856 }; 46857 var cancelTimerHandler = function cancelTimerHandler() { 46858 return _this.cancelTimer_(); 46859 }; 46860 var fixesBadSeeksHandler = function fixesBadSeeksHandler() { 46861 return _this.fixesBadSeeks_(); 46862 }; 46863 46864 this.tech_.on('seekablechanged', fixesBadSeeksHandler); 46865 this.tech_.on('waiting', waitingHandler); 46866 this.tech_.on(timerCancelEvents, cancelTimerHandler); 46867 this.tech_.on('canplay', canPlayHandler); 46868 46869 // Define the dispose function to clean up our events 46870 this.dispose = function () { 46871 _this.logger_('dispose'); 46872 _this.tech_.off('seekablechanged', fixesBadSeeksHandler); 46873 _this.tech_.off('waiting', waitingHandler); 46874 _this.tech_.off(timerCancelEvents, cancelTimerHandler); 46875 _this.tech_.off('canplay', canPlayHandler); 46876 if (_this.checkCurrentTimeTimeout_) { 46877 window_1.clearTimeout(_this.checkCurrentTimeTimeout_); 46878 } 46879 _this.cancelTimer_(); 46880 }; 46881 } 46882 46883 /** 46884 * Periodically check current time to see if playback stopped 46885 * 46886 * @private 46887 */ 46888 46889 createClass$2(PlaybackWatcher, [{ 46890 key: 'monitorCurrentTime_', 46891 value: function monitorCurrentTime_() { 46892 this.checkCurrentTime_(); 46893 46894 if (this.checkCurrentTimeTimeout_) { 46895 window_1.clearTimeout(this.checkCurrentTimeTimeout_); 46896 } 46897 46898 // 42 = 24 fps // 250 is what Webkit uses // FF uses 15 46899 this.checkCurrentTimeTimeout_ = window_1.setTimeout(this.monitorCurrentTime_.bind(this), 250); 46900 } 46901 46902 /** 46903 * The purpose of this function is to emulate the "waiting" event on 46904 * browsers that do not emit it when they are waiting for more 46905 * data to continue playback 46906 * 46907 * @private 46908 */ 46909 46910 }, { 46911 key: 'checkCurrentTime_', 46912 value: function checkCurrentTime_() { 46913 if (this.tech_.seeking() && this.fixesBadSeeks_()) { 46914 this.consecutiveUpdates = 0; 46915 this.lastRecordedTime = this.tech_.currentTime(); 46916 return; 46917 } 46918 46919 if (this.tech_.paused() || this.tech_.seeking()) { 46920 return; 46921 } 46922 46923 var currentTime = this.tech_.currentTime(); 46924 var buffered = this.tech_.buffered(); 46925 46926 if (this.lastRecordedTime === currentTime && (!buffered.length || currentTime + SAFE_TIME_DELTA >= buffered.end(buffered.length - 1))) { 46927 // If current time is at the end of the final buffered region, then any playback 46928 // stall is most likely caused by buffering in a low bandwidth environment. The tech 46929 // should fire a `waiting` event in this scenario, but due to browser and tech 46930 // inconsistencies. Calling `techWaiting_` here allows us to simulate 46931 // responding to a native `waiting` event when the tech fails to emit one. 46932 return this.techWaiting_(); 46933 } 46934 46935 if (this.consecutiveUpdates >= 5 && currentTime === this.lastRecordedTime) { 46936 this.consecutiveUpdates++; 46937 this.waiting_(); 46938 } else if (currentTime === this.lastRecordedTime) { 46939 this.consecutiveUpdates++; 46940 } else { 46941 this.consecutiveUpdates = 0; 46942 this.lastRecordedTime = currentTime; 46943 } 46944 } 46945 46946 /** 46947 * Cancels any pending timers and resets the 'timeupdate' mechanism 46948 * designed to detect that we are stalled 46949 * 46950 * @private 46951 */ 46952 46953 }, { 46954 key: 'cancelTimer_', 46955 value: function cancelTimer_() { 46956 this.consecutiveUpdates = 0; 46957 46958 if (this.timer_) { 46959 this.logger_('cancelTimer_'); 46960 clearTimeout(this.timer_); 46961 } 46962 46963 this.timer_ = null; 46964 } 46965 46966 /** 46967 * Fixes situations where there's a bad seek 46968 * 46969 * @return {Boolean} whether an action was taken to fix the seek 46970 * @private 46971 */ 46972 46973 }, { 46974 key: 'fixesBadSeeks_', 46975 value: function fixesBadSeeks_() { 46976 var seeking = this.tech_.seeking(); 46977 var seekable = this.seekable(); 46978 var currentTime = this.tech_.currentTime(); 46979 var seekTo = void 0; 46980 46981 if (seeking && this.afterSeekableWindow_(seekable, currentTime)) { 46982 var seekableEnd = seekable.end(seekable.length - 1); 46983 46984 // sync to live point (if VOD, our seekable was updated and we're simply adjusting) 46985 seekTo = seekableEnd; 46986 } 46987 46988 if (seeking && this.beforeSeekableWindow_(seekable, currentTime)) { 46989 var seekableStart = seekable.start(0); 46990 46991 // sync to the beginning of the live window 46992 // provide a buffer of .1 seconds to handle rounding/imprecise numbers 46993 seekTo = seekableStart + SAFE_TIME_DELTA; 46994 } 46995 46996 if (typeof seekTo !== 'undefined') { 46997 this.logger_('Trying to seek outside of seekable at time ' + currentTime + ' with ' + ('seekable range ' + printableRange(seekable) + '. Seeking to ') + (seekTo + '.')); 46998 46999 this.tech_.setCurrentTime(seekTo); 47000 return true; 47001 } 47002 47003 return false; 47004 } 47005 47006 /** 47007 * Handler for situations when we determine the player is waiting. 47008 * 47009 * @private 47010 */ 47011 47012 }, { 47013 key: 'waiting_', 47014 value: function waiting_() { 47015 if (this.techWaiting_()) { 47016 return; 47017 } 47018 47019 // All tech waiting checks failed. Use last resort correction 47020 var currentTime = this.tech_.currentTime(); 47021 var buffered = this.tech_.buffered(); 47022 var currentRange = findRange(buffered, currentTime); 47023 47024 // Sometimes the player can stall for unknown reasons within a contiguous buffered 47025 // region with no indication that anything is amiss (seen in Firefox). Seeking to 47026 // currentTime is usually enough to kickstart the player. This checks that the player 47027 // is currently within a buffered region before attempting a corrective seek. 47028 // Chrome does not appear to continue `timeupdate` events after a `waiting` event 47029 // until there is ~ 3 seconds of forward buffer available. PlaybackWatcher should also 47030 // make sure there is ~3 seconds of forward buffer before taking any corrective action 47031 // to avoid triggering an `unknownwaiting` event when the network is slow. 47032 if (currentRange.length && currentTime + 3 <= currentRange.end(0)) { 47033 this.cancelTimer_(); 47034 this.tech_.setCurrentTime(currentTime); 47035 47036 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.'); 47037 47038 // unknown waiting corrections may be useful for monitoring QoS 47039 this.tech_.trigger({ type: 'usage', name: 'hls-unknown-waiting' }); 47040 return; 47041 } 47042 } 47043 47044 /** 47045 * Handler for situations when the tech fires a `waiting` event 47046 * 47047 * @return {Boolean} 47048 * True if an action (or none) was needed to correct the waiting. False if no 47049 * checks passed 47050 * @private 47051 */ 47052 47053 }, { 47054 key: 'techWaiting_', 47055 value: function techWaiting_() { 47056 var seekable = this.seekable(); 47057 var currentTime = this.tech_.currentTime(); 47058 47059 if (this.tech_.seeking() && this.fixesBadSeeks_()) { 47060 // Tech is seeking or bad seek fixed, no action needed 47061 return true; 47062 } 47063 47064 if (this.tech_.seeking() || this.timer_ !== null) { 47065 // Tech is seeking or already waiting on another action, no action needed 47066 return true; 47067 } 47068 47069 if (this.beforeSeekableWindow_(seekable, currentTime)) { 47070 var livePoint = seekable.end(seekable.length - 1); 47071 47072 this.logger_('Fell out of live window at time ' + currentTime + '. Seeking to ' + ('live point (seekable end) ' + livePoint)); 47073 this.cancelTimer_(); 47074 this.tech_.setCurrentTime(livePoint); 47075 47076 // live window resyncs may be useful for monitoring QoS 47077 this.tech_.trigger({ type: 'usage', name: 'hls-live-resync' }); 47078 return true; 47079 } 47080 47081 var buffered = this.tech_.buffered(); 47082 var nextRange = findNextRange(buffered, currentTime); 47083 47084 if (this.videoUnderflow_(nextRange, buffered, currentTime)) { 47085 // Even though the video underflowed and was stuck in a gap, the audio overplayed 47086 // the gap, leading currentTime into a buffered range. Seeking to currentTime 47087 // allows the video to catch up to the audio position without losing any audio 47088 // (only suffering ~3 seconds of frozen video and a pause in audio playback). 47089 this.cancelTimer_(); 47090 this.tech_.setCurrentTime(currentTime); 47091 47092 // video underflow may be useful for monitoring QoS 47093 this.tech_.trigger({ type: 'usage', name: 'hls-video-underflow' }); 47094 return true; 47095 } 47096 47097 // check for gap 47098 if (nextRange.length > 0) { 47099 var difference = nextRange.start(0) - currentTime; 47100 47101 this.logger_('Stopped at ' + currentTime + ', setting timer for ' + difference + ', seeking ' + ('to ' + nextRange.start(0))); 47102 47103 this.timer_ = setTimeout(this.skipTheGap_.bind(this), difference * 1000, currentTime); 47104 return true; 47105 } 47106 47107 // All checks failed. Returning false to indicate failure to correct waiting 47108 return false; 47109 } 47110 }, { 47111 key: 'afterSeekableWindow_', 47112 value: function afterSeekableWindow_(seekable, currentTime) { 47113 if (!seekable.length) { 47114 // we can't make a solid case if there's no seekable, default to false 47115 return false; 47116 } 47117 47118 if (currentTime > seekable.end(seekable.length - 1) + SAFE_TIME_DELTA) { 47119 return true; 47120 } 47121 47122 return false; 47123 } 47124 }, { 47125 key: 'beforeSeekableWindow_', 47126 value: function beforeSeekableWindow_(seekable, currentTime) { 47127 if (seekable.length && 47128 // can't fall before 0 and 0 seekable start identifies VOD stream 47129 seekable.start(0) > 0 && currentTime < seekable.start(0) - SAFE_TIME_DELTA) { 47130 return true; 47131 } 47132 47133 return false; 47134 } 47135 }, { 47136 key: 'videoUnderflow_', 47137 value: function videoUnderflow_(nextRange, buffered, currentTime) { 47138 if (nextRange.length === 0) { 47139 // Even if there is no available next range, there is still a possibility we are 47140 // stuck in a gap due to video underflow. 47141 var gap = this.gapFromVideoUnderflow_(buffered, currentTime); 47142 47143 if (gap) { 47144 this.logger_('Encountered a gap in video from ' + gap.start + ' to ' + gap.end + '. ' + ('Seeking to current time ' + currentTime)); 47145 47146 return true; 47147 } 47148 } 47149 47150 return false; 47151 } 47152 47153 /** 47154 * Timer callback. If playback still has not proceeded, then we seek 47155 * to the start of the next buffered region. 47156 * 47157 * @private 47158 */ 47159 47160 }, { 47161 key: 'skipTheGap_', 47162 value: function skipTheGap_(scheduledCurrentTime) { 47163 var buffered = this.tech_.buffered(); 47164 var currentTime = this.tech_.currentTime(); 47165 var nextRange = findNextRange(buffered, currentTime); 47166 47167 this.cancelTimer_(); 47168 47169 if (nextRange.length === 0 || currentTime !== scheduledCurrentTime) { 47170 return; 47171 } 47172 47173 this.logger_('skipTheGap_:', 'currentTime:', currentTime, 'scheduled currentTime:', scheduledCurrentTime, 'nextRange start:', nextRange.start(0)); 47174 47175 // only seek if we still have not played 47176 this.tech_.setCurrentTime(nextRange.start(0) + TIME_FUDGE_FACTOR); 47177 47178 this.tech_.trigger({ type: 'usage', name: 'hls-gap-skip' }); 47179 } 47180 }, { 47181 key: 'gapFromVideoUnderflow_', 47182 value: function gapFromVideoUnderflow_(buffered, currentTime) { 47183 // At least in Chrome, if there is a gap in the video buffer, the audio will continue 47184 // playing for ~3 seconds after the video gap starts. This is done to account for 47185 // video buffer underflow/underrun (note that this is not done when there is audio 47186 // buffer underflow/underrun -- in that case the video will stop as soon as it 47187 // encounters the gap, as audio stalls are more noticeable/jarring to a user than 47188 // video stalls). The player's time will reflect the playthrough of audio, so the 47189 // time will appear as if we are in a buffered region, even if we are stuck in a 47190 // "gap." 47191 // 47192 // Example: 47193 // video buffer: 0 => 10.1, 10.2 => 20 47194 // audio buffer: 0 => 20 47195 // overall buffer: 0 => 10.1, 10.2 => 20 47196 // current time: 13 47197 // 47198 // Chrome's video froze at 10 seconds, where the video buffer encountered the gap, 47199 // however, the audio continued playing until it reached ~3 seconds past the gap 47200 // (13 seconds), at which point it stops as well. Since current time is past the 47201 // gap, findNextRange will return no ranges. 47202 // 47203 // To check for this issue, we see if there is a gap that starts somewhere within 47204 // a 3 second range (3 seconds +/- 1 second) back from our current time. 47205 var gaps = findGaps(buffered); 47206 47207 for (var i = 0; i < gaps.length; i++) { 47208 var start = gaps.start(i); 47209 var end = gaps.end(i); 47210 47211 // gap is starts no more than 4 seconds back 47212 if (currentTime - start < 4 && currentTime - start > 2) { 47213 return { 47214 start: start, 47215 end: end 47216 }; 47217 } 47218 } 47219 47220 return null; 47221 } 47222 }]); 47223 return PlaybackWatcher; 47224 }(); 47225 47226 var defaultOptions = { 47227 errorInterval: 30, 47228 getSource: function getSource(next) { 47229 var tech = this.tech({ IWillNotUseThisInPlugins: true }); 47230 var sourceObj = tech.currentSource_; 47231 47232 return next(sourceObj); 47233 } 47234 }; 47235 47236 /** 47237 * Main entry point for the plugin 47238 * 47239 * @param {Player} player a reference to a videojs Player instance 47240 * @param {Object} [options] an object with plugin options 47241 * @private 47242 */ 47243 var initPlugin = function initPlugin(player, options) { 47244 var lastCalled = 0; 47245 var seekTo = 0; 47246 var localOptions = videojs$1.mergeOptions(defaultOptions, options); 47247 47248 player.ready(function () { 47249 player.trigger({ type: 'usage', name: 'hls-error-reload-initialized' }); 47250 }); 47251 47252 /** 47253 * Player modifications to perform that must wait until `loadedmetadata` 47254 * has been triggered 47255 * 47256 * @private 47257 */ 47258 var loadedMetadataHandler = function loadedMetadataHandler() { 47259 if (seekTo) { 47260 player.currentTime(seekTo); 47261 } 47262 }; 47263 47264 /** 47265 * Set the source on the player element, play, and seek if necessary 47266 * 47267 * @param {Object} sourceObj An object specifying the source url and mime-type to play 47268 * @private 47269 */ 47270 var setSource = function setSource(sourceObj) { 47271 if (sourceObj === null || sourceObj === undefined) { 47272 return; 47273 } 47274 seekTo = player.duration() !== Infinity && player.currentTime() || 0; 47275 47276 player.one('loadedmetadata', loadedMetadataHandler); 47277 47278 player.src(sourceObj); 47279 player.trigger({ type: 'usage', name: 'hls-error-reload' }); 47280 player.play(); 47281 }; 47282 47283 /** 47284 * Attempt to get a source from either the built-in getSource function 47285 * or a custom function provided via the options 47286 * 47287 * @private 47288 */ 47289 var errorHandler = function errorHandler() { 47290 // Do not attempt to reload the source if a source-reload occurred before 47291 // 'errorInterval' time has elapsed since the last source-reload 47292 if (Date.now() - lastCalled < localOptions.errorInterval * 1000) { 47293 player.trigger({ type: 'usage', name: 'hls-error-reload-canceled' }); 47294 return; 47295 } 47296 47297 if (!localOptions.getSource || typeof localOptions.getSource !== 'function') { 47298 videojs$1.log.error('ERROR: reloadSourceOnError - The option getSource must be a function!'); 47299 return; 47300 } 47301 lastCalled = Date.now(); 47302 47303 return localOptions.getSource.call(player, setSource); 47304 }; 47305 47306 /** 47307 * Unbind any event handlers that were bound by the plugin 47308 * 47309 * @private 47310 */ 47311 var cleanupEvents = function cleanupEvents() { 47312 player.off('loadedmetadata', loadedMetadataHandler); 47313 player.off('error', errorHandler); 47314 player.off('dispose', cleanupEvents); 47315 }; 47316 47317 /** 47318 * Cleanup before re-initializing the plugin 47319 * 47320 * @param {Object} [newOptions] an object with plugin options 47321 * @private 47322 */ 47323 var reinitPlugin = function reinitPlugin(newOptions) { 47324 cleanupEvents(); 47325 initPlugin(player, newOptions); 47326 }; 47327 47328 player.on('error', errorHandler); 47329 player.on('dispose', cleanupEvents); 47330 47331 // Overwrite the plugin function so that we can correctly cleanup before 47332 // initializing the plugin 47333 player.reloadSourceOnError = reinitPlugin; 47334 }; 47335 47336 /** 47337 * Reload the source when an error is detected as long as there 47338 * wasn't an error previously within the last 30 seconds 47339 * 47340 * @param {Object} [options] an object with plugin options 47341 */ 47342 var reloadSourceOnError = function reloadSourceOnError(options) { 47343 initPlugin(this, options); 47344 }; 47345 47346 var version$3 = "1.1.0"; 47347 47348 /** 47349 * @file videojs-http-streaming.js 47350 * 47351 * The main file for the HLS project. 47352 * License: https://github.com/videojs/videojs-http-streaming/blob/master/LICENSE 47353 */ 47354 47355 var Hls$1 = { 47356 PlaylistLoader: PlaylistLoader, 47357 Playlist: Playlist, 47358 Decrypter: Decrypter, 47359 AsyncStream: AsyncStream, 47360 decrypt: decrypt, 47361 utils: utils, 47362 47363 STANDARD_PLAYLIST_SELECTOR: lastBandwidthSelector, 47364 INITIAL_PLAYLIST_SELECTOR: lowestBitrateCompatibleVariantSelector, 47365 comparePlaylistBandwidth: comparePlaylistBandwidth, 47366 comparePlaylistResolution: comparePlaylistResolution, 47367 47368 xhr: xhrFactory() 47369 }; 47370 47371 // 0.5 MB/s 47372 var INITIAL_BANDWIDTH = 4194304; 47373 47374 // Define getter/setters for config properites 47375 ['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) { 47376 Object.defineProperty(Hls$1, prop, { 47377 get: function get$$1() { 47378 videojs$1.log.warn('using Hls.' + prop + ' is UNSAFE be sure you know what you are doing'); 47379 return Config[prop]; 47380 }, 47381 set: function set$$1(value) { 47382 videojs$1.log.warn('using Hls.' + prop + ' is UNSAFE be sure you know what you are doing'); 47383 47384 if (typeof value !== 'number' || value < 0) { 47385 videojs$1.log.warn('value of Hls.' + prop + ' must be greater than or equal to 0'); 47386 return; 47387 } 47388 47389 Config[prop] = value; 47390 } 47391 }); 47392 }); 47393 47394 var simpleTypeFromSourceType = function simpleTypeFromSourceType(type) { 47395 var mpegurlRE = /^(audio|video|application)\/(x-|vnd\.apple\.)?mpegurl/i; 47396 47397 if (mpegurlRE.test(type)) { 47398 return 'hls'; 47399 } 47400 47401 var dashRE = /^application\/dash\+xml/i; 47402 47403 if (dashRE.test(type)) { 47404 return 'dash'; 47405 } 47406 47407 return null; 47408 }; 47409 47410 /** 47411 * Updates the selectedIndex of the QualityLevelList when a mediachange happens in hls. 47412 * 47413 * @param {QualityLevelList} qualityLevels The QualityLevelList to update. 47414 * @param {PlaylistLoader} playlistLoader PlaylistLoader containing the new media info. 47415 * @function handleHlsMediaChange 47416 */ 47417 var handleHlsMediaChange = function handleHlsMediaChange(qualityLevels, playlistLoader) { 47418 var newPlaylist = playlistLoader.media(); 47419 var selectedIndex = -1; 47420 47421 for (var i = 0; i < qualityLevels.length; i++) { 47422 if (qualityLevels[i].id === newPlaylist.uri) { 47423 selectedIndex = i; 47424 break; 47425 } 47426 } 47427 47428 qualityLevels.selectedIndex_ = selectedIndex; 47429 qualityLevels.trigger({ 47430 selectedIndex: selectedIndex, 47431 type: 'change' 47432 }); 47433 }; 47434 47435 /** 47436 * Adds quality levels to list once playlist metadata is available 47437 * 47438 * @param {QualityLevelList} qualityLevels The QualityLevelList to attach events to. 47439 * @param {Object} hls Hls object to listen to for media events. 47440 * @function handleHlsLoadedMetadata 47441 */ 47442 var handleHlsLoadedMetadata = function handleHlsLoadedMetadata(qualityLevels, hls) { 47443 hls.representations().forEach(function (rep) { 47444 qualityLevels.addQualityLevel(rep); 47445 }); 47446 handleHlsMediaChange(qualityLevels, hls.playlists); 47447 }; 47448 47449 // HLS is a source handler, not a tech. Make sure attempts to use it 47450 // as one do not cause exceptions. 47451 Hls$1.canPlaySource = function () { 47452 return videojs$1.log.warn('HLS is no longer a tech. Please remove it from ' + 'your player\'s techOrder.'); 47453 }; 47454 47455 var emeKeySystems = function emeKeySystems(keySystemOptions, videoPlaylist, audioPlaylist) { 47456 if (!keySystemOptions) { 47457 return keySystemOptions; 47458 } 47459 47460 // upsert the content types based on the selected playlist 47461 var keySystemContentTypes = {}; 47462 47463 for (var keySystem in keySystemOptions) { 47464 keySystemContentTypes[keySystem] = { 47465 audioContentType: 'audio/mp4; codecs="' + audioPlaylist.attributes.CODECS + '"', 47466 videoContentType: 'video/mp4; codecs="' + videoPlaylist.attributes.CODECS + '"' 47467 }; 47468 47469 if (videoPlaylist.contentProtection && videoPlaylist.contentProtection[keySystem] && videoPlaylist.contentProtection[keySystem].pssh) { 47470 keySystemContentTypes[keySystem].pssh = videoPlaylist.contentProtection[keySystem].pssh; 47471 } 47472 47473 // videojs-contrib-eme accepts the option of specifying: 'com.some.cdm': 'url' 47474 // so we need to prevent overwriting the URL entirely 47475 if (typeof keySystemOptions[keySystem] === 'string') { 47476 keySystemContentTypes[keySystem].url = keySystemOptions[keySystem]; 47477 } 47478 } 47479 47480 return videojs$1.mergeOptions(keySystemOptions, keySystemContentTypes); 47481 }; 47482 47483 var setupEmeOptions = function setupEmeOptions(hlsHandler) { 47484 if (hlsHandler.options_.sourceType !== 'dash') { 47485 return; 47486 } 47487 var player = videojs$1.players[hlsHandler.tech_.options_.playerId]; 47488 47489 if (player.eme) { 47490 var sourceOptions = emeKeySystems(hlsHandler.source_.keySystems, hlsHandler.playlists.media(), hlsHandler.masterPlaylistController_.mediaTypes_.AUDIO.activePlaylistLoader.media()); 47491 47492 if (sourceOptions) { 47493 player.currentSource().keySystems = sourceOptions; 47494 } 47495 } 47496 }; 47497 47498 /** 47499 * Whether the browser has built-in HLS support. 47500 */ 47501 Hls$1.supportsNativeHls = function () { 47502 var video = document_1.createElement('video'); 47503 47504 // native HLS is definitely not supported if HTML5 video isn't 47505 if (!videojs$1.getTech('Html5').isSupported()) { 47506 return false; 47507 } 47508 47509 // HLS manifests can go by many mime-types 47510 var canPlay = [ 47511 // Apple santioned 47512 'application/vnd.apple.mpegurl', 47513 // Apple sanctioned for backwards compatibility 47514 'audio/mpegurl', 47515 // Very common 47516 'audio/x-mpegurl', 47517 // Very common 47518 'application/x-mpegurl', 47519 // Included for completeness 47520 'video/x-mpegurl', 'video/mpegurl', 'application/mpegurl']; 47521 47522 return canPlay.some(function (canItPlay) { 47523 return (/maybe|probably/i.test(video.canPlayType(canItPlay)) 47524 ); 47525 }); 47526 }(); 47527 47528 Hls$1.supportsNativeDash = function () { 47529 if (!videojs$1.getTech('Html5').isSupported()) { 47530 return false; 47531 } 47532 47533 return (/maybe|probably/i.test(document_1.createElement('video').canPlayType('application/dash+xml')) 47534 ); 47535 }(); 47536 47537 Hls$1.supportsTypeNatively = function (type) { 47538 if (type === 'hls') { 47539 return Hls$1.supportsNativeHls; 47540 } 47541 47542 if (type === 'dash') { 47543 return Hls$1.supportsNativeDash; 47544 } 47545 47546 return false; 47547 }; 47548 47549 /** 47550 * HLS is a source handler, not a tech. Make sure attempts to use it 47551 * as one do not cause exceptions. 47552 */ 47553 Hls$1.isSupported = function () { 47554 return videojs$1.log.warn('HLS is no longer a tech. Please remove it from ' + 'your player\'s techOrder.'); 47555 }; 47556 47557 var Component$1 = videojs$1.getComponent('Component'); 47558 47559 /** 47560 * The Hls Handler object, where we orchestrate all of the parts 47561 * of HLS to interact with video.js 47562 * 47563 * @class HlsHandler 47564 * @extends videojs.Component 47565 * @param {Object} source the soruce object 47566 * @param {Tech} tech the parent tech object 47567 * @param {Object} options optional and required options 47568 */ 47569 47570 var HlsHandler = function (_Component) { 47571 inherits$3(HlsHandler, _Component); 47572 47573 function HlsHandler(source, tech, options) { 47574 classCallCheck$3(this, HlsHandler); 47575 47576 // tech.player() is deprecated but setup a reference to HLS for 47577 // backwards-compatibility 47578 var _this = possibleConstructorReturn$3(this, (HlsHandler.__proto__ || Object.getPrototypeOf(HlsHandler)).call(this, tech, options.hls)); 47579 47580 if (tech.options_ && tech.options_.playerId) { 47581 var _player = videojs$1(tech.options_.playerId); 47582 47583 if (!_player.hasOwnProperty('hls')) { 47584 Object.defineProperty(_player, 'hls', { 47585 get: function get$$1() { 47586 videojs$1.log.warn('player.hls is deprecated. Use player.tech_.hls instead.'); 47587 tech.trigger({ type: 'usage', name: 'hls-player-access' }); 47588 return _this; 47589 } 47590 }); 47591 } 47592 47593 // Set up a reference to the HlsHandler from player.vhs. This allows users to start 47594 // migrating from player.tech_.hls... to player.vhs... for API access. Although this 47595 // isn't the most appropriate form of reference for video.js (since all APIs should 47596 // be provided through core video.js), it is a common pattern for plugins, and vhs 47597 // will act accordingly. 47598 _player.vhs = _this; 47599 // deprecated, for backwards compatibility 47600 _player.dash = _this; 47601 } 47602 47603 _this.tech_ = tech; 47604 _this.source_ = source; 47605 _this.stats = {}; 47606 _this.ignoreNextSeekingEvent_ = false; 47607 _this.setOptions_(); 47608 47609 if (_this.options_.overrideNative && tech.overrideNativeAudioTracks && tech.overrideNativeVideoTracks) { 47610 tech.overrideNativeAudioTracks(true); 47611 tech.overrideNativeVideoTracks(true); 47612 } else if (_this.options_.overrideNative && (tech.featuresNativeVideoTracks || tech.featuresNativeAudioTracks)) { 47613 // overriding native HLS only works if audio tracks have been emulated 47614 // error early if we're misconfigured 47615 throw new Error('Overriding native HLS requires emulated tracks. ' + 'See https://git.io/vMpjB'); 47616 } 47617 47618 // listen for fullscreenchange events for this player so that we 47619 // can adjust our quality selection quickly 47620 _this.on(document_1, ['fullscreenchange', 'webkitfullscreenchange', 'mozfullscreenchange', 'MSFullscreenChange'], function (event) { 47621 var fullscreenElement = document_1.fullscreenElement || document_1.webkitFullscreenElement || document_1.mozFullScreenElement || document_1.msFullscreenElement; 47622 47623 if (fullscreenElement && fullscreenElement.contains(_this.tech_.el())) { 47624 _this.masterPlaylistController_.fastQualityChange_(); 47625 } 47626 }); 47627 47628 _this.on(_this.tech_, 'seeking', function () { 47629 if (this.ignoreNextSeekingEvent_) { 47630 this.ignoreNextSeekingEvent_ = false; 47631 return; 47632 } 47633 47634 this.setCurrentTime(this.tech_.currentTime()); 47635 }); 47636 _this.on(_this.tech_, 'error', function () { 47637 if (this.masterPlaylistController_) { 47638 this.masterPlaylistController_.pauseLoading(); 47639 } 47640 }); 47641 47642 _this.on(_this.tech_, 'play', _this.play); 47643 return _this; 47644 } 47645 47646 createClass$2(HlsHandler, [{ 47647 key: 'setOptions_', 47648 value: function setOptions_() { 47649 var _this2 = this; 47650 47651 // defaults 47652 this.options_.withCredentials = this.options_.withCredentials || false; 47653 47654 if (typeof this.options_.blacklistDuration !== 'number') { 47655 this.options_.blacklistDuration = 5 * 60; 47656 } 47657 47658 // start playlist selection at a reasonable bandwidth for 47659 // broadband internet (0.5 MB/s) or mobile (0.0625 MB/s) 47660 if (typeof this.options_.bandwidth !== 'number') { 47661 this.options_.bandwidth = INITIAL_BANDWIDTH; 47662 } 47663 47664 // If the bandwidth number is unchanged from the initial setting 47665 // then this takes precedence over the enableLowInitialPlaylist option 47666 this.options_.enableLowInitialPlaylist = this.options_.enableLowInitialPlaylist && this.options_.bandwidth === INITIAL_BANDWIDTH; 47667 47668 // grab options passed to player.src 47669 ['withCredentials', 'bandwidth'].forEach(function (option) { 47670 if (typeof _this2.source_[option] !== 'undefined') { 47671 _this2.options_[option] = _this2.source_[option]; 47672 } 47673 }); 47674 47675 this.bandwidth = this.options_.bandwidth; 47676 } 47677 /** 47678 * called when player.src gets called, handle a new source 47679 * 47680 * @param {Object} src the source object to handle 47681 */ 47682 47683 }, { 47684 key: 'src', 47685 value: function src(_src, type) { 47686 var _this3 = this; 47687 47688 // do nothing if the src is falsey 47689 if (!_src) { 47690 return; 47691 } 47692 this.setOptions_(); 47693 // add master playlist controller options 47694 this.options_.url = this.source_.src; 47695 this.options_.tech = this.tech_; 47696 this.options_.externHls = Hls$1; 47697 this.options_.sourceType = simpleTypeFromSourceType(type); 47698 47699 this.masterPlaylistController_ = new MasterPlaylistController(this.options_); 47700 this.playbackWatcher_ = new PlaybackWatcher(videojs$1.mergeOptions(this.options_, { 47701 seekable: function seekable$$1() { 47702 return _this3.seekable(); 47703 } 47704 })); 47705 47706 this.masterPlaylistController_.on('error', function () { 47707 var player = videojs$1.players[_this3.tech_.options_.playerId]; 47708 47709 player.error(_this3.masterPlaylistController_.error); 47710 }); 47711 47712 // `this` in selectPlaylist should be the HlsHandler for backwards 47713 // compatibility with < v2 47714 this.masterPlaylistController_.selectPlaylist = this.selectPlaylist ? this.selectPlaylist.bind(this) : Hls$1.STANDARD_PLAYLIST_SELECTOR.bind(this); 47715 47716 this.masterPlaylistController_.selectInitialPlaylist = Hls$1.INITIAL_PLAYLIST_SELECTOR.bind(this); 47717 47718 // re-expose some internal objects for backwards compatibility with < v2 47719 this.playlists = this.masterPlaylistController_.masterPlaylistLoader_; 47720 this.mediaSource = this.masterPlaylistController_.mediaSource; 47721 47722 // Proxy assignment of some properties to the master playlist 47723 // controller. Using a custom property for backwards compatibility 47724 // with < v2 47725 Object.defineProperties(this, { 47726 selectPlaylist: { 47727 get: function get$$1() { 47728 return this.masterPlaylistController_.selectPlaylist; 47729 }, 47730 set: function set$$1(selectPlaylist) { 47731 this.masterPlaylistController_.selectPlaylist = selectPlaylist.bind(this); 47732 } 47733 }, 47734 throughput: { 47735 get: function get$$1() { 47736 return this.masterPlaylistController_.mainSegmentLoader_.throughput.rate; 47737 }, 47738 set: function set$$1(throughput) { 47739 this.masterPlaylistController_.mainSegmentLoader_.throughput.rate = throughput; 47740 // By setting `count` to 1 the throughput value becomes the starting value 47741 // for the cumulative average 47742 this.masterPlaylistController_.mainSegmentLoader_.throughput.count = 1; 47743 } 47744 }, 47745 bandwidth: { 47746 get: function get$$1() { 47747 return this.masterPlaylistController_.mainSegmentLoader_.bandwidth; 47748 }, 47749 set: function set$$1(bandwidth) { 47750 this.masterPlaylistController_.mainSegmentLoader_.bandwidth = bandwidth; 47751 // setting the bandwidth manually resets the throughput counter 47752 // `count` is set to zero that current value of `rate` isn't included 47753 // in the cumulative average 47754 this.masterPlaylistController_.mainSegmentLoader_.throughput = { 47755 rate: 0, 47756 count: 0 47757 }; 47758 } 47759 }, 47760 /** 47761 * `systemBandwidth` is a combination of two serial processes bit-rates. The first 47762 * is the network bitrate provided by `bandwidth` and the second is the bitrate of 47763 * the entire process after that - decryption, transmuxing, and appending - provided 47764 * by `throughput`. 47765 * 47766 * Since the two process are serial, the overall system bandwidth is given by: 47767 * sysBandwidth = 1 / (1 / bandwidth + 1 / throughput) 47768 */ 47769 systemBandwidth: { 47770 get: function get$$1() { 47771 var invBandwidth = 1 / (this.bandwidth || 1); 47772 var invThroughput = void 0; 47773 47774 if (this.throughput > 0) { 47775 invThroughput = 1 / this.throughput; 47776 } else { 47777 invThroughput = 0; 47778 } 47779 47780 var systemBitrate = Math.floor(1 / (invBandwidth + invThroughput)); 47781 47782 return systemBitrate; 47783 }, 47784 set: function set$$1() { 47785 videojs$1.log.error('The "systemBandwidth" property is read-only'); 47786 } 47787 } 47788 }); 47789 47790 Object.defineProperties(this.stats, { 47791 bandwidth: { 47792 get: function get$$1() { 47793 return _this3.bandwidth || 0; 47794 }, 47795 enumerable: true 47796 }, 47797 mediaRequests: { 47798 get: function get$$1() { 47799 return _this3.masterPlaylistController_.mediaRequests_() || 0; 47800 }, 47801 enumerable: true 47802 }, 47803 mediaRequestsAborted: { 47804 get: function get$$1() { 47805 return _this3.masterPlaylistController_.mediaRequestsAborted_() || 0; 47806 }, 47807 enumerable: true 47808 }, 47809 mediaRequestsTimedout: { 47810 get: function get$$1() { 47811 return _this3.masterPlaylistController_.mediaRequestsTimedout_() || 0; 47812 }, 47813 enumerable: true 47814 }, 47815 mediaRequestsErrored: { 47816 get: function get$$1() { 47817 return _this3.masterPlaylistController_.mediaRequestsErrored_() || 0; 47818 }, 47819 enumerable: true 47820 }, 47821 mediaTransferDuration: { 47822 get: function get$$1() { 47823 return _this3.masterPlaylistController_.mediaTransferDuration_() || 0; 47824 }, 47825 enumerable: true 47826 }, 47827 mediaBytesTransferred: { 47828 get: function get$$1() { 47829 return _this3.masterPlaylistController_.mediaBytesTransferred_() || 0; 47830 }, 47831 enumerable: true 47832 }, 47833 mediaSecondsLoaded: { 47834 get: function get$$1() { 47835 return _this3.masterPlaylistController_.mediaSecondsLoaded_() || 0; 47836 }, 47837 enumerable: true 47838 }, 47839 buffered: { 47840 get: function get$$1() { 47841 return timeRangesToArray(_this3.tech_.buffered()); 47842 }, 47843 enumerable: true 47844 }, 47845 currentTime: { 47846 get: function get$$1() { 47847 return _this3.tech_.currentTime(); 47848 }, 47849 enumerable: true 47850 }, 47851 currentSource: { 47852 get: function get$$1() { 47853 return _this3.tech_.currentSource_; 47854 }, 47855 enumerable: true 47856 }, 47857 currentTech: { 47858 get: function get$$1() { 47859 return _this3.tech_.name_; 47860 }, 47861 enumerable: true 47862 }, 47863 duration: { 47864 get: function get$$1() { 47865 return _this3.tech_.duration(); 47866 }, 47867 enumerable: true 47868 }, 47869 master: { 47870 get: function get$$1() { 47871 return _this3.playlists.master; 47872 }, 47873 enumerable: true 47874 }, 47875 playerDimensions: { 47876 get: function get$$1() { 47877 return _this3.tech_.currentDimensions(); 47878 }, 47879 enumerable: true 47880 }, 47881 seekable: { 47882 get: function get$$1() { 47883 return timeRangesToArray(_this3.tech_.seekable()); 47884 }, 47885 enumerable: true 47886 }, 47887 timestamp: { 47888 get: function get$$1() { 47889 return Date.now(); 47890 }, 47891 enumerable: true 47892 }, 47893 videoPlaybackQuality: { 47894 get: function get$$1() { 47895 return _this3.tech_.getVideoPlaybackQuality(); 47896 }, 47897 enumerable: true 47898 } 47899 }); 47900 47901 this.tech_.one('canplay', this.masterPlaylistController_.setupFirstPlay.bind(this.masterPlaylistController_)); 47902 47903 this.masterPlaylistController_.on('selectedinitialmedia', function () { 47904 // Add the manual rendition mix-in to HlsHandler 47905 renditionSelectionMixin(_this3); 47906 setupEmeOptions(_this3); 47907 }); 47908 47909 // the bandwidth of the primary segment loader is our best 47910 // estimate of overall bandwidth 47911 this.on(this.masterPlaylistController_, 'progress', function () { 47912 this.tech_.trigger('progress'); 47913 }); 47914 47915 // In the live case, we need to ignore the very first `seeking` event since 47916 // that will be the result of the seek-to-live behavior 47917 this.on(this.masterPlaylistController_, 'firstplay', function () { 47918 this.ignoreNextSeekingEvent_ = true; 47919 }); 47920 47921 this.tech_.ready(function () { 47922 return _this3.setupQualityLevels_(); 47923 }); 47924 47925 // do nothing if the tech has been disposed already 47926 // this can occur if someone sets the src in player.ready(), for instance 47927 if (!this.tech_.el()) { 47928 return; 47929 } 47930 47931 this.tech_.src(videojs$1.URL.createObjectURL(this.masterPlaylistController_.mediaSource)); 47932 } 47933 47934 /** 47935 * Initializes the quality levels and sets listeners to update them. 47936 * 47937 * @method setupQualityLevels_ 47938 * @private 47939 */ 47940 47941 }, {
47942 key: 'setupQualityLevels_', 47943 value: function setupQualityLevels_() { 47944 var _this4 = this; 47945 47946 var player = videojs$1.players[this.tech_.options_.playerId]; 47947 47948 if (player && player.qualityLevels) { 47949 this.qualityLevels_ = player.qualityLevels(); 47950 47951 this.masterPlaylistController_.on('selectedinitialmedia', function () { 47952 handleHlsLoadedMetadata(_this4.qualityLevels_, _this4); 47953 }); 47954 47955 this.playlists.on('mediachange', function () { 47956 handleHlsMediaChange(_this4.qualityLevels_, _this4.playlists); 47957 }); 47958 } 47959 } 47960 47961 /** 47962 * Begin playing the video. 47963 */ 47964 47965 }, { 47966 key: 'play', 47967 value: function play() { 47968 this.masterPlaylistController_.play(); 47969 } 47970 47971 /** 47972 * a wrapper around the function in MasterPlaylistController 47973 */ 47974 47975 }, { 47976 key: 'setCurrentTime', 47977 value: function setCurrentTime(currentTime) { 47978 this.masterPlaylistController_.setCurrentTime(currentTime); 47979 } 47980 47981 /** 47982 * a wrapper around the function in MasterPlaylistController 47983 */ 47984 47985 }, { 47986 key: 'duration', 47987 value: function duration$$1() { 47988 return this.masterPlaylistController_.duration(); 47989 } 47990 47991 /** 47992 * a wrapper around the function in MasterPlaylistController 47993 */ 47994 47995 }, { 47996 key: 'seekable', 47997 value: function seekable$$1() { 47998 return this.masterPlaylistController_.seekable(); 47999 } 48000 48001 /** 48002 * Abort all outstanding work and cleanup. 48003 */ 48004 48005 }, { 48006 key: 'dispose', 48007 value: function dispose() { 48008 if (this.playbackWatcher_) { 48009 this.playbackWatcher_.dispose(); 48010 } 48011 if (this.masterPlaylistController_) { 48012 this.masterPlaylistController_.dispose(); 48013 }
vendor: 2,813 bytes, lines 48014-48094
48014 if (this.qualityLevels_) { 48015 this.qualityLevels_.dispose(); 48016 } 48017 get$2(HlsHandler.prototype.__proto__ || Object.getPrototypeOf(HlsHandler.prototype), 'dispose', this).call(this); 48018 } 48019 }]); 48020 return HlsHandler; 48021 }(Component$1); 48022 48023 /** 48024 * The Source Handler object, which informs video.js what additional 48025 * MIME types are supported and sets up playback. It is registered 48026 * automatically to the appropriate tech based on the capabilities of 48027 * the browser it is running in. It is not necessary to use or modify 48028 * this object in normal usage. 48029 */ 48030 48031 var HlsSourceHandler = { 48032 name: 'videojs-http-streaming', 48033 VERSION: version$3, 48034 canHandleSource: function canHandleSource(srcObj) { 48035 var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 48036 48037 var localOptions = videojs$1.mergeOptions(videojs$1.options, options); 48038 48039 return HlsSourceHandler.canPlayType(srcObj.type, localOptions); 48040 }, 48041 handleSource: function handleSource(source, tech) { 48042 var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 48043 48044 var localOptions = videojs$1.mergeOptions(videojs$1.options, options); 48045 48046 tech.hls = new HlsHandler(source, tech, localOptions); 48047 tech.hls.xhr = xhrFactory(); 48048 48049 tech.hls.src(source.src, source.type); 48050 return tech.hls; 48051 }, 48052 canPlayType: function canPlayType(type) { 48053 var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 48054 48055 var _videojs$mergeOptions = videojs$1.mergeOptions(videojs$1.options, options), 48056 overrideNative = _videojs$mergeOptions.hls.overrideNative; 48057 48058 var supportedType = simpleTypeFromSourceType(type); 48059 var canUseMsePlayback = supportedType && (!Hls$1.supportsTypeNatively(supportedType) || overrideNative); 48060 48061 return canUseMsePlayback ? 'maybe' : ''; 48062 } 48063 }; 48064 48065 if (typeof videojs$1.MediaSource === 'undefined' || typeof videojs$1.URL === 'undefined') { 48066 videojs$1.MediaSource = MediaSource; 48067 videojs$1.URL = URL$1; 48068 } 48069 48070 // register source handlers with the appropriate techs 48071 if (MediaSource.supportsNativeMediaSources()) { 48072 videojs$1.getTech('Html5').registerSourceHandler(HlsSourceHandler, 0); 48073 } 48074 48075 videojs$1.HlsHandler = HlsHandler; 48076 videojs$1.HlsSourceHandler = HlsSourceHandler; 48077 videojs$1.Hls = Hls$1; 48078 if (!videojs$1.use) { 48079 videojs$1.registerComponent('Hls', Hls$1); 48080 } 48081 videojs$1.options.hls = videojs$1.options.hls || {}; 48082 48083 if (videojs$1.registerPlugin) { 48084 videojs$1.registerPlugin('reloadSourceOnError', reloadSourceOnError); 48085 } else { 48086 videojs$1.plugin('reloadSourceOnError', reloadSourceOnError); 48087 } 48088 48089 return videojs$1; 48090 48091}))); 48092 48093!function(){!function(a){var b=a&&a.videojs;b&&(b.CDN_VERSION="7.1.0")}(window)}(); 48094
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.